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