Files
galaxy/crates/command/src/wsl.rs
T

129 lines
4.7 KiB
Rust

//! WSL detection and Linux-side binary resolution for subprocess
//! invocations made through this crate's [`Command`](crate::r#async::Command)
//! and [`Command`](crate::blocking::Command) wrappers.
//!
//! Warp ships as a Linux ELF that users routinely run inside WSL.
//! WSL's default `appendWindowsPath = true` (in `/etc/wsl.conf`)
//! puts directories like `/mnt/c/Program Files/Git/cmd/` on `PATH`,
//! so a bare `Command::new("git")` can resolve to Windows `git.exe`
//! through WSL interop. That path is dramatically slower, can
//! mishandle Linux paths, and breaks Linux-side hooks.
//!
//! [`translate_program_for_spawn`] is invoked from the wrappers'
//! `new` constructors and transparently substitutes the program
//! string when (a) we're inside WSL and (b) the program is a bare
//! name in [`KNOWN_NAMES`]. Path-qualified or unknown programs are
//! passed through unchanged. Resolution is cached for the life of
//! the process — PATH is effectively static for the host process.
//!
//! The same `/mnt/*` filtering precedent is used for `compgen` in
//! `app/src/terminal/model/session/command_executor/wsl_command_executor.rs`.
use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
#[cfg(test)]
#[path = "wsl_tests.rs"]
mod tests;
/// Bare program names whose resolution Warp wants to override under
/// WSL. Anything not in this list is passed through unchanged.
const KNOWN_NAMES: &[&str] = &["git", "gh"];
/// True when the current process is running inside a WSL guest.
/// Cached for the life of the process.
pub fn is_wsl() -> bool {
static IS_WSL: OnceLock<bool> = OnceLock::new();
*IS_WSL.get_or_init(|| Path::new("/proc/sys/fs/binfmt_misc/WSLInterop").exists())
}
/// Translate a `Command::new` program string. On WSL, bare names in
/// [`KNOWN_NAMES`] are resolved to the first executable on `PATH`
/// outside `/mnt/*`; everything else is returned unchanged so the OS
/// performs its normal lookup at spawn time.
pub(crate) fn translate_program_for_spawn(program: &OsStr) -> OsString {
if !is_wsl() {
return program.to_owned();
}
let Some(name) = known_bare_name(program) else {
return program.to_owned();
};
cached_resolve(name).to_owned()
}
/// Returns the program's name when it's a bare entry in
/// [`KNOWN_NAMES`]. Paths and absolute names are filtered out so a
/// caller that already specified `/usr/bin/git` is not touched.
fn known_bare_name(program: &OsStr) -> Option<&'static str> {
let s = program.to_str()?;
if s.contains('/') || s.contains('\\') {
return None;
}
KNOWN_NAMES.iter().copied().find(|&n| n == s)
}
fn cached_resolve(name: &'static str) -> &'static OsString {
static GIT: OnceLock<OsString> = OnceLock::new();
static GH: OnceLock<OsString> = OnceLock::new();
let cell = match name {
"git" => &GIT,
"gh" => &GH,
// `KNOWN_NAMES` is exhaustive over the static cells above; if
// a new name is added there it must also be wired here.
_ => unreachable!("unknown name {name:?}"),
};
cell.get_or_init(|| {
let path_env = std::env::var_os("PATH");
match resolve_binary_in_wsl_safe_path(name, path_env.as_deref(), true) {
Some(p) => p.into_os_string(),
None => {
log::warn!(
"wsl: no Linux-side `{name}` found on PATH (excluding /mnt/*); \
falling back to bare `{name}` which may resolve to a Windows .exe"
);
OsString::from(name)
}
}
})
}
/// Returns the first executable named `name` on `path_env`, skipping
/// any PATH entry under `/mnt/` when `is_wsl` is true. Returns `None`
/// if no acceptable match exists. Pure — exposed for unit testing
/// without depending on a real WSL host.
pub fn resolve_binary_in_wsl_safe_path(
name: &str,
path_env: Option<&OsStr>,
is_wsl: bool,
) -> Option<PathBuf> {
let path_env = path_env?;
for dir in std::env::split_paths(path_env) {
if is_wsl && dir.starts_with("/mnt") {
continue;
}
let candidate = dir.join(name);
if is_executable_file(&candidate) {
return Some(candidate);
}
}
None
}
#[cfg(unix)]
fn is_executable_file(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt as _;
match std::fs::metadata(path) {
Ok(md) => md.is_file() && (md.permissions().mode() & 0o111 != 0),
Err(_) => false,
}
}
#[cfg(not(unix))]
fn is_executable_file(_path: &Path) -> bool {
// The WSL-safe resolver only runs on Linux. Other targets short-
// circuit through `is_wsl() == false`, so this stub is unreachable
// in practice — present only to keep the crate compiling.
false
}