Skip to content

std/signal ​

POSIX signal handling.

milo
from "std/signal" import { onSignal, ignoreSignal, resetSignal, SIGINT, SIGTERM, SIGHUP, SIGQUIT, SIGABRT, SIGKILL, SIGALRM }

Constants ​

NameValueDescription
SIGHUP1Hangup
SIGINT2Interrupt (Ctrl+C)
SIGQUIT3Quit
SIGABRT6Abort
SIGKILL9Kill (cannot be caught)
SIGALRM14Alarm timer
SIGTERM15Termination

onSignal takes a top-level fn (i32): void passed as a raw pointer (myHandler as *u8), never a closure: a C signal handler has no user-data slot, so a captured environment has nowhere to live.

Example ​

milo
from "std/signal" import { onSignal, SIGINT, SIGTERM }
from "std/os" import { exit }

fn onInterrupt(_sig: i32): void {
    print("caught interrupt, cleaning up...")
    exit(0)
}

fn onTerminate(_sig: i32): void {
    print("terminated")
    exit(0)
}

fn main(): i32 {
    // @unsafe: C calls whatever address it is handed, so it must be a top-level fn.
    unsafe {
        onSignal(SIGINT, onInterrupt as *u8)
        onSignal(SIGTERM, onTerminate as *u8)
    }

    // main loop...
    return 0
}

API reference ​

SignalPipe ​

milo
pub struct SignalPipe

An installed self-pipe for one signal (see installSignalPipe). It goes readable each time the signal fires: arm it with sel.onRead(p) and call p.drain() after it wakes. Dropping it uninstalls: the signal goes back to its default action, and both ends of the pipe close. It owns the descriptors, so the read end cannot be closed under a Select that still has it armed, nor outlive the handler that writes to it.

SignalPipe.channel ​

milo
fn SignalPipe.channel(self: &SignalPipe): Channel<string>

One message per wake, pumped on a green task (see fdChannel): for _ in p.channel() runs its body each time the signal fires. Keep the SignalPipe alive while consuming; dropping it closes the pipe under the pump.

SignalPipe.drain ​

milo
fn SignalPipe.drain(self: &SignalPipe): bool

Drain pending bytes (call after the pipe wakes a Select). True when there were any, i.e. the signal fired since the last drain: a loop that wakes for several reasons asks this rather than peeking at the pipe itself.

Functions ​

ignoreSignal ​

milo
pub fn ignoreSignal(sig: i32): void

Ignore a signal.

installSignalPipe ​

milo
pub fn installSignalPipe(sig: i32): Result<SignalPipe, string>

Install a self-pipe for sig. One pipe per signal, so several can be armed at once (SIGWINCH for resizes and SIGCHLD for child exits is the motivating pair). Re-installing the same signal replaces its pipe. Errs on a signal number out of range or when the pipe cannot be created.

onSignal ​

milo
pub fn onSignal(sig: i32, handler: *u8): void

Register a handler for a signal. The handler receives the signal number.

handler must be a top-level fn (i32): void passed as a raw pointer — onSignal(sigchld(), myHandler as *u8) — NOT a closure. A C signal handler has no user-data slot, so a captured environment has nowhere to live. This used to be typed (i32) => void, which made it a closure whose code pointer takes (env, sig); C then called it with the signal number in the env slot and the handler read garbage as its sig (observed: 1794499728 instead of 20). Nothing caught it because the only in-tree handler, _sigPipeHandler, ignores its argument.

resetSignal ​

milo
pub fn resetSignal(sig: i32): void

Reset a signal to default behavior.

sigchld ​

milo
pub fn sigchld(): i32

Child stopped or exited. A fn, not a let like its siblings above, for two reasons that both bite: its number is NOT the same on both platforms (20 darwin / 17 linux, verified against sys/signal.h and asm-generic/signal.h), so it has to come from the std/platform split; and module-scope runtime initialization is rejected, so let SIGCHLD = ...() cannot work at all — the checker catches that rather than letting it become 0. This is what a child-exit Select arm needs: arm it through installSignalPipe and a reaped child becomes just another readable fd.