use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex as StdMutex};
use std::time::Duration;
use base64::Engine as _;
use command_stream::{quote::quote, ProcessRunner, RunOptions, StdinOption};
use serde_json::{json, Map, Value};
use thiserror::Error;
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
use tokio::sync::{mpsc, oneshot, Mutex};
use crate::utilities::subprocess::kill_owned_process_tree;
pub const JS_CLI_ENV: &str = "BROWSER_COMMANDER_JS_CLI";
const STDERR_LINES: usize = 50;
const EXIT_GRACE: Duration = Duration::from_secs(5);
#[derive(Debug, Error)]
pub enum BridgeError {
#[error("{name}: {message}")]
Remote {
code: i64,
name: String,
message: String,
stack: Option<String>,
},
#[error("serve --stdio closed: {0}")]
Closed(String),
#[error("serve --stdio I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("serve --stdio sent invalid JSON: {0}")]
Json(#[from] serde_json::Error),
#[error("expected {expected} from the bridge, got {value}")]
Decode {
expected: &'static str,
value: Value,
},
#[error("serve --stdio unavailable: {0}")]
Unavailable(String),
}
impl BridgeError {
pub fn is_timeout(&self) -> bool {
matches!(self, BridgeError::Remote { name, .. } if name == "TimeoutError")
}
fn decode(expected: &'static str, value: Value) -> Self {
BridgeError::Decode { expected, value }
}
}
fn remote_error(error: &Value) -> BridgeError {
let data = error.get("data");
let text = |value: Option<&Value>| value.and_then(Value::as_str).map(str::to_string);
BridgeError::Remote {
code: error.get("code").and_then(Value::as_i64).unwrap_or(-32000),
name: text(data.and_then(|d| d.get("name"))).unwrap_or_else(|| "RpcError".into()),
message: text(error.get("message")).unwrap_or_default(),
stack: text(data.and_then(|d| d.get("stack"))),
}
}
struct Waiter {
sender: oneshot::Sender<Result<Value, BridgeError>>,
subscribe: bool,
}
type Pending = HashMap<u64, Waiter>;
type Subscribers = HashMap<String, mpsc::UnboundedSender<Vec<Value>>>;
type Receivers = HashMap<String, mpsc::UnboundedReceiver<Vec<Value>>>;
struct Inner {
writer: Mutex<Box<dyn AsyncWrite + Send + Unpin>>,
next_id: AtomicU64,
pending: StdMutex<Pending>,
subscribers: StdMutex<Subscribers>,
receivers: StdMutex<Receivers>,
closed: StdMutex<Option<String>>,
reader: StdMutex<Option<tokio::task::JoinHandle<()>>>,
}
impl Drop for Inner {
fn drop(&mut self) {
if let Some(task) = self.reader.get_mut().ok().and_then(Option::take) {
task.abort();
}
}
}
#[derive(Clone)]
pub struct BridgeClient {
inner: Arc<Inner>,
}
impl fmt::Debug for BridgeClient {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BridgeClient")
.field("closed", &self.close_reason())
.finish()
}
}
impl BridgeClient {
pub fn new<R, W>(reader: R, writer: W) -> Self
where
R: AsyncRead + Send + Unpin + 'static,
W: AsyncWrite + Send + Unpin + 'static,
{
let inner = Arc::new(Inner {
writer: Mutex::new(Box::new(writer)),
next_id: AtomicU64::new(1),
pending: StdMutex::new(HashMap::new()),
subscribers: StdMutex::new(HashMap::new()),
receivers: StdMutex::new(HashMap::new()),
closed: StdMutex::new(None),
reader: StdMutex::new(None),
});
let weak = Arc::downgrade(&inner);
let task = tokio::spawn(async move {
let mut lines = BufReader::new(reader).lines();
let reason = loop {
let line = match lines.next_line().await {
Ok(Some(line)) => line,
Ok(None) => break "the server closed its output".to_string(),
Err(err) => break format!("reading from the server failed: {err}"),
};
let Some(inner) = weak.upgrade() else {
return;
};
let client = BridgeClient { inner };
match serde_json::from_str::<Value>(&line) {
Ok(message) => client.dispatch(message),
Err(err) => {
tracing::debug!(target: "browser_commander::puppeteer", "ignored line {line:?}: {err}");
}
}
};
if let Some(inner) = weak.upgrade() {
BridgeClient { inner }.mark_closed(reason);
}
});
if let Ok(mut slot) = inner.reader.lock() {
*slot = Some(task);
}
Self { inner }
}
pub async fn request(&self, method: &str, params: Value) -> Result<Value, BridgeError> {
self.send_request(method, params, false).await
}
async fn send_request(
&self,
method: &str,
params: Value,
subscribe: bool,
) -> Result<Value, BridgeError> {
if let Some(reason) = self.close_reason() {
return Err(BridgeError::Closed(reason));
}
let id = self.inner.next_id.fetch_add(1, Ordering::Relaxed);
let (sender, receiver) = oneshot::channel();
self.lock_pending().insert(id, Waiter { sender, subscribe });
let mut line = serde_json::to_vec(&json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
"params": params,
}))?;
line.push(b'\n');
let written = {
let mut writer = self.inner.writer.lock().await;
match writer.write_all(&line).await {
Ok(()) => writer.flush().await,
Err(err) => Err(err),
}
};
if let Err(err) = written {
self.lock_pending().remove(&id);
return Err(err.into());
}
match receiver.await {
Ok(result) => result,
Err(_) => Err(BridgeError::Closed(
self.close_reason()
.unwrap_or_else(|| "the response was dropped".to_string()),
)),
}
}
pub async fn root(&self, name: &str) -> Result<RemoteHandle, BridgeError> {
let value = self.request("handle.root", json!({ "name": name })).await?;
RemoteHandle::from_wire(self, value)
}
pub fn close_reason(&self) -> Option<String> {
self.inner
.closed
.lock()
.ok()
.and_then(|reason| reason.clone())
}
pub async fn close_input(&self) {
let mut writer = self.inner.writer.lock().await;
let _ = writer.shutdown().await;
*writer = Box::new(tokio::io::sink());
drop(writer);
if let Ok(mut closed) = self.inner.closed.lock() {
closed.get_or_insert_with(|| "the bridge was closed".to_string());
}
}
fn lock_pending(&self) -> std::sync::MutexGuard<'_, Pending> {
self.inner
.pending
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn lock_subscribers(&self) -> std::sync::MutexGuard<'_, Subscribers> {
self.inner
.subscribers
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn lock_receivers(&self) -> std::sync::MutexGuard<'_, Receivers> {
self.inner
.receivers
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn mark_closed(&self, reason: String) {
if let Ok(mut closed) = self.inner.closed.lock() {
closed.get_or_insert(reason.clone());
}
let pending: Vec<_> = self.lock_pending().drain().collect();
for (_, waiter) in pending {
let _ = waiter.sender.send(Err(BridgeError::Closed(reason.clone())));
}
self.lock_subscribers().clear();
self.lock_receivers().clear();
}
fn dispatch(&self, message: Value) {
if let Some(id) = message.get("id").and_then(Value::as_u64) {
let Some(waiter) = self.lock_pending().remove(&id) else {
return;
};
let outcome = match message.get("error") {
Some(error) => Err(remote_error(error)),
None => Ok(message.get("result").cloned().unwrap_or(Value::Null)),
};
if let (true, Ok(result)) = (waiter.subscribe, &outcome) {
if let Some(subscription) = result.get("subscription").and_then(Value::as_str) {
let (sender, receiver) = mpsc::unbounded_channel();
self.lock_subscribers()
.insert(subscription.to_string(), sender);
self.lock_receivers()
.insert(subscription.to_string(), receiver);
}
}
let _ = waiter.sender.send(outcome);
return;
}
if message.get("method").and_then(Value::as_str) != Some("events.emit") {
return;
}
let params = message.get("params").cloned().unwrap_or(Value::Null);
let Some(subscription) = params.get("subscription").and_then(Value::as_str) else {
return;
};
let args = match params.get("args") {
Some(Value::Array(args)) => args.clone(),
_ => Vec::new(),
};
let mut subscribers = self.lock_subscribers();
if let Some(sender) = subscribers.get(subscription) {
if sender.send(args).is_err() {
subscribers.remove(subscription);
}
}
}
}
#[derive(Clone)]
pub struct RemoteHandle {
client: BridgeClient,
id: Arc<str>,
type_name: Arc<str>,
}
impl fmt::Debug for RemoteHandle {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RemoteHandle")
.field("id", &self.id)
.field("type", &self.type_name)
.finish()
}
}
impl PartialEq for RemoteHandle {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.client.inner, &other.client.inner) && self.id == other.id
}
}
impl RemoteHandle {
pub fn id(&self) -> &str {
&self.id
}
pub fn type_name(&self) -> &str {
&self.type_name
}
pub fn client(&self) -> &BridgeClient {
&self.client
}
pub fn to_wire(&self) -> Value {
json!({ "$handle": &*self.id })
}
pub async fn call<T: FromWire>(
&self,
method: &str,
args: Vec<Option<Value>>,
) -> Result<T, BridgeError> {
let value = self
.client
.request(
"handle.call",
json!({ "handle": &*self.id, "method": method, "args": encode_args(args) }),
)
.await?;
T::from_wire(&self.client, value)
}
pub async fn get<T: FromWire>(&self, property: &str) -> Result<T, BridgeError> {
let value = self
.client
.request(
"handle.get",
json!({ "handle": &*self.id, "property": property }),
)
.await?;
T::from_wire(&self.client, value)
}
pub async fn describe(&self) -> Result<Value, BridgeError> {
self.client
.request("handle.describe", json!({ "handle": &*self.id }))
.await
}
pub async fn release(&self) -> Result<(), BridgeError> {
self.client
.request("handle.dispose", json!({ "handle": &*self.id }))
.await
.map(|_| ())
}
pub async fn subscribe(&self, event: &str) -> Result<Subscription, BridgeError> {
let result = self
.client
.send_request(
"events.subscribe",
json!({ "handle": &*self.id, "event": event }),
true,
)
.await?;
let id = result
.get("subscription")
.and_then(Value::as_str)
.ok_or_else(|| BridgeError::decode("a subscription id", result.clone()))?
.to_string();
let receiver = self.client.lock_receivers().remove(&id).ok_or_else(|| {
BridgeError::Closed(
self.client
.close_reason()
.unwrap_or_else(|| "the subscription was dropped".to_string()),
)
})?;
Ok(Subscription {
client: self.client.clone(),
id,
receiver,
})
}
}
fn encode_args(mut args: Vec<Option<Value>>) -> Value {
while matches!(args.last(), Some(None)) {
args.pop();
}
Value::Array(
args.into_iter()
.map(|arg| arg.unwrap_or_else(|| json!({ "$undefined": true })))
.collect(),
)
}
#[derive(Debug)]
pub struct Subscription {
client: BridgeClient,
id: String,
receiver: mpsc::UnboundedReceiver<Vec<Value>>,
}
impl Subscription {
pub fn id(&self) -> &str {
&self.id
}
pub async fn next(&mut self) -> Option<Vec<Value>> {
self.receiver.recv().await
}
pub async fn close(self) -> Result<(), BridgeError> {
self.client.lock_subscribers().remove(&self.id);
self.client
.request("events.unsubscribe", json!({ "subscription": &self.id }))
.await
.map(|_| ())
}
}
impl Drop for Subscription {
fn drop(&mut self) {
self.client.lock_subscribers().remove(&self.id);
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct JsFunction(Value);
impl JsFunction {
pub fn source(source: impl Into<String>) -> Self {
Self(json!({ "$function": source.into() }))
}
pub fn text(text: impl Into<String>) -> Self {
Self(Value::String(text.into()))
}
pub fn to_wire(&self) -> Value {
self.0.clone()
}
}
impl From<&str> for JsFunction {
fn from(text: &str) -> Self {
Self::text(text)
}
}
impl From<String> for JsFunction {
fn from(text: String) -> Self {
Self::text(text)
}
}
pub fn binary(bytes: &[u8]) -> Value {
json!({ "$binary": base64::engine::general_purpose::STANDARD.encode(bytes) })
}
pub trait FromWire: Sized {
fn from_wire(client: &BridgeClient, value: Value) -> Result<Self, BridgeError>;
}
fn is_undefined(value: &Value) -> bool {
value.get("$undefined").is_some()
}
impl FromWire for () {
fn from_wire(_: &BridgeClient, _: Value) -> Result<Self, BridgeError> {
Ok(())
}
}
impl FromWire for Value {
fn from_wire(_: &BridgeClient, value: Value) -> Result<Self, BridgeError> {
Ok(value)
}
}
impl FromWire for String {
fn from_wire(_: &BridgeClient, value: Value) -> Result<Self, BridgeError> {
match value {
Value::String(text) => Ok(text),
other => Err(BridgeError::decode("a string", other)),
}
}
}
impl FromWire for f64 {
fn from_wire(_: &BridgeClient, value: Value) -> Result<Self, BridgeError> {
match &value {
Value::Number(number) => number
.as_f64()
.ok_or_else(|| BridgeError::decode("a number", value.clone())),
Value::Null => Ok(f64::NAN),
_ => Err(BridgeError::decode("a number", value)),
}
}
}
impl FromWire for bool {
fn from_wire(_: &BridgeClient, value: Value) -> Result<Self, BridgeError> {
value
.as_bool()
.ok_or_else(|| BridgeError::decode("a boolean", value))
}
}
impl FromWire for Vec<u8> {
fn from_wire(_: &BridgeClient, value: Value) -> Result<Self, BridgeError> {
value
.get("$binary")
.and_then(Value::as_str)
.and_then(|data| base64::engine::general_purpose::STANDARD.decode(data).ok())
.ok_or_else(|| BridgeError::decode("bytes", value))
}
}
impl<T: FromWire> FromWire for Option<T> {
fn from_wire(client: &BridgeClient, value: Value) -> Result<Self, BridgeError> {
if value.is_null() || is_undefined(&value) {
return Ok(None);
}
T::from_wire(client, value).map(Some)
}
}
impl<T: FromWire> FromWire for Vec<T> {
fn from_wire(client: &BridgeClient, value: Value) -> Result<Self, BridgeError> {
match value {
Value::Array(items) => items
.into_iter()
.map(|item| T::from_wire(client, item))
.collect(),
other => Err(BridgeError::decode("a list", other)),
}
}
}
impl FromWire for RemoteHandle {
fn from_wire(client: &BridgeClient, value: Value) -> Result<Self, BridgeError> {
let Some(id) = value.get("$handle").and_then(Value::as_str) else {
return Err(BridgeError::decode("a remote object", value));
};
Ok(RemoteHandle {
client: client.clone(),
id: Arc::from(id),
type_name: Arc::from(value.get("type").and_then(Value::as_str).unwrap_or("")),
})
}
}
pub trait Remote: Sized {
const TYPE: &'static str;
fn from_remote(remote: RemoteHandle) -> Self;
fn remote(&self) -> &RemoteHandle;
fn cast<T: Remote>(&self) -> T {
T::from_remote(self.remote().clone())
}
}
pub fn decode_handle<T: Remote>(client: &BridgeClient, value: Value) -> Result<T, BridgeError> {
RemoteHandle::from_wire(client, value).map(T::from_remote)
}
macro_rules! remote_type {
($(#[$meta:meta])* $name:ident) => {
$(#[$meta])*
#[derive(Clone, Debug, PartialEq)]
pub struct $name {
pub(crate) remote: $crate::puppeteer::bridge::RemoteHandle,
}
impl $crate::puppeteer::bridge::Remote for $name {
const TYPE: &'static str = stringify!($name);
fn from_remote(remote: $crate::puppeteer::bridge::RemoteHandle) -> Self {
Self { remote }
}
fn remote(&self) -> &$crate::puppeteer::bridge::RemoteHandle {
&self.remote
}
}
impl $crate::puppeteer::bridge::FromWire for $name {
fn from_wire(
client: &$crate::puppeteer::bridge::BridgeClient,
value: serde_json::Value,
) -> Result<Self, $crate::puppeteer::bridge::BridgeError> {
$crate::puppeteer::bridge::decode_handle(client, value)
}
}
};
}
pub(crate) use remote_type;
pub fn js_cli_path(working_dir: Option<&Path>) -> Result<PathBuf, BridgeError> {
let candidates = if let Some(configured) = std::env::var_os(JS_CLI_ENV) {
vec![PathBuf::from(configured)]
} else {
let base = working_dir
.map(Path::to_path_buf)
.or_else(|| std::env::current_dir().ok())
.unwrap_or_default();
vec![
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../js/bin/browser-commander.js"),
base.join("node_modules/browser-commander/bin/browser-commander.js"),
]
};
candidates
.into_iter()
.find(|path| path.is_file())
.ok_or_else(|| {
BridgeError::Unavailable(format!(
"the JavaScript CLI was not found; install the browser-commander npm package or set {JS_CLI_ENV}"
))
})
}
#[derive(Debug, Clone, Default)]
pub struct BridgeOptions {
pub node: Option<PathBuf>,
pub cli: Option<PathBuf>,
pub working_dir: Option<PathBuf>,
pub verbose: bool,
}
pub struct PuppeteerBridge {
client: BridgeClient,
runner: Mutex<Option<ProcessRunner>>,
pid: Option<u32>,
stderr: Arc<StdMutex<VecDeque<String>>>,
}
impl fmt::Debug for PuppeteerBridge {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PuppeteerBridge")
.field("pid", &self.pid)
.finish()
}
}
impl PuppeteerBridge {
pub async fn launch(options: BridgeOptions) -> Result<Self, BridgeError> {
let node = options
.node
.clone()
.or_else(|| std::env::var_os("BROWSER_COMMANDER_NODE").map(PathBuf::from))
.unwrap_or_else(|| PathBuf::from("node"));
let node = node.to_string_lossy().into_owned();
let cli = match options.cli.clone() {
Some(cli) => cli,
None => js_cli_path(options.working_dir.as_deref())?,
};
let cli = cli.to_string_lossy().into_owned();
let command = [node.as_str(), cli.as_str(), "serve", "--stdio"]
.into_iter()
.map(quote)
.collect::<Vec<_>>()
.join(" ");
let mut runner = ProcessRunner::new(
command,
RunOptions {
mirror: false,
capture: true,
stdin: StdinOption::Pipe,
cwd: options.working_dir.clone(),
shell_operators: false,
trace: false,
..RunOptions::default()
},
);
runner
.start()
.await
.map_err(|err| BridgeError::Unavailable(format!("failed to start {node}: {err}")))?;
let pid = runner.pid();
let (stdin, stdout, stderr) = {
let mut child = runner.child().ok_or_else(|| {
BridgeError::Unavailable("the server process did not start".to_string())
})?;
let native = child.native_mut();
(
native.stdin.take(),
native.stdout.take(),
native.stderr.take(),
)
};
let (Some(stdin), Some(stdout)) = (stdin, stdout) else {
if let Some(pid) = pid {
kill_owned_process_tree(pid);
}
return Err(BridgeError::Unavailable(
"the server's stdin and stdout were not piped".to_string(),
));
};
let stderr_lines = Arc::new(StdMutex::new(VecDeque::new()));
if let Some(stderr) = stderr {
let lines = Arc::clone(&stderr_lines);
let verbose = options.verbose;
tokio::spawn(async move {
let mut reader = BufReader::new(stderr).lines();
while let Ok(Some(line)) = reader.next_line().await {
if verbose {
eprintln!("[serve --stdio] {line}");
}
tracing::debug!(target: "browser_commander::puppeteer", "{line}");
if let Ok(mut lines) = lines.lock() {
if lines.len() == STDERR_LINES {
lines.pop_front();
}
lines.push_back(line);
}
}
});
}
Ok(Self {
client: BridgeClient::new(stdout, stdin),
runner: Mutex::new(Some(runner)),
pid,
stderr: stderr_lines,
})
}
pub fn client(&self) -> &BridgeClient {
&self.client
}
pub async fn puppeteer(&self) -> Result<super::api::PuppeteerNode, BridgeError> {
let remote = self.client.root("puppeteer").await?;
Ok(<super::api::PuppeteerNode as Remote>::from_remote(remote))
}
pub fn pid(&self) -> Option<u32> {
self.pid
}
pub fn stderr_tail(&self) -> Vec<String> {
self.stderr
.lock()
.map(|lines| lines.iter().cloned().collect())
.unwrap_or_default()
}
pub async fn close(&self) {
self.client.close_input().await;
let Some(mut runner) = self.runner.lock().await.take() else {
return;
};
let deadline = tokio::time::Instant::now() + EXIT_GRACE;
loop {
let exited = runner
.child()
.map(|mut child| matches!(child.native_mut().try_wait(), Ok(Some(_))))
.unwrap_or(true);
if exited || tokio::time::Instant::now() >= deadline {
break;
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
if let Some(pid) = self.pid {
kill_owned_process_tree(pid);
}
}
}
impl Drop for PuppeteerBridge {
fn drop(&mut self) {
let still_owned = self
.runner
.try_lock()
.map(|runner| runner.is_some())
.unwrap_or(true);
if still_owned {
if let Some(pid) = self.pid {
kill_owned_process_tree(pid);
}
}
}
}
pub fn options(fields: impl IntoIterator<Item = (&'static str, Option<Value>)>) -> Value {
Value::Object(
fields
.into_iter()
.filter_map(|(key, value)| value.map(|value| (key.to_string(), value)))
.collect::<Map<String, Value>>(),
)
}
#[cfg(test)]
#[path = "bridge_tests.rs"]
mod tests;