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 Error + Send + Sync + 'static) -> Self {
256        Self::Other(vec![Arc::new(error)])
257    }
258}
259/// Inner errors only compare `Arc` pointer.
260impl PartialEq for SetupTaskError {
261    fn eq(&self, other: &Self) -> bool {
262        match (self, other) {
263            (Self::CorruptedTaskData(a), Self::CorruptedTaskData(b)) => Arc::ptr_eq(a, b),
264            (Self::UnknownType(a), Self::UnknownType(b)) => a == b,
265            (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),
266            (Self::Other(a), Self::Other(b)) => a.len() == b.len() && a.iter().zip(b).all(|(a, b)| Arc::ptr_eq(a, b)),
267            _ => false,
268        }
269    }
270}
271impl fmt::Display for SetupTaskError {
272    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273        match self {
274            SetupTaskError::CorruptedTaskData(e) => write!(f, "corrupted task data, {e}"),
275            SetupTaskError::UnknownType(t) => write!(f, "unknown task type {t}"),
276            SetupTaskError::Io(e) => {
277                let tab = if e.len() > 1 { "   " } else { "" };
278                let mut sep = "";
279                if e.len() > 1 {
280                    write!(f, "{} io errors:", e.len())?;
281                    sep = "\n";
282                }
283                for (p, e) in e.iter() {
284                    write!(f, "{sep}{tab}{e}\n{tab}   related path: {}", p.display())?;
285                    sep = "\n";
286                }
287                if e.is_empty() { write!(f, "unknown io error") } else { Ok(()) }
288            }
289            SetupTaskError::Other(e) => {
290                let tab = if e.len() > 1 { "   " } else { "" };
291                let mut sep = "";
292                if e.len() > 1 {
293                    write!(f, "{} errors:", e.len())?;
294                    sep = "\n";
295                }
296                for e in e.iter() {
297                    write!(f, "{sep}{tab}{e}")?;
298                }
299                if e.is_empty() { write!(f, "unknown error") } else { Ok(()) }
300            }
301        }
302    }
303}
304impl Error for SetupTaskError {
305    fn source(&self) -> Option<&(dyn Error + 'static)> {
306        match self {
307            SetupTaskError::CorruptedTaskData(e) => Some(&**e),
308            Self::UnknownType(_) => None,
309            Self::Io(e) => Some(&e.first()?.1),
310            SetupTaskError::Other(e) => Some(&**e.first()?),
311        }
312    }
313}
314
315/// Error in a [`SetupTask::install`] task run.
316pub struct InstallTaskError<I> {
317    /// The error.
318    pub error: SetupTaskError,
319    /// Cleanup [`SetupTask::Install`] data.
320    ///
321    /// This must contain data to uninstall the partial committed changes, if there where any. Task runners may
322    /// use this to attempt a [`SetupTask::uninstall`] to cleanup the corrupted install.
323    ///
324    /// In case the install is an [update], this must also contain all data from the previous install that has not
325    /// been invalidated by the failed install.
326    ///
327    /// If this is `None` the task runner will show that the task corrupted the install and the changes made
328    /// cannot even be uninstalled. It will also assume that the [`SetupTask::PrepareInstall`] data was not
329    /// fully cleaned before the error.
330    ///
331    /// If this is `Some` the task runner will assume the [`SetupTask::PrepareInstall`] is fully cleaned. The
332    /// task must attempt to run a *cancel* on the partial prepared data that has not committed yet, if the
333    /// error was encountered before any changes where actually committed and all prepared changes where successfully
334    /// canceled this must be set to `Some` value that represents an *empty install* that the uninstall task will
335    /// recognize and immediately return success for.
336    ///
337    /// [update]: PrepareInstallArgs::update
338    pub clean_data: Option<I>,
339}
340impl<I> fmt::Debug for InstallTaskError<I> {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        f.debug_struct("InstallTaskError")
343            .field("error", &self.error)
344            .field("clean_data.is_some()", &self.clean_data.is_some())
345            .finish()
346    }
347}
348impl<I> fmt::Display for InstallTaskError<I> {
349    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
350        fmt::Display::fmt(&self.error, f)
351    }
352}
353impl<I> std::error::Error for InstallTaskError<I> {
354    fn source(&self) -> Option<&(dyn Error + 'static)> {
355        Some(&self.error)
356    }
357}
358
359type BoxFutResult<T, E> = Pin<Box<dyn Future<Output = Result<T, E>> + Send + 'static>>;
360
361fn value_de<T: ConfigValue>(raw: RawConfigValue) -> Result<T, SetupTaskError> {
362    match raw.deserialize() {
363        Ok(r) => Ok(r),
364        Err(e) => Err(SetupTaskError::CorruptedTaskData(Arc::new(e))),
365    }
366}
367
368#[derive(Clone)]
369pub(crate) struct SetupTaskType {
370    pub task_type_id: fn() -> TaskTypeId,
371    #[allow(clippy::type_complexity)]
372    pub prepare_install:
373        fn(Box<dyn Any + Send>, Option<RawConfigValue>, Var<Progress>, Var<bool>) -> BoxFutResult<RawConfigValue, SetupTaskError>,
374    pub install: fn(RawConfigValue, Var<Progress>) -> BoxFutResult<RawConfigValue, InstallTaskError<RawConfigValue>>,
375    pub cancel_install: fn(RawConfigValue, Var<Progress>) -> BoxFutResult<(), SetupTaskError>,
376    pub validate_uninstall: fn(RawConfigValue, Var<Progress>, Var<bool>) -> BoxFutResult<RawConfigValue, SetupTaskError>,
377    pub uninstall: fn(RawConfigValue, Var<Progress>) -> BoxFutResult<(), SetupTaskError>,
378}
379impl SetupTaskType {
380    /// New task instance.
381    pub fn new<T: SetupTask>() -> Self {
382        Self {
383            task_type_id: T::task_type_id,
384            prepare_install: Self::raw_prepare_install::<T>,
385            install: Self::raw_install::<T>,
386            cancel_install: Self::raw_cancel_install::<T>,
387            validate_uninstall: Self::raw_validate_uninstall::<T>,
388            uninstall: Self::raw_uninstall::<T>,
389        }
390    }
391    fn raw_prepare_install<T: SetupTask>(
392        config: Box<dyn Any + Send>,
393        update: Option<RawConfigValue>,
394        progress: Var<Progress>,
395        cancel: Var<bool>,
396    ) -> BoxFutResult<RawConfigValue, SetupTaskError> {
397        Box::pin(async move {
398            let args = PrepareInstallArgs {
399                config: *config.downcast().unwrap(),
400                update: match update {
401                    Some(d) => value_de(d)?,
402                    None => None,
403                },
404                progress,
405                cancel,
406            };
407            let r = T::prepare_install(args).await?;
408            Ok(RawConfigValue::serialize(r).unwrap())
409        })
410    }
411    fn raw_install<T: SetupTask>(
412        data: RawConfigValue,
413        progress: Var<Progress>,
414    ) -> BoxFutResult<RawConfigValue, InstallTaskError<RawConfigValue>> {
415        Box::pin(async move {
416            let args = InstallArgs {
417                data: match value_de(data) {
418                    Ok(d) => d,
419                    Err(e) => {
420                        return Err(InstallTaskError {
421                            error: e,
422                            // can't cancel either without the data
423                            clean_data: None,
424                        });
425                    }
426                },
427                progress,
428            };
429            match T::install(args).await {
430                Ok(r) => Ok(RawConfigValue::serialize(r).unwrap()),
431                Err(e) => Err(InstallTaskError {
432                    error: e.error,
433                    clean_data: e.clean_data.map(|d| RawConfigValue::serialize(d).unwrap()),
434                }),
435            }
436        })
437    }
438    fn raw_cancel_install<T: SetupTask>(data: RawConfigValue, progress: Var<Progress>) -> BoxFutResult<(), SetupTaskError> {
439        Box::pin(async move {
440            let args = CancelInstallArgs {
441                data: value_de(data)?,
442                progress,
443            };
444            T::cancel_install(args).await
445        })
446    }
447    fn raw_validate_uninstall<T: SetupTask>(
448        data: RawConfigValue,
449        progress: Var<Progress>,
450        cancel: Var<bool>,
451    ) -> BoxFutResult<RawConfigValue, SetupTaskError> {
452        Box::pin(async move {
453            let args = ValidateUninstallArgs {
454                data: value_de(data)?,
455                progress,
456                cancel,
457            };
458            let r = T::validate_uninstall(args).await?;
459            Ok(RawConfigValue::serialize(r).unwrap())
460        })
461    }
462    fn raw_uninstall<T: SetupTask>(data: RawConfigValue, progress: Var<Progress>) -> BoxFutResult<(), SetupTaskError> {
463        Box::pin(async move {
464            let args = UninstallArgs {
465                data: value_de(data)?,
466                progress,
467            };
468            T::uninstall(args).await
469        })
470    }
471}
472
473#[allow(unused)]
474pub(crate) fn path_utf8(p: PathBuf) -> Result<String, SetupTaskError> {
475    match p.to_str() {
476        Some(s) => Ok(if cfg!(windows) {
477            s.replace('/', "\\")
478        } else {
479            s.replace('\\', "/")
480        }),
481        None => Err(SetupTaskError::io(
482            p,
483            io::Error::new(io::ErrorKind::InvalidData, "path must be utf-8"),
484        )),
485    }
486}
487#[allow(unused)]
488pub(crate) fn escape_arg(arg: &str) -> Cow<'_, str> {
489    #[cfg(windows)]
490    {
491        shell_escape::windows::escape(Cow::Borrowed(arg))
492    }
493    #[cfg(not(windows))]
494    {
495        shell_escape::unix::escape(Cow::Borrowed(arg))
496    }
497}