Skip to content

Instantly share code, notes, and snippets.

@krisselden
Created May 5, 2021 18:40
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save krisselden/199a2dc4187c944a96318e64b11a9f33 to your computer and use it in GitHub Desktop.
Save krisselden/199a2dc4187c944a96318e64b11a9f33 to your computer and use it in GitHub Desktop.
Chrome Devtools Protocol with Rust
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