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