1use std::path::{Path, PathBuf};
16use std::process::{Child, Command, Stdio};
17use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender};
18use std::time::Duration;
19
20use crate::environment::{
21 resolve_environment, EnvironmentLayer, EnvironmentReport, EnvironmentSource, ResolveOptions,
22};
23use crate::error::{fail, Error, Result};
24use crate::execution::assert_execution_files;
25use crate::filesystem::collect_files;
26use crate::path::{join_relative, safe_relative_path};
27use crate::prepare::{
28 verify_and_extract_box, verify_required_assets, EnvironmentReportOptions, PrepareOptions,
29 PreparedBox,
30};
31use crate::release::Execution;
32use crate::trust::TrustAnchors;
33
34pub struct BoxInvocation<'a> {
40 pub program: &'a Path,
42 pub args: &'a [String],
44 pub cwd: &'a Path,
46 pub environment: &'a std::collections::BTreeMap<String, String>,
48 pub stdin: StdioMode,
50 pub stdout: StdioMode,
52 pub stderr: StdioMode,
54}
55
56pub trait SpawnBox {
58 fn spawn(&self, invocation: &BoxInvocation<'_>) -> std::io::Result<Box<dyn RunningBox>>;
64}
65
66pub trait RunningBox {
68 fn try_wait(&mut self) -> std::io::Result<Option<(Option<i32>, Option<String>)>>;
74
75 fn forward(&mut self, signal: ForwardedSignal);
77}
78
79pub struct ProcessSpawner;
82
83impl SpawnBox for ProcessSpawner {
84 fn spawn(&self, invocation: &BoxInvocation<'_>) -> std::io::Result<Box<dyn RunningBox>> {
85 let mut command = Command::new(invocation.program);
86 command
87 .args(invocation.args)
88 .current_dir(invocation.cwd)
89 .env_clear()
90 .envs(invocation.environment)
91 .stdin(invocation.stdin.to_stdio())
92 .stdout(invocation.stdout.to_stdio())
93 .stderr(invocation.stderr.to_stdio());
94 Ok(Box::new(ChildProcess(command.spawn()?)))
95 }
96}
97
98struct ChildProcess(Child);
99
100impl RunningBox for ChildProcess {
101 fn try_wait(&mut self) -> std::io::Result<Option<(Option<i32>, Option<String>)>> {
102 Ok(self
103 .0
104 .try_wait()?
105 .map(|status| (status.code(), terminating_signal(status))))
106 }
107
108 fn forward(&mut self, signal: ForwardedSignal) {
109 send_signal(&mut self.0, signal);
110 }
111}
112
113const POLL_INTERVAL: Duration = Duration::from_millis(50);
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
118pub enum ForwardedSignal {
119 Interrupt,
121 Terminate,
123 Hangup,
125}
126
127impl ForwardedSignal {
128 #[must_use]
130 pub fn as_str(self) -> &'static str {
131 match self {
132 Self::Interrupt => "SIGINT",
133 Self::Terminate => "SIGTERM",
134 Self::Hangup => "SIGHUP",
135 }
136 }
137}
138
139pub type SignalSender = Sender<ForwardedSignal>;
141
142pub type SignalReceiver = Receiver<ForwardedSignal>;
144
145#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
147pub enum StdioMode {
148 #[default]
150 Inherit,
151 Null,
153 Piped,
155}
156
157impl StdioMode {
158 fn to_stdio(self) -> Stdio {
159 match self {
160 Self::Inherit => Stdio::inherit(),
161 Self::Null => Stdio::null(),
162 Self::Piped => Stdio::piped(),
163 }
164 }
165}
166
167#[derive(Default)]
169pub struct RunOptions<'a> {
170 pub args: Vec<String>,
172 pub env: Vec<(String, String)>,
174 pub stdin: StdioMode,
176 pub stdout: StdioMode,
178 pub stderr: StdioMode,
180 pub signals: Option<&'a SignalReceiver>,
182 pub on_environment_report: Option<&'a dyn Fn(&EnvironmentReport)>,
184 pub environment: EnvironmentReportOptions,
186 pub host_environment: Option<Vec<(String, String)>>,
189 pub spawn: Option<&'a dyn SpawnBox>,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
195pub struct BoxRunResult {
196 pub exit_code: Option<i32>,
198 pub signal: Option<String>,
200 pub environment_report: EnvironmentReport,
202}
203
204fn resolve_run_environment(
206 prepared: &PreparedBox,
207 options: &RunOptions<'_>,
208) -> Result<crate::environment::ResolvedEnvironment> {
209 let release = prepared.release();
210 let adapter = prepared.adapter();
211 let host: Vec<(String, String)> = options
214 .host_environment
215 .clone()
216 .unwrap_or_else(|| std::env::vars().collect());
217 let declared = release.environment.clone().unwrap_or_default();
218 resolve_environment(&ResolveOptions {
219 platform: adapter.platform,
220 layers: vec![
221 EnvironmentLayer {
222 source: EnvironmentSource::Host,
223 values: host
224 .iter()
225 .map(|(name, value)| (name.as_str(), value.as_str()))
226 .collect(),
227 },
228 EnvironmentLayer {
229 source: EnvironmentSource::Caller,
230 values: options
231 .env
232 .iter()
233 .map(|(name, value)| (name.as_str(), value.as_str()))
234 .collect(),
235 },
236 EnvironmentLayer {
237 source: EnvironmentSource::Release,
238 values: declared
239 .iter()
240 .map(|(name, value)| (name.as_str(), value.as_str()))
241 .collect(),
242 },
243 ],
244 execution_affecting_variables: adapter.execution_affecting_environment_variables,
245 expanded: options.environment.env_report || options.environment.env_report_values,
246 reveal_host_values: options.environment.env_report_values,
247 })
248}
249
250pub fn run_extracted_box(prepared: &PreparedBox, options: &RunOptions<'_>) -> Result<BoxRunResult> {
258 let release = prepared.release();
259 let Some(execution) = release.execution.as_ref() else {
260 fail!("Box does not declare an execution entry point.");
261 };
262 let adapter = prepared.adapter();
263 if crate::contract::targets::assert_native_host(adapter).is_err() {
264 fail!(
265 "Box target {} cannot run on {}/{}; it requires {}/{}.",
266 prepared.target_id(),
267 std::env::consts::OS,
268 std::env::consts::ARCH,
269 adapter.host_os,
270 adapter.host_arch
271 );
272 }
273
274 prepared.assert_root_unchanged()?;
277
278 let root = prepared.root();
279 let files = collect_files(root)?;
280 if !files.contains(&release.python_entry_point) {
281 fail!("Prepared box is missing {}.", release.python_entry_point);
282 }
283 assert_execution_files(
284 Some(execution),
285 adapter,
286 &release.provenance.python_version,
287 &files,
288 )?;
289 verify_required_assets(root, prepared.required_assets())?;
290
291 let python = join_relative(root, &safe_relative_path(&release.python_entry_point)?);
292 let mut arguments: Vec<String> = match execution {
293 Execution::PythonScript { script, .. } => vec![join_relative(root, &safe_relative_path(script)?)
294 .to_string_lossy()
295 .into_owned()],
296 Execution::PythonModule { module, .. } => vec!["-m".to_string(), module.clone()],
297 };
298 match execution {
299 Execution::PythonScript { default_args, .. }
300 | Execution::PythonModule { default_args, .. } => {
301 arguments.extend(default_args.iter().cloned());
302 }
303 }
304 arguments.extend(options.args.iter().cloned());
305
306 let resolved = resolve_run_environment(prepared, options)?;
307 if let Some(report) = options.on_environment_report {
308 report(&resolved.report);
309 }
310
311 let invocation = BoxInvocation {
312 program: &python,
313 args: &arguments,
314 cwd: root,
315 environment: &resolved.environment,
316 stdin: options.stdin,
317 stdout: options.stdout,
318 stderr: options.stderr,
319 };
320 let spawner: &dyn SpawnBox = options.spawn.unwrap_or(&ProcessSpawner);
321 let child = spawner.spawn(&invocation).map_err(|error| {
322 Error::new(format!(
323 "Box interpreter failed to start: {}: {error}",
324 python.display()
325 ))
326 })?;
327
328 let (exit_code, signal) = wait_for(child, options.signals)?;
329 Ok(BoxRunResult {
330 exit_code,
331 signal,
332 environment_report: resolved.report,
333 })
334}
335
336fn wait_for(
338 mut child: Box<dyn RunningBox>,
339 signals: Option<&SignalReceiver>,
340) -> Result<(Option<i32>, Option<String>)> {
341 loop {
342 if let Some(result) = child.try_wait().map_err(Error::from)? {
343 return Ok(result);
344 }
345 let Some(receiver) = signals else {
346 std::thread::sleep(POLL_INTERVAL);
347 continue;
348 };
349 match receiver.recv_timeout(POLL_INTERVAL) {
350 Ok(signal) => child.forward(signal),
351 Err(RecvTimeoutError::Timeout | RecvTimeoutError::Disconnected) => {}
354 }
355 }
356}
357
358#[cfg(unix)]
359fn send_signal(child: &mut Child, signal: ForwardedSignal) {
360 let Some(pid) = i32::try_from(child.id())
361 .ok()
362 .and_then(rustix::process::Pid::from_raw)
363 else {
364 return;
365 };
366 let native = match signal {
367 ForwardedSignal::Interrupt => rustix::process::Signal::INT,
368 ForwardedSignal::Terminate => rustix::process::Signal::TERM,
369 ForwardedSignal::Hangup => rustix::process::Signal::HUP,
370 };
371 let _ = rustix::process::kill_process(pid, native);
373}
374
375#[cfg(not(unix))]
376fn send_signal(child: &mut Child, _signal: ForwardedSignal) {
377 let _ = child.kill();
380}
381
382#[cfg(unix)]
383fn terminating_signal(status: std::process::ExitStatus) -> Option<String> {
384 use std::os::unix::process::ExitStatusExt as _;
385 status.signal().map(|number| match number {
386 2 => "SIGINT".to_string(),
387 15 => "SIGTERM".to_string(),
388 1 => "SIGHUP".to_string(),
389 9 => "SIGKILL".to_string(),
390 other => format!("SIG{other}"),
391 })
392}
393
394#[cfg(not(unix))]
395fn terminating_signal(_status: std::process::ExitStatus) -> Option<String> {
396 None
397}
398
399pub struct RunBoxOptions<'a> {
401 pub trust: TrustAnchors<'a>,
403 pub archive: Option<&'a Path>,
405 pub temporary_root: &'a Path,
407 pub run: RunOptions<'a>,
409}
410
411pub fn run_box(release_document_path: &Path, options: &RunBoxOptions<'_>) -> Result<BoxRunResult> {
420 std::fs::create_dir_all(options.temporary_root)?;
421 let destination: PathBuf = options.temporary_root.join(format!(
422 "scrollcase-run-{}-{}",
423 std::process::id(),
424 std::time::SystemTime::now()
425 .duration_since(std::time::UNIX_EPOCH)
426 .map(|elapsed| elapsed.as_nanos())
427 .unwrap_or_default()
428 ));
429
430 let prepared = verify_and_extract_box(
431 release_document_path,
432 &PrepareOptions {
433 trust: options.trust,
434 archive: options.archive,
435 destination: &destination,
436 environment: options.run.environment.clone(),
437 },
438 );
439 let result = match prepared {
440 Ok(prepared) => run_extracted_box(&prepared, &options.run),
441 Err(error) => Err(error),
442 };
443 let _ = std::fs::remove_dir_all(&destination);
444 result
445}