mirror of
https://github.com/openai/codex.git
synced 2026-05-15 16:53:05 +00:00
Recently, I merged a number of PRs to increase startup timeouts for scripts that ran under PowerShell, but in the failure for `suite::codex_tool::test_shell_command_approval_triggers_elicitation`, I found this in the error logs when running on Bazel with BuildBuddy: ``` [mcp stderr] 2026-04-02T19:54:10.758951Z ERROR codex_core::tools::router: error=Exit code: 1 [mcp stderr] Wall time: 0.2 seconds [mcp stderr] Output: [mcp stderr] 'New-Item' is not recognized as an internal or external command, [mcp stderr] operable program or batch file. [mcp stderr] ``` This error implies that the command was run under `cmd.exe` instead of `pwsh.exe`. Under GitHub Actions, I suspect that the `%PATH%` that is passed to our Bazel builder is scrubbed such that our tests cannot find PowerShell where GitHub installs it. Having these explicit fallback paths should help. While we could enable these only for tests, I don't see any harm in keeping them in production, as well.
410 lines
12 KiB
Rust
410 lines
12 KiB
Rust
use crate::shell_detect::detect_shell_type;
|
|
use crate::shell_snapshot::ShellSnapshot;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use tokio::sync::watch;
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
|
pub enum ShellType {
|
|
Zsh,
|
|
Bash,
|
|
PowerShell,
|
|
Sh,
|
|
Cmd,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Shell {
|
|
pub(crate) shell_type: ShellType,
|
|
pub(crate) shell_path: PathBuf,
|
|
#[serde(
|
|
skip_serializing,
|
|
skip_deserializing,
|
|
default = "empty_shell_snapshot_receiver"
|
|
)]
|
|
pub(crate) shell_snapshot: watch::Receiver<Option<Arc<ShellSnapshot>>>,
|
|
}
|
|
|
|
impl Shell {
|
|
pub fn name(&self) -> &'static str {
|
|
match self.shell_type {
|
|
ShellType::Zsh => "zsh",
|
|
ShellType::Bash => "bash",
|
|
ShellType::PowerShell => "powershell",
|
|
ShellType::Sh => "sh",
|
|
ShellType::Cmd => "cmd",
|
|
}
|
|
}
|
|
|
|
/// Takes a string of shell and returns the full list of command args to
|
|
/// use with `exec()` to run the shell command.
|
|
pub fn derive_exec_args(&self, command: &str, use_login_shell: bool) -> Vec<String> {
|
|
match self.shell_type {
|
|
ShellType::Zsh | ShellType::Bash | ShellType::Sh => {
|
|
let arg = if use_login_shell { "-lc" } else { "-c" };
|
|
vec![
|
|
self.shell_path.to_string_lossy().to_string(),
|
|
arg.to_string(),
|
|
command.to_string(),
|
|
]
|
|
}
|
|
ShellType::PowerShell => {
|
|
let mut args = vec![self.shell_path.to_string_lossy().to_string()];
|
|
if !use_login_shell {
|
|
args.push("-NoProfile".to_string());
|
|
}
|
|
|
|
args.push("-Command".to_string());
|
|
args.push(command.to_string());
|
|
args
|
|
}
|
|
ShellType::Cmd => {
|
|
let mut args = vec![self.shell_path.to_string_lossy().to_string()];
|
|
args.push("/c".to_string());
|
|
args.push(command.to_string());
|
|
args
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Return the shell snapshot if existing.
|
|
pub fn shell_snapshot(&self) -> Option<Arc<ShellSnapshot>> {
|
|
self.shell_snapshot.borrow().clone()
|
|
}
|
|
}
|
|
|
|
pub(crate) fn empty_shell_snapshot_receiver() -> watch::Receiver<Option<Arc<ShellSnapshot>>> {
|
|
let (_tx, rx) = watch::channel(None);
|
|
rx
|
|
}
|
|
|
|
impl PartialEq for Shell {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.shell_type == other.shell_type && self.shell_path == other.shell_path
|
|
}
|
|
}
|
|
|
|
impl Eq for Shell {}
|
|
|
|
#[cfg(unix)]
|
|
fn get_user_shell_path() -> Option<PathBuf> {
|
|
let uid = unsafe { libc::getuid() };
|
|
use std::ffi::CStr;
|
|
use std::mem::MaybeUninit;
|
|
use std::ptr;
|
|
|
|
let mut passwd = MaybeUninit::<libc::passwd>::uninit();
|
|
|
|
// We cannot use getpwuid here: it returns pointers into libc-managed
|
|
// storage, which is not safe to read concurrently on all targets (the musl
|
|
// static build used by the CLI can segfault when parallel callers race on
|
|
// that buffer). getpwuid_r keeps the passwd data in caller-owned memory.
|
|
let suggested_buffer_len = unsafe { libc::sysconf(libc::_SC_GETPW_R_SIZE_MAX) };
|
|
let buffer_len = usize::try_from(suggested_buffer_len)
|
|
.ok()
|
|
.filter(|len| *len > 0)
|
|
.unwrap_or(1024);
|
|
let mut buffer = vec![0; buffer_len];
|
|
|
|
loop {
|
|
let mut result = ptr::null_mut();
|
|
let status = unsafe {
|
|
libc::getpwuid_r(
|
|
uid,
|
|
passwd.as_mut_ptr(),
|
|
buffer.as_mut_ptr().cast(),
|
|
buffer.len(),
|
|
&mut result,
|
|
)
|
|
};
|
|
|
|
if status == 0 {
|
|
if result.is_null() {
|
|
return None;
|
|
}
|
|
|
|
let passwd = unsafe { passwd.assume_init_ref() };
|
|
if passwd.pw_shell.is_null() {
|
|
return None;
|
|
}
|
|
|
|
let shell_path = unsafe { CStr::from_ptr(passwd.pw_shell) }
|
|
.to_string_lossy()
|
|
.into_owned();
|
|
return Some(PathBuf::from(shell_path));
|
|
}
|
|
|
|
if status != libc::ERANGE {
|
|
return None;
|
|
}
|
|
|
|
// Retry with a larger buffer until libc can materialize the passwd entry.
|
|
let new_len = buffer.len().checked_mul(2)?;
|
|
if new_len > 1024 * 1024 {
|
|
return None;
|
|
}
|
|
buffer.resize(new_len, 0);
|
|
}
|
|
}
|
|
|
|
#[cfg(not(unix))]
|
|
fn get_user_shell_path() -> Option<PathBuf> {
|
|
None
|
|
}
|
|
|
|
fn file_exists(path: &PathBuf) -> Option<PathBuf> {
|
|
if std::fs::metadata(path).is_ok_and(|metadata| metadata.is_file()) {
|
|
Some(PathBuf::from(path))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
fn get_shell_path(
|
|
shell_type: ShellType,
|
|
provided_path: Option<&PathBuf>,
|
|
binary_name: &str,
|
|
fallback_paths: &[&str],
|
|
) -> Option<PathBuf> {
|
|
// If exact provided path exists, use it
|
|
if provided_path.and_then(file_exists).is_some() {
|
|
return provided_path.cloned();
|
|
}
|
|
|
|
// Check if the shell we are trying to load is user's default shell
|
|
// if just use it
|
|
let default_shell_path = get_user_shell_path();
|
|
if let Some(default_shell_path) = default_shell_path
|
|
&& detect_shell_type(&default_shell_path) == Some(shell_type)
|
|
&& file_exists(&default_shell_path).is_some()
|
|
{
|
|
return Some(default_shell_path);
|
|
}
|
|
|
|
if let Ok(path) = which::which(binary_name) {
|
|
return Some(path);
|
|
}
|
|
|
|
for path in fallback_paths {
|
|
//check exists
|
|
if let Some(path) = file_exists(&PathBuf::from(path)) {
|
|
return Some(path);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
const ZSH_FALLBACK_PATHS: &[&str] = &["/bin/zsh"];
|
|
|
|
fn get_zsh_shell(path: Option<&PathBuf>) -> Option<Shell> {
|
|
let shell_path = get_shell_path(ShellType::Zsh, path, "zsh", ZSH_FALLBACK_PATHS);
|
|
|
|
shell_path.map(|shell_path| Shell {
|
|
shell_type: ShellType::Zsh,
|
|
shell_path,
|
|
shell_snapshot: empty_shell_snapshot_receiver(),
|
|
})
|
|
}
|
|
|
|
const BASH_FALLBACK_PATHS: &[&str] = &["/bin/bash"];
|
|
|
|
fn get_bash_shell(path: Option<&PathBuf>) -> Option<Shell> {
|
|
let shell_path = get_shell_path(ShellType::Bash, path, "bash", BASH_FALLBACK_PATHS);
|
|
|
|
shell_path.map(|shell_path| Shell {
|
|
shell_type: ShellType::Bash,
|
|
shell_path,
|
|
shell_snapshot: empty_shell_snapshot_receiver(),
|
|
})
|
|
}
|
|
|
|
const SH_FALLBACK_PATHS: &[&str] = &["/bin/sh"];
|
|
|
|
fn get_sh_shell(path: Option<&PathBuf>) -> Option<Shell> {
|
|
let shell_path = get_shell_path(ShellType::Sh, path, "sh", SH_FALLBACK_PATHS);
|
|
|
|
shell_path.map(|shell_path| Shell {
|
|
shell_type: ShellType::Sh,
|
|
shell_path,
|
|
shell_snapshot: empty_shell_snapshot_receiver(),
|
|
})
|
|
}
|
|
|
|
// Note the `pwsh` and `powershell` fallback paths are where the respective
|
|
// shells are commonly installed on GitHub Actions Windows runners, but may not
|
|
// be present on all Windows machines:
|
|
// https://docs.github.com/en/actions/tutorials/build-and-test-code/powershell
|
|
|
|
#[cfg(windows)]
|
|
const PWSH_FALLBACK_PATHS: &[&str] = &[r#"C:\Program Files\PowerShell\7\pwsh.exe"#];
|
|
#[cfg(not(windows))]
|
|
const PWSH_FALLBACK_PATHS: &[&str] = &["/usr/local/bin/pwsh"];
|
|
|
|
#[cfg(windows)]
|
|
const POWERSHELL_FALLBACK_PATHS: &[&str] =
|
|
&[r#"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe"#];
|
|
#[cfg(not(windows))]
|
|
const POWERSHELL_FALLBACK_PATHS: &[&str] = &[];
|
|
|
|
fn get_powershell_shell(path: Option<&PathBuf>) -> Option<Shell> {
|
|
let shell_path = get_shell_path(ShellType::PowerShell, path, "pwsh", PWSH_FALLBACK_PATHS)
|
|
.or_else(|| {
|
|
get_shell_path(
|
|
ShellType::PowerShell,
|
|
path,
|
|
"powershell",
|
|
POWERSHELL_FALLBACK_PATHS,
|
|
)
|
|
});
|
|
|
|
shell_path.map(|shell_path| Shell {
|
|
shell_type: ShellType::PowerShell,
|
|
shell_path,
|
|
shell_snapshot: empty_shell_snapshot_receiver(),
|
|
})
|
|
}
|
|
|
|
fn get_cmd_shell(path: Option<&PathBuf>) -> Option<Shell> {
|
|
let shell_path = get_shell_path(ShellType::Cmd, path, "cmd", &[]);
|
|
|
|
shell_path.map(|shell_path| Shell {
|
|
shell_type: ShellType::Cmd,
|
|
shell_path,
|
|
shell_snapshot: empty_shell_snapshot_receiver(),
|
|
})
|
|
}
|
|
|
|
fn ultimate_fallback_shell() -> Shell {
|
|
if cfg!(windows) {
|
|
Shell {
|
|
shell_type: ShellType::Cmd,
|
|
shell_path: PathBuf::from("cmd.exe"),
|
|
shell_snapshot: empty_shell_snapshot_receiver(),
|
|
}
|
|
} else {
|
|
Shell {
|
|
shell_type: ShellType::Sh,
|
|
shell_path: PathBuf::from("/bin/sh"),
|
|
shell_snapshot: empty_shell_snapshot_receiver(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn get_shell_by_model_provided_path(shell_path: &PathBuf) -> Shell {
|
|
detect_shell_type(shell_path)
|
|
.and_then(|shell_type| get_shell(shell_type, Some(shell_path)))
|
|
.unwrap_or(ultimate_fallback_shell())
|
|
}
|
|
|
|
pub fn get_shell(shell_type: ShellType, path: Option<&PathBuf>) -> Option<Shell> {
|
|
match shell_type {
|
|
ShellType::Zsh => get_zsh_shell(path),
|
|
ShellType::Bash => get_bash_shell(path),
|
|
ShellType::PowerShell => get_powershell_shell(path),
|
|
ShellType::Sh => get_sh_shell(path),
|
|
ShellType::Cmd => get_cmd_shell(path),
|
|
}
|
|
}
|
|
|
|
pub fn default_user_shell() -> Shell {
|
|
default_user_shell_from_path(get_user_shell_path())
|
|
}
|
|
|
|
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())
|
|
} else {
|
|
let user_default_shell = user_shell_path
|
|
.and_then(|shell| detect_shell_type(&shell))
|
|
.and_then(|shell_type| get_shell(shell_type, /*path*/ None));
|
|
|
|
let shell_with_fallback = if cfg!(target_os = "macos") {
|
|
user_default_shell
|
|
.or_else(|| get_shell(ShellType::Zsh, /*path*/ None))
|
|
.or_else(|| get_shell(ShellType::Bash, /*path*/ None))
|
|
} else {
|
|
user_default_shell
|
|
.or_else(|| get_shell(ShellType::Bash, /*path*/ None))
|
|
.or_else(|| get_shell(ShellType::Zsh, /*path*/ None))
|
|
};
|
|
|
|
shell_with_fallback.unwrap_or(ultimate_fallback_shell())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod detect_shell_type_tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_detect_shell_type() {
|
|
assert_eq!(
|
|
detect_shell_type(&PathBuf::from("zsh")),
|
|
Some(ShellType::Zsh)
|
|
);
|
|
assert_eq!(
|
|
detect_shell_type(&PathBuf::from("bash")),
|
|
Some(ShellType::Bash)
|
|
);
|
|
assert_eq!(
|
|
detect_shell_type(&PathBuf::from("pwsh")),
|
|
Some(ShellType::PowerShell)
|
|
);
|
|
assert_eq!(
|
|
detect_shell_type(&PathBuf::from("powershell")),
|
|
Some(ShellType::PowerShell)
|
|
);
|
|
assert_eq!(detect_shell_type(&PathBuf::from("fish")), None);
|
|
assert_eq!(detect_shell_type(&PathBuf::from("other")), None);
|
|
assert_eq!(
|
|
detect_shell_type(&PathBuf::from("/bin/zsh")),
|
|
Some(ShellType::Zsh)
|
|
);
|
|
assert_eq!(
|
|
detect_shell_type(&PathBuf::from("/bin/bash")),
|
|
Some(ShellType::Bash)
|
|
);
|
|
assert_eq!(
|
|
detect_shell_type(&PathBuf::from("powershell.exe")),
|
|
Some(ShellType::PowerShell)
|
|
);
|
|
assert_eq!(
|
|
detect_shell_type(&PathBuf::from(if cfg!(windows) {
|
|
"C:\\windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
|
|
} else {
|
|
"/usr/local/bin/pwsh"
|
|
})),
|
|
Some(ShellType::PowerShell)
|
|
);
|
|
assert_eq!(
|
|
detect_shell_type(&PathBuf::from("pwsh.exe")),
|
|
Some(ShellType::PowerShell)
|
|
);
|
|
assert_eq!(
|
|
detect_shell_type(&PathBuf::from("/usr/local/bin/pwsh")),
|
|
Some(ShellType::PowerShell)
|
|
);
|
|
assert_eq!(
|
|
detect_shell_type(&PathBuf::from("/bin/sh")),
|
|
Some(ShellType::Sh)
|
|
);
|
|
assert_eq!(detect_shell_type(&PathBuf::from("sh")), Some(ShellType::Sh));
|
|
assert_eq!(
|
|
detect_shell_type(&PathBuf::from("cmd")),
|
|
Some(ShellType::Cmd)
|
|
);
|
|
assert_eq!(
|
|
detect_shell_type(&PathBuf::from("cmd.exe")),
|
|
Some(ShellType::Cmd)
|
|
);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[cfg(unix)]
|
|
#[path = "shell_tests.rs"]
|
|
mod tests;
|