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