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}