const std = @import("std"); /// Ctrl+C / SIGINT handling for graceful shutdown /// Implements UX contract semantics from cli-tui-ux-contract-v1.md pub const SignalHandler = struct { /// Current operation type for context-aware shutdown pub const Operation = enum { idle, queue, // Job submission dry_run, // Validation only watch, // Status watch mode sync, // File synchronization hash, // Dataset hashing other, }; var current_operation: Operation = .idle; var shutdown_requested: bool = false; var original_termios: ?std.posix.termios = null; /// Initialize signal handler pub fn init() void { // Set up Ctrl+C handler const act = std.posix.Sigaction{ .handler = .{ .handler = handleSignal }, .mask = std.posix.empty_sigset, .flags = 0, }; std.posix.sigaction(std.posix.SIG.INT, &act, null) catch |err| { std.log.warn("Failed to set up SIGINT handler: {}", .{err}); }; // Also handle SIGTERM for container environments std.posix.sigaction(std.posix.SIG.TERM, &act, null) catch |err| { std.log.warn("Failed to set up SIGTERM handler: {}", .{err}); }; } /// Signal handler callback fn handleSignal(sig: i32) callconv(.C) void { _ = sig; shutdown_requested = true; // Context-aware behavior per UX contract switch (current_operation) { .dry_run => { // Immediate exit, no side effects std.log.info("Dry-run interrupted - exiting immediately", .{}); std.process.exit(0); }, .queue => { // Attempt to cancel submission, show status std.log.info("Queue interrupted - attempting to cancel...", .{}); // Note: actual cancellation logic handled by caller }, .watch => { // Exit watch mode gracefully std.log.info("Watch mode interrupted - exiting", .{}); // Caller should detect shutdown_requested and exit cleanly }, .sync => { // Stop after current file, preserve partial state std.log.info("Sync interrupted - stopping after current operation", .{}); }, .hash => { // Stop hashing, preserve partial results std.log.info("Hash operation interrupted", .{}); }, .idle, .other => { std.log.info("Interrupted - cleaning up...", .{}); }, } } /// Set current operation for context-aware handling pub fn setOperation(op: Operation) void { current_operation = op; } /// Clear current operation pub fn clearOperation() void { current_operation = .idle; } /// Check if shutdown was requested pub fn isShutdownRequested() bool { return shutdown_requested; } /// Reset shutdown flag (for testing) pub fn reset() void { shutdown_requested = false; current_operation = .idle; } /// Run cleanup and exit gracefully pub fn shutdown(exit_code: u8) noreturn { // Restore terminal state if modified if (original_termios) |termios| { _ = std.posix.tcsetattr(std.posix.STDIN_FILENO, .FLUSH, &termios) catch {}; } std.log.info("Shutting down gracefully (exit code: {d})", .{exit_code}); std.process.exit(exit_code); } /// Wait for condition or shutdown request pub fn waitOrShutdown(condition: *bool, timeout_ms: u32) bool { const start = std.time.milliTimestamp(); while (!condition.*) { if (shutdown_requested) return false; const now = std.time.milliTimestamp(); if (now - start > timeout_ms) return false; std.time.sleep(100_000); // 100ms } return true; } /// Print current status before exiting pub fn printStatus(writer: anytype) !void { try writer.print("\nInterrupted during: {s}\n", .{@tagName(current_operation)}); try writer.writeAll("Current operation status preserved.\n"); } }; /// Convenience functions pub fn init() void { SignalHandler.init(); } pub fn setOperation(op: SignalHandler.Operation) void { SignalHandler.setOperation(op); } pub fn clearOperation() void { SignalHandler.clearOperation(); } pub fn isShutdownRequested() bool { return SignalHandler.isShutdownRequested(); } pub fn checkAndShutdown(exit_code: u8) void { if (SignalHandler.isShutdownRequested()) { SignalHandler.shutdown(exit_code); } } /// RAII guard for operation context pub const OperationGuard = struct { pub fn init(op: SignalHandler.Operation) OperationGuard { setOperation(op); return .{}; } pub fn deinit(_: OperationGuard) void { clearOperation(); } }; /// Export for integration with main.zig pub const default_signal_handler = SignalHandler.init;