prs_lib/
systemd_bin.rs

1use std::ffi::OsStr;
2use std::process::{Command, ExitStatus, Stdio};
3
4use anyhow::Result;
5use thiserror::Error;
6
7/// sudo binary.
8pub const SUDO_BIN: &str = "sudo";
9
10/// systemd-run binary.
11pub const SYSTEMD_RUN_BIN: &str = "systemd-run";
12
13/// systemctl binary.
14pub const SYSTEMCTL_BIN: &str = "systemctl";
15
16/// Spawn systemd timer to run the given command.
17///
18/// This may ask for root privileges through sudo.
19pub fn systemd_cmd_timer(time: u32, description: &str, unit: &str, cmd: &[&str]) -> Result<()> {
20    // Remove unit first if it failed before
21    let _ = systemd_remove_timer(unit);
22    let _ = systemctl_reset_failed_timer(unit);
23
24    // TODO: do not set -q flag if in verbose mode?
25    let time = format!("{time}");
26    let mut systemd_cmd = vec![
27        "--quiet",
28        "--system",
29        "--on-active",
30        &time,
31        "--timer-property=AccuracySec=1s",
32        "--description",
33        description,
34        "--unit",
35        unit,
36        "--",
37    ];
38    systemd_cmd.extend(cmd);
39    systemd_run(&systemd_cmd)
40}
41
42/// Reset a given failed unit.
43///
44/// This errors if the given unit is unknown, or if it didn't fail.
45///
46/// Because this involves timers, if this operation fails it will also internally try to do the
47/// same for the given unit with a `.timer` suffix.
48fn systemctl_reset_failed_timer(unit: &str) -> Result<()> {
49    // Invoke command, collect result
50    let result = cmd_systemctl(["--quiet", "--system", "reset-failed", unit])
51        .stderr(Stdio::null())
52        .status()
53        .map_err(Err::Systemctl);
54
55    // Do the same with .timer unit suffix, ensure one succeeds
56    if unit.ends_with(".service") {
57        let unit = format!("{}.timer", unit.trim_end_matches(".service"));
58        systemctl_reset_failed_timer(&unit).or_else(|_| cmd_assert_status(result?))
59    } else {
60        cmd_assert_status(result?)
61    }
62}
63
64/// Check whether the given unit (transient timer) is running.
65///
66/// This may ask for root privileges through sudo.
67///
68/// Because this involves timers, if this operation fails it will also internally try to do the
69/// same for the given unit with a `.timer` suffix.
70pub fn systemd_has_timer(unit: &str) -> Result<bool> {
71    // TODO: check whether we can optimize this, the status command may be expensive
72    let cmd = cmd_systemctl(["--system", "--no-pager", "--quiet", "status", unit])
73        .stdout(Stdio::null())
74        .stderr(Stdio::null())
75        .status()
76        .map_err(Err::Systemctl)?;
77
78    // Check special status codes
79    match cmd.code() {
80        Some(0) | Some(3) => Ok(true),
81        Some(4) if unit.ends_with(".service") => {
82            let unit = format!("{}.timer", unit.trim_end_matches(".service"));
83            systemd_has_timer(&unit)
84        }
85        Some(4) => Ok(false),
86        _ => cmd_assert_status(cmd).map(|_| false),
87    }
88}
89
90/// Remove a systemd transient timer.
91///
92/// Errors if the timer is not available.
93/// This may ask for root privileges through sudo.
94///
95/// Because this involves timers, if this operation fails it will also internally try to do the
96/// same for the given unit with a `.timer` suffix.
97pub fn systemd_remove_timer(unit: &str) -> Result<()> {
98    // Invoke command, collect result
99    let result = cmd_systemctl(["--system", "--quiet", "stop", unit])
100        .stderr(Stdio::null())
101        .status()
102        .map_err(Err::Systemctl);
103
104    // Do the same with .timer unit suffix, ensure one succeeds
105    if unit.ends_with(".service") {
106        let unit = format!("{}.timer", unit.trim_end_matches(".service"));
107        systemd_remove_timer(&unit).or_else(|_| cmd_assert_status(result?))
108    } else {
109        cmd_assert_status(result?)
110    }
111}
112
113/// Invoke a systemd-run command with the given arguments.
114fn systemd_run<I, S>(args: I) -> Result<()>
115where
116    I: IntoIterator<Item = S>,
117    S: AsRef<OsStr>,
118{
119    cmd_assert_status(cmd_systemd_run(args).status().map_err(Err::SystemdRun)?)
120}
121
122// /// Invoke a systemctl command with the given arguments.
123// fn systemctl<I, S>(args: I) -> Result<()>
124// where
125//     I: IntoIterator<Item = S>,
126//     S: AsRef<OsStr>,
127// {
128//     cmd_assert_status(cmd_systemctl(args).status().map_err(Err::Systemctl)?)
129// }
130
131/// Build a systemd-run command to run.
132fn cmd_systemd_run<I, S>(args: I) -> Command
133where
134    I: IntoIterator<Item = S>,
135    S: AsRef<OsStr>,
136{
137    let mut cmd = Command::new(SUDO_BIN);
138    cmd.arg("--");
139    cmd.arg(SYSTEMD_RUN_BIN);
140    cmd.args(args);
141    cmd
142}
143
144/// Build a systemctl command to run.
145fn cmd_systemctl<I, S>(args: I) -> Command
146where
147    I: IntoIterator<Item = S>,
148    S: AsRef<OsStr>,
149{
150    let mut cmd = Command::new(SUDO_BIN);
151    cmd.arg("--");
152    cmd.arg(SYSTEMCTL_BIN);
153    cmd.args(args);
154    cmd
155}
156
157/// Assert the exit status of a command.
158///
159/// Returns error is status is not succesful.
160fn cmd_assert_status(status: ExitStatus) -> Result<()> {
161    if !status.success() {
162        return Err(Err::Status(status).into());
163    }
164    Ok(())
165}
166
167#[derive(Debug, Error)]
168pub enum Err {
169    #[error("failed to invoke systemd-run command")]
170    SystemdRun(#[source] std::io::Error),
171
172    #[error("failed to invoke systemctl command")]
173    Systemctl(#[source] std::io::Error),
174
175    #[error("systemd exited with non-zero status code: {0}")]
176    Status(std::process::ExitStatus),
177}