chore: migrate to gpui updater #42
108
Cargo.lock
generated
108
Cargo.lock
generated
@@ -546,17 +546,10 @@ dependencies = [
|
|||||||
name = "auto_update"
|
name = "auto_update"
|
||||||
version = "1.0.0-beta5"
|
version = "1.0.0-beta5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
|
||||||
"common",
|
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"gpui-updater",
|
||||||
"instant",
|
"instant",
|
||||||
"log",
|
"log",
|
||||||
"semver",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"smallvec",
|
|
||||||
"smol",
|
|
||||||
"tempfile",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1375,6 +1368,35 @@ dependencies = [
|
|||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.18.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
"time",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie_store"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
|
||||||
|
dependencies = [
|
||||||
|
"cookie",
|
||||||
|
"document-features",
|
||||||
|
"idna",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "coop"
|
name = "coop"
|
||||||
version = "1.0.0-beta5"
|
version = "1.0.0-beta5"
|
||||||
@@ -2713,6 +2735,22 @@ dependencies = [
|
|||||||
"zed-scap",
|
"zed-scap",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gpui-updater"
|
||||||
|
version = "0.0.6"
|
||||||
|
source = "git+https://github.com/AprilNEA/gpui-updater?tag=v0.0.6#e8a85768793ac3238b0c1bc1f32d6a1544667ec3"
|
||||||
|
dependencies = [
|
||||||
|
"gpui",
|
||||||
|
"minisign-verify",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2 0.10.9",
|
||||||
|
"thiserror 2.0.19",
|
||||||
|
"tracing",
|
||||||
|
"ureq",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpui_linux"
|
name = "gpui_linux"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -4106,6 +4144,12 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minisign-verify"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@@ -7137,6 +7181,7 @@ dependencies = [
|
|||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"time-core",
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -7145,6 +7190,16 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiny-keccak"
|
name = "tiny-keccak"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
@@ -7718,6 +7773,37 @@ version = "0.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq"
|
||||||
|
version = "3.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"cookie_store",
|
||||||
|
"log",
|
||||||
|
"percent-encoding",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"ureq-proto",
|
||||||
|
"utf8-zero",
|
||||||
|
"webpki-roots 1.0.9",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq-proto"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -7764,6 +7850,12 @@ version = "0.7.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8-zero"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8_iter"
|
name = "utf8_iter"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|||||||
@@ -5,18 +5,8 @@ edition.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
common = { path = "../common" }
|
|
||||||
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
instant.workspace = true
|
instant.workspace = true
|
||||||
anyhow.workspace = true
|
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
smallvec.workspace = true
|
|
||||||
serde = { workspace = true, features = ["derive"] }
|
|
||||||
serde_json.workspace = true
|
|
||||||
|
|
||||||
semver = "1.0.27"
|
gpui-updater = { git = "https://github.com/AprilNEA/gpui-updater", tag = "v0.0.6", features = ["gpui"] }
|
||||||
tempfile = "3.23.0"
|
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
|
||||||
smol.workspace = true
|
|
||||||
|
|||||||
@@ -1,24 +1,9 @@
|
|||||||
#![cfg(not(target_arch = "wasm32"))]
|
#![cfg(not(target_arch = "wasm32"))]
|
||||||
|
|
||||||
use std::ffi::OsString;
|
use gpui::{App, AppContext, Context, Entity, Global, Subscription, Window};
|
||||||
use std::path::{Path, PathBuf};
|
use gpui_updater::{EngineConfig, GitHubSource, UpdateStatus, Updater, Version};
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use anyhow::{Context as AnyhowContext, Error, anyhow};
|
|
||||||
use gpui::http_client::{AsyncBody, HttpClient};
|
|
||||||
use gpui::{
|
|
||||||
App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, Global, Subscription, Task,
|
|
||||||
Window,
|
|
||||||
};
|
|
||||||
use instant::Duration;
|
use instant::Duration;
|
||||||
use semver::Version;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use smallvec::{SmallVec, smallvec};
|
|
||||||
use smol::fs::File;
|
|
||||||
use smol::io::AsyncReadExt;
|
|
||||||
use smol::process::Command;
|
|
||||||
|
|
||||||
const GITHUB_API_URL: &str = "https://api.github.com";
|
|
||||||
const COOP_UPDATE_EXPLANATION: &str = "COOP_UPDATE_EXPLANATION";
|
const COOP_UPDATE_EXPLANATION: &str = "COOP_UPDATE_EXPLANATION";
|
||||||
|
|
||||||
fn get_github_repo_owner() -> String {
|
fn get_github_repo_owner() -> String {
|
||||||
@@ -30,12 +15,15 @@ fn get_github_repo_name() -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_flatpak_installation() -> bool {
|
fn is_flatpak_installation() -> bool {
|
||||||
// Check if app is installed via Flatpak
|
|
||||||
std::env::var("FLATPAK_ID").is_ok() || std::env::var(COOP_UPDATE_EXPLANATION).is_ok()
|
std::env::var("FLATPAK_ID").is_ok() || std::env::var(COOP_UPDATE_EXPLANATION).is_ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initialize the auto-update system.
|
||||||
|
///
|
||||||
|
/// Skips initialization when running as a Flatpak (updates are handled by the
|
||||||
|
/// Flatpak distribution channel). Otherwise creates the global [`AutoUpdater`]
|
||||||
|
/// entity and schedules a check for updates after a 2-minute delay.
|
||||||
pub fn init(window: &mut Window, cx: &mut App) {
|
pub fn init(window: &mut Window, cx: &mut App) {
|
||||||
// Skip auto-update initialization if installed via Flatpak
|
|
||||||
if is_flatpak_installation() {
|
if is_flatpak_installation() {
|
||||||
log::info!("Skipping auto-update initialization: App is installed via Flatpak");
|
log::info!("Skipping auto-update initialization: App is installed via Flatpak");
|
||||||
return;
|
return;
|
||||||
@@ -48,516 +36,81 @@ struct GlobalAutoUpdater(Entity<AutoUpdater>);
|
|||||||
|
|
||||||
impl Global for GlobalAutoUpdater {}
|
impl Global for GlobalAutoUpdater {}
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
/// Observable auto-update status — re-exported from [`gpui_updater::UpdateStatus`].
|
||||||
struct InstallerDir(tempfile::TempDir);
|
pub use gpui_updater::UpdateStatus as AutoUpdateStatus;
|
||||||
|
|
||||||
#[cfg(not(target_os = "windows"))]
|
/// The global auto-updater entity.
|
||||||
impl InstallerDir {
|
///
|
||||||
async fn new() -> Result<Self, Error> {
|
/// Wraps [`gpui_updater::Updater`] with Coop-specific configuration
|
||||||
Ok(Self(
|
/// (GitHub repo, Flatpak detection, delayed auto-check).
|
||||||
tempfile::Builder::new()
|
///
|
||||||
.prefix("coop-auto-update")
|
/// Retrieve the global instance via [`AutoUpdater::global`].
|
||||||
.tempdir()?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn path(&self) -> &Path {
|
|
||||||
self.0.path()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
struct InstallerDir(PathBuf);
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
impl InstallerDir {
|
|
||||||
async fn new() -> Result<Self, Error> {
|
|
||||||
let installer_dir = std::env::current_exe()?
|
|
||||||
.parent()
|
|
||||||
.context("No parent dir for Coop.exe")?
|
|
||||||
.join("updates");
|
|
||||||
|
|
||||||
if smol::fs::metadata(&installer_dir).await.is_ok() {
|
|
||||||
smol::fs::remove_dir_all(&installer_dir).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
smol::fs::create_dir(&installer_dir).await?;
|
|
||||||
|
|
||||||
Ok(Self(installer_dir))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn path(&self) -> &Path {
|
|
||||||
self.0.as_path()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MacOsUnmounter<'a> {
|
|
||||||
mount_path: PathBuf,
|
|
||||||
background_executor: &'a BackgroundExecutor,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for MacOsUnmounter<'_> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
let mount_path = std::mem::take(&mut self.mount_path);
|
|
||||||
|
|
||||||
self.background_executor
|
|
||||||
.spawn(async move {
|
|
||||||
let unmount_output = Command::new("hdiutil")
|
|
||||||
.args(["detach", "-force"])
|
|
||||||
.arg(&mount_path)
|
|
||||||
.output()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match unmount_output {
|
|
||||||
Ok(output) if output.status.success() => {
|
|
||||||
log::info!("Successfully unmounted the disk image");
|
|
||||||
}
|
|
||||||
Ok(output) => {
|
|
||||||
log::error!(
|
|
||||||
"Failed to unmount disk image: {:?}",
|
|
||||||
String::from_utf8_lossy(&output.stderr)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(error) => {
|
|
||||||
log::error!("Error while trying to unmount disk image: {:?}", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
|
||||||
pub enum AutoUpdateStatus {
|
|
||||||
Idle,
|
|
||||||
Checking,
|
|
||||||
Checked { download_url: String },
|
|
||||||
Installing,
|
|
||||||
Updated,
|
|
||||||
Errored { msg: Box<String> },
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRef<AutoUpdateStatus> for AutoUpdateStatus {
|
|
||||||
fn as_ref(&self) -> &AutoUpdateStatus {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AutoUpdateStatus {
|
|
||||||
pub fn is_updating(&self) -> bool {
|
|
||||||
matches!(self, Self::Checked { .. } | Self::Installing)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_updated(&self) -> bool {
|
|
||||||
matches!(self, Self::Updated)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn checked(download_url: String) -> Self {
|
|
||||||
Self::Checked { download_url }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error(e: String) -> Self {
|
|
||||||
Self::Errored { msg: Box::new(e) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct GitHubRelease {
|
|
||||||
pub tag_name: String,
|
|
||||||
pub assets: Vec<GitHubAsset>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct GitHubAsset {
|
|
||||||
pub name: String,
|
|
||||||
pub browser_download_url: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AutoUpdater {
|
pub struct AutoUpdater {
|
||||||
/// Current status of the auto updater
|
/// The underlying gpui-updater entity that does the heavy lifting.
|
||||||
pub status: AutoUpdateStatus,
|
pub updater: Entity<Updater>,
|
||||||
|
/// Currently running app version.
|
||||||
/// Current version of the application
|
|
||||||
pub version: Version,
|
pub version: Version,
|
||||||
|
/// Keeps the observer subscription alive.
|
||||||
/// Event subscriptions
|
_subscription: Subscription,
|
||||||
_subscriptions: SmallVec<[Subscription; 1]>,
|
|
||||||
|
|
||||||
/// Background tasks
|
|
||||||
tasks: Vec<Task<Result<(), Error>>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AutoUpdater {
|
impl AutoUpdater {
|
||||||
/// Retrieve the global auto updater instance
|
/// Retrieve the global auto updater instance.
|
||||||
pub fn global(cx: &App) -> Entity<Self> {
|
pub fn global(cx: &App) -> Entity<Self> {
|
||||||
cx.global::<GlobalAutoUpdater>().0.clone()
|
cx.global::<GlobalAutoUpdater>().0.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the global auto updater instance
|
|
||||||
fn set_global(state: Entity<Self>, cx: &mut App) {
|
fn set_global(state: Entity<Self>, cx: &mut App) {
|
||||||
cx.set_global(GlobalAutoUpdater(state));
|
cx.set_global(GlobalAutoUpdater(state));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
let version = Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
|
||||||
let mut subscriptions = smallvec![];
|
|
||||||
|
|
||||||
subscriptions.push(
|
|
||||||
// Observe the status
|
|
||||||
cx.observe_self(|this, cx| {
|
|
||||||
if let AutoUpdateStatus::Checked { download_url } = this.status.clone() {
|
|
||||||
this.download_and_install(&download_url, cx);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Run at the end of current cycle
|
|
||||||
cx.defer_in(window, |this, _window, cx| {
|
|
||||||
this.check(cx);
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
status: AutoUpdateStatus::Idle,
|
|
||||||
version,
|
|
||||||
tasks: vec![],
|
|
||||||
_subscriptions: subscriptions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_status(&mut self, status: AutoUpdateStatus, cx: &mut Context<Self>) {
|
|
||||||
self.status = status;
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check(&mut self, cx: &mut Context<Self>) {
|
|
||||||
let version = self.version.clone();
|
|
||||||
let duration = Duration::from_secs(120);
|
|
||||||
let task = self.check_for_updates(version, cx);
|
|
||||||
|
|
||||||
// Check for updates after 2 minutes
|
|
||||||
self.tasks.push(cx.spawn(async move |this, cx| {
|
|
||||||
cx.background_executor().timer(duration).await;
|
|
||||||
|
|
||||||
// Update the status to checking
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_status(AutoUpdateStatus::Checking, cx);
|
|
||||||
})?;
|
|
||||||
|
|
||||||
match task.await {
|
|
||||||
Ok(download_url) => {
|
|
||||||
// Update the status to checked with download URL
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_status(AutoUpdateStatus::checked(download_url), cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!("Failed to check for updates: {e}");
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_status(AutoUpdateStatus::Idle, cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_for_updates(&self, version: Version, cx: &App) -> Task<Result<String, Error>> {
|
|
||||||
let http_client = cx.http_client();
|
|
||||||
let repo_owner = get_github_repo_owner();
|
let repo_owner = get_github_repo_owner();
|
||||||
let repo_name = get_github_repo_name();
|
let repo_name = get_github_repo_name();
|
||||||
|
|
||||||
cx.background_spawn(async move {
|
let source =
|
||||||
let url = format!(
|
GitHubSource::new(&repo_owner, &repo_name).asset_contains(match std::env::consts::OS {
|
||||||
"{}/repos/{}/{}/releases/latest",
|
"macos" => "macos",
|
||||||
GITHUB_API_URL, repo_owner, repo_name
|
"linux" => "linux",
|
||||||
);
|
_ => "",
|
||||||
|
});
|
||||||
|
|
||||||
let async_body = AsyncBody::default();
|
let updater: Entity<Updater> =
|
||||||
let mut body = Vec::new();
|
cx.new(|cx| Updater::new(source, EngineConfig::new(version.clone()), cx));
|
||||||
let mut response = http_client.get(&url, async_body, false).await?;
|
|
||||||
|
|
||||||
// Read the response body into a vector
|
// When an update becomes available, automatically download and install it.
|
||||||
response.body_mut().read_to_end(&mut body).await?;
|
let subscription = cx.observe(&updater, |this: &mut AutoUpdater, _updater, cx| {
|
||||||
|
let status = this.updater.read(cx).status().clone();
|
||||||
if !response.status().is_success() {
|
if matches!(status, UpdateStatus::Available(_)) {
|
||||||
return Err(anyhow!("GitHub API returned error: {}", response.status()));
|
this.updater.update(cx, |updater, cx| {
|
||||||
|
updater.download_and_install(cx);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
cx.notify();
|
||||||
// Parse the response body as JSON
|
|
||||||
let release: GitHubRelease = serde_json::from_slice(&body)?;
|
|
||||||
|
|
||||||
// Parse version from tag (remove 'v' prefix if present)
|
|
||||||
let tag_version = release.tag_name.trim_start_matches('v');
|
|
||||||
let new_version = Version::parse(tag_version).context(format!(
|
|
||||||
"Failed to parse version from tag: {}",
|
|
||||||
release.tag_name
|
|
||||||
))?;
|
|
||||||
|
|
||||||
if new_version > version {
|
|
||||||
// Find the appropriate asset for the current platform
|
|
||||||
let current_os = std::env::consts::OS;
|
|
||||||
let asset_name = match current_os {
|
|
||||||
"macos" => "Coop.dmg",
|
|
||||||
"linux" => "coop.tar.gz",
|
|
||||||
"windows" => "Coop.exe",
|
|
||||||
_ => return Err(anyhow!("Unsupported OS: {}", current_os)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let download_url = release
|
|
||||||
.assets
|
|
||||||
.iter()
|
|
||||||
.find(|asset| asset.name == asset_name)
|
|
||||||
.map(|asset| asset.browser_download_url.clone())
|
|
||||||
.context(format!(
|
|
||||||
"No {} asset found in release {}",
|
|
||||||
asset_name, release.tag_name
|
|
||||||
))?;
|
|
||||||
|
|
||||||
Ok(download_url)
|
|
||||||
} else {
|
|
||||||
Err(anyhow!(
|
|
||||||
"No update available. Current: {}, Latest: {}",
|
|
||||||
version,
|
|
||||||
new_version
|
|
||||||
))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn download_and_install(&mut self, download_url: &str, cx: &mut Context<Self>) {
|
|
||||||
let http_client = cx.http_client();
|
|
||||||
let download_url = download_url.to_string();
|
|
||||||
|
|
||||||
let task: Task<Result<(InstallerDir, PathBuf), Error>> = cx.background_spawn(async move {
|
|
||||||
let installer_dir = InstallerDir::new().await?;
|
|
||||||
let target_path = Self::target_path(&installer_dir).await?;
|
|
||||||
|
|
||||||
// Download the release
|
|
||||||
download(&download_url, &target_path, http_client).await?;
|
|
||||||
|
|
||||||
Ok((installer_dir, target_path))
|
|
||||||
});
|
});
|
||||||
|
|
||||||
self.tasks.push(
|
// Schedule an auto-check after a 2-minute delay (deferred to run at the
|
||||||
// Install the new release
|
// end of the current frame so the window is fully set up).
|
||||||
|
cx.defer_in(window, |_this, _window, cx| {
|
||||||
|
let duration = Duration::from_secs(120);
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
|
cx.background_executor().timer(duration).await;
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.set_status(AutoUpdateStatus::Installing, cx);
|
this.updater.update(cx, |updater, cx| {
|
||||||
})?;
|
updater.check(cx);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
});
|
||||||
|
|
||||||
match task.await {
|
Self {
|
||||||
Ok((installer_dir, target_path)) => {
|
updater,
|
||||||
if Self::install(installer_dir, target_path, cx).await.is_ok() {
|
version,
|
||||||
// Update the status to updated
|
_subscription: subscription,
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_status(AutoUpdateStatus::Updated, cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
// Update the status to error including the error message
|
|
||||||
this.update(cx, |this, cx| {
|
|
||||||
this.set_status(AutoUpdateStatus::error(e.to_string()), cx);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf, Error> {
|
|
||||||
let filename = match std::env::consts::OS {
|
|
||||||
"macos" => anyhow::Ok("Coop.dmg"),
|
|
||||||
"linux" => Ok("coop.tar.gz"),
|
|
||||||
"windows" => Ok("Coop.exe"),
|
|
||||||
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
|
|
||||||
}?;
|
|
||||||
|
|
||||||
Ok(installer_dir.path().join(filename))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn install(
|
|
||||||
installer_dir: InstallerDir,
|
|
||||||
target_path: PathBuf,
|
|
||||||
cx: &AsyncApp,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
match std::env::consts::OS {
|
|
||||||
"macos" => install_release_macos(&installer_dir, target_path, cx).await,
|
|
||||||
"linux" => install_release_linux(&installer_dir, target_path, cx).await,
|
|
||||||
"windows" => install_release_windows(target_path).await,
|
|
||||||
unsupported_os => anyhow::bail!("Not supported: {unsupported_os}"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn download(
|
|
||||||
url: &str,
|
|
||||||
target_path: &std::path::Path,
|
|
||||||
client: Arc<dyn HttpClient>,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let body = AsyncBody::default();
|
|
||||||
let mut target_file = File::create(&target_path).await?;
|
|
||||||
let mut response = client.get(url, body, true).await?;
|
|
||||||
|
|
||||||
// Copy the response body to the target file
|
|
||||||
smol::io::copy(response.body_mut(), &mut target_file).await?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn install_release_macos(
|
|
||||||
temp_dir: &InstallerDir,
|
|
||||||
downloaded_dmg: PathBuf,
|
|
||||||
cx: &AsyncApp,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let running_app_path = cx.update(|cx| cx.app_path())?;
|
|
||||||
let running_app_filename = running_app_path
|
|
||||||
.file_name()
|
|
||||||
.with_context(|| format!("invalid running app path {running_app_path:?}"))?;
|
|
||||||
|
|
||||||
let mount_path = temp_dir.path().join("Coop");
|
|
||||||
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
|
|
||||||
|
|
||||||
mounted_app_path.push("/");
|
|
||||||
|
|
||||||
let output = Command::new("hdiutil")
|
|
||||||
.args(["attach", "-nobrowse"])
|
|
||||||
.arg(&downloaded_dmg)
|
|
||||||
.arg("-mountroot")
|
|
||||||
.arg(temp_dir.path())
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
anyhow::ensure!(
|
|
||||||
output.status.success(),
|
|
||||||
"failed to mount: {:?}",
|
|
||||||
String::from_utf8_lossy(&output.stderr)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create an MacOsUnmounter that will be dropped (and thus unmount the disk) when this function exits
|
|
||||||
let _unmounter = MacOsUnmounter {
|
|
||||||
mount_path: mount_path.clone(),
|
|
||||||
background_executor: cx.background_executor(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let output = Command::new("rsync")
|
|
||||||
.args(["-av", "--delete"])
|
|
||||||
.arg(&mounted_app_path)
|
|
||||||
.arg(&running_app_path)
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
anyhow::ensure!(
|
|
||||||
output.status.success(),
|
|
||||||
"failed to copy app: {:?}",
|
|
||||||
String::from_utf8_lossy(&output.stderr)
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn install_release_linux(
|
|
||||||
temp_dir: &InstallerDir,
|
|
||||||
downloaded_tar_gz: PathBuf,
|
|
||||||
cx: &AsyncApp,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let running_app_path = cx.update(|cx| cx.app_path())?;
|
|
||||||
|
|
||||||
// Extract the tar.gz file
|
|
||||||
let extracted = temp_dir.path().join("coop");
|
|
||||||
smol::fs::create_dir_all(&extracted)
|
|
||||||
.await
|
|
||||||
.context("failed to create directory to extract update")?;
|
|
||||||
|
|
||||||
let output = Command::new("tar")
|
|
||||||
.arg("-xzf")
|
|
||||||
.arg(&downloaded_tar_gz)
|
|
||||||
.arg("-C")
|
|
||||||
.arg(&extracted)
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
anyhow::ensure!(
|
|
||||||
output.status.success(),
|
|
||||||
"failed to extract {:?} to {:?}: {:?}",
|
|
||||||
downloaded_tar_gz,
|
|
||||||
extracted,
|
|
||||||
String::from_utf8_lossy(&output.stderr)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Find the extracted app directory
|
|
||||||
let mut entries = smol::fs::read_dir(&extracted).await?;
|
|
||||||
let mut app_dir = None;
|
|
||||||
|
|
||||||
use smol::stream::StreamExt;
|
|
||||||
|
|
||||||
while let Some(entry) = entries.next().await {
|
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_dir() {
|
|
||||||
app_dir = Some(path);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let from = app_dir.context("No app directory found in archive")?;
|
|
||||||
|
|
||||||
// Copy to the current installation directory
|
|
||||||
let output = Command::new("rsync")
|
|
||||||
.args(["-av", "--delete"])
|
|
||||||
.arg(&from)
|
|
||||||
.arg(
|
|
||||||
running_app_path
|
|
||||||
.parent()
|
|
||||||
.context("No parent directory for app")?,
|
|
||||||
)
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
anyhow::ensure!(
|
|
||||||
output.status.success(),
|
|
||||||
"failed to copy app from {:?} to {:?}: {:?}",
|
|
||||||
from,
|
|
||||||
running_app_path.parent(),
|
|
||||||
String::from_utf8_lossy(&output.stderr)
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn install_release_windows(downloaded_installer: PathBuf) -> Result<(), Error> {
|
|
||||||
//const CREATE_NO_WINDOW: u32 = 0x08000000;
|
|
||||||
|
|
||||||
let system_root = std::env::var("SYSTEMROOT");
|
|
||||||
let powershell_path = system_root.as_ref().map_or_else(
|
|
||||||
|_| "powershell.exe".to_string(),
|
|
||||||
|p| format!("{p}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"),
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut installer_path = std::ffi::OsString::new();
|
|
||||||
installer_path.push("\"");
|
|
||||||
installer_path.push(&downloaded_installer);
|
|
||||||
installer_path.push("\"");
|
|
||||||
|
|
||||||
let output = Command::new(powershell_path)
|
|
||||||
//.creation_flags(CREATE_NO_WINDOW)
|
|
||||||
.args(["-NoProfile", "-WindowStyle", "Hidden"])
|
|
||||||
.args(["Start-Process"])
|
|
||||||
.arg(installer_path)
|
|
||||||
.arg("-ArgumentList")
|
|
||||||
.args(["/P", "/R"])
|
|
||||||
.output()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
anyhow::ensure!(
|
|
||||||
output.status.success(),
|
|
||||||
"failed to start installer: {:?}",
|
|
||||||
String::from_utf8_lossy(&output.stderr)
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -44,13 +44,12 @@ struct MsgRelayNotification;
|
|||||||
#[action(namespace = workspace, no_json)]
|
#[action(namespace = workspace, no_json)]
|
||||||
enum Command {
|
enum Command {
|
||||||
ToggleTheme,
|
ToggleTheme,
|
||||||
|
Update,
|
||||||
RefreshMessagingRelays,
|
RefreshMessagingRelays,
|
||||||
BackupEncryption,
|
BackupEncryption,
|
||||||
ImportEncryption,
|
ImportEncryption,
|
||||||
RefreshEncryption,
|
RefreshEncryption,
|
||||||
ResetEncryption,
|
ResetEncryption,
|
||||||
|
|
||||||
ShowRelayList,
|
ShowRelayList,
|
||||||
ShowMessaging,
|
ShowMessaging,
|
||||||
ShowProfile,
|
ShowProfile,
|
||||||
@@ -379,6 +378,9 @@ impl Workspace {
|
|||||||
Command::ImportEncryption => {
|
Command::ImportEncryption => {
|
||||||
self.import_encryption(window, cx);
|
self.import_encryption(window, cx);
|
||||||
}
|
}
|
||||||
|
Command::Update => {
|
||||||
|
//
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,6 +588,11 @@ impl Workspace {
|
|||||||
Box::new(Command::ToggleTheme),
|
Box::new(Command::ToggleTheme),
|
||||||
)
|
)
|
||||||
.separator()
|
.separator()
|
||||||
|
.menu_with_icon(
|
||||||
|
"Check for Updates",
|
||||||
|
IconName::Device,
|
||||||
|
Box::new(Command::Update),
|
||||||
|
)
|
||||||
.menu_with_icon(
|
.menu_with_icon(
|
||||||
"Settings",
|
"Settings",
|
||||||
IconName::Settings,
|
IconName::Settings,
|
||||||
|
|||||||
Reference in New Issue
Block a user