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.
This commit is contained in:
Conrad Irwin 2025-11-10 23:00:55 -07:00 committed by GitHub
parent 823844ef18
commit 9e717c7711
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 354 additions and 226 deletions

6
Cargo.lock generated
View file

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

View file

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

View file

@ -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<Arc<str>>,
release_channel: Option<&'static str>,
telemetry: bool,
is_staff: Option<bool>,
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<bool>,
}
#[derive(Clone, Debug)]
pub enum AutoUpdateStatus {
Idle,
Checking,
@ -66,6 +66,31 @@ pub enum AutoUpdateStatus {
Errored { error: Arc<anyhow::Error> },
}
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<HttpClientWithUrl>,
client: Arc<Client>,
pending_poll: Option<Task<Option<()>>>,
quit_subscription: Option<gpui::Subscription>,
}
#[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<Entity<AutoUpdater>>);
impl Global for GlobalAutoUpdate {}
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
pub fn init(client: Arc<Client>, 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<HttpClientWithUrl>, 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::<GlobalAutoUpdate>().0.clone()
}
fn new(
current_version: SemanticVersion,
http_client: Arc<HttpClientWithUrl>,
cx: &mut Context<Self>,
) -> Self {
fn new(current_version: SemanticVersion, client: Arc<Client>, cx: &mut Context<Self>) -> 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<SemanticVersion>,
os: &str,
arch: &str,
set_status: impl Fn(&str, &mut AsyncApp) + Send + 'static,
cx: &mut AsyncApp,
) -> Result<PathBuf> {
@ -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<SemanticVersion>,
os: &str,
arch: &str,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncApp,
) -> Result<Option<(String, String)>> {
) -> Result<Option<String>> {
let this = cx.update(|cx| {
cx.default_global::<GlobalAutoUpdate>()
.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 release =
Self::get_release_asset(&this, channel, version, "zed-remote-server", os, arch, cx)
.await?;
let update_request_body = build_remote_server_update_request_body(cx)?;
let body = serde_json::to_string(&update_request_body)?;
Ok(Some((release.url, body)))
Ok(Some(release.url))
}
async fn get_release(
async fn get_release_asset(
this: &Entity<Self>,
asset: &str,
os: &str,
arch: &str,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
release_channel: Option<ReleaseChannel>,
cx: &mut AsyncApp,
) -> Result<JsonRelease> {
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),
})
} 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;
}
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?;
anyhow::ensure!(
response.status().is_success(),
"failed to fetch release: {:?}",
String::from_utf8_lossy(&body),
);
serde_json::from_slice(body.as_slice()).with_context(|| {
format!(
"error deserializing release {:?}",
String::from_utf8_lossy(&body),
)
})
}
}
async fn get_latest_release(
this: &Entity<Self>,
asset: &str,
os: &str,
arch: &str,
release_channel: Option<ReleaseChannel>,
cx: &mut AsyncApp,
) -> Result<JsonRelease> {
Self::get_release(this, asset, os, arch, None, release_channel, cx).await
) -> Result<ReleaseAsset> {
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 {
(None, None, None)
};
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,
},
)?;
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?;
anyhow::ensure!(
response.status().is_success(),
"failed to fetch release: {:?}",
String::from_utf8_lossy(&body),
);
serde_json::from_slice(body.as_slice()).with_context(|| {
format!(
"error deserializing release {:?}",
String::from_utf8_lossy(&body),
)
})
}
async fn update(this: Entity<Self>, mut cx: AsyncApp) -> Result<()> {
async fn update(this: Entity<Self>, 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<Option<PathBuf>> {
#[cfg(test)]
if let Some(test_install) =
cx.try_read_global::<tests::InstallOverride, _>(|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<HttpClientWithUrl>,
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<UpdateRequestBody> {
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<HttpClientWithUrl>,
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<dyn Fn(PathBuf, &AsyncApp) -> Result<Option<PathBuf>>>,
);
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::<String>();
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("<fake-zed-update>".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(), "<fake-zed-update>");
}
#[test]
fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
let release_channel = ReleaseChannel::Stable;

View file

@ -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}"))

View file

@ -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<Task<Result<Connection>>> {
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(),

View file

@ -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<Option<PathBuf>> {
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<TestScreenCaptureSource>) {

View file

@ -36,6 +36,7 @@ pub(crate) struct TestPlatform {
screen_capture_sources: RefCell<Vec<TestScreenCaptureSource>>,
pub opened_url: RefCell<Option<String>>,
pub text_system: Arc<dyn PlatformTextSystem>,
pub expect_restart: RefCell<Option<oneshot::Sender<Option<PathBuf>>>>,
#[cfg(target_os = "windows")]
bitmap_factory: std::mem::ManuallyDrop<IWICImagingFactory>,
weak: Weak<Self>,
@ -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<PathBuf>) {
//
fn restart(&self, path: Option<PathBuf>) {
if let Some(tx) = self.expect_restart.take() {
tx.send(path).unwrap();
}
}
fn activate(&self, _ignoring_other_apps: bool) {

View file

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

View file

@ -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<Url> {
pub fn build_zed_cloud_url(&self, path: &str) -> Result<Url> {
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<Url> {
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.

View file

@ -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<SemanticVersion>,
cx: &mut AsyncApp,
) -> Task<Result<Option<(String, String)>>> {
) -> Task<Result<Option<String>>> {
cx.spawn(async move |cx| {
AutoUpdater::get_remote_server_release_url(
platform.os,
platform.arch,
release_channel,
version,
platform.os,
platform.arch,
cx,
)
.await

View file

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

View file

@ -67,13 +67,13 @@ pub trait RemoteClientDelegate: Send + Sync {
tx: oneshot::Sender<EncryptedPassword>,
cx: &mut AsyncApp,
);
fn get_download_params(
fn get_download_url(
&self,
platform: RemotePlatform,
release_channel: ReleaseChannel,
version: Option<SemanticVersion>,
cx: &mut AsyncApp,
) -> Task<Result<Option<(String, String)>>>;
) -> Task<Result<Option<String>>>;
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<SemanticVersion>,
_cx: &mut AsyncApp,
) -> Task<Result<Option<(String, String)>>> {
) -> Task<Result<Option<String>>> {
unreachable!()
}

View file

@ -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<dyn RemoteClientDelegate>,
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

View file

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

View file

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