use std::ffi::OsStr;
use std::path::{Path, PathBuf};
use std::process::ExitStatus;
use tokio::process::{Child as TokioChild, Command as TokioCommand};
use super::{Child, ChildIo, Config, Preset};
use crate::{Context as _, co, pio, str::PathExtension as _};
pub type CommandBuilder = Command<(), (), ()>;
pub struct Command<Out, Err, In> {
command: TokioCommand,
current_dir: Option<PathBuf>,
name: Option<String>,
stdout: Out,
stderr: Err,
stdin: In,
}
impl CommandBuilder {
pub fn new(bin: impl AsRef<OsStr>) -> Self {
Self {
command: TokioCommand::new(bin),
name: None,
current_dir: None,
stdout: (),
stderr: (),
stdin: (),
}
}
}
#[rustfmt::skip]
impl<Out, Err, In> Command<Out, Err, In> {
#[inline(always)]
pub fn arg(mut self, arg: impl AsRef<OsStr>) -> Self { self.command.arg(arg); self }
#[inline(always)]
pub fn args<I: IntoIterator<Item=S>, S:AsRef<OsStr>>(mut self, args: I) -> Self { self.command.args(args); self }
#[inline(always)]
pub fn env_clear(mut self) -> Self { self.command.env_clear(); self }
#[inline(always)]
pub fn env_remove(mut self, env: impl AsRef<OsStr>) -> Self { self.command.env_remove(env); self }
#[inline(always)]
pub fn env(mut self, k: impl AsRef<OsStr>, v: impl AsRef<OsStr>) -> Self { self.command.env(k, v); self }
#[inline(always)]
pub fn envs<I: IntoIterator<Item=(K,V)>,K:AsRef<OsStr>,V:AsRef<OsStr>>(mut self, envs: I) -> Self { self.command.envs(envs); self }
#[inline(always)]
#[allow(clippy::should_implement_trait)]
pub fn add(mut self, config: impl Config) -> Self {
config.configure(&mut self.command);
self
}
#[inline(always)]
pub fn current_dir(mut self, dir: impl Into<PathBuf>) -> Self {
self.current_dir = Some(dir.into());
self
}
#[inline(always)]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[inline(always)]
pub fn stdin<T: pio::ChildInConfig>(self, config:T) -> Command<Out, Err, T> {
Command {
command: self.command,
current_dir: self.current_dir,
name: self.name,
stdout: self.stdout,
stderr: self.stderr,
stdin: config,
}
}
#[inline(always)]
pub fn stdin_null(self) -> Command<Out, Err, pio::Null> {
self.stdin(pio::null())
}
#[inline(always)]
pub fn stdin_inherit(self) -> Command<Out, Err, pio::Inherit> {
self.stdin(pio::inherit())
}
#[inline(always)]
pub fn stdout<T: pio::ChildOutConfig>(self, config: T) -> Command<T, Err, In> {
Command {
command: self.command,
current_dir: self.current_dir,
name: self.name,
stdout: config,
stderr: self.stderr,
stdin: self.stdin
}
}
#[inline(always)]
pub fn stdout_inherit(self) -> Command<pio::Inherit, Err, In> {
self.stdout(pio::inherit())
}
#[inline(always)]
pub fn stderr<T: pio::ChildOutConfig>(self, config: T) -> Command<Out, T, In> {
Command {
command: self.command,
current_dir: self.current_dir,
name: self.name,
stdout: self.stdout,
stderr: config,
stdin: self.stdin
}
}
#[inline(always)]
pub fn stderr_inherit(self) -> Command<Out, pio::Inherit, In> {
self.stderr(pio::inherit())
}
#[inline(always)]
pub fn stdoe<T: pio::ChildOutConfig + Clone>(self, config: T) -> Command<T, T, In> {
Command {
command: self.command,
current_dir: self.current_dir,
name: self.name,
stdout: config.clone(),
stderr: config,
stdin: self.stdin
}
}
#[inline(always)]
pub fn stdout_null(self) -> Command<pio::Null, Err, In> {
self.stdout(pio::null())
}
#[inline(always)]
pub fn stderr_null(self) -> Command<Out, pio::Null, In> {
self.stderr(pio::null())
}
#[inline(always)]
pub fn stdoe_null(self) -> Command<pio::Null, pio::Null, In> {
self.stdoe(pio::null())
}
#[inline(always)]
pub fn stdie_null(self) -> Command<Out, pio::Null, pio::Null> {
self.stdin_null().stderr_null()
}
#[inline(always)]
pub fn stdio_null(self) -> Command<pio::Null, Err, pio::Null> {
self.stdin_null().stdout_null()
}
#[inline(always)]
pub fn all_null(self) -> Command<pio::Null, pio::Null, pio::Null> {
self.stdin_null().stdout_null().stderr_null()
}
#[inline(always)]
pub fn all_inherit(self) -> Command<pio::Inherit, pio::Inherit, pio::Inherit> {
self.stdin_inherit().stdout_inherit().stderr_inherit()
}
#[inline(always)]
pub fn preset<P: Preset>(self, preset: P) -> P::Output {
preset.configure(self)
}
#[cfg(doc)]
pub fn spawn(self) -> crate::Result<EitherOf<Child, (Child, Out, Err)>> {
panic!("this is a placeholder for documetnation, see below for implementation")
}
#[cfg(doc)]
pub async fn co_spawn(self) -> crate::Result<EitherOf<Child, (Child, Out, Err)>> {
panic!("this is a placeholder for documetnation, see below for implementation")
}
}
#[cfg(doc)]
struct EitherOf<A, B>(A, B);
impl<
Out: pio::ChildOutConfig<__Null = pio::__OCNull>,
Err: pio::ChildOutConfig<__Null = pio::__OCNull>,
In: pio::ChildInConfig,
> Command<Out, Err, In>
{
#[inline(always)]
pub fn wait_nz(self) -> crate::Result<()> {
self.spawn()?.wait_nz()
}
#[inline(always)]
pub fn wait(self) -> crate::Result<ExitStatus> {
self.spawn()?.wait()
}
#[inline(always)]
pub async fn co_wait_nz(self) -> crate::Result<()> {
self.co_spawn().await?.co_wait_nz().await
}
#[inline(always)]
pub async fn co_wait(self) -> crate::Result<ExitStatus> {
self.co_spawn().await?.co_wait().await
}
}
pub trait Spawn<Target>
where
Target: Send + 'static,
{
fn spawn(self) -> crate::Result<Target>;
fn co_spawn(self) -> crate::BoxedFuture<crate::Result<Target>>;
}
#[cfg(not(doc))]
macro_rules! Spawned {
() => {
$crate::Child
};
(Out) => {
(
$crate::Child,
<Out::Task as $crate::process::pio::ChildOutTask>::Output,
)
};
($A:ident, $B:ident) => {
(
$crate::Child,
<$A::Task as $crate::process::pio::ChildOutTask>::Output,
<$B::Task as $crate::process::pio::ChildOutTask>::Output,
)
};
}
#[rustfmt::skip]
#[cfg(not(doc))]
impl< Out: pio::ChildOutConfig<__Null=pio::__OCNull>, Err: pio::ChildOutConfig<__Null=pio::__OCNull>, In: pio::ChildInConfig> Spawn<Spawned![]> for Command<Out, Err, In> {
#[inline(always)]
fn spawn(self) -> crate::Result<Spawned![]> {
spawn_internal(self).map(|x| x.0)
}
#[inline(always)]
fn co_spawn(self) -> crate::BoxedFuture<crate::Result<Spawned![]>> {
Box::pin(async move {
co_spawn_internal(self).await.map(|x| x.0)
})
}
}
#[rustfmt::skip]
#[cfg(not(doc))]
impl< Out: pio::ChildOutConfig<__Null=pio::__OCNonNull>, Err: pio::ChildOutConfig<__Null=pio::__OCNull>, In: pio::ChildInConfig> Spawn<Spawned![Out]> for Command<Out, Err, In> {
#[inline(always)]
fn spawn(self) -> crate::Result<Spawned![Out]> {
spawn_internal(self).map(|(c,o,_)| (c,o))
}
#[inline(always)]
fn co_spawn(self) -> crate::BoxedFuture<crate::Result<Spawned![Out]>> {
Box::pin(async move {
co_spawn_internal(self).await.map(|(c,o,_)| (c,o))
})
}
}
#[rustfmt::skip]
#[cfg(not(doc))]
impl< Out: pio::ChildOutConfig, Err: pio::ChildOutConfig<__Null=pio::__OCNonNull>, In: pio::ChildInConfig> Spawn<Spawned![Out, Err]> for Command<Out, Err, In> {
#[inline(always)]
fn spawn(self) -> crate::Result<Spawned![Out, Err]> {
spawn_internal(self)
}
#[inline(always)]
fn co_spawn(self) -> crate::BoxedFuture<crate::Result<Spawned![Out, Err]>> {
Box::pin(co_spawn_internal(self))
}
}
#[allow(clippy::type_complexity)]
fn spawn_internal<Out: pio::ChildOutConfig, Err: pio::ChildOutConfig, In: pio::ChildInConfig>(
mut self_: Command<Out, Err, In>,
) -> crate::Result<(
Child,
<Out::Task as pio::ChildOutTask>::Output,
<Err::Task as pio::ChildOutTask>::Output,
)> {
pre_spawn(&mut self_)?;
co::spawn(async move {
let child = self_.command.spawn().context("failed to spawn command")?;
post_spawn(self_, child)
})
.join()?
}
#[allow(clippy::type_complexity)]
async fn co_spawn_internal<
Out: pio::ChildOutConfig,
Err: pio::ChildOutConfig,
In: pio::ChildInConfig,
>(
mut self_: Command<Out, Err, In>,
) -> crate::Result<(
Child,
<Out::Task as pio::ChildOutTask>::Output,
<Err::Task as pio::ChildOutTask>::Output,
)> {
pre_spawn(&mut self_)?;
co::spawn(async move {
let child = self_.command.spawn().context("failed to spawn command")?;
post_spawn(self_, child)
})
.co_join()
.await?
}
fn pre_spawn<Out: pio::ChildOutConfig, Err: pio::ChildOutConfig, In: pio::ChildInConfig>(
self_: &mut Command<Out, Err, In>,
) -> crate::Result<()> {
use std::fmt::Write as _;
let mut trace = String::new();
let log_enabled = crate::lv::T.enabled();
if log_enabled {
let command = self_.command.as_std();
let _ = write!(
&mut trace,
"spawning '{}', args: [",
command.get_program().display()
);
let mut args = command.get_args();
if let Some(a) = args.next() {
let arg = a.display().to_string().replace('\'', "\\'");
let _ = write!(&mut trace, "'{arg}'");
}
for arg in args {
let arg = arg.display().to_string().replace('\'', "\\'");
let _ = write!(&mut trace, ", '{arg}'");
}
let _ = write!(&mut trace, "]");
}
if let Some(cd) = &self_.current_dir {
let cd = cd.normalize_exists().with_context(|| {
if log_enabled {
crate::trace!("error while {trace}");
}
crate::error!("cannot canonicalize current_dir: {}", cd.display());
"cannot canonicalize current_dir while spawning child"
})?;
if log_enabled {
let _ = write!(&mut trace, ", current_dir: '{}'", cd.display());
}
self_.command.current_dir(cd);
}
if log_enabled {
match &self_.name {
Some(name) => crate::trace!("[{name}] {trace}"),
_ => crate::trace!("{trace}"),
}
}
self_.stdout.configure_stdout(&mut self_.command);
self_.stderr.configure_stderr(&mut self_.command);
self_
.stdin
.configure_stdin(&mut self_.command)
.context("failed to configure child stdin")?;
Ok(())
}
#[allow(clippy::type_complexity)]
fn post_spawn<Out: pio::ChildOutConfig, Err: pio::ChildOutConfig, In: pio::ChildInConfig>(
self_: Command<Out, Err, In>,
mut child: TokioChild,
) -> crate::Result<(
Child,
<Out::Task as pio::ChildOutTask>::Output,
<Err::Task as pio::ChildOutTask>::Output,
)> {
let name = self_.name;
let stdout = self_
.stdout
.take(&mut child, name.as_deref(), true)
.context("failed to take child stdout")?;
let stderr = self_
.stderr
.take(&mut child, name.as_deref(), false)
.context("failed to take child stderr")?;
let stdin = self_
.stdin
.take(&mut child)
.context("failed to take child stdin")?;
let name = match name {
Some(x) => format!("[{x}]"),
None => {
let command = self_.command.as_std();
let program = Path::new(command.get_program())
.file_name()
.map(|x| x.display().to_string());
match program {
Some(x) => format!("program '{x}'"),
None => "child process".to_string(),
}
}
};
use pio::ChildInTask as _;
use pio::ChildOutTask as _;
let (stdout_future, stdout) = stdout.run();
let (stderr_future, stderr) = stderr.run();
let stdin_future = stdin.run();
let io = ChildIo::start(stdin_future, stdout_future, stderr_future);
Ok((
Child {
name,
inner: child,
io,
},
stdout,
stderr,
))
}