service_install/install/init/cron/
teardown.rs

1use std::iter;
2use std::path::PathBuf;
3use std::str::FromStr;
4
5use itertools::Itertools;
6
7use crate::install::init::extract_path;
8use crate::install::init::{autogenerated_comment, ExeLocation, RSteps, TearDownError};
9use crate::install::{Mode, Tense};
10use crate::install::{RemoveError, RemoveStep};
11
12use super::Line;
13use super::{current_crontab, set_crontab, GetCrontabError};
14
15#[derive(Debug, thiserror::Error)]
16pub enum Error {
17    #[error("Could not get the current crontab")]
18    GetCrontab(#[from] #[source] GetCrontabError),
19    #[error("Comment for previous install at the end of the crontab")]
20    CrontabCorrupt,
21    #[error(transparent)]
22    CrontabChanged(#[from] CrontabChanged),
23    #[error("Rule in crontab corrupt, too short")]
24    CorruptTooShort,
25}
26
27pub(crate) fn path_from_rule(rule: &str) -> PathBuf {
28    let command = if let Some(command) = rule.strip_prefix("@reboot") {
29        command.to_string()
30    } else {
31        rule.splitn(5 + 1, char::is_whitespace).skip(5).collect()
32    };
33    let command = match command.split_once("&&") {
34        Some((_cd, command)) => command.to_string(),
35        None => command,
36    };
37
38    let command = command.trim_start();
39    let command = extract_path::unshell_escape::split_unescaped_whitespace_once(command);
40
41    PathBuf::from_str(&command).expect("infallible")
42}
43
44#[cfg(test)]
45mod test {
46    use std::path::Path;
47
48    use super::*;
49
50    #[test]
51    fn test_from_rule() {
52        let case = "10 10 * * *  '/home/david/.local/hi bin/cron_only'";
53        assert_eq!(
54            &path_from_rule(case),
55            Path::new("/home/david/.local/hi bin/cron_only")
56        )
57    }
58}
59
60pub(crate) fn tear_down_steps(
61    bin_name: &str,
62    mode: Mode,
63    user: Option<&str>,
64) -> Result<Option<(RSteps, ExeLocation)>, TearDownError> {
65    assert!(
66        !(mode.is_user() && user.is_some()),
67        "need to run as system to set a different users crontab"
68    );
69
70    let current = current_crontab(user).map_err(Error::GetCrontab)?;
71    let landmark_comment = autogenerated_comment(bin_name);
72
73    let to_remove = current
74        .windows(landmark_comment.lines().count() + 1)
75        .map(|w| w.split_last().expect("window size always >= 2"))
76        .find(|(_, comments)| comments.iter().map(Line::text).eq(landmark_comment.lines()));
77
78    let Some((rule, comment)) = to_remove else {
79        return Ok(None);
80    };
81
82    let install_path = path_from_rule(&rule.text);
83    let step = Box::new(RemoveInstalled {
84        comments: comment.to_vec(),
85        rule: rule.clone(),
86        user: user.map(str::to_owned),
87    }) as Box<dyn RemoveStep>;
88    Ok(Some((vec![step], install_path)))
89}
90
91struct RemoveInstalled {
92    user: Option<String>,
93    comments: Vec<Line>,
94    rule: Line,
95}
96
97impl RemoveStep for RemoveInstalled {
98    fn describe(&self, tense: Tense) -> String {
99        let verb = match tense {
100            Tense::Past => "Removed",
101            Tense::Questioning => "Remove",
102            Tense::Future => "Will remove",
103            Tense::Active => "Removing",
104        };
105        let user = self
106            .user
107            .as_ref()
108            .map(|n| format!("{n}'s "))
109            .unwrap_or_default();
110        format!("{verb} the installs comment and rule from {user}crontab")
111    }
112
113    fn describe_detailed(&self, tense: Tense) -> String {
114        let verb = match tense {
115            Tense::Past => "Removed",
116            Tense::Questioning => "Remove",
117            Tense::Future => "Will remove",
118            Tense::Active => "Removing",
119        };
120        let user = self
121            .user
122            .as_ref()
123            .map(|n| format!("{n}'s "))
124            .unwrap_or_default();
125        #[allow(clippy::format_collect)]
126        let comment: String = self
127            .comments
128            .iter()
129            .map(|Line { pos, text }| format!("\n|\t{pos}: {text}"))
130            .collect();
131        let rule = format!("|\t{}: {}", self.rule.pos, self.rule.text);
132        format!("{verb} the installs comment and rule from {user}crontab:\n| comment:{comment}\n| rule:\n{rule}")
133    }
134
135    fn perform(&mut self) -> Result<(), RemoveError> {
136        let Self {
137            comments,
138            rule,
139            user,
140        } = self;
141        let current_crontab = current_crontab(user.as_deref())?;
142        let new_lines = filter_out(&current_crontab, rule, comments)?;
143
144        let new_crontab: String = new_lines
145            .into_iter()
146            .interleave_shortest(iter::once("\n").cycle())
147            .collect();
148        set_crontab(&new_crontab, user.as_deref())?;
149
150        Ok(())
151    }
152}
153
154#[derive(Debug, thiserror::Error)]
155#[error(
156    "Crontab was modified between preparation and running this step, you should manually verify it"
157)]
158pub struct CrontabChanged;
159
160pub(super) fn filter_out<'a>(
161    current_crontab: &'a [Line],
162    rule: &Line,
163    comments: &[Line],
164) -> Result<Vec<&'a str>, CrontabChanged> {
165    // someone could store the steps and execute later, if
166    // anything changed refuse to remove lines and abort
167    let mut output = Vec::new();
168    let mut to_remove = comments.iter().chain(iter::once(rule)).fuse();
169    let mut next_to_remove = to_remove.next();
170    for line in current_crontab {
171        if let Some(next) = next_to_remove {
172            if line.pos != next.pos {
173                continue;
174            }
175
176            if line.text != next.text {
177                return Err(CrontabChanged);
178            }
179
180            next_to_remove = to_remove.next();
181            continue;
182        }
183        output.push(line.text.as_str());
184    }
185
186    Ok(output)
187}