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 const 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) -> 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 #[cfg(target_os = "windows")]
314 let quit_subscription = Some(cx.on_app_quit(|_, _| finalize_auto_update_on_quit()));
315 #[cfg(not(target_os = "windows"))]
316 let quit_subscription = None;
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 const 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 "Could not auto-update because the required rsync utility was not found."
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.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")
946 .join("auto_update_helper.exe");
947 Ok(Some(helper_path))
948}
949
950pub async fn finalize_auto_update_on_quit() {
951 let Some(installer_path) = std::env::current_exe()
952 .ok()
953 .and_then(|p| p.parent().map(|p| p.join("updates")))
954 else {
955 return;
956 };
957
958 // The installer will create a flag file after it finishes updating
959 let flag_file = installer_path.join("versions.txt");
960 if flag_file.exists()
961 && let Some(helper) = installer_path
962 .parent()
963 .map(|p| p.join("tools").join("auto_update_helper.exe"))
964 {
965 let mut command = smol::process::Command::new(helper);
966 command.arg("--launch");
967 command.arg("false");
968 if let Ok(mut cmd) = command.spawn() {
969 _ = cmd.status().await;
970 }
971 }
972}
973
974#[cfg(test)]
975mod tests {
976 use gpui::TestAppContext;
977 use settings::default_settings;
978
979 use super::*;
980
981 #[gpui::test]
982 fn test_auto_update_defaults_to_true(cx: &mut TestAppContext) {
983 cx.update(|cx| {
984 let mut store = SettingsStore::new(cx, &settings::default_settings());
985 store
986 .set_default_settings(&default_settings(), cx)
987 .expect("Unable to set default settings");
988 store
989 .set_user_settings("{}", cx)
990 .expect("Unable to set user settings");
991 cx.set_global(store);
992 AutoUpdateSetting::register(cx);
993 assert!(AutoUpdateSetting::get_global(cx).0);
994 });
995 }
996
997 #[test]
998 fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
999 let release_channel = ReleaseChannel::Stable;
1000 let app_commit_sha = Ok(Some("a".to_string()));
1001 let installed_version = SemanticVersion::new(1, 0, 0);
1002 let status = AutoUpdateStatus::Idle;
1003 let fetched_version = SemanticVersion::new(1, 0, 0);
1004
1005 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1006 release_channel,
1007 app_commit_sha,
1008 installed_version,
1009 fetched_version.to_string(),
1010 status,
1011 );
1012
1013 assert_eq!(newer_version.unwrap(), None);
1014 }
1015
1016 #[test]
1017 fn test_stable_does_update_when_fetched_version_is_higher() {
1018 let release_channel = ReleaseChannel::Stable;
1019 let app_commit_sha = Ok(Some("a".to_string()));
1020 let installed_version = SemanticVersion::new(1, 0, 0);
1021 let status = AutoUpdateStatus::Idle;
1022 let fetched_version = SemanticVersion::new(1, 0, 1);
1023
1024 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1025 release_channel,
1026 app_commit_sha,
1027 installed_version,
1028 fetched_version.to_string(),
1029 status,
1030 );
1031
1032 assert_eq!(
1033 newer_version.unwrap(),
1034 Some(VersionCheckType::Semantic(fetched_version))
1035 );
1036 }
1037
1038 #[test]
1039 fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
1040 let release_channel = ReleaseChannel::Stable;
1041 let app_commit_sha = Ok(Some("a".to_string()));
1042 let installed_version = SemanticVersion::new(1, 0, 0);
1043 let status = AutoUpdateStatus::Updated {
1044 version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
1045 };
1046 let fetched_version = SemanticVersion::new(1, 0, 1);
1047
1048 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1049 release_channel,
1050 app_commit_sha,
1051 installed_version,
1052 fetched_version.to_string(),
1053 status,
1054 );
1055
1056 assert_eq!(newer_version.unwrap(), None);
1057 }
1058
1059 #[test]
1060 fn test_stable_does_update_when_fetched_version_is_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, 2);
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!(
1078 newer_version.unwrap(),
1079 Some(VersionCheckType::Semantic(fetched_version))
1080 );
1081 }
1082
1083 #[test]
1084 fn test_nightly_does_not_update_when_fetched_sha_is_same() {
1085 let release_channel = ReleaseChannel::Nightly;
1086 let app_commit_sha = Ok(Some("a".to_string()));
1087 let installed_version = SemanticVersion::new(1, 0, 0);
1088 let status = AutoUpdateStatus::Idle;
1089 let fetched_sha = "a".to_string();
1090
1091 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1092 release_channel,
1093 app_commit_sha,
1094 installed_version,
1095 fetched_sha,
1096 status,
1097 );
1098
1099 assert_eq!(newer_version.unwrap(), None);
1100 }
1101
1102 #[test]
1103 fn test_nightly_does_update_when_fetched_sha_is_not_same() {
1104 let release_channel = ReleaseChannel::Nightly;
1105 let app_commit_sha = Ok(Some("a".to_string()));
1106 let installed_version = SemanticVersion::new(1, 0, 0);
1107 let status = AutoUpdateStatus::Idle;
1108 let fetched_sha = "b".to_string();
1109
1110 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1111 release_channel,
1112 app_commit_sha,
1113 installed_version,
1114 fetched_sha.clone(),
1115 status,
1116 );
1117
1118 assert_eq!(
1119 newer_version.unwrap(),
1120 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1121 );
1122 }
1123
1124 #[test]
1125 fn test_nightly_does_not_update_when_fetched_sha_is_same_as_cached() {
1126 let release_channel = ReleaseChannel::Nightly;
1127 let app_commit_sha = Ok(Some("a".to_string()));
1128 let installed_version = SemanticVersion::new(1, 0, 0);
1129 let status = AutoUpdateStatus::Updated {
1130 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1131 };
1132 let fetched_sha = "b".to_string();
1133
1134 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1135 release_channel,
1136 app_commit_sha,
1137 installed_version,
1138 fetched_sha,
1139 status,
1140 );
1141
1142 assert_eq!(newer_version.unwrap(), None);
1143 }
1144
1145 #[test]
1146 fn test_nightly_does_update_when_fetched_sha_is_not_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 = "c".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.clone(),
1160 status,
1161 );
1162
1163 assert_eq!(
1164 newer_version.unwrap(),
1165 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1166 );
1167 }
1168
1169 #[test]
1170 fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
1171 let release_channel = ReleaseChannel::Nightly;
1172 let app_commit_sha = Ok(None);
1173 let installed_version = SemanticVersion::new(1, 0, 0);
1174 let status = AutoUpdateStatus::Idle;
1175 let fetched_sha = "a".to_string();
1176
1177 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1178 release_channel,
1179 app_commit_sha,
1180 installed_version,
1181 fetched_sha.clone(),
1182 status,
1183 );
1184
1185 assert_eq!(
1186 newer_version.unwrap(),
1187 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1188 );
1189 }
1190
1191 #[test]
1192 fn test_nightly_does_not_update_when_cached_update_is_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1193 {
1194 let release_channel = ReleaseChannel::Nightly;
1195 let app_commit_sha = Ok(None);
1196 let installed_version = SemanticVersion::new(1, 0, 0);
1197 let status = AutoUpdateStatus::Updated {
1198 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1199 };
1200 let fetched_sha = "b".to_string();
1201
1202 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1203 release_channel,
1204 app_commit_sha,
1205 installed_version,
1206 fetched_sha,
1207 status,
1208 );
1209
1210 assert_eq!(newer_version.unwrap(), None);
1211 }
1212
1213 #[test]
1214 fn test_nightly_does_update_when_cached_update_is_not_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1215 {
1216 let release_channel = ReleaseChannel::Nightly;
1217 let app_commit_sha = Ok(None);
1218 let installed_version = SemanticVersion::new(1, 0, 0);
1219 let status = AutoUpdateStatus::Updated {
1220 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1221 };
1222 let fetched_sha = "c".to_string();
1223
1224 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1225 release_channel,
1226 app_commit_sha,
1227 installed_version,
1228 fetched_sha.clone(),
1229 status,
1230 );
1231
1232 assert_eq!(
1233 newer_version.unwrap(),
1234 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1235 );
1236 }
1237}