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