use std::fs;
use std::path::Path;
pub fn unlock_exe() {
if !cfg!(target_os = "windows") {
return;
}
if std::env::var_os("ZCCACHE_NO_UNLOCK").is_some() {
return;
}
let my_exe = match std::env::current_exe() {
Ok(p) => p,
Err(_) => return,
};
if exe_is_under_runtime_binaries(&my_exe) {
return;
}
let rand_id: u32 = std::process::id()
^ (std::time::UNIX_EPOCH
.elapsed()
.unwrap_or_default()
.subsec_nanos());
let old_exe = my_exe.with_extension(format!("exe.old.{rand_id}"));
if fs::rename(&my_exe, &old_exe).is_err() {
tracing::warn!(
"could not unlock exe for hot-reload; pip install may fail while zccache is running"
);
return;
}
let _ = fs::copy(&old_exe, &my_exe);
let parent = match my_exe.parent() {
Some(p) => p.to_path_buf(),
None => return,
};
let stem = match my_exe.file_name().and_then(|n| n.to_str()) {
Some(s) => s.to_string(),
None => return,
};
std::thread::spawn(move || gc_old_files(&parent, &stem));
}
pub fn release_cwd() {
if let Some(stable) = zccache_home_dir() {
let _ = std::fs::create_dir_all(&stable);
if std::env::set_current_dir(&stable).is_ok() {
return;
}
}
let _ = std::env::set_current_dir(std::env::temp_dir());
}
fn zccache_home_dir() -> Option<std::path::PathBuf> {
let home = std::env::var_os("HOME").or_else(|| std::env::var_os("USERPROFILE"))?;
if home.is_empty() {
return None;
}
Some(std::path::Path::new(&home).join(".zccache"))
}
pub fn detach_stdio() {
#[cfg(unix)]
detach_stdio_unix();
#[cfg(windows)]
detach_stdio_windows();
}
pub fn redirect_stdio_to_log(log_path: &Path) {
#[cfg(unix)]
if !redirect_stdio_to_log_unix(log_path) {
detach_stdio_unix();
}
#[cfg(windows)]
if !redirect_stdio_to_log_windows(log_path) {
detach_stdio_windows();
}
}
#[cfg(unix)]
fn redirect_stdio_to_log_unix(log_path: &Path) -> bool {
use std::ffi::CString;
use std::os::unix::ffi::OsStrExt;
let path_c = match CString::new(log_path.as_os_str().as_bytes()) {
Ok(c) => c,
Err(_) => return false,
};
unsafe {
let null = libc::open(c"/dev/null".as_ptr(), libc::O_RDONLY);
if null < 0 {
return false;
}
let _ = libc::dup2(null, libc::STDIN_FILENO);
if null > libc::STDERR_FILENO {
let _ = libc::close(null);
}
let log_fd = libc::open(
path_c.as_ptr(),
libc::O_WRONLY | libc::O_CREAT | libc::O_APPEND,
0o644,
);
if log_fd < 0 {
return false;
}
let _ = libc::dup2(log_fd, libc::STDOUT_FILENO);
let _ = libc::dup2(log_fd, libc::STDERR_FILENO);
if log_fd > libc::STDERR_FILENO {
let _ = libc::close(log_fd);
}
}
true
}
#[cfg(windows)]
fn redirect_stdio_to_log_windows(log_path: &Path) -> bool {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use std::ptr;
extern "system" {
fn CreateFileW(
lp_file_name: *const u16,
dw_desired_access: u32,
dw_share_mode: u32,
lp_security_attributes: *mut std::ffi::c_void,
dw_creation_disposition: u32,
dw_flags_and_attributes: u32,
h_template_file: *mut std::ffi::c_void,
) -> *mut std::ffi::c_void;
fn GetStdHandle(n_std_handle: u32) -> *mut std::ffi::c_void;
fn SetStdHandle(n_std_handle: u32, h_handle: *mut std::ffi::c_void) -> i32;
fn CloseHandle(h_object: *mut std::ffi::c_void) -> i32;
fn SetFilePointerEx(
h_file: *mut std::ffi::c_void,
li_distance_to_move: i64,
lp_new_file_pointer: *mut i64,
dw_move_method: u32,
) -> i32;
}
const GENERIC_READ: u32 = 0x8000_0000;
const GENERIC_WRITE: u32 = 0x4000_0000;
const FILE_SHARE_READ: u32 = 0x0000_0001;
const FILE_SHARE_WRITE: u32 = 0x0000_0002;
const OPEN_EXISTING: u32 = 3;
const OPEN_ALWAYS: u32 = 4;
const FILE_END: u32 = 2;
const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6;
const STD_OUTPUT_HANDLE: u32 = 0xFFFF_FFF5;
const STD_ERROR_HANDLE: u32 = 0xFFFF_FFF4;
const INVALID_HANDLE_VALUE: *mut std::ffi::c_void = -1isize as *mut std::ffi::c_void;
let path_w: Vec<u16> = log_path.as_os_str().encode_wide().chain(Some(0)).collect();
let nul_w: Vec<u16> = OsStr::new("NUL").encode_wide().chain(Some(0)).collect();
unsafe {
let nul_in = CreateFileW(
nul_w.as_ptr(),
GENERIC_READ,
FILE_SHARE_READ | FILE_SHARE_WRITE,
ptr::null_mut(),
OPEN_EXISTING,
0,
ptr::null_mut(),
);
if !nul_in.is_null() && nul_in != INVALID_HANDLE_VALUE {
let old = GetStdHandle(STD_INPUT_HANDLE);
let _ = SetStdHandle(STD_INPUT_HANDLE, nul_in);
if !old.is_null() && old != INVALID_HANDLE_VALUE {
let _ = CloseHandle(old);
}
}
let log_handle = CreateFileW(
path_w.as_ptr(),
GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
ptr::null_mut(),
OPEN_ALWAYS,
0,
ptr::null_mut(),
);
if log_handle.is_null() || log_handle == INVALID_HANDLE_VALUE {
return false;
}
let _ = SetFilePointerEx(log_handle, 0, ptr::null_mut(), FILE_END);
for slot in [STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
let old = GetStdHandle(slot);
let _ = SetStdHandle(slot, log_handle);
if !old.is_null() && old != INVALID_HANDLE_VALUE {
let _ = CloseHandle(old);
}
}
}
true
}
#[cfg(unix)]
fn detach_stdio_unix() {
unsafe {
let null = libc::open(c"/dev/null".as_ptr(), libc::O_RDWR);
if null < 0 {
return;
}
let _ = libc::dup2(null, libc::STDIN_FILENO);
let _ = libc::dup2(null, libc::STDOUT_FILENO);
let _ = libc::dup2(null, libc::STDERR_FILENO);
if null > libc::STDERR_FILENO {
let _ = libc::close(null);
}
}
}
#[cfg(windows)]
fn detach_stdio_windows() {
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
use std::ptr;
extern "system" {
fn CreateFileW(
lp_file_name: *const u16,
dw_desired_access: u32,
dw_share_mode: u32,
lp_security_attributes: *mut std::ffi::c_void,
dw_creation_disposition: u32,
dw_flags_and_attributes: u32,
h_template_file: *mut std::ffi::c_void,
) -> *mut std::ffi::c_void;
fn GetStdHandle(n_std_handle: u32) -> *mut std::ffi::c_void;
fn SetStdHandle(n_std_handle: u32, h_handle: *mut std::ffi::c_void) -> i32;
fn CloseHandle(h_object: *mut std::ffi::c_void) -> i32;
}
const GENERIC_READ: u32 = 0x8000_0000;
const GENERIC_WRITE: u32 = 0x4000_0000;
const FILE_SHARE_READ: u32 = 0x0000_0001;
const FILE_SHARE_WRITE: u32 = 0x0000_0002;
const OPEN_EXISTING: u32 = 3;
const STD_INPUT_HANDLE: u32 = 0xFFFF_FFF6; const STD_OUTPUT_HANDLE: u32 = 0xFFFF_FFF5; const STD_ERROR_HANDLE: u32 = 0xFFFF_FFF4; const INVALID_HANDLE_VALUE: *mut std::ffi::c_void = -1isize as *mut std::ffi::c_void;
let nul: Vec<u16> = OsStr::new("NUL").encode_wide().chain(Some(0)).collect();
for (slot, access) in [
(STD_INPUT_HANDLE, GENERIC_READ),
(STD_OUTPUT_HANDLE, GENERIC_WRITE),
(STD_ERROR_HANDLE, GENERIC_WRITE),
] {
unsafe {
let nul_handle = CreateFileW(
nul.as_ptr(),
access,
FILE_SHARE_READ | FILE_SHARE_WRITE,
ptr::null_mut(),
OPEN_EXISTING,
0,
ptr::null_mut(),
);
if nul_handle.is_null() || nul_handle == INVALID_HANDLE_VALUE {
continue;
}
let old = GetStdHandle(slot);
let _ = SetStdHandle(slot, nul_handle);
if !old.is_null() && old != INVALID_HANDLE_VALUE {
let _ = CloseHandle(old);
}
}
}
}
fn exe_is_under_runtime_binaries(exe: &Path) -> bool {
let runtime_dir = crate::core::config::default_cache_dir().join("runtime-binaries");
let runtime_canon = match fs::canonicalize(&runtime_dir) {
Ok(p) => p,
Err(_) => return false,
};
let exe_parent = match exe.parent() {
Some(p) => p,
None => return false,
};
let exe_parent_canon = match fs::canonicalize(exe_parent) {
Ok(p) => p,
Err(_) => return false,
};
exe_parent_canon == runtime_canon
}
fn gc_old_files(dir: &Path, stem: &str) {
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with(stem) && name_str.contains(".old") {
let _ = fs::remove_file(entry.path());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_gc_old_files() {
let tmp = std::env::temp_dir().join("zccache-unlock-test");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
fs::write(tmp.join("stem.exe"), b"current").unwrap();
fs::write(tmp.join("stem.exe.old.1"), b"old1").unwrap();
fs::write(tmp.join("stem.exe.old.2"), b"old2").unwrap();
fs::write(tmp.join("other.exe"), b"unrelated").unwrap();
gc_old_files(&tmp, "stem.exe");
assert!(tmp.join("stem.exe").is_file()); assert!(!tmp.join("stem.exe.old.1").exists()); assert!(!tmp.join("stem.exe.old.2").exists()); assert!(tmp.join("other.exe").is_file());
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn test_gc_missing_dir() {
gc_old_files(Path::new("/nonexistent/dir"), "stem.exe");
}
#[test]
fn test_release_cwd_changes_dir() {
let tmp = std::env::temp_dir().join("zccache-release-cwd-test");
let _ = fs::remove_dir_all(&tmp);
fs::create_dir_all(&tmp).unwrap();
let tmp_canon = fs::canonicalize(&tmp).unwrap();
std::env::set_current_dir(&tmp_canon).unwrap();
assert_eq!(std::env::current_dir().unwrap(), tmp_canon);
release_cwd();
assert_ne!(std::env::current_dir().unwrap(), tmp_canon);
let _ = fs::remove_dir_all(&tmp);
}
#[test]
fn zccache_home_dir_resolves_to_dot_zccache_under_home_or_userprofile() {
let home = std::env::var_os("HOME")
.or_else(|| std::env::var_os("USERPROFILE"))
.expect("test host must have HOME or USERPROFILE set");
let resolved = zccache_home_dir().expect("home discoverable");
assert_eq!(resolved, Path::new(&home).join(".zccache"));
}
}