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
12// Each platform module is only compiled on its own OS. The alternative — compiling
13// all three everywhere under `#![allow(dead_code)]` — silences the lint for genuinely
14// dead items too, which is how orphaned helpers accumulate.
15#[cfg(target_os = "linux")]
16mod linux;
17#[cfg(target_os = "macos")]
18mod macos;
19#[cfg(target_os = "windows")]
20mod windows;
21
22use anyhow::Result;
23use std::fmt;
24
25/// Status of the background daemon task.
26pub enum DaemonStatus {
27    Installed,
28    NotInstalled,
29    Unknown(String),
30}
31
32impl fmt::Display for DaemonStatus {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            DaemonStatus::Installed => write!(f, "Installed"),
36            DaemonStatus::NotInstalled => write!(f, "Not Installed"),
37            DaemonStatus::Unknown(msg) => write!(f, "Unknown: {}", msg),
38        }
39    }
40}
41
42/// Install the OS-native daemon/scheduled task, firing every `interval_days` days.
43///
44/// The scheduled command always passes `--yes`: there is no terminal attached to a
45/// scheduler-launched process, so a run that stopped to ask for confirmation would
46/// simply abort every time. Safety still comes from the idle check and lockfile
47/// enforcement, both of which the daemon run performs normally.
48pub fn install_daemon(interval_days: u64) -> Result<()> {
49    let interval_days = interval_days.max(1);
50    #[cfg(target_os = "windows")]
51    {
52        windows::install(interval_days)
53    }
54    #[cfg(target_os = "macos")]
55    {
56        macos::install(interval_days)
57    }
58    #[cfg(target_os = "linux")]
59    {
60        linux::install(interval_days)
61    }
62    #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
63    {
64        anyhow::bail!("Unsupported operating system for daemon installation");
65    }
66}
67
68/// Uninstall the daemon/scheduled task.
69pub fn uninstall_daemon() -> Result<()> {
70    #[cfg(target_os = "windows")]
71    {
72        windows::uninstall()
73    }
74    #[cfg(target_os = "macos")]
75    {
76        macos::uninstall()
77    }
78    #[cfg(target_os = "linux")]
79    {
80        linux::uninstall()
81    }
82    #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
83    {
84        anyhow::bail!("Unsupported operating system for daemon uninstallation");
85    }
86}
87
88/// Check if the daemon is installed and its status.
89pub fn daemon_status() -> Result<DaemonStatus> {
90    #[cfg(target_os = "windows")]
91    {
92        windows::status()
93    }
94    #[cfg(target_os = "macos")]
95    {
96        macos::status()
97    }
98    #[cfg(target_os = "linux")]
99    {
100        linux::status()
101    }
102    #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
103    {
104        anyhow::bail!("Unsupported operating system for daemon status");
105    }
106}
107
108/// The binary path to register with the scheduler.
109///
110/// Not `current_exe()`: a scheduled task outlives the process that created it, so the
111/// path it records has to outlive it too. See [`crate::setup::stable_exe_path`] for what
112/// goes wrong when it does not.
113pub fn get_exe_path() -> std::path::PathBuf {
114    crate::setup::stable_exe_path()
115}
116
117/// Whether the installed scheduler entry should be re-registered to stop it flashing a
118/// console window at the logged-in user. Only Windows attaches a console to a scheduled
119/// task; the other platforms' schedulers never open a terminal, so there the answer is
120/// always no.
121pub fn wants_hidden_upgrade() -> bool {
122    #[cfg(target_os = "windows")]
123    {
124        windows::wants_hidden_upgrade()
125    }
126    #[cfg(not(target_os = "windows"))]
127    {
128        false
129    }
130}
131
132/// Rebuild the windowless scheduler binary after an upgrade, when one is in use.
133///
134/// Windows-only: the twin (`devpw.exe`) is a patched copy of the managed binary, so
135/// replacing the binary without refreshing the twin would leave the daemon running the
136/// previous release. The other platforms register the real binary directly and have
137/// nothing to refresh.
138pub fn refresh_hidden_twin() {
139    #[cfg(target_os = "windows")]
140    {
141        windows::refresh_hidden_twin();
142    }
143}
144
145/// The binary the installed scheduler entry will actually run, when that can be read.
146///
147/// `None` means "could not determine", never "nothing is registered" — use
148/// [`daemon_status`] for that question. This exists so `devp doctor` can tell a working
149/// scheduler apart from one still pointing at a directory that has since been deleted,
150/// which is otherwise completely silent: the task keeps reporting itself as `Ready` and
151/// fails the instant it fires, every interval, forever.
152pub fn registered_exe_path() -> Option<std::path::PathBuf> {
153    #[cfg(target_os = "windows")]
154    {
155        windows::registered_exe_path()
156    }
157    #[cfg(target_os = "macos")]
158    {
159        macos::registered_exe_path()
160    }
161    #[cfg(target_os = "linux")]
162    {
163        linux::registered_exe_path()
164    }
165    #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
166    {
167        None
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_daemon_status_display() {
177        assert_eq!(DaemonStatus::Installed.to_string(), "Installed");
178        assert_eq!(DaemonStatus::NotInstalled.to_string(), "Not Installed");
179        assert_eq!(
180            DaemonStatus::Unknown("error".into()).to_string(),
181            "Unknown: error"
182        );
183    }
184}