first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -2,13 +2,13 @@
|
||||
// wrapper implementation that allows us to not import the above type elsewhere in this workspace.
|
||||
#![allow(clippy::disallowed_types)]
|
||||
|
||||
use async_process::Child;
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
use std::future::Future;
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
use std::process::{ExitStatus, Output, Stdio};
|
||||
use std::{fmt, io};
|
||||
|
||||
use async_process::Child;
|
||||
|
||||
/// Wrapper around a [`async_process::Command`] that ensures any new Command is set with the windows
|
||||
/// `CREATE_NO_WINDOW` flag to avoid a console window temporarily popping up.
|
||||
@@ -39,6 +39,7 @@ impl Command {
|
||||
/// let mut cmd = Command::new("ls");
|
||||
/// ```
|
||||
pub fn new<S: AsRef<OsStr>>(program: S) -> Command {
|
||||
let program = crate::wsl::translate_program_for_spawn(program.as_ref());
|
||||
let inner = async_process::Command::new(program);
|
||||
Self::new_internal(inner)
|
||||
}
|
||||
@@ -51,6 +52,7 @@ impl Command {
|
||||
/// See [`setsid(2)`](https://man7.org/linux/man-pages/man2/setsid.2.html).
|
||||
#[cfg(unix)]
|
||||
pub fn new_with_session<S: AsRef<OsStr>>(program: S) -> Command {
|
||||
let program = crate::wsl::translate_program_for_spawn(program.as_ref());
|
||||
let mut command = std::process::Command::new(program);
|
||||
|
||||
// SAFETY: `pre_exec` requires the closure to be async-signal-safe.
|
||||
@@ -77,6 +79,7 @@ impl Command {
|
||||
/// This allows for killing any other processes spawned by this process
|
||||
/// when we kill this process.
|
||||
pub fn new_with_process_group<S: AsRef<OsStr>>(program: S) -> Command {
|
||||
let program = crate::wsl::translate_program_for_spawn(program.as_ref());
|
||||
#[allow(unused_mut)]
|
||||
let mut command = std::process::Command::new(program);
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ impl Command {
|
||||
/// .expect("sh command failed to start");
|
||||
/// ```
|
||||
pub fn new<S: AsRef<OsStr>>(program: S) -> Command {
|
||||
let program = crate::wsl::translate_program_for_spawn(program.as_ref());
|
||||
#[cfg_attr(not(windows), expect(unused_mut))]
|
||||
let mut inner = std::process::Command::new(program);
|
||||
|
||||
@@ -97,7 +98,6 @@ impl Command {
|
||||
///
|
||||
/// [1]: https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863(v=vs.85).aspx
|
||||
pub fn creation_flags(&mut self, flags: u32) -> &mut Self {
|
||||
use std::os::windows::process::CommandExt;
|
||||
let flags = windows::Win32::System::Threading::CREATE_NO_WINDOW.0 | flags;
|
||||
self.inner.creation_flags(flags);
|
||||
self
|
||||
|
||||
@@ -13,5 +13,6 @@ pub mod blocking;
|
||||
pub mod unix;
|
||||
#[cfg(windows)]
|
||||
pub mod windows;
|
||||
pub mod wsl;
|
||||
|
||||
pub use std::process::{ExitStatus, Output, Stdio};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use async_process::unix::CommandExt as _;
|
||||
use std::ffi::OsStr;
|
||||
use std::io;
|
||||
use std::io::Error;
|
||||
use std::os::unix::process::CommandExt as _;
|
||||
|
||||
use async_process::unix::CommandExt as _;
|
||||
|
||||
/// Unix-specific extensions to the [`Command`] builder.
|
||||
///
|
||||
/// This trait is sealed: it cannot be implemented outside `async-process`.
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
//! 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
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
use std::ffi::OsStr;
|
||||
#[cfg(unix)]
|
||||
use std::ffi::OsString;
|
||||
#[cfg(unix)]
|
||||
use std::fs;
|
||||
#[cfg(unix)]
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::{known_bare_name, resolve_binary_in_wsl_safe_path};
|
||||
|
||||
#[cfg(unix)]
|
||||
fn make_executable(path: &std::path::Path) {
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
let mut perms = fs::metadata(path).unwrap().permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(path, perms).unwrap();
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn write_exec(path: &std::path::Path) {
|
||||
fs::write(path, b"#!/bin/sh\nexit 0\n").unwrap();
|
||||
make_executable(path);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn join(parts: &[PathBuf]) -> OsString {
|
||||
std::env::join_paths(parts).unwrap()
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn picks_first_linux_path_when_wsl() {
|
||||
let linux_dir = tempfile::tempdir().unwrap();
|
||||
write_exec(&linux_dir.path().join("git"));
|
||||
|
||||
// `/mnt/c/...` paths in the synthetic PATH that don't exist on
|
||||
// disk simulate the WSL-with-Windows-git layout: the resolver
|
||||
// should skip them on the prefix and never stat them.
|
||||
let path_env = join(&[
|
||||
PathBuf::from("/mnt/c/Program Files/Git/cmd"),
|
||||
linux_dir.path().to_path_buf(),
|
||||
]);
|
||||
|
||||
let resolved =
|
||||
resolve_binary_in_wsl_safe_path("git", Some(path_env.as_os_str()), true).unwrap();
|
||||
assert_eq!(resolved, linux_dir.path().join("git"));
|
||||
assert!(!resolved.starts_with("/mnt"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn passes_through_first_match_when_not_wsl() {
|
||||
// When not WSL, `/mnt/...` is just another directory; the resolver
|
||||
// should pick the first dir on PATH that contains an exec match.
|
||||
let mnt_dir = tempfile::tempdir().unwrap();
|
||||
write_exec(&mnt_dir.path().join("git"));
|
||||
let other_dir = tempfile::tempdir().unwrap();
|
||||
write_exec(&other_dir.path().join("git"));
|
||||
|
||||
let path_env = join(&[mnt_dir.path().to_path_buf(), other_dir.path().to_path_buf()]);
|
||||
|
||||
let resolved =
|
||||
resolve_binary_in_wsl_safe_path("git", Some(path_env.as_os_str()), false).unwrap();
|
||||
assert_eq!(resolved, mnt_dir.path().join("git"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn falls_back_to_none_when_only_mnt_has_git() {
|
||||
let path_env = join(&[
|
||||
PathBuf::from("/mnt/c/Program Files/Git/cmd"),
|
||||
PathBuf::from("/mnt/c/Windows/System32"),
|
||||
]);
|
||||
assert!(resolve_binary_in_wsl_safe_path("git", Some(path_env.as_os_str()), true).is_none());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn picks_user_local_bin() {
|
||||
let bin_dir = tempfile::tempdir().unwrap();
|
||||
let empty_dir = bin_dir.path().join("empty");
|
||||
fs::create_dir_all(&empty_dir).unwrap();
|
||||
let local_bin = bin_dir.path().join("home/.local/bin");
|
||||
fs::create_dir_all(&local_bin).unwrap();
|
||||
write_exec(&local_bin.join("git"));
|
||||
|
||||
// PATH order: an empty dir, then a `/mnt/...` candidate that
|
||||
// should be skipped on WSL, then the directory with the real
|
||||
// exec. The resolver must walk past the first two and land on
|
||||
// the third.
|
||||
let path_env = join(&[
|
||||
empty_dir,
|
||||
PathBuf::from("/mnt/c/Program Files/Git/cmd"),
|
||||
local_bin.clone(),
|
||||
]);
|
||||
let resolved =
|
||||
resolve_binary_in_wsl_safe_path("git", Some(path_env.as_os_str()), true).unwrap();
|
||||
assert_eq!(resolved, local_bin.join("git"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn follows_symlinks() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let real = dir.path().join("git-wrapper");
|
||||
write_exec(&real);
|
||||
let link = dir.path().join("git");
|
||||
std::os::unix::fs::symlink(&real, &link).unwrap();
|
||||
|
||||
let path_env = join(&[dir.path().to_path_buf()]);
|
||||
let resolved =
|
||||
resolve_binary_in_wsl_safe_path("git", Some(path_env.as_os_str()), true).unwrap();
|
||||
assert_eq!(resolved, link);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn skips_non_executable() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
fs::write(dir.path().join("git"), b"not exec").unwrap();
|
||||
// Intentionally do NOT chmod +x.
|
||||
|
||||
let path_env = join(&[dir.path().to_path_buf()]);
|
||||
assert!(resolve_binary_in_wsl_safe_path("git", Some(path_env.as_os_str()), true).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_empty_path_env() {
|
||||
assert!(resolve_binary_in_wsl_safe_path("git", None, true).is_none());
|
||||
assert!(resolve_binary_in_wsl_safe_path("git", None, false).is_none());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn handles_non_utf8_path_components() {
|
||||
use std::os::unix::ffi::OsStringExt as _;
|
||||
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
write_exec(&dir.path().join("git"));
|
||||
|
||||
// Build a PATH whose first component is a non-UTF-8 byte sequence,
|
||||
// followed by a real directory. The resolver must walk past the
|
||||
// garbage entry without panicking and find the valid one.
|
||||
let mut bytes = b"/\xff\xfe/bad:".to_vec();
|
||||
bytes.extend_from_slice(dir.path().as_os_str().as_encoded_bytes());
|
||||
let path_env = OsString::from_vec(bytes);
|
||||
|
||||
let resolved =
|
||||
resolve_binary_in_wsl_safe_path("git", Some(path_env.as_os_str()), true).unwrap();
|
||||
assert_eq!(resolved, dir.path().join("git"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_bare_name_recognizes_git_and_gh() {
|
||||
assert_eq!(known_bare_name(OsStr::new("git")), Some("git"));
|
||||
assert_eq!(known_bare_name(OsStr::new("gh")), Some("gh"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_bare_name_skips_paths() {
|
||||
assert_eq!(known_bare_name(OsStr::new("/usr/bin/git")), None);
|
||||
assert_eq!(known_bare_name(OsStr::new("./git")), None);
|
||||
assert_eq!(known_bare_name(OsStr::new("bin/git")), None);
|
||||
#[cfg(windows)]
|
||||
assert_eq!(known_bare_name(OsStr::new("C:\\git\\git.exe")), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_bare_name_skips_unknowns() {
|
||||
assert_eq!(known_bare_name(OsStr::new("ls")), None);
|
||||
assert_eq!(known_bare_name(OsStr::new("python")), None);
|
||||
assert_eq!(known_bare_name(OsStr::new("")), None);
|
||||
}
|
||||
Reference in New Issue
Block a user