bxcore, loopd, inferproxy: a lock file keeps each to one instance
Co-Authored-By: ornith-1.5-35b-a3b <agent@wntrmute.dev>
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
//! One `inferproxy` per socket (M5b spec, section 9a): the lock is `<socket>.lock`, beside the
|
||||
//! `--listen` socket. A second `inferproxy` on the same socket is refused, and the first keeps
|
||||
//! forwarding; once the first is killed, a new one removes the stale socket and forwards. Every
|
||||
//! wait has a deadline, and every process is killed and every path removed by a guard (tip T44).
|
||||
//! Do not edit.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpListener;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::os::unix::net::UnixStream;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
const POINTER: &str = "see docs/runbook.md#already-running";
|
||||
|
||||
static NEXT: AtomicU32 = AtomicU32::new(0);
|
||||
|
||||
/// A temporary directory for the socket, removed when dropped, first with its owner's permissions
|
||||
/// put back.
|
||||
struct Dir(PathBuf);
|
||||
|
||||
impl Dir {
|
||||
fn new() -> Dir {
|
||||
let n = NEXT.fetch_add(1, Ordering::SeqCst);
|
||||
let dir = std::env::temp_dir().join(format!("ip-lock-{}-{n}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
Dir(dir)
|
||||
}
|
||||
|
||||
fn socket(&self) -> PathBuf {
|
||||
self.0.join("infer.sock")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Dir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::set_permissions(&self.0, std::fs::Permissions::from_mode(0o700));
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// An upstream that reads four bytes from each connection, answers `pong` and closes.
|
||||
fn upstream() -> String {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap().to_string();
|
||||
thread::spawn(move || {
|
||||
for stream in listener.incoming() {
|
||||
let Ok(mut stream) = stream else { break };
|
||||
thread::spawn(move || {
|
||||
let mut ping = [0u8; 4];
|
||||
if stream.read_exact(&mut ping).is_ok() {
|
||||
let _ = stream.write_all(b"pong");
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
addr
|
||||
}
|
||||
|
||||
/// A running `inferproxy`, killed and waited for when dropped. Declared after the `Dir`, so it
|
||||
/// drops first.
|
||||
struct Proxy(Child);
|
||||
|
||||
impl Drop for Proxy {
|
||||
fn drop(&mut self) {
|
||||
let _ = self.0.kill();
|
||||
let _ = self.0.wait();
|
||||
}
|
||||
}
|
||||
|
||||
impl Proxy {
|
||||
fn running(&mut self) -> bool {
|
||||
matches!(self.0.try_wait(), Ok(None))
|
||||
}
|
||||
}
|
||||
|
||||
fn command(socket: &Path, upstream: &str) -> Command {
|
||||
let mut command = Command::new(env!("CARGO_BIN_EXE_inferproxy"));
|
||||
command
|
||||
.arg("--listen")
|
||||
.arg(socket)
|
||||
.args(["--upstream", upstream])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null());
|
||||
command
|
||||
}
|
||||
|
||||
fn start(socket: &Path, upstream: &str) -> Proxy {
|
||||
Proxy(
|
||||
command(socket, upstream)
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Runs `inferproxy` to its end, at most 10 s (then it is killed), and gives its exit code
|
||||
/// (`None` if it had to be killed) and its stderr.
|
||||
fn run_to_end(socket: &Path, upstream: &str) -> (Option<i32>, String) {
|
||||
let mut child = Proxy(
|
||||
command(socket, upstream)
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.unwrap(),
|
||||
);
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
let mut code = None;
|
||||
while Instant::now() < deadline {
|
||||
if let Ok(Some(status)) = child.0.try_wait() {
|
||||
code = status.code();
|
||||
break;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(20));
|
||||
}
|
||||
let _ = child.0.kill();
|
||||
let _ = child.0.wait();
|
||||
let mut stderr = String::new();
|
||||
if let Some(mut pipe) = child.0.stderr.take() {
|
||||
let _ = pipe.read_to_string(&mut stderr);
|
||||
}
|
||||
(code, stderr)
|
||||
}
|
||||
|
||||
/// True when a `ping` sent to `socket` comes back from the upstream as `pong` within 5 s.
|
||||
fn forwards(socket: &Path) -> bool {
|
||||
let Ok(mut stream) = UnixStream::connect(socket) else {
|
||||
return false;
|
||||
};
|
||||
let limit = Some(Duration::from_secs(5));
|
||||
if stream.set_read_timeout(limit).is_err() || stream.set_write_timeout(limit).is_err() {
|
||||
return false;
|
||||
}
|
||||
if stream.write_all(b"ping").is_err() {
|
||||
return false;
|
||||
}
|
||||
let mut answer = Vec::new();
|
||||
let _ = stream.read_to_end(&mut answer);
|
||||
answer == b"pong"
|
||||
}
|
||||
|
||||
/// Waits up to 10 s for an `inferproxy` to forward on `socket`.
|
||||
fn wait_until_it_forwards(socket: &Path) -> bool {
|
||||
let deadline = Instant::now() + Duration::from_secs(10);
|
||||
while Instant::now() < deadline {
|
||||
if forwards(socket) {
|
||||
return true;
|
||||
}
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn inode(path: &Path) -> u64 {
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
std::fs::symlink_metadata(path)
|
||||
.map(|m| m.ino())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_second_inferproxy_on_the_same_socket_is_refused_and_the_first_keeps_forwarding() {
|
||||
let dir = Dir::new();
|
||||
let socket = dir.socket();
|
||||
let upstream = upstream();
|
||||
let mut first = start(&socket, &upstream);
|
||||
assert!(wait_until_it_forwards(&socket), "the first never forwarded");
|
||||
let before = inode(&socket);
|
||||
|
||||
let (code, stderr) = run_to_end(&socket, &upstream);
|
||||
let line = format!(
|
||||
"inferproxy: another inferproxy is running (it holds {}.lock); {POINTER}",
|
||||
socket.display()
|
||||
);
|
||||
assert_eq!(code, Some(1), "the second inferproxy: {stderr}");
|
||||
assert!(
|
||||
stderr.lines().any(|l| l == line),
|
||||
"wanted the line\n{line}\nstderr: {stderr}"
|
||||
);
|
||||
|
||||
assert!(first.running(), "the first inferproxy is still running");
|
||||
assert_eq!(inode(&socket), before, "the first's socket is still there");
|
||||
assert!(forwards(&socket), "the first still forwards");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn after_the_first_is_killed_a_new_inferproxy_removes_the_stale_socket_and_forwards() {
|
||||
let dir = Dir::new();
|
||||
let socket = dir.socket();
|
||||
let upstream = upstream();
|
||||
let mut first = start(&socket, &upstream);
|
||||
assert!(wait_until_it_forwards(&socket), "the first never forwarded");
|
||||
let _ = first.0.kill();
|
||||
let _ = first.0.wait();
|
||||
assert!(socket.exists(), "a killed inferproxy leaves its socket");
|
||||
assert!(!forwards(&socket), "nobody serves the stale socket");
|
||||
|
||||
let _next = start(&socket, &upstream);
|
||||
assert!(
|
||||
wait_until_it_forwards(&socket),
|
||||
"the new one never forwarded"
|
||||
);
|
||||
let lock = dir.0.join("infer.sock.lock");
|
||||
let mode = std::fs::metadata(&lock).map(|m| m.permissions().mode() & 0o777);
|
||||
assert_eq!(mode.ok(), Some(0o600), "the lock file, beside the socket");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_socket_directory_that_cannot_be_written_stops_inferproxy_naming_the_error() {
|
||||
let dir = Dir::new();
|
||||
let socket = dir.socket();
|
||||
std::fs::set_permissions(&dir.0, std::fs::Permissions::from_mode(0o500)).unwrap();
|
||||
|
||||
let (code, stderr) = run_to_end(&socket, &upstream());
|
||||
let line = format!(
|
||||
"inferproxy: cannot take the lock {}.lock: Permission denied (os error 13); {POINTER}",
|
||||
socket.display()
|
||||
);
|
||||
assert_eq!(code, Some(1), "{stderr}");
|
||||
assert!(
|
||||
stderr.lines().any(|l| l == line),
|
||||
"wanted the line\n{line}\nstderr: {stderr}"
|
||||
);
|
||||
assert!(!socket.exists(), "no socket");
|
||||
}
|
||||
Reference in New Issue
Block a user