chore(vendor): import pi 1.3.0
Imported as a linear vendor commit so forge rebases do not replay the upstream root tree. git-subtree-dir: third_party/pi git-subtree-split: 71023aec1546d6d677a65669c5665c0927ce0dd0
This commit is contained in:
665
third_party/pi/packages/env/daemon/src/sys/windows.rs
vendored
Normal file
665
third_party/pi/packages/env/daemon/src/sys/windows.rs
vendored
Normal file
@@ -0,0 +1,665 @@
|
||||
//! Windows, following libuv (`src/win/error.c`, `src/win/process.c`, `src/win/fs.c`) and Node's `child_process`.
|
||||
|
||||
use super::{OpenMode, ShellConfig, path_exists, synthetic};
|
||||
use crate::errors::Failure;
|
||||
use serde_json::{Map, Value};
|
||||
use std::collections::hash_map::RandomState;
|
||||
use std::ffi::OsStr;
|
||||
use std::fs::{File, Metadata};
|
||||
use std::hash::{BuildHasher, Hasher};
|
||||
use std::io;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
use std::os::windows::fs::FileExt;
|
||||
use std::os::windows::io::{AsRawHandle, FromRawHandle};
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::path::Path;
|
||||
use std::process::{Child, Command, ExitStatus, Stdio};
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::time::UNIX_EPOCH;
|
||||
use windows_sys::Win32::Foundation::{CloseHandle, INVALID_HANDLE_VALUE};
|
||||
use windows_sys::Win32::Storage::FileSystem::{
|
||||
BY_HANDLE_FILE_INFORMATION, CREATE_ALWAYS, CreateFileW, FILE_APPEND_DATA,
|
||||
FILE_ATTRIBUTE_NORMAL, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,
|
||||
FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
|
||||
FILE_WRITE_DATA, GetFileInformationByHandle, MOVEFILE_REPLACE_EXISTING, MoveFileExW,
|
||||
OPEN_ALWAYS, OPEN_EXISTING,
|
||||
};
|
||||
use windows_sys::Win32::System::IO::CancelSynchronousIo;
|
||||
use windows_sys::Win32::System::JobObjects::{
|
||||
AssignProcessToJobObject, CreateJobObjectW, JOB_OBJECT_LIMIT_BREAKAWAY_OK,
|
||||
JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||
JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK, JOBOBJECT_EXTENDED_LIMIT_INFORMATION,
|
||||
JobObjectExtendedLimitInformation, SetInformationJobObject,
|
||||
};
|
||||
use windows_sys::Win32::System::Threading::CREATE_NO_WINDOW;
|
||||
|
||||
/// libuv's `uv_translate_sys_error` for the errors file and process operations produce.
|
||||
pub fn os_error_name(error: &io::Error) -> &'static str {
|
||||
let Some(code) = error.raw_os_error() else {
|
||||
return "UNKNOWN";
|
||||
};
|
||||
match code {
|
||||
1 => "EISDIR", // ERROR_INVALID_FUNCTION
|
||||
2 | 3 | 15 | 123 | 161 | 4392 => "ENOENT", // FILE_NOT_FOUND, PATH_NOT_FOUND, INVALID_DRIVE, INVALID_NAME, BAD_PATHNAME, INVALID_REPARSE_DATA
|
||||
4 => "EMFILE", // ERROR_TOO_MANY_OPEN_FILES
|
||||
5 | 1314 => "EPERM", // ACCESS_DENIED, PRIVILEGE_NOT_HELD
|
||||
6 => "EBADF", // ERROR_INVALID_HANDLE
|
||||
8 | 14 => "ENOMEM", // NOT_ENOUGH_MEMORY, OUTOFMEMORY
|
||||
17 => "EXDEV", // ERROR_NOT_SAME_DEVICE
|
||||
19 => "EROFS", // ERROR_WRITE_PROTECT
|
||||
32 | 33 => "EBUSY", // SHARING_VIOLATION, LOCK_VIOLATION
|
||||
39 | 82 | 112 => "ENOSPC", // HANDLE_DISK_FULL, CANNOT_MAKE, DISK_FULL
|
||||
50 => "ENOTSUP", // ERROR_NOT_SUPPORTED
|
||||
80 | 183 => "EEXIST", // FILE_EXISTS, ALREADY_EXISTS
|
||||
13 | 87 | 122 | 131 | 1464 => "EINVAL", // INVALID_DATA, INVALID_PARAMETER, INSUFFICIENT_BUFFER, NEGATIVE_SEEK, SYMLINK_NOT_SUPPORTED
|
||||
126 => "ENOENT", // ERROR_MOD_NOT_FOUND
|
||||
145 => "ENOTEMPTY", // ERROR_DIR_NOT_EMPTY
|
||||
206 => "ENAMETOOLONG", // ERROR_FILENAME_EXCED_RANGE
|
||||
267 => "ENOENT", // ERROR_DIRECTORY (libuv maps it to ENOENT)
|
||||
998 => "EFAULT", // ERROR_NOACCESS
|
||||
740 | 1920 => "EACCES", // ELEVATION_REQUIRED, CANT_ACCESS_FILE
|
||||
1921 => "ELOOP", // ERROR_CANT_RESOLVE_FILENAME
|
||||
_ => "UNKNOWN",
|
||||
}
|
||||
}
|
||||
|
||||
pub const PATH_SEPARATOR: &str = "\\";
|
||||
|
||||
pub fn read_at(file: &File, buffer: &mut [u8], offset: u64) -> io::Result<usize> {
|
||||
file.seek_read(buffer, offset)
|
||||
}
|
||||
|
||||
/// Modification time as seconds and nanoseconds since the epoch; before 1970 the seconds are negative.
|
||||
pub fn mtime(metadata: &Metadata) -> (i64, i64) {
|
||||
let Ok(time) = metadata.modified() else {
|
||||
return (0, 0);
|
||||
};
|
||||
match time.duration_since(UNIX_EPOCH) {
|
||||
Ok(after) => (after.as_secs() as i64, i64::from(after.subsec_nanos())),
|
||||
Err(before) => {
|
||||
let before = before.duration();
|
||||
let nanos = i64::from(before.subsec_nanos());
|
||||
if nanos == 0 {
|
||||
(-(before.as_secs() as i64), 0)
|
||||
} else {
|
||||
(-(before.as_secs() as i64) - 1, 1_000_000_000 - nanos)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// File index numbers need a handle per file; the creation time tells a replaced file or directory apart instead.
|
||||
pub fn identity(metadata: &Metadata) -> (u64, u64) {
|
||||
use std::os::windows::fs::MetadataExt;
|
||||
(0, metadata.creation_time())
|
||||
}
|
||||
|
||||
/// Node's `os.tmpdir()` on Windows.
|
||||
pub fn tmpdir() -> String {
|
||||
let variable = |key: &str| std::env::var(key).ok().filter(|value| !value.is_empty());
|
||||
let mut path = variable("TEMP")
|
||||
.or_else(|| variable("TMP"))
|
||||
.unwrap_or_else(|| {
|
||||
format!(
|
||||
"{}\\temp",
|
||||
variable("SystemRoot")
|
||||
.or_else(|| variable("windir"))
|
||||
.unwrap_or_default()
|
||||
)
|
||||
});
|
||||
if path.len() > 1 && path.ends_with('\\') && !path.ends_with(":\\") {
|
||||
path.pop();
|
||||
}
|
||||
path
|
||||
}
|
||||
|
||||
/// libuv's `uv_os_homedir`: `USERPROFILE` first.
|
||||
pub fn home() -> String {
|
||||
std::env::var("USERPROFILE").unwrap_or_default()
|
||||
}
|
||||
|
||||
/// A NUL-terminated wide path; an embedded NUL fails like the standard library's path conversion (Node rejects such
|
||||
/// paths before any system call, which the client reports with the same code).
|
||||
fn wide(text: &str) -> io::Result<Vec<u16>> {
|
||||
if text.contains('\0') {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"path contains a NUL byte",
|
||||
));
|
||||
}
|
||||
Ok(OsStr::new(text)
|
||||
.encode_wide()
|
||||
.chain(std::iter::once(0))
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// libuv's `fs__mkdtemp`: `prefix` plus six characters from `[a-zA-Z0-9]`, retried while the name exists.
|
||||
pub fn mkdtemp(prefix: &str) -> io::Result<String> {
|
||||
const CHARACTERS: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
let state = RandomState::new();
|
||||
for _ in 0..1000 {
|
||||
let mut hasher = state.build_hasher();
|
||||
hasher.write_u64(COUNTER.fetch_add(1, Ordering::Relaxed));
|
||||
let mut random = hasher.finish();
|
||||
let mut path = prefix.to_string();
|
||||
for _ in 0..6 {
|
||||
path.push(CHARACTERS[(random % CHARACTERS.len() as u64) as usize] as char);
|
||||
random /= CHARACTERS.len() as u64;
|
||||
}
|
||||
match std::fs::create_dir(&path) {
|
||||
Ok(()) => return Ok(path),
|
||||
Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
|
||||
Err(error) => return Err(error),
|
||||
}
|
||||
}
|
||||
Err(io::Error::from_raw_os_error(80))
|
||||
}
|
||||
|
||||
/// Windows cannot open a directory as a file and has no `O_NOFOLLOW`: check the path first, as `NodeExecutionEnv` does
|
||||
/// there (not race-free).
|
||||
pub fn open_reader(path: &str, no_follow: bool) -> Result<File, Failure> {
|
||||
if no_follow
|
||||
&& std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink())
|
||||
{
|
||||
return Err(Failure::new("SYMLINK", "Refusing to follow a symbolic link").path(path));
|
||||
}
|
||||
if std::fs::metadata(path).is_ok_and(|metadata| metadata.is_dir()) {
|
||||
return Err(
|
||||
Failure::new("EISDIR", "EISDIR: illegal operation on a directory, read").path(path),
|
||||
);
|
||||
}
|
||||
File::open(path).map_err(|error| Failure::io(&error, "open", path))
|
||||
}
|
||||
|
||||
/// libuv's `fs__open` for Node's write modes: `CreateFileW` with its access, sharing and disposition, always with
|
||||
/// `FILE_FLAG_BACKUP_SEMANTICS`, so a directory opens and later calls on it fail as in Node; creating a file where a
|
||||
/// directory exists reports `EISDIR`. (Rust's `File::create` opens with `OPEN_ALWAYS` and truncates afterwards.)
|
||||
pub fn open(path: &str, mode: OpenMode) -> io::Result<File> {
|
||||
let (access, disposition) = match mode {
|
||||
OpenMode::Read => (FILE_GENERIC_READ, OPEN_EXISTING),
|
||||
OpenMode::Write => (FILE_GENERIC_WRITE, CREATE_ALWAYS),
|
||||
OpenMode::Append => (
|
||||
(FILE_GENERIC_WRITE & !FILE_WRITE_DATA) | FILE_APPEND_DATA,
|
||||
OPEN_ALWAYS,
|
||||
),
|
||||
OpenMode::ReadWrite => (FILE_GENERIC_READ | FILE_GENERIC_WRITE, OPEN_EXISTING),
|
||||
};
|
||||
let name = wide(path)?;
|
||||
// SAFETY: a NUL-terminated wide path; the returned handle is owned by the File on success.
|
||||
let handle = unsafe {
|
||||
CreateFileW(
|
||||
name.as_ptr(),
|
||||
access,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
std::ptr::null(),
|
||||
disposition,
|
||||
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_BACKUP_SEMANTICS,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if handle == INVALID_HANDLE_VALUE {
|
||||
let error = io::Error::last_os_error();
|
||||
// ERROR_FILE_EXISTS while creating means the path is a directory.
|
||||
if error.raw_os_error() == Some(80) && matches!(mode, OpenMode::Write | OpenMode::Append) {
|
||||
return Err(synthetic("EISDIR"));
|
||||
}
|
||||
return Err(error);
|
||||
}
|
||||
// SAFETY: `handle` is a valid, owned file handle.
|
||||
Ok(unsafe { File::from_raw_handle(handle as _) })
|
||||
}
|
||||
|
||||
/// libuv's `uv_fs_realpath`: the final path without the `\\?\` prefix.
|
||||
pub fn realpath(path: &str) -> io::Result<String> {
|
||||
let resolved = std::fs::canonicalize(path)?.to_string_lossy().into_owned();
|
||||
Ok(if let Some(unc) = resolved.strip_prefix("\\\\?\\UNC\\") {
|
||||
format!("\\\\{unc}")
|
||||
} else {
|
||||
resolved
|
||||
.strip_prefix("\\\\?\\")
|
||||
.map(str::to_string)
|
||||
.unwrap_or(resolved)
|
||||
})
|
||||
}
|
||||
|
||||
/// libuv's `fs__rename`: `MoveFileExW` replacing an existing destination.
|
||||
pub fn rename(from: &str, to: &str) -> io::Result<()> {
|
||||
let (from, to) = (wide(from)?, wide(to)?);
|
||||
// SAFETY: both arguments are NUL-terminated wide strings that outlive the call.
|
||||
if unsafe { MoveFileExW(from.as_ptr(), to.as_ptr(), MOVEFILE_REPLACE_EXISTING) } == 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Node's `rm` on Windows: a symbolic link or junction to a directory is removed as a directory entry (libuv's
|
||||
/// `fs__unlink`), and a read-only file is retried after making it writable.
|
||||
pub fn remove_file(path: &str) -> io::Result<()> {
|
||||
use std::os::windows::fs::FileTypeExt;
|
||||
if std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink_dir()) {
|
||||
return std::fs::remove_dir(path);
|
||||
}
|
||||
match std::fs::remove_file(path) {
|
||||
Err(error) if error.kind() == io::ErrorKind::PermissionDenied => {
|
||||
let mut permissions = std::fs::metadata(path)?.permissions();
|
||||
#[allow(clippy::permissions_set_readonly_false)]
|
||||
permissions.set_readonly(false);
|
||||
std::fs::set_permissions(path, permissions)?;
|
||||
std::fs::remove_file(path)
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// A pipe a reader thread reads with blocking calls; `interrupt_reader` cancels a blocked read.
|
||||
pub trait Pipe: io::Read {}
|
||||
impl<T: io::Read> Pipe for T {}
|
||||
|
||||
/// Read `pipe` until it ends, `on_data` returns false, or `stop` is set; the pipe closes when this returns.
|
||||
pub fn read_pipe(mut pipe: impl Pipe, stop: &AtomicBool, mut on_data: impl FnMut(&[u8]) -> bool) {
|
||||
let mut buffer = vec![0u8; 64 * 1024];
|
||||
while !stop.load(Ordering::SeqCst) {
|
||||
match pipe.read(&mut buffer) {
|
||||
Ok(0) | Err(_) => return,
|
||||
Ok(read) => {
|
||||
if !on_data(&buffer[..read]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel a reader blocked in `ReadFile`, so it sees `stop` and closes its pipe.
|
||||
pub fn interrupt_reader(reader: &std::thread::JoinHandle<()>) {
|
||||
// SAFETY: the handle of a live or finished thread; cancelling no pending I/O is harmless.
|
||||
unsafe {
|
||||
CancelSynchronousIo(reader.as_raw_handle() as _);
|
||||
}
|
||||
}
|
||||
|
||||
/// libuv's `search_path` for an argv command: a name with a directory is tried as given, otherwise the working
|
||||
/// directory and then each `PATH` entry; a name without an extension also tries `.com` and `.exe`. Node refuses
|
||||
/// batch files without a shell.
|
||||
pub fn resolve_program(
|
||||
program: &str,
|
||||
cwd: &str,
|
||||
env: &Map<String, Value>,
|
||||
) -> Result<String, Failure> {
|
||||
let not_found = || Failure::new("spawn_error", format!("spawn {program} ENOENT"));
|
||||
let has_extension = Path::new(program).extension().is_some();
|
||||
let candidates = |base: &Path| -> Vec<std::path::PathBuf> {
|
||||
let mut names = Vec::new();
|
||||
if has_extension {
|
||||
names.push(base.to_path_buf());
|
||||
}
|
||||
for extension in ["com", "exe"] {
|
||||
let mut name = base.as_os_str().to_owned();
|
||||
name.push(".");
|
||||
name.push(extension);
|
||||
names.push(name.into());
|
||||
}
|
||||
names
|
||||
};
|
||||
let found = if program.contains(['\\', '/', ':']) {
|
||||
candidates(&Path::new(cwd).join(program))
|
||||
.into_iter()
|
||||
.find(|path| path.is_file())
|
||||
} else {
|
||||
let path_variable = env
|
||||
.iter()
|
||||
.find(|(key, _)| key.eq_ignore_ascii_case("PATH"))
|
||||
.and_then(|(_, value)| value.as_str().map(str::to_string))
|
||||
.or_else(|| std::env::var("PATH").ok())
|
||||
.unwrap_or_default();
|
||||
std::iter::once(std::path::PathBuf::from(cwd))
|
||||
.chain(std::env::split_paths(&path_variable))
|
||||
.flat_map(|directory| candidates(&directory.join(program)))
|
||||
.find(|path| path.is_file())
|
||||
};
|
||||
let found = found.ok_or_else(not_found)?;
|
||||
let lower = found.to_string_lossy().to_lowercase();
|
||||
if lower.ends_with(".bat") || lower.ends_with(".cmd") {
|
||||
return Err(Failure::new(
|
||||
"spawn_error",
|
||||
format!("spawn {program} EINVAL"),
|
||||
));
|
||||
}
|
||||
Ok(found.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
/// Node's `isLegacyWslBashPath`: `^[a-z]:\\windows\\(system32|sysnative)\\bash\.exe$`, case-insensitive.
|
||||
fn is_legacy_wsl_bash(path: &str) -> bool {
|
||||
let normalized = path.replace('/', "\\").to_lowercase();
|
||||
let mut characters = normalized.chars();
|
||||
characters
|
||||
.next()
|
||||
.is_some_and(|drive| drive.is_ascii_lowercase())
|
||||
&& matches!(
|
||||
characters.as_str(),
|
||||
":\\windows\\system32\\bash.exe" | ":\\windows\\sysnative\\bash.exe"
|
||||
)
|
||||
}
|
||||
|
||||
fn bash_config(program: &str) -> ShellConfig {
|
||||
if is_legacy_wsl_bash(program) {
|
||||
ShellConfig {
|
||||
program: program.into(),
|
||||
args: vec!["-s".into()],
|
||||
command_on_stdin: true,
|
||||
}
|
||||
} else {
|
||||
ShellConfig {
|
||||
program: program.into(),
|
||||
args: vec!["-c".into()],
|
||||
command_on_stdin: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn where_bash() -> Option<String> {
|
||||
let output = Command::new("where")
|
||||
.arg("bash.exe")
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.stdin(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.output()
|
||||
.ok()?;
|
||||
if !output.status.success() {
|
||||
return None;
|
||||
}
|
||||
let first = String::from_utf8_lossy(&output.stdout)
|
||||
.trim()
|
||||
.lines()
|
||||
.next()?
|
||||
.trim()
|
||||
.to_string();
|
||||
path_exists(&first).then_some(first)
|
||||
}
|
||||
|
||||
/// Node's shell resolution on Windows: a configured shell, Git Bash, `where bash.exe`, or `shell_unavailable`.
|
||||
pub fn shell_config(shell_path: Option<&str>) -> Result<ShellConfig, Failure> {
|
||||
if let Some(configured) = shell_path {
|
||||
if path_exists(configured) {
|
||||
return Ok(bash_config(configured));
|
||||
}
|
||||
return Err(Failure::new(
|
||||
"shell_unavailable",
|
||||
format!("Custom shell path not found: {configured}"),
|
||||
));
|
||||
}
|
||||
let mut candidates = Vec::new();
|
||||
for variable in ["ProgramFiles", "ProgramFiles(x86)"] {
|
||||
if let Ok(directory) = std::env::var(variable) {
|
||||
candidates.push(format!("{directory}\\Git\\bin\\bash.exe"));
|
||||
}
|
||||
}
|
||||
if let Some(found) = candidates.iter().find(|candidate| path_exists(candidate)) {
|
||||
return Ok(bash_config(found));
|
||||
}
|
||||
if let Some(found) = where_bash() {
|
||||
return Ok(bash_config(&found));
|
||||
}
|
||||
let searched: String = candidates
|
||||
.iter()
|
||||
.map(|candidate| format!(" {candidate}\n"))
|
||||
.collect();
|
||||
Err(Failure::new(
|
||||
"shell_unavailable",
|
||||
format!(
|
||||
"No bash shell found. Options:\n 1. Install Git for Windows: https://git-scm.com/download/win\n 2. Add your bash to PATH (Cygwin, MSYS2, etc.)\n 3. Configure an explicit shellPath\n\nSearched Git Bash in:\n{}",
|
||||
searched.trim_end()
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
/// libuv's `quote_cmd_arg`: how one argument is written into the command line the child splits again.
|
||||
pub fn quote_argument(argument: &str) -> String {
|
||||
if argument.is_empty() {
|
||||
return "\"\"".into();
|
||||
}
|
||||
if !argument.contains([' ', '\t', '"']) {
|
||||
return argument.into();
|
||||
}
|
||||
if !argument.contains(['"', '\\']) {
|
||||
return format!("\"{argument}\"");
|
||||
}
|
||||
// Walk backwards: backslashes before a quote (or the closing quote) are doubled, and quotes are escaped.
|
||||
let mut reversed = Vec::new();
|
||||
let mut quote_hit = true;
|
||||
for character in argument.chars().rev() {
|
||||
reversed.push(character);
|
||||
if quote_hit && character == '\\' {
|
||||
reversed.push('\\');
|
||||
} else if character == '"' {
|
||||
quote_hit = true;
|
||||
reversed.push('\\');
|
||||
} else {
|
||||
quote_hit = false;
|
||||
}
|
||||
}
|
||||
let mut quoted = String::from("\"");
|
||||
quoted.extend(reversed.into_iter().rev());
|
||||
quoted.push('"');
|
||||
quoted
|
||||
}
|
||||
|
||||
/// Variables libuv copies from the parent into every child environment that lacks them.
|
||||
const REQUIRED_VARIABLES: [&str; 11] = [
|
||||
"HOMEDRIVE",
|
||||
"HOMEPATH",
|
||||
"LOGONSERVER",
|
||||
"PATH",
|
||||
"SYSTEMDRIVE",
|
||||
"SYSTEMROOT",
|
||||
"TEMP",
|
||||
"USERDOMAIN",
|
||||
"USERNAME",
|
||||
"USERPROFILE",
|
||||
"WINDIR",
|
||||
];
|
||||
|
||||
/// The child environment as Node builds it on Windows: the variables in object order, sorted by UTF-16 code units,
|
||||
/// keeping the first of names that differ only in case; then libuv adds required variables the result lacks.
|
||||
fn child_environment(env: &Map<String, Value>, inherit_env: bool) -> Vec<(String, String)> {
|
||||
let mut entries: Vec<(String, String)> = Vec::new();
|
||||
let mut set =
|
||||
|key: String, value: String| match entries.iter_mut().find(|(name, _)| *name == key) {
|
||||
Some(entry) => entry.1 = value,
|
||||
None => entries.push((key, value)),
|
||||
};
|
||||
if inherit_env {
|
||||
for (key, value) in std::env::vars_os() {
|
||||
set(
|
||||
key.to_string_lossy().into_owned(),
|
||||
value.to_string_lossy().into_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
for (key, value) in env {
|
||||
if let Some(value) = value.as_str() {
|
||||
set(key.clone(), value.to_string());
|
||||
}
|
||||
}
|
||||
entries.sort_by(|(a, _), (b, _)| a.encode_utf16().cmp(b.encode_utf16()));
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
entries.retain(|(name, _)| seen.insert(name.to_uppercase()));
|
||||
for required in REQUIRED_VARIABLES {
|
||||
if !entries
|
||||
.iter()
|
||||
.any(|(name, _)| name.eq_ignore_ascii_case(required))
|
||||
&& let Ok(value) = std::env::var(required)
|
||||
{
|
||||
entries.push((required.to_string(), value));
|
||||
}
|
||||
}
|
||||
entries
|
||||
}
|
||||
|
||||
/// Arguments quoted as libuv quotes them, Node's environment, and a hidden console.
|
||||
pub fn configure(
|
||||
command: &mut Command,
|
||||
args: &[String],
|
||||
env: &Map<String, Value>,
|
||||
inherit_env: bool,
|
||||
) {
|
||||
for argument in args {
|
||||
command.raw_arg(quote_argument(argument));
|
||||
}
|
||||
command.env_clear();
|
||||
for (key, value) in child_environment(env, inherit_env) {
|
||||
command.env(key, value);
|
||||
}
|
||||
command.creation_flags(CREATE_NO_WINDOW);
|
||||
}
|
||||
|
||||
/// libuv puts every child it starts into one job that ends its processes when the daemon exits.
|
||||
fn job() -> Option<isize> {
|
||||
static JOB: OnceLock<Option<isize>> = OnceLock::new();
|
||||
*JOB.get_or_init(|| {
|
||||
// SAFETY: plain Win32 calls with a zeroed, correctly sized information struct.
|
||||
unsafe {
|
||||
let job = CreateJobObjectW(std::ptr::null(), std::ptr::null());
|
||||
if job.is_null() {
|
||||
return None;
|
||||
}
|
||||
let mut information: JOBOBJECT_EXTENDED_LIMIT_INFORMATION = std::mem::zeroed();
|
||||
information.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_BREAKAWAY_OK
|
||||
| JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK
|
||||
| JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION
|
||||
| JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
SetInformationJobObject(
|
||||
job,
|
||||
JobObjectExtendedLimitInformation,
|
||||
(&information as *const JOBOBJECT_EXTENDED_LIMIT_INFORMATION).cast(),
|
||||
std::mem::size_of::<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>() as u32,
|
||||
);
|
||||
Some(job as isize)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn after_spawn(child: &Child) {
|
||||
if let Some(job) = job() {
|
||||
// SAFETY: both handles are valid for the duration of the call.
|
||||
unsafe {
|
||||
AssignProcessToJobObject(job as _, child.as_raw_handle() as _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Node's `killProcessTree` on Windows: `taskkill /F /T`, not waited for.
|
||||
pub fn kill_tree(pid: u32) {
|
||||
let system_root = std::env::var("SystemRoot").unwrap_or_else(|_| "C:\\Windows".into());
|
||||
let _ = Command::new(format!("{system_root}\\System32\\taskkill.exe"))
|
||||
.args(["/F", "/T", "/PID", &pid.to_string()])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn();
|
||||
}
|
||||
|
||||
/// Exit codes are unsigned 32-bit values on Windows (an access violation is 3221225477).
|
||||
pub fn exit_code(status: ExitStatus) -> i64 {
|
||||
i64::from(status.code().unwrap_or(1) as u32)
|
||||
}
|
||||
|
||||
/// Windows listings keep the file system's order; nothing sorts by raw names.
|
||||
pub fn raw_name(_name: &OsStr) -> Option<Vec<u8>> {
|
||||
None
|
||||
}
|
||||
|
||||
/// The per-drive working directories of the `=C:` variables, which Node's `path.resolve` uses for drive-relative paths.
|
||||
pub fn drive_cwds() -> Map<String, Value> {
|
||||
std::env::vars_os()
|
||||
.filter_map(|(key, value)| {
|
||||
let key = key.to_str()?;
|
||||
let bytes = key.as_bytes();
|
||||
(bytes.len() == 3
|
||||
&& bytes[0] == b'='
|
||||
&& bytes[1].is_ascii_alphabetic()
|
||||
&& bytes[2] == b':')
|
||||
.then(|| {
|
||||
(
|
||||
key[1..].to_ascii_uppercase(),
|
||||
Value::String(value.to_string_lossy().into_owned()),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// libuv's `scandir` and `opendir` report a file as `ENOTDIR` (elsewhere `ERROR_DIRECTORY` is `ENOENT`).
|
||||
pub fn read_dir(path: &str) -> io::Result<std::fs::ReadDir> {
|
||||
std::fs::read_dir(path).map_err(|error| {
|
||||
if error.raw_os_error() == Some(267) {
|
||||
synthetic("ENOTDIR")
|
||||
} else {
|
||||
error
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Volume serial number and file index of `path`, as libuv's `stat` reports `dev` and `ino`: the creation time is not
|
||||
/// enough, because NTFS gives a file created under a recently removed name the old one's creation time ("tunneling").
|
||||
/// Opens a handle, like libuv; falls back to the metadata when the path cannot be opened.
|
||||
pub fn path_identity(path: &str, metadata: &Metadata, follow: bool) -> (u64, u64) {
|
||||
let Ok(name) = wide(path) else {
|
||||
return identity(metadata);
|
||||
};
|
||||
let flags = FILE_FLAG_BACKUP_SEMANTICS
|
||||
| if follow {
|
||||
0
|
||||
} else {
|
||||
FILE_FLAG_OPEN_REPARSE_POINT
|
||||
};
|
||||
// SAFETY: a NUL-terminated wide path; the handle is closed below.
|
||||
let handle = unsafe {
|
||||
CreateFileW(
|
||||
name.as_ptr(),
|
||||
0,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
std::ptr::null(),
|
||||
OPEN_EXISTING,
|
||||
flags,
|
||||
std::ptr::null_mut(),
|
||||
)
|
||||
};
|
||||
if handle == INVALID_HANDLE_VALUE {
|
||||
return identity(metadata);
|
||||
}
|
||||
// SAFETY: a valid handle and a zeroed buffer for the call to fill.
|
||||
let mut information: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
|
||||
let found = unsafe { GetFileInformationByHandle(handle, &mut information) } != 0;
|
||||
// SAFETY: the handle opened above.
|
||||
unsafe { CloseHandle(handle) };
|
||||
if !found {
|
||||
return identity(metadata);
|
||||
}
|
||||
(
|
||||
u64::from(information.dwVolumeSerialNumber),
|
||||
(u64::from(information.nFileIndexHigh) << 32) | u64::from(information.nFileIndexLow),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::quote_argument;
|
||||
|
||||
#[test]
|
||||
fn quotes_arguments_like_libuv() {
|
||||
assert_eq!(quote_argument(""), "\"\"");
|
||||
assert_eq!(quote_argument("plain"), "plain");
|
||||
assert_eq!(quote_argument("a b"), "\"a b\"");
|
||||
assert_eq!(quote_argument("*.txt"), "*.txt");
|
||||
assert_eq!(quote_argument("say \"hi\""), "\"say \\\"hi\\\"\"");
|
||||
assert_eq!(
|
||||
quote_argument("C:\\dir with space\\"),
|
||||
"\"C:\\dir with space\\\\\""
|
||||
);
|
||||
assert_eq!(quote_argument("a\\\\b c"), "\"a\\\\b c\"");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user