use std::sync::Arc; use obscura_browser::{BrowserContext, Page}; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; #[derive(Debug, Deserialize)] #[serde(tag = "cmd")] enum WorkerCommand { #[serde(rename = "navigate")] Navigate { url: String }, #[serde(rename = "evaluate")] Evaluate { expression: String }, #[serde(rename = "title")] Title, #[serde(rename = "dump_html")] DumpHtml, #[serde(rename = "dump_text")] DumpText, #[serde(rename = "shutdown")] Shutdown, } #[derive(Debug, Serialize)] struct WorkerResponse { ok: bool, #[serde(skip_serializing_if = "Option::is_none")] result: Option, #[serde(skip_serializing_if = "Option::is_none")] error: Option, } impl WorkerResponse { fn success(result: serde_json::Value) -> Self { WorkerResponse { ok: true, result: Some(result), error: None } } fn error(msg: String) -> Self { WorkerResponse { ok: false, result: None, error: Some(msg) } } } #[tokio::main(flavor = "current_thread")] async fn main() { tracing_subscriber::fmt() .with_env_filter("warn") .with_writer(std::io::stderr) .init(); let proxy = std::env::var("OBSCURA_PROXY") .ok() .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()); let stealth = std::env::var("OBSCURA_STEALTH") .map(|v| matches!(v.trim(), "1" | "true" | "yes" | "on")) .unwrap_or(false); let context = Arc::new(BrowserContext::with_options("worker".to_string(), proxy, stealth)); let mut page = Page::new("page-1".to_string(), context); let stdin = tokio::io::stdin(); let mut stdout = tokio::io::stdout(); let mut reader = BufReader::new(stdin); let mut line = String::new(); loop { line.clear(); match reader.read_line(&mut line).await { Ok(0) => break, Ok(_) => {} Err(e) => { eprintln!("Worker stdin error: {}", e); break; } } let trimmed = line.trim(); if trimmed.is_empty() { continue; } let cmd: WorkerCommand = match serde_json::from_str(trimmed) { Ok(c) => c, Err(e) => { let resp = WorkerResponse::error(format!("Invalid command: {}", e)); let mut out = serde_json::to_string(&resp).unwrap(); out.push('\n'); let _ = stdout.write_all(out.as_bytes()).await; let _ = stdout.flush().await; continue; } }; let resp = match cmd { WorkerCommand::Navigate { url } => { match page.navigate(&url).await { Ok(()) => WorkerResponse::success(serde_json::json!({ "title": page.title, "url": page.url_string(), })), Err(e) => WorkerResponse::error(e.to_string()), } } WorkerCommand::Evaluate { expression } => { let result = page.evaluate(&expression); WorkerResponse::success(result) } WorkerCommand::Title => { WorkerResponse::success(serde_json::json!(page.title)) } WorkerCommand::DumpHtml => { let html = page.with_dom(|dom| { if let Ok(Some(html_node)) = dom.query_selector("html") { dom.outer_html(html_node) } else { dom.inner_html(dom.document()) } }).unwrap_or_default(); WorkerResponse::success(serde_json::json!(html)) } WorkerCommand::DumpText => { let text = page.with_dom(|dom| { if let Ok(Some(body)) = dom.query_selector("body") { dom.text_content(body) } else { String::new() } }).unwrap_or_default(); WorkerResponse::success(serde_json::json!(text)) } WorkerCommand::Shutdown => { let resp = WorkerResponse::success(serde_json::json!("bye")); let mut out = serde_json::to_string(&resp).unwrap(); out.push('\n'); let _ = stdout.write_all(out.as_bytes()).await; let _ = stdout.flush().await; break; } }; let mut out = serde_json::to_string(&resp).unwrap(); out.push('\n'); let _ = stdout.write_all(out.as_bytes()).await; let _ = stdout.flush().await; } }