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_windowless_upgrade() -> bool {
122    #[cfg(target_os = "windows")]
123    {
124        windows::wants_windowless_upgrade()
125    }
126    #[cfg(not(target_os = "windows"))]
127    {
128        false
129    }
130}
131
132/// Whether the installed scheduler entry still carries the power gates Windows puts on
133/// a bare `schtasks /Create` registration — refuse to start on battery, die on unplug,
134/// never catch up a missed trigger. Only Windows has them; launchd and systemd user
135/// timers run on battery without being asked.
136pub fn wants_power_upgrade() -> bool {
137    #[cfg(target_os = "windows")]
138    {
139        windows::wants_power_upgrade()
140    }
141    #[cfg(not(target_os = "windows"))]
142    {
143        false
144    }
145}
146
147/// Lift those power gates off the installed task, keeping its trigger time, logon type
148/// and binary as they are. No-op on the other platforms.
149pub fn apply_power_settings() -> Result<()> {
150    #[cfg(target_os = "windows")]
151    {
152        windows::apply_power_settings()
153    }
154    #[cfg(not(target_os = "windows"))]
155    {
156        Ok(())
157    }
158}
159
160/// Replace the windowless scheduler binary after an upgrade, when one is in use.
161///
162/// Windows-only: the twin (`devpw.exe`) is a separate build target shipped beside the
163/// managed binary, so replacing that binary without replacing the twin would leave the
164/// daemon running the previous release. The other platforms register the real binary
165/// directly and have nothing to refresh.
166///
167/// It is *placed* from the delivery, never generated here — see the note on
168/// `windows::place_windowless_twin` for the release that learnt why.
169pub fn refresh_windowless_twin() {
170    #[cfg(target_os = "windows")]
171    {
172        windows::refresh_windowless_twin();
173    }
174}
175
176/// The binary the installed scheduler entry will actually run, when that can be read.
177///
178/// `None` means "could not determine", never "nothing is registered" — use
179/// [`daemon_status`] for that question. This exists so `devp doctor` can tell a working
180/// scheduler apart from one still pointing at a directory that has since been deleted,
181/// which is otherwise completely silent: the task keeps reporting itself as `Ready` and
182/// fails the instant it fires, every interval, forever.
183pub fn registered_exe_path() -> Option<std::path::PathBuf> {
184    #[cfg(target_os = "windows")]
185    {
186        windows::registered_exe_path()
187    }
188    #[cfg(target_os = "macos")]
189    {
190        macos::registered_exe_path()
191    }
192    #[cfg(target_os = "linux")]
193    {
194        linux::registered_exe_path()
195    }
196    #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
197    {
198        None
199    }
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205
206    #[test]
207    fn test_daemon_status_display() {
208        assert_eq!(DaemonStatus::Installed.to_string(), "Installed");
209        assert_eq!(DaemonStatus::NotInstalled.to_string(), "Not Installed");
210        assert_eq!(
211            DaemonStatus::Unknown("error".into()).to_string(),
212            "Unknown: error"
213        );
214    }
215}