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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
// SPDX-License-Identifier: Apache-2.0
//! #405 — single-instance lock per node_id.
//!
//! Two `iicp-node serve` processes for the SAME node_id fight: each
//! registration rotates the directory-issued token and invalidates the other's,
//! so they enter a 401 → re-register war that makes the node flap in the
//! directory. This guard prevents that by holding a pidfile at
//! `~/.iicp/run/<node_id>.pid`; a second live process for the same node_id is
//! refused (unless `--force`). Distinct node_ids are unaffected — a fleet of N
//! nodes runs fine (each has its own lock).
//!
//! **Fail-open**: any filesystem error degrades to a no-op lock (with a warning)
//! — the guard must never prevent a node from starting.
use std::path::PathBuf;
/// Held for the lifetime of `serve`; removes the pidfile on drop.
pub struct InstanceLock {
path: PathBuf,
}
impl InstanceLock {
/// Acquire the per-node_id lock. `Err(message)` if another LIVE process
/// already serves this node_id and `force` is false. Fails open on I/O error.
pub fn acquire(node_id: &str, force: bool) -> Result<Self, String> {
let dir = match crate::identity::config_dir() {
Ok(d) => d.join("run"),
Err(_) => {
return Ok(Self {
path: PathBuf::new(),
})
} // fail open
};
if std::fs::create_dir_all(&dir).is_err() {
return Ok(Self {
path: PathBuf::new(),
}); // fail open
}
let path = dir.join(format!("{node_id}.pid"));
if !force {
if let Ok(existing) = std::fs::read_to_string(&path) {
if let Ok(pid) = existing.trim().parse::<i32>() {
if pid != std::process::id() as i32 && pid_alive(pid) {
return Err(format!(
"node_id {node_id} is already being served by PID {pid}. \
Stop that process, choose a different --node, or pass --force to take over."
));
}
}
}
}
// Best-effort write; a failure here still yields a (weaker) lock guard.
let _ = std::fs::write(&path, std::process::id().to_string());
Ok(Self { path })
}
}
impl Drop for InstanceLock {
fn drop(&mut self) {
if !self.path.as_os_str().is_empty() {
let _ = std::fs::remove_file(&self.path);
}
}
}
/// True if a process with `pid` is alive. Unix: `kill -0` (no signal sent).
/// Non-unix: fail open (assume not alive) so the lock never blocks startup.
#[cfg(unix)]
fn pid_alive(pid: i32) -> bool {
std::process::Command::new("kill")
.args(["-0", &pid.to_string()])
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[cfg(not(unix))]
fn pid_alive(_pid: i32) -> bool {
false
}
#[cfg(test)]
mod tests {
use super::InstanceLock;
fn with_tmp_home<F: FnOnce()>(f: F) {
let tmp = std::env::temp_dir().join(format!("iicp_lock_test_{}", std::process::id()));
let _ = std::fs::create_dir_all(&tmp);
std::env::set_var("IICP_HOME", &tmp);
f();
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn live_foreign_pid_is_refused() {
with_tmp_home(|| {
// Simulate another live process (same user, signalable) holding the lock
// by spawning a real child and writing its PID into the pidfile.
let mut child = std::process::Command::new("sleep")
.arg("30")
.spawn()
.expect("spawn sleep");
let dir = crate::identity::config_dir().unwrap().join("run");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("dup-node.pid"), child.id().to_string()).unwrap();
let r = InstanceLock::acquire("dup-node", false);
let forced = InstanceLock::acquire("dup-node", true);
let _ = child.kill();
let _ = child.wait();
assert!(r.is_err(), "a live foreign PID must refuse the acquire");
assert!(forced.is_ok(), "force must override");
});
}
#[test]
fn distinct_nodes_and_release_on_drop() {
with_tmp_home(|| {
// distinct node_ids never conflict (fleet case)
let a = InstanceLock::acquire("node-a", false);
let b = InstanceLock::acquire("node-b", false);
assert!(
a.is_ok() && b.is_ok(),
"distinct node_ids must both acquire"
);
// releasing (drop) frees the lock so it can be re-acquired
drop(a);
assert!(
InstanceLock::acquire("node-a", false).is_ok(),
"lock must be re-acquirable after drop"
);
});
}
}