service_install/install/init/cron/
setup.rs

1use std::iter;
2use std::path::PathBuf;
3
4use itertools::Itertools;
5
6use super::{teardown, Params, SetupError, Steps};
7use crate::install::builder::Trigger;
8use crate::install::init::{autogenerated_comment, ShellEscape};
9use crate::install::{InstallError, InstallStep, RollbackStep, Tense};
10use crate::schedule::Schedule;
11
12use super::Line;
13use super::RollbackImpossible;
14use super::{current_crontab, set_crontab};
15
16#[derive(Debug, thiserror::Error)]
17pub enum Error {
18    #[error("Command `crontab -l` failed, stderr:\n\t{stderr}")]
19    ListFailed { stderr: String },
20    #[error("Could not get the current crontab")]
21    GetCrontab(#[source] super::GetCrontabError),
22    #[error("Comment for previous install at the end of the crontab")]
23    CrontabCorrupt,
24    #[error("Failed to open crontab stdin")]
25    StdinClosed,
26    #[error("Error while writing to crontab's stdin")]
27    WritingStdin(#[source] std::io::Error),
28    #[error("Could not wait on output of crontab program")]
29    FailedToWait(#[source] std::io::Error),
30    #[error("Crontab was modified while installation ran, you should manually verify it")]
31    CrontabChanged,
32    #[error("Could not find an existing install in crontab")]
33    NoExistingInstallFound,
34}
35
36pub(crate) fn set_up_steps(params: &Params) -> Result<Steps, SetupError> {
37    use Schedule as S;
38    use Trigger::{OnBoot, OnSchedule};
39
40    let current = current_crontab(params.run_as.as_deref()).map_err(Error::GetCrontab)?;
41    let landmark_comment = autogenerated_comment(params.bin_name);
42
43    let to_remove = current
44        .windows(landmark_comment.lines().count() + 1)
45        .map(|w| w.split_last().expect("window size always >= 2"))
46        .find(|(_, comments)| comments.iter().map(Line::text).eq(landmark_comment.lines()));
47
48    let mut steps = Vec::new();
49    if let Some((rule, comment)) = to_remove {
50        steps.push(Box::new(RemovePrevious {
51            comments: comment.to_vec(),
52            rule: rule.clone(),
53            user: params.run_as.clone(),
54        }) as Box<dyn InstallStep>);
55    }
56
57    let when = match params.trigger {
58        OnSchedule(S::Daily(time)) => format!("{} {} * * *", time.minute(), time.hour()),
59        OnSchedule(S::Every(dur)) => format!("{}, * * * *", dur.as_secs()),
60        OnBoot => "@reboot".to_owned(),
61    };
62
63    let exe_path = params.exe_path.shell_escaped();
64    let exe_args: String = params.exe_args.iter().map(String::shell_escaped).join(" ");
65    let set_working_dir = params
66        .working_dir
67        .as_ref()
68        .map(PathBuf::shell_escaped)
69        .map(|dir| format!("cd {dir} && "))
70        .unwrap_or_default();
71    let set_env_vars = if params.environment.is_empty() {
72        String::new()
73    } else {
74        "export".to_owned()
75            + &params
76                .environment
77                .iter()
78                .map(|(key, val)| format!("{}={}", key.shell_escaped(), val.shell_escaped()))
79                .join(" ")
80            + ";"
81    };
82
83    let command = format!("{set_env_vars}{set_working_dir}{exe_path} {exe_args}");
84    let rule = format!("{when} {command}");
85
86    steps.push(Box::new(Add {
87        user: params.run_as.clone(),
88        comment: landmark_comment,
89        rule,
90    }));
91    Ok(steps)
92}
93
94#[derive(Debug, Clone)]
95pub(crate) struct Add {
96    pub(crate) user: Option<String>,
97    pub(crate) comment: String,
98    pub(crate) rule: String,
99}
100
101impl InstallStep for Add {
102    fn describe(&self, tense: Tense) -> String {
103        let verb = match tense {
104            Tense::Past => "Appended",
105            Tense::Questioning => "Append",
106            Tense::Future => "Will append",
107            Tense::Active => "Appending",
108        };
109        if let Some(run_as) = &self.user {
110            format!(
111                "{verb} comment and rule to {run_as}'s crontab{}",
112                tense.punct()
113            )
114        } else {
115            format!("{verb} comment and rule to crontab{}", tense.punct())
116        }
117    }
118
119    fn describe_detailed(&self, tense: Tense) -> String {
120        let verb = match tense {
121            Tense::Past => "Appended",
122            Tense::Questioning => "Append",
123            Tense::Future => "Will append",
124            Tense::Active => "Appending",
125        };
126        let Self {
127            comment,
128            rule,
129            user,
130        } = self;
131        let comment = comment.replace('\n', "\n|\t");
132        if let Some(run_as) = user {
133            format!(
134                "{verb} comment and rule to {run_as}'s crontab{}\n| comment:\n|\t{comment}\n| rule:\n|\t{rule}", tense.punct()
135            )
136        } else {
137            format!(
138                "{verb} comment and rule to crontab{}\n| comment:\n|\t{comment}\n| rule:\n|\t{rule}", tense.punct()
139            )
140        }
141    }
142
143    fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError> {
144        let Self {
145            comment,
146            rule,
147            user,
148        } = self.clone();
149        let current_crontab = current_crontab(user.as_deref())?;
150        let new_crontab: String = current_crontab
151            .iter()
152            .map(Line::text)
153            .chain(iter::once(comment.as_str()))
154            .chain(iter::once(rule.as_str()))
155            .interleave_shortest(iter::once("\n").cycle())
156            .chain(iter::once("\n")) // some say cron likes a newline at the end
157            .collect();
158        set_crontab(&new_crontab, user.as_deref())?;
159
160        Ok(Some(Box::new(RollbackImpossible)))
161    }
162}
163pub(crate) struct RemovePrevious {
164    pub(crate) comments: Vec<Line>,
165    pub(crate) rule: Line,
166    pub(crate) user: Option<String>,
167}
168impl InstallStep for RemovePrevious {
169    fn describe(&self, tense: Tense) -> String {
170        let verb = match tense {
171            Tense::Past => "Removed",
172            Tense::Questioning => "Remove",
173            Tense::Future => "Will remove",
174            Tense::Active => "Removing",
175        };
176        let user = self
177            .user
178            .as_ref()
179            .map(|n| format!("{n}'s "))
180            .unwrap_or_default();
181        format!(
182            "{verb} comment and rule from previous installation from {user}crontab{}",
183            tense.punct()
184        )
185    }
186
187    fn describe_detailed(&self, tense: Tense) -> String {
188        let verb = match tense {
189            Tense::Past => "Removed",
190            Tense::Questioning => "Remove",
191            Tense::Future => "Will remove",
192            Tense::Active => "Removing",
193        };
194        let user = self
195            .user
196            .as_ref()
197            .map(|n| format!("{n}'s "))
198            .unwrap_or_default();
199        #[allow(clippy::format_collect)]
200        let comment: String = self
201            .comments
202            .iter()
203            .map(|Line { pos, text }| format!("\n|\t{pos}: {text}"))
204            .collect();
205        let rule = format!("|\t{}: {}", self.rule.pos, self.rule.text);
206        format!("{verb} a comment and rule from previous installation from {user}crontab{}\n| comment:\t{comment}\n| rule:\n{rule}", tense.punct())
207    }
208
209    fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError> {
210        let Self {
211            comments,
212            rule,
213            user,
214        } = self;
215        let current_crontab = current_crontab(user.as_deref())?;
216
217        let new_lines = teardown::filter_out(&current_crontab, rule, comments)?;
218
219        let new_crontab: String = new_lines
220            .into_iter()
221            .interleave_shortest(iter::repeat("\n"))
222            .collect();
223        set_crontab(&new_crontab, user.as_deref())?;
224
225        Ok(Some(Box::new(RollbackImpossible)))
226    }
227}