util: Check whether discovered powershell is actually executable (#43044)

Lukas Wirth created

Closes https://github.com/zed-industries/zed/issues/42944

The powershell we discovered might be in a directory with higher
permission requirements which will cause us to fail using it.

Release Notes:

- Fixed powershell discovery disregarding admin requirements

Change summary

crates/askpass/src/askpass.rs                |  1 
crates/gpui/src/platform/windows/platform.rs | 10 +-
crates/util/src/shell.rs                     | 80 +++++++++++++--------
3 files changed, 55 insertions(+), 36 deletions(-)

Detailed changes

crates/askpass/src/askpass.rs 🔗

@@ -250,6 +250,7 @@ impl PasswordProxy {
             .await
             .with_context(|| format!("creating askpass script at {askpass_script_path:?}"))?;
         make_file_executable(&askpass_script_path).await?;
+        // todo(shell): There might be no powershell on the system
         #[cfg(target_os = "windows")]
         let askpass_helper = format!(
             "powershell.exe -ExecutionPolicy Bypass -File {}",

crates/gpui/src/platform/windows/platform.rs 🔗

@@ -390,10 +390,12 @@ impl Platform for WindowsPlatform {
             clippy::disallowed_methods,
             reason = "We are restarting ourselves, using std command thus is fine"
         )]
-        let restart_process = util::command::new_std_command("powershell.exe")
-            .arg("-command")
-            .arg(script)
-            .spawn();
+        // todo(shell): There might be no powershell on the system
+        let restart_process =
+            util::command::new_std_command(util::shell::get_windows_system_shell())
+                .arg("-command")
+                .arg(script)
+                .spawn();
 
         match restart_process {
             Ok(_) => self.quit(),

crates/util/src/shell.rs 🔗

@@ -2,6 +2,8 @@ use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use std::{borrow::Cow, fmt, path::Path, sync::LazyLock};
 
+use crate::command::new_std_command;
+
 /// Shell configuration to open the terminal with.
 #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, Hash)]
 #[serde(rename_all = "snake_case")]
@@ -108,16 +110,12 @@ pub fn get_windows_system_shell() -> String {
     use std::path::PathBuf;
 
     fn find_pwsh_in_programfiles(find_alternate: bool, find_preview: bool) -> Option<PathBuf> {
-        #[cfg(target_pointer_width = "64")]
-        let env_var = if find_alternate {
-            "ProgramFiles(x86)"
-        } else {
-            "ProgramFiles"
-        };
-
-        #[cfg(target_pointer_width = "32")]
         let env_var = if find_alternate {
-            "ProgramW6432"
+            if cfg!(target_pointer_width = "64") {
+                "ProgramFiles(x86)"
+            } else {
+                "ProgramW6432"
+            }
         } else {
             "ProgramFiles"
         };
@@ -165,23 +163,19 @@ pub fn get_windows_system_shell() -> String {
         } else {
             "Microsoft.PowerShell_"
         };
-        msix_app_dir
-            .read_dir()
-            .ok()?
-            .filter_map(|entry| {
-                let entry = entry.ok()?;
-                if !matches!(entry.file_type(), Ok(ft) if ft.is_dir()) {
-                    return None;
-                }
+        msix_app_dir.read_dir().ok()?.find_map(|entry| {
+            let entry = entry.ok()?;
+            if !matches!(entry.file_type(), Ok(ft) if ft.is_dir()) {
+                return None;
+            }
 
-                if !entry.file_name().to_string_lossy().starts_with(prefix) {
-                    return None;
-                }
+            if !entry.file_name().to_string_lossy().starts_with(prefix) {
+                return None;
+            }
 
-                let exe_path = entry.path().join("pwsh.exe");
-                exe_path.exists().then_some(exe_path)
-            })
-            .next()
+            let exe_path = entry.path().join("pwsh.exe");
+            exe_path.exists().then_some(exe_path)
+        })
     }
 
     fn find_pwsh_in_scoop() -> Option<PathBuf> {
@@ -190,15 +184,37 @@ pub fn get_windows_system_shell() -> String {
         pwsh_exe.exists().then_some(pwsh_exe)
     }
 
+    // check whether the found powershell is executable for us
     static SYSTEM_SHELL: LazyLock<String> = LazyLock::new(|| {
-        find_pwsh_in_programfiles(false, false)
-            .or_else(|| find_pwsh_in_programfiles(true, false))
-            .or_else(|| find_pwsh_in_msix(false))
-            .or_else(|| find_pwsh_in_programfiles(false, true))
-            .or_else(|| find_pwsh_in_msix(true))
-            .or_else(|| find_pwsh_in_programfiles(true, true))
-            .or_else(find_pwsh_in_scoop)
-            .map(|p| p.to_string_lossy().into_owned())
+        let can_execute_pwsh = |p: &PathBuf| {
+            #[allow(clippy::disallowed_methods)]
+            let status = new_std_command(p).arg("-NoProfile").arg("-Help").status();
+            let success = status.as_ref().is_ok_and(|status| status.success());
+            if !success {
+                log::warn!(
+                    "Powershell found at `{}` is not executable: {status:?}",
+                    p.display()
+                );
+            }
+            success
+        };
+
+        let locations = [
+            || find_pwsh_in_programfiles(false, false),
+            || find_pwsh_in_programfiles(true, false),
+            || find_pwsh_in_msix(false),
+            || find_pwsh_in_programfiles(false, true),
+            || find_pwsh_in_msix(true),
+            || find_pwsh_in_programfiles(true, true),
+            || find_pwsh_in_scoop(),
+            || which::which_global("pwsh.exe").ok(),
+            || which::which_global("powershell.exe").ok(),
+        ];
+        locations
+            .into_iter()
+            .filter_map(|f| f())
+            .find(|p| can_execute_pwsh(&p))
+            .map(|p| p.to_string_lossy().trim().to_owned())
             .inspect(|shell| log::info!("Found powershell in: {}", shell))
             .unwrap_or_else(|| {
                 log::warn!("Powershell not found, falling back to `cmd`");