agentmux_srv/backend/lsp/
supervisor.rs1#[cfg(windows)]
20use agentmux_common::win32::NoWindow;
21use std::collections::HashMap;
22use std::path::PathBuf;
23use std::process::Stdio;
24use std::sync::Arc;
25
26use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
27use tokio::process::{Child, ChildStderr, ChildStdin, ChildStdout, Command};
28use tokio::sync::Mutex;
29
30use crate::backend::eventbus::EventBus;
31
32pub type ServerId = String;
33
34pub struct StartArgs {
35 pub language: String,
36 pub workspace_root: PathBuf,
37}
38
39pub struct StartResult {
40 pub server_id: ServerId,
41 pub workspace_root: String,
42}
43
44#[derive(thiserror::Error, Debug)]
45pub enum LspError {
46 #[error("server_binary_not_found:{language}")]
49 BinaryNotFound { language: String },
50
51 #[error("unsupported language: {0}")]
52 UnsupportedLanguage(String),
53
54 #[error("spawn failed: {0}")]
55 SpawnFailed(String),
56
57 #[error("server not running: {0}")]
58 ServerNotFound(String),
59
60 #[error("io: {0}")]
61 Io(#[from] std::io::Error),
62}
63
64impl LspError {
65 pub fn to_wire_string(&self) -> String {
66 self.to_string()
67 }
68}
69
70struct ServerHandle {
71 stdin: Mutex<ChildStdin>,
72 refcount: std::sync::atomic::AtomicUsize,
73 _child: Mutex<Child>, }
75
76pub struct LspSupervisor {
77 servers: Mutex<HashMap<(String, String), Arc<ServerHandle>>>,
78 event_bus: Arc<EventBus>,
79}
80
81impl LspSupervisor {
82 pub fn new(event_bus: Arc<EventBus>) -> Self {
83 Self {
84 servers: Mutex::new(HashMap::new()),
85 event_bus,
86 }
87 }
88
89 pub async fn start(&self, args: StartArgs) -> Result<StartResult, LspError> {
91 let lang = args.language.clone();
92 let root_str = args.workspace_root.to_string_lossy().to_string();
93 let key = (root_str.clone(), lang.clone());
94 let server_id = make_server_id(&root_str, &lang);
95
96 {
98 let servers = self.servers.lock().await;
99 if let Some(handle) = servers.get(&key) {
100 handle
101 .refcount
102 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
103 return Ok(StartResult {
104 server_id,
105 workspace_root: root_str,
106 });
107 }
108 }
109
110 let binary = binary_for_language(&lang)
113 .ok_or_else(|| LspError::UnsupportedLanguage(lang.clone()))?;
114 let resolved = which::which(binary).map_err(|_| LspError::BinaryNotFound {
115 language: lang.clone(),
116 })?;
117
118 tracing::info!(
119 language = %lang,
120 workspace = %root_str,
121 binary = %resolved.display(),
122 "LSP: spawning server"
123 );
124
125 let mut cmd = Command::new(&resolved);
126 crate::backend::pane_env::sanitize_external_command(&mut cmd);
129 cmd.arg("--stdio")
133 .current_dir(&args.workspace_root)
134 .stdin(Stdio::piped())
135 .stdout(Stdio::piped())
136 .stderr(Stdio::piped())
137 .kill_on_drop(true);
138
139 #[cfg(windows)]
145 {
146 cmd.no_window();
147 }
148
149 let mut child = cmd
150 .spawn()
151 .map_err(|e| LspError::SpawnFailed(e.to_string()))?;
152 let stdin = child
153 .stdin
154 .take()
155 .ok_or_else(|| LspError::SpawnFailed("no stdin".to_string()))?;
156 let stdout = child
157 .stdout
158 .take()
159 .ok_or_else(|| LspError::SpawnFailed("no stdout".to_string()))?;
160 let stderr = child
161 .stderr
162 .take()
163 .ok_or_else(|| LspError::SpawnFailed("no stderr".to_string()))?;
164
165 let event_bus = self.event_bus.clone();
168 let server_id_for_stdout = server_id.clone();
169 tokio::spawn(async move {
170 read_lsp_messages(stdout, server_id_for_stdout, event_bus).await;
171 });
172
173 let server_id_for_stderr = server_id.clone();
177 tokio::spawn(async move {
178 drain_lsp_stderr(stderr, server_id_for_stderr).await;
179 });
180
181 let handle = Arc::new(ServerHandle {
182 stdin: Mutex::new(stdin),
183 refcount: std::sync::atomic::AtomicUsize::new(1),
184 _child: Mutex::new(child),
185 });
186
187 let mut servers = self.servers.lock().await;
192 if let Some(existing) = servers.get(&key) {
193 existing
194 .refcount
195 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
196 drop(servers);
197 drop(handle); return Ok(StartResult {
199 server_id,
200 workspace_root: root_str,
201 });
202 }
203 servers.insert(key, handle);
204
205 Ok(StartResult {
206 server_id,
207 workspace_root: root_str,
208 })
209 }
210
211 pub async fn send(&self, server_id: &str, message_json: &str) -> Result<(), LspError> {
213 let key = parse_server_id(server_id)?;
214 let handle = {
215 let servers = self.servers.lock().await;
216 servers
217 .get(&key)
218 .cloned()
219 .ok_or_else(|| LspError::ServerNotFound(server_id.to_string()))?
220 };
221 let mut stdin = handle.stdin.lock().await;
222 let body = message_json.as_bytes();
223 let header = format!("Content-Length: {}\r\n\r\n", body.len());
225 stdin.write_all(header.as_bytes()).await?;
226 stdin.write_all(body).await?;
227 stdin.flush().await?;
228 Ok(())
229 }
230
231 pub async fn stop(&self, server_id: &str) -> Result<(), LspError> {
236 let key = parse_server_id(server_id)?;
237 let mut servers = self.servers.lock().await;
238 if let Some(handle) = servers.get(&key) {
239 let prev = handle
240 .refcount
241 .fetch_sub(1, std::sync::atomic::Ordering::SeqCst);
242 if prev <= 1 {
243 tracing::info!(server_id = %server_id, "LSP: shutting down (refcount=0)");
244 servers.remove(&key);
245 }
247 }
248 Ok(())
249 }
250}
251
252fn make_server_id(workspace_root: &str, language: &str) -> ServerId {
253 format!("lsp://{language}::{workspace_root}")
255}
256
257fn parse_server_id(server_id: &str) -> Result<(String, String), LspError> {
258 let stripped = server_id
259 .strip_prefix("lsp://")
260 .ok_or_else(|| LspError::ServerNotFound(server_id.to_string()))?;
261 let (lang, root) = stripped
262 .split_once("::")
263 .ok_or_else(|| LspError::ServerNotFound(server_id.to_string()))?;
264 Ok((root.to_string(), lang.to_string()))
265}
266
267fn binary_for_language(language: &str) -> Option<&'static str> {
270 Some(match language {
271 "typescript" | "javascript" => "typescript-language-server",
272 "rust" => "rust-analyzer",
275 "python" => "pyright-langserver",
276 "go" => "gopls",
277 "c" | "cpp" => "clangd",
278 _ => return None,
279 })
280}
281
282async fn read_lsp_messages(stdout: ChildStdout, server_id: ServerId, event_bus: Arc<EventBus>) {
287 let mut reader = BufReader::new(stdout);
288 loop {
289 let mut content_length: usize = 0;
290 loop {
292 let mut line = String::new();
293 match reader.read_line(&mut line).await {
294 Ok(0) => {
295 tracing::info!(server_id = %server_id, "LSP: stdout EOF (server exited)");
296 return;
297 }
298 Ok(_) => {}
299 Err(e) => {
300 tracing::warn!(server_id = %server_id, error = %e, "LSP: stdout read error");
301 return;
302 }
303 }
304 let trimmed = line.trim_end_matches(['\r', '\n']);
305 if trimmed.is_empty() {
306 break;
307 }
308 if let Some(value) = trimmed.strip_prefix("Content-Length:") {
309 content_length = value.trim().parse().unwrap_or(0);
310 }
311 }
314 if content_length == 0 {
315 continue;
316 }
317 let mut buf = vec![0u8; content_length];
318 if reader.read_exact(&mut buf).await.is_err() {
319 tracing::warn!(server_id = %server_id, "LSP: body read failed");
320 return;
321 }
322 let body = match std::str::from_utf8(&buf) {
323 Ok(s) => s,
324 Err(_) => {
325 tracing::warn!(server_id = %server_id, "LSP: non-UTF8 body");
326 continue;
327 }
328 };
329 let message = match serde_json::from_str::<serde_json::Value>(body) {
332 Ok(v) => v,
333 Err(e) => {
334 tracing::warn!(server_id = %server_id, error = %e, "LSP: invalid JSON body");
335 continue;
336 }
337 };
338 let event = crate::backend::mps_events::LspMessageEvent { server_id: server_id.to_string(), message };
339 event_bus.broadcast_event(&EventBus::typed_ws_event::<crate::backend::mps_events::LspMessage>(&event, String::new()));
340 }
341}
342
343async fn drain_lsp_stderr(stderr: ChildStderr, server_id: ServerId) {
348 let mut reader = BufReader::new(stderr);
349 let mut line = String::new();
350 loop {
351 line.clear();
352 match reader.read_line(&mut line).await {
353 Ok(0) => return, Ok(_) => {
355 tracing::debug!(
356 server_id = %server_id,
357 stderr = %line.trim_end_matches(['\r', '\n']),
358 "LSP stderr"
359 );
360 }
361 Err(e) => {
362 tracing::warn!(server_id = %server_id, error = %e, "LSP: stderr read error");
363 return;
364 }
365 }
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn server_id_roundtrip() {
375 let id = make_server_id("/Users/asaf/project", "typescript");
376 let (root, lang) = parse_server_id(&id).unwrap();
377 assert_eq!(root, "/Users/asaf/project");
378 assert_eq!(lang, "typescript");
379 }
380
381 #[test]
382 fn server_id_with_double_colon_in_root() {
383 let id = make_server_id("C:\\Users\\asaf\\project", "rust");
386 let (root, lang) = parse_server_id(&id).unwrap();
387 assert_eq!(root, "C:\\Users\\asaf\\project");
388 assert_eq!(lang, "rust");
389 }
390
391 #[test]
392 fn binary_lookup_table() {
393 assert_eq!(binary_for_language("typescript"), Some("typescript-language-server"));
394 assert_eq!(binary_for_language("javascript"), Some("typescript-language-server"));
395 assert_eq!(binary_for_language("rust"), Some("rust-analyzer"));
396 assert_eq!(binary_for_language("python"), Some("pyright-langserver"));
397 assert_eq!(binary_for_language("go"), Some("gopls"));
398 assert_eq!(binary_for_language("c"), Some("clangd"));
399 assert_eq!(binary_for_language("cpp"), Some("clangd"));
400 assert_eq!(binary_for_language("brainfuck"), None);
401 }
402}