1use std::fmt::Write as _;
8use std::io::{BufRead, BufReader, Read, Write};
9use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
10use std::sync::mpsc::{Receiver, channel};
11use std::sync::{Arc, Mutex};
12use std::time::Duration;
13
14use super::host::{REPLY, Reply, Request};
15use super::server::{Launch, Link};
16use crate::contract::codes::{self, Code};
17use crate::project::{FILE, Project};
18
19const TAIL: usize = 6000;
21
22#[derive(Debug, Clone, Copy, Default)]
24pub struct Cargo;
25
26impl<'a> Launch<'a> for Cargo {
27 fn launch(&self, project: &Project) -> Result<Box<dyn Link + 'a>, (Code, String)> {
28 let Some(program) = &project.config.game else {
29 return Err((
30 codes::PROJECT_FILE,
31 format!("{FILE} names no game tool program (`game`), and the tool runs the game"),
32 ));
33 };
34 let cargo = crate::cargo();
35 let mut child = Command::new(cargo)
36 .args(program.cargo_args("mcp-host", &[]))
37 .current_dir(&project.root)
38 .stdin(Stdio::piped())
39 .stdout(Stdio::piped())
40 .stderr(Stdio::piped())
41 .spawn()
42 .map_err(|e| (codes::TOOLCHAIN, format!("cannot run cargo: {e}")))?;
43 let (Some(stdin), Some(stdout), Some(stderr)) =
44 (child.stdin.take(), child.stdout.take(), child.stderr.take())
45 else {
46 let _ = child.kill();
47 return Err((
48 codes::TOOLCHAIN,
49 "cargo started without its pipes".to_owned(),
50 ));
51 };
52 let tail = Arc::new(Mutex::new(Vec::new()));
53 let kept = Arc::clone(&tail);
54 let (ended, copied) = channel();
55 std::thread::spawn(move || {
56 keep_tail(stderr, &kept);
57 let _ = ended.send(());
58 });
59 Ok(Box::new(Program {
60 child,
61 stdin: Some(stdin),
62 stdout: BufReader::new(stdout),
63 tail,
64 copied,
65 }))
66 }
67}
68
69pub(super) fn keep_tail(mut from: impl Read, tail: &Mutex<Vec<u8>>) {
72 let mut buf = [0u8; 4096];
73 while let Ok(n) = from.read(&mut buf) {
74 if n == 0 {
75 break;
76 }
77 let _ = std::io::stderr().write_all(&buf[..n]);
78 if let Ok(mut tail) = tail.lock() {
79 tail.extend_from_slice(&buf[..n]);
80 let over = tail.len().saturating_sub(TAIL);
81 tail.drain(..over);
82 }
83 }
84}
85
86struct Program {
88 child: Child,
89 stdin: Option<ChildStdin>,
90 stdout: BufReader<ChildStdout>,
91 tail: Arc<Mutex<Vec<u8>>>,
92 copied: Receiver<()>,
94}
95
96impl Program {
97 fn stopped(&mut self) -> String {
100 self.stdin = None;
101 let status = self.child.wait().ok().and_then(|status| status.code());
102 let _ = self.copied.recv_timeout(Duration::from_secs(1));
104 let tail = self
105 .tail
106 .lock()
107 .map(|tail| String::from_utf8_lossy(&tail).trim().to_owned())
108 .unwrap_or_default();
109 let mut out = "the game's tool program stopped before it answered".to_owned();
110 if let Some(code) = status {
111 let _ = write!(out, " (exit code {code})");
112 }
113 if !tail.is_empty() {
114 out.push_str(": ");
115 out.push_str(&tail);
116 }
117 out
118 }
119}
120
121impl Link for Program {
122 fn call(&mut self, request: &Request) -> Result<Reply, String> {
123 let line = serde_json::to_string(request).map_err(|e| e.to_string())?;
124 let sent = self
125 .stdin
126 .as_mut()
127 .map(|stdin| writeln!(stdin, "{line}").and_then(|()| stdin.flush()));
128 if !matches!(sent, Some(Ok(()))) {
129 return Err(self.stopped());
130 }
131 let mut line = String::new();
132 loop {
133 line.clear();
134 match self.stdout.read_line(&mut line) {
135 Ok(0) | Err(_) => return Err(self.stopped()),
136 Ok(_) => {}
137 }
138 if let Some(reply) = line.trim_end().strip_prefix(REPLY) {
140 return serde_json::from_str(reply)
141 .map_err(|e| format!("the game's tool program sent a broken reply ({e})"));
142 }
143 }
144 }
145}
146
147impl Drop for Program {
148 fn drop(&mut self) {
151 self.stdin = None;
152 let _ = self.child.kill();
153 let _ = self.child.wait();
154 }
155}
156
157#[cfg(test)]
158mod tests;