auto_update.rs

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