If at all possible, avoid signals. They are tricky to use correctly, and signal-handling code is perhaps the most difficult to debug. Despite these warnings, there are situations in which signals are the only solution. In this chapter, I will describe the reasons why you may have to deal with signals and how to handle them. I will also present related details of the wait
and exit
commands.
Signals are software interrupts. They can be generated for a variety of reasons such as the pressing of certain keystrokes. In cooked mode, pressing control-C usually generates an interrupt signal in the foreground process. Processes can also generate signals in other processes—or even in themselves. This is commonly referred to as sending a signal. Finally, the operating system can generate signals for a number of reasons, such as if a power failure is imminent and the system is about to halt. For more in-depth information on signals, read your local man page on signal
.
Specific signals are commonly referred to in several ways. For example, signal number 9 is usually written as SIGKILL
in C programs. However, many utilities (e.g., kill
) only accept 9
or KILL
(without the SIG
prefix). Expect accepts all three forms (9
, KILL
, or SIGKILL
). For clarity in this book, I like to use the C-style although I will give examples of why the other two forms are occasionally useful.
The exact list of signals varies from one system to another but modern systems include those shown in the following table. There are others but the signals shown here are the ones you are most likely to deal with in an Expect script.
Name |
Description |
---|---|
hangup | |
interrupt | |
quit | |
|
kill |
pipe write failure | |
software termination | |
stop (really “suspend”) | |
keyboard stop | |
continue | |
child termination | |
window size change | |
|
user-defined |
|
user-defined |
Assuming you have permission, these signals can be generated by using the kill
command from a shell script or "exec kill
" from an Expect script. For example, from an Expect script the following command sends an interrupt signal to process 1389.
exec kill -INT 1389
Expect processes can receive as well as generate signals. In the example above, if process 1389 is an Expect process, upon receiving a signal, the process looks for a command that is associated with the signal. An associated command is known as a signal handler or trap. If there is a handler, it is evaluated. When the handler has completed execution, the script (usually) returns to what it was doing before the signal arrived.
The association between a signal and its handler is created by the command trap
. Only one handler can be associated with a signal at a time. If you make the association from within a procedure, the association remains in effect even after the procedure returns. Each association replaces the previous one for the signal of the same name.
For example, the following command causes a script to print "bye bye
" and then to exit if an interrupt signal (SIGINT
) is received.
trap {send_user "bye bye"; exit} SIGINT
The first argument of the trap
command is the handler. A handler can be as simple as a procedure name or as complex as a long list of commands. Here are more examples:
trap intproc SIGINT trap { send_user "bye bye" exit } SIGINT
A handler can also return in the middle as if it were a procedure. Any return value is discarded after evaluation.
trap { if [expr $test] return morestuff } SIGINT
Multiple signals can be associated with the same command by enclosing them in a list. The following command associates the procedure sigproc
with the signals SIGINT
, SIGUSR1
, and SIGUSR2
. Using the SIG
prefix in a long list of signals is tiresome, so I do not specify it in such cases.
trap sigproc {INT USR1 USR2}
If you associate a common procedure with multiple signals, you can use trap
with the -name
or -number
flag to find out what signal is being processed.
trap { puts "got signal named [trap -name]" puts "got signal numbered [trap -number]" } {INT USR1 USR2}
The command "trap -name
" returns the name without the SIG
prefix. If you want the SIG
prefix, just append SIG
to the result.
trap { puts "got signal named SIG[trap -name]" } {INT USR1 USR2}
You can redefine a signal while its handler is being evaluated. The change does not take effect until the next evaluation of the handler.
Signals may be ignored by using the keyword SIG_IGN
as the first argument of the trap command. The SIG
and underscore are not optional.
trap SIG_IGN {INT USR1 USR2}
By default, most signals cause Expect to terminate ungracefully. So if you intend to send signals to Expect, you should trap them. Scripts that terminate ungracefully do not have their exit handlers run and can also leave the terminal in raw mode.
You can reset the default behavior of a signal to that defined by the operating system by using the keyword SIG_DFL
. If Expect’s default behavior is different than SIG_DFL
, I will mention it when describing the details of each signal (later). Otherwise, you can assume Expect’s default behavior is precisely SIG_DFL
.
As with "SIG_IGN
“, the "SIG_
" prefix is required in "SIG_DFL
“.
trap SIG_DFL {INT USR1 USR2}
Most of this chapter covers signals occurring in the Expect process itself. But signals are also of concern to spawned processes. Unfortunately, there is little Expect can do to control the signal activity of a spawned process. In particular, there is no analog to the expect
command for signals.
Signals can, however, be sent. As I mentioned on page 304, the UNIX kill
command can be used to send arbitrary signals to a process.
Signals in spawned processes start out with the default behavior—corresponding to SIG_DFL
. Processes override this for signals that they expect and care about. However, some unexpected signals may be delivered and the Expect programmer can control this to some extent.
As an example, recall that I mentioned in Chapter 4 (p. 101) that SIGHUP
is delivered to a process when the Expect process closes its side of the connection. The default behavior of SIGHUP
forces the spawned process to exit. Therefore, if you want the spawned process to continue after closing the connection, you must arrange for the signal to be ignored.
A signal is initially ignored in a spawned process by using spawn
with the -ignore
flag followed by a signal name. The -ignore
flag understands the same style of signal names as the trap
command; however, the signal names must be separated, one per flag. For example, the following command creates a sleep
process immune to SIGHUP
and SIGPIPE
.
spawn -ignore SIGHUP -ignore SIGPIPE sleep 1000
Unless the spawned process overrides this signal handling, ignored signals are also initially ignored by children of the spawned process (and so on for children related in any way to the spawned process). This is particularly important in the case of SIGHUP
because hangup signals are sent to the children of a spawned process when the spawned process dies. This is analogous to the behavior of most shells where the nohup
command prevents processes from receiving a SIGHUP
when the shell exits.
An explanation of the rationale for this is beyond the scope of this book, but it is related to job control. Job control-aware processes such as shells do not have problems with signals since they are generally carefully written with respect to signals, and they always reset all of their signals upon initialization.
Signals are highly nonportable. Their behavior varies quite a bit from one system to another. Nonetheless, it is possible to state some generalizations about each one.
SIGINT
is an interrupt signal. It is usually generated by pressing ^C from the keyboard. The specific key can be changed using stty
. The signal can, of course, also be generated via the kill
command. If the SIGINT
handler is set to SIG_DFL
, a SIGINT
will cause Expect to die without evaluating exit
.
By default, Expect traps SIGINT
and defines it to call exit
. This association is defined with the command "trap exit SIGINT
“, which is evaluated when Expect starts. If you redefine the exit
procedure, the trap will invoke your exit
.[53]
If Expect is in raw mode, the ^C will not automatically generate a SIGINT
but will instead be handled like any other character. For example, interact
implicitly puts the terminal in raw mode so that a ^C is sent to the spawned process. You can define a pattern to match ^C and generate a SIGINT
using kill
, but that is not common practice and would be confusing to users.
In Chapter 9 (p. 219), I described how the debugger is enabled if Expect is started with the -D
flag. Part of what -D
does is to redefine the behavior of SIGINT
as follows:
trap {exp_debug 1} SIGINT
Pressing ^C will then invoke the debugger rather than causing Expect to exit. If you want to redefine SIGINT
so that it performs some other action (and does not exit), you can have the best of both worlds by defining it only if the debugger is not active:
if ![exp_debug] {trap myproc SIGINT}
The procedure myproc
will only be called when the debugger is not active. If the debugger is active, ^C will invoke the debugger.
There is nothing special about using SIGINT
to invoke the debugger. This is just common practice. You can associate the debugger with no interrupts, a different interrupt, or several different interrupts. For example, to associate the debugger with both SIGUSR1
and SIGUSR2
:
trap {exp_debug 1} {SIGUSR1 SIGUSR2}
While Expect comes with a debugger, you are free to use a different one, arranging it so that -D
calls another routine on SIGINT
. To do this, define the environment variable EXPECT_DEBUG_INIT
. If this variable is defined, it is evaluated instead of the default trap
definition for SIGINT
. In fact, you are not limited to defining a handler for SIGINT
. You can define it to be any command you want.
SIGTERM
is similar to SIGINT
except that SIGTERM
usually implies that the process should clean itself up and exit. Expect defines SIGINT
to do this initially, but SIGINT
is frequently redefined to do other things that allow the process to continue.
Expect’s default definition of SIGTERM
is:
trap exit SIGTERM
If the SIGTERM
handler is set to SIG_DFL
, SIGTERM
will cause Expect to die without evaluating exit
.
SIGQUIT
is similar to SIGTERM
; however, SIGQUIT
is not usually caught. Instead, SIGQUIT
provides a simple and reliable way to kill an Expect process. This is very useful if the script (or perhaps even Expect) has a bug and you want to stop the process as soon as possible. When the Expect process dies, a file called core
is written to the current directory. The core file provides a representation of what was in memory when the SIGQUIT
was received. With a C debugger, it is possible to look at this and see what was going on.
SIGKILL
cannot be caught. It provides the surest way of killing an Expect process (short of rebooting). Do not worry about the fact that you cannot catch SIGKILL
. It should only be used in the event that the process has already made some obvious error or is wildly out of control. There is no point in trying to clean up gracefully as if the process actually knew what it was doing. If it did, it would not be getting a SIGKILL
in the first place.
SIGCHLD
is generated on the death of a child process. By default, the signal has no effect on an Expect process. That means you do not have to define a SIGCHLD
handler. However, a SIGCHLD
handler is useful if you want to get the exit status but do not want to block the script while waiting for the spawned process.
Some systems claim SIGCHLD
is spelled SIGCLD
but Expect insists that it be spelled SIGCHLD
(as per POSIX) for portability. Take this as an omen. Expect goes to great lengths to make SIGCHLD
work the same on all systems, but it is still a good idea to avoid trapping or ignoring SIGCHLD
to avoid portability problems.
A signal handler for SIGCHLD
must call wait
within the signal handler. The wait
command will fail if no child is waiting, if another signal handler fails during its execution, or if other reasons not having to do with a particular process occur. Otherwise, wait
returns a list describing a process that was waited upon.
The list contains the process id, spawn id, and a 0 or −1. A 0 indicates that the process was waited upon successfully. In this case, the next value is the status.
expect1.3> wait
13866 4 0 7
In this sample output, the process id was 13866 and the spawn id was 4. The 0 indicates the process was waited upon successfully and that the next value (7 in this example) was the status returned by the program.
If the spawned process ends due to a signal, three additional elements appear in the return value. The first is the the string CHILDKILLED
, the second is the C-style signal name, and the last is a short textual description. For example:
expect1.1>spawn cat
spawn cat 2462 expect1.2>exec kill -ILL 2462
expect1.3>expect; wait
2462 4 0 0 CHILDKILLED SIGILL {illegal instruction}
If the third element returned by wait
is −1, then an error occurred and the fourth element is a numeric error code describing the error further. Additional elements appear in the return value following the style of Tcl’s errorCode
variable. For example, if a system error occurred, three additional elements appear. The first element is the string "POSIX
“. The second element is the symbolic name of the errno
error code. The third element is a short textual description of it.
SIGCHLD
is unusual among signals in that a SIGCHLD
is guaranteed to be delivered for each child termination. (In comparison, if you press ^C three times in a row, you are guaranteed only that at least one SIGINT
will be delivered.) Therefore, the SIGCHLD
handler need not call wait
more than once—the handler will be recalled as necessary.
No assumption can be made about the ordering of processes to be waited on. In order to wait on any spawned process, use the flags "-i −1
“. Since SIGCHLD
can be generated for any child (not just spawned processes), such a wait
should be embedded in a catch
so that other deaths can be ignored.
Here is a sample SIGCHLD
handler.
trap { if [catch {wait −i −1} output] return puts "caught SIGCHLD" puts "pid is [lindex $output 0]" puts "status is [lindex $output 3]" } SIGCHLD
Here is an example of using the handler above to catch the completion of the date
command. Notice that the output begins where the next command is about to be typed.
expect2.2> spawn date
spawn date
5945
expect2.3> caught SIGCHLD
pid is 5945
status is 0
SIGHUP
is named after “hang up” to denote the historical action of hanging up the phone line connecting a user to a computer. Most shells preserve this ritual by sending a SIGHUP
to each process started by the shell just before the shell itself exits.
Thus, if a user logs in, starts an Expect process in the background, and then logs out, SIGHUP
will be sent to the Expect process.
By default, SIGHUP
causes the Expect process to die without executing exit
. If you want the Expect process to continue running, ignore SIGHUP
:
trap SIG_IGN SIGHUP
For analogous reasons, Expect sends a SIGHUP
to each spawned process when Expect closes the connection to the process. Normally, this is desirable. It means that when you call close
, the spawned process gets a signal and exits. If you want the process to continue running, add the flag "-ignore HUP
" to the spawn
command. If the process does not reset the signal handler, then the SIGHUP
will be ignored.
SIGPIPE
is generated by writing to a pipe after the process at the other end has died. This can happen in pipelines started by Tcl’s open
command, and for this reason SIGPIPE
is ignored (SIG_IGN
) by default. If the handler is set to SIG_DFL
, the Expect process will die without executing exit
.
A SIGWINCH
signal can be generated when the window in which Expect is running changes size. For example, if you are using X and you interactively resize the xterm
within which an Expect script is running, the Expect process can receive a SIGWINCH
. By default, Expect ignores SIGWINCH
. The SIG_DFL
and SIG_IGN
handlers both cause SIGWINCH
to be ignored.
Some spawned processes are not interested in the size of a window. But some processes are. For example, editors need this information in order to know how much information can fit in the window.
Initially, a spawned process inherits its window size by copying that of the Expect process. (If the Expect process has no associated window, the window size is set to zero rows and zero columns.) This suffices for many applications; however, if you wish to resize the window, you have to provide a SIGWINCH
handler.
In some cases, it is possible to send a command to the spawned process to inform it of the window size change. For example, if the spawned process is an rlogin
that in turn is speaking to a shell, you can send it a stty
command. In practice, however, the spawned process is almost certainly going to be something that does not provide any direct interface (or even an escape) to the shell. Fortunately, a simpler and more portable solution is possible.
All that is necessary is to change the window size of the spawned process. The following command establishes such a handler.
trap { set rows [stty rows] set cols [stty columns] stty rows $rows columns $cols < $spawn_out(slave,name) } WINCH
The "stty rows
" returns the number of rows of the local window, and "stty columns
" returns the number of columns. (The assignments are not necessary, of course, but the resulting code is a little more readable.) The final stty
command changes the window size of the spawned process. When stty
changes the window size, a SIGWINCH
is automatically generated and given to the spawned process. It is then up to the spawned process to react appropriately. For example, in the case of rlogin
, the spawned process (the rlogin
client) will fetch the new window size and send a message to rlogind
(the rlogin
server), informing it of the new size. The rlogind
process, in turn, will set its window size, thereby generating a SIGWINCH
which can then be detected by any application running in the remote session.
This SIGWINCH
handler must have the true name of the pty of the spawned process. As written, the example handler assumes the pty name has been left in spawn_out(slave,name)
. However, this variable is reset by every spawn
command, so you probably want to save a copy in another variable and refer to the other variable in the handler.
By default, if the suspend character (normally ^Z) is pressed while Expect is in cooked mode, Expect stops (some people say “suspends”). If a shell that understands job control invoked Expect, the shell will prompt.
1%expect
expect1.1>^Z
Stopped 2%
Expect is oblivious to its suspension (although when it is restarted, it may notice that significant time has passed).
If you want to perform some activity just before Expect stops, associate a handler with SIGTSTP
. The final command in the handler should send a SIGSTOP
. SIGSTOP
cannot be trapped and necessarily forces Expect to stop. Expect does not allow you to define a trap for SIGSTOP
.
For example, if a script has turned echo off, the following handler changes it back before stopping.
trap { puts "I'm stopping now" stty echo exec kill -STOP 0 } SIGTSTP
When interact
is executing, SIGTSTP
cannot be generated from the keyboard by default. Instead, a ^Z is given to the spawned process. However, it is possible to get the effect of suspending Expect when pressing ^Z. The following command does this by having the Expect process send a stop signal back to itself. It is triggered by pressing a tilde followed by a ^Z. (I will describe these features of interact
in Chapter 15 (p. 340).) Although the tilde is not necessary, it allows a bare ^Z to still be sent to the spawned process conveniently.
interact ~ 32 -reset { exec kill -STOP 0 }
The -reset
action automatically restores the terminal modes to those which were in effect before the interact
. If the modes were not cooked and echo, you will have to explicitly set them with another command before stopping.
When Expect is stopped, it can be restarted by typing fg
from the shell or by sending a SIGCONT
. Common reasons to catch SIGCONT
are to restore the terminal modes or to redraw the screen. If SIGCONT
is not caught, it has no other effect than to continue the process.
If Expect was stopped from an action using the -reset
flag within interact
, the terminal modes are restored automatically. In all other cases, you must restore them explicitly.
SIGUSR1
and SIGUSR2
are signals that have no special meaning attached to them by the operating system. Therefore, you can use them for your own purposes.
Of course, your purposes must still fit within the capabilities of signals. For example, you must not assume that signals can be counted. After a signal is generated but before it is processed by Expect, further signals of the same type are discarded. For example, pressing ^C (and generating SIGINT
) twice in a row is not guaranteed to do any more or less than pressing it once. SIGUSR1
and SIGUSR2
work the same way. Once the signal handler has run, additional signals of the same type can again be received.
The SIGUSR1
and SIGUSR2
signals by themselves carry no other information other than the fact that they have occurred. If you generate one of these two signals for different reasons at different times, you also need some mechanism for allowing the receiving process to know what the reason is, such as by reading it from a file.
With this lack of ability to communicate extra information, it is rather mysterious that only two such user-defined signals exist. It is similarly mysterious that more than one exists. Chalk it up to the wonders of UNIX.
By default, SIGUSR1
and SIGUSR2
cause Expect to die.
Many other signals exist but it is generally not useful to catch them within an Expect script for their intended purposes. You might consider using other signals as additional user-defined signals, but the details are beyond the scope of this text.
One signal specifically worth mentioning is SIGALRM
. SIGALRM
is reserved to Expect and must not be sent or generated artificially. Expect does not allow it to be trapped.
While not shown here, other signals are all named in the same fashion. See your /usr/include/signal.h
file for more information.
The signal.h
file also defines the mapping between names and signal numbers (see page 303). The minimum signal number is 1. The -max
flag causes the trap
command to return the highest signal number. Identifying the signals by number is particularly convenient for trapping all signals. The trap
command is nested in a catch
since some of the signals (i.e., SIGSTOP
) cannot be caught.
for {set i 1} {$i<=[trap -max]} {incr i} { catch {trap $handler $i} }
After executing this loop, selected signals can be redefined appropriately.
Signal handlers are evaluated in the global scope. Global variables are directly accessible. Local variables in current procedures are inaccessible.
Ideally, handlers are evaluated immediately after the signals are received. However, in reality there may be a delay before evaluating handlers in order to preserve the consistency of the internal data structures of Expect.
Generally, you can count on signal handlers being evaluated before each Tcl command. Consider the following command:
set a [expr $b*4]
A signal that arrives just prior to this line in a script has its handler evaluated immediately. If a signal arrives while expr
is executing, expr
completes, the signal handler is run, and then the set
command is executed. If a signal arrives while set
is executing, the handler is deferred until just before the next command in the script.
Signal handlers are also evaluated during most time-consuming operations such as I/O. For example, if an expect
command is waiting for a process to produce output, signal handlers can be executed.
Because signals are handled between each command and in the middle of long-running commands, delays in handling signals should not be significant and you should not be able to notice them even when using Expect interactively. There is one exception however. If a signal handler is in the process of being evaluated, no other signal handlers can be evaluated. For example, the following fragment prints acb
after a ^C is pressed.
trap { send_user "b" } SIGUSR1 trap { send_user "a" exec kill -USR1 [pid] sleep 10 send_user "c" } SIGINT
The reason this fragment behaves the way it does is as follows: A ^C generates a SIGINT
. The first line prints a
. Then kill
generates a SIGUSR1
signal back to the Expect process. But because a signal is currently being processed, the SIGUSR1
is not processed. Instead, Expect continues with the sleep
command, causing the script to sleep for 10 seconds. Then c
is printed. When the trap finishes, Expect processes the SIGUSR1
trap. This simply prints out b
and returns. Thus, the total effect is to print acb
.
Keep signal handlers short (in duration) to avoid these kinds of surprises. Even better, do not depend on the ordering of signals. If you find yourself thinking very hard about how a script is going to react to a number of signals that arrive very close to one another, you probably should be using some other communications mechanism instead of signals in the first place.
If you are just using Expect as an extension, it is possible that signals may be evaluated in a different way than described here. See Chapter 22 (p. 509) for further information.
When designing signal handlers, consider the consequences of evaluating them between any commands in your program. For example, if you manipulate a data structure from within a signal handler while the data structure is simultaneously being manipulated outside of the handler, your data structure may end up partly with new values and partly with old values.
To avoid this kind of problem, stick to simple commands set as "set sigint 1
“, indicating that the signal handler has been run. Outside of the signal handler, check the sigint
variable when it is safe to do so and take the relevant action at that time.
Another type of problem caused by signal handlers is that they can disturb time-sensitive operations. For example, a signal handler can cause an expect
command to timeout if the handler takes sufficiently long to execute.
These are just a sampling of the difficulties of using signals. Further discussion of these tricky problems is beyond the scope of this book but may be found in most advanced UNIX programming texts.
If Expect is evaluating a Tcl or Expect procedure or command when a signal occurs, it is possible to change the return code that would otherwise be returned. Given a -code
flag, the trap
command substitutes the return code of the trap
handler for the return code that would have been returned. For example, a break
command in the handler causes the interrupted loop to break. A return
command causes the interrupted procedure to return
. And a normal return
causes a command that is failing to succeed.
Clearly, this can be very confusing and disruptive to normal script flow, so you should avoid using it if possible. However, there are valid uses. For example, you can force an interpreter
command to stop what it is doing and reprompt. This can be done on ^C using the following command:
trap -code { error unwound -nostack } SIGINT
The error
command generates an error and the -code
flag forces the error to override whatever code would have been returned. The precise handling of error
in this context is further described in Chapter 9 (p. 224).
If no command is in execution when the signal occurs, the return code is thrown away. In vanilla Expect (with no change from the way it is distributed), a command is always in execution, but when using Expect with Tk, there can be times when no command is in execution.
This section is only useful if you have multiple interpreters in a single process. If you are using vanilla Expect, then you can skip this section.
By default, the signal handler is evaluated in the interpreter in which the trap
command was evaluated. It is possible to evaluate the handler in the interpreter active at the time the signal occurred by using the -interp
flag.
For example, if you are running several simulations whose speeds are controlled by the variables speed
(one per interpreter), you could reverse the speed by pressing ^C with the following definition in effect:
trap -interp { set speed [expr -speed] } SIGINT
It is often useful to execute commands when a script is about to exit. For example, you might want to make sure all temporary files are deleted before exiting. A list of commands can be declared in such a way that it is automatically executed when the script exits. Such a list is called an exit handler.
To declare an exit handler, invoke the exit
command with the -onexit
flag followed by the commands to execute. The commands are saved and will be invoked later when the script is about to exit.
exit -onexit { exec rm $tmpfile puts "bye bye!" }
The exit handler runs whether a script exits by an explicit exit
command or by running out of commands in a script. Signals which normally call exit
, in turn run the exit handler. Thus, if you press ^C and have not changed the default action for SIGINT
, the exit handler will be called. Signals that cause an ungraceful exit (i.e., core dump) do not execute the signal handler.
There are a few things which do not make sense inside an exit handler. Redefining the exit handler inside the exit handler does not cause the new exit handler to execute. No attempt is made to execute the exit handler twice. If an error (without a catch
) occurs in the exit handler, there can be no recovery.
The exit handler can be removed and queried in the same way as signals. An empty command removes the exit handler. If the -onexit
flag is given with no handler at all, the current handler is returned.
expect1.1>exit -onexit foo ;# set
expect1.2>exit -onexit ;# query
foo expect1.3>exit -onexit {} ;# unset
expect1.4>exit -onexit ;# query
expect1.5>
Write a procedure that defines reasonable default handlers for all signals.
Write a script that sends signals back to itself. Do the signals arrive while the kill
command is still executing? After? Long after? What happens when the system is heavily loaded?
On page 315, I described several problems that signals can cause even when they are caught and handled. Do these problems apply to any example scripts in this book?
Write a script without using signals. Reread the first sentence in this chapter.
[53] The exp_exit
command is an alias for Expect’s exit
. You should either invoke exp_exit
from your exit
or you should change the trap to invoke exp_exit
. This will make sure that the terminal modes are reset correctly. I will describe the "exp_
" aliases in more detail in Chapter 22 (p. 509)