superfluid_daemon/
compat.rs1use std::process::Command;
4
5use serde_json::{json, Map, Value};
6
7use crate::runtimes::Catalog;
8
9pub fn json(catalog: &Catalog) -> Value {
11 let mut runtimes: Map<String, Value> = Map::new();
12 for rt in crate::linked::runtimes() {
13 runtimes.insert(rt.info().id.into(), rt.compat());
14 }
15 for id in catalog.shipped_ids() {
16 if runtimes.contains_key(id.name()) {
17 continue;
18 }
19 let Some(worker) = catalog.shipped(id) else { continue };
20 let mut cmd = Command::new(&worker);
21 crate::exec::without_secrets(&mut cmd).args(["compat", "--json"]);
22 let answer = match crate::exec::output_within(&mut cmd, crate::exec::ANSWER_WITHIN) {
23 Ok(None) => json!({"error": format!("{} compat did not answer within {}s", worker.display(), crate::exec::ANSWER_WITHIN.as_secs())}),
24 Ok(Some(out)) if out.status.success() => serde_json::from_slice::<Value>(&out.stdout)
25 .unwrap_or_else(|e| json!({"error": format!("{}: compat is not JSON: {e}", worker.display())})),
26 Ok(Some(out)) => json!({"error": format!("{} compat exited {}{}", worker.display(), out.status, crate::exec::stderr_tail(&out.stderr))}),
27 Err(e) => json!({"error": format!("{}: {e}", worker.display())}),
28 };
29 runtimes.insert(id.name().into(), answer);
30 }
31 json!({"superfluid": env!("CARGO_PKG_VERSION"), "runtimes": runtimes})
32}
33
34pub fn unanswered(c: &Value) -> Vec<&str> {
37 c["runtimes"].as_object().map(|r| r.iter().filter(|(_, v)| v["error"].is_string()).map(|(id, _)| id.as_str()).collect()).unwrap_or_default()
38}
39
40pub fn text(c: &Value) -> String {
42 let mut out = format!("{}\n", crate::cli::version());
43 let Some(runtimes) = c["runtimes"].as_object() else { return out };
44 for (id, r) in runtimes {
45 if let Some(e) = r["error"].as_str() {
46 out.push_str(&format!("{id}: {e}\n"));
47 continue;
48 }
49 let line = match id.as_str() {
50 "basert" => {
51 let abi = r["abi"].as_u64().map(|n| format!("engine ABI {n}")).unwrap_or_else(|| "no engine ABI number (a header before 0.4.0)".into());
52 let unnumbered = match r["unnumbered_minor"].as_str() {
53 Some(minor) => format!("one that states no number serves if it is baseRT {minor}.x"),
54 None => "one that states no number does not serve".to_string(),
55 };
56 let pinned: Vec<String> = r["pinned_engines"]
57 .as_array()
58 .map(|a| a.iter().filter_map(|p| p["version"].as_str().map(str::to_string)).collect())
59 .unwrap_or_default();
60 format!(
61 "built against baseRT {} headers, {abi}. A libbaseRT with the same ABI number serves, whatever its version; {unnumbered}. \
62 Installs on its own: {}",
63 r["basert_version"].as_str().unwrap_or("?"),
64 if pinned.is_empty() { "nothing".to_string() } else { pinned.join(", ") }
65 )
66 }
67 _ => {
68 let tested: Vec<String> =
69 r["tested"].as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default();
70 let python = r["python"].as_str().map(|p| format!(" on Python {p}")).unwrap_or_default();
71 format!("installs on its own{python}: {}", if tested.is_empty() { "nothing".to_string() } else { tested.join(", ") })
72 }
73 };
74 out.push_str(&format!("{id}: {line}\n"));
75 }
76 out
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use std::os::unix::fs::PermissionsExt;
83 use std::path::PathBuf;
84
85 fn scratch(name: &str) -> PathBuf {
86 let dir = std::env::temp_dir().join(format!("superfluid-compat-{name}-{}", std::process::id()));
87 let _ = std::fs::remove_dir_all(&dir);
88 std::fs::create_dir_all(&dir).unwrap();
89 dir
90 }
91
92 #[test]
93 fn every_shipped_adapter_answers_for_its_runtime() {
94 let root = scratch("shipped");
95 let beside = root.join("bin");
96 let libexec = root.join("libexec").join("superfluid");
97 std::fs::create_dir_all(&beside).unwrap();
98 std::fs::create_dir_all(&libexec).unwrap();
99 let toy = libexec.join("superfluid-worker-toyrt");
100 std::fs::write(&toy, "#!/bin/sh\n[ \"$1\" = compat ] || exit 2\necho \"{\\\"tested\\\":[\\\"t2\\\",\\\"t1\\\"],\\\"saw\\\":\\\"${SUPERFLUID_COMPAT_SENTINEL_TOKEN:-}\\\"}\"\n").unwrap();
102 std::fs::set_permissions(&toy, std::fs::Permissions::from_mode(0o755)).unwrap();
103 let broken = libexec.join("superfluid-worker-oldrt");
104 std::fs::write(&broken, "#!/bin/sh\nexit 2\n").unwrap();
105 std::fs::set_permissions(&broken, std::fs::Permissions::from_mode(0o755)).unwrap();
106 let fails = libexec.join("superfluid-worker-failrt");
107 std::fs::write(&fails, "#!/bin/sh\necho 'superfluid-worker-failrt: no recipe.json beside the adapter' >&2\nexit 1\n").unwrap();
108 std::fs::set_permissions(&fails, std::fs::Permissions::from_mode(0o755)).unwrap();
109 let cat = Catalog::new(root.join("home"), Some(beside.clone()), beside.join("superfluid-workerd"));
110 std::env::set_var("SUPERFLUID_COMPAT_SENTINEL_TOKEN", "hunter2");
111 let c = json(&cat);
112 std::env::remove_var("SUPERFLUID_COMPAT_SENTINEL_TOKEN");
113 assert_eq!(c["superfluid"], env!("CARGO_PKG_VERSION"));
114 assert_eq!(c["runtimes"]["toyrt"]["tested"][0], "t2");
115 assert_eq!(c["runtimes"]["toyrt"]["saw"], "", "a credential-named variable is not handed to the worker");
116 for id in crate::linked::ids() {
117 assert!(c["runtimes"][id.name()].is_object(), "the linked {} adapter answers in process: {c}", id.name());
118 }
119 assert!(c["runtimes"]["oldrt"]["error"].as_str().unwrap().contains("exited"), "{c}");
120 assert_eq!(c["runtimes"]["failrt"]["error"].as_str().unwrap().rsplit(": ").next(), Some("no recipe.json beside the adapter"), "{c}");
121 assert_eq!(unanswered(&c), ["failrt", "oldrt"], "{c}");
122 #[cfg(feature = "basert")]
123 {
124 assert!(c["runtimes"]["basert"]["basert_version"].is_string(), "{c}");
125 assert!(c["runtimes"]["basert"]["pinned_engines"].is_array(), "{c}");
126 }
127 let t = text(&c);
128 assert!(t.contains("toyrt: installs on its own: t2, t1"), "{t}");
129 assert!(t.contains("oldrt: ") && t.contains("exited"), "{t}");
130 assert!(unanswered(&json!({"runtimes": {"toyrt": {"tested": []}}})).is_empty());
131 let _ = std::fs::remove_dir_all(&root);
132 }
133}