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