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