Skip to main content

analyzed_daemon/
lib.rs

1use std::{
2    env,
3    io::{self, BufReader},
4    path::PathBuf,
5    process::{self, Child, Command, Stdio},
6    sync::{
7        Arc,
8        atomic::{AtomicUsize, Ordering},
9    },
10    thread::{self, JoinHandle},
11    time::{Duration, Instant, SystemTime, UNIX_EPOCH},
12};
13
14use analyzed_ipc::{
15    AnalysisConfigKey, BackendKey, BackendSnapshot, CargoConfigKey, ClientInfo, DaemonRequest,
16    DaemonResponse, DaemonSnapshot, Hello, IpcStream, LspSession, ProcMacroServerKey,
17    ProtocolError, RuntimePaths, SharedWorldConfigKey, SharedWorldKey, SharedWorldLoadKey,
18    StartupLock, Stop, WorkspaceSnapshot, WorkspaceViewKey, accept_client, bind_listener,
19    read_json_line, write_json_line,
20};
21use crossbeam_channel::unbounded;
22use lsp_server::{Connection, Message};
23use ra_ap_rust_analyzer::{SharedAnalyzerProvider, shared_analyzer_registry};
24use serde::Serialize;
25
26pub fn version() -> String {
27    format!(
28        "{} (rust-analyzer {})",
29        env!("CARGO_PKG_VERSION"),
30        *ra_ap_rust_analyzer::RUST_ANALYZER_VERSION
31    )
32}
33
34#[derive(Debug, Serialize)]
35pub struct DaemonStatus {
36    running: bool,
37    pid: Option<u32>,
38    started_at_unix_seconds: Option<u64>,
39    client_sessions: usize,
40    backend_sessions: Vec<BackendSnapshot>,
41    workspaces: usize,
42    paths: RuntimePaths,
43    hello: Option<Hello>,
44    connection_error: Option<String>,
45}
46
47pub fn offline_status(paths: RuntimePaths) -> DaemonStatus {
48    DaemonStatus {
49        running: false,
50        pid: None,
51        started_at_unix_seconds: None,
52        client_sessions: 0,
53        backend_sessions: Vec::new(),
54        workspaces: 0,
55        paths,
56        hello: None,
57        connection_error: None,
58    }
59}
60
61pub fn status(paths: RuntimePaths) -> DaemonStatus {
62    match analyzed_ipc::connect_hello(&paths) {
63        Ok(hello) => online_status(paths, hello),
64        Err(error) => DaemonStatus {
65            connection_error: Some(error.to_string()),
66            ..offline_status(paths)
67        },
68    }
69}
70
71pub fn online_status(paths: RuntimePaths, hello: Hello) -> DaemonStatus {
72    let snapshot = hello.state.as_ref();
73
74    DaemonStatus {
75        running: true,
76        pid: Some(snapshot.map_or(hello.pid, |state| state.pid)),
77        started_at_unix_seconds: snapshot.map(|state| state.started_at_unix_seconds),
78        client_sessions: snapshot.map_or(0, |state| state.client_sessions),
79        backend_sessions: snapshot.map_or_else(Vec::new, |state| state.backend_sessions.clone()),
80        workspaces: snapshot.map_or(0, |state| state.workspaces),
81        paths,
82        hello: Some(hello),
83        connection_error: None,
84    }
85}
86
87pub fn stop(paths: RuntimePaths) -> analyzed_ipc::Result<Stop> {
88    analyzed_ipc::request_stop(&paths)
89}
90
91pub fn connect_lsp_session(paths: RuntimePaths) -> anyhow::Result<IpcStream> {
92    ensure_daemon(paths.clone())?;
93    Ok(analyzed_ipc::connect_lsp_session(&paths)?)
94}
95
96pub fn ensure_daemon(paths: RuntimePaths) -> anyhow::Result<Hello> {
97    if let Ok(hello) = analyzed_ipc::connect_hello(&paths) {
98        return Ok(hello);
99    }
100
101    let _lock = StartupLock::acquire(&paths)?;
102
103    if let Ok(hello) = analyzed_ipc::connect_hello(&paths) {
104        return Ok(hello);
105    }
106
107    let mut command = Command::new(env::current_exe()?);
108    command
109        .arg("daemon")
110        .arg("--foreground")
111        .arg("--startup-lock-owned")
112        .current_dir(daemon_working_directory()?)
113        .stdin(Stdio::null())
114        .stdout(Stdio::null())
115        .stderr(Stdio::null());
116
117    let mut child = spawn_daemon_command(command)?;
118    let mut child_exited = false;
119    let start = Instant::now();
120    let mut delay = Duration::from_millis(10);
121
122    while start.elapsed() < Duration::from_secs(5) {
123        if let Ok(hello) = analyzed_ipc::connect_hello(&paths) {
124            return Ok(hello);
125        }
126
127        if !child_exited && let Some(status) = child.try_wait()? {
128            if !status.success() {
129                anyhow::bail!("daemon exited before accepting connections: {status}");
130            }
131
132            child_exited = true;
133        }
134
135        thread::sleep(delay);
136        delay = delay.saturating_mul(2).min(Duration::from_millis(100));
137    }
138
139    if child_exited {
140        anyhow::bail!("daemon exited before accepting connections")
141    } else {
142        anyhow::bail!("daemon did not accept connections before the startup timeout")
143    }
144}
145
146#[cfg(unix)]
147fn daemon_working_directory() -> anyhow::Result<PathBuf> {
148    Ok(PathBuf::from("/"))
149}
150
151#[cfg(windows)]
152fn daemon_working_directory() -> anyhow::Result<PathBuf> {
153    env::var_os("SystemRoot")
154        .map(PathBuf::from)
155        .ok_or_else(|| anyhow::anyhow!("SystemRoot is not set"))
156}
157
158#[cfg(unix)]
159fn spawn_daemon_command(mut command: Command) -> io::Result<Child> {
160    use std::os::unix::process::CommandExt;
161
162    unsafe extern "C" {
163        fn setsid() -> i32;
164    }
165
166    unsafe {
167        command.pre_exec(|| {
168            if setsid() == -1 {
169                return Err(io::Error::last_os_error());
170            }
171
172            Ok(())
173        });
174    }
175
176    command.spawn()
177}
178
179#[cfg(windows)]
180fn spawn_daemon_command(mut command: Command) -> io::Result<Child> {
181    use std::os::windows::process::CommandExt;
182    use windows_sys::Win32::System::Threading::{CREATE_NEW_PROCESS_GROUP, CREATE_NO_WINDOW};
183
184    command.creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP | breakaway_flag());
185    command.spawn()
186}
187
188// Editors commonly place LSP servers in a kill-on-close job object. The
189// daemon outlives any single editor, so it must break away when the job
190// permits it. Whether breakaway is possible is decided by querying the job
191// up front; silent breakaway needs no flag, and a job that forbids breakaway
192// would reject the flag at spawn time.
193#[cfg(windows)]
194fn breakaway_flag() -> u32 {
195    use std::ptr;
196
197    use windows_sys::Win32::System::{
198        JobObjects::{
199            IsProcessInJob, JOB_OBJECT_LIMIT_BREAKAWAY_OK, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
200            JobObjectExtendedLimitInformation, QueryInformationJobObject,
201        },
202        Threading::{CREATE_BREAKAWAY_FROM_JOB, GetCurrentProcess},
203    };
204
205    unsafe {
206        let mut in_job = 0;
207        if IsProcessInJob(GetCurrentProcess(), ptr::null_mut(), &mut in_job) == 0 || in_job == 0 {
208            return 0;
209        }
210
211        let mut info = JOBOBJECT_EXTENDED_LIMIT_INFORMATION::default();
212        let queried = QueryInformationJobObject(
213            ptr::null_mut(),
214            JobObjectExtendedLimitInformation,
215            (&raw mut info).cast(),
216            size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
217            ptr::null_mut(),
218        );
219        if queried != 0
220            && info.BasicLimitInformation.LimitFlags & JOB_OBJECT_LIMIT_BREAKAWAY_OK != 0
221        {
222            CREATE_BREAKAWAY_FROM_JOB
223        } else {
224            0
225        }
226    }
227}
228
229pub fn run_foreground(paths: RuntimePaths, startup_lock_owned: bool) -> anyhow::Result<()> {
230    let state = ServiceState::load();
231    let mut listener = {
232        let _lock = (!startup_lock_owned)
233            .then(|| StartupLock::acquire(&paths))
234            .transpose()?;
235        if !startup_lock_owned && let Ok(hello) = analyzed_ipc::connect_hello(&paths) {
236            anyhow::bail!("daemon is already running with pid {}", hello.pid);
237        }
238        bind_listener(&paths)?
239    };
240
241    loop {
242        let stream = accept_client(&mut listener)?;
243        let state = Arc::clone(&state);
244        thread::spawn(move || {
245            state.client_sessions.fetch_add(1, Ordering::SeqCst);
246            let session_id = state.next_session_id.fetch_add(1, Ordering::SeqCst);
247            let result = handle_client(stream, state.clone(), session_id);
248            state.client_sessions.fetch_sub(1, Ordering::SeqCst);
249
250            if let Err(error) = result {
251                eprintln!("{error}");
252            }
253        });
254    }
255}
256
257struct ServiceState {
258    pid: u32,
259    started_at_unix_seconds: u64,
260    client_sessions: AtomicUsize,
261    next_session_id: AtomicUsize,
262}
263
264impl ServiceState {
265    fn load() -> Arc<Self> {
266        Arc::new(Self {
267            pid: process::id(),
268            started_at_unix_seconds: SystemTime::now()
269                .duration_since(UNIX_EPOCH)
270                .map_or(0, |duration| duration.as_secs()),
271            client_sessions: AtomicUsize::new(0),
272            next_session_id: AtomicUsize::new(1),
273        })
274    }
275
276    fn snapshot(&self) -> DaemonSnapshot {
277        let registry = shared_analyzer_registry();
278        let backend_sessions = registry
279            .backend_snapshots()
280            .into_iter()
281            .map(backend_snapshot_from_shared)
282            .collect::<Vec<_>>();
283        let workspace_loads = registry
284            .workspace_loads()
285            .into_iter()
286            .map(workspace_snapshot_from_shared)
287            .collect::<Vec<_>>();
288
289        DaemonSnapshot {
290            pid: self.pid,
291            started_at_unix_seconds: self.started_at_unix_seconds,
292            client_sessions: self.client_sessions.load(Ordering::SeqCst),
293            backend_sessions,
294            workspaces: workspace_loads.len(),
295            workspace_loads,
296        }
297    }
298}
299
300fn handle_client(
301    mut stream: IpcStream,
302    state: Arc<ServiceState>,
303    session_id: usize,
304) -> analyzed_ipc::Result<()> {
305    let request: DaemonRequest = read_json_line(&mut stream)?;
306    if let Some(error) = validate_client(request.client_info()) {
307        write_json_line(&mut stream, &DaemonResponse::Error(error))?;
308        return Ok(());
309    }
310
311    match request {
312        DaemonRequest::Hello(_) => {
313            write_json_line(
314                &mut stream,
315                &DaemonResponse::Hello(Hello::with_state(
316                    state.snapshot(),
317                    ra_ap_rust_analyzer::RUST_ANALYZER_VERSION.to_owned(),
318                )),
319            )?;
320        }
321        DaemonRequest::Lsp(_) => {
322            write_json_line(
323                &mut stream,
324                &DaemonResponse::Lsp(LspSession {
325                    accepted: true,
326                    pid: state.pid,
327                }),
328            )?;
329            handle_lsp_session(stream, state, session_id).map_err(|error| {
330                analyzed_ipc::IpcError::Protocol(format!("lsp session failed: {error}"))
331            })?;
332        }
333        DaemonRequest::Stop(_) => {
334            write_json_line(
335                &mut stream,
336                &DaemonResponse::Stop(Stop {
337                    accepted: true,
338                    pid: state.pid,
339                }),
340            )?;
341            process::exit(0);
342        }
343    }
344
345    Ok(())
346}
347
348struct LspStreamThreads {
349    reader: JoinHandle<anyhow::Result<()>>,
350    writer: JoinHandle<anyhow::Result<()>>,
351}
352
353impl LspStreamThreads {
354    fn join(self) -> anyhow::Result<()> {
355        match (self.reader.join(), self.writer.join()) {
356            (Ok(Ok(())), Ok(Ok(()))) => Ok(()),
357            (Ok(Err(reader)), Ok(Err(writer))) => anyhow::bail!("{reader}\n{writer}"),
358            (Ok(Err(error)), _) | (_, Ok(Err(error))) => Err(error),
359            (Err(_), _) | (_, Err(_)) => anyhow::bail!("lsp stream thread panicked"),
360        }
361    }
362}
363
364fn handle_lsp_session(
365    stream: IpcStream,
366    _state: Arc<ServiceState>,
367    _session_id: usize,
368) -> anyhow::Result<()> {
369    let (connection, threads) = lsp_stream_connection(stream)?;
370    let registry = shared_analyzer_registry();
371    let provider = SharedAnalyzerProvider::new(move |key, shared_config, reload_path| {
372        registry.register(key, shared_config, reload_path)
373    });
374    let result = ra_ap_rust_analyzer::run_shared_rust_analyzer_lsp_session(connection, provider);
375    let join_result = threads.join();
376
377    match (result, join_result) {
378        (Ok(()), Ok(())) => Ok(()),
379        (Err(session), Err(threads)) => anyhow::bail!("{session}\n{threads}"),
380        (Err(error), _) | (_, Err(error)) => Err(error),
381    }
382}
383
384fn lsp_stream_connection(stream: IpcStream) -> anyhow::Result<(Connection, LspStreamThreads)> {
385    let reader_stream = stream.try_clone()?;
386    let mut reader = BufReader::new(reader_stream);
387    let initialize = Message::read(&mut reader)?.ok_or_else(|| {
388        anyhow::anyhow!("lsp client disconnected before sending initialize request")
389    })?;
390    validate_initialize(&initialize)?;
391    let (writer_sender, writer_receiver) = unbounded::<Message>();
392    let (reader_sender, reader_receiver) = unbounded::<Message>();
393    reader_sender.send(initialize)?;
394
395    let reader = thread::spawn(move || {
396        while let Some(message) = Message::read(&mut reader)? {
397            if reader_sender.send(message).is_err() {
398                break;
399            }
400        }
401
402        Ok(())
403    });
404    let writer = thread::spawn(move || {
405        let mut writer = stream;
406        for message in writer_receiver {
407            message.write(&mut writer)?;
408        }
409
410        Ok(())
411    });
412
413    Ok((
414        Connection {
415            sender: writer_sender,
416            receiver: reader_receiver,
417        },
418        LspStreamThreads { reader, writer },
419    ))
420}
421
422fn validate_initialize(message: &Message) -> anyhow::Result<()> {
423    let Message::Request(request) = message else {
424        anyhow::bail!("lsp client sent a non-request message before initialize");
425    };
426    if request.method != "initialize" {
427        anyhow::bail!(
428            "lsp client sent {} before initialize request",
429            request.method
430        );
431    }
432
433    Ok(())
434}
435
436fn backend_snapshot_from_shared(
437    snapshot: ra_ap_rust_analyzer::SharedAnalyzerBackendSnapshot,
438) -> BackendSnapshot {
439    BackendSnapshot {
440        key: backend_key_from_shared(snapshot.key),
441        client_sessions: snapshot.client_sessions,
442        overlay_sessions: snapshot.overlay_sessions,
443        overlay_files: snapshot.overlay_files,
444        workspace_loads: snapshot
445            .workspace_loads
446            .into_iter()
447            .map(workspace_snapshot_from_shared)
448            .collect(),
449    }
450}
451
452fn workspace_snapshot_from_shared(
453    summary: ra_ap_rust_analyzer::WorkspaceSummary,
454) -> WorkspaceSnapshot {
455    WorkspaceSnapshot {
456        root: summary.root,
457        manifest: summary.manifest,
458        packages: summary.packages,
459        files: summary.files,
460        proc_macro_server: summary.proc_macro_server,
461    }
462}
463
464fn backend_key_from_shared(key: ra_ap_rust_analyzer::SharedAnalyzerBackendKey) -> BackendKey {
465    BackendKey {
466        shared_world: SharedWorldKey {
467            rust_analyzer_version: key.shared_world.rust_analyzer_version,
468            toolchain: key.shared_world.toolchain,
469            sysroot: key.shared_world.sysroot,
470            cargo_target: key.shared_world.cargo_target,
471            config: SharedWorldConfigKey {
472                cargo: cargo_config_key_from_shared(key.shared_world.config.cargo),
473            },
474            load: load_key_from_shared(key.shared_world.load),
475        },
476        workspace_view: WorkspaceViewKey {
477            workspace_roots: key.workspace_view.workspace_roots,
478            analysis: AnalysisConfigKey {
479                initialization_options: key.workspace_view.analysis.initialization_options,
480                workspace_configuration: key.workspace_view.analysis.workspace_configuration,
481            },
482        },
483    }
484}
485
486fn cargo_config_key_from_shared(
487    key: ra_ap_rust_analyzer::SharedAnalyzerCargoConfigKey,
488) -> CargoConfigKey {
489    CargoConfigKey {
490        all_targets: key.all_targets,
491        features: key.features,
492        target: key.target,
493        sysroot: key.sysroot,
494        sysroot_src: key.sysroot_src,
495        rustc_source: key.rustc_source,
496        extra_includes: key.extra_includes,
497        cfg_overrides: key.cfg_overrides,
498        wrap_rustc_in_build_scripts: key.wrap_rustc_in_build_scripts,
499        invocation_strategy: key.invocation_strategy,
500        run_build_script_command: key.run_build_script_command,
501        extra_args: key.extra_args,
502        extra_env: key.extra_env,
503        target_dir_config: key.target_dir_config,
504        set_test: key.set_test,
505        no_deps: key.no_deps,
506        metadata_extra_args: key.metadata_extra_args,
507    }
508}
509
510fn load_key_from_shared(key: ra_ap_rust_analyzer::SharedAnalyzerLoadKey) -> SharedWorldLoadKey {
511    SharedWorldLoadKey {
512        load_out_dirs_from_check: key.load_out_dirs_from_check,
513        proc_macro_server: match key.proc_macro_server {
514            ra_ap_rust_analyzer::SharedAnalyzerProcMacroServerKey::None => ProcMacroServerKey::None,
515            ra_ap_rust_analyzer::SharedAnalyzerProcMacroServerKey::Sysroot => {
516                ProcMacroServerKey::Sysroot
517            }
518            ra_ap_rust_analyzer::SharedAnalyzerProcMacroServerKey::Explicit(path) => {
519                ProcMacroServerKey::Explicit(path)
520            }
521        },
522        prefill_caches: key.prefill_caches,
523        num_worker_threads: key.num_worker_threads,
524        proc_macro_processes: key.proc_macro_processes,
525    }
526}
527
528fn validate_client(client: &ClientInfo) -> Option<ProtocolError> {
529    (client.protocol_version != analyzed_ipc::PROTOCOL_VERSION).then(|| ProtocolError {
530        message: format!(
531            "unsupported protocol version {}, expected {}",
532            client.protocol_version,
533            analyzed_ipc::PROTOCOL_VERSION
534        ),
535    })
536}