use std::path::{Path, PathBuf};
use std::process::{Child, Command, Stdio};
use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};
use std::time::Duration;
use crate::environment::{
resolve_environment, EnvironmentLayer, EnvironmentReport, EnvironmentSource, ResolveOptions,
};
use crate::error::{fail, Error, Result};
use crate::execution::assert_execution_files;
use crate::filesystem::collect_files;
use crate::path::{join_relative, safe_relative_path};
use crate::prepare::{
verify_and_extract_box, verify_required_assets, EnvironmentReportOptions, PrepareOptions,
PreparedBox,
};
use crate::release::Execution;
use crate::trust::TrustAnchors;
pub struct BoxInvocation<'a> {
pub program: &'a Path,
pub args: &'a [String],
pub cwd: &'a Path,
pub environment: &'a std::collections::BTreeMap<String, String>,
pub stdin: StdioMode,
pub stdout: StdioMode,
pub stderr: StdioMode,
}
pub trait SpawnBox {
fn spawn(&self, invocation: &BoxInvocation<'_>) -> std::io::Result<Box<dyn RunningBox>>;
}
pub trait RunningBox {
fn try_wait(&mut self) -> std::io::Result<Option<(Option<i32>, Option<String>)>>;
fn forward(&mut self, signal: ForwardedSignal);
}
pub struct ProcessSpawner;
impl SpawnBox for ProcessSpawner {
fn spawn(&self, invocation: &BoxInvocation<'_>) -> std::io::Result<Box<dyn RunningBox>> {
let mut command = Command::new(invocation.program);
command
.args(invocation.args)
.current_dir(invocation.cwd)
.env_clear()
.envs(invocation.environment)
.stdin(invocation.stdin.to_stdio())
.stdout(invocation.stdout.to_stdio())
.stderr(invocation.stderr.to_stdio());
Ok(Box::new(ChildProcess(command.spawn()?)))
}
}
struct ChildProcess(Child);
impl RunningBox for ChildProcess {
fn try_wait(&mut self) -> std::io::Result<Option<(Option<i32>, Option<String>)>> {
Ok(self
.0
.try_wait()?
.map(|status| (status.code(), terminating_signal(status))))
}
fn forward(&mut self, signal: ForwardedSignal) {
send_signal(&mut self.0, signal);
}
}
const POLL_INTERVAL: Duration = Duration::from_millis(50);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ForwardedSignal {
Interrupt,
Terminate,
Hangup,
}
impl ForwardedSignal {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::Interrupt => "SIGINT",
Self::Terminate => "SIGTERM",
Self::Hangup => "SIGHUP",
}
}
}
pub type SignalSender = Sender<ForwardedSignal>;
pub type SignalReceiver = Receiver<ForwardedSignal>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum StdioMode {
#[default]
Inherit,
Null,
Piped,
}
impl StdioMode {
fn to_stdio(self) -> Stdio {
match self {
Self::Inherit => Stdio::inherit(),
Self::Null => Stdio::null(),
Self::Piped => Stdio::piped(),
}
}
}
#[derive(Default)]
pub struct RunOptions<'a> {
pub args: Vec<String>,
pub env: Vec<(String, String)>,
pub stdin: StdioMode,
pub stdout: StdioMode,
pub stderr: StdioMode,
pub signals: Option<&'a SignalReceiver>,
pub on_environment_report: Option<&'a dyn Fn(&EnvironmentReport)>,
pub environment: EnvironmentReportOptions,
pub host_environment: Option<Vec<(String, String)>>,
pub spawn: Option<&'a dyn SpawnBox>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BoxRunResult {
pub exit_code: Option<i32>,
pub signal: Option<String>,
pub environment_report: EnvironmentReport,
}
fn resolve_run_environment(
prepared: &PreparedBox,
options: &RunOptions<'_>,
) -> Result<crate::environment::ResolvedEnvironment> {
let release = prepared.release();
let adapter = prepared.adapter();
let host: Vec<(String, String)> = options
.host_environment
.clone()
.unwrap_or_else(|| std::env::vars().collect());
let declared = release.environment.clone().unwrap_or_default();
resolve_environment(&ResolveOptions {
platform: adapter.platform,
layers: vec![
EnvironmentLayer {
source: EnvironmentSource::Host,
values: host
.iter()
.map(|(name, value)| (name.as_str(), value.as_str()))
.collect(),
},
EnvironmentLayer {
source: EnvironmentSource::Caller,
values: options
.env
.iter()
.map(|(name, value)| (name.as_str(), value.as_str()))
.collect(),
},
EnvironmentLayer {
source: EnvironmentSource::Release,
values: declared
.iter()
.map(|(name, value)| (name.as_str(), value.as_str()))
.collect(),
},
],
execution_affecting_variables: adapter.execution_affecting_environment_variables,
expanded: options.environment.env_report || options.environment.env_report_values,
reveal_host_values: options.environment.env_report_values,
})
}
pub fn run_extracted_box(prepared: &PreparedBox, options: &RunOptions<'_>) -> Result<BoxRunResult> {
let release = prepared.release();
let Some(execution) = release.execution.as_ref() else {
fail!("Box does not declare an execution entry point.");
};
let adapter = prepared.adapter();
if crate::contract::targets::assert_native_host(adapter).is_err() {
fail!(
"Box target {} cannot run on {}/{}; it requires {}/{}.",
prepared.target_id(),
std::env::consts::OS,
std::env::consts::ARCH,
adapter.host_os,
adapter.host_arch
);
}
prepared.assert_root_unchanged()?;
let root = prepared.root();
let files = collect_files(root)?;
if !files.contains(&release.python_entry_point) {
fail!("Prepared box is missing {}.", release.python_entry_point);
}
assert_execution_files(
Some(execution),
adapter,
&release.provenance.python_version,
&files,
)?;
verify_required_assets(root, prepared.required_assets())?;
let python = join_relative(root, &safe_relative_path(&release.python_entry_point)?);
let mut arguments: Vec<String> = match execution {
Execution::PythonScript { script, .. } => vec![join_relative(root, &safe_relative_path(script)?)
.to_string_lossy()
.into_owned()],
Execution::PythonModule { module, .. } => vec!["-m".to_string(), module.clone()],
};
match execution {
Execution::PythonScript { default_args, .. }
| Execution::PythonModule { default_args, .. } => {
arguments.extend(default_args.iter().cloned());
}
}
arguments.extend(options.args.iter().cloned());
let resolved = resolve_run_environment(prepared, options)?;
if let Some(report) = options.on_environment_report {
report(&resolved.report);
}
let invocation = BoxInvocation {
program: &python,
args: &arguments,
cwd: root,
environment: &resolved.environment,
stdin: options.stdin,
stdout: options.stdout,
stderr: options.stderr,
};
let spawner: &dyn SpawnBox = options.spawn.unwrap_or(&ProcessSpawner);
let child = spawner.spawn(&invocation).map_err(|error| {
Error::new(format!(
"Box interpreter failed to start: {}: {error}",
python.display()
))
})?;
let (exit_code, signal) = wait_for(child, options.signals)?;
Ok(BoxRunResult {
exit_code,
signal,
environment_report: resolved.report,
})
}
fn wait_for(
mut child: Box<dyn RunningBox>,
signals: Option<&SignalReceiver>,
) -> Result<(Option<i32>, Option<String>)> {
loop {
if let Some(result) = child.try_wait().map_err(Error::from)? {
return Ok(result);
}
let Some(receiver) = signals else {
std::thread::sleep(POLL_INTERVAL);
continue;
};
match receiver.recv_timeout(POLL_INTERVAL) {
Ok(signal) => child.forward(signal),
Err(RecvTimeoutError::Timeout | RecvTimeoutError::Disconnected) => {}
}
}
}
#[cfg(unix)]
fn send_signal(child: &mut Child, signal: ForwardedSignal) {
let Some(pid) = i32::try_from(child.id())
.ok()
.and_then(rustix::process::Pid::from_raw)
else {
return;
};
let native = match signal {
ForwardedSignal::Interrupt => rustix::process::Signal::INT,
ForwardedSignal::Terminate => rustix::process::Signal::TERM,
ForwardedSignal::Hangup => rustix::process::Signal::HUP,
};
let _ = rustix::process::kill_process(pid, native);
}
#[cfg(not(unix))]
fn send_signal(child: &mut Child, _signal: ForwardedSignal) {
let _ = child.kill();
}
#[cfg(unix)]
fn terminating_signal(status: std::process::ExitStatus) -> Option<String> {
use std::os::unix::process::ExitStatusExt as _;
status.signal().map(|number| match number {
2 => "SIGINT".to_string(),
15 => "SIGTERM".to_string(),
1 => "SIGHUP".to_string(),
9 => "SIGKILL".to_string(),
other => format!("SIG{other}"),
})
}
#[cfg(not(unix))]
fn terminating_signal(_status: std::process::ExitStatus) -> Option<String> {
None
}
pub struct RunBoxOptions<'a> {
pub trust: TrustAnchors<'a>,
pub archive: Option<&'a Path>,
pub temporary_root: &'a Path,
pub run: RunOptions<'a>,
}
pub fn run_box(release_document_path: &Path, options: &RunBoxOptions<'_>) -> Result<BoxRunResult> {
std::fs::create_dir_all(options.temporary_root)?;
let destination: PathBuf = options.temporary_root.join(format!(
"scrollcase-run-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|elapsed| elapsed.as_nanos())
.unwrap_or_default()
));
let prepared = verify_and_extract_box(
release_document_path,
&PrepareOptions {
trust: options.trust,
archive: options.archive,
destination: &destination,
environment: options.run.environment.clone(),
},
);
let result = match prepared {
Ok(prepared) => run_extracted_box(&prepared, &options.run),
Err(error) => Err(error),
};
let _ = std::fs::remove_dir_all(&destination);
result
}