chore: migrate to gpui updater #42

Merged
reya merged 2 commits from feat/auto-update into master 2026-08-03 01:54:25 +00:00
6 changed files with 238 additions and 522 deletions

109
Cargo.lock generated
View File

@@ -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"
@@ -9025,6 +9117,7 @@ name = "workspace"
version = "1.0.0-beta5" version = "1.0.0-beta5"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"auto_update",
"browser-signer-proxy", "browser-signer-proxy",
"chat", "chat",
"chat_ui", "chat_ui",

View File

@@ -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

View File

@@ -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, SharedString, Subscription, Window};
use std::path::{Path, PathBuf}; use gpui_updater::{EngineConfig, GitHubSource, UpdateStatus, Updater, Version};
use std::sync::Arc; use instant::{Duration, Instant};
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 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,145 @@ 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]>, /// When the last error was recorded, so we can reset to idle after 5s.
error_time: Option<Instant>,
/// 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 mut body = Vec::new();
let mut response = http_client.get(&url, async_body, false).await?;
// Read the response body into a vector
response.body_mut().read_to_end(&mut body).await?;
if !response.status().is_success() {
return Err(anyhow!("GitHub API returned error: {}", response.status()));
}
// 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( let updater: Entity<Updater> =
// Install the new release cx.new(|cx| Updater::new(source, EngineConfig::new(version.clone()), cx));
// When an update becomes available, automatically download and install it.
let subscription = cx.observe(&updater, |this: &mut AutoUpdater, _updater, cx| {
let status = this.updater.read(cx).status().clone();
if matches!(status, UpdateStatus::Available(_)) {
this.updater.update(cx, |updater, cx| {
updater.download_and_install(cx);
});
}
if matches!(status, UpdateStatus::Errored(_)) {
this.error_time = Some(Instant::now());
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
cx.background_executor().timer(Duration::from_secs(5)).await;
this.update(cx, |_this, cx| cx.notify()).ok();
})
.detach();
} else {
this.error_time = None;
}
cx.notify();
});
// Schedule an auto-check after a 2-minute delay (deferred to run at the
// 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.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| { error_time: None,
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(()) pub fn idle(&self, cx: &App) -> bool {
}), let status = self.updater.read(cx).status();
); if status == &UpdateStatus::Idle {
return true;
}
if matches!(status, UpdateStatus::Errored(_))
&& self
.error_time
.is_some_and(|t| t.elapsed() >= Duration::from_secs(5))
{
return true;
}
false
} }
async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf, Error> { pub fn status(&self, cx: &App) -> SharedString {
let filename = match std::env::consts::OS { let status = self.updater.read(cx).status();
"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)) match status {
UpdateStatus::Idle => "Up to date".into(),
UpdateStatus::Checking => "Checking for updates…".into(),
UpdateStatus::UpToDate => "Up to date".into(),
UpdateStatus::Available(version) => format!("Version {version} available").into(),
UpdateStatus::Downloading { downloaded, total } => {
let total_mb = total.map(|t| t as f64 / 1_048_576.0);
let downloaded_mb = *downloaded as f64 / 1_048_576.0;
match total_mb {
Some(t) => format!("Downloading {downloaded_mb:.1} / {t:.1} MB").into(),
None => format!("Downloading {downloaded_mb:.1} MB").into(),
} }
}
async fn install( UpdateStatus::Installing => "Installing update…".into(),
installer_dir: InstallerDir, UpdateStatus::Staged(version) => {
target_path: PathBuf, format!("Version {version} ready — restart to apply").into()
cx: &AsyncApp, }
) -> Result<(), Error> { UpdateStatus::Errored(msg) => {
match std::env::consts::OS { if self
"macos" => install_release_macos(&installer_dir, target_path, cx).await, .error_time
"linux" => install_release_linux(&installer_dir, target_path, cx).await, .is_some_and(|t| t.elapsed() >= Duration::from_secs(5))
"windows" => install_release_windows(target_path).await, {
unsupported_os => anyhow::bail!("Not supported: {unsupported_os}"), "Up to date".into()
} else {
format!("Update failed: {msg}").into()
} }
} }
} }
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(())
}

View File

@@ -164,12 +164,8 @@
</head> </head>
<body> <body>
<div class="card"> <div class="card">
<div class="logo">
<div class="logo__mark">C</div>
<span class="logo__text">Coop</span>
</div>
<h1 class="heading">Web Signer</h1> <h1 class="heading">Web Signer</h1>
<p class="subtitle"> <p class="subtitle">
This page connects the app to your Nostr Web Signer extension so you can sign in and use Coop securely. This page connects the app to your Nostr Web Signer extension so you can sign in and use Coop securely.
</p> </p>

View File

@@ -14,6 +14,7 @@ chat = { path = "../chat" }
chat_ui = { path = "../chat_ui" } chat_ui = { path = "../chat_ui" }
settings = { path = "../settings" } settings = { path = "../settings" }
person = { path = "../person" } person = { path = "../person" }
auto_update = { path = "../auto_update" }
gpui.workspace = true gpui.workspace = true
nostr-sdk.workspace = true nostr-sdk.workspace = true

View File

@@ -2,6 +2,7 @@ use std::sync::Arc;
use ::settings::AppSettings; use ::settings::AppSettings;
use anyhow::Error; use anyhow::Error;
use auto_update::AutoUpdater;
use chat::{ChatEvent, ChatRegistry}; use chat::{ChatEvent, ChatRegistry};
use common::{CoopImageCache, download_dir}; use common::{CoopImageCache, download_dir};
use device::{DeviceEvent, DeviceRegistry}; use device::{DeviceEvent, DeviceRegistry};
@@ -44,13 +45,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 +379,14 @@ impl Workspace {
Command::ImportEncryption => { Command::ImportEncryption => {
self.import_encryption(window, cx); self.import_encryption(window, cx);
} }
Command::Update => {
let auto_updater = AutoUpdater::global(cx);
auto_updater.update(cx, |this, cx| {
this.updater.update(cx, |updater, cx| {
updater.check(cx);
});
});
}
} }
} }
@@ -586,6 +594,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,
@@ -597,6 +610,7 @@ impl Workspace {
} }
fn titlebar_right(&mut self, cx: &mut Context<Self>) -> impl IntoElement { fn titlebar_right(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let updater = AutoUpdater::global(cx);
let chat = ChatRegistry::global(cx); let chat = ChatRegistry::global(cx);
let nip4e_enabled = AppSettings::get_nip4e(cx); let nip4e_enabled = AppSettings::get_nip4e(cx);
let nostr = NostrRegistry::global(cx); let nostr = NostrRegistry::global(cx);
@@ -608,10 +622,15 @@ impl Workspace {
let persons = PersonRegistry::global(cx); let persons = PersonRegistry::global(cx);
let profile = persons.read(cx).get(&public_key, cx); let profile = persons.read(cx).get(&public_key, cx);
let announcement = profile.announcement(); let announcement = profile.announcement();
let updater_idle = updater.read(cx).idle(cx);
h_flex() h_flex()
.when(!cx.theme().platform.is_mac(), |this| this.pr_2()) .when(!cx.theme().platform.is_mac(), |this| this.pr_2())
.gap_2() .gap_2()
.when(!updater_idle, |this| {
let status = updater.read(cx).status(cx);
this.child(div().text_xs().italic().child(status))
})
.when(nip4e_enabled, |this| { .when(nip4e_enabled, |this| {
this.child( this.child(
Button::new("key") Button::new("key")