Skip to main content

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}