Files
oak-gpui/crates/auto_update/src/auto_update.rs
T

1379 lines
45 KiB
Rust

use anyhow::{Context as _, Result};
use client::Client;
use db::kvp::KEY_VALUE_STORE;
use gpui::{
App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, Task, Window,
actions,
};
use http_client::{HttpClient, HttpClientWithUrl};
use paths::remote_servers_dir;
use release_channel::{AppCommitSha, ReleaseChannel};
use semver::Version;
use serde::{Deserialize, Serialize};
use settings::{RegisterSetting, Settings, SettingsStore};
use smol::fs::File;
use smol::{fs, io::AsyncReadExt};
use std::mem;
use std::{
env::{
self,
consts::{ARCH, OS},
},
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use util::command::new_smol_command;
use workspace::Workspace;
const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
actions!(
auto_update,
[
/// Checks for available updates.
Check,
/// Dismisses the update error message.
DismissMessage,
/// Opens the release notes for the current version in a browser.
ViewReleaseNotes,
]
);
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum VersionCheckType {
Sha(AppCommitSha),
Semantic(Version),
}
#[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,
Downloading { version: VersionCheckType },
Installing { version: VersionCheckType },
Updated { version: VersionCheckType },
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 { .. })
}
}
pub struct AutoUpdater {
status: AutoUpdateStatus,
current_version: Version,
client: Arc<Client>,
pending_poll: Option<Task<Option<()>>>,
quit_subscription: Option<gpui::Subscription>,
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct ReleaseAsset {
pub version: String,
pub url: String,
}
struct MacOsUnmounter<'a> {
mount_path: PathBuf,
background_executor: &'a BackgroundExecutor,
}
impl Drop for MacOsUnmounter<'_> {
fn drop(&mut self) {
let mount_path = mem::take(&mut self.mount_path);
self.background_executor
.spawn(async move {
let unmount_output = new_smol_command("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(Clone, Copy, Debug, RegisterSetting)]
struct AutoUpdateSetting(bool);
/// Whether or not to automatically check for updates.
///
/// Default: true
impl Settings for AutoUpdateSetting {
fn from_settings(content: &settings::SettingsContent) -> Self {
Self(content.auto_update.unwrap())
}
}
#[derive(Default)]
struct GlobalAutoUpdate(Option<Entity<AutoUpdater>>);
impl Global for GlobalAutoUpdate {}
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));
workspace.register_action(|_, action, _, cx| {
view_release_notes(action, cx);
});
})
.detach();
let version = release_channel::AppVersion::global(cx);
let auto_updater = cx.new(|cx| {
let updater = AutoUpdater::new(version, client, cx);
let poll_for_updates = ReleaseChannel::try_global(cx)
.map(|channel| channel.poll_for_updates())
.unwrap_or(false);
if option_env!("ZED_UPDATE_EXPLANATION").is_none()
&& env::var("ZED_UPDATE_EXPLANATION").is_err()
&& poll_for_updates
{
let mut update_subscription = AutoUpdateSetting::get_global(cx)
.0
.then(|| updater.start_polling(cx));
cx.observe_global::<SettingsStore>(move |updater: &mut AutoUpdater, cx| {
if AutoUpdateSetting::get_global(cx).0 {
if update_subscription.is_none() {
update_subscription = Some(updater.start_polling(cx))
}
} else {
update_subscription.take();
}
})
.detach();
}
updater
});
cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
}
pub fn check(_: &Check, window: &mut Window, cx: &mut App) {
if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {
drop(window.prompt(
gpui::PromptLevel::Info,
"Zed was installed via a package manager.",
Some(message),
&["Ok"],
cx,
));
return;
}
if let Ok(message) = env::var("ZED_UPDATE_EXPLANATION") {
drop(window.prompt(
gpui::PromptLevel::Info,
"Zed was installed via a package manager.",
Some(&message),
&["Ok"],
cx,
));
return;
}
if !ReleaseChannel::try_global(cx)
.map(|channel| channel.poll_for_updates())
.unwrap_or(false)
{
return;
}
if let Some(updater) = AutoUpdater::get(cx) {
updater.update(cx, |updater, cx| updater.poll(UpdateCheckType::Manual, cx));
} else {
drop(window.prompt(
gpui::PromptLevel::Info,
"Could not check for updates",
Some("Auto-updates disabled for non-bundled app."),
&["Ok"],
cx,
));
}
}
pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut App) -> Option<()> {
let auto_updater = AutoUpdater::get(cx)?;
let release_channel = ReleaseChannel::try_global(cx)?;
match release_channel {
ReleaseChannel::Stable | ReleaseChannel::Preview => {
let auto_updater = auto_updater.read(cx);
let current_version = auto_updater.current_version.clone();
let release_channel = release_channel.dev_name();
let path = format!("/releases/{release_channel}/{current_version}");
let url = &auto_updater.client.http_client().build_url(&path);
cx.open_url(url);
}
ReleaseChannel::Nightly => {
cx.open_url("https://github.com/zed-industries/zed/commits/nightly/");
}
ReleaseChannel::Dev => {
cx.open_url("https://github.com/zed-industries/zed/commits/main/");
}
}
None
}
#[cfg(not(target_os = "windows"))]
struct InstallerDir(tempfile::TempDir);
#[cfg(not(target_os = "windows"))]
impl InstallerDir {
async fn new() -> Result<Self> {
Ok(Self(
tempfile::Builder::new()
.prefix("zed-auto-update")
.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> {
let installer_dir = std::env::current_exe()?
.parent()
.context("No parent dir for Zed.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()
}
}
pub enum UpdateCheckType {
Automatic,
Manual,
}
impl AutoUpdater {
pub fn get(cx: &mut App) -> Option<Entity<Self>> {
cx.default_global::<GlobalAutoUpdate>().0.clone()
}
fn new(current_version: Version, 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
// to finish the auto update process after Zed exits. When restarting
// the app after an update, we use `set_restart_path` to run the auto
// update helper instead of the app, so that it can overwrite the app
// and then spawn the new binary.
#[cfg(target_os = "windows")]
let quit_subscription = Some(cx.on_app_quit(|_, _| finalize_auto_update_on_quit()));
#[cfg(not(target_os = "windows"))]
let quit_subscription = None;
cx.on_app_restart(|this, _| {
this.quit_subscription.take();
})
.detach();
Self {
status: AutoUpdateStatus::Idle,
current_version,
client,
pending_poll: None,
quit_subscription,
}
}
pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
cx.spawn(async move |this, cx| {
if cfg!(target_os = "windows") {
use util::ResultExt;
cleanup_windows()
.await
.context("failed to cleanup old directories")
.log_err();
}
loop {
this.update(cx, |this, cx| this.poll(UpdateCheckType::Automatic, cx))?;
cx.background_executor().timer(POLL_INTERVAL).await;
}
})
}
pub fn poll(&mut self, check_type: UpdateCheckType, cx: &mut Context<Self>) {
if self.pending_poll.is_some() {
return;
}
cx.notify();
self.pending_poll = Some(cx.spawn(async move |this, cx| {
let result = Self::update(this.upgrade()?, cx).await;
this.update(cx, |this, cx| {
this.pending_poll = None;
if let Err(error) = result {
this.status = match check_type {
// Be quiet if the check was automated (e.g. when offline)
UpdateCheckType::Automatic => {
log::info!("auto-update check failed: error:{:?}", error);
AutoUpdateStatus::Idle
}
UpdateCheckType::Manual => {
log::error!("auto-update failed: error:{:?}", error);
AutoUpdateStatus::Errored {
error: Arc::new(error),
}
}
};
cx.notify();
}
})
.ok()
}));
}
pub fn current_version(&self) -> Version {
self.current_version.clone()
}
pub fn status(&self) -> AutoUpdateStatus {
self.status.clone()
}
pub fn dismiss(&mut self, cx: &mut Context<Self>) -> bool {
if let AutoUpdateStatus::Idle = self.status {
return false;
}
self.status = AutoUpdateStatus::Idle;
cx.notify();
true
}
// If you are packaging Zed and need to override the place it downloads SSH remotes from,
// 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(
release_channel: ReleaseChannel,
version: Option<Version>,
os: &str,
arch: &str,
set_status: impl Fn(&str, &mut AsyncApp) + Send + 'static,
cx: &mut AsyncApp,
) -> Result<PathBuf> {
let this = cx.update(|cx| {
cx.default_global::<GlobalAutoUpdate>()
.0
.clone()
.context("auto-update not initialized")
})??;
set_status("Fetching remote server release", cx);
let release = Self::get_release_asset(
&this,
release_channel,
version,
"zed-remote-server",
os,
arch,
cx,
)
.await?;
let servers_dir = paths::remote_servers_dir();
let channel_dir = servers_dir.join(release_channel.dev_name());
let platform_dir = channel_dir.join(format!("{}-{}", os, arch));
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.client.http_client())?;
if smol::fs::metadata(&version_path).await.is_err() {
log::info!(
"downloading zed-remote-server {os} {arch} version {}",
release.version
);
set_status("Downloading remote server", cx);
download_remote_server_binary(&version_path, release, client).await?;
}
Ok(version_path)
}
pub async fn get_remote_server_release_url(
channel: ReleaseChannel,
version: Option<Version>,
os: &str,
arch: &str,
cx: &mut AsyncApp,
) -> Result<Option<String>> {
let this = cx.update(|cx| {
cx.default_global::<GlobalAutoUpdate>()
.0
.clone()
.context("auto-update not initialized")
})??;
let release =
Self::get_release_asset(&this, channel, version, "zed-remote-server", os, arch, cx)
.await?;
Ok(Some(release.url))
}
async fn get_release_asset(
this: &Entity<Self>,
release_channel: ReleaseChannel,
version: Option<Version>,
asset: &str,
os: &str,
arch: &str,
cx: &mut AsyncApp,
) -> 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(mut version) = version {
version.pre = semver::Prerelease::EMPTY;
version.build = semver::BuildMetadata::EMPTY;
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>, cx: &mut AsyncApp) -> Result<()> {
let (client, installed_version, previous_status, release_channel) =
this.read_with(cx, |this, cx| {
(
this.client.http_client(),
this.current_version.clone(),
this.status.clone(),
ReleaseChannel::try_global(cx).unwrap_or(ReleaseChannel::Stable),
)
})?;
Self::check_dependencies()?;
this.update(cx, |this, cx| {
this.status = AutoUpdateStatus::Checking;
log::info!("Auto Update: checking for updates");
cx.notify();
})?;
let fetched_release_data =
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,
app_commit_sha,
installed_version,
fetched_version,
previous_status.clone(),
)?;
let Some(newer_version) = newer_version else {
return this.update(cx, |this, cx| {
let status = match previous_status {
AutoUpdateStatus::Updated { .. } => previous_status,
_ => AutoUpdateStatus::Idle,
};
this.status = status;
cx.notify();
});
};
this.update(cx, |this, cx| {
this.status = AutoUpdateStatus::Downloading {
version: newer_version.clone(),
};
cx.notify();
})?;
let installer_dir = InstallerDir::new().await?;
let target_path = Self::target_path(&installer_dir).await?;
download_release(&target_path, fetched_release_data, client).await?;
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?;
if let Some(new_binary_path) = new_binary_path {
cx.update(|cx| cx.set_restart_path(new_binary_path))?;
}
this.update(cx, |this, cx| {
this.set_should_show_update_notification(true, cx)
.detach_and_log_err(cx);
this.status = AutoUpdateStatus::Updated {
version: newer_version,
};
cx.notify();
})
}
fn check_if_fetched_version_is_newer(
release_channel: ReleaseChannel,
app_commit_sha: Result<Option<String>>,
installed_version: Version,
fetched_version: String,
status: AutoUpdateStatus,
) -> Result<Option<VersionCheckType>> {
let parsed_fetched_version = fetched_version.parse::<Version>();
if let AutoUpdateStatus::Updated { version, .. } = status {
match version {
VersionCheckType::Sha(cached_version) => {
let should_download =
parsed_fetched_version.as_ref().ok().is_none_or(|version| {
version.build.as_str().rsplit('.').next()
!= Some(&cached_version.full())
});
let newer_version = should_download
.then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
return Ok(newer_version);
}
VersionCheckType::Semantic(cached_version) => {
return Self::check_if_fetched_version_is_newer_non_nightly(
cached_version,
parsed_fetched_version?,
);
}
}
}
match release_channel {
ReleaseChannel::Nightly => {
let should_download = app_commit_sha
.ok()
.flatten()
.map(|sha| {
parsed_fetched_version.as_ref().ok().is_none_or(|version| {
version.build.as_str().rsplit('.').next() != Some(&sha)
})
})
.unwrap_or(true);
let newer_version = should_download
.then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
Ok(newer_version)
}
_ => Self::check_if_fetched_version_is_newer_non_nightly(
installed_version,
parsed_fetched_version?,
),
}
}
fn check_dependencies() -> Result<()> {
#[cfg(not(target_os = "windows"))]
anyhow::ensure!(
which::which("rsync").is_ok(),
"Could not auto-update because the required rsync utility was not found."
);
Ok(())
}
async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf> {
let filename = match OS {
"macos" => anyhow::Ok("Zed.dmg"),
"linux" => Ok("zed.tar.gz"),
"windows" => Ok("Zed.exe"),
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
}?;
Ok(installer_dir.path().join(filename))
}
async fn install_release(
installer_dir: InstallerDir,
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,
"windows" => install_release_windows(target_path).await,
unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
}
}
fn check_if_fetched_version_is_newer_non_nightly(
mut installed_version: Version,
fetched_version: Version,
) -> Result<Option<VersionCheckType>> {
// For non-nightly releases, ignore build and pre-release fields as they're not provided by our endpoints right now.
installed_version.build = semver::BuildMetadata::EMPTY;
installed_version.pre = semver::Prerelease::EMPTY;
let should_download = fetched_version > installed_version;
let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
Ok(newer_version)
}
pub fn set_should_show_update_notification(
&self,
should_show: bool,
cx: &App,
) -> Task<Result<()>> {
cx.background_spawn(async move {
if should_show {
KEY_VALUE_STORE
.write_kvp(
SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
"".to_string(),
)
.await?;
} else {
KEY_VALUE_STORE
.delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
.await?;
}
Ok(())
})
}
pub fn should_show_update_notification(&self, cx: &App) -> Task<Result<bool>> {
cx.background_spawn(async move {
Ok(KEY_VALUE_STORE
.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
.is_some())
})
}
}
async fn download_remote_server_binary(
target_path: &PathBuf,
release: ReleaseAsset,
client: Arc<HttpClientWithUrl>,
) -> Result<()> {
let temp = tempfile::Builder::new().tempfile_in(remote_servers_dir())?;
let mut temp_file = File::create(&temp).await?;
let mut response = client.get(&release.url, Default::default(), true).await?;
anyhow::ensure!(
response.status().is_success(),
"failed to download remote server release: {:?}",
response.status()
);
smol::io::copy(response.body_mut(), &mut temp_file).await?;
smol::fs::rename(&temp, &target_path).await?;
Ok(())
}
async fn download_release(
target_path: &Path,
release: ReleaseAsset,
client: Arc<HttpClientWithUrl>,
) -> Result<()> {
let mut target_file = File::create(&target_path).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);
Ok(())
}
async fn install_release_linux(
temp_dir: &InstallerDir,
downloaded_tar_gz: PathBuf,
cx: &AsyncApp,
) -> Result<Option<PathBuf>> {
let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
let running_app_path = cx.update(|cx| cx.app_path())??;
let extracted = temp_dir.path().join("zed");
fs::create_dir_all(&extracted)
.await
.context("failed to create directory into which to extract update")?;
let output = new_smol_command("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)
);
let suffix = if channel != "stable" {
format!("-{}", channel)
} else {
String::default()
};
let app_folder_name = format!("zed{}.app", suffix);
let from = extracted.join(&app_folder_name);
let mut to = home_dir.join(".local");
let expected_suffix = format!("{}/libexec/zed-editor", app_folder_name);
if let Some(prefix) = running_app_path
.to_str()
.and_then(|str| str.strip_suffix(&expected_suffix))
{
to = PathBuf::from(prefix);
}
let output = new_smol_command("rsync")
.args(["-av", "--delete"])
.arg(&from)
.arg(&to)
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to copy Zed update from {:?} to {:?}: {:?}",
from,
to,
String::from_utf8_lossy(&output.stderr)
);
Ok(Some(to.join(expected_suffix)))
}
async fn install_release_macos(
temp_dir: &InstallerDir,
downloaded_dmg: PathBuf,
cx: &AsyncApp,
) -> Result<Option<PathBuf>> {
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("Zed");
let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
mounted_app_path.push("/");
let output = new_smol_command("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 = new_smol_command("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(None)
}
async fn cleanup_windows() -> Result<()> {
let parent = std::env::current_exe()?
.parent()
.context("No parent dir for Zed.exe")?
.to_owned();
// keep in sync with crates/auto_update_helper/src/updater.rs
_ = smol::fs::remove_dir(parent.join("updates")).await;
_ = smol::fs::remove_dir(parent.join("install")).await;
_ = smol::fs::remove_dir(parent.join("old")).await;
Ok(())
}
async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option<PathBuf>> {
let output = new_smol_command(downloaded_installer)
.arg("/verysilent")
.arg("/update=true")
.arg("!desktopicon")
.arg("!quicklaunchicon")
.output()
.await?;
anyhow::ensure!(
output.status.success(),
"failed to start installer: {:?}",
String::from_utf8_lossy(&output.stderr)
);
// We return the path to the update helper program, because it will
// perform the final steps of the update process, copying the new binary,
// deleting the old one, and launching the new binary.
let helper_path = std::env::current_exe()?
.parent()
.context("No parent dir for Zed.exe")?
.join("tools")
.join("auto_update_helper.exe");
Ok(Some(helper_path))
}
pub async fn finalize_auto_update_on_quit() {
let Some(installer_path) = std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|p| p.join("updates")))
else {
return;
};
// The installer will create a flag file after it finishes updating
let flag_file = installer_path.join("versions.txt");
if flag_file.exists()
&& let Some(helper) = installer_path
.parent()
.map(|p| p.join("tools").join("auto_update_helper.exe"))
{
let mut command = util::command::new_smol_command(helper);
command.arg("--launch");
command.arg("false");
if let Ok(mut cmd) = command.spawn() {
_ = cmd.status().await;
}
}
}
#[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| {
let mut store = SettingsStore::new(cx, &settings::default_settings());
store
.set_default_settings(&default_settings(), cx)
.expect("Unable to set default settings");
store
.set_user_settings("{}", cx)
.expect("Unable to set user settings");
cx.set_global(store);
assert!(AutoUpdateSetting::get_global(cx).0);
});
}
#[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 = semver::Version::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(), semver::Version::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(semver::Version::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(semver::Version::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;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_version = semver::Version::new(1, 0, 0);
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
app_commit_sha,
installed_version,
fetched_version.to_string(),
status,
);
assert_eq!(newer_version.unwrap(), None);
}
#[test]
fn test_stable_does_update_when_fetched_version_is_higher() {
let release_channel = ReleaseChannel::Stable;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_version = semver::Version::new(1, 0, 1);
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
app_commit_sha,
installed_version,
fetched_version.to_string(),
status,
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Semantic(fetched_version))
);
}
#[test]
fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
let release_channel = ReleaseChannel::Stable;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
version: VersionCheckType::Semantic(semver::Version::new(1, 0, 1)),
};
let fetched_version = semver::Version::new(1, 0, 1);
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
app_commit_sha,
installed_version,
fetched_version.to_string(),
status,
);
assert_eq!(newer_version.unwrap(), None);
}
#[test]
fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
let release_channel = ReleaseChannel::Stable;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
version: VersionCheckType::Semantic(semver::Version::new(1, 0, 1)),
};
let fetched_version = semver::Version::new(1, 0, 2);
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
app_commit_sha,
installed_version,
fetched_version.to_string(),
status,
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Semantic(fetched_version))
);
}
#[test]
fn test_nightly_does_not_update_when_fetched_sha_is_same() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let mut installed_version = semver::Version::new(1, 0, 0);
installed_version.build = semver::BuildMetadata::new("a").unwrap();
let status = AutoUpdateStatus::Idle;
let fetched_sha = "1.0.0+a".to_string();
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
app_commit_sha,
installed_version,
fetched_sha,
status,
);
assert_eq!(newer_version.unwrap(), None);
}
#[test]
fn test_nightly_does_update_when_fetched_sha_is_not_same() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_sha = "b".to_string();
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
app_commit_sha,
installed_version,
fetched_sha.clone(),
status,
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
);
}
#[test]
fn test_nightly_does_not_update_when_fetched_version_is_same_as_cached() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let mut installed_version = semver::Version::new(1, 0, 0);
installed_version.build = semver::BuildMetadata::new("a").unwrap();
let status = AutoUpdateStatus::Updated {
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
};
let fetched_sha = "1.0.0+b".to_string();
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
app_commit_sha,
installed_version,
fetched_sha,
status,
);
assert_eq!(newer_version.unwrap(), None);
}
#[test]
fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(Some("a".to_string()));
let mut installed_version = semver::Version::new(1, 0, 0);
installed_version.build = semver::BuildMetadata::new("a").unwrap();
let status = AutoUpdateStatus::Updated {
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
};
let fetched_sha = "1.0.0+c".to_string();
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
app_commit_sha,
installed_version,
fetched_sha.clone(),
status,
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
);
}
#[test]
fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(None);
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Idle;
let fetched_sha = "a".to_string();
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
app_commit_sha,
installed_version,
fetched_sha.clone(),
status,
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
);
}
#[test]
fn test_nightly_does_not_update_when_cached_update_is_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
{
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(None);
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
};
let fetched_sha = "1.0.0+b".to_string();
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
app_commit_sha,
installed_version,
fetched_sha,
status,
);
assert_eq!(newer_version.unwrap(), None);
}
#[test]
fn test_nightly_does_update_when_cached_update_is_not_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
{
let release_channel = ReleaseChannel::Nightly;
let app_commit_sha = Ok(None);
let installed_version = semver::Version::new(1, 0, 0);
let status = AutoUpdateStatus::Updated {
version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
};
let fetched_sha = "c".to_string();
let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
release_channel,
app_commit_sha,
installed_version,
fetched_sha.clone(),
status,
);
assert_eq!(
newer_version.unwrap(),
Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
);
}
}