1use std::collections::BTreeMap;
9use std::io::{BufRead, BufReader, Read, Write};
10use std::process::{Child, Command, Stdio};
11use std::sync::Arc;
12use std::sync::atomic::{AtomicBool, Ordering};
13use std::sync::mpsc::{Receiver, Sender, channel};
14use std::thread::JoinHandle;
15use std::time::{Duration, Instant};
16
17use serde_json::{Map, Value};
18
19use crate::PROTOCOL_VERSION;
20use crate::error::{Error, Result};
21use crate::registry::{AdapterRow, SourceRow, render_config_flags};
22use crate::source::{SourceCapabilities, SourceEntry, SourceItem, SyncSource};
23
24pub struct ExternalSource {
26 command: String,
27 caps: SourceCapabilities,
28 stdin: Option<Box<dyn Write + Send>>,
29 responses: Receiver<String>,
30 batches: Option<Receiver<Vec<String>>>,
31 watching: Arc<AtomicBool>,
34 next_id: u64,
35 child: Option<Child>,
36 reader: Option<JoinHandle<()>>,
37 pub sent: Vec<String>,
39 pub trace: bool,
41}
42
43impl std::fmt::Debug for ExternalSource {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 f.debug_struct("ExternalSource")
46 .field("command", &self.command)
47 .field("caps", &self.caps)
48 .finish_non_exhaustive()
49 }
50}
51
52fn route(
56 reader: Box<dyn Read + Send>,
57 responses: Sender<String>,
58 batches: Sender<Vec<String>>,
59 watching: Arc<AtomicBool>,
60) {
61 let buf = BufReader::new(reader);
62 for line in buf.lines() {
63 let Ok(line) = line else { break };
64 let trimmed = line.trim();
65 if trimmed.is_empty() {
66 continue;
67 }
68 if let Ok(Value::Object(obj)) = serde_json::from_str::<Value>(trimmed) {
69 if obj.get("event").and_then(Value::as_str) == Some("batch") {
70 let paths: Vec<String> = obj
71 .get("paths")
72 .and_then(Value::as_array)
73 .map(|a| {
74 a.iter()
75 .filter_map(Value::as_str)
76 .map(str::to_owned)
77 .collect()
78 })
79 .unwrap_or_default();
80 if watching.load(Ordering::SeqCst) {
81 let _ = batches.send(paths);
82 }
83 continue;
84 }
85 }
86 if responses.send(trimmed.to_owned()).is_err() {
89 break;
90 }
91 }
92}
93
94impl ExternalSource {
95 pub fn spawn(command: &str, args: &[String], env: &BTreeMap<String, String>) -> Result<Self> {
97 let mut cmd = Command::new(command);
98 cmd.args(args)
99 .stdin(Stdio::piped())
100 .stdout(Stdio::piped())
101 .stderr(Stdio::inherit());
102 for (k, v) in env {
103 cmd.env(k, v);
104 }
105 let mut child = cmd.spawn().map_err(|e| Error::AdapterSpawn {
106 command: command.to_owned(),
107 message: e.to_string(),
108 })?;
109 let stdin = child.stdin.take().ok_or_else(|| Error::AdapterSpawn {
110 command: command.to_owned(),
111 message: "no stdin pipe".to_owned(),
112 })?;
113 let stdout = child.stdout.take().ok_or_else(|| Error::AdapterSpawn {
114 command: command.to_owned(),
115 message: "no stdout pipe".to_owned(),
116 })?;
117 Self::connect_inner(command, Box::new(stdout), Box::new(stdin), Some(child))
118 }
119
120 pub fn spawn_source(source: &SourceRow, adapter: &AdapterRow) -> Result<Self> {
123 let mut args = adapter.args.clone();
124 args.extend(render_config_flags(&source.config));
125 Self::spawn(&adapter.command, &args, &source.env)
126 }
127
128 #[must_use]
130 pub fn argv(source: &SourceRow, adapter: &AdapterRow) -> Vec<String> {
131 let mut argv = vec![adapter.command.clone()];
132 argv.extend(adapter.args.iter().cloned());
133 argv.extend(render_config_flags(&source.config));
134 argv
135 }
136
137 pub fn connect(
141 label: &str,
142 from_adapter: impl Read + Send + 'static,
143 to_adapter: impl Write + Send + 'static,
144 ) -> Result<Self> {
145 Self::connect_inner(label, Box::new(from_adapter), Box::new(to_adapter), None)
146 }
147
148 fn connect_inner(
149 command: &str,
150 from_adapter: Box<dyn Read + Send>,
151 to_adapter: Box<dyn Write + Send>,
152 child: Option<Child>,
153 ) -> Result<Self> {
154 let (resp_tx, resp_rx) = channel();
155 let (batch_tx, batch_rx) = channel();
156 let watching = Arc::new(AtomicBool::new(false));
157 let flag = Arc::clone(&watching);
158 let reader = std::thread::spawn(move || route(from_adapter, resp_tx, batch_tx, flag));
159 let mut source = Self {
160 command: command.to_owned(),
161 caps: SourceCapabilities::default(),
162 stdin: Some(to_adapter),
163 responses: resp_rx,
164 batches: Some(batch_rx),
165 watching,
166 next_id: 1,
167 child,
168 reader: Some(reader),
169 sent: Vec::new(),
170 trace: false,
171 };
172 let line = match source.responses.recv() {
174 Ok(l) => l,
175 Err(_) => {
176 let err = Error::AdapterExited {
177 command: command.to_owned(),
178 };
179 let _ = source.close();
180 return Err(err);
181 }
182 };
183 let parsed: Option<Value> = serde_json::from_str(&line).ok();
184 let protocol_ok = parsed
185 .as_ref()
186 .and_then(|v| v.get("protocol"))
187 .and_then(Value::as_u64)
188 == Some(PROTOCOL_VERSION);
189 let Some(hs) = parsed.filter(|_| protocol_ok) else {
190 let err = Error::AdapterHandshake {
191 command: command.to_owned(),
192 line,
193 };
194 let _ = source.close();
195 return Err(err);
196 };
197 source.caps = SourceCapabilities::from_json(hs.get("capabilities"));
198 Ok(source)
199 }
200
201 #[must_use]
203 pub fn command(&self) -> &str {
204 &self.command
205 }
206
207 pub fn call(&mut self, method: &str, params: Value) -> Result<Value> {
211 let id = self.next_id;
212 self.next_id += 1;
213 let line = serde_json::json!({ "id": id, "method": method, "params": params }).to_string();
214 if self.trace {
215 self.sent.push(line.clone());
216 }
217 let stdin = self.stdin.as_mut().ok_or_else(|| Error::AdapterExited {
218 command: self.command.clone(),
219 })?;
220 stdin
221 .write_all(format!("{line}\n").as_bytes())
222 .and_then(|()| stdin.flush())
223 .map_err(|_| Error::AdapterExited {
224 command: self.command.clone(),
225 })?;
226 loop {
227 let raw = self.responses.recv().map_err(|_| Error::AdapterExited {
228 command: self.command.clone(),
229 })?;
230 let msg: Value = serde_json::from_str(&raw)?;
231 if msg.get("id").and_then(Value::as_u64) != Some(id) {
232 continue;
233 }
234 if let Some(err) = msg.get("error").filter(|e| !e.is_null()) {
235 let message = match err {
236 Value::String(s) if s.is_empty() => continue,
237 Value::String(s) => s.clone(),
238 Value::Bool(false) => continue,
239 other => other.to_string(),
240 };
241 return Err(Error::AdapterError {
242 method: method.to_owned(),
243 message,
244 });
245 }
246 return Ok(msg.get("result").cloned().unwrap_or(Value::Null));
247 }
248 }
249
250 pub fn alive(&mut self) -> bool {
252 match &mut self.child {
253 Some(c) => matches!(c.try_wait(), Ok(None)),
254 None => self.reader.as_ref().is_some_and(|r| !r.is_finished()),
255 }
256 }
257}
258
259impl SyncSource for ExternalSource {
260 fn capabilities(&self) -> SourceCapabilities {
261 self.caps
262 }
263
264 fn enumerate(&mut self) -> Result<Vec<SourceEntry>> {
265 let r = self.call("enumerate", Value::Object(Map::new()))?;
266 Ok(r.get("entries")
267 .and_then(Value::as_array)
268 .map(|a| a.iter().filter_map(SourceEntry::from_json).collect())
269 .unwrap_or_default())
270 }
271
272 fn fetch(&mut self, path: &str) -> Result<Option<SourceItem>> {
273 let r = self.call("fetch", serde_json::json!({ "path": path }))?;
274 Ok(SourceItem::from_json(r.get("item")))
275 }
276
277 fn write(&mut self, path: &str, content: &str) -> Result<()> {
278 if !self.caps.write_through {
279 return Err(Error::Unsupported("write".to_owned()));
280 }
281 self.call(
282 "write",
283 serde_json::json!({ "path": path, "content": content }),
284 )?;
285 Ok(())
286 }
287
288 fn remove(&mut self, path: &str) -> Result<()> {
289 if !self.caps.write_through {
290 return Err(Error::Unsupported("remove".to_owned()));
291 }
292 self.call("remove", serde_json::json!({ "path": path }))?;
293 Ok(())
294 }
295
296 fn watch(&mut self) -> Result<Receiver<Vec<String>>> {
297 if !self.caps.watch {
298 return Err(Error::Unsupported("watch".to_owned()));
299 }
300 let rx = self
301 .batches
302 .take()
303 .ok_or_else(|| Error::Other("the watch stream was already taken".to_owned()))?;
304 self.watching.store(true, Ordering::SeqCst);
306 if let Err(e) = self.call("watch", Value::Object(Map::new())) {
307 self.watching.store(false, Ordering::SeqCst);
308 self.batches = Some(rx);
309 return Err(e);
310 }
311 Ok(rx)
312 }
313
314 fn unwatch(&mut self) -> Result<()> {
317 self.watching.store(false, Ordering::SeqCst);
318 let _ = self.call("unwatch", Value::Object(Map::new()));
319 Ok(())
320 }
321
322 fn close(&mut self) -> Result<()> {
325 self.stdin = None;
326 if let Some(mut child) = self.child.take() {
327 let deadline = Instant::now() + Duration::from_millis(500);
328 let mut exited = false;
329 while Instant::now() < deadline {
330 if matches!(child.try_wait(), Ok(Some(_))) {
331 exited = true;
332 break;
333 }
334 std::thread::sleep(Duration::from_millis(10));
335 }
336 if !exited {
337 crate::lock::send_sigterm(i64::from(child.id()));
338 let deadline = Instant::now() + Duration::from_millis(500);
339 while Instant::now() < deadline {
340 if matches!(child.try_wait(), Ok(Some(_))) {
341 exited = true;
342 break;
343 }
344 std::thread::sleep(Duration::from_millis(10));
345 }
346 }
347 if !exited {
348 let _ = child.kill();
349 let _ = child.wait();
350 }
351 }
352 if let Some(reader) = self.reader.take() {
353 let _ = reader.join();
354 }
355 Ok(())
356 }
357}
358
359impl Drop for ExternalSource {
360 fn drop(&mut self) {
361 if self.child.is_some() || self.stdin.is_some() {
362 let _ = self.close();
363 }
364 }
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use crate::source::SourceIdentity;
371 use std::io::Cursor;
372 use std::sync::{Arc, Mutex};
373
374 #[derive(Clone, Default)]
376 struct Sink(Arc<Mutex<Vec<u8>>>);
377
378 impl Write for Sink {
379 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
380 self.0.lock().unwrap().extend_from_slice(buf);
381 Ok(buf.len())
382 }
383 fn flush(&mut self) -> std::io::Result<()> {
384 Ok(())
385 }
386 }
387
388 fn lines(sink: &Sink) -> Vec<String> {
389 String::from_utf8(sink.0.lock().unwrap().clone())
390 .unwrap()
391 .lines()
392 .map(str::to_owned)
393 .collect()
394 }
395
396 fn scripted(adapter_lines: &str) -> (ExternalSource, Sink) {
397 let sink = Sink::default();
398 let src =
399 ExternalSource::connect("fake", Cursor::new(adapter_lines.to_owned()), sink.clone())
400 .unwrap();
401 (src, sink)
402 }
403
404 #[test]
405 fn handshake_and_calls() {
406 use crate::pipe::{Dir, ScriptedAdapter};
407 let t = |d: Dir, l: &str| (d, l.to_owned());
408 let transcript = vec![
409 t(
410 Dir::In,
411 r#"{"protocol":1,"capabilities":{"identity":"inferred","writeThrough":true,"watch":true}}"#,
412 ),
413 t(Dir::In, ""),
414 t(Dir::In, r#"{"event":"batch","paths":["early.md"]}"#),
415 t(Dir::Out, r#"{"id":1,"method":"enumerate","params":{}}"#),
416 t(
417 Dir::In,
418 r#"{"id":1,"result":{"entries":[{"path":"a.md","revision":"1:2"}]}}"#,
419 ),
420 t(
421 Dir::Out,
422 r#"{"id":2,"method":"fetch","params":{"path":"a.md"}}"#,
423 ),
424 t(Dir::In, r#"{"id":99,"result":{}}"#),
425 t(
426 Dir::In,
427 r##"{"id":2,"result":{"item":{"path":"a.md","revision":"1:2","content":"# A\n"}}}"##,
428 ),
429 t(
430 Dir::Out,
431 r#"{"id":3,"method":"fetch","params":{"path":"gone.md"}}"#,
432 ),
433 t(Dir::In, r#"{"id":3,"result":{"item":null}}"#),
434 t(
435 Dir::Out,
436 r#"{"id":4,"method":"write","params":{"path":"b.md","content":"x\n"}}"#,
437 ),
438 t(Dir::In, r#"{"id":4,"result":{"ok":true}}"#),
439 t(
440 Dir::Out,
441 r#"{"id":5,"method":"remove","params":{"path":"b.md"}}"#,
442 ),
443 t(Dir::In, r#"{"id":5,"error":"nope"}"#),
444 t(Dir::Out, r#"{"id":6,"method":"watch","params":{}}"#),
445 t(Dir::In, r#"{"id":6,"result":{"ok":true}}"#),
446 t(Dir::In, r#"{"event":"batch","paths":["a.md","b.md"]}"#),
447 t(Dir::Out, r#"{"id":7,"method":"unwatch","params":{}}"#),
448 t(Dir::In, r#"{"id":7,"result":{"ok":true}}"#),
449 ];
450 let expected_out: Vec<String> = transcript
451 .iter()
452 .filter(|(d, _)| *d == Dir::Out)
453 .map(|(_, l)| l.clone())
454 .collect();
455 let (adapter, from_adapter, to_adapter) = ScriptedAdapter::spawn(transcript);
456 let mut src = ExternalSource::connect("fake", from_adapter, to_adapter).unwrap();
457 src.trace = true;
458 assert_eq!(
459 src.capabilities(),
460 SourceCapabilities {
461 identity: SourceIdentity::Inferred,
462 write_through: true,
463 watch: true
464 }
465 );
466 let entries = src.enumerate().unwrap();
467 assert_eq!(entries.len(), 1);
468 assert_eq!(entries[0].revision, "1:2");
469 let item = src.fetch("a.md").unwrap().unwrap();
470 assert_eq!(
471 item.content,
472 "# A
473"
474 );
475 assert!(
476 src.fetch("gone.md").unwrap().is_none(),
477 "a stray response (id 99) was skipped"
478 );
479 src.write(
480 "b.md", "x
481",
482 )
483 .unwrap();
484 let err = src.remove("b.md").unwrap_err();
485 assert!(
486 matches!(err, Error::AdapterError { ref method, ref message } if method == "remove" && message == "nope"),
487 "{err}"
488 );
489 let rx = src.watch().unwrap();
490 assert_eq!(
491 rx.recv().unwrap(),
492 ["a.md", "b.md"],
493 "the pre-watch event was dropped"
494 );
495 src.unwatch().unwrap();
496 assert!(rx.try_recv().is_err());
497 assert_eq!(src.sent, expected_out);
498 assert!(src.alive());
499 src.close().unwrap();
500 assert!(!src.alive());
501 assert_eq!(adapter.received(), expected_out);
502 }
503
504 #[test]
505 fn eof_after_the_script_reports_the_adapter_gone() {
506 let (mut src, sink) = scripted("{\"protocol\":1,\"capabilities\":{}}\n");
507 assert!(matches!(src.fetch("x"), Err(Error::AdapterExited { .. })));
508 assert_eq!(
509 lines(&sink),
510 [r#"{"id":1,"method":"fetch","params":{"path":"x"}}"#]
511 );
512 src.close().unwrap();
513 }
514
515 #[test]
516 fn bad_handshakes_fail() {
517 for bad in [
518 "",
519 "not json\n",
520 "{\"protocol\":2,\"capabilities\":{}}\n",
521 "{\"capabilities\":{}}\n",
522 ] {
523 let sink = Sink::default();
524 let err =
525 ExternalSource::connect("fake", Cursor::new(bad.to_owned()), sink).expect_err(bad);
526 if bad.is_empty() {
527 assert!(
528 matches!(err, Error::AdapterExited { .. }),
529 "{bad:?} → {err}"
530 );
531 } else {
532 assert!(
533 matches!(err, Error::AdapterHandshake { .. }),
534 "{bad:?} → {err}"
535 );
536 }
537 }
538 }
539
540 #[test]
541 fn read_only_sources_refuse_writes_and_watch() {
542 let (mut src, _sink) = scripted("{\"protocol\":1,\"capabilities\":{}}\n");
543 assert!(matches!(src.write("a", "b"), Err(Error::Unsupported(_))));
544 assert!(matches!(src.remove("a"), Err(Error::Unsupported(_))));
545 assert!(matches!(src.watch(), Err(Error::Unsupported(_))));
546 }
547
548 #[test]
549 fn spawns_a_real_process_and_closes_it() {
550 let err = ExternalSource::spawn("definitely-not-a-command-xyz", &[], &BTreeMap::new())
552 .err()
553 .unwrap();
554 assert!(matches!(err, Error::AdapterSpawn { .. }), "{err}");
555 let script = "printf '%s\\n' '{\"protocol\":1,\"capabilities\":{\"watch\":true}}'; while IFS= read -r line; do case \"$line\" in *enumerate*) echo '{\"id\":1,\"result\":{\"entries\":[]}}';; *) echo \"{\\\"id\\\":${line#*\\\"id\\\":}\" | sed 's/,.*//;s/$/,\"result\":{}}/';; esac; done";
556 let env: BTreeMap<String, String> = [("OMGBASE_TEST_ENV".to_owned(), "1".to_owned())]
557 .into_iter()
558 .collect();
559 let mut src =
560 ExternalSource::spawn("sh", &["-c".to_owned(), script.to_owned()], &env).unwrap();
561 assert!(src.capabilities().watch);
562 assert!(src.enumerate().unwrap().is_empty());
563 assert!(src.alive());
564 src.close().unwrap();
565 assert!(!src.alive());
566 }
567
568 #[test]
569 fn argv_is_command_args_then_flags() {
570 let adapter = AdapterRow {
571 name: "fs".into(),
572 command: "omgbase-fs-adapter".into(),
573 args: vec!["--v".into()],
574 };
575 let source = SourceRow {
576 source_id: "src_0".into(),
577 name: "x-fs".into(),
578 adapter: "fs".into(),
579 config: serde_json::json!({"root": "/r", "debounce": 750})
580 .as_object()
581 .cloned()
582 .unwrap(),
583 env: BTreeMap::new(),
584 };
585 assert_eq!(
586 ExternalSource::argv(&source, &adapter),
587 [
588 "omgbase-fs-adapter",
589 "--v",
590 "--root",
591 "/r",
592 "--debounce",
593 "750"
594 ]
595 );
596 }
597}