#![cfg(all(feature = "embed-helper", target_os = "windows"))]
use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use running_process_probe::inject_into_pid;
fn target_profile_dir() -> PathBuf {
let exe = std::env::current_exe().expect("current_exe");
exe.parent() .and_then(|p| p.parent()) .expect("walk up from test exe")
.to_path_buf()
}
fn target_triple_for_profile_dir(profile_dir: &Path) -> Option<String> {
let triple_dir = profile_dir.parent()?;
let triple = triple_dir.file_name()?.to_str()?;
if triple.contains("-pc-windows-") {
Some(triple.to_string())
} else {
None
}
}
fn add_current_test_target_flags(cmd: &mut Command, profile_dir: &Path) {
if profile_dir
.file_name()
.and_then(|s| s.to_str())
.is_some_and(|s| s == "release")
{
cmd.arg("--release");
}
if let Some(triple) = target_triple_for_profile_dir(profile_dir) {
cmd.arg("--target").arg(triple);
}
}
fn cargo_command() -> Command {
if let Some(soldr) = which("soldr") {
if let Ok(output) = Command::new(soldr)
.args(["rustup", "which", "cargo"])
.output()
{
if output.status.success() {
let cargo = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !cargo.is_empty() {
return Command::new(cargo);
}
}
}
}
Command::new("cargo")
}
fn build_and_locate_interposer_dll() -> PathBuf {
let profile_dir = target_profile_dir();
let mut cmd = cargo_command();
cmd.args(["build", "-p", "running-process-probe-interposer-windows"]);
add_current_test_target_flags(&mut cmd, &profile_dir);
let status = cmd.status().expect("spawn cargo to build interposer dll");
assert!(
status.success(),
"cargo build of interposer DLL failed: {status:?}"
);
let dll = profile_dir.join("running_process_probe_interposer_windows.dll");
assert!(
dll.exists(),
"expected interposer DLL at {dll:?} after cargo build"
);
dll
}
fn build_createfilew_probe() {
let profile_dir = target_profile_dir();
let mut cmd = cargo_command();
cmd.args([
"build",
"-p",
"testbins",
"--bin",
"testbin-createfilew-probe",
]);
add_current_test_target_flags(&mut cmd, &profile_dir);
let status = cmd
.status()
.expect("spawn cargo to build testbin-createfilew-probe");
assert!(
status.success(),
"cargo build of testbin-createfilew-probe failed: {status:?}"
);
}
fn which(name: &str) -> Option<PathBuf> {
let candidates: Vec<String> = if name.ends_with(".exe") {
vec![name.to_string()]
} else {
vec![name.to_string(), format!("{name}.exe")]
};
let path = std::env::var_os("PATH")?;
for entry in std::env::split_paths(&path) {
for cand in &candidates {
let p = entry.join(cand);
if p.is_file() {
return Some(p);
}
}
}
None
}
#[test]
fn interposer_dll_fires_rpp_hook_after_inject() {
let dll = build_and_locate_interposer_dll();
build_createfilew_probe();
let tmp = tempfile::tempdir().expect("tempdir");
let probe_path = tmp.path().join("probe.txt");
std::fs::write(&probe_path, b"hello from slice 7\n").expect("write probe");
let fixture = target_profile_dir().join("testbin-createfilew-probe.exe");
assert!(
fixture.exists(),
"testbin-createfilew-probe not built — \
run `cargo build -p testbins --bin testbin-createfilew-probe` \
(or rely on a workspace-wide `cargo build` having done so). \
expected at {fixture:?}"
);
let mut child = Command::new(&fixture)
.arg("2000")
.arg(&probe_path)
.stdout(Stdio::null())
.stderr(Stdio::piped())
.spawn()
.expect("spawn testbin-createfilew-probe");
let pid = child.id();
std::thread::sleep(Duration::from_millis(200));
let inject_result = inject_into_pid(pid, &dll);
std::thread::sleep(Duration::from_millis(200));
let stderr_text: Arc<Mutex<String>> = Arc::new(Mutex::new(String::new()));
let stderr_pipe = child.stderr.take().expect("stderr piped");
let reader_text = Arc::clone(&stderr_text);
let reader = std::thread::spawn(move || {
let mut pipe = stderr_pipe;
let mut buf = [0u8; 4096];
loop {
match pipe.read(&mut buf) {
Ok(0) => break,
Ok(n) => {
if let Ok(mut s) = reader_text.lock() {
s.push_str(&String::from_utf8_lossy(&buf[..n]));
}
}
Err(_) => break,
}
}
});
let deadline = Instant::now() + Duration::from_secs(10);
while Instant::now() < deadline {
if stderr_text
.lock()
.map(|s| s.contains("RPP_HOOK file-open") && s.contains("probe.txt"))
.unwrap_or(false)
{
break;
}
std::thread::sleep(Duration::from_millis(50));
}
let _ = child.kill();
let _ = child.wait();
let _ = reader.join();
let hmodule = inject_result.expect("inject_into_pid should succeed");
assert!(hmodule != 0, "remote LoadLibraryW returned NULL");
let captured = stderr_text.lock().map(|s| s.clone()).unwrap_or_default();
assert!(
captured.contains("RPP_HOOK"),
"expected at least one RPP_HOOK line on the child's stderr; \
got: {captured:?}"
);
assert!(
captured.contains("RPP_HOOK file-open") && captured.contains("probe.txt"),
"expected `RPP_HOOK file-open path=...probe.txt...` after \
the detoured CreateFileW call; got: {captured:?}"
);
}