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