blue_build_process_management/
signal_handler.rs1use 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 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
127pub fn init<F>(app_exec: F)
133where
134 F: FnOnce() + Send + 'static,
135{
136 let term_now = Arc::new(AtomicBool::new(false));
138 for sig in TERM_SIGNALS {
139 flag::register_conditional_shutdown(*sig, 1, Arc::clone(&term_now))
142 .expect("Register conditional shutdown");
143 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
237pub 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
255pub 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
272pub 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
285pub 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}