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;