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