Skip to main content

ralph_workflow/interrupt/
runtime.rs

1//! Runtime module for interrupt - contains OS-boundary code.
2//!
3//! This module satisfies the dylint boundary-module check for code that uses
4//! std::fs for cleanup operations during signal handling, std::process::exit
5//! for termination, and interior mutability for global interrupt state.
6
7use std::path::Path;
8use std::sync::{Mutex, OnceLock};
9
10use super::checkpoint::InterruptContext;
11
12/// Global interrupt context for checkpoint saving on interrupt.
13///
14/// This is set during pipeline initialization and used by the interrupt
15/// handler to save a checkpoint when the user presses Ctrl+C.
16pub(crate) static INTERRUPT_CONTEXT: OnceLock<Mutex<Option<InterruptContext>>> = OnceLock::new();
17
18fn interrupt_context_slot() -> &'static Mutex<Option<InterruptContext>> {
19    INTERRUPT_CONTEXT.get_or_init(|| Mutex::new(None))
20}
21
22fn lock_slot<'a>(
23    slot: &'a Mutex<Option<InterruptContext>>,
24) -> std::sync::MutexGuard<'a, Option<InterruptContext>> {
25    slot.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
26}
27
28/// Set the global interrupt context.
29pub fn set_interrupt_context(context: InterruptContext) {
30    let mut guard = lock_slot(interrupt_context_slot());
31    *guard = Some(context);
32}
33
34/// Clear the global interrupt context.
35pub fn clear_interrupt_context() {
36    if let Some(slot) = INTERRUPT_CONTEXT.get() {
37        let mut guard = lock_slot(slot);
38        *guard = None;
39    }
40}
41
42/// Get the global interrupt context.
43pub fn get_interrupt_context() -> Option<InterruptContext> {
44    INTERRUPT_CONTEXT.get().and_then(|slot| {
45        let guard = lock_slot(slot);
46        guard.clone()
47    })
48}
49
50/// Exit the process with the standard SIGINT exit code.
51///
52/// This is called from the signal handler when immediate termination is required.
53#[expect(
54    clippy::exit,
55    reason = "Signal handler requires immediate process termination"
56)]
57pub fn exit_sigint() -> ! {
58    std::process::exit(130)
59}
60
61/// Restore prompt.md to writable mode using std::fs.
62///
63/// This is called from the signal handler to ensure the prompt file
64/// is not left read-only if the process is interrupted.
65#[cfg(unix)]
66pub fn restore_prompt_md_writable(path: &Path) -> bool {
67    use std::os::unix::fs::PermissionsExt;
68
69    fn make_writable(path: &Path) -> bool {
70        let Ok(metadata) = std::fs::metadata(path) else {
71            return false;
72        };
73
74        let mut perms = metadata.permissions();
75        perms.set_mode(perms.mode() | 0o200);
76        std::fs::set_permissions(path, perms).is_ok()
77    }
78
79    make_writable(path)
80}
81
82#[cfg(unix)]
83pub fn restore_prompt_md_writable_in_repo(repo_root: &Path) -> bool {
84    use std::os::unix::fs::PermissionsExt;
85
86    fn make_writable(path: &Path) -> bool {
87        let Ok(metadata) = std::fs::metadata(path) else {
88            return false;
89        };
90
91        let mut perms = metadata.permissions();
92        perms.set_mode(perms.mode() | 0o200);
93        std::fs::set_permissions(path, perms).is_ok()
94    }
95
96    let prompt_path = repo_root.join("PROMPT.md");
97    make_writable(&prompt_path)
98}
99
100#[cfg(not(unix))]
101pub fn restore_prompt_md_writable(_path: &Path) -> bool {
102    false
103}
104
105#[cfg(not(unix))]
106pub fn restore_prompt_md_writable_in_repo(_repo_root: &Path) -> bool {
107    false
108}
109
110/// Remove the .git/ralph directory using std::fs.
111pub fn remove_ralph_dir(repo_root: &Path) {
112    let _ = std::fs::remove_dir_all(repo_root.join(".git/ralph"));
113}