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