Compare commits

...

1 Commits

Author SHA1 Message Date
David Wiesen
9d38d1241a fix: fall back when Windows PowerShell shell startup fails 2026-04-10 10:50:53 -07:00
2 changed files with 93 additions and 4 deletions

View File

@@ -3,6 +3,10 @@ use crate::shell_snapshot::ShellSnapshot;
use serde::Deserialize;
use serde::Serialize;
use std::path::PathBuf;
#[cfg(windows)]
use std::process::Command;
#[cfg(windows)]
use std::process::Stdio;
use std::sync::Arc;
use tokio::sync::watch;
@@ -315,7 +319,11 @@ pub fn default_user_shell() -> Shell {
fn default_user_shell_from_path(user_shell_path: Option<PathBuf>) -> Shell {
if cfg!(windows) {
get_shell(ShellType::PowerShell, /*path*/ None).unwrap_or(ultimate_fallback_shell())
default_windows_shell(
get_shell(ShellType::PowerShell, /*path*/ None),
get_shell(ShellType::Cmd, /*path*/ None),
shell_starts_successfully,
)
} else {
let user_default_shell = user_shell_path
.and_then(|shell| detect_shell_type(&shell))
@@ -335,6 +343,38 @@ fn default_user_shell_from_path(user_shell_path: Option<PathBuf>) -> Shell {
}
}
fn default_windows_shell(
powershell_shell: Option<Shell>,
cmd_shell: Option<Shell>,
powershell_probe: impl Fn(&Shell) -> bool,
) -> Shell {
powershell_shell
.filter(powershell_probe)
.or(cmd_shell)
.unwrap_or_else(ultimate_fallback_shell)
}
#[cfg(windows)]
fn shell_starts_successfully(shell: &Shell) -> bool {
let args = shell.derive_exec_args("$PSVersionTable.PSVersion | Out-Null", false);
let Some((program, args)) = args.split_first() else {
return false;
};
Command::new(program)
.args(args)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|status| status.success())
}
#[cfg(not(windows))]
fn shell_starts_successfully(_shell: &Shell) -> bool {
true
}
#[cfg(test)]
mod detect_shell_type_tests {
use super::*;

View File

@@ -86,6 +86,44 @@ fn can_run_on_shell_test() {
}
}
#[test]
fn windows_default_shell_prefers_healthy_powershell() {
let powershell = Shell {
shell_type: ShellType::PowerShell,
shell_path: PathBuf::from("pwsh.exe"),
shell_snapshot: empty_shell_snapshot_receiver(),
};
let cmd = Shell {
shell_type: ShellType::Cmd,
shell_path: PathBuf::from("cmd.exe"),
shell_snapshot: empty_shell_snapshot_receiver(),
};
assert_eq!(
default_windows_shell(Some(powershell), Some(cmd), |_| true).shell_type,
ShellType::PowerShell
);
}
#[test]
fn windows_default_shell_falls_back_to_cmd_when_powershell_fails_probe() {
let powershell = Shell {
shell_type: ShellType::PowerShell,
shell_path: PathBuf::from("pwsh.exe"),
shell_snapshot: empty_shell_snapshot_receiver(),
};
let cmd = Shell {
shell_type: ShellType::Cmd,
shell_path: PathBuf::from("cmd.exe"),
shell_snapshot: empty_shell_snapshot_receiver(),
};
assert_eq!(
default_windows_shell(Some(powershell), Some(cmd), |_| false).shell_type,
ShellType::Cmd
);
}
fn shell_works(shell: Option<Shell>, command: &str, required: bool) -> bool {
if let Some(shell) = shell {
let args = shell.derive_exec_args(command, /*use_login_shell*/ false);
@@ -173,10 +211,21 @@ async fn detects_powershell_as_default() {
return;
}
let powershell_shell = default_user_shell();
let shell_path = powershell_shell.shell_path;
let default_shell = default_user_shell();
assert!(shell_path.ends_with("pwsh.exe") || shell_path.ends_with("powershell.exe"));
match default_shell.shell_type {
ShellType::PowerShell => {
let shell_path = default_shell.shell_path;
assert!(shell_path.ends_with("pwsh.exe") || shell_path.ends_with("powershell.exe"));
}
ShellType::Cmd => {
assert!(
!get_shell(ShellType::PowerShell, /*path*/ None)
.is_some_and(|shell| shell_starts_successfully(&shell))
);
}
other => panic!("unexpected Windows default shell: {other:?}"),
}
}
#[test]