auto_update.rs

   1use anyhow::{Context as _, Result};
   2use client::Client;
   3use db::kvp::KEY_VALUE_STORE;
   4use gpui::{
   5    App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, SemanticVersion,
   6    Task, Window, actions,
   7};
   8use http_client::{HttpClient, HttpClientWithUrl};
   9use paths::remote_servers_dir;
  10use release_channel::{AppCommitSha, ReleaseChannel};
  11use serde::{Deserialize, Serialize};
  12use settings::{RegisterSetting, Settings, SettingsStore};
  13use smol::{fs, io::AsyncReadExt};
  14use smol::{fs::File, process::Command};
  15use std::mem;
  16use std::{
  17    env::{
  18        self,
  19        consts::{ARCH, OS},
  20    },
  21    ffi::OsString,
  22    path::{Path, PathBuf},
  23    sync::Arc,
  24    time::Duration,
  25};
  26use workspace::Workspace;
  27
  28const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
  29const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
  30
  31actions!(
  32    auto_update,
  33    [
  34        /// Checks for available updates.
  35        Check,
  36        /// Dismisses the update error message.
  37        DismissMessage,
  38        /// Opens the release notes for the current version in a browser.
  39        ViewReleaseNotes,
  40    ]
  41);
  42
  43#[derive(Clone, Debug, PartialEq, Eq)]
  44pub enum VersionCheckType {
  45    Sha(AppCommitSha),
  46    Semantic(SemanticVersion),
  47}
  48
  49#[derive(Serialize, Debug)]
  50pub struct AssetQuery<'a> {
  51    asset: &'a str,
  52    os: &'a str,
  53    arch: &'a str,
  54    metrics_id: Option<&'a str>,
  55    system_id: Option<&'a str>,
  56    is_staff: Option<bool>,
  57}
  58
  59#[derive(Clone, Debug)]
  60pub enum AutoUpdateStatus {
  61    Idle,
  62    Checking,
  63    Downloading { version: VersionCheckType },
  64    Installing { version: VersionCheckType },
  65    Updated { version: VersionCheckType },
  66    Errored { error: Arc<anyhow::Error> },
  67}
  68
  69impl PartialEq for AutoUpdateStatus {
  70    fn eq(&self, other: &Self) -> bool {
  71        match (self, other) {
  72            (AutoUpdateStatus::Idle, AutoUpdateStatus::Idle) => true,
  73            (AutoUpdateStatus::Checking, AutoUpdateStatus::Checking) => true,
  74            (
  75                AutoUpdateStatus::Downloading { version: v1 },
  76                AutoUpdateStatus::Downloading { version: v2 },
  77            ) => v1 == v2,
  78            (
  79                AutoUpdateStatus::Installing { version: v1 },
  80                AutoUpdateStatus::Installing { version: v2 },
  81            ) => v1 == v2,
  82            (
  83                AutoUpdateStatus::Updated { version: v1 },
  84                AutoUpdateStatus::Updated { version: v2 },
  85            ) => v1 == v2,
  86            (AutoUpdateStatus::Errored { error: e1 }, AutoUpdateStatus::Errored { error: e2 }) => {
  87                e1.to_string() == e2.to_string()
  88            }
  89            _ => false,
  90        }
  91    }
  92}
  93
  94impl AutoUpdateStatus {
  95    pub fn is_updated(&self) -> bool {
  96        matches!(self, Self::Updated { .. })
  97    }
  98}
  99
 100pub struct AutoUpdater {
 101    status: AutoUpdateStatus,
 102    current_version: SemanticVersion,
 103    client: Arc<Client>,
 104    pending_poll: Option<Task<Option<()>>>,
 105    quit_subscription: Option<gpui::Subscription>,
 106}
 107
 108#[derive(Deserialize, Serialize, Clone, Debug)]
 109pub struct ReleaseAsset {
 110    pub version: String,
 111    pub url: String,
 112}
 113
 114struct MacOsUnmounter<'a> {
 115    mount_path: PathBuf,
 116    background_executor: &'a BackgroundExecutor,
 117}
 118
 119impl Drop for MacOsUnmounter<'_> {
 120    fn drop(&mut self) {
 121        let mount_path = mem::take(&mut self.mount_path);
 122        self.background_executor
 123            .spawn(async move {
 124                let unmount_output = Command::new("hdiutil")
 125                    .args(["detach", "-force"])
 126                    .arg(&mount_path)
 127                    .output()
 128                    .await;
 129                match unmount_output {
 130                    Ok(output) if output.status.success() => {
 131                        log::info!("Successfully unmounted the disk image");
 132                    }
 133                    Ok(output) => {
 134                        log::error!(
 135                            "Failed to unmount disk image: {:?}",
 136                            String::from_utf8_lossy(&output.stderr)
 137                        );
 138                    }
 139                    Err(error) => {
 140                        log::error!("Error while trying to unmount disk image: {:?}", error);
 141                    }
 142                }
 143            })
 144            .detach();
 145    }
 146}
 147
 148#[derive(Clone, Copy, Debug, RegisterSetting)]
 149struct AutoUpdateSetting(bool);
 150
 151/// Whether or not to automatically check for updates.
 152///
 153/// Default: true
 154impl Settings for AutoUpdateSetting {
 155    fn from_settings(content: &settings::SettingsContent) -> Self {
 156        Self(content.auto_update.unwrap())
 157    }
 158}
 159
 160#[derive(Default)]
 161struct GlobalAutoUpdate(Option<Entity<AutoUpdater>>);
 162
 163impl Global for GlobalAutoUpdate {}
 164
 165pub fn init(client: Arc<Client>, cx: &mut App) {
 166    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
 167        workspace.register_action(|_, action, window, cx| check(action, window, cx));
 168
 169        workspace.register_action(|_, action, _, cx| {
 170            view_release_notes(action, cx);
 171        });
 172    })
 173    .detach();
 174
 175    let version = release_channel::AppVersion::global(cx);
 176    let auto_updater = cx.new(|cx| {
 177        let updater = AutoUpdater::new(version, client, cx);
 178
 179        let poll_for_updates = ReleaseChannel::try_global(cx)
 180            .map(|channel| channel.poll_for_updates())
 181            .unwrap_or(false);
 182
 183        if option_env!("ZED_UPDATE_EXPLANATION").is_none()
 184            && env::var("ZED_UPDATE_EXPLANATION").is_err()
 185            && poll_for_updates
 186        {
 187            let mut update_subscription = AutoUpdateSetting::get_global(cx)
 188                .0
 189                .then(|| updater.start_polling(cx));
 190
 191            cx.observe_global::<SettingsStore>(move |updater: &mut AutoUpdater, cx| {
 192                if AutoUpdateSetting::get_global(cx).0 {
 193                    if update_subscription.is_none() {
 194                        update_subscription = Some(updater.start_polling(cx))
 195                    }
 196                } else {
 197                    update_subscription.take();
 198                }
 199            })
 200            .detach();
 201        }
 202
 203        updater
 204    });
 205    cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
 206}
 207
 208pub fn check(_: &Check, window: &mut Window, cx: &mut App) {
 209    if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {
 210        drop(window.prompt(
 211            gpui::PromptLevel::Info,
 212            "Zed was installed via a package manager.",
 213            Some(message),
 214            &["Ok"],
 215            cx,
 216        ));
 217        return;
 218    }
 219
 220    if let Ok(message) = env::var("ZED_UPDATE_EXPLANATION") {
 221        drop(window.prompt(
 222            gpui::PromptLevel::Info,
 223            "Zed was installed via a package manager.",
 224            Some(&message),
 225            &["Ok"],
 226            cx,
 227        ));
 228        return;
 229    }
 230
 231    if !ReleaseChannel::try_global(cx)
 232        .map(|channel| channel.poll_for_updates())
 233        .unwrap_or(false)
 234    {
 235        return;
 236    }
 237
 238    if let Some(updater) = AutoUpdater::get(cx) {
 239        updater.update(cx, |updater, cx| updater.poll(UpdateCheckType::Manual, cx));
 240    } else {
 241        drop(window.prompt(
 242            gpui::PromptLevel::Info,
 243            "Could not check for updates",
 244            Some("Auto-updates disabled for non-bundled app."),
 245            &["Ok"],
 246            cx,
 247        ));
 248    }
 249}
 250
 251pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut App) -> Option<()> {
 252    let auto_updater = AutoUpdater::get(cx)?;
 253    let release_channel = ReleaseChannel::try_global(cx)?;
 254
 255    match release_channel {
 256        ReleaseChannel::Stable | ReleaseChannel::Preview => {
 257            let auto_updater = auto_updater.read(cx);
 258            let current_version = auto_updater.current_version;
 259            let release_channel = release_channel.dev_name();
 260            let path = format!("/releases/{release_channel}/{current_version}");
 261            let url = &auto_updater.client.http_client().build_url(&path);
 262            cx.open_url(url);
 263        }
 264        ReleaseChannel::Nightly => {
 265            cx.open_url("https://github.com/zed-industries/zed/commits/nightly/");
 266        }
 267        ReleaseChannel::Dev => {
 268            cx.open_url("https://github.com/zed-industries/zed/commits/main/");
 269        }
 270    }
 271    None
 272}
 273
 274#[cfg(not(target_os = "windows"))]
 275struct InstallerDir(tempfile::TempDir);
 276
 277#[cfg(not(target_os = "windows"))]
 278impl InstallerDir {
 279    async fn new() -> Result<Self> {
 280        Ok(Self(
 281            tempfile::Builder::new()
 282                .prefix("zed-auto-update")
 283                .tempdir()?,
 284        ))
 285    }
 286
 287    fn path(&self) -> &Path {
 288        self.0.path()
 289    }
 290}
 291
 292#[cfg(target_os = "windows")]
 293struct InstallerDir(PathBuf);
 294
 295#[cfg(target_os = "windows")]
 296impl InstallerDir {
 297    async fn new() -> Result<Self> {
 298        let installer_dir = std::env::current_exe()?
 299            .parent()
 300            .context("No parent dir for Zed.exe")?
 301            .join("updates");
 302        if smol::fs::metadata(&installer_dir).await.is_ok() {
 303            smol::fs::remove_dir_all(&installer_dir).await?;
 304        }
 305        smol::fs::create_dir(&installer_dir).await?;
 306        Ok(Self(installer_dir))
 307    }
 308
 309    fn path(&self) -> &Path {
 310        self.0.as_path()
 311    }
 312}
 313
 314pub enum UpdateCheckType {
 315    Automatic,
 316    Manual,
 317}
 318
 319impl AutoUpdater {
 320    pub fn get(cx: &mut App) -> Option<Entity<Self>> {
 321        cx.default_global::<GlobalAutoUpdate>().0.clone()
 322    }
 323
 324    fn new(current_version: SemanticVersion, client: Arc<Client>, cx: &mut Context<Self>) -> Self {
 325        // On windows, executable files cannot be overwritten while they are
 326        // running, so we must wait to overwrite the application until quitting
 327        // or restarting. When quitting the app, we spawn the auto update helper
 328        // to finish the auto update process after Zed exits. When restarting
 329        // the app after an update, we use `set_restart_path` to run the auto
 330        // update helper instead of the app, so that it can overwrite the app
 331        // and then spawn the new binary.
 332        #[cfg(target_os = "windows")]
 333        let quit_subscription = Some(cx.on_app_quit(|_, _| finalize_auto_update_on_quit()));
 334        #[cfg(not(target_os = "windows"))]
 335        let quit_subscription = None;
 336
 337        cx.on_app_restart(|this, _| {
 338            this.quit_subscription.take();
 339        })
 340        .detach();
 341
 342        Self {
 343            status: AutoUpdateStatus::Idle,
 344            current_version,
 345            client,
 346            pending_poll: None,
 347            quit_subscription,
 348        }
 349    }
 350
 351    pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
 352        cx.spawn(async move |this, cx| {
 353            if cfg!(target_os = "windows") {
 354                use util::ResultExt;
 355
 356                cleanup_windows()
 357                    .await
 358                    .context("failed to cleanup old directories")
 359                    .log_err();
 360            }
 361
 362            loop {
 363                this.update(cx, |this, cx| this.poll(UpdateCheckType::Automatic, cx))?;
 364                cx.background_executor().timer(POLL_INTERVAL).await;
 365            }
 366        })
 367    }
 368
 369    pub fn poll(&mut self, check_type: UpdateCheckType, cx: &mut Context<Self>) {
 370        if self.pending_poll.is_some() {
 371            return;
 372        }
 373
 374        cx.notify();
 375
 376        self.pending_poll = Some(cx.spawn(async move |this, cx| {
 377            let result = Self::update(this.upgrade()?, cx).await;
 378            this.update(cx, |this, cx| {
 379                this.pending_poll = None;
 380                if let Err(error) = result {
 381                    this.status = match check_type {
 382                        // Be quiet if the check was automated (e.g. when offline)
 383                        UpdateCheckType::Automatic => {
 384                            log::info!("auto-update check failed: error:{:?}", error);
 385                            AutoUpdateStatus::Idle
 386                        }
 387                        UpdateCheckType::Manual => {
 388                            log::error!("auto-update failed: error:{:?}", error);
 389                            AutoUpdateStatus::Errored {
 390                                error: Arc::new(error),
 391                            }
 392                        }
 393                    };
 394
 395                    cx.notify();
 396                }
 397            })
 398            .ok()
 399        }));
 400    }
 401
 402    pub fn current_version(&self) -> SemanticVersion {
 403        self.current_version
 404    }
 405
 406    pub fn status(&self) -> AutoUpdateStatus {
 407        self.status.clone()
 408    }
 409
 410    pub fn dismiss(&mut self, cx: &mut Context<Self>) -> bool {
 411        if let AutoUpdateStatus::Idle = self.status {
 412            return false;
 413        }
 414        self.status = AutoUpdateStatus::Idle;
 415        cx.notify();
 416        true
 417    }
 418
 419    // If you are packaging Zed and need to override the place it downloads SSH remotes from,
 420    // you can override this function. You should also update get_remote_server_release_url to return
 421    // Ok(None).
 422    pub async fn download_remote_server_release(
 423        release_channel: ReleaseChannel,
 424        version: Option<SemanticVersion>,
 425        os: &str,
 426        arch: &str,
 427        set_status: impl Fn(&str, &mut AsyncApp) + Send + 'static,
 428        cx: &mut AsyncApp,
 429    ) -> Result<PathBuf> {
 430        let this = cx.update(|cx| {
 431            cx.default_global::<GlobalAutoUpdate>()
 432                .0
 433                .clone()
 434                .context("auto-update not initialized")
 435        })??;
 436
 437        set_status("Fetching remote server release", cx);
 438        let release = Self::get_release_asset(
 439            &this,
 440            release_channel,
 441            version,
 442            "zed-remote-server",
 443            os,
 444            arch,
 445            cx,
 446        )
 447        .await?;
 448
 449        let servers_dir = paths::remote_servers_dir();
 450        let channel_dir = servers_dir.join(release_channel.dev_name());
 451        let platform_dir = channel_dir.join(format!("{}-{}", os, arch));
 452        let version_path = platform_dir.join(format!("{}.gz", release.version));
 453        smol::fs::create_dir_all(&platform_dir).await.ok();
 454
 455        let client = this.read_with(cx, |this, _| this.client.http_client())?;
 456
 457        if smol::fs::metadata(&version_path).await.is_err() {
 458            log::info!(
 459                "downloading zed-remote-server {os} {arch} version {}",
 460                release.version
 461            );
 462            set_status("Downloading remote server", cx);
 463            download_remote_server_binary(&version_path, release, client).await?;
 464        }
 465
 466        Ok(version_path)
 467    }
 468
 469    pub async fn get_remote_server_release_url(
 470        channel: ReleaseChannel,
 471        version: Option<SemanticVersion>,
 472        os: &str,
 473        arch: &str,
 474        cx: &mut AsyncApp,
 475    ) -> Result<Option<String>> {
 476        let this = cx.update(|cx| {
 477            cx.default_global::<GlobalAutoUpdate>()
 478                .0
 479                .clone()
 480                .context("auto-update not initialized")
 481        })??;
 482
 483        let release =
 484            Self::get_release_asset(&this, channel, version, "zed-remote-server", os, arch, cx)
 485                .await?;
 486
 487        Ok(Some(release.url))
 488    }
 489
 490    async fn get_release_asset(
 491        this: &Entity<Self>,
 492        release_channel: ReleaseChannel,
 493        version: Option<SemanticVersion>,
 494        asset: &str,
 495        os: &str,
 496        arch: &str,
 497        cx: &mut AsyncApp,
 498    ) -> Result<ReleaseAsset> {
 499        let client = this.read_with(cx, |this, _| this.client.clone())?;
 500
 501        let (system_id, metrics_id, is_staff) = if client.telemetry().metrics_enabled() {
 502            (
 503                client.telemetry().system_id(),
 504                client.telemetry().metrics_id(),
 505                client.telemetry().is_staff(),
 506            )
 507        } else {
 508            (None, None, None)
 509        };
 510
 511        let version = if let Some(version) = version {
 512            version.to_string()
 513        } else {
 514            "latest".to_string()
 515        };
 516        let http_client = client.http_client();
 517
 518        let path = format!("/releases/{}/{}/asset", release_channel.dev_name(), version,);
 519        let url = http_client.build_zed_cloud_url_with_query(
 520            &path,
 521            AssetQuery {
 522                os,
 523                arch,
 524                asset,
 525                metrics_id: metrics_id.as_deref(),
 526                system_id: system_id.as_deref(),
 527                is_staff: is_staff,
 528            },
 529        )?;
 530
 531        let mut response = http_client
 532            .get(url.as_str(), Default::default(), true)
 533            .await?;
 534        let mut body = Vec::new();
 535        response.body_mut().read_to_end(&mut body).await?;
 536
 537        anyhow::ensure!(
 538            response.status().is_success(),
 539            "failed to fetch release: {:?}",
 540            String::from_utf8_lossy(&body),
 541        );
 542
 543        serde_json::from_slice(body.as_slice()).with_context(|| {
 544            format!(
 545                "error deserializing release {:?}",
 546                String::from_utf8_lossy(&body),
 547            )
 548        })
 549    }
 550
 551    async fn update(this: Entity<Self>, cx: &mut AsyncApp) -> Result<()> {
 552        let (client, installed_version, previous_status, release_channel) =
 553            this.read_with(cx, |this, cx| {
 554                (
 555                    this.client.http_client(),
 556                    this.current_version,
 557                    this.status.clone(),
 558                    ReleaseChannel::try_global(cx).unwrap_or(ReleaseChannel::Stable),
 559                )
 560            })?;
 561
 562        Self::check_dependencies()?;
 563
 564        this.update(cx, |this, cx| {
 565            this.status = AutoUpdateStatus::Checking;
 566            log::info!("Auto Update: checking for updates");
 567            cx.notify();
 568        })?;
 569
 570        let fetched_release_data =
 571            Self::get_release_asset(&this, release_channel, None, "zed", OS, ARCH, cx).await?;
 572        let fetched_version = fetched_release_data.clone().version;
 573        let app_commit_sha = cx.update(|cx| AppCommitSha::try_global(cx).map(|sha| sha.full()));
 574        let newer_version = Self::check_if_fetched_version_is_newer(
 575            release_channel,
 576            app_commit_sha,
 577            installed_version,
 578            fetched_version,
 579            previous_status.clone(),
 580        )?;
 581
 582        let Some(newer_version) = newer_version else {
 583            return this.update(cx, |this, cx| {
 584                let status = match previous_status {
 585                    AutoUpdateStatus::Updated { .. } => previous_status,
 586                    _ => AutoUpdateStatus::Idle,
 587                };
 588                this.status = status;
 589                cx.notify();
 590            });
 591        };
 592
 593        this.update(cx, |this, cx| {
 594            this.status = AutoUpdateStatus::Downloading {
 595                version: newer_version.clone(),
 596            };
 597            cx.notify();
 598        })?;
 599
 600        let installer_dir = InstallerDir::new().await?;
 601        let target_path = Self::target_path(&installer_dir).await?;
 602        download_release(&target_path, fetched_release_data, client).await?;
 603
 604        this.update(cx, |this, cx| {
 605            this.status = AutoUpdateStatus::Installing {
 606                version: newer_version.clone(),
 607            };
 608            cx.notify();
 609        })?;
 610
 611        let new_binary_path = Self::install_release(installer_dir, target_path, cx).await?;
 612        if let Some(new_binary_path) = new_binary_path {
 613            cx.update(|cx| cx.set_restart_path(new_binary_path))?;
 614        }
 615
 616        this.update(cx, |this, cx| {
 617            this.set_should_show_update_notification(true, cx)
 618                .detach_and_log_err(cx);
 619            this.status = AutoUpdateStatus::Updated {
 620                version: newer_version,
 621            };
 622            cx.notify();
 623        })
 624    }
 625
 626    fn check_if_fetched_version_is_newer(
 627        release_channel: ReleaseChannel,
 628        app_commit_sha: Result<Option<String>>,
 629        installed_version: SemanticVersion,
 630        fetched_version: String,
 631        status: AutoUpdateStatus,
 632    ) -> Result<Option<VersionCheckType>> {
 633        let parsed_fetched_version = fetched_version.parse::<SemanticVersion>();
 634
 635        if let AutoUpdateStatus::Updated { version, .. } = status {
 636            match version {
 637                VersionCheckType::Sha(cached_version) => {
 638                    let should_download = fetched_version != cached_version.full();
 639                    let newer_version = should_download
 640                        .then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
 641                    return Ok(newer_version);
 642                }
 643                VersionCheckType::Semantic(cached_version) => {
 644                    return Self::check_if_fetched_version_is_newer_non_nightly(
 645                        cached_version,
 646                        parsed_fetched_version?,
 647                    );
 648                }
 649            }
 650        }
 651
 652        match release_channel {
 653            ReleaseChannel::Nightly => {
 654                let should_download = app_commit_sha
 655                    .ok()
 656                    .flatten()
 657                    .map(|sha| fetched_version != sha)
 658                    .unwrap_or(true);
 659                let newer_version = should_download
 660                    .then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
 661                Ok(newer_version)
 662            }
 663            _ => Self::check_if_fetched_version_is_newer_non_nightly(
 664                installed_version,
 665                parsed_fetched_version?,
 666            ),
 667        }
 668    }
 669
 670    fn check_dependencies() -> Result<()> {
 671        #[cfg(not(target_os = "windows"))]
 672        anyhow::ensure!(
 673            which::which("rsync").is_ok(),
 674            "Could not auto-update because the required rsync utility was not found."
 675        );
 676        Ok(())
 677    }
 678
 679    async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf> {
 680        let filename = match OS {
 681            "macos" => anyhow::Ok("Zed.dmg"),
 682            "linux" => Ok("zed.tar.gz"),
 683            "windows" => Ok("Zed.exe"),
 684            unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
 685        }?;
 686
 687        Ok(installer_dir.path().join(filename))
 688    }
 689
 690    async fn install_release(
 691        installer_dir: InstallerDir,
 692        target_path: PathBuf,
 693        cx: &AsyncApp,
 694    ) -> Result<Option<PathBuf>> {
 695        #[cfg(test)]
 696        if let Some(test_install) =
 697            cx.try_read_global::<tests::InstallOverride, _>(|g, _| g.0.clone())
 698        {
 699            return test_install(target_path, cx);
 700        }
 701        match OS {
 702            "macos" => install_release_macos(&installer_dir, target_path, cx).await,
 703            "linux" => install_release_linux(&installer_dir, target_path, cx).await,
 704            "windows" => install_release_windows(target_path).await,
 705            unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
 706        }
 707    }
 708
 709    fn check_if_fetched_version_is_newer_non_nightly(
 710        installed_version: SemanticVersion,
 711        fetched_version: SemanticVersion,
 712    ) -> Result<Option<VersionCheckType>> {
 713        let should_download = fetched_version > installed_version;
 714        let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
 715        Ok(newer_version)
 716    }
 717
 718    pub fn set_should_show_update_notification(
 719        &self,
 720        should_show: bool,
 721        cx: &App,
 722    ) -> Task<Result<()>> {
 723        cx.background_spawn(async move {
 724            if should_show {
 725                KEY_VALUE_STORE
 726                    .write_kvp(
 727                        SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
 728                        "".to_string(),
 729                    )
 730                    .await?;
 731            } else {
 732                KEY_VALUE_STORE
 733                    .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
 734                    .await?;
 735            }
 736            Ok(())
 737        })
 738    }
 739
 740    pub fn should_show_update_notification(&self, cx: &App) -> Task<Result<bool>> {
 741        cx.background_spawn(async move {
 742            Ok(KEY_VALUE_STORE
 743                .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
 744                .is_some())
 745        })
 746    }
 747}
 748
 749async fn download_remote_server_binary(
 750    target_path: &PathBuf,
 751    release: ReleaseAsset,
 752    client: Arc<HttpClientWithUrl>,
 753) -> Result<()> {
 754    let temp = tempfile::Builder::new().tempfile_in(remote_servers_dir())?;
 755    let mut temp_file = File::create(&temp).await?;
 756
 757    let mut response = client.get(&release.url, Default::default(), true).await?;
 758    anyhow::ensure!(
 759        response.status().is_success(),
 760        "failed to download remote server release: {:?}",
 761        response.status()
 762    );
 763    smol::io::copy(response.body_mut(), &mut temp_file).await?;
 764    smol::fs::rename(&temp, &target_path).await?;
 765
 766    Ok(())
 767}
 768
 769async fn download_release(
 770    target_path: &Path,
 771    release: ReleaseAsset,
 772    client: Arc<HttpClientWithUrl>,
 773) -> Result<()> {
 774    let mut target_file = File::create(&target_path).await?;
 775
 776    let mut response = client.get(&release.url, Default::default(), true).await?;
 777    anyhow::ensure!(
 778        response.status().is_success(),
 779        "failed to download update: {:?}",
 780        response.status()
 781    );
 782    smol::io::copy(response.body_mut(), &mut target_file).await?;
 783    log::info!("downloaded update. path:{:?}", target_path);
 784
 785    Ok(())
 786}
 787
 788async fn install_release_linux(
 789    temp_dir: &InstallerDir,
 790    downloaded_tar_gz: PathBuf,
 791    cx: &AsyncApp,
 792) -> Result<Option<PathBuf>> {
 793    let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
 794    let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
 795    let running_app_path = cx.update(|cx| cx.app_path())??;
 796
 797    let extracted = temp_dir.path().join("zed");
 798    fs::create_dir_all(&extracted)
 799        .await
 800        .context("failed to create directory into which to extract update")?;
 801
 802    let output = Command::new("tar")
 803        .arg("-xzf")
 804        .arg(&downloaded_tar_gz)
 805        .arg("-C")
 806        .arg(&extracted)
 807        .output()
 808        .await?;
 809
 810    anyhow::ensure!(
 811        output.status.success(),
 812        "failed to extract {:?} to {:?}: {:?}",
 813        downloaded_tar_gz,
 814        extracted,
 815        String::from_utf8_lossy(&output.stderr)
 816    );
 817
 818    let suffix = if channel != "stable" {
 819        format!("-{}", channel)
 820    } else {
 821        String::default()
 822    };
 823    let app_folder_name = format!("zed{}.app", suffix);
 824
 825    let from = extracted.join(&app_folder_name);
 826    let mut to = home_dir.join(".local");
 827
 828    let expected_suffix = format!("{}/libexec/zed-editor", app_folder_name);
 829
 830    if let Some(prefix) = running_app_path
 831        .to_str()
 832        .and_then(|str| str.strip_suffix(&expected_suffix))
 833    {
 834        to = PathBuf::from(prefix);
 835    }
 836
 837    let output = Command::new("rsync")
 838        .args(["-av", "--delete"])
 839        .arg(&from)
 840        .arg(&to)
 841        .output()
 842        .await?;
 843
 844    anyhow::ensure!(
 845        output.status.success(),
 846        "failed to copy Zed update from {:?} to {:?}: {:?}",
 847        from,
 848        to,
 849        String::from_utf8_lossy(&output.stderr)
 850    );
 851
 852    Ok(Some(to.join(expected_suffix)))
 853}
 854
 855async fn install_release_macos(
 856    temp_dir: &InstallerDir,
 857    downloaded_dmg: PathBuf,
 858    cx: &AsyncApp,
 859) -> Result<Option<PathBuf>> {
 860    let running_app_path = cx.update(|cx| cx.app_path())??;
 861    let running_app_filename = running_app_path
 862        .file_name()
 863        .with_context(|| format!("invalid running app path {running_app_path:?}"))?;
 864
 865    let mount_path = temp_dir.path().join("Zed");
 866    let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
 867
 868    mounted_app_path.push("/");
 869    let output = Command::new("hdiutil")
 870        .args(["attach", "-nobrowse"])
 871        .arg(&downloaded_dmg)
 872        .arg("-mountroot")
 873        .arg(temp_dir.path())
 874        .output()
 875        .await?;
 876
 877    anyhow::ensure!(
 878        output.status.success(),
 879        "failed to mount: {:?}",
 880        String::from_utf8_lossy(&output.stderr)
 881    );
 882
 883    // Create an MacOsUnmounter that will be dropped (and thus unmount the disk) when this function exits
 884    let _unmounter = MacOsUnmounter {
 885        mount_path: mount_path.clone(),
 886        background_executor: cx.background_executor(),
 887    };
 888
 889    let output = Command::new("rsync")
 890        .args(["-av", "--delete"])
 891        .arg(&mounted_app_path)
 892        .arg(&running_app_path)
 893        .output()
 894        .await?;
 895
 896    anyhow::ensure!(
 897        output.status.success(),
 898        "failed to copy app: {:?}",
 899        String::from_utf8_lossy(&output.stderr)
 900    );
 901
 902    Ok(None)
 903}
 904
 905async fn cleanup_windows() -> Result<()> {
 906    let parent = std::env::current_exe()?
 907        .parent()
 908        .context("No parent dir for Zed.exe")?
 909        .to_owned();
 910
 911    // keep in sync with crates/auto_update_helper/src/updater.rs
 912    _ = smol::fs::remove_dir(parent.join("updates")).await;
 913    _ = smol::fs::remove_dir(parent.join("install")).await;
 914    _ = smol::fs::remove_dir(parent.join("old")).await;
 915
 916    Ok(())
 917}
 918
 919async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option<PathBuf>> {
 920    let output = Command::new(downloaded_installer)
 921        .arg("/verysilent")
 922        .arg("/update=true")
 923        .arg("!desktopicon")
 924        .arg("!quicklaunchicon")
 925        .output()
 926        .await?;
 927    anyhow::ensure!(
 928        output.status.success(),
 929        "failed to start installer: {:?}",
 930        String::from_utf8_lossy(&output.stderr)
 931    );
 932    // We return the path to the update helper program, because it will
 933    // perform the final steps of the update process, copying the new binary,
 934    // deleting the old one, and launching the new binary.
 935    let helper_path = std::env::current_exe()?
 936        .parent()
 937        .context("No parent dir for Zed.exe")?
 938        .join("tools")
 939        .join("auto_update_helper.exe");
 940    Ok(Some(helper_path))
 941}
 942
 943pub async fn finalize_auto_update_on_quit() {
 944    let Some(installer_path) = std::env::current_exe()
 945        .ok()
 946        .and_then(|p| p.parent().map(|p| p.join("updates")))
 947    else {
 948        return;
 949    };
 950
 951    // The installer will create a flag file after it finishes updating
 952    let flag_file = installer_path.join("versions.txt");
 953    if flag_file.exists()
 954        && let Some(helper) = installer_path
 955            .parent()
 956            .map(|p| p.join("tools").join("auto_update_helper.exe"))
 957    {
 958        let mut command = util::command::new_smol_command(helper);
 959        command.arg("--launch");
 960        command.arg("false");
 961        if let Ok(mut cmd) = command.spawn() {
 962            _ = cmd.status().await;
 963        }
 964    }
 965}
 966
 967#[cfg(test)]
 968mod tests {
 969    use client::Client;
 970    use clock::FakeSystemClock;
 971    use futures::channel::oneshot;
 972    use gpui::TestAppContext;
 973    use http_client::{FakeHttpClient, Response};
 974    use settings::default_settings;
 975    use std::{
 976        rc::Rc,
 977        sync::{
 978            Arc,
 979            atomic::{self, AtomicBool},
 980        },
 981    };
 982    use tempfile::tempdir;
 983
 984    #[ctor::ctor]
 985    fn init_logger() {
 986        zlog::init_test();
 987    }
 988
 989    use super::*;
 990
 991    pub(super) struct InstallOverride(
 992        pub Rc<dyn Fn(PathBuf, &AsyncApp) -> Result<Option<PathBuf>>>,
 993    );
 994    impl Global for InstallOverride {}
 995
 996    #[gpui::test]
 997    fn test_auto_update_defaults_to_true(cx: &mut TestAppContext) {
 998        cx.update(|cx| {
 999            let mut store = SettingsStore::new(cx, &settings::default_settings());
1000            store
1001                .set_default_settings(&default_settings(), cx)
1002                .expect("Unable to set default settings");
1003            store
1004                .set_user_settings("{}", cx)
1005                .expect("Unable to set user settings");
1006            cx.set_global(store);
1007            assert!(AutoUpdateSetting::get_global(cx).0);
1008        });
1009    }
1010
1011    #[gpui::test]
1012    async fn test_auto_update_downloads(cx: &mut TestAppContext) {
1013        cx.background_executor.allow_parking();
1014        zlog::init_test();
1015        let release_available = Arc::new(AtomicBool::new(false));
1016
1017        let (dmg_tx, dmg_rx) = oneshot::channel::<String>();
1018
1019        cx.update(|cx| {
1020            settings::init(cx);
1021
1022            let current_version = SemanticVersion::new(0, 100, 0);
1023            release_channel::init_test(current_version, ReleaseChannel::Stable, cx);
1024
1025            let clock = Arc::new(FakeSystemClock::new());
1026            let release_available = Arc::clone(&release_available);
1027            let dmg_rx = Arc::new(parking_lot::Mutex::new(Some(dmg_rx)));
1028            let fake_client_http = FakeHttpClient::create(move |req| {
1029                let release_available = release_available.load(atomic::Ordering::Relaxed);
1030                let dmg_rx = dmg_rx.clone();
1031                async move {
1032                if req.uri().path() == "/releases/stable/latest/asset" {
1033                    if release_available {
1034                        return Ok(Response::builder().status(200).body(
1035                            r#"{"version":"0.100.1","url":"https://test.example/new-download"}"#.into()
1036                        ).unwrap());
1037                    } else {
1038                        return Ok(Response::builder().status(200).body(
1039                            r#"{"version":"0.100.0","url":"https://test.example/old-download"}"#.into()
1040                        ).unwrap());
1041                    }
1042                } else if req.uri().path() == "/new-download" {
1043                    return Ok(Response::builder().status(200).body({
1044                        let dmg_rx = dmg_rx.lock().take().unwrap();
1045                        dmg_rx.await.unwrap().into()
1046                    }).unwrap());
1047                }
1048                Ok(Response::builder().status(404).body("".into()).unwrap())
1049                }
1050            });
1051            let client = Client::new(clock, fake_client_http, cx);
1052            crate::init(client, cx);
1053        });
1054
1055        let auto_updater = cx.update(|cx| AutoUpdater::get(cx).expect("auto updater should exist"));
1056
1057        cx.background_executor.run_until_parked();
1058
1059        auto_updater.read_with(cx, |updater, _| {
1060            assert_eq!(updater.status(), AutoUpdateStatus::Idle);
1061            assert_eq!(updater.current_version(), SemanticVersion::new(0, 100, 0));
1062        });
1063
1064        release_available.store(true, atomic::Ordering::SeqCst);
1065        cx.background_executor.advance_clock(POLL_INTERVAL);
1066        cx.background_executor.run_until_parked();
1067
1068        loop {
1069            cx.background_executor.timer(Duration::from_millis(0)).await;
1070            cx.run_until_parked();
1071            let status = auto_updater.read_with(cx, |updater, _| updater.status());
1072            if !matches!(status, AutoUpdateStatus::Idle) {
1073                break;
1074            }
1075        }
1076        let status = auto_updater.read_with(cx, |updater, _| updater.status());
1077        assert_eq!(
1078            status,
1079            AutoUpdateStatus::Downloading {
1080                version: VersionCheckType::Semantic(SemanticVersion::new(0, 100, 1))
1081            }
1082        );
1083
1084        dmg_tx.send("<fake-zed-update>".to_owned()).unwrap();
1085
1086        let tmp_dir = Arc::new(tempdir().unwrap());
1087
1088        cx.update(|cx| {
1089            let tmp_dir = tmp_dir.clone();
1090            cx.set_global(InstallOverride(Rc::new(move |target_path, _cx| {
1091                let tmp_dir = tmp_dir.clone();
1092                let dest_path = tmp_dir.path().join("zed");
1093                std::fs::copy(&target_path, &dest_path)?;
1094                Ok(Some(dest_path))
1095            })));
1096        });
1097
1098        loop {
1099            cx.background_executor.timer(Duration::from_millis(0)).await;
1100            cx.run_until_parked();
1101            let status = auto_updater.read_with(cx, |updater, _| updater.status());
1102            if !matches!(status, AutoUpdateStatus::Downloading { .. }) {
1103                break;
1104            }
1105        }
1106        let status = auto_updater.read_with(cx, |updater, _| updater.status());
1107        assert_eq!(
1108            status,
1109            AutoUpdateStatus::Updated {
1110                version: VersionCheckType::Semantic(SemanticVersion::new(0, 100, 1))
1111            }
1112        );
1113        let will_restart = cx.expect_restart();
1114        cx.update(|cx| cx.restart());
1115        let path = will_restart.await.unwrap().unwrap();
1116        assert_eq!(path, tmp_dir.path().join("zed"));
1117        assert_eq!(std::fs::read_to_string(path).unwrap(), "<fake-zed-update>");
1118    }
1119
1120    #[test]
1121    fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
1122        let release_channel = ReleaseChannel::Stable;
1123        let app_commit_sha = Ok(Some("a".to_string()));
1124        let installed_version = SemanticVersion::new(1, 0, 0);
1125        let status = AutoUpdateStatus::Idle;
1126        let fetched_version = SemanticVersion::new(1, 0, 0);
1127
1128        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1129            release_channel,
1130            app_commit_sha,
1131            installed_version,
1132            fetched_version.to_string(),
1133            status,
1134        );
1135
1136        assert_eq!(newer_version.unwrap(), None);
1137    }
1138
1139    #[test]
1140    fn test_stable_does_update_when_fetched_version_is_higher() {
1141        let release_channel = ReleaseChannel::Stable;
1142        let app_commit_sha = Ok(Some("a".to_string()));
1143        let installed_version = SemanticVersion::new(1, 0, 0);
1144        let status = AutoUpdateStatus::Idle;
1145        let fetched_version = SemanticVersion::new(1, 0, 1);
1146
1147        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1148            release_channel,
1149            app_commit_sha,
1150            installed_version,
1151            fetched_version.to_string(),
1152            status,
1153        );
1154
1155        assert_eq!(
1156            newer_version.unwrap(),
1157            Some(VersionCheckType::Semantic(fetched_version))
1158        );
1159    }
1160
1161    #[test]
1162    fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
1163        let release_channel = ReleaseChannel::Stable;
1164        let app_commit_sha = Ok(Some("a".to_string()));
1165        let installed_version = SemanticVersion::new(1, 0, 0);
1166        let status = AutoUpdateStatus::Updated {
1167            version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
1168        };
1169        let fetched_version = SemanticVersion::new(1, 0, 1);
1170
1171        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1172            release_channel,
1173            app_commit_sha,
1174            installed_version,
1175            fetched_version.to_string(),
1176            status,
1177        );
1178
1179        assert_eq!(newer_version.unwrap(), None);
1180    }
1181
1182    #[test]
1183    fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
1184        let release_channel = ReleaseChannel::Stable;
1185        let app_commit_sha = Ok(Some("a".to_string()));
1186        let installed_version = SemanticVersion::new(1, 0, 0);
1187        let status = AutoUpdateStatus::Updated {
1188            version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
1189        };
1190        let fetched_version = SemanticVersion::new(1, 0, 2);
1191
1192        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1193            release_channel,
1194            app_commit_sha,
1195            installed_version,
1196            fetched_version.to_string(),
1197            status,
1198        );
1199
1200        assert_eq!(
1201            newer_version.unwrap(),
1202            Some(VersionCheckType::Semantic(fetched_version))
1203        );
1204    }
1205
1206    #[test]
1207    fn test_nightly_does_not_update_when_fetched_sha_is_same() {
1208        let release_channel = ReleaseChannel::Nightly;
1209        let app_commit_sha = Ok(Some("a".to_string()));
1210        let installed_version = SemanticVersion::new(1, 0, 0);
1211        let status = AutoUpdateStatus::Idle;
1212        let fetched_sha = "a".to_string();
1213
1214        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1215            release_channel,
1216            app_commit_sha,
1217            installed_version,
1218            fetched_sha,
1219            status,
1220        );
1221
1222        assert_eq!(newer_version.unwrap(), None);
1223    }
1224
1225    #[test]
1226    fn test_nightly_does_update_when_fetched_sha_is_not_same() {
1227        let release_channel = ReleaseChannel::Nightly;
1228        let app_commit_sha = Ok(Some("a".to_string()));
1229        let installed_version = SemanticVersion::new(1, 0, 0);
1230        let status = AutoUpdateStatus::Idle;
1231        let fetched_sha = "b".to_string();
1232
1233        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1234            release_channel,
1235            app_commit_sha,
1236            installed_version,
1237            fetched_sha.clone(),
1238            status,
1239        );
1240
1241        assert_eq!(
1242            newer_version.unwrap(),
1243            Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1244        );
1245    }
1246
1247    #[test]
1248    fn test_nightly_does_not_update_when_fetched_sha_is_same_as_cached() {
1249        let release_channel = ReleaseChannel::Nightly;
1250        let app_commit_sha = Ok(Some("a".to_string()));
1251        let installed_version = SemanticVersion::new(1, 0, 0);
1252        let status = AutoUpdateStatus::Updated {
1253            version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1254        };
1255        let fetched_sha = "b".to_string();
1256
1257        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1258            release_channel,
1259            app_commit_sha,
1260            installed_version,
1261            fetched_sha,
1262            status,
1263        );
1264
1265        assert_eq!(newer_version.unwrap(), None);
1266    }
1267
1268    #[test]
1269    fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
1270        let release_channel = ReleaseChannel::Nightly;
1271        let app_commit_sha = Ok(Some("a".to_string()));
1272        let installed_version = SemanticVersion::new(1, 0, 0);
1273        let status = AutoUpdateStatus::Updated {
1274            version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1275        };
1276        let fetched_sha = "c".to_string();
1277
1278        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1279            release_channel,
1280            app_commit_sha,
1281            installed_version,
1282            fetched_sha.clone(),
1283            status,
1284        );
1285
1286        assert_eq!(
1287            newer_version.unwrap(),
1288            Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1289        );
1290    }
1291
1292    #[test]
1293    fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
1294        let release_channel = ReleaseChannel::Nightly;
1295        let app_commit_sha = Ok(None);
1296        let installed_version = SemanticVersion::new(1, 0, 0);
1297        let status = AutoUpdateStatus::Idle;
1298        let fetched_sha = "a".to_string();
1299
1300        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1301            release_channel,
1302            app_commit_sha,
1303            installed_version,
1304            fetched_sha.clone(),
1305            status,
1306        );
1307
1308        assert_eq!(
1309            newer_version.unwrap(),
1310            Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1311        );
1312    }
1313
1314    #[test]
1315    fn test_nightly_does_not_update_when_cached_update_is_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1316     {
1317        let release_channel = ReleaseChannel::Nightly;
1318        let app_commit_sha = Ok(None);
1319        let installed_version = SemanticVersion::new(1, 0, 0);
1320        let status = AutoUpdateStatus::Updated {
1321            version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1322        };
1323        let fetched_sha = "b".to_string();
1324
1325        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1326            release_channel,
1327            app_commit_sha,
1328            installed_version,
1329            fetched_sha,
1330            status,
1331        );
1332
1333        assert_eq!(newer_version.unwrap(), None);
1334    }
1335
1336    #[test]
1337    fn test_nightly_does_update_when_cached_update_is_not_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1338     {
1339        let release_channel = ReleaseChannel::Nightly;
1340        let app_commit_sha = Ok(None);
1341        let installed_version = SemanticVersion::new(1, 0, 0);
1342        let status = AutoUpdateStatus::Updated {
1343            version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1344        };
1345        let fetched_sha = "c".to_string();
1346
1347        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1348            release_channel,
1349            app_commit_sha,
1350            installed_version,
1351            fetched_sha.clone(),
1352            status,
1353        );
1354
1355        assert_eq!(
1356            newer_version.unwrap(),
1357            Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1358        );
1359    }
1360}