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