ane/commands/lsp_engine/
installer.rs1use std::io::{BufRead, BufReader};
2use std::process::{Command, Stdio};
3use std::sync::Arc;
4
5use anyhow::{Result, bail};
6
7use crate::data::lsp::types::LspServerInfo;
8
9pub trait InstallProgress: Send + Sync {
10 fn on_stdout(&self, line: &str);
11 fn on_stderr(&self, line: &str);
12 fn on_failed(&self, message: &str);
13 fn on_complete(&self);
14}
15
16pub fn is_installed(server: &LspServerInfo) -> bool {
17 Command::new("sh")
18 .arg("-c")
19 .arg(server.check_command)
20 .stdout(Stdio::null())
21 .stderr(Stdio::null())
22 .output()
23 .map(|out| out.status.success())
24 .unwrap_or(false)
25}
26
27pub fn install(server: &LspServerInfo, progress: Option<&Arc<dyn InstallProgress>>) -> Result<()> {
28 let mut child = Command::new("sh")
29 .arg("-c")
30 .arg(server.install_command)
31 .stdout(Stdio::piped())
32 .stderr(Stdio::piped())
33 .spawn()?;
34
35 let stdout = child.stdout.take().unwrap();
36 let stderr = child.stderr.take().unwrap();
37
38 let prog_out = progress.cloned();
39 let prog_err = progress.cloned();
40
41 let out_handle = std::thread::spawn(move || {
42 let mut lines = Vec::new();
43 let reader = BufReader::new(stdout);
44 for line in reader.lines().map_while(Result::ok) {
45 if let Some(ref p) = prog_out {
46 p.on_stdout(&line);
47 }
48 lines.push(line);
49 }
50 lines
51 });
52
53 let err_handle = std::thread::spawn(move || {
54 let mut lines = Vec::new();
55 let reader = BufReader::new(stderr);
56 for line in reader.lines().map_while(Result::ok) {
57 if let Some(ref p) = prog_err {
58 p.on_stderr(&line);
59 }
60 lines.push(line);
61 }
62 lines
63 });
64
65 let status = child.wait()?;
66 let stdout_lines = out_handle.join().unwrap_or_default();
67 let stderr_lines = err_handle.join().unwrap_or_default();
68
69 if !status.success() {
70 let log_path = write_install_log(server.server_name, &stdout_lines, &stderr_lines);
71 let msg = match log_path {
72 Some(p) => format!(
73 "install of {} failed (exit {}); log: {}",
74 server.server_name,
75 status.code().unwrap_or(-1),
76 p,
77 ),
78 None => format!(
79 "install of {} failed (exit {})",
80 server.server_name,
81 status.code().unwrap_or(-1),
82 ),
83 };
84 if let Some(p) = progress {
85 p.on_failed(&msg);
86 }
87 bail!("{msg}");
88 }
89
90 if let Some(p) = progress {
91 p.on_complete();
92 }
93
94 Ok(())
95}
96
97fn write_install_log(
98 server_name: &str,
99 stdout_lines: &[String],
100 stderr_lines: &[String],
101) -> Option<String> {
102 use std::io::Write;
103 let path = std::env::temp_dir().join(format!("ane-install-{server_name}.log"));
104 let mut f = std::fs::File::create(&path).ok()?;
105 if !stdout_lines.is_empty() {
106 let _ = writeln!(f, "=== stdout ===");
107 for line in stdout_lines {
108 let _ = writeln!(f, "{line}");
109 }
110 }
111 if !stderr_lines.is_empty() {
112 let _ = writeln!(f, "=== stderr ===");
113 for line in stderr_lines {
114 let _ = writeln!(f, "{line}");
115 }
116 }
117 Some(path.to_string_lossy().into_owned())
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123 use crate::data::lsp::types::Language;
124
125 static MISSING_SERVER: LspServerInfo = LspServerInfo {
126 language: Language::Rust,
127 server_name: "missing-server",
128 binary_name: "ane-test-binary-does-not-exist-xyz",
129 install_command: "exit 0",
130 check_command: "ane-test-binary-does-not-exist-xyz --version",
131 default_args: &[],
132 init_options_json: "",
133 };
134
135 static TRUE_SERVER: LspServerInfo = LspServerInfo {
136 language: Language::Rust,
137 server_name: "true-server",
138 binary_name: "true",
139 install_command: "true",
140 check_command: "true",
141 default_args: &[],
142 init_options_json: "",
143 };
144
145 static FAIL_SERVER: LspServerInfo = LspServerInfo {
146 language: Language::Rust,
147 server_name: "fail-server",
148 binary_name: "false",
149 install_command: "false",
150 check_command: "false",
151 default_args: &[],
152 init_options_json: "",
153 };
154
155 #[test]
156 fn not_installed_when_binary_missing() {
157 assert!(!is_installed(&MISSING_SERVER));
158 }
159
160 #[test]
161 fn installed_when_check_command_succeeds() {
162 assert!(is_installed(&TRUE_SERVER));
163 }
164
165 #[test]
166 fn not_installed_when_check_command_fails() {
167 assert!(!is_installed(&FAIL_SERVER));
168 }
169
170 #[test]
171 fn install_succeeds_when_command_exits_zero() {
172 assert!(install(&TRUE_SERVER, None).is_ok());
173 }
174
175 #[test]
176 fn install_fails_when_command_exits_nonzero() {
177 let err = install(&FAIL_SERVER, None).unwrap_err();
178 assert!(err.to_string().contains("fail-server"));
179 }
180
181 use std::sync::Mutex;
182
183 struct TestProgress {
184 lines: Mutex<Vec<String>>,
185 failed: Mutex<Option<String>>,
186 completed: Mutex<bool>,
187 }
188
189 impl TestProgress {
190 fn new() -> Self {
191 Self {
192 lines: Mutex::new(Vec::new()),
193 failed: Mutex::new(None),
194 completed: Mutex::new(false),
195 }
196 }
197 }
198
199 impl InstallProgress for TestProgress {
200 fn on_stdout(&self, line: &str) {
201 self.lines.lock().unwrap().push(format!("out: {line}"));
202 }
203 fn on_stderr(&self, line: &str) {
204 self.lines.lock().unwrap().push(format!("err: {line}"));
205 }
206 fn on_failed(&self, message: &str) {
207 *self.failed.lock().unwrap() = Some(message.to_string());
208 }
209 fn on_complete(&self) {
210 *self.completed.lock().unwrap() = true;
211 }
212 }
213
214 #[test]
215 fn install_streams_lines_via_progress_trait() {
216 static ECHO_SERVER: LspServerInfo = LspServerInfo {
217 language: Language::Rust,
218 server_name: "echo-server",
219 binary_name: "true",
220 install_command: "echo hello && echo world",
221 check_command: "true",
222 default_args: &[],
223 init_options_json: "",
224 };
225 let prog = Arc::new(TestProgress::new());
226 let dyn_prog: Arc<dyn InstallProgress> = Arc::clone(&prog) as Arc<dyn InstallProgress>;
227 install(&ECHO_SERVER, Some(&dyn_prog)).unwrap();
228 let lines = prog.lines.lock().unwrap();
229 assert!(lines.contains(&"out: hello".to_string()));
230 assert!(lines.contains(&"out: world".to_string()));
231 assert!(*prog.completed.lock().unwrap());
232 }
233
234 #[test]
235 fn install_failure_calls_on_failed_with_log_path() {
236 static NOISY_FAIL: LspServerInfo = LspServerInfo {
237 language: Language::Rust,
238 server_name: "noisy-fail",
239 binary_name: "false",
240 install_command: "echo out-line && echo err-line >&2 && exit 1",
241 check_command: "false",
242 default_args: &[],
243 init_options_json: "",
244 };
245 let prog = Arc::new(TestProgress::new());
246 let dyn_prog: Arc<dyn InstallProgress> = Arc::clone(&prog) as Arc<dyn InstallProgress>;
247 let err = install(&NOISY_FAIL, Some(&dyn_prog)).unwrap_err();
248 assert!(err.to_string().contains("noisy-fail"));
249 let failed = prog.failed.lock().unwrap();
250 let msg = failed.as_ref().unwrap();
251 assert!(msg.contains("log:"));
252 assert!(msg.contains("noisy-fail"));
253 }
254}