1use anyhow::{Context, Result};
12use std::io::Write;
13use std::process::Command;
14use std::time::Duration;
15
16#[derive(Debug)]
18pub struct SubprocessResult {
19 pub valid: bool,
21 pub exit_code: Option<i32>,
23 pub stderr: String,
25 pub timed_out: bool,
27}
28
29pub fn subprocess_verify(bundle_data: &[u8], timeout: Duration) -> Result<SubprocessResult> {
34 let tmp = tempfile::Builder::new()
35 .prefix("assay-sim-")
36 .suffix(".tar.gz")
37 .tempfile()
38 .context("creating temp file for subprocess verify")?;
39
40 tmp.as_file()
41 .write_all(bundle_data)
42 .context("writing bundle to temp file")?;
43
44 let assay_bin = find_assay_binary()?;
45
46 let mut child = Command::new(&assay_bin)
47 .args(["evidence", "verify", &tmp.path().to_string_lossy()])
48 .stdout(std::process::Stdio::null())
49 .stderr(std::process::Stdio::piped())
50 .spawn()
51 .with_context(|| format!("spawning assay binary: {}", assay_bin.display()))?;
52
53 let result = match child.wait_timeout(timeout) {
55 Ok(Some(status)) => {
56 let stderr = read_stderr(&mut child);
57 SubprocessResult {
58 valid: status.success(),
59 exit_code: status.code(),
60 stderr,
61 timed_out: false,
62 }
63 }
64 Ok(None) => {
65 let _ = child.kill();
67 let _ = child.wait(); SubprocessResult {
69 valid: false,
70 exit_code: None,
71 stderr: "subprocess timed out".into(),
72 timed_out: true,
73 }
74 }
75 Err(e) => {
76 let _ = child.kill();
77 let _ = child.wait();
78 return Err(e).context("waiting for subprocess");
79 }
80 };
81
82 Ok(result)
83}
84
85fn find_assay_binary() -> Result<std::path::PathBuf> {
87 if let Ok(bin) = std::env::var("ASSAY_BIN") {
89 let path = std::path::PathBuf::from(bin);
90 if path.exists() {
91 return Ok(path);
92 }
93 }
94
95 if let Ok(exe) = std::env::current_exe() {
97 if let Some(dir) = exe.parent() {
98 let sibling = dir.join("assay");
99 if sibling.exists() {
100 return Ok(sibling);
101 }
102 }
103 }
104
105 Ok(std::path::PathBuf::from("assay"))
107}
108
109fn read_stderr(child: &mut std::process::Child) -> String {
110 use std::io::Read;
111 let mut buf = String::new();
112 if let Some(ref mut stderr) = child.stderr {
113 let _ = stderr.read_to_string(&mut buf);
114 }
115 buf.truncate(4096);
117 buf
118}
119
120trait ChildExt {
122 fn wait_timeout(
123 &mut self,
124 timeout: Duration,
125 ) -> std::io::Result<Option<std::process::ExitStatus>>;
126}
127
128impl ChildExt for std::process::Child {
129 fn wait_timeout(
130 &mut self,
131 timeout: Duration,
132 ) -> std::io::Result<Option<std::process::ExitStatus>> {
133 let start = std::time::Instant::now();
134 let poll_interval = Duration::from_millis(50);
135
136 loop {
137 match self.try_wait()? {
138 Some(status) => return Ok(Some(status)),
139 None => {
140 if start.elapsed() >= timeout {
141 return Ok(None);
142 }
143 std::thread::sleep(poll_interval);
144 }
145 }
146 }
147 }
148}