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.
165 lines
5 KiB
Zig
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;
|