use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use serde_json::{Value, json};
use tokio::sync::broadcast::error::RecvError;
use tokio::time::{Instant, timeout_at};
use super::core::CdpCore;
use crate::{Error, Result};
pub struct ChromiumDebugger {
core: Arc<CdpCore>,
}
impl ChromiumDebugger {
pub(crate) fn new(core: Arc<CdpCore>) -> Self {
Self { core }
}
pub async fn enable(&self) -> Result<()> {
self.core.send("Debugger.enable", json!({})).await?;
Ok(())
}
pub async fn disable(&self) -> Result<()> {
let _ = self.core.send("Debugger.disable", json!({})).await;
Ok(())
}
pub async fn break_on_xhr(&self, url_substr: &str) -> Result<()> {
self.enable().await?;
self.core
.send("DOMDebugger.setXHRBreakpoint", json!({ "url": url_substr }))
.await?;
Ok(())
}
pub async fn break_on_xhr_any(&self) -> Result<()> {
self.break_on_xhr("").await
}
pub async fn clear_xhr_breakpoint(&self, url_substr: &str) -> Result<()> {
let _ = self
.core
.send(
"DOMDebugger.removeXHRBreakpoint",
json!({ "url": url_substr }),
)
.await;
Ok(())
}
pub async fn break_on_event(&self, event_name: &str) -> Result<()> {
self.enable().await?;
self.core
.send(
"DOMDebugger.setEventListenerBreakpoint",
json!({ "eventName": event_name }),
)
.await?;
Ok(())
}
pub async fn clear_event_breakpoint(&self, event_name: &str) -> Result<()> {
let _ = self
.core
.send(
"DOMDebugger.removeEventListenerBreakpoint",
json!({ "eventName": event_name }),
)
.await;
Ok(())
}
pub async fn break_at(
&self,
url_regex: &str,
line: u32,
column: Option<u32>,
condition: Option<&str>,
) -> Result<String> {
self.enable().await?;
let mut p = json!({ "lineNumber": line, "urlRegex": url_regex });
if let Some(c) = column {
p["columnNumber"] = json!(c);
}
if let Some(c) = condition.filter(|s| !s.is_empty()) {
p["condition"] = json!(c);
}
let r = self.core.send("Debugger.setBreakpointByUrl", p).await?;
Ok(r["breakpointId"].as_str().unwrap_or_default().to_string())
}
pub async fn remove_breakpoint(&self, breakpoint_id: &str) -> Result<()> {
let _ = self
.core
.send(
"Debugger.removeBreakpoint",
json!({ "breakpointId": breakpoint_id }),
)
.await;
Ok(())
}
pub async fn set_breakpoints_active(&self, active: bool) -> Result<()> {
self.core
.send("Debugger.setBreakpointsActive", json!({ "active": active }))
.await?;
Ok(())
}
pub async fn blackbox(&self, patterns: &[&str]) -> Result<()> {
self.enable().await?;
self.core
.send(
"Debugger.setBlackboxPatterns",
json!({ "patterns": patterns }),
)
.await?;
Ok(())
}
pub async fn pause(&self) -> Result<()> {
self.enable().await?;
self.core.send("Debugger.pause", json!({})).await?;
Ok(())
}
pub async fn set_skip_all_pauses(&self, skip: bool) -> Result<()> {
self.enable().await?;
self.core
.send("Debugger.setSkipAllPauses", json!({ "skip": skip }))
.await?;
Ok(())
}
pub async fn anti_anti_debug(&self) -> Result<()> {
self.core
.send(
"Page.addScriptToEvaluateOnNewDocument",
json!({ "source": ANTI_DEBUG_JS }),
)
.await?;
let _ = self.core.eval_value(ANTI_DEBUG_JS).await;
Ok(())
}
pub async fn wait_paused(&self, timeout: Option<Duration>) -> Result<Option<PausedStack>> {
let mut events = self.core.conn.subscribe();
let sid = self.core.session_id.clone();
let deadline = Instant::now() + timeout.unwrap_or_else(|| self.core.timeout());
let got = timeout_at(deadline, async {
loop {
match events.recv().await {
Ok(ev)
if ev.method == "Debugger.paused"
&& ev.session_id.as_deref() == Some(&sid) =>
{
return Some(ev.params);
}
Ok(_) => continue,
Err(RecvError::Lagged(_)) => continue,
Err(RecvError::Closed) => return None,
}
}
})
.await
.ok()
.flatten();
Ok(got.map(|params| PausedStack::from_event(self.core.clone(), ¶ms)))
}
pub async fn count_pauses(
&self,
window: Duration,
auto_resume: bool,
) -> Result<(usize, String)> {
let mut events = self.core.conn.subscribe();
let sid = self.core.session_id.clone();
let deadline = Instant::now() + window;
let mut count = 0usize;
let mut first_bt = String::new();
loop {
let remain = deadline.saturating_duration_since(Instant::now());
if remain.is_zero() {
break;
}
match tokio::time::timeout(remain, events.recv()).await {
Ok(Ok(ev))
if ev.method == "Debugger.paused" && ev.session_id.as_deref() == Some(&sid) =>
{
count += 1;
if first_bt.is_empty() {
first_bt = backtrace_from_params(&ev.params);
}
if auto_resume {
let _ =
self.core
.conn
.fire_session("Debugger.resume", json!({}), Some(&sid));
}
}
Ok(Ok(_)) => continue,
Ok(Err(RecvError::Lagged(_))) => continue,
Ok(Err(RecvError::Closed)) => break,
Err(_) => break,
}
}
Ok((count, first_bt))
}
#[allow(clippy::too_many_arguments)]
pub async fn break_at_then<F, Fut>(
&self,
url_regex: &str,
line: u32,
column: Option<u32>,
condition: Option<&str>,
trigger: F,
timeout: Option<Duration>,
) -> Result<Option<PausedStack>>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<()>>,
{
self.break_at(url_regex, line, column, condition).await?;
let mut events = self.core.conn.subscribe(); trigger().await?;
let sid = self.core.session_id.clone();
let deadline = Instant::now() + timeout.unwrap_or_else(|| self.core.timeout());
let got = timeout_at(deadline, async {
loop {
match events.recv().await {
Ok(ev)
if ev.method == "Debugger.paused"
&& ev.session_id.as_deref() == Some(&sid) =>
{
return Some(ev.params);
}
Ok(_) => continue,
Err(RecvError::Lagged(_)) => continue,
Err(RecvError::Closed) => return None,
}
}
})
.await
.ok()
.flatten();
Ok(got.map(|params| PausedStack::from_event(self.core.clone(), ¶ms)))
}
pub async fn break_on_xhr_then<F, Fut>(
&self,
url_substr: &str,
trigger: F,
timeout: Option<Duration>,
) -> Result<Option<PausedStack>>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<()>>,
{
self.break_on_xhr(url_substr).await?;
let mut events = self.core.conn.subscribe(); trigger().await?;
let sid = self.core.session_id.clone();
let deadline = Instant::now() + timeout.unwrap_or_else(|| self.core.timeout());
let got = timeout_at(deadline, async {
loop {
match events.recv().await {
Ok(ev)
if ev.method == "Debugger.paused"
&& ev.session_id.as_deref() == Some(&sid) =>
{
return Some(ev.params);
}
Ok(_) => continue,
Err(RecvError::Lagged(_)) => continue,
Err(RecvError::Closed) => return None,
}
}
})
.await
.ok()
.flatten();
Ok(got.map(|params| PausedStack::from_event(self.core.clone(), ¶ms)))
}
}
#[derive(Debug, Clone)]
pub struct CallFrame {
pub call_frame_id: String,
pub function_name: String,
pub url: String,
pub script_id: String,
pub line: u32,
pub column: u32,
local_scope_object_id: String,
}
pub struct PausedStack {
core: Arc<CdpCore>,
reason: String,
hit_breakpoints: Vec<String>,
frames: Vec<CallFrame>,
resumed: AtomicBool,
}
impl PausedStack {
fn from_event(core: Arc<CdpCore>, params: &Value) -> Self {
let frames = params["callFrames"]
.as_array()
.map(|a| a.iter().map(parse_call_frame).collect())
.unwrap_or_default();
let hit_breakpoints = params["hitBreakpoints"]
.as_array()
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
Self {
core,
reason: params["reason"].as_str().unwrap_or_default().to_string(),
hit_breakpoints,
frames,
resumed: AtomicBool::new(false),
}
}
pub fn reason(&self) -> &str {
&self.reason
}
pub fn hit_breakpoints(&self) -> &[String] {
&self.hit_breakpoints
}
pub fn frames(&self) -> &[CallFrame] {
&self.frames
}
pub fn backtrace(&self) -> String {
self.frames
.iter()
.enumerate()
.map(|(i, f)| format_frame(i, f))
.collect::<Vec<_>>()
.join("\n")
}
pub async fn eval(&self, frame_idx: usize, expression: &str) -> Result<Value> {
let frame = self
.frames
.get(frame_idx)
.ok_or_else(|| Error::Other(format!("调用栈无第 {frame_idx} 帧")))?;
let r = self
.core
.send(
"Debugger.evaluateOnCallFrame",
json!({
"callFrameId": frame.call_frame_id,
"expression": expression,
"returnByValue": true,
"silent": true,
}),
)
.await?;
if let Some(exc) = r.get("exceptionDetails") {
let msg = exc["exception"]["description"]
.as_str()
.or_else(|| exc["text"].as_str())
.unwrap_or("求值异常");
return Err(Error::Protocol(format!("evaluateOnCallFrame: {msg}")));
}
Ok(r["result"]["value"].clone())
}
pub async fn eval_str(&self, frame_idx: usize, expression: &str) -> Result<String> {
let v = self.eval(frame_idx, expression).await?;
Ok(match v {
Value::String(s) => s,
Value::Null => String::new(),
other => other.to_string(),
})
}
pub async fn expose_as_global(
&self,
frame_idx: usize,
global_name: &str,
expr: &str,
) -> Result<String> {
let js = build_expose_js(global_name, expr);
self.eval_str(frame_idx, &js).await
}
pub async fn locals(&self, frame_idx: usize) -> Result<Vec<(String, Value)>> {
let frame = self
.frames
.get(frame_idx)
.ok_or_else(|| Error::Other(format!("调用栈无第 {frame_idx} 帧")))?;
if frame.local_scope_object_id.is_empty() {
return Ok(Vec::new());
}
let r = self
.core
.send(
"Runtime.getProperties",
json!({
"objectId": frame.local_scope_object_id,
"ownProperties": true,
"generatePreview": false,
}),
)
.await?;
let mut out = Vec::new();
if let Some(list) = r["result"].as_array() {
for p in list {
let name = p["name"].as_str().unwrap_or_default().to_string();
if name.is_empty() {
continue;
}
out.push((name, remote_object_to_value(&p["value"])));
}
}
Ok(out)
}
pub async fn resume(self) -> Result<()> {
self.resumed.store(true, Ordering::Relaxed);
self.core.send("Debugger.resume", json!({})).await?;
Ok(())
}
pub async fn step_over(self) -> Result<Option<PausedStack>> {
self.step("Debugger.stepOver").await
}
pub async fn step_into(self) -> Result<Option<PausedStack>> {
self.step("Debugger.stepInto").await
}
pub async fn step_out(self) -> Result<Option<PausedStack>> {
self.step("Debugger.stepOut").await
}
async fn step(self, method: &str) -> Result<Option<PausedStack>> {
self.resumed.store(true, Ordering::Relaxed);
let mut events = self.core.conn.subscribe();
let sid = self.core.session_id.clone();
self.core.send(method, json!({})).await?;
let deadline = Instant::now() + self.core.timeout();
let got = timeout_at(deadline, async {
loop {
match events.recv().await {
Ok(ev)
if ev.method == "Debugger.paused"
&& ev.session_id.as_deref() == Some(&sid) =>
{
return Some(ev.params);
}
Ok(_) => continue,
Err(RecvError::Lagged(_)) => continue,
Err(RecvError::Closed) => return None,
}
}
})
.await
.ok()
.flatten();
Ok(got.map(|params| PausedStack::from_event(self.core.clone(), ¶ms)))
}
}
impl Drop for PausedStack {
fn drop(&mut self) {
if !self.resumed.load(Ordering::Relaxed) {
let _ = self.core.conn.fire_session(
"Debugger.resume",
json!({}),
Some(&self.core.session_id),
);
}
}
}
fn build_expose_js(global_name: &str, expr: &str) -> String {
let name = serde_json::to_string(global_name).unwrap_or_else(|_| "\"__oracle\"".to_string());
format!(
"(function(){{try{{window[{name}]=({expr});return typeof window[{name}];}}catch(e){{return 'ERR:'+e;}}}})()"
)
}
fn format_frame(i: usize, f: &CallFrame) -> String {
let name = if f.function_name.is_empty() {
"(anonymous)"
} else {
&f.function_name
};
format!("#{i} {name} @ {}:{}:{}", f.url, f.line, f.column)
}
fn backtrace_from_params(params: &Value) -> String {
params["callFrames"]
.as_array()
.map(|a| {
a.iter()
.enumerate()
.map(|(i, cf)| format_frame(i, &parse_call_frame(cf)))
.collect::<Vec<_>>()
.join("\n")
})
.unwrap_or_default()
}
fn parse_call_frame(cf: &Value) -> CallFrame {
let loc = &cf["location"];
let local_scope_object_id = cf["scopeChain"]
.as_array()
.and_then(|scopes| {
scopes
.iter()
.find(|s| s["type"].as_str() == Some("local"))
.and_then(|s| s["object"]["objectId"].as_str())
})
.unwrap_or_default()
.to_string();
CallFrame {
call_frame_id: cf["callFrameId"].as_str().unwrap_or_default().to_string(),
function_name: cf["functionName"].as_str().unwrap_or_default().to_string(),
url: cf["url"].as_str().unwrap_or_default().to_string(),
script_id: loc["scriptId"].as_str().unwrap_or_default().to_string(),
line: loc["lineNumber"].as_u64().unwrap_or(0) as u32,
column: loc["columnNumber"].as_u64().unwrap_or(0) as u32,
local_scope_object_id,
}
}
fn remote_object_to_value(ro: &Value) -> Value {
if let Some(v) = ro.get("value") {
if !v.is_null() {
return v.clone();
}
}
if ro["type"].as_str() == Some("undefined") {
return Value::String("undefined".into());
}
let ty = ro["subtype"]
.as_str()
.or_else(|| ro["type"].as_str())
.unwrap_or("object");
let desc = ro["description"]
.as_str()
.or_else(|| ro["className"].as_str())
.unwrap_or(ty);
json!({ "type": ty, "description": desc })
}
pub(crate) const ANTI_DEBUG_JS: &str = r#"(function(){
try{
var _si=window.setInterval, _st=window.setTimeout;
function clean(fn){
try{
if(typeof fn==='function'){
var s=Function.prototype.toString.call(fn);
if(/\bdebugger\b/.test(s)) return function(){};
} else if(typeof fn==='string' && /\bdebugger\b/.test(fn)){
return fn.replace(/\bdebugger\b/g,'');
}
}catch(e){}
return fn;
}
try{ window.setInterval=function(fn){ var a=Array.prototype.slice.call(arguments); a[0]=clean(fn); return _si.apply(this,a); }; }catch(e){}
try{ window.setTimeout=function(fn){ var a=Array.prototype.slice.call(arguments); a[0]=clean(fn); return _st.apply(this,a); }; }catch(e){}
try{
var _F=window.Function;
var FN=function(){
var a=Array.prototype.slice.call(arguments);
if(a.length){ var last=a[a.length-1]; if(typeof last==='string') a[a.length-1]=last.replace(/\bdebugger\b/g,''); }
return _F.apply(this,a);
};
FN.prototype=_F.prototype;
try{ FN.toString=function(){ return _F.toString(); }; }catch(e){}
window.Function=FN;
try{ Object.defineProperty(_F.prototype,'constructor',{value:FN,configurable:true,writable:true}); }catch(e){}
}catch(e){}
try{
var _eval=window.eval;
var EV=function(c){ if(typeof c==='string') c=c.replace(/\bdebugger\b/g,''); return _eval(c); };
try{ EV.toString=function(){ return _eval.toString(); }; }catch(e){}
window.eval=EV;
}catch(e){}
}catch(e){}
})();"#;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn anti_debug_js_targets_known_patterns() {
assert!(ANTI_DEBUG_JS.contains("setInterval"));
assert!(ANTI_DEBUG_JS.contains("setTimeout"));
assert!(ANTI_DEBUG_JS.contains("window.Function"));
assert!(ANTI_DEBUG_JS.contains("window.eval"));
assert!(ANTI_DEBUG_JS.contains("constructor"));
assert!(ANTI_DEBUG_JS.contains(r"\bdebugger\b"));
assert!(ANTI_DEBUG_JS.trim_start().starts_with("(function()"));
}
#[test]
fn build_expose_js_wraps_and_escapes() {
let js = build_expose_js("__ytNsig", "function(u){return (new g.g7(u,!0)).get('n')}");
assert!(js.contains("window[\"__ytNsig\"]=("));
assert!(js.contains("new g.g7(u,!0)"));
assert!(js.contains("return typeof window[\"__ytNsig\"]"));
assert!(js.contains("catch(e)") && js.contains("'ERR:'+e"));
let js2 = build_expose_js("a\"b", "1");
assert!(js2.contains("window[\"a\\\"b\"]=(1)"));
}
#[test]
fn parse_call_frame_extracts_fields_and_local_scope() {
let cf = json!({
"callFrameId": "cf1",
"functionName": "sign",
"url": "https://api.x.com/app.js",
"location": { "scriptId": "42", "lineNumber": 120, "columnNumber": 8 },
"scopeChain": [
{ "type": "local", "object": { "objectId": "obj-local" } },
{ "type": "closure", "object": { "objectId": "obj-closure" } }
]
});
let f = parse_call_frame(&cf);
assert_eq!(f.call_frame_id, "cf1");
assert_eq!(f.function_name, "sign");
assert_eq!(f.script_id, "42");
assert_eq!((f.line, f.column), (120, 8));
assert_eq!(f.local_scope_object_id, "obj-local");
}
#[test]
fn paused_stack_backtrace_and_anon() {
let params = json!({
"reason": "XHR",
"hitBreakpoints": ["1:0:https://x/app.js"],
"callFrames": [
{ "callFrameId":"a", "functionName":"", "url":"https://x/app.js",
"location": { "scriptId":"1", "lineNumber":10, "columnNumber":2 }, "scopeChain": [] },
{ "callFrameId":"b", "functionName":"send", "url":"https://x/app.js",
"location": { "scriptId":"1", "lineNumber":99, "columnNumber":4 }, "scopeChain": [] }
]
});
let frames: Vec<CallFrame> = params["callFrames"]
.as_array()
.unwrap()
.iter()
.map(parse_call_frame)
.collect();
assert_eq!(frames.len(), 2);
assert_eq!(frames[0].function_name, "");
assert_eq!(frames[1].function_name, "send");
assert_eq!(frames[1].line, 99);
}
#[test]
fn backtrace_from_params_formats_and_marks_anon() {
let params = json!({
"callFrames": [
{ "functionName":"", "url":"https://x/app.js",
"location": { "scriptId":"1", "lineNumber":10, "columnNumber":2 }, "scopeChain": [] },
{ "functionName":"sign", "url":"https://x/app.js",
"location": { "scriptId":"1", "lineNumber":99, "columnNumber":4 }, "scopeChain": [] }
]
});
let bt = backtrace_from_params(¶ms);
assert!(bt.contains("#0 (anonymous) @ https://x/app.js:10:2"));
assert!(bt.contains("#1 sign @ https://x/app.js:99:4"));
}
#[test]
fn remote_object_primitive_and_object() {
assert_eq!(
remote_object_to_value(&json!({ "type": "string", "value": "abc" })),
json!("abc")
);
assert_eq!(
remote_object_to_value(&json!({ "type": "number", "value": 42 })),
json!(42)
);
assert_eq!(
remote_object_to_value(&json!({ "type": "undefined" })),
json!("undefined")
);
let v = remote_object_to_value(
&json!({ "type": "object", "subtype": "array", "description": "Array(3)" }),
);
assert_eq!(v["type"], json!("array"));
assert_eq!(v["description"], json!("Array(3)"));
}
}