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#[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}