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