Skip to main content

ane/commands/lsp_engine/
installer.rs

1use 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}