quarb_session/daemon.rs
1//! The daemon-backed executor: reuse `qua`'s resident session.
2//!
3//! Rather than reimplement the socket, spawn, keying, TTL, and
4//! security machinery, [`DaemonExecutor`] shells out to
5//! `qua --resident`, which materializes the source once in a
6//! background process keyed by the target set and answers later
7//! queries from the standing arbor. `quai` sends the combined query
8//! text (its macro table prepended) and wraps the rendered lines as
9//! [`Cell`]s.
10//!
11//! The win is a *shared, persistent* hot arbor: it survives `quai`
12//! exiting and can be shared with other clients (a Jupyter kernel).
13//! For a single session over a RAM-sized source the in-process
14//! [`LocalExecutor`](crate::LocalExecutor) is faster (no IPC); the
15//! daemon earns its keep on expensive sources reused across runs.
16
17use crate::{Cell, Executor};
18use anyhow::{Context, Result, bail};
19use std::path::PathBuf;
20use std::process::Command;
21
22pub struct DaemonExecutor {
23 /// The `qua` binary to drive.
24 qua: PathBuf,
25 /// The source paths (the resident session's identity).
26 paths: Vec<PathBuf>,
27 /// A pinned `--now` (reproducible; also keys the session). Omitted
28 /// so the daemon reads the clock per query — which keeps the
29 /// session reusable across `quai` restarts.
30 pinned_now: Option<String>,
31 /// Semantics flags forwarded verbatim (`--allow-shell`, …).
32 flags: Vec<String>,
33}
34
35impl DaemonExecutor {
36 pub fn new(
37 paths: Vec<PathBuf>,
38 pinned_now: Option<String>,
39 allow_shell: bool,
40 hidden: bool,
41 no_ignore: bool,
42 descend: bool,
43 cache: bool,
44 refs: Option<PathBuf>,
45 model: Option<PathBuf>,
46 ) -> Result<Self> {
47 let qua = resolve_qua();
48 let mut flags = Vec::new();
49 if let Some(f) = refs {
50 flags.push("--refs".to_string());
51 flags.push(f.display().to_string());
52 }
53 if let Some(f) = model {
54 flags.push("--model".to_string());
55 flags.push(f.display().to_string());
56 }
57 if allow_shell {
58 flags.push("--allow-shell".to_string());
59 }
60 if hidden {
61 flags.push("--hidden".to_string());
62 }
63 if no_ignore {
64 flags.push("--no-ignore".to_string());
65 }
66 if descend {
67 flags.push("--descend".to_string());
68 }
69 // Cache and daemon are layers, not alternatives: the resident
70 // arbor's cold start reads parsed ASTs from the on-disk cache,
71 // and populates it as it materializes.
72 if cache {
73 flags.push("--cache".to_string());
74 }
75 Ok(Self {
76 qua,
77 paths,
78 pinned_now,
79 flags,
80 })
81 }
82}
83
84/// Prefer a `qua` sitting beside the running binary (installed as a
85/// pair); otherwise rely on `PATH`.
86fn resolve_qua() -> PathBuf {
87 if let Ok(exe) = std::env::current_exe()
88 && let Some(dir) = exe.parent()
89 {
90 for name in ["qua", "qua.exe"] {
91 let sib = dir.join(name);
92 if sib.exists() {
93 return sib;
94 }
95 }
96 }
97 PathBuf::from("qua")
98}
99
100impl DaemonExecutor {
101 /// Invoke `qua`: `resident` hits the standing arbor (`&N`), while
102 /// a one-shot (not resident) re-materializes the source live
103 /// (`&N!`). Results are opaque rendered lines across this boundary
104 /// (typed values are a later protocol upgrade for Jupyter).
105 fn invoke(&self, resident: bool, query: &str) -> Result<Vec<Cell>> {
106 let mut cmd = Command::new(&self.qua);
107 if resident {
108 cmd.arg("--resident");
109 }
110 if let Some(iso) = &self.pinned_now {
111 cmd.arg("--now").arg(iso);
112 }
113 for f in &self.flags {
114 cmd.arg(f);
115 }
116 cmd.arg(query);
117 for p in &self.paths {
118 cmd.arg(p);
119 }
120 let out = cmd.output().with_context(|| {
121 format!(
122 "invoking '{}' (is qua installed and on PATH?)",
123 self.qua.display()
124 )
125 })?;
126 if !out.status.success() {
127 bail!("{}", String::from_utf8_lossy(&out.stderr).trim());
128 }
129 Ok(String::from_utf8_lossy(&out.stdout)
130 .lines()
131 .map(|l| Cell::Node(l.to_string()))
132 .collect())
133 }
134}
135
136impl Executor for DaemonExecutor {
137 fn run(&self, query: &str) -> Result<Vec<Cell>> {
138 self.invoke(true, query)
139 }
140
141 fn run_fresh(&self, query: &str) -> Result<Vec<Cell>> {
142 // A one-shot qua re-reads the source, bypassing the (fixed)
143 // resident arbor — a genuinely live reading.
144 self.invoke(false, query)
145 }
146}