use std::fs::OpenOptions;
use std::io::{self, Read, Write};
use std::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command, Stdio};
use std::sync::{Arc, LazyLock};
use std::thread;
use std::time::{Duration, Instant};
use harn_vm::process_sandbox;
use super::handle::{
EnvMode, ExitStatus, OutputCapture, ProcessCleanupReport, ProcessError, ProcessHandle,
ProcessKiller, ProcessSpawner, SpawnSpec, WaitOutcome,
};
pub struct RealSpawner;
static REAL_SPAWNER: LazyLock<Arc<dyn ProcessSpawner>> =
LazyLock::new(|| Arc::new(RealSpawner) as Arc<dyn ProcessSpawner>);
pub fn default_spawner() -> Arc<dyn ProcessSpawner> {
Arc::clone(&REAL_SPAWNER)
}
impl ProcessSpawner for RealSpawner {
fn spawn(&self, spec: SpawnSpec) -> Result<Box<dyn ProcessHandle>, ProcessError> {
let (mut command, cleanup_token) = prepare_command(&spec, None)?;
let child = command.spawn().map_err(map_spawn_error)?;
let pid = child.id();
let pgid = child_process_group_id(pid);
let killer: Arc<dyn ProcessKiller> = Arc::new(RealKiller {
pid,
cleanup_token: cleanup_token.clone(),
});
Ok(Box::new(RealProcess {
pid,
pgid,
cleanup_token,
killer,
child: Some(child),
stdin: None,
stdout: None,
stderr: None,
stdin_taken: false,
stdout_taken: false,
stderr_taken: false,
}))
}
}
fn prepare_command(
spec: &SpawnSpec,
cleanup_token: Option<String>,
) -> Result<(Command, String), ProcessError> {
if spec.program.is_empty() {
return Err(ProcessError::InvalidArgv(
"first element of argv must be a non-empty program name".to_string(),
));
}
let mut command = process_sandbox::std_command_for(&spec.program, &spec.args)
.map_err(|e| ProcessError::SandboxSetup(format!("{e:?}")))?;
if let Some(cwd) = spec.cwd.as_ref() {
process_sandbox::enforce_process_cwd(cwd)
.map_err(|e| ProcessError::SandboxCwd(format!("{e:?}")))?;
command.current_dir(cwd);
}
match spec.env_mode {
EnvMode::Replace => {
command.env_clear();
}
EnvMode::InheritClean | EnvMode::Patch => {
for (key, _) in std::env::vars_os() {
if let Some(name) = key.to_str() {
if super::handle::is_sensitive_env_name(name) {
command.env_remove(&key);
}
}
}
}
}
for key in &spec.env_remove {
command.env_remove(key);
}
for (key, value) in &spec.env {
command.env(key, value);
}
for (key, value) in process_sandbox::active_workspace_tmpdir_env() {
if spec.env.contains_key(&key) {
continue;
}
command.env(key, value);
}
if !spec
.env
.contains_key(process_sandbox::MESSAGE_LOCALE_OVERRIDE_ENV)
{
command.env_remove(process_sandbox::MESSAGE_LOCALE_OVERRIDE_ENV);
}
for (key, value) in process_sandbox::deterministic_message_locale_env() {
if spec.env.contains_key(&key) {
continue;
}
command.env(key, value);
}
if spec.configure_process_group {
configure_background_process_group(&mut command);
}
let cleanup_token =
cleanup_token.unwrap_or_else(harn_vm::op_interrupt::new_process_cleanup_token);
command.env(
harn_vm::op_interrupt::PROCESS_CLEANUP_TOKEN_ENV,
&cleanup_token,
);
match &spec.output_capture {
OutputCapture::Inherit => {
command.stdout(Stdio::inherit());
command.stderr(Stdio::inherit());
}
OutputCapture::Pipe => {
command.stdout(Stdio::piped());
command.stderr(Stdio::piped());
}
OutputCapture::File {
stdout_path,
stderr_path,
} => {
let stdout = OpenOptions::new()
.write(true)
.truncate(true)
.open(stdout_path)
.map_err(|error| ProcessError::Spawn(format!("open stdout capture: {error}")))?;
let stderr = OpenOptions::new()
.write(true)
.truncate(true)
.open(stderr_path)
.map_err(|error| ProcessError::Spawn(format!("open stderr capture: {error}")))?;
command.stdout(Stdio::from(stdout));
command.stderr(Stdio::from(stderr));
}
}
command.stdin(match (&spec.output_capture, spec.use_stdin) {
(OutputCapture::Inherit, true) => Stdio::inherit(),
(_, true) => Stdio::piped(),
(_, false) => Stdio::null(),
});
Ok((command, cleanup_token))
}
fn map_spawn_error(error: io::Error) -> ProcessError {
if let Some(violation) = process_sandbox::process_spawn_error(&error) {
return ProcessError::SandboxSpawn(format!("{violation:?}"));
}
ProcessError::Spawn(error.to_string())
}
#[cfg(unix)]
pub fn replace_current_process(spec: SpawnSpec) -> Result<std::convert::Infallible, ProcessError> {
use std::os::unix::process::CommandExt;
super::handle::validate_process_spec(&spec)?;
let inherited_cleanup_token = std::env::var(harn_vm::op_interrupt::PROCESS_CLEANUP_TOKEN_ENV)
.ok()
.filter(|token| !token.is_empty());
let (mut command, _cleanup_token) = prepare_command(&spec, inherited_cleanup_token)?;
Err(map_spawn_error(command.exec()))
}
struct RealProcess {
pid: u32,
pgid: Option<u32>,
cleanup_token: String,
killer: Arc<dyn ProcessKiller>,
child: Option<Child>,
stdin: Option<ChildStdin>,
stdout: Option<ChildStdout>,
stderr: Option<ChildStderr>,
stdin_taken: bool,
stdout_taken: bool,
stderr_taken: bool,
}
impl RealProcess {
fn ensure_pipes_taken(&mut self) {
if let Some(child) = self.child.as_mut() {
if self.stdin.is_none() && !self.stdin_taken {
self.stdin = child.stdin.take();
}
if self.stdout.is_none() && !self.stdout_taken {
self.stdout = child.stdout.take();
}
if self.stderr.is_none() && !self.stderr_taken {
self.stderr = child.stderr.take();
}
}
}
}
impl ProcessHandle for RealProcess {
fn pid(&self) -> Option<u32> {
Some(self.pid)
}
fn process_group_id(&self) -> Option<u32> {
self.pgid
}
fn killer(&self) -> Arc<dyn ProcessKiller> {
Arc::clone(&self.killer)
}
fn take_stdin(&mut self) -> Option<Box<dyn Write + Send>> {
self.ensure_pipes_taken();
self.stdin_taken = true;
self.stdin
.take()
.map(|s| Box::new(s) as Box<dyn Write + Send>)
}
fn take_stdout(&mut self) -> Option<Box<dyn Read + Send>> {
self.ensure_pipes_taken();
self.stdout_taken = true;
self.stdout
.take()
.map(|s| Box::new(s) as Box<dyn Read + Send>)
}
fn take_stderr(&mut self) -> Option<Box<dyn Read + Send>> {
self.ensure_pipes_taken();
self.stderr_taken = true;
self.stderr
.take()
.map(|s| Box::new(s) as Box<dyn Read + Send>)
}
fn wait_with_timeout(
&mut self,
timeout: Option<Duration>,
interrupt: &dyn Fn() -> bool,
) -> io::Result<WaitOutcome> {
let killer = Arc::clone(&self.killer);
let Some(child) = self.child.as_mut() else {
return Err(io::Error::other("child already reaped"));
};
let deadline = timeout.map(|timeout| Instant::now() + timeout);
loop {
match child.try_wait()? {
Some(status) => return Ok(WaitOutcome::Exited(decode_status(status))),
None => {
if interrupt() {
let (_, report) =
harn_vm::op_interrupt::terminate_child_group_with_cleanup_token_report(
child,
Some(&self.cleanup_token),
);
return Ok(WaitOutcome::Interrupted(report));
}
if deadline.is_some_and(|deadline| Instant::now() >= deadline) {
let mut report = killer.kill();
let _ = child.kill();
let _ = child.wait();
report.refresh_survivor_status();
return Ok(WaitOutcome::TimedOut(report));
}
let sleep = deadline
.map(|deadline| deadline.saturating_duration_since(Instant::now()))
.unwrap_or(Duration::MAX)
.min(Duration::from_millis(20));
thread::sleep(sleep);
}
}
}
}
fn wait(&mut self) -> io::Result<ExitStatus> {
let child = self
.child
.as_mut()
.ok_or_else(|| io::Error::other("child already reaped"))?;
let status = child.wait()?;
Ok(decode_status(status))
}
}
struct RealKiller {
pid: u32,
cleanup_token: String,
}
impl ProcessKiller for RealKiller {
fn kill(&self) -> ProcessCleanupReport {
let report = harn_vm::op_interrupt::signal_pid_tree_group_and_token_with_report(
self.pid,
Some(&self.cleanup_token),
9,
);
#[cfg(target_os = "windows")]
terminate_process(self.pid);
report
}
}
#[cfg(target_os = "windows")]
fn terminate_process(pid: u32) {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE};
let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) };
if handle.is_null() {
return;
}
unsafe {
TerminateProcess(handle, 1);
CloseHandle(handle);
}
}
#[cfg(unix)]
fn decode_status(status: std::process::ExitStatus) -> ExitStatus {
use std::os::unix::process::ExitStatusExt;
if let Some(code) = status.code() {
ExitStatus::from_code(code)
} else if let Some(sig) = status.signal() {
ExitStatus::from_signal(sig)
} else {
ExitStatus {
code: None,
signal: None,
}
}
}
#[cfg(not(unix))]
fn decode_status(status: std::process::ExitStatus) -> ExitStatus {
ExitStatus::from_code(status.code().unwrap_or(-1))
}
pub(crate) fn child_process_group_id(pid: u32) -> Option<u32> {
#[cfg(unix)]
{
extern "C" {
fn getpgid(pid: i32) -> i32;
}
let pgid = unsafe { getpgid(pid as i32) };
if pgid > 0 {
Some(pgid as u32)
} else {
None
}
}
#[cfg(not(unix))]
{
Some(pid)
}
}
pub(crate) fn configure_background_process_group(command: &mut std::process::Command) {
#[cfg(unix)]
unsafe {
use std::os::unix::process::CommandExt;
command.pre_exec(|| {
extern "C" {
fn setpgid(pid: i32, pgid: i32) -> i32;
}
if setpgid(0, 0) == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
#[cfg(not(unix))]
{
let _ = command;
}
}