linux: Store binary path before restart to handle deleted binary file (#11568)

Thorsten Ball created

This fixes restart after updates not working on Linux.

On Linux we can't reliably get the binary path after an update, because
the original binary was deleted and the path will contain ` (deleted)`.

See: https://github.com/rust-lang/rust/issues/69343

We *could* strip ` (deleted)` off, but that feels nasty. So instead we
save the original binary path, before we do the installation, then
restart.

Later on, we can also change this to be a _new_ binary path returned by
the installers, which we then have to start.


Release Notes:

- N/A

Change summary

crates/activity_indicator/src/activity_indicator.rs |  9 ++++--
crates/auto_update/src/auto_update.rs               | 21 +++++++++++---
crates/collab_ui/src/collab_titlebar_item.rs        |  4 +-
crates/gpui/src/app.rs                              |  4 +-
crates/gpui/src/platform.rs                         |  2 
crates/gpui/src/platform/linux/platform.rs          | 16 +++++++----
crates/gpui/src/platform/mac/platform.rs            |  2 
crates/gpui/src/platform/test/platform.rs           |  2 
crates/gpui/src/platform/windows/platform.rs        |  2 
crates/workspace/src/workspace.rs                   | 12 ++++++--
10 files changed, 49 insertions(+), 25 deletions(-)

Detailed changes

crates/activity_indicator/src/activity_indicator.rs πŸ”—

@@ -281,11 +281,14 @@ impl ActivityIndicator {
                     message: "Installing Zed update…".to_string(),
                     on_click: None,
                 },
-                AutoUpdateStatus::Updated => Content {
+                AutoUpdateStatus::Updated { binary_path } => Content {
                     icon: None,
                     message: "Click to restart and update Zed".to_string(),
-                    on_click: Some(Arc::new(|_, cx| {
-                        workspace::restart(&Default::default(), cx)
+                    on_click: Some(Arc::new({
+                        let restart = workspace::Restart {
+                            binary_path: Some(binary_path.clone()),
+                        };
+                        move |_, cx| workspace::restart(&restart, cx)
                     })),
                 },
                 AutoUpdateStatus::Errored => Content {

crates/auto_update/src/auto_update.rs πŸ”—

@@ -56,16 +56,22 @@ struct UpdateRequestBody {
     telemetry: bool,
 }
 
-#[derive(Clone, Copy, PartialEq, Eq)]
+#[derive(Clone, PartialEq, Eq)]
 pub enum AutoUpdateStatus {
     Idle,
     Checking,
     Downloading,
     Installing,
-    Updated,
+    Updated { binary_path: PathBuf },
     Errored,
 }
 
+impl AutoUpdateStatus {
+    pub fn is_updated(&self) -> bool {
+        matches!(self, Self::Updated { .. })
+    }
+}
+
 pub struct AutoUpdater {
     status: AutoUpdateStatus,
     current_version: SemanticVersion,
@@ -306,7 +312,7 @@ impl AutoUpdater {
     }
 
     pub fn poll(&mut self, cx: &mut ModelContext<Self>) {
-        if self.pending_poll.is_some() || self.status == AutoUpdateStatus::Updated {
+        if self.pending_poll.is_some() || self.status.is_updated() {
             return;
         }
 
@@ -328,7 +334,7 @@ impl AutoUpdater {
     }
 
     pub fn status(&self) -> AutoUpdateStatus {
-        self.status
+        self.status.clone()
     }
 
     pub fn dismiss_error(&mut self, cx: &mut ModelContext<Self>) {
@@ -404,6 +410,11 @@ impl AutoUpdater {
             cx.notify();
         })?;
 
+        // We store the path of our current binary, before we install, since installation might
+        // delete it. Once deleted, it's hard to get the path to our binary on Linux.
+        // So we cache it here, which allows us to then restart later on.
+        let binary_path = cx.update(|cx| cx.app_path())??;
+
         match OS {
             "macos" => install_release_macos(&temp_dir, downloaded_asset, &cx).await,
             "linux" => install_release_linux(&temp_dir, downloaded_asset, &cx).await,
@@ -413,7 +424,7 @@ impl AutoUpdater {
         this.update(&mut cx, |this, cx| {
             this.set_should_show_update_notification(true, cx)
                 .detach_and_log_err(cx);
-            this.status = AutoUpdateStatus::Updated;
+            this.status = AutoUpdateStatus::Updated { binary_path };
             cx.notify();
         })?;
 

crates/collab_ui/src/collab_titlebar_item.rs πŸ”—

@@ -677,7 +677,7 @@ impl CollabTitlebarItem {
             client::Status::UpgradeRequired => {
                 let auto_updater = auto_update::AutoUpdater::get(cx);
                 let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) {
-                    Some(AutoUpdateStatus::Updated) => "Please restart Zed to Collaborate",
+                    Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate",
                     Some(AutoUpdateStatus::Installing)
                     | Some(AutoUpdateStatus::Downloading)
                     | Some(AutoUpdateStatus::Checking) => "Updating...",
@@ -691,7 +691,7 @@ impl CollabTitlebarItem {
                         .label_size(LabelSize::Small)
                         .on_click(|_, cx| {
                             if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) {
-                                if auto_updater.read(cx).status() == AutoUpdateStatus::Updated {
+                                if auto_updater.read(cx).status().is_updated() {
                                     workspace::restart(&Default::default(), cx);
                                     return;
                                 }

crates/gpui/src/app.rs πŸ”—

@@ -642,8 +642,8 @@ impl AppContext {
     }
 
     /// Restart the application.
-    pub fn restart(&self) {
-        self.platform.restart()
+    pub fn restart(&self, binary_path: Option<PathBuf>) {
+        self.platform.restart(binary_path)
     }
 
     /// Returns the local timezone at the platform level.

crates/gpui/src/platform.rs πŸ”—

@@ -98,7 +98,7 @@ pub(crate) trait Platform: 'static {
 
     fn run(&self, on_finish_launching: Box<dyn 'static + FnOnce()>);
     fn quit(&self);
-    fn restart(&self);
+    fn restart(&self, binary_path: Option<PathBuf>);
     fn activate(&self, ignoring_other_apps: bool);
     fn hide(&self);
     fn hide_other_apps(&self);

crates/gpui/src/platform/linux/platform.rs πŸ”—

@@ -136,17 +136,21 @@ impl<P: LinuxClient + 'static> Platform for P {
         self.with_common(|common| common.signal.stop());
     }
 
-    fn restart(&self) {
+    fn restart(&self, binary_path: Option<PathBuf>) {
         use std::os::unix::process::CommandExt as _;
 
         // get the process id of the current process
         let app_pid = std::process::id().to_string();
         // get the path to the executable
-        let app_path = match self.app_path() {
-            Ok(path) => path,
-            Err(err) => {
-                log::error!("Failed to get app path: {:?}", err);
-                return;
+        let app_path = if let Some(path) = binary_path {
+            path
+        } else {
+            match self.app_path() {
+                Ok(path) => path,
+                Err(err) => {
+                    log::error!("Failed to get app path: {:?}", err);
+                    return;
+                }
             }
         };
 

crates/gpui/src/platform/mac/platform.rs πŸ”—

@@ -396,7 +396,7 @@ impl Platform for MacPlatform {
         }
     }
 
-    fn restart(&self) {
+    fn restart(&self, _binary_path: Option<PathBuf>) {
         use std::os::unix::process::CommandExt as _;
 
         let app_pid = std::process::id().to_string();

crates/gpui/src/platform/windows/platform.rs πŸ”—

@@ -268,7 +268,7 @@ impl Platform for WindowsPlatform {
             .detach();
     }
 
-    fn restart(&self) {
+    fn restart(&self, _: Option<PathBuf>) {
         let pid = std::process::id();
         let Some(app_path) = self.app_path().log_err() else {
             return;

crates/workspace/src/workspace.rs πŸ”—

@@ -130,7 +130,6 @@ actions!(
         NewCenterTerminal,
         NewSearch,
         Feedback,
-        Restart,
         Welcome,
         ToggleZoom,
         ToggleLeftDock,
@@ -185,6 +184,11 @@ pub struct CloseInactiveTabsAndPanes {
 #[derive(Clone, Deserialize, PartialEq)]
 pub struct SendKeystrokes(pub String);
 
+#[derive(Clone, Deserialize, PartialEq, Default)]
+pub struct Restart {
+    pub binary_path: Option<PathBuf>,
+}
+
 impl_actions!(
     workspace,
     [
@@ -194,6 +198,7 @@ impl_actions!(
         CloseInactiveTabsAndPanes,
         NewFileInDirection,
         OpenTerminal,
+        Restart,
         Save,
         SaveAll,
         SwapPaneInDirection,
@@ -5020,7 +5025,7 @@ pub fn join_in_room_project(
     })
 }
 
-pub fn restart(_: &Restart, cx: &mut AppContext) {
+pub fn restart(restart: &Restart, cx: &mut AppContext) {
     let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit;
     let mut workspace_windows = cx
         .windows()
@@ -5046,6 +5051,7 @@ pub fn restart(_: &Restart, cx: &mut AppContext) {
             .ok();
     }
 
+    let binary_path = restart.binary_path.clone();
     cx.spawn(|mut cx| async move {
         if let Some(prompt) = prompt {
             let answer = prompt.await?;
@@ -5065,7 +5071,7 @@ pub fn restart(_: &Restart, cx: &mut AppContext) {
             }
         }
 
-        cx.update(|cx| cx.restart())
+        cx.update(|cx| cx.restart(binary_path))
     })
     .detach_and_log_err(cx);
 }