use std::collections::HashSet;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::net::SocketAddr;
use std::os::unix::process::CommandExt as _;
use std::path::Path;
use std::path::PathBuf;
use std::process::Child;
use std::process::Command;
use std::process::Stdio;
use std::thread::sleep;
use std::time::Duration;
use std::time::Instant;
use anyhow::bail;
use anyhow::Context as _;
use anyhow::Result;
use libc::killpg;
use libc::setpgid;
use libc::SIGKILL;
use crate::socket;
use crate::tcp;
use crate::util::check;
const CHROME_DRIVER: &str = "chromedriver";
const PORT_FIND_TIMEOUT: Duration = Duration::from_secs(30);
fn find_localhost_port(pid: u32) -> Result<u16> {
let start = Instant::now();
let port = loop {
let inodes = socket::socket_inodes(pid)?.collect::<Result<HashSet<_>>>()?;
let result = tcp::parse(pid)?.find(|result| match result {
Ok(entry) => {
if inodes.contains(&entry.inode) {
entry.addr == Ipv4Addr::LOCALHOST
} else {
false
}
},
Err(_) => true,
});
match result {
None => {
if start.elapsed() >= PORT_FIND_TIMEOUT {
bail!("failed to find local host port for process {pid}");
}
sleep(Duration::from_millis(1))
},
Some(result) => {
break result
.context("failed to find localhost proc tcp entry")?
.port
},
}
};
Ok(port)
}
#[derive(Debug)]
pub struct Builder {
chromedriver: PathBuf,
timeout: Duration,
}
impl Builder {
pub fn set_chromedriver(mut self, chromedriver: impl AsRef<Path>) -> Self {
self.chromedriver = chromedriver.as_ref().to_path_buf();
self
}
pub fn set_timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
pub fn launch(self) -> Result<Chromedriver> {
let process = unsafe {
Command::new(CHROME_DRIVER)
.arg("--port=0")
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.pre_exec(|| {
let result = setpgid(0, 0);
check(result, -1)
})
.spawn()
.with_context(|| format!("failed to launch `{CHROME_DRIVER}` instance"))
}?;
let pid = process.id();
let port = find_localhost_port(pid)?;
let slf = Chromedriver { process, port };
Ok(slf)
}
}
impl Default for Builder {
fn default() -> Self {
Self {
chromedriver: PathBuf::from(CHROME_DRIVER),
timeout: PORT_FIND_TIMEOUT,
}
}
}
#[derive(Debug)]
pub struct Chromedriver {
process: Child,
port: u16,
}
impl Chromedriver {
pub fn launch() -> Result<Self> {
Self::builder().launch()
}
pub fn builder() -> Builder {
Builder::default()
}
fn destroy_impl(&mut self) -> Result<()> {
let pid = self.process.id();
let result = unsafe { killpg(pid as _, SIGKILL) };
let () = check(result, -1).context("failed to shut down chromedriver process group")?;
let _status = self.process.wait()?;
Ok(())
}
#[inline]
pub fn destroy(mut self) -> Result<()> {
self.destroy_impl()
}
#[inline]
pub fn socket_addr(&self) -> SocketAddr {
SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), self.port)
}
}
impl Drop for Chromedriver {
fn drop(&mut self) {
let _result = self.destroy_impl();
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::net::TcpListener;
use std::process;
#[test]
fn localhost_port_finding() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let port = find_localhost_port(process::id()).unwrap();
assert_eq!(port, addr.port());
}
}