service_install/install/init/cron/
setup.rs1use 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 + ¶ms
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")) .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(¤t_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}