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