Skip to main content

zng_ext_setup/
task.rs

1//! Install and uninstall tasks.
2//!
3//! See [`SetupTask`] docs for a description of the steps an install or uninstall task runs.
4
5mod extract_tar;
6pub use extract_tar::{ExtractTar, ExtractTarConfig};
7
8mod create_shortcut;
9#[cfg(any(windows, target_os = "linux"))]
10pub use create_shortcut::{CreateShortcut, CreateShortcutConfig};
11
12mod register_uninstaller;
13#[cfg(windows)]
14pub use register_uninstaller::{RegisterUninstaller, RegisterUninstallerConfig};
15
16mod copy_current_exe;
17pub use copy_current_exe::{CopyCurrentExe, CopyCurrentExeConfig};
18
19mod register_file_types;
20#[cfg(windows)]
21pub use register_file_types::{RegisterFileTypes, RegisterFileTypesConfig};
22
23use zng_task::Progress;
24use zng_txt::Txt;
25use zng_var::{Var, impl_from_and_into_var};
26
27use std::{any::Any, borrow::Cow, error::Error, fmt, io, ops, path::PathBuf, pin::Pin, sync::Arc};
28
29use zng_ext_config::{ConfigValue, RawConfigValue};
30
31/// Unique name for an install or uninstall task.
32#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
33#[serde(transparent)]
34pub struct TaskTypeId(pub Txt);
35impl ops::Deref for TaskTypeId {
36    type Target = Txt;
37
38    fn deref(&self) -> &Self::Target {
39        &self.0
40    }
41}
42impl_from_and_into_var! {
43    fn from(id: &'static str) -> TaskTypeId {
44        TaskTypeId(id.into())
45    }
46    fn from(id: Txt) -> TaskTypeId {
47        TaskTypeId(id)
48    }
49}
50impl fmt::Display for TaskTypeId {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        write!(f, "{}", self.0)
53    }
54}
55
56/// Represents an install and uninstall task implementation.
57///
58/// Setup tasks runs in steps, steps do not necessarily run on the same process, communication
59/// between steps is done using serialized data. The steps are implemented as associated functions,
60/// not methods, the task type is not instantiated.
61///
62/// Each step runs for all tasks on the setup list, before moving to the next step.
63///
64/// # Install Steps
65///
66/// 1 - If the user did not cancel, [`SetupTask::prepare_install`] is called.
67/// 2.a - If did not cancel, [`SetupTask::install`] is called, the user cannot cancel once this starts.
68/// 2.b - If did cancel, [`SetupTask::cancel_install`] is called.
69///
70/// Note that steps 1 and 2 might not run on the same process. A case where this happens is a self-updater
71/// that starts preparing to install the update while it is still running.
72///
73/// # Uninstall Steps
74///
75/// 1 - [`SetupTask::Install`] data is deserialized from the install log.
76/// 2 - [`SetupTask::validate_uninstall`] is called.
77/// 3 - If did not cancel, [`SetupTask::uninstall`] is called.
78///
79/// # Register
80///
81/// Custom setup task types must be registered with [`SETUP.register_task_type`] otherwise install and uninstall
82/// will fail with [`SetupTaskError::UnknownType`].
83///
84/// # Async
85///
86/// The `async` functions must not block on IO, offload all blocking IO to [`zng_task::wait`].
87/// CPU heavy operations are ok, the tasks run in worker threads.
88///
89/// [`SETUP.register_task_type`]: crate::SETUP::register_task_type
90pub trait SetupTask: Sized {
91    /// Install config type.
92    type InstallConfig: Any + Send;
93    /// Prepared install data type.
94    type PrepareInstall: ConfigValue;
95    /// Installed data type.
96    type Install: ConfigValue;
97
98    /// Unique ID for the task type.
99    fn task_type_id() -> TaskTypeId;
100
101    /// Run all expensive install operations that can run without affecting the system or previous installs.
102    ///
103    /// This step **must not** cause any change that affects existing install, even if reversible, it must only
104    /// run all potentially expensive tasks in such a way that the final *commit* can happen quickly.
105    ///
106    /// The user may cancel the install at any time, if possible monitor the [`cancel`] var and return
107    /// early on cancel. Implement cancellation cleanup on [`cancel_install`].
108    ///
109    /// [`cancel_install`]: Self::cancel_install
110    /// [`cancel`]: PrepareInstallArgs::cancel
111    fn prepare_install(
112        args: PrepareInstallArgs<Self>,
113    ) -> impl Future<Output = Result<Self::PrepareInstall, SetupTaskError>> + Send + 'static;
114
115    /// Commit prepared install changes.
116    ///
117    /// The user cannot cancel installation when this step is running. Progress indicators will only show *indeterminate*
118    /// with the expectation this step will finish quickly.
119    ///
120    /// Install must not fail at the first error encountered, a best attempt to apply all install steps must be made,
121    /// errors can be aggregated on the [`InstallTaskError::error`]. The [`InstallTaskError::clean_data`] must include uninstall instructions
122    /// for all successful steps, best attempt of partial steps and any data from the previous version that was not replaced in case
123    /// it is installing an update.
124    ///
125    /// [`prepare_install`]: Self::prepare_install
126    fn install(args: InstallArgs<Self>) -> impl Future<Output = Result<Self::Install, InstallTaskError<Self::Install>>> + Send + 'static;
127
128    /// Cancel prepared install changes.
129    ///
130    /// This is called if the user requested cancel during or after [`prepare_install`] and before [`install`].
131    ///
132    /// This step must find and cleanup all prepared changes, such as temporary files. The cancel logic must be resilient to
133    /// partial changes as [`prepare_install`] might return early due to user cancel or an error.
134    ///
135    /// [`prepare_install`]: Self::prepare_install
136    /// [`install`]: Self::install
137    fn cancel_install(args: CancelInstallArgs<Self>) -> impl Future<Output = Result<(), SetupTaskError>> + Send + 'static;
138
139    /// Validate the install state for uninstall.
140    ///
141    /// This step **must not** make any changes to the file system, not even creating temp files. This step
142    /// allows tasks to validate the install state before [`uninstall`] makes irreversible changes.
143    ///
144    /// This step is not expected to take long, but if it does check the [`cancel`] flag to avoid unnecessary work.
145    /// If the uninstall is canceled when another task is preparing after this one the returned data is just dropped.
146    ///
147    /// This step returns a validation error or the corrected install data.
148    ///
149    /// [`uninstall`]: Self::uninstall
150    /// [`cancel`]: ValidateUninstallArgs::cancel
151    fn validate_uninstall(
152        args: ValidateUninstallArgs<Self>,
153    ) -> impl Future<Output = Result<Self::Install, SetupTaskError>> + Send + 'static;
154
155    /// Uninstall.
156    ///
157    /// The user cannot cancel uninstallation when this step is running.
158    ///
159    /// Uninstall is idempotent, it must not fail in case a step is already completed, for example, if the task must remove a file
160    /// and it is not found, that is not an error. Task runners can retry partially run uninstall with an install data clone.
161    ///
162    /// Uninstall must not fail at the first error encountered, a best attempt to apply all uninstall steps must be made,
163    /// errors can be aggregated on the [`SetupTaskError`].
164    fn uninstall(args: UninstallArgs<Self>) -> impl Future<Output = Result<(), SetupTaskError>> + Send + 'static;
165}
166
167/// Arguments for [`SetupTask::prepare_install`]
168#[non_exhaustive]
169pub struct PrepareInstallArgs<T: SetupTask> {
170    /// Config for the new installation.
171    pub config: T::InstallConfig,
172
173    /// Data from the previous installation that is being replaced with this one.
174    ///
175    /// This is set if is installing over a previous installation and the same task is present
176    /// on the new installation.
177    pub update: Option<T::Install>,
178
179    /// Progress indicator for the task. Starts as [`Progress::indeterminate`] by default.
180    pub progress: Var<Progress>,
181    /// Read-only var that is `true` if the user cancels the installation.
182    ///
183    /// If possible check this flag often and return immediately on cancel. The *prepare install*
184    /// step is not expected to cleanup on cancel, just return immediately.
185    pub cancel: Var<bool>,
186}
187
188/// Arguments for [`SetupTask::install`].
189#[non_exhaustive]
190pub struct InstallArgs<T: SetupTask> {
191    /// Data generated by [`SetupTask::prepare_install`].
192    pub data: T::PrepareInstall,
193
194    /// Progress indicator for the task cancellation. Starts as [`Progress::indeterminate`] by default.
195    pub progress: Var<Progress>,
196}
197
198/// Arguments for [`SetupTask::cancel_install`].
199#[non_exhaustive]
200pub struct CancelInstallArgs<T: SetupTask> {
201    /// Data generated by [`SetupTask::prepare_install`].
202    ///
203    /// Data may be partial if it was returned because user requested cancel.
204    pub data: T::PrepareInstall,
205
206    /// Progress indicator for the task cancellation. Starts as [`Progress::indeterminate`] by default.
207    pub progress: Var<Progress>,
208}
209
210/// Arguments for [`SetupTask::validate_uninstall`].
211#[non_exhaustive]
212pub struct ValidateUninstallArgs<T: SetupTask> {
213    /// Data generated by [`SetupTask::install`].
214    pub data: T::Install,
215
216    /// Progress indicator for the task uninstall. Starts as [`Progress::indeterminate`] by default.
217    pub progress: Var<Progress>,
218    /// Read-only var that is `true` if the user cancels uninstallation.
219    ///
220    /// If possible check this flag often and return immediately on cancel.
221    pub cancel: Var<bool>,
222}
223
224/// Arguments for [`SetupTask::uninstall`].
225#[non_exhaustive]
226pub struct UninstallArgs<T: SetupTask> {
227    /// Data generated by [`SetupTask::install`].
228    pub data: T::Install,
229    /// Progress indicator for the task uninstall. Continues from [`ValidateUninstallArgs::progress`].
230    pub progress: Var<Progress>,
231}
232
233/// Represents a [`SetupTask`] step error.
234///
235/// Some tasks may continue after an error in a best attempt to at least complete most of the work,
236/// this can cause multiple errors to aggregate. In these cases the [`Error::source`] is the first
237/// error.
238#[derive(Debug, Clone)]
239#[non_exhaustive]
240pub enum SetupTaskError {
241    /// Task data is in an unexpected format.
242    CorruptedTaskData(Arc<dyn Error + Send + Sync>),
243    /// Task type is not [registered].
244    ///
245    /// [registered]: crate::SETUP::register_task_type
246    UnknownType(TaskTypeId),
247    /// IO errors associated with a file or directory path.
248    Io(Vec<(PathBuf, Arc<std::io::Error>)>),
249    /// Other errors.
250    Other(Vec<Arc<dyn Error + Send + Sync>>),
251}
252impl SetupTaskError {
253    /// New `Io` error with a single entry.
254    pub fn io(related_path: PathBuf, error: std::io::Error) -> Self {
255        Self::Io(vec![(related_path, Arc::new(error))])
256    }
257
258    /// New `Other` error with a single entry.
259    pub fn other(error: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
260        // Into<Box.. because this conversion is implemented for types like String
261        Self::Other(vec![error.into().into()])
262    }
263}
264/// Inner errors only compare `Arc` pointer.
265impl PartialEq for SetupTaskError {
266    fn eq(&self, other: &Self) -> bool {
267        match (self, other) {
268            (Self::CorruptedTaskData(a), Self::CorruptedTaskData(b)) => Arc::ptr_eq(a, b),
269            (Self::UnknownType(a), Self::UnknownType(b)) => a == b,
270            (Self::Io(a), Self::Io(b)) => a.len() == b.len() && a.iter().zip(b).all(|(a, b)| Arc::ptr_eq(&a.1, &b.1) && a.0 == b.0),
271            (Self::Other(a), Self::Other(b)) => a.len() == b.len() && a.iter().zip(b).all(|(a, b)| Arc::ptr_eq(a, b)),
272            _ => false,
273        }
274    }
275}
276impl fmt::Display for SetupTaskError {
277    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278        match self {
279            SetupTaskError::CorruptedTaskData(e) => write!(f, "corrupted task data, {e}"),
280            SetupTaskError::UnknownType(t) => write!(f, "unknown task type {t}"),
281            SetupTaskError::Io(e) => {
282                let tab = if e.len() > 1 { "   " } else { "" };
283                let mut sep = "";
284                if e.len() > 1 {
285                    write!(f, "{} io errors:", e.len())?;
286                    sep = "\n";
287                }
288                for (p, e) in e.iter() {
289                    write!(f, "{sep}{tab}{e}\n{tab}   related path: {}", p.display())?;
290                    sep = "\n";
291                }
292                if e.is_empty() { write!(f, "unknown io error") } else { Ok(()) }
293            }
294            SetupTaskError::Other(e) => {
295                let tab = if e.len() > 1 { "   " } else { "" };
296                let mut sep = "";
297                if e.len() > 1 {
298                    write!(f, "{} errors:", e.len())?;
299                    sep = "\n";
300                }
301                for e in e.iter() {
302                    write!(f, "{sep}{tab}{e}")?;
303                }
304                if e.is_empty() { write!(f, "unknown error") } else { Ok(()) }
305            }
306        }
307    }
308}
309impl Error for SetupTaskError {
310    fn source(&self) -> Option<&(dyn Error + 'static)> {
311        match self {
312            SetupTaskError::CorruptedTaskData(e) => Some(&**e),
313            Self::UnknownType(_) => None,
314            Self::Io(e) => Some(&e.first()?.1),
315            SetupTaskError::Other(e) => Some(&**e.first()?),
316        }
317    }
318}
319
320/// Error in a [`SetupTask::install`] task run.
321pub struct InstallTaskError<I> {
322    /// The error.
323    pub error: SetupTaskError,
324    /// Cleanup [`SetupTask::Install`] data.
325    ///
326    /// This must contain data to uninstall the partial committed changes, if there where any. Task runners may
327    /// use this to attempt a [`SetupTask::uninstall`] to cleanup the corrupted install.
328    ///
329    /// In case the install is an [update], this must also contain all data from the previous install that has not
330    /// been invalidated by the failed install.
331    ///
332    /// If this is `None` the task runner will show that the task corrupted the install and the changes made
333    /// cannot even be uninstalled. It will also assume that the [`SetupTask::PrepareInstall`] data was not
334    /// fully cleaned before the error.
335    ///
336    /// If this is `Some` the task runner will assume the [`SetupTask::PrepareInstall`] is fully cleaned. The
337    /// task must attempt to run a *cancel* on the partial prepared data that has not committed yet, if the
338    /// error was encountered before any changes where actually committed and all prepared changes where successfully
339    /// canceled this must be set to `Some` value that represents an *empty install* that the uninstall task will
340    /// recognize and immediately return success for.
341    ///
342    /// [update]: PrepareInstallArgs::update
343    pub clean_data: Option<I>,
344}
345impl<I> fmt::Debug for InstallTaskError<I> {
346    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
347        f.debug_struct("InstallTaskError")
348            .field("error", &self.error)
349            .field("clean_data.is_some()", &self.clean_data.is_some())
350            .finish()
351    }
352}
353impl<I> fmt::Display for InstallTaskError<I> {
354    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
355        fmt::Display::fmt(&self.error, f)
356    }
357}
358impl<I> std::error::Error for InstallTaskError<I> {
359    fn source(&self) -> Option<&(dyn Error + 'static)> {
360        Some(&self.error)
361    }
362}
363
364type BoxFutResult<T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'static>>;
365
366fn value_de<T: ConfigValue>(raw: RawConfigValue) -> Result<T, SetupTaskError> {
367    match raw.deserialize() {
368        Ok(r) => Ok(r),
369        Err(e) => Err(SetupTaskError::CorruptedTaskData(Arc::new(e))),
370    }
371}
372
373#[derive(Clone)]
374pub(crate) struct SetupTaskType {
375    pub task_type_id: fn() -> TaskTypeId,
376    #[allow(clippy::type_complexity)]
377    pub prepare_install:
378        fn(Box<dyn Any + Send>, Option<RawConfigValue>, Var<Progress>, Var<bool>) -> BoxFutResult<RawConfigValue, SetupTaskError>,
379    pub install: fn(RawConfigValue, Var<Progress>) -> BoxFutResult<RawConfigValue, InstallTaskError<RawConfigValue>>,
380    pub cancel_install: fn(RawConfigValue, Var<Progress>) -> BoxFutResult<(), SetupTaskError>,
381    pub validate_uninstall: fn(RawConfigValue, Var<Progress>, Var<bool>) -> BoxFutResult<RawConfigValue, SetupTaskError>,
382    pub uninstall: fn(RawConfigValue, Var<Progress>) -> BoxFutResult<(), SetupTaskError>,
383}
384impl SetupTaskType {
385    /// New task instance.
386    pub fn new<T: SetupTask>() -> Self {
387        Self {
388            task_type_id: T::task_type_id,
389            prepare_install: Self::raw_prepare_install::<T>,
390            install: Self::raw_install::<T>,
391            cancel_install: Self::raw_cancel_install::<T>,
392            validate_uninstall: Self::raw_validate_uninstall::<T>,
393            uninstall: Self::raw_uninstall::<T>,
394        }
395    }
396    fn raw_prepare_install<T: SetupTask>(
397        config: Box<dyn Any + Send>,
398        update: Option<RawConfigValue>,
399        progress: Var<Progress>,
400        cancel: Var<bool>,
401    ) -> BoxFutResult<RawConfigValue, SetupTaskError> {
402        Box::pin(async move {
403            let args = PrepareInstallArgs {
404                config: *config.downcast().unwrap(),
405                update: match update {
406                    Some(d) => value_de(d)?,
407                    None => None,
408                },
409                progress,
410                cancel,
411            };
412            let r = T::prepare_install(args).await?;
413            Ok(RawConfigValue::serialize(r).unwrap())
414        })
415    }
416    fn raw_install<T: SetupTask>(
417        data: RawConfigValue,
418        progress: Var<Progress>,
419    ) -> BoxFutResult<RawConfigValue, InstallTaskError<RawConfigValue>> {
420        Box::pin(async move {
421            let args = InstallArgs {
422                data: match value_de(data) {
423                    Ok(d) => d,
424                    Err(e) => {
425                        return Err(InstallTaskError {
426                            error: e,
427                            // can't cancel either without the data
428                            clean_data: None,
429                        });
430                    }
431                },
432                progress,
433            };
434            match T::install(args).await {
435                Ok(r) => Ok(RawConfigValue::serialize(r).unwrap()),
436                Err(e) => Err(InstallTaskError {
437                    error: e.error,
438                    clean_data: e.clean_data.map(|d| RawConfigValue::serialize(d).unwrap()),
439                }),
440            }
441        })
442    }
443    fn raw_cancel_install<T: SetupTask>(data: RawConfigValue, progress: Var<Progress>) -> BoxFutResult<(), SetupTaskError> {
444        Box::pin(async move {
445            let args = CancelInstallArgs {
446                data: value_de(data)?,
447                progress,
448            };
449            T::cancel_install(args).await
450        })
451    }
452    fn raw_validate_uninstall<T: SetupTask>(
453        data: RawConfigValue,
454        progress: Var<Progress>,
455        cancel: Var<bool>,
456    ) -> BoxFutResult<RawConfigValue, SetupTaskError> {
457        Box::pin(async move {
458            let args = ValidateUninstallArgs {
459                data: value_de(data)?,
460                progress,
461                cancel,
462            };
463            let r = T::validate_uninstall(args).await?;
464            Ok(RawConfigValue::serialize(r).unwrap())
465        })
466    }
467    fn raw_uninstall<T: SetupTask>(data: RawConfigValue, progress: Var<Progress>) -> BoxFutResult<(), SetupTaskError> {
468        Box::pin(async move {
469            let args = UninstallArgs {
470                data: value_de(data)?,
471                progress,
472            };
473            T::uninstall(args).await
474        })
475    }
476}
477
478#[allow(unused)]
479pub(crate) fn path_utf8(p: PathBuf) -> Result<String, SetupTaskError> {
480    match p.to_str() {
481        Some(s) => Ok(if cfg!(windows) {
482            s.replace('/', "\\")
483        } else {
484            s.replace('\\', "/")
485        }),
486        None => Err(SetupTaskError::io(
487            p,
488            io::Error::new(io::ErrorKind::InvalidData, "path must be utf-8"),
489        )),
490    }
491}
492#[allow(unused)]
493pub(crate) fn escape_arg(arg: &str) -> Cow<'_, str> {
494    #[cfg(windows)]
495    {
496        shell_escape::windows::escape(Cow::Borrowed(arg))
497    }
498    #[cfg(not(windows))]
499    {
500        shell_escape::unix::escape(Cow::Borrowed(arg))
501    }
502}