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