std.signal
import std.signal lets a program receive SIGTERM and SIGINT as an
event instead of being ended by them. Supervisors such as systemd and
container runtimes stop a service with SIGTERM; a terminal sends SIGINT
on Ctrl-C. A program that never calls listen keeps the default action:
either signal ends it at once.
The runtime supplies one primitive (opcode 500): it installs the handler and
returns the program’s end of a socket pair. Waiting, decoding and the
cancel_on helper are checked Plenty in stdlib/std/signal.plenty.
import std.process
import std.signal
from std.ready import WaitError
from std.signal import SignalError
from std.sync import CancellationToken
from std.time import deadline_after, monotonic_now, seconds
def main() -> Result[(), AllocError | IoError | RangeError | SignalError | WaitError]:
mut signals = std.signal.listen()?
# Play the supervisor: a shell sends SIGTERM to its parent, this program.
mut kill = std.process.shell("kill -TERM $PPID")?
kill.spawn()?.wait()?
deadline = deadline_after(&monotonic_now(), &seconds(5))?
received = signals.wait(&deadline, &CancellationToken()?)?
print("caught", received)?
signals.close()?
Ok(())
caught std.signal.Signal.Terminate
Types and functions
| Item | Contents |
|---|---|
Signal | Interrupt (SIGINT) or Terminate (SIGTERM) |
SignalError | AlreadyListening, or Stream(StreamError) |
Signals | The handle on which caught signals arrive |
listen() | Result[Signals, SignalError] |
cancel_on(signals: Signals, stop: CancellationToken) | Result[Option[Signal], WaitError] |
| Method | Result |
|---|---|
signals.wait(deadline: &Deadline, cancel: &CancellationToken) | Result[Signal, WaitError]: the next signal |
signals.received() | Result[Option[Signal], WaitError]: a signal already caught, without waiting |
signals.watch(selector: &mut Selector, key: u64) | Result[(), WaitError]: register for reading |
signals.unwatch(selector: &mut Selector) | Result[(), WaitError] |
signals.close() | Result[(), WaitError] |
wait follows the std.ready rules for deadlines and
cancellation. After a selector reports the handle,
received takes the signal. WaitError is the std.ready type.
Behavior
listencatches both signals for the whole process, whichever thread they arrive on. Each caught signal is sent as one byte on a socket pair; the handler makes no allocation and takes no lock.- A process listens once. A second
listenisAlreadyListening, even after the first handle is closed. - Only the main thread may call
listen: a worker that reaches it is rejected at compile time, likestd.env.set_current_dir. See native effects. A worker may own or borrow theSignalshandle and wait on it. - After the first signal, both signals have their default action again, so a second one ends the process at once. A drain that takes too long can be cut short with a second Ctrl-C.
- Closing or dropping the handle puts the default action back in effect: the next signal ends the process as if it had never listened.
- The handler is installed with
SA_RESTART, so most interrupted system calls resume.std.readywaits retryEINTRagainst their deadline. - Child processes started by
std.processstart with the default action:execresets caught signals, and the signal mask is not changed. A Ctrl-C at a terminal goes to the whole foreground process group, children included. - The handle holds one descriptor, opened close-on-exec. The runtime keeps the other end for the life of the process.
Stopping a server
cancel_on waits for a signal and cancels stop, returning which signal it
was. If something else cancels stop first, it returns Nothing. Run it on
its own thread beside std.http.server.serve,
whose shutdown and drain begin when stop is cancelled:
signals = std.signal.listen()?
stop = CancellationToken()?
with spawn(std.signal.cancel_on, signals, stop.share()).drop_job()? as watcher, stop.cancel_on_exit():
served = std.http.server.serve(listener, &routes, &handler, &config, &stop)
stop.cancel()
watcher.join()?
report = served?
Leaving the block joins the watcher, which waits until a signal arrives or
stop is cancelled. stop.cancel_on_exit() cancels stop on every exit, and
the explicit stop.cancel() comes before the in-block join; see
cancel on every exit. The
tutorial lesson runs a
complete program.
Limits
- x86-64 and AArch64 GNU/Linux; elsewhere
listenisStream(StreamError.Io(IoError.Unsupported)). - Only
SIGTERMandSIGINT. There is noSIGHUPreload, no other signal, and no way to catch a signal more than once. - A library compiled from Plenty does not catch signals unless its code
calls
listen; the host’s dispositions are its own.
Acknowledgements
- Uses D. J. Bernstein’s self-pipe trick, with a socket pair in place of the pipe.
- Uses the Rust
libccrate for Linux signal action and socket bindings (MIT OR Apache-2.0).