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 DismissMessage,
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)]
59pub enum AutoUpdateStatus {
60 Idle,
61 Checking,
62 Downloading { version: VersionCheckType },
63 Installing { version: VersionCheckType },
64 Updated { version: VersionCheckType },
65 Errored { error: Arc<anyhow::Error> },
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 error: Arc::new(error),
388 }
389 }
390 };
391
392 cx.notify();
393 }
394 })
395 .ok()
396 }));
397 }
398
399 pub fn current_version(&self) -> SemanticVersion {
400 self.current_version
401 }
402
403 pub fn status(&self) -> AutoUpdateStatus {
404 self.status.clone()
405 }
406
407 pub fn dismiss(&mut self, cx: &mut Context<Self>) -> bool {
408 if let AutoUpdateStatus::Idle = self.status {
409 return false;
410 }
411 self.status = AutoUpdateStatus::Idle;
412 cx.notify();
413 true
414 }
415
416 // If you are packaging Zed and need to override the place it downloads SSH remotes from,
417 // you can override this function. You should also update get_remote_server_release_url to return
418 // Ok(None).
419 pub async fn download_remote_server_release(
420 os: &str,
421 arch: &str,
422 release_channel: ReleaseChannel,
423 version: Option<SemanticVersion>,
424 cx: &mut AsyncApp,
425 ) -> Result<PathBuf> {
426 let this = cx.update(|cx| {
427 cx.default_global::<GlobalAutoUpdate>()
428 .0
429 .clone()
430 .context("auto-update not initialized")
431 })??;
432
433 let release = Self::get_release(
434 &this,
435 "zed-remote-server",
436 os,
437 arch,
438 version,
439 Some(release_channel),
440 cx,
441 )
442 .await?;
443
444 let servers_dir = paths::remote_servers_dir();
445 let channel_dir = servers_dir.join(release_channel.dev_name());
446 let platform_dir = channel_dir.join(format!("{}-{}", os, arch));
447 let version_path = platform_dir.join(format!("{}.gz", release.version));
448 smol::fs::create_dir_all(&platform_dir).await.ok();
449
450 let client = this.read_with(cx, |this, _| this.http_client.clone())?;
451
452 if smol::fs::metadata(&version_path).await.is_err() {
453 log::info!(
454 "downloading zed-remote-server {os} {arch} version {}",
455 release.version
456 );
457 download_remote_server_binary(&version_path, release, client, cx).await?;
458 }
459
460 Ok(version_path)
461 }
462
463 pub async fn get_remote_server_release_url(
464 os: &str,
465 arch: &str,
466 release_channel: ReleaseChannel,
467 version: Option<SemanticVersion>,
468 cx: &mut AsyncApp,
469 ) -> Result<Option<(String, String)>> {
470 let this = cx.update(|cx| {
471 cx.default_global::<GlobalAutoUpdate>()
472 .0
473 .clone()
474 .context("auto-update not initialized")
475 })??;
476
477 let release = Self::get_release(
478 &this,
479 "zed-remote-server",
480 os,
481 arch,
482 version,
483 Some(release_channel),
484 cx,
485 )
486 .await?;
487
488 let update_request_body = build_remote_server_update_request_body(cx)?;
489 let body = serde_json::to_string(&update_request_body)?;
490
491 Ok(Some((release.url, body)))
492 }
493
494 async fn get_release(
495 this: &Entity<Self>,
496 asset: &str,
497 os: &str,
498 arch: &str,
499 version: Option<SemanticVersion>,
500 release_channel: Option<ReleaseChannel>,
501 cx: &mut AsyncApp,
502 ) -> Result<JsonRelease> {
503 let client = this.read_with(cx, |this, _| this.http_client.clone())?;
504
505 if let Some(version) = version {
506 let channel = release_channel.map(|c| c.dev_name()).unwrap_or("stable");
507
508 let url = format!("/api/releases/{channel}/{version}/{asset}-{os}-{arch}.gz?update=1",);
509
510 Ok(JsonRelease {
511 version: version.to_string(),
512 url: client.build_url(&url),
513 })
514 } else {
515 let mut url_string = client.build_url(&format!(
516 "/api/releases/latest?asset={}&os={}&arch={}",
517 asset, os, arch
518 ));
519 if let Some(param) = release_channel.and_then(|c| c.release_query_param()) {
520 url_string += "&";
521 url_string += param;
522 }
523
524 let mut response = client.get(&url_string, Default::default(), true).await?;
525 let mut body = Vec::new();
526 response.body_mut().read_to_end(&mut body).await?;
527
528 anyhow::ensure!(
529 response.status().is_success(),
530 "failed to fetch release: {:?}",
531 String::from_utf8_lossy(&body),
532 );
533
534 serde_json::from_slice(body.as_slice()).with_context(|| {
535 format!(
536 "error deserializing release {:?}",
537 String::from_utf8_lossy(&body),
538 )
539 })
540 }
541 }
542
543 async fn get_latest_release(
544 this: &Entity<Self>,
545 asset: &str,
546 os: &str,
547 arch: &str,
548 release_channel: Option<ReleaseChannel>,
549 cx: &mut AsyncApp,
550 ) -> Result<JsonRelease> {
551 Self::get_release(this, asset, os, arch, None, release_channel, cx).await
552 }
553
554 async fn update(this: Entity<Self>, mut cx: AsyncApp) -> Result<()> {
555 let (client, installed_version, previous_status, release_channel) =
556 this.read_with(&cx, |this, cx| {
557 (
558 this.http_client.clone(),
559 this.current_version,
560 this.status.clone(),
561 ReleaseChannel::try_global(cx),
562 )
563 })?;
564
565 Self::check_dependencies()?;
566
567 this.update(&mut cx, |this, cx| {
568 this.status = AutoUpdateStatus::Checking;
569 log::info!("Auto Update: checking for updates");
570 cx.notify();
571 })?;
572
573 let fetched_release_data =
574 Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
575 let fetched_version = fetched_release_data.clone().version;
576 let app_commit_sha = cx.update(|cx| AppCommitSha::try_global(cx).map(|sha| sha.full()));
577 let newer_version = Self::check_if_fetched_version_is_newer(
578 *RELEASE_CHANNEL,
579 app_commit_sha,
580 installed_version,
581 fetched_version,
582 previous_status.clone(),
583 )?;
584
585 let Some(newer_version) = newer_version else {
586 return this.update(&mut cx, |this, cx| {
587 let status = match previous_status {
588 AutoUpdateStatus::Updated { .. } => previous_status,
589 _ => AutoUpdateStatus::Idle,
590 };
591 this.status = status;
592 cx.notify();
593 });
594 };
595
596 this.update(&mut cx, |this, cx| {
597 this.status = AutoUpdateStatus::Downloading {
598 version: newer_version.clone(),
599 };
600 cx.notify();
601 })?;
602
603 let installer_dir = InstallerDir::new().await?;
604 let target_path = Self::target_path(&installer_dir).await?;
605 download_release(&target_path, fetched_release_data, client, &cx).await?;
606
607 this.update(&mut cx, |this, cx| {
608 this.status = AutoUpdateStatus::Installing {
609 version: newer_version.clone(),
610 };
611 cx.notify();
612 })?;
613
614 let new_binary_path = Self::install_release(installer_dir, target_path, &cx).await?;
615 if let Some(new_binary_path) = new_binary_path {
616 cx.update(|cx| cx.set_restart_path(new_binary_path))?;
617 }
618
619 this.update(&mut cx, |this, cx| {
620 this.set_should_show_update_notification(true, cx)
621 .detach_and_log_err(cx);
622 this.status = AutoUpdateStatus::Updated {
623 version: newer_version,
624 };
625 cx.notify();
626 })
627 }
628
629 fn check_if_fetched_version_is_newer(
630 release_channel: ReleaseChannel,
631 app_commit_sha: Result<Option<String>>,
632 installed_version: SemanticVersion,
633 fetched_version: String,
634 status: AutoUpdateStatus,
635 ) -> Result<Option<VersionCheckType>> {
636 let parsed_fetched_version = fetched_version.parse::<SemanticVersion>();
637
638 if let AutoUpdateStatus::Updated { version, .. } = status {
639 match version {
640 VersionCheckType::Sha(cached_version) => {
641 let should_download = fetched_version != cached_version.full();
642 let newer_version = should_download
643 .then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
644 return Ok(newer_version);
645 }
646 VersionCheckType::Semantic(cached_version) => {
647 return Self::check_if_fetched_version_is_newer_non_nightly(
648 cached_version,
649 parsed_fetched_version?,
650 );
651 }
652 }
653 }
654
655 match release_channel {
656 ReleaseChannel::Nightly => {
657 let should_download = app_commit_sha
658 .ok()
659 .flatten()
660 .map(|sha| fetched_version != sha)
661 .unwrap_or(true);
662 let newer_version = should_download
663 .then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
664 Ok(newer_version)
665 }
666 _ => Self::check_if_fetched_version_is_newer_non_nightly(
667 installed_version,
668 parsed_fetched_version?,
669 ),
670 }
671 }
672
673 fn check_dependencies() -> Result<()> {
674 #[cfg(not(target_os = "windows"))]
675 anyhow::ensure!(
676 which::which("rsync").is_ok(),
677 "Aborting. Could not find rsync which is required for auto-updates."
678 );
679 Ok(())
680 }
681
682 async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf> {
683 let filename = match OS {
684 "macos" => anyhow::Ok("Zed.dmg"),
685 "linux" => Ok("zed.tar.gz"),
686 "windows" => Ok("zed_editor_installer.exe"),
687 unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
688 }?;
689
690 Ok(installer_dir.path().join(filename))
691 }
692
693 async fn install_release(
694 installer_dir: InstallerDir,
695 target_path: PathBuf,
696 cx: &AsyncApp,
697 ) -> Result<Option<PathBuf>> {
698 match OS {
699 "macos" => install_release_macos(&installer_dir, target_path, cx).await,
700 "linux" => install_release_linux(&installer_dir, target_path, cx).await,
701 "windows" => install_release_windows(target_path).await,
702 unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
703 }
704 }
705
706 fn check_if_fetched_version_is_newer_non_nightly(
707 installed_version: SemanticVersion,
708 fetched_version: SemanticVersion,
709 ) -> Result<Option<VersionCheckType>> {
710 let should_download = fetched_version > installed_version;
711 let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
712 Ok(newer_version)
713 }
714
715 pub fn set_should_show_update_notification(
716 &self,
717 should_show: bool,
718 cx: &App,
719 ) -> Task<Result<()>> {
720 cx.background_spawn(async move {
721 if should_show {
722 KEY_VALUE_STORE
723 .write_kvp(
724 SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
725 "".to_string(),
726 )
727 .await?;
728 } else {
729 KEY_VALUE_STORE
730 .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
731 .await?;
732 }
733 Ok(())
734 })
735 }
736
737 pub fn should_show_update_notification(&self, cx: &App) -> Task<Result<bool>> {
738 cx.background_spawn(async move {
739 Ok(KEY_VALUE_STORE
740 .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
741 .is_some())
742 })
743 }
744}
745
746async fn download_remote_server_binary(
747 target_path: &PathBuf,
748 release: JsonRelease,
749 client: Arc<HttpClientWithUrl>,
750 cx: &AsyncApp,
751) -> Result<()> {
752 let temp = tempfile::Builder::new().tempfile_in(remote_servers_dir())?;
753 let mut temp_file = File::create(&temp).await?;
754 let update_request_body = build_remote_server_update_request_body(cx)?;
755 let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?);
756
757 let mut response = client.get(&release.url, request_body, true).await?;
758 anyhow::ensure!(
759 response.status().is_success(),
760 "failed to download remote server release: {:?}",
761 response.status()
762 );
763 smol::io::copy(response.body_mut(), &mut temp_file).await?;
764 smol::fs::rename(&temp, &target_path).await?;
765
766 Ok(())
767}
768
769fn build_remote_server_update_request_body(cx: &AsyncApp) -> Result<UpdateRequestBody> {
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 Ok(UpdateRequestBody {
787 installation_id,
788 release_channel,
789 telemetry: telemetry_enabled,
790 is_staff,
791 destination: "remote",
792 })
793}
794
795async fn download_release(
796 target_path: &Path,
797 release: JsonRelease,
798 client: Arc<HttpClientWithUrl>,
799 cx: &AsyncApp,
800) -> Result<()> {
801 let mut target_file = File::create(&target_path).await?;
802
803 let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
804 let telemetry = Client::global(cx).telemetry().clone();
805 let is_staff = telemetry.is_staff();
806 let installation_id = telemetry.installation_id();
807 let release_channel =
808 ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
809 let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
810
811 (
812 installation_id,
813 release_channel,
814 telemetry_enabled,
815 is_staff,
816 )
817 })?;
818
819 let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
820 installation_id,
821 release_channel,
822 telemetry: telemetry_enabled,
823 is_staff,
824 destination: "local",
825 })?);
826
827 let mut response = client.get(&release.url, request_body, true).await?;
828 smol::io::copy(response.body_mut(), &mut target_file).await?;
829 log::info!("downloaded update. path:{:?}", target_path);
830
831 Ok(())
832}
833
834async fn install_release_linux(
835 temp_dir: &InstallerDir,
836 downloaded_tar_gz: PathBuf,
837 cx: &AsyncApp,
838) -> Result<Option<PathBuf>> {
839 let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
840 let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
841 let running_app_path = cx.update(|cx| cx.app_path())??;
842
843 let extracted = temp_dir.path().join("zed");
844 fs::create_dir_all(&extracted)
845 .await
846 .context("failed to create directory into which to extract update")?;
847
848 let output = Command::new("tar")
849 .arg("-xzf")
850 .arg(&downloaded_tar_gz)
851 .arg("-C")
852 .arg(&extracted)
853 .output()
854 .await?;
855
856 anyhow::ensure!(
857 output.status.success(),
858 "failed to extract {:?} to {:?}: {:?}",
859 downloaded_tar_gz,
860 extracted,
861 String::from_utf8_lossy(&output.stderr)
862 );
863
864 let suffix = if channel != "stable" {
865 format!("-{}", channel)
866 } else {
867 String::default()
868 };
869 let app_folder_name = format!("zed{}.app", suffix);
870
871 let from = extracted.join(&app_folder_name);
872 let mut to = home_dir.join(".local");
873
874 let expected_suffix = format!("{}/libexec/zed-editor", app_folder_name);
875
876 if let Some(prefix) = running_app_path
877 .to_str()
878 .and_then(|str| str.strip_suffix(&expected_suffix))
879 {
880 to = PathBuf::from(prefix);
881 }
882
883 let output = Command::new("rsync")
884 .args(["-av", "--delete"])
885 .arg(&from)
886 .arg(&to)
887 .output()
888 .await?;
889
890 anyhow::ensure!(
891 output.status.success(),
892 "failed to copy Zed update from {:?} to {:?}: {:?}",
893 from,
894 to,
895 String::from_utf8_lossy(&output.stderr)
896 );
897
898 Ok(Some(to.join(expected_suffix)))
899}
900
901async fn install_release_macos(
902 temp_dir: &InstallerDir,
903 downloaded_dmg: PathBuf,
904 cx: &AsyncApp,
905) -> Result<Option<PathBuf>> {
906 let running_app_path = cx.update(|cx| cx.app_path())??;
907 let running_app_filename = running_app_path
908 .file_name()
909 .with_context(|| format!("invalid running app path {running_app_path:?}"))?;
910
911 let mount_path = temp_dir.path().join("Zed");
912 let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
913
914 mounted_app_path.push("/");
915 let output = Command::new("hdiutil")
916 .args(["attach", "-nobrowse"])
917 .arg(&downloaded_dmg)
918 .arg("-mountroot")
919 .arg(temp_dir.path())
920 .output()
921 .await?;
922
923 anyhow::ensure!(
924 output.status.success(),
925 "failed to mount: {:?}",
926 String::from_utf8_lossy(&output.stderr)
927 );
928
929 // Create an MacOsUnmounter that will be dropped (and thus unmount the disk) when this function exits
930 let _unmounter = MacOsUnmounter {
931 mount_path: mount_path.clone(),
932 };
933
934 let output = Command::new("rsync")
935 .args(["-av", "--delete"])
936 .arg(&mounted_app_path)
937 .arg(&running_app_path)
938 .output()
939 .await?;
940
941 anyhow::ensure!(
942 output.status.success(),
943 "failed to copy app: {:?}",
944 String::from_utf8_lossy(&output.stderr)
945 );
946
947 Ok(None)
948}
949
950async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option<PathBuf>> {
951 let output = Command::new(downloaded_installer)
952 .arg("/verysilent")
953 .arg("/update=true")
954 .arg("!desktopicon")
955 .arg("!quicklaunchicon")
956 .output()
957 .await?;
958 anyhow::ensure!(
959 output.status.success(),
960 "failed to start installer: {:?}",
961 String::from_utf8_lossy(&output.stderr)
962 );
963 // We return the path to the update helper program, because it will
964 // perform the final steps of the update process, copying the new binary,
965 // deleting the old one, and launching the new binary.
966 let helper_path = std::env::current_exe()?
967 .parent()
968 .context("No parent dir for Zed.exe")?
969 .join("tools\\auto_update_helper.exe");
970 Ok(Some(helper_path))
971}
972
973pub fn finalize_auto_update_on_quit() {
974 let Some(installer_path) = std::env::current_exe()
975 .ok()
976 .and_then(|p| p.parent().map(|p| p.join("updates")))
977 else {
978 return;
979 };
980
981 // The installer will create a flag file after it finishes updating
982 let flag_file = installer_path.join("versions.txt");
983 if flag_file.exists()
984 && let Some(helper) = installer_path
985 .parent()
986 .map(|p| p.join("tools\\auto_update_helper.exe"))
987 {
988 let mut command = std::process::Command::new(helper);
989 command.arg("--launch");
990 command.arg("false");
991 let _ = command.spawn();
992 }
993}
994
995#[cfg(test)]
996mod tests {
997 use gpui::TestAppContext;
998 use settings::default_settings;
999
1000 use super::*;
1001
1002 #[gpui::test]
1003 fn test_auto_update_defaults_to_true(cx: &mut TestAppContext) {
1004 cx.update(|cx| {
1005 let mut store = SettingsStore::new(cx);
1006 store
1007 .set_default_settings(&default_settings(), cx)
1008 .expect("Unable to set default settings");
1009 store
1010 .set_user_settings("{}", cx)
1011 .expect("Unable to set user settings");
1012 cx.set_global(store);
1013 AutoUpdateSetting::register(cx);
1014 assert!(AutoUpdateSetting::get_global(cx).0);
1015 });
1016 }
1017
1018 #[test]
1019 fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
1020 let release_channel = ReleaseChannel::Stable;
1021 let app_commit_sha = Ok(Some("a".to_string()));
1022 let installed_version = SemanticVersion::new(1, 0, 0);
1023 let status = AutoUpdateStatus::Idle;
1024 let fetched_version = SemanticVersion::new(1, 0, 0);
1025
1026 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1027 release_channel,
1028 app_commit_sha,
1029 installed_version,
1030 fetched_version.to_string(),
1031 status,
1032 );
1033
1034 assert_eq!(newer_version.unwrap(), None);
1035 }
1036
1037 #[test]
1038 fn test_stable_does_update_when_fetched_version_is_higher() {
1039 let release_channel = ReleaseChannel::Stable;
1040 let app_commit_sha = Ok(Some("a".to_string()));
1041 let installed_version = SemanticVersion::new(1, 0, 0);
1042 let status = AutoUpdateStatus::Idle;
1043 let fetched_version = SemanticVersion::new(1, 0, 1);
1044
1045 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1046 release_channel,
1047 app_commit_sha,
1048 installed_version,
1049 fetched_version.to_string(),
1050 status,
1051 );
1052
1053 assert_eq!(
1054 newer_version.unwrap(),
1055 Some(VersionCheckType::Semantic(fetched_version))
1056 );
1057 }
1058
1059 #[test]
1060 fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
1061 let release_channel = ReleaseChannel::Stable;
1062 let app_commit_sha = Ok(Some("a".to_string()));
1063 let installed_version = SemanticVersion::new(1, 0, 0);
1064 let status = AutoUpdateStatus::Updated {
1065 version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
1066 };
1067 let fetched_version = SemanticVersion::new(1, 0, 1);
1068
1069 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1070 release_channel,
1071 app_commit_sha,
1072 installed_version,
1073 fetched_version.to_string(),
1074 status,
1075 );
1076
1077 assert_eq!(newer_version.unwrap(), None);
1078 }
1079
1080 #[test]
1081 fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
1082 let release_channel = ReleaseChannel::Stable;
1083 let app_commit_sha = Ok(Some("a".to_string()));
1084 let installed_version = SemanticVersion::new(1, 0, 0);
1085 let status = AutoUpdateStatus::Updated {
1086 version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
1087 };
1088 let fetched_version = SemanticVersion::new(1, 0, 2);
1089
1090 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1091 release_channel,
1092 app_commit_sha,
1093 installed_version,
1094 fetched_version.to_string(),
1095 status,
1096 );
1097
1098 assert_eq!(
1099 newer_version.unwrap(),
1100 Some(VersionCheckType::Semantic(fetched_version))
1101 );
1102 }
1103
1104 #[test]
1105 fn test_nightly_does_not_update_when_fetched_sha_is_same() {
1106 let release_channel = ReleaseChannel::Nightly;
1107 let app_commit_sha = Ok(Some("a".to_string()));
1108 let installed_version = SemanticVersion::new(1, 0, 0);
1109 let status = AutoUpdateStatus::Idle;
1110 let fetched_sha = "a".to_string();
1111
1112 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1113 release_channel,
1114 app_commit_sha,
1115 installed_version,
1116 fetched_sha,
1117 status,
1118 );
1119
1120 assert_eq!(newer_version.unwrap(), None);
1121 }
1122
1123 #[test]
1124 fn test_nightly_does_update_when_fetched_sha_is_not_same() {
1125 let release_channel = ReleaseChannel::Nightly;
1126 let app_commit_sha = Ok(Some("a".to_string()));
1127 let installed_version = SemanticVersion::new(1, 0, 0);
1128 let status = AutoUpdateStatus::Idle;
1129 let fetched_sha = "b".to_string();
1130
1131 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1132 release_channel,
1133 app_commit_sha,
1134 installed_version,
1135 fetched_sha.clone(),
1136 status,
1137 );
1138
1139 assert_eq!(
1140 newer_version.unwrap(),
1141 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1142 );
1143 }
1144
1145 #[test]
1146 fn test_nightly_does_not_update_when_fetched_sha_is_same_as_cached() {
1147 let release_channel = ReleaseChannel::Nightly;
1148 let app_commit_sha = Ok(Some("a".to_string()));
1149 let installed_version = SemanticVersion::new(1, 0, 0);
1150 let status = AutoUpdateStatus::Updated {
1151 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1152 };
1153 let fetched_sha = "b".to_string();
1154
1155 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1156 release_channel,
1157 app_commit_sha,
1158 installed_version,
1159 fetched_sha,
1160 status,
1161 );
1162
1163 assert_eq!(newer_version.unwrap(), None);
1164 }
1165
1166 #[test]
1167 fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
1168 let release_channel = ReleaseChannel::Nightly;
1169 let app_commit_sha = Ok(Some("a".to_string()));
1170 let installed_version = SemanticVersion::new(1, 0, 0);
1171 let status = AutoUpdateStatus::Updated {
1172 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1173 };
1174 let fetched_sha = "c".to_string();
1175
1176 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1177 release_channel,
1178 app_commit_sha,
1179 installed_version,
1180 fetched_sha.clone(),
1181 status,
1182 );
1183
1184 assert_eq!(
1185 newer_version.unwrap(),
1186 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1187 );
1188 }
1189
1190 #[test]
1191 fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
1192 let release_channel = ReleaseChannel::Nightly;
1193 let app_commit_sha = Ok(None);
1194 let installed_version = SemanticVersion::new(1, 0, 0);
1195 let status = AutoUpdateStatus::Idle;
1196 let fetched_sha = "a".to_string();
1197
1198 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1199 release_channel,
1200 app_commit_sha,
1201 installed_version,
1202 fetched_sha.clone(),
1203 status,
1204 );
1205
1206 assert_eq!(
1207 newer_version.unwrap(),
1208 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1209 );
1210 }
1211
1212 #[test]
1213 fn test_nightly_does_not_update_when_cached_update_is_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1214 {
1215 let release_channel = ReleaseChannel::Nightly;
1216 let app_commit_sha = Ok(None);
1217 let installed_version = SemanticVersion::new(1, 0, 0);
1218 let status = AutoUpdateStatus::Updated {
1219 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1220 };
1221 let fetched_sha = "b".to_string();
1222
1223 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1224 release_channel,
1225 app_commit_sha,
1226 installed_version,
1227 fetched_sha,
1228 status,
1229 );
1230
1231 assert_eq!(newer_version.unwrap(), None);
1232 }
1233
1234 #[test]
1235 fn test_nightly_does_update_when_cached_update_is_not_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1236 {
1237 let release_channel = ReleaseChannel::Nightly;
1238 let app_commit_sha = Ok(None);
1239 let installed_version = SemanticVersion::new(1, 0, 0);
1240 let status = AutoUpdateStatus::Updated {
1241 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1242 };
1243 let fetched_sha = "c".to_string();
1244
1245 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1246 release_channel,
1247 app_commit_sha,
1248 installed_version,
1249 fetched_sha.clone(),
1250 status,
1251 );
1252
1253 assert_eq!(
1254 newer_version.unwrap(),
1255 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1256 );
1257 }
1258}