Created
May 5, 2021 18:40
-
-
Save krisselden/199a2dc4187c944a96318e64b11a9f33 to your computer and use it in GitHub Desktop.
Chrome Devtools Protocol with Rust
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use libc; | |
use serde_json::{json, Value}; | |
use std::error; | |
use std::ffi::OsString; | |
use std::fs::File; | |
use std::io; | |
use std::io::prelude::*; | |
use std::io::{BufReader, BufWriter}; | |
use std::os::unix::io::RawFd; | |
use std::os::unix::prelude::*; | |
use std::os::unix::process::CommandExt; | |
use std::path::Path; | |
use std::process::{Child, Command}; | |
use std::thread; | |
use std::time::Duration; | |
use tempdir::TempDir; | |
const CHROME_PATH: &'static str = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; | |
const CHROME_ARGS: &'static [&'static str] = &[ | |
"--remote-debugging-pipe", | |
"--no-first-run", | |
"--disable-default-apps", | |
"--no-default-browser-check", | |
"--no-sync", | |
]; | |
fn main() -> Result<(), Box<dyn error::Error>> { | |
// need the temp dir lifetime to live as long as the process | |
let user_data_dir = TempDir::new("chrome-user")?; | |
let (mut chrome, reader, writer) = spawn_chrome(user_data_dir.path())?; | |
let read_thread = spawn_reader(reader, move |msg| { | |
io::stdout().write(msg.as_slice())?; | |
io::stdout().write(b"\n")?; | |
Ok(()) | |
}); | |
let mut writer = CommandWriter::new(BufWriter::new(writer)); | |
writer.write( | |
"Target.setDiscoverTargets", | |
json!({ | |
"discover": true | |
}), | |
)?; | |
writer.write( | |
"Target.createTarget", | |
json!({ | |
"url": "https://tracerbench.com" | |
}), | |
)?; | |
writer.flush()?; | |
thread::sleep(Duration::from_secs(5)); | |
writer.write("Browser.close", json!({}))?; | |
writer.flush()?; | |
chrome.wait()?; | |
read_thread.join().unwrap()?; | |
user_data_dir.close()?; | |
Ok(()) | |
} | |
fn spawn_chrome(temp_dir: &Path) -> io::Result<(Child, impl Read, impl Write)> { | |
let mut command = Command::new(CHROME_PATH); | |
let args = build_args(temp_dir); | |
command.args(&args[..]); | |
// FD 3 4 | |
let (their_read, our_write) = pipe()?; | |
// FD 5 6 | |
let (our_read, their_write) = pipe()?; | |
unsafe { | |
// safe since closure just takes ownership of inheritable FD | |
// this closure is run after forking inside the child process | |
// but before exec | |
command.pre_exec(move || { | |
// leave 3 | |
// close 4 5 | |
// dup 6 onto 4 | |
close(our_write)?; | |
close(our_read)?; | |
let their_write = dup(their_write)?; | |
// assert expectation of --remote-debugging-pipe | |
assert_eq!(their_read, 3); | |
assert_eq!(their_write, 4); | |
Ok(()) | |
}) | |
}; | |
let child = command.spawn()?; | |
close(their_read)?; | |
close(their_write)?; | |
let our_read = unsafe { File::from_raw_fd(our_read) }; | |
let our_write = unsafe { File::from_raw_fd(our_write) }; | |
Ok((child, our_read, our_write)) | |
} | |
fn build_args(temp_user_dir: &Path) -> Vec<OsString> { | |
let mut args: Vec<OsString> = Vec::with_capacity(CHROME_ARGS.len() + 1); | |
let mut user_dir_arg: OsString = "--user-data-dir=".into(); | |
user_dir_arg.push(temp_user_dir); | |
args.push(user_dir_arg); | |
args.extend(CHROME_ARGS.iter().map(|arg| arg.into())); | |
args | |
} | |
fn spawn_reader<R, F>(reader: R, handle_msg: F) -> thread::JoinHandle<io::Result<()>> | |
where | |
F: Fn(Vec<u8>) -> io::Result<()>, | |
F: Send + 'static, | |
R: Read + Send + 'static, | |
{ | |
thread::spawn(move || -> io::Result<()> { | |
let buffered = BufReader::new(reader); | |
for msg in buffered.split(b'\x00') { | |
handle_msg(msg?)?; | |
} | |
Ok(()) | |
}) | |
} | |
struct CommandWriter<W> { | |
seq: u32, | |
writer: W, | |
} | |
impl<W: Write> CommandWriter<W> { | |
fn new(writer: W) -> Self { | |
CommandWriter { seq: 0, writer } | |
} | |
fn write(&mut self, method: &str, params: Value) -> io::Result<()> { | |
self.seq += 1; | |
let msg = json!({ | |
"id": self.seq, | |
"method": method, | |
"params": params, | |
}); | |
let encoded = serde_json::to_vec(&msg)?; | |
self.writer.write(encoded.as_slice())?; | |
self.writer.write(b"\x00")?; | |
Ok(()) | |
} | |
fn flush(&mut self) -> io::Result<()> { | |
self.writer.flush() | |
} | |
} | |
fn close(fd: RawFd) -> io::Result<()> { | |
unsafe { | |
if libc::close(fd) == -1 { | |
Err(io::Error::last_os_error()) | |
} else { | |
Ok(()) | |
} | |
} | |
} | |
fn dup(oldfd: RawFd) -> io::Result<RawFd> { | |
unsafe { | |
let newfd = libc::dup(oldfd); | |
if newfd == -1 { | |
Err(io::Error::last_os_error()) | |
} else { | |
Ok(newfd) | |
} | |
} | |
} | |
fn pipe() -> io::Result<(RawFd, RawFd)> { | |
unsafe { | |
let mut fds = std::mem::MaybeUninit::<[RawFd; 2]>::uninit(); | |
if libc::pipe(fds.as_mut_ptr() as *mut RawFd) == -1 { | |
Err(io::Error::last_os_error()) | |
} else { | |
let fds = fds.assume_init(); | |
let read = fds[0]; | |
let write = fds[1]; | |
Ok((read, write)) | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment