auto_update.rs

  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!(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                .context("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                .context("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            anyhow::ensure!(
469                response.status().is_success(),
470                "failed to fetch release: {:?}",
471                String::from_utf8_lossy(&body),
472            );
473
474            serde_json::from_slice(body.as_slice()).with_context(|| {
475                format!(
476                    "error deserializing release {:?}",
477                    String::from_utf8_lossy(&body),
478                )
479            })
480        }
481    }
482
483    async fn get_latest_release(
484        this: &Entity<Self>,
485        asset: &str,
486        os: &str,
487        arch: &str,
488        release_channel: Option<ReleaseChannel>,
489        cx: &mut AsyncApp,
490    ) -> Result<JsonRelease> {
491        Self::get_release(this, asset, os, arch, None, release_channel, cx).await
492    }
493
494    fn installed_update_version(&self) -> Option<VersionCheckType> {
495        match &self.status {
496            AutoUpdateStatus::Updated { version, .. } => Some(version.clone()),
497            _ => None,
498        }
499    }
500
501    async fn update(this: Entity<Self>, mut cx: AsyncApp) -> Result<()> {
502        let (client, current_version, installed_update_version, release_channel) =
503            this.update(&mut cx, |this, cx| {
504                this.status = AutoUpdateStatus::Checking;
505                cx.notify();
506                (
507                    this.http_client.clone(),
508                    this.current_version,
509                    this.installed_update_version(),
510                    ReleaseChannel::try_global(cx),
511                )
512            })?;
513
514        let release =
515            Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
516
517        let update_version_to_install = match *RELEASE_CHANNEL {
518            ReleaseChannel::Nightly => {
519                let should_download = cx
520                    .update(|cx| AppCommitSha::try_global(cx).map(|sha| release.version != sha.0))
521                    .ok()
522                    .flatten()
523                    .unwrap_or(true);
524
525                should_download.then(|| VersionCheckType::Sha(release.version.clone()))
526            }
527            _ => {
528                let installed_version =
529                    installed_update_version.unwrap_or(VersionCheckType::Semantic(current_version));
530                match installed_version {
531                    VersionCheckType::Sha(_) => {
532                        log::warn!("Unexpected SHA-based version in non-nightly build");
533                        Some(installed_version)
534                    }
535                    VersionCheckType::Semantic(semantic_comparison_version) => {
536                        let latest_release_version = release.version.parse::<SemanticVersion>()?;
537                        let should_download = latest_release_version > semantic_comparison_version;
538                        should_download.then(|| VersionCheckType::Semantic(latest_release_version))
539                    }
540                }
541            }
542        };
543
544        let Some(update_version) = update_version_to_install else {
545            this.update(&mut cx, |this, cx| {
546                this.status = AutoUpdateStatus::Idle;
547                cx.notify();
548            })?;
549            return Ok(());
550        };
551
552        this.update(&mut cx, |this, cx| {
553            this.status = AutoUpdateStatus::Downloading;
554            cx.notify();
555        })?;
556
557        let installer_dir = InstallerDir::new().await?;
558        let filename = match OS {
559            "macos" => anyhow::Ok("Zed.dmg"),
560            "linux" => Ok("zed.tar.gz"),
561            "windows" => Ok("ZedUpdateInstaller.exe"),
562            unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
563        }?;
564
565        #[cfg(not(target_os = "windows"))]
566        anyhow::ensure!(
567            which::which("rsync").is_ok(),
568            "Aborting. Could not find rsync which is required for auto-updates."
569        );
570
571        let downloaded_asset = installer_dir.path().join(filename);
572        download_release(&downloaded_asset, release.clone(), client, &cx).await?;
573
574        this.update(&mut cx, |this, cx| {
575            this.status = AutoUpdateStatus::Installing;
576            cx.notify();
577        })?;
578
579        let binary_path = match OS {
580            "macos" => install_release_macos(&installer_dir, downloaded_asset, &cx).await,
581            "linux" => install_release_linux(&installer_dir, downloaded_asset, &cx).await,
582            "windows" => install_release_windows(downloaded_asset).await,
583            unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
584        }?;
585
586        this.update(&mut cx, |this, cx| {
587            this.set_should_show_update_notification(true, cx)
588                .detach_and_log_err(cx);
589            this.status = AutoUpdateStatus::Updated {
590                binary_path,
591                version: update_version,
592            };
593            cx.notify();
594        })?;
595
596        Ok(())
597    }
598
599    pub fn set_should_show_update_notification(
600        &self,
601        should_show: bool,
602        cx: &App,
603    ) -> Task<Result<()>> {
604        cx.background_spawn(async move {
605            if should_show {
606                KEY_VALUE_STORE
607                    .write_kvp(
608                        SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
609                        "".to_string(),
610                    )
611                    .await?;
612            } else {
613                KEY_VALUE_STORE
614                    .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
615                    .await?;
616            }
617            Ok(())
618        })
619    }
620
621    pub fn should_show_update_notification(&self, cx: &App) -> Task<Result<bool>> {
622        cx.background_spawn(async move {
623            Ok(KEY_VALUE_STORE
624                .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
625                .is_some())
626        })
627    }
628}
629
630async fn download_remote_server_binary(
631    target_path: &PathBuf,
632    release: JsonRelease,
633    client: Arc<HttpClientWithUrl>,
634    cx: &AsyncApp,
635) -> Result<()> {
636    let temp = tempfile::Builder::new().tempfile_in(remote_servers_dir())?;
637    let mut temp_file = File::create(&temp).await?;
638    let update_request_body = build_remote_server_update_request_body(cx)?;
639    let request_body = AsyncBody::from(serde_json::to_string(&update_request_body)?);
640
641    let mut response = client.get(&release.url, request_body, true).await?;
642    anyhow::ensure!(
643        response.status().is_success(),
644        "failed to download remote server release: {:?}",
645        response.status()
646    );
647    smol::io::copy(response.body_mut(), &mut temp_file).await?;
648    smol::fs::rename(&temp, &target_path).await?;
649
650    Ok(())
651}
652
653fn build_remote_server_update_request_body(cx: &AsyncApp) -> Result<UpdateRequestBody> {
654    let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
655        let telemetry = Client::global(cx).telemetry().clone();
656        let is_staff = telemetry.is_staff();
657        let installation_id = telemetry.installation_id();
658        let release_channel =
659            ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
660        let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
661
662        (
663            installation_id,
664            release_channel,
665            telemetry_enabled,
666            is_staff,
667        )
668    })?;
669
670    Ok(UpdateRequestBody {
671        installation_id,
672        release_channel,
673        telemetry: telemetry_enabled,
674        is_staff,
675        destination: "remote",
676    })
677}
678
679async fn download_release(
680    target_path: &Path,
681    release: JsonRelease,
682    client: Arc<HttpClientWithUrl>,
683    cx: &AsyncApp,
684) -> Result<()> {
685    let mut target_file = File::create(&target_path).await?;
686
687    let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
688        let telemetry = Client::global(cx).telemetry().clone();
689        let is_staff = telemetry.is_staff();
690        let installation_id = telemetry.installation_id();
691        let release_channel =
692            ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
693        let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
694
695        (
696            installation_id,
697            release_channel,
698            telemetry_enabled,
699            is_staff,
700        )
701    })?;
702
703    let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
704        installation_id,
705        release_channel,
706        telemetry: telemetry_enabled,
707        is_staff,
708        destination: "local",
709    })?);
710
711    let mut response = client.get(&release.url, request_body, true).await?;
712    smol::io::copy(response.body_mut(), &mut target_file).await?;
713    log::info!("downloaded update. path:{:?}", target_path);
714
715    Ok(())
716}
717
718async fn install_release_linux(
719    temp_dir: &InstallerDir,
720    downloaded_tar_gz: PathBuf,
721    cx: &AsyncApp,
722) -> Result<PathBuf> {
723    let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
724    let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
725    let running_app_path = cx.update(|cx| cx.app_path())??;
726
727    let extracted = temp_dir.path().join("zed");
728    fs::create_dir_all(&extracted)
729        .await
730        .context("failed to create directory into which to extract update")?;
731
732    let output = Command::new("tar")
733        .arg("-xzf")
734        .arg(&downloaded_tar_gz)
735        .arg("-C")
736        .arg(&extracted)
737        .output()
738        .await?;
739
740    anyhow::ensure!(
741        output.status.success(),
742        "failed to extract {:?} to {:?}: {:?}",
743        downloaded_tar_gz,
744        extracted,
745        String::from_utf8_lossy(&output.stderr)
746    );
747
748    let suffix = if channel != "stable" {
749        format!("-{}", channel)
750    } else {
751        String::default()
752    };
753    let app_folder_name = format!("zed{}.app", suffix);
754
755    let from = extracted.join(&app_folder_name);
756    let mut to = home_dir.join(".local");
757
758    let expected_suffix = format!("{}/libexec/zed-editor", app_folder_name);
759
760    if let Some(prefix) = running_app_path
761        .to_str()
762        .and_then(|str| str.strip_suffix(&expected_suffix))
763    {
764        to = PathBuf::from(prefix);
765    }
766
767    let output = Command::new("rsync")
768        .args(["-av", "--delete"])
769        .arg(&from)
770        .arg(&to)
771        .output()
772        .await?;
773
774    anyhow::ensure!(
775        output.status.success(),
776        "failed to copy Zed update from {:?} to {:?}: {:?}",
777        from,
778        to,
779        String::from_utf8_lossy(&output.stderr)
780    );
781
782    Ok(to.join(expected_suffix))
783}
784
785async fn install_release_macos(
786    temp_dir: &InstallerDir,
787    downloaded_dmg: PathBuf,
788    cx: &AsyncApp,
789) -> Result<PathBuf> {
790    let running_app_path = cx.update(|cx| cx.app_path())??;
791    let running_app_filename = running_app_path
792        .file_name()
793        .with_context(|| format!("invalid running app path {running_app_path:?}"))?;
794
795    let mount_path = temp_dir.path().join("Zed");
796    let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
797
798    mounted_app_path.push("/");
799    let output = Command::new("hdiutil")
800        .args(["attach", "-nobrowse"])
801        .arg(&downloaded_dmg)
802        .arg("-mountroot")
803        .arg(temp_dir.path())
804        .output()
805        .await?;
806
807    anyhow::ensure!(
808        output.status.success(),
809        "failed to mount: {:?}",
810        String::from_utf8_lossy(&output.stderr)
811    );
812
813    // Create an MacOsUnmounter that will be dropped (and thus unmount the disk) when this function exits
814    let _unmounter = MacOsUnmounter {
815        mount_path: mount_path.clone(),
816    };
817
818    let output = Command::new("rsync")
819        .args(["-av", "--delete"])
820        .arg(&mounted_app_path)
821        .arg(&running_app_path)
822        .output()
823        .await?;
824
825    anyhow::ensure!(
826        output.status.success(),
827        "failed to copy app: {:?}",
828        String::from_utf8_lossy(&output.stderr)
829    );
830
831    Ok(running_app_path)
832}
833
834async fn install_release_windows(downloaded_installer: PathBuf) -> Result<PathBuf> {
835    let output = Command::new(downloaded_installer)
836        .arg("/verysilent")
837        .arg("/update=true")
838        .arg("!desktopicon")
839        .arg("!quicklaunchicon")
840        .output()
841        .await?;
842    anyhow::ensure!(
843        output.status.success(),
844        "failed to start installer: {:?}",
845        String::from_utf8_lossy(&output.stderr)
846    );
847    Ok(std::env::current_exe()?)
848}
849
850pub fn check_pending_installation() -> bool {
851    let Some(installer_path) = std::env::current_exe()
852        .ok()
853        .and_then(|p| p.parent().map(|p| p.join("updates")))
854    else {
855        return false;
856    };
857
858    // The installer will create a flag file after it finishes updating
859    let flag_file = installer_path.join("versions.txt");
860    if flag_file.exists() {
861        if let Some(helper) = installer_path
862            .parent()
863            .map(|p| p.join("tools\\auto_update_helper.exe"))
864        {
865            let _ = std::process::Command::new(helper).spawn();
866            return true;
867        }
868    }
869    false
870}