From 9e717c771168f6368b7a63acee5b0adf025677c3 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 10 Nov 2025 23:00:55 -0700 Subject: [PATCH] Use cloud for auto-update (#42246) We've had several outages with a proximate cause of "vercel is complicated", and auto-update is considered a critical feature; so lets not use vercel for that. Release Notes: - Auto Updates (and remote server binaries) are now downloaded via https://cloud.zed.dev instead of https://zed.dev. As before, these URLs redirect to the GitHub release for actual downloads. --- Cargo.lock | 6 + crates/auto_update/Cargo.toml | 5 + crates/auto_update/src/auto_update.rs | 438 +++++++++++------- crates/client/src/client.rs | 2 +- .../cloud_api_client/src/cloud_api_client.rs | 8 +- crates/gpui/src/app/test_context.rs | 11 +- crates/gpui/src/platform/test/platform.rs | 8 +- crates/http_client/Cargo.toml | 1 + crates/http_client/src/http_client.rs | 21 +- .../recent_projects/src/remote_connections.rs | 12 +- crates/release_channel/src/lib.rs | 6 + crates/remote/src/remote_client.rs | 8 +- crates/remote/src/transport/ssh.rs | 22 +- crates/zed/src/main.rs | 2 +- script/bundle-mac | 22 +- 15 files changed, 350 insertions(+), 222 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index faae1259d9d5c08559ec6ba02463367e84b3aa4d..bee290f2f17ffba973d432272c91344b8caa99f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1330,10 +1330,14 @@ version = "0.1.0" dependencies = [ "anyhow", "client", + "clock", + "ctor", "db", + "futures 0.3.31", "gpui", "http_client", "log", + "parking_lot", "paths", "release_channel", "serde", @@ -1344,6 +1348,7 @@ dependencies = [ "util", "which 6.0.3", "workspace", + "zlog", ] [[package]] @@ -7799,6 +7804,7 @@ dependencies = [ "parking_lot", "serde", "serde_json", + "serde_urlencoded", "sha2", "tempfile", "url", diff --git a/crates/auto_update/Cargo.toml b/crates/auto_update/Cargo.toml index 630be043dca120ca76b2552f0a729a03a684f934..ae7c869493d8ca33528800f91c446e9546c952d0 100644 --- a/crates/auto_update/Cargo.toml +++ b/crates/auto_update/Cargo.toml @@ -33,4 +33,9 @@ workspace.workspace = true which.workspace = true [dev-dependencies] +ctor.workspace = true +clock= { workspace = true, "features" = ["test-support"] } +futures.workspace = true gpui = { workspace = true, "features" = ["test-support"] } +parking_lot.workspace = true +zlog.workspace = true diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index 1997beaf11cb2b1d29cc759c5e5f8a6ad6f51eb8..bd44eb714c08f9a5c698e92570a9edb518c5c806 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -1,12 +1,11 @@ use anyhow::{Context as _, Result}; -use client::{Client, TelemetrySettings}; -use db::RELEASE_CHANNEL; +use client::Client; use db::kvp::KEY_VALUE_STORE; use gpui::{ App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, SemanticVersion, Task, Window, actions, }; -use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; +use http_client::{HttpClient, HttpClientWithUrl}; use paths::remote_servers_dir; use release_channel::{AppCommitSha, ReleaseChannel}; use serde::{Deserialize, Serialize}; @@ -41,22 +40,23 @@ actions!( ] ); -#[derive(Serialize)] -struct UpdateRequestBody { - installation_id: Option>, - release_channel: Option<&'static str>, - telemetry: bool, - is_staff: Option, - destination: &'static str, -} - #[derive(Clone, Debug, PartialEq, Eq)] pub enum VersionCheckType { Sha(AppCommitSha), Semantic(SemanticVersion), } -#[derive(Clone)] +#[derive(Serialize, Debug)] +pub struct AssetQuery<'a> { + asset: &'a str, + os: &'a str, + arch: &'a str, + metrics_id: Option<&'a str>, + system_id: Option<&'a str>, + is_staff: Option, +} + +#[derive(Clone, Debug)] pub enum AutoUpdateStatus { Idle, Checking, @@ -66,6 +66,31 @@ pub enum AutoUpdateStatus { Errored { error: Arc }, } +impl PartialEq for AutoUpdateStatus { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (AutoUpdateStatus::Idle, AutoUpdateStatus::Idle) => true, + (AutoUpdateStatus::Checking, AutoUpdateStatus::Checking) => true, + ( + AutoUpdateStatus::Downloading { version: v1 }, + AutoUpdateStatus::Downloading { version: v2 }, + ) => v1 == v2, + ( + AutoUpdateStatus::Installing { version: v1 }, + AutoUpdateStatus::Installing { version: v2 }, + ) => v1 == v2, + ( + AutoUpdateStatus::Updated { version: v1 }, + AutoUpdateStatus::Updated { version: v2 }, + ) => v1 == v2, + (AutoUpdateStatus::Errored { error: e1 }, AutoUpdateStatus::Errored { error: e2 }) => { + e1.to_string() == e2.to_string() + } + _ => false, + } + } +} + impl AutoUpdateStatus { pub fn is_updated(&self) -> bool { matches!(self, Self::Updated { .. }) @@ -75,13 +100,13 @@ impl AutoUpdateStatus { pub struct AutoUpdater { status: AutoUpdateStatus, current_version: SemanticVersion, - http_client: Arc, + client: Arc, pending_poll: Option>>, quit_subscription: Option, } -#[derive(Deserialize, Clone, Debug)] -pub struct JsonRelease { +#[derive(Deserialize, Serialize, Clone, Debug)] +pub struct ReleaseAsset { pub version: String, pub url: String, } @@ -137,7 +162,7 @@ struct GlobalAutoUpdate(Option>); impl Global for GlobalAutoUpdate {} -pub fn init(http_client: Arc, cx: &mut App) { +pub fn init(client: Arc, cx: &mut App) { cx.observe_new(|workspace: &mut Workspace, _window, _cx| { workspace.register_action(|_, action, window, cx| check(action, window, cx)); @@ -149,7 +174,7 @@ pub fn init(http_client: Arc, cx: &mut App) { let version = release_channel::AppVersion::global(cx); let auto_updater = cx.new(|cx| { - let updater = AutoUpdater::new(version, http_client, cx); + let updater = AutoUpdater::new(version, client, cx); let poll_for_updates = ReleaseChannel::try_global(cx) .map(|channel| channel.poll_for_updates()) @@ -233,7 +258,7 @@ pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut App) -> Option<()> { let current_version = auto_updater.current_version; let release_channel = release_channel.dev_name(); let path = format!("/releases/{release_channel}/{current_version}"); - let url = &auto_updater.http_client.build_url(&path); + let url = &auto_updater.client.http_client().build_url(&path); cx.open_url(url); } ReleaseChannel::Nightly => { @@ -296,11 +321,7 @@ impl AutoUpdater { cx.default_global::().0.clone() } - fn new( - current_version: SemanticVersion, - http_client: Arc, - cx: &mut Context, - ) -> Self { + fn new(current_version: SemanticVersion, client: Arc, cx: &mut Context) -> Self { // On windows, executable files cannot be overwritten while they are // running, so we must wait to overwrite the application until quitting // or restarting. When quitting the app, we spawn the auto update helper @@ -321,7 +342,7 @@ impl AutoUpdater { Self { status: AutoUpdateStatus::Idle, current_version, - http_client, + client, pending_poll: None, quit_subscription, } @@ -354,7 +375,7 @@ impl AutoUpdater { cx.notify(); self.pending_poll = Some(cx.spawn(async move |this, cx| { - let result = Self::update(this.upgrade()?, cx.clone()).await; + let result = Self::update(this.upgrade()?, cx).await; this.update(cx, |this, cx| { this.pending_poll = None; if let Err(error) = result { @@ -400,10 +421,10 @@ impl AutoUpdater { // you can override this function. You should also update get_remote_server_release_url to return // Ok(None). pub async fn download_remote_server_release( - os: &str, - arch: &str, release_channel: ReleaseChannel, version: Option, + os: &str, + arch: &str, set_status: impl Fn(&str, &mut AsyncApp) + Send + 'static, cx: &mut AsyncApp, ) -> Result { @@ -415,13 +436,13 @@ impl AutoUpdater { })??; set_status("Fetching remote server release", cx); - let release = Self::get_release( + let release = Self::get_release_asset( &this, + release_channel, + version, "zed-remote-server", os, arch, - version, - Some(release_channel), cx, ) .await?; @@ -432,7 +453,7 @@ impl AutoUpdater { let version_path = platform_dir.join(format!("{}.gz", release.version)); smol::fs::create_dir_all(&platform_dir).await.ok(); - let client = this.read_with(cx, |this, _| this.http_client.clone())?; + let client = this.read_with(cx, |this, _| this.client.http_client())?; if smol::fs::metadata(&version_path).await.is_err() { log::info!( @@ -440,19 +461,19 @@ impl AutoUpdater { release.version ); set_status("Downloading remote server", cx); - download_remote_server_binary(&version_path, release, client, cx).await?; + download_remote_server_binary(&version_path, release, client).await?; } Ok(version_path) } pub async fn get_remote_server_release_url( + channel: ReleaseChannel, + version: Option, os: &str, arch: &str, - release_channel: ReleaseChannel, - version: Option, cx: &mut AsyncApp, - ) -> Result> { + ) -> Result> { let this = cx.update(|cx| { cx.default_global::() .0 @@ -460,108 +481,99 @@ impl AutoUpdater { .context("auto-update not initialized") })??; - let release = Self::get_release( - &this, - "zed-remote-server", - os, - arch, - version, - Some(release_channel), - cx, - ) - .await?; - - let update_request_body = build_remote_server_update_request_body(cx)?; - let body = serde_json::to_string(&update_request_body)?; + let release = + Self::get_release_asset(&this, channel, version, "zed-remote-server", os, arch, cx) + .await?; - Ok(Some((release.url, body))) + Ok(Some(release.url)) } - async fn get_release( + async fn get_release_asset( this: &Entity, + release_channel: ReleaseChannel, + version: Option, asset: &str, os: &str, arch: &str, - version: Option, - release_channel: Option, cx: &mut AsyncApp, - ) -> Result { - let client = this.read_with(cx, |this, _| this.http_client.clone())?; - - if let Some(version) = version { - let channel = release_channel.map(|c| c.dev_name()).unwrap_or("stable"); - - let url = format!("/api/releases/{channel}/{version}/{asset}-{os}-{arch}.gz?update=1",); - - Ok(JsonRelease { - version: version.to_string(), - url: client.build_url(&url), - }) + ) -> Result { + let client = this.read_with(cx, |this, _| this.client.clone())?; + + let (system_id, metrics_id, is_staff) = if client.telemetry().metrics_enabled() { + ( + client.telemetry().system_id(), + client.telemetry().metrics_id(), + client.telemetry().is_staff(), + ) } else { - let mut url_string = client.build_url(&format!( - "/api/releases/latest?asset={}&os={}&arch={}", - asset, os, arch - )); - if let Some(param) = release_channel.and_then(|c| c.release_query_param()) { - url_string += "&"; - url_string += param; - } + (None, None, None) + }; - let mut response = client.get(&url_string, Default::default(), true).await?; - let mut body = Vec::new(); - response.body_mut().read_to_end(&mut body).await?; + let version = if let Some(version) = version { + version.to_string() + } else { + "latest".to_string() + }; + let http_client = client.http_client(); + + let path = format!("/releases/{}/{}/asset", release_channel.dev_name(), version,); + let url = http_client.build_zed_cloud_url_with_query( + &path, + AssetQuery { + os, + arch, + asset, + metrics_id: metrics_id.as_deref(), + system_id: system_id.as_deref(), + is_staff: is_staff, + }, + )?; - anyhow::ensure!( - response.status().is_success(), - "failed to fetch release: {:?}", - String::from_utf8_lossy(&body), - ); + let mut response = http_client + .get(url.as_str(), Default::default(), true) + .await?; + let mut body = Vec::new(); + response.body_mut().read_to_end(&mut body).await?; - serde_json::from_slice(body.as_slice()).with_context(|| { - format!( - "error deserializing release {:?}", - String::from_utf8_lossy(&body), - ) - }) - } - } + anyhow::ensure!( + response.status().is_success(), + "failed to fetch release: {:?}", + String::from_utf8_lossy(&body), + ); - async fn get_latest_release( - this: &Entity, - asset: &str, - os: &str, - arch: &str, - release_channel: Option, - cx: &mut AsyncApp, - ) -> Result { - Self::get_release(this, asset, os, arch, None, release_channel, cx).await + serde_json::from_slice(body.as_slice()).with_context(|| { + format!( + "error deserializing release {:?}", + String::from_utf8_lossy(&body), + ) + }) } - async fn update(this: Entity, mut cx: AsyncApp) -> Result<()> { + async fn update(this: Entity, cx: &mut AsyncApp) -> Result<()> { let (client, installed_version, previous_status, release_channel) = - this.read_with(&cx, |this, cx| { + this.read_with(cx, |this, cx| { ( - this.http_client.clone(), + this.client.http_client(), this.current_version, this.status.clone(), - ReleaseChannel::try_global(cx), + ReleaseChannel::try_global(cx).unwrap_or(ReleaseChannel::Stable), ) })?; Self::check_dependencies()?; - this.update(&mut cx, |this, cx| { + this.update(cx, |this, cx| { this.status = AutoUpdateStatus::Checking; log::info!("Auto Update: checking for updates"); cx.notify(); })?; let fetched_release_data = - Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?; + Self::get_release_asset(&this, release_channel, None, "zed", OS, ARCH, cx).await?; let fetched_version = fetched_release_data.clone().version; let app_commit_sha = cx.update(|cx| AppCommitSha::try_global(cx).map(|sha| sha.full())); let newer_version = Self::check_if_fetched_version_is_newer( - *RELEASE_CHANNEL, + release_channel, app_commit_sha, installed_version, fetched_version, @@ -569,7 +581,7 @@ impl AutoUpdater { )?; let Some(newer_version) = newer_version else { - return this.update(&mut cx, |this, cx| { + return this.update(cx, |this, cx| { let status = match previous_status { AutoUpdateStatus::Updated { .. } => previous_status, _ => AutoUpdateStatus::Idle, @@ -579,7 +591,7 @@ impl AutoUpdater { }); }; - this.update(&mut cx, |this, cx| { + this.update(cx, |this, cx| { this.status = AutoUpdateStatus::Downloading { version: newer_version.clone(), }; @@ -588,21 +600,21 @@ impl AutoUpdater { let installer_dir = InstallerDir::new().await?; let target_path = Self::target_path(&installer_dir).await?; - download_release(&target_path, fetched_release_data, client, &cx).await?; + download_release(&target_path, fetched_release_data, client).await?; - this.update(&mut cx, |this, cx| { + this.update(cx, |this, cx| { this.status = AutoUpdateStatus::Installing { version: newer_version.clone(), }; cx.notify(); })?; - let new_binary_path = Self::install_release(installer_dir, target_path, &cx).await?; + let new_binary_path = Self::install_release(installer_dir, target_path, cx).await?; if let Some(new_binary_path) = new_binary_path { cx.update(|cx| cx.set_restart_path(new_binary_path))?; } - this.update(&mut cx, |this, cx| { + this.update(cx, |this, cx| { this.set_should_show_update_notification(true, cx) .detach_and_log_err(cx); this.status = AutoUpdateStatus::Updated { @@ -681,6 +693,12 @@ impl AutoUpdater { target_path: PathBuf, cx: &AsyncApp, ) -> Result> { + #[cfg(test)] + if let Some(test_install) = + cx.try_read_global::(|g, _| g.0.clone()) + { + return test_install(target_path, cx); + } match OS { "macos" => install_release_macos(&installer_dir, target_path, cx).await, "linux" => install_release_linux(&installer_dir, target_path, cx).await, @@ -731,16 +749,13 @@ impl AutoUpdater { async fn download_remote_server_binary( target_path: &PathBuf, - release: JsonRelease, + release: ReleaseAsset, client: Arc, - cx: &AsyncApp, ) -> Result<()> { let temp = tempfile::Builder::new().tempfile_in(remote_servers_dir())?; let mut temp_file = File::create(&temp).await?; - let update_request_body = build_remote_server_update_request_body(cx)?; - let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?); - let mut response = client.get(&release.url, request_body, true).await?; + let mut response = client.get(&release.url, Default::default(), true).await?; anyhow::ensure!( response.status().is_success(), "failed to download remote server release: {:?}", @@ -752,65 +767,19 @@ async fn download_remote_server_binary( Ok(()) } -fn build_remote_server_update_request_body(cx: &AsyncApp) -> Result { - let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| { - let telemetry = Client::global(cx).telemetry().clone(); - let is_staff = telemetry.is_staff(); - let installation_id = telemetry.installation_id(); - let release_channel = - ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name()); - let telemetry_enabled = TelemetrySettings::get_global(cx).metrics; - - ( - installation_id, - release_channel, - telemetry_enabled, - is_staff, - ) - })?; - - Ok(UpdateRequestBody { - installation_id, - release_channel, - telemetry: telemetry_enabled, - is_staff, - destination: "remote", - }) -} - async fn download_release( target_path: &Path, - release: JsonRelease, + release: ReleaseAsset, client: Arc, - cx: &AsyncApp, ) -> Result<()> { let mut target_file = File::create(&target_path).await?; - let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| { - let telemetry = Client::global(cx).telemetry().clone(); - let is_staff = telemetry.is_staff(); - let installation_id = telemetry.installation_id(); - let release_channel = - ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name()); - let telemetry_enabled = TelemetrySettings::get_global(cx).metrics; - - ( - installation_id, - release_channel, - telemetry_enabled, - is_staff, - ) - })?; - - let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody { - installation_id, - release_channel, - telemetry: telemetry_enabled, - is_staff, - destination: "local", - })?); - - let mut response = client.get(&release.url, request_body, true).await?; + let mut response = client.get(&release.url, Default::default(), true).await?; + anyhow::ensure!( + response.status().is_success(), + "failed to download update: {:?}", + response.status() + ); smol::io::copy(response.body_mut(), &mut target_file).await?; log::info!("downloaded update. path:{:?}", target_path); @@ -1010,11 +979,33 @@ pub async fn finalize_auto_update_on_quit() { #[cfg(test)] mod tests { + use client::Client; + use clock::FakeSystemClock; + use futures::channel::oneshot; use gpui::TestAppContext; + use http_client::{FakeHttpClient, Response}; use settings::default_settings; + use std::{ + rc::Rc, + sync::{ + Arc, + atomic::{self, AtomicBool}, + }, + }; + use tempfile::tempdir; + + #[ctor::ctor] + fn init_logger() { + zlog::init_test(); + } use super::*; + pub(super) struct InstallOverride( + pub Rc Result>>, + ); + impl Global for InstallOverride {} + #[gpui::test] fn test_auto_update_defaults_to_true(cx: &mut TestAppContext) { cx.update(|cx| { @@ -1030,6 +1021,115 @@ mod tests { }); } + #[gpui::test] + async fn test_auto_update_downloads(cx: &mut TestAppContext) { + cx.background_executor.allow_parking(); + zlog::init_test(); + let release_available = Arc::new(AtomicBool::new(false)); + + let (dmg_tx, dmg_rx) = oneshot::channel::(); + + cx.update(|cx| { + settings::init(cx); + + let current_version = SemanticVersion::new(0, 100, 0); + release_channel::init_test(current_version, ReleaseChannel::Stable, cx); + + let clock = Arc::new(FakeSystemClock::new()); + let release_available = Arc::clone(&release_available); + let dmg_rx = Arc::new(parking_lot::Mutex::new(Some(dmg_rx))); + let fake_client_http = FakeHttpClient::create(move |req| { + let release_available = release_available.load(atomic::Ordering::Relaxed); + let dmg_rx = dmg_rx.clone(); + async move { + if req.uri().path() == "/releases/stable/latest/asset" { + if release_available { + return Ok(Response::builder().status(200).body( + r#"{"version":"0.100.1","url":"https://test.example/new-download"}"#.into() + ).unwrap()); + } else { + return Ok(Response::builder().status(200).body( + r#"{"version":"0.100.0","url":"https://test.example/old-download"}"#.into() + ).unwrap()); + } + } else if req.uri().path() == "/new-download" { + return Ok(Response::builder().status(200).body({ + let dmg_rx = dmg_rx.lock().take().unwrap(); + dmg_rx.await.unwrap().into() + }).unwrap()); + } + Ok(Response::builder().status(404).body("".into()).unwrap()) + } + }); + let client = Client::new(clock, fake_client_http, cx); + crate::init(client, cx); + }); + + let auto_updater = cx.update(|cx| AutoUpdater::get(cx).expect("auto updater should exist")); + + cx.background_executor.run_until_parked(); + + auto_updater.read_with(cx, |updater, _| { + assert_eq!(updater.status(), AutoUpdateStatus::Idle); + assert_eq!(updater.current_version(), SemanticVersion::new(0, 100, 0)); + }); + + release_available.store(true, atomic::Ordering::SeqCst); + cx.background_executor.advance_clock(POLL_INTERVAL); + cx.background_executor.run_until_parked(); + + loop { + cx.background_executor.timer(Duration::from_millis(0)).await; + cx.run_until_parked(); + let status = auto_updater.read_with(cx, |updater, _| updater.status()); + if !matches!(status, AutoUpdateStatus::Idle) { + break; + } + } + let status = auto_updater.read_with(cx, |updater, _| updater.status()); + assert_eq!( + status, + AutoUpdateStatus::Downloading { + version: VersionCheckType::Semantic(SemanticVersion::new(0, 100, 1)) + } + ); + + dmg_tx.send("".to_owned()).unwrap(); + + let tmp_dir = Arc::new(tempdir().unwrap()); + + cx.update(|cx| { + let tmp_dir = tmp_dir.clone(); + cx.set_global(InstallOverride(Rc::new(move |target_path, _cx| { + let tmp_dir = tmp_dir.clone(); + let dest_path = tmp_dir.path().join("zed"); + std::fs::copy(&target_path, &dest_path)?; + Ok(Some(dest_path)) + }))); + }); + + loop { + cx.background_executor.timer(Duration::from_millis(0)).await; + cx.run_until_parked(); + let status = auto_updater.read_with(cx, |updater, _| updater.status()); + if !matches!(status, AutoUpdateStatus::Downloading { .. }) { + break; + } + } + let status = auto_updater.read_with(cx, |updater, _| updater.status()); + assert_eq!( + status, + AutoUpdateStatus::Updated { + version: VersionCheckType::Semantic(SemanticVersion::new(0, 100, 1)) + } + ); + let will_restart = cx.expect_restart(); + cx.update(|cx| cx.restart()); + let path = will_restart.await.unwrap().unwrap(); + assert_eq!(path, tmp_dir.path().join("zed")); + assert_eq!(std::fs::read_to_string(path).unwrap(), ""); + } + #[test] fn test_stable_does_not_update_when_fetched_version_is_not_higher() { let release_channel = ReleaseChannel::Stable; diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 3e220b0275270c04098ebb8cf3f5564cc3ca0342..96b15dc9fb13deea3cdc706f1927c4d6f016b57a 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1487,7 +1487,7 @@ impl Client { let url = self .http - .build_zed_cloud_url("/internal/users/impersonate", &[])?; + .build_zed_cloud_url("/internal/users/impersonate")?; let request = Request::post(url.as_str()) .header("Content-Type", "application/json") .header("Authorization", format!("Bearer {api_token}")) diff --git a/crates/cloud_api_client/src/cloud_api_client.rs b/crates/cloud_api_client/src/cloud_api_client.rs index 53b2b16a6a7c9447face6daa199bc4b2125445b9..9206e5e7efe51e99e4d57b708f09c682283612ed 100644 --- a/crates/cloud_api_client/src/cloud_api_client.rs +++ b/crates/cloud_api_client/src/cloud_api_client.rs @@ -62,7 +62,7 @@ impl CloudApiClient { let request = self.build_request( Request::builder().method(Method::GET).uri( self.http_client - .build_zed_cloud_url("/client/users/me", &[])? + .build_zed_cloud_url("/client/users/me")? .as_ref(), ), AsyncBody::default(), @@ -89,7 +89,7 @@ impl CloudApiClient { pub fn connect(&self, cx: &App) -> Result>> { let mut connect_url = self .http_client - .build_zed_cloud_url("/client/users/connect", &[])?; + .build_zed_cloud_url("/client/users/connect")?; connect_url .set_scheme(match connect_url.scheme() { "https" => "wss", @@ -123,7 +123,7 @@ impl CloudApiClient { .method(Method::POST) .uri( self.http_client - .build_zed_cloud_url("/client/llm_tokens", &[])? + .build_zed_cloud_url("/client/llm_tokens")? .as_ref(), ) .when_some(system_id, |builder, system_id| { @@ -154,7 +154,7 @@ impl CloudApiClient { let request = build_request( Request::builder().method(Method::GET).uri( self.http_client - .build_zed_cloud_url("/client/users/me", &[])? + .build_zed_cloud_url("/client/users/me")? .as_ref(), ), AsyncBody::default(), diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index d974823396d9f0d546a6b035f47b569145eb021b..4a7b73c359ed3dd55b136b22e9487dee1735e42e 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -10,7 +10,9 @@ use crate::{ use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt, channel::oneshot}; use rand::{SeedableRng, rngs::StdRng}; -use std::{cell::RefCell, future::Future, ops::Deref, rc::Rc, sync::Arc, time::Duration}; +use std::{ + cell::RefCell, future::Future, ops::Deref, path::PathBuf, rc::Rc, sync::Arc, time::Duration, +}; /// A TestAppContext is provided to tests created with `#[gpui::test]`, it provides /// an implementation of `Context` with additional methods that are useful in tests. @@ -331,6 +333,13 @@ impl TestAppContext { self.test_window(window_handle).simulate_resize(size); } + /// Returns true if there's an alert dialog open. + pub fn expect_restart(&self) -> oneshot::Receiver> { + let (tx, rx) = futures::channel::oneshot::channel(); + self.test_platform.expect_restart.borrow_mut().replace(tx); + rx + } + /// Causes the given sources to be returned if the application queries for screen /// capture sources. pub fn set_screen_capture_sources(&self, sources: Vec) { diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 15b909199fbd53b974e6a140f3223641dc0ac6ae..dfada364667989792325e02f8530e6c91bdf4716 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -36,6 +36,7 @@ pub(crate) struct TestPlatform { screen_capture_sources: RefCell>, pub opened_url: RefCell>, pub text_system: Arc, + pub expect_restart: RefCell>>>, #[cfg(target_os = "windows")] bitmap_factory: std::mem::ManuallyDrop, weak: Weak, @@ -112,6 +113,7 @@ impl TestPlatform { active_cursor: Default::default(), active_display: Rc::new(TestDisplay::new()), active_window: Default::default(), + expect_restart: Default::default(), current_clipboard_item: Mutex::new(None), #[cfg(any(target_os = "linux", target_os = "freebsd"))] current_primary_item: Mutex::new(None), @@ -250,8 +252,10 @@ impl Platform for TestPlatform { fn quit(&self) {} - fn restart(&self, _: Option) { - // + fn restart(&self, path: Option) { + if let Some(tx) = self.expect_restart.take() { + tx.send(path).unwrap(); + } } fn activate(&self, _ignoring_other_apps: bool) { diff --git a/crates/http_client/Cargo.toml b/crates/http_client/Cargo.toml index f4ce028b1c650ba3c85081d7737c99e9d1434e44..16600627a77f6a73fa913340f29f5a2da0875de9 100644 --- a/crates/http_client/Cargo.toml +++ b/crates/http_client/Cargo.toml @@ -31,6 +31,7 @@ parking_lot.workspace = true reqwest.workspace = true serde.workspace = true serde_json.workspace = true +serde_urlencoded.workspace = true sha2.workspace = true tempfile.workspace = true url.workspace = true diff --git a/crates/http_client/src/http_client.rs b/crates/http_client/src/http_client.rs index 6050d75c3edc8aefb1122df6e6af3bf078673217..a75df61646f31c9dc997bea83acc9d669bf1e29e 100644 --- a/crates/http_client/src/http_client.rs +++ b/crates/http_client/src/http_client.rs @@ -13,6 +13,7 @@ use futures::{ future::{self, BoxFuture}, }; use parking_lot::Mutex; +use serde::Serialize; #[cfg(feature = "test-support")] use std::fmt; use std::{any::type_name, sync::Arc}; @@ -255,7 +256,7 @@ impl HttpClientWithUrl { } /// Builds a Zed Cloud URL using the given path. - pub fn build_zed_cloud_url(&self, path: &str, query: &[(&str, &str)]) -> Result { + pub fn build_zed_cloud_url(&self, path: &str) -> Result { let base_url = self.base_url(); let base_api_url = match base_url.as_ref() { "https://zed.dev" => "https://cloud.zed.dev", @@ -264,10 +265,20 @@ impl HttpClientWithUrl { other => other, }; - Ok(Url::parse_with_params( - &format!("{}{}", base_api_url, path), - query, - )?) + Ok(Url::parse(&format!("{}{}", base_api_url, path))?) + } + + /// Builds a Zed Cloud URL using the given path and query params. + pub fn build_zed_cloud_url_with_query(&self, path: &str, query: impl Serialize) -> Result { + let base_url = self.base_url(); + let base_api_url = match base_url.as_ref() { + "https://zed.dev" => "https://cloud.zed.dev", + "https://staging.zed.dev" => "https://cloud.zed.dev", + "http://localhost:3000" => "http://localhost:8787", + other => other, + }; + let query = serde_urlencoded::to_string(&query)?; + Ok(Url::parse(&format!("{}{}?{}", base_api_url, path, query))?) } /// Builds a Zed LLM URL using the given path. diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs index ef6ce2e8deda150f352a88a466822a44ed02b55b..50d7912b80d0842854c36810378c5f8abbf7a2f7 100644 --- a/crates/recent_projects/src/remote_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -486,10 +486,10 @@ impl remote::RemoteClientDelegate for RemoteClientDelegate { let this = self.clone(); cx.spawn(async move |cx| { AutoUpdater::download_remote_server_release( - platform.os, - platform.arch, release_channel, version, + platform.os, + platform.arch, move |status, cx| this.set_status(Some(status), cx), cx, ) @@ -507,19 +507,19 @@ impl remote::RemoteClientDelegate for RemoteClientDelegate { }) } - fn get_download_params( + fn get_download_url( &self, platform: RemotePlatform, release_channel: ReleaseChannel, version: Option, cx: &mut AsyncApp, - ) -> Task>> { + ) -> Task>> { cx.spawn(async move |cx| { AutoUpdater::get_remote_server_release_url( - platform.os, - platform.arch, release_channel, version, + platform.os, + platform.arch, cx, ) .await diff --git a/crates/release_channel/src/lib.rs b/crates/release_channel/src/lib.rs index ba8d2e767503b00ed7f39921780a262b3e6c3624..c0ceafc760a5949d636dc2df3e93dc8926111417 100644 --- a/crates/release_channel/src/lib.rs +++ b/crates/release_channel/src/lib.rs @@ -126,6 +126,12 @@ pub fn init(app_version: SemanticVersion, cx: &mut App) { cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL)) } +/// Initializes the release channel for tests that rely on fake release channel. +pub fn init_test(app_version: SemanticVersion, release_channel: ReleaseChannel, cx: &mut App) { + cx.set_global(GlobalAppVersion(app_version)); + cx.set_global(GlobalReleaseChannel(release_channel)) +} + impl ReleaseChannel { /// Returns the global [`ReleaseChannel`]. pub fn global(cx: &App) -> Self { diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs index 54ec6644b9abef23446aaf0f8ddd21c0da6bdf05..1c14a0e244c3f09bb8b02e4aa99bd6b282435db5 100644 --- a/crates/remote/src/remote_client.rs +++ b/crates/remote/src/remote_client.rs @@ -67,13 +67,13 @@ pub trait RemoteClientDelegate: Send + Sync { tx: oneshot::Sender, cx: &mut AsyncApp, ); - fn get_download_params( + fn get_download_url( &self, platform: RemotePlatform, release_channel: ReleaseChannel, version: Option, cx: &mut AsyncApp, - ) -> Task>>; + ) -> Task>>; fn download_server_binary_locally( &self, platform: RemotePlatform, @@ -1669,13 +1669,13 @@ mod fake { unreachable!() } - fn get_download_params( + fn get_download_url( &self, _platform: RemotePlatform, _release_channel: ReleaseChannel, _version: Option, _cx: &mut AsyncApp, - ) -> Task>> { + ) -> Task>> { unreachable!() } diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs index 433c4b017aac81b73b15d388518e6349632435f6..ec020cba0b321ea3cb5929a3fa17cb6c425b1ef7 100644 --- a/crates/remote/src/transport/ssh.rs +++ b/crates/remote/src/transport/ssh.rs @@ -606,12 +606,12 @@ impl SshRemoteConnection { .unwrap(), ); if !self.socket.connection_options.upload_binary_over_ssh - && let Some((url, body)) = delegate - .get_download_params(self.ssh_platform, release_channel, wanted_version, cx) + && let Some(url) = delegate + .get_download_url(self.ssh_platform, release_channel, wanted_version, cx) .await? { match self - .download_binary_on_server(&url, &body, &tmp_path_gz, delegate, cx) + .download_binary_on_server(&url, &tmp_path_gz, delegate, cx) .await { Ok(_) => { @@ -644,7 +644,6 @@ impl SshRemoteConnection { async fn download_binary_on_server( &self, url: &str, - body: &str, tmp_path_gz: &RelPath, delegate: &Arc, cx: &mut AsyncApp, @@ -670,12 +669,6 @@ impl SshRemoteConnection { &[ "-f", "-L", - "-X", - "GET", - "-H", - "Content-Type: application/json", - "-d", - body, url, "-o", &tmp_path_gz.display(self.path_style()), @@ -700,14 +693,7 @@ impl SshRemoteConnection { .run_command( self.ssh_shell_kind, "wget", - &[ - "--header=Content-Type: application/json", - "--body-data", - body, - url, - "-O", - &tmp_path_gz.display(self.path_style()), - ], + &[url, "-O", &tmp_path_gz.display(self.path_style())], true, ) .await diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 180e0f1f04d1c7b1eddb0156659f697f423967ea..14e718ec2457b7d0f49c60cbc923cc7f215f9a15 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -539,7 +539,7 @@ pub fn main() { }); AppState::set_global(Arc::downgrade(&app_state), cx); - auto_update::init(client.http_client(), cx); + auto_update::init(client.clone(), cx); dap_adapters::init(cx); auto_update_ui::init(cx); reliability::init( diff --git a/script/bundle-mac b/script/bundle-mac index c647424d7ee657f6ca3e94c3cb94957fcf50ad98..248cb10203a16299e33b1d997aeee8cfca46250e 100755 --- a/script/bundle-mac +++ b/script/bundle-mac @@ -22,7 +22,7 @@ Build the application bundle for macOS. Options: -d Compile in debug mode -o Open dir with the resulting DMG or launch the app itself in local mode. - -i Install the resulting DMG into /Applications in local mode. Noop without -l. + -i Install the resulting DMG into /Applications. -h Display this help and exit. " } @@ -209,16 +209,6 @@ function sign_app_binaries() { codesign --force --deep --entitlements "${app_path}/Contents/Resources/zed.entitlements" --sign ${MACOS_SIGNING_KEY:- -} "${app_path}" -v fi - if [[ "$target_dir" = "debug" ]]; then - if [ "$open_result" = true ]; then - open "$app_path" - else - echo "Created application bundle:" - echo "$app_path" - fi - exit 0 - fi - bundle_name=$(basename "$app_path") if [ "$local_install" = true ]; then @@ -229,6 +219,16 @@ function sign_app_binaries() { echo "Opening /Applications/$bundle_name" open "/Applications/$bundle_name" fi + elif [ "$open_result" = true ]; then + open "$app_path" + fi + + if [[ "$target_dir" = "debug" ]]; then + echo "Debug build detected - skipping DMG creation and signing" + if [ "$local_install" = false ]; then + echo "Created application bundle:" + echo "$app_path" + fi else dmg_target_directory="target/${target_triple}/${target_dir}" dmg_source_directory="${dmg_target_directory}/dmg"