service_install/
install.rs

1mod builder;
2
3/// Errors and settings related to installing files
4pub mod files;
5/// Errors and settings related to the supported init systems
6pub 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/// Whether to install system wide or for the current user only
24#[derive(Debug, Clone, Copy)]
25pub enum Mode {
26    /// install for the current user, does not require running the installation
27    /// as superuser/admin
28    User,
29    /// install to the entire system, the installation/removal must be ran as
30    /// superuser/admin or it will return
31    /// [`PrepareInstallError::NeedRootForSysInstall`] or [`PrepareRemoveError::NeedRoot`]
32    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/// Errors that can occur when preparing for or performing an installation
54#[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/// The init system was found and we tried to set up the service but ran into an
82/// error.
83///
84/// When there is another init system that does work this error is ignored. If
85/// no other system is available or there is but it/they fail too this error is
86/// reported.
87///
88/// A warning is always issued if the `tracing` feature is enabled.
89#[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/// Errors that can occur when preparing for or removing an installation
97#[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/// One step in the install process. Can be executed or described.
177#[allow(clippy::module_name_repetitions)]
178pub trait InstallStep {
179    /// A short (one line) description of what running perform will
180    /// do. Pass in the tense you want for the description (past, present or
181    /// future)
182    fn describe(&self, tense: Tense) -> String;
183    /// A verbose description of what running perform will do to the
184    /// system. Includes as many details as possible. Pass in the tense you want
185    /// for the description (past, present or future)
186    fn describe_detailed(&self, tense: Tense) -> String {
187        self.describe(tense)
188    }
189    /// Perform this install step making a change to the system. This may return
190    /// a [`RollbackStep`] that can be used to undo the change made in the
191    /// future. This can be used in an install wizard to roll back changes when
192    /// an error happens.
193    ///
194    /// # Errors
195    /// The system can change between preparing to install and actually
196    /// installing. For example all disk space could be used. Or the install
197    /// could run into an error that was not checked for while preparing. If you
198    /// find this happens please make an issue.
199    fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError>;
200    /// Is this a question and if so what options does the user have for responding?
201    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
248/// One step in the remove process. Can be executed or described.
249pub trait RemoveStep {
250    /// A short (one line) description of what this step will do to the
251    /// system. Pass in the tense you want for the description (past, present
252    /// or future)
253    fn describe(&self, tense: Tense) -> String;
254    /// A verbose description of what this step will do to the
255    /// system. Includes as many details as possible. Pass in the tense you want
256    /// for the description (past, present or future)
257    fn describe_detailed(&self, tense: Tense) -> String {
258        self.describe(tense)
259    }
260    /// Executes this remove step. This can be used when building an
261    /// uninstall/remove wizard. For example to ask the user confirmation
262    /// before each step.
263    ///
264    /// # Errors
265    /// The system can change between preparing to remove and actually removing
266    /// the install. For example a file could have been removed by the user of
267    /// the system. Or the removal could run into an error that was not checked
268    /// for while preparing. If you find this happens please make an issue.
269    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
324/// Undoes a [`InstallStep`]. Can be executed or described.
325pub trait RollbackStep {
326    /// Executes this rollback step. This can be used when building an install
327    /// wizard. You can [`describe()`](RollbackStep::describe) and then ask the
328    /// end user if the want to perform it.
329    ///
330    /// # Errors
331    /// The system could have changed between the install and the rollback.
332    /// Leading to various errors, mostly IO.
333    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/// Changes to the system that need to be applied to do the installation.
360///
361/// Returned by [`Spec::prepare_install`].Use
362/// [`install()`](InstallSteps::install) to apply all changes at once. This
363/// implements [`IntoIterator`] yielding [`InstallSteps`](InstallStep). These
364/// steps can be described possibly in detail and/or performed one by one.
365#[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    /// Perform all steps needed to install.
401    ///
402    /// # Errors
403    /// The system can change between preparing to install and actually
404    /// installing. For example all disk space could be used. Or the install
405    /// could run into an error that was not checked for while preparing. If you
406    /// find this happens please make an issue.
407    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    /// Prepare for installing. This makes a number of checks and if they are
420    /// passed it returns the [`InstallSteps`]. These implement [`IntoIterator`] and
421    /// can be inspected and executed one by one or executed in one step using
422    /// [`InstallSteps::install`].
423    ///
424    /// # Errors
425    /// Returns an error if:
426    ///  - the install is set to be system wide install while not running as admin/superuser.
427    ///  - the service should run as another user then the current one while not running as admin/superuser.
428    ///  - the service should run for a non-existing user.
429    ///  - no suitable install directory could be found.
430    ///  - the path for the executable does not point to a file.
431    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(&params) {
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
518/// Changes to the system that need to be applied to remove the installation.
519///
520/// Returned by [`Spec::prepare_remove`].Use
521/// [`remove()`](RemoveSteps::remove) to apply all changes at once. This
522/// implements [`IntoIterator`] yielding [`RemoveSteps`](RemoveStep). These
523/// steps can be described possibly in detail and/or performed one by one.
524pub 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    /// Perform all steps needed to remove an installation. Report what was done
559    /// at the end. Aborts on error.
560    ///
561    /// # Errors
562    /// The system can change between preparing to remove and actually removing
563    /// the install. For example a file could have been removed by the user of
564    /// the system. Or the removal could run into an error that was not checked
565    /// for while preparing. If you find this happens please make an issue.
566    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    /// Perform all steps needed to remove an installation. If any fail keep
577    /// going. Collect all the errors and report them at the end.
578    ///
579    /// # Errors
580    /// The system can change between preparing to remove and actually removing
581    /// the install. For example a file could have been removed by the user of
582    /// the system. Or the removal could run into an error that was not checked
583    /// for while preparing. If you find this happens please make an issue.
584    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    /// Prepare for removing an install. This makes a number of checks and if
620    /// they are passed it returns the [`RemoveSteps`]. These implement
621    /// [`IntoIterator`] and can be inspected and executed one by one or
622    /// executed in one step using [`RemoveSteps::remove`].
623    ///
624    /// # Errors
625    /// Returns an error if:
626    ///  - trying to remove a system install while not running as admin/superuser.
627    ///  - no install is found.
628    ///  - anything goes wrong setting up the removal.
629    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}