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