1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
//! Regression test for `trampoline::unlock_exe()`.
//!
//! On Windows the OS file-locks a running executable, so without
//! `unlock_exe()` `pip install --upgrade zccache` would fail to overwrite
//! `Scripts/zccache-daemon.exe` while the daemon is alive. `unlock_exe()`
//! sidesteps the lock by renaming the canonical path to
//! `zccache-daemon.exe.old.<rand>` and copying back; the running process
//! keeps executing from the renamed file and the canonical path is now an
//! unlocked copy.
//!
//! This test proves the lock was actually lifted by trying to overwrite
//! the canonical path while the daemon is running.
//!
//! Windows-only: on Unix, running binaries can be replaced freely so the
//! test would pass without proving anything.
//!
//! See issue #134 / PR #135.
#![cfg(windows)]
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
#[test]
fn daemon_exe_path_is_overwritable_while_running() {
let daemon_src = env!("CARGO_BIN_EXE_zccache-daemon");
let tmp = tempfile::tempdir().expect("create tempdir");
let dest = tmp.path().join("zccache-daemon.exe");
std::fs::copy(daemon_src, &dest).expect("copy daemon binary into tempdir");
let endpoint = zccache::ipc::unique_test_endpoint();
let cache_dir = tmp.path().join("cache");
std::fs::create_dir_all(&cache_dir).expect("create per-test cache dir");
let mut child = Command::new(&dest)
.args(["--foreground", "--endpoint", &endpoint])
.env("ZCCACHE_CACHE_DIR", &cache_dir)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn daemon");
// Poll until the canonical path is overwritable. unlock_exe() runs
// synchronously at the top of main(), but the daemon hasn't necessarily
// reached it yet at the instant spawn() returns. If the daemon ever
// exits before the overwrite succeeds, fail loudly — that would mean
// the file is overwritable for the wrong reason.
let deadline = Instant::now() + Duration::from_secs(5);
let mut last_err: Option<std::io::Error> = None;
let overwrote = loop {
if let Some(status) = child.try_wait().expect("query daemon child") {
let _ = child.wait();
panic!("daemon exited prematurely (status {status:?}); last write error: {last_err:?}");
}
match std::fs::write(&dest, b"replaced") {
Ok(()) => break true,
Err(e) => last_err = Some(e),
}
if Instant::now() >= deadline {
break false;
}
std::thread::sleep(Duration::from_millis(50));
};
// The daemon is running from `<tmp>/zccache-daemon.exe.old.<rand>`,
// so killing by PID still works even though the canonical path now
// contains "replaced".
let _ = child.kill();
let _ = child.wait();
assert!(
overwrote,
"daemon exe path remained locked after spawn — last error: {last_err:?}"
);
}