use std::ffi::{OsStr, OsString};
use std::io;
use std::path::PathBuf;
use std::process::{ExitStatus, Output, Stdio};
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
#[cfg(windows)]
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamMode {
Inherit,
Piped,
Null,
}
impl StreamMode {
fn apply(self) -> Stdio {
match self {
Self::Inherit => Stdio::inherit(),
Self::Piped => Stdio::piped(),
Self::Null => Stdio::null(),
}
}
}
#[derive(Debug, Clone)]
pub struct SpawnSpec {
program: OsString,
args: Vec<OsString>,
current_dir: Option<PathBuf>,
env: Vec<(OsString, OsString)>,
clear_env: bool,
stdin: StreamMode,
stdout: StreamMode,
stderr: StreamMode,
create_process_group: bool,
kill_when_owner_dies: bool,
}
impl SpawnSpec {
pub fn new(program: impl Into<OsString>) -> Self {
Self {
program: program.into(),
args: Vec::new(),
current_dir: None,
env: Vec::new(),
clear_env: false,
stdin: StreamMode::Inherit,
stdout: StreamMode::Inherit,
stderr: StreamMode::Inherit,
create_process_group: false,
kill_when_owner_dies: false,
}
}
pub fn arg(mut self, arg: impl Into<OsString>) -> Self {
self.args.push(arg.into());
self
}
pub fn current_dir(mut self, path: impl Into<PathBuf>) -> Self {
self.current_dir = Some(path.into());
self
}
pub fn env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
self.env.push((key.into(), value.into()));
self
}
pub fn clear_env(mut self, clear: bool) -> Self {
self.clear_env = clear;
self
}
pub fn stdin(mut self, mode: StreamMode) -> Self {
self.stdin = mode;
self
}
pub fn stdout(mut self, mode: StreamMode) -> Self {
self.stdout = mode;
self
}
pub fn stderr(mut self, mode: StreamMode) -> Self {
self.stderr = mode;
self
}
pub fn create_process_group(mut self, create: bool) -> Self {
self.create_process_group = create;
self
}
pub fn kill_when_owner_dies(mut self, kill: bool) -> Self {
self.kill_when_owner_dies = kill;
self
}
pub async fn spawn(self) -> io::Result<PlatformChild> {
let mut command = Command::new(&self.program);
command.args(&self.args);
if let Some(current_dir) = self.current_dir.as_deref() {
command.current_dir(current_dir);
}
if self.clear_env {
command.env_clear();
}
for (key, value) in &self.env {
command.env(key, value);
}
command
.stdin(self.stdin.apply())
.stdout(self.stdout.apply())
.stderr(self.stderr.apply());
if self.create_process_group {
#[cfg(unix)]
command.process_group(0);
#[cfg(windows)]
command.creation_flags(CREATE_NEW_PROCESS_GROUP);
}
#[cfg(target_os = "linux")]
if self.kill_when_owner_dies {
let owner_pid = unsafe { libc::getpid() };
unsafe {
command.pre_exec(move || {
if libc::prctl(
libc::PR_SET_PDEATHSIG,
libc::SIGTERM as libc::c_ulong,
0,
0,
0,
) == -1
{
return Err(io::Error::last_os_error());
}
if libc::getppid() != owner_pid {
libc::kill(libc::getpid(), libc::SIGTERM);
}
Ok(())
});
}
}
#[cfg(target_os = "macos")]
if self.kill_when_owner_dies {
let owner_pid = unsafe { libc::getpid() };
unsafe {
command.pre_exec(move || {
let supervisor = libc::fork();
if supervisor < 0 {
return Err(io::Error::last_os_error());
}
if supervisor == 0 {
macos_owner_death_supervisor(owner_pid);
}
Ok(())
});
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
let _ = self.kill_when_owner_dies;
let child = command.spawn()?;
#[cfg(windows)]
if self.kill_when_owner_dies {
windows_owner_death_job::assign(child.raw_handle());
}
Ok(PlatformChild::new(child, self.create_process_group))
}
}
#[cfg(target_os = "macos")]
fn macos_owner_death_supervisor(owner_pid: libc::pid_t) -> ! {
let target_pid = unsafe { libc::getppid() };
unsafe {
for fd in 3..1024 {
libc::close(fd);
}
}
let queue = unsafe { libc::kqueue() };
if queue < 0 {
unsafe { libc::_exit(127) };
}
let mut watches = [
libc::kevent {
ident: owner_pid as libc::uintptr_t,
filter: libc::EVFILT_PROC,
flags: libc::EV_ADD | libc::EV_ONESHOT,
fflags: libc::NOTE_EXIT,
data: 0,
udata: std::ptr::null_mut(),
},
libc::kevent {
ident: target_pid as libc::uintptr_t,
filter: libc::EVFILT_PROC,
flags: libc::EV_ADD | libc::EV_ONESHOT,
fflags: libc::NOTE_EXIT,
data: 0,
udata: std::ptr::null_mut(),
},
];
let registered = unsafe {
libc::kevent(
queue,
watches.as_mut_ptr(),
watches.len() as i32,
std::ptr::null_mut(),
0,
std::ptr::null(),
)
};
if registered < 0 {
unsafe {
libc::close(queue);
libc::_exit(127);
}
}
if unsafe { libc::kill(owner_pid, 0) } < 0
&& io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH)
{
unsafe {
libc::kill(target_pid, libc::SIGTERM);
libc::close(queue);
libc::_exit(0);
}
}
let mut events = [unsafe { std::mem::zeroed::<libc::kevent>() }];
loop {
let count = unsafe {
libc::kevent(
queue,
std::ptr::null(),
0,
events.as_mut_ptr(),
1,
std::ptr::null(),
)
};
if count <= 0 {
if count < 0 && io::Error::last_os_error().raw_os_error() == Some(libc::EINTR) {
continue;
}
break;
}
if events[0].ident == owner_pid as libc::uintptr_t {
unsafe {
libc::kill(target_pid, libc::SIGTERM);
}
}
break;
}
unsafe {
libc::close(queue);
libc::_exit(0);
}
}
#[cfg(windows)]
mod windows_owner_death_job {
use std::sync::OnceLock;
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::System::JobObjects::{
AssignProcessToJobObject, CreateJobObjectW, JobObjectExtendedLimitInformation,
SetInformationJobObject, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
};
struct Job(HANDLE);
unsafe impl Send for Job {}
unsafe impl Sync for Job {}
static JOB: OnceLock<Option<Job>> = OnceLock::new();
fn create() -> Option<Job> {
unsafe {
let handle = CreateJobObjectW(std::ptr::null(), std::ptr::null());
if handle.is_null() {
return None;
}
let mut info: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if SetInformationJobObject(
handle,
JobObjectExtendedLimitInformation,
&info as *const _ as *const _,
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
) == 0
{
return None;
}
Some(Job(handle))
}
}
pub(super) fn assign(child: Option<HANDLE>) {
let Some(child) = child else { return };
let Some(job) = JOB.get_or_init(create).as_ref() else {
return;
};
unsafe {
AssignProcessToJobObject(job.0, child);
}
}
}
pub struct PlatformChild {
child: Child,
stdin: Option<ChildStdin>,
stdout: Option<ChildStdout>,
stderr: Option<ChildStderr>,
signal: PlatformEmergencySignal,
}
impl PlatformChild {
fn new(mut child: Child, own_process_group: bool) -> Self {
let signal = PlatformEmergencySignal {
pid: child.id(),
own_process_group,
};
Self {
stdin: child.stdin.take(),
stdout: child.stdout.take(),
stderr: child.stderr.take(),
child,
signal,
}
}
pub fn id(&self) -> Option<u32> {
self.child.id()
}
pub async fn wait(&mut self) -> io::Result<ExitStatus> {
self.child.wait().await
}
pub async fn kill(&mut self) -> io::Result<()> {
self.child.kill().await
}
pub async fn wait_with_output(self) -> io::Result<Output> {
let Self {
mut child,
stdin,
stdout,
stderr,
..
} = self;
drop(stdin);
let (status, stdout, stderr) = tokio::try_join!(
child.wait(),
read_owned_to_end(stdout),
read_owned_to_end(stderr),
)?;
Ok(Output {
status,
stdout,
stderr,
})
}
pub async fn write_stdin(&mut self, bytes: &[u8]) -> io::Result<()> {
let stdin = self.stdin.as_mut().ok_or_else(stdin_not_piped)?;
stdin.write_all(bytes).await?;
stdin.flush().await
}
pub fn close_stdin(&mut self) {
drop(self.stdin.take());
}
pub async fn read_stdout_to_end(&mut self) -> io::Result<Vec<u8>> {
let stdout = self.stdout.as_mut().ok_or_else(stdout_not_piped)?;
let mut bytes = Vec::new();
stdout.read_to_end(&mut bytes).await?;
Ok(bytes)
}
pub async fn read_stderr_to_end(&mut self) -> io::Result<Vec<u8>> {
let stderr = self.stderr.as_mut().ok_or_else(stderr_not_piped)?;
let mut bytes = Vec::new();
stderr.read_to_end(&mut bytes).await?;
Ok(bytes)
}
pub fn into_actor_parts(
self,
) -> (
PlatformLifecycle,
PlatformEmergencySignal,
Option<PlatformStdin>,
Option<PlatformOutput>,
Option<PlatformOutput>,
) {
(
PlatformLifecycle { child: self.child },
self.signal,
self.stdin.map(|stdin| PlatformStdin { stdin }),
self.stdout.map(PlatformOutput::stdout),
self.stderr.map(PlatformOutput::stderr),
)
}
}
pub struct PlatformLifecycle {
child: Child,
}
impl PlatformLifecycle {
pub async fn wait(&mut self) -> io::Result<ExitStatus> {
self.child.wait().await
}
}
pub struct PlatformEmergencySignal {
pid: Option<u32>,
own_process_group: bool,
}
impl PlatformEmergencySignal {
pub fn kill(&self) -> io::Result<()> {
signal_process(self.target()?)
}
pub fn terminate_group_soft(&self) -> io::Result<bool> {
if !self.own_process_group {
return Ok(false);
}
signal_process_group(self.target()?).map(|()| true)
}
fn target(&self) -> io::Result<u32> {
self.pid.ok_or_else(|| {
io::Error::new(
io::ErrorKind::BrokenPipe,
"child process no longer has an emergency signal target",
)
})
}
}
pub struct PlatformStdin {
stdin: ChildStdin,
}
impl PlatformStdin {
pub async fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
self.stdin.write_all(bytes).await?;
self.stdin.flush().await
}
}
pub struct PlatformOutput {
reader: OutputReader,
}
enum OutputReader {
Stdout(ChildStdout),
Stderr(ChildStderr),
}
impl PlatformOutput {
fn stdout(stdout: ChildStdout) -> Self {
Self {
reader: OutputReader::Stdout(stdout),
}
}
fn stderr(stderr: ChildStderr) -> Self {
Self {
reader: OutputReader::Stderr(stderr),
}
}
pub async fn read_to_end(self) -> io::Result<Vec<u8>> {
match self.reader {
OutputReader::Stdout(stdout) => read_owned_to_end(Some(stdout)).await,
OutputReader::Stderr(stderr) => read_owned_to_end(Some(stderr)).await,
}
}
pub async fn read_chunk(&mut self, buffer: &mut [u8]) -> io::Result<usize> {
match &mut self.reader {
OutputReader::Stdout(stdout) => stdout.read(buffer).await,
OutputReader::Stderr(stderr) => stderr.read(buffer).await,
}
}
}
fn stdin_not_piped() -> io::Error {
io::Error::new(io::ErrorKind::BrokenPipe, "child stdin is not piped")
}
fn stdout_not_piped() -> io::Error {
io::Error::new(io::ErrorKind::BrokenPipe, "child stdout is not piped")
}
fn stderr_not_piped() -> io::Error {
io::Error::new(io::ErrorKind::BrokenPipe, "child stderr is not piped")
}
async fn read_owned_to_end<R>(reader: Option<R>) -> io::Result<Vec<u8>>
where
R: AsyncRead + Unpin,
{
let Some(mut reader) = reader else {
return Ok(Vec::new());
};
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
Ok(bytes)
}
#[cfg(unix)]
fn signal_process(pid: u32) -> io::Result<()> {
unix_kill(pid as i32, libc::SIGKILL)
}
#[cfg(unix)]
fn signal_process_group(pid: u32) -> io::Result<()> {
unix_kill(-(pid as i32), libc::SIGTERM)
}
#[cfg(unix)]
fn unix_kill(target: i32, signal: i32) -> io::Result<()> {
let result = unsafe { libc::kill(target, signal) };
if result == 0 {
return Ok(());
}
let error = io::Error::last_os_error();
if error.raw_os_error() == Some(libc::ESRCH) {
Ok(())
} else {
Err(error)
}
}
#[cfg(windows)]
fn signal_process(pid: u32) -> io::Result<()> {
use windows_sys::Win32::Foundation::{CloseHandle, ERROR_INVALID_PARAMETER};
use windows_sys::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) };
if handle.is_null() {
let error = io::Error::last_os_error();
return if error.raw_os_error() == Some(ERROR_INVALID_PARAMETER as i32) {
Ok(())
} else {
Err(error)
};
}
let terminated = unsafe { TerminateProcess(handle, 1) };
let termination_error = if terminated == 0 {
Some(io::Error::last_os_error())
} else {
None
};
unsafe { CloseHandle(handle) };
termination_error.map_or(Ok(()), Err)
}
#[cfg(windows)]
fn signal_process_group(pid: u32) -> io::Result<()> {
use windows_sys::Win32::Foundation::ERROR_INVALID_HANDLE;
use windows_sys::Win32::System::Console::{GenerateConsoleCtrlEvent, CTRL_BREAK_EVENT};
if unsafe { GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid) } != 0 {
return Ok(());
}
let error = io::Error::last_os_error();
if error.raw_os_error() == Some(ERROR_INVALID_HANDLE as i32) {
Ok(())
} else {
Err(error)
}
}
pub fn shell_spec(command: impl AsRef<OsStr>) -> SpawnSpec {
#[cfg(windows)]
{
SpawnSpec::new("cmd.exe").arg("/C").arg(command.as_ref())
}
#[cfg(not(windows))]
{
SpawnSpec::new("/bin/sh").arg("-c").arg(command.as_ref())
}
}
#[cfg(test)]
mod tests {
use super::{shell_spec, SpawnSpec, StreamMode};
fn fixture_command() -> SpawnSpec {
#[cfg(windows)]
{
shell_spec("echo async-platform-internal")
}
#[cfg(not(windows))]
{
shell_spec("printf async-platform-internal")
}
}
#[tokio::test]
async fn blessed_spawn_captures_output_without_sync_wait() {
let output = fixture_command()
.stdout(StreamMode::Piped)
.stderr(StreamMode::Piped)
.spawn()
.await
.expect("spawn")
.wait_with_output()
.await
.expect("wait with output");
assert!(output.status.success());
let expected = if cfg!(windows) {
b"async-platform-internal\r\n".as_slice()
} else {
b"async-platform-internal".as_slice()
};
assert_eq!(output.stdout, expected);
assert!(output.stderr.is_empty());
}
#[tokio::test]
async fn blessed_spawn_reports_missing_program() {
let result = SpawnSpec::new("running-process-program-that-does-not-exist")
.spawn()
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn one_shot_output_closes_owned_stdin() {
#[cfg(windows)]
let spec = shell_spec("more > nul & echo done");
#[cfg(not(windows))]
let spec = shell_spec("cat > /dev/null; printf done");
let output = tokio::time::timeout(
std::time::Duration::from_secs(2),
spec.stdin(StreamMode::Piped)
.stdout(StreamMode::Piped)
.stderr(StreamMode::Piped)
.spawn()
.await
.expect("spawn")
.wait_with_output(),
)
.await
.expect("stdin is closed for one-shot output")
.expect("output succeeds");
let expected = if cfg!(windows) {
b"done\r\n".as_slice()
} else {
b"done".as_slice()
};
assert_eq!(output.stdout, expected);
}
}