service_install/install/init/cron/
teardown.rs1use 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(¤t_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 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}