Skip to repository content1857 lines · 61.5 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:40:50.249Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
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, 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