service_install/tui/
install.rs

1use std::collections::VecDeque;
2
3use crate::install::InstallError;
4use crate::install::InstallSteps;
5use crate::install::RollbackError;
6use crate::install::RollbackStep;
7use crate::install::StepOptions;
8use crate::Tense;
9
10use dialoguer::Confirm;
11use dialoguer::Select;
12
13#[derive(Debug, thiserror::Error)]
14pub enum Error {
15    #[error("canceled by the user")]
16    Canceled,
17    #[error("could not get input from the user")]
18    UserInputFailed(
19        #[from]
20        #[source]
21        dialoguer::Error,
22    ),
23    #[error("ran into one or more errors and user chose to abort, errors: {0:#?}")]
24    AbortedAfterError(Vec<InstallError>),
25    #[error("user chose to cancel and rollback however rollback failed")]
26    RollbackFollowingCancel(#[source] RollbackError),
27    #[error("ran into error user chose to abort and rollback however rollback failed")]
28    RollbackFollowingError(#[source] RollbackError),
29}
30
31/// Start an interactive installation wizard using the provided [install
32/// steps](InstallSteps). This wizard will ask the user to confirm each of the
33/// step. If anything goes wrong the user will be prompted if they wish to
34/// abort, abort and try to roll back the changes made or continue.
35///
36/// # Errors
37/// This returns an error if the user canceled the removal, something
38/// went wrong getting user input or anything during the removal failed.
39///
40/// In that last case either [`AbortedAfterError`](Error::AbortedAfterError),
41/// [`RollbackFollowingError`](Error::RollbackFollowingError) or
42/// [`RollbackFollowingCancel`](Error::RollbackFollowingCancel) is returned.
43/// Which depends on whether: the user aborted after the error, a rollback failed
44/// was started after an install error but the rollback failed *or* happened
45/// during install or a rollback was started after the user canceled but it
46/// failed.
47pub fn start(steps: InstallSteps, detailed: bool) -> Result<(), Error> {
48    let mut errors = Vec::new();
49    let mut rollback_steps = VecDeque::new();
50    for mut step in steps {
51        if detailed {
52            println!("{}", step.describe_detailed(Tense::Questioning));
53        } else {
54            println!("{}", step.describe(Tense::Questioning));
55        }
56
57        match step.options() {
58            Some(StepOptions::YesOrAbort) => {
59                if !Confirm::new().interact()? {
60                    rollback_if_user_wants_to(rollback_steps)?;
61                    return Err(Error::Canceled);
62                }
63            }
64            None => (),
65        }
66
67        match step.perform() {
68            Ok(None) => (),
69            Ok(Some(rollback)) => rollback_steps.push_front(rollback),
70            Err(e) => {
71                let details = format_error_chain(&e).replace('\n', "\n\t");
72                errors.push(e);
73
74                println!("An error occurred, details:\n\t{details}\t");
75                match Select::new()
76                    .with_prompt("What do you want to do?")
77                    .items(&["rollback and abort", "abort", "continue"])
78                    .default(0)
79                    .interact()?
80                {
81                    2 => continue,
82                    0 => rollback(rollback_steps).map_err(Error::RollbackFollowingError)?,
83                    _ => (),
84                }
85                return Err(Error::AbortedAfterError(errors));
86            }
87        }
88    }
89
90    Ok(())
91}
92
93fn rollback_if_user_wants_to(rollback_steps: VecDeque<Box<dyn RollbackStep>>) -> Result<(), Error> {
94    if rollback_steps.is_empty() {
95        println!("Install aborted, no changes have been made");
96    } else if Confirm::new()
97        .with_prompt("Install aborted, do you want to roll back any changes made?")
98        .interact()?
99    {
100        rollback(rollback_steps).map_err(Error::RollbackFollowingCancel)?;
101    }
102
103    Ok(())
104}
105
106fn rollback(mut rollback_steps: VecDeque<Box<dyn RollbackStep>>) -> Result<(), RollbackError> {
107    for step in &mut rollback_steps {
108        let did = step.describe(Tense::Past);
109        step.perform()?;
110        println!("{did}");
111    }
112    Ok(())
113}
114
115fn format_error_chain(root: &dyn std::error::Error) -> String {
116    let mut res = format!("1 {root}\n");
117
118    let mut i = 2;
119    let mut curr = root;
120    while let Some(next) = curr.source() {
121        res.push_str(&format!("{i} {next}\n"));
122        curr = next;
123        i += 1;
124    }
125
126    res
127}