#![allow(clippy::disallowed_macros, clippy::disallowed_methods)]
use std::io::Write;
use std::net::TcpListener;
use std::time::{Duration, Instant};
const LIFETIME: Duration = Duration::from_secs(120);
fn main() {
let flag = |name: &str| std::env::args().any(|a| a == name);
if flag("--ignore-term") {
ignore_term();
}
if flag("--takeover") {
takeover();
return;
}
if flag("--no-port") {
println!("PORT 0");
std::io::stdout().flush().expect("flush stdout");
let deadline = Instant::now() + LIFETIME;
while Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(500));
}
return;
}
let port_arg = std::env::args()
.skip_while(|a| a != "--port")
.nth(1)
.unwrap_or_else(|| "0".to_string());
let host = if flag("--v6") { "[::1]" } else { "127.0.0.1" };
let listener = TcpListener::bind(format!("{host}:{port_arg}")).expect("bind the listener");
let port = listener.local_addr().expect("local addr").port();
println!("PORT {port}");
std::io::stdout().flush().expect("flush stdout");
let also: usize = std::env::args()
.skip_while(|a| a != "--also")
.nth(1)
.and_then(|n| n.parse().ok())
.unwrap_or(0);
let _also_listeners: Vec<TcpListener> = loop {
let probes: Vec<TcpListener> = (0..also)
.map(|_| TcpListener::bind(format!("{host}:0")).expect("bind a free port"))
.collect();
let mut ports: Vec<u16> = probes
.iter()
.map(|l| l.local_addr().expect("local addr").port())
.collect();
drop(probes);
ports.sort_unstable_by(|a, b| b.cmp(a));
let bound: std::io::Result<Vec<TcpListener>> = ports
.iter()
.map(|port| TcpListener::bind(format!("{host}:{port}")))
.collect();
if let Ok(bound) = bound {
for extra in &bound {
println!("ALSO {}", extra.local_addr().expect("local addr").port());
}
std::io::stdout().flush().expect("flush stdout");
break bound;
}
};
let deadline = Instant::now() + LIFETIME;
if flag("--share") {
share_socket();
}
let listener = if flag("--respawn") {
match respawn_when_free(listener, port, deadline) {
Some(listener) => listener,
None => return,
}
} else {
listener
};
let busy = flag("--busy");
while Instant::now() < deadline {
if busy {
std::hint::black_box(0u64);
} else {
std::thread::sleep(Duration::from_millis(500));
}
}
drop(listener);
}
#[cfg(unix)]
fn ignore_term() {
unsafe {
libc::signal(libc::SIGTERM, libc::SIG_IGN);
}
}
#[cfg(not(unix))]
fn ignore_term() {}
#[cfg(unix)]
fn share_socket() {
let pid = unsafe { libc::fork() };
assert!(pid >= 0, "fork failed");
if pid > 0 {
println!("CHILD {pid}");
std::io::stdout().flush().expect("flush stdout");
}
}
#[cfg(not(unix))]
fn share_socket() {}
#[cfg(unix)]
fn respawn_when_free(listener: TcpListener, port: u16, deadline: Instant) -> Option<TcpListener> {
let pid = unsafe { libc::fork() };
assert!(pid >= 0, "fork failed");
if pid > 0 {
println!("CHILD {pid}");
std::io::stdout().flush().expect("flush stdout");
return Some(listener);
}
drop(listener);
while Instant::now() < deadline {
if let Ok(taken) = TcpListener::bind(("127.0.0.1", port)) {
return Some(taken);
}
std::thread::sleep(Duration::from_millis(1));
}
None
}
#[cfg(not(unix))]
fn respawn_when_free(listener: TcpListener, port: u16, _deadline: Instant) -> Option<TcpListener> {
#[allow(clippy::zombie_processes)]
let child = std::process::Command::new(std::env::current_exe().expect("own path"))
.args(["--takeover", &port.to_string()])
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("start the takeover copy");
println!("CHILD {}", child.id());
std::io::stdout().flush().expect("flush stdout");
Some(listener)
}
fn takeover() {
let port: u16 = std::env::args()
.skip_while(|a| a != "--takeover")
.nth(1)
.and_then(|p| p.parse().ok())
.expect("--takeover <port>");
let deadline = Instant::now() + LIFETIME;
while Instant::now() < deadline {
if let Ok(taken) = TcpListener::bind(("127.0.0.1", port)) {
while Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(500));
}
drop(taken);
return;
}
std::thread::sleep(Duration::from_millis(1));
}
}