1use std::collections::{BTreeMap, BTreeSet, VecDeque};
18use std::ffi::OsString;
19use std::io::{self, BufRead, BufReader, Read, Write};
20use std::panic::{AssertUnwindSafe, catch_unwind};
21use std::path::{Path, PathBuf};
22use std::process::{Child, ChildStdin, Command, Stdio};
23use std::sync::atomic::{AtomicBool, Ordering};
24use std::sync::mpsc::{Receiver, RecvTimeoutError, Sender, SyncSender, TrySendError, channel, sync_channel};
25use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
26use std::thread;
27use std::time::{Duration, Instant};
28
29use byob_tools::{Access, FileChange, Tool, ToolContext, ToolOutput, ToolSpec};
30use serde_json::{Value, json};
31
32use crate::PluginRef;
33use crate::manifest::{ManifestTool, path_inside};
34use crate::native::{NativeBackend, Outbox, dispatch, panic_text};
35
36pub const WIRE_VERSION: u32 = 1;
38pub const MAX_LINE: usize = 1024 * 1024;
40pub const STDERR_KEEP: usize = 16 * 1024;
42pub const MAX_FAULTS: usize = 8;
44const MAX_FAULT: usize = 2000;
46const MAX_QUEUED: usize = 256;
49const MAX_QUEUED_BYTES: usize = 16 * 1024 * 1024;
51const FAULT_EVERY: Duration = Duration::from_secs(1);
53const WAIT_POLL: Duration = Duration::from_millis(20);
55const DRAIN_GRACE: Duration = Duration::from_millis(500);
57pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
59pub const MAX_CRASHES: u32 = 5;
61const FIRST_BACKOFF: Duration = Duration::from_millis(500);
62const MAX_BACKOFF: Duration = Duration::from_secs(30);
63const STABLE: Duration = Duration::from_secs(30);
65const STOP_GRACE: Duration = Duration::from_millis(500);
67pub const CAPS: [&str; 9] = ["event", "request", "reply", "view", "notify", "notice", "log", "fault", "web"];
69
70#[derive(Debug, Clone, Default)]
72pub struct Launch {
73 pub argv: Vec<OsString>,
75 pub cwd: PathBuf,
76 pub env: BTreeMap<String, String>,
78 pub remove_env: Vec<String>,
80}
81
82pub enum Start {
84 Process(Launch),
85 Native(Box<dyn NativeBackend>),
86}
87
88impl From<Launch> for Start {
89 fn from(l: Launch) -> Self {
90 Self::Process(l)
91 }
92}
93
94pub type Prepare = Box<dyn Fn(&dyn Fn() -> bool) -> Result<Start, String> + Send + Sync>;
97
98#[derive(Debug, Clone, PartialEq)]
100pub enum Out {
101 Msg(Value),
103 Started,
104 Exited(String),
106}
107
108pub type Sink = Arc<dyn Fn(Out) + Send + Sync>;
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum Phase {
112 Idle,
114 Starting,
115 Running,
116 Crashed,
118 Failed,
120 Stopped,
121}
122
123impl Phase {
124 pub fn label(self) -> &'static str {
125 match self {
126 Self::Idle => "idle",
127 Self::Starting => "starting",
128 Self::Running => "running",
129 Self::Crashed => "crashed",
130 Self::Failed => "failed",
131 Self::Stopped => "stopped",
132 }
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct Status {
139 pub phase: Phase,
140 pub pid: Option<u32>,
141 pub starts: u32,
142 pub crashes: u32,
144 pub stderr: String,
146 pub faults: Vec<String>,
147 pub error: Option<String>,
149 pub subscribed: Vec<String>,
150}
151
152type Reply = Result<Value, String>;
153
154struct Inner {
155 phase: Phase,
156 generation: u64,
158 writer: Option<SyncSender<Vec<u8>>>,
159 queued: VecDeque<Vec<u8>>,
160 queued_bytes: usize,
161 child: Option<Child>,
162 pid: Option<u32>,
163 pending: BTreeMap<u64, Sender<Reply>>,
164 next_id: u64,
165 stderr: Ring,
166 faults: VecDeque<String>,
167 subscribed: BTreeSet<String>,
168 starts: u32,
169 crashes: u32,
170 started_at: Option<Instant>,
171 error: Option<String>,
172 fault_posted: Option<Instant>,
174 fault_held: u32,
176 fault_flush: bool,
178}
179
180struct Shared {
181 plugin: String,
182 init: Value,
183 prepare: Prepare,
184 sink: Sink,
185 inner: Mutex<Inner>,
186}
187
188impl Shared {
189 fn lock(&self) -> MutexGuard<'_, Inner> {
190 self.inner.lock().unwrap_or_else(PoisonError::into_inner)
191 }
192}
193
194pub struct Backend {
196 shared: Arc<Shared>,
197}
198
199impl Backend {
200 pub fn new(plugin: &str, init: Value, events: &[String], prepare: Prepare, sink: Sink) -> Self {
203 let inner = Inner {
204 phase: Phase::Idle,
205 generation: 0,
206 writer: None,
207 queued: VecDeque::new(),
208 queued_bytes: 0,
209 child: None,
210 pid: None,
211 pending: BTreeMap::new(),
212 next_id: 1,
213 stderr: Ring::default(),
214 faults: VecDeque::new(),
215 subscribed: events.iter().cloned().collect(),
216 starts: 0,
217 crashes: 0,
218 started_at: None,
219 error: None,
220 fault_posted: None,
221 fault_held: 0,
222 fault_flush: false,
223 };
224 Self { shared: Arc::new(Shared { plugin: plugin.to_owned(), init, prepare, sink, inner: Mutex::new(inner) }) }
225 }
226
227 pub fn plugin(&self) -> &str {
228 &self.shared.plugin
229 }
230
231 pub fn wants(&self, event: &str) -> bool {
233 self.shared.lock().subscribed.contains(event)
234 }
235
236 pub fn status(&self) -> Status {
237 let g = self.shared.lock();
238 Status {
239 phase: g.phase,
240 pid: g.pid,
241 starts: g.starts,
242 crashes: g.crashes,
243 stderr: g.stderr.text(),
244 faults: g.faults.iter().cloned().collect(),
245 error: g.error.clone(),
246 subscribed: g.subscribed.iter().cloned().collect(),
247 }
248 }
249
250 pub fn start(&self) {
252 let mut g = self.shared.lock();
253 if matches!(g.phase, Phase::Idle | Phase::Crashed) {
254 begin_start(&self.shared, &mut g);
255 }
256 }
257
258 pub fn event(&self, msg: Value) -> bool {
261 let Ok(line) = serde_json::to_vec(&msg) else { return false };
262 if line.len() > MAX_LINE {
263 log::warn!("plugin {}: an event over {MAX_LINE} bytes wasn't sent", self.shared.plugin);
264 return false;
265 }
266 let mut g = self.shared.lock();
267 if matches!(g.phase, Phase::Idle | Phase::Crashed) {
268 begin_start(&self.shared, &mut g);
269 }
270 push_line(&self.shared, &mut g, line).is_ok()
271 }
272
273 pub fn request(&self, method: &str, params: Value, timeout: Duration, cancelled: &dyn Fn() -> bool) -> Result<Value, String> {
275 let (tx, rx) = channel();
276 let id = {
277 let mut g = self.shared.lock();
278 match g.phase {
279 Phase::Stopped => return Err("the backend was stopped (its plugin changed, or was switched off or removed); try again".into()),
280 Phase::Failed => return Err(g.error.clone().unwrap_or_else(|| "the backend couldn't start".into())),
281 Phase::Idle | Phase::Crashed => begin_start(&self.shared, &mut g),
282 Phase::Starting | Phase::Running => {}
283 }
284 let id = g.next_id;
285 g.next_id = g.next_id.wrapping_add(1);
286 let line = serde_json::to_vec(&json!({"type": "request", "id": id, "method": method, "params": params})).map_err(|e| e.to_string())?;
287 if line.len() > MAX_LINE {
288 return Err(format!("the request is over {MAX_LINE} bytes"));
289 }
290 g.pending.insert(id, tx);
291 if let Err(why) = push_line(&self.shared, &mut g, line) {
292 g.pending.remove(&id);
293 return Err(why.into());
294 }
295 id
296 };
297 let started = Instant::now();
298 loop {
299 match rx.recv_timeout(Duration::from_millis(20)) {
300 Ok(r) => return r,
301 Err(RecvTimeoutError::Disconnected) => return Err("the backend went away".into()),
302 Err(RecvTimeoutError::Timeout) => {}
303 }
304 let gave_up = if cancelled() {
305 Some("Stopped by the user.".to_owned())
306 } else if started.elapsed() >= timeout {
307 Some(if timeout < Duration::from_secs(1) {
308 format!("the backend didn't answer within {} ms", timeout.as_millis())
309 } else {
310 format!("the backend didn't answer within {} seconds", timeout.as_secs())
311 })
312 } else {
313 None
314 };
315 if let Some(why) = gave_up {
316 let mut g = self.shared.lock();
317 g.pending.remove(&id);
318 if let Ok(line) = serde_json::to_vec(&json!({"type": "cancel", "id": id})) {
319 let _ = push_line(&self.shared, &mut g, line);
320 }
321 return Err(why);
322 }
323 }
324 }
325
326 pub fn report_fault(&self, text: &str) {
329 fault(&self.shared, text.to_owned());
330 }
331
332 pub fn post(&self, msg: Value) {
335 (self.shared.sink)(Out::Msg(msg));
336 }
337
338 pub fn stop(&self, why: &str) {
340 stop(&self.shared, why);
341 }
342}
343
344impl Drop for Backend {
345 fn drop(&mut self) {
346 stop(&self.shared, "the backend was stopped");
347 }
348}
349
350fn stop(shared: &Arc<Shared>, why: &str) {
351 let mut g = shared.lock();
352 if g.phase == Phase::Stopped {
353 return;
354 }
355 let was = g.phase;
356 g.phase = Phase::Stopped;
357 if let Some(tx) = g.writer.take()
358 && let Ok(line) = serde_json::to_vec(&json!({"type": "shutdown"}))
359 {
360 let _ = tx.try_send(line);
361 }
362 take_queued(&mut g);
363 g.pid = None;
364 let child = g.child.take();
365 for (_, tx) in std::mem::take(&mut g.pending) {
366 let _ = tx.send(Err(why.to_owned()));
367 }
368 drop(g);
369 if let Some(child) = child {
370 log::info!("plugin {}: stopping its backend ({why})", shared.plugin);
371 guarded("byob-backend-stop", move || {
372 reap(child, STOP_GRACE);
373 });
374 } else if was != Phase::Idle {
375 log::info!("plugin {}: backend stopped ({why})", shared.plugin);
376 }
377}
378
379fn reap(mut child: Child, grace: Duration) -> String {
382 let pid = child.id();
383 let deadline = Instant::now() + grace;
384 let how = loop {
385 match child.try_wait() {
386 Ok(Some(status)) => break exit_text(status),
387 Ok(None) if Instant::now() < deadline => thread::sleep(Duration::from_millis(10)),
388 _ => {
389 byob_sandbox::kill_group(pid);
391 let _ = child.kill();
392 break child.wait().map_or_else(|e| e.to_string(), exit_text);
393 }
394 }
395 };
396 byob_sandbox::kill_group(pid);
397 how
398}
399
400fn exit_text(status: std::process::ExitStatus) -> String {
401 match status.code() {
402 Some(c) => format!("exit code {c}"),
403 None => "killed by a signal".into(),
404 }
405}
406
407fn push_line(shared: &Arc<Shared>, g: &mut Inner, line: Vec<u8>) -> Result<(), &'static str> {
411 const NOT_RUNNING: &str = "the backend isn't running";
412 match g.phase {
413 Phase::Running => match &g.writer {
414 Some(tx) => match tx.try_send(line) {
415 Ok(()) => Ok(()),
416 Err(TrySendError::Full(_)) => {
417 record_fault(shared, g, format!("it isn't reading its input: {MAX_QUEUED} messages wait, so one was dropped"), false);
418 Err("the backend isn't reading its input (too many messages wait); the message was dropped")
419 }
420 Err(TrySendError::Disconnected(_)) => Err(NOT_RUNNING),
421 },
422 None => Err(NOT_RUNNING),
423 },
424 Phase::Starting => {
425 let mut dropped = false;
426 while g.queued.len() >= MAX_QUEUED || (!g.queued.is_empty() && g.queued_bytes.saturating_add(line.len()) > MAX_QUEUED_BYTES) {
427 let Some(old) = g.queued.pop_front() else { break };
428 g.queued_bytes = g.queued_bytes.saturating_sub(old.len());
429 dropped = true;
430 }
431 if dropped {
432 log::warn!("plugin {}: its backend is slow to start; an old message was dropped", shared.plugin);
433 }
434 g.queued_bytes = g.queued_bytes.saturating_add(line.len());
435 g.queued.push_back(line);
436 Ok(())
437 }
438 Phase::Idle | Phase::Crashed | Phase::Failed | Phase::Stopped => Err(NOT_RUNNING),
439 }
440}
441
442fn take_queued(g: &mut Inner) -> VecDeque<Vec<u8>> {
444 g.queued_bytes = 0;
445 std::mem::take(&mut g.queued)
446}
447
448fn begin_start(shared: &Arc<Shared>, g: &mut Inner) {
449 g.phase = Phase::Starting;
450 let shared = Arc::clone(shared);
451 guarded("byob-backend-start", move || start_now(&shared));
452}
453
454enum Running {
456 Process(Spawned),
457 Native(Box<dyn NativeBackend>),
458}
459
460fn start_now(shared: &Arc<Shared>) {
461 let called_off = || shared.lock().phase != Phase::Starting;
462 let prepared = catch_unwind(AssertUnwindSafe(|| (shared.prepare)(&called_off)));
463 let spawned = prepared.unwrap_or_else(|_| Err("preparing the backend crashed".into())).and_then(|s| match s {
464 Start::Process(l) => spawn(&l).map(Running::Process),
465 Start::Native(n) => Ok(Running::Native(n)),
466 });
467 let mut g = shared.lock();
468 if g.phase != Phase::Starting {
469 if let Ok(Running::Process((child, ..))) = spawned {
471 drop(g);
472 reap(child, Duration::ZERO);
473 }
474 return;
475 }
476 let running = match spawned {
477 Ok(parts) => parts,
478 Err(e) => {
479 g.phase = Phase::Failed;
480 g.error = Some(e.clone());
481 take_queued(&mut g);
482 for (_, tx) in std::mem::take(&mut g.pending) {
483 let _ = tx.send(Err(e.clone()));
484 }
485 drop(g);
486 log::warn!("plugin {}: its backend couldn't start: {e}", shared.plugin);
487 (shared.sink)(Out::Exited(e));
488 return;
489 }
490 };
491 g.generation = g.generation.wrapping_add(1);
492 let generation = g.generation;
493 let (tx, rx) = sync_channel::<Vec<u8>>(MAX_QUEUED + 2);
495 let mut init = shared.init.clone();
496 if let Some(o) = init.as_object_mut() {
497 o.insert("restart".into(), json!(g.starts));
498 }
499 if let Ok(line) = serde_json::to_vec(&init) {
500 let _ = tx.try_send(line);
501 }
502 for line in take_queued(&mut g) {
503 let _ = tx.try_send(line);
504 }
505 g.writer = Some(tx);
506 g.phase = Phase::Running;
507 g.starts = g.starts.saturating_add(1);
508 g.started_at = Some(Instant::now());
509 g.error = None;
510 let (child, stdin, stdout, stderr) = match running {
511 Running::Process(parts) => parts,
512 Running::Native(backend) => {
513 drop(g);
514 log::info!("plugin {}: native backend started", shared.plugin);
515 let s = Arc::clone(shared);
516 guarded("byob-backend-native", move || {
517 let how = native_loop(&s, backend, &rx);
518 exited(&s, generation, &how);
519 });
520 (shared.sink)(Out::Started);
521 return;
522 }
523 };
524 let pid = child.id();
525 g.pid = Some(pid);
526 g.child = Some(child);
527 drop(g);
528 log::info!("plugin {}: backend started", shared.plugin);
529 let out_done = Arc::new(AtomicBool::new(false));
530 let err_done = Arc::new(AtomicBool::new(false));
531 guarded("byob-backend-in", move || write_loop(stdin, &rx));
532 let (s, done) = (Arc::clone(shared), Arc::clone(&err_done));
533 guarded("byob-backend-err", move || {
534 let _ = catch_unwind(AssertUnwindSafe(|| stderr_loop(stderr, &s, generation)));
535 done.store(true, Ordering::SeqCst);
536 });
537 let (s, done) = (Arc::clone(shared), Arc::clone(&out_done));
538 guarded("byob-backend-out", move || {
539 let read = catch_unwind(AssertUnwindSafe(|| read_loop(stdout, &s, generation)));
540 if read.is_err() {
541 log::error!("plugin {}: the backend reader crashed", s.plugin);
542 }
543 done.store(true, Ordering::SeqCst);
544 });
545 let s = Arc::clone(shared);
546 guarded("byob-backend-wait", move || wait_loop(&s, generation, pid, &out_done, &err_done));
547 (shared.sink)(Out::Started);
548}
549
550fn wait_loop(shared: &Arc<Shared>, generation: u64, pid: u32, out_done: &AtomicBool, err_done: &AtomicBool) {
554 let ended = loop {
555 thread::sleep(WAIT_POLL);
556 let mut g = shared.lock();
557 if g.generation != generation || g.phase != Phase::Running {
558 return; }
560 let Some(child) = g.child.as_mut() else { return };
561 match child.try_wait() {
562 Ok(Some(status)) => {
563 g.child = None;
564 break Ok(exit_text(status));
565 }
566 Ok(None) if !out_done.load(Ordering::SeqCst) => {}
567 Ok(None) | Err(_) => break Err(g.child.take()),
569 }
570 };
571 let how = match ended {
572 Ok(how) => {
573 byob_sandbox::kill_group(pid);
574 how
575 }
576 Err(Some(child)) => reap(child, Duration::from_secs(2)),
577 Err(None) => "it ended".to_owned(),
578 };
579 let deadline = Instant::now() + DRAIN_GRACE;
580 while !(out_done.load(Ordering::SeqCst) && err_done.load(Ordering::SeqCst)) && Instant::now() < deadline {
581 thread::sleep(Duration::from_millis(5));
582 }
583 exited(shared, generation, &how);
584}
585
586type Spawned = (Child, ChildStdin, std::process::ChildStdout, std::process::ChildStderr);
587
588fn spawn(l: &Launch) -> Result<Spawned, String> {
589 let Some((program, args)) = l.argv.split_first() else { return Err("no command to start".into()) };
590 let mut cmd = Command::new(program);
591 cmd.args(args).current_dir(&l.cwd).envs(&l.env).stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped());
592 #[cfg(unix)]
594 std::os::unix::process::CommandExt::process_group(&mut cmd, 0);
595 for k in &l.remove_env {
596 cmd.env_remove(k);
597 }
598 let mut child = cmd.spawn().map_err(|e| format!("couldn't start {}: {e}", Path::new(program).display()))?;
599 match (child.stdin.take(), child.stdout.take(), child.stderr.take()) {
600 (Some(i), Some(o), Some(e)) => Ok((child, i, o, e)),
601 _ => {
602 reap(child, Duration::ZERO);
603 Err("no pipes to the backend".into())
604 }
605 }
606}
607
608fn native_loop(shared: &Arc<Shared>, mut backend: Box<dyn NativeBackend>, rx: &Receiver<Vec<u8>>) -> String {
612 let s = Arc::clone(shared);
613 let out = Outbox::new(move |msg: Value| {
614 let size = serde_json::to_vec(&msg).map_or(0, |v| v.len());
615 if size > MAX_LINE {
616 fault(&s, format!("a message over {MAX_LINE} bytes was dropped"));
617 } else if msg.is_object() {
618 handle_msg(&s, msg);
619 }
620 });
621 for line in rx {
622 let Ok(msg) = serde_json::from_slice::<Value>(&line) else { continue };
623 match catch_unwind(AssertUnwindSafe(|| dispatch(backend.as_mut(), &msg, &out))) {
624 Ok(true) => {}
625 Ok(false) => return "it shut down".into(),
626 Err(p) => {
627 let text = panic_text(p.as_ref());
628 shared.lock().stderr.push(format!("(panic) {text}\n").as_bytes());
629 fault(shared, format!("the native backend panicked: {text}"));
630 return format!("it panicked: {text}");
631 }
632 }
633 }
634 "it ended".into()
635}
636
637fn write_loop(mut stdin: ChildStdin, rx: &Receiver<Vec<u8>>) {
638 for mut line in rx {
639 line.push(b'\n');
640 if stdin.write_all(&line).and_then(|()| stdin.flush()).is_err() {
641 return;
642 }
643 }
644}
645
646fn stderr_loop(mut stderr: impl Read, shared: &Shared, generation: u64) {
647 let mut buf = [0u8; 4096];
648 loop {
649 match stderr.read(&mut buf) {
650 Ok(0) => return,
651 Ok(n) => {
652 let mut g = shared.lock();
653 if g.generation != generation {
654 return; }
656 g.stderr.push(buf.get(..n).unwrap_or_default());
657 }
658 Err(e) if e.kind() == io::ErrorKind::Interrupted => {}
659 Err(_) => return,
660 }
661 }
662}
663
664fn read_loop(stdout: impl Read, shared: &Arc<Shared>, generation: u64) {
665 let mut r = BufReader::new(stdout);
666 let mut buf = Vec::new();
667 loop {
668 let line = read_line(&mut r, &mut buf, MAX_LINE);
669 if shared.lock().generation != generation {
670 return; }
672 match line {
673 Ok(LineRead::Line) => handle_line(shared, &buf),
674 Ok(LineRead::TooLong) => fault(shared, format!("a message over {MAX_LINE} bytes was dropped")),
675 Ok(LineRead::Eof) | Err(_) => return,
676 }
677 }
678}
679
680fn fault(shared: &Arc<Shared>, text: String) {
683 let post = record_fault(shared, &mut shared.lock(), text, true);
684 if let Some(text) = post {
685 post_fault(shared, &text);
686 }
687}
688
689fn record_fault(shared: &Arc<Shared>, g: &mut Inner, text: String, may_post: bool) -> Option<String> {
692 let text: String = text.chars().take(MAX_FAULT).collect();
693 if g.faults.len() >= MAX_FAULTS {
694 g.faults.pop_front();
695 }
696 g.faults.push_back(text.clone());
697 let wait = g.fault_posted.map_or(Duration::ZERO, |t| FAULT_EVERY.saturating_sub(t.elapsed()));
698 if may_post && wait.is_zero() && !g.fault_flush {
699 g.fault_posted = Some(Instant::now());
700 return Some(text);
701 }
702 g.fault_held = g.fault_held.saturating_add(1);
703 if !g.fault_flush {
704 g.fault_flush = true;
705 let s = Arc::clone(shared);
706 guarded("byob-backend-faults", move || {
707 thread::sleep(wait);
708 flush_faults(&s);
709 });
710 }
711 None
712}
713
714fn flush_faults(shared: &Shared) {
716 let text = {
717 let mut g = shared.lock();
718 g.fault_flush = false;
719 let held = std::mem::take(&mut g.fault_held);
720 if held == 0 {
721 return;
722 }
723 g.fault_posted = Some(Instant::now());
724 let last = g.faults.back().cloned().unwrap_or_default();
725 if held > 1 { format!("{last} ({} more)", held - 1) } else { last }
726 };
727 post_fault(shared, &text);
728}
729
730fn post_fault(shared: &Shared, text: &str) {
731 log::warn!("plugin {}: {text}", shared.plugin);
732 (shared.sink)(Out::Msg(json!({"type": "fault", "text": text})));
733}
734
735fn handle_line(shared: &Arc<Shared>, line: &[u8]) {
736 if line.iter().all(u8::is_ascii_whitespace) {
737 return;
738 }
739 let msg = match serde_json::from_slice::<Value>(line) {
740 Ok(v @ Value::Object(_)) => v,
741 _ => {
742 let text = String::from_utf8_lossy(line);
743 shared.lock().stderr.push(format!("(stdout) {text}\n").as_bytes());
744 let short: String = text.chars().take(200).collect();
745 fault(shared, format!("stdout is for protocol messages (JSON objects, one per line); this isn't one: {short}"));
746 return;
747 }
748 };
749 handle_msg(shared, msg);
750}
751
752fn handle_msg(shared: &Arc<Shared>, msg: Value) {
754 let kind = msg.get("type").and_then(Value::as_str).unwrap_or_default();
755 match kind {
756 "reply" => {
757 let Some(id) = msg.get("id").and_then(Value::as_u64) else {
758 fault(shared, "a reply without a numeric id".into());
759 return;
760 };
761 let tx = shared.lock().pending.remove(&id);
762 let Some(tx) = tx else { return };
763 let result = match msg.get("error").filter(|e| !e.is_null()) {
764 Some(Value::String(e)) => Err(e.clone()),
765 Some(e) => Err(e.to_string()),
766 None => Ok(msg.get("result").cloned().unwrap_or(Value::Null)),
767 };
768 let _ = tx.send(result);
769 }
770 "fault" => {
771 let text = msg.get("text").map(|t| t.as_str().map_or_else(|| t.to_string(), str::to_owned)).unwrap_or_default();
772 fault(shared, text);
773 }
774 "log" => {
775 let text = msg.get("text").and_then(Value::as_str).unwrap_or_default();
776 shared.lock().stderr.push(format!("(log) {text}\n").as_bytes());
777 log::info!("plugin {}: {}", shared.plugin, text.chars().take(500).collect::<String>());
778 }
779 "subscribe" | "unsubscribe" => {
780 let events: Vec<String> = msg.get("events").and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_str).map(str::to_owned).collect();
781 let unknown: Vec<&String> = events.iter().filter(|e| !crate::manifest::BACKEND_EVENTS.contains(&e.as_str())).collect();
782 if !unknown.is_empty() {
783 fault(shared, format!("no such engine events: {unknown:?} (there are {})", crate::manifest::BACKEND_EVENTS.join(", ")));
784 }
785 let mut g = shared.lock();
786 for e in events.iter().filter(|e| crate::manifest::BACKEND_EVENTS.contains(&e.as_str())) {
787 if kind == "subscribe" {
788 g.subscribed.insert(e.clone());
789 } else {
790 g.subscribed.remove(e);
791 }
792 }
793 }
794 _ => (shared.sink)(Out::Msg(msg)),
795 }
796}
797
798fn exited(shared: &Arc<Shared>, generation: u64, how: &str) {
800 let mut g = shared.lock();
801 if g.generation != generation || g.phase != Phase::Running {
802 return;
803 }
804 g.writer = None;
805 g.pid = None;
806 g.child = None;
807 let tail = g.stderr.last_lines(15);
808 let mut why = format!("the backend exited ({how})");
809 if !tail.is_empty() {
810 why.push_str(&format!("; its last output:\n{tail}"));
811 }
812 let stable = g.started_at.is_some_and(|t| t.elapsed() >= STABLE);
813 g.crashes = if stable { 1 } else { g.crashes.saturating_add(1) };
814 for (_, tx) in std::mem::take(&mut g.pending) {
815 let _ = tx.send(Err(why.clone()));
816 }
817 let crashes = g.crashes;
818 if crashes >= MAX_CRASHES {
819 g.phase = Phase::Failed;
820 why.push_str(&format!("\nIt exited {crashes} times in a row, so byob stopped starting it; fix it and reload the plugin."));
821 } else {
822 g.phase = Phase::Crashed;
823 let backoff = FIRST_BACKOFF.saturating_mul(1 << crashes.saturating_sub(1).min(8)).min(MAX_BACKOFF);
824 let s = Arc::clone(shared);
825 guarded("byob-backend-retry", move || {
826 thread::sleep(backoff);
827 let mut g = s.lock();
828 if g.phase == Phase::Crashed && g.generation == generation {
829 begin_start(&s, &mut g);
830 }
831 });
832 }
833 g.error = Some(why.clone());
834 drop(g);
835 log::warn!("plugin {}: {why}", shared.plugin);
836 (shared.sink)(Out::Exited(why));
837}
838
839fn guarded(name: &str, work: impl FnOnce() + Send + 'static) {
840 let label = name.to_owned();
841 let spawned = thread::Builder::new().name(name.into()).spawn(move || {
842 if catch_unwind(AssertUnwindSafe(work)).is_err() {
843 log::error!("{label} crashed");
844 }
845 });
846 if let Err(e) = spawned {
847 log::error!("could not start {name}: {e}");
848 }
849}
850
851#[derive(Default)]
853struct Ring(VecDeque<u8>);
854
855impl Ring {
856 fn push(&mut self, bytes: &[u8]) {
857 let bytes = bytes.get(bytes.len().saturating_sub(STDERR_KEEP)..).unwrap_or_default();
858 let overflow = (self.0.len() + bytes.len()).saturating_sub(STDERR_KEEP);
859 self.0.drain(..overflow.min(self.0.len()));
860 self.0.extend(bytes);
861 }
862
863 fn text(&self) -> String {
864 let (a, b) = self.0.as_slices();
865 let mut v = Vec::with_capacity(a.len() + b.len());
866 v.extend_from_slice(a);
867 v.extend_from_slice(b);
868 String::from_utf8_lossy(&v).into_owned()
869 }
870
871 fn last_lines(&self, n: usize) -> String {
872 last_lines(&self.text(), n)
873 }
874}
875
876pub fn last_lines(text: &str, n: usize) -> String {
878 let lines: Vec<&str> = text.trim_end().lines().collect();
879 lines.get(lines.len().saturating_sub(n)..).unwrap_or_default().join("\n")
880}
881
882#[derive(Debug, PartialEq, Eq)]
883enum LineRead {
884 Line,
885 TooLong,
886 Eof,
887}
888
889fn read_line(r: &mut impl BufRead, buf: &mut Vec<u8>, max: usize) -> io::Result<LineRead> {
892 buf.clear();
893 let mut overflow = false;
894 let mut any = false;
895 loop {
896 let avail = match r.fill_buf() {
897 Ok(b) => b,
898 Err(e) if e.kind() == io::ErrorKind::Interrupted => continue,
899 Err(e) => return Err(e),
900 };
901 if avail.is_empty() {
902 return Ok(match (overflow, any) {
903 (true, _) => LineRead::TooLong,
904 (false, true) => LineRead::Line,
905 (false, false) => LineRead::Eof,
906 });
907 }
908 any = true;
909 let newline = avail.iter().position(|&b| b == b'\n');
910 let (chunk, used) = match newline {
911 Some(i) => (avail.split_at(i).0, i.saturating_add(1)),
912 None => (avail, avail.len()),
913 };
914 if !overflow {
915 if buf.len().saturating_add(chunk.len()) > max {
916 overflow = true;
917 buf.clear();
918 buf.shrink_to(64 * 1024);
919 } else {
920 buf.extend_from_slice(chunk);
921 }
922 }
923 r.consume(used);
924 if newline.is_some() {
925 if buf.last() == Some(&b'\r') {
926 buf.pop();
927 }
928 return Ok(if overflow { LineRead::TooLong } else { LineRead::Line });
929 }
930 }
931}
932
933pub struct BackendTool {
935 name: String,
936 plugin: PluginRef,
937 decl: ManifestTool,
938 backend: Arc<Backend>,
939 timeout: Duration,
940 types: byob_tools::ArtifactTypes,
942 cards: Vec<(String, bool)>,
944}
945
946impl BackendTool {
947 pub fn new(name: String, plugin: PluginRef, decl: ManifestTool, backend: Arc<Backend>) -> Self {
948 Self { name, plugin, decl, backend, timeout: REQUEST_TIMEOUT, types: byob_tools::ArtifactTypes::builtin(), cards: Vec::new() }
949 }
950
951 pub fn with_cards(mut self, cards: Vec<(String, bool)>) -> Self {
953 self.cards = cards;
954 self
955 }
956
957 pub fn with_types(mut self, types: byob_tools::ArtifactTypes) -> Self {
959 self.types = types;
960 self
961 }
962
963 pub fn with_timeout(mut self, timeout: Duration) -> Self {
964 self.timeout = timeout;
965 self
966 }
967
968 pub fn declared(&self) -> &ManifestTool {
969 &self.decl
970 }
971}
972
973impl Tool for BackendTool {
974 fn spec(&self) -> ToolSpec {
975 ToolSpec { name: self.name.clone(), description: self.decl.description.clone(), parameters: self.decl.params.clone() }
976 }
977
978 fn title(&self, args: &Value) -> String {
979 match &self.decl.title {
980 Some(t) => crate::python::fill_title(t, args),
981 None => format!("Used {}: {}", self.plugin.name, self.decl.name),
982 }
983 }
984
985 fn access(&self, _args: &Value) -> Access {
986 self.decl.access.unwrap_or(Access::Execute)
987 }
988
989 fn run(&self, args: &Value, ctx: &ToolContext<'_>) -> ToolOutput {
990 let args = if args.is_object() { args.clone() } else { Value::Object(serde_json::Map::new()) };
991 let params = json!({
992 "name": self.decl.name,
993 "args": args,
994 "workspace": ctx.workspace.display().to_string(),
995 "chat": ctx.chat(),
996 });
997 match self.backend.request("tool", params, self.timeout, &|| ctx.cancelled()) {
998 Ok(v) => {
999 self.attach_card(&v, ctx);
1000 let mut out = tool_output(v.clone(), ctx.workspace);
1001 save_artifacts(&v, &self.types, ctx, &mut out);
1002 out
1003 }
1004 Err(e) if e.starts_with("Stopped by the user") => ToolOutput::err(e),
1005 Err(e) => ToolOutput::err(format!("{} (backend) ran into a problem:\n{e}", self.plugin.name)),
1006 }
1007 }
1008}
1009
1010impl BackendTool {
1011 fn attach_card(&self, v: &Value, ctx: &ToolContext<'_>) {
1016 let Some(o) = v.as_object() else { return };
1017 let Some(tree) = o.get("card").filter(|c| c.is_object()) else { return };
1018 let card = match o.get("card_view").and_then(Value::as_str) {
1019 Some(want) => match self.cards.iter().find(|(id, _)| id == want) {
1020 Some(c) => c,
1021 None => {
1022 let ids: Vec<&str> = self.cards.iter().map(|(id, _)| id.as_str()).collect();
1023 self.backend.report_fault(&format!("card_view {want:?} isn't a card view in byob-plugin.json (card views: {})", ids.join(", ")));
1024 return;
1025 }
1026 },
1027 None => match self.cards.first() {
1028 Some(c) => c,
1029 None => {
1030 self.backend.report_fault("a tool returned a \"card\", but byob-plugin.json declares no view with \"site\": \"card\"");
1031 return;
1032 }
1033 },
1034 };
1035 let (view, web) = (card.0.clone(), card.1);
1036 let key = match o.get("card_key").and_then(Value::as_str) {
1037 Some(k) if !k.is_empty() => k.chars().take(byob_protocol::sites::MAX_VIEW_KEY_CHARS).collect(),
1038 _ => format!("{}-{}", self.name, std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_nanos())),
1039 };
1040 if web {
1041 let text = o.get("text").and_then(Value::as_str).unwrap_or_default().to_owned();
1042 let card = byob_protocol::web::WebCard { plugin: self.plugin.id.clone(), view, key, data: tree.clone(), text };
1043 ctx.attach_view(byob_protocol::View::of(&card));
1044 return;
1045 }
1046 let (node, _faults) = byob_protocol::tree::decode(tree.clone());
1047 let mut shown = byob_protocol::View::of(&node);
1048 shown.owner = Some(byob_protocol::ViewRef::new(&self.plugin.id, &view, &key));
1049 if ctx.attach_view(shown) {
1051 self.backend.post(json!({"type": CARD_MSG, "view": view, "key": key, "tree": tree}));
1052 }
1053 }
1054}
1055
1056pub const CARD_MSG: &str = "_tool_card";
1059
1060const MAX_SAVED: usize = 4;
1062
1063fn save_artifacts(v: &Value, types: &byob_tools::ArtifactTypes, ctx: &ToolContext<'_>, out: &mut ToolOutput) {
1069 let Some(o) = v.as_object() else { return };
1070 let items: Vec<&Value> = match (o.get("artifact"), o.get("artifacts")) {
1071 (Some(a), _) if a.is_object() => vec![a],
1072 (_, Some(Value::Array(list))) => list.iter().filter(|a| a.is_object()).take(MAX_SAVED).collect(),
1073 _ => return,
1074 };
1075 let chat = ctx.chat().unwrap_or_default();
1076 for a in items {
1077 let s = |k: &str| a.get(k).and_then(Value::as_str);
1078 let result = (|| {
1079 let file = s("file").filter(|f| !f.is_empty()).ok_or("the artifact has no \"file\" (the path of its bytes)")?;
1080 let path = if Path::new(file).is_absolute() { PathBuf::from(file) } else { ctx.workspace.join(file) };
1081 let bytes = std::fs::metadata(&path)
1082 .map_err(|e| format!("{file}: {e}"))
1083 .and_then(|m| if m.len() > byob_tools::artifact::MAX_FILE_BYTES { Err(format!("{file} is too big")) } else { Ok(()) })
1084 .and_then(|()| std::fs::read(&path).map_err(|e| format!("{file}: {e}")))?;
1085 let save = byob_tools::artifact::SaveBytes {
1086 chat,
1087 id: s("id"),
1088 title: s("title"),
1089 filename: s("filename"),
1090 bytes: &bytes,
1091 note: s("note").unwrap_or_default(),
1092 replace: None,
1093 };
1094 byob_tools::artifact::save_bytes(types, ctx.workspace, &save)
1095 })();
1096 match result {
1097 Ok(saved) => {
1098 out.text.push_str(&format!(
1099 "\nSaved: artifact {}, version {} ({}){}.",
1100 saved.artifact.id,
1101 saved.version,
1102 saved.artifact.filename,
1103 if saved.created { "; a new artifact, use this id from now on" } else { "" }
1104 ));
1105 out.changes.push(FileChange::Created(saved.file));
1106 }
1107 Err(e) => {
1108 out.text.push_str(&format!("\nThe result couldn't be saved as an artifact: {e}"));
1109 out.failed = true;
1110 }
1111 }
1112 }
1113}
1114
1115fn tool_output(v: Value, workspace: &Path) -> ToolOutput {
1117 let (text, files) = match v {
1118 Value::String(s) => (s, Vec::new()),
1119 Value::Null => (String::new(), Vec::new()),
1120 Value::Object(o) if ["text", "files", "card", "artifact", "artifacts"].iter().any(|k| o.contains_key(*k)) => {
1121 let text = o.get("text").map(|t| t.as_str().map_or_else(|| t.to_string(), str::to_owned)).unwrap_or_default();
1122 let files = o.get("files").and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_str).map(str::to_owned).collect();
1123 (text, files)
1124 }
1125 other => (other.to_string(), Vec::new()),
1126 };
1127 let changes = files.iter().filter(|f| path_inside(f)).map(|f| workspace.join(f)).filter(|p| p.is_file()).map(FileChange::Modified).collect();
1128 let text = if text.trim().is_empty() { "(no result)".to_owned() } else { text };
1129 ToolOutput { text, failed: false, changes, images: Vec::new() }
1130}
1131
1132#[cfg(test)]
1133mod tests {
1134 use super::*;
1135
1136 fn lines(input: &[u8], max: usize) -> Vec<Result<String, LineRead>> {
1137 let mut r = io::BufReader::with_capacity(3, input);
1138 let mut buf = Vec::new();
1139 let mut out = Vec::new();
1140 loop {
1141 match read_line(&mut r, &mut buf, max).unwrap() {
1142 LineRead::Line => out.push(Ok(String::from_utf8(buf.clone()).unwrap())),
1143 LineRead::Eof => return out,
1144 other => out.push(Err(other)),
1145 }
1146 }
1147 }
1148
1149 #[test]
1150 fn long_lines_are_dropped_not_fatal() {
1151 assert_eq!(lines(b"ab\r\nwaaaaaaaaaaay too long\nok", 8), vec![Ok("ab".into()), Err(LineRead::TooLong), Ok("ok".into())]);
1152 }
1153
1154 #[test]
1155 fn ring_keeps_the_tail() {
1156 let mut r = Ring::default();
1157 r.push(&vec![b'a'; STDERR_KEEP]);
1158 r.push(b"\nlast line\n");
1159 assert_eq!(r.0.len(), STDERR_KEEP);
1160 assert_eq!(r.last_lines(1), "last line");
1161 }
1162
1163 #[test]
1164 fn tool_replies() {
1165 let ws = std::env::temp_dir();
1166 assert_eq!(tool_output(json!("hi"), &ws).text, "hi");
1167 assert_eq!(tool_output(json!({"text": "t", "files": ["../x"]}), &ws).text, "t");
1168 assert_eq!(tool_output(json!({"n": 1}), &ws).text, "{\"n\":1}");
1169 assert_eq!(tool_output(Value::Null, &ws).text, "(no result)");
1170 }
1171
1172 #[test]
1173 fn a_backend_that_cannot_start_fails_its_requests() {
1174 let sink: Sink = Arc::new(|_| {});
1175 let b = Backend::new("x", json!({"type": "init"}), &[], Box::new(|_: &dyn Fn() -> bool| Err("no python".into())), sink);
1176 let e = b.request("tool", json!({}), Duration::from_secs(5), &|| false).unwrap_err();
1177 assert_eq!(e, "no python");
1178 assert_eq!(b.status().phase, Phase::Failed);
1179 assert!(!b.event(json!({"type": "event"})));
1180 }
1181
1182 #[test]
1183 fn faults_reach_the_sink_at_most_once_a_second() {
1184 let posted = Arc::new(Mutex::new(Vec::new()));
1185 let p = Arc::clone(&posted);
1186 let sink: Sink = Arc::new(move |o| {
1187 if let Out::Msg(m) = o {
1188 p.lock().unwrap().push(m["text"].as_str().unwrap_or_default().to_owned());
1189 }
1190 });
1191 let b = Backend::new("x", json!({"type": "init"}), &[], Box::new(|_: &dyn Fn() -> bool| Err("-".into())), sink);
1192 for i in 0..100 {
1193 b.report_fault(&format!("not json {i}"));
1194 }
1195 assert_eq!(b.status().faults.last().map(String::as_str), Some("not json 99"));
1197 assert_eq!(b.status().faults.len(), MAX_FAULTS);
1198 assert_eq!(*posted.lock().unwrap(), ["not json 0"]);
1199 let deadline = Instant::now() + Duration::from_secs(5);
1201 while posted.lock().unwrap().len() < 2 && Instant::now() < deadline {
1202 thread::sleep(Duration::from_millis(10));
1203 }
1204 thread::sleep(Duration::from_millis(200));
1205 assert_eq!(*posted.lock().unwrap(), ["not json 0", "not json 99 (98 more)"]);
1206 }
1207
1208 fn never_starts() -> (Backend, Arc<std::sync::atomic::AtomicBool>) {
1211 let saw = Arc::new(AtomicBool::new(false));
1212 let s = Arc::clone(&saw);
1213 let prepare = Box::new(move |called_off: &dyn Fn() -> bool| {
1214 while !called_off() {
1215 thread::sleep(Duration::from_millis(5));
1216 }
1217 s.store(true, Ordering::SeqCst);
1218 Err("called off".to_owned())
1219 });
1220 (Backend::new("x", json!({"type": "init"}), &[], prepare, Arc::new(|_| {})), saw)
1221 }
1222
1223 #[test]
1224 fn a_stop_calls_off_a_long_preparation() {
1225 let (b, saw) = never_starts();
1226 b.start();
1227 thread::sleep(Duration::from_millis(50));
1228 assert_eq!(b.status().phase, Phase::Starting);
1229 b.stop("reloaded");
1230 let deadline = Instant::now() + Duration::from_secs(5);
1231 while !saw.load(Ordering::SeqCst) {
1232 assert!(Instant::now() < deadline, "the preparation went on");
1233 thread::sleep(Duration::from_millis(5));
1234 }
1235 }
1236
1237 #[test]
1238 fn a_slow_start_keeps_a_bounded_number_of_bytes() {
1239 let (b, _saw) = never_starts();
1240 let big = "x".repeat(MAX_LINE - 100);
1241 for _ in 0..40 {
1242 assert!(b.event(json!({"type": "event", "event": "client", "msg": big})));
1243 }
1244 let g = b.shared.lock();
1245 assert!(g.queued_bytes <= MAX_QUEUED_BYTES, "{}", g.queued_bytes);
1246 assert_eq!(g.queued_bytes, g.queued.iter().map(Vec::len).sum::<usize>());
1247 assert!(g.queued.len() < 40);
1248 }
1249}