use std::cell::RefCell;
use std::rc::Rc;
use serde_json::json;
use crate::inspector::cdp::{CdpDispatcher, DispatchOutcome};
use crate::interpreter::GlobalEnv;
pub struct InProcessInspectorSession {
id: u32,
dispatcher: CdpDispatcher,
cached: Option<Vec<u8>>,
}
impl InProcessInspectorSession {
fn new(id: u32, globals: Rc<RefCell<GlobalEnv>>) -> Self {
Self {
id,
dispatcher: CdpDispatcher::with_globals(globals),
cached: None,
}
}
pub fn id(&self) -> u32 {
self.id
}
pub fn dispatch_json(&mut self, text: &str) -> DispatchOutcome {
self.cached = None;
self.dispatcher.dispatch_json(text)
}
pub fn dispatch_parse_error(&mut self, message: String) -> DispatchOutcome {
self.cached = None;
self.dispatcher.push_parse_error(message);
DispatchOutcome::ParseError
}
pub fn pending_count(&self) -> usize {
self.dispatcher.pending_count()
}
pub fn take_next_bytes(&mut self) -> Option<&[u8]> {
let next = self.dispatcher.take_next()?;
self.cached = Some(next.into_bytes());
self.cached.as_deref()
}
pub fn debugger_enabled(&self) -> bool {
self.dispatcher.debugger_enabled()
}
pub(crate) fn dispatcher_mut(&mut self) -> &mut CdpDispatcher {
&mut self.dispatcher
}
}
#[derive(Debug, Clone)]
pub struct RegisteredScript {
pub id: u32,
pub source: String,
}
pub struct InProcessInspector {
globals: Rc<RefCell<GlobalEnv>>,
sessions: Vec<Box<InProcessInspectorSession>>,
scripts: Vec<RegisteredScript>,
next_script_id: u32,
}
impl InProcessInspector {
pub fn new(globals: Rc<RefCell<GlobalEnv>>) -> Self {
Self {
globals,
sessions: Vec::new(),
scripts: Vec::new(),
next_script_id: 1,
}
}
pub fn connect(&mut self, session_id: u32) -> &mut InProcessInspectorSession {
let session = Box::new(InProcessInspectorSession::new(
session_id,
Rc::clone(&self.globals),
));
self.sessions.push(session);
self.sessions.last_mut().expect("just pushed").as_mut()
}
pub fn disconnect(&mut self, session: *const InProcessInspectorSession) -> bool {
if let Some(idx) = self
.sessions
.iter()
.position(|s| s.as_ref() as *const _ == session)
{
self.sessions.remove(idx);
true
} else {
false
}
}
pub fn session_count(&self) -> usize {
self.sessions.len()
}
pub fn session_by_id_mut(&mut self, id: u32) -> Option<&mut InProcessInspectorSession> {
self.sessions
.iter_mut()
.find(|s| s.id() == id)
.map(|s| s.as_mut())
}
pub fn register_script(&mut self, source: String) -> u32 {
let id = self.next_script_id;
let Some(next_script_id) = self.next_script_id.checked_add(1) else {
return 0;
};
self.next_script_id = next_script_id;
let line_count = source.lines().count().max(1) as u32;
let last_line_columns = source
.lines()
.last()
.map(|s| s.chars().count() as u32)
.unwrap_or(0);
self.scripts.push(RegisteredScript {
id,
source: source.clone(),
});
let params = json!({
"scriptId": id.to_string(),
"url": "",
"startLine": 0,
"startColumn": 0,
"endLine": line_count.saturating_sub(1),
"endColumn": last_line_columns,
"executionContextId": 1,
"hash": "",
});
let event = json!({
"method": "Debugger.scriptParsed",
"params": params,
});
let serialised = event.to_string();
for session in self.sessions.iter_mut() {
if session.debugger_enabled() {
session.dispatcher_mut().push_raw(serialised.clone());
}
}
id
}
pub fn scripts(&self) -> &[RegisteredScript] {
&self.scripts
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
fn new_inspector() -> InProcessInspector {
InProcessInspector::new(Rc::new(RefCell::new(GlobalEnv::new())))
}
fn drain_to_strings(session: &mut InProcessInspectorSession) -> Vec<String> {
let mut out = Vec::new();
while let Some(bytes) = session.take_next_bytes() {
out.push(std::str::from_utf8(bytes).unwrap().to_string());
}
out
}
#[test]
fn runtime_enable_emits_context_created_then_ack() {
let mut inspector = new_inspector();
let session = inspector.connect(1);
assert_eq!(
session.dispatch_json(r#"{"id":7,"method":"Runtime.enable","params":{}}"#),
DispatchOutcome::Ok
);
assert_eq!(session.pending_count(), 2);
let msgs = drain_to_strings(session);
assert_eq!(msgs.len(), 2);
let event: Value = serde_json::from_str(&msgs[0]).unwrap();
assert_eq!(event["method"], "Runtime.executionContextCreated");
let ack: Value = serde_json::from_str(&msgs[1]).unwrap();
assert_eq!(ack["id"], 7u64);
assert!(ack.get("error").is_none(), "ack should not carry error");
}
#[test]
fn runtime_evaluate_basic_response() {
let mut inspector = new_inspector();
let session = inspector.connect(2);
let outcome = session
.dispatch_json(r#"{"id":1,"method":"Runtime.evaluate","params":{"expression":"2+3"}}"#);
assert_eq!(outcome, DispatchOutcome::Ok);
let msgs = drain_to_strings(session);
assert_eq!(msgs.len(), 1);
let resp: Value = serde_json::from_str(&msgs[0]).unwrap();
assert_eq!(resp["id"], 1u64);
assert_eq!(resp["result"]["result"]["type"], "number");
assert_eq!(resp["result"]["result"]["value"], 5);
}
#[test]
fn unknown_method_returns_protocol_error_with_ok_outcome() {
let mut inspector = new_inspector();
let session = inspector.connect(3);
let outcome = session.dispatch_json(r#"{"id":11,"method":"NoSuch.method","params":{}}"#);
assert_eq!(outcome, DispatchOutcome::Ok);
let msgs = drain_to_strings(session);
assert_eq!(msgs.len(), 1);
let resp: Value = serde_json::from_str(&msgs[0]).unwrap();
assert_eq!(resp["id"], 11u64);
assert!(resp.get("error").is_some(), "should carry error");
assert!(
resp["error"]["message"]
.as_str()
.unwrap_or("")
.contains("NoSuch.method")
);
}
#[test]
fn malformed_json_returns_parse_error_outcome_and_pushes_response() {
let mut inspector = new_inspector();
let session = inspector.connect(4);
let outcome = session.dispatch_json("not-json");
assert_eq!(outcome, DispatchOutcome::ParseError);
let msgs = drain_to_strings(session);
assert_eq!(msgs.len(), 1);
let resp: Value = serde_json::from_str(&msgs[0]).unwrap();
assert!(resp.get("error").is_some(), "parse error response missing");
assert_eq!(resp["error"]["code"], -32700);
}
#[test]
fn take_next_bytes_invalidates_on_next_call() {
let mut inspector = new_inspector();
let session = inspector.connect(5);
session.dispatch_json(r#"{"id":1,"method":"Runtime.enable","params":{}}"#);
assert_eq!(session.pending_count(), 2);
let _first = session.take_next_bytes().expect("event").to_vec();
let _second = session.take_next_bytes().expect("ack").to_vec();
assert!(session.take_next_bytes().is_none());
}
#[test]
fn register_script_assigns_monotonic_ids_and_fans_out_to_debugger_enabled_sessions() {
let mut inspector = new_inspector();
{
let s1 = inspector.connect(10);
assert_eq!(
s1.dispatch_json(r#"{"id":1,"method":"Debugger.enable","params":{}}"#),
DispatchOutcome::Ok
);
drain_to_strings(s1);
}
{
let _s2 = inspector.connect(11);
}
let id_a = inspector.register_script("var a = 1;\nvar b = 2;\n".to_string());
let id_b = inspector.register_script("// second".to_string());
assert_eq!(id_a, 1);
assert_eq!(id_b, 2);
assert_eq!(inspector.scripts().len(), 2);
let s1 = inspector.session_by_id_mut(10).expect("s1");
assert_eq!(s1.pending_count(), 2);
let msgs = drain_to_strings(s1);
let first: Value = serde_json::from_str(&msgs[0]).unwrap();
assert_eq!(first["method"], "Debugger.scriptParsed");
assert_eq!(first["params"]["scriptId"], "1");
let second: Value = serde_json::from_str(&msgs[1]).unwrap();
assert_eq!(second["params"]["scriptId"], "2");
let s2 = inspector.session_by_id_mut(11).expect("s2");
assert_eq!(
s2.pending_count(),
0,
"session without Debugger.enable must not receive scriptParsed events"
);
}
#[test]
fn register_script_returns_zero_instead_of_wrapping_ids() {
let mut inspector = new_inspector();
inspector.next_script_id = u32::MAX;
let id = inspector.register_script("let overflow = true;".to_string());
assert_eq!(id, 0);
assert!(inspector.scripts().is_empty());
}
#[test]
fn disconnect_drops_session() {
let mut inspector = new_inspector();
let s1_ptr: *const InProcessInspectorSession = inspector.connect(20);
let _s2 = inspector.connect(21);
assert_eq!(inspector.session_count(), 2);
assert!(inspector.disconnect(s1_ptr));
assert_eq!(inspector.session_count(), 1);
assert!(
!inspector.disconnect(s1_ptr),
"double-disconnect is a no-op"
);
}
}