1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
//! Signal handling for graceful cleanup on process interruption
//!
//! This module provides signal handlers that update execution status when the process
//! is interrupted by signals like SIGINT (Ctrl+C), SIGTERM (kill), or SIGHUP (terminal close).
use std::sync::atomic::{AtomicBool, AtomicI32, Ordering};
use std::sync::Mutex;
use crate::execution_store::{ExecutionRecord, ExecutionStore};
// Global state for signal handling cleanup
// These are used to update execution status when the process is interrupted
static SIGNAL_RECEIVED: AtomicBool = AtomicBool::new(false);
static SIGNAL_EXIT_CODE: AtomicI32 = AtomicI32::new(0);
static CURRENT_EXECUTION: Mutex<Option<(ExecutionRecord, ExecutionStore)>> = Mutex::new(None);
// When set, a signal only records its exit code; the running code polls
// `was_signal_received()` and finishes itself (`--use-formal-ai`, issue #255,
// which must stop its tool's process group and its runtime before exiting).
static DEFERRED: AtomicBool = AtomicBool::new(false);
/// Make signals only record their exit code (true) or exit at once (false).
pub fn set_deferred_signals(deferred: bool) {
DEFERRED.store(deferred, Ordering::SeqCst);
}
/// Check if a signal has been received
#[allow(dead_code)]
pub fn was_signal_received() -> bool {
SIGNAL_RECEIVED.load(Ordering::SeqCst)
}
/// Get the exit code from the received signal
#[allow(dead_code)]
pub fn get_signal_exit_code() -> i32 {
SIGNAL_EXIT_CODE.load(Ordering::SeqCst)
}
/// Set up signal handlers for graceful cleanup on interruption
#[cfg(unix)]
pub fn setup_signal_handlers() {
use std::sync::Once;
static INIT: Once = Once::new();
INIT.call_once(|| {
unsafe {
// SIGINT (Ctrl+C) - exit code 130 (128 + 2)
libc::signal(libc::SIGINT, signal_handler as *const () as usize);
// SIGTERM (kill command) - exit code 143 (128 + 15)
libc::signal(libc::SIGTERM, signal_handler as *const () as usize);
// SIGHUP (terminal closed) - exit code 129 (128 + 1)
libc::signal(libc::SIGHUP, signal_handler as *const () as usize);
}
});
}
#[cfg(not(unix))]
pub fn setup_signal_handlers() {
// Signal handling not supported on non-Unix platforms
}
/// Signal handler function
#[cfg(unix)]
extern "C" fn signal_handler(sig: i32) {
// Calculate exit code based on signal (128 + signal number)
let exit_code = 128 + sig;
SIGNAL_EXIT_CODE.store(exit_code, Ordering::SeqCst);
SIGNAL_RECEIVED.store(true, Ordering::SeqCst);
if DEFERRED.load(Ordering::SeqCst) {
return;
}
// Try to clean up the current execution record
cleanup_execution_on_signal(sig, exit_code);
// Exit with the appropriate code
std::process::exit(exit_code);
}
/// Clean up execution record when a signal is received
#[cfg(unix)]
fn cleanup_execution_on_signal(signal: i32, exit_code: i32) {
if let Ok(mut guard) = CURRENT_EXECUTION.lock() {
if let Some((ref mut record, ref store)) = *guard {
// Mark as completed with signal exit code
record.complete(exit_code);
if let Err(e) = store.save(record) {
// Log error to stderr (can't easily check config here)
eprintln!(
"\n[Tracking] Warning: Could not save execution record on signal {}: {}",
signal, e
);
}
// Clear the record to prevent double cleanup
*guard = None;
}
}
}
/// Set the current execution record for signal cleanup
pub fn set_current_execution(record: ExecutionRecord, store: ExecutionStore) {
if let Ok(mut guard) = CURRENT_EXECUTION.lock() {
*guard = Some((record, store));
}
}
/// Clear the current execution record (call after normal completion)
pub fn clear_current_execution() {
if let Ok(mut guard) = CURRENT_EXECUTION.lock() {
*guard = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_signal_received_initially_false() {
// Note: This test might be flaky if run after other tests that set the flag
// In a fresh process, SIGNAL_RECEIVED should be false
// We can't reliably test this after setup_signal_handlers is called
}
#[test]
fn test_set_and_clear_current_execution() {
// Just verify the functions don't panic
clear_current_execution();
}
}