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