In long-running applications (such as HTTP servers, background workers, or daemons), catching operating system signals (such as SIGINT from Ctrl+C or SIGTERM from process supervisors) and performing a graceful shutdown is crucial for preventing state corruption and resource leaks.

Key concepts:

  • std.posix.Sigaction: Configures signal response characteristics, specifying a C-calling-convention handler function (callconv(.c)) and an initial signal mask via std.posix.sigemptyset();
  • std.posix.sigaction(sig, &act, null): Registers the signal action with the OS kernel;
  • Atomic flags: Signal handlers must execute minimal, async-signal-safe logic. Setting an atomic boolean (std.atomic.Value(bool)) allows the main event or worker loop to detect shutdown requests and safely perform multistep cleanup (draining requests, flushing disk buffers, closing sockets).

//! Demonstrate POSIX signal handling and graceful shutdown in Zig.

const builtin = @import("builtin");
const std = @import("std");
const posix = std.posix;
const print = std.debug.print;

// Atomic flag indicating whether a shutdown signal has been caught.
var should_exit = std.atomic.Value(bool).init(false);

// C-compatible signal handler function.
fn handleSignal(sig: posix.SIG) callconv(.c) void {
    _ = sig;
    should_exit.store(true, .monotonic);
}

pub fn main(init: std.process.Init) !void {
    if (comptime builtin.os.tag == .windows) {
        print("POSIX signals are not supported on Windows.\n", .{});
        return;
    }

    const io = init.io;

    // 1. Configure the signal action struct.
    const act: posix.Sigaction = .{
        .handler = .{ .handler = handleSignal },
        .mask = posix.sigemptyset(),
        .flags = 0,
    };

    // 2. Register signal handlers for SIGINT (Ctrl-C) and SIGTERM (kill).
    posix.sigaction(posix.SIG.INT, &act, null);
    posix.sigaction(posix.SIG.TERM, &act, null);

    print("Signal handlers registered for SIGINT and SIGTERM.\n", .{});

    // 3. For automated testing and demonstration, send SIGINT to ourselves.
    // In a real server application, the loop below would run until an external signal arrives.
    print("Simulating arrival of SIGINT signal...\n", .{});
    try posix.kill(posix.system.getpid(), posix.SIG.INT);

    // 4. Main worker loop periodically checking the shutdown flag.
    while (!should_exit.load(.monotonic)) {
        try std.Io.sleep(io, .fromMilliseconds(50), .awake);
    }

    // 5. Graceful shutdown sequence.
    print("Shutdown signal detected! Starting cleanup sequence...\n", .{});
    // In actual applications: flush logs, close open connections, commit pending transactions.
    try std.Io.sleep(io, .fromMilliseconds(20), .awake);
    print("Cleanup completed. Exiting cleanly.\n", .{});
}