#[cfg(windows)]
mod job_object;
#[cfg(windows)]
use job_object::JobObject;
use std::io;
use std::process::ExitStatus;
pub(crate) fn prepare_command(
command: &mut tokio::process::Command,
) -> &mut tokio::process::Command {
#[cfg(windows)]
{
use windows_sys::Win32::System::Threading::CREATE_NEW_PROCESS_GROUP;
command.creation_flags(CREATE_NEW_PROCESS_GROUP)
}
#[cfg(unix)]
{
command.process_group(0)
}
#[cfg(all(not(windows), not(unix)))]
{
command
}
}
pub(crate) struct ProcessGroup {
leader: tokio::process::Child,
#[cfg(windows)]
job: Option<JobObject>,
}
impl std::fmt::Debug for ProcessGroup {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("ProcessGroup");
s.field("leader", &self.leader);
#[cfg(windows)]
s.field("job", &self.job);
s.finish()
}
}
impl ProcessGroup {
#[allow(clippy::unnecessary_wraps)]
pub(crate) fn from_spawned_child(child: tokio::process::Child) -> Result<Self, io::Error> {
#[cfg(windows)]
{
let job = match child.id() {
Some(_id) => match JobObject::new_with_child(&child) {
Ok(job) => Some(job),
Err(source) => {
let mut child = child;
if let Err(kill_err) = child.start_kill() {
tracing::warn!(
error = %kill_err,
"Failed to kill spawned child after JobObject attachment failed."
);
}
return Err(source);
}
},
None => None,
};
Ok(Self { leader: child, job })
}
#[cfg(not(windows))]
{
Ok(Self { leader: child })
}
}
pub(crate) fn take_stdin(&mut self) -> Option<tokio::process::ChildStdin> {
self.leader.stdin.take()
}
pub(crate) fn take_stdout(&mut self) -> Option<tokio::process::ChildStdout> {
self.leader.stdout.take()
}
pub(crate) fn take_stderr(&mut self) -> Option<tokio::process::ChildStderr> {
self.leader.stderr.take()
}
pub(crate) fn id(&self) -> Option<u32> {
self.leader.id()
}
pub(crate) fn try_wait(&mut self) -> io::Result<Option<ExitStatus>> {
self.leader.try_wait()
}
pub(crate) async fn wait(&mut self) -> io::Result<ExitStatus> {
self.leader.wait().await
}
#[cfg(unix)]
pub(crate) fn send_interrupt(&self) -> io::Result<()> {
let Some(pid) = self.leader.id() else {
return Ok(());
};
send_to_process_group(pid, nix::sys::signal::Signal::SIGINT)
}
#[cfg(unix)]
pub(crate) fn send_terminate(&self) -> io::Result<()> {
let Some(pid) = self.leader.id() else {
return Ok(());
};
send_to_process_group(pid, nix::sys::signal::Signal::SIGTERM)
}
#[cfg(windows)]
pub(crate) fn send_ctrl_break(&self) -> io::Result<()> {
use windows_sys::Win32::System::Console::CTRL_BREAK_EVENT;
use windows_sys::Win32::System::Console::GenerateConsoleCtrlEvent;
let Some(pid) = self.leader.id() else {
return Ok(());
};
let success = unsafe { GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid) };
if success == 0 {
return Err(io::Error::last_os_error());
}
Ok(())
}
pub(crate) fn send_kill(&mut self) -> io::Result<()> {
#[cfg(unix)]
{
match self.leader.id() {
Some(pid) => send_kill_to_process_group(pid),
None => Ok(()),
}
}
#[cfg(windows)]
{
match self.leader.id() {
Some(_) => match &self.job {
Some(job) => job.terminate(1),
None => self.leader.start_kill(),
},
None => Ok(()),
}
}
#[cfg(not(any(unix, windows)))]
{
self.leader.start_kill()
}
}
pub(crate) fn into_leader(self) -> tokio::process::Child {
self.leader
}
}
#[cfg(unix)]
fn send_to_process_group(pid: u32, signal: nix::sys::signal::Signal) -> io::Result<()> {
use nix::sys::signal;
use nix::unistd::Pid;
signal::killpg(Pid::from_raw(pid.cast_signed()), signal).map_err(io::Error::other)?;
Ok(())
}
#[cfg(unix)]
fn send_kill_to_process_group(pid: u32) -> io::Result<()> {
send_to_process_group(pid, nix::sys::signal::Signal::SIGKILL)
}