use std::path::Path;
use std::process::ExitStatus;
#[cfg(unix)]
pub fn short_ipc_dir() -> std::path::PathBuf {
std::path::PathBuf::from("/tmp")
}
#[cfg(windows)]
pub fn short_ipc_dir() -> std::path::PathBuf {
std::env::temp_dir()
}
pub fn slash_path(path: &Path) -> String {
path.components()
.map(|component| match component {
std::path::Component::RootDir => String::new(),
other => other.as_os_str().to_string_lossy().into_owned(),
})
.collect::<Vec<_>>()
.join("/")
}
pub fn canonicalize(path: impl AsRef<Path>) -> std::io::Result<std::path::PathBuf> {
std::fs::canonicalize(path).map(|resolved| without_verbatim_prefix(&resolved))
}
pub fn without_verbatim_prefix(path: &Path) -> std::path::PathBuf {
let shown = path.to_string_lossy();
match shown.strip_prefix(r"\\?\UNC\") {
Some(rest) => std::path::PathBuf::from(format!(r"\\{rest}")),
None => match shown.strip_prefix(r"\\?\") {
Some(rest) => std::path::PathBuf::from(rest),
None => path.to_path_buf(),
},
}
}
#[cfg(unix)]
pub fn is_executable(path: &Path) -> bool {
use std::os::unix::fs::PermissionsExt;
std::fs::metadata(path)
.map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0)
.unwrap_or(false)
}
#[cfg(windows)]
pub fn is_executable(path: &Path) -> bool {
if !std::fs::metadata(path).map(|m| m.is_file()).unwrap_or(false) {
return false;
}
let Some(extension) = path.extension().and_then(|e| e.to_str()) else {
return false;
};
let extension = extension.to_ascii_lowercase();
["exe", "com", "bat", "cmd", "ps1"].contains(&extension.as_str())
}
#[cfg(target_os = "macos")]
pub fn exclude_from_backups(path: &Path) -> Result<(), String> {
if excluded_from_backups(path)? {
return Ok(());
}
let marked = crate::git_backend::command_without_repo_scope("tmutil")
.arg("addexclusion")
.arg(path)
.output()
.map_err(|e| format!("running tmutil addexclusion {}: {e}", path.display()))?;
if marked.status.success() {
Ok(())
} else {
Err(format!(
"tmutil could not exclude {} from backups: {}",
path.display(),
String::from_utf8_lossy(&marked.stderr).trim()
))
}
}
#[cfg(not(target_os = "macos"))]
pub fn exclude_from_backups(_path: &Path) -> Result<(), String> {
Ok(())
}
#[cfg(target_os = "macos")]
pub fn excluded_from_backups(path: &Path) -> Result<bool, String> {
let asked = crate::git_backend::command_without_repo_scope("tmutil")
.arg("isexcluded")
.arg(path)
.output()
.map_err(|e| format!("asking tmutil about {}: {e}", path.display()))?;
Ok(String::from_utf8_lossy(&asked.stdout).contains("[Excluded]"))
}
#[cfg(not(target_os = "macos"))]
pub fn excluded_from_backups(_path: &Path) -> Result<bool, String> {
Ok(true)
}
#[cfg(unix)]
pub fn make_executable(path: &Path) -> std::io::Result<()> {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755))
}
#[cfg(windows)]
pub fn make_executable(path: &Path) -> std::io::Result<()> {
std::fs::metadata(path).map(|_| ())
}
#[cfg(unix)]
pub fn command_file_names(name: &str) -> Vec<String> {
vec![name.to_string()]
}
#[cfg(windows)]
pub fn command_file_names(name: &str) -> Vec<String> {
if std::path::Path::new(name).extension().is_some() {
return vec![name.to_string()];
}
["exe", "cmd", "bat", "com", "ps1"]
.iter()
.map(|extension| format!("{name}.{extension}"))
.collect()
}
#[cfg(unix)]
pub fn restrict_to_owner(path: &Path) -> std::io::Result<()> {
use std::os::unix::fs::PermissionsExt;
let mode = if path.is_dir() { 0o700 } else { 0o600 };
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
}
#[cfg(windows)]
pub fn restrict_to_owner(path: &Path) -> std::io::Result<()> {
let account = std::env::var("USERNAME").map_err(|_| {
std::io::Error::other("USERNAME is not set, so there is no account to grant")
})?;
let grant = if path.is_dir() {
format!("{account}:(OI)(CI)(F)")
} else {
format!("{account}:(F)")
};
let output = crate::output::Command::new("icacls")
.arg(path)
.arg("/inheritance:r")
.arg("/grant:r")
.arg(grant)
.output()?;
if !output.status.success() {
return Err(std::io::Error::other(format!(
"restricting {} to its owner failed: {}",
path.display(),
String::from_utf8_lossy(&output.stderr).trim()
)));
}
Ok(())
}
#[cfg(unix)]
pub fn set_mode(path: &Path, mode: u32) -> std::io::Result<()> {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(mode))
}
#[cfg(windows)]
pub fn set_mode(path: &Path, mode: u32) -> std::io::Result<()> {
std::fs::metadata(path)?;
if mode & 0o077 == 0 {
return restrict_to_owner(path);
}
Ok(())
}
#[cfg(unix)]
pub fn link_to(target: &Path, link: &Path) -> std::io::Result<()> {
std::os::unix::fs::symlink(target, link)
}
#[cfg(windows)]
pub fn link_to(target: &Path, link: &Path) -> std::io::Result<()> {
std::fs::hard_link(target, link)
}
#[cfg(unix)]
pub fn symlink_file(target: &Path, link: &Path) -> std::io::Result<()> {
std::os::unix::fs::symlink(target, link)
}
#[cfg(windows)]
pub fn symlink_file(target: &Path, link: &Path) -> std::io::Result<()> {
std::os::windows::fs::symlink_file(target, link)
}
#[cfg(unix)]
pub fn is_a_socket(path: &Path) -> bool {
use std::os::unix::fs::FileTypeExt;
std::fs::metadata(path)
.map(|m| m.file_type().is_socket())
.unwrap_or(false)
}
#[cfg(windows)]
pub fn is_a_socket(_path: &Path) -> bool {
false
}
#[cfg(unix)]
pub fn processes() -> Result<Vec<(i32, String)>, String> {
let output = crate::git_backend::command_without_repo_scope("ps")
.args(["-axww", "-o", "pid=,command="])
.output()
.map_err(|e| format!("cannot enumerate processes via ps: {e}"))?;
if !output.status.success() {
return Err("ps failed to enumerate processes".to_string());
}
Ok(parse_table(&String::from_utf8_lossy(&output.stdout)))
}
#[cfg(windows)]
pub fn processes() -> Result<Vec<(i32, String)>, String> {
let output = crate::git_backend::command_without_repo_scope("powershell")
.args([
"-NoProfile",
"-NonInteractive",
"-Command",
"Get-CimInstance Win32_Process | ForEach-Object { \
\"$($_.ProcessId) $($_.CommandLine)\" }",
])
.output()
.map_err(|e| format!("cannot enumerate processes: {e}"))?;
if !output.status.success() {
return Err(format!(
"enumerating processes failed: {}",
String::from_utf8_lossy(&output.stderr).trim()
));
}
Ok(parse_table(&String::from_utf8_lossy(&output.stdout)))
}
fn parse_table(table: &str) -> Vec<(i32, String)> {
let mut out = Vec::new();
for line in table.lines() {
let line = line.trim_start();
let Some((pid, argv)) = line.split_once(' ') else {
continue;
};
let Ok(pid) = pid.trim().parse::<i32>() else {
continue;
};
let argv = argv.trim();
if argv.is_empty() {
continue;
}
out.push((pid, argv.to_string()));
}
out
}
#[cfg(unix)]
pub fn parent_process_id() -> Option<i32> {
Some(unsafe { libc::getppid() })
}
#[cfg(windows)]
pub fn parent_process_id() -> Option<i32> {
let output = crate::git_backend::command_without_repo_scope("powershell")
.args([
"-NoProfile",
"-NonInteractive",
"-Command",
&format!(
"(Get-CimInstance Win32_Process -Filter \"ProcessId={}\").ParentProcessId",
std::process::id()
),
])
.output()
.ok()?;
String::from_utf8_lossy(&output.stdout).trim().parse().ok()
}
#[cfg(unix)]
pub fn end_politely(pid: i32) {
unsafe {
libc::kill(pid, libc::SIGTERM);
}
}
#[cfg(windows)]
pub fn end_politely(pid: i32) {
#[link(name = "kernel32")]
extern "system" {
fn GenerateConsoleCtrlEvent(dwCtrlEvent: u32, dwProcessGroupId: u32) -> i32;
}
const CTRL_BREAK_EVENT: u32 = 1;
unsafe {
GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT, pid as u32);
}
let _ = crate::git_backend::command_without_repo_scope("taskkill")
.args(["/PID", &pid.to_string()])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
}
#[cfg(unix)]
pub fn end_hard(pid: i32) {
unsafe {
libc::kill(pid, libc::SIGKILL);
}
}
#[cfg(windows)]
pub fn end_hard(pid: i32) {
let _ = crate::git_backend::command_without_repo_scope("taskkill")
.args(["/F", "/PID", &pid.to_string()])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
}
#[cfg(unix)]
pub fn end_group(pid: u32, polite: bool) {
unsafe {
libc::kill(
-(pid as i32),
if polite { libc::SIGTERM } else { libc::SIGKILL },
);
}
}
#[cfg(windows)]
pub fn end_group(pid: u32, polite: bool) {
let mut command = crate::git_backend::command_without_repo_scope("taskkill");
if !polite {
command.arg("/F");
}
let _ = command
.args(["/T", "/PID", &pid.to_string()])
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status();
}
#[cfg(unix)]
pub fn withhold_standard_handles() -> std::io::Result<()> {
Ok(())
}
#[cfg(windows)]
pub fn withhold_standard_handles() -> std::io::Result<()> {
use std::ffi::c_void;
const STD_INPUT_HANDLE: u32 = -10i32 as u32;
const STD_OUTPUT_HANDLE: u32 = -11i32 as u32;
const STD_ERROR_HANDLE: u32 = -12i32 as u32;
const HANDLE_FLAG_INHERIT: u32 = 0x0000_0001;
const INVALID_HANDLE_VALUE: isize = -1;
#[link(name = "kernel32")]
extern "system" {
fn GetStdHandle(which: u32) -> *mut c_void;
fn SetHandleInformation(handle: *mut c_void, mask: u32, flags: u32) -> i32;
}
for which in [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
unsafe {
let handle = GetStdHandle(which);
if handle.is_null() || handle as isize == INVALID_HANDLE_VALUE {
continue;
}
if SetHandleInformation(handle, HANDLE_FLAG_INHERIT, 0) == 0 {
return Err(std::io::Error::last_os_error());
}
}
}
Ok(())
}
#[cfg(unix)]
pub fn alive(pid: i32) -> bool {
unsafe { libc::kill(pid, 0) == 0 }
}
#[cfg(windows)]
pub fn alive(pid: i32) -> bool {
crate::git_backend::command_without_repo_scope("tasklist")
.args(["/FI", &format!("PID eq {pid}"), "/NH", "/FO", "CSV"])
.output()
.map(|out| String::from_utf8_lossy(&out.stdout).contains(&format!("\"{pid}\"")))
.unwrap_or(false)
}
#[cfg(unix)]
pub fn stop_requested() -> std::sync::Arc<std::sync::atomic::AtomicBool> {
use std::sync::OnceLock;
static FLAG: OnceLock<std::sync::Arc<std::sync::atomic::AtomicBool>> = OnceLock::new();
extern "C" fn on_signal(_: i32) {
if let Some(flag) = FLAG.get() {
flag.store(true, std::sync::atomic::Ordering::SeqCst);
}
}
let mut installed_now = false;
let flag = FLAG.get_or_init(|| {
installed_now = true;
std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false))
});
if installed_now {
unsafe {
libc::signal(libc::SIGINT, on_signal as *const () as libc::sighandler_t);
libc::signal(libc::SIGTERM, on_signal as *const () as libc::sighandler_t);
}
}
std::sync::Arc::clone(flag)
}
#[cfg(windows)]
pub fn stop_requested() -> std::sync::Arc<std::sync::atomic::AtomicBool> {
use std::sync::OnceLock;
static FLAG: OnceLock<std::sync::Arc<std::sync::atomic::AtomicBool>> = OnceLock::new();
let mut installed_now = false;
let flag = FLAG.get_or_init(|| {
installed_now = true;
std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false))
});
if !installed_now {
return std::sync::Arc::clone(flag);
}
unsafe extern "system" fn on_control(_event: u32) -> i32 {
if let Some(flag) = FLAG.get() {
flag.store(true, std::sync::atomic::Ordering::SeqCst);
}
1
}
unsafe extern "system" {
fn SetConsoleCtrlHandler(
handler: Option<unsafe extern "system" fn(u32) -> i32>,
add: i32,
) -> i32;
}
unsafe {
SetConsoleCtrlHandler(Some(on_control), 1);
}
std::sync::Arc::clone(flag)
}
#[cfg(unix)]
pub fn killed_by_signal(status: &ExitStatus) -> Option<i32> {
use std::os::unix::process::ExitStatusExt;
status.signal()
}
#[cfg(windows)]
pub fn killed_by_signal(_status: &ExitStatus) -> Option<i32> {
None
}
#[cfg(unix)]
pub fn account() -> String {
unsafe { libc::getuid() }.to_string()
}
#[cfg(windows)]
pub fn account() -> String {
std::env::var("USERNAME").unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test11435_the_process_table_parses_the_same_either_way() {
let table = " 1234 /usr/bin/cargo test --release\n\
5678 C:\\Program Files\\thing.exe --flag\n\
\n\
PID COMMAND\n\
91011 \n";
let parsed = parse_table(table);
assert_eq!(
parsed,
vec![
(1234, "/usr/bin/cargo test --release".to_string()),
(5678, "C:\\Program Files\\thing.exe --flag".to_string()),
],
"a header line and a process with no command line are not processes"
);
}
#[test]
fn test11436_only_a_runnable_file_is_executable() {
let dir = tempfile::tempdir().expect("a temporary directory");
assert!(
!is_executable(dir.path()),
"a directory carries the execute bit and is not a program"
);
let plain = dir.path().join("notes.txt");
std::fs::write(&plain, b"text").expect("the file");
assert!(!is_executable(&plain), "a text file is not a program");
let program = dir.path().join(if cfg!(windows) { "tool.exe" } else { "tool" });
std::fs::write(&program, b"#!/bin/sh\n").expect("the file");
#[cfg(unix)]
assert!(
!is_executable(&program),
"on unix the bit is what decides, and it is not set yet"
);
make_executable(&program).expect("making it executable");
assert!(is_executable(&program), "it is a program now");
assert!(
!is_executable(&dir.path().join("absent")),
"a path with nothing at it is not a program"
);
}
#[test]
fn test12163_a_resolved_path_carries_no_verbatim_prefix() {
use std::path::{Path, PathBuf};
assert_eq!(
without_verbatim_prefix(Path::new(r"\\?\C:\mg-test\workspace")),
PathBuf::from(r"C:\mg-test\workspace")
);
assert_eq!(
without_verbatim_prefix(Path::new(r"\\?\UNC\server\share\ws")),
PathBuf::from(r"\\server\share\ws"),
"a UNC share keeps its double backslash"
);
assert_eq!(
without_verbatim_prefix(Path::new("/Users/me/ws")),
PathBuf::from("/Users/me/ws"),
"any other path is unchanged"
);
let dir = tempfile::tempdir().expect("a temporary directory");
let resolved = canonicalize(dir.path()).expect("resolving it");
assert!(!resolved.to_string_lossy().starts_with(r"\\?\"), "{}", resolved.display());
assert!(resolved.is_absolute());
}
#[test]
fn test12912_no_path_is_resolved_around_this_module() {
fn walk(dir: &Path, out: &mut Vec<std::path::PathBuf>) {
let entries =
std::fs::read_dir(dir).unwrap_or_else(|e| panic!("cannot read {}: {e}", dir.display()));
for entry in entries {
let path = entry.expect("a directory entry").path();
if path.is_dir() {
walk(&path, out);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
let crate_root = Path::new(env!("CARGO_MANIFEST_DIR"));
let mut files = Vec::new();
for tree in ["src", "tests", "daemon/src"] {
walk(&crate_root.join(tree), &mut files);
}
assert!(files.len() > 100, "the sources should have been walked, found {}", files.len());
let this = crate_root.join("src").join("platform.rs");
let mut offences = Vec::new();
for file in files.iter().filter(|f| **f != this) {
let text = std::fs::read_to_string(file).expect("a source file must be readable");
for (number, line) in text.lines().enumerate() {
let code = line.split("//").next().unwrap_or_default();
let bypasses = code.contains(".canonicalize()")
|| code.match_indices("fs::canonicalize(").any(|(at, _)| !code[..at].ends_with("platform::"));
if bypasses {
offences.push(format!("{}:{}: {}", file.display(), number + 1, line.trim()));
}
}
}
assert!(
offences.is_empty(),
"resolve paths with crate::platform::canonicalize (wsb::platform::canonicalize outside \
the crate), which never answers in the verbatim form:\n{}",
offences.join("\n")
);
}
}