auto_update.rs

  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}