Skip to main content

dre_protocol/
host.rs

1//! Core's side of the protocol: spawn a plugin, negotiate a version, send requests.
2//!
3//! Frames are read on a background thread so core can time out a silent plugin during the
4//! handshake and never blocks forever on a plugin that has died. stderr is drained on another
5//! thread, forwarded to a log sink and kept (last lines) for error messages. A plugin's `log` and
6//! `progress` messages (protocol 1) go to the same sink.
7
8use std::collections::VecDeque;
9use std::io::{BufRead, BufReader, BufWriter, Read, Write};
10use std::path::{Path, PathBuf};
11use std::process::{Child, ChildStdin, Command, ExitStatus, Stdio};
12use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
13use std::sync::mpsc::{Receiver, RecvTimeoutError, channel};
14use std::sync::{Arc, Mutex, OnceLock};
15use std::time::Duration;
16
17use arrow::array::RecordBatch;
18use arrow::datatypes::SchemaRef;
19use serde_json::{Map, Value};
20
21use crate::frame::{self, Frame, FrameError};
22use crate::msg::{ConnectionField, DeliveryFile, Envelope, LogLevel, Request, Response, ResultSetMeta};
23use crate::options::OptionField;
24use crate::{
25    CORE_MIN_VERSION, Kind, MAX_VERSION, PluginId, parse_executable_name, parse_package_executable_name,
26};
27
28/// Receives each line a plugin logs: `(plugin, line)`. A `log` message arrives prefixed by its
29/// level (`info: `, `warning: ` for warn and error, `debug: ` for debug and trace), as does
30/// `progress` (`info: `); a stderr line arrives as written.
31pub type LogSink = Arc<dyn Fn(&str, &str) + Send + Sync>;
32
33/// A log sink that prefixes each plugin line with the plugin's file name, on core's stderr.
34pub fn stderr_log() -> LogSink {
35    Arc::new(|plugin, line| eprintln!("[{plugin}] {line}"))
36}
37
38/// Waits for a plugin's stderr to be logged. Lines reach the [`LogSink`] from a background thread,
39/// so a line the plugin wrote before answering a request can be logged after core has the answer.
40/// A host that logs a request's lines differently (hiding them while the request carries a secret)
41/// syncs before it changes how it logs.
42#[derive(Clone)]
43pub struct StderrSync {
44    progress: Arc<StderrProgress>,
45    pipe: RawPipe,
46}
47
48impl StderrSync {
49    /// Wait up to `timeout` until every complete line the plugin has written to stderr so far has
50    /// been logged. False when the plugin kept writing (or the pipe can't be inspected) until then.
51    pub fn wait(&self, timeout: Duration) -> bool {
52        let deadline = std::time::Instant::now() + timeout;
53        loop {
54            let p = &self.progress;
55            if p.done.load(Ordering::SeqCst) {
56                return true;
57            }
58            // Blocked on an empty pipe, and still blocked in the same read after looking: every
59            // line read before that read has been logged, and nothing more is waiting.
60            let reads = p.reads.load(Ordering::SeqCst);
61            if p.reading.load(Ordering::SeqCst)
62                && pending(self.pipe) == Some(0)
63                && p.reading.load(Ordering::SeqCst)
64                && p.reads.load(Ordering::SeqCst) == reads
65            {
66                return true;
67            }
68            if std::time::Instant::now() >= deadline {
69                return false;
70            }
71            std::thread::sleep(Duration::from_millis(1));
72        }
73    }
74}
75
76/// What the stderr forwarder is doing, for [`StderrSync`].
77#[derive(Default)]
78struct StderrProgress {
79    /// In a read of the pipe: every complete line read before it has been logged.
80    reading: AtomicBool,
81    /// Reads that returned.
82    reads: AtomicU64,
83    /// At end of stream: everything has been logged.
84    done: AtomicBool,
85}
86
87/// The pipe, read through [`StderrProgress`] so [`StderrSync`] knows where the forwarder is.
88struct TrackedPipe<R> {
89    pipe: R,
90    progress: Arc<StderrProgress>,
91}
92
93impl<R: Read> Read for TrackedPipe<R> {
94    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
95        self.progress.reading.store(true, Ordering::SeqCst);
96        let n = self.pipe.read(buf);
97        self.progress.reading.store(false, Ordering::SeqCst);
98        self.progress.reads.fetch_add(1, Ordering::SeqCst);
99        n
100    }
101}
102
103#[cfg(unix)]
104type RawPipe = std::os::fd::RawFd;
105#[cfg(windows)]
106type RawPipe = usize;
107#[cfg(not(any(unix, windows)))]
108type RawPipe = ();
109
110#[cfg(unix)]
111fn raw_pipe(pipe: &std::process::ChildStderr) -> RawPipe {
112    std::os::fd::AsRawFd::as_raw_fd(pipe)
113}
114#[cfg(windows)]
115fn raw_pipe(pipe: &std::process::ChildStderr) -> RawPipe {
116    std::os::windows::io::AsRawHandle::as_raw_handle(pipe) as usize
117}
118#[cfg(not(any(unix, windows)))]
119fn raw_pipe(_: &std::process::ChildStderr) -> RawPipe {}
120
121/// Bytes waiting in the pipe, unread.
122#[cfg(unix)]
123fn pending(fd: RawPipe) -> Option<usize> {
124    let mut n: libc::c_int = 0;
125    // SAFETY: FIONREAD writes one c_int; the forwarder keeps the descriptor open until it is done.
126    let r = unsafe { libc::ioctl(fd, libc::FIONREAD, &mut n) };
127    (r == 0).then_some(n.max(0) as usize)
128}
129#[cfg(windows)]
130fn pending(handle: RawPipe) -> Option<usize> {
131    let mut n: u32 = 0;
132    // SAFETY: peeks without reading; the forwarder keeps the handle open until it is done.
133    let ok = unsafe {
134        windows_sys::Win32::System::Pipes::PeekNamedPipe(
135            handle as windows_sys::Win32::Foundation::HANDLE,
136            std::ptr::null_mut(),
137            0,
138            std::ptr::null_mut(),
139            &mut n,
140            std::ptr::null_mut(),
141        )
142    };
143    (ok != 0).then_some(n as usize)
144}
145#[cfg(not(any(unix, windows)))]
146fn pending(_: RawPipe) -> Option<usize> {
147    None
148}
149
150pub const DEFAULT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
151
152/// Set to `1` in a plugin's environment when a person is at core's terminal, else `0`.
153pub const INTERACTIVE_ENV: &str = "DRE_INTERACTIVE";
154const STDERR_TAIL: usize = 20;
155
156#[derive(Debug, Clone, PartialEq)]
157pub struct PluginInfo {
158    pub protocol_version: u32,
159    pub kind: Kind,
160    pub name: String,
161    pub version: String,
162    pub capabilities: Vec<String>,
163    /// Every plugin the executable serves (at least the one it's serving now).
164    pub provides: Vec<PluginId>,
165}
166
167/// A plugin's `describe` reply.
168#[derive(Debug, Clone, PartialEq, Default)]
169pub struct Description {
170    pub connection_fields: Vec<ConnectionField>,
171    pub option_fields: Vec<OptionField>,
172    /// A source's identifier quote character.
173    pub identifier_quote: Option<String>,
174    /// How a source wants string literals spelled (`chr` when absent; `backslash`).
175    pub string_literal: Option<String>,
176    /// A message destination's length limit, in characters.
177    pub message_limit: Option<u64>,
178    /// A source's field naming a file only one process may write at a time.
179    pub single_writer: Option<crate::msg::SingleWriter>,
180}
181
182#[derive(Debug)]
183pub enum HostError {
184    Spawn {
185        plugin: String,
186        error: std::io::Error,
187    },
188    Crashed {
189        plugin: String,
190        status: Option<ExitStatus>,
191        stderr: Vec<String>,
192    },
193    Malformed {
194        plugin: String,
195        message: String,
196    },
197    Incompatible {
198        plugin: String,
199        core: (u32, u32),
200        plugin_range: (u32, u32),
201    },
202    Timeout {
203        plugin: String,
204        waiting_for: &'static str,
205    },
206    Unexpected {
207        plugin: String,
208        expected: &'static str,
209        got: String,
210    },
211    /// The plugin reported an error for a request, with its kind and code when it gave them.
212    Plugin {
213        plugin: String,
214        message: String,
215        kind: Option<String>,
216        code: Option<String>,
217    },
218    Arrow {
219        plugin: String,
220        message: String,
221    },
222}
223
224impl std::fmt::Display for HostError {
225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        match self {
227            HostError::Spawn { plugin, error } => write!(f, "can't start plugin `{plugin}`: {error}"),
228            HostError::Crashed {
229                plugin,
230                status,
231                stderr,
232            } => {
233                match status {
234                    Some(s) => write!(f, "plugin `{plugin}` exited unexpectedly ({s})")?,
235                    None => write!(f, "plugin `{plugin}` closed its output unexpectedly")?,
236                }
237                if !stderr.is_empty() {
238                    write!(f, "; its last log lines:\n  {}", stderr.join("\n  "))?;
239                }
240                Ok(())
241            }
242            HostError::Malformed { plugin, message } => {
243                write!(f, "plugin `{plugin}` broke the protocol: {message}")
244            }
245            HostError::Incompatible {
246                plugin,
247                core,
248                plugin_range,
249            } => {
250                write!(
251                    f,
252                    "plugin `{plugin}` speaks protocol versions {}..={}, but this DRE core speaks {}..={}; ",
253                    plugin_range.0, plugin_range.1, core.0, core.1,
254                )?;
255                if plugin_range.1 >= core.0 {
256                    return f.write_str("update DRE");
257                }
258                let package = parse_package_executable_name(plugin)
259                    .or_else(|| parse_executable_name(plugin).map(|(_, n)| n));
260                match package {
261                    Some(p) => write!(f, "update the plugin with `dre plugin update {p}`"),
262                    None => f.write_str("update the plugin"),
263                }
264            }
265            HostError::Timeout { plugin, waiting_for } => {
266                write!(
267                    f,
268                    "plugin `{plugin}` didn't answer in time (waiting for {waiting_for})"
269                )
270            }
271            HostError::Unexpected {
272                plugin,
273                expected,
274                got,
275            } => {
276                write!(f, "plugin `{plugin}` sent {got} where {expected} was expected")
277            }
278            HostError::Plugin { message, .. } => f.write_str(message),
279            HostError::Arrow { plugin, message } => {
280                write!(f, "plugin `{plugin}` sent invalid Arrow data: {message}")
281            }
282        }
283    }
284}
285
286impl std::error::Error for HostError {}
287
288impl HostError {
289    fn plugin(plugin: &str, message: impl Into<String>) -> HostError {
290        HostError::Plugin {
291            plugin: plugin.to_string(),
292            message: message.into(),
293            kind: None,
294            code: None,
295        }
296    }
297}
298
299pub type Result<T> = std::result::Result<T, HostError>;
300
301/// Asks a running request to stop, from any thread (see [`PluginProcess::canceller`]), and
302/// can kill the plugin.
303#[derive(Clone)]
304pub struct Canceller {
305    stdin: Arc<Mutex<Option<BufWriter<ChildStdin>>>>,
306    current: Arc<AtomicU64>,
307    child: Arc<Mutex<Child>>,
308}
309
310impl Canceller {
311    /// Send `cancel` for the request running now, if any. Returns whether one was sent.
312    pub fn cancel(&self) -> bool {
313        let id = self.current.load(Ordering::SeqCst);
314        if id == 0 {
315            return false;
316        }
317        // A poisoned lock means a writer panicked mid-frame: nothing more can be sent.
318        self.stdin.lock().is_ok_and(|mut stdin| {
319            stdin
320                .as_mut()
321                .is_some_and(|w| frame::write_json(w, &Envelope::new(Some(id), Request::Cancel {})).is_ok())
322        })
323    }
324
325    /// Kill the plugin process.
326    pub fn kill(&self) {
327        if let Ok(mut c) = self.child.lock() {
328            let _ = c.kill();
329        }
330    }
331}
332
333/// A message from the plugin.
334pub enum Incoming {
335    Json(Response),
336    Arrow(Vec<u8>),
337}
338
339impl std::fmt::Debug for Incoming {
340    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
341        match self {
342            Incoming::Json(r) => write!(f, "Json({r:?})"),
343            Incoming::Arrow(b) => write!(f, "Arrow frame ({} bytes)", b.len()),
344        }
345    }
346}
347
348/// The outcome of a source `execute`.
349#[derive(Debug)]
350pub enum Execution {
351    NoResult { rows_affected: Option<u64> },
352    Result { schema: SchemaRef, rows: u64 },
353}
354
355pub struct PluginProcess {
356    label: String,
357    path: PathBuf,
358    /// Shared with [`Canceller`]s, which can kill it.
359    child: Arc<Mutex<Child>>,
360    /// Shared with [`Canceller`]s; `None` once closed.
361    stdin: Arc<Mutex<Option<BufWriter<ChildStdin>>>>,
362    rx: Receiver<std::result::Result<Frame, FrameError>>,
363    log: LogSink,
364    /// The last request id given out.
365    last_id: u64,
366    /// The request running now (0: none).
367    current: Arc<AtomicU64>,
368    stderr: Arc<Mutex<VecDeque<String>>>,
369    stderr_sync: StderrSync,
370    /// Forwards stderr; joined on close/drop so no log line is lost when the plugin exits.
371    stderr_thread: Option<std::thread::JoinHandle<()>>,
372    info: Option<PluginInfo>,
373    /// A reply that arrived while core was still sending (a format failing mid-stream).
374    early: Option<std::result::Result<Frame, FrameError>>,
375    /// How many tries the last delivery took (1 without a retry).
376    last_attempts: u32,
377    /// The plugin asked for in the handshake. By default the one a `dre-<kind>-<name>` file name
378    /// names; none for a package executable, which then serves its first plugin.
379    serve: Option<PluginId>,
380}
381
382impl PluginProcess {
383    /// Spawn the plugin at `path` and complete the handshake.
384    pub fn start(path: &Path, log: LogSink) -> Result<PluginProcess> {
385        Self::start_in(path, log, None)
386    }
387
388    /// Like `start`, running the plugin in `cwd` (core uses the project directory).
389    pub fn start_in(path: &Path, log: LogSink, cwd: Option<&Path>) -> Result<PluginProcess> {
390        Self::start_for(path, None, log, cwd)
391    }
392
393    /// Like `start_in`, asking the executable for `plugin` (one of a package's plugins).
394    pub fn start_for(
395        path: &Path,
396        plugin: Option<&PluginId>,
397        log: LogSink,
398        cwd: Option<&Path>,
399    ) -> Result<PluginProcess> {
400        let timeout = std::env::var("DRE_PLUGIN_HANDSHAKE_TIMEOUT_MS")
401            .ok()
402            .and_then(|v| v.parse().ok())
403            .map(Duration::from_millis)
404            .unwrap_or(DEFAULT_HANDSHAKE_TIMEOUT);
405        let mut p = Self::spawn_in(path, log, &[], cwd)?;
406        if let Some(id) = plugin {
407            p.serve = Some(id.clone());
408        }
409        p.handshake((CORE_MIN_VERSION, MAX_VERSION), timeout)?;
410        Ok(p)
411    }
412
413    /// Spawn and handshake offering an explicit version range (the conformance suite uses this).
414    pub fn start_with(
415        path: &Path,
416        log: LogSink,
417        versions: (u32, u32),
418        timeout: Duration,
419    ) -> Result<PluginProcess> {
420        let mut p = Self::spawn(path, log)?;
421        p.handshake(versions, timeout)?;
422        Ok(p)
423    }
424
425    /// Spawn without a handshake (for tests that probe raw protocol behaviour).
426    pub fn spawn(path: &Path, log: LogSink) -> Result<PluginProcess> {
427        Self::spawn_env(path, log, &[])
428    }
429
430    /// Spawn with extra environment variables, without a handshake.
431    pub fn spawn_env(path: &Path, log: LogSink, env: &[(&str, &str)]) -> Result<PluginProcess> {
432        Self::spawn_in(path, log, env, None)
433    }
434
435    /// Spawn with extra environment variables and a working directory, without a handshake.
436    ///
437    /// # Panics
438    ///
439    /// The thread forwarding the plugin's stderr panics if its log tail is poisoned (a thread
440    /// panicked while changing it).
441    pub fn spawn_in(
442        path: &Path,
443        log: LogSink,
444        env: &[(&str, &str)],
445        cwd: Option<&Path>,
446    ) -> Result<PluginProcess> {
447        let label = path
448            .file_name()
449            .map(|f| f.to_string_lossy().to_string())
450            .unwrap_or_default();
451        let mut cmd = Command::new(path);
452        if let Some(dir) = cwd {
453            cmd.current_dir(dir);
454        }
455        // Plugins can't see the terminal (their stdio is piped), so tell them whether a person is
456        // there, e.g. to finish a browser sign-in. An existing DRE_INTERACTIVE is left alone.
457        if std::env::var_os(INTERACTIVE_ENV).is_none() {
458            use std::io::IsTerminal;
459            let there = std::io::stderr().is_terminal() && std::io::stdin().is_terminal();
460            cmd.env(INTERACTIVE_ENV, if there { "1" } else { "0" });
461        }
462        // Its own process group: Ctrl-C at the terminal reaches core, which asks the plugin to
463        // stop with `cancel`, instead of killing the plugin mid-request.
464        #[cfg(unix)]
465        {
466            use std::os::unix::process::CommandExt;
467            cmd.process_group(0);
468        }
469        // Files on a FUSE mount (a Databricks Volume as the target path) close before `execve`,
470        // or the plugin may never start (see `fuse_fds`).
471        #[cfg(target_os = "linux")]
472        crate::fuse_fds::close_in_child(&mut cmd);
473        #[cfg(windows)]
474        {
475            use std::os::windows::process::CommandExt;
476            const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
477            cmd.creation_flags(CREATE_NEW_PROCESS_GROUP);
478        }
479        let mut child = cmd
480            .envs(env.iter().copied())
481            .stdin(Stdio::piped())
482            .stdout(Stdio::piped())
483            .stderr(Stdio::piped())
484            .spawn()
485            .map_err(|error| HostError::Spawn {
486                plugin: label.clone(),
487                error,
488            })?;
489        let (Some(stdout), Some(stderr_pipe)) = (child.stdout.take(), child.stderr.take()) else {
490            let _ = child.kill();
491            return Err(HostError::Spawn {
492                plugin: label,
493                error: std::io::Error::other("the plugin's stdout or stderr isn't piped"),
494            });
495        };
496        let stdin = Arc::new(Mutex::new(child.stdin.take().map(BufWriter::new)));
497
498        let (tx, rx) = channel();
499        std::thread::spawn(move || {
500            let mut r = BufReader::new(stdout);
501            loop {
502                let f = frame::read_frame(&mut r);
503                let stop = f.is_err();
504                if tx.send(f).is_err() || stop {
505                    break;
506                }
507            }
508        });
509        let stderr = Arc::new(Mutex::new(VecDeque::new()));
510        let tail = stderr.clone();
511        let who = label.clone();
512        let sink = log.clone();
513        let stderr_sync = StderrSync {
514            progress: Arc::default(),
515            pipe: raw_pipe(&stderr_pipe),
516        };
517        let progress = stderr_sync.progress.clone();
518        let stderr_thread = std::thread::spawn(move || {
519            let mut reader = BufReader::new(TrackedPipe {
520                pipe: stderr_pipe,
521                progress: progress.clone(),
522            });
523            for line in reader.by_ref().lines() {
524                let Ok(line) = line else { break };
525                sink(&who, &line);
526                let mut t = tail.lock().unwrap();
527                if t.len() == STDERR_TAIL {
528                    t.pop_front();
529                }
530                t.push_back(line);
531            }
532            // Before the pipe closes, so a sync never inspects a closed descriptor.
533            progress.done.store(true, Ordering::SeqCst);
534            drop(reader);
535        });
536        let serve = parse_executable_name(&label).map(|(k, n)| PluginId::new(k, n));
537        let child = Arc::new(Mutex::new(child));
538        let current = Arc::new(AtomicU64::new(0));
539        Ok(PluginProcess {
540            label,
541            path: path.to_path_buf(),
542            child,
543            stdin,
544            rx,
545            log,
546            last_id: 0,
547            current,
548            stderr,
549            stderr_sync,
550            stderr_thread: Some(stderr_thread),
551            info: None,
552            early: None,
553            last_attempts: 1,
554            serve,
555        })
556    }
557
558    /// Name the plugin to ask for in the handshake (`None`: a package's first).
559    pub fn ask_for(&mut self, plugin: Option<PluginId>) {
560        self.serve = plugin;
561    }
562
563    /// Complete the handshake on a process from `spawn`/`spawn_env`.
564    pub fn handshake(&mut self, (min, max): (u32, u32), timeout: Duration) -> Result<()> {
565        self.send(&Request::Hello {
566            min_version: min,
567            max_version: max,
568            core_version: core_version(),
569            plugin: self.serve.clone(),
570        })?;
571        match self.recv(Some(timeout), "the hello reply")? {
572            Incoming::Json(Response::Hello {
573                protocol_version,
574                kind,
575                name,
576                version,
577                capabilities,
578                mut provides,
579            }) => {
580                if protocol_version < min || protocol_version > max {
581                    return Err(HostError::Incompatible {
582                        plugin: self.label.clone(),
583                        core: (min, max),
584                        plugin_range: (protocol_version, protocol_version),
585                    });
586                }
587                if let Some(want) = &self.serve
588                    && (want.kind != kind || want.name != name)
589                {
590                    return Err(HostError::plugin(
591                        &self.label,
592                        format!(
593                            "`{}` was asked for the {want} plugin but serves {kind}/{name}",
594                            self.label
595                        ),
596                    ));
597                }
598                if provides.is_empty() {
599                    provides.push(PluginId::new(kind, name.clone()));
600                }
601                self.info = Some(PluginInfo {
602                    protocol_version,
603                    kind,
604                    name,
605                    version,
606                    capabilities,
607                    provides,
608                });
609                Ok(())
610            }
611            Incoming::Json(Response::VersionMismatch {
612                min_version,
613                max_version,
614            }) => Err(HostError::Incompatible {
615                plugin: self.label.clone(),
616                core: (min, max),
617                plugin_range: (min_version, max_version),
618            }),
619            Incoming::Json(Response::Error { message, kind, code }) => Err(HostError::Plugin {
620                plugin: self.label.clone(),
621                message,
622                kind,
623                code,
624            }),
625            other => Err(self.unexpected("a hello reply", &other)),
626        }
627    }
628
629    pub fn label(&self) -> &str {
630        &self.label
631    }
632
633    pub fn path(&self) -> &Path {
634        &self.path
635    }
636
637    /// What the plugin said in its handshake.
638    ///
639    /// # Panics
640    ///
641    /// Panics if the process was spawned without a handshake ([`Self::spawn_env`],
642    /// [`Self::spawn_in`]).
643    pub fn info(&self) -> &PluginInfo {
644        self.info.as_ref().expect("handshake completed")
645    }
646
647    /// The process's exit status if it has exited (the lock is held only for the call).
648    fn try_wait(&self) -> std::io::Result<Option<ExitStatus>> {
649        self.child.lock().unwrap().try_wait()
650    }
651
652    /// A handle that can cancel the running request from another thread. Only a plugin on
653    /// protocol 1 or later understands `cancel`.
654    pub fn canceller(&self) -> Canceller {
655        Canceller {
656            stdin: self.stdin.clone(),
657            current: self.current.clone(),
658            child: self.child.clone(),
659        }
660    }
661
662    /// A handle that waits for the plugin's stderr to be logged; see [`StderrSync`].
663    pub fn stderr_sync(&self) -> StderrSync {
664        self.stderr_sync.clone()
665    }
666
667    /// The protocol version settled on in the handshake (0 before it).
668    fn version(&self) -> u32 {
669        self.info.as_ref().map_or(0, |i| i.protocol_version)
670    }
671
672    pub fn has(&self, capability: &str) -> bool {
673        self.info
674            .as_ref()
675            .is_some_and(|i| i.capabilities.iter().any(|c| c == capability))
676    }
677
678    fn unexpected(&self, expected: &'static str, got: &Incoming) -> HostError {
679        let got = match got {
680            Incoming::Json(r) => format!("{:?}", r)
681                .split([' ', '{'])
682                .next()
683                .unwrap_or("?")
684                .to_string(),
685            Incoming::Arrow(_) => "Arrow data".into(),
686        };
687        HostError::Unexpected {
688            plugin: self.label.clone(),
689            expected,
690            got: format!("`{got}`"),
691        }
692    }
693
694    fn crashed(&mut self) -> HostError {
695        // Give the process a moment to finish exiting so its status and last stderr are known.
696        let mut status = None;
697        for _ in 0..50 {
698            if let Ok(Some(s)) = self.try_wait() {
699                status = Some(s);
700                break;
701            }
702            std::thread::sleep(Duration::from_millis(20));
703        }
704        std::thread::sleep(Duration::from_millis(50));
705        let stderr = self.stderr.lock().unwrap().iter().cloned().collect();
706        HostError::Crashed {
707            plugin: self.label.clone(),
708            status,
709            stderr,
710        }
711    }
712
713    /// Send a request. From protocol 1 it carries an id: a new one, or for the messages that
714    /// continue a request (`result_set_end`, `finish`, `cancel`) the running request's.
715    pub fn send(&mut self, req: &Request) -> Result<()> {
716        let id = match req {
717            _ if self.version() == 0 => None,
718            Request::Hello { .. } => None,
719            Request::ResultSetEnd {} | Request::Finish {} | Request::Cancel {} | Request::Fonts { .. } => {
720                Some(self.current.load(Ordering::SeqCst)).filter(|id| *id != 0)
721            }
722            _ => {
723                self.last_id += 1;
724                self.current.store(self.last_id, Ordering::SeqCst);
725                Some(self.last_id)
726            }
727        };
728        // A poisoned lock means a writer panicked mid-frame: report a failed write.
729        let ok = self.stdin.lock().is_ok_and(|mut stdin| {
730            stdin
731                .as_mut()
732                .is_some_and(|w| frame::write_json(w, &Envelope::new(id, req)).is_ok())
733        });
734        if ok { Ok(()) } else { Err(self.write_failed()) }
735    }
736
737    pub fn send_batch(&mut self, batch: &RecordBatch) -> Result<()> {
738        // A plugin that fails part-way replies before reading the rest: stop sending and
739        // surface its error rather than streaming everything into a plugin that gave up.
740        if self.early.is_none()
741            && let Ok(f) = self.rx.try_recv()
742        {
743            self.early = Some(f);
744        }
745        if let Some(Ok(Frame::Json(v))) = &self.early
746            && let Ok(Response::Error { message, kind, code }) = serde_json::from_value::<Response>(v.clone())
747        {
748            return Err(HostError::Plugin {
749                plugin: self.label.clone(),
750                message,
751                kind,
752                code,
753            });
754        }
755        let ipc = frame::encode_batch(batch).map_err(|e| HostError::Arrow {
756            plugin: self.label.clone(),
757            message: e.to_string(),
758        })?;
759        let ok = self.stdin.lock().is_ok_and(|mut stdin| {
760            stdin
761                .as_mut()
762                .is_some_and(|w| frame::write_arrow(w, &ipc).is_ok())
763        });
764        if ok { Ok(()) } else { Err(self.write_failed()) }
765    }
766
767    /// Writing failed: the plugin has gone away. Prefer its own error message if it sent one.
768    fn write_failed(&mut self) -> HostError {
769        match self.rx.recv_timeout(Duration::from_secs(2)) {
770            Ok(Ok(Frame::Json(v))) => match serde_json::from_value::<Response>(v) {
771                Ok(Response::Error { message, kind, code }) => HostError::Plugin {
772                    plugin: self.label.clone(),
773                    message,
774                    kind,
775                    code,
776                },
777                _ => self.crashed(),
778            },
779            _ => self.crashed(),
780        }
781    }
782
783    /// Receive the next message. `timeout` of `None` waits as long as the plugin is alive. `log`
784    /// and `progress` messages go to the log sink on the way.
785    pub fn recv(&mut self, timeout: Option<Duration>, waiting_for: &'static str) -> Result<Incoming> {
786        loop {
787            match self.recv_any(timeout, waiting_for)? {
788                Incoming::Json(Response::Log {
789                    level,
790                    message,
791                    fields,
792                }) => {
793                    (self.log)(&self.label, &log_line(level, &message, &fields));
794                }
795                Incoming::Json(Response::Progress { message, done, total }) => {
796                    (self.log)(&self.label, &progress_line(message.as_deref(), done, total));
797                }
798                other => return Ok(other),
799            }
800        }
801    }
802
803    /// The next frame's JSON as sent, with nothing skipped or checked (conformance tests).
804    pub fn recv_raw(&mut self, timeout: Duration) -> Result<Value> {
805        match self.rx.recv_timeout(timeout) {
806            Ok(Ok(Frame::Json(v))) => Ok(v),
807            Ok(Ok(Frame::Arrow(_))) => Err(HostError::Malformed {
808                plugin: self.label.clone(),
809                message: "Arrow data where a JSON message was expected".into(),
810            }),
811            Ok(Err(FrameError::Malformed(m))) => Err(HostError::Malformed {
812                plugin: self.label.clone(),
813                message: m,
814            }),
815            Err(RecvTimeoutError::Timeout) => Err(HostError::Timeout {
816                plugin: self.label.clone(),
817                waiting_for: "a message",
818            }),
819            _ => Err(self.crashed()),
820        }
821    }
822
823    /// The next frame as sent, JSON or Arrow, with nothing skipped or decoded (the conformance
824    /// runner).
825    pub fn recv_frame(&mut self, timeout: Duration) -> Result<Frame> {
826        match self.rx.recv_timeout(timeout) {
827            Ok(Ok(f)) => Ok(f),
828            Ok(Err(FrameError::Malformed(m))) => Err(HostError::Malformed {
829                plugin: self.label.clone(),
830                message: m,
831            }),
832            Err(RecvTimeoutError::Timeout) => Err(HostError::Timeout {
833                plugin: self.label.clone(),
834                waiting_for: "a message",
835            }),
836            _ => Err(self.crashed()),
837        }
838    }
839
840    fn recv_any(&mut self, timeout: Option<Duration>, waiting_for: &'static str) -> Result<Incoming> {
841        let got = match timeout {
842            _ if self.early.is_some() => self.early.take(),
843            Some(t) => match self.rx.recv_timeout(t) {
844                Ok(f) => Some(f),
845                Err(RecvTimeoutError::Timeout) => {
846                    return Err(HostError::Timeout {
847                        plugin: self.label.clone(),
848                        waiting_for,
849                    });
850                }
851                Err(RecvTimeoutError::Disconnected) => None,
852            },
853            None => self.rx.recv().ok(),
854        };
855        match got {
856            Some(Ok(Frame::Json(v))) => match serde_json::from_value::<Envelope<Response>>(v.clone()) {
857                Ok(Envelope { id: Some(id), body })
858                    if !matches!(body, Response::Log { .. } | Response::Progress { .. }) =>
859                {
860                    let current = self.current.load(Ordering::SeqCst);
861                    if id != current {
862                        return Err(HostError::Malformed {
863                            plugin: self.label.clone(),
864                            message: format!(
865                                "a reply to request {id} arrived while request {current} was running"
866                            ),
867                        });
868                    }
869                    Ok(Incoming::Json(body))
870                }
871                Ok(e) => Ok(Incoming::Json(e.body)),
872                Err(e) => Err(HostError::Malformed {
873                    plugin: self.label.clone(),
874                    message: format!("unknown message {v}: {e}"),
875                }),
876            },
877            Some(Ok(Frame::Arrow(b))) => Ok(Incoming::Arrow(b)),
878            Some(Err(FrameError::Malformed(m))) => Err(HostError::Malformed {
879                plugin: self.label.clone(),
880                message: m,
881            }),
882            Some(Err(_)) | None => Err(self.crashed()),
883        }
884    }
885
886    /// Receive a JSON response, turning `error` into `HostError::Plugin`.
887    pub fn recv_json(&mut self, waiting_for: &'static str) -> Result<Response> {
888        match self.recv(None, waiting_for)? {
889            Incoming::Json(Response::Error { message, kind, code }) => Err(HostError::Plugin {
890                plugin: self.label.clone(),
891                message,
892                kind,
893                code,
894            }),
895            Incoming::Json(r) => Ok(r),
896            other => Err(self.unexpected(waiting_for, &other)),
897        }
898    }
899
900    /// Send a request and expect `ok`.
901    fn call_ok(&mut self, req: &Request, what: &'static str) -> Result<()> {
902        self.send(req)?;
903        match self.recv_json(what)? {
904            Response::Ok {} => Ok(()),
905            other => Err(self.unexpected(what, &Incoming::Json(other))),
906        }
907    }
908
909    pub fn describe(&mut self) -> Result<Vec<ConnectionField>> {
910        Ok(self.describe_all()?.0)
911    }
912
913    /// The connection fields and the options the plugin declares.
914    pub fn describe_all(&mut self) -> Result<(Vec<ConnectionField>, Vec<OptionField>)> {
915        let d = self.description()?;
916        Ok((d.connection_fields, d.option_fields))
917    }
918
919    /// The whole `describe` reply.
920    pub fn description(&mut self) -> Result<Description> {
921        self.send(&Request::Describe {})?;
922        match self.recv_json("a describe reply")? {
923            Response::Describe {
924                connection_fields,
925                option_fields,
926                identifier_quote,
927                string_literal,
928                message_limit,
929                single_writer,
930            } => Ok(Description {
931                connection_fields,
932                option_fields,
933                identifier_quote,
934                string_literal,
935                message_limit,
936                single_writer,
937            }),
938            other => Err(self.unexpected("a describe reply", &Incoming::Json(other))),
939        }
940    }
941
942    /// Check a profile entry's connection settings without connecting (protocol 1): the errors
943    /// and the warnings. `unresolved` keys (an unset `env_var()`) are sent as `null`.
944    pub fn validate_connection(
945        &mut self,
946        mut connection: Map<String, Value>,
947        unresolved: Vec<String>,
948    ) -> Result<(Vec<String>, Vec<String>)> {
949        for k in &unresolved {
950            connection.insert(k.clone(), Value::Null);
951        }
952        self.send(&Request::ValidateConnection {
953            connection,
954            unresolved,
955        })?;
956        match self.recv_json("a validated reply")? {
957            Response::Validated { errors, warnings } => Ok((errors, warnings)),
958            other => Err(self.unexpected("a validated reply", &Incoming::Json(other))),
959        }
960    }
961
962    /// Check a config block of options (needs `validate`); returns every problem found.
963    pub fn validate(&mut self, options: Map<String, Value>) -> Result<Vec<String>> {
964        Ok(self.validate_with(options, None)?.0)
965    }
966
967    /// Check a format's config block together with its `template` (as `write` would send it);
968    /// returns the errors, then the warnings.
969    pub fn validate_with(
970        &mut self,
971        options: Map<String, Value>,
972        template: Option<Value>,
973    ) -> Result<(Vec<String>, Vec<String>)> {
974        self.send(&Request::Validate { options, template })?;
975        match self.recv_json("a validate reply")? {
976            Response::Validated { errors, warnings } => Ok((errors, warnings)),
977            other => Err(self.unexpected("a validate reply", &Incoming::Json(other))),
978        }
979    }
980
981    pub fn open(&mut self, connection: Map<String, Value>, read_only: bool) -> Result<()> {
982        self.call_ok(
983            &Request::Open {
984                connection,
985                read_only,
986                sandbox: false,
987            },
988            "an open reply",
989        )
990    }
991
992    /// Open a read-only session that may not reach files, programs or the network beyond the
993    /// database, where the plugin supports it (`dre query`).
994    pub fn open_sandboxed(&mut self, connection: Map<String, Value>) -> Result<()> {
995        self.call_ok(
996            &Request::Open {
997                connection,
998                read_only: true,
999                sandbox: true,
1000            },
1001            "an open reply",
1002        )
1003    }
1004
1005    pub fn check(&mut self, sql: &str) -> Result<()> {
1006        self.call_ok(&Request::Check { sql: sql.to_string() }, "a check reply")
1007    }
1008
1009    /// Discover a source's catalogue (`catalog` capability), returning its existing
1010    /// `dre/catalog/v1` document.
1011    pub fn catalog(&mut self, tables: Vec<String>, schemas: Vec<String>) -> Result<Value> {
1012        self.send(&Request::Catalog { tables, schemas })?;
1013        match self.recv_json("a catalog reply")? {
1014            Response::Catalog { catalog } => Ok(catalog),
1015            other => Err(self.unexpected("a catalog reply", &Incoming::Json(other))),
1016        }
1017    }
1018
1019    /// Run one statement, handing every batch to `on_batch` as it arrives.
1020    pub fn execute(
1021        &mut self,
1022        sql: &str,
1023        row_limit: Option<u64>,
1024        mut on_batch: impl FnMut(&SchemaRef, RecordBatch) -> std::result::Result<(), String>,
1025    ) -> Result<Execution> {
1026        self.send(&Request::Execute {
1027            sql: sql.to_string(),
1028            row_limit,
1029        })?;
1030        match self.recv_json("an execute reply")? {
1031            Response::NoResult { rows_affected } => return Ok(Execution::NoResult { rows_affected }),
1032            Response::Result { .. } => {}
1033            other => return Err(self.unexpected("an execute reply", &Incoming::Json(other))),
1034        }
1035        let mut schema: Option<SchemaRef> = None;
1036        let mut rows = 0u64;
1037        let mut sink_error = None;
1038        loop {
1039            match self.recv(None, "result data")? {
1040                Incoming::Arrow(ipc) => {
1041                    let (s, batches) = frame::decode_batches(&ipc).map_err(|e| HostError::Arrow {
1042                        plugin: self.label.clone(),
1043                        message: e.to_string(),
1044                    })?;
1045                    let schema = schema.get_or_insert(s);
1046                    for b in batches {
1047                        rows += b.num_rows() as u64;
1048                        if sink_error.is_none()
1049                            && let Err(e) = on_batch(schema, b)
1050                        {
1051                            // Keep draining so the session stays usable; report after.
1052                            sink_error = Some(e);
1053                        }
1054                    }
1055                }
1056                Incoming::Json(Response::ResultEnd { .. }) => break,
1057                Incoming::Json(Response::Error { message, kind, code }) => {
1058                    return Err(HostError::Plugin {
1059                        plugin: self.label.clone(),
1060                        message,
1061                        kind,
1062                        code,
1063                    });
1064                }
1065                other => return Err(self.unexpected("result data", &other)),
1066            }
1067        }
1068        if let Some(e) = sink_error {
1069            return Err(HostError::plugin(&self.label, e));
1070        }
1071        let schema = schema.ok_or_else(|| HostError::Malformed {
1072            plugin: self.label.clone(),
1073            message: "a result ended without any Arrow frame carrying its schema".into(),
1074        })?;
1075        Ok(Execution::Result { schema, rows })
1076    }
1077
1078    /// Start a format `write`; follow with `write_result_set` per result set, then `write_finish`.
1079    pub fn write_begin(
1080        &mut self,
1081        path: &str,
1082        format: &str,
1083        options: Map<String, Value>,
1084        result_sets: Vec<ResultSetMeta>,
1085        template: Option<Value>,
1086    ) -> Result<()> {
1087        self.write_begin_with_font_requests(path, format, options, result_sets, template, false)
1088    }
1089
1090    /// Start a write, advertising whether the caller handles nested font requests.
1091    pub fn write_begin_with_font_requests(
1092        &mut self,
1093        path: &str,
1094        format: &str,
1095        options: Map<String, Value>,
1096        result_sets: Vec<ResultSetMeta>,
1097        template: Option<Value>,
1098        font_requests: bool,
1099    ) -> Result<()> {
1100        self.send(&Request::Write {
1101            font_requests,
1102            path: path.to_string(),
1103            format: format.to_string(),
1104            options,
1105            result_sets,
1106            template,
1107        })
1108    }
1109
1110    /// Stream one result set: at least one batch (carrying the schema), then the end marker.
1111    pub fn write_result_set(
1112        &mut self,
1113        schema: &SchemaRef,
1114        batches: impl IntoIterator<Item = RecordBatch>,
1115    ) -> Result<()> {
1116        let mut any = false;
1117        for b in batches {
1118            any = true;
1119            self.send_batch(&b)?;
1120        }
1121        if !any {
1122            self.send_batch(&RecordBatch::new_empty(schema.clone()))?;
1123        }
1124        self.send(&Request::ResultSetEnd {})
1125    }
1126
1127    /// Load rows into a temporary table on the source's session. Returns the relation to use in
1128    /// SQL, and the plugin's warning, if any. Only for plugins advertising `load`.
1129    pub fn load(
1130        &mut self,
1131        name: &str,
1132        schema: &SchemaRef,
1133        batches: impl IntoIterator<Item = RecordBatch>,
1134    ) -> Result<(String, Option<String>)> {
1135        self.send(&Request::Load {
1136            name: name.to_string(),
1137        })?;
1138        self.write_result_set(schema, batches)?;
1139        match self.recv_json("a loaded reply")? {
1140            Response::Loaded {
1141                relation, warning, ..
1142            } => Ok((relation, warning)),
1143            other => Err(self.unexpected("a loaded reply", &Incoming::Json(other))),
1144        }
1145    }
1146
1147    pub fn write_finish(&mut self) -> Result<Vec<String>> {
1148        Ok(self.write_finish_with_warnings()?.0)
1149    }
1150
1151    /// `write_finish`, also returning the format's warnings.
1152    pub fn write_finish_with_warnings(&mut self) -> Result<(Vec<String>, Vec<String>)> {
1153        self.write_finish_with_fonts(&mut |_| Err("this host cannot fetch fonts".into()))
1154    }
1155
1156    /// Complete a write, resolving nested font requests through the caller's registry.
1157    pub fn write_finish_with_fonts(
1158        &mut self,
1159        resolve: &mut dyn FnMut(
1160            &crate::msg::FontRequest,
1161        ) -> std::result::Result<Vec<String>, crate::msg::FontError>,
1162    ) -> Result<(Vec<String>, Vec<String>)> {
1163        self.send(&Request::Finish {})?;
1164        loop {
1165            match self.recv_json("a written reply")? {
1166                Response::NeedFonts { font } => {
1167                    let reply = match resolve(&font) {
1168                        Ok(paths) => Request::Fonts {
1169                            paths,
1170                            error: None,
1171                            repeated: false,
1172                        },
1173                        Err(e) => Request::Fonts {
1174                            paths: Vec::new(),
1175                            error: Some(e.message),
1176                            repeated: e.repeated,
1177                        },
1178                    };
1179                    self.send(&reply)?;
1180                }
1181                Response::Written { files, warnings } => return Ok((files, warnings)),
1182                other => return Err(self.unexpected("a written reply", &Incoming::Json(other))),
1183            }
1184        }
1185    }
1186
1187    /// Deliver one file with no plugin options.
1188    pub fn deliver(
1189        &mut self,
1190        local_path: &str,
1191        remote_path: Option<&str>,
1192        connection: Map<String, Value>,
1193    ) -> Result<String> {
1194        let file = DeliveryFile {
1195            local_path: local_path.to_string(),
1196            remote_path: remote_path.map(str::to_string),
1197        };
1198        self.deliver_files(&[file], connection, Map::new())
1199    }
1200
1201    /// Deliver `files` in one request with the destination entry's `options`. More than one file
1202    /// needs a plugin advertising `multi_file`; call once per file otherwise.
1203    pub fn deliver_files(
1204        &mut self,
1205        files: &[DeliveryFile],
1206        connection: Map<String, Value>,
1207        options: Map<String, Value>,
1208    ) -> Result<String> {
1209        let req = match files {
1210            [] => {
1211                return Err(HostError::Malformed {
1212                    plugin: self.label.clone(),
1213                    message: "nothing to deliver".into(),
1214                });
1215            }
1216            [one] => Request::Deliver {
1217                local_path: Some(one.local_path.clone()),
1218                remote_path: one.remote_path.clone(),
1219                files: Vec::new(),
1220                connection,
1221                options,
1222                message: None,
1223            },
1224            many => {
1225                if !self.has(crate::CAP_MULTI_FILE) {
1226                    return Err(HostError::plugin(
1227                        &self.label,
1228                        format!(
1229                            "{} files in one delivery, but the plugin doesn't advertise `multi_file`",
1230                            many.len()
1231                        ),
1232                    ));
1233                }
1234                Request::Deliver {
1235                    local_path: None,
1236                    remote_path: None,
1237                    files: many.to_vec(),
1238                    connection,
1239                    options,
1240                    message: None,
1241                }
1242            }
1243        };
1244        self.send(&req)?;
1245        match self.recv_json("a delivered reply")? {
1246            Response::Delivered { location, attempts } => {
1247                self.last_attempts = attempts.unwrap_or(1);
1248                Ok(location)
1249            }
1250            other => Err(self.unexpected("a delivered reply", &Incoming::Json(other))),
1251        }
1252    }
1253
1254    /// Deliver a message, with `attach` files, to a plugin advertising `message`.
1255    pub fn deliver_message(
1256        &mut self,
1257        message: &crate::msg::Message,
1258        attach: &[DeliveryFile],
1259        connection: Map<String, Value>,
1260        options: Map<String, Value>,
1261    ) -> Result<String> {
1262        if !self.has(crate::CAP_MESSAGE) {
1263            return Err(HostError::plugin(
1264                &self.label,
1265                "a message, but the plugin doesn't advertise `message`",
1266            ));
1267        }
1268        self.send(&Request::Deliver {
1269            local_path: None,
1270            remote_path: None,
1271            files: attach.to_vec(),
1272            connection,
1273            options,
1274            message: Some(message.clone()),
1275        })?;
1276        match self.recv_json("a delivered reply")? {
1277            Response::Delivered { location, attempts } => {
1278                self.last_attempts = attempts.unwrap_or(1);
1279                Ok(location)
1280            }
1281            other => Err(self.unexpected("a delivered reply", &Incoming::Json(other))),
1282        }
1283    }
1284
1285    /// How many tries the last delivery took: 1, or more when the plugin retried a temporary
1286    /// error.
1287    pub fn last_attempts(&self) -> u32 {
1288        self.last_attempts
1289    }
1290
1291    /// Ask the plugin to exit, and wait for it.
1292    pub fn close(mut self) -> Result<()> {
1293        let _ = self.send(&Request::Close {});
1294        let _ = self.recv(Some(Duration::from_secs(5)), "the close reply");
1295        if let Ok(mut stdin) = self.stdin.lock() {
1296            stdin.take();
1297        }
1298        for _ in 0..250 {
1299            if let Ok(Some(_)) = self.try_wait() {
1300                return Ok(());
1301            }
1302            std::thread::sleep(Duration::from_millis(20));
1303        }
1304        if let Ok(mut child) = self.child.lock() {
1305            let _ = child.kill();
1306        }
1307        Ok(())
1308    }
1309
1310    /// Raw access for protocol tests: write arbitrary bytes to the plugin.
1311    pub fn write_raw(&mut self, bytes: &[u8]) -> Result<()> {
1312        let ok = self.stdin.lock().is_ok_and(|mut stdin| {
1313            stdin
1314                .as_mut()
1315                .is_some_and(|w| w.write_all(bytes).and_then(|_| w.flush()).is_ok())
1316        });
1317        if ok { Ok(()) } else { Err(self.write_failed()) }
1318    }
1319
1320    /// Close the plugin's stdin: end of input, as when core goes away.
1321    pub fn close_input(&mut self) {
1322        if let Ok(mut stdin) = self.stdin.lock() {
1323            stdin.take();
1324        }
1325    }
1326
1327    /// Wait up to `timeout` for the process to exit.
1328    pub fn wait_exit(&mut self, timeout: Duration) -> Option<ExitStatus> {
1329        if let Ok(mut stdin) = self.stdin.lock() {
1330            stdin.take();
1331        }
1332        let deadline = std::time::Instant::now() + timeout;
1333        while std::time::Instant::now() < deadline {
1334            if let Ok(Some(s)) = self.try_wait() {
1335                return Some(s);
1336            }
1337            std::thread::sleep(Duration::from_millis(20));
1338        }
1339        None
1340    }
1341}
1342
1343impl PluginProcess {
1344    /// Wait (briefly) for the stderr forwarder to reach end of stream.
1345    fn drain_stderr(&mut self) {
1346        if let Some(t) = self.stderr_thread.take() {
1347            let deadline = std::time::Instant::now() + Duration::from_secs(2);
1348            while !t.is_finished() && std::time::Instant::now() < deadline {
1349                std::thread::sleep(Duration::from_millis(5));
1350            }
1351            if t.is_finished() {
1352                let _ = t.join();
1353            }
1354        }
1355    }
1356}
1357
1358impl Drop for PluginProcess {
1359    fn drop(&mut self) {
1360        if let Ok(mut stdin) = self.stdin.lock() {
1361            stdin.take();
1362        }
1363        if let Ok(None) = self.try_wait() {
1364            // Give it a moment to exit on its own (stdin is closed), then insist.
1365            for _ in 0..25 {
1366                if let Ok(Some(_)) = self.try_wait() {
1367                    break;
1368                }
1369                std::thread::sleep(Duration::from_millis(10));
1370            }
1371            let _ = self.child.lock().unwrap().kill();
1372            let _ = self.child.lock().unwrap().wait();
1373        }
1374        self.drain_stderr();
1375    }
1376}
1377
1378/// A `log` message as a log sink line: prefixed by its level, then its fields as `key=value`.
1379fn log_line(level: LogLevel, message: &str, fields: &Map<String, Value>) -> String {
1380    let prefix = match level {
1381        LogLevel::Error | LogLevel::Warn => "warning: ",
1382        LogLevel::Info => "info: ",
1383        LogLevel::Debug | LogLevel::Trace => "debug: ",
1384    };
1385    let mut line = format!("{prefix}{message}");
1386    for (k, v) in fields {
1387        match v {
1388            Value::String(s) => line.push_str(&format!(" {k}={s}")),
1389            v => line.push_str(&format!(" {k}={v}")),
1390        }
1391    }
1392    line
1393}
1394
1395/// A `progress` message as a log sink line.
1396fn progress_line(message: Option<&str>, done: Option<u64>, total: Option<u64>) -> String {
1397    let count = match (done, total) {
1398        (Some(d), Some(t)) => format!("{d}/{t}"),
1399        (Some(d), None) => d.to_string(),
1400        _ => String::new(),
1401    };
1402    match (message, count.is_empty()) {
1403        (Some(m), true) => format!("info: {m}"),
1404        (Some(m), false) => format!("info: {m} ({count})"),
1405        (None, _) => format!("info: {count}"),
1406    }
1407}
1408
1409static CORE_VERSION: OnceLock<String> = OnceLock::new();
1410
1411/// Set the DRE version sent to plugins in the handshake (`core_version`). This crate is versioned
1412/// on its own, so the host names the version of DRE it's part of; unset, it's "unreleased".
1413pub fn set_core_version(version: &str) {
1414    let _ = CORE_VERSION.set(version.to_string());
1415}
1416
1417fn core_version() -> String {
1418    CORE_VERSION.get().cloned().unwrap_or_else(|| "unreleased".into())
1419}