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