auto_update.rs

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