Instance Method
signalStream(_:)
Yields the signal number each time one of signals fires on this loop.
func signalStream(_ signals: Int32...) -> AsyncStream<Int32>
Parameters
-
signals
-
One or more signal numbers to observe (e.g. SIGTERM, SIGINT, SIGHUP, SIGUSR1).
Return Value
An AsyncStream<Int32> yielding the signal number each time any of the requested signals fires.
Mentioned In
Discussion
Backed by libevent’s evsignal_* machinery. libevent installs its own signal-safe handler internally (the C-side handler only notes the signal and lets the loop dispatch the callback in normal context), so the closure body runs in regular Swift Concurrency context — none of the async-signal-safety restrictions of raw signal(2) handlers apply. This mirrors the safety story of Apple’s DispatchSource.makeSignalSource(_:queue:), which also defers callbacks off the signal handler.
The stream is unbuffered by default (matches AsyncStream’s no-arg constructor): values are delivered to the iterator if it is currently awaiting, or buffered until it does. Stream termination — for await … break, the iterating task being cancelled, or the returned stream being dropped — calls event_del(3) on each registered signal event, restoring the prior signal disposition.
Important
libevent permits at most one event_base to claim a given signal at a time. If two EventLoop instances in the same process call signalStream for the same signal number, the second registration’s behavior is undefined. In practice you should register process-global signals on a single event loop — shared is the typical choice.
Tip
When unit-testing code that consumes a signal stream, fire signals with kill(getpid(), SIGUSR1) rather than raise(SIGUSR1). raise() is thread-directed (equivalent to pthread_kill(pthread_self(), …)) and only delivers to the calling thread; if the loop is being driven on a different thread — the typical test pattern — libevent’s handler never sees the signal and the test hangs. kill(getpid(), …) is process-directed so any unblocked thread, including the loop’s driver, can receive it. Production users whose signals come from external processes (kill -TERM <pid>, Ctrl-C from a shell) never hit this — those are already process-directed.
Composes cleanly with swift-service-lifecycle: a service’s run() body can iterate this stream and call gracefulShutdown() on the first value, breaking out of the loop to trigger automatic teardown.