Skip to main content

lean_ctx/
daemon.rs

1use std::fs;
2use std::path::PathBuf;
3use std::process::Command;
4use std::sync::atomic::{AtomicBool, Ordering};
5
6use anyhow::{Context, Result};
7
8use crate::ipc;
9
10/// True once this process is the long-lived foreground daemon
11/// (`serve --_foreground-daemon`). Sessions delegate index builds to the daemon
12/// (#460); the daemon itself must never delegate to itself, so it checks this.
13static IS_FOREGROUND_DAEMON: AtomicBool = AtomicBool::new(false);
14
15/// Mark this process as the foreground daemon. Called once at daemon init.
16pub fn mark_foreground_daemon() {
17    IS_FOREGROUND_DAEMON.store(true, Ordering::Relaxed);
18}
19
20/// Whether this process is the long-lived foreground daemon.
21#[must_use]
22pub fn is_foreground_daemon() -> bool {
23    IS_FOREGROUND_DAEMON.load(Ordering::Relaxed)
24}
25
26fn data_dir() -> PathBuf {
27    ipc::endpoint_dir()
28}
29
30pub fn daemon_pid_path() -> PathBuf {
31    data_dir().join("daemon.pid")
32}
33
34/// The daemon's process identity, next to `daemon.pid` (#2176 review #1).
35/// A separate file keeps `daemon.pid` a plain PID, which earlier releases
36/// (still running MCP servers during an update) read and parse.
37fn daemon_identity_path() -> PathBuf {
38    data_dir().join("daemon.identity.json")
39}
40
41#[derive(serde::Serialize, serde::Deserialize)]
42struct DaemonIdentityFile {
43    pid: u32,
44    identity: ipc::process::ProcessIdentity,
45}
46
47/// The recorded daemon: the PID from `daemon.pid` and, when the identity
48/// file names the same PID, the identity recorded at start. A PID file of an
49/// earlier release has no identity; it is gated by command line only.
50#[must_use]
51pub fn read_daemon_record() -> Option<ipc::process::RecordedPid> {
52    let pid = read_daemon_pid()?;
53    let identity = fs::read(daemon_identity_path())
54        .ok()
55        .filter(|bytes| bytes.len() <= 16 * 1024)
56        .and_then(|bytes| serde_json::from_slice::<DaemonIdentityFile>(&bytes).ok())
57        .filter(|file| file.pid == pid)
58        .map(|file| file.identity);
59    Some(ipc::process::RecordedPid { pid, identity })
60}
61
62fn remove_daemon_record() {
63    let _ = fs::remove_file(daemon_pid_path());
64    let _ = fs::remove_file(daemon_identity_path());
65}
66
67/// The PID of the recorded daemon when it really is this user's lean-ctx
68/// daemon: command line `serve --_foreground-daemon`, lean-ctx executable,
69/// same user, and the recorded identity where there is one. Slower than
70/// [`is_daemon_running`] (reads the command line); for `doctor` and `stop`.
71#[must_use]
72pub fn verified_daemon() -> Option<ipc::process::ServiceProcess> {
73    let record = read_daemon_record()?;
74    ipc::process::find_service_processes(
75        &[record],
76        ipc::process::ServiceSweep::Only(ipc::process::ServiceKind::Daemon),
77    )
78    .into_iter()
79    .next()
80}
81
82/// The PID of the running daemon as `daemon status` reports it: this process
83/// when it is the foreground daemon `daemon.pid` names, otherwise
84/// [`verified_daemon`]. The process sweep behind [`verified_daemon`] excludes
85/// its own PID, so the daemon answering its own `/v1/local/daemon` found no
86/// daemon (`running: false`) while `lean-ctx daemon status` saw it.
87#[must_use]
88pub fn running_daemon_pid() -> Option<u32> {
89    let own = std::process::id();
90    if is_recorded_self(is_foreground_daemon(), read_daemon_pid(), own) {
91        return Some(own);
92    }
93    verified_daemon().map(|daemon| daemon.pid)
94}
95
96/// Whether this process is the recorded daemon: it runs as the foreground
97/// daemon and `daemon.pid` names its PID. Being the foreground daemon alone
98/// is not enough: a second daemon that lost the start race is not the one
99/// `daemon.pid` and the socket belong to.
100fn is_recorded_self(foreground: bool, recorded: Option<u32>, own: u32) -> bool {
101    foreground && recorded == Some(own)
102}
103
104/// Releases up to 3.12.0 kept `daemon.pid` and `daemon.sock` in the OS
105/// data-local directory. A daemon such a release started is stopped before a
106/// new one starts and by `stop_daemon`, so the two never run side by side. Only
107/// a live `lean-ctx` process is signalled, never a reused PID. A process with a
108/// directory override (a test, an isolated instance) never touches it: that
109/// daemon belongs to the default install (GH #2007).
110fn stop_legacy_daemon() {
111    if cfg!(test) || crate::core::paths::dir_override_active() {
112        return;
113    }
114    let legacy = ipc::legacy_endpoint_dir();
115    if legacy == data_dir() {
116        return;
117    }
118    let pid_path = legacy.join("daemon.pid");
119    let Some(pid) = ipc::process::read_pid_file(&pid_path) else {
120        return;
121    };
122    // Only when the PID still is a lean-ctx daemon of this user (command
123    // line, executable, identity): a name match alone also matched MCP
124    // servers on Windows, where PIDs are reused quickly (#2176 review #1).
125    let report = ipc::process::stop_services(
126        &[ipc::process::RecordedPid::bare(pid)],
127        ipc::process::ServiceSweep::Only(ipc::process::ServiceKind::Daemon),
128    );
129    if !report.stopped.is_empty() {
130        eprintln!("Stopped the daemon of an earlier release (PID {pid}).");
131    }
132    let _ = fs::remove_file(&pid_path);
133    #[cfg(unix)]
134    let _ = fs::remove_file(legacy.join("daemon.sock"));
135}
136
137pub fn daemon_addr() -> ipc::DaemonAddr {
138    ipc::DaemonAddr::default_for_current_os()
139}
140
141/// Fast check on the hot path (every MCP request): the recorded PID is
142/// alive and, when the daemon recorded its identity, still that process. A
143/// PID file of an earlier release has no identity; [`verified_daemon`] also
144/// checks the command line.
145pub fn is_daemon_running() -> bool {
146    let Some(record) = read_daemon_record() else {
147        return false;
148    };
149    if record.pid == std::process::id() {
150        return true;
151    }
152    let pid_alive = ipc::process::is_alive(record.pid);
153    match record.identity.as_ref() {
154        Some(identity) if pid_alive && ipc::process::is_same_process(record.pid, identity) => {
155            return true;
156        }
157        // A PID file without identity (an earlier release, or a PID reused
158        // after a reboot): only a verified lean-ctx daemon counts (#2176 VM).
159        // Rare, so the slower command-line gate is fine. A live process that
160        // cannot be verified keeps its files: `daemon start`/`stop` decide.
161        None if pid_alive => return legacy_record_verified(record.pid),
162        Some(_) | None => {}
163    }
164    remove_daemon_record();
165    ipc::cleanup(&daemon_addr());
166    false
167}
168
169/// [`verified_daemon`] for an identity-less PID file, remembered per PID for
170/// this process: the hot path must not read command lines on every request.
171fn legacy_record_verified(pid: u32) -> bool {
172    // Bit 33: cached; bit 32: verified; low 32 bits: the PID.
173    static LAST: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
174    let last = LAST.load(Ordering::Relaxed);
175    if last & (1 << 33) != 0 && last as u32 == pid {
176        return last & (1 << 32) != 0;
177    }
178    let verified = verified_daemon().is_some_and(|daemon| daemon.pid == pid);
179    LAST.store(
180        (1 << 33) | (u64::from(verified) << 32) | u64::from(pid),
181        Ordering::Relaxed,
182    );
183    verified
184}
185
186/// The PID in `daemon.pid`. Empty, garbage, 0, negative or overflowing
187/// content is warned about and the stale file removed; it never becomes a
188/// signal target (`ipc::process::read_pid_file`).
189pub fn read_daemon_pid() -> Option<u32> {
190    ipc::process::read_pid_file(&daemon_pid_path())
191}
192
193/// Exclusive, bounded-wait lock that serializes the daemon-start critical
194/// section (liveness check → spawn → PID write). Several MCP servers launching
195/// at once (Claude Code + OpenCode + Cursor) would otherwise all pass the
196/// `is_daemon_running()` check in the TOCTOU window and each spawn a daemon —
197/// the process proliferation seen in #453. The advisory flock is tied to the
198/// open fd, so it is released automatically if a holder crashes; the bounded
199/// wait keeps a wedged holder from blocking startup forever (the
200/// `is_daemon_running()` re-check remains the last line of defense).
201fn acquire_start_lock() -> Option<fs::File> {
202    use fs2::FileExt;
203    let lock_path = data_dir().join("daemon.start.lock");
204    if let Some(parent) = lock_path.parent() {
205        let _ = fs::create_dir_all(parent);
206    }
207    let file = fs::OpenOptions::new()
208        .create(true)
209        .write(true)
210        .truncate(false)
211        .open(&lock_path)
212        .ok()?;
213    let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
214    loop {
215        match file.try_lock_exclusive() {
216            Ok(()) => return Some(file),
217            Err(_) if std::time::Instant::now() < deadline => {
218                std::thread::sleep(std::time::Duration::from_millis(50));
219            }
220            Err(_) => return None,
221        }
222    }
223}
224
225pub fn start_daemon(args: &[String]) -> Result<()> {
226    start_daemon_with_binary(args, None)
227}
228
229/// Source installs launch the verified installed copy, preserving the caller's CWD.
230pub fn start_daemon_with_binary(args: &[String], binary: Option<&std::path::Path>) -> Result<()> {
231    // Held for the whole critical section; released when `_start_lock` drops.
232    let _start_lock = acquire_start_lock();
233
234    // #2176 VM: a reused PID in daemon.pid (any live process) must not block
235    // the start; only a verified lean-ctx daemon of this user does.
236    if let Some(daemon) = verified_daemon() {
237        anyhow::bail!(
238            "Daemon already running (PID {}). Use --stop to stop it first.",
239            daemon.pid
240        );
241    }
242    if let Some(pid) = read_daemon_pid() {
243        eprintln!("{}", stale_pid_message(pid));
244        remove_daemon_record();
245    }
246
247    stop_legacy_daemon();
248    ipc::cleanup(&daemon_addr());
249
250    if let Ok(data_dir) = crate::core::data_dir::lean_ctx_data_dir() {
251        crate::config_io::cleanup_legacy_backups(&data_dir);
252    }
253
254    let exe = binary.map_or_else(
255        || std::path::PathBuf::from(crate::core::portable_binary::resolve_portable_binary()),
256        std::path::Path::to_path_buf,
257    );
258
259    let mut cmd_args = vec!["serve".to_string()];
260    for arg in args {
261        if arg == "--daemon" || arg == "-d" {
262            continue;
263        }
264        cmd_args.push(arg.clone());
265    }
266    cmd_args.push("--_foreground-daemon".to_string());
267
268    let log_dir = data_dir();
269    let _ = fs::create_dir_all(&log_dir);
270    let stderr_log = log_dir.join("daemon-stderr.log");
271    let stderr_file = fs::OpenOptions::new()
272        .create(true)
273        .write(true)
274        .truncate(true)
275        .open(&stderr_log);
276    let stderr_cfg = match stderr_file {
277        Ok(f) => ipc::process::DetachedStderr::File(f),
278        Err(error) => {
279            eprintln!(
280                "Cannot open {}: {error}; daemon stderr disabled.",
281                stderr_log.display()
282            );
283            ipc::process::DetachedStderr::Null
284        }
285    };
286
287    let mut cmd = Command::new(&exe);
288    cmd.args(&cmd_args);
289    // #356: if the *spawner* is a launchd-standalone process (e.g. the proxy
290    // auto-starting the daemon), the spawned daemon's ppid is the spawner — not
291    // 1 — so `getppid()`-based detection would miss it and the daemon's TCC path
292    // guards would stay off. Propagate the marker explicitly so the daemon
293    // treats itself as standalone regardless of where it sits in the tree.
294    if crate::core::pathutil::process_is_tcc_standalone() {
295        cmd.env("LEAN_CTX_TCC_STANDALONE", "1");
296    }
297    // Detached spawn: on Windows the daemon leaves the caller's job and
298    // inherits none of its handles (GL #545, #2176).
299    let child = ipc::process::spawn_detached(&mut cmd, stderr_cfg)
300        .with_context(|| format!("failed to spawn daemon: {}", exe.display()))?;
301
302    let pid = child.id();
303    write_pid_file(pid)?;
304
305    std::thread::sleep(std::time::Duration::from_millis(200));
306
307    if !ipc::process::is_alive(pid) {
308        remove_daemon_record();
309        let stderr_content = fs::read_to_string(&stderr_log).unwrap_or_default();
310        let stderr_trimmed = stderr_content.trim();
311        if stderr_trimmed.is_empty() {
312            anyhow::bail!("Daemon process exited immediately. Check logs for errors.");
313        }
314        anyhow::bail!("Daemon process exited immediately:\n{stderr_trimmed}");
315    }
316
317    let addr = daemon_addr();
318    if crate::core::protocol::meta_visible() {
319        eprintln!(
320            "lean-ctx daemon started (PID {pid})\n  Endpoint: {}\n  PID file: {}",
321            addr.display(),
322            daemon_pid_path().display()
323        );
324    }
325
326    Ok(())
327}
328
329/// What `stop_daemon` says when the recorded daemon had already exited. A
330/// stale PID file is normal after a reboot, a crash or an editor closing, so
331/// the line is neutral rather than alarming (#2176).
332fn stale_pid_message(pid: u32) -> String {
333    format!("No daemon was running (stale PID file for PID {pid} removed).")
334}
335
336/// What `stop_daemon` found for the active data dir.
337#[derive(Debug, Clone, Copy, PartialEq, Eq)]
338pub enum DaemonStop {
339    /// The daemon recorded in this data dir was stopped.
340    Stopped(u32),
341    /// No daemon runs for this data dir; no process was signalled.
342    NotRunning,
343}
344
345/// The line `serve --stop` and `daemon stop` print, and exit 1 after, when no
346/// daemon runs for the active data dir. Names the dir so that a caller with
347/// a scratch `LEAN_CTX_DATA_DIR` sees which instance it asked about.
348#[must_use]
349pub fn not_running_message() -> String {
350    format!(
351        "No lean-ctx daemon is running for data dir {}. Nothing was stopped.",
352        data_dir().display()
353    )
354}
355
356/// Stop the daemon of the active data dir (`LEAN_CTX_DATA_DIR`, else the
357/// default under HOME), resolved only through that dir's PID file, identity
358/// file and socket — never through a default path, a process name or a
359/// service name. The autostart service is stopped too, but only when it runs
360/// exactly this daemon's PID (5 Nov 2026: `systemctl --user stop` by unit
361/// name stopped the user's real daemon from a scratch data dir).
362pub fn stop_daemon() -> Result<DaemonStop> {
363    stop_legacy_daemon();
364
365    // No record in this data dir: nothing is signalled, nothing is removed.
366    let Some(record) = read_daemon_record() else {
367        return Ok(DaemonStop::NotRunning);
368    };
369    let pid = record.pid;
370
371    // #2176 review #1: the PID file survives a reboot or crash, and Windows
372    // reuses PIDs quickly — the PID may now be an MCP server, an editor or
373    // any other process. It is signalled only while it is still this user's
374    // lean-ctx daemon (command line, executable, recorded identity).
375    let Some(daemon) = ipc::process::find_service_processes(
376        std::slice::from_ref(&record),
377        ipc::process::ServiceSweep::Only(ipc::process::ServiceKind::Daemon),
378    )
379    .into_iter()
380    .next() else {
381        // Not provably the daemon: never signalled. Should the check have
382        // failed for a real daemon (unreadable command line, or a daemon in
383        // the caller's own process group), the endpoint shutdown still ends
384        // it before its endpoint files are removed. When that shutdown was
385        // accepted and the PID then ended, a daemon was stopped: that is
386        // reported and exits 0, never "Nothing was stopped".
387        let stopped_via_endpoint =
388            ipc::process::is_alive(pid) && try_http_shutdown() && wait_for_exit(pid);
389        ipc::cleanup(&daemon_addr());
390        remove_daemon_record();
391        if stopped_via_endpoint {
392            eprintln!("lean-ctx daemon stopped (PID {pid}).");
393            return Ok(DaemonStop::Stopped(pid));
394        }
395        eprintln!("{}", stale_pid_message(pid));
396        return Ok(DaemonStop::NotRunning);
397    };
398
399    // A supervised daemon is stopped through its supervisor, so that
400    // `Restart=`/`KeepAlive` does not bring it back — only when the service
401    // runs this very PID; a service of another instance is never addressed.
402    if crate::daemon_autostart::supervised_pid() == Some(daemon.pid) {
403        crate::daemon_autostart::stop();
404        for _ in 0..30 {
405            if !daemon.is_running() {
406                break;
407            }
408            std::thread::sleep(std::time::Duration::from_millis(100));
409        }
410    }
411
412    // The HTTP shutdown goes to this instance's daemon endpoint, never to a PID.
413    if try_http_shutdown() {
414        for _ in 0..30 {
415            std::thread::sleep(std::time::Duration::from_millis(100));
416            if !daemon.is_running() {
417                break;
418            }
419        }
420    }
421
422    if daemon.is_running() {
423        daemon.signal(ipc::process::StopSignal::Terminate);
424        for _ in 0..20 {
425            std::thread::sleep(std::time::Duration::from_millis(100));
426            if !daemon.is_running() {
427                break;
428            }
429        }
430    }
431
432    if daemon.is_running() {
433        eprintln!("Daemon (PID {pid}) did not stop gracefully, force killing.");
434        daemon.signal(ipc::process::StopSignal::Kill);
435        std::thread::sleep(std::time::Duration::from_millis(200));
436    }
437
438    remove_daemon_record();
439    ipc::cleanup(&daemon_addr());
440    eprintln!("lean-ctx daemon stopped (PID {pid}).");
441
442    // #1292: stopping one daemon ends that daemon only. A machine-wide
443    // name-based sweep here reached every other lean-ctx process — the real
444    // user's daemon and proxy under a sandbox HOME, other worktrees' daemons,
445    // other agents' `lean-ctx -c` commands — from `setup`, `daemon stop` and
446    // `serve`. The lifecycle commands (`stop`, `restart`, `config apply`,
447    // `uninstall`, `dev-install`) sweep the background services explicitly
448    // with `ipc::process::stop_services`, never MCP servers (#2176).
449    Ok(DaemonStop::Stopped(pid))
450}
451
452/// Wait up to 3 s for `pid` to end after an accepted endpoint shutdown.
453fn wait_for_exit(pid: u32) -> bool {
454    for _ in 0..30 {
455        if !ipc::process::is_alive(pid) {
456            return true;
457        }
458        std::thread::sleep(std::time::Duration::from_millis(100));
459    }
460    !ipc::process::is_alive(pid)
461}
462
463fn try_http_shutdown() -> bool {
464    let Ok(rt) = tokio::runtime::Runtime::new() else {
465        return false;
466    };
467
468    rt.block_on(async {
469        crate::daemon_client::daemon_request("POST", "/v1/shutdown", "")
470            .await
471            .is_ok()
472    })
473}
474
475pub fn daemon_status() -> String {
476    let addr = daemon_addr();
477    if let Some(pid) = read_daemon_pid() {
478        if verified_daemon().is_some() {
479            let listening = addr.is_listening();
480            return format!(
481                "Daemon running (PID {pid})\n  Endpoint: {} ({})\n  PID file: {}",
482                addr.display(),
483                if listening { "ready" } else { "missing" },
484                daemon_pid_path().display()
485            );
486        }
487        return format!(
488            "Daemon not running for data dir {} (stale PID file for PID {pid})",
489            data_dir().display()
490        );
491    }
492    format!("Daemon not running for data dir {}", data_dir().display())
493}
494
495fn write_pid_file(pid: u32) -> Result<()> {
496    let pid_path = daemon_pid_path();
497    if let Some(parent) = pid_path.parent() {
498        fs::create_dir_all(parent)
499            .with_context(|| format!("cannot create dir: {}", parent.display()))?;
500    }
501    // Atomic (temp + rename): a reader never sees an empty file, which
502    // `read_daemon_pid` would remove as stale.
503    crate::core::atomic_fs::try_atomic_write(&pid_path, pid.to_string().as_bytes(), None)
504        .with_context(|| format!("cannot write PID file: {}", pid_path.display()))?;
505    // The identity defends the PID against reuse after a reboot or crash;
506    // without one (unsupported platform) the PID is gated by command line.
507    let identity_path = daemon_identity_path();
508    match ipc::process::identity(pid) {
509        Some(identity) => {
510            let file = DaemonIdentityFile { pid, identity };
511            if let Ok(bytes) = serde_json::to_vec(&file) {
512                let _ = crate::core::updater::atomic_write_bytes(&identity_path, &bytes);
513            }
514        }
515        None => {
516            let _ = fs::remove_file(&identity_path);
517        }
518    }
519    Ok(())
520}
521
522/// Initialize the foreground-daemon process. Commits the XDG layout pin (and
523/// drains a residual `~/.lean-ctx`) *before* the daemon writes anything, so this
524/// long-running, possibly launchd/systemd-autostarted writer can never
525/// re-collapse config/data/state/cache onto a stray legacy dir (GL #623). The
526/// MCP server pins on its own start; the daemon is the other independent entry
527/// point (e.g. `serve --_foreground-daemon`), so it must heal too. `heal()` is
528/// idempotent and cheap — a no-op once pinned and when no residual dir exists.
529pub fn init_foreground_daemon() -> Result<()> {
530    mark_foreground_daemon();
531    crate::core::layout_pin::heal();
532    // The daemon is the single long-lived owner for local agent-presence
533    // maintenance. Starting this in every short-lived MCP process creates a
534    // reaper swarm precisely when many coding agents connect at once.
535    crate::core::agents::reaper::spawn_reaper();
536    // The daemon starts at login: an Engine without its Cockpit (an update
537    // from a release without one) gets it here, without a command.
538    crate::core::intelligence_runtime::cockpit::install_if_missing_in_background();
539    let pid = std::process::id();
540    write_pid_file(pid)?;
541    Ok(())
542}
543
544/// PIDs of the background services this instance (data dir) recorded: the
545/// daemon PID file, the proxy that answers `/health` on the configured port,
546/// and the Cockpit launch record. Nothing else is ever a stop candidate —
547/// MCP stdio servers, `lean-ctx -c` runners, hooks and other instances'
548/// services have no record here (#2176).
549#[must_use]
550pub fn owned_service_pids() -> Vec<ipc::process::RecordedPid> {
551    let mut pids = Vec::new();
552    pids.extend(read_daemon_record());
553    pids.extend(crate::proxy_autostart::recorded_proxy_pid().map(ipc::process::RecordedPid::bare));
554    // #2176 review #6: the Cockpit record carries the identity it started
555    // with; a record whose PID was reused is not a candidate.
556    pids.extend(crate::core::cockpit_launch_record::read_record().map(|r| {
557        ipc::process::RecordedPid {
558            pid: r.pid,
559            identity: r.identity,
560        }
561    }));
562    pids
563}
564
565/// Stop this instance's recorded background services that `sweep` includes
566/// (see [`owned_service_pids`]); every PID is gated again by command line,
567/// user and process identity in `ipc::process::stop_services`.
568pub fn stop_owned_services(sweep: ipc::process::ServiceSweep) -> ipc::process::ServiceStopReport {
569    ipc::process::stop_services(&owned_service_pids(), sweep)
570}
571
572/// lean-ctx background services of this user that this instance has no
573/// record of. Listed by `stop`, `uninstall` and `doctor`, never stopped
574/// (owner decision 02.11.).
575#[must_use]
576pub fn unrecorded_services() -> Vec<ipc::process::ServiceProcess> {
577    let recorded: Vec<u32> = owned_service_pids().iter().map(|r| r.pid).collect();
578    ipc::process::find_unrecorded_services(&recorded)
579}
580
581/// The two lines `stop`/`uninstall`/`doctor` print for
582/// [`unrecorded_services`] — what still runs, and how to stop it — or `None`.
583#[must_use]
584pub fn unrecorded_services_warning() -> Option<(String, String)> {
585    ipc::process::unrecorded_services_lines(&unrecorded_services(), cfg!(windows))
586}
587
588/// Start a proxy that `restart`/`config apply` stopped again when no
589/// autostart service brought it back (#2176 review #4): detached, with the
590/// arguments it ran with, by the installed binary. `None` when there was
591/// nothing to restore; otherwise the PID or the spawn error.
592pub fn restore_stopped_proxy(
593    report: &ipc::process::ServiceStopReport,
594    autostart_restarted: bool,
595) -> Option<std::io::Result<u32>> {
596    let args = ipc::process::proxy_respawn_args(&report.stopped, autostart_restarted)?;
597    let binary = crate::core::portable_binary::resolve_portable_binary();
598    let mut cmd = Command::new(binary);
599    cmd.args(&args);
600    Some(
601        ipc::process::spawn_detached(&mut cmd, ipc::process::DetachedStderr::Null)
602            .map(|child| child.id()),
603    )
604}
605
606/// Cleanup PID file and IPC endpoint on shutdown.
607pub fn cleanup_daemon_files() {
608    remove_daemon_record();
609    ipc::cleanup(&daemon_addr());
610}
611
612#[cfg(test)]
613mod tests {
614    /// #2176: a stale PID file on `restart` read as an alarm ("is not
615    /// running. Cleaning up stale files."); it is a neutral note now.
616    #[test]
617    fn stale_pid_message_is_neutral() {
618        let line = super::stale_pid_message(36492);
619        assert_eq!(
620            line,
621            "No daemon was running (stale PID file for PID 36492 removed)."
622        );
623        assert!(!line.contains("Cleaning up"));
624    }
625
626    /// The daemon answering its own `/v1/local/daemon` is the running
627    /// daemon when `daemon.pid` names it; the process sweep cannot see it.
628    #[test]
629    fn foreground_daemon_named_by_the_record_is_running() {
630        assert!(super::is_recorded_self(true, Some(4242), 4242));
631        // Not the foreground daemon, another recorded PID, or no record:
632        // fall back to the verified sweep.
633        assert!(!super::is_recorded_self(false, Some(4242), 4242));
634        assert!(!super::is_recorded_self(true, Some(4243), 4242));
635        assert!(!super::is_recorded_self(true, None, 4242));
636    }
637}