use std::cfg_select;
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};
pub mod platform;
cfg_select! {
target_os = "windows" => {
mod platform_win;
pub(crate) use platform_win as platform_imp;
}
target_os = "linux" => {
mod platform_linux;
pub(crate) use platform_linux as platform_imp;
}
target_os = "macos" => {
mod platform_macos;
pub(crate) use platform_macos as platform_imp;
}
}
pub fn configure_compat_tokio_command(
command: &mut Command,
show_console: bool,
kill_when_owner_dies: bool,
) -> io::Result<()> {
platform_imp::configure_compat_tokio_command(command, show_console, kill_when_owner_dies)
}
pub fn after_compat_tokio_spawn(child: &Child, kill_when_owner_dies: bool) {
platform_imp::after_compat_tokio_spawn(child, kill_when_owner_dies)
}
#[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());
platform_imp::configure_command(
&mut command,
self.create_process_group,
self.kill_when_owner_dies,
)?;
let child = command.spawn()?;
platform_imp::after_spawn(&child, self.kill_when_owner_dies);
Ok(PlatformChild::new(child, self.create_process_group))
}
}
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<()> {
platform_imp::signal_process(self.target()?)
}
pub fn terminate_group_soft(&self) -> io::Result<bool> {
if !self.own_process_group {
return Ok(false);
}
platform_imp::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)
}
pub fn shell_spec(command: impl AsRef<OsStr>) -> SpawnSpec {
platform_imp::shell_spec(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);
}
}