Skip to main content

gpui_updater/
lib.rs

1//! GPUI integration: an observable `Entity<Updater>` that drives the blocking
2//! [`UpdateEngine`] on the app's executors, or an external [`UpdateDriver`].
3//!
4//! The update runs on the background executor so the UI thread never blocks;
5//! state transitions land back on the foreground via the entity and call
6//! [`Context::notify`], so any view can `cx.observe(&updater, …)` to re-render.
7//! When an update is staged, `App::set_restart_path` is set so the next
8//! `cx.restart()` launches the new version.
9//!
10//! With [`Updater::with_driver`], an external [`UpdateDriver`] (such as
11//! Sparkle, via `gpui-updater-sparkle`) runs the whole flow instead and
12//! reports [`DriverEvent`]s, which the entity maps onto the same
13//! [`UpdateStatus`]. Views do not need to know which backend is in use.
14#![doc = include_str!("../docs/guide.md")]
15#![cfg_attr(docsrs, feature(doc_cfg))]
16#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
17
18use std::any::Any;
19use std::sync::Arc;
20use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
21use std::time::Duration;
22
23use futures::StreamExt as _;
24use futures::channel::mpsc;
25use gpui::{Context, Task};
26
27pub use gpui_updater_core::*;
28
29type BoxedEngine = UpdateEngine<Box<dyn UpdateSource>>;
30
31/// A GPUI entity that checks for, downloads, and installs updates.
32///
33/// Construct it with [`cx.new`](gpui::AppContext::new) and hold the resulting
34/// `Entity<Updater>`. All work is triggered explicitly — there is no background
35/// polling — which suits a privacy-conscious "Check for updates" button. To
36/// poll, call [`check`](Self::check) yourself on a timer.
37///
38/// Build it with [`Updater::new`] to run the built-in [`UpdateEngine`], or
39/// with [`Updater::with_driver`] to delegate to an external
40/// [`UpdateDriver`]. A driver may also check on its own schedule (Sparkle
41/// does when automatic checks are on).
42///
43/// Dropping the entity cancels a running download (as
44/// [`cancel`](Self::cancel) does), but not an install that has started. It
45/// does not roll back an installation.
46pub struct Updater {
47    status: UpdateStatus,
48    available: Option<Release>,
49    backend: Backend,
50    task: Option<Task<()>>,
51    /// Stops the download in progress, if any.
52    cancel: Option<CancelToken>,
53}
54
55enum Backend {
56    Engine(Arc<BoxedEngine>),
57    Driver {
58        driver: Box<dyn UpdateDriver>,
59        /// Applies queued driver events on the foreground; dropped with the
60        /// `Updater`, which stops the pump.
61        _events: Task<()>,
62    },
63}
64
65impl Updater {
66    /// Create an updater from a [`UpdateSource`] and [`EngineConfig`].
67    pub fn new<S: UpdateSource>(source: S, config: EngineConfig, _cx: &mut Context<Self>) -> Self {
68        let engine = UpdateEngine::new(Box::new(source) as Box<dyn UpdateSource>, config);
69        Self {
70            status: UpdateStatus::Idle,
71            available: None,
72            backend: Backend::Engine(Arc::new(engine)),
73            task: None,
74            cancel: None,
75        }
76    }
77
78    /// Create an updater that delegates the whole flow to `driver`.
79    ///
80    /// The driver is started immediately; its [`DriverEvent`]s update
81    /// [`status`](Self::status) on the foreground. [`check`](Self::check),
82    /// [`download_and_install`](Self::download_and_install) and
83    /// [`restart`](Self::restart) forward to the driver.
84    pub fn with_driver<D: UpdateDriver>(mut driver: D, cx: &mut Context<Self>) -> Self {
85        let (tx, mut rx) = mpsc::unbounded::<DriverEvent>();
86        // Sending fails only once the pump below is gone, i.e. the `Updater`
87        // was dropped. Nobody is left to tell, so the event is discarded.
88        driver.start(DriverEvents::new(move |event| {
89            tx.unbounded_send(event).ok();
90        }));
91        let events = cx.spawn(async move |this, cx| {
92            while let Some(event) = rx.next().await {
93                if this
94                    .update(cx, |this, cx| this.apply_driver_event(event, cx))
95                    .is_err()
96                {
97                    break;
98                }
99            }
100        });
101        Self {
102            status: UpdateStatus::Idle,
103            available: None,
104            backend: Backend::Driver {
105                driver: Box::new(driver),
106                _events: events,
107            },
108            task: None,
109            cancel: None,
110        }
111    }
112
113    /// Current status (cheap to clone; read this in `render`).
114    #[must_use]
115    pub fn status(&self) -> &UpdateStatus {
116        &self.status
117    }
118
119    /// The release discovered by the last successful [`check`](Self::check),
120    /// if it was newer than the running version.
121    ///
122    /// With a driver this is the release the driver last reported; its asset
123    /// and signature fields describe what the driver will install, but the
124    /// driver performs the download itself.
125    #[must_use]
126    pub fn available(&self) -> Option<&Release> {
127        self.available.as_ref()
128    }
129
130    /// Whether this updater delegates to an external [`UpdateDriver`].
131    #[must_use]
132    pub fn is_driver_backed(&self) -> bool {
133        matches!(self.backend, Backend::Driver { .. })
134    }
135
136    /// The driver of type `D`, if this updater was built with
137    /// [`with_driver`](Self::with_driver) and that driver is a `D`. Use it to
138    /// reach driver-specific settings (such as Sparkle's automatic checks).
139    pub fn driver_mut<D: UpdateDriver>(&mut self) -> Option<&mut D> {
140        match &mut self.backend {
141            Backend::Driver { driver, .. } => (driver.as_mut() as &mut dyn Any).downcast_mut::<D>(),
142            Backend::Engine(_) => None,
143        }
144    }
145
146    fn set_status(&mut self, status: UpdateStatus, cx: &mut Context<Self>) {
147        self.status = status;
148        cx.notify();
149    }
150
151    fn apply_driver_event(&mut self, event: DriverEvent, cx: &mut Context<Self>) {
152        let status = match event {
153            DriverEvent::Checking => UpdateStatus::Checking,
154            DriverEvent::UpToDate => {
155                // Unlike an engine check, a driver's "up to date" means it has
156                // nothing to install, so `Settled` must not resurrect an
157                // earlier release.
158                self.available = None;
159                UpdateStatus::UpToDate
160            }
161            DriverEvent::Found(release) => {
162                let version = release.version.clone();
163                self.available = Some(release);
164                UpdateStatus::Available(version)
165            }
166            DriverEvent::Downloading { downloaded, total } => {
167                UpdateStatus::Downloading { downloaded, total }
168            }
169            DriverEvent::Installing => UpdateStatus::Installing,
170            DriverEvent::ReadyToRelaunch(version) => UpdateStatus::Staged(version),
171            DriverEvent::Failed(error) => UpdateStatus::Errored(error),
172            DriverEvent::Settled => {
173                if !self.status.is_busy() {
174                    return;
175                }
176                self.available
177                    .as_ref()
178                    .map_or(UpdateStatus::Idle, |release| {
179                        UpdateStatus::Available(release.version.clone())
180                    })
181            }
182            // `DriverEvent` is non-exhaustive; ignore events this adapter
183            // does not know yet.
184            _ => return,
185        };
186        self.set_status(status, cx);
187    }
188
189    /// Check for a newer release.
190    ///
191    /// With the engine this is a no-op while already busy. With a driver it
192    /// forwards to [`UpdateDriver::check`], which may also bring an update
193    /// prompt the driver is already showing to the front.
194    pub fn check(&mut self, cx: &mut Context<Self>) {
195        let engine = match &mut self.backend {
196            Backend::Driver { driver, .. } => {
197                driver.check();
198                return;
199            }
200            Backend::Engine(engine) => engine.clone(),
201        };
202        if self.status.is_busy() {
203            return;
204        }
205        self.set_status(UpdateStatus::Checking, cx);
206        self.task = Some(cx.spawn(async move |this, cx| {
207            let result = cx
208                .background_executor()
209                .spawn(async move { engine.check() })
210                .await;
211            this.update(cx, |this, cx| {
212                this.task = None;
213                match result {
214                    Ok(Some(release)) => {
215                        let version = release.version.clone();
216                        this.available = Some(release);
217                        this.set_status(UpdateStatus::Available(version), cx);
218                    }
219                    Ok(None) => this.set_status(UpdateStatus::UpToDate, cx),
220                    Err(e) => this.set_status(UpdateStatus::Errored(e.into()), cx),
221                }
222            })
223            .ok();
224        }));
225    }
226
227    /// Download the available update, verify it, and swap it into place.
228    /// With the engine this is a no-op unless a newer release is
229    /// [`available`](Self::available).
230    ///
231    /// On success the status becomes [`UpdateStatus::Staged`] and the app's
232    /// restart path is set to the new binary; call [`restart`](Self::restart).
233    /// With a driver this forwards to [`UpdateDriver::install`].
234    pub fn download_and_install(&mut self, cx: &mut Context<Self>) {
235        let engine = match &mut self.backend {
236            Backend::Driver { driver, .. } => {
237                driver.install();
238                return;
239            }
240            Backend::Engine(engine) => engine.clone(),
241        };
242        if self.status.is_busy() {
243            return;
244        }
245        let Some(release) = self.available.clone() else {
246            return;
247        };
248        let cancel = CancelToken::new();
249        self.cancel = Some(cancel.clone());
250        self.set_status(
251            UpdateStatus::Downloading {
252                downloaded: 0,
253                total: None,
254            },
255            cx,
256        );
257        self.task = Some(cx.spawn(async move |this, cx| {
258            // The blocking download runs on a background thread and reports
259            // progress into shared atomics; this foreground task polls them so
260            // the UI updates live without being spammed by every 64 KiB chunk.
261            let downloaded = Arc::new(AtomicU64::new(0));
262            let total = Arc::new(AtomicU64::new(0)); // 0 = unknown
263            let done = Arc::new(AtomicBool::new(false));
264
265            let dl_task = {
266                let (engine, release) = (engine.clone(), release.clone());
267                let (d, t, fin) = (downloaded.clone(), total.clone(), done.clone());
268                cx.background_executor().spawn(async move {
269                    let result = engine.download_cancellable(&release, &cancel, |got, tot| {
270                        d.store(got, Ordering::Relaxed);
271                        t.store(tot.unwrap_or(0), Ordering::Relaxed);
272                    });
273                    fin.store(true, Ordering::Relaxed);
274                    result
275                })
276            };
277
278            loop {
279                let got = downloaded.load(Ordering::Relaxed);
280                let tot = total.load(Ordering::Relaxed);
281                this.update(cx, |this, cx| {
282                    this.set_status(
283                        UpdateStatus::Downloading {
284                            downloaded: got,
285                            total: (tot != 0).then_some(tot),
286                        },
287                        cx,
288                    );
289                })
290                .ok();
291                if done.load(Ordering::Relaxed) {
292                    break;
293                }
294                cx.background_executor()
295                    .timer(Duration::from_millis(120))
296                    .await;
297            }
298
299            let artifact = match dl_task.await {
300                Ok(artifact) => artifact,
301                Err(e) => {
302                    this.update(cx, |this, cx| {
303                        this.task = None;
304                        this.cancel = None;
305                        this.set_status(download_failed(e, release.version.clone()), cx);
306                    })
307                    .ok();
308                    return;
309                }
310            };
311
312            // An install cannot be cancelled.
313            this.update(cx, |this, cx| {
314                this.cancel = None;
315                this.set_status(UpdateStatus::Installing, cx);
316            })
317            .ok();
318
319            let installed = {
320                let engine = engine.clone();
321                cx.background_executor()
322                    .spawn(async move { engine.install(&artifact) })
323                    .await
324            };
325            this.update(cx, |this, cx| {
326                this.task = None;
327                let status = installed_status(installed, release.version, |path| {
328                    cx.set_restart_path(path);
329                });
330                this.set_status(status, cx);
331            })
332            .ok();
333        }));
334    }
335
336    /// Stop the download in progress.
337    ///
338    /// With the engine the download stops at its next checkpoint (see
339    /// [`CancelToken`]), removes its partial file, and the status returns to
340    /// [`UpdateStatus::Available`]. An install that has started is not
341    /// stopped, and a call while nothing is downloading does nothing. With a
342    /// driver this forwards to [`UpdateDriver::cancel`].
343    pub fn cancel(&mut self, _cx: &mut Context<Self>) {
344        match &mut self.backend {
345            Backend::Driver { driver, .. } => driver.cancel(),
346            Backend::Engine(_) => {
347                if let Some(cancel) = &self.cancel {
348                    cancel.cancel();
349                }
350            }
351        }
352    }
353
354    /// Relaunch into the staged update.
355    ///
356    /// With the engine this uses the restart path set during
357    /// [`download_and_install`](Self::download_and_install). With a driver it
358    /// forwards to [`UpdateDriver::relaunch`].
359    pub fn restart(&mut self, cx: &mut Context<Self>) {
360        match &mut self.backend {
361            Backend::Driver { driver, .. } => driver.relaunch(),
362            Backend::Engine(_) => cx.restart(),
363        }
364    }
365}
366
367// Official GPUI 0.2.2's TestPlatform discards restart paths. Keep the handoff
368// isolated so its exact path and error/None behavior can be tested safely.
369/// The status after a download that failed with `error`. Cancelling is not a
370/// failure: the update is still there to download.
371fn download_failed(error: Error, version: Version) -> UpdateStatus {
372    if error.kind() == ErrorKind::Cancelled {
373        UpdateStatus::Available(version)
374    } else {
375        UpdateStatus::Errored(error.into())
376    }
377}
378
379impl Drop for Updater {
380    fn drop(&mut self) {
381        // Dropping the entity drops its foreground task, but the blocking
382        // download runs on a background thread and only stops when told to.
383        if let Some(cancel) = &self.cancel {
384            cancel.cancel();
385        }
386    }
387}
388
389fn installed_status(
390    installed: Result<Installed>,
391    version: Version,
392    set_restart_path: impl FnOnce(std::path::PathBuf),
393) -> UpdateStatus {
394    match installed {
395        Ok(installed) => {
396            if let Some(path) = installed.restart_path {
397                set_restart_path(path);
398            }
399            UpdateStatus::Staged(version)
400        }
401        Err(e) => UpdateStatus::Errored(e.into()),
402    }
403}
404
405#[cfg(test)]
406mod tests;