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