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