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