amiss_bootstrap/
supervise.rs1use std::process::{Child, ExitStatus};
2use std::time::{Duration, Instant};
3
4use amiss_wire::digest::hj;
5use amiss_wire::json::{Value, canonical, parse};
6use amiss_wire::report::PAYLOAD_SCHEMA;
7
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum AcceptanceDefect {
12 Shape,
14 Noncanonical,
16 PayloadDigest,
18 Engine,
20 BaseIdentity,
22 CandidateIdentity,
24 Completeness,
26 FindingCount,
28}
29
30#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct Expectations {
36 pub engine_digest: String,
37 pub base_commit: String,
38 pub candidate_commit: Option<String>,
39}
40
41fn member<'value>(value: &'value Value, key: &str) -> Option<&'value Value> {
42 match value {
43 Value::Object(members) => members
44 .iter()
45 .find(|(name, _)| name == key)
46 .map(|(_, member)| member),
47 Value::Null | Value::Bool(_) | Value::Integer(_) | Value::String(_) | Value::Array(_) => {
48 None
49 }
50 }
51}
52
53fn text<'value>(value: &'value Value, key: &str) -> Option<&'value str> {
54 match member(value, key) {
55 Some(Value::String(text)) => Some(text),
56 _ => None,
57 }
58}
59
60pub fn accept(wire: &[u8], expectations: &Expectations) -> Result<i64, AcceptanceDefect> {
72 let trimmed = wire
73 .strip_suffix(b"\n")
74 .ok_or(AcceptanceDefect::Noncanonical)?;
75 let envelope = parse(trimmed).map_err(|_defect| AcceptanceDefect::Shape)?;
76 if canonical(&envelope) != trimmed {
77 return Err(AcceptanceDefect::Noncanonical);
78 }
79 let payload = member(&envelope, "payload").ok_or(AcceptanceDefect::Shape)?;
80 let recorded = text(&envelope, "payload_digest").ok_or(AcceptanceDefect::Shape)?;
81 if hj(PAYLOAD_SCHEMA, payload).to_string() != recorded {
82 return Err(AcceptanceDefect::PayloadDigest);
83 }
84 let engine_row = member(payload, "engine").ok_or(AcceptanceDefect::Shape)?;
85 if text(engine_row, "engine_digest") != Some(expectations.engine_digest.as_str()) {
86 return Err(AcceptanceDefect::Engine);
87 }
88 let evaluation = member(payload, "evaluation").ok_or(AcceptanceDefect::Shape)?;
89 let resolved = text(evaluation, "status") != Some("unavailable");
90 if resolved {
91 let base = member(evaluation, "base").ok_or(AcceptanceDefect::Shape)?;
92 if text(base, "commit_oid") != Some(expectations.base_commit.as_str()) {
93 return Err(AcceptanceDefect::BaseIdentity);
94 }
95 let candidate = member(evaluation, "candidate").ok_or(AcceptanceDefect::Shape)?;
96 if let (Some(expected), Some("git-commit")) = (
97 expectations.candidate_commit.as_deref(),
98 text(candidate, "kind"),
99 ) && text(candidate, "commit_oid") != Some(expected)
100 {
101 return Err(AcceptanceDefect::CandidateIdentity);
102 }
103 }
104 let result = member(payload, "result").ok_or(AcceptanceDefect::Shape)?;
105 let exit_code = match member(result, "exit_code") {
106 Some(Value::Integer(code)) => *code,
107 _ => return Err(AcceptanceDefect::Shape),
108 };
109 let complete = member(result, "complete") == Some(&Value::Bool(true));
110 if complete != (exit_code == 0 || exit_code == 1) {
111 return Err(AcceptanceDefect::Completeness);
112 }
113 let count = match member(result, "finding_count") {
114 Some(Value::Integer(count)) => *count,
115 _ => return Err(AcceptanceDefect::Shape),
116 };
117 let findings = match member(payload, "findings") {
118 Some(Value::Array(rows)) => rows.len(),
119 _ => return Err(AcceptanceDefect::Shape),
120 };
121 if i64::try_from(findings).map_err(|_defect| AcceptanceDefect::Shape)? != count {
122 return Err(AcceptanceDefect::FindingCount);
123 }
124 Ok(exit_code)
125}
126
127#[derive(Debug)]
129pub enum Supervised {
130 Completed(ExitStatus),
132 Killed,
135}
136
137pub fn supervise(child: &mut Child, ceiling: Duration) -> std::io::Result<Supervised> {
147 let start = Instant::now();
148 loop {
149 if let Some(status) = child.try_wait()? {
150 return Ok(Supervised::Completed(status));
151 }
152 if start.elapsed() >= ceiling {
153 let _signalled = child.kill();
154 let _reaped = child.wait();
155 return Ok(Supervised::Killed);
156 }
157 std::thread::sleep(Duration::from_millis(25));
158 }
159}
160
161#[derive(Clone, Copy, Debug, PartialEq, Eq)]
165pub enum Defect {
166 Killed,
168 Signalled,
170 Oversize,
172 ExitMismatch,
174 Acceptance(AcceptanceDefect),
176}
177
178pub fn settle(
187 outcome: &Supervised,
188 stdout: &[u8],
189 expectations: &Expectations,
190) -> Result<i64, Defect> {
191 let status = match *outcome {
192 Supervised::Killed => return Err(Defect::Killed),
193 Supervised::Completed(status) => status,
194 };
195 if u64::try_from(stdout.len()).unwrap_or(u64::MAX) > amiss_wire::report::MACHINE_JSON_BYTES {
196 return Err(Defect::Oversize);
197 }
198 let code = status.code().ok_or(Defect::Signalled)?;
199 let class = accept(stdout, expectations).map_err(Defect::Acceptance)?;
200 if i64::from(code) != class {
201 return Err(Defect::ExitMismatch);
202 }
203 Ok(class)
204}