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}