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