chromedriver_launch/
chromedriver.rs1use std::collections::HashSet;
5use std::net::IpAddr;
6use std::net::Ipv4Addr;
7use std::net::SocketAddr;
8use std::os::unix::process::CommandExt as _;
9use std::path::Path;
10use std::path::PathBuf;
11use std::process::Child;
12use std::process::Command;
13use std::process::Stdio;
14use std::thread::sleep;
15use std::time::Duration;
16use std::time::Instant;
17
18use anyhow::bail;
19use anyhow::Context as _;
20use anyhow::Result;
21
22use libc::killpg;
23use libc::setpgid;
24use libc::SIGKILL;
25
26use crate::socket;
27use crate::tcp;
28use crate::util::check;
29
30
31const CHROME_DRIVER: &str = "chromedriver";
33const PORT_FIND_TIMEOUT: Duration = Duration::from_secs(30);
35
36
37fn find_localhost_port(pid: u32) -> Result<u16> {
38 let start = Instant::now();
39
40 let port = loop {
42 let inodes = socket::socket_inodes(pid)?.collect::<Result<HashSet<_>>>()?;
43 let result = tcp::parse(pid)?.find(|result| match result {
44 Ok(entry) => {
45 if inodes.contains(&entry.inode) {
46 entry.addr == Ipv4Addr::LOCALHOST
47 } else {
48 false
49 }
50 },
51 Err(_) => true,
52 });
53 match result {
54 None => {
55 if start.elapsed() >= PORT_FIND_TIMEOUT {
56 bail!("failed to find local host port for process {pid}");
57 }
58 sleep(Duration::from_millis(1))
59 },
60 Some(result) => {
61 break result
62 .context("failed to find localhost proc tcp entry")?
63 .port
64 },
65 }
66 };
67
68 Ok(port)
69}
70
71
72#[derive(Debug)]
74pub struct Builder {
75 chromedriver: PathBuf,
77 timeout: Duration,
80}
81
82impl Builder {
83 pub fn set_chromedriver(mut self, chromedriver: impl AsRef<Path>) -> Self {
85 self.chromedriver = chromedriver.as_ref().to_path_buf();
86 self
87 }
88
89 pub fn set_timeout(mut self, timeout: Duration) -> Self {
91 self.timeout = timeout;
92 self
93 }
94
95 pub fn launch(self) -> Result<Chromedriver> {
98 let process = unsafe {
99 Command::new(CHROME_DRIVER)
100 .arg("--port=0")
101 .stdout(Stdio::piped())
102 .stderr(Stdio::piped())
103 .pre_exec(|| {
104 let result = setpgid(0, 0);
108 check(result, -1)
109 })
110 .spawn()
111 .with_context(|| format!("failed to launch `{CHROME_DRIVER}` instance"))
112 }?;
113
114 let pid = process.id();
115 let port = find_localhost_port(pid)?;
116
117 let slf = Chromedriver { process, port };
118 Ok(slf)
119 }
120}
121
122impl Default for Builder {
123 fn default() -> Self {
124 Self {
125 chromedriver: PathBuf::from(CHROME_DRIVER),
126 timeout: PORT_FIND_TIMEOUT,
127 }
128 }
129}
130
131
132#[derive(Debug)]
134pub struct Chromedriver {
135 process: Child,
137 port: u16,
139}
140
141impl Chromedriver {
142 pub fn launch() -> Result<Self> {
145 Self::builder().launch()
146 }
147
148 pub fn builder() -> Builder {
151 Builder::default()
152 }
153
154 fn destroy_impl(&mut self) -> Result<()> {
156 let pid = self.process.id();
159 let result = unsafe { killpg(pid as _, SIGKILL) };
161 let () = check(result, -1).context("failed to shut down chromedriver process group")?;
162
163 let _status = self.process.wait()?;
169 Ok(())
170 }
171
172 #[inline]
174 pub fn destroy(mut self) -> Result<()> {
175 self.destroy_impl()
176 }
177
178 #[inline]
181 pub fn socket_addr(&self) -> SocketAddr {
182 SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), self.port)
183 }
184}
185
186impl Drop for Chromedriver {
187 fn drop(&mut self) {
188 let _result = self.destroy_impl();
189 }
190}
191
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 use std::net::TcpListener;
198 use std::process;
199
200
201 #[test]
203 fn localhost_port_finding() {
204 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
205 let addr = listener.local_addr().unwrap();
206 let port = find_localhost_port(process::id()).unwrap();
207 assert_eq!(port, addr.port());
208 }
209}