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