use std::process::{Child, Command};
pub const ORIGINATOR_ENV_VAR: &str = "RUNNING_PROCESS_ORIGINATOR";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum Containment {
#[default]
Contained,
Detached,
}
pub struct ContainedProcessGroup {
originator: Option<String>,
#[cfg(windows)]
job: super::WindowsJobHandle,
#[cfg(unix)]
pgid: std::sync::Mutex<Option<i32>>,
#[cfg(unix)]
child_pids: std::sync::Mutex<Vec<u32>>,
}
pub struct ContainedChild {
pub child: Child,
pub containment: Containment,
}
fn format_originator_value(tool: &str) -> String {
format!("{}:{}", tool, std::process::id())
}
impl ContainedProcessGroup {
pub fn new() -> Result<Self, std::io::Error> {
Self::build(None)
}
pub fn with_originator(originator: &str) -> Result<Self, std::io::Error> {
Self::build(Some(originator.to_string()))
}
fn build(originator: Option<String>) -> Result<Self, std::io::Error> {
#[cfg(windows)]
{
Self::new_windows(originator)
}
#[cfg(unix)]
{
Ok(Self {
originator,
pgid: std::sync::Mutex::new(None),
child_pids: std::sync::Mutex::new(Vec::new()),
})
}
}
pub fn originator(&self) -> Option<&str> {
self.originator.as_deref()
}
pub fn originator_value(&self) -> Option<String> {
self.originator.as_ref().map(|o| format_originator_value(o))
}
fn inject_originator_env(&self, command: &mut Command) {
if let Some(ref originator) = self.originator {
command.env(ORIGINATOR_ENV_VAR, format_originator_value(originator));
}
}
pub fn spawn(&self, command: &mut Command) -> Result<ContainedChild, std::io::Error> {
self.spawn_with_containment(command, Containment::Contained)
}
pub fn spawn_detached(&self, command: &mut Command) -> Result<ContainedChild, std::io::Error> {
self.spawn_with_containment(command, Containment::Detached)
}
pub fn spawn_with_containment(
&self,
command: &mut Command,
containment: Containment,
) -> Result<ContainedChild, std::io::Error> {
self.inject_originator_env(command);
#[cfg(windows)]
{
self.spawn_windows(command, containment)
}
#[cfg(unix)]
{
self.spawn_unix(command, containment)
}
}
}
#[cfg(windows)]
impl ContainedProcessGroup {
fn new_windows(originator: Option<String>) -> Result<Self, std::io::Error> {
use std::mem::zeroed;
use winapi::shared::minwindef::FALSE;
use winapi::um::handleapi::INVALID_HANDLE_VALUE;
use winapi::um::jobapi2::{CreateJobObjectW, SetInformationJobObject};
use winapi::um::winnt::{
JobObjectExtendedLimitInformation, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JOB_OBJECT_LIMIT_BREAKAWAY_OK, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
};
let job = unsafe { CreateJobObjectW(std::ptr::null_mut(), std::ptr::null()) };
if job.is_null() || job == INVALID_HANDLE_VALUE {
return Err(std::io::Error::last_os_error());
}
let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = unsafe { zeroed() };
info.BasicLimitInformation.LimitFlags =
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_BREAKAWAY_OK;
let ok = unsafe {
SetInformationJobObject(
job,
JobObjectExtendedLimitInformation,
(&mut info as *mut JOBOBJECT_EXTENDED_LIMIT_INFORMATION).cast(),
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
)
};
if ok == FALSE {
let err = std::io::Error::last_os_error();
unsafe { winapi::um::handleapi::CloseHandle(job) };
return Err(err);
}
Ok(Self {
originator,
job: super::WindowsJobHandle(job as usize),
})
}
fn spawn_windows(
&self,
command: &mut Command,
containment: Containment,
) -> Result<ContainedChild, std::io::Error> {
use winapi::shared::minwindef::FALSE;
use winapi::um::jobapi2::AssignProcessToJobObject;
match containment {
Containment::Contained => {
let child = command.spawn()?;
let handle = {
use std::os::windows::io::AsRawHandle;
child.as_raw_handle()
};
let ok = unsafe {
AssignProcessToJobObject(
self.job.0 as winapi::shared::ntdef::HANDLE,
handle.cast(),
)
};
if ok == FALSE {
return Err(std::io::Error::last_os_error());
}
Ok(ContainedChild { child, containment })
}
Containment::Detached => {
let child = command.spawn()?;
Ok(ContainedChild { child, containment })
}
}
}
}
#[cfg(unix)]
impl ContainedProcessGroup {
fn spawn_unix(
&self,
command: &mut Command,
containment: Containment,
) -> Result<ContainedChild, std::io::Error> {
use std::os::unix::process::CommandExt;
match containment {
Containment::Contained => {
let pgid_lock = self.pgid.lock().expect("pgid mutex poisoned");
let target_pgid = *pgid_lock;
drop(pgid_lock);
unsafe {
command.pre_exec(move || {
let pgid = target_pgid.unwrap_or(0);
if libc::setpgid(0, pgid) == -1 {
return Err(std::io::Error::last_os_error());
}
#[cfg(target_os = "linux")]
{
if libc::prctl(libc::PR_SET_PDEATHSIG, libc::SIGKILL) == -1 {
return Err(std::io::Error::last_os_error());
}
if libc::getppid() == 1 {
libc::_exit(1);
}
}
Ok(())
});
}
let child = command.spawn()?;
let pid = child.id();
let mut pgid_lock = self.pgid.lock().expect("pgid mutex poisoned");
let group_pgid = if let Some(existing) = *pgid_lock {
existing
} else {
*pgid_lock = Some(pid as i32);
pid as i32
};
drop(pgid_lock);
unsafe {
libc::setpgid(pid as i32, group_pgid);
}
self.child_pids
.lock()
.expect("child_pids mutex poisoned")
.push(pid);
Ok(ContainedChild { child, containment })
}
Containment::Detached => {
unsafe {
command.pre_exec(|| {
if libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
let child = command.spawn()?;
Ok(ContainedChild { child, containment })
}
}
}
}
#[cfg(unix)]
impl Drop for ContainedProcessGroup {
fn drop(&mut self) {
let pgid = self.pgid.lock().expect("pgid mutex poisoned");
if let Some(pgid) = *pgid {
unsafe {
libc::killpg(pgid, libc::SIGKILL);
}
}
drop(pgid);
let pids = self.child_pids.lock().expect("child_pids mutex poisoned");
for &pid in pids.iter() {
unsafe {
libc::kill(pid as i32, libc::SIGKILL);
}
}
for &pid in pids.iter() {
unsafe {
libc::waitpid(pid as i32, std::ptr::null_mut(), 0);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn containment_default_is_contained() {
assert_eq!(Containment::default(), Containment::Contained);
}
#[test]
fn containment_clone_and_copy() {
let c = Containment::Contained;
let c2 = c;
assert_eq!(c, c2);
}
#[test]
fn containment_debug_format() {
assert_eq!(format!("{:?}", Containment::Contained), "Contained");
assert_eq!(format!("{:?}", Containment::Detached), "Detached");
}
#[test]
fn contained_process_group_creates_successfully() {
let group = ContainedProcessGroup::new();
assert!(group.is_ok());
}
#[test]
fn with_originator_creates_successfully() {
let group = ContainedProcessGroup::with_originator("CLUD");
assert!(group.is_ok());
let group = group.unwrap();
assert_eq!(group.originator(), Some("CLUD"));
}
#[test]
fn originator_value_format() {
let group = ContainedProcessGroup::with_originator("CLUD").unwrap();
let value = group.originator_value().unwrap();
let expected = format!("CLUD:{}", std::process::id());
assert_eq!(value, expected);
}
#[test]
fn no_originator_returns_none() {
let group = ContainedProcessGroup::new().unwrap();
assert!(group.originator().is_none());
assert!(group.originator_value().is_none());
}
#[test]
fn format_originator_value_correct() {
let value = format_originator_value("JUPYTER");
let parts: Vec<&str> = value.splitn(2, ':').collect();
assert_eq!(parts.len(), 2);
assert_eq!(parts[0], "JUPYTER");
assert_eq!(parts[1], std::process::id().to_string());
}
}