Skip to main content

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        graft: bool,
43        no_graft: bool,
44        cache: bool,
45        refs: Option<PathBuf>,
46        model: Option<PathBuf>,
47    ) -> Result<Self> {
48        let qua = resolve_qua();
49        let mut flags = Vec::new();
50        if let Some(f) = refs {
51            flags.push("--refs".to_string());
52            flags.push(f.display().to_string());
53        }
54        if let Some(f) = model {
55            flags.push("--model".to_string());
56            flags.push(f.display().to_string());
57        }
58        if allow_shell {
59            flags.push("--allow-shell".to_string());
60        }
61        if hidden {
62            flags.push("--hidden".to_string());
63        }
64        if no_ignore {
65            flags.push("--no-ignore".to_string());
66        }
67        if graft {
68            flags.push("--graft".to_string());
69        }
70        if no_graft {
71            flags.push("--no-graft".to_string());
72        }
73        // Cache and daemon are layers, not alternatives: the resident
74        // arbor's cold start reads parsed ASTs from the on-disk cache,
75        // and populates it as it materializes.
76        if cache {
77            flags.push("--cache".to_string());
78        }
79        Ok(Self {
80            qua,
81            paths,
82            pinned_now,
83            flags,
84        })
85    }
86}
87
88/// Prefer a `qua` sitting beside the running binary (installed as a
89/// pair); otherwise rely on `PATH`.
90fn resolve_qua() -> PathBuf {
91    if let Ok(exe) = std::env::current_exe()
92        && let Some(dir) = exe.parent()
93    {
94        for name in ["qua", "qua.exe"] {
95            let sib = dir.join(name);
96            if sib.exists() {
97                return sib;
98            }
99        }
100    }
101    PathBuf::from("qua")
102}
103
104impl DaemonExecutor {
105    /// Invoke `qua`: `resident` hits the standing arbor (`&N`), while
106    /// a one-shot (not resident) re-materializes the source live
107    /// (`&N!`). Results are opaque rendered lines across this boundary
108    /// (typed values are a later protocol upgrade for Jupyter).
109    fn invoke(&self, resident: bool, query: &str) -> Result<Vec<Cell>> {
110        let mut cmd = Command::new(&self.qua);
111        if resident {
112            cmd.arg("--resident");
113        }
114        if let Some(iso) = &self.pinned_now {
115            cmd.arg("--now").arg(iso);
116        }
117        for f in &self.flags {
118            cmd.arg(f);
119        }
120        cmd.arg(query);
121        for p in &self.paths {
122            cmd.arg(p);
123        }
124        let out = cmd.output().with_context(|| {
125            format!(
126                "invoking '{}' (is qua installed and on PATH?)",
127                self.qua.display()
128            )
129        })?;
130        if !out.status.success() {
131            bail!("{}", String::from_utf8_lossy(&out.stderr).trim());
132        }
133        Ok(String::from_utf8_lossy(&out.stdout)
134            .lines()
135            .map(|l| Cell::Node(l.to_string()))
136            .collect())
137    }
138}
139
140impl Executor for DaemonExecutor {
141    fn run(&self, query: &str) -> Result<Vec<Cell>> {
142        self.invoke(true, query)
143    }
144
145    fn run_fresh(&self, query: &str) -> Result<Vec<Cell>> {
146        // A one-shot qua re-reads the source, bypassing the (fixed)
147        // resident arbor — a genuinely live reading.
148        self.invoke(false, query)
149    }
150}