service_install/install/init/cron/
disable.rs1use std::iter;
2use std::path::Path;
3use std::thread;
4use std::time::Duration;
5use std::time::Instant;
6
7use itertools::Itertools;
8use sysinfo::Pid;
9use sysinfo::ProcessRefreshKind;
10use sysinfo::ProcessesToUpdate;
11use sysinfo::Signal;
12
13use crate::install::init::autogenerated_comment;
14use crate::install::init::cron::setup::RemovePrevious;
15use crate::install::init::cron::Line;
16use crate::install::InstallError;
17use crate::install::InstallStep;
18use crate::install::RollbackError;
19use crate::install::RollbackStep;
20use crate::Tense;
21
22use super::current_crontab;
23use super::set_crontab;
24use super::teardown::CrontabChanged;
25use super::GetCrontabError;
26
27#[derive(Debug, thiserror::Error)]
28pub enum Error {
29 #[error("Could not get the current crontab")]
30 GetCrontab(#[source] GetCrontabError),
31 #[error("Failed to find a rule starting the target")]
32 NoRuleFound,
33 #[error("Process spawnedby cron will not stop")]
34 FailedToStop,
35}
36
37pub(crate) fn step(
38 target: &Path,
39 pid: Pid,
40 run_as: Option<&str>,
41) -> Result<Vec<Box<dyn InstallStep>>, Error> {
42 let crontab = current_crontab(run_as).map_err(Error::GetCrontab)?;
43
44 let bin_name = target
45 .file_name()
46 .expect("target always gets a file name")
47 .to_str()
48 .expect("file name is valid ascii");
49 let landmark_comment = autogenerated_comment(bin_name);
50
51 let previous_install = crontab
52 .windows(landmark_comment.lines().count() + 1)
53 .map(|w| w.split_last().expect("window size always >= 2"))
54 .find(|(_, comments)| comments.iter().map(Line::text).eq(landmark_comment.lines()));
55
56 if let Some((rule, comment)) = previous_install {
57 Ok(vec![
58 Box::new(RemovePrevious {
59 comments: comment.to_vec(),
60 rule: rule.clone(),
61 user: run_as.map(String::from),
62 }) as Box<dyn InstallStep>,
63 Box::new(Kill { pid }) as Box<dyn InstallStep>,
64 ])
65 } else if let Some(line) = crontab
66 .into_iter()
67 .filter_map(|line| line.exec().zip(Some(line)))
68 .find(|(exec, _)| exec == target)
69 .map(|(_, line)| line)
70 {
71 Ok(vec![
72 Box::new(CommentOutRule {
73 rule: line,
74 user: run_as.map(String::from),
75 }) as Box<dyn InstallStep>,
76 Box::new(Kill { pid }) as Box<dyn InstallStep>,
77 ])
78 } else {
79 Ok(vec![Box::new(Kill { pid }) as Box<dyn InstallStep>])
80 }
81}
82
83struct Kill {
84 pid: Pid,
85}
86
87impl InstallStep for Kill {
88 fn describe(&self, tense: Tense) -> String {
89 let verb = match tense {
90 Tense::Past => "Stopped",
91 Tense::Questioning => "Stop",
92 Tense::Future => "Will stop",
93 Tense::Active => "Stopping",
94 };
95 let pid = self.pid;
96 format!("{verb} the service started by cron with pid: `{pid}`")
97 }
98
99 fn describe_detailed(&self, tense: Tense) -> String {
100 let verb = match tense {
101 Tense::Past => "Stopped",
102 Tense::Questioning => "Stop",
103 Tense::Future => "Will stop",
104 Tense::Active => "Stopping",
105 };
106 let pid = self.pid;
107 format!("{verb} the service started by cron with pid: `{pid}`\n| using signal:\n|\t - Stop\n| if that does not work:\n|\t - Kill\n| and if that fails:\n|\t - Abort")
108 }
109
110 fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError> {
111 const ESCALATE: Duration = Duration::from_millis(200);
112 let mut last_attempt = Instant::now()
113 .checked_sub(ESCALATE)
114 .expect("Instant should not be at unix zero aka 1970");
115 let mut signals = [Signal::Stop, Signal::Kill, Signal::Abort].into_iter();
116
117 loop {
118 let mut s = sysinfo::System::new();
119 s.refresh_processes_specifics(
120 ProcessesToUpdate::Some([self.pid].as_slice()),
121 true,
122 ProcessRefreshKind::nothing(),
123 );
124 let Some(process) = s.process(self.pid) else {
125 return Ok(None);
126 };
127
128 if last_attempt.elapsed() < ESCALATE {
129 continue;
130 }
131
132 last_attempt = Instant::now();
133 let signal = signals.next().ok_or(InstallError::CouldNotStop)?;
134 let send_ok = process
135 .kill_with(signal)
136 .expect("signal should exist on linux");
137 if !send_ok {
138 for _ in 0..10 {
139 let mut s = sysinfo::System::new();
141 s.refresh_processes_specifics(
142 ProcessesToUpdate::Some([self.pid].as_slice()),
143 true,
144 ProcessRefreshKind::nothing(),
145 );
146 if s.process(self.pid).is_none() {
147 return Ok(None);
148 }
149 thread::sleep(Duration::from_millis(100));
150 }
151 panic!("cant kill :(");
152 }
153 }
154 }
155}
156
157struct CommentOutRule {
158 rule: Line,
159 user: Option<String>,
160}
161
162impl InstallStep for CommentOutRule {
163 fn describe(&self, tense: Tense) -> String {
164 let verb = match tense {
165 Tense::Past => "Commented out",
166 Tense::Questioning => "Comment out",
167 Tense::Future => "Will comment out",
168 Tense::Active => "Commenting out",
169 };
170 format!("{verb} a cron rule that is preventing the installation")
171 }
172
173 fn describe_detailed(&self, tense: Tense) -> String {
174 let verb = match tense {
175 Tense::Past => "Commented out",
176 Tense::Questioning => "Comment out",
177 Tense::Future => "Will comment out",
178 Tense::Active => "Commenting out",
179 };
180 format!(
181 "{verb} a cron rule that is preventing the installation\n| rule:\n|\t{}",
182 self.rule
183 )
184 }
185
186 fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError> {
187 let Self { rule, user } = self;
188 let mut crontab = current_crontab(user.as_deref())?;
189
190 let commented_rule = Line {
191 text: "# ".to_string() + &rule.text,
192 pos: rule.pos,
193 };
194 for line in &mut crontab {
195 if line.pos == rule.pos {
196 if line.text == rule.text {
197 line.text.clone_from(&commented_rule.text);
198 } else {
199 return Err(InstallError::CrontabChanged(CrontabChanged));
200 }
201 }
202 }
203
204 let new_crontab: String = crontab
205 .iter()
206 .map(Line::text)
207 .interleave_shortest(iter::repeat("\n"))
208 .collect();
209 set_crontab(&new_crontab, user.as_deref())?;
210
211 Ok(Some(Box::new(RollbackCommentOut {
212 commented_rule,
213 original_rule: rule.clone(),
214 user: user.clone(),
215 })))
216 }
217}
218
219struct RollbackCommentOut {
220 commented_rule: Line,
221 original_rule: Line,
222 user: Option<String>,
223}
224
225impl RollbackStep for RollbackCommentOut {
226 fn perform(&mut self) -> Result<(), RollbackError> {
227 let Self {
228 commented_rule,
229 original_rule,
230 user,
231 } = self;
232
233 let mut crontab = current_crontab(user.as_deref())?;
234
235 for line in &mut crontab {
236 if line.pos == commented_rule.pos {
237 if line.text == commented_rule.text {
238 line.text.clone_from(&original_rule.text);
239 } else {
240 return Err(RollbackError::CrontabChanged(CrontabChanged));
241 }
242 }
243 }
244
245 let new_crontab: String = crontab
246 .iter()
247 .map(Line::text)
248 .interleave_shortest(iter::repeat("\n"))
249 .collect();
250 Ok(set_crontab(&new_crontab, user.as_deref())?)
251 }
252
253 fn describe(&self, tense: Tense) -> String {
254 let verb = match tense {
255 Tense::Past => "Uncommented",
256 Tense::Questioning => "Uncomment",
257 Tense::Future => "Will uncomment",
258 Tense::Active => "Uncommenting",
259 };
260 format!(
261 "{verb} a cron rule that was commented out as it prevented the installation\n| rule:\n|\t{}",
262 self.commented_rule
263 )
264 }
265}