#![doc = include_str!("../docs/guide.md")]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(test, allow(clippy::unwrap_used, clippy::expect_used))]
use std::any::Any;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::time::Duration;
use futures::StreamExt as _;
use futures::channel::mpsc;
use gpui::{Context, Task};
pub use gpui_updater_core::*;
type BoxedEngine = UpdateEngine<Box<dyn UpdateSource>>;
pub struct Updater {
status: UpdateStatus,
available: Option<Release>,
backend: Backend,
task: Option<Task<()>>,
cancel: Option<CancelToken>,
}
enum Backend {
Engine(Arc<BoxedEngine>),
Driver {
driver: Box<dyn UpdateDriver>,
_events: Task<()>,
},
}
impl Updater {
pub fn new<S: UpdateSource>(source: S, config: EngineConfig, _cx: &mut Context<Self>) -> Self {
let engine = UpdateEngine::new(Box::new(source) as Box<dyn UpdateSource>, config);
Self {
status: UpdateStatus::Idle,
available: None,
backend: Backend::Engine(Arc::new(engine)),
task: None,
cancel: None,
}
}
pub fn with_driver<D: UpdateDriver>(mut driver: D, cx: &mut Context<Self>) -> Self {
let (tx, mut rx) = mpsc::unbounded::<DriverEvent>();
driver.start(DriverEvents::new(move |event| {
tx.unbounded_send(event).ok();
}));
let events = cx.spawn(async move |this, cx| {
while let Some(event) = rx.next().await {
if this
.update(cx, |this, cx| this.apply_driver_event(event, cx))
.is_err()
{
break;
}
}
});
Self {
status: UpdateStatus::Idle,
available: None,
backend: Backend::Driver {
driver: Box::new(driver),
_events: events,
},
task: None,
cancel: None,
}
}
#[must_use]
pub fn status(&self) -> &UpdateStatus {
&self.status
}
#[must_use]
pub fn available(&self) -> Option<&Release> {
self.available.as_ref()
}
#[must_use]
pub fn is_driver_backed(&self) -> bool {
matches!(self.backend, Backend::Driver { .. })
}
pub fn driver_mut<D: UpdateDriver>(&mut self) -> Option<&mut D> {
match &mut self.backend {
Backend::Driver { driver, .. } => (driver.as_mut() as &mut dyn Any).downcast_mut::<D>(),
Backend::Engine(_) => None,
}
}
fn set_status(&mut self, status: UpdateStatus, cx: &mut Context<Self>) {
self.status = status;
cx.notify();
}
fn apply_driver_event(&mut self, event: DriverEvent, cx: &mut Context<Self>) {
let status = match event {
DriverEvent::Checking => UpdateStatus::Checking,
DriverEvent::UpToDate => {
self.available = None;
UpdateStatus::UpToDate
}
DriverEvent::Found(release) => {
let version = release.version.clone();
self.available = Some(release);
UpdateStatus::Available(version)
}
DriverEvent::Downloading { downloaded, total } => {
UpdateStatus::Downloading { downloaded, total }
}
DriverEvent::Installing => UpdateStatus::Installing,
DriverEvent::ReadyToRelaunch(version) => UpdateStatus::Staged(version),
DriverEvent::Failed(error) => UpdateStatus::Errored(error),
DriverEvent::Settled => {
if !self.status.is_busy() {
return;
}
self.available
.as_ref()
.map_or(UpdateStatus::Idle, |release| {
UpdateStatus::Available(release.version.clone())
})
}
_ => return,
};
self.set_status(status, cx);
}
pub fn check(&mut self, cx: &mut Context<Self>) {
let engine = match &mut self.backend {
Backend::Driver { driver, .. } => {
driver.check();
return;
}
Backend::Engine(engine) => engine.clone(),
};
if self.status.is_busy() {
return;
}
self.set_status(UpdateStatus::Checking, cx);
self.task = Some(cx.spawn(async move |this, cx| {
let result = cx
.background_executor()
.spawn(async move { engine.check() })
.await;
this.update(cx, |this, cx| {
this.task = None;
match result {
Ok(Some(release)) => {
let version = release.version.clone();
this.available = Some(release);
this.set_status(UpdateStatus::Available(version), cx);
}
Ok(None) => this.set_status(UpdateStatus::UpToDate, cx),
Err(e) => this.set_status(UpdateStatus::Errored(e.into()), cx),
}
})
.ok();
}));
}
pub fn download_and_install(&mut self, cx: &mut Context<Self>) {
let engine = match &mut self.backend {
Backend::Driver { driver, .. } => {
driver.install();
return;
}
Backend::Engine(engine) => engine.clone(),
};
if self.status.is_busy() {
return;
}
let Some(release) = self.available.clone() else {
return;
};
let cancel = CancelToken::new();
self.cancel = Some(cancel.clone());
self.set_status(
UpdateStatus::Downloading {
downloaded: 0,
total: None,
},
cx,
);
self.task = Some(cx.spawn(async move |this, cx| {
let downloaded = Arc::new(AtomicU64::new(0));
let total = Arc::new(AtomicU64::new(0)); let done = Arc::new(AtomicBool::new(false));
let dl_task = {
let (engine, release) = (engine.clone(), release.clone());
let (d, t, fin) = (downloaded.clone(), total.clone(), done.clone());
cx.background_executor().spawn(async move {
let result = engine.download_cancellable(&release, &cancel, |got, tot| {
d.store(got, Ordering::Relaxed);
t.store(tot.unwrap_or(0), Ordering::Relaxed);
});
fin.store(true, Ordering::Relaxed);
result
})
};
loop {
let got = downloaded.load(Ordering::Relaxed);
let tot = total.load(Ordering::Relaxed);
this.update(cx, |this, cx| {
this.set_status(
UpdateStatus::Downloading {
downloaded: got,
total: (tot != 0).then_some(tot),
},
cx,
);
})
.ok();
if done.load(Ordering::Relaxed) {
break;
}
cx.background_executor()
.timer(Duration::from_millis(120))
.await;
}
let artifact = match dl_task.await {
Ok(artifact) => artifact,
Err(e) => {
this.update(cx, |this, cx| {
this.task = None;
this.cancel = None;
this.set_status(download_failed(e, release.version.clone()), cx);
})
.ok();
return;
}
};
this.update(cx, |this, cx| {
this.cancel = None;
this.set_status(UpdateStatus::Installing, cx);
})
.ok();
let installed = {
let engine = engine.clone();
cx.background_executor()
.spawn(async move { engine.install(&artifact) })
.await
};
this.update(cx, |this, cx| {
this.task = None;
let status = installed_status(installed, release.version, |path| {
cx.set_restart_path(path);
});
this.set_status(status, cx);
})
.ok();
}));
}
pub fn cancel(&mut self, _cx: &mut Context<Self>) {
match &mut self.backend {
Backend::Driver { driver, .. } => driver.cancel(),
Backend::Engine(_) => {
if let Some(cancel) = &self.cancel {
cancel.cancel();
}
}
}
}
pub fn restart(&mut self, cx: &mut Context<Self>) {
match &mut self.backend {
Backend::Driver { driver, .. } => driver.relaunch(),
Backend::Engine(_) => cx.restart(),
}
}
}
fn download_failed(error: Error, version: Version) -> UpdateStatus {
if error.kind() == ErrorKind::Cancelled {
UpdateStatus::Available(version)
} else {
UpdateStatus::Errored(error.into())
}
}
impl Drop for Updater {
fn drop(&mut self) {
if let Some(cancel) = &self.cancel {
cancel.cancel();
}
}
}
fn installed_status(
installed: Result<Installed>,
version: Version,
set_restart_path: impl FnOnce(std::path::PathBuf),
) -> UpdateStatus {
match installed {
Ok(installed) => {
if let Some(path) = installed.restart_path {
set_restart_path(path);
}
UpdateStatus::Staged(version)
}
Err(e) => UpdateStatus::Errored(e.into()),
}
}
#[cfg(test)]
mod tests;