Skip to main content

dev_prune/daemon/
mod.rs

1// Copyright 2026 VKrishna04
2// SPDX-License-Identifier: Apache-2.0
3
4// Cross-platform daemon/scheduler management.
5//
6// Installs, uninstalls, and checks the status of a background task that runs
7// `dev-prune run` on a schedule. Platform-specific implementations:
8// - Windows: Task Scheduler (schtasks)
9// - macOS: LaunchAgent (.plist)
10// - Linux: systemd user timer
11
12mod linux;
13mod macos;
14mod windows;
15
16use anyhow::Result;
17use std::fmt;
18
19/// Status of the background daemon task.
20pub enum DaemonStatus {
21    Installed,
22    NotInstalled,
23    Unknown(String),
24}
25
26impl fmt::Display for DaemonStatus {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        match self {
29            DaemonStatus::Installed => write!(f, "Installed"),
30            DaemonStatus::NotInstalled => write!(f, "Not Installed"),
31            DaemonStatus::Unknown(msg) => write!(f, "Unknown: {}", msg),
32        }
33    }
34}
35
36/// Install the OS-native daemon/scheduled task, firing every `interval_days` days.
37///
38/// The scheduled command always passes `--yes`: there is no terminal attached to a
39/// scheduler-launched process, so a run that stopped to ask for confirmation would
40/// simply abort every time. Safety still comes from the idle check and lockfile
41/// enforcement, both of which the daemon run performs normally.
42pub fn install_daemon(interval_days: u64) -> Result<()> {
43    let interval_days = interval_days.max(1);
44    #[cfg(target_os = "windows")]
45    {
46        windows::install(interval_days)
47    }
48    #[cfg(target_os = "macos")]
49    {
50        macos::install(interval_days)
51    }
52    #[cfg(target_os = "linux")]
53    {
54        linux::install(interval_days)
55    }
56    #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
57    {
58        anyhow::bail!("Unsupported operating system for daemon installation");
59    }
60}
61
62/// Uninstall the daemon/scheduled task.
63pub fn uninstall_daemon() -> Result<()> {
64    #[cfg(target_os = "windows")]
65    {
66        windows::uninstall()
67    }
68    #[cfg(target_os = "macos")]
69    {
70        macos::uninstall()
71    }
72    #[cfg(target_os = "linux")]
73    {
74        linux::uninstall()
75    }
76    #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
77    {
78        anyhow::bail!("Unsupported operating system for daemon uninstallation");
79    }
80}
81
82/// Check if the daemon is installed and its status.
83pub fn daemon_status() -> Result<DaemonStatus> {
84    #[cfg(target_os = "windows")]
85    {
86        windows::status()
87    }
88    #[cfg(target_os = "macos")]
89    {
90        macos::status()
91    }
92    #[cfg(target_os = "linux")]
93    {
94        linux::status()
95    }
96    #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
97    {
98        anyhow::bail!("Unsupported operating system for daemon status");
99    }
100}
101
102/// The binary path to register with the scheduler.
103///
104/// Not `current_exe()`: a scheduled task outlives the process that created it, so the
105/// path it records has to outlive it too. See [`crate::setup::stable_exe_path`] for what
106/// goes wrong when it does not.
107pub fn get_exe_path() -> std::path::PathBuf {
108    crate::setup::stable_exe_path()
109}
110
111/// The binary the installed scheduler entry will actually run, when that can be read.
112///
113/// `None` means "could not determine", never "nothing is registered" — use
114/// [`daemon_status`] for that question. This exists so `devp doctor` can tell a working
115/// scheduler apart from one still pointing at a directory that has since been deleted,
116/// which is otherwise completely silent: the task keeps reporting itself as `Ready` and
117/// fails the instant it fires, every interval, forever.
118pub fn registered_exe_path() -> Option<std::path::PathBuf> {
119    #[cfg(target_os = "windows")]
120    {
121        windows::registered_exe_path()
122    }
123    #[cfg(target_os = "macos")]
124    {
125        macos::registered_exe_path()
126    }
127    #[cfg(target_os = "linux")]
128    {
129        linux::registered_exe_path()
130    }
131    #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
132    {
133        None
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_daemon_status_display() {
143        assert_eq!(DaemonStatus::Installed.to_string(), "Installed");
144        assert_eq!(DaemonStatus::NotInstalled.to_string(), "Not Installed");
145        assert_eq!(
146            DaemonStatus::Unknown("error".into()).to_string(),
147            "Unknown: error"
148        );
149    }
150}