1use std::path::{Path, PathBuf};
8use std::process::Stdio;
9use std::sync::Arc;
10
11use async_trait::async_trait;
12use base64::Engine as _;
13use serde::Deserialize;
14use serde_json::{json, Value};
15use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
16use tokio::process::{Child, ChildStdin, ChildStdout, Command};
17use tokio::sync::Mutex;
18use tokio::task::JoinHandle;
19
20use crate::browser::launcher::LaunchOptions;
21use crate::core::engine::{ElementInfo, EngineAdapter, EngineError, EngineType, PdfOptions};
22
23const BRIDGE_SCRIPT: &str = include_str!("node_engine_bridge.js");
24
25pub struct NodeBridgePage {
27 engine: EngineType,
28 inner: Arc<Mutex<NodeBridgeProcess>>,
29 stderr_task: Arc<Mutex<Option<JoinHandle<()>>>>,
30}
31
32struct NodeBridgeProcess {
33 child: Child,
34 stdin: ChildStdin,
35 stdout: BufReader<ChildStdout>,
36 next_id: u64,
37}
38
39#[derive(Debug, Deserialize)]
40struct BridgeResponse {
41 id: u64,
42 ok: bool,
43 #[serde(default)]
44 result: Value,
45 #[serde(default)]
46 error: Option<String>,
47}
48
49impl NodeBridgePage {
50 pub(crate) async fn launch(
51 options: LaunchOptions,
52 user_data_dir: PathBuf,
53 ) -> Result<Self, anyhow::Error> {
54 let engine = options.engine;
55 if !matches!(engine, EngineType::Playwright | EngineType::Puppeteer) {
56 return Err(anyhow::anyhow!(
57 "Node bridge only supports playwright and puppeteer engines"
58 ));
59 }
60
61 let node = options
62 .node_executable
63 .clone()
64 .unwrap_or_else(|| PathBuf::from("node"));
65 let mut command = Command::new(node);
66 command
67 .arg("--input-type=module")
68 .arg("-e")
69 .arg(BRIDGE_SCRIPT)
70 .stdin(Stdio::piped())
71 .stdout(Stdio::piped())
72 .stderr(Stdio::piped());
73
74 if let Some(ref working_dir) = options.node_working_dir {
75 command.current_dir(working_dir);
76 }
77
78 let mut child = command
79 .spawn()
80 .map_err(|err| anyhow::anyhow!("failed to start Node.js bridge: {}", err))?;
81
82 let stdin = child
83 .stdin
84 .take()
85 .ok_or_else(|| anyhow::anyhow!("Node.js bridge stdin was not captured"))?;
86 let stdout = child
87 .stdout
88 .take()
89 .ok_or_else(|| anyhow::anyhow!("Node.js bridge stdout was not captured"))?;
90 let stderr = child.stderr.take();
91
92 let stderr_task = stderr.map(|stderr| {
93 tokio::spawn(async move {
94 let mut lines = BufReader::new(stderr).lines();
95 while let Ok(Some(line)) = lines.next_line().await {
96 tracing::debug!(target: "browser_commander::node_bridge", "{line}");
97 }
98 })
99 });
100
101 let page = Self {
102 engine,
103 inner: Arc::new(Mutex::new(NodeBridgeProcess {
104 child,
105 stdin,
106 stdout: BufReader::new(stdout),
107 next_id: 0,
108 })),
109 stderr_task: Arc::new(Mutex::new(stderr_task)),
110 };
111
112 page.request(
113 "launch",
114 json!({
115 "engine": engine.to_string(),
116 "userDataDir": path_to_string(&user_data_dir),
117 "headless": options.headless,
118 "slowMo": options.slow_mo,
119 "verbose": options.verbose,
120 "args": options.all_chrome_args(),
121 "colorScheme": options.color_scheme.as_ref().map(|cs| cs.as_str()),
122 "sandbox": options.sandbox,
123 }),
124 )
125 .await
126 .map_err(|err| anyhow::anyhow!("{}", err))?;
127
128 Ok(page)
129 }
130
131 pub async fn close(&self) -> Result<(), EngineError> {
133 let close_result = self.request("close", json!({})).await;
134 let mut inner = self.inner.lock().await;
135 let _ = inner.child.start_kill();
136 if let Some(task) = self.stderr_task.lock().await.take() {
137 task.abort();
138 }
139 close_result.map(|_| ())
140 }
141
142 async fn request(&self, method: &str, params: Value) -> Result<Value, EngineError> {
143 let mut inner = self.inner.lock().await;
144 inner.next_id = inner.next_id.saturating_add(1);
145 let id = inner.next_id;
146 let request = json!({
147 "id": id,
148 "method": method,
149 "params": params,
150 });
151
152 let mut encoded = serde_json::to_vec(&request).map_err(|err| {
153 EngineError::Browser(format!("bridge request encoding failed: {err}"))
154 })?;
155 encoded.push(b'\n');
156
157 inner
158 .stdin
159 .write_all(&encoded)
160 .await
161 .map_err(|err| EngineError::Browser(format!("bridge write failed: {err}")))?;
162 inner
163 .stdin
164 .flush()
165 .await
166 .map_err(|err| EngineError::Browser(format!("bridge flush failed: {err}")))?;
167
168 let mut line = String::new();
169 loop {
170 line.clear();
171 let bytes = inner
172 .stdout
173 .read_line(&mut line)
174 .await
175 .map_err(|err| EngineError::Browser(format!("bridge read failed: {err}")))?;
176 if bytes == 0 {
177 return Err(EngineError::Browser(
178 "Node.js bridge exited before responding".to_string(),
179 ));
180 }
181
182 let response: BridgeResponse =
183 serde_json::from_str(line.trim_end()).map_err(|err| {
184 EngineError::Browser(format!(
185 "bridge returned invalid JSON: {err}; line={}",
186 line.trim_end()
187 ))
188 })?;
189
190 if response.id != id {
191 tracing::debug!(
192 expected_id = id,
193 actual_id = response.id,
194 "ignoring out-of-order bridge response"
195 );
196 continue;
197 }
198
199 if response.ok {
200 return Ok(response.result);
201 }
202
203 return Err(EngineError::Browser(
204 response
205 .error
206 .unwrap_or_else(|| "Node.js bridge command failed".to_string()),
207 ));
208 }
209 }
210
211 async fn string_request(&self, method: &str, params: Value) -> Result<String, EngineError> {
212 let value = self.request(method, params).await?;
213 Ok(value.as_str().unwrap_or_default().to_string())
214 }
215
216 async fn bool_request(&self, method: &str, params: Value) -> Result<bool, EngineError> {
217 let value = self.request(method, params).await?;
218 Ok(value.as_bool().unwrap_or(false))
219 }
220}
221
222impl std::fmt::Debug for NodeBridgePage {
223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
224 f.debug_struct("NodeBridgePage")
225 .field("engine", &self.engine)
226 .finish()
227 }
228}
229
230impl Drop for NodeBridgePage {
231 fn drop(&mut self) {
232 if let Ok(mut inner) = self.inner.try_lock() {
233 let _ = inner.child.start_kill();
234 }
235 if let Ok(mut task) = self.stderr_task.try_lock() {
236 if let Some(handle) = task.take() {
237 handle.abort();
238 }
239 }
240 }
241}
242
243#[async_trait]
244impl EngineAdapter for NodeBridgePage {
245 fn engine_type(&self) -> EngineType {
246 self.engine
247 }
248
249 async fn url(&self) -> Result<String, EngineError> {
250 self.string_request("url", json!({})).await
251 }
252
253 async fn goto(&self, url: &str) -> Result<(), EngineError> {
254 self.request("goto", json!({ "url": url })).await?;
255 Ok(())
256 }
257
258 async fn query_selector(&self, selector: &str) -> Result<Option<ElementInfo>, EngineError> {
259 let value = self
260 .request("querySelector", json!({ "selector": selector }))
261 .await?;
262 element_info_from_value(&value)
263 }
264
265 async fn query_selector_all(&self, selector: &str) -> Result<Vec<ElementInfo>, EngineError> {
266 let value = self
267 .request("querySelectorAll", json!({ "selector": selector }))
268 .await?;
269 let Some(items) = value.as_array() else {
270 return Ok(Vec::new());
271 };
272 let mut infos = Vec::with_capacity(items.len());
273 for item in items {
274 if let Some(info) = element_info_from_value(item)? {
275 infos.push(info);
276 }
277 }
278 Ok(infos)
279 }
280
281 async fn count(&self, selector: &str) -> Result<usize, EngineError> {
282 let value = self
283 .request("count", json!({ "selector": selector }))
284 .await?;
285 Ok(value.as_u64().unwrap_or(0) as usize)
286 }
287
288 async fn click(&self, selector: &str) -> Result<(), EngineError> {
289 self.request("click", json!({ "selector": selector }))
290 .await?;
291 Ok(())
292 }
293
294 async fn fill(&self, selector: &str, text: &str) -> Result<(), EngineError> {
295 self.request("fill", json!({ "selector": selector, "text": text }))
296 .await?;
297 Ok(())
298 }
299
300 async fn type_text(&self, selector: &str, text: &str) -> Result<(), EngineError> {
301 self.request("typeText", json!({ "selector": selector, "text": text }))
302 .await?;
303 Ok(())
304 }
305
306 async fn text_content(&self, selector: &str) -> Result<Option<String>, EngineError> {
307 let value = self
308 .request("textContent", json!({ "selector": selector }))
309 .await?;
310 Ok(value.as_str().map(ToString::to_string))
311 }
312
313 async fn input_value(&self, selector: &str) -> Result<Option<String>, EngineError> {
314 let value = self
315 .request("inputValue", json!({ "selector": selector }))
316 .await?;
317 Ok(value.as_str().map(ToString::to_string))
318 }
319
320 async fn get_attribute(
321 &self,
322 selector: &str,
323 attribute: &str,
324 ) -> Result<Option<String>, EngineError> {
325 let value = self
326 .request(
327 "getAttribute",
328 json!({ "selector": selector, "attribute": attribute }),
329 )
330 .await?;
331 Ok(value.as_str().map(ToString::to_string))
332 }
333
334 async fn is_visible(&self, selector: &str) -> Result<bool, EngineError> {
335 self.bool_request("isVisible", json!({ "selector": selector }))
336 .await
337 }
338
339 async fn is_enabled(&self, selector: &str) -> Result<bool, EngineError> {
340 self.bool_request("isEnabled", json!({ "selector": selector }))
341 .await
342 }
343
344 async fn wait_for_selector(&self, selector: &str, timeout_ms: u64) -> Result<(), EngineError> {
345 self.request(
346 "waitForSelector",
347 json!({ "selector": selector, "timeoutMs": timeout_ms }),
348 )
349 .await?;
350 Ok(())
351 }
352
353 async fn scroll_into_view(&self, selector: &str) -> Result<(), EngineError> {
354 self.request("scrollIntoView", json!({ "selector": selector }))
355 .await?;
356 Ok(())
357 }
358
359 async fn evaluate(&self, script: &str) -> Result<Value, EngineError> {
360 self.request("evaluate", json!({ "script": script })).await
361 }
362
363 async fn screenshot(&self) -> Result<Vec<u8>, EngineError> {
364 decode_base64(self.string_request("screenshot", json!({})).await?)
365 }
366
367 async fn pdf(&self, options: PdfOptions) -> Result<Vec<u8>, EngineError> {
368 let pdf_options = json!({
369 "format": options.format,
370 "printBackground": options.print_background,
371 "margin": {
372 "top": options.margin_top,
373 "right": options.margin_right,
374 "bottom": options.margin_bottom,
375 "left": options.margin_left,
376 },
377 "scale": options.scale,
378 "path": options.path,
379 });
380 let encoded = self
381 .request("pdf", pdf_options)
382 .await?
383 .as_str()
384 .unwrap_or_default()
385 .to_string();
386 decode_base64(encoded)
387 }
388
389 async fn bring_to_front(&self) -> Result<(), EngineError> {
390 self.request("bringToFront", json!({})).await?;
391 Ok(())
392 }
393
394 async fn wait_for_navigation(&self, timeout_ms: u64) -> Result<(), EngineError> {
395 self.request("waitForNavigation", json!({ "timeoutMs": timeout_ms }))
396 .await?;
397 Ok(())
398 }
399
400 async fn keyboard_press(&self, key: &str) -> Result<(), EngineError> {
401 self.request("keyboardPress", json!({ "key": key })).await?;
402 Ok(())
403 }
404
405 async fn keyboard_type(&self, text: &str) -> Result<(), EngineError> {
406 self.request("keyboardType", json!({ "text": text }))
407 .await?;
408 Ok(())
409 }
410
411 async fn keyboard_down(&self, key: &str) -> Result<(), EngineError> {
412 self.request("keyboardDown", json!({ "key": key })).await?;
413 Ok(())
414 }
415
416 async fn keyboard_up(&self, key: &str) -> Result<(), EngineError> {
417 self.request("keyboardUp", json!({ "key": key })).await?;
418 Ok(())
419 }
420}
421
422fn path_to_string(path: &Path) -> String {
423 path.to_string_lossy().into_owned()
424}
425
426fn decode_base64(value: impl AsRef<str>) -> Result<Vec<u8>, EngineError> {
427 base64::engine::general_purpose::STANDARD
428 .decode(value.as_ref())
429 .map_err(|err| EngineError::Browser(format!("bridge returned invalid base64: {err}")))
430}
431
432fn element_info_from_value(value: &Value) -> Result<Option<ElementInfo>, EngineError> {
433 if value.is_null() {
434 return Ok(None);
435 }
436
437 let bounding_box = value.get("boundingBox").and_then(|box_value| {
438 let items = box_value.as_array()?;
439 if items.len() != 4 {
440 return None;
441 }
442 Some((
443 items[0].as_f64().unwrap_or_default(),
444 items[1].as_f64().unwrap_or_default(),
445 items[2].as_f64().unwrap_or_default(),
446 items[3].as_f64().unwrap_or_default(),
447 ))
448 });
449
450 Ok(Some(ElementInfo {
451 tag_name: value
452 .get("tagName")
453 .and_then(Value::as_str)
454 .unwrap_or("UNKNOWN")
455 .to_string(),
456 text_content: value
457 .get("textContent")
458 .and_then(Value::as_str)
459 .map(ToString::to_string),
460 is_visible: value
461 .get("isVisible")
462 .and_then(Value::as_bool)
463 .unwrap_or(false),
464 is_enabled: value
465 .get("isEnabled")
466 .and_then(Value::as_bool)
467 .unwrap_or(true),
468 bounding_box,
469 }))
470}