Skip to content

Commit 7840e51

Browse files
committed
rust: add supervisor function to forward signals to child processes
1 parent 8e9526c commit 7840e51

File tree

4 files changed

+141
-13
lines changed

4 files changed

+141
-13
lines changed

rust/bear/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ nom.workspace = true
4040
regex.workspace = true
4141
rand.workspace = true
4242
tempfile.workspace = true
43+
nix = { version = "0.29", optional = true, features = ["signal", "process"] }
44+
winapi = { version = "0.3", optional = true, features = ["processthreadsapi", "winnt", "handleapi"] }
45+
signal-hook = "0.3.17"
46+
47+
[target.'cfg(unix)'.dependencies]
48+
nix = { version = "0.29", features = ["signal", "process"] }
49+
50+
[target.'cfg(windows)'.dependencies]
51+
winapi = { version = "0.3", features = ["processthreadsapi", "winnt", "handleapi"] }
4352

4453
[profile.release]
4554
strip = true

rust/bear/src/bin/wrapper.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
extern crate core;
1919

2020
use anyhow::{Context, Result};
21+
use bear::intercept::supervise::supervise;
2122
use bear::intercept::tcp::ReporterOnTcp;
2223
use bear::intercept::Reporter;
2324
use bear::intercept::KEY_DESTINATION;
@@ -46,13 +47,11 @@ fn main() -> Result<()> {
4647
}
4748

4849
// Execute the real executable with the same arguments
49-
// TODO: handle signals and forward them to the child process.
50-
let status = std::process::Command::new(real_executable)
51-
.args(std::env::args().skip(1))
52-
.status()?;
53-
log::info!("Execution finished with status: {:?}", status);
50+
let mut command = std::process::Command::new(real_executable);
51+
let exit_status = supervise(command.args(std::env::args().skip(1)))?;
52+
log::info!("Execution finished with status: {:?}", exit_status);
5453
// Return the child process status code
55-
std::process::exit(status.code().unwrap_or(1));
54+
std::process::exit(exit_status.code().unwrap_or(1));
5655
}
5756

5857
/// Get the file name of the executable from the arguments.

rust/bear/src/intercept/mod.rs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
//! The module provides abstractions for the reporter and the collector. And it also defines
1010
//! the data structures that are used to represent the events.
1111
12+
use crate::intercept::supervise::supervise;
1213
use crate::{args, config};
1314
use serde::{Deserialize, Serialize};
1415
use std::collections::HashMap;
@@ -19,6 +20,7 @@ use std::sync::Arc;
1920
use std::{env, fmt, thread};
2021

2122
pub mod persistence;
23+
pub mod supervise;
2224
pub mod tcp;
2325

2426
/// Declare the environment variables used by the intercept mode.
@@ -267,17 +269,17 @@ impl InterceptEnvironment {
267269
// TODO: record the execution of the build command
268270

269271
let environment = self.environment();
270-
let mut child = Command::new(input.arguments[0].clone())
271-
.args(input.arguments[1..].iter())
272-
.envs(environment)
273-
.spawn()?;
272+
let process = input.arguments[0].clone();
273+
let arguments = input.arguments[1..].to_vec();
274274

275-
// TODO: forward signals to the child process
276-
let result = child.wait()?;
275+
let mut child = Command::new(process);
276+
277+
let exit_status = supervise(child.args(arguments).envs(environment))?;
278+
log::info!("Execution finished with status: {:?}", exit_status);
277279

278280
// The exit code is not always available. When the process is killed by a signal,
279281
// the exit code is not available. In this case, we return the `FAILURE` exit code.
280-
let exit_code = result
282+
let exit_code = exit_status
281283
.code()
282284
.map(|code| ExitCode::from(code as u8))
283285
.unwrap_or(ExitCode::FAILURE);
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
3+
use anyhow::Result;
4+
use nix::libc::c_int;
5+
#[cfg(unix)]
6+
use nix::sys::signal::{kill, Signal};
7+
#[cfg(unix)]
8+
use nix::unistd::Pid;
9+
use std::process::{Command, ExitStatus};
10+
use std::sync::atomic::{AtomicBool, Ordering};
11+
use std::sync::Arc;
12+
use std::thread;
13+
use std::time::Duration;
14+
#[cfg(windows)]
15+
use winapi::shared::minwindef::FALSE;
16+
#[cfg(windows)]
17+
use winapi::um::processthreadsapi::{OpenProcess, TerminateProcess};
18+
#[cfg(windows)]
19+
use winapi::um::winnt::{PROCESS_TERMINATE, SYNCHRONIZE};
20+
21+
/// This method supervises the execution of a command.
22+
///
23+
/// It starts the command and waits for its completion. It also forwards
24+
/// signals to the child process. The method returns the exit status of the
25+
/// child process.
26+
pub fn supervise(command: &mut Command) -> Result<ExitStatus> {
27+
let mut child = command.spawn()?;
28+
29+
let child_pid = child.id();
30+
let running = Arc::new(AtomicBool::new(true));
31+
let running_in_thread = running.clone();
32+
33+
let mut signals = signal_hook::iterator::Signals::new([
34+
signal_hook::consts::SIGINT,
35+
signal_hook::consts::SIGTERM,
36+
])?;
37+
38+
#[cfg(unix)]
39+
{
40+
signals.add_signal(signal_hook::consts::SIGHUP)?;
41+
signals.add_signal(signal_hook::consts::SIGQUIT)?;
42+
signals.add_signal(signal_hook::consts::SIGALRM)?;
43+
signals.add_signal(signal_hook::consts::SIGUSR1)?;
44+
signals.add_signal(signal_hook::consts::SIGUSR2)?;
45+
signals.add_signal(signal_hook::consts::SIGCONT)?;
46+
signals.add_signal(signal_hook::consts::SIGSTOP)?;
47+
}
48+
49+
let handler = thread::spawn(move || {
50+
for signal in signals.forever() {
51+
log::debug!("Received signal: {:?}", signal);
52+
if forward_signal(signal, child_pid) {
53+
// If the signal caused termination, we should stop the process.
54+
running_in_thread.store(false, Ordering::SeqCst);
55+
break;
56+
}
57+
}
58+
});
59+
60+
while running.load(Ordering::SeqCst) {
61+
thread::sleep(Duration::from_millis(100));
62+
}
63+
handler.join().unwrap();
64+
65+
let exit_status = child.wait()?;
66+
67+
Ok(exit_status)
68+
}
69+
70+
#[cfg(windows)]
71+
fn forward_signal(_: c_int, child_pid: u32) -> bool {
72+
let process_handle = unsafe { OpenProcess(PROCESS_TERMINATE | SYNCHRONIZE, FALSE, child_pid) };
73+
if process_handle.is_null() {
74+
let err = unsafe { winapi::um::errhandling::GetLastError() };
75+
log::error!("Failed to open process: {}", err);
76+
// If the process handle is not valid, presume the process is not running anymore.
77+
return true;
78+
}
79+
80+
let terminated = unsafe { TerminateProcess(process_handle, 1) };
81+
if terminated == FALSE {
82+
let err = unsafe { winapi::um::errhandling::GetLastError() };
83+
log::error!("Failed to terminate process: {}", err);
84+
}
85+
86+
// Ensure proper handle closure
87+
unsafe { winapi::um::handleapi::CloseHandle(process_handle) };
88+
89+
// Return true if the process was terminated.
90+
terminated == TRUE
91+
}
92+
93+
#[cfg(unix)]
94+
fn forward_signal(signal: c_int, child_pid: u32) -> bool {
95+
// Forward the signal to the child process
96+
if let Err(e) = kill(
97+
Pid::from_raw(child_pid as i32),
98+
Signal::try_from(signal).ok(),
99+
) {
100+
log::error!("Error forwarding signal: {}", e);
101+
}
102+
103+
// Return true if the process was terminated.
104+
match kill(Pid::from_raw(child_pid as i32), None) {
105+
Ok(_) => {
106+
log::debug!("Checking if the process is still running... yes");
107+
false
108+
}
109+
Err(nix::Error::ESRCH) => {
110+
log::debug!("Checking if the process is still running... no");
111+
true
112+
}
113+
Err(_) => {
114+
log::debug!("Checking if the process is still running... presume dead");
115+
true
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)