Skip to main content

corundum_framework_package/
updater.rs

1//! Updates: found and downloaded by a thread of the updater's own, applied only when the
2//! app asks, after its `before_apply`.
3
4use std::ffi::OsString;
5use std::sync::atomic::{AtomicBool, Ordering};
6use std::sync::mpsc::{self, Receiver, RecvTimeoutError, Sender};
7use std::sync::{Arc, Mutex, MutexGuard, Weak};
8use std::time::{Duration, Instant};
9
10use corundum_framework_platform::package;
11use corundum_framework_update::{PackageState, PackageStatus};
12use semver::Version;
13use velopack::UpdateInfo;
14
15use crate::backend::{Applied, Backend, Velopack};
16use crate::error::Error;
17use crate::services::{self, ServiceDriver};
18use crate::start::{self, PackageSpec};
19
20/// Where a package's updates are published.
21#[derive(Clone, Debug)]
22pub enum Source {
23    /// A GitHub repository's releases, by its URL: `https://github.com/owner/name`.
24    /// Velopack reads only the 10 newest releases.
25    Github { repo: String, prerelease: bool },
26    /// A feed of the app's own, by its base URL.
27    Http(String),
28}
29
30/// What happens once an update is applied, and who ends the process for it.
31#[derive(Clone, Debug)]
32pub enum After {
33    /// Start the app again without arguments: the GUI. The caller ends the process.
34    RestartApp,
35    /// Start the app again with these arguments. The caller ends the process.
36    RestartWith(Vec<OsString>),
37    /// Exit; something else starts it again (a service manager), or nothing. The process
38    /// exits in [`Updater::apply`]: a daemon or a command line has nothing to close.
39    Exit,
40}
41
42/// A package's updates: one thread checks and downloads, the state is shared, and nothing
43/// is applied until [`Updater::apply`]. Clones share the thread and the state; the thread
44/// ends with the last clone.
45#[derive(Clone)]
46pub struct Updater {
47    shared: Arc<Shared>,
48}
49
50type Listener = Arc<dyn Fn() + Send + Sync>;
51
52struct Shared {
53    spec: &'static PackageSpec,
54    backend: Arc<dyn Backend>,
55    /// The app's services, stopped before an apply and started again when it fails.
56    services: Arc<dyn ServiceDriver>,
57    current: Version,
58    state: Mutex<State>,
59    listeners: Mutex<Vec<Listener>>,
60    /// The worker's requests, `None` until the first check. Dropping it with the last
61    /// clone ends the worker.
62    worker: Mutex<Option<Sender<Request>>>,
63    /// Whether an apply runs: a status-bar badge and the update panel may both ask at
64    /// once. It stays set once an update is handed to Velopack, as the process is ending.
65    applying: AtomicBool,
66}
67
68/// The backend of [`Updater::for_tests`]: nothing to find, nothing to apply.
69#[cfg(feature = "test-support")]
70struct Inert;
71
72#[cfg(feature = "test-support")]
73impl Backend for Inert {
74    fn current(&self) -> Version {
75        Version::new(1, 0, 0)
76    }
77
78    fn check(&self) -> Result<Option<UpdateInfo>, Error> {
79        Ok(None)
80    }
81
82    fn download(&self, _info: &UpdateInfo, _progress: &dyn Fn(u8)) -> Result<(), Error> {
83        Ok(())
84    }
85
86    fn pending(&self) -> Option<Version> {
87        None
88    }
89
90    fn apply(&self, _applied: Applied, _after: &After) -> Result<(), Error> {
91        Ok(())
92    }
93}
94
95struct State {
96    state: PackageState,
97    /// What `Ready` stands for.
98    downloaded: Option<UpdateInfo>,
99}
100
101enum Request {
102    Check,
103    Every(Duration),
104}
105
106impl Updater {
107    /// `Error::NotPackaged` outside a package.
108    pub fn new(spec: &'static PackageSpec, source: Source) -> Result<Updater, Error> {
109        // Told from the files first, as `start` does, so a development build never asks
110        // Velopack to find a manifest that is not there.
111        if package::current().is_none() {
112            return Err(Error::NotPackaged);
113        }
114        Ok(Updater::with_backend(
115            spec,
116            Arc::new(Velopack::new(&source)?),
117            services::driver(spec),
118        ))
119    }
120
121    pub(crate) fn with_backend(
122        spec: &'static PackageSpec,
123        backend: Arc<dyn Backend>,
124        services: Arc<dyn ServiceDriver>,
125    ) -> Updater {
126        Updater {
127            shared: Arc::new(Shared {
128                spec,
129                services,
130                current: backend.current(),
131                backend,
132                state: Mutex::new(State {
133                    state: PackageState::Idle,
134                    downloaded: None,
135                }),
136                listeners: Mutex::new(Vec::new()),
137                worker: Mutex::new(None),
138                applying: AtomicBool::new(false),
139            }),
140        }
141    }
142
143    /// An updater standing in `state`, over a backend that checks nothing and never applies:
144    /// [`apply`](Self::apply) returns `Ok` without doing anything when `state` is `Ready`,
145    /// and an error (nothing downloaded) otherwise. For a GUI's tests.
146    #[cfg(feature = "test-support")]
147    #[doc(hidden)]
148    pub fn for_tests(state: PackageState) -> Updater {
149        static SPEC: PackageSpec = PackageSpec {
150            app_name: "test",
151            on_installed: None,
152            on_uninstalling: None,
153            before_apply: None,
154            cli: None,
155            services: crate::Services::NONE,
156        };
157        let updater = Updater::with_backend(&SPEC, Arc::new(Inert), Arc::new(()));
158        let downloaded = match &state {
159            PackageState::Ready { version } => Some(UpdateInfo {
160                TargetFullRelease: velopack::VelopackAsset {
161                    Version: version.to_string(),
162                    ..Default::default()
163                },
164                ..Default::default()
165            }),
166            _ => None,
167        };
168        *lock(&updater.shared.state) = State { state, downloaded };
169        updater
170    }
171
172    /// Called after every change of state: a GUI asks for a repaint here. Runs on the
173    /// updater's thread, or on the thread calling `apply` when `before_apply` refuses.
174    pub fn on_change(&self, f: impl Fn() + Send + Sync + 'static) {
175        lock(&self.shared.listeners).push(Arc::new(f));
176    }
177
178    /// The running version and what the updater is doing. Never waits for a check.
179    pub fn status(&self) -> PackageStatus {
180        PackageStatus {
181            current: Some(self.shared.current.clone()),
182            state: lock(&self.shared.state).state.clone(),
183        }
184    }
185
186    /// Look now (the Check button, `<app> update`), and download what is found.
187    pub fn check_now(&self) {
188        self.send(Request::Check);
189    }
190
191    /// Look at once and then every `every`, downloading what is found. A second call
192    /// replaces the interval.
193    pub fn run_in_background(&self, every: Duration) {
194        self.send(Request::Every(every));
195    }
196
197    /// Runs `before_apply`, then hands the downloaded update to Velopack's updater, which
198    /// waits for this process to end before it replaces the files.
199    ///
200    /// Who ends the process: for [`After::Exit`] this call exits it and returns only on an
201    /// error. For [`After::RestartApp`] and [`After::RestartWith`] it returns `Ok`, and the
202    /// caller must end the process soon (Velopack waits 60 seconds), a GUI by closing its
203    /// window so that what it saves on exit is saved.
204    ///
205    /// Once this returns `Ok`, the update is Velopack's: if the app cancels the close (a
206    /// "save your changes?" it answers with Cancel) or otherwise keeps running, a later
207    /// call in this process fails with [`Error::Applying`]. Whether the update is applied
208    /// then is up to Velopack's updater, which waits for this process to end.
209    ///
210    /// An error: nothing downloaded, an apply already running on another thread
211    /// ([`Error::Applying`]), `before_apply` refused (nothing is applied, and a later call
212    /// tries again), or Velopack failed.
213    pub fn apply(&self, after: After) -> Result<(), Error> {
214        let applying = self.shared.begin_apply()?;
215        // Whatever the state shows: after a refused `before_apply` it is `Failed`, and the
216        // update is still there to apply.
217        let info = lock(&self.shared.state)
218            .downloaded
219            .clone()
220            .ok_or_else(|| Error::Velopack("nothing downloaded".into()))?;
221        self.shared.prepare()?;
222        self.shared.apply(Applied::Downloaded(info), &after)?;
223        applying.keep();
224        Ok(())
225    }
226
227    /// For a GUI's start only, before its window opens: apply an update downloaded earlier
228    /// and never applied, and start the app again; this call then exits the process itself,
229    /// as there is no window to close. Looks at the downloaded packages only, never the
230    /// network, so the start is not delayed. `Ok` when there is none, and on a start by
231    /// Velopack's updater: an update it failed to apply would otherwise be tried at every
232    /// start.
233    ///
234    /// Only the app's single GUI instance may call this: an app that can run two must
235    /// prevent the second first (corundum-framework-platform's `instance`, or the gui
236    /// crate's `WindowOptions::single_instance`), or the second applies under the first.
237    pub fn apply_pending_at_start(&self) -> Result<(), Error> {
238        if self.apply_pending(start::restarted())? {
239            std::process::exit(0);
240        }
241        Ok(())
242    }
243
244    /// [`apply_pending_at_start`](Self::apply_pending_at_start) but the exit: whether an
245    /// update was handed to Velopack.
246    fn apply_pending(&self, restarted: bool) -> Result<bool, Error> {
247        if restarted {
248            return Ok(false);
249        }
250        match self.shared.backend.pending() {
251            Some(pending) if pending > self.shared.current => {
252                let applying = self.shared.begin_apply()?;
253                self.shared.prepare()?;
254                self.shared.apply(Applied::Pending, &After::RestartApp)?;
255                applying.keep();
256                Ok(true)
257            }
258            _ => Ok(false),
259        }
260    }
261
262    /// Hands `request` to the worker, started on the first request.
263    fn send(&self, request: Request) {
264        let mut worker = lock(&self.shared.worker);
265        let sender = worker.get_or_insert_with(|| {
266            let (tx, rx) = mpsc::channel();
267            let shared = Arc::downgrade(&self.shared);
268            let backend = Arc::clone(&self.shared.backend);
269            std::thread::Builder::new()
270                .name("package-updater".into())
271                .spawn(move || work(shared, backend, rx))
272                .expect("could not start the updater's thread");
273            tx
274        });
275        // The worker ends only once the sender is dropped, so it is listening.
276        let _ = sender.send(request);
277    }
278}
279
280/// An apply under way: dropped, it lets the next one run, unless [`keep`](Self::keep)
281/// says the update was handed to Velopack.
282struct ApplyGuard<'a>(Option<&'a AtomicBool>);
283
284impl ApplyGuard<'_> {
285    fn keep(mut self) {
286        self.0 = None;
287    }
288}
289
290impl Drop for ApplyGuard<'_> {
291    fn drop(&mut self) {
292        if let Some(applying) = self.0 {
293            applying.store(false, Ordering::SeqCst);
294        }
295    }
296}
297
298impl Shared {
299    /// Marks an apply as running, or [`Error::Applying`] when one already is.
300    fn begin_apply(&self) -> Result<ApplyGuard<'_>, Error> {
301        self.applying
302            .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
303            .map_err(|_| Error::Applying)?;
304        Ok(ApplyGuard(Some(&self.applying)))
305    }
306
307    /// Runs the app's `before_apply`, then stops its running services; an error from
308    /// either is the state and stops the update.
309    fn prepare(&self) -> Result<(), Error> {
310        let before_apply = self.spec.before_apply.unwrap_or(|| Ok(()));
311        before_apply()
312            .and_then(|()| self.services.stop_running())
313            .map_err(|e| {
314                self.set(PackageState::Failed(e.clone()));
315                Error::Prepare(e)
316            })
317    }
318
319    /// Hands the update to the backend; when that fails, the services [`prepare`]
320    /// stopped start again, as no restart will.
321    ///
322    /// [`prepare`]: Self::prepare
323    fn apply(&self, applied: Applied, after: &After) -> Result<(), Error> {
324        self.backend.apply(applied, after).inspect_err(|_| {
325            self.services.restart_stopped();
326        })
327    }
328
329    /// Sets the state and tells the listeners, if it changed.
330    fn set(&self, new: PackageState) {
331        {
332            let mut state = lock(&self.state);
333            if state.state == new {
334                return;
335            }
336            state.state = new;
337        }
338        self.notify();
339    }
340
341    fn notify(&self) {
342        // Called without the lock, so a listener may register another.
343        let listeners = lock(&self.listeners).clone();
344        for listener in listeners {
345            listener();
346        }
347    }
348
349    /// One check, and the download of what it finds. Once an update is downloaded, later
350    /// checks run quietly: the state stays `Ready` (what the badge and the button show)
351    /// unless one finds a newer update and has downloaded it, and their errors are only
352    /// logged. A refused `before_apply` left `Failed`: a check (a GUI's Retry) is `Ready`
353    /// again for the update still downloaded.
354    fn check(&self, backend: &dyn Backend) {
355        let ready = {
356            let state = lock(&self.state);
357            match (&state.state, &state.downloaded) {
358                (PackageState::Failed(_), Some(info)) => {
359                    Version::parse(&info.TargetFullRelease.Version).ok()
360                }
361                _ => None,
362            }
363        };
364        if let Some(version) = ready {
365            self.set(PackageState::Ready { version });
366        }
367        let quiet = lock(&self.state).downloaded.is_some();
368        let fail = |msg: String| {
369            if quiet {
370                tracing::warn!("update check failed, keeping the downloaded update: {msg}");
371            } else {
372                self.set(PackageState::Failed(msg));
373            }
374        };
375        if !quiet {
376            self.set(PackageState::Checking);
377        }
378        let info = match backend.check() {
379            Ok(Some(info)) => info,
380            Ok(None) => {
381                if !quiet {
382                    self.set(PackageState::UpToDate);
383                }
384                return;
385            }
386            Err(e) => return fail(e.to_string()),
387        };
388        let version = match Version::parse(&info.TargetFullRelease.Version) {
389            Ok(version) => version,
390            Err(e) => {
391                return fail(format!(
392                    "the update's version {:?} is not a version: {e}",
393                    info.TargetFullRelease.Version
394                ));
395            }
396        };
397        if !quiet {
398            self.set(PackageState::Downloading { percent: 0 });
399        }
400        let progress = |percent: u8| {
401            if !quiet {
402                self.set(PackageState::Downloading { percent });
403            }
404        };
405        match backend.download(&info, &progress) {
406            Ok(()) => {
407                {
408                    let mut state = lock(&self.state);
409                    state.state = PackageState::Ready { version };
410                    state.downloaded = Some(info);
411                }
412                self.notify();
413            }
414            Err(e) => fail(e.to_string()),
415        }
416    }
417}
418
419/// The worker: a check per request, and one every interval once asked to. It holds the
420/// state only while it works, so it ends when the last [`Updater`] is dropped.
421fn work(shared: Weak<Shared>, backend: Arc<dyn Backend>, requests: Receiver<Request>) {
422    let mut every: Option<Duration> = None;
423    let mut next: Option<Instant> = None;
424    loop {
425        let request = match next {
426            Some(at) => match requests.recv_timeout(at.saturating_duration_since(Instant::now())) {
427                Ok(request) => request,
428                Err(RecvTimeoutError::Timeout) => Request::Check,
429                Err(RecvTimeoutError::Disconnected) => return,
430            },
431            None => match requests.recv() {
432                Ok(request) => request,
433                Err(_) => return,
434            },
435        };
436        if let Request::Every(interval) = request {
437            every = Some(interval);
438        }
439        let Some(shared) = shared.upgrade() else {
440            return;
441        };
442        shared.check(&*backend);
443        drop(shared);
444        next = every.map(|every| Instant::now() + every);
445    }
446}
447
448/// Locks `mutex`, through a panic elsewhere: the state is replaced whole, never left
449/// half-written.
450fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
451    mutex.lock().unwrap_or_else(|e| e.into_inner())
452}
453
454#[cfg(test)]
455mod tests {
456    use std::cell::RefCell;
457    use std::collections::VecDeque;
458    use std::sync::atomic::{AtomicUsize, Ordering};
459    use std::sync::{Arc, Mutex};
460    use std::time::{Duration, Instant};
461
462    use corundum_framework_update::{PackageState, PackageStatus};
463    use semver::Version;
464    use velopack::{UpdateInfo, VelopackAsset};
465
466    use super::*;
467    use crate::backend::{Applied, Backend};
468    use crate::error::Error;
469    use crate::services::ServiceDriver;
470    use crate::start::PackageSpec;
471
472    thread_local! {
473        /// What ran on the thread that applies, in order: `before_apply` and the fake's
474        /// `apply` both run on the caller's thread, so each test sees only its own.
475        static APPLY_LOG: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
476    }
477
478    fn log(entry: impl Into<String>) {
479        APPLY_LOG.with(|l| l.borrow_mut().push(entry.into()));
480    }
481
482    fn apply_log() -> Vec<String> {
483        APPLY_LOG.with(|l| l.borrow().clone())
484    }
485
486    fn v(s: &str) -> Version {
487        Version::parse(s).unwrap()
488    }
489
490    fn info(version: &str) -> UpdateInfo {
491        UpdateInfo {
492            TargetFullRelease: VelopackAsset {
493                Version: version.to_owned(),
494                FileName: format!("app-{version}-full.nupkg"),
495                ..Default::default()
496            },
497            ..Default::default()
498        }
499    }
500
501    fn spec(before_apply: Option<fn() -> Result<(), String>>) -> &'static PackageSpec {
502        Box::leak(Box::new(PackageSpec {
503            app_name: "updater-test",
504            on_installed: None,
505            on_uninstalling: None,
506            before_apply,
507            cli: None,
508            services: crate::Services::NONE,
509        }))
510    }
511
512    fn stopping() -> Result<(), String> {
513        log("before_apply");
514        Ok(())
515    }
516
517    fn failing_to_stop() -> Result<(), String> {
518        log("before_apply");
519        Err("stop failed".to_owned())
520    }
521
522    /// A Velopack that answers checks from a list, downloads in three steps and records an
523    /// apply instead of handing it over.
524    struct Fake {
525        current: Version,
526        checks: Mutex<VecDeque<Result<Option<UpdateInfo>, Error>>>,
527        checked: AtomicUsize,
528        pending: Option<Version>,
529        /// Whether `apply` fails, after recording itself.
530        fails_apply: std::sync::atomic::AtomicBool,
531    }
532
533    impl Fake {
534        fn new(
535            current: &str,
536            checks: Vec<Result<Option<UpdateInfo>, Error>>,
537            pending: Option<&str>,
538        ) -> Arc<Fake> {
539            Arc::new(Fake {
540                current: v(current),
541                checks: Mutex::new(checks.into()),
542                checked: AtomicUsize::new(0),
543                pending: pending.map(v),
544                fails_apply: std::sync::atomic::AtomicBool::new(false),
545            })
546        }
547    }
548
549    impl Backend for Fake {
550        fn current(&self) -> Version {
551            self.current.clone()
552        }
553
554        fn check(&self) -> Result<Option<UpdateInfo>, Error> {
555            self.checked.fetch_add(1, Ordering::SeqCst);
556            self.checks.lock().unwrap().pop_front().unwrap_or(Ok(None))
557        }
558
559        fn download(&self, _info: &UpdateInfo, progress: &dyn Fn(u8)) -> Result<(), Error> {
560            for percent in [0, 50, 100] {
561                progress(percent);
562            }
563            Ok(())
564        }
565
566        fn pending(&self) -> Option<Version> {
567            self.pending.clone()
568        }
569
570        fn apply(&self, applied: Applied, after: &After) -> Result<(), Error> {
571            let what = match applied {
572                Applied::Downloaded(info) => info.TargetFullRelease.Version,
573                Applied::Pending => "pending".to_owned(),
574            };
575            log(format!("apply {what} {after:?}"));
576            if self.fails_apply.load(Ordering::SeqCst) {
577                return Err(Error::Velopack("apply failed".into()));
578            }
579            Ok(())
580        }
581    }
582
583    /// Services that record their stop and restart in the apply log, and fail the stop
584    /// when asked to.
585    struct RecordedServices {
586        fails_stop: bool,
587    }
588
589    impl ServiceDriver for RecordedServices {
590        fn stop_running(&self) -> Result<(), String> {
591            log("stop services");
592            if self.fails_stop {
593                return Err("demo could not be stopped".to_owned());
594            }
595            Ok(())
596        }
597
598        fn restart_stopped(&self) {
599            log("restart services");
600        }
601    }
602
603    fn updater(spec: &'static PackageSpec, fake: Arc<Fake>) -> Updater {
604        Updater::with_backend(spec, fake, Arc::new(()))
605    }
606
607    fn updater_with_services(
608        spec: &'static PackageSpec,
609        fake: Arc<Fake>,
610        fails_stop: bool,
611    ) -> Updater {
612        Updater::with_backend(spec, fake, Arc::new(RecordedServices { fails_stop }))
613    }
614
615    /// Polls the state until `done` holds, for 5 seconds at most.
616    fn wait_for(updater: &Updater, done: impl Fn(&PackageState) -> bool) -> PackageStatus {
617        let deadline = Instant::now() + Duration::from_secs(5);
618        loop {
619            let status = updater.status();
620            if done(&status.state) {
621                return status;
622            }
623            assert!(
624                Instant::now() < deadline,
625                "still {:?} after 5 seconds",
626                status.state
627            );
628            std::thread::sleep(Duration::from_millis(5));
629        }
630    }
631
632    fn settled(state: &PackageState) -> bool {
633        matches!(
634            state,
635            PackageState::UpToDate | PackageState::Ready { .. } | PackageState::Failed(_)
636        )
637    }
638
639    #[test]
640    fn a_found_update_is_downloaded_and_ready() {
641        let updater = updater(
642            spec(None),
643            Fake::new("1.0.0", vec![Ok(Some(info("1.1.0")))], None),
644        );
645        assert_eq!(
646            updater.status(),
647            PackageStatus {
648                current: Some(v("1.0.0")),
649                state: PackageState::Idle
650            }
651        );
652        updater.check_now();
653        let status = wait_for(&updater, settled);
654        assert_eq!(
655            status.state,
656            PackageState::Ready {
657                version: v("1.1.0")
658            }
659        );
660        assert_eq!(status.current, Some(v("1.0.0")));
661    }
662
663    #[test]
664    fn nothing_found_is_up_to_date() {
665        let updater = updater(spec(None), Fake::new("1.0.0", vec![Ok(None)], None));
666        updater.check_now();
667        assert_eq!(wait_for(&updater, settled).state, PackageState::UpToDate);
668    }
669
670    #[test]
671    fn a_check_error_is_failed_and_a_later_check_recovers() {
672        let updater = updater(
673            spec(None),
674            Fake::new(
675                "1.0.0",
676                vec![Err(Error::Network("offline".into())), Ok(None)],
677                None,
678            ),
679        );
680        updater.check_now();
681        let failed = wait_for(&updater, settled).state;
682        assert!(
683            matches!(&failed, PackageState::Failed(msg) if msg.contains("offline")),
684            "{failed:?}"
685        );
686        updater.check_now();
687        assert_eq!(
688            wait_for(&updater, |s| *s == PackageState::UpToDate).state,
689            PackageState::UpToDate
690        );
691    }
692
693    #[test]
694    fn apply_runs_before_apply_first() {
695        let updater = updater(
696            spec(Some(stopping)),
697            Fake::new("1.0.0", vec![Ok(Some(info("1.1.0")))], None),
698        );
699        updater.check_now();
700        wait_for(&updater, settled);
701        updater.apply(After::RestartApp).unwrap();
702        assert_eq!(apply_log(), ["before_apply", "apply 1.1.0 RestartApp"]);
703    }
704
705    #[test]
706    fn apply_stops_services_after_before_apply() {
707        let updater = updater_with_services(
708            spec(Some(stopping)),
709            Fake::new("1.0.0", vec![Ok(Some(info("1.1.0")))], None),
710            false,
711        );
712        updater.check_now();
713        wait_for(&updater, settled);
714        updater.apply(After::RestartApp).unwrap();
715        assert_eq!(
716            apply_log(),
717            ["before_apply", "stop services", "apply 1.1.0 RestartApp"]
718        );
719    }
720
721    #[test]
722    fn a_failed_apply_starts_the_stopped_services_again() {
723        let fake = Fake::new("1.0.0", vec![Ok(Some(info("1.1.0")))], Some("1.1.0"));
724        fake.fails_apply.store(true, Ordering::SeqCst);
725        let updater = updater_with_services(spec(Some(stopping)), fake, false);
726        updater.check_now();
727        wait_for(&updater, settled);
728        let err = updater.apply(After::RestartApp).unwrap_err();
729        assert!(matches!(err, Error::Velopack(_)), "{err:?}");
730        let err = updater.apply_pending(false).unwrap_err();
731        assert!(matches!(err, Error::Velopack(_)), "{err:?}");
732        let once = [
733            "before_apply",
734            "stop services",
735            "apply 1.1.0 RestartApp",
736            "restart services",
737        ];
738        let pending = [
739            "before_apply",
740            "stop services",
741            "apply pending RestartApp",
742            "restart services",
743        ];
744        assert_eq!(apply_log(), [once, pending].concat());
745    }
746
747    #[test]
748    fn a_failed_stop_applies_nothing() {
749        let updater = updater_with_services(
750            spec(Some(stopping)),
751            Fake::new("1.0.0", vec![Ok(Some(info("1.1.0")))], None),
752            true,
753        );
754        updater.check_now();
755        wait_for(&updater, settled);
756        let err = updater.apply(After::Exit).unwrap_err();
757        assert!(
758            matches!(err, Error::Prepare(ref m) if m == "demo could not be stopped"),
759            "{err:?}"
760        );
761        assert_eq!(apply_log(), ["before_apply", "stop services"]);
762        assert_eq!(
763            updater.status().state,
764            PackageState::Failed("demo could not be stopped".to_owned())
765        );
766    }
767
768    #[test]
769    fn a_second_apply_while_one_runs_is_refused() {
770        static UPDATER: std::sync::OnceLock<Updater> = std::sync::OnceLock::new();
771        /// Asks for an apply again while the first is still in `before_apply`: the badge
772        /// and the panel, clicked together.
773        fn applying_again() -> Result<(), String> {
774            let again = UPDATER.get().unwrap().apply(After::RestartApp);
775            log(format!("again {again:?}"));
776            Ok(())
777        }
778        let updater = updater(
779            spec(Some(applying_again)),
780            Fake::new("1.0.0", vec![Ok(Some(info("1.1.0")))], None),
781        );
782        UPDATER.set(updater.clone()).ok().unwrap();
783        updater.check_now();
784        wait_for(&updater, settled);
785        updater.apply(After::RestartApp).unwrap();
786        assert_eq!(
787            apply_log(),
788            ["again Err(Applying)", "apply 1.1.0 RestartApp"]
789        );
790        // Handed to Velopack: the process is ending, and nothing applies again.
791        assert!(matches!(
792            updater.apply(After::RestartApp),
793            Err(Error::Applying)
794        ));
795    }
796
797    #[test]
798    fn a_failing_before_apply_applies_nothing() {
799        let updater = updater(
800            spec(Some(failing_to_stop)),
801            Fake::new("1.0.0", vec![Ok(Some(info("1.1.0")))], None),
802        );
803        updater.check_now();
804        wait_for(&updater, settled);
805        let err = updater.apply(After::Exit).unwrap_err();
806        assert!(
807            matches!(err, Error::Prepare(ref m) if m == "stop failed"),
808            "{err:?}"
809        );
810        assert_eq!(apply_log(), ["before_apply"]);
811        assert_eq!(
812            updater.status().state,
813            PackageState::Failed("stop failed".to_owned())
814        );
815    }
816
817    #[test]
818    fn apply_without_a_download_is_an_error() {
819        let updater = updater(spec(Some(stopping)), Fake::new("1.0.0", vec![], None));
820        let err = updater.apply(After::RestartApp).unwrap_err();
821        assert!(
822            matches!(err, Error::Velopack(ref m) if m == "nothing downloaded"),
823            "{err:?}"
824        );
825        assert!(apply_log().is_empty());
826    }
827
828    #[test]
829    fn a_pending_update_newer_than_current_is_applied_at_start() {
830        let updater = updater(
831            spec(Some(stopping)),
832            Fake::new("1.0.0", vec![], Some("1.1.0")),
833        );
834        assert!(updater.apply_pending(false).unwrap());
835        assert_eq!(apply_log(), ["before_apply", "apply pending RestartApp"]);
836    }
837
838    #[test]
839    fn a_pending_package_not_newer_is_left_alone() {
840        for pending in [Some("1.0.0"), Some("0.9.0"), None] {
841            let updater = updater(spec(Some(stopping)), Fake::new("1.0.0", vec![], pending));
842            assert!(!updater.apply_pending(false).unwrap());
843        }
844        assert!(apply_log().is_empty());
845    }
846
847    #[test]
848    fn a_start_by_velopack_s_updater_applies_no_pending_package() {
849        let updater = updater(
850            spec(Some(stopping)),
851            Fake::new("1.0.0", vec![], Some("1.1.0")),
852        );
853        assert!(!updater.apply_pending(true).unwrap());
854        assert!(apply_log().is_empty());
855    }
856
857    #[test]
858    fn on_change_is_called_on_every_state_change() {
859        let updater = updater(
860            spec(None),
861            Fake::new("1.0.0", vec![Ok(Some(info("1.1.0")))], None),
862        );
863        let first = Arc::new(AtomicUsize::new(0));
864        let second = Arc::new(AtomicUsize::new(0));
865        for counter in [&first, &second] {
866            let counter = Arc::clone(counter);
867            updater.on_change(move || {
868                counter.fetch_add(1, Ordering::SeqCst);
869            });
870        }
871        updater.check_now();
872        wait_for(&updater, settled);
873        // Checking, Downloading at 0, 50 and 100, Ready. The callbacks run after the state
874        // is set, so wait for the last one too.
875        let deadline = Instant::now() + Duration::from_secs(5);
876        while second.load(Ordering::SeqCst) < 5 && Instant::now() < deadline {
877            std::thread::sleep(Duration::from_millis(5));
878        }
879        assert_eq!(first.load(Ordering::SeqCst), 5);
880        assert_eq!(second.load(Ordering::SeqCst), 5);
881    }
882
883    #[test]
884    fn a_failing_check_after_a_download_keeps_it_ready() {
885        let fake = Fake::new(
886            "1.0.0",
887            vec![
888                Ok(Some(info("1.1.0"))),
889                Err(Error::Network("offline".into())),
890            ],
891            None,
892        );
893        let updater = updater(spec(Some(stopping)), Arc::clone(&fake));
894        let changes = Arc::new(AtomicUsize::new(0));
895        let counter = Arc::clone(&changes);
896        updater.on_change(move || {
897            counter.fetch_add(1, Ordering::SeqCst);
898        });
899        updater.check_now();
900        wait_for(&updater, settled);
901        // A check that fails, then one that finds nothing: once the fake sees the third
902        // check, the worker is done with the failing one.
903        updater.check_now();
904        updater.check_now();
905        let deadline = Instant::now() + Duration::from_secs(5);
906        while fake.checked.load(Ordering::SeqCst) < 3 {
907            assert!(Instant::now() < deadline, "the checks never ran");
908            std::thread::sleep(Duration::from_millis(5));
909        }
910        assert_eq!(
911            updater.status().state,
912            PackageState::Ready {
913                version: v("1.1.0")
914            }
915        );
916        // Checking, Downloading at 0, 50 and 100, Ready; nothing for the later checks.
917        assert_eq!(changes.load(Ordering::SeqCst), 5);
918        updater.apply(After::RestartApp).unwrap();
919        assert_eq!(apply_log(), ["before_apply", "apply 1.1.0 RestartApp"]);
920    }
921
922    #[test]
923    fn apply_is_retried_after_a_failing_before_apply() {
924        static REFUSE: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
925        fn refusing_once() -> Result<(), String> {
926            log("before_apply");
927            if REFUSE.swap(false, Ordering::SeqCst) {
928                Err("stop failed".to_owned())
929            } else {
930                Ok(())
931            }
932        }
933        let updater = updater(
934            spec(Some(refusing_once)),
935            Fake::new("1.0.0", vec![Ok(Some(info("1.1.0")))], None),
936        );
937        updater.check_now();
938        wait_for(&updater, settled);
939        let err = updater.apply(After::Exit).unwrap_err();
940        assert!(matches!(err, Error::Prepare(_)), "{err:?}");
941        assert_eq!(
942            updater.status().state,
943            PackageState::Failed("stop failed".to_owned())
944        );
945        updater.apply(After::Exit).unwrap();
946        assert_eq!(
947            apply_log(),
948            ["before_apply", "before_apply", "apply 1.1.0 Exit"]
949        );
950    }
951
952    #[test]
953    fn a_check_after_a_refused_before_apply_is_ready_again() {
954        let fake = Fake::new(
955            "1.0.0",
956            vec![
957                Ok(Some(info("1.1.0"))),
958                Err(Error::Network("offline".into())),
959            ],
960            None,
961        );
962        let updater = updater(spec(Some(failing_to_stop)), Arc::clone(&fake));
963        updater.check_now();
964        wait_for(&updater, settled);
965        updater.apply(After::RestartApp).unwrap_err();
966        assert_eq!(
967            updater.status().state,
968            PackageState::Failed("stop failed".to_owned())
969        );
970        // The GUI's Retry: a check, which fails here, and the update is still downloaded.
971        updater.check_now();
972        let status = wait_for(&updater, |s| matches!(s, PackageState::Ready { .. }));
973        assert_eq!(
974            status.state,
975            PackageState::Ready {
976                version: v("1.1.0")
977            }
978        );
979    }
980
981    #[test]
982    fn run_in_background_checks_at_once_and_stops_with_the_last_updater() {
983        let fake = Fake::new("1.0.0", vec![Ok(None)], None);
984        let updater = updater(spec(None), Arc::clone(&fake));
985        updater.run_in_background(Duration::from_secs(3600));
986        assert_eq!(wait_for(&updater, settled).state, PackageState::UpToDate);
987        let clone = updater.clone();
988        drop(updater);
989        drop(clone);
990        // The worker holds the fake through the updater only: once it ends, the test holds
991        // the last reference.
992        let deadline = Instant::now() + Duration::from_secs(5);
993        while Arc::strong_count(&fake) > 1 {
994            assert!(Instant::now() < deadline, "the worker outlived the updater");
995            std::thread::sleep(Duration::from_millis(5));
996        }
997    }
998}