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