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