os.signal
Waiting for operating system signals: stopping cleanly on Ctrl+C or SIGTERM, noticing terminal resizes, and controlling which thread receives a signal.
import os.signal
A signal is a short message the operating system sends to a process: Ctrl+C sends SIGINT, kill and container runtimes send SIGTERM, and a terminal sends SIGWINCH when its window changes size. By default most signals end the program on the spot, in the middle of whatever it was writing. The os.signal package lets a program take the signals it cares about as ordinary events and react to them at a safe point in its own code.
The model is the same for every use: block the signals, then wait for them. A blocked signal is not delivered; it waits in a queue until a thread asks for it with WaitSignal, Wait or WaitTimeout. Blocking is per thread, and a new thread copies the mask of the thread that started it, so block in main before you start any thread. Then no worker is interrupted, and the one thread that waits receives the signal.
Sets, masks and snapshots are memory blocks the package allocates for you; free each one with mem.Free when you are done. The package grew out of the terminal library tui, which uses it to redraw when the window is resized.
Signals exist on Linux, Android, macOS and the BSDs. On Windows every function is a safe stub: WinchSupported is false, sets are null and waits return WaitInvalid, so the same program compiles everywhere and can fall back to polling.
Stop cleanly on Ctrl+C or SIGTERM
Put SIGINT and SIGTERM in a set, block it, do the work, then wait. WaitSignal returns the number of the signal that arrived, so the program can save its state and exit on its own terms. The example sends itself SIGTERM with Raise so it finishes by itself; in a real program the signal comes from the keyboard or from kill.
import mem
import os.signal
func main:
stop := signal.NewSet()
defer mem.Free(stop)
_ := signal.AddSignal(stop, signal.SigInt)
_ := signal.AddSignal(stop, signal.SigTerm)
if not signal.Block(stop):
println "could not block the stop signals"
ret 1
end
println "working; press Ctrl+C or run kill to stop"
_ := signal.Raise(signal.SigTerm)
sig := signal.WaitSignal(stop)
if sig == signal.SigInt:
println "Ctrl+C: saving and leaving"
else sig == signal.SigTerm:
println "SIGTERM: saving and leaving"
else:
println "wait failed with status", sig
ret 1
end
ret 0
endWorkers keep running, one thread waits
Block first, then start the threads: they inherit the mask, so a signal never lands in the middle of a worker. main waits for the signal and then tells the workers to stop through an atomic flag (see atomic), and joins them.
import mem
import sync
import atomic
import os.signal
mut stopping := atomic.NewFlag(false)
func worker(_arg: void*):
until not atomic.Get(stopping):
sync.SleepMs(10)
end
println "worker saw the stop request"
end
func main:
stop := signal.NewSet()
defer mem.Free(stop)
_ := signal.AddSignal(stop, signal.SigTerm)
_ := signal.Block(stop)
t := spawn(worker, null)
_ := signal.Raise(signal.SigTerm)
println "main got signal", signal.WaitSignal(stop)
atomic.Set(stopping, true)
join(t)
atomic.DestroyFlag(stopping)
println "clean exit"
endNotice a terminal resize
SIGWINCH is ignored by default, and on macOS an ignored signal is thrown away even when it is blocked. InstallWinchHandler therefore installs a handler that does nothing, and blocks the signal, so resizes queue up. WaitTimeout waits with a limit, which lets a loop check other things between resizes. Hand the value it returned back to Restore at the end. A signal only says that the size may have changed, and several resizes can arrive as one, so read the new size with term.Size() after every wake-up.
import mem
import os.signal
func main:
if not signal.WinchSupported():
println "no SIGWINCH here: poll term.Size() instead"
ret 0
end
prev := signal.InstallWinchHandler()
if prev == null:
println "could not install the handler"
ret 1
end
set := signal.NewWinchSet()
out := mem.AllocateZeroed(8 as u64)
println "nothing yet:", signal.WaitTimeout(set, out, 100) == signal.WaitTimedOut
_ := signal.WakeWinch()
println "after a resize:", signal.WaitTimeout(set, out, 1000) == signal.WaitArrived
println "signal number:", (out as i32*)[0]
mem.Free(out)
mem.Free(set)
println "restored:", signal.Restore(signal.WinchSignal, prev)
mem.Free(prev)
ret 0
end| Need | Use |
|---|---|
| Wait for Ctrl+C or SIGTERM | NewSet, AddSignal, Block, WaitSignal |
| Wait with a time limit | Install, then WaitTimeout |
| React to terminal resizes | InstallWinchHandler, NewWinchSet, WaitWinch or WaitTimeout |
| Wake a waiting thread | Raise, or WakeWinch for SIGWINCH |
| Save and put back a thread's mask | CurrentMask, SetMask |
| Send a signal to another program | Child.Signal in os.process |
| A full-screen terminal program | tui, which does all of this for you |
What to keep in mind:
- SIGKILL and SIGSTOP can never be blocked, waited for or handled.
- Only one thread in the process may wait at a time; a second waiter gets
WaitBusyinstead of half the signals. - Signals of one kind do not stack up: two SIGWINCH sent while one is already pending arrive as one.
- A handler passed to
Installruns on whatever thread receives the signal and may only do async-signal-safe work. Blocking and waiting, as above, avoids handlers entirely.
Constants and variables
WinchSignal
const WinchSignal: i32 = 28import mem
import os.signal
func main:
if not signal.WinchSupported():
println "Windows: poll the terminal size"
ret
end
prev := signal.InstallWinchHandler()
_ := signal.BlockWinch()
set := signal.NewWinchSet()
_ := signal.WakeWinch()
println "woke with", signal.WaitWinch(set), "which is", signal.WinchSignal
mem.Free(set)
_ := signal.Restore(signal.WinchSignal, prev)
mem.Free(prev)
endWait statuses Wait…
Wait and WaitTimeout return, and the negative values WaitSignal and WaitWinch return when no signal arrived. WaitArrived is 1, WaitTimedOut is 0, and the failures are negative: WaitInvalid (a bad argument, a missing Install before a timed wait, or Windows), WaitFailed (the system call failed) and WaitBusy (another thread is already waiting).| Name | Value |
|---|---|
WaitArrived | 1 |
WaitTimedOut | 0 |
WaitInvalid | -1 |
WaitFailed | -2 |
WaitBusy | -3 |
import mem
import os.signal
func ignore(_sig: int):
_ := _sig
end
func main:
println "SIGINT", signal.SigInt, "SIGTERM", signal.SigTerm, "SIGKILL", signal.SigKill
println "others differ by system:", signal.SigUsr1 > 0, signal.SigUsr2 > 0, signal.SigChld > 0
println "fixed numbers:", signal.SigHup, signal.SigQuit, signal.SigPipe, signal.SigAlrm, signal.SigWinch
println "job control:", signal.SigCont > 0, signal.SigStop > 0, signal.SigTstp > 0
prev := signal.Install(signal.SigUsr1, &ignore)
set := signal.NewSet()
_ := signal.AddSignal(set, signal.SigUsr1)
out := mem.AllocateZeroed(8 as u64)
println "timed out:", signal.WaitTimeout(set, out, 50) == signal.WaitTimedOut
_ := signal.Raise(signal.SigUsr1)
println "arrived:", signal.Wait(set, out) == signal.WaitArrived
_ := signal.Raise(signal.SigUsr1)
println "number:", signal.WaitSignal(set) == signal.SigUsr1
println "bad argument:", signal.Wait(null, out) == signal.WaitInvalid
println "statuses:", signal.WaitFailed < 0, signal.WaitBusy < 0
mem.Free(out)
mem.Free(set)
println "restored:", signal.Restore(signal.SigUsr1, prev)
mem.Free(prev)
endSigHup
const SigHup: i32 = 1The example for this item is shown with the ones above: os-signal-Wait.salam
SigInt
const SigInt: i32 = 2The example for this item is shown with the ones above: os-signal-Wait.salam
SigQuit
const SigQuit: i32 = 3The example for this item is shown with the ones above: os-signal-Wait.salam
SigKill
const SigKill: i32 = 9The example for this item is shown with the ones above: os-signal-Wait.salam
SigPipe
const SigPipe: i32 = 13The example for this item is shown with the ones above: os-signal-Wait.salam
SigAlrm
const SigAlrm: i32 = 14The example for this item is shown with the ones above: os-signal-Wait.salam
SigTerm
const SigTerm: i32 = 15kill, service managers and container runtimes.The example for this item is shown with the ones above: os-signal-Wait.salam
SigWinch
const SigWinch: i32 = 28WinchSignal.The example for this item is shown with the ones above: os-signal-Winch.salam
SigUsr1
const SigUsr1: i32 = 10The example for this item is shown with the ones above: os-signal-Wait.salam
SigUsr2
const SigUsr2: i32 = 12The example for this item is shown with the ones above: os-signal-Wait.salam
SigChld
const SigChld: i32 = 17The example for this item is shown with the ones above: os-signal-Wait.salam
SigCont
const SigCont: i32 = 18The example for this item is shown with the ones above: os-signal-Wait.salam
SigStop
const SigStop: i32 = 19The example for this item is shown with the ones above: os-signal-Wait.salam
SigTstp
const SigTstp: i32 = 20The example for this item is shown with the ones above: os-signal-Wait.salam
SignalSetSize
const SignalSetSize: i32 = 128import mem
import os.signal
func main:
println "set size in bytes:", signal.SignalSetSize > 0
set := signal.NewSet()
_ := signal.AddSignal(set, signal.SigUsr1)
_ := signal.AddSignal(set, signal.SigHup)
println "has SIGUSR1:", signal.IsSignal(set, signal.SigUsr1)
_ := signal.DelSignal(set, signal.SigHup)
println "has SIGHUP:", signal.IsSignal(set, signal.SigHup)
saved := signal.CurrentMask()
_ := signal.Block(set)
now := signal.CurrentMask()
println "SIGUSR1 blocked:", signal.IsSignal(now, signal.SigUsr1)
_ := signal.Unblock(set)
_ := signal.SetMask(saved)
all := signal.NewSet()
signal.FillSet(all)
println "full set has SIGTERM:", signal.IsSignal(all, signal.SigTerm)
mem.Free(all)
mem.Free(now)
mem.Free(saved)
mem.Free(set)
endFunctions
WinchSupported
func WinchSupported(): boolterm.Size() instead.The example for this item is shown with the ones above: os-signal-Winch.salam
NewWinchSet
func NewWinchSet(): void*mem.Free. Null on Windows or when memory runs out.The example for this item is shown with the ones above: os-signal-Winch.salam
InstallWinchHandler
func InstallWinchHandler(): void*Restore when you are done, then free it with mem.Free. Null on Windows or when it fails, and then nothing was changed.The example for this item is shown with the ones above: os-signal-Winch.salam
Install
func Install(sig: int, handler: void*): void*Restore and then free it with mem.Free. Installing the same handler again only counts one more owner; a different handler for a signal that is already owned is refused with null. The handler runs on whatever thread receives the signal and may only do async-signal-safe work: no allocation, no locks, no printing.The example for this item is shown with the ones above: os-signal-Wait.salam
Restore
func Restore(sig: int, prev: void*): boolInstall snapshot recorded, on the thread that installed it. While other owners remain, only the owner count drops and the signal stays blocked for them; the last owner puts back the original handler and mask. Restoring again afterwards does nothing and returns true. It does not free the snapshot.The example for this item is shown with the ones above: os-signal-Wait.salam
BlockWinch
func BlockWinch(): boolThe example for this item is shown with the ones above: os-signal-Winch.salam
Wait
func Wait(set: void*, out: void*): inti32 at out[0] and returns WaitArrived. Block the signals first so they queue instead of being delivered. Returns WaitInvalid for a null argument and on Windows, WaitBusy when another thread is already waiting, and WaitFailed when the wait itself failed. Only one thread may wait at a time.The example for this item is shown with the ones above: os-signal-Wait.salam
WaitTimeout
func WaitTimeout(set: void*, out: void*, millis: int): intWait, but gives up after millis milliseconds and returns WaitTimedOut; 0 checks once without waiting and a negative value is WaitInvalid. The signal must have been installed with Install first. On macOS the set must hold exactly one signal; Linux takes any set.The example for this item is shown with the ones above: os-signal-Wait.salam
WaitWinch
func WaitWinch(set: void*): intNewWinchSet and returns 28 when it arrives, or a negative Wait status. Returns 0 only on Windows, where there is no SIGWINCH.The example for this item is shown with the ones above: os-signal-Winch.salam
WakeWinch
func WakeWinch(): boolWaitWinch; useful to stop a watcher thread. False on Windows or when sending failed.The example for this item is shown with the ones above: os-signal-Winch.salam
NewSet
func NewSet(): void*mem.Free. Null on Windows or when memory runs out.The example for this item is shown with the ones above: os-signal-Sets.salam
FillSet
func FillSet(set: void*)The example for this item is shown with the ones above: os-signal-Sets.salam
AddSignal
func AddSignal(set: void*, sig: int): boolThe example for this item is shown with the ones above: os-signal-Sets.salam
DelSignal
func DelSignal(set: void*, sig: int): boolThe example for this item is shown with the ones above: os-signal-Sets.salam
IsSignal
func IsSignal(set: void*, sig: int): boolThe example for this item is shown with the ones above: os-signal-Sets.salam
Block
func Block(set: void*): boolThe example for this item is shown with the ones above: os-signal-Sets.salam
Unblock
func Unblock(set: void*): boolThe example for this item is shown with the ones above: os-signal-Sets.salam
SetMask
func SetMask(set: void*): boolCurrentMask. False for a null set, on Windows, or on failure.The example for this item is shown with the ones above: os-signal-Sets.salam
CurrentMask
func CurrentMask(): void*mem.Free. Null on Windows or on failure.The example for this item is shown with the ones above: os-signal-Sets.salam
WaitSignal
func WaitSignal(set: void*): intWait status when the wait could not run. Block the signals first. This is the simplest way to wait for Ctrl+C or SIGTERM.The example for this item is shown with the ones above: os-signal-Wait.salam
Raise
func Raise(sig: int): boolWait. False for a non-positive number, on Windows, or when sending failed.The example for this item is shown with the ones above: os-signal-Wait.salam