1mod builder;
2
3pub mod files;
5pub mod init;
7
8use std::ffi::OsString;
9use std::fmt::Display;
10
11pub use builder::Spec;
12use files::MoveBackError;
13use init::systemd;
14use itertools::{Either, Itertools};
15
16use crate::Tense;
17
18use self::builder::ToAssign;
19use self::init::cron::teardown::CrontabChanged;
20use self::init::cron::{GetCrontabError, SetCrontabError};
21use self::init::SetupError;
22
23#[derive(Debug, Clone, Copy)]
25pub enum Mode {
26 User,
29 System,
33}
34
35impl Mode {
36 fn is_user(self) -> bool {
37 match self {
38 Mode::User => true,
39 Mode::System => false,
40 }
41 }
42}
43
44impl Display for Mode {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Mode::User => f.write_str("user"),
48 Mode::System => f.write_str("system"),
49 }
50 }
51}
52
53#[allow(clippy::module_name_repetitions)]
55#[derive(thiserror::Error, Debug)]
56pub enum PrepareInstallError {
57 #[error("Error setting up init")]
58 Init(
59 #[from]
60 #[source]
61 init::SetupError,
62 ),
63 #[error("Failed to move files")]
64 Move(
65 #[from]
66 #[source]
67 files::MoveError,
68 ),
69 #[error("Need to run as root to install to system")]
70 NeedRootForSysInstall,
71 #[error("Need to run as root to setup service to run as another user")]
72 NeedRootToRunAs,
73 #[error("Could not find an init system we can set things up for")]
74 NoInitSystemRecognized,
75 #[error("Install configured to run as a user: `{0}` however this user does not exist")]
76 UserDoesNotExist(String),
77 #[error("All supported init systems found failed, errors: {0:?}")]
78 SupportedInitSystemFailed(Vec<InitSystemFailure>),
79}
80
81#[derive(Debug, thiserror::Error)]
90#[error("Init system: {name} ran into error: {error}")]
91pub struct InitSystemFailure {
92 name: String,
93 error: SetupError,
94}
95
96#[derive(thiserror::Error, Debug)]
98pub enum PrepareRemoveError {
99 #[error("Could not find this executable's location")]
100 GetExeLocation(#[source] std::io::Error),
101 #[error("Failed to remove files")]
102 Move(
103 #[from]
104 #[source]
105 files::DeleteError,
106 ),
107 #[error("Removing from init system")]
108 Init(
109 #[from]
110 #[source]
111 init::TearDownError,
112 ),
113 #[error("Could not find any installation in any init system")]
114 NoInstallFound,
115 #[error("Need to run as root to remove a system install")]
116 NeedRoot,
117}
118
119#[allow(clippy::module_name_repetitions)]
120#[derive(Debug, thiserror::Error)]
121pub enum InstallError {
122 #[error("Could not get crontab, needed to add our lines")]
123 GetCrontab(
124 #[from]
125 #[source]
126 init::cron::GetCrontabError,
127 ),
128 #[error(transparent)]
129 CrontabChanged(#[from] init::cron::teardown::CrontabChanged),
130 #[error("Could not set crontab, needed to add our lines")]
131 SetCrontab(
132 #[from]
133 #[source]
134 init::cron::SetCrontabError,
135 ),
136 #[error("Something went wrong interacting with systemd")]
137 Systemd(
138 #[from]
139 #[source]
140 init::systemd::Error,
141 ),
142 #[error("Could not set the owner of the installed executable to be root")]
143 SetRootOwner(#[source] std::io::Error),
144 #[error("Could not make the installed executable read only")]
145 SetReadOnly(
146 #[from]
147 #[source]
148 files::SetReadOnlyError,
149 ),
150 #[error("Can not disable Cron service, process will not stop.")]
151 CouldNotStop,
152 #[error("Could not kill the process preventing installing the new binary")]
153 KillOld(#[source] files::process_parent::KillOldError),
154 #[error("Could not copy executable to install location")]
155 CopyExeError(#[source] std::io::Error),
156 #[error("Failed to make short lived backup of file taking up install location")]
157 Backup(#[source] BackupError),
158 #[error("Could not spawn a tokio runtime for interacting with systemd")]
159 TokioRt(#[source] std::io::Error),
160}
161
162#[derive(Debug, thiserror::Error)]
163pub enum BackupError {
164 #[error("Could not create temporary file")]
165 Create(#[source] std::io::Error),
166 #[error("Could not write to temporary file")]
167 Write(#[source] std::io::Error),
168 #[error("Could not read from file")]
169 Read(#[source] std::io::Error),
170}
171
172pub enum StepOptions {
173 YesOrAbort,
174}
175
176#[allow(clippy::module_name_repetitions)]
178pub trait InstallStep {
179 fn describe(&self, tense: Tense) -> String;
183 fn describe_detailed(&self, tense: Tense) -> String {
187 self.describe(tense)
188 }
189 fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError>;
200 fn options(&self) -> Option<StepOptions> {
202 Some(StepOptions::YesOrAbort)
203 }
204}
205
206impl std::fmt::Debug for &dyn InstallStep {
207 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208 write!(f, "{}", self.describe(Tense::Future))
209 }
210}
211
212impl Display for &dyn InstallStep {
213 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
214 write!(f, "{}", self.describe_detailed(Tense::Future))
215 }
216}
217
218#[derive(Debug, thiserror::Error)]
219pub enum RemoveError {
220 #[error("Could not get crontab, needed tot filter out our added lines")]
221 GetCrontab(
222 #[from]
223 #[source]
224 init::cron::GetCrontabError,
225 ),
226 #[error(transparent)]
227 CrontabChanged(#[from] init::cron::teardown::CrontabChanged),
228 #[error("Could not set crontab, needed tot filter out our added lines")]
229 SetCrontab(
230 #[from]
231 #[source]
232 init::cron::SetCrontabError,
233 ),
234 #[error("Could not remove file(s), error")]
235 DeleteError(
236 #[from]
237 #[source]
238 files::DeleteError,
239 ),
240 #[error("Something went wrong interacting with systemd")]
241 Systemd(
242 #[from]
243 #[source]
244 init::systemd::Error,
245 ),
246}
247
248pub trait RemoveStep {
250 fn describe(&self, tense: Tense) -> String;
254 fn describe_detailed(&self, tense: Tense) -> String {
258 self.describe(tense)
259 }
260 fn perform(&mut self) -> Result<(), RemoveError>;
270}
271
272impl std::fmt::Debug for &dyn RemoveStep {
273 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274 write!(f, "{}", self.describe(Tense::Future))
275 }
276}
277
278impl Display for &dyn RemoveStep {
279 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280 write!(f, "{}", self.describe_detailed(Tense::Future))
281 }
282}
283
284#[derive(Debug, thiserror::Error)]
285pub enum RollbackError {
286 #[error("Could not remove file, error")]
287 Removing(
288 #[from]
289 #[source]
290 RemoveError,
291 ),
292 #[error("error restoring file permissions")]
293 RestoringPermissions(#[source] std::io::Error),
294 #[error("error re-enabling service")]
295 ReEnabling(
296 #[from]
297 #[source]
298 systemd::Error,
299 ),
300 #[error("Can not rollback setting up cron, must be done manually")]
301 Impossible,
302 #[error("Crontab changed undoing changes might overwrite the change")]
303 CrontabChanged(
304 #[from]
305 #[source]
306 CrontabChanged,
307 ),
308 #[error("Could not get the crontab, needed to undo a change to it")]
309 GetCrontab(
310 #[from]
311 #[source]
312 GetCrontabError,
313 ),
314 #[error("Could not revert to the original crontab")]
315 SetCrontab(
316 #[from]
317 #[source]
318 SetCrontabError,
319 ),
320 #[error("Could not restore original file")]
321 MovingBack(#[source] MoveBackError),
322}
323
324pub trait RollbackStep {
326 fn perform(&mut self) -> Result<(), RollbackError>;
334 fn describe(&self, tense: Tense) -> String;
335}
336
337impl std::fmt::Debug for &dyn RollbackStep {
338 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
339 write!(f, "{}", self.describe(Tense::Future))
340 }
341}
342
343impl Display for &dyn RollbackStep {
344 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
345 write!(f, "{}", self.describe(Tense::Future))
346 }
347}
348
349impl<T: RemoveStep> RollbackStep for T {
350 fn perform(&mut self) -> Result<(), RollbackError> {
351 Ok(self.perform()?)
352 }
353
354 fn describe(&self, tense: Tense) -> String {
355 self.describe(tense)
356 }
357}
358
359#[allow(clippy::module_name_repetitions)]
366pub struct InstallSteps(pub(crate) Vec<Box<dyn InstallStep>>);
367
368impl std::fmt::Debug for InstallSteps {
369 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
370 for step in self.0.iter().map(|step| step.describe(Tense::Future)) {
371 write!(f, "{step\n}")?;
372 }
373 Ok(())
374 }
375}
376
377impl Display for InstallSteps {
378 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
379 for step in self
380 .0
381 .iter()
382 .map(|step| step.describe_detailed(Tense::Future))
383 {
384 write!(f, "{step\n}")?;
385 }
386 Ok(())
387 }
388}
389
390impl IntoIterator for InstallSteps {
391 type Item = Box<dyn InstallStep>;
392 type IntoIter = std::vec::IntoIter<Self::Item>;
393
394 fn into_iter(self) -> Self::IntoIter {
395 self.0.into_iter()
396 }
397}
398
399impl InstallSteps {
400 pub fn install(self) -> Result<String, InstallError> {
408 let mut description = Vec::new();
409 for mut step in self.0 {
410 description.push(step.describe(Tense::Past));
411 step.perform()?;
412 }
413
414 Ok(description.join("\n"))
415 }
416}
417
418impl<T: ToAssign> Spec<builder::PathIsSet, builder::NameIsSet, builder::TriggerIsSet, T> {
419 pub fn prepare_install(self) -> Result<InstallSteps, PrepareInstallError> {
432 let builder::Spec {
433 mode,
434 path: Some(source),
435 service_name: Some(name),
436 bin_name,
437 args,
438 environment,
439 trigger: Some(trigger),
440 overwrite_existing,
441 working_dir,
442 run_as,
443 description,
444 ..
445 } = self
446 else {
447 unreachable!("type sys guarantees path, name and trigger set")
448 };
449
450 let not_root = matches!(sudo::check(), sudo::RunningAs::User);
451 if let Mode::System = mode {
452 if not_root {
453 return Err(PrepareInstallError::NeedRootForSysInstall);
454 }
455 }
456
457 if let Some(ref user) = run_as {
458 let curr_user = uzers::get_current_username()
459 .ok_or_else(|| PrepareInstallError::UserDoesNotExist(user.clone()))?;
460 if curr_user != OsString::from(user) && not_root {
461 return Err(PrepareInstallError::NeedRootToRunAs);
462 }
463 }
464
465 let init_systems = self.init_systems.unwrap_or_else(init::System::all);
466 let (mut steps, exe_path) = files::move_files(
467 source,
468 mode,
469 run_as.as_deref(),
470 overwrite_existing,
471 &init_systems,
472 )?;
473 let params = init::Params {
474 name,
475 bin_name,
476 description,
477
478 exe_path,
479 exe_args: args,
480 environment,
481 working_dir,
482
483 trigger,
484 run_as,
485 mode,
486 };
487
488 let mut errors = Vec::new();
489 for init in init_systems {
490 if init.not_available().map_err(PrepareInstallError::Init)? {
491 continue;
492 }
493
494 match init.set_up_steps(¶ms) {
495 Ok(init_steps) => {
496 steps.extend(init_steps);
497 return Ok(InstallSteps(steps));
498 }
499 Err(err) => {
500 #[cfg(feature = "tracing")]
501 tracing::warn!("Could not set up init using {}, error: {err}", init.name());
502 errors.push(InitSystemFailure {
503 name: init.name().to_owned(),
504 error: err,
505 });
506 }
507 };
508 }
509
510 if errors.is_empty() {
511 Err(PrepareInstallError::NoInitSystemRecognized)
512 } else {
513 Err(PrepareInstallError::SupportedInitSystemFailed(errors))
514 }
515 }
516}
517
518pub struct RemoveSteps(pub(crate) Vec<Box<dyn RemoveStep>>);
525
526impl std::fmt::Debug for RemoveSteps {
527 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
528 for step in self.0.iter().map(|step| step.describe(Tense::Future)) {
529 write!(f, "{step\n}")?;
530 }
531 Ok(())
532 }
533}
534
535impl Display for RemoveSteps {
536 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
537 for step in self
538 .0
539 .iter()
540 .map(|step| step.describe_detailed(Tense::Future))
541 {
542 write!(f, "{step\n}")?;
543 }
544 Ok(())
545 }
546}
547
548impl IntoIterator for RemoveSteps {
549 type Item = Box<dyn RemoveStep>;
550 type IntoIter = std::vec::IntoIter<Self::Item>;
551
552 fn into_iter(self) -> Self::IntoIter {
553 self.0.into_iter()
554 }
555}
556
557impl RemoveSteps {
558 pub fn remove(self) -> Result<String, RemoveError> {
567 let mut description = Vec::new();
568 for mut step in self.0 {
569 description.push(step.describe(Tense::Past));
570 step.perform()?;
571 }
572
573 Ok(description.join("\n"))
574 }
575
576 pub fn best_effort_remove(self) -> Result<String, BestEffortRemoveError> {
585 let (description, failures): (Vec<_>, Vec<_>) =
586 self.0
587 .into_iter()
588 .partition_map(|mut step| match step.perform() {
589 Ok(()) => Either::Left(step.describe(Tense::Past)),
590 Err(e) => Either::Right((step.describe_detailed(Tense::Active), e)),
591 });
592
593 if failures.is_empty() {
594 Ok(description.join("\n"))
595 } else {
596 Err(BestEffortRemoveError { failures })
597 }
598 }
599}
600
601#[derive(Debug, thiserror::Error)]
602pub struct BestEffortRemoveError {
603 failures: Vec<(String, RemoveError)>,
604}
605
606impl Display for BestEffortRemoveError {
607 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
608 writeln!(f, "Ran into one or more issues trying to remove an install")?;
609 writeln!(f, "You should resolve/check these issues manually")?;
610 for (task, error) in &self.failures {
611 let task = task.to_lowercase();
612 writeln!(f, "* Tried to {task}\nfailed because: {error}")?;
613 }
614 Ok(())
615 }
616}
617
618impl<M: ToAssign, P: ToAssign, T: ToAssign, I: ToAssign> Spec<M, P, T, I> {
619 pub fn prepare_remove(self) -> Result<RemoveSteps, PrepareRemoveError> {
630 let builder::Spec {
631 mode,
632 bin_name,
633 run_as,
634 ..
635 } = self;
636
637 if let Mode::System = mode {
638 if let sudo::RunningAs::User = sudo::check() {
639 return Err(PrepareRemoveError::NeedRoot);
640 }
641 }
642
643 let mut inits = self.init_systems.unwrap_or(init::System::all()).into_iter();
644 let (mut steps, path) = loop {
645 let Some(init) = inits.next() else {
646 return Err(PrepareRemoveError::NoInstallFound);
647 };
648
649 if let Some(install) = init.tear_down_steps(bin_name, mode, run_as.as_deref())? {
650 break install;
651 }
652 };
653
654 let remove_step = files::remove_files(path);
655 steps.push(Box::new(remove_step));
656 Ok(RemoveSteps(steps))
657 }
658}