service_install/install/init/
cron.rs

1use crate::install::init::cron::teardown::path_from_rule;
2use std::fmt;
3use std::io::Write;
4use std::path::{Path, PathBuf};
5use std::process::{Command, Stdio};
6
7use super::{Params, SetupError, Steps};
8use crate::install::{RollbackError, RollbackStep};
9use crate::Tense;
10
11pub mod disable;
12pub mod setup;
13pub mod teardown;
14
15pub(crate) use setup::set_up_steps;
16use sysinfo::ProcessesToUpdate;
17pub(crate) use teardown::tear_down_steps;
18
19pub(super) fn not_available() -> bool {
20    use sysinfo::{ProcessRefreshKind, System, UpdateKind};
21    let mut s = System::new();
22    s.refresh_processes_specifics(
23        ProcessesToUpdate::All,
24        true,
25        ProcessRefreshKind::nothing().with_cmd(UpdateKind::Always),
26    );
27    let cron_running = s.processes().iter().any(|(_, process)| {
28        process
29            .cmd()
30            .iter()
31            .any(|part| part.to_string_lossy().ends_with("/cron"))
32    });
33    !cron_running
34}
35
36pub(crate) fn is_init_path(path: &Path) -> bool {
37    path.ends_with("cron")
38}
39
40struct RollbackImpossible;
41impl RollbackStep for RollbackImpossible {
42    fn perform(&mut self) -> Result<(), RollbackError> {
43        Err(RollbackError::Impossible)
44    }
45
46    fn describe(&self, _: Tense) -> String {
47        "Rollback of cron setup is not possible.\n\tsuggestion: inspect and fix crontab manually\n\tnote: you can safely edit crontab using `crontab -e`".to_string()
48    }
49}
50
51#[derive(Debug, Clone)]
52pub(crate) struct Line {
53    /// line number in the crontab
54    pos: usize,
55    text: String,
56}
57
58impl Line {
59    fn text(&self) -> &str {
60        &self.text
61    }
62
63    fn comment(&self) -> bool {
64        self.text.trim_start().starts_with('#')
65    }
66
67    fn exec(&self) -> Option<PathBuf> {
68        if self.comment() {
69            Some(path_from_rule(&self.text))
70        } else {
71            None
72        }
73    }
74}
75
76impl fmt::Display for Line {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        let Self { pos, text } = self;
79        f.write_fmt(format_args!("{pos}: {text}"))
80    }
81}
82
83#[must_use]
84fn crontab_lines(text: &str) -> Vec<Line> {
85    const HEADER_ADDED_BY_LIST_CMD: &str = "# DO NOT EDIT THIS FILE";
86    let header_lines = if text.starts_with(HEADER_ADDED_BY_LIST_CMD) {
87        3
88    } else {
89        0
90    };
91
92    text.lines()
93        .skip(header_lines)
94        .map(str::to_owned)
95        .enumerate()
96        .map(|(source, text)| Line { text, pos: source })
97        .collect()
98}
99
100#[derive(Debug, thiserror::Error)]
101pub enum GetCrontabError {
102    #[error("Could not run the crontab program")]
103    CouldNotRun(#[source] std::io::Error),
104    #[error("Command `crontab -l` failed, stderr: \"{stderr}\"\n\t")]
105    CommandFailed { stderr: String },
106}
107
108fn current_crontab(user: Option<&str>) -> Result<Vec<Line>, GetCrontabError> {
109    let mut command = Command::new("crontab");
110    command.arg("-l");
111    if let Some(user) = user {
112        command.arg("-u");
113        command.arg(user);
114    }
115
116    let output = command.output().map_err(GetCrontabError::CouldNotRun)?;
117
118    if output.status.success() {
119        let stdout = String::from_utf8(output.stdout).expect("crontab should return utf8");
120        let crontab = crontab_lines(&stdout);
121        return Ok(crontab);
122    }
123
124    let stderr = String::from_utf8(output.stderr)
125        .expect("crontab should return utf8")
126        .trim()
127        .to_string();
128
129    if stderr.starts_with("no crontab for") {
130        return Ok(Vec::new());
131    }
132
133    Err(GetCrontabError::CommandFailed { stderr })
134}
135
136#[derive(Debug, thiserror::Error)]
137pub enum SetCrontabError {
138    #[error("Could not run the crontab program")]
139    CouldNotRun(#[source] std::io::Error),
140    #[error("Command `crontab -` failed, stderr:\n\t{stderr}")]
141    CommandFailed { stderr: String },
142    #[error("Failed to open crontab stdin")]
143    StdinClosed,
144    #[error("Error while writing to crontab's stdin")]
145    WritingStdin(#[source] std::io::Error),
146    #[error("Could not wait on output of crontab program")]
147    FailedToWait(#[source] std::io::Error),
148}
149
150fn set_crontab(new_crontab: &str, user: Option<&str>) -> Result<(), SetCrontabError> {
151    let mut command = Command::new("crontab");
152    command.arg("-");
153    if let Some(user) = user {
154        command.arg("-u");
155        command.arg(user);
156    }
157    let mut child = command
158        .stdin(Stdio::piped())
159        .stdout(Stdio::piped())
160        .stderr(Stdio::piped())
161        .spawn()
162        .map_err(SetCrontabError::CouldNotRun)?;
163
164    let mut stdin = child.stdin.take().ok_or(SetCrontabError::StdinClosed)?;
165    stdin
166        .write_all(new_crontab.as_bytes())
167        .map_err(SetCrontabError::WritingStdin)?;
168    drop(stdin);
169
170    let output = child
171        .wait_with_output()
172        .map_err(SetCrontabError::FailedToWait)?;
173    if output.status.success() {
174        Ok(())
175    } else {
176        let stderr = String::from_utf8(output.stderr).expect("crontab should return utf8");
177        Err(SetCrontabError::CommandFailed { stderr })
178    }
179}