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::{ENVELOPE_SCHEMA, 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 if text(&envelope, "schema") != Some(ENVELOPE_SCHEMA) {
80 return Err(AcceptanceDefect::Shape);
81 }
82 let payload = member(&envelope, "payload").ok_or(AcceptanceDefect::Shape)?;
83 if text(payload, "schema") != Some(PAYLOAD_SCHEMA) {
84 return Err(AcceptanceDefect::Shape);
85 }
86 let recorded = text(&envelope, "payload_digest").ok_or(AcceptanceDefect::Shape)?;
87 if hj(PAYLOAD_SCHEMA, payload).to_string() != recorded {
88 return Err(AcceptanceDefect::PayloadDigest);
89 }
90 let engine_row = member(payload, "engine").ok_or(AcceptanceDefect::Shape)?;
91 if text(engine_row, "engine_digest") != Some(expectations.engine_digest.as_str()) {
92 return Err(AcceptanceDefect::Engine);
93 }
94 let evaluation = member(payload, "evaluation").ok_or(AcceptanceDefect::Shape)?;
95 let resolved = text(evaluation, "status") != Some("unavailable");
96 if resolved {
97 let base = member(evaluation, "base").ok_or(AcceptanceDefect::Shape)?;
98 if text(base, "commit_oid") != Some(expectations.base_commit.as_str()) {
99 return Err(AcceptanceDefect::BaseIdentity);
100 }
101 let candidate = member(evaluation, "candidate").ok_or(AcceptanceDefect::Shape)?;
102 if let (Some(expected), Some("git-commit")) = (
103 expectations.candidate_commit.as_deref(),
104 text(candidate, "kind"),
105 ) && text(candidate, "commit_oid") != Some(expected)
106 {
107 return Err(AcceptanceDefect::CandidateIdentity);
108 }
109 }
110 let result = member(payload, "result").ok_or(AcceptanceDefect::Shape)?;
111 let exit_code = match member(result, "exit_code") {
112 Some(Value::Integer(code)) => *code,
113 _ => return Err(AcceptanceDefect::Shape),
114 };
115 let complete = member(result, "complete") == Some(&Value::Bool(true));
116 if complete != (exit_code == 0 || exit_code == 1) {
117 return Err(AcceptanceDefect::Completeness);
118 }
119 let count = match member(result, "finding_count") {
120 Some(Value::Integer(count)) => *count,
121 _ => return Err(AcceptanceDefect::Shape),
122 };
123 let findings = match member(payload, "findings") {
124 Some(Value::Array(rows)) => rows.len(),
125 _ => return Err(AcceptanceDefect::Shape),
126 };
127 if i64::try_from(findings).map_err(|_defect| AcceptanceDefect::Shape)? != count {
128 return Err(AcceptanceDefect::FindingCount);
129 }
130 Ok(exit_code)
131}
132
133#[derive(Debug)]
135pub enum Supervised {
136 Completed(ExitStatus),
138 Killed,
141}
142
143pub fn supervise(child: &mut Child, ceiling: Duration) -> std::io::Result<Supervised> {
153 let start = Instant::now();
154 loop {
155 if let Some(status) = child.try_wait()? {
156 return Ok(Supervised::Completed(status));
157 }
158 if start.elapsed() >= ceiling {
159 let _signalled = child.kill();
160 let _reaped = child.wait();
161 return Ok(Supervised::Killed);
162 }
163 std::thread::sleep(Duration::from_millis(25));
164 }
165}
166
167#[derive(Clone, Copy, Debug, PartialEq, Eq)]
171pub enum Defect {
172 Killed,
174 Signalled,
176 Oversize,
178 ExitMismatch,
180 Acceptance(AcceptanceDefect),
182}
183
184pub fn settle(
193 outcome: &Supervised,
194 stdout: &[u8],
195 expectations: &Expectations,
196) -> Result<i64, Defect> {
197 let status = match *outcome {
198 Supervised::Killed => return Err(Defect::Killed),
199 Supervised::Completed(status) => status,
200 };
201 if u64::try_from(stdout.len()).unwrap_or(u64::MAX) > amiss_wire::report::MACHINE_JSON_BYTES {
202 return Err(Defect::Oversize);
203 }
204 let code = status.code().ok_or(Defect::Signalled)?;
205 let class = accept(stdout, expectations).map_err(Defect::Acceptance)?;
206 if i64::from(code) != class {
207 return Err(Defect::ExitMismatch);
208 }
209 Ok(class)
210}