Skip to main content

zng_ext_setup/
service.rs

1#![allow(clippy::result_large_err)]
2
3use core::fmt;
4use std::{any::Any, collections::VecDeque, pin::Pin, sync::Arc};
5
6use zng_app::{event::app_local, update::UPDATES};
7use zng_clone_move::clmv;
8use zng_ext_config::RawConfigValue;
9use zng_task::{Progress, parking_lot::Mutex};
10use zng_txt::Txt;
11use zng_var::{ResponderVar, ResponseVar, Var, VarEq, VarValue, const_var, response_var, var};
12
13use crate::task::{SetupTask, SetupTaskError, SetupTaskType, TaskTypeId};
14
15/// Setup service.
16///
17/// This service runs [install] and [uninstall] operations sequentially. Operations
18/// start once the current app update finishes and there are no other running operations.
19///
20/// [install]:  Self::install
21/// [uninstall]:  Self::uninstall
22pub struct SETUP;
23
24impl SETUP {
25    /// Register a custom task type.
26    pub fn register_task_type<T: SetupTask>(&self) {
27        self.register_task_type_impl(SetupTaskType::new::<T>());
28    }
29    fn register_task_type_impl(&self, t: SetupTaskType) {
30        UPDATES.once_update("register_task_type", move || {
31            let mut sv = SETUP_SV.write();
32            let id = (t.task_type_id)();
33            if let Some(e) = sv.task_types.iter_mut().find(|t| (t.task_type_id)() == id) {
34                *e = t;
35            } else {
36                sv.task_types.push(t);
37            }
38        });
39    }
40
41    /// Enqueue a new install operation.
42    ///
43    /// This will [prepare] and [commit] an install.
44    ///
45    /// Returns a response var that updates once with the result of the operation. If
46    /// successful an [`UninstallConfig`], this data can be (de)serialized and used with
47    /// to [`uninstall`].
48    ///
49    /// [prepare]: Self::prepare_install
50    /// [commit]: Self::commit_install
51    /// [`uninstall`]: Self::uninstall
52    pub fn install(&self, config: InstallConfig, update: Option<UninstallConfig>) -> ResponseVar<Result<UninstallConfig, SetupError>> {
53        let (r, rsp) = response_var();
54        UPDATES.once_update("install", move || {
55            SETUP_SV.write().run(async move { install(config, update).await }, r);
56        });
57        rsp
58    }
59
60    /// Enqueue a new prepare install operation.
61    ///
62    /// This will run all expensive install operations that can run without affecting the system or previous installs.
63    /// One reason to run this step separate is to begin an update installation while the application is still running.
64    ///
65    /// If `update` is set the tasks will use it to find and patch/replace a previous install.
66    ///
67    /// Returns a response var that updates once with the result of the operation. If
68    /// successful a [`PreparedInstallConfig`], this data can be (de)serialized and used with
69    /// to [`commit_install`] or [`cancel_prepared`].
70    ///
71    /// [`commit_install`]: Self::commit_install
72    /// [`cancel_prepared`]: Self::cancel_prepared
73    pub fn prepare_install(
74        &self,
75        config: InstallConfig,
76        update: Option<UninstallConfig>,
77    ) -> ResponseVar<Result<PreparedInstallConfig, SetupError>> {
78        let (r, rsp) = response_var();
79        UPDATES.once_update("prepare_install", move || {
80            SETUP_SV.write().run(async move { prepare_install(config, update).await }, r);
81        });
82        rsp
83    }
84
85    /// Enqueue a prepared install cancellation operation.
86    ///
87    /// Note that [`prepare_install`] will automatically cancel if requested. This method cancels
88    /// a prepared install that already completed.
89    ///
90    /// [`prepare_install`]: Self::prepare_install
91    pub fn cancel_prepared(&self, config: PreparedInstallConfig) -> ResponseVar<Result<(), SetupError>> {
92        let (r, rsp) = response_var();
93        UPDATES.once_update("cancel_prepared", move || {
94            SETUP_SV.write().run(async move { cancel_prepared(config).await }, r);
95        });
96        rsp
97    }
98
99    /// Enqueue a new commit prepared install operation.
100    ///
101    /// This operation cannot be canceled once it starts, if cancel is requested while enqueued
102    /// the [`cancel_prepared`] operation will run instead.
103    ///
104    /// Returns a response var that updates once with the result of the operation. If
105    /// successful an [`UninstallConfig`], this data can be (de)serialized and used with
106    /// to [`uninstall`].
107    ///
108    /// [`cancel_prepared`]: Self::cancel_prepared
109    /// [`uninstall`]: Self::uninstall
110    pub fn commit_install(&self, config: PreparedInstallConfig) -> ResponseVar<Result<UninstallConfig, SetupError>> {
111        let (r, rsp) = response_var();
112        UPDATES.once_update("commit_install", move || {
113            SETUP_SV.write().run(async move { commit_install(config).await }, r);
114        });
115        rsp
116    }
117
118    /// Enqueue a new uninstall operation.
119    ///
120    /// This will quickly [validate] the installation and uninstall. During uninstall
121    /// the operation cannot be canceled.
122    ///
123    /// Returns a response var that updates once with the result of the operation.
124    ///
125    /// [validate]: Self::validate_uninstall
126    pub fn uninstall(&self, config: UninstallConfig) -> ResponseVar<Result<(), SetupError>> {
127        let (r, rsp) = response_var();
128        UPDATES.once_update("uninstall", move || {
129            SETUP_SV.write().run(async move { uninstall(config).await }, r);
130        });
131        rsp
132    }
133
134    /// Enqueue a new validate uninstall config operation.
135    ///
136    /// This will verify that the uninstall config can still be used to [`uninstall`].
137    ///
138    /// Returns a response var that updates once with the result of the operation. If
139    /// successful the config data is returned, potentially corrected if recoverable issues where found.
140    ///
141    /// [`uninstall`]: Self::uninstall
142    pub fn validate_uninstall(&self, config: UninstallConfig) -> ResponseVar<Result<UninstallConfig, SetupError>> {
143        let (r, rsp) = response_var();
144        UPDATES.once_update("validate_uninstall", move || {
145            SETUP_SV.write().run(async move { validate_uninstall(config).await }, r);
146        });
147        rsp
148    }
149
150    /// Status of running operation.
151    pub fn status(&self) -> Var<SetupStatus> {
152        SETUP_SV.read().status.read_only()
153    }
154}
155
156type SetupOp = Pin<Box<dyn Future<Output = ()> + Send>>;
157
158struct Setup {
159    task_types: Vec<SetupTaskType>,
160    queue: Mutex<VecDeque<SetupOp>>, // Mutex for +Sync only
161    status: Var<SetupStatus>,
162    cancel: Var<bool>,
163}
164app_local! {
165    static SETUP_SV: Setup = Setup {
166        task_types: vec![
167            SetupTaskType::new::<crate::task::ExtractTar>(),
168            #[cfg(any(windows, target_os = "linux"))]
169            SetupTaskType::new::<crate::task::CreateShortcut>(),
170            #[cfg(windows)]
171            SetupTaskType::new::<crate::task::RegisterUninstaller>(),
172        ],
173        queue: Mutex::default(),
174        status: var(SetupStatus::Idle),
175        cancel: var(false),
176    };
177}
178
179/// Represents a list of tasks for a [`SETUP.install`] operation.
180///
181/// [`SETUP.install`]: SETUP::install
182#[derive(Default)]
183pub struct InstallConfig {
184    cfg: Vec<(SetupTaskType, Box<dyn Any + Send>)>,
185    tasks: Vec<(TaskTypeId, Txt)>,
186}
187impl InstallConfig {
188    /// New empty.
189    pub fn new() -> Self {
190        Self::default()
191    }
192
193    /// Push a task to run.
194    ///
195    /// The `name` is used to identify the task instance in progress status.
196    pub fn push<T: SetupTask>(&mut self, name: impl Into<Txt>, config: T::InstallConfig) {
197        let t = SetupTaskType::new::<T>();
198        self.tasks.push(((t.task_type_id)(), name.into()));
199        self.cfg.push((t, Box::new(config)))
200    }
201
202    /// Task types and names in order they will execute.
203    pub fn tasks(&self) -> &[(TaskTypeId, Txt)] {
204        &self.tasks
205    }
206
207    /// Inspect the task config.
208    pub fn config<T: SetupTask>(&self, index: usize) -> Option<&T::InstallConfig> {
209        self.cfg.get(index)?.1.downcast_ref()
210    }
211}
212
213/// Represents data generated  by [`SETUP.prepare_install`] that can be used to run a [`SETUP.commit_install`] operation.
214///
215/// [`SETUP.prepare_install`]: SETUP::prepare_install
216/// [`SETUP.commit_install`]: SETUP::commit_install
217#[derive(Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
218pub struct PreparedInstallConfig {
219    tasks: Vec<(TaskTypeId, Txt)>,
220    cfg: Vec<RawConfigValue>,
221}
222impl PreparedInstallConfig {
223    /// Task types and names in order they will execute.
224    pub fn tasks(&self) -> &[(TaskTypeId, Txt)] {
225        &self.tasks
226    }
227}
228
229/// Represents data generated by [`SETUP.install`] that can be used to run a [`SETUP.uninstall`] operation.
230///
231/// [`SETUP.install`]: SETUP::install
232/// [`SETUP.uninstall`]: SETUP::uninstall
233#[derive(Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize)]
234pub struct UninstallConfig {
235    tasks: Vec<(TaskTypeId, Txt)>,
236    cfg: Vec<RawConfigValue>,
237}
238impl UninstallConfig {
239    /// Task types and names in order they will execute.
240    pub fn tasks(&self) -> &[(TaskTypeId, Txt)] {
241        &self.tasks
242    }
243}
244
245#[cfg(feature = "save")]
246macro_rules! impl_save {
247    ($($Config:ident),+) => {$(
248
249#[cfg(feature = "save")]
250impl $Config {
251    /// Serialize and write config to file.
252    ///
253    /// The format is a ZStandard compressed JSON.
254    pub fn save_blocking(&self, file: &std::path::Path) -> std::io::Result<()> {
255        save(self, file)
256    }
257
258    /// Serialize and write config to file.
259    ///
260    /// The format is a ZStandard compressed JSON.
261    pub async fn save(self, file: std::path::PathBuf) -> std::io::Result<()> {
262        zng_task::wait(move || save(&self, &file)).await
263    }
264
265    /// Read and deserialize config from file.
266    ///
267    /// The format must be a ZStandard compressed JSON.
268    pub fn load_blocking(file: &std::path::Path) -> std::io::Result<Self> {
269        load(file)
270    }
271
272    /// Read and deserialize config from file.
273    ///
274    /// The format must be a ZStandard compressed JSON.
275    pub async fn load(file: std::path::PathBuf) -> std::io::Result<Self> {
276        zng_task::wait(move || load(&file)).await
277    }
278}
279    )+};
280}
281#[cfg(feature = "save")]
282impl_save! { UninstallConfig, PreparedInstallConfig }
283
284#[cfg(feature = "save")]
285fn save(config: &impl serde::Serialize, file: &std::path::Path) -> std::io::Result<()> {
286    let file = std::fs::File::create(file)?;
287    let mut zstd = zstd::Encoder::new(file, 22)?;
288    serde_json::to_writer(&mut zstd, config)?;
289    zstd.finish()?;
290    Ok(())
291}
292
293#[cfg(feature = "save")]
294fn load<T: serde::de::DeserializeOwned>(file: &std::path::Path) -> std::io::Result<T> {
295    let file = std::fs::File::open(file)?;
296    // optimal BufReader created internally by decoder
297    let zstd = zstd::Decoder::new(file)?;
298    let cfg = serde_json::from_reader(zstd)?;
299    Ok(cfg)
300}
301
302/// Represents a [`SETUP`] operation error.
303#[derive(Clone, PartialEq, Debug)]
304#[non_exhaustive]
305pub struct SetupError {
306    /// Error associated with operation itself that affects all tasks.
307    ///
308    /// This is often [`SetupTaskError::CorruptedTaskData`] detected
309    pub op_error: Option<SetupTaskError>,
310
311    /// Errors associated with a task in the operation.
312    ///
313    /// Each task is identified by index on the operation, type and name.
314    pub task_errors: Vec<((usize, TaskTypeId, Txt), SetupTaskError)>,
315
316    /// Operation state after not completing successfully.
317    pub state: SetupErrorState,
318}
319impl SetupError {
320    /// No actual error, canceled by request.
321    pub fn canceled() -> Self {
322        Self {
323            op_error: None,
324            task_errors: vec![],
325            state: SetupErrorState::Canceled,
326        }
327    }
328
329    /// One or more tasks failed.
330    ///
331    /// Each task is identified by index on the operation, type and name.
332    ///
333    /// If the tasks managed to reverse all changes before committing the `state` must be `Canceled`.
334    pub fn task_errors(errors: Vec<((usize, TaskTypeId, Txt), SetupTaskError)>, state: SetupErrorState) -> Self {
335        Self {
336            op_error: None,
337            task_errors: errors,
338            state,
339        }
340    }
341
342    /// Operation failed with error associated with operation itself that affects all tasks.
343    ///
344    /// If the error is detected before any task runs and any non-destructive change was made the `state` must be `Canceled`.
345    pub fn op_error(error: SetupTaskError, state: SetupErrorState) -> Self {
346        Self {
347            op_error: Some(error),
348            task_errors: vec![],
349            state,
350        }
351    }
352
353    /// Operation cannot start due to corrupted config data.
354    pub fn corrupted_op_config(config_name: &'static str) -> Self {
355        #[derive(Debug)]
356        struct CorruptedOpConfig(&'static str);
357        impl fmt::Display for CorruptedOpConfig {
358            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359                write!(f, "{} is corrupted", self.0)
360            }
361        }
362        impl std::error::Error for CorruptedOpConfig {}
363        Self::op_error(
364            SetupTaskError::CorruptedTaskData(Arc::new(CorruptedOpConfig(config_name))),
365            SetupErrorState::Canceled,
366        )
367    }
368}
369
370/// Represents state of a setup operation that ended in a [`SetupError`].
371#[derive(Clone, PartialEq, Debug)]
372pub enum SetupErrorState {
373    /// Canceled successfully, all changes where reverted.
374    ///
375    /// The operation is canceled by request or by error before it starts committing irreversible changes,
376    /// either way the system and any previous installation is not affected when ended in this state.
377    Canceled,
378
379    /// Install operation failed before it started making irreversible changes and failed to cleanup
380    /// temporary changes.
381    ///
382    /// When an install fails during the preparing phase it automatically attempts to *cancel*, this is
383    /// the error when that cancel fails. If the install was an update the previous version will still be valid.
384    PartialPrepareInstall,
385
386    /// Install operation failed while making irreversible changes to the system.
387    ///
388    /// Operation attempts to complete as much of the install as possible, the associated `data`
389    /// can be used to uninstall the committed changes. Well designed tasks will cleanup all temporary
390    /// *prepared* data on error and generate uninstall data that cleanups even partial written files, but
391    /// there is no guarantee that this data will fully uninstall every change.
392    ///
393    /// If the operation was replacing a previous install (update or repair) the `data` will also
394    /// uninstall the previous installation.
395    PartialInstall {
396        /// Data that can uninstall all the successfully committed changes made during the failed install.
397        data: UninstallConfig,
398        /// Tasks that failed without generating uninstall data.
399        ///
400        /// If this is not empty uninstalling `data` will definitely not fully cleanup the broken install.
401        ///
402        /// Tasks are identified by index on the operation, type and name.
403        no_data: Vec<(usize, TaskTypeId, Txt)>,
404    },
405    /// Uninstall operation failed while making irreversible changes to the system.
406    ///
407    /// Operation attempts to complete as much of the uninstall as possible, so all tasks without error
408    /// have completed successfully.
409    ///
410    ///
411    PartialUninstall,
412}
413
414/// Represents status of [`SETUP`].
415#[derive(Debug, PartialEq, Clone)]
416#[non_exhaustive]
417pub enum SetupStatus {
418    /// No setup operation is running.
419    Idle,
420    /// Prepare install is running or complete.
421    PrepareInstall(SetupOpStatus),
422    /// Commit install is running or complete.
423    CommitInstall(SetupOpStatus),
424    /// Validate uninstall is running or complete.
425    ValidateUninstall(SetupOpStatus),
426    /// Uninstall is running or complete.
427    Uninstall(SetupOpStatus),
428}
429impl SetupStatus {
430    /// If is `Idle` or op is complete.
431    pub fn is_idle(&self) -> bool {
432        match self {
433            SetupStatus::Idle => true,
434            SetupStatus::PrepareInstall(s)
435            | SetupStatus::CommitInstall(s)
436            | SetupStatus::ValidateUninstall(s)
437            | SetupStatus::Uninstall(s) => s.is_complete(),
438        }
439    }
440
441    /// Get operation status.
442    pub fn op_status(&self) -> Option<&SetupOpStatus> {
443        match self {
444            SetupStatus::Idle => None,
445            SetupStatus::PrepareInstall(s)
446            | SetupStatus::CommitInstall(s)
447            | SetupStatus::ValidateUninstall(s)
448            | SetupStatus::Uninstall(s) => Some(s),
449        }
450    }
451}
452
453/// Represents status of a [`SETUP`] install or uninstall operation.
454#[derive(Debug, PartialEq, Clone)]
455#[non_exhaustive]
456pub struct SetupOpStatus {
457    /// Is cancelling.
458    pub cancel: bool,
459    /// Current task.
460    pub task: (TaskTypeId, Txt),
461    /// Task index of len.
462    pub progress: (usize, usize),
463    /// Progress report from task.
464    pub task_progress: VarEq<Progress>,
465
466    /// Errors.
467    ///
468    /// The task is identified by index, type and name.
469    pub errors: Vec<((usize, TaskTypeId, Txt), SetupTaskError)>,
470}
471impl SetupOpStatus {
472    /// If `progress` is last task and `task_progress` is complete.
473    ///
474    /// Note that tasks report completion on error.
475    pub fn is_complete(&self) -> bool {
476        self.progress.0 == self.progress.1.saturating_sub(1) && self.task_progress.with(|p| p.is_complete())
477    }
478
479    /// If `cancel` and `is_complete`.
480    ///
481    /// If this is `true` and `errors` is not empty the tasks managed to cleanup and the install is not corrupted.
482    pub fn is_canceled(&self) -> bool {
483        self.cancel && self.is_complete()
484    }
485
486    /// If `is_complete`, not `cancel` and has `errors`.
487    ///
488    /// If this is `true` the tasks did not manage to cleanup and the install is in a corrupted state.
489    pub fn is_corrupted(&self) -> bool {
490        !self.errors.is_empty() && !self.cancel && self.is_complete()
491    }
492}
493
494impl Setup {
495    fn run<R: VarValue>(
496        &mut self,
497        op: impl Future<Output = Result<R, SetupError>> + Send + 'static,
498        r: ResponderVar<Result<R, SetupError>>,
499    ) {
500        self.run_impl(Box::pin(async move {
501            let res = op.await;
502            r.respond(res);
503        }));
504    }
505    fn run_impl(&mut self, op: SetupOp) {
506        let q = self.queue.get_mut();
507        q.push_back(op);
508        if q.len() == 1 {
509            zng_task::spawn(async {
510                fn next_op() -> Option<SetupOp> {
511                    let mut sv = SETUP_SV.write();
512                    let op = sv.queue.get_mut().pop_front();
513                    if op.is_some() {
514                        // ensure op will not retain errors from previous op
515                        sv.status.set(SetupStatus::Idle);
516                    }
517                    op
518                }
519                while let Some(op) = next_op() {
520                    op.await;
521                }
522            });
523        }
524    }
525
526    fn task_type(&self, id: &TaskTypeId) -> Result<SetupTaskType, SetupTaskError> {
527        for t in &self.task_types {
528            if &(t.task_type_id)() == id {
529                return Ok(t.clone());
530            }
531        }
532        Err(SetupTaskError::UnknownType(id.clone()))
533    }
534}
535
536async fn install(config: InstallConfig, update: Option<UninstallConfig>) -> Result<UninstallConfig, SetupError> {
537    let config = prepare_install(config, update).await?;
538    if SETUP_SV.read().cancel.get() {
539        cancel_prepared(config).await?;
540        Err(SetupError::canceled())
541    } else {
542        commit_install(config).await
543    }
544}
545
546async fn prepare_install(config: InstallConfig, update: Option<UninstallConfig>) -> Result<PreparedInstallConfig, SetupError> {
547    let (status, cancel) = {
548        let sv = SETUP_SV.read();
549        (sv.status.clone(), sv.cancel.clone())
550    };
551
552    if let Some(u) = &update
553        && u.tasks.len() != u.cfg.len()
554    {
555        return Err(SetupError::corrupted_op_config("UninstallConfig"));
556    }
557
558    let tasks_len = config.tasks.len();
559    if tasks_len != config.cfg.len() {
560        return Err(SetupError::corrupted_op_config("InstallConfig"));
561    }
562    let mut prepared_cfg: Vec<RawConfigValue> = Vec::with_capacity(tasks_len);
563    let mut error = None;
564    for (i, (id, (task_ty, cfg))) in config.tasks.iter().zip(config.cfg).enumerate() {
565        let task_progress = var(Progress::indeterminate());
566        // notify new task started
567        let task_progress_s = task_progress.read_only();
568        status.modify(clmv!(id, |a| {
569            match a.value_mut() {
570                SetupStatus::PrepareInstall(s) => {
571                    s.task = id;
572                    s.progress.0 = i;
573                    s.task_progress = VarEq(task_progress_s);
574                }
575                _ => {
576                    **a = SetupStatus::PrepareInstall(SetupOpStatus {
577                        cancel: false,
578                        task: id,
579                        progress: (i, tasks_len),
580                        task_progress: VarEq(task_progress_s),
581                        errors: vec![],
582                    });
583                }
584            }
585        }));
586
587        // find previous install
588        let mut uninstall_data = None;
589        if let Some(u) = &update {
590            // uninstall is reversed
591            if let Some(i) = u.cfg.len().checked_sub(i + 1)
592                && id == &u.tasks[i]
593            {
594                uninstall_data = Some(u.cfg[i].clone());
595            }
596
597            if uninstall_data.is_none() {
598                #[derive(Debug)]
599                struct TaskTypeMismatch;
600                impl fmt::Display for TaskTypeMismatch {
601                    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
602                        write!(f, "expected different task type")
603                    }
604                }
605                impl std::error::Error for TaskTypeMismatch {}
606                error = Some((
607                    (i, id.0.clone(), id.1.clone()),
608                    SetupTaskError::CorruptedTaskData(Arc::new(TaskTypeMismatch)),
609                ));
610                break;
611            }
612        }
613
614        // run task
615        let r = (task_ty.prepare_install)(cfg, uninstall_data, task_progress.clone(), cancel.read_only()).await;
616        task_progress.set(Progress::complete());
617        match r {
618            Ok(r) => prepared_cfg.push(r),
619            Err(e) => {
620                // go to cancel due to error
621                error = Some(((i, id.0.clone(), id.1.clone()), e));
622                break;
623            }
624        }
625        if cancel.get() {
626            break;
627        }
628    }
629
630    let prepared_cfg = PreparedInstallConfig {
631        tasks: config.tasks,
632        cfg: prepared_cfg,
633    };
634
635    if let Some(e) = error {
636        status.modify(clmv!(e, |a| {
637            // add error, cancel_prepared will preserve it as it updates status
638            if let SetupStatus::PrepareInstall(s) = a.value_mut() {
639                s.errors.push(e);
640                // immediately indicate cancelling too, to avoid notifying completion
641                s.task_progress = VarEq(const_var(Progress::indeterminate()));
642                s.cancel = true;
643            }
644        }));
645
646        // cancel due to error
647        if let Err(mut ce) = cancel_prepared(prepared_cfg).await {
648            // did not cleanup prepared either.
649            ce.state = SetupErrorState::PartialPrepareInstall;
650            ce.task_errors.insert(0, e);
651            Err(ce)
652        } else {
653            Err(SetupError::task_errors(vec![e], SetupErrorState::Canceled))
654        }
655    } else if cancel.get() {
656        // cancel due to request
657        cancel_prepared(prepared_cfg).await?;
658        Err(SetupError::canceled())
659    } else {
660        // ensure general status updates to complete
661        status.modify(clmv!(|a| {
662            if let SetupStatus::PrepareInstall(s) = a.value_mut() {
663                s.task_progress = VarEq(const_var(Progress::complete()));
664            }
665        }));
666        Ok(prepared_cfg)
667    }
668}
669
670async fn cancel_prepared(config: PreparedInstallConfig) -> Result<(), SetupError> {
671    let status = SETUP_SV.read().status.clone();
672
673    let tasks_len = config.cfg.len();
674    if tasks_len > config.tasks.len() {
675        return Err(SetupError::corrupted_op_config("PreparedInstallConfig"));
676    }
677
678    let mut errors = vec![];
679
680    for (i, (id, cfg)) in config.tasks.into_iter().zip(config.cfg).enumerate() {
681        let task_progress = var(Progress::indeterminate());
682        // notify new task started
683        let task_progress_s = task_progress.read_only();
684        status.modify(clmv!(id, |a| {
685            match a.value_mut() {
686                SetupStatus::PrepareInstall(s) => {
687                    s.cancel = true;
688                    s.task = id;
689                    s.progress = (i, tasks_len);
690                    s.task_progress = VarEq(task_progress_s);
691                }
692                _ => {
693                    **a = SetupStatus::PrepareInstall(SetupOpStatus {
694                        cancel: true,
695                        task: id,
696                        progress: (i, tasks_len),
697                        task_progress: VarEq(task_progress_s),
698                        errors: vec![],
699                    });
700                }
701            }
702        }));
703
704        // run task
705        let task_ty = SETUP_SV.read().task_type(&id.0);
706        let error = match task_ty {
707            Ok(task_ty) => {
708                let r = (task_ty.cancel_install)(cfg, task_progress.clone()).await;
709                r.err()
710            }
711            Err(e) => Some(e),
712        };
713
714        if let Some(e) = error {
715            let e = ((i, id.0, id.1), e);
716            // notify error
717            status.modify(clmv!(e, |a| {
718                if let SetupStatus::PrepareInstall(s) = a.value_mut() {
719                    s.errors.push(e);
720                    s.cancel = false;
721                    s.task_progress = VarEq(const_var(Progress::complete()));
722                }
723            }));
724            errors.push(e);
725
726            // continues trying to cancel other tasks for best effort cleanup
727        } else {
728            task_progress.set(Progress::complete());
729        }
730    }
731
732    if errors.is_empty() {
733        Ok(())
734    } else {
735        Err(SetupError::task_errors(errors, SetupErrorState::PartialPrepareInstall))
736    }
737}
738
739async fn commit_install(config: PreparedInstallConfig) -> Result<UninstallConfig, SetupError> {
740    let status = SETUP_SV.read().status.clone();
741
742    let tasks_len = config.tasks.len();
743    if tasks_len != config.cfg.len() {
744        return Err(SetupError::corrupted_op_config("PreparedInstallConfig"));
745    }
746
747    let mut errors = vec![];
748    let mut uninstall_cfg = vec![];
749    let mut err_no_clean = vec![];
750
751    for (i, (id, cfg)) in config.tasks.iter().zip(config.cfg).enumerate() {
752        let task_progress = var(Progress::indeterminate());
753        // notify new task started
754        let task_progress_s = task_progress.read_only();
755        status.modify(clmv!(id, |a| {
756            match a.value_mut() {
757                SetupStatus::CommitInstall(s) => {
758                    s.task = id;
759                    s.progress.0 = i;
760                    s.task_progress = VarEq(task_progress_s);
761                }
762                _ => {
763                    **a = SetupStatus::CommitInstall(SetupOpStatus {
764                        cancel: false,
765                        task: id,
766                        progress: (i, tasks_len),
767                        task_progress: VarEq(task_progress_s),
768                        errors: vec![],
769                    })
770                }
771            }
772        }));
773
774        // run task
775        let task_ty = SETUP_SV.read().task_type(&id.0);
776        let mut error = None;
777        match task_ty {
778            Ok(task_ty) => match (task_ty.install)(cfg, task_progress.clone()).await {
779                Ok(c) => {
780                    uninstall_cfg.push(c);
781                }
782                Err(e) => {
783                    error = Some(e.error);
784                    if let Some(d) = e.clean_data {
785                        uninstall_cfg.push(d);
786                    } else {
787                        err_no_clean.push((i, id.0.clone(), id.1.clone()));
788                    }
789                }
790            },
791            Err(e) => error = Some(e),
792        };
793
794        if let Some(e) = error {
795            let e = ((i, id.0.clone(), id.1.clone()), e);
796            // notify error
797            status.modify(clmv!(e, |a| {
798                if let SetupStatus::CommitInstall(s) = a.value_mut() {
799                    s.errors.push(e);
800                    s.cancel = false;
801                    s.task_progress = VarEq(const_var(Progress::complete()));
802                }
803            }));
804            errors.push(e);
805
806            // continues trying to commit other tasks, since cannot recover at this
807            // point might as well try to deliver a partial install
808        } else {
809            task_progress.set(Progress::complete());
810        }
811    }
812
813    let mut tasks = config.tasks;
814    tasks.reverse();
815    uninstall_cfg.reverse();
816    let data = UninstallConfig { tasks, cfg: uninstall_cfg };
817
818    if errors.is_empty() {
819        // ensure general status updates to complete
820        status.modify(clmv!(|a| {
821            if let SetupStatus::CommitInstall(s) = a.value_mut() {
822                s.task_progress = VarEq(const_var(Progress::complete()));
823            }
824        }));
825
826        Ok(data)
827    } else {
828        Err(SetupError::task_errors(
829            errors,
830            SetupErrorState::PartialInstall {
831                data,
832                no_data: err_no_clean,
833            },
834        ))
835    }
836}
837
838async fn uninstall(config: UninstallConfig) -> Result<(), SetupError> {
839    let config = validate_uninstall(config).await?;
840
841    let status = SETUP_SV.read().status.clone();
842
843    let tasks_len = config.tasks.len();
844    if tasks_len != config.cfg.len() {
845        return Err(SetupError::corrupted_op_config("UninstallConfig"));
846    }
847
848    let mut errors = vec![];
849
850    for (i, (id, cfg)) in config.tasks.into_iter().zip(config.cfg).enumerate() {
851        let task_progress = var(Progress::indeterminate());
852        // notify new task started
853        let task_progress_s = task_progress.read_only();
854        status.modify(clmv!(id, |a| {
855            match a.value_mut() {
856                SetupStatus::Uninstall(s) => {
857                    s.task = id;
858                    s.progress.0 = i;
859                    s.task_progress = VarEq(task_progress_s);
860                }
861                _ => {
862                    **a = SetupStatus::Uninstall(SetupOpStatus {
863                        cancel: false,
864                        task: id,
865                        progress: (i, tasks_len),
866                        task_progress: VarEq(task_progress_s),
867                        errors: vec![],
868                    })
869                }
870            }
871        }));
872
873        // run task
874        let task_ty = SETUP_SV.read().task_type(&id.0);
875        let error = match task_ty {
876            Ok(task_ty) => (task_ty.uninstall)(cfg, task_progress.clone()).await.err(),
877            Err(e) => Some(e),
878        };
879
880        if let Some(e) = error {
881            let e = ((i, id.0.clone(), id.1.clone()), e);
882            // notify error
883            status.modify(clmv!(e, |a| {
884                if let SetupStatus::Uninstall(s) = a.value_mut() {
885                    s.errors.push(e);
886                    s.cancel = false;
887                    s.task_progress = VarEq(const_var(Progress::complete()));
888                }
889            }));
890            errors.push(e);
891
892            // continues trying to commit other tasks, since cannot recover at this
893            // point might as well try to deliver a partial install
894        } else {
895            task_progress.set(Progress::complete());
896        }
897    }
898    if errors.is_empty() {
899        // ensure general status updates to complete
900        status.modify(clmv!(|a| {
901            if let SetupStatus::Uninstall(s) = a.value_mut() {
902                s.task_progress = VarEq(const_var(Progress::complete()));
903            }
904        }));
905        Ok(())
906    } else {
907        Err(SetupError::task_errors(errors, SetupErrorState::PartialUninstall))
908    }
909}
910
911async fn validate_uninstall(config: UninstallConfig) -> Result<UninstallConfig, SetupError> {
912    let (status, cancel) = {
913        let sv = SETUP_SV.read();
914        (sv.status.clone(), sv.cancel.clone())
915    };
916
917    let tasks_len = config.tasks.len();
918    if tasks_len != config.cfg.len() {
919        return Err(SetupError::corrupted_op_config("UninstallConfig"));
920    }
921
922    let UninstallConfig { tasks, mut cfg } = config;
923    let empty_cfg = RawConfigValue::serialize(()).unwrap();
924
925    let mut errors = vec![];
926
927    for (i, (id, cfg)) in tasks.iter().zip(cfg.iter_mut()).enumerate() {
928        let task_progress = var(Progress::indeterminate());
929        // notify new task started
930        let task_progress_s = task_progress.read_only();
931        status.modify(clmv!(id, |a| {
932            match a.value_mut() {
933                SetupStatus::ValidateUninstall(s) => {
934                    s.task = id;
935                    s.progress.0 = i;
936                    s.task_progress = VarEq(task_progress_s);
937                }
938                _ => {
939                    **a = SetupStatus::ValidateUninstall(SetupOpStatus {
940                        cancel: false,
941                        task: id,
942                        progress: (i, tasks_len),
943                        task_progress: VarEq(task_progress_s),
944                        errors: vec![],
945                    })
946                }
947            }
948        }));
949
950        // run task
951        let task_ty = SETUP_SV.read().task_type(&id.0);
952        let error = match task_ty {
953            Ok(task_ty) => {
954                match (task_ty.validate_uninstall)(std::mem::replace(cfg, empty_cfg.clone()), task_progress.clone(), cancel.clone()).await {
955                    Ok(c) => {
956                        *cfg = c;
957                        None
958                    }
959                    Err(e) => Some(e),
960                }
961            }
962            Err(e) => Some(e),
963        };
964
965        if let Some(e) = error {
966            let e = ((i, id.0.clone(), id.1.clone()), e);
967            // notify error
968            status.modify(clmv!(e, |a| {
969                if let SetupStatus::ValidateUninstall(s) = a.value_mut() {
970                    s.errors.push(e);
971                    s.cancel = false;
972                    s.task_progress = VarEq(const_var(Progress::complete()));
973                }
974            }));
975            errors.push(e);
976
977            // continues trying to commit other tasks, since cannot recover at this
978            // point might as well try to deliver a partial install
979        } else {
980            task_progress.set(Progress::complete());
981        }
982
983        if cancel.get() {
984            break;
985        }
986    }
987
988    let canceled = cancel.get();
989    if errors.is_empty() && !canceled {
990        // ensure general status updates to complete
991        status.modify(clmv!(|a| {
992            if let SetupStatus::ValidateUninstall(s) = a.value_mut() {
993                s.task_progress = VarEq(const_var(Progress::complete()));
994            }
995        }));
996        Ok(UninstallConfig { tasks, cfg })
997    } else {
998        Err(SetupError::task_errors(errors, SetupErrorState::Canceled))
999    }
1000}