Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:40:50.249Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

auto_update.rs

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