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}