1use anyhow::{Context as _, Result};
2use client::Client;
3use db::kvp::KEY_VALUE_STORE;
4use gpui::{
5 App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, SemanticVersion,
6 Task, Window, actions,
7};
8use http_client::{HttpClient, HttpClientWithUrl};
9use paths::remote_servers_dir;
10use release_channel::{AppCommitSha, ReleaseChannel};
11use serde::{Deserialize, Serialize};
12use settings::{RegisterSetting, Settings, SettingsStore};
13use smol::{fs, io::AsyncReadExt};
14use smol::{fs::File, process::Command};
15use std::mem;
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 DismissMessage,
38 /// Opens the release notes for the current version in a browser.
39 ViewReleaseNotes,
40 ]
41);
42
43#[derive(Clone, Debug, PartialEq, Eq)]
44pub enum VersionCheckType {
45 Sha(AppCommitSha),
46 Semantic(SemanticVersion),
47}
48
49#[derive(Serialize, Debug)]
50pub struct AssetQuery<'a> {
51 asset: &'a str,
52 os: &'a str,
53 arch: &'a str,
54 metrics_id: Option<&'a str>,
55 system_id: Option<&'a str>,
56 is_staff: Option<bool>,
57}
58
59#[derive(Clone, Debug)]
60pub enum AutoUpdateStatus {
61 Idle,
62 Checking,
63 Downloading { version: VersionCheckType },
64 Installing { version: VersionCheckType },
65 Updated { version: VersionCheckType },
66 Errored { error: Arc<anyhow::Error> },
67}
68
69impl PartialEq for AutoUpdateStatus {
70 fn eq(&self, other: &Self) -> bool {
71 match (self, other) {
72 (AutoUpdateStatus::Idle, AutoUpdateStatus::Idle) => true,
73 (AutoUpdateStatus::Checking, AutoUpdateStatus::Checking) => true,
74 (
75 AutoUpdateStatus::Downloading { version: v1 },
76 AutoUpdateStatus::Downloading { version: v2 },
77 ) => v1 == v2,
78 (
79 AutoUpdateStatus::Installing { version: v1 },
80 AutoUpdateStatus::Installing { version: v2 },
81 ) => v1 == v2,
82 (
83 AutoUpdateStatus::Updated { version: v1 },
84 AutoUpdateStatus::Updated { version: v2 },
85 ) => v1 == v2,
86 (AutoUpdateStatus::Errored { error: e1 }, AutoUpdateStatus::Errored { error: e2 }) => {
87 e1.to_string() == e2.to_string()
88 }
89 _ => false,
90 }
91 }
92}
93
94impl AutoUpdateStatus {
95 pub fn is_updated(&self) -> bool {
96 matches!(self, Self::Updated { .. })
97 }
98}
99
100pub struct AutoUpdater {
101 status: AutoUpdateStatus,
102 current_version: SemanticVersion,
103 client: Arc<Client>,
104 pending_poll: Option<Task<Option<()>>>,
105 quit_subscription: Option<gpui::Subscription>,
106}
107
108#[derive(Deserialize, Serialize, Clone, Debug)]
109pub struct ReleaseAsset {
110 pub version: String,
111 pub url: String,
112}
113
114struct MacOsUnmounter<'a> {
115 mount_path: PathBuf,
116 background_executor: &'a BackgroundExecutor,
117}
118
119impl Drop for MacOsUnmounter<'_> {
120 fn drop(&mut self) {
121 let mount_path = mem::take(&mut self.mount_path);
122 self.background_executor
123 .spawn(async move {
124 let unmount_output = Command::new("hdiutil")
125 .args(["detach", "-force"])
126 .arg(&mount_path)
127 .output()
128 .await;
129 match unmount_output {
130 Ok(output) if output.status.success() => {
131 log::info!("Successfully unmounted the disk image");
132 }
133 Ok(output) => {
134 log::error!(
135 "Failed to unmount disk image: {:?}",
136 String::from_utf8_lossy(&output.stderr)
137 );
138 }
139 Err(error) => {
140 log::error!("Error while trying to unmount disk image: {:?}", error);
141 }
142 }
143 })
144 .detach();
145 }
146}
147
148#[derive(Clone, Copy, Debug, RegisterSetting)]
149struct AutoUpdateSetting(bool);
150
151/// Whether or not to automatically check for updates.
152///
153/// Default: true
154impl Settings for AutoUpdateSetting {
155 fn from_settings(content: &settings::SettingsContent) -> Self {
156 Self(content.auto_update.unwrap())
157 }
158}
159
160#[derive(Default)]
161struct GlobalAutoUpdate(Option<Entity<AutoUpdater>>);
162
163impl Global for GlobalAutoUpdate {}
164
165pub fn init(client: Arc<Client>, cx: &mut App) {
166 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
167 workspace.register_action(|_, action, 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, client, cx);
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.client.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, client: Arc<Client>, cx: &mut Context<Self>) -> Self {
325 // On windows, executable files cannot be overwritten while they are
326 // running, so we must wait to overwrite the application until quitting
327 // or restarting. When quitting the app, we spawn the auto update helper
328 // to finish the auto update process after Zed exits. When restarting
329 // the app after an update, we use `set_restart_path` to run the auto
330 // update helper instead of the app, so that it can overwrite the app
331 // and then spawn the new binary.
332 #[cfg(target_os = "windows")]
333 let quit_subscription = Some(cx.on_app_quit(|_, _| finalize_auto_update_on_quit()));
334 #[cfg(not(target_os = "windows"))]
335 let quit_subscription = None;
336
337 cx.on_app_restart(|this, _| {
338 this.quit_subscription.take();
339 })
340 .detach();
341
342 Self {
343 status: AutoUpdateStatus::Idle,
344 current_version,
345 client,
346 pending_poll: None,
347 quit_subscription,
348 }
349 }
350
351 pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
352 cx.spawn(async move |this, cx| {
353 if cfg!(target_os = "windows") {
354 use util::ResultExt;
355
356 cleanup_windows()
357 .await
358 .context("failed to cleanup old directories")
359 .log_err();
360 }
361
362 loop {
363 this.update(cx, |this, cx| this.poll(UpdateCheckType::Automatic, cx))?;
364 cx.background_executor().timer(POLL_INTERVAL).await;
365 }
366 })
367 }
368
369 pub fn poll(&mut self, check_type: UpdateCheckType, cx: &mut Context<Self>) {
370 if self.pending_poll.is_some() {
371 return;
372 }
373
374 cx.notify();
375
376 self.pending_poll = Some(cx.spawn(async move |this, cx| {
377 let result = Self::update(this.upgrade()?, cx).await;
378 this.update(cx, |this, cx| {
379 this.pending_poll = None;
380 if let Err(error) = result {
381 this.status = match check_type {
382 // Be quiet if the check was automated (e.g. when offline)
383 UpdateCheckType::Automatic => {
384 log::info!("auto-update check failed: error:{:?}", error);
385 AutoUpdateStatus::Idle
386 }
387 UpdateCheckType::Manual => {
388 log::error!("auto-update failed: error:{:?}", error);
389 AutoUpdateStatus::Errored {
390 error: Arc::new(error),
391 }
392 }
393 };
394
395 cx.notify();
396 }
397 })
398 .ok()
399 }));
400 }
401
402 pub fn current_version(&self) -> SemanticVersion {
403 self.current_version
404 }
405
406 pub fn status(&self) -> AutoUpdateStatus {
407 self.status.clone()
408 }
409
410 pub fn dismiss(&mut self, cx: &mut Context<Self>) -> bool {
411 if let AutoUpdateStatus::Idle = self.status {
412 return false;
413 }
414 self.status = AutoUpdateStatus::Idle;
415 cx.notify();
416 true
417 }
418
419 // If you are packaging Zed and need to override the place it downloads SSH remotes from,
420 // you can override this function. You should also update get_remote_server_release_url to return
421 // Ok(None).
422 pub async fn download_remote_server_release(
423 release_channel: ReleaseChannel,
424 version: Option<SemanticVersion>,
425 os: &str,
426 arch: &str,
427 set_status: impl Fn(&str, &mut AsyncApp) + Send + 'static,
428 cx: &mut AsyncApp,
429 ) -> Result<PathBuf> {
430 let this = cx.update(|cx| {
431 cx.default_global::<GlobalAutoUpdate>()
432 .0
433 .clone()
434 .context("auto-update not initialized")
435 })??;
436
437 set_status("Fetching remote server release", cx);
438 let release = Self::get_release_asset(
439 &this,
440 release_channel,
441 version,
442 "zed-remote-server",
443 os,
444 arch,
445 cx,
446 )
447 .await?;
448
449 let servers_dir = paths::remote_servers_dir();
450 let channel_dir = servers_dir.join(release_channel.dev_name());
451 let platform_dir = channel_dir.join(format!("{}-{}", os, arch));
452 let version_path = platform_dir.join(format!("{}.gz", release.version));
453 smol::fs::create_dir_all(&platform_dir).await.ok();
454
455 let client = this.read_with(cx, |this, _| this.client.http_client())?;
456
457 if smol::fs::metadata(&version_path).await.is_err() {
458 log::info!(
459 "downloading zed-remote-server {os} {arch} version {}",
460 release.version
461 );
462 set_status("Downloading remote server", cx);
463 download_remote_server_binary(&version_path, release, client).await?;
464 }
465
466 Ok(version_path)
467 }
468
469 pub async fn get_remote_server_release_url(
470 channel: ReleaseChannel,
471 version: Option<SemanticVersion>,
472 os: &str,
473 arch: &str,
474 cx: &mut AsyncApp,
475 ) -> Result<Option<String>> {
476 let this = cx.update(|cx| {
477 cx.default_global::<GlobalAutoUpdate>()
478 .0
479 .clone()
480 .context("auto-update not initialized")
481 })??;
482
483 let release =
484 Self::get_release_asset(&this, channel, version, "zed-remote-server", os, arch, cx)
485 .await?;
486
487 Ok(Some(release.url))
488 }
489
490 async fn get_release_asset(
491 this: &Entity<Self>,
492 release_channel: ReleaseChannel,
493 version: Option<SemanticVersion>,
494 asset: &str,
495 os: &str,
496 arch: &str,
497 cx: &mut AsyncApp,
498 ) -> Result<ReleaseAsset> {
499 let client = this.read_with(cx, |this, _| this.client.clone())?;
500
501 let (system_id, metrics_id, is_staff) = if client.telemetry().metrics_enabled() {
502 (
503 client.telemetry().system_id(),
504 client.telemetry().metrics_id(),
505 client.telemetry().is_staff(),
506 )
507 } else {
508 (None, None, None)
509 };
510
511 let version = if let Some(version) = version {
512 version.to_string()
513 } else {
514 "latest".to_string()
515 };
516 let http_client = client.http_client();
517
518 let path = format!("/releases/{}/{}/asset", release_channel.dev_name(), version,);
519 let url = http_client.build_zed_cloud_url_with_query(
520 &path,
521 AssetQuery {
522 os,
523 arch,
524 asset,
525 metrics_id: metrics_id.as_deref(),
526 system_id: system_id.as_deref(),
527 is_staff: is_staff,
528 },
529 )?;
530
531 let mut response = http_client
532 .get(url.as_str(), Default::default(), true)
533 .await?;
534 let mut body = Vec::new();
535 response.body_mut().read_to_end(&mut body).await?;
536
537 anyhow::ensure!(
538 response.status().is_success(),
539 "failed to fetch release: {:?}",
540 String::from_utf8_lossy(&body),
541 );
542
543 serde_json::from_slice(body.as_slice()).with_context(|| {
544 format!(
545 "error deserializing release {:?}",
546 String::from_utf8_lossy(&body),
547 )
548 })
549 }
550
551 async fn update(this: Entity<Self>, cx: &mut AsyncApp) -> Result<()> {
552 let (client, installed_version, previous_status, release_channel) =
553 this.read_with(cx, |this, cx| {
554 (
555 this.client.http_client(),
556 this.current_version,
557 this.status.clone(),
558 ReleaseChannel::try_global(cx).unwrap_or(ReleaseChannel::Stable),
559 )
560 })?;
561
562 Self::check_dependencies()?;
563
564 this.update(cx, |this, cx| {
565 this.status = AutoUpdateStatus::Checking;
566 log::info!("Auto Update: checking for updates");
567 cx.notify();
568 })?;
569
570 let fetched_release_data =
571 Self::get_release_asset(&this, release_channel, None, "zed", OS, ARCH, cx).await?;
572 let fetched_version = fetched_release_data.clone().version;
573 let app_commit_sha = cx.update(|cx| AppCommitSha::try_global(cx).map(|sha| sha.full()));
574 let newer_version = Self::check_if_fetched_version_is_newer(
575 release_channel,
576 app_commit_sha,
577 installed_version,
578 fetched_version,
579 previous_status.clone(),
580 )?;
581
582 let Some(newer_version) = newer_version else {
583 return this.update(cx, |this, cx| {
584 let status = match previous_status {
585 AutoUpdateStatus::Updated { .. } => previous_status,
586 _ => AutoUpdateStatus::Idle,
587 };
588 this.status = status;
589 cx.notify();
590 });
591 };
592
593 this.update(cx, |this, cx| {
594 this.status = AutoUpdateStatus::Downloading {
595 version: newer_version.clone(),
596 };
597 cx.notify();
598 })?;
599
600 let installer_dir = InstallerDir::new().await?;
601 let target_path = Self::target_path(&installer_dir).await?;
602 download_release(&target_path, fetched_release_data, client).await?;
603
604 this.update(cx, |this, cx| {
605 this.status = AutoUpdateStatus::Installing {
606 version: newer_version.clone(),
607 };
608 cx.notify();
609 })?;
610
611 let new_binary_path = Self::install_release(installer_dir, target_path, cx).await?;
612 if let Some(new_binary_path) = new_binary_path {
613 cx.update(|cx| cx.set_restart_path(new_binary_path))?;
614 }
615
616 this.update(cx, |this, cx| {
617 this.set_should_show_update_notification(true, cx)
618 .detach_and_log_err(cx);
619 this.status = AutoUpdateStatus::Updated {
620 version: newer_version,
621 };
622 cx.notify();
623 })
624 }
625
626 fn check_if_fetched_version_is_newer(
627 release_channel: ReleaseChannel,
628 app_commit_sha: Result<Option<String>>,
629 installed_version: SemanticVersion,
630 fetched_version: String,
631 status: AutoUpdateStatus,
632 ) -> Result<Option<VersionCheckType>> {
633 let parsed_fetched_version = fetched_version.parse::<SemanticVersion>();
634
635 if let AutoUpdateStatus::Updated { version, .. } = status {
636 match version {
637 VersionCheckType::Sha(cached_version) => {
638 let should_download = fetched_version != cached_version.full();
639 let newer_version = should_download
640 .then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
641 return Ok(newer_version);
642 }
643 VersionCheckType::Semantic(cached_version) => {
644 return Self::check_if_fetched_version_is_newer_non_nightly(
645 cached_version,
646 parsed_fetched_version?,
647 );
648 }
649 }
650 }
651
652 match release_channel {
653 ReleaseChannel::Nightly => {
654 let should_download = app_commit_sha
655 .ok()
656 .flatten()
657 .map(|sha| fetched_version != sha)
658 .unwrap_or(true);
659 let newer_version = should_download
660 .then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
661 Ok(newer_version)
662 }
663 _ => Self::check_if_fetched_version_is_newer_non_nightly(
664 installed_version,
665 parsed_fetched_version?,
666 ),
667 }
668 }
669
670 fn check_dependencies() -> Result<()> {
671 #[cfg(not(target_os = "windows"))]
672 anyhow::ensure!(
673 which::which("rsync").is_ok(),
674 "Could not auto-update because the required rsync utility was not found."
675 );
676 Ok(())
677 }
678
679 async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf> {
680 let filename = match OS {
681 "macos" => anyhow::Ok("Zed.dmg"),
682 "linux" => Ok("zed.tar.gz"),
683 "windows" => Ok("Zed.exe"),
684 unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
685 }?;
686
687 Ok(installer_dir.path().join(filename))
688 }
689
690 async fn install_release(
691 installer_dir: InstallerDir,
692 target_path: PathBuf,
693 cx: &AsyncApp,
694 ) -> Result<Option<PathBuf>> {
695 #[cfg(test)]
696 if let Some(test_install) =
697 cx.try_read_global::<tests::InstallOverride, _>(|g, _| g.0.clone())
698 {
699 return test_install(target_path, cx);
700 }
701 match OS {
702 "macos" => install_release_macos(&installer_dir, target_path, cx).await,
703 "linux" => install_release_linux(&installer_dir, target_path, cx).await,
704 "windows" => install_release_windows(target_path).await,
705 unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
706 }
707 }
708
709 fn check_if_fetched_version_is_newer_non_nightly(
710 installed_version: SemanticVersion,
711 fetched_version: SemanticVersion,
712 ) -> Result<Option<VersionCheckType>> {
713 let should_download = fetched_version > installed_version;
714 let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
715 Ok(newer_version)
716 }
717
718 pub fn set_should_show_update_notification(
719 &self,
720 should_show: bool,
721 cx: &App,
722 ) -> Task<Result<()>> {
723 cx.background_spawn(async move {
724 if should_show {
725 KEY_VALUE_STORE
726 .write_kvp(
727 SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
728 "".to_string(),
729 )
730 .await?;
731 } else {
732 KEY_VALUE_STORE
733 .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
734 .await?;
735 }
736 Ok(())
737 })
738 }
739
740 pub fn should_show_update_notification(&self, cx: &App) -> Task<Result<bool>> {
741 cx.background_spawn(async move {
742 Ok(KEY_VALUE_STORE
743 .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
744 .is_some())
745 })
746 }
747}
748
749async fn download_remote_server_binary(
750 target_path: &PathBuf,
751 release: ReleaseAsset,
752 client: Arc<HttpClientWithUrl>,
753) -> Result<()> {
754 let temp = tempfile::Builder::new().tempfile_in(remote_servers_dir())?;
755 let mut temp_file = File::create(&temp).await?;
756
757 let mut response = client.get(&release.url, Default::default(), true).await?;
758 anyhow::ensure!(
759 response.status().is_success(),
760 "failed to download remote server release: {:?}",
761 response.status()
762 );
763 smol::io::copy(response.body_mut(), &mut temp_file).await?;
764 smol::fs::rename(&temp, &target_path).await?;
765
766 Ok(())
767}
768
769async fn download_release(
770 target_path: &Path,
771 release: ReleaseAsset,
772 client: Arc<HttpClientWithUrl>,
773) -> Result<()> {
774 let mut target_file = File::create(&target_path).await?;
775
776 let mut response = client.get(&release.url, Default::default(), true).await?;
777 anyhow::ensure!(
778 response.status().is_success(),
779 "failed to download update: {:?}",
780 response.status()
781 );
782 smol::io::copy(response.body_mut(), &mut target_file).await?;
783 log::info!("downloaded update. path:{:?}", target_path);
784
785 Ok(())
786}
787
788async fn install_release_linux(
789 temp_dir: &InstallerDir,
790 downloaded_tar_gz: PathBuf,
791 cx: &AsyncApp,
792) -> Result<Option<PathBuf>> {
793 let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
794 let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
795 let running_app_path = cx.update(|cx| cx.app_path())??;
796
797 let extracted = temp_dir.path().join("zed");
798 fs::create_dir_all(&extracted)
799 .await
800 .context("failed to create directory into which to extract update")?;
801
802 let output = Command::new("tar")
803 .arg("-xzf")
804 .arg(&downloaded_tar_gz)
805 .arg("-C")
806 .arg(&extracted)
807 .output()
808 .await?;
809
810 anyhow::ensure!(
811 output.status.success(),
812 "failed to extract {:?} to {:?}: {:?}",
813 downloaded_tar_gz,
814 extracted,
815 String::from_utf8_lossy(&output.stderr)
816 );
817
818 let suffix = if channel != "stable" {
819 format!("-{}", channel)
820 } else {
821 String::default()
822 };
823 let app_folder_name = format!("zed{}.app", suffix);
824
825 let from = extracted.join(&app_folder_name);
826 let mut to = home_dir.join(".local");
827
828 let expected_suffix = format!("{}/libexec/zed-editor", app_folder_name);
829
830 if let Some(prefix) = running_app_path
831 .to_str()
832 .and_then(|str| str.strip_suffix(&expected_suffix))
833 {
834 to = PathBuf::from(prefix);
835 }
836
837 let output = Command::new("rsync")
838 .args(["-av", "--delete"])
839 .arg(&from)
840 .arg(&to)
841 .output()
842 .await?;
843
844 anyhow::ensure!(
845 output.status.success(),
846 "failed to copy Zed update from {:?} to {:?}: {:?}",
847 from,
848 to,
849 String::from_utf8_lossy(&output.stderr)
850 );
851
852 Ok(Some(to.join(expected_suffix)))
853}
854
855async fn install_release_macos(
856 temp_dir: &InstallerDir,
857 downloaded_dmg: PathBuf,
858 cx: &AsyncApp,
859) -> Result<Option<PathBuf>> {
860 let running_app_path = cx.update(|cx| cx.app_path())??;
861 let running_app_filename = running_app_path
862 .file_name()
863 .with_context(|| format!("invalid running app path {running_app_path:?}"))?;
864
865 let mount_path = temp_dir.path().join("Zed");
866 let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
867
868 mounted_app_path.push("/");
869 let output = Command::new("hdiutil")
870 .args(["attach", "-nobrowse"])
871 .arg(&downloaded_dmg)
872 .arg("-mountroot")
873 .arg(temp_dir.path())
874 .output()
875 .await?;
876
877 anyhow::ensure!(
878 output.status.success(),
879 "failed to mount: {:?}",
880 String::from_utf8_lossy(&output.stderr)
881 );
882
883 // Create an MacOsUnmounter that will be dropped (and thus unmount the disk) when this function exits
884 let _unmounter = MacOsUnmounter {
885 mount_path: mount_path.clone(),
886 background_executor: cx.background_executor(),
887 };
888
889 let output = Command::new("rsync")
890 .args(["-av", "--delete"])
891 .arg(&mounted_app_path)
892 .arg(&running_app_path)
893 .output()
894 .await?;
895
896 anyhow::ensure!(
897 output.status.success(),
898 "failed to copy app: {:?}",
899 String::from_utf8_lossy(&output.stderr)
900 );
901
902 Ok(None)
903}
904
905async fn cleanup_windows() -> Result<()> {
906 let parent = std::env::current_exe()?
907 .parent()
908 .context("No parent dir for Zed.exe")?
909 .to_owned();
910
911 // keep in sync with crates/auto_update_helper/src/updater.rs
912 _ = smol::fs::remove_dir(parent.join("updates")).await;
913 _ = smol::fs::remove_dir(parent.join("install")).await;
914 _ = smol::fs::remove_dir(parent.join("old")).await;
915
916 Ok(())
917}
918
919async fn install_release_windows(downloaded_installer: PathBuf) -> Result<Option<PathBuf>> {
920 let output = Command::new(downloaded_installer)
921 .arg("/verysilent")
922 .arg("/update=true")
923 .arg("!desktopicon")
924 .arg("!quicklaunchicon")
925 .output()
926 .await?;
927 anyhow::ensure!(
928 output.status.success(),
929 "failed to start installer: {:?}",
930 String::from_utf8_lossy(&output.stderr)
931 );
932 // We return the path to the update helper program, because it will
933 // perform the final steps of the update process, copying the new binary,
934 // deleting the old one, and launching the new binary.
935 let helper_path = std::env::current_exe()?
936 .parent()
937 .context("No parent dir for Zed.exe")?
938 .join("tools")
939 .join("auto_update_helper.exe");
940 Ok(Some(helper_path))
941}
942
943pub async fn finalize_auto_update_on_quit() {
944 let Some(installer_path) = std::env::current_exe()
945 .ok()
946 .and_then(|p| p.parent().map(|p| p.join("updates")))
947 else {
948 return;
949 };
950
951 // The installer will create a flag file after it finishes updating
952 let flag_file = installer_path.join("versions.txt");
953 if flag_file.exists()
954 && let Some(helper) = installer_path
955 .parent()
956 .map(|p| p.join("tools").join("auto_update_helper.exe"))
957 {
958 let mut command = util::command::new_smol_command(helper);
959 command.arg("--launch");
960 command.arg("false");
961 if let Ok(mut cmd) = command.spawn() {
962 _ = cmd.status().await;
963 }
964 }
965}
966
967#[cfg(test)]
968mod tests {
969 use client::Client;
970 use clock::FakeSystemClock;
971 use futures::channel::oneshot;
972 use gpui::TestAppContext;
973 use http_client::{FakeHttpClient, Response};
974 use settings::default_settings;
975 use std::{
976 rc::Rc,
977 sync::{
978 Arc,
979 atomic::{self, AtomicBool},
980 },
981 };
982 use tempfile::tempdir;
983
984 #[ctor::ctor]
985 fn init_logger() {
986 zlog::init_test();
987 }
988
989 use super::*;
990
991 pub(super) struct InstallOverride(
992 pub Rc<dyn Fn(PathBuf, &AsyncApp) -> Result<Option<PathBuf>>>,
993 );
994 impl Global for InstallOverride {}
995
996 #[gpui::test]
997 fn test_auto_update_defaults_to_true(cx: &mut TestAppContext) {
998 cx.update(|cx| {
999 let mut store = SettingsStore::new(cx, &settings::default_settings());
1000 store
1001 .set_default_settings(&default_settings(), cx)
1002 .expect("Unable to set default settings");
1003 store
1004 .set_user_settings("{}", cx)
1005 .expect("Unable to set user settings");
1006 cx.set_global(store);
1007 assert!(AutoUpdateSetting::get_global(cx).0);
1008 });
1009 }
1010
1011 #[gpui::test]
1012 async fn test_auto_update_downloads(cx: &mut TestAppContext) {
1013 cx.background_executor.allow_parking();
1014 zlog::init_test();
1015 let release_available = Arc::new(AtomicBool::new(false));
1016
1017 let (dmg_tx, dmg_rx) = oneshot::channel::<String>();
1018
1019 cx.update(|cx| {
1020 settings::init(cx);
1021
1022 let current_version = SemanticVersion::new(0, 100, 0);
1023 release_channel::init_test(current_version, ReleaseChannel::Stable, cx);
1024
1025 let clock = Arc::new(FakeSystemClock::new());
1026 let release_available = Arc::clone(&release_available);
1027 let dmg_rx = Arc::new(parking_lot::Mutex::new(Some(dmg_rx)));
1028 let fake_client_http = FakeHttpClient::create(move |req| {
1029 let release_available = release_available.load(atomic::Ordering::Relaxed);
1030 let dmg_rx = dmg_rx.clone();
1031 async move {
1032 if req.uri().path() == "/releases/stable/latest/asset" {
1033 if release_available {
1034 return Ok(Response::builder().status(200).body(
1035 r#"{"version":"0.100.1","url":"https://test.example/new-download"}"#.into()
1036 ).unwrap());
1037 } else {
1038 return Ok(Response::builder().status(200).body(
1039 r#"{"version":"0.100.0","url":"https://test.example/old-download"}"#.into()
1040 ).unwrap());
1041 }
1042 } else if req.uri().path() == "/new-download" {
1043 return Ok(Response::builder().status(200).body({
1044 let dmg_rx = dmg_rx.lock().take().unwrap();
1045 dmg_rx.await.unwrap().into()
1046 }).unwrap());
1047 }
1048 Ok(Response::builder().status(404).body("".into()).unwrap())
1049 }
1050 });
1051 let client = Client::new(clock, fake_client_http, cx);
1052 crate::init(client, cx);
1053 });
1054
1055 let auto_updater = cx.update(|cx| AutoUpdater::get(cx).expect("auto updater should exist"));
1056
1057 cx.background_executor.run_until_parked();
1058
1059 auto_updater.read_with(cx, |updater, _| {
1060 assert_eq!(updater.status(), AutoUpdateStatus::Idle);
1061 assert_eq!(updater.current_version(), SemanticVersion::new(0, 100, 0));
1062 });
1063
1064 release_available.store(true, atomic::Ordering::SeqCst);
1065 cx.background_executor.advance_clock(POLL_INTERVAL);
1066 cx.background_executor.run_until_parked();
1067
1068 loop {
1069 cx.background_executor.timer(Duration::from_millis(0)).await;
1070 cx.run_until_parked();
1071 let status = auto_updater.read_with(cx, |updater, _| updater.status());
1072 if !matches!(status, AutoUpdateStatus::Idle) {
1073 break;
1074 }
1075 }
1076 let status = auto_updater.read_with(cx, |updater, _| updater.status());
1077 assert_eq!(
1078 status,
1079 AutoUpdateStatus::Downloading {
1080 version: VersionCheckType::Semantic(SemanticVersion::new(0, 100, 1))
1081 }
1082 );
1083
1084 dmg_tx.send("<fake-zed-update>".to_owned()).unwrap();
1085
1086 let tmp_dir = Arc::new(tempdir().unwrap());
1087
1088 cx.update(|cx| {
1089 let tmp_dir = tmp_dir.clone();
1090 cx.set_global(InstallOverride(Rc::new(move |target_path, _cx| {
1091 let tmp_dir = tmp_dir.clone();
1092 let dest_path = tmp_dir.path().join("zed");
1093 std::fs::copy(&target_path, &dest_path)?;
1094 Ok(Some(dest_path))
1095 })));
1096 });
1097
1098 loop {
1099 cx.background_executor.timer(Duration::from_millis(0)).await;
1100 cx.run_until_parked();
1101 let status = auto_updater.read_with(cx, |updater, _| updater.status());
1102 if !matches!(status, AutoUpdateStatus::Downloading { .. }) {
1103 break;
1104 }
1105 }
1106 let status = auto_updater.read_with(cx, |updater, _| updater.status());
1107 assert_eq!(
1108 status,
1109 AutoUpdateStatus::Updated {
1110 version: VersionCheckType::Semantic(SemanticVersion::new(0, 100, 1))
1111 }
1112 );
1113 let will_restart = cx.expect_restart();
1114 cx.update(|cx| cx.restart());
1115 let path = will_restart.await.unwrap().unwrap();
1116 assert_eq!(path, tmp_dir.path().join("zed"));
1117 assert_eq!(std::fs::read_to_string(path).unwrap(), "<fake-zed-update>");
1118 }
1119
1120 #[test]
1121 fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
1122 let release_channel = ReleaseChannel::Stable;
1123 let app_commit_sha = Ok(Some("a".to_string()));
1124 let installed_version = SemanticVersion::new(1, 0, 0);
1125 let status = AutoUpdateStatus::Idle;
1126 let fetched_version = SemanticVersion::new(1, 0, 0);
1127
1128 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1129 release_channel,
1130 app_commit_sha,
1131 installed_version,
1132 fetched_version.to_string(),
1133 status,
1134 );
1135
1136 assert_eq!(newer_version.unwrap(), None);
1137 }
1138
1139 #[test]
1140 fn test_stable_does_update_when_fetched_version_is_higher() {
1141 let release_channel = ReleaseChannel::Stable;
1142 let app_commit_sha = Ok(Some("a".to_string()));
1143 let installed_version = SemanticVersion::new(1, 0, 0);
1144 let status = AutoUpdateStatus::Idle;
1145 let fetched_version = SemanticVersion::new(1, 0, 1);
1146
1147 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1148 release_channel,
1149 app_commit_sha,
1150 installed_version,
1151 fetched_version.to_string(),
1152 status,
1153 );
1154
1155 assert_eq!(
1156 newer_version.unwrap(),
1157 Some(VersionCheckType::Semantic(fetched_version))
1158 );
1159 }
1160
1161 #[test]
1162 fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
1163 let release_channel = ReleaseChannel::Stable;
1164 let app_commit_sha = Ok(Some("a".to_string()));
1165 let installed_version = SemanticVersion::new(1, 0, 0);
1166 let status = AutoUpdateStatus::Updated {
1167 version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
1168 };
1169 let fetched_version = SemanticVersion::new(1, 0, 1);
1170
1171 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1172 release_channel,
1173 app_commit_sha,
1174 installed_version,
1175 fetched_version.to_string(),
1176 status,
1177 );
1178
1179 assert_eq!(newer_version.unwrap(), None);
1180 }
1181
1182 #[test]
1183 fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
1184 let release_channel = ReleaseChannel::Stable;
1185 let app_commit_sha = Ok(Some("a".to_string()));
1186 let installed_version = SemanticVersion::new(1, 0, 0);
1187 let status = AutoUpdateStatus::Updated {
1188 version: VersionCheckType::Semantic(SemanticVersion::new(1, 0, 1)),
1189 };
1190 let fetched_version = SemanticVersion::new(1, 0, 2);
1191
1192 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1193 release_channel,
1194 app_commit_sha,
1195 installed_version,
1196 fetched_version.to_string(),
1197 status,
1198 );
1199
1200 assert_eq!(
1201 newer_version.unwrap(),
1202 Some(VersionCheckType::Semantic(fetched_version))
1203 );
1204 }
1205
1206 #[test]
1207 fn test_nightly_does_not_update_when_fetched_sha_is_same() {
1208 let release_channel = ReleaseChannel::Nightly;
1209 let app_commit_sha = Ok(Some("a".to_string()));
1210 let installed_version = SemanticVersion::new(1, 0, 0);
1211 let status = AutoUpdateStatus::Idle;
1212 let fetched_sha = "a".to_string();
1213
1214 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1215 release_channel,
1216 app_commit_sha,
1217 installed_version,
1218 fetched_sha,
1219 status,
1220 );
1221
1222 assert_eq!(newer_version.unwrap(), None);
1223 }
1224
1225 #[test]
1226 fn test_nightly_does_update_when_fetched_sha_is_not_same() {
1227 let release_channel = ReleaseChannel::Nightly;
1228 let app_commit_sha = Ok(Some("a".to_string()));
1229 let installed_version = SemanticVersion::new(1, 0, 0);
1230 let status = AutoUpdateStatus::Idle;
1231 let fetched_sha = "b".to_string();
1232
1233 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1234 release_channel,
1235 app_commit_sha,
1236 installed_version,
1237 fetched_sha.clone(),
1238 status,
1239 );
1240
1241 assert_eq!(
1242 newer_version.unwrap(),
1243 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1244 );
1245 }
1246
1247 #[test]
1248 fn test_nightly_does_not_update_when_fetched_sha_is_same_as_cached() {
1249 let release_channel = ReleaseChannel::Nightly;
1250 let app_commit_sha = Ok(Some("a".to_string()));
1251 let installed_version = SemanticVersion::new(1, 0, 0);
1252 let status = AutoUpdateStatus::Updated {
1253 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1254 };
1255 let fetched_sha = "b".to_string();
1256
1257 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1258 release_channel,
1259 app_commit_sha,
1260 installed_version,
1261 fetched_sha,
1262 status,
1263 );
1264
1265 assert_eq!(newer_version.unwrap(), None);
1266 }
1267
1268 #[test]
1269 fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
1270 let release_channel = ReleaseChannel::Nightly;
1271 let app_commit_sha = Ok(Some("a".to_string()));
1272 let installed_version = SemanticVersion::new(1, 0, 0);
1273 let status = AutoUpdateStatus::Updated {
1274 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1275 };
1276 let fetched_sha = "c".to_string();
1277
1278 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1279 release_channel,
1280 app_commit_sha,
1281 installed_version,
1282 fetched_sha.clone(),
1283 status,
1284 );
1285
1286 assert_eq!(
1287 newer_version.unwrap(),
1288 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1289 );
1290 }
1291
1292 #[test]
1293 fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
1294 let release_channel = ReleaseChannel::Nightly;
1295 let app_commit_sha = Ok(None);
1296 let installed_version = SemanticVersion::new(1, 0, 0);
1297 let status = AutoUpdateStatus::Idle;
1298 let fetched_sha = "a".to_string();
1299
1300 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1301 release_channel,
1302 app_commit_sha,
1303 installed_version,
1304 fetched_sha.clone(),
1305 status,
1306 );
1307
1308 assert_eq!(
1309 newer_version.unwrap(),
1310 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1311 );
1312 }
1313
1314 #[test]
1315 fn test_nightly_does_not_update_when_cached_update_is_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1316 {
1317 let release_channel = ReleaseChannel::Nightly;
1318 let app_commit_sha = Ok(None);
1319 let installed_version = SemanticVersion::new(1, 0, 0);
1320 let status = AutoUpdateStatus::Updated {
1321 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1322 };
1323 let fetched_sha = "b".to_string();
1324
1325 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1326 release_channel,
1327 app_commit_sha,
1328 installed_version,
1329 fetched_sha,
1330 status,
1331 );
1332
1333 assert_eq!(newer_version.unwrap(), None);
1334 }
1335
1336 #[test]
1337 fn test_nightly_does_update_when_cached_update_is_not_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1338 {
1339 let release_channel = ReleaseChannel::Nightly;
1340 let app_commit_sha = Ok(None);
1341 let installed_version = SemanticVersion::new(1, 0, 0);
1342 let status = AutoUpdateStatus::Updated {
1343 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1344 };
1345 let fetched_sha = "c".to_string();
1346
1347 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1348 release_channel,
1349 app_commit_sha,
1350 installed_version,
1351 fetched_sha.clone(),
1352 status,
1353 );
1354
1355 assert_eq!(
1356 newer_version.unwrap(),
1357 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1358 );
1359 }
1360}