service_install/
install.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
mod builder;

/// Errors and settings related to installing files
pub mod files;
/// Errors and settings related to the supported init systems
pub mod init;

use std::ffi::OsString;
use std::fmt::Display;

pub use builder::Spec;
use itertools::{Either, Itertools};

use crate::Tense;

use self::builder::ToAssign;
use self::init::cron::teardown::CrontabChanged;
use self::init::cron::{GetCrontabError, SetCrontabError};
use self::init::systemd::SystemCtlError;
use self::init::SetupError;

/// Whether to install system wide or for the current user only
#[derive(Debug, Clone, Copy)]
pub enum Mode {
    /// install for the current user, does not require running the installation
    /// as superuser/admin
    User,
    /// install to the entire system, the installation/removal must be ran as
    /// superuser/admin or it will return
    /// [`InstallError::NeedRootForSysInstall`] or [`PrepareRemoveError::NeedRoot`]
    System,
}

impl Mode {
    fn is_user(self) -> bool {
        match self {
            Mode::User => true,
            Mode::System => false,
        }
    }
}

impl Display for Mode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Mode::User => f.write_str("user"),
            Mode::System => f.write_str("system"),
        }
    }
}

/// Errors that can occur when preparing for or performing an installation
#[allow(clippy::module_name_repetitions)]
#[derive(thiserror::Error, Debug)]
pub enum PrepareInstallError {
    #[error("Error setting up init: {0}")]
    Init(#[from] init::SetupError),
    #[error("Failed to move files: {0}")]
    Move(#[from] files::MoveError),
    #[error("Need to run as root to install to system")]
    NeedRootForSysInstall,
    #[error("Need to run as root to setup service to run as another user")]
    NeedRootToRunAs,
    #[error("Could not find an init system we can set things up for")]
    NoInitSystemRecognized,
    #[error("Install configured to run as a user: `{0}` however this user does not exist")]
    UserDoesNotExist(String),
    #[error("All supported init systems found failed, errors: {0:?}")]
    SupportedInitSystemFailed(Vec<InitSystemFailure>),
}

/// The init system was found and we tried to set up the service but ran into an
/// error.
///
/// When there is another init system that does work this error is ignored. If
/// no other system is available or there is but it/they fail too this error is
/// reported.
///
/// A warning is always issued if the `tracing` feature is enabled.
#[derive(Debug, thiserror::Error)]
#[error("Init system: {name} ran into error: {error}")]
pub struct InitSystemFailure {
    name: String,
    error: SetupError,
}

/// Errors that can occur when preparing for or removing an installation
#[derive(thiserror::Error, Debug)]
pub enum PrepareRemoveError {
    #[error("Could not find this executable's location: {0}")]
    GetExeLocation(std::io::Error),
    #[error("Failed to remove files: {0}")]
    Move(#[from] files::DeleteError),
    #[error("Removing from init system: {0}")]
    Init(#[from] init::TearDownError),
    #[error("Could not find any installation in any init system")]
    NoInstallFound,
    #[error("Need to run as root to remove a system install")]
    NeedRoot,
}

#[allow(clippy::module_name_repetitions)]
#[derive(Debug, thiserror::Error)]
pub enum InstallError {
    #[error("Could not get crontab, needed to add our lines, error: {0}")]
    GetCrontab(#[from] init::cron::GetCrontabError),
    #[error("{0}")]
    CrontabChanged(#[from] init::cron::teardown::CrontabChanged),
    #[error("Could not set crontab, needed to add our lines, error: {0}")]
    SetCrontab(#[from] init::cron::SetCrontabError),
    #[error("Something went wrong interacting with systemd: {0}")]
    Systemd(#[from] init::systemd::Error),
    #[error("Could not copy executable: {0}")]
    CopyExe(std::io::Error),
    #[error("Could not set the owner of the installed executable to be root: {0}")]
    SetRootOwner(std::io::Error),
    #[error("Could not make the installed executable read only: {0}")]
    SetReadOnly(#[from] files::SetReadOnlyError),
    #[error("Can not disable Cron service, process will not stop.")]
    CouldNotStop,
}

/// One step in the install process. Can be executed or described.
#[allow(clippy::module_name_repetitions)]
pub trait InstallStep {
    /// A short (one line) description of what this performing this step will
    /// do. Pass in the tense you want for the description (past, present or
    /// future)
    fn describe(&self, tense: Tense) -> String;
    /// A verbose description of what performing this step will do to the
    /// system. Includes as many details as possible. Pass in the tense you want
    /// for the description (past, present or future)
    fn describe_detailed(&self, tense: Tense) -> String {
        self.describe(tense)
    }
    /// Perform this install step making a change to the system. This may return
    /// a [`RollbackStep`] that can be used to undo the change made in the
    /// future. This can be used in an install wizard to roll back changes when
    /// an error happens.
    ///
    /// # Errors
    /// The system can change between preparing to install and actually
    /// installing. For example all disk space could be used. Or the install
    /// could run into an error that was not checked for while preparing. If you
    /// find this happens please make an issue.
    fn perform(&mut self) -> Result<Option<Box<dyn RollbackStep>>, InstallError>;
}

impl std::fmt::Debug for &dyn InstallStep {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.describe(Tense::Future))
    }
}

impl Display for &dyn InstallStep {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.describe_detailed(Tense::Future))
    }
}

#[derive(Debug, thiserror::Error)]
pub enum RemoveError {
    #[error("Could not get crontab, needed tot filter out our added lines, error: {0}")]
    GetCrontab(#[from] init::cron::GetCrontabError),
    #[error("{0}")]
    CrontabChanged(#[from] init::cron::teardown::CrontabChanged),
    #[error("Could not set crontab, needed tot filter out our added lines, error: {0}")]
    SetCrontab(#[from] init::cron::SetCrontabError),
    #[error("Could not remove file(s), error: {0}")]
    DeleteError(#[from] files::DeleteError),
    #[error("Something went wrong interacting with systemd: {0}")]
    Systemd(#[from] init::systemd::Error),
}

/// One step in the remove process. Can be executed or described.
pub trait RemoveStep {
    /// A short (one line) description of what this performing this step will
    /// do. Pass in the tense you want for the description (past, present or future)
    fn describe(&self, tense: Tense) -> String;
    /// A verbose description of what performing this step will do to the
    /// system. Includes as many details as possible. Pass in the tense you want
    /// for the description (past, present or future)
    fn describe_detailed(&self, tense: Tense) -> String {
        self.describe(tense)
    }
    /// Executes this remove step. This can be used when building an
    /// uninstall/remove wizard. For example to ask the user confirmation
    /// before each step.
    ///
    /// # Errors
    /// The system can change between preparing to remove and actually removing
    /// the install. For example a file could have been removed by the user of
    /// the system. Or the removal could run into an error that was not checked
    /// for while preparing. If you find this happens please make an issue.
    fn perform(&mut self) -> Result<(), RemoveError>;
}

impl std::fmt::Debug for &dyn RemoveStep {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.describe(Tense::Future))
    }
}

impl Display for &dyn RemoveStep {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.describe_detailed(Tense::Future))
    }
}

#[derive(Debug, thiserror::Error)]
pub enum RollbackError {
    #[error("Could not rollback, error: {0}")]
    Removing(#[from] RemoveError),
    #[error("Could not rollback, error restoring file permissions: {0}")]
    RestoringPermissions(std::io::Error),
    #[error("Could not rollback, error re-enabling service: {0}")]
    ReEnabling(#[from] SystemCtlError),
    #[error("Can not rollback setting up cron, must be done manually")]
    Impossible,
    #[error("Crontab changed undoing changes might overwrite the change")]
    CrontabChanged(#[from] CrontabChanged),
    #[error("Could not get the crontab, needed to undo a change to it: {0}")]
    GetCrontab(#[from] GetCrontabError),
    #[error("Could not revert to the original crontab: {0}")]
    SetCrontab(#[from] SetCrontabError),
}

/// Undoes a [`InstallStep`]. Can be executed or described.
pub trait RollbackStep {
    /// Executes this rollback step. This can be used when building an install
    /// wizard. You can [`describe()`](RollbackStep::describe) and then ask the
    /// end user if the want to perform it.
    ///
    /// # Errors
    /// The system could have changed between the install and the rollback.
    /// Leading to various errors, mostly IO.
    fn perform(&mut self) -> Result<(), RollbackError>;
    fn describe(&self, tense: Tense) -> String;
}

impl std::fmt::Debug for &dyn RollbackStep {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.describe(Tense::Future))
    }
}

impl Display for &dyn RollbackStep {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.describe(Tense::Future))
    }
}

impl<T: RemoveStep> RollbackStep for T {
    fn perform(&mut self) -> Result<(), RollbackError> {
        Ok(self.perform()?)
    }

    fn describe(&self, tense: Tense) -> String {
        self.describe(tense)
    }
}

/// Changes to the system that need to be applied to do the installation.
///
/// Returned by [`Spec::prepare_install`].Use
/// [`install()`](InstallSteps::install) to apply all changes at once. This
/// implements [`IntoIterator`] yielding [`InstallSteps`](InstallStep). These
/// steps can be described possibly in detail and/or performed one by one.
#[allow(clippy::module_name_repetitions)]
pub struct InstallSteps(pub(crate) Vec<Box<dyn InstallStep>>);

impl std::fmt::Debug for InstallSteps {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for step in self.0.iter().map(|step| step.describe(Tense::Future)) {
            write!(f, "{step\n}")?;
        }
        Ok(())
    }
}

impl Display for InstallSteps {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for step in self
            .0
            .iter()
            .map(|step| step.describe_detailed(Tense::Future))
        {
            write!(f, "{step\n}")?;
        }
        Ok(())
    }
}

impl IntoIterator for InstallSteps {
    type Item = Box<dyn InstallStep>;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl InstallSteps {
    /// Perform all steps needed to install.
    ///
    /// # Errors
    /// The system can change between preparing to install and actually
    /// installing. For example all disk space could be used. Or the install
    /// could run into an error that was not checked for while preparing. If you
    /// find this happens please make an issue.
    pub fn install(self) -> Result<String, Box<dyn std::error::Error + Send + Sync + 'static>> {
        let mut description = Vec::new();
        for mut step in self.0 {
            description.push(step.describe(Tense::Past));
            step.perform()?;
        }

        Ok(description.join("\n"))
    }
}

impl<T: ToAssign> Spec<builder::Set, builder::Set, builder::Set, T> {
    /// Prepare for installing. This makes a number of checks and if they are
    /// passed it returns the [`InstallSteps`]. These implement [`IntoIterator`] and
    /// can be inspected and executed one by one or executed in one step using
    /// [`InstallSteps::install`].
    ///
    /// # Errors
    /// Returns an error if:
    ///  - the install is set to be system wide install while not running as admin/superuser.
    ///  - the service should run as another user then the current one while not running as admin/superuser.
    ///  - the service should run for a non-existing user.
    ///  - no suitable install directory could be found.
    ///  - the path for the executable does not point to a file.
    pub fn prepare_install(self) -> Result<InstallSteps, PrepareInstallError> {
        let builder::Spec {
            mode,
            path: Some(source),
            service_name: Some(name),
            bin_name,
            args,
            trigger: Some(trigger),
            overwrite_existing,
            working_dir,
            run_as,
            description,
            ..
        } = self
        else {
            unreachable!("type sys guarantees path, name and trigger set")
        };

        let not_root = matches!(sudo::check(), sudo::RunningAs::User);
        if let Mode::System = mode {
            if not_root {
                return Err(PrepareInstallError::NeedRootForSysInstall);
            }
        }

        if let Some(ref user) = run_as {
            let curr_user = uzers::get_current_username()
                .ok_or_else(|| PrepareInstallError::UserDoesNotExist(user.clone()))?;
            if curr_user != OsString::from(user) && not_root {
                return Err(PrepareInstallError::NeedRootToRunAs);
            }
        }

        let init_systems = self.init_systems.unwrap_or_else(init::System::all);
        let (mut steps, exe_path) = files::move_files(
            source,
            mode,
            run_as.as_deref(),
            overwrite_existing,
            &init_systems,
        )?;
        let params = init::Params {
            name,
            bin_name,
            description,

            exe_path,
            exe_args: args,
            working_dir,

            trigger,
            run_as,
            mode,
        };

        let mut errors = Vec::new();
        for init in init_systems {
            if init.not_available().map_err(PrepareInstallError::Init)? {
                continue;
            }

            match init.set_up_steps(&params) {
                Ok(init_steps) => {
                    steps.extend(init_steps);
                    return Ok(InstallSteps(steps));
                }
                Err(err) => {
                    #[cfg(feature = "tracing")]
                    tracing::warn!("Could not set up init using {}, error: {err}", init.name());
                    errors.push(InitSystemFailure {
                        name: init.name().to_owned(),
                        error: err,
                    });
                }
            };
        }

        if errors.is_empty() {
            Err(PrepareInstallError::NoInitSystemRecognized)
        } else {
            Err(PrepareInstallError::SupportedInitSystemFailed(errors))
        }
    }
}

/// Changes to the system that need to be applied to remove the installation.
///
/// Returned by [`Spec::prepare_remove`].Use
/// [`remove()`](RemoveSteps::remove) to apply all changes at once. This
/// implements [`IntoIterator`] yielding [`RemoveSteps`](RemoveStep). These
/// steps can be described possibly in detail and/or performed one by one.
pub struct RemoveSteps(pub(crate) Vec<Box<dyn RemoveStep>>);

impl std::fmt::Debug for RemoveSteps {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for step in self.0.iter().map(|step| step.describe(Tense::Future)) {
            write!(f, "{step\n}")?;
        }
        Ok(())
    }
}

impl Display for RemoveSteps {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        for step in self
            .0
            .iter()
            .map(|step| step.describe_detailed(Tense::Future))
        {
            write!(f, "{step\n}")?;
        }
        Ok(())
    }
}

impl IntoIterator for RemoveSteps {
    type Item = Box<dyn RemoveStep>;
    type IntoIter = std::vec::IntoIter<Self::Item>;

    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}

impl RemoveSteps {
    /// Perform all steps needed to remove an installation. Report what was done
    /// at the end. Aborts on error.
    ///
    /// # Errors
    /// The system can change between preparing to remove and actually removing
    /// the install. For example a file could have been removed by the user of
    /// the system. Or the removal could run into an error that was not checked
    /// for while preparing. If you find this happens please make an issue.
    pub fn remove(self) -> Result<String, Box<dyn std::error::Error + Send + Sync + 'static>> {
        let mut description = Vec::new();
        for mut step in self.0 {
            description.push(step.describe(Tense::Past));
            step.perform()?;
        }

        Ok(description.join("\n"))
    }

    /// Perform all steps needed to remove an installation. If any fail keep
    /// going. Collect all the errors and report them at the end.
    ///
    /// # Errors
    /// The system can change between preparing to remove and actually removing
    /// the install. For example a file could have been removed by the user of
    /// the system. Or the removal could run into an error that was not checked
    /// for while preparing. If you find this happens please make an issue.
    pub fn best_effort_remove(self) -> Result<String, BestEffortRemoveError> {
        let (description, failures): (Vec<_>, Vec<_>) =
            self.0
                .into_iter()
                .partition_map(|mut step| match step.perform() {
                    Ok(()) => Either::Left(step.describe(Tense::Past)),
                    Err(e) => Either::Right((step.describe_detailed(Tense::Active), e)),
                });

        if failures.is_empty() {
            Ok(description.join("\n"))
        } else {
            Err(BestEffortRemoveError { failures })
        }
    }
}

#[derive(Debug, thiserror::Error)]
pub struct BestEffortRemoveError {
    failures: Vec<(String, RemoveError)>,
}

impl Display for BestEffortRemoveError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "Ran into one or more issues trying to remove an install")?;
        writeln!(f, "You should resolve/check these issues manually")?;
        for (task, error) in &self.failures {
            let task = task.to_lowercase();
            writeln!(f, "* Tried to {task}\nfailed because: {error}")?;
        }
        Ok(())
    }
}

impl<M: ToAssign, P: ToAssign, T: ToAssign, I: ToAssign> Spec<M, P, T, I> {
    /// Prepare for removing an install. This makes a number of checks and if
    /// they are passed it returns the [`RemoveSteps`]. These implement
    /// [`IntoIterator`] and can be inspected and executed one by one or
    /// executed in one step using [`RemoveSteps::remove`].
    ///
    /// # Errors
    /// Returns an error if:
    ///  - trying to remove a system install while not running as admin/superuser.
    ///  - no install is found.
    ///  - anything goes wrong setting up the removal.
    pub fn prepare_remove(self) -> Result<RemoveSteps, PrepareRemoveError> {
        let builder::Spec {
            mode,
            service_name: Some(name),
            bin_name,
            run_as,
            ..
        } = self
        else {
            unreachable!("type sys guarantees name and trigger set")
        };

        if let Mode::System = mode {
            if let sudo::RunningAs::User = sudo::check() {
                return Err(PrepareRemoveError::NeedRoot);
            }
        }

        let mut inits = self.init_systems.unwrap_or(init::System::all()).into_iter();
        let (mut steps, path) = loop {
            let Some(init) = inits.next() else {
                return Err(PrepareRemoveError::NoInstallFound);
            };

            if let Some(install) = init.tear_down_steps(&name, bin_name, mode, run_as.as_deref())? {
                break install;
            }
        };

        let remove_step = files::remove_files(path);
        steps.push(Box::new(remove_step));
        Ok(RemoveSteps(steps))
    }
}