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