Skip to main content

blue_build_process_management/
signal_handler.rs

1use std::{
2    fs,
3    path::{Path, PathBuf},
4    process::{self, Command, ExitStatus},
5    sync::{Arc, Mutex, atomic::AtomicBool},
6    thread,
7};
8
9use blue_build_utils::{
10    constants::SUDO_ASKPASS, container::ContainerId, has_env_var, running_as_root,
11};
12use comlexr::cmd;
13use log::{debug, error, trace, warn};
14use miette::{IntoDiagnostic, Result, bail};
15use nix::{
16    libc::{SIGABRT, SIGCONT, SIGHUP, SIGTSTP},
17    sys::signal::{Signal, kill},
18    unistd::Pid,
19};
20use signal_hook::{
21    consts::TERM_SIGNALS,
22    flag,
23    iterator::{SignalsInfo, exfiltrator::WithOrigin},
24    low_level,
25};
26
27use crate::logging::Logger;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct ContainerSignalId {
31    cid_path: PathBuf,
32    requires_sudo: bool,
33    container_runtime: ContainerRuntime,
34}
35
36#[derive(Debug, Copy, Clone, PartialEq, Eq)]
37pub enum ContainerRuntime {
38    Podman,
39    Docker,
40}
41
42impl std::fmt::Display for ContainerRuntime {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        f.write_str(match *self {
45            Self::Podman => "podman",
46            Self::Docker => "docker",
47        })
48    }
49}
50
51impl ContainerSignalId {
52    pub fn new<P>(cid_path: P, container_runtime: ContainerRuntime, requires_sudo: bool) -> Self
53    where
54        P: Into<PathBuf>,
55    {
56        let cid_path = cid_path.into();
57        Self {
58            cid_path,
59            requires_sudo,
60            container_runtime,
61        }
62    }
63}
64
65#[derive(Debug, PartialEq, Eq)]
66pub struct DetachedContainer {
67    signal_id: ContainerSignalId,
68    container_id: ContainerId,
69}
70
71impl DetachedContainer {
72    /// Runs the provided command to start a container with the given signal ID,
73    /// taking the output of the command as the container ID.
74    ///
75    /// # Errors
76    /// Returns an error if the command fails or if the output of the command
77    /// is invalid UTF-8.
78    pub fn start(signal_id: ContainerSignalId, mut cmd: Command) -> Result<Self> {
79        trace!("DetachedContainer::start({signal_id:#?}, {cmd:#?})");
80
81        add_cid(&signal_id);
82
83        let output = cmd.output().into_diagnostic()?;
84        if !output.status.success() {
85            remove_cid(&signal_id);
86            bail!(
87                "Failed to start detached container image.\nstderr: {}",
88                String::from_utf8_lossy(&output.stderr)
89            );
90        }
91        let container_id = {
92            let mut stdout = output.stdout;
93            while stdout.pop_if(|byte| byte.is_ascii_whitespace()).is_some() {}
94            ContainerId(String::from_utf8(stdout).into_diagnostic()?)
95        };
96
97        Ok(Self {
98            signal_id,
99            container_id,
100        })
101    }
102
103    #[must_use]
104    pub const fn id(&self) -> &ContainerId {
105        &self.container_id
106    }
107
108    #[must_use]
109    pub fn cid_path(&self) -> &Path {
110        &self.signal_id.cid_path
111    }
112}
113
114impl Drop for DetachedContainer {
115    fn drop(&mut self) {
116        if kill_container(&self.signal_id).is_ok_and(|exit_status| exit_status.success()) {
117            remove_cid(&self.signal_id);
118        }
119    }
120}
121
122static PID_LIST: std::sync::LazyLock<Arc<Mutex<Vec<i32>>>> =
123    std::sync::LazyLock::new(|| Arc::new(Mutex::new(vec![])));
124static CID_LIST: std::sync::LazyLock<Arc<Mutex<Vec<ContainerSignalId>>>> =
125    std::sync::LazyLock::new(|| Arc::new(Mutex::new(vec![])));
126
127/// Initialize Ctrl-C handler. This should be done at the start
128/// of a binary.
129///
130/// # Panics
131/// Will panic if initialized more than once.
132pub fn init<F>(app_exec: F)
133where
134    F: FnOnce() + Send + 'static,
135{
136    // Make sure double CTRL+C and similar kills
137    let term_now = Arc::new(AtomicBool::new(false));
138    for sig in TERM_SIGNALS {
139        // When terminated by a second term signal, exit with exit code 1.
140        // This will do nothing the first time (because term_now is false).
141        flag::register_conditional_shutdown(*sig, 1, Arc::clone(&term_now))
142            .expect("Register conditional shutdown");
143        // But this will "arm" the above for the second time, by setting it to true.
144        // The order of registering these is important, if you put this one first, it will
145        // first arm and then terminate ‒ all in the first round.
146        flag::register(*sig, Arc::clone(&term_now)).expect("Register signal");
147    }
148
149    let mut signals = vec![SIGABRT, SIGHUP, SIGTSTP, SIGCONT];
150    signals.extend(TERM_SIGNALS);
151    let mut signals = SignalsInfo::<WithOrigin>::new(signals).expect("Need signal info");
152
153    thread::spawn(|| {
154        let app = thread::spawn(app_exec);
155
156        if matches!(app.join(), Ok(())) {
157            exit_unwind(0);
158        } else {
159            error!("App thread panic!");
160            exit_unwind(2);
161        }
162    });
163
164    let mut has_terminal = true;
165    for info in &mut signals {
166        match info.signal {
167            termsig if TERM_SIGNALS.contains(&termsig) => {
168                warn!("Received termination signal, cleaning up...");
169                trace!("{info:#?}");
170
171                Logger::multi_progress()
172                    .clear()
173                    .expect("Should clear multi_progress");
174
175                send_signal_processes(termsig);
176
177                let cid_list = CID_LIST.clone();
178                let cid_list = cid_list.lock().expect("Should lock mutex");
179                cid_list.iter().for_each(|cid| {
180                    let _ = kill_container(cid);
181                });
182                drop(cid_list);
183
184                exit_unwind(1);
185            }
186            SIGTSTP => {
187                if has_terminal {
188                    send_signal_processes(SIGTSTP);
189                    has_terminal = false;
190                    low_level::emulate_default_handler(SIGTSTP).expect("Should stop");
191                }
192            }
193            SIGCONT => {
194                if !has_terminal {
195                    send_signal_processes(SIGCONT);
196                    has_terminal = true;
197                }
198            }
199            _ => {
200                trace!("Received signal {info:#?}");
201            }
202        }
203    }
204}
205
206struct ExitCode {
207    code: i32,
208}
209
210impl Drop for ExitCode {
211    fn drop(&mut self) {
212        process::exit(self.code);
213    }
214}
215
216fn exit_unwind(code: i32) {
217    std::panic::resume_unwind(Box::new(ExitCode { code }));
218}
219
220fn send_signal_processes(sig: i32) {
221    let pid_list = PID_LIST.clone();
222    let pid_list = pid_list.lock().expect("Should lock mutex");
223
224    pid_list.iter().for_each(|pid| {
225        if let Err(e) = kill(
226            Pid::from_raw(*pid),
227            Signal::try_from(sig).expect("Should be valid signal"),
228        ) {
229            error!("Failed to kill process {pid}: Error {e}");
230        } else {
231            trace!("Killed process {pid}");
232        }
233    });
234    drop(pid_list);
235}
236
237/// Add a pid to the list to kill when the program
238/// recieves a kill signal.
239///
240/// # Panics
241/// Will panic if the mutex cannot be locked.
242pub fn add_pid<T>(pid: T)
243where
244    T: TryInto<i32>,
245{
246    if let Ok(pid) = pid.try_into() {
247        let mut pid_list = PID_LIST.lock().expect("Should lock pid_list");
248
249        if !pid_list.contains(&pid) {
250            pid_list.push(pid);
251        }
252    }
253}
254
255/// Remove a pid from the list of pids to kill.
256///
257/// # Panics
258/// Will panic if the mutex cannot be locked.
259pub fn remove_pid<T>(pid: T)
260where
261    T: TryInto<i32>,
262{
263    if let Ok(pid) = pid.try_into() {
264        let mut pid_list = PID_LIST.lock().expect("Should lock pid_list");
265
266        if let Some(index) = pid_list.iter().position(|val| *val == pid) {
267            pid_list.swap_remove(index);
268        }
269    }
270}
271
272/// Add a cid to the list to kill when the program
273/// recieves a kill signal.
274///
275/// # Panics
276/// Will panic if the mutex cannot be locked.
277pub fn add_cid(cid: &ContainerSignalId) {
278    let mut cid_list = CID_LIST.lock().expect("Should lock cid_list");
279
280    if !cid_list.contains(cid) {
281        cid_list.push(cid.clone());
282    }
283}
284
285/// Remove a cid from the list of pids to kill.
286///
287/// # Panics
288/// Will panic if the mutex cannot be locked.
289pub fn remove_cid(cid: &ContainerSignalId) {
290    let mut cid_list = CID_LIST.lock().expect("Should lock cid_list");
291
292    if let Some(index) = cid_list.iter().position(|val| *val == *cid) {
293        cid_list.swap_remove(index);
294    }
295}
296
297fn kill_container(cid: &ContainerSignalId) -> Result<ExitStatus> {
298    fs::read_to_string(&cid.cid_path)
299        .into_diagnostic()
300        .and_then(|id| {
301            let id = id.trim();
302            debug!("Killing container {id}");
303
304            let status = cmd!(
305                if cid.requires_sudo && !running_as_root() {
306                    "sudo".to_string()
307                } else {
308                    cid.container_runtime.to_string()
309                },
310                if cid.requires_sudo && !running_as_root() && has_env_var(SUDO_ASKPASS) => [
311                    "-A",
312                    "-p",
313                    format!("Password needed to kill container {id}"),
314                ],
315                if cid.requires_sudo && !running_as_root() => cid.container_runtime.to_string(),
316                "stop",
317                id
318            )
319            .status();
320
321            if let Err(e) = &status {
322                error!("Failed to kill container {id}: Error {e}");
323            }
324            status.into_diagnostic()
325        })
326}