Skip to main content

pitchfork_cli/
boot_manager.rs

1use crate::{Result, env};
2#[cfg(target_os = "linux")]
3use auto_launcher::LinuxLaunchMode;
4#[cfg(target_os = "macos")]
5use auto_launcher::MacOSLaunchMode;
6use auto_launcher::{AutoLaunch, AutoLaunchBuilder};
7use miette::IntoDiagnostic;
8
9#[cfg(any(target_os = "macos", target_os = "linux"))]
10fn build_launcher(
11    app_path: &str,
12    #[cfg(target_os = "macos")] macos_mode: MacOSLaunchMode,
13    #[cfg(target_os = "linux")] linux_mode: LinuxLaunchMode,
14) -> Result<AutoLaunch> {
15    let mut builder = AutoLaunchBuilder::new();
16    builder
17        .set_app_name("pitchfork")
18        .set_app_path(app_path)
19        .set_args(&["supervisor", "run", "--boot"]);
20
21    #[cfg(target_os = "macos")]
22    builder.set_macos_launch_mode(macos_mode);
23
24    #[cfg(target_os = "linux")]
25    builder.set_linux_launch_mode(linux_mode);
26
27    builder.build().into_diagnostic()
28}
29
30pub struct BootManager {
31    /// The launcher matching the current privilege level (used for enable).
32    current: AutoLaunch,
33    /// The other level's launcher (used to detect cross-level registrations).
34    other: AutoLaunch,
35}
36
37impl BootManager {
38    pub fn new() -> Result<Self> {
39        let app_path = env::PITCHFORK_BIN.to_string_lossy().to_string();
40
41        #[cfg(target_os = "macos")]
42        let (current, other) = {
43            let is_root = nix::unistd::Uid::effective().is_root();
44            let (current_mode, other_mode) = if is_root {
45                (
46                    MacOSLaunchMode::LaunchAgentSystem,
47                    MacOSLaunchMode::LaunchAgentUser,
48                )
49            } else {
50                (
51                    MacOSLaunchMode::LaunchAgentUser,
52                    MacOSLaunchMode::LaunchAgentSystem,
53                )
54            };
55            (
56                build_launcher(&app_path, current_mode)?,
57                build_launcher(&app_path, other_mode)?,
58            )
59        };
60
61        #[cfg(target_os = "linux")]
62        let (current, other) = {
63            let is_root = nix::unistd::Uid::effective().is_root();
64            let (current_mode, other_mode) = if is_root {
65                (LinuxLaunchMode::SystemdSystem, LinuxLaunchMode::SystemdUser)
66            } else {
67                (LinuxLaunchMode::SystemdUser, LinuxLaunchMode::SystemdSystem)
68            };
69            (
70                build_launcher(&app_path, current_mode)?,
71                build_launcher(&app_path, other_mode)?,
72            )
73        };
74
75        // On Windows there is no root/user distinction; build two identical
76        // launchers (AutoLaunch does not implement Clone).
77        #[cfg(windows)]
78        let (current, other) = (
79            AutoLaunchBuilder::new()
80                .set_app_name("pitchfork")
81                .set_app_path(&app_path)
82                .set_args(&["supervisor", "run", "--boot"])
83                .build()
84                .into_diagnostic()?,
85            AutoLaunchBuilder::new()
86                .set_app_name("pitchfork")
87                .set_app_path(&app_path)
88                .set_args(&["supervisor", "run", "--boot"])
89                .build()
90                .into_diagnostic()?,
91        );
92
93        // Unsupported platforms: auto_launcher only supports macOS, Linux, and Windows.
94        #[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
95        compile_error!("pitchfork boot management is only supported on macOS, Linux, and Windows");
96
97        Ok(Self { current, other })
98    }
99
100    /// Whether any registration (user- or system-level) exists.
101    pub fn is_enabled(&self) -> Result<bool> {
102        Ok(self.current.is_enabled().into_diagnostic()?
103            || self.other.is_enabled().into_diagnostic()?)
104    }
105
106    /// Whether a registration at the *current* privilege level exists.
107    pub fn is_current_level_enabled(&self) -> Result<bool> {
108        self.current.is_enabled().into_diagnostic()
109    }
110
111    /// Whether a registration at the *other* privilege level exists.
112    /// Used to warn the user about cross-level mismatches.
113    pub fn is_other_level_enabled(&self) -> Result<bool> {
114        self.other.is_enabled().into_diagnostic()
115    }
116
117    /// Register at the current privilege level.
118    ///
119    /// Returns an error if a registration at the other privilege level already
120    /// exists, preventing user-level and system-level entries from coexisting.
121    pub fn enable(&self) -> Result<()> {
122        if self.other.is_enabled().into_diagnostic()? {
123            miette::bail!(
124                "boot start is already registered at the other privilege level; \
125                run `pitchfork boot disable` (with appropriate privileges) to remove \
126                it first"
127            );
128        }
129        self.current.enable().into_diagnostic()
130    }
131
132    /// Remove registrations at *both* levels so cross-level leftovers are also
133    /// cleaned up.
134    pub fn disable(&self) -> Result<()> {
135        // Only disable if registered; propagate real errors (e.g. permission denied).
136        if self.current.is_enabled().into_diagnostic()? {
137            self.current.disable().into_diagnostic()?;
138        }
139        if self.other.is_enabled().into_diagnostic()? {
140            self.other.disable().into_diagnostic()?;
141        }
142        Ok(())
143    }
144}