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}