//! SIGINT and SIGTERM, turned into one `std.Io.Event`. //! //! No signalfd, no self-pipe, no epoll: the handler does exactly one thing, and //! `std.Io.Event.set` is async-signal-safe on the Threaded Linux backend — a //! raw `futex` wake with no allocation and no lock (`Io.zig:1855` → //! `Threaded.futexWake`). The `.mask`/`.flags` shape is the one Threaded uses //! for its own `SIG.IO`/`SIG.PIPE` handlers (`Threaded.zig:1653`): an empty //! mask and no `SA_RESTART`, so a blocking syscall returns `EINTR` and the //! backend's retry loop re-reads the cancellation state. //! //! Everything a shutdown actually has to do — drain the query log, cancel the //! task group, close the databases — happens on the task blocked in `wait`. //! //! The previous handlers are not restored. The process is leaving, and a //! second SIGTERM during teardown should still terminate it the default way //! only if the operator sends it before this module is armed. const std = @import("std"); const posix = std.posix; var event: std.Io.Event = .unset; /// Read by the signal handler, written by `install` before the handler exists. /// A `std.Io` is two pointers and cannot be stored atomically, so ordering is /// what makes the read safe: the store precedes the `sigaction` syscall that /// arms the handler, and no signal can reach the handler before that call /// returns. var handler_io: ?std.Io = null; var installed: bool = false; /// Arms the handlers for INT and TERM. Calling it again is a no-op: the process /// has one event and one pair of handlers, and a second boot inside one process /// (which only a test does) must not re-arm anything. pub fn install(io: std.Io) void { if (installed) return; handler_io = io; installed = true; const act: posix.Sigaction = .{ .handler = .{ .handler = onSignal }, .mask = posix.sigemptyset(), .flags = 0, }; posix.sigaction(.INT, &act, null); posix.sigaction(.TERM, &act, null); } fn onSignal(_: posix.SIG) callconv(.c) void { const io = handler_io orelse return; event.set(io); } /// Blocks until a shutdown is requested. A canceled wait is the caller's cue to /// tear down as well, which is why `app.run` treats both results the same. pub fn wait(io: std.Io) std.Io.Cancelable!void { return event.wait(io); } /// The programmatic equivalent of the signal: what a test uses to shut the app /// down, and what a Phase 8 restart endpoint would call. pub fn trigger(io: std.Io) void { event.set(io); } pub fn isRequested() bool { return event.isSet(); } /// Clears the request so the next `wait` blocks again. Only a test that boots /// the app more than once in one process needs this; a served process shuts /// down once. pub fn reset() void { event.reset(); } const testing = std.testing; test "trigger releases a waiter and isRequested reports it" { var threaded: std.Io.Threaded = .init(testing.allocator, .{}); defer threaded.deinit(); const io = threaded.io(); reset(); try testing.expect(!isRequested()); trigger(io); try testing.expect(isRequested()); // Already set, so this returns without blocking. try wait(io); reset(); try testing.expect(!isRequested()); } test "a waiting task is released by a later trigger" { var threaded: std.Io.Threaded = .init(testing.allocator, .{}); defer threaded.deinit(); const io = threaded.io(); reset(); var group: std.Io.Group = .init; try group.concurrent(io, waitThenSet, .{ io, &done }); trigger(io); try group.await(io); try testing.expect(done.isSet()); reset(); done.reset(); } var done: std.Io.Event = .unset; fn waitThenSet(io: std.Io, flag: *std.Io.Event) std.Io.Cancelable!void { try wait(io); flag.set(io); }