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