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