In Progress
Unit 1, Lesson 21
In Progress

Trap

Video transcript & code

Today I want to talk about signals and how to trap them in Ruby programs. Signals are one of those things that you can get by for a long time without really understanding them. But by familiarizing yourself with signals and how to harness them, you can equip yourself with some new tools for managing programs.

So one day we're playing around with Ruby IO, and we write this little program that just accepts some text and then returns it in uppercase.

while (text = gets)
  puts text.upcase
end

We open a terminal and play with it for a little while. Then we think of a change we want to make, and realize: we didn't include a way to quit the program. Whoops.

But that's OK. Because we know that we can always—well, almost always—get out of a program by hitting Ctrl-C.

$ ruby upcase.rb
hello
HELLO
goodbye
GOODBYE
^Cupcase.rb:2:in `gets': Interrupt
        from upcase.rb:2:in `gets'
        from upcase.rb:2:in `<main>'

But hang on a moment: what exactly just happened? What did our Ctrl-C command actually do, and why do we now have a stack trace? What does the word "Interrupt" in this stack trace mean?

To understand what we just did, we have to understand the concept of signals. Signal is kind of a generic term, but in the context of UNIX-like operating systems such as Linux and Mac OS X, it has a very specific meaning.

A signal is one of the simplest forms of Inter-Process Communication, or IPC. A signal is a very simple message that one process can send to another.

And when I say simple, I mean simple. A UNIX signal consists of a number, and nothing else. And not just any old number, either. The standard set of signal numbers ranges from 1 to 31. Each of these signal numbers has a standard meaning associated with it. We can ask Ruby for a list of the signal numbers and their associated names using the Signal class.

Signal.list
# => {"EXIT"=>0, "HUP"=>1, "INT"=>2, "QUIT"=>3, "ILL"=>4, "TRAP"=>5, "ABRT"=>...

Let's lay these out in a table.

Signal.list.each do |name, number|
  puts number.to_s.rjust(3) + " " + name
end

# >>   0 EXIT
# >>   1 HUP
# >>   2 INT
# >>   3 QUIT
# >>   4 ILL
# >>   5 TRAP
# >>   6 ABRT
# >>   6 IOT
# >>   8 FPE
# >>   9 KILL
# >>   7 BUS
# >>  11 SEGV
# >>  31 SYS
# >>  13 PIPE
# >>  14 ALRM
# >>  15 TERM
# >>  23 URG
# >>  19 STOP
# >>  20 TSTP
# >>  18 CONT
# >>  17 CHLD
# >>  17 CLD
# >>  21 TTIN
# >>  22 TTOU
# >>  29 IO
# >>  24 XCPU
# >>  25 XFSZ
# >>  26 VTALRM
# >>  27 PROF
# >>  28 WINCH
# >>  10 USR1
# >>  12 USR2
# >>  30 PWR
# >>  29 POLL

When we talk about signals, we usually refer to them by their mnemonic, prefaced by the word "SIG". So for instance, we talk about "SIGHUP" and "SIGQUIT".

Some of these signals are associated with familiar events. For instance, SIGTERM is the signal a process receives when the operating system tells it to terminate. SIGSEGV is short for "segmentation violation", and it represents the dreaded "segmentation fault", which we usually see when C-level code has a bug and does something it's not supposed to. And SIGKILL is what a process receives when it's not responding and we tell the operating system to force-quit that program.

And then there's the signal that got us started today: SIGINT. This signal is what the shell sends to a program when we press Ctrl-C at the command line.

"INT" is short for interrupt. And as we saw before, hitting Ctrl-C definitely interrupted the program. In a permanent way. In fact, when we look back at the aftermath of hitting Ctrl-C, it looks a lot like we triggered an exception, which caused the program to crash with a stack trace.

It looks like that, because that's exactly what happened. Let's surround our program with a begin…rescue…end block. We'll explicitly specify the root Exception class, in order to guarantee that we will capture any possible exception which might be raised. Then we'll print out the class of exception which was actually captured.

begin
  while (text = gets)
    puts text.upcase
  end
rescue Exception => e
  puts "Got exception:"
  p e.class
end

Let's start this program, and then immediately hit Ctrl-C. What we see is that we rescued an exception whose name is Interrupt.

$ ruby upcase2.rb
^CGot exception:
Interrupt

Asking Ruby about the ancestors of Interrupt, we see that it is descended from SignalException.

Interrupt.ancestors
# => [Interrupt, SignalException, Exception, Object, JSON::Ext::Generator::Ge...

Signal exceptions are exactly what they sound like: exceptions triggered by receiving a signal from the operating system. SIGINT is a sufficiently common signal that it gets its own custom subclass, called simply Interrupt. If we ask an Interrupt exception what its signal number is, we can see that it corresponds to the number of SIGINT in our table.

Interrupt.new.signo             # => 2

Now, a minute ago I said that signals are a simple form of inter-process communication. But when we think about communication, we usually think of things like listening to a socket or reading from a pipe. As communication goes, triggering an immediate exceptions seems a little, well… violent. It's a bit like having your mail carrier deliver a letter by tying it to a brick and chucking it through your office window.

But that's just one of the unique features of signals as a communication mechanism. Signals are intended to let a process know something happened right away, no matter what it happens to be doing at the time. And the way most signals are handled by default is all-or-nothing: for certain signals, the default is to ignore the signal entirely. For others, the default is to raise an exception from whatever code happens to be executing when the signal is received.

As we've seen here, one way to handle signals such as SIGINT is to wrap our entire program in an exception handling block, and then catch the appropriate signal exception. But that's not always convenient, or even possible. If we're writing a library, for instance, we may not have the opportunity to wrap the program in a begin…rescue…end block.

And anyway, there's another reason we might not want to handle signals by rescuing an exception: by the time the exception has bubbled up and out of every method in the program, there's no chance for us to pick up where we left off. Our only option is to exit the program gracefully.

And then there are those other signals, for which the default behavior is to ignore them entirely. We might like to be able to opt-in to handling them instead of letting them pass us by.

Fortunately, there's another way to handle signals. Instead of dealing with them in an exception handler, we can preemptively register handlers for signals with the operating system.

We do this using the Signal.trap method. Or, if we're feeling lazy, we can just use the trap alias that's available in the Kernal module.

Signal.trap
trap

Let's trap SIGINT. We could use the number, but that's not very easy for later programmers to decipher, and it also makes our program operating-system specific. Instead of the number, we are also allowed to use a string specifying one of the signal mnemonics. Then we can supply a block, telling Ruby what to do when it receives this signal. We'll just have it output a message.

trap("INT") do
  puts "Nah... I don't think so."
end

while (text = gets)
  puts text.upcase
end

When we run this program and try to interrupt it with Ctrl-C, we now get a rude message, and the program doesn't quit. Not only does it not quit, but it picks up exactly where it left off. We can see this by typing in some text.

$ ruby upcase3.rb
^CNah... I don't think so.
^CNah... I don't think so.
^CNah... I don't think so.
hello
HELLO
goodbye
GOODBYE

As we can see, this program is still in the midst of the read, capitalize, print loop. Trapping the interrupt signal prevented any exception from being raised. Instead, when the signal arrived our program briefly interrupted what it was doing to execute our custom signal handler, then returned to the state it was in before the signal showed up.

By the way, you may be wondering how we can exit this program since Ctrl-C no longer works. There are a few ways to do it, but one way, at least on Linux, is by pressing Ctrl-. This triggers the shell to send SIGQUIT instead of SIGINT. It's possible for a program to ignore SIGQUIT as well, but it's a lot less common.

OK, that's all we have time for today. In the next episode we'll talk about some more advanced uses of trap. Until then, happy hacking!

Responses