service_install/install/init/cron/
disable.rs

1use 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                    // retry a limited amount
140                    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}