use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio_util::sync::CancellationToken;
use crate::dispatch::PipelinePosition;
use crate::interpreter::{ExecResult, Scope};
use crate::scheduler::{
drain_to_stream_teed, BoundedStream, JobId, JobManager, PipeReader, DEFAULT_STREAM_MAX_SIZE,
};
use crate::tools::ExecContext;
pub(crate) enum StdinPolicy {
Null,
Inherit,
Piped {
prefix: Option<Vec<u8>>,
pipe: Option<PipeReader>,
},
}
pub(crate) enum OutputPolicy {
Captured,
Inherit {
#[cfg(unix)]
terminal_state: Option<Arc<crate::terminal::TerminalState>>,
},
}
pub(crate) struct SpawnRequest {
pub executable: PathBuf,
pub argv: Vec<String>,
pub cwd: PathBuf,
pub env: Vec<(String, String)>,
pub stdin: StdinPolicy,
pub output: OutputPolicy,
pub label: String,
}
pub(crate) struct SpawnContext {
pub cancel: CancellationToken,
pub kill_grace: Duration,
pub kill_children_on_parent_death: bool,
pub pipeline_position: PipelinePosition,
pub job_manager: Option<Arc<JobManager>>,
pub background_job: Option<JobId>,
}
impl SpawnContext {
pub fn from_exec_context(ctx: &ExecContext) -> Self {
Self {
cancel: ctx.cancel.clone(),
kill_grace: ctx.kill_grace,
kill_children_on_parent_death: ctx.kill_children_on_parent_death,
pipeline_position: ctx.pipeline_position,
job_manager: ctx.job_manager.clone(),
background_job: ctx.background_job,
}
}
}
pub(crate) fn hermetic_env(scope: &Scope) -> anyhow::Result<Vec<(String, String)>> {
let exported = scope.exported_vars();
if let Some(message) = crate::interpreter::structured_export_error(&exported) {
return Err(anyhow::anyhow!(message));
}
let mut env = Vec::with_capacity(exported.len());
for (name, value) in exported {
let text = crate::interpreter::value_to_text_sink_named(
&value,
"an exported environment variable value",
)
.map_err(|e| anyhow::anyhow!("{e}"))?;
env.push((name, text));
}
Ok(env)
}
struct AbortStdinCopyOnDrop(Option<tokio::task::JoinHandle<()>>);
impl Drop for AbortStdinCopyOnDrop {
fn drop(&mut self) {
if let Some(task) = self.0.take() {
task.abort();
}
}
}
pub(crate) async fn spawn_process(request: SpawnRequest, spawn_ctx: &SpawnContext) -> ExecResult {
use tokio::process::Command;
let SpawnRequest {
executable,
argv,
cwd,
env,
stdin,
output,
label,
} = request;
let mut cmd = Command::new(&executable);
cmd.args(&argv);
cmd.current_dir(&cwd);
cmd.env_clear();
for (name, value) in env {
cmd.env(name, value);
}
cmd.stdin(match &stdin {
StdinPolicy::Piped { .. } => std::process::Stdio::piped(),
StdinPolicy::Inherit => std::process::Stdio::inherit(),
StdinPolicy::Null => std::process::Stdio::null(),
});
let inherit_output = matches!(output, OutputPolicy::Inherit { .. });
if inherit_output {
cmd.stdout(std::process::Stdio::inherit());
cmd.stderr(std::process::Stdio::inherit());
} else {
cmd.stdout(std::process::Stdio::piped());
cmd.stderr(std::process::Stdio::piped());
}
#[cfg(unix)]
let terminal_state = match &output {
OutputPolicy::Inherit { terminal_state } => terminal_state.clone(),
OutputPolicy::Captured => None,
};
#[cfg(unix)]
{
let restore_jc_signals = terminal_state.is_some() && inherit_output;
let kill_on_parent_death = spawn_ctx.kill_children_on_parent_death;
let parent_pid = std::process::id();
#[allow(unsafe_code)]
unsafe {
cmd.pre_exec(move || {
nix::unistd::setpgid(nix::unistd::Pid::from_raw(0), nix::unistd::Pid::from_raw(0))
.map_err(|e| std::io::Error::from_raw_os_error(e as i32))?;
if kill_on_parent_death {
crate::dispatch::arm_parent_death_signal(parent_pid)?;
}
if restore_jc_signals {
use nix::libc::{sigaction, SIGINT, SIGTSTP, SIGTTIN, SIGTTOU, SIG_DFL};
let mut sa: nix::libc::sigaction = std::mem::zeroed();
sa.sa_sigaction = SIG_DFL;
if sigaction(SIGTSTP, &sa, std::ptr::null_mut()) != 0 {
return Err(std::io::Error::last_os_error());
}
if sigaction(SIGTTOU, &sa, std::ptr::null_mut()) != 0 {
return Err(std::io::Error::last_os_error());
}
if sigaction(SIGTTIN, &sa, std::ptr::null_mut()) != 0 {
return Err(std::io::Error::last_os_error());
}
if sigaction(SIGINT, &sa, std::ptr::null_mut()) != 0 {
return Err(std::io::Error::last_os_error());
}
}
Ok(())
});
}
}
#[cfg(unix)]
let in_jc_inherit_path = inherit_output && terminal_state.is_some();
#[cfg(not(unix))]
let in_jc_inherit_path = false;
if !in_jc_inherit_path {
cmd.kill_on_drop(true);
}
let mut child = match cmd.spawn() {
Ok(child) => child,
Err(e) => return ExecResult::failure(127, format!("{}: {}", label, e)),
};
#[cfg(unix)]
let kill_target = crate::pidfd::KillTarget::from_child(&child);
#[cfg(not(unix))]
let kill_target: Option<()> = None;
if let (Some(jobs), Some(job_id)) = (&spawn_ctx.job_manager, spawn_ctx.background_job)
&& let Some(pid) = child.id()
{
jobs.add_pgid(job_id, pid).await;
}
let job_streams = match (&spawn_ctx.job_manager, spawn_ctx.background_job) {
(Some(jobs), Some(job_id)) => jobs.streams(job_id).await,
_ => None,
};
let stdin_task: Option<tokio::task::JoinHandle<()>> = match stdin {
StdinPolicy::Piped {
prefix,
pipe: Some(mut pipe_in),
} => child.stdin.take().map(|mut child_stdin| {
tokio::spawn(async move {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
if let Some(data) = prefix
&& child_stdin.write_all(&data).await.is_err()
{
return; }
let mut buf = [0u8; 8192];
loop {
match pipe_in.read(&mut buf).await {
Ok(0) => break, Ok(n) => {
if child_stdin.write_all(&buf[..n]).await.is_err() {
break; }
}
Err(_) => break,
}
}
})
}),
StdinPolicy::Piped {
prefix: Some(data),
pipe: None,
} => child.stdin.take().map(|mut child_stdin| {
tokio::spawn(async move {
use tokio::io::AsyncWriteExt;
let _ = child_stdin.write_all(&data).await;
})
}),
StdinPolicy::Piped {
prefix: None,
pipe: None,
}
| StdinPolicy::Inherit
| StdinPolicy::Null => None,
};
let _stdin_copy_guard = AbortStdinCopyOnDrop(stdin_task);
if inherit_output {
#[cfg(unix)]
if let Some(ref term) = terminal_state {
let child_id = child.id().unwrap_or(0);
let pid = nix::unistd::Pid::from_raw(child_id as i32);
let pgid = pid;
if let Err(e) = term.give_terminal_to(pgid) {
tracing::warn!("failed to give terminal to child: {}", e);
}
let term_clone = term.clone();
let cmd_name = label.clone();
let cmd_display = format!("{} {}", label, argv.join(" "));
let jobs = spawn_ctx.job_manager.clone();
let kill_grace = spawn_ctx.kill_grace;
let wait_complete = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let cancel_watcher = {
let cancel = spawn_ctx.cancel.clone();
let wc = wait_complete.clone();
let target = kill_target
.as_ref()
.map(|t| crate::pidfd::KillTarget::from_pid(t.pid()));
tokio::spawn(async move {
cancel.cancelled().await;
if wc.load(std::sync::atomic::Ordering::SeqCst) {
return;
}
use nix::sys::signal::Signal;
if let Some(t) = &target {
t.signal(Signal::SIGTERM);
t.signal_pg(Signal::SIGTERM);
} else {
let _ = nix::sys::signal::kill(pid, Signal::SIGTERM);
let _ = nix::sys::signal::killpg(pid, Signal::SIGTERM);
}
if kill_grace > Duration::ZERO {
tokio::time::sleep(kill_grace).await;
if wc.load(std::sync::atomic::Ordering::SeqCst) {
return;
}
}
if let Some(t) = &target {
t.signal(Signal::SIGKILL);
t.signal_pg(Signal::SIGKILL);
} else {
let _ = nix::sys::signal::kill(pid, Signal::SIGKILL);
let _ = nix::sys::signal::killpg(pid, Signal::SIGKILL);
}
})
};
struct AbortOnDrop(tokio::task::JoinHandle<()>);
impl Drop for AbortOnDrop {
fn drop(&mut self) {
self.0.abort();
}
}
let _watcher_guard = AbortOnDrop(cancel_watcher);
let wait_complete_setter = wait_complete.clone();
let code = tokio::task::block_in_place(move || {
let result = term_clone.wait_for_foreground(pid);
wait_complete_setter.store(true, std::sync::atomic::Ordering::SeqCst);
if let Err(e) = term_clone.reclaim_terminal() {
tracing::warn!("failed to reclaim terminal: {}", e);
}
match result {
crate::terminal::WaitResult::Exited(code) => code as i64,
crate::terminal::WaitResult::Signaled(sig) => 128 + sig as i64,
crate::terminal::WaitResult::Stopped(_sig) => {
let Some(jobs) = jobs else {
tracing::error!(
command = %cmd_name,
"stopped child cannot be registered: no job manager on this context"
);
return 148;
};
let rt = tokio::runtime::Handle::current();
let job_id = rt.block_on(jobs.register_stopped(
cmd_display,
child_id,
child_id, ));
eprintln!("\n[{}]+ Stopped\t{}", job_id, cmd_name);
148 }
}
});
return ExecResult::from_output(code, String::new(), String::new());
}
let status = match crate::kernel::wait_or_kill(
&mut child,
kill_target.as_ref(),
&spawn_ctx.cancel,
spawn_ctx.kill_grace,
)
.await
{
Ok(s) => s,
Err(e) => {
return ExecResult::failure(1, format!("{}: failed to wait: {}", label, e));
}
};
let code = crate::kernel::exit_code_from_status(&status);
ExecResult::from_output(code, String::new(), String::new())
} else {
let stdout_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
let stderr_stream = Arc::new(BoundedStream::new(DEFAULT_STREAM_MAX_SIZE));
let stdout_pipe = child.stdout.take();
let stderr_pipe = child.stderr.take();
let stdout_clone = stdout_stream.clone();
let stderr_clone = stderr_stream.clone();
let stdout_tee = job_streams.as_ref().and_then(|s| {
matches!(
spawn_ctx.pipeline_position,
PipelinePosition::Only | PipelinePosition::Last
)
.then(|| s.stdout.clone())
});
let stderr_tee = job_streams.as_ref().map(|s| s.stderr.clone());
let stdout_task = stdout_pipe.map(|pipe| {
tokio::spawn(async move {
drain_to_stream_teed(pipe, stdout_clone, stdout_tee).await;
})
});
let stderr_task = stderr_pipe.map(|pipe| {
tokio::spawn(async move {
drain_to_stream_teed(pipe, stderr_clone, stderr_tee).await;
})
});
let cancelled_before_wait = spawn_ctx.cancel.is_cancelled();
let status = match crate::kernel::wait_or_kill(
&mut child,
kill_target.as_ref(),
&spawn_ctx.cancel,
spawn_ctx.kill_grace,
)
.await
{
Ok(s) => s,
Err(e) => {
if let Some(task) = stdout_task {
task.abort();
let _ = task.await;
}
if let Some(task) = stderr_task {
task.abort();
let _ = task.await;
}
return ExecResult::failure(1, format!("{}: failed to wait: {}", label, e));
}
};
if cancelled_before_wait || spawn_ctx.cancel.is_cancelled() {
if let Some(task) = stdout_task {
task.abort();
let _ = task.await;
}
if let Some(task) = stderr_task {
task.abort();
let _ = task.await;
}
} else {
if let Some(task) = stdout_task {
let _ = task.await;
}
if let Some(task) = stderr_task {
let _ = task.await;
}
}
let code = crate::kernel::exit_code_from_status(&status);
let stdout = stdout_stream.read().await;
let mut stderr = stderr_stream.read_string().await;
let mut result = ExecResult::success_text_or_bytes(stdout).with_code(code);
if stderr_stream.has_overflowed().await {
let stats = stderr_stream.stats().await;
stderr = format!("{}{stderr}", stats.overflow_marker("stderr"));
}
if stdout_stream.has_overflowed().await {
let stats = stdout_stream.stats().await;
stderr = format!("{}{stderr}", stats.overflow_marker("stdout"));
result.did_spill = true;
}
result.err = stderr;
result
}
}