auto_update.rs

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