Standard library · System and tools

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.

os-signal-guide-shutdown.salam
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
end
Outputworking; press Ctrl+C or run kill to stop SIGTERM: saving and leaving

Workers 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.

os-signal-guide-threads.salam
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"
end
Outputmain got signal 15 worker saw the stop request clean exit

Notice 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.

os-signal-guide-resize.salam
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
Outputnothing yet: true after a resize: true signal number: 28 restored: true
NeedUse
Wait for Ctrl+C or SIGTERMNewSet, AddSignal, Block, WaitSignal
Wait with a time limitInstall, then WaitTimeout
React to terminal resizesInstallWinchHandler, NewWinchSet, WaitWinch or WaitTimeout
Wake a waiting threadRaise, or WakeWinch for SIGWINCH
Save and put back a thread's maskCurrentMask, SetMask
Send a signal to another programChild.Signal in os.process
A full-screen terminal programtui, 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 WaitBusy instead 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 Install runs 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 = 28
The SIGWINCH number (28), the signal a terminal sends when its window changes size.
os-signal-Winch.salam
import 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)
end
Outputwoke with 28 which is 28

Wait statuses Wait…

What 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).
NameValue
WaitArrived1
WaitTimedOut0
WaitInvalid-1
WaitFailed-2
WaitBusy-3
os-signal-Wait.salam
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)
end
OutputSIGINT 2 SIGTERM 15 SIGKILL 9 others differ by system: true true true fixed numbers: 1 3 13 14 28 job control: true true true timed out: true arrived: true number: true bad argument: true statuses: true true restored: true

SigHup

const SigHup: i32 = 1
SIGHUP (1): the terminal was closed, or a daemon is asked to reload its settings.

The example for this item is shown with the ones above: os-signal-Wait.salam

SigInt

const SigInt: i32 = 2
SIGINT (2): the user pressed Ctrl+C.

The example for this item is shown with the ones above: os-signal-Wait.salam

SigQuit

const SigQuit: i32 = 3
SIGQUIT (3): the user pressed Ctrl+\.

The example for this item is shown with the ones above: os-signal-Wait.salam

SigKill

const SigKill: i32 = 9
SIGKILL (9): ends the process at once. It cannot be blocked, waited for or handled.

The example for this item is shown with the ones above: os-signal-Wait.salam

SigPipe

const SigPipe: i32 = 13
SIGPIPE (13): a write to a pipe or socket whose reader has gone.

The example for this item is shown with the ones above: os-signal-Wait.salam

SigAlrm

const SigAlrm: i32 = 14
SIGALRM (14): a timer set with alarm expired.

The example for this item is shown with the ones above: os-signal-Wait.salam

SigTerm

const SigTerm: i32 = 15
SIGTERM (15): the polite request to stop, sent by kill, service managers and container runtimes.

The example for this item is shown with the ones above: os-signal-Wait.salam

SigWinch

const SigWinch: i32 = 28
SIGWINCH (28): the terminal window changed size. The same value as WinchSignal.

The example for this item is shown with the ones above: os-signal-Winch.salam

SigUsr1

const SigUsr1: i32 = 10
SIGUSR1: free for the program's own use. 10 on Linux and Android, 30 on macOS and the BSDs.

The example for this item is shown with the ones above: os-signal-Wait.salam

SigUsr2

const SigUsr2: i32 = 12
SIGUSR2: free for the program's own use. 12 on Linux and Android, 31 on macOS and the BSDs.

The example for this item is shown with the ones above: os-signal-Wait.salam

SigChld

const SigChld: i32 = 17
SIGCHLD: a child process stopped or ended. 17 on Linux, 20 on macOS and the BSDs.

The example for this item is shown with the ones above: os-signal-Wait.salam

SigCont

const SigCont: i32 = 18
SIGCONT: a stopped process continues. 18 on Linux, 19 on macOS and the BSDs.

The example for this item is shown with the ones above: os-signal-Wait.salam

SigStop

const SigStop: i32 = 19
SIGSTOP: stops the process. Like SIGKILL it cannot be blocked or handled. 19 on Linux, 17 on macOS and the BSDs.

The example for this item is shown with the ones above: os-signal-Wait.salam

SigTstp

const SigTstp: i32 = 20
SIGTSTP: the user pressed Ctrl+Z. 20 on Linux, 18 on macOS and the BSDs.

The example for this item is shown with the ones above: os-signal-Wait.salam

SignalSetSize

const SignalSetSize: i32 = 128
The size in bytes of one signal set on this system: 128 on Linux, 8 on 64-bit Android, 4 on macOS, 16 on FreeBSD and NetBSD, 0 on Windows.
os-signal-Sets.salam
import 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)
end
Outputset size in bytes: true has SIGUSR1: true has SIGHUP: false SIGUSR1 blocked: true full set has SIGTERM: true

Functions

WinchSupported

func WinchSupported(): bool
True when the process can receive SIGWINCH, which is every system except Windows. On Windows, poll term.Size() instead.

The example for this item is shown with the ones above: os-signal-Winch.salam

NewWinchSet

func NewWinchSet(): void*
Returns a new signal set that holds only SIGWINCH. Free it with 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*
Gives SIGWINCH a handler that does nothing and blocks it on the calling thread, so resizes queue up for a wait. Returns the previous state: pass it to 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*
Installs handler for sig (null means the default action) and blocks sig on the calling thread. Returns a snapshot of the previous handler and mask: pass it to 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*): bool
Gives back the state an Install 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(): bool
Blocks SIGWINCH on the calling thread so resizes wait in the queue. Threads started afterwards inherit the mask, which lets one watcher thread own the wait. False on Windows or on failure.

The example for this item is shown with the ones above: os-signal-Winch.salam

Wait

func Wait(set: void*, out: void*): int
Waits until a signal in set is pending, writes its number as an i32 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): int
Like Wait, 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*): int
Waits for SIGWINCH on a set from NewWinchSet 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(): bool
Sends SIGWINCH to this process, which wakes a thread waiting in WaitWinch; 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*
Returns a new, empty signal set. Free it with 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*)
Adds every signal the system knows to set. Does nothing for a null set or on Windows.

The example for this item is shown with the ones above: os-signal-Sets.salam

AddSignal

func AddSignal(set: void*, sig: int): bool
Adds sig to set. False when set is null, sig is not positive, or the system rejects the number.

The example for this item is shown with the ones above: os-signal-Sets.salam

DelSignal

func DelSignal(set: void*, sig: int): bool
Removes sig from set. False when set is null, sig is not positive, or the system rejects the number.

The example for this item is shown with the ones above: os-signal-Sets.salam

IsSignal

func IsSignal(set: void*, sig: int): bool
True when sig is in set. False for a null set, a non-positive number, and on Windows.

The example for this item is shown with the ones above: os-signal-Sets.salam

Block

func Block(set: void*): bool
Adds the signals in set to the calling thread's mask, so they wait in the queue instead of interrupting the thread. Do it before starting other threads so they inherit it. 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

Unblock

func Unblock(set: void*): bool
Removes the signals in set from the calling thread's mask. A pending signal is delivered right away. 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

SetMask

func SetMask(set: void*): bool
Replaces the calling thread's whole mask with set, for example to put back a mask saved with CurrentMask. 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*
Returns a new set holding the calling thread's current mask. Free it with 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*): int
Waits until a signal in set is pending and returns its number, or a negative Wait 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): bool
Sends sig to this process. When the signal is blocked it only waits in the queue for a Wait. 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