auto_update.rs

  1mod update_notification;
  2
  3use anyhow::{anyhow, Context, Result};
  4use client::{Client, TelemetrySettings};
  5use db::kvp::KEY_VALUE_STORE;
  6use db::RELEASE_CHANNEL;
  7use editor::{Editor, MultiBuffer};
  8use gpui::{
  9    actions, AppContext, AsyncAppContext, Context as _, Global, Model, ModelContext,
 10    SemanticVersion, SharedString, Task, View, ViewContext, VisualContext, WindowContext,
 11};
 12use isahc::AsyncBody;
 13
 14use markdown_preview::markdown_preview_view::{MarkdownPreviewMode, MarkdownPreviewView};
 15use schemars::JsonSchema;
 16use serde::Deserialize;
 17use serde_derive::Serialize;
 18use smol::{fs, io::AsyncReadExt};
 19
 20use settings::{Settings, SettingsSources, SettingsStore};
 21use smol::{fs::File, process::Command};
 22
 23use http_client::{HttpClient, HttpClientWithUrl};
 24use release_channel::{AppCommitSha, AppVersion, ReleaseChannel};
 25use std::{
 26    env::{
 27        self,
 28        consts::{ARCH, OS},
 29    },
 30    ffi::OsString,
 31    path::{Path, PathBuf},
 32    sync::Arc,
 33    time::Duration,
 34};
 35use update_notification::UpdateNotification;
 36use util::ResultExt;
 37use workspace::notifications::NotificationId;
 38use workspace::Workspace;
 39
 40const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
 41const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
 42
 43actions!(
 44    auto_update,
 45    [
 46        Check,
 47        DismissErrorMessage,
 48        ViewReleaseNotes,
 49        ViewReleaseNotesLocally
 50    ]
 51);
 52
 53#[derive(Serialize)]
 54struct UpdateRequestBody {
 55    installation_id: Option<Arc<str>>,
 56    release_channel: Option<&'static str>,
 57    telemetry: bool,
 58    is_staff: Option<bool>,
 59    destination: &'static str,
 60}
 61
 62#[derive(Clone, PartialEq, Eq)]
 63pub enum AutoUpdateStatus {
 64    Idle,
 65    Checking,
 66    Downloading,
 67    Installing,
 68    Updated { binary_path: PathBuf },
 69    Errored,
 70}
 71
 72impl AutoUpdateStatus {
 73    pub fn is_updated(&self) -> bool {
 74        matches!(self, Self::Updated { .. })
 75    }
 76}
 77
 78pub struct AutoUpdater {
 79    status: AutoUpdateStatus,
 80    current_version: SemanticVersion,
 81    http_client: Arc<HttpClientWithUrl>,
 82    pending_poll: Option<Task<Option<()>>>,
 83}
 84
 85#[derive(Deserialize)]
 86struct JsonRelease {
 87    version: String,
 88    url: String,
 89}
 90
 91struct AutoUpdateSetting(bool);
 92
 93/// Whether or not to automatically check for updates.
 94///
 95/// Default: true
 96#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize)]
 97#[serde(transparent)]
 98struct AutoUpdateSettingContent(bool);
 99
100impl Settings for AutoUpdateSetting {
101    const KEY: Option<&'static str> = Some("auto_update");
102
103    type FileContent = Option<AutoUpdateSettingContent>;
104
105    fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
106        let auto_update = [sources.release_channel, sources.user]
107            .into_iter()
108            .find_map(|value| value.copied().flatten())
109            .unwrap_or(sources.default.ok_or_else(Self::missing_default)?);
110
111        Ok(Self(auto_update.0))
112    }
113}
114
115#[derive(Default)]
116struct GlobalAutoUpdate(Option<Model<AutoUpdater>>);
117
118impl Global for GlobalAutoUpdate {}
119
120#[derive(Deserialize)]
121struct ReleaseNotesBody {
122    title: String,
123    release_notes: String,
124}
125
126pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut AppContext) {
127    AutoUpdateSetting::register(cx);
128
129    cx.observe_new_views(|workspace: &mut Workspace, _cx| {
130        workspace.register_action(|_, action: &Check, cx| check(action, cx));
131
132        workspace.register_action(|_, action, cx| {
133            view_release_notes(action, cx);
134        });
135
136        workspace.register_action(|workspace, _: &ViewReleaseNotesLocally, cx| {
137            view_release_notes_locally(workspace, cx);
138        });
139    })
140    .detach();
141
142    let version = release_channel::AppVersion::global(cx);
143    let auto_updater = cx.new_model(|cx| {
144        let updater = AutoUpdater::new(version, http_client);
145
146        let poll_for_updates = ReleaseChannel::try_global(cx)
147            .map(|channel| channel.poll_for_updates())
148            .unwrap_or(false);
149
150        if option_env!("ZED_UPDATE_EXPLANATION").is_none()
151            && env::var("ZED_UPDATE_EXPLANATION").is_err()
152            && poll_for_updates
153        {
154            let mut update_subscription = AutoUpdateSetting::get_global(cx)
155                .0
156                .then(|| updater.start_polling(cx));
157
158            cx.observe_global::<SettingsStore>(move |updater, cx| {
159                if AutoUpdateSetting::get_global(cx).0 {
160                    if update_subscription.is_none() {
161                        update_subscription = Some(updater.start_polling(cx))
162                    }
163                } else {
164                    update_subscription.take();
165                }
166            })
167            .detach();
168        }
169
170        updater
171    });
172    cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
173}
174
175pub fn check(_: &Check, cx: &mut WindowContext) {
176    if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION") {
177        drop(cx.prompt(
178            gpui::PromptLevel::Info,
179            "Zed was installed via a package manager.",
180            Some(message),
181            &["Ok"],
182        ));
183        return;
184    }
185
186    if let Some(message) = env::var("ZED_UPDATE_EXPLANATION").ok() {
187        drop(cx.prompt(
188            gpui::PromptLevel::Info,
189            "Zed was installed via a package manager.",
190            Some(&message),
191            &["Ok"],
192        ));
193        return;
194    }
195
196    if !ReleaseChannel::try_global(cx)
197        .map(|channel| channel.poll_for_updates())
198        .unwrap_or(false)
199    {
200        return;
201    }
202
203    if let Some(updater) = AutoUpdater::get(cx) {
204        updater.update(cx, |updater, cx| updater.poll(cx));
205    } else {
206        drop(cx.prompt(
207            gpui::PromptLevel::Info,
208            "Could not check for updates",
209            Some("Auto-updates disabled for non-bundled app."),
210            &["Ok"],
211        ));
212    }
213}
214
215pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut AppContext) -> Option<()> {
216    let auto_updater = AutoUpdater::get(cx)?;
217    let release_channel = ReleaseChannel::try_global(cx)?;
218
219    if matches!(
220        release_channel,
221        ReleaseChannel::Stable | ReleaseChannel::Preview
222    ) {
223        let auto_updater = auto_updater.read(cx);
224        let release_channel = release_channel.dev_name();
225        let current_version = auto_updater.current_version;
226        let url = &auto_updater
227            .http_client
228            .build_url(&format!("/releases/{release_channel}/{current_version}"));
229        cx.open_url(&url);
230    }
231
232    None
233}
234
235fn view_release_notes_locally(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
236    let release_channel = ReleaseChannel::global(cx);
237    let version = AppVersion::global(cx).to_string();
238
239    let client = client::Client::global(cx).http_client();
240    let url = client.build_url(&format!(
241        "/api/release_notes/{}/{}",
242        release_channel.dev_name(),
243        version
244    ));
245
246    let markdown = workspace
247        .app_state()
248        .languages
249        .language_for_name("Markdown");
250
251    workspace
252        .with_local_workspace(cx, move |_, cx| {
253            cx.spawn(|workspace, mut cx| async move {
254                let markdown = markdown.await.log_err();
255                let response = client.get(&url, Default::default(), true).await;
256                let Some(mut response) = response.log_err() else {
257                    return;
258                };
259
260                let mut body = Vec::new();
261                response.body_mut().read_to_end(&mut body).await.ok();
262
263                let body: serde_json::Result<ReleaseNotesBody> =
264                    serde_json::from_slice(body.as_slice());
265
266                if let Ok(body) = body {
267                    workspace
268                        .update(&mut cx, |workspace, cx| {
269                            let project = workspace.project().clone();
270                            let buffer = project.update(cx, |project, cx| {
271                                project.create_local_buffer("", markdown, cx)
272                            });
273                            buffer.update(cx, |buffer, cx| {
274                                buffer.edit([(0..0, body.release_notes)], None, cx)
275                            });
276                            let language_registry = project.read(cx).languages().clone();
277
278                            let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
279
280                            let tab_description = SharedString::from(body.title.to_string());
281                            let editor = cx.new_view(|cx| {
282                                Editor::for_multibuffer(buffer, Some(project), true, cx)
283                            });
284                            let workspace_handle = workspace.weak_handle();
285                            let view: View<MarkdownPreviewView> = MarkdownPreviewView::new(
286                                MarkdownPreviewMode::Default,
287                                editor,
288                                workspace_handle,
289                                language_registry,
290                                Some(tab_description),
291                                cx,
292                            );
293                            workspace.add_item_to_active_pane(
294                                Box::new(view.clone()),
295                                None,
296                                true,
297                                cx,
298                            );
299                            cx.notify();
300                        })
301                        .log_err();
302                }
303            })
304            .detach();
305        })
306        .detach();
307}
308
309pub fn notify_of_any_new_update(cx: &mut ViewContext<Workspace>) -> Option<()> {
310    let updater = AutoUpdater::get(cx)?;
311    let version = updater.read(cx).current_version;
312    let should_show_notification = updater.read(cx).should_show_update_notification(cx);
313
314    cx.spawn(|workspace, mut cx| async move {
315        let should_show_notification = should_show_notification.await?;
316        if should_show_notification {
317            workspace.update(&mut cx, |workspace, cx| {
318                workspace.show_notification(
319                    NotificationId::unique::<UpdateNotification>(),
320                    cx,
321                    |cx| cx.new_view(|_| UpdateNotification::new(version)),
322                );
323                updater
324                    .read(cx)
325                    .set_should_show_update_notification(false, cx)
326                    .detach_and_log_err(cx);
327            })?;
328        }
329        anyhow::Ok(())
330    })
331    .detach();
332
333    None
334}
335
336impl AutoUpdater {
337    pub fn get(cx: &mut AppContext) -> Option<Model<Self>> {
338        cx.default_global::<GlobalAutoUpdate>().0.clone()
339    }
340
341    fn new(current_version: SemanticVersion, http_client: Arc<HttpClientWithUrl>) -> Self {
342        Self {
343            status: AutoUpdateStatus::Idle,
344            current_version,
345            http_client,
346            pending_poll: None,
347        }
348    }
349
350    pub fn start_polling(&self, cx: &mut ModelContext<Self>) -> Task<Result<()>> {
351        cx.spawn(|this, mut cx| async move {
352            loop {
353                this.update(&mut cx, |this, cx| this.poll(cx))?;
354                cx.background_executor().timer(POLL_INTERVAL).await;
355            }
356        })
357    }
358
359    pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
360        if self.pending_poll.is_some() || self.status.is_updated() {
361            return;
362        }
363
364        cx.notify();
365
366        self.pending_poll = Some(cx.spawn(|this, mut cx| async move {
367            let result = Self::update(this.upgrade()?, cx.clone()).await;
368            this.update(&mut cx, |this, cx| {
369                this.pending_poll = None;
370                if let Err(error) = result {
371                    log::error!("auto-update failed: error:{:?}", error);
372                    this.status = AutoUpdateStatus::Errored;
373                    cx.notify();
374                }
375            })
376            .ok()
377        }));
378    }
379
380    pub fn status(&self) -> AutoUpdateStatus {
381        self.status.clone()
382    }
383
384    pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
385        self.status = AutoUpdateStatus::Idle;
386        cx.notify();
387    }
388
389    pub async fn get_latest_remote_server_release(
390        os: &str,
391        arch: &str,
392        mut release_channel: ReleaseChannel,
393        cx: &mut AsyncAppContext,
394    ) -> Result<PathBuf> {
395        let this = cx.update(|cx| {
396            cx.default_global::<GlobalAutoUpdate>()
397                .0
398                .clone()
399                .ok_or_else(|| anyhow!("auto-update not initialized"))
400        })??;
401
402        if release_channel == ReleaseChannel::Dev {
403            release_channel = ReleaseChannel::Nightly;
404        }
405
406        let release = Self::get_latest_release(
407            &this,
408            "zed-remote-server",
409            os,
410            arch,
411            Some(release_channel),
412            cx,
413        )
414        .await?;
415
416        let servers_dir = paths::remote_servers_dir();
417        let channel_dir = servers_dir.join(release_channel.dev_name());
418        let platform_dir = channel_dir.join(format!("{}-{}", os, arch));
419        let version_path = platform_dir.join(format!("{}.gz", release.version));
420        smol::fs::create_dir_all(&platform_dir).await.ok();
421
422        let client = this.read_with(cx, |this, _| this.http_client.clone())?;
423        if smol::fs::metadata(&version_path).await.is_err() {
424            log::info!("downloading zed-remote-server {os} {arch}");
425            download_remote_server_binary(&version_path, release, client, cx).await?;
426        }
427
428        Ok(version_path)
429    }
430
431    async fn get_latest_release(
432        this: &Model<Self>,
433        asset: &str,
434        os: &str,
435        arch: &str,
436        release_channel: Option<ReleaseChannel>,
437        cx: &mut AsyncAppContext,
438    ) -> Result<JsonRelease> {
439        let client = this.read_with(cx, |this, _| this.http_client.clone())?;
440        let mut url_string = client.build_url(&format!(
441            "/api/releases/latest?asset={}&os={}&arch={}",
442            asset, os, arch
443        ));
444        if let Some(param) = release_channel.and_then(|c| c.release_query_param()) {
445            url_string += "&";
446            url_string += param;
447        }
448
449        let mut response = client.get(&url_string, Default::default(), true).await?;
450
451        let mut body = Vec::new();
452        response
453            .body_mut()
454            .read_to_end(&mut body)
455            .await
456            .context("error reading release")?;
457
458        if !response.status().is_success() {
459            Err(anyhow!(
460                "failed to fetch release: {:?}",
461                String::from_utf8_lossy(&body),
462            ))?;
463        }
464
465        serde_json::from_slice(body.as_slice()).with_context(|| {
466            format!(
467                "error deserializing release {:?}",
468                String::from_utf8_lossy(&body),
469            )
470        })
471    }
472
473    async fn update(this: Model<Self>, mut cx: AsyncAppContext) -> Result<()> {
474        let (client, current_version, release_channel) = this.update(&mut cx, |this, cx| {
475            this.status = AutoUpdateStatus::Checking;
476            cx.notify();
477            (
478                this.http_client.clone(),
479                this.current_version,
480                ReleaseChannel::try_global(cx),
481            )
482        })?;
483
484        let release =
485            Self::get_latest_release(&this, "zed", OS, ARCH, release_channel, &mut cx).await?;
486
487        let should_download = match *RELEASE_CHANNEL {
488            ReleaseChannel::Nightly => cx
489                .update(|cx| AppCommitSha::try_global(cx).map(|sha| release.version != sha.0))
490                .ok()
491                .flatten()
492                .unwrap_or(true),
493            _ => release.version.parse::<SemanticVersion>()? > current_version,
494        };
495
496        if !should_download {
497            this.update(&mut cx, |this, cx| {
498                this.status = AutoUpdateStatus::Idle;
499                cx.notify();
500            })?;
501            return Ok(());
502        }
503
504        this.update(&mut cx, |this, cx| {
505            this.status = AutoUpdateStatus::Downloading;
506            cx.notify();
507        })?;
508
509        let temp_dir = tempfile::Builder::new()
510            .prefix("zed-auto-update")
511            .tempdir()?;
512
513        let filename = match OS {
514            "macos" => Ok("Zed.dmg"),
515            "linux" => Ok("zed.tar.gz"),
516            _ => Err(anyhow!("not supported: {:?}", OS)),
517        }?;
518        let downloaded_asset = temp_dir.path().join(filename);
519        download_release(&downloaded_asset, release, client, &cx).await?;
520
521        this.update(&mut cx, |this, cx| {
522            this.status = AutoUpdateStatus::Installing;
523            cx.notify();
524        })?;
525
526        let binary_path = match OS {
527            "macos" => install_release_macos(&temp_dir, downloaded_asset, &cx).await,
528            "linux" => install_release_linux(&temp_dir, downloaded_asset, &cx).await,
529            _ => Err(anyhow!("not supported: {:?}", OS)),
530        }?;
531
532        this.update(&mut cx, |this, cx| {
533            this.set_should_show_update_notification(true, cx)
534                .detach_and_log_err(cx);
535            this.status = AutoUpdateStatus::Updated { binary_path };
536            cx.notify();
537        })?;
538
539        Ok(())
540    }
541
542    fn set_should_show_update_notification(
543        &self,
544        should_show: bool,
545        cx: &AppContext,
546    ) -> Task<Result<()>> {
547        cx.background_executor().spawn(async move {
548            if should_show {
549                KEY_VALUE_STORE
550                    .write_kvp(
551                        SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
552                        "".to_string(),
553                    )
554                    .await?;
555            } else {
556                KEY_VALUE_STORE
557                    .delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
558                    .await?;
559            }
560            Ok(())
561        })
562    }
563
564    fn should_show_update_notification(&self, cx: &AppContext) -> Task<Result<bool>> {
565        cx.background_executor().spawn(async move {
566            Ok(KEY_VALUE_STORE
567                .read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?
568                .is_some())
569        })
570    }
571}
572
573async fn download_remote_server_binary(
574    target_path: &PathBuf,
575    release: JsonRelease,
576    client: Arc<HttpClientWithUrl>,
577    cx: &AsyncAppContext,
578) -> Result<()> {
579    let mut target_file = File::create(&target_path).await?;
580    let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
581        let telemetry = Client::global(cx).telemetry().clone();
582        let is_staff = telemetry.is_staff();
583        let installation_id = telemetry.installation_id();
584        let release_channel =
585            ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
586        let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
587
588        (
589            installation_id,
590            release_channel,
591            telemetry_enabled,
592            is_staff,
593        )
594    })?;
595    let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
596        installation_id,
597        release_channel,
598        telemetry: telemetry_enabled,
599        is_staff,
600        destination: "remote",
601    })?);
602
603    let mut response = client.get(&release.url, request_body, true).await?;
604    smol::io::copy(response.body_mut(), &mut target_file).await?;
605    Ok(())
606}
607
608async fn download_release(
609    target_path: &Path,
610    release: JsonRelease,
611    client: Arc<HttpClientWithUrl>,
612    cx: &AsyncAppContext,
613) -> Result<()> {
614    let mut target_file = File::create(&target_path).await?;
615
616    let (installation_id, release_channel, telemetry_enabled, is_staff) = cx.update(|cx| {
617        let telemetry = Client::global(cx).telemetry().clone();
618        let is_staff = telemetry.is_staff();
619        let installation_id = telemetry.installation_id();
620        let release_channel =
621            ReleaseChannel::try_global(cx).map(|release_channel| release_channel.display_name());
622        let telemetry_enabled = TelemetrySettings::get_global(cx).metrics;
623
624        (
625            installation_id,
626            release_channel,
627            telemetry_enabled,
628            is_staff,
629        )
630    })?;
631
632    let request_body = AsyncBody::from(serde_json::to_string(&UpdateRequestBody {
633        installation_id,
634        release_channel,
635        telemetry: telemetry_enabled,
636        is_staff,
637        destination: "local",
638    })?);
639
640    let mut response = client.get(&release.url, request_body, true).await?;
641    smol::io::copy(response.body_mut(), &mut target_file).await?;
642    log::info!("downloaded update. path:{:?}", target_path);
643
644    Ok(())
645}
646
647async fn install_release_linux(
648    temp_dir: &tempfile::TempDir,
649    downloaded_tar_gz: PathBuf,
650    cx: &AsyncAppContext,
651) -> Result<PathBuf> {
652    let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name())?;
653    let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
654    let running_app_path = cx.update(|cx| cx.app_path())??;
655
656    let extracted = temp_dir.path().join("zed");
657    fs::create_dir_all(&extracted)
658        .await
659        .context("failed to create directory into which to extract update")?;
660
661    let output = Command::new("tar")
662        .arg("-xzf")
663        .arg(&downloaded_tar_gz)
664        .arg("-C")
665        .arg(&extracted)
666        .output()
667        .await?;
668
669    anyhow::ensure!(
670        output.status.success(),
671        "failed to extract {:?} to {:?}: {:?}",
672        downloaded_tar_gz,
673        extracted,
674        String::from_utf8_lossy(&output.stderr)
675    );
676
677    let suffix = if channel != "stable" {
678        format!("-{}", channel)
679    } else {
680        String::default()
681    };
682    let app_folder_name = format!("zed{}.app", suffix);
683
684    let from = extracted.join(&app_folder_name);
685    let mut to = home_dir.join(".local");
686
687    let expected_suffix = format!("{}/libexec/zed-editor", app_folder_name);
688
689    if let Some(prefix) = running_app_path
690        .to_str()
691        .and_then(|str| str.strip_suffix(&expected_suffix))
692    {
693        to = PathBuf::from(prefix);
694    }
695
696    let output = Command::new("rsync")
697        .args(&["-av", "--delete"])
698        .arg(&from)
699        .arg(&to)
700        .output()
701        .await?;
702
703    anyhow::ensure!(
704        output.status.success(),
705        "failed to copy Zed update from {:?} to {:?}: {:?}",
706        from,
707        to,
708        String::from_utf8_lossy(&output.stderr)
709    );
710
711    Ok(to.join(expected_suffix))
712}
713
714async fn install_release_macos(
715    temp_dir: &tempfile::TempDir,
716    downloaded_dmg: PathBuf,
717    cx: &AsyncAppContext,
718) -> Result<PathBuf> {
719    let running_app_path = cx.update(|cx| cx.app_path())??;
720    let running_app_filename = running_app_path
721        .file_name()
722        .ok_or_else(|| anyhow!("invalid running app path"))?;
723
724    let mount_path = temp_dir.path().join("Zed");
725    let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
726
727    mounted_app_path.push("/");
728    let output = Command::new("hdiutil")
729        .args(&["attach", "-nobrowse"])
730        .arg(&downloaded_dmg)
731        .arg("-mountroot")
732        .arg(&temp_dir.path())
733        .output()
734        .await?;
735
736    anyhow::ensure!(
737        output.status.success(),
738        "failed to mount: {:?}",
739        String::from_utf8_lossy(&output.stderr)
740    );
741
742    let output = Command::new("rsync")
743        .args(&["-av", "--delete"])
744        .arg(&mounted_app_path)
745        .arg(&running_app_path)
746        .output()
747        .await?;
748
749    anyhow::ensure!(
750        output.status.success(),
751        "failed to copy app: {:?}",
752        String::from_utf8_lossy(&output.stderr)
753    );
754
755    let output = Command::new("hdiutil")
756        .args(&["detach"])
757        .arg(&mount_path)
758        .output()
759        .await?;
760
761    anyhow::ensure!(
762        output.status.success(),
763        "failed to unount: {:?}",
764        String::from_utf8_lossy(&output.stderr)
765    );
766
767    Ok(running_app_path)
768}