///|
/// CDP Protocol Layer
///
/// JSON-RPC message handling for Chrome DevTools Protocol.
/// Handles parsing, dispatching, and response formatting.
///|
/// CDP Request message
pub struct CdpRequest {
id : Int?
request_method : String
params : Json?
session_id : String?
}
///|
/// CDP Response message
pub struct CdpResponse {
id : Int?
result : Json?
error : CdpResponseError?
session_id : String?
}
///|
/// CDP Response error
pub struct CdpResponseError {
code : Int
message : String
}
///|
/// Create new CDP response
pub fn CdpResponse::new(
id : Int?,
result : Json?,
error : CdpResponseError?,
session_id : String?,
) -> CdpResponse {
{ id, result, error, session_id }
}
///|
/// CDP Event message (no id)
pub struct CdpEvent {
event_method : String
params : Json?
session_id : String?
}
///|
/// Create new CDP event
pub fn CdpEvent::new(
event_method : String,
params : Json?,
session_id : String?,
) -> CdpEvent {
{ event_method, params, session_id }
}
///|
/// Message types that can be sent to clients
pub enum CdpOutMessage {
Response(CdpResponse)
Event(CdpEvent)
}
///|
/// Create Response message
pub fn CdpOutMessage::response(resp : CdpResponse) -> CdpOutMessage {
Response(resp)
}
///|
/// Create Event message
pub fn CdpOutMessage::event(evt : CdpEvent) -> CdpOutMessage {
Event(evt)
}
///|
/// CDP Protocol handler
pub struct CdpProtocol {
/// Target domain
target : TargetDomain
/// Network manager
network : @scheduler.NetworkManager
/// Outgoing message buffer
outbox : Array[CdpOutMessage]
/// Enable lifecycle events
mut lifecycle_events : Bool
}
///|
/// Create new protocol handler
pub fn CdpProtocol::new() -> CdpProtocol {
{
target: TargetDomain::new(),
network: @scheduler.NetworkManager::new(),
outbox: [],
lifecycle_events: false,
}
}
///|
/// Process incoming JSON message
pub fn CdpProtocol::process_message(
self : CdpProtocol,
json_str : String,
) -> Result[Unit, String] {
// Parse JSON
let json = @json.parse(json_str) catch { _ => return Err("Invalid JSON") }
// Extract fields
let id = get_int_field(json, "id")
let request_method = match get_string_field(json, "method") {
Some(m) => m
None => return Err("Missing method field")
}
let params = get_field(json, "params")
let session_id = get_string_field(json, "sessionId")
// Create request
let request : CdpRequest = { id, request_method, params, session_id }
// Dispatch
self.dispatch(request)
}
///|
/// Dispatch request to appropriate domain
fn CdpProtocol::dispatch(
self : CdpProtocol,
request : CdpRequest,
) -> Result[Unit, String] {
// Parse domain.action from method
let parts = split_method(request.request_method)
let (domain, action) = match parts {
Some(p) => p
None => return Err("Invalid method format")
}
// Dispatch to domain
match domain {
"Target" => self.dispatch_target(request, action)
"DOM" => self.dispatch_dom(request, action)
"Page" => self.dispatch_page(request, action)
"Input" => self.dispatch_input(request, action)
"Runtime" => self.dispatch_runtime(request, action)
"Browser" => self.dispatch_browser(request, action)
"Network" => self.dispatch_network(request, action)
"Emulation" => self.dispatch_emulation(request, action)
"Security" => self.dispatch_security(request, action)
"Performance" => self.dispatch_performance(request, action)
"Log" => self.dispatch_log(request, action)
_ => {
self.send_error(
request.id,
request.session_id,
-32601,
"Unknown domain: " + domain,
)
Ok(())
}
}
}
///|
/// Take all pending outgoing messages
pub fn CdpProtocol::take_messages(self : CdpProtocol) -> Array[CdpOutMessage] {
let messages = self.outbox.copy()
self.outbox.clear()
messages
}
///|
/// Serialize outgoing message to JSON
pub fn CdpOutMessage::to_json(self : CdpOutMessage) -> String {
match self {
Response(resp) => response_to_json(resp)
Event(evt) => event_to_json(evt)
}
}
// =============================================================================
// Domain Dispatchers
// =============================================================================
///|
fn CdpProtocol::dispatch_target(
self : CdpProtocol,
request : CdpRequest,
action : String,
) -> Result[Unit, String] {
match action {
"createBrowserContext" =>
match self.target.create_browser_context() {
Ok(id) =>
self.send_result(
request.id,
request.session_id,
Some(make_object({ "browserContextId": Json::string(id) })),
)
Err(e) =>
self.send_error(request.id, request.session_id, -32000, e.message)
}
"disposeBrowserContext" => {
let context_id = get_param_string(request.params, "browserContextId")
match context_id {
Some(id) =>
match self.target.dispose_browser_context(id) {
Ok(_) => self.send_result(request.id, request.session_id, None)
Err(e) =>
self.send_error(request.id, request.session_id, -32602, e.message)
}
None =>
self.send_error(
request.id,
request.session_id,
-32602,
"Missing browserContextId",
)
}
}
"getBrowserContexts" => {
let ids = self.target.get_browser_contexts()
let json_ids = ids.map(fn(id) { Json::string(id) })
self.send_result(
request.id,
request.session_id,
Some(make_object({ "browserContextIds": Json::array(json_ids) })),
)
}
"createTarget" => {
let url = get_param_string(request.params, "url").unwrap_or("about:blank")
let browser_context_id = get_param_string(
request.params,
"browserContextId",
)
match self.target.create_target(url, browser_context_id) {
Ok(target_id) => {
// Send targetCreated event if discover targets enabled
if self.target.discover_targets {
self.send_event(
"Target.targetCreated",
request.session_id,
Some(
make_object({
"targetInfo": target_info_to_json({
target_id,
target_type: "page",
title: "about:blank",
url,
attached: self.target.is_auto_attach(),
browser_context_id,
}),
}),
),
)
}
// Send attachedToTarget event if auto-attach is enabled
if self.target.is_auto_attach() {
// Get session ID from the context
let session_id = match self.target.get_active_context() {
Some(ctx) =>
match ctx.get_session_id() {
Some(sid) => sid
None => "SID-" + target_id
}
None => "SID-" + target_id
}
self.send_event(
"Target.attachedToTarget",
None,
Some(
make_object({
"sessionId": Json::string(session_id),
"targetInfo": target_info_to_json({
target_id,
target_type: "page",
title: "about:blank",
url,
attached: true,
browser_context_id,
}),
"waitingForDebugger": Json::boolean(true),
}),
),
)
}
self.send_result(
request.id,
request.session_id,
Some(make_object({ "targetId": Json::string(target_id) })),
)
}
Err(e) =>
self.send_error(request.id, request.session_id, -32000, e.message)
}
}
"closeTarget" => {
let target_id = get_param_string(request.params, "targetId")
match target_id {
Some(tid) =>
match self.target.close_target(tid) {
Ok(_) => {
// Get session ID before closing
let session_id = match self.target.get_active_context() {
Some(ctx) =>
match ctx.get_session_id() {
Some(sid) => sid
None => "SID-1"
}
None => "SID-1"
}
self.send_result(
request.id,
request.session_id,
Some(make_object({ "success": Json::boolean(true) })),
)
// Send detachedFromTarget event (for flatten mode)
self.send_event(
"Target.detachedFromTarget",
None,
Some(
make_object({
"sessionId": Json::string(session_id),
"targetId": Json::string(tid),
}),
),
)
// Send targetDestroyed event
self.send_event(
"Target.targetDestroyed",
None,
Some(make_object({ "targetId": Json::string(tid) })),
)
}
Err(e) =>
self.send_error(request.id, request.session_id, -32000, e.message)
}
None =>
self.send_error(
request.id,
request.session_id,
-32602,
"Missing targetId",
)
}
}
"attachToTarget" => {
let target_id = get_param_string(request.params, "targetId")
match target_id {
Some(tid) =>
match self.target.attach_to_target(tid) {
Ok(session_id) => {
// Send attachedToTarget event
self.send_event(
"Target.attachedToTarget",
None,
Some(
make_object({
"sessionId": Json::string(session_id),
"targetInfo": target_info_to_json({
target_id: tid,
target_type: "page",
title: "about:blank",
url: "about:blank",
attached: true,
browser_context_id: None,
}),
"waitingForDebugger": Json::boolean(false),
}),
),
)
self.send_result(
request.id,
request.session_id,
Some(make_object({ "sessionId": Json::string(session_id) })),
)
}
Err(e) =>
self.send_error(request.id, request.session_id, -32000, e.message)
}
None =>
self.send_error(
request.id,
request.session_id,
-32602,
"Missing targetId",
)
}
}
"detachFromTarget" => {
let session_id = get_param_string(request.params, "sessionId")
let target_id = get_param_string(request.params, "targetId")
match self.target.detach_from_target(session_id, target_id) {
Ok(_) => self.send_result(request.id, request.session_id, None)
Err(e) =>
self.send_error(request.id, request.session_id, -32000, e.message)
}
}
"getTargets" => {
let targets = self.target.get_targets()
let json_targets = targets.map(fn(t) { target_info_to_json(t) })
self.send_result(
request.id,
request.session_id,
Some(make_object({ "targetInfos": Json::array(json_targets) })),
)
}
"getTargetInfo" => {
let target_id = get_param_string(request.params, "targetId")
match self.target.get_target_info(target_id) {
Ok(info) =>
self.send_result(
request.id,
request.session_id,
Some(make_object({ "targetInfo": target_info_to_json(info) })),
)
Err(e) =>
self.send_error(request.id, request.session_id, -32000, e.message)
}
}
"setAutoAttach" => {
let auto_attach = get_param_bool(request.params, "autoAttach").unwrap_or(
false,
)
let wait_for_debugger = get_param_bool(
request.params,
"waitForDebuggerOnStart",
).unwrap_or(false)
self.target.set_auto_attach(auto_attach, wait_for_debugger)
self.send_result(request.id, request.session_id, None)
}
"setDiscoverTargets" => {
let discover = get_param_bool(request.params, "discover").unwrap_or(false)
self.target.set_discover_targets(discover)
self.send_result(request.id, request.session_id, None)
}
_ =>
self.send_error(
request.id,
request.session_id,
-32601,
"Unknown Target method: " + action,
)
}
Ok(())
}
///|
fn CdpProtocol::dispatch_dom(
self : CdpProtocol,
request : CdpRequest,
action : String,
) -> Result[Unit, String] {
// Get active context session
let ctx = match self.target.get_active_context() {
Some(c) => c
None => {
self.send_error(
request.id,
request.session_id,
-32000,
"No browser context",
)
return Ok(())
}
}
let dom = ctx.get_session().get_dom()
match action {
"getDocument" => {
let depth = get_param_int(request.params, "depth")
match dom.get_document(depth) {
Ok(doc) =>
self.send_result(
request.id,
request.session_id,
Some(make_object({ "root": cdp_node_to_json(doc) })),
)
Err(e) =>
self.send_error(request.id, request.session_id, -32000, e.message)
}
}
"querySelector" => {
let node_id = get_param_int(request.params, "nodeId").unwrap_or(0)
let selector = get_param_string(request.params, "selector").unwrap_or("")
match dom.query_selector(node_id, selector) {
Ok(result) => {
let result_json = match result {
Some(id) => Json::number(id.to_double())
None => Json::number(0.0)
}
self.send_result(
request.id,
request.session_id,
Some(make_object({ "nodeId": result_json })),
)
}
Err(e) =>
self.send_error(request.id, request.session_id, -32000, e.message)
}
}
"querySelectorAll" => {
let node_id = get_param_int(request.params, "nodeId").unwrap_or(0)
let selector = get_param_string(request.params, "selector").unwrap_or("")
match dom.query_selector_all(node_id, selector) {
Ok(ids) => {
let json_ids = ids.map(fn(id) { Json::number(id.to_double()) })
self.send_result(
request.id,
request.session_id,
Some(make_object({ "nodeIds": Json::array(json_ids) })),
)
}
Err(e) =>
self.send_error(request.id, request.session_id, -32000, e.message)
}
}
"enable" | "disable" =>
self.send_result(request.id, request.session_id, None)
"describeNode" =>
// Return a fake node description for objectId-based queries
self.send_result(
request.id,
request.session_id,
Some(
make_object({
"node": make_object({
"nodeId": Json::number(1.0),
"backendNodeId": Json::number(1.0),
"nodeType": Json::number(9.0), // DOCUMENT_NODE
"nodeName": Json::string("#document"),
"localName": Json::string(""),
"nodeValue": Json::string(""),
"childNodeCount": Json::number(1.0),
"frameId": Json::string(ctx.get_session().get_target_id()),
}),
}),
),
)
"resolveNode" =>
// Return a remote object reference for a node
self.send_result(
request.id,
request.session_id,
Some(
make_object({
"object": make_object({
"type": Json::string("object"),
"subtype": Json::string("node"),
"className": Json::string("HTMLDocument"),
"description": Json::string("#document"),
"objectId": Json::string("{\"injectedScriptId\":1,\"id\":1}"),
}),
}),
),
)
"requestNode" =>
// Return a node ID for a remote object
self.send_result(
request.id,
request.session_id,
Some(make_object({ "nodeId": Json::number(1.0) })),
)
_ =>
self.send_error(
request.id,
request.session_id,
-32601,
"Unknown DOM method: " + action,
)
}
Ok(())
}
///|
fn CdpProtocol::dispatch_page(
self : CdpProtocol,
request : CdpRequest,
action : String,
) -> Result[Unit, String] {
let ctx = match self.target.get_active_context() {
Some(c) => c
None => {
self.send_error(
request.id,
request.session_id,
-32000,
"No browser context",
)
return Ok(())
}
}
match action {
"navigate" => {
let url = get_param_string(request.params, "url").unwrap_or("about:blank")
match ctx.navigate(url) {
Ok(result) =>
self.send_result(
request.id,
request.session_id,
Some(
make_object({
"frameId": Json::string(result.frame_id),
"loaderId": Json::string(result.loader_id),
}),
),
)
Err(e) =>
self.send_error(request.id, request.session_id, -32000, e.message)
}
}
"getFrameTree" => {
let target_id = ctx.get_target_id().unwrap_or("frame-0")
self.send_result(
request.id,
request.session_id,
Some(
make_object({
"frameTree": make_object({
"frame": make_object({
"id": Json::string(target_id),
"loaderId": Json::string("loader-0"),
"url": Json::string(ctx.get_url()),
"securityOrigin": Json::string("://"),
"mimeType": Json::string("text/html"),
}),
}),
}),
),
)
}
"setLifecycleEventsEnabled" => {
self.lifecycle_events = get_param_bool(request.params, "enabled").unwrap_or(
false,
)
self.send_result(request.id, request.session_id, None)
// Send lifecycle events immediately for "blank" pages
if self.lifecycle_events {
let frame_id = ctx.get_target_id().unwrap_or("frame-0")
let loader_id = "loader-1"
// Send commit, DOMContentLoaded, load events
self.send_event(
"Page.lifecycleEvent",
request.session_id,
Some(
make_object({
"frameId": Json::string(frame_id),
"loaderId": Json::string(loader_id),
"name": Json::string("commit"),
"timestamp": Json::number(0.0),
}),
),
)
self.send_event(
"Page.lifecycleEvent",
request.session_id,
Some(
make_object({
"frameId": Json::string(frame_id),
"loaderId": Json::string(loader_id),
"name": Json::string("DOMContentLoaded"),
"timestamp": Json::number(0.0),
}),
),
)
self.send_event(
"Page.lifecycleEvent",
request.session_id,
Some(
make_object({
"frameId": Json::string(frame_id),
"loaderId": Json::string(loader_id),
"name": Json::string("load"),
"timestamp": Json::number(0.0),
}),
),
)
}
}
"enable" | "disable" | "bringToFront" | "stopLoading" =>
self.send_result(request.id, request.session_id, None)
"addScriptToEvaluateOnNewDocument" =>
// Return a fake identifier - we don't actually execute scripts
self.send_result(
request.id,
request.session_id,
Some(make_object({ "identifier": Json::string("script-1") })),
)
"removeScriptToEvaluateOnNewDocument" =>
self.send_result(request.id, request.session_id, None)
"createIsolatedWorld" =>
// Return an execution context ID
self.send_result(
request.id,
request.session_id,
Some(make_object({ "executionContextId": Json::number(1.0) })),
)
"setBypassCSP" | "setInterceptFileChooserDialog" =>
self.send_result(request.id, request.session_id, None)
_ =>
self.send_error(
request.id,
request.session_id,
-32601,
"Unknown Page method: " + action,
)
}
Ok(())
}
///|
fn CdpProtocol::dispatch_input(
self : CdpProtocol,
request : CdpRequest,
action : String,
) -> Result[Unit, String] {
match action {
"dispatchMouseEvent" => {
// Parse mouse event parameters
let event_type = get_param_string(request.params, "type").unwrap_or(
"mousePressed",
)
let x = get_param_number(request.params, "x").unwrap_or(0.0)
let y = get_param_number(request.params, "y").unwrap_or(0.0)
let button = get_param_string(request.params, "button").unwrap_or("left")
let click_count = get_param_number(request.params, "clickCount").unwrap_or(
1.0,
)
let modifiers = get_param_number(request.params, "modifiers").unwrap_or(
0.0,
)
// Convert to internal types
let mouse_type : @scheduler.MouseEventType = match event_type {
"mousePressed" => @scheduler.MouseEventType::MousePressed
"mouseReleased" => @scheduler.MouseEventType::MouseReleased
"mouseMoved" => @scheduler.MouseEventType::MouseMoved
"mouseWheel" => @scheduler.MouseEventType::MouseWheel
_ => @scheduler.MouseEventType::MousePressed
}
let mouse_button : @scheduler.MouseButton = match button {
"left" => @scheduler.MouseButton::Left
"middle" => @scheduler.MouseButton::Middle
"right" => @scheduler.MouseButton::Right
"back" => @scheduler.MouseButton::Back
"forward" => @scheduler.MouseButton::Forward
_ => @scheduler.MouseButton::None
}
// Create mouse event
let event = @scheduler.MouseEvent::new(mouse_type, x, y)
.with_button(mouse_button)
.with_modifiers(modifiers.to_int())
// Queue event for processing (actual DOM event dispatch would happen here)
// For now, just acknowledge the event
let _ = event
let _ = click_count
self.send_result(request.id, request.session_id, None)
}
"dispatchKeyEvent" => {
// Parse keyboard event parameters
let event_type = get_param_string(request.params, "type").unwrap_or(
"keyDown",
)
let key = get_param_string(request.params, "key").unwrap_or("")
let code = get_param_string(request.params, "code").unwrap_or("")
let modifiers = get_param_number(request.params, "modifiers").unwrap_or(
0.0,
)
let text = get_param_string(request.params, "text")
// Convert to internal types
let key_type : @scheduler.KeyEventType = match event_type {
"keyDown" => @scheduler.KeyEventType::KeyDown
"keyUp" => @scheduler.KeyEventType::KeyUp
"rawKeyDown" => @scheduler.KeyEventType::RawKeyDown
"char" => @scheduler.KeyEventType::Char
_ => @scheduler.KeyEventType::KeyDown
}
// Create keyboard event
let event = @scheduler.KeyboardEvent::new(key_type, key)
.with_code(code)
.with_modifiers(modifiers.to_int())
let event = match text {
Some(t) => event.with_text(t)
None => event
}
// Queue event for processing
let _ = event
self.send_result(request.id, request.session_id, None)
}
"insertText" => {
// Parse text parameter
let text = get_param_string(request.params, "text").unwrap_or("")
// Create text input event
let event = @scheduler.InputEvent::Text(text)
// Queue event for processing
let _ = event
self.send_result(request.id, request.session_id, None)
}
_ =>
self.send_error(
request.id,
request.session_id,
-32601,
"Unknown Input method: " + action,
)
}
Ok(())
}
///|
fn CdpProtocol::dispatch_runtime(
self : CdpProtocol,
request : CdpRequest,
action : String,
) -> Result[Unit, String] {
match action {
"enable" => {
self.send_result(request.id, request.session_id, None)
// Send executionContextCreated event for the main frame
let target_id = match self.target.get_active_context() {
Some(ctx) =>
match ctx.get_target_id() {
Some(tid) => tid
None => "TID-1"
}
None => "TID-1"
}
self.send_event(
"Runtime.executionContextCreated",
request.session_id,
Some(
make_object({
"context": make_object({
"id": Json::number(1.0),
"origin": Json::string("://"),
"name": Json::string(""),
"uniqueId": Json::string("-1"),
"auxData": make_object({
"isDefault": Json::boolean(true),
"type": Json::string("default"),
"frameId": Json::string(target_id),
}),
}),
}),
),
)
// Also send isolated world context
self.send_event(
"Runtime.executionContextCreated",
request.session_id,
Some(
make_object({
"context": make_object({
"id": Json::number(2.0),
"origin": Json::string("://"),
"name": Json::string("__puppeteer_utility_world__24.34.0"),
"uniqueId": Json::string("-2"),
"auxData": make_object({
"isDefault": Json::boolean(false),
"type": Json::string("isolated"),
"frameId": Json::string(target_id),
}),
}),
}),
),
)
}
"disable" | "runIfWaitingForDebugger" =>
self.send_result(request.id, request.session_id, None)
"evaluate" => {
// Get expression from params
let expression = get_param_string(request.params, "expression").unwrap_or(
"",
)
// Only handle document.title for page.title() support
let result = if expression.contains("document.title") {
let title = match self.target.get_active_context() {
Some(ctx) => ctx.get_title()
None => ""
}
make_object({
"result": make_object({
"type": Json::string("string"),
"value": Json::string(title),
}),
})
} else {
// JS execution not implemented - return undefined
make_object({
"result": make_object({ "type": Json::string("undefined") }),
})
}
self.send_result(request.id, request.session_id, Some(result))
}
"callFunctionOn" => {
// Only handle patterns required by puppeteer internals:
// - cssQuerySelector for page.$()
// - document.write for page.setContent()
let func_decl = get_param_string(request.params, "functionDeclaration").unwrap_or(
"",
)
let result = if func_decl.contains("cssQuerySelector") &&
!func_decl.contains("cssQuerySelectorAll") {
// Handle puppeteer's querySelector: cssQuerySelector(element, selector)
let selector = match request.params {
Some(Object(map)) =>
match map.get("arguments") {
Some(Array(args)) if args.length() >= 2 =>
match args[1] {
Object(arg_map) =>
match arg_map.get("value") {
Some(String(s)) => s
_ => ""
}
_ => ""
}
_ => ""
}
_ => ""
}
if selector.length() > 0 {
match self.target.get_active_context() {
Some(ctx) => {
let session = ctx.get_session()
match session.query_selector(selector) {
Ok(Some(node_id)) =>
make_object({
"result": make_object({
"type": Json::string("object"),
"subtype": Json::string("node"),
"className": Json::string("HTMLElement"),
"description": Json::string(selector),
"objectId": Json::string(
"{\"injectedScriptId\":1,\"id\":" +
node_id.to_string() +
"}",
),
}),
})
Ok(None) | Err(_) =>
make_object({
"result": make_object({
"type": Json::string("object"),
"subtype": Json::string("null"),
"value": Json::null(),
}),
})
}
}
None =>
make_object({
"result": make_object({
"type": Json::string("object"),
"subtype": Json::string("null"),
"value": Json::null(),
}),
})
}
} else {
make_object({
"result": make_object({
"type": Json::string("object"),
"subtype": Json::string("null"),
"value": Json::null(),
}),
})
}
} else if func_decl.contains("document.write") ||
func_decl.contains("document.open") {
// Handle setContent - extract HTML from arguments
match request.params {
Some(Object(map)) =>
match map.get("arguments") {
Some(Array(args)) =>
for arg in args {
match get_string_field(arg, "value") {
Some(html) =>
match self.target.get_active_context() {
Some(ctx) => {
ctx.get_session().load_html(html)
// Also extract title from HTML if present
match (html.find(""), html.find("")) {
(Some(start), Some(end_)) if end_ > start => {
let title = html.unsafe_substring(
start=start + 7,
end=end_,
)
ctx.set_title(title)
}
_ => ()
}
}
None => ()
}
None => ()
}
}
_ => ()
}
_ => ()
}
make_object({
"result": make_object({ "type": Json::string("undefined") }),
})
} else {
// JS execution not implemented - return undefined
make_object({
"result": make_object({ "type": Json::string("undefined") }),
})
}
self.send_result(request.id, request.session_id, Some(result))
}
"getProperties" | "releaseObject" | "releaseObjectGroup" =>
self.send_result(
request.id,
request.session_id,
Some(
make_object({
"result": Json::array([]),
"internalProperties": Json::array([]),
"privateProperties": Json::array([]),
}),
),
)
"addBinding" | "discardConsoleEntries" =>
self.send_result(request.id, request.session_id, None)
_ =>
self.send_error(
request.id,
request.session_id,
-32601,
"Unknown Runtime method: " + action,
)
}
Ok(())
}
///|
fn CdpProtocol::dispatch_browser(
self : CdpProtocol,
request : CdpRequest,
action : String,
) -> Result[Unit, String] {
match action {
"getVersion" =>
self.send_result(
request.id,
request.session_id,
Some(
make_object({
"protocolVersion": Json::string("1.3"),
"product": Json::string("Crater/1.0"),
"revision": Json::string("0"),
"userAgent": Json::string("Crater"),
"jsVersion": Json::string("0"),
}),
),
)
"close" => self.send_result(request.id, request.session_id, None)
_ =>
self.send_error(
request.id,
request.session_id,
-32601,
"Unknown Browser method: " + action,
)
}
Ok(())
}
///|
fn CdpProtocol::dispatch_network(
self : CdpProtocol,
request : CdpRequest,
action : String,
) -> Result[Unit, String] {
match action {
"enable" => {
self.network.enable()
self.send_result(request.id, request.session_id, None)
}
"disable" => {
self.network.disable()
self.send_result(request.id, request.session_id, None)
}
"setCacheDisabled" | "setExtraHTTPHeaders" | "setUserAgentOverride" =>
self.send_result(request.id, request.session_id, None)
_ =>
self.send_error(
request.id,
request.session_id,
-32601,
"Unknown Network method: " + action,
)
}
Ok(())
}
///|
fn CdpProtocol::dispatch_emulation(
self : CdpProtocol,
request : CdpRequest,
action : String,
) -> Result[Unit, String] {
match action {
"setDeviceMetricsOverride"
| "setTouchEmulationEnabled"
| "setEmitTouchEventsForMouse" =>
self.send_result(request.id, request.session_id, None)
_ =>
self.send_error(
request.id,
request.session_id,
-32601,
"Unknown Emulation method: " + action,
)
}
Ok(())
}
///|
fn CdpProtocol::dispatch_security(
self : CdpProtocol,
request : CdpRequest,
action : String,
) -> Result[Unit, String] {
match action {
"enable" | "disable" | "setIgnoreCertificateErrors" =>
self.send_result(request.id, request.session_id, None)
_ =>
self.send_error(
request.id,
request.session_id,
-32601,
"Unknown Security method: " + action,
)
}
Ok(())
}
///|
fn CdpProtocol::dispatch_performance(
self : CdpProtocol,
request : CdpRequest,
action : String,
) -> Result[Unit, String] {
match action {
"enable" | "disable" =>
self.send_result(request.id, request.session_id, None)
_ =>
self.send_error(
request.id,
request.session_id,
-32601,
"Unknown Performance method: " + action,
)
}
Ok(())
}
///|
fn CdpProtocol::dispatch_log(
self : CdpProtocol,
request : CdpRequest,
action : String,
) -> Result[Unit, String] {
match action {
"enable" | "disable" | "clear" =>
self.send_result(request.id, request.session_id, None)
_ =>
self.send_error(
request.id,
request.session_id,
-32601,
"Unknown Log method: " + action,
)
}
Ok(())
}
// =============================================================================
// Response Helpers
// =============================================================================
///|
fn CdpProtocol::send_result(
self : CdpProtocol,
id : Int?,
session_id : String?,
result : Json?,
) -> Unit {
self.outbox.push(Response({ id, result, error: None, session_id }))
}
///|
fn CdpProtocol::send_error(
self : CdpProtocol,
id : Int?,
session_id : String?,
code : Int,
message : String,
) -> Unit {
self.outbox.push(
Response({ id, result: None, error: Some({ code, message }), session_id }),
)
}
///|
fn CdpProtocol::send_event(
self : CdpProtocol,
event_method : String,
session_id : String?,
params : Json?,
) -> Unit {
self.outbox.push(Event({ event_method, params, session_id }))
}
// =============================================================================
// JSON Helpers
// =============================================================================
///|
fn split_method(input : String) -> (String, String)? {
let chars = input.to_array()
for i, c in chars {
if c == '.' {
let domain = substring_arr(chars, 0, i)
let action = substring_arr(chars, i + 1, chars.length())
return Some((domain, action))
}
}
None
}
///|
fn substring_arr(chars : Array[Char], start : Int, end : Int) -> String {
let buf = StringBuilder::new()
for i = start; i < end; i = i + 1 {
buf.write_char(chars[i])
}
buf.to_string()
}
///|
fn get_field(json : Json, key : String) -> Json? {
match json {
Object(map) => map.get(key)
_ => None
}
}
///|
fn get_string_field(json : Json, key : String) -> String? {
match get_field(json, key) {
Some(String(s)) => Some(s)
_ => None
}
}
///|
fn get_int_field(json : Json, key : String) -> Int? {
match get_field(json, key) {
Some(Number(n, ..)) => Some(n.to_int())
_ => None
}
}
///|
fn get_param_string(params : Json?, key : String) -> String? {
match params {
Some(p) => get_string_field(p, key)
None => None
}
}
///|
fn get_param_int(params : Json?, key : String) -> Int? {
match params {
Some(p) => get_int_field(p, key)
None => None
}
}
///|
fn get_param_bool(params : Json?, key : String) -> Bool? {
match params {
Some(Object(map)) =>
match map.get(key) {
Some(True) => Some(true)
Some(False) => Some(false)
_ => None
}
_ => None
}
}
///|
fn get_param_number(params : Json?, key : String) -> Double? {
match params {
Some(Object(map)) =>
match map.get(key) {
Some(Number(n, ..)) => Some(n)
_ => None
}
_ => None
}
}
///|
fn make_object(map : Map[String, Json]) -> Json {
Json::object(map)
}
///|
fn response_to_json(resp : CdpResponse) -> String {
let obj : Map[String, Json] = {}
match resp.id {
Some(id) => obj["id"] = Json::number(id.to_double())
None => ()
}
match resp.result {
Some(r) => obj["result"] = r
None =>
match resp.error {
Some(_) => ()
None => obj["result"] = make_object({})
}
}
match resp.error {
Some(e) =>
obj["error"] = make_object({
"code": Json::number(e.code.to_double()),
"message": Json::string(e.message),
})
None => ()
}
match resp.session_id {
Some(sid) => obj["sessionId"] = Json::string(sid)
None => ()
}
make_object(obj).stringify()
}
///|
fn event_to_json(evt : CdpEvent) -> String {
let obj : Map[String, Json] = {}
obj["method"] = Json::string(evt.event_method)
match evt.params {
Some(p) => obj["params"] = p
None => obj["params"] = make_object({})
}
match evt.session_id {
Some(sid) => obj["sessionId"] = Json::string(sid)
None => ()
}
make_object(obj).stringify()
}
///|
fn target_info_to_json(info : TargetInfo) -> Json {
let obj : Map[String, Json] = {}
obj["targetId"] = Json::string(info.target_id)
obj["type"] = Json::string(info.target_type)
obj["title"] = Json::string(info.title)
obj["url"] = Json::string(info.url)
obj["attached"] = Json::boolean(info.attached)
obj["canAccessOpener"] = Json::boolean(false)
match info.browser_context_id {
Some(id) => obj["browserContextId"] = Json::string(id)
None => ()
}
make_object(obj)
}
///|
fn cdp_node_to_json(node : CdpNode) -> Json {
let obj : Map[String, Json] = {}
obj["nodeId"] = Json::number(node.node_id.to_double())
obj["backendNodeId"] = Json::number(node.backend_node_id.to_double())
obj["nodeType"] = Json::number(node.node_type.to_double())
obj["nodeName"] = Json::string(node.node_name)
obj["localName"] = Json::string(node.local_name)
obj["nodeValue"] = Json::string(node.node_value)
match node.child_node_count {
Some(c) => obj["childNodeCount"] = Json::number(c.to_double())
None => ()
}
match node.children {
Some(children) => {
let json_children = children.map(fn(c) { cdp_node_to_json(c) })
obj["children"] = Json::array(json_children)
}
None => ()
}
match node.shadow_roots {
Some(shadow_roots) => {
let json_shadow_roots = shadow_roots.map(fn(root) {
cdp_node_to_json(root)
})
obj["shadowRoots"] = Json::array(json_shadow_roots)
}
None => ()
}
match node.attributes {
Some(attrs) => {
let json_attrs = attrs.map(fn(a) { Json::string(a) })
obj["attributes"] = Json::array(json_attrs)
}
None => ()
}
match node.shadow_root_type {
Some(shadow_root_type) =>
obj["shadowRootType"] = Json::string(shadow_root_type)
None => ()
}
make_object(obj)
}
// =============================================================================
// Network API (外部から呼び出される)
// =============================================================================
///|
/// Create a new network request (returns request ID for tracking)
pub fn CdpProtocol::create_network_request(
self : CdpProtocol,
url : String,
resource_type : @scheduler.ResourceType,
document_url : String,
frame_id : String,
) -> @scheduler.RequestId {
let request = @scheduler.NetworkRequest::new(
url,
resource_type,
document_url~,
frame_id~,
)
let request_id = request.id
self.network.start_request(request)
request_id
}
///|
/// Create a new network request with explicit loader ID
pub fn CdpProtocol::create_network_request_with_loader(
self : CdpProtocol,
url : String,
resource_type : @scheduler.ResourceType,
document_url : String,
frame_id : String,
loader_id : String,
) -> @scheduler.RequestId {
let request = @scheduler.NetworkRequest::new(
url,
resource_type,
document_url~,
frame_id~,
).with_loader_id(loader_id)
let request_id = request.id
self.network.start_request(request)
request_id
}
///|
/// Notify response received
pub fn CdpProtocol::notify_response_received(
self : CdpProtocol,
request_id : @scheduler.RequestId,
url : String,
status : Int,
mime_type : String,
headers : Map[String, String],
) -> Unit {
let response = @scheduler.NetworkResponse::new(request_id, url, status)
.with_mime_type(mime_type)
.with_headers(headers)
self.network.receive_response(response)
}
///|
/// Notify response received with explicit loader ID
pub fn CdpProtocol::notify_response_received_with_loader(
self : CdpProtocol,
request_id : @scheduler.RequestId,
url : String,
status : Int,
mime_type : String,
headers : Map[String, String],
loader_id : String,
) -> Unit {
let response = @scheduler.NetworkResponse::new(request_id, url, status)
.with_mime_type(mime_type)
.with_headers(headers)
.with_loader_id(loader_id)
self.network.receive_response(response)
}
///|
/// Notify loading finished
pub fn CdpProtocol::notify_loading_finished(
self : CdpProtocol,
request_id : @scheduler.RequestId,
encoded_data_length : Int,
) -> Unit {
self.network.finish_loading(request_id, encoded_data_length)
}
///|
/// Notify loading failed
pub fn CdpProtocol::notify_loading_failed(
self : CdpProtocol,
request_id : @scheduler.RequestId,
error : String,
) -> Unit {
self.network.fail_loading(request_id, error)
}
///|
/// Process pending network events and add them to outbox
pub fn CdpProtocol::flush_network_events(
self : CdpProtocol,
session_id : String?,
) -> Unit {
let events = self.network.drain_events()
for event in events {
let (event_method, params) = network_event_to_cdp(event)
self.send_event(event_method, session_id, Some(params))
}
}
///|
/// Check if there are pending network events
pub fn CdpProtocol::has_pending_network_events(self : CdpProtocol) -> Bool {
self.network.has_pending_events()
}
///|
/// Get network manager for direct access
pub fn CdpProtocol::get_network_manager(
self : CdpProtocol,
) -> @scheduler.NetworkManager {
self.network
}
// =============================================================================
// Network Event to CDP JSON
// =============================================================================
///|
fn network_event_to_cdp(event : @scheduler.NetworkEvent) -> (String, Json) {
match event {
@scheduler.RequestWillBeSent(req) =>
("Network.requestWillBeSent", request_will_be_sent_to_json(req))
@scheduler.ResponseReceived(res) =>
("Network.responseReceived", response_received_to_json(res))
@scheduler.LoadingFinished(request_id~, encoded_data_length~) =>
(
"Network.loadingFinished",
loading_finished_to_json(request_id, encoded_data_length),
)
@scheduler.LoadingFailed(request_id~, error~) =>
("Network.loadingFailed", loading_failed_to_json(request_id, error))
}
}
///|
fn request_will_be_sent_to_json(req : @scheduler.NetworkRequest) -> Json {
let @scheduler.RequestId(id) = req.id
let request_id = "REQ-\{id}"
make_object({
"requestId": Json::string(request_id),
"loaderId": Json::string(req.loader_id),
"documentURL": Json::string(req.document_url),
"request": make_object({
"url": Json::string(req.url),
"method": Json::string(req.http_method.to_string()),
"headers": headers_to_json(req.headers),
}),
"timestamp": Json::number(req.timestamp),
"wallTime": Json::number(req.timestamp),
"initiator": make_object({ "type": Json::string("other") }),
"type": Json::string(resource_type_to_string(req.resource_type)),
"frameId": Json::string(req.frame_id),
})
}
///|
fn response_received_to_json(res : @scheduler.NetworkResponse) -> Json {
let @scheduler.RequestId(id) = res.request_id
let request_id = "REQ-\{id}"
make_object({
"requestId": Json::string(request_id),
"loaderId": Json::string(res.loader_id),
"timestamp": Json::number(res.timestamp),
"type": Json::string("Document"),
"response": make_object({
"url": Json::string(res.url),
"status": Json::number(res.status.to_double()),
"statusText": Json::string(res.status_text),
"headers": headers_to_json(res.headers),
"mimeType": Json::string(res.mime_type),
}),
})
}
///|
fn loading_finished_to_json(
request_id : @scheduler.RequestId,
encoded_data_length : Int,
) -> Json {
let @scheduler.RequestId(id) = request_id
make_object({
"requestId": Json::string("REQ-\{id}"),
"timestamp": Json::number(0.0),
"encodedDataLength": Json::number(encoded_data_length.to_double()),
})
}
///|
fn loading_failed_to_json(
request_id : @scheduler.RequestId,
error : String,
) -> Json {
let @scheduler.RequestId(id) = request_id
make_object({
"requestId": Json::string("REQ-\{id}"),
"timestamp": Json::number(0.0),
"type": Json::string("Document"),
"errorText": Json::string(error),
"canceled": Json::boolean(false),
})
}
///|
fn headers_to_json(headers : Map[String, String]) -> Json {
let obj : Map[String, Json] = {}
headers.each(fn(k, v) { obj[k] = Json::string(v) })
make_object(obj)
}
///|
fn resource_type_to_string(rt : @scheduler.ResourceType) -> String {
match rt {
@scheduler.Document => "Document"
@scheduler.Stylesheet => "Stylesheet"
@scheduler.Script => "Script"
@scheduler.Image => "Image"
@scheduler.Font => "Font"
@scheduler.Other => "Other"
}
}