use std::path::{Path, PathBuf};
pub fn fnv1a_64(data: &[u8]) -> u64 {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for &byte in data {
hash ^= byte as u64;
hash = hash.wrapping_mul(0x0000_0100_0000_01B3);
}
hash
}
pub fn scope_hash(cwd: &Path) -> String {
let canonical = cwd.canonicalize().unwrap_or_else(|_| cwd.to_path_buf());
let normalized = canonical.to_string_lossy().to_lowercase();
format!("{:016x}", fnv1a_64(normalized.as_bytes()))
}
pub fn is_in_build_output(exe: &Path) -> bool {
let s = exe.to_string_lossy();
s.contains("target/debug")
|| s.contains("target\\debug")
|| s.contains("target/release")
|| s.contains("target\\release")
}
pub fn shadow_dir() -> PathBuf {
#[cfg(target_os = "macos")]
{
dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("running-process")
.join("run")
}
#[cfg(target_os = "linux")]
{
if let Ok(runtime) = std::env::var("XDG_RUNTIME_DIR") {
PathBuf::from(runtime).join("running-process").join("run")
} else {
dirs::data_local_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("running-process")
.join("run")
}
}
#[cfg(target_os = "windows")]
{
dirs::data_local_dir()
.unwrap_or_else(|| PathBuf::from("C:\\ProgramData"))
.join("running-process")
.join("run")
}
}
const SHADOW_MARKER_ENV: &str = "RUNNING_PROCESS_DAEMON_SHADOWED";
pub fn maybe_self_relocate() -> Result<bool, Box<dyn std::error::Error>> {
if std::env::var(SHADOW_MARKER_ENV).is_ok() {
return Ok(false);
}
let current_exe = std::env::current_exe()?;
if !is_in_build_output(¤t_exe) {
return Ok(false);
}
let shadow = shadow_dir();
std::fs::create_dir_all(&shadow)?;
let file_name = current_exe
.file_name()
.ok_or("current exe has no file name")?;
let dest = shadow.join(file_name);
std::fs::copy(¤t_exe, &dest)?;
reexec_from_shadow(&dest)?;
Ok(true) }
#[cfg(unix)]
fn reexec_from_shadow(exe: &Path) -> Result<(), Box<dyn std::error::Error>> {
use std::os::unix::process::CommandExt;
let args: Vec<_> = std::env::args_os().skip(1).collect();
let err = std::process::Command::new(exe)
.args(&args)
.env(SHADOW_MARKER_ENV, "1")
.exec(); Err(Box::new(err))
}
#[cfg(windows)]
fn reexec_from_shadow(exe: &Path) -> Result<(), Box<dyn std::error::Error>> {
use std::os::windows::process::CommandExt;
const DETACHED_PROCESS: u32 = 0x0000_0008;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
let args: Vec<_> = std::env::args_os().skip(1).collect();
std::process::Command::new(exe)
.args(&args)
.env(SHADOW_MARKER_ENV, "1")
.creation_flags(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP)
.spawn()?;
std::process::exit(0);
}
pub fn cleanup_stale_shadows() {
let dir = shadow_dir();
if !dir.exists() {
return;
}
let current_exe = match std::env::current_exe() {
Ok(p) => p,
Err(_) => return,
};
let entries = match std::fs::read_dir(&dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_file() && path != current_exe {
let _ = std::fs::remove_file(&path);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fnv1a_known_vector() {
assert_eq!(fnv1a_64(b""), 0xcbf2_9ce4_8422_2325);
}
#[test]
fn scope_hash_deterministic() {
let a = scope_hash(Path::new("/tmp/foo"));
let b = scope_hash(Path::new("/tmp/foo"));
assert_eq!(a, b);
assert_eq!(a.len(), 16); }
#[test]
fn build_output_detection() {
assert!(is_in_build_output(Path::new(
"/home/user/project/target/debug/daemon"
)));
assert!(is_in_build_output(Path::new(
"C:\\dev\\project\\target\\release\\daemon.exe"
)));
assert!(!is_in_build_output(Path::new("/usr/local/bin/daemon")));
}
#[test]
fn shadow_dir_is_not_empty() {
let d = shadow_dir();
assert!(!d.as_os_str().is_empty());
}
}