serlib/
lib.rs

1pub mod platform;
2pub mod plist;
3pub mod systemd;
4
5use std::process::Command;
6use std::sync::atomic::{AtomicBool, Ordering};
7
8static VERBOSE: AtomicBool = AtomicBool::new(false);
9
10/// Set verbose mode. When enabled, all executed commands are printed to stderr.
11pub fn set_verbose(verbose: bool) {
12    VERBOSE.store(verbose, Ordering::SeqCst);
13}
14
15/// Check if verbose mode is enabled.
16pub fn is_verbose() -> bool {
17    VERBOSE.load(Ordering::SeqCst)
18}
19
20/// Run a command, optionally logging it to stderr if verbose mode is enabled.
21/// Returns the command output.
22pub fn run_command(cmd: &mut Command) -> std::io::Result<std::process::Output> {
23    if is_verbose() {
24        let program = cmd.get_program().to_string_lossy();
25        let args: Vec<_> = cmd.get_args().map(|a| a.to_string_lossy()).collect();
26        eprintln!("+ {} {}", program, args.join(" "));
27    }
28    cmd.output()
29}
30
31/// Spawn a command, optionally logging it to stderr if verbose mode is enabled.
32/// Returns the child process.
33pub fn spawn_command(cmd: &mut Command) -> std::io::Result<std::process::Child> {
34    if is_verbose() {
35        let program = cmd.get_program().to_string_lossy();
36        let args: Vec<_> = cmd.get_args().map(|a| a.to_string_lossy()).collect();
37        eprintln!("+ {} {}", program, args.join(" "));
38    }
39    cmd.spawn()
40}
41
42/// Run a command and wait for status, optionally logging it to stderr if verbose mode is enabled.
43pub fn run_command_status(cmd: &mut Command) -> std::io::Result<std::process::ExitStatus> {
44    if is_verbose() {
45        let program = cmd.get_program().to_string_lossy();
46        let args: Vec<_> = cmd.get_args().map(|a| a.to_string_lossy()).collect();
47        eprintln!("+ {} {}", program, args.join(" "));
48    }
49    cmd.status()
50}
51
52/// Represents a calendar-based schedule for running services.
53/// Fields are optional - None means "any" (like * in cron).
54#[derive(Debug, Clone, Default)]
55pub struct CalendarSchedule {
56    /// Month (1-12)
57    pub month: Option<u8>,
58    /// Day of month (1-31)
59    pub day: Option<u8>,
60    /// Day of week (0=Sunday, 1=Monday, ..., 6=Saturday)
61    pub weekday: Option<u8>,
62    /// Hour (0-23)
63    pub hour: Option<u8>,
64    /// Minute (0-59)
65    pub minute: Option<u8>,
66}
67
68impl CalendarSchedule {
69    /// Convert to systemd OnCalendar format.
70    /// Examples: "*-*-* 03:00:00" (daily at 3am), "Mon *-*-* 00:00:00" (every Monday)
71    pub fn to_systemd_oncalendar(&self) -> String {
72        let weekday_str = match self.weekday {
73            Some(0) => "Sun ",
74            Some(1) => "Mon ",
75            Some(2) => "Tue ",
76            Some(3) => "Wed ",
77            Some(4) => "Thu ",
78            Some(5) => "Fri ",
79            Some(6) => "Sat ",
80            _ => "",
81        };
82
83        let month = self
84            .month
85            .map(|m| format!("{:02}", m))
86            .unwrap_or_else(|| "*".to_string());
87        let day = self
88            .day
89            .map(|d| format!("{:02}", d))
90            .unwrap_or_else(|| "*".to_string());
91        let hour = self
92            .hour
93            .map(|h| format!("{:02}", h))
94            .unwrap_or_else(|| "*".to_string());
95        let minute = self
96            .minute
97            .map(|m| format!("{:02}", m))
98            .unwrap_or_else(|| "00".to_string());
99
100        format!("{weekday_str}*-{month}-{day} {hour}:{minute}:00")
101    }
102
103    /// Convert to launchd StartCalendarInterval dictionary entries.
104    pub fn to_launchd_dict(&self) -> Vec<(String, i64)> {
105        let mut entries = Vec::new();
106        if let Some(month) = self.month {
107            entries.push(("Month".to_string(), month as i64));
108        }
109        if let Some(day) = self.day {
110            entries.push(("Day".to_string(), day as i64));
111        }
112        if let Some(weekday) = self.weekday {
113            entries.push(("Weekday".to_string(), weekday as i64));
114        }
115        if let Some(hour) = self.hour {
116            entries.push(("Hour".to_string(), hour as i64));
117        }
118        if let Some(minute) = self.minute {
119            entries.push(("Minute".to_string(), minute as i64));
120        }
121        entries
122    }
123
124    /// Format schedule for human-readable display.
125    pub fn display(&self) -> String {
126        let mut parts = Vec::new();
127
128        if let Some(weekday) = self.weekday {
129            let days = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
130            if let Some(day) = days.get(weekday as usize) {
131                parts.push(day.to_string());
132            }
133        }
134
135        if let Some(day) = self.day {
136            parts.push(format!("day {}", day));
137        }
138
139        if let Some(hour) = self.hour {
140            let minute = self.minute.unwrap_or(0);
141            parts.push(format!("{:02}:{:02}", hour, minute));
142        }
143
144        if parts.is_empty() {
145            "scheduled".to_string()
146        } else {
147            parts.join(" ")
148        }
149    }
150}
151
152#[derive(Debug, Clone)]
153pub struct ServiceDetails {
154    pub name: String,
155    pub program: String,
156    pub arguments: Vec<String>,
157    pub working_directory: Option<String>,
158    pub run_at_load: bool,
159    pub keep_alive: bool,
160    pub env_file: Option<String>,
161    pub env_vars: Vec<(String, String)>,
162    pub after: Vec<String>,
163    pub schedule: Option<CalendarSchedule>,
164}
165
166#[derive(Debug, Clone)]
167pub struct FsServiceDetails {
168    pub service: ServiceDetails,
169    pub path: String,
170    pub enabled: bool,
171    pub running: bool,
172}