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(UpdateCheckType::Manual, 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
 299pub enum UpdateCheckType {
 300    Automatic,
 301    Manual,
 302}
 303
 304impl AutoUpdater {
 305    pub fn get(cx: &mut App) -> Option<Entity<Self>> {
 306        cx.default_global::<GlobalAutoUpdate>().0.clone()
 307    }
 308
 309    fn new(current_version: SemanticVersion, http_client: Arc<HttpClientWithUrl>) -> Self {
 310        Self {
 311            status: AutoUpdateStatus::Idle,
 312            current_version,
 313            http_client,
 314            pending_poll: None,
 315        }
 316    }
 317
 318    pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
 319        cx.spawn(async move |this, cx| {
 320            loop {
 321                this.update(cx, |this, cx| this.poll(UpdateCheckType::Automatic, cx))?;
 322                cx.background_executor().timer(POLL_INTERVAL).await;
 323            }
 324        })
 325    }
 326
 327    pub fn poll(&mut self, check_type: UpdateCheckType, cx: &mut Context<Self>) {
 328        if self.pending_poll.is_some() {
 329            return;
 330        }
 331
 332        cx.notify();
 333
 334        self.pending_poll = Some(cx.spawn(async move |this, cx| {
 335            let result = Self::update(this.upgrade()?, cx.clone()).await;
 336            this.update(cx, |this, cx| {
 337                this.pending_poll = None;
 338                if let Err(error) = result {
 339                    this.status = match check_type {
 340                        // Be quiet if the check was automated (e.g. when offline)
 341                        UpdateCheckType::Automatic => {
 342                            log::info!("auto-update check failed: error:{:?}", error);
 343                            AutoUpdateStatus::Idle
 344                        }
 345                        UpdateCheckType::Manual => {
 346                            log::error!("auto-update failed: error:{:?}", error);
 347                            AutoUpdateStatus::Errored
 348                        }
 349                    };
 350
 351                    cx.notify();
 352                }
 353            })
 354            .ok()
 355        }));
 356    }
 357
 358    pub fn current_version(&self) -> SemanticVersion {
 359        self.current_version
 360    }
 361
 362    pub fn status(&self) -> AutoUpdateStatus {
 363        self.status.clone()
 364    }
 365
 366    pub fn dismiss_error(&mut self, cx: &mut Context<Self>) -> bool {
 367        if self.status == AutoUpdateStatus::Idle {
 368            return false;
 369        }
 370        self.status = AutoUpdateStatus::Idle;
 371        cx.notify();
 372        true
 373    }
 374
 375    // If you are packaging Zed and need to override the place it downloads SSH remotes from,
 376    // you can override this function. You should also update get_remote_server_release_url to return
 377    // Ok(None).
 378    pub async fn download_remote_server_release(
 379        os: &str,
 380        arch: &str,
 381        release_channel: ReleaseChannel,
 382        version: Option<SemanticVersion>,
 383        cx: &mut AsyncApp,
 384    ) -> Result<PathBuf> {
 385        let this = cx.update(|cx| {
 386            cx.default_global::<GlobalAutoUpdate>()
 387                .0
 388                .clone()
 389                .context("auto-update not initialized")
 390        })??;
 391
 392        let release = Self::get_release(
 393            &this,
 394            "zed-remote-server",
 395            os,
 396            arch,
 397            version,
 398            Some(release_channel),
 399            cx,
 400        )
 401        .await?;
 402
 403        let servers_dir = paths::remote_servers_dir();
 404        let channel_dir = servers_dir.join(release_channel.dev_name());
 405        let platform_dir = channel_dir.join(format!("{}-{}", os, arch));
 406        let version_path = platform_dir.join(format!("{}.gz", release.version));
 407        smol::fs::create_dir_all(&platform_dir).await.ok();
 408
 409        let client = this.read_with(cx, |this, _| this.http_client.clone())?;
 410
 411        if smol::fs::metadata(&version_path).await.is_err() {
 412            log::info!(
 413                "downloading zed-remote-server {os} {arch} version {}",
 414                release.version
 415            );
 416            download_remote_server_binary(&version_path, release, client, cx).await?;
 417        }
 418
 419        Ok(version_path)
 420    }
 421
 422    pub async fn get_remote_server_release_url(
 423        os: &str,
 424        arch: &str,
 425        release_channel: ReleaseChannel,
 426        version: Option<SemanticVersion>,
 427        cx: &mut AsyncApp,
 428    ) -> Result<Option<(String, String)>> {
 429        let this = cx.update(|cx| {
 430            cx.default_global::<GlobalAutoUpdate>()
 431                .0
 432                .clone()
 433                .context("auto-update not initialized")
 434        })??;
 435
 436        let release = Self::get_release(
 437            &this,
 438            "zed-remote-server",
 439            os,
 440            arch,
 441            version,
 442            Some(release_channel),
 443            cx,
 444        )
 445        .await?;
 446
 447        let update_request_body = build_remote_server_update_request_body(cx)?;
 448        let body = serde_json::to_string(&update_request_body)?;
 449
 450        Ok(Some((release.url, body)))
 451    }
 452
 453    async fn get_release(
 454        this: &Entity<Self>,
 455        asset: &str,
 456        os: &str,
 457        arch: &str,
 458        version: Option<SemanticVersion>,
 459        release_channel: Option<ReleaseChannel>,
 460        cx: &mut AsyncApp,
 461    ) -> Result<JsonRelease> {
 462        let client = this.read_with(cx, |this, _| this.http_client.clone())?;
 463
 464        if let Some(version) = version {
 465            let channel = release_channel.map(|c| c.dev_name()).unwrap_or("stable");
 466
 467            let url = format!("/api/releases/{channel}/{version}/{asset}-{os}-{arch}.gz?update=1",);
 468
 469            Ok(JsonRelease {
 470                version: version.to_string(),
 471                url: client.build_url(&url),
 472            })
 473        } else {
 474            let mut url_string = client.build_url(&format!(
 475                "/api/releases/latest?asset={}&os={}&arch={}",
 476                asset, os, arch
 477            ));
 478            if let Some(param) = release_channel.and_then(|c| c.release_query_param()) {
 479                url_string += "&";
 480                url_string += param;
 481            }
 482
 483            let mut response = client.get(&url_string, Default::default(), true).await?;
 484            let mut body = Vec::new();
 485            response.body_mut().read_to_end(&mut body).await?;
 486
 487            anyhow::ensure!(
 488                response.status().is_success(),
 489                "failed to fetch release: {:?}",
 490                String::from_utf8_lossy(&body),
 491            );
 492
 493            serde_json::from_slice(body.as_slice()).with_context(|| {
 494                format!(
 495                    "error deserializing release {:?}",
 496                    String::from_utf8_lossy(&body),
 497                )
 498            })
 499        }
 500    }
 501
 502    async fn get_latest_release(
 503        this: &Entity<Self>,
 504        asset: &str,
 505        os: &str,
 506        arch: &str,
 507        release_channel: Option<ReleaseChannel>,
 508        cx: &mut AsyncApp,
 509    ) -> Result<JsonRelease> {
 510        Self::get_release(this, asset, os, arch, None, release_channel, cx).await
 511    }
 512
 513    async fn update(this: Entity<Self>, mut cx: AsyncApp) -> Result<()> {
 514        let (client, installed_version, previous_status, release_channel) =
 515            this.read_with(&mut cx, |this, cx| {
 516                (
 517                    this.http_client.clone(),
 518                    this.current_version,
 519                    this.status.clone(),
 520                    ReleaseChannel::try_global(cx),
 521                )
 522            })?;
 523
 524        this.update(&mut cx, |this, cx| {
 525            this.status = AutoUpdateStatus::Checking;
 526            cx.notify();
 527        })?;
 528
 529        let fetched_release_data =
 530            Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
 531        let fetched_version = fetched_release_data.clone().version;
 532        let app_commit_sha = cx.update(|cx| AppCommitSha::try_global(cx).map(|sha| sha.full()));
 533        let newer_version = Self::check_if_fetched_version_is_newer(
 534            *RELEASE_CHANNEL,
 535            app_commit_sha,
 536            installed_version,
 537            fetched_version,
 538            previous_status.clone(),
 539        )?;
 540
 541        let Some(newer_version) = newer_version else {
 542            return this.update(&mut cx, |this, cx| {
 543                let status = match previous_status {
 544                    AutoUpdateStatus::Updated { .. } => previous_status,
 545                    _ => AutoUpdateStatus::Idle,
 546                };
 547                this.status = status;
 548                cx.notify();
 549            });
 550        };
 551
 552        this.update(&mut cx, |this, cx| {
 553            this.status = AutoUpdateStatus::Downloading {
 554                version: newer_version.clone(),
 555            };
 556            cx.notify();
 557        })?;
 558
 559        let installer_dir = InstallerDir::new().await?;
 560        let target_path = Self::target_path(&installer_dir).await?;
 561        download_release(&target_path, fetched_release_data, client, &cx).await?;
 562
 563        this.update(&mut cx, |this, cx| {
 564            this.status = AutoUpdateStatus::Installing {
 565                version: newer_version.clone(),
 566            };
 567            cx.notify();
 568        })?;
 569
 570        let binary_path = Self::binary_path(installer_dir, target_path, &cx).await?;
 571
 572        this.update(&mut cx, |this, cx| {
 573            this.set_should_show_update_notification(true, cx)
 574                .detach_and_log_err(cx);
 575            this.status = AutoUpdateStatus::Updated {
 576                binary_path,
 577                version: newer_version,
 578            };
 579            cx.notify();
 580        })
 581    }
 582
 583    fn check_if_fetched_version_is_newer(
 584        release_channel: ReleaseChannel,
 585        app_commit_sha: Result<Option<String>>,
 586        installed_version: SemanticVersion,
 587        fetched_version: String,
 588        status: AutoUpdateStatus,
 589    ) -> Result<Option<VersionCheckType>> {
 590        let parsed_fetched_version = fetched_version.parse::<SemanticVersion>();
 591
 592        if let AutoUpdateStatus::Updated { version, .. } = status {
 593            match version {
 594                VersionCheckType::Sha(cached_version) => {
 595                    let should_download = fetched_version != cached_version.full();
 596                    let newer_version = should_download
 597                        .then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
 598                    return Ok(newer_version);
 599                }
 600                VersionCheckType::Semantic(cached_version) => {
 601                    return Self::check_if_fetched_version_is_newer_non_nightly(
 602                        cached_version,
 603                        parsed_fetched_version?,
 604                    );
 605                }
 606            }
 607        }
 608
 609        match release_channel {
 610            ReleaseChannel::Nightly => {
 611                let should_download = app_commit_sha
 612                    .ok()
 613                    .flatten()
 614                    .map(|sha| fetched_version != sha)
 615                    .unwrap_or(true);
 616                let newer_version = should_download
 617                    .then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
 618                Ok(newer_version)
 619            }
 620            _ => Self::check_if_fetched_version_is_newer_non_nightly(
 621                installed_version,
 622                parsed_fetched_version?,
 623            ),
 624        }
 625    }
 626
 627    async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf> {
 628        let filename = match OS {
 629            "macos" => anyhow::Ok("Zed.dmg"),
 630            "linux" => Ok("zed.tar.gz"),
 631            "windows" => Ok("ZedUpdateInstaller.exe"),
 632            unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
 633        }?;
 634
 635        #[cfg(not(target_os = "windows"))]
 636        anyhow::ensure!(
 637            which::which("rsync").is_ok(),
 638            "Aborting. Could not find rsync which is required for auto-updates."
 639        );
 640
 641        Ok(installer_dir.path().join(filename))
 642    }
 643
 644    async fn binary_path(
 645        installer_dir: InstallerDir,
 646        target_path: PathBuf,
 647        cx: &AsyncApp,
 648    ) -> Result<PathBuf> {
 649        match OS {
 650            "macos" => install_release_macos(&installer_dir, target_path, cx).await,
 651            "linux" => install_release_linux(&installer_dir, target_path, cx).await,
 652            "windows" => install_release_windows(target_path).await,
 653            unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
 654        }
 655    }
 656
 657    fn check_if_fetched_version_is_newer_non_nightly(
 658        installed_version: SemanticVersion,
 659        fetched_version: SemanticVersion,
 660    ) -> Result<Option<VersionCheckType>> {
 661        let should_download = fetched_version > installed_version;
 662        let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
 663        Ok(newer_version)
 664    }
 665
 666    pub fn set_should_show_update_notification(
 667        &self,
 668        should_show: bool,
 669        cx: &App,
 670    ) -> Task<Result<()>> {
 671        cx.background_spawn(async move {
 672            if should_show {
 673                KEY_VALUE_STORE
 674                    .write_kvp(
 675                        SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
 676                        "".to_string(),
 677                    )
 678                    .await?;
 679            } else {
 680                KEY_VALUE_STORE
 681                    .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
 682                    .await?;
 683            }
 684            Ok(())
 685        })
 686    }
 687
 688    pub fn should_show_update_notification(&self, cx: &App) -> Task<Result<bool>> {
 689        cx.background_spawn(async move {
 690            Ok(KEY_VALUE_STORE
 691                .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
 692                .is_some())
 693        })
 694    }
 695}
 696
 697async fn download_remote_server_binary(
 698    target_path: &PathBuf,
 699    release: JsonRelease,
 700    client: Arc<HttpClientWithUrl>,
 701    cx: &AsyncApp,
 702) -> Result<()> {
 703    let temp = tempfile::Builder::new().tempfile_in(remote_servers_dir())?;
 704    let mut temp_file = File::create(&temp).await?;
 705    let update_request_body = build_remote_server_update_request_body(cx)?;
 706    let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?);
 707
 708    let mut response = client.get(&release.url, request_body, true).await?;
 709    anyhow::ensure!(
 710        response.status().is_success(),
 711        "failed to download remote server release: {:?}",
 712        response.status()
 713    );
 714    smol::io::copy(response.body_mut(), &mut temp_file).await?;
 715    smol::fs::rename(&temp, &target_path).await?;
 716
 717    Ok(())
 718}
 719
 720fn build_remote_server_update_request_body(cx: &AsyncApp) -> Result<UpdateRequestBody> {
 721    let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
 722        let telemetry = Client::global(cx).telemetry().clone();
 723        let is_staff = telemetry.is_staff();
 724        let installation_id = telemetry.installation_id();
 725        let release_channel =
 726            ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
 727        let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
 728
 729        (
 730            installation_id,
 731            release_channel,
 732            telemetry_enabled,
 733            is_staff,
 734        )
 735    })?;
 736
 737    Ok(UpdateRequestBody {
 738        installation_id,
 739        release_channel,
 740        telemetry: telemetry_enabled,
 741        is_staff,
 742        destination: "remote",
 743    })
 744}
 745
 746async fn download_release(
 747    target_path: &Path,
 748    release: JsonRelease,
 749    client: Arc<HttpClientWithUrl>,
 750    cx: &AsyncApp,
 751) -> Result<()> {
 752    let mut target_file = File::create(&target_path).await?;
 753
 754    let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
 755        let telemetry = Client::global(cx).telemetry().clone();
 756        let is_staff = telemetry.is_staff();
 757        let installation_id = telemetry.installation_id();
 758        let release_channel =
 759            ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
 760        let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
 761
 762        (
 763            installation_id,
 764            release_channel,
 765            telemetry_enabled,
 766            is_staff,
 767        )
 768    })?;
 769
 770    let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
 771        installation_id,
 772        release_channel,
 773        telemetry: telemetry_enabled,
 774        is_staff,
 775        destination: "local",
 776    })?);
 777
 778    let mut response = client.get(&release.url, request_body, true).await?;
 779    smol::io::copy(response.body_mut(), &mut target_file).await?;
 780    log::info!("downloaded update. path:{:?}", target_path);
 781
 782    Ok(())
 783}
 784
 785async fn install_release_linux(
 786    temp_dir: &InstallerDir,
 787    downloaded_tar_gz: PathBuf,
 788    cx: &AsyncApp,
 789) -> Result<PathBuf> {
 790    let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
 791    let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
 792    let running_app_path = cx.update(|cx| cx.app_path())??;
 793
 794    let extracted = temp_dir.path().join("zed");
 795    fs::create_dir_all(&extracted)
 796        .await
 797        .context("failed to create directory into which to extract update")?;
 798
 799    let output = Command::new("tar")
 800        .arg("-xzf")
 801        .arg(&downloaded_tar_gz)
 802        .arg("-C")
 803        .arg(&extracted)
 804        .output()
 805        .await?;
 806
 807    anyhow::ensure!(
 808        output.status.success(),
 809        "failed to extract {:?} to {:?}: {:?}",
 810        downloaded_tar_gz,
 811        extracted,
 812        String::from_utf8_lossy(&output.stderr)
 813    );
 814
 815    let suffix = if channel != "stable" {
 816        format!("-{}", channel)
 817    } else {
 818        String::default()
 819    };
 820    let app_folder_name = format!("zed{}.app", suffix);
 821
 822    let from = extracted.join(&app_folder_name);
 823    let mut to = home_dir.join(".local");
 824
 825    let expected_suffix = format!("{}/libexec/zed-editor", app_folder_name);
 826
 827    if let Some(prefix) = running_app_path
 828        .to_str()
 829        .and_then(|str| str.strip_suffix(&expected_suffix))
 830    {
 831        to = PathBuf::from(prefix);
 832    }
 833
 834    let output = Command::new("rsync")
 835        .args(["-av", "--delete"])
 836        .arg(&from)
 837        .arg(&to)
 838        .output()
 839        .await?;
 840
 841    anyhow::ensure!(
 842        output.status.success(),
 843        "failed to copy Zed update from {:?} to {:?}: {:?}",
 844        from,
 845        to,
 846        String::from_utf8_lossy(&output.stderr)
 847    );
 848
 849    Ok(to.join(expected_suffix))
 850}
 851
 852async fn install_release_macos(
 853    temp_dir: &InstallerDir,
 854    downloaded_dmg: PathBuf,
 855    cx: &AsyncApp,
 856) -> Result<PathBuf> {
 857    let running_app_path = cx.update(|cx| cx.app_path())??;
 858    let running_app_filename = running_app_path
 859        .file_name()
 860        .with_context(|| format!("invalid running app path {running_app_path:?}"))?;
 861
 862    let mount_path = temp_dir.path().join("Zed");
 863    let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
 864
 865    mounted_app_path.push("/");
 866    let output = Command::new("hdiutil")
 867        .args(["attach", "-nobrowse"])
 868        .arg(&downloaded_dmg)
 869        .arg("-mountroot")
 870        .arg(temp_dir.path())
 871        .output()
 872        .await?;
 873
 874    anyhow::ensure!(
 875        output.status.success(),
 876        "failed to mount: {:?}",
 877        String::from_utf8_lossy(&output.stderr)
 878    );
 879
 880    // Create an MacOsUnmounter that will be dropped (and thus unmount the disk) when this function exits
 881    let _unmounter = MacOsUnmounter {
 882        mount_path: mount_path.clone(),
 883    };
 884
 885    let output = Command::new("rsync")
 886        .args(["-av", "--delete"])
 887        .arg(&mounted_app_path)
 888        .arg(&running_app_path)
 889        .output()
 890        .await?;
 891
 892    anyhow::ensure!(
 893        output.status.success(),
 894        "failed to copy app: {:?}",
 895        String::from_utf8_lossy(&output.stderr)
 896    );
 897
 898    Ok(running_app_path)
 899}
 900
 901async fn install_release_windows(downloaded_installer: PathBuf) -> Result<PathBuf> {
 902    let output = Command::new(downloaded_installer)
 903        .arg("/verysilent")
 904        .arg("/update=true")
 905        .arg("!desktopicon")
 906        .arg("!quicklaunchicon")
 907        .output()
 908        .await?;
 909    anyhow::ensure!(
 910        output.status.success(),
 911        "failed to start installer: {:?}",
 912        String::from_utf8_lossy(&output.stderr)
 913    );
 914    Ok(std::env::current_exe()?)
 915}
 916
 917pub fn check_pending_installation() -> bool {
 918    let Some(installer_path) = std::env::current_exe()
 919        .ok()
 920        .and_then(|p| p.parent().map(|p| p.join("updates")))
 921    else {
 922        return false;
 923    };
 924
 925    // The installer will create a flag file after it finishes updating
 926    let flag_file = installer_path.join("versions.txt");
 927    if flag_file.exists() {
 928        if let Some(helper) = installer_path
 929            .parent()
 930            .map(|p| p.join("tools\\auto_update_helper.exe"))
 931        {
 932            let _ = std::process::Command::new(helper).spawn();
 933            return true;
 934        }
 935    }
 936    false
 937}
 938
 939#[cfg(test)]
 940mod tests {
 941    use super::*;
 942
 943    #[test]
 944    fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
 945        let release_channel = ReleaseChannel::Stable;
 946        let app_commit_sha = Ok(Some("a".to_string()));
 947        let installed_version = SemanticVersion::new(1, 0, 0);
 948        let status = AutoUpdateStatus::Idle;
 949        let fetched_version = SemanticVersion::new(1, 0, 0);
 950
 951        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
 952            release_channel,
 953            app_commit_sha,
 954            installed_version,
 955            fetched_version.to_string(),
 956            status,
 957        );
 958
 959        assert_eq!(newer_version.unwrap(), None);
 960    }
 961
 962    #[test]
 963    fn test_stable_does_update_when_fetched_version_is_higher() {
 964        let release_channel = ReleaseChannel::Stable;
 965        let app_commit_sha = Ok(Some("a".to_string()));
 966        let installed_version = SemanticVersion::new(1, 0, 0);
 967        let status = AutoUpdateStatus::Idle;
 968        let fetched_version = SemanticVersion::new(1, 0, 1);
 969
 970        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
 971            release_channel,
 972            app_commit_sha,
 973            installed_version,
 974            fetched_version.to_string(),
 975            status,
 976        );
 977
 978        assert_eq!(
 979            newer_version.unwrap(),
 980            Some(VersionCheckType::Semantic(fetched_version))
 981        );
 982    }
 983
 984    #[test]
 985    fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
 986        let release_channel = ReleaseChannel::Stable;
 987        let app_commit_sha = Ok(Some("a".to_string()));
 988        let installed_version = SemanticVersion::new(1, 0, 0);
 989        let status = AutoUpdateStatus::Updated {
 990            binary_path: PathBuf::new(),
 991            version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
 992        };
 993        let fetched_version = SemanticVersion::new(1, 0, 1);
 994
 995        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
 996            release_channel,
 997            app_commit_sha,
 998            installed_version,
 999            fetched_version.to_string(),
1000            status,
1001        );
1002
1003        assert_eq!(newer_version.unwrap(), None);
1004    }
1005
1006    #[test]
1007    fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
1008        let release_channel = ReleaseChannel::Stable;
1009        let app_commit_sha = Ok(Some("a".to_string()));
1010        let installed_version = SemanticVersion::new(1, 0, 0);
1011        let status = AutoUpdateStatus::Updated {
1012            binary_path: PathBuf::new(),
1013            version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
1014        };
1015        let fetched_version = SemanticVersion::new(1, 0, 2);
1016
1017        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1018            release_channel,
1019            app_commit_sha,
1020            installed_version,
1021            fetched_version.to_string(),
1022            status,
1023        );
1024
1025        assert_eq!(
1026            newer_version.unwrap(),
1027            Some(VersionCheckType::Semantic(fetched_version))
1028        );
1029    }
1030
1031    #[test]
1032    fn test_nightly_does_not_update_when_fetched_sha_is_same() {
1033        let release_channel = ReleaseChannel::Nightly;
1034        let app_commit_sha = Ok(Some("a".to_string()));
1035        let installed_version = SemanticVersion::new(1, 0, 0);
1036        let status = AutoUpdateStatus::Idle;
1037        let fetched_sha = "a".to_string();
1038
1039        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1040            release_channel,
1041            app_commit_sha,
1042            installed_version,
1043            fetched_sha,
1044            status,
1045        );
1046
1047        assert_eq!(newer_version.unwrap(), None);
1048    }
1049
1050    #[test]
1051    fn test_nightly_does_update_when_fetched_sha_is_not_same() {
1052        let release_channel = ReleaseChannel::Nightly;
1053        let app_commit_sha = Ok(Some("a".to_string()));
1054        let installed_version = SemanticVersion::new(1, 0, 0);
1055        let status = AutoUpdateStatus::Idle;
1056        let fetched_sha = "b".to_string();
1057
1058        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1059            release_channel,
1060            app_commit_sha,
1061            installed_version,
1062            fetched_sha.clone(),
1063            status,
1064        );
1065
1066        assert_eq!(
1067            newer_version.unwrap(),
1068            Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1069        );
1070    }
1071
1072    #[test]
1073    fn test_nightly_does_not_update_when_fetched_sha_is_same_as_cached() {
1074        let release_channel = ReleaseChannel::Nightly;
1075        let app_commit_sha = Ok(Some("a".to_string()));
1076        let installed_version = SemanticVersion::new(1, 0, 0);
1077        let status = AutoUpdateStatus::Updated {
1078            binary_path: PathBuf::new(),
1079            version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1080        };
1081        let fetched_sha = "b".to_string();
1082
1083        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1084            release_channel,
1085            app_commit_sha,
1086            installed_version,
1087            fetched_sha,
1088            status,
1089        );
1090
1091        assert_eq!(newer_version.unwrap(), None);
1092    }
1093
1094    #[test]
1095    fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
1096        let release_channel = ReleaseChannel::Nightly;
1097        let app_commit_sha = Ok(Some("a".to_string()));
1098        let installed_version = SemanticVersion::new(1, 0, 0);
1099        let status = AutoUpdateStatus::Updated {
1100            binary_path: PathBuf::new(),
1101            version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1102        };
1103        let fetched_sha = "c".to_string();
1104
1105        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1106            release_channel,
1107            app_commit_sha,
1108            installed_version,
1109            fetched_sha.clone(),
1110            status,
1111        );
1112
1113        assert_eq!(
1114            newer_version.unwrap(),
1115            Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1116        );
1117    }
1118
1119    #[test]
1120    fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
1121        let release_channel = ReleaseChannel::Nightly;
1122        let app_commit_sha = Ok(None);
1123        let installed_version = SemanticVersion::new(1, 0, 0);
1124        let status = AutoUpdateStatus::Idle;
1125        let fetched_sha = "a".to_string();
1126
1127        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1128            release_channel,
1129            app_commit_sha,
1130            installed_version,
1131            fetched_sha.clone(),
1132            status,
1133        );
1134
1135        assert_eq!(
1136            newer_version.unwrap(),
1137            Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1138        );
1139    }
1140
1141    #[test]
1142    fn test_nightly_does_not_update_when_cached_update_is_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1143     {
1144        let release_channel = ReleaseChannel::Nightly;
1145        let app_commit_sha = Ok(None);
1146        let installed_version = SemanticVersion::new(1, 0, 0);
1147        let status = AutoUpdateStatus::Updated {
1148            binary_path: PathBuf::new(),
1149            version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1150        };
1151        let fetched_sha = "b".to_string();
1152
1153        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1154            release_channel,
1155            app_commit_sha,
1156            installed_version,
1157            fetched_sha,
1158            status,
1159        );
1160
1161        assert_eq!(newer_version.unwrap(), None);
1162    }
1163
1164    #[test]
1165    fn test_nightly_does_update_when_cached_update_is_not_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1166     {
1167        let release_channel = ReleaseChannel::Nightly;
1168        let app_commit_sha = Ok(None);
1169        let installed_version = SemanticVersion::new(1, 0, 0);
1170        let status = AutoUpdateStatus::Updated {
1171            binary_path: PathBuf::new(),
1172            version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1173        };
1174        let fetched_sha = "c".to_string();
1175
1176        let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1177            release_channel,
1178            app_commit_sha,
1179            installed_version,
1180            fetched_sha.clone(),
1181            status,
1182        );
1183
1184        assert_eq!(
1185            newer_version.unwrap(),
1186            Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1187        );
1188    }
1189}