@@ -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 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<Self>,
+ release_channel: ReleaseChannel,
+ version: Option<SemanticVersion>,
asset: &str,
os: &str,
arch: &str,
- 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),
- })
+ ) -> 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 {
- 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<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
+ 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;