choir_ci_local/choir-ci-local.rs
1//! The reference CI executor helper: runs a batch locally, over a pipe.
2//!
3//! Reads the D18 executor protocol on stdin, runs the batch with
4//! [`choir_queue::local::LocalRunner`], and writes verdicts to stdout.
5//! Two jobs at once, both real:
6//!
7//! It is the second party the protocol needs in order to be tested at
8//! all -- a wire format with one implementation is a data structure with
9//! extra steps. And it is the worked example for anyone writing a
10//! driver for a real isolation boundary: a Firecracker or Cloud
11//! Hypervisor helper differs from this file only in what it hands the
12//! batch to, and this repository cannot build one because both require
13//! KVM.
14//!
15//! Interaction failures are data, not a nonzero exit, the same rule
16//! `choir-differential` follows: a helper that dies loudly tells the
17//! host nothing it can attribute to a job.
18
19use choir_queue::executor::{CiExecutor, Verdict, PROTOCOL};
20use choir_queue::local::LocalRunner;
21use choir_queue::remote::{job_from_json, verdict_to_json};
22use std::io::{BufRead, Write};
23
24fn main() {
25 let stdin = std::io::BufReader::new(std::io::stdin());
26 let mut lines = stdin.lines();
27 let stdout = std::io::stdout();
28 let mut out = stdout.lock();
29
30 // The handshake, before anything is read that could fail: a host
31 // whose protocol we cannot speak must learn that from the version
32 // rather than from a batch that half worked.
33 let Some(Ok(hello)) = lines.next() else {
34 return;
35 };
36 let hello: serde_json::Value = match serde_json::from_str(&hello) {
37 Ok(v) => v,
38 Err(_) => return,
39 };
40 let _ = writeln!(
41 out,
42 "{}",
43 serde_json::json!({ "name": "choir-ci-local", "protocol": PROTOCOL })
44 );
45 let _ = out.flush();
46 if hello["protocol"].as_u64() != Some(u64::from(PROTOCOL)) {
47 return;
48 }
49 let count = hello["jobs"].as_u64().unwrap_or(0) as usize;
50
51 // Collect the whole batch before running any of it. Streaming one
52 // job at a time would answer sooner and cost the concurrency the
53 // batch call exists for, which is the defect this seam was carved
54 // out of `bool` to prevent.
55 let mut jobs = Vec::with_capacity(count);
56 let mut refusal = None;
57 for _ in 0..count {
58 let Some(Ok(line)) = lines.next() else {
59 refusal = Some("host stopped sending jobs".to_string());
60 break;
61 };
62 match serde_json::from_str(&line)
63 .map_err(|e| e.to_string())
64 .and_then(|v: serde_json::Value| job_from_json(&v))
65 {
66 Ok(job) => jobs.push(job),
67 Err(why) => {
68 refusal = Some(why);
69 break;
70 }
71 }
72 }
73
74 // A job we could not read is a job we did not run, and saying so
75 // for every remaining slot keeps the host's indices aligned.
76 let verdicts = if let Some(why) = refusal {
77 let mut answered = LocalRunner::new().run(&jobs).unwrap_or_default();
78 while answered.len() < count {
79 answered.push(Verdict::Errored {
80 provider: "choir-ci-local".into(),
81 detail: why.clone(),
82 });
83 }
84 answered
85 } else {
86 match LocalRunner::new().run(&jobs) {
87 Ok(v) => v,
88 Err(e) => (0..count)
89 .map(|_| Verdict::Errored {
90 provider: "choir-ci-local".into(),
91 detail: e.to_string(),
92 })
93 .collect(),
94 }
95 };
96
97 for verdict in &verdicts {
98 let _ = writeln!(out, "{}", verdict_to_json(verdict));
99 }
100 let _ = out.flush();
101}