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.
6
7use std::collections::VecDeque;
8use std::io::{BufRead, BufReader, BufWriter, Write};
9use std::path::{Path, PathBuf};
10use std::process::{Child, ChildStdin, Command, ExitStatus, Stdio};
11use std::sync::mpsc::{Receiver, RecvTimeoutError, channel};
12use std::sync::{Arc, Mutex, OnceLock};
13use std::time::Duration;
14
15use arrow::array::RecordBatch;
16use arrow::datatypes::SchemaRef;
17use serde_json::{Map, Value};
18
19use crate::frame::{self, Frame, FrameError};
20use crate::msg::{ConnectionField, DeliveryFile, Request, Response, ResultSetMeta};
21use crate::options::OptionField;
22use crate::{Kind, MAX_VERSION, MIN_VERSION, PluginId, parse_executable_name};
23
24/// Receives each stderr line a plugin writes.
25pub type LogSink = Arc<dyn Fn(&str, &str) + Send + Sync>;
26
27/// A log sink that prefixes each plugin line with the plugin's file name, on core's stderr.
28pub fn stderr_log() -> LogSink {
29    Arc::new(|plugin, line| eprintln!("[{plugin}] {line}"))
30}
31
32pub const DEFAULT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(30);
33
34/// Set to `1` in a plugin's environment when a person is at core's terminal, else `0`.
35pub const INTERACTIVE_ENV: &str = "DRE_INTERACTIVE";
36const STDERR_TAIL: usize = 20;
37
38#[derive(Debug, Clone, PartialEq)]
39pub struct PluginInfo {
40    pub protocol_version: u32,
41    pub kind: Kind,
42    pub name: String,
43    pub version: String,
44    pub capabilities: Vec<String>,
45    /// Every plugin the executable serves (at least the one it's serving now).
46    pub provides: Vec<PluginId>,
47}
48
49/// A plugin's `describe` reply.
50#[derive(Debug, Clone, PartialEq, Default)]
51pub struct Description {
52    pub connection_fields: Vec<ConnectionField>,
53    pub option_fields: Vec<OptionField>,
54    /// A source's identifier quote character.
55    pub identifier_quote: Option<String>,
56    /// A message destination's length limit, in characters.
57    pub message_limit: Option<u64>,
58}
59
60#[derive(Debug)]
61pub enum HostError {
62    Spawn {
63        plugin: String,
64        error: std::io::Error,
65    },
66    Crashed {
67        plugin: String,
68        status: Option<ExitStatus>,
69        stderr: Vec<String>,
70    },
71    Malformed {
72        plugin: String,
73        message: String,
74    },
75    Incompatible {
76        plugin: String,
77        core: (u32, u32),
78        plugin_range: (u32, u32),
79    },
80    Timeout {
81        plugin: String,
82        waiting_for: &'static str,
83    },
84    Unexpected {
85        plugin: String,
86        expected: &'static str,
87        got: String,
88    },
89    /// The plugin reported an error for a request.
90    Plugin {
91        plugin: String,
92        message: String,
93    },
94    Arrow {
95        plugin: String,
96        message: String,
97    },
98}
99
100impl std::fmt::Display for HostError {
101    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
102        match self {
103            HostError::Spawn { plugin, error } => write!(f, "can't start plugin `{plugin}`: {error}"),
104            HostError::Crashed {
105                plugin,
106                status,
107                stderr,
108            } => {
109                match status {
110                    Some(s) => write!(f, "plugin `{plugin}` exited unexpectedly ({s})")?,
111                    None => write!(f, "plugin `{plugin}` closed its output unexpectedly")?,
112                }
113                if !stderr.is_empty() {
114                    write!(f, "; its last log lines:\n  {}", stderr.join("\n  "))?;
115                }
116                Ok(())
117            }
118            HostError::Malformed { plugin, message } => {
119                write!(f, "plugin `{plugin}` broke the protocol: {message}")
120            }
121            HostError::Incompatible {
122                plugin,
123                core,
124                plugin_range,
125            } => write!(
126                f,
127                "plugin `{plugin}` speaks protocol versions {}..={}, but this DRE core speaks {}..={}; update {}",
128                plugin_range.0,
129                plugin_range.1,
130                core.0,
131                core.1,
132                if plugin_range.1 < core.0 {
133                    "the plugin"
134                } else {
135                    "DRE"
136                }
137            ),
138            HostError::Timeout { plugin, waiting_for } => {
139                write!(
140                    f,
141                    "plugin `{plugin}` didn't answer in time (waiting for {waiting_for})"
142                )
143            }
144            HostError::Unexpected {
145                plugin,
146                expected,
147                got,
148            } => {
149                write!(f, "plugin `{plugin}` sent {got} where {expected} was expected")
150            }
151            HostError::Plugin { message, .. } => f.write_str(message),
152            HostError::Arrow { plugin, message } => {
153                write!(f, "plugin `{plugin}` sent invalid Arrow data: {message}")
154            }
155        }
156    }
157}
158
159impl std::error::Error for HostError {}
160
161pub type Result<T> = std::result::Result<T, HostError>;
162
163/// A message from the plugin.
164pub enum Incoming {
165    Json(Response),
166    Arrow(Vec<u8>),
167}
168
169impl std::fmt::Debug for Incoming {
170    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
171        match self {
172            Incoming::Json(r) => write!(f, "Json({r:?})"),
173            Incoming::Arrow(b) => write!(f, "Arrow frame ({} bytes)", b.len()),
174        }
175    }
176}
177
178/// The outcome of a source `execute`.
179#[derive(Debug)]
180pub enum Execution {
181    NoResult { rows_affected: Option<u64> },
182    Result { schema: SchemaRef, rows: u64 },
183}
184
185pub struct PluginProcess {
186    label: String,
187    path: PathBuf,
188    child: Child,
189    stdin: Option<BufWriter<ChildStdin>>,
190    rx: Receiver<std::result::Result<Frame, FrameError>>,
191    stderr: Arc<Mutex<VecDeque<String>>>,
192    /// Forwards stderr; joined on close/drop so no log line is lost when the plugin exits.
193    stderr_thread: Option<std::thread::JoinHandle<()>>,
194    info: Option<PluginInfo>,
195    /// A reply that arrived while core was still sending (a format failing mid-stream).
196    early: Option<std::result::Result<Frame, FrameError>>,
197    /// The plugin asked for in the handshake. By default the one a `dre-<kind>-<name>` file name
198    /// names; none for a package executable, which then serves its first plugin.
199    serve: Option<PluginId>,
200}
201
202impl PluginProcess {
203    /// Spawn the plugin at `path` and complete the handshake.
204    pub fn start(path: &Path, log: LogSink) -> Result<PluginProcess> {
205        Self::start_in(path, log, None)
206    }
207
208    /// Like `start`, running the plugin in `cwd` (core uses the project directory).
209    pub fn start_in(path: &Path, log: LogSink, cwd: Option<&Path>) -> Result<PluginProcess> {
210        Self::start_for(path, None, log, cwd)
211    }
212
213    /// Like `start_in`, asking the executable for `plugin` (one of a package's plugins).
214    pub fn start_for(
215        path: &Path,
216        plugin: Option<&PluginId>,
217        log: LogSink,
218        cwd: Option<&Path>,
219    ) -> Result<PluginProcess> {
220        let timeout = std::env::var("DRE_PLUGIN_HANDSHAKE_TIMEOUT_MS")
221            .ok()
222            .and_then(|v| v.parse().ok())
223            .map(Duration::from_millis)
224            .unwrap_or(DEFAULT_HANDSHAKE_TIMEOUT);
225        let mut p = Self::spawn_in(path, log, &[], cwd)?;
226        if let Some(id) = plugin {
227            p.serve = Some(id.clone());
228        }
229        p.handshake((MIN_VERSION, MAX_VERSION), timeout)?;
230        Ok(p)
231    }
232
233    /// Spawn and handshake offering an explicit version range (the conformance suite uses this).
234    pub fn start_with(
235        path: &Path,
236        log: LogSink,
237        versions: (u32, u32),
238        timeout: Duration,
239    ) -> Result<PluginProcess> {
240        let mut p = Self::spawn(path, log)?;
241        p.handshake(versions, timeout)?;
242        Ok(p)
243    }
244
245    /// Spawn without a handshake (for tests that probe raw protocol behaviour).
246    pub fn spawn(path: &Path, log: LogSink) -> Result<PluginProcess> {
247        Self::spawn_env(path, log, &[])
248    }
249
250    /// Spawn with extra environment variables, without a handshake.
251    pub fn spawn_env(path: &Path, log: LogSink, env: &[(&str, &str)]) -> Result<PluginProcess> {
252        Self::spawn_in(path, log, env, None)
253    }
254
255    /// Spawn with extra environment variables and a working directory, without a handshake.
256    pub fn spawn_in(
257        path: &Path,
258        log: LogSink,
259        env: &[(&str, &str)],
260        cwd: Option<&Path>,
261    ) -> Result<PluginProcess> {
262        let label = path
263            .file_name()
264            .map(|f| f.to_string_lossy().to_string())
265            .unwrap_or_default();
266        let mut cmd = Command::new(path);
267        if let Some(dir) = cwd {
268            cmd.current_dir(dir);
269        }
270        // Plugins can't see the terminal (their stdio is piped), so tell them whether a person is
271        // there, e.g. to finish a browser sign-in. An existing DRE_INTERACTIVE is left alone.
272        if std::env::var_os(INTERACTIVE_ENV).is_none() {
273            use std::io::IsTerminal;
274            let there = std::io::stderr().is_terminal() && std::io::stdin().is_terminal();
275            cmd.env(INTERACTIVE_ENV, if there { "1" } else { "0" });
276        }
277        let mut child = cmd
278            .envs(env.iter().copied())
279            .stdin(Stdio::piped())
280            .stdout(Stdio::piped())
281            .stderr(Stdio::piped())
282            .spawn()
283            .map_err(|error| HostError::Spawn {
284                plugin: label.clone(),
285                error,
286            })?;
287        let stdout = child.stdout.take().unwrap();
288        let stderr_pipe = child.stderr.take().unwrap();
289        let stdin = child.stdin.take().map(BufWriter::new);
290
291        let (tx, rx) = channel();
292        std::thread::spawn(move || {
293            let mut r = BufReader::new(stdout);
294            loop {
295                let f = frame::read_frame(&mut r);
296                let stop = f.is_err();
297                if tx.send(f).is_err() || stop {
298                    break;
299                }
300            }
301        });
302        let stderr = Arc::new(Mutex::new(VecDeque::new()));
303        let tail = stderr.clone();
304        let who = label.clone();
305        let stderr_thread = std::thread::spawn(move || {
306            for line in BufReader::new(stderr_pipe).lines() {
307                let Ok(line) = line else { break };
308                log(&who, &line);
309                let mut t = tail.lock().unwrap();
310                if t.len() == STDERR_TAIL {
311                    t.pop_front();
312                }
313                t.push_back(line);
314            }
315        });
316        let serve = parse_executable_name(&label).map(|(k, n)| PluginId::new(k, n));
317        Ok(PluginProcess {
318            label,
319            path: path.to_path_buf(),
320            child,
321            stdin,
322            rx,
323            stderr,
324            stderr_thread: Some(stderr_thread),
325            info: None,
326            early: None,
327            serve,
328        })
329    }
330
331    /// Name the plugin to ask for in the handshake (`None`: a package's first).
332    pub fn ask_for(&mut self, plugin: Option<PluginId>) {
333        self.serve = plugin;
334    }
335
336    /// Complete the handshake on a process from `spawn`/`spawn_env`.
337    pub fn handshake(&mut self, (min, max): (u32, u32), timeout: Duration) -> Result<()> {
338        self.send(&Request::Hello {
339            min_version: min,
340            max_version: max,
341            core_version: core_version(),
342            plugin: self.serve.clone(),
343        })?;
344        match self.recv(Some(timeout), "the hello reply")? {
345            Incoming::Json(Response::Hello {
346                protocol_version,
347                kind,
348                name,
349                version,
350                capabilities,
351                mut provides,
352            }) => {
353                if protocol_version < min || protocol_version > max {
354                    return Err(HostError::Incompatible {
355                        plugin: self.label.clone(),
356                        core: (min, max),
357                        plugin_range: (protocol_version, protocol_version),
358                    });
359                }
360                if let Some(want) = &self.serve
361                    && (want.kind != kind || want.name != name)
362                {
363                    return Err(HostError::Plugin {
364                        plugin: self.label.clone(),
365                        message: format!(
366                            "`{}` was asked for the {want} plugin but serves {kind}/{name}",
367                            self.label
368                        ),
369                    });
370                }
371                if provides.is_empty() {
372                    provides.push(PluginId::new(kind, name.clone()));
373                }
374                self.info = Some(PluginInfo {
375                    protocol_version,
376                    kind,
377                    name,
378                    version,
379                    capabilities,
380                    provides,
381                });
382                Ok(())
383            }
384            Incoming::Json(Response::VersionMismatch {
385                min_version,
386                max_version,
387            }) => Err(HostError::Incompatible {
388                plugin: self.label.clone(),
389                core: (min, max),
390                plugin_range: (min_version, max_version),
391            }),
392            Incoming::Json(Response::Error { message }) => Err(HostError::Plugin {
393                plugin: self.label.clone(),
394                message,
395            }),
396            other => Err(self.unexpected("a hello reply", &other)),
397        }
398    }
399
400    pub fn label(&self) -> &str {
401        &self.label
402    }
403
404    pub fn path(&self) -> &Path {
405        &self.path
406    }
407
408    pub fn info(&self) -> &PluginInfo {
409        self.info.as_ref().expect("handshake completed")
410    }
411
412    pub fn has(&self, capability: &str) -> bool {
413        self.info
414            .as_ref()
415            .is_some_and(|i| i.capabilities.iter().any(|c| c == capability))
416    }
417
418    fn unexpected(&self, expected: &'static str, got: &Incoming) -> HostError {
419        let got = match got {
420            Incoming::Json(r) => format!("{:?}", r)
421                .split([' ', '{'])
422                .next()
423                .unwrap_or("?")
424                .to_string(),
425            Incoming::Arrow(_) => "Arrow data".into(),
426        };
427        HostError::Unexpected {
428            plugin: self.label.clone(),
429            expected,
430            got: format!("`{got}`"),
431        }
432    }
433
434    fn crashed(&mut self) -> HostError {
435        // Give the process a moment to finish exiting so its status and last stderr are known.
436        let mut status = None;
437        for _ in 0..50 {
438            if let Ok(Some(s)) = self.child.try_wait() {
439                status = Some(s);
440                break;
441            }
442            std::thread::sleep(Duration::from_millis(20));
443        }
444        std::thread::sleep(Duration::from_millis(50));
445        let stderr = self.stderr.lock().unwrap().iter().cloned().collect();
446        HostError::Crashed {
447            plugin: self.label.clone(),
448            status,
449            stderr,
450        }
451    }
452
453    pub fn send(&mut self, req: &Request) -> Result<()> {
454        let ok = match self.stdin.as_mut() {
455            Some(w) => frame::write_json(w, req).is_ok(),
456            None => false,
457        };
458        if ok { Ok(()) } else { Err(self.write_failed()) }
459    }
460
461    pub fn send_batch(&mut self, batch: &RecordBatch) -> Result<()> {
462        // A plugin that fails part-way replies before reading the rest: stop sending and
463        // surface its error rather than streaming everything into a plugin that gave up.
464        if self.early.is_none()
465            && let Ok(f) = self.rx.try_recv()
466        {
467            self.early = Some(f);
468        }
469        if let Some(Ok(Frame::Json(v))) = &self.early
470            && let Ok(Response::Error { message }) = serde_json::from_value::<Response>(v.clone())
471        {
472            return Err(HostError::Plugin {
473                plugin: self.label.clone(),
474                message,
475            });
476        }
477        let ipc = frame::encode_batch(batch).map_err(|e| HostError::Arrow {
478            plugin: self.label.clone(),
479            message: e.to_string(),
480        })?;
481        let ok = match self.stdin.as_mut() {
482            Some(w) => frame::write_arrow(w, &ipc).is_ok(),
483            None => false,
484        };
485        if ok { Ok(()) } else { Err(self.write_failed()) }
486    }
487
488    /// Writing failed: the plugin has gone away. Prefer its own error message if it sent one.
489    fn write_failed(&mut self) -> HostError {
490        match self.rx.recv_timeout(Duration::from_secs(2)) {
491            Ok(Ok(Frame::Json(v))) => match serde_json::from_value::<Response>(v) {
492                Ok(Response::Error { message }) => HostError::Plugin {
493                    plugin: self.label.clone(),
494                    message,
495                },
496                _ => self.crashed(),
497            },
498            _ => self.crashed(),
499        }
500    }
501
502    /// Receive the next message. `timeout` of `None` waits as long as the plugin is alive.
503    pub fn recv(&mut self, timeout: Option<Duration>, waiting_for: &'static str) -> Result<Incoming> {
504        let got = match timeout {
505            _ if self.early.is_some() => self.early.take(),
506            Some(t) => match self.rx.recv_timeout(t) {
507                Ok(f) => Some(f),
508                Err(RecvTimeoutError::Timeout) => {
509                    return Err(HostError::Timeout {
510                        plugin: self.label.clone(),
511                        waiting_for,
512                    });
513                }
514                Err(RecvTimeoutError::Disconnected) => None,
515            },
516            None => self.rx.recv().ok(),
517        };
518        match got {
519            Some(Ok(Frame::Json(v))) => match serde_json::from_value::<Response>(v.clone()) {
520                Ok(r) => Ok(Incoming::Json(r)),
521                Err(e) => Err(HostError::Malformed {
522                    plugin: self.label.clone(),
523                    message: format!("unknown message {v}: {e}"),
524                }),
525            },
526            Some(Ok(Frame::Arrow(b))) => Ok(Incoming::Arrow(b)),
527            Some(Err(FrameError::Malformed(m))) => Err(HostError::Malformed {
528                plugin: self.label.clone(),
529                message: m,
530            }),
531            Some(Err(_)) | None => Err(self.crashed()),
532        }
533    }
534
535    /// Receive a JSON response, turning `error` into `HostError::Plugin`.
536    pub fn recv_json(&mut self, waiting_for: &'static str) -> Result<Response> {
537        match self.recv(None, waiting_for)? {
538            Incoming::Json(Response::Error { message }) => Err(HostError::Plugin {
539                plugin: self.label.clone(),
540                message,
541            }),
542            Incoming::Json(r) => Ok(r),
543            other => Err(self.unexpected(waiting_for, &other)),
544        }
545    }
546
547    /// Send a request and expect `ok`.
548    fn call_ok(&mut self, req: &Request, what: &'static str) -> Result<()> {
549        self.send(req)?;
550        match self.recv_json(what)? {
551            Response::Ok {} => Ok(()),
552            other => Err(self.unexpected(what, &Incoming::Json(other))),
553        }
554    }
555
556    pub fn describe(&mut self) -> Result<Vec<ConnectionField>> {
557        Ok(self.describe_all()?.0)
558    }
559
560    /// The connection fields and the options the plugin declares.
561    pub fn describe_all(&mut self) -> Result<(Vec<ConnectionField>, Vec<OptionField>)> {
562        let d = self.description()?;
563        Ok((d.connection_fields, d.option_fields))
564    }
565
566    /// The whole `describe` reply.
567    pub fn description(&mut self) -> Result<Description> {
568        self.send(&Request::Describe {})?;
569        match self.recv_json("a describe reply")? {
570            Response::Describe {
571                connection_fields,
572                option_fields,
573                identifier_quote,
574                message_limit,
575            } => Ok(Description {
576                connection_fields,
577                option_fields,
578                identifier_quote,
579                message_limit,
580            }),
581            other => Err(self.unexpected("a describe reply", &Incoming::Json(other))),
582        }
583    }
584
585    /// Check a config block of options (needs `validate`); returns every problem found.
586    pub fn validate(&mut self, options: Map<String, Value>) -> Result<Vec<String>> {
587        self.send(&Request::Validate { options })?;
588        match self.recv_json("a validate reply")? {
589            Response::Validated { errors } => Ok(errors),
590            other => Err(self.unexpected("a validate reply", &Incoming::Json(other))),
591        }
592    }
593
594    pub fn open(&mut self, connection: Map<String, Value>, read_only: bool) -> Result<()> {
595        self.call_ok(
596            &Request::Open {
597                connection,
598                read_only,
599            },
600            "an open reply",
601        )
602    }
603
604    pub fn check(&mut self, sql: &str) -> Result<()> {
605        self.call_ok(&Request::Check { sql: sql.to_string() }, "a check reply")
606    }
607
608    /// Run one statement, handing every batch to `on_batch` as it arrives.
609    pub fn execute(
610        &mut self,
611        sql: &str,
612        row_limit: Option<u64>,
613        mut on_batch: impl FnMut(&SchemaRef, RecordBatch) -> std::result::Result<(), String>,
614    ) -> Result<Execution> {
615        self.send(&Request::Execute {
616            sql: sql.to_string(),
617            row_limit,
618        })?;
619        match self.recv_json("an execute reply")? {
620            Response::NoResult { rows_affected } => return Ok(Execution::NoResult { rows_affected }),
621            Response::Result { .. } => {}
622            other => return Err(self.unexpected("an execute reply", &Incoming::Json(other))),
623        }
624        let mut schema: Option<SchemaRef> = None;
625        let mut rows = 0u64;
626        let mut sink_error = None;
627        loop {
628            match self.recv(None, "result data")? {
629                Incoming::Arrow(ipc) => {
630                    let (s, batches) = frame::decode_batches(&ipc).map_err(|e| HostError::Arrow {
631                        plugin: self.label.clone(),
632                        message: e.to_string(),
633                    })?;
634                    let schema = schema.get_or_insert(s);
635                    for b in batches {
636                        rows += b.num_rows() as u64;
637                        if sink_error.is_none()
638                            && let Err(e) = on_batch(schema, b)
639                        {
640                            // Keep draining so the session stays usable; report after.
641                            sink_error = Some(e);
642                        }
643                    }
644                }
645                Incoming::Json(Response::ResultEnd { .. }) => break,
646                Incoming::Json(Response::Error { message }) => {
647                    return Err(HostError::Plugin {
648                        plugin: self.label.clone(),
649                        message,
650                    });
651                }
652                other => return Err(self.unexpected("result data", &other)),
653            }
654        }
655        if let Some(e) = sink_error {
656            return Err(HostError::Plugin {
657                plugin: self.label.clone(),
658                message: e,
659            });
660        }
661        let schema = schema.ok_or_else(|| HostError::Malformed {
662            plugin: self.label.clone(),
663            message: "a result ended without any Arrow frame carrying its schema".into(),
664        })?;
665        Ok(Execution::Result { schema, rows })
666    }
667
668    /// Start a format `write`; follow with `write_result_set` per result set, then `write_finish`.
669    pub fn write_begin(
670        &mut self,
671        path: &str,
672        format: &str,
673        options: Map<String, Value>,
674        result_sets: Vec<ResultSetMeta>,
675        template: Option<Value>,
676    ) -> Result<()> {
677        self.send(&Request::Write {
678            path: path.to_string(),
679            format: format.to_string(),
680            options,
681            result_sets,
682            template,
683        })
684    }
685
686    /// Stream one result set: at least one batch (carrying the schema), then the end marker.
687    pub fn write_result_set(
688        &mut self,
689        schema: &SchemaRef,
690        batches: impl IntoIterator<Item = RecordBatch>,
691    ) -> Result<()> {
692        let mut any = false;
693        for b in batches {
694            any = true;
695            self.send_batch(&b)?;
696        }
697        if !any {
698            self.send_batch(&RecordBatch::new_empty(schema.clone()))?;
699        }
700        self.send(&Request::ResultSetEnd {})
701    }
702
703    /// Load rows into a temporary table on the source's session. Returns the relation to use in
704    /// SQL, and the plugin's warning, if any. Only for plugins advertising `load`.
705    pub fn load(
706        &mut self,
707        name: &str,
708        schema: &SchemaRef,
709        batches: impl IntoIterator<Item = RecordBatch>,
710    ) -> Result<(String, Option<String>)> {
711        self.send(&Request::Load {
712            name: name.to_string(),
713        })?;
714        self.write_result_set(schema, batches)?;
715        match self.recv_json("a loaded reply")? {
716            Response::Loaded {
717                relation, warning, ..
718            } => Ok((relation, warning)),
719            other => Err(self.unexpected("a loaded reply", &Incoming::Json(other))),
720        }
721    }
722
723    pub fn write_finish(&mut self) -> Result<Vec<String>> {
724        Ok(self.write_finish_with_warnings()?.0)
725    }
726
727    /// `write_finish`, also returning the format's warnings.
728    pub fn write_finish_with_warnings(&mut self) -> Result<(Vec<String>, Vec<String>)> {
729        self.send(&Request::Finish {})?;
730        match self.recv_json("a written reply")? {
731            Response::Written { files, warnings } => Ok((files, warnings)),
732            other => Err(self.unexpected("a written reply", &Incoming::Json(other))),
733        }
734    }
735
736    /// Deliver one file with no plugin options.
737    pub fn deliver(
738        &mut self,
739        local_path: &str,
740        remote_path: Option<&str>,
741        connection: Map<String, Value>,
742    ) -> Result<String> {
743        let file = DeliveryFile {
744            local_path: local_path.to_string(),
745            remote_path: remote_path.map(str::to_string),
746        };
747        self.deliver_files(&[file], connection, Map::new())
748    }
749
750    /// Deliver `files` in one request with the destination entry's `options`. More than one file
751    /// needs a plugin advertising `multi_file`; call once per file otherwise.
752    pub fn deliver_files(
753        &mut self,
754        files: &[DeliveryFile],
755        connection: Map<String, Value>,
756        options: Map<String, Value>,
757    ) -> Result<String> {
758        let req = match files {
759            [] => {
760                return Err(HostError::Malformed {
761                    plugin: self.label.clone(),
762                    message: "nothing to deliver".into(),
763                });
764            }
765            [one] => Request::Deliver {
766                local_path: Some(one.local_path.clone()),
767                remote_path: one.remote_path.clone(),
768                files: Vec::new(),
769                connection,
770                options,
771                message: None,
772            },
773            many => {
774                if !self.has(crate::CAP_MULTI_FILE) {
775                    return Err(HostError::Plugin {
776                        plugin: self.label.clone(),
777                        message: format!(
778                            "{} files in one delivery, but the plugin doesn't advertise `multi_file`",
779                            many.len()
780                        ),
781                    });
782                }
783                Request::Deliver {
784                    local_path: None,
785                    remote_path: None,
786                    files: many.to_vec(),
787                    connection,
788                    options,
789                    message: None,
790                }
791            }
792        };
793        self.send(&req)?;
794        match self.recv_json("a delivered reply")? {
795            Response::Delivered { location } => Ok(location),
796            other => Err(self.unexpected("a delivered reply", &Incoming::Json(other))),
797        }
798    }
799
800    /// Deliver a message, with `attach` files, to a plugin advertising `message`.
801    pub fn deliver_message(
802        &mut self,
803        message: &crate::msg::Message,
804        attach: &[DeliveryFile],
805        connection: Map<String, Value>,
806        options: Map<String, Value>,
807    ) -> Result<String> {
808        if !self.has(crate::CAP_MESSAGE) {
809            return Err(HostError::Plugin {
810                plugin: self.label.clone(),
811                message: "a message, but the plugin doesn't advertise `message`".into(),
812            });
813        }
814        self.send(&Request::Deliver {
815            local_path: None,
816            remote_path: None,
817            files: attach.to_vec(),
818            connection,
819            options,
820            message: Some(message.clone()),
821        })?;
822        match self.recv_json("a delivered reply")? {
823            Response::Delivered { location } => Ok(location),
824            other => Err(self.unexpected("a delivered reply", &Incoming::Json(other))),
825        }
826    }
827
828    /// Ask the plugin to exit, and wait for it.
829    pub fn close(mut self) -> Result<()> {
830        let _ = self.send(&Request::Close {});
831        let _ = self.recv(Some(Duration::from_secs(5)), "the close reply");
832        self.stdin.take();
833        for _ in 0..250 {
834            if let Ok(Some(_)) = self.child.try_wait() {
835                return Ok(());
836            }
837            std::thread::sleep(Duration::from_millis(20));
838        }
839        let _ = self.child.kill();
840        Ok(())
841    }
842
843    /// Raw access for protocol tests: write arbitrary bytes to the plugin.
844    pub fn write_raw(&mut self, bytes: &[u8]) -> Result<()> {
845        let ok = match self.stdin.as_mut() {
846            Some(w) => w.write_all(bytes).and_then(|_| w.flush()).is_ok(),
847            None => false,
848        };
849        if ok { Ok(()) } else { Err(self.write_failed()) }
850    }
851
852    /// Wait up to `timeout` for the process to exit.
853    pub fn wait_exit(&mut self, timeout: Duration) -> Option<ExitStatus> {
854        self.stdin.take();
855        let deadline = std::time::Instant::now() + timeout;
856        while std::time::Instant::now() < deadline {
857            if let Ok(Some(s)) = self.child.try_wait() {
858                return Some(s);
859            }
860            std::thread::sleep(Duration::from_millis(20));
861        }
862        None
863    }
864}
865
866impl PluginProcess {
867    /// Wait (briefly) for the stderr forwarder to reach end of stream.
868    fn drain_stderr(&mut self) {
869        if let Some(t) = self.stderr_thread.take() {
870            let deadline = std::time::Instant::now() + Duration::from_secs(2);
871            while !t.is_finished() && std::time::Instant::now() < deadline {
872                std::thread::sleep(Duration::from_millis(5));
873            }
874            if t.is_finished() {
875                let _ = t.join();
876            }
877        }
878    }
879}
880
881impl Drop for PluginProcess {
882    fn drop(&mut self) {
883        self.stdin.take();
884        if let Ok(None) = self.child.try_wait() {
885            // Give it a moment to exit on its own (stdin is closed), then insist.
886            for _ in 0..25 {
887                if let Ok(Some(_)) = self.child.try_wait() {
888                    break;
889                }
890                std::thread::sleep(Duration::from_millis(10));
891            }
892            let _ = self.child.kill();
893            let _ = self.child.wait();
894        }
895        self.drain_stderr();
896    }
897}
898
899static CORE_VERSION: OnceLock<String> = OnceLock::new();
900
901/// Set the DRE version sent to plugins in the handshake (`core_version`). This crate is versioned
902/// on its own, so the host names the version of DRE it's part of; unset, it's "unreleased".
903pub fn set_core_version(version: &str) {
904    let _ = CORE_VERSION.set(version.to_string());
905}
906
907fn core_version() -> String {
908    CORE_VERSION.get().cloned().unwrap_or_else(|| "unreleased".into())
909}