In Progress
Unit 1, Lesson 21
In Progress

Advanced Trap

Video transcript & code

In the previous episode we learned about UNIX signals, and how to use Ruby's trap method to attach handlers to them. Today I thought we'd continue in the same vein, and go over some more advanced uses of trap.

Sometimes we may want to register a special signal handler for a period of time, but then revert to the default behavior. Here's the tiny program we wrote in episode #353. It sits and waits for a string to work on, and traps any interrupt signals that come in while it's waiting.

Supposing we want our program to ask us if we're really sure before triggering an interrupt. Do do that, inside the interrupt handler we could output a message, and then call trap again. In the second call to trap, we reinstate the default signal handler by passing the special string ="DEFAULT"=.

trap("INT") do
  puts "Are you sure?"
  trap("INT", "DEFAULT")
end

while (text = gets)
  puts text.upcase
end

When we run this, our first Ctrl-C gets us a question, and the second raises an exception as usual.

$ ruby upcase4.rb
^CAre you sure?
^Cupcase4.rb:7:in `gets': Interrupt
        from upcase4.rb:7:in `gets'
        from upcase4.rb:7:in `<main>'

We can also tell the operating system about signals that we simply aren't interested in receiving. We do that using the special ="IGNORE"= string.

trap("INT", "IGNORE")

while (text = gets)
  puts text.upcase
end

This might be appropriate in some cases, but in general it's a feature to be used with care.

We've seen a case so far where we reinstate the Ruby default handling of a signal after temporarily customizing it. But what if the signal in question was already customized, and we return it to the "default" handler? In that case, we've effectively thrown away the original customization. Which may not be what we want.

Here's an example. Consider a simple countdown timer application. It prompts us for a number of seconds, then counts down. At the end of the countdown it prints "DING!", and then prompts us for another number of seconds.

$ ruby timer.rb
Enter # of seconds:
3
2
1
DING!
Enter # of seconds:

If we press Ctrl-C while we are at the prompt, the program exits cleanly with a message.

Enter # of seconds:
^CGoodbye!

But if we hit Ctrl-C during a countdown, it doesn't quit the program. Instead, it interrupts the countdown and returns us to the prompt.

$ ruby timer.rb
Enter # of seconds:
5
4
3
^CCountdown interrupted.
DING!
Enter # of seconds:

Let's see how this is accomplished. In the code for our timer application, we start off by trapping SIGINT and setting up a clean exit handler.

Then, we start a prompt loop. We output a prompt, and accept a number of seconds.

Then we override the current handler for SIGINT with another trap. This time, we supply a handler that will break out of the current loop.

Next we implement our countdown timer. Finally, after it has expired, we reinstate the old SIGINT trap with the goodbye message and exit.

trap("INT") do
  puts "Goodbye!"
  exit 0
end

loop do
  puts "Enter # of seconds: "
  seconds = gets
  trap("INT") do
    puts "Countdown interrupted."
    break
  end
  (seconds.to_i - 1).downto(0) do |i|
    sleep 1
    puts i unless i.zero?
  end
  puts "DING!"
  trap("INT") do
    puts "Goodbye!"
    exit 0
  end
end

In this program, we've repeated the same signal handler twice. It would be better if we could just tell trap to go back to using the previous signal handler, whatever that was.

Fortunately, there's a way to do this. trap always returns the old signal handler strategy when setting up a new one. We can save that in a variable. Then, when it comes time to reinstate the original handler, we can simply supply the saved value as the new handler.

trap("INT") do
  puts "Goodbye!"
  exit 0
end

loop do
  puts "Enter # of seconds: "
  seconds = gets
  old_handler = trap("INT") do
    puts "Countdown interrupted."
    break
  end
  (seconds.to_i - 1).downto(0) do |i|
    sleep 1
    puts i unless i.zero?
  end
  puts "DING!"
  trap("INT", old_handler)
end

UPDATE: Here's a version that avoids LocalJumpErrors in newer versions of Ruby

trap("INT") do
  puts "Goodbye!"
  exit 0
end

loop do
  puts "Enter # of seconds: "
  seconds = gets
  old_handler = trap("INT") do
    puts "Countdown interrupted."
    throw :interrupted
  end
  catch(:interrupted) do
    (seconds.to_i - 1).downto(0) do |i|
      sleep 1
      puts i unless i.zero?
    end
    puts "DING!"
  end
  trap("INT", old_handler)
end

This gives us a much easier way to temporarily override the current signal handler.

OK, now you should understand the basics of handling signals in Ruby. In future episodes, we'll play with some more practical uses for signal handlers. Until then: happy hacking!

Responses