fetch_ml/cli/src/core/signals.zig
Jeremie Fraeys 524f440fe4
feat(cli): add core system components for CLI hardening
Add signal handling, environment detection, and secrets management:
- signals.zig: graceful Ctrl+C handling and signal management
- environment.zig: user environment detection for telemetry
- secrets.zig: secrets redaction for secure logging

Improves CLI reliability and security posture.
2026-03-04 20:23:12 -05:00

165 lines
5 KiB
Zig

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;