auto_update.rs

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