refactor(protocol): split runtime dispatcher responsibilities

This commit is contained in:
ldm0
2026-09-27 17:04:40 +08:00
committed by Donough Liu
parent 920c5fb83a
commit b414c927a4
12 changed files with 8585 additions and 8501 deletions
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,446 @@
use super::*;
pub(super) fn bidi_call_function_value_at_path_mut<'a>(
command: &'a mut DevToolsCallFunctionCommand,
root: BidiCallFunctionValueRoot,
path: &[BidiValuePathSegment],
) -> Option<&'a mut Value> {
let mut value = match root {
BidiCallFunctionValueRoot::This => command.this_parameter.as_mut()?,
BidiCallFunctionValueRoot::Argument(index) => command.arguments.get_mut(index)?,
};
for segment in path {
match segment {
BidiValuePathSegment::Key(key) => {
value = value.as_object_mut()?.get_mut(key)?;
}
BidiValuePathSegment::Index(index) => {
value = value.as_array_mut()?.get_mut(*index)?;
}
}
}
Some(value)
}
pub(super) async fn materialize_bidi_channel_argument_proxies_async(
conn: &mut CdpConnection,
target: &DevToolsRuntimeTarget,
command: &mut DevToolsCallFunctionCommand,
) -> Result<(), String> {
let realm_id = devtools_realm_id_for_runtime_target_async(conn, target).await;
if let Some(this_parameter) = command.this_parameter.as_mut() {
materialize_bidi_channel_value_async(conn, target, realm_id.as_ref(), this_parameter)
.await?;
}
for argument in &mut command.arguments {
materialize_bidi_channel_value_async(conn, target, realm_id.as_ref(), argument).await?;
}
Ok(())
}
pub(super) async fn materialize_bidi_channel_value_async(
conn: &mut CdpConnection,
target: &DevToolsRuntimeTarget,
realm_id: Option<&DevToolsRealmId>,
value: &mut Value,
) -> Result<(), String> {
let Some(map) = value.as_object_mut() else {
return Ok(());
};
if bidi_remote_reference_object_id(map).is_some() {
return Ok(());
}
match map.get("type").and_then(Value::as_str) {
Some("channel") => {
let properties = bidi_channel_properties_from_local_value(map)?;
let proxy_handle = create_bidi_channel_proxy_and_start_listener_async(
conn, target, realm_id, properties,
)
.await?;
*value = json!({
"__moliBidiChannelProxy": true,
"handle": proxy_handle.as_str(),
});
}
Some("array" | "set") => {
if let Some(items) = map.get_mut("value").and_then(Value::as_array_mut) {
for item in items {
Box::pin(materialize_bidi_channel_value_async(
conn, target, realm_id, item,
))
.await?;
}
}
}
Some("object" | "map") => {
if let Some(entries) = map.get_mut("value").and_then(Value::as_array_mut) {
for entry in entries {
let Some(pair) = entry.as_array_mut() else {
continue;
};
for item in pair {
Box::pin(materialize_bidi_channel_value_async(
conn, target, realm_id, item,
))
.await?;
}
}
}
}
_ => {}
}
Ok(())
}
pub(super) fn bidi_channel_properties_from_local_value(
map: &serde_json::Map<String, Value>,
) -> Result<DevToolsBidiChannelProperties, String> {
let channel = map
.get("value")
.and_then(Value::as_object)
.ok_or_else(|| "InvalidChannelArgument".to_owned())?;
Ok(DevToolsBidiChannelProperties {
channel: channel
.get("channel")
.and_then(Value::as_str)
.ok_or_else(|| "InvalidChannelArgument".to_owned())?
.to_owned(),
ownership: bidi_script_message_ownership_from_value(channel.get("ownership"))?,
serialization_options: match channel.get("serializationOptions") {
Some(value) => Some(bidi_script_message_serialization_options_from_value(value)?),
None => None,
},
})
}
pub(super) async fn bidi_preload_channel_proxy_handle_async(
conn: &mut CdpConnection,
target: &DevToolsRuntimeTarget,
realm_id: Option<&DevToolsRealmId>,
handoff_id: &str,
token: &str,
channel_object_group: &str,
) -> Result<Option<DevToolsRemoteHandleId>, DevToolsError> {
let value = devtools_probe_remote_value_async(
conn,
target.clone(),
DevToolsCommand::CallFunction(DevToolsCallFunctionCommand {
context: DevToolsCommandContext {
protocol: DevToolsProtocol::WebDriverBidi,
session_id: None,
target_id: None,
browser_context_id: None,
},
realm_id: realm_id.cloned(),
world_name: None,
object_id: None,
this_parameter: None,
function_declaration: bidi_preload_channel_proxy_handle_source().to_owned(),
arguments: vec![json!(handoff_id), json!(token)],
await_promise: false,
user_gesture: false,
webdriver_bidi_file_prompt_handler: None,
result_ownership: DevToolsResultOwnership::Root,
object_group: Some(channel_object_group.to_owned()),
preserve_remote_metadata: false,
materialize_bidi_script_result: false,
serialization_options: None,
}),
)
.await?;
Ok(value.and_then(|value| value.handle.or(value.shared_id)))
}
pub(super) fn bidi_preload_channel_properties_from_handoff(
handoff: &BidiPreloadChannelHandoff,
) -> Result<DevToolsBidiChannelProperties, String> {
Ok(DevToolsBidiChannelProperties {
channel: handoff.channel.clone(),
ownership: bidi_script_message_ownership_from_str(handoff.ownership.as_deref())?,
serialization_options: match handoff.serialization_options.as_ref() {
Some(value) => Some(bidi_script_message_serialization_options_from_value(value)?),
None => None,
},
})
}
pub(super) async fn release_bidi_channel_object_group_for_target_best_effort_async(
conn: &mut CdpConnection,
target: &DevToolsRuntimeTarget,
session_id: Option<&str>,
object_group: &str,
) {
let owner = session_id
.map(CommandOwnerScope::for_session)
.unwrap_or_else(|| CommandOwnerScope::for_route(target.route.clone()));
conn.release_bidi_channel_object_group_for_owner_best_effort_async(&owner, object_group)
.await;
}
pub(super) fn bidi_channel_page_owner_for_runtime_target(
conn: &CdpConnection,
target: &DevToolsRuntimeTarget,
owner: &CommandOwnerScope,
) -> Option<BidiChannelPageOwner> {
let exact_owner = owner
.session_id()
.map(CommandOwnerScope::for_session)
.unwrap_or_else(|| CommandOwnerScope::for_route(target.route.clone()));
BidiChannelPageOwner::capture_for_owner(conn, exact_owner)
}
pub(super) async fn create_bidi_channel_proxy_and_start_listener_async(
conn: &mut CdpConnection,
target: &DevToolsRuntimeTarget,
realm_id: Option<&DevToolsRealmId>,
properties: DevToolsBidiChannelProperties,
) -> Result<DevToolsRemoteHandleId, String> {
let listener_target_id = target
.window_context_id
.clone()
.ok_or_else(|| "NoSuchBidiChannelTarget".to_owned())?;
let listener_realm_id = realm_id
.cloned()
.ok_or_else(|| "NoSuchBidiChannelRealm".to_owned())?;
let listener_owner = bidi_channel_page_owner_for_runtime_target(
conn,
target,
&CommandOwnerScope::for_route(target.route.clone()),
)
.ok_or_else(|| "NoSuchBidiChannelTarget".to_owned())?;
let channel_object_group = conn.next_bidi_channel_object_group();
let proxy_handle = match create_bidi_channel_proxy_async(
conn,
target,
realm_id,
&channel_object_group,
)
.await
{
Ok(Some(handle)) => handle,
Ok(None) => {
release_bidi_channel_object_group_for_target_best_effort_async(
conn,
target,
None,
&channel_object_group,
)
.await;
return Err("CannotCreateBidiChannelProxy".to_owned());
}
Err(error) => {
release_bidi_channel_object_group_for_target_best_effort_async(
conn,
target,
None,
&channel_object_group,
)
.await;
return Err(error);
}
};
let listener = PendingBidiChannelListener::new(
Some(listener_target_id),
Some(listener_realm_id),
proxy_handle.clone(),
channel_object_group,
properties,
)
.ok_or_else(|| "NoSuchBidiChannelRealm".to_owned())?;
conn.publish_bidi_channel_listener_start(BidiChannelListenerResidence::new(
listener_owner,
listener,
));
Ok(proxy_handle)
}
pub(super) async fn create_bidi_channel_proxy_async(
conn: &mut CdpConnection,
target: &DevToolsRuntimeTarget,
realm_id: Option<&DevToolsRealmId>,
channel_object_group: &str,
) -> Result<Option<DevToolsRemoteHandleId>, String> {
let call_target = if let Some(realm_id) = realm_id
&& target.execution_context_id.is_none()
{
devtools_runtime_realm_target_async(conn, realm_id)
.await
.map_err(|error| error.message)?
} else {
target.clone()
};
let command_target_id = if realm_id.is_some() {
None
} else {
call_target.window_context_id.clone()
};
let value = Box::pin(devtools_probe_remote_value_async(
conn,
call_target,
DevToolsCommand::CallFunction(DevToolsCallFunctionCommand {
context: DevToolsCommandContext {
protocol: DevToolsProtocol::WebDriverBidi,
session_id: None,
target_id: command_target_id,
browser_context_id: None,
},
realm_id: realm_id.cloned(),
world_name: None,
object_id: None,
this_parameter: None,
function_declaration: format!(
"function() {{ return {}; }}",
bidi_channel_proxy_expression_source()
),
arguments: Vec::new(),
await_promise: false,
user_gesture: false,
webdriver_bidi_file_prompt_handler: None,
result_ownership: DevToolsResultOwnership::Root,
object_group: Some(channel_object_group.to_owned()),
preserve_remote_metadata: false,
materialize_bidi_script_result: false,
serialization_options: None,
}),
))
.await
.map_err(|error| error.message)?;
Ok(value.and_then(|value| value.handle.or(value.shared_id)))
}
pub(super) fn bidi_channel_proxy_expression_source() -> String {
let mut source = String::new();
source.push_str("(() => {\n");
source.push_str(
"const queue = [];\n\
let queueNonEmptyResolver = null;\n\
return {\n\
async getMessage() {\n\
const onMessage = queue.length > 0 ? Promise.resolve() : new Promise((resolve) => { queueNonEmptyResolver = resolve; });\n\
await onMessage;\n\
return queue.shift();\n\
},\n\
sendMessage(message) {\n\
queue.push(message);\n\
if (queueNonEmptyResolver !== null) {\n\
queueNonEmptyResolver();\n\
queueNonEmptyResolver = null;\n\
}\n\
},\n\
};\n\
})()",
);
source
}
pub(super) fn bidi_preload_channel_delegate_source(has_channel_handoffs: bool) -> String {
if !has_channel_handoffs {
return String::new();
}
let mut source = String::new();
source.push_str(
"const __moliPutBidiPreloadChannelProxy = (handoffId, token, proxy) => {\n\
if (typeof handoffId !== 'string' || handoffId.length === 0 || typeof token !== 'string') { return; }\n\
const take = (providedToken) => {\n\
if (providedToken !== token) { return undefined; }\n\
delete globalThis[handoffId];\n\
return proxy;\n\
};\n\
Object.defineProperty(globalThis, handoffId, {\n\
value: take,\n\
configurable: true,\n\
enumerable: false,\n\
writable: false,\n\
});\n\
};\n",
);
source.push_str(
"const __moliCreateBidiChannelDelegate = (properties) => {\n\
const queue = [];\n\
let queueNonEmptyResolver = null;\n\
const proxy = {\n\
async getMessage() {\n\
const onMessage = queue.length > 0 ? Promise.resolve() : new Promise((resolve) => { queueNonEmptyResolver = resolve; });\n\
await onMessage;\n\
return queue.shift();\n\
},\n\
sendMessage(message) {\n\
queue.push(message);\n\
if (queueNonEmptyResolver !== null) {\n\
queueNonEmptyResolver();\n\
queueNonEmptyResolver = null;\n\
}\n\
},\n\
};\n\
__moliPutBidiPreloadChannelProxy(properties && properties.handoffId, properties && properties.handoffToken, proxy);\n\
return proxy.sendMessage.bind(proxy);\n\
};",
);
source
}
pub(super) fn bidi_preload_channel_proxy_handle_source() -> &'static str {
"(function(handoffId, token) {\n\
const take = globalThis[handoffId];\n\
if (typeof take !== 'function') { return undefined; }\n\
return take(token);\n\
})"
}
pub(super) fn new_bidi_preload_channel_handoff(
channel: &serde_json::Map<String, Value>,
) -> Result<BidiPreloadChannelHandoff, String> {
let properties = bidi_channel_properties_from_channel_map(channel)?;
let channel_name = channel
.get("channel")
.and_then(Value::as_str)
.ok_or_else(|| "InvalidChannelArgument".to_owned())?
.to_owned();
Ok(BidiPreloadChannelHandoff {
handoff_id: format!(
"__lmBidiPreloadChannel_{}",
random_bidi_preload_channel_handoff_id()?
),
token: random_bidi_preload_channel_handoff_id()?,
channel: channel_name,
ownership: match properties.ownership {
DevToolsResultOwnership::Root => Some("root".to_owned()),
DevToolsResultOwnership::None | DevToolsResultOwnership::ByValue => None,
},
serialization_options: channel.get("serializationOptions").cloned(),
})
}
pub(super) fn bidi_channel_properties_from_channel_map(
channel: &serde_json::Map<String, Value>,
) -> Result<DevToolsBidiChannelProperties, String> {
Ok(DevToolsBidiChannelProperties {
channel: channel
.get("channel")
.and_then(Value::as_str)
.ok_or_else(|| "InvalidChannelArgument".to_owned())?
.to_owned(),
ownership: bidi_script_message_ownership_from_value(channel.get("ownership"))?,
serialization_options: match channel.get("serializationOptions") {
Some(value) => Some(bidi_script_message_serialization_options_from_value(value)?),
None => None,
},
})
}
pub(super) fn random_bidi_preload_channel_handoff_id() -> Result<String, String> {
let mut bytes = [0_u8; 16];
moli_crypto::fill_secure_random(&mut bytes)
.map_err(|error| format!("failed to generate BiDi preload channel id: {error}"))?;
Ok(hex_bytes(&bytes))
}
pub(super) fn hex_bytes(bytes: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
out
}
@@ -0,0 +1,740 @@
use super::*;
pub(super) async fn start_protocol_neutral_runtime_command(
conn: &mut CdpConnection,
target: DevToolsRuntimeTarget,
command: DevToolsCommand,
internal_command_id: u64,
) -> RuntimeCommandTaskStep {
let owner = CommandOwnerScope::for_route(target.route.clone());
match command {
DevToolsCommand::EvaluateScript(command) => {
let params = devtools_evaluate_script_params(&command, target.execution_context_id);
let json =
runtime_inspector_command_json(internal_command_id, "Runtime.evaluate", &params);
let parsed = match parse_synthesized_runtime_command(json) {
Ok(command) => command,
Err(message) => {
return RuntimeCommandTaskStep::Complete(runtime_inspector_error_plan(
Some(internal_command_id),
message,
));
}
};
let cmd = Cmd::from_parsed(&parsed)
.expect("synthesized Runtime command must contain a domain separator");
let command = DevToolsCommand::EvaluateScript(command);
match prepare_pending_devtools_runtime_inspector_json_for_owner(
conn, &cmd, &owner, &command,
) {
Ok(inspector_json) => start_devtools_runtime_command_for_owner(
conn,
&cmd,
command,
inspector_json,
runtime_command_awaits_promise(&cmd, RuntimeAction::Evaluate),
RendererInspectorResponseDelivery::AdapterReply,
owner.clone(),
),
Err(message) => {
RuntimeCommandTaskStep::Complete(runtime_inspector_error_plan(cmd.id, message))
}
}
}
DevToolsCommand::CallFunction(mut command) => {
if matches!(command.context.protocol, DevToolsProtocol::WebDriverBidi)
&& let Err(message) = Box::pin(materialize_bidi_channel_argument_proxies_async(
conn,
&target,
&mut command,
))
.await
{
return RuntimeCommandTaskStep::Complete(runtime_inspector_error_plan(
Some(internal_command_id),
message,
));
}
match devtools_call_function_params_async(
conn,
&owner,
&command,
target.execution_context_id,
)
.await
{
Ok(params) => {
let json = runtime_inspector_command_json(
internal_command_id,
"Runtime.callFunctionOn",
&params,
);
let parsed = match parse_synthesized_runtime_command(json) {
Ok(command) => command,
Err(message) => {
return RuntimeCommandTaskStep::Complete(runtime_inspector_error_plan(
Some(internal_command_id),
message,
));
}
};
let cmd = Cmd::from_parsed(&parsed)
.expect("synthesized Runtime command must contain a domain separator");
let command = DevToolsCommand::CallFunction(command);
match prepare_pending_devtools_runtime_inspector_json_for_owner(
conn, &cmd, &owner, &command,
) {
Ok(inspector_json) => start_devtools_runtime_command_for_owner(
conn,
&cmd,
command,
inspector_json,
runtime_command_awaits_promise(&cmd, RuntimeAction::CallFunctionOn),
RendererInspectorResponseDelivery::AdapterReply,
owner.clone(),
),
Err(message) => RuntimeCommandTaskStep::Complete(
runtime_inspector_error_plan(cmd.id, message),
),
}
}
Err(message) => RuntimeCommandTaskStep::Complete(runtime_inspector_error_plan(
Some(internal_command_id),
message,
)),
}
}
DevToolsCommand::TerminateExecution(_) => {
let json = runtime_inspector_command_json(
internal_command_id,
"Runtime.terminateExecution",
&json!({}),
);
let parsed = match parse_synthesized_runtime_command(json) {
Ok(command) => command,
Err(message) => {
return RuntimeCommandTaskStep::Complete(runtime_inspector_error_plan(
Some(internal_command_id),
message,
));
}
};
let cmd = Cmd::from_parsed(&parsed)
.expect("synthesized Runtime command must contain a domain separator")
.with_terminal_response_delivery_override(Some(
RendererInspectorResponseDelivery::AdapterReply,
));
let MainRuntimeCommand::Inspector(command) =
MainRuntimeCommand::classify(RuntimeAction::TerminateExecution)
else {
unreachable!("Runtime.terminateExecution must use an Inspector command route")
};
start_main_runtime_inspector_command_for_owner(conn, &cmd, command, owner.clone())
}
_ => RuntimeCommandTaskStep::Complete(CommandOutputPlan::from_devtools_error(
DevToolsError::new(DevToolsErrorKind::Unsupported, "UnsupportedDevToolsCommand"),
)),
}
}
pub(super) fn runtime_inspector_command_json(
command_id: u64,
method: &str,
params: &Value,
) -> String {
json!({
"id": command_id,
"method": method,
"params": params,
})
.to_string()
}
pub(super) fn devtools_evaluate_script_params(
command: &DevToolsEvaluateScriptCommand,
execution_context_id: Option<i64>,
) -> Value {
let expression = if command.materialize_bidi_script_result {
bidi_window_remote_result_expression_source(&command.expression, command.await_promise)
} else {
command.expression.clone()
};
let mut params = json!({
"expression": expression,
"awaitPromise": command.await_promise,
"returnByValue": devtools_result_ownership_returns_by_value(
command.result_ownership,
command.preserve_remote_metadata,
),
});
if command.user_gesture
&& let Some(map) = params.as_object_mut()
{
map.insert("userGesture".to_owned(), Value::Bool(true));
}
if let Some(handler) = command.webdriver_bidi_file_prompt_handler.as_deref()
&& let Some(map) = params.as_object_mut()
{
map.insert(
WEBDRIVER_BIDI_FILE_PROMPT_HANDLER_PARAM.to_owned(),
Value::String(handler.to_owned()),
);
}
apply_devtools_serialization_options(&mut params, command.serialization_options.as_ref());
if let Some(execution_context_id) = execution_context_id
&& let Some(map) = params.as_object_mut()
{
map.insert("contextId".to_owned(), json!(execution_context_id));
}
if matches!(command.result_ownership, DevToolsResultOwnership::Root)
&& let Some(map) = params.as_object_mut()
{
map.insert("objectGroup".to_owned(), json!("webdriver-bidi"));
}
params
}
pub(super) async fn devtools_call_function_params_async(
conn: &mut CdpConnection,
owner: &CommandOwnerScope,
command: &DevToolsCallFunctionCommand,
execution_context_id: Option<i64>,
) -> Result<Value, String> {
let deserialize_bidi_local_values =
devtools_call_function_deserializes_bidi_local_values(command);
let primary_argument_count = devtools_call_function_arguments(command).len();
let function_declaration = devtools_call_function_declaration(
command,
deserialize_bidi_local_values,
primary_argument_count,
);
let arguments = devtools_call_function_cdp_arguments(command, deserialize_bidi_local_values);
let mut params = json!({
"functionDeclaration": function_declaration,
"arguments": arguments,
"awaitPromise": command.await_promise,
"returnByValue": devtools_result_ownership_returns_by_value(
command.result_ownership,
command.preserve_remote_metadata,
),
});
if command.user_gesture
&& let Some(map) = params.as_object_mut()
{
map.insert("userGesture".to_owned(), Value::Bool(true));
}
if let Some(handler) = command.webdriver_bidi_file_prompt_handler.as_deref()
&& let Some(map) = params.as_object_mut()
{
map.insert(
WEBDRIVER_BIDI_FILE_PROMPT_HANDLER_PARAM.to_owned(),
Value::String(handler.to_owned()),
);
}
apply_devtools_serialization_options(&mut params, command.serialization_options.as_ref());
if let Some(map) = params.as_object_mut() {
if let Some(object_id) = command.object_id.as_ref() {
map.insert("objectId".to_owned(), json!(object_id.as_str()));
} else if let Some(execution_context_id) = execution_context_id {
map.insert("executionContextId".to_owned(), json!(execution_context_id));
} else if command.realm_id.is_none() {
let Some(execution_context_id) = conn
.runtime_default_or_initial_execution_context_id_for_owner_async(owner)
.await?
else {
return Err("NoDefaultExecutionContext".to_owned());
};
map.insert("executionContextId".to_owned(), json!(execution_context_id));
}
}
let object_group = command.object_group.as_deref().or_else(|| {
matches!(command.result_ownership, DevToolsResultOwnership::Root)
.then_some("webdriver-bidi")
});
if let Some(object_group) = object_group
&& let Some(map) = params.as_object_mut()
{
map.insert("objectGroup".to_owned(), json!(object_group));
}
Ok(params)
}
pub(super) fn devtools_call_function_declaration(
command: &DevToolsCallFunctionCommand,
deserialize_bidi_local_values: bool,
primary_argument_count: usize,
) -> String {
let declaration = if command.this_parameter.is_none() && !deserialize_bidi_local_values {
command.function_declaration.clone()
} else if deserialize_bidi_local_values {
let this_expression = if command.this_parameter.is_some() {
"__deserialize(__primaryArgs.shift())"
} else {
"undefined"
};
let call_expression = if command.this_parameter.is_some() {
"f.apply(deserializedThis, deserializedArgs)"
} else {
"f(...deserializedArgs)"
};
let deserializer_source = bidi_local_value_deserializer_source();
format!(
"(...args) => {{\n\
function callFunction(f, args) {{\n\
const __primaryArgs = args.slice(0, {primary_argument_count});\n\
const __remoteReferences = args.slice({primary_argument_count});\n\
{deserializer_source}\n\
const deserializedThis = {this_expression};\n\
const deserializedArgs = __primaryArgs.map(__deserialize);\n\
return {call_expression};\n\
}}\n\
return callFunction((\n\
{}\n\
), args);\n\
}}",
command.function_declaration
)
} else {
format!(
"(...args) => {{\n\
function callFunction(f, args) {{\n\
const deserializedThis = args.shift();\n\
const deserializedArgs = args;\n\
return f.apply(deserializedThis, deserializedArgs);\n\
}}\n\
return callFunction((\n\
{}\n\
), args);\n\
}}",
command.function_declaration
)
};
if command.materialize_bidi_script_result {
bidi_window_remote_result_function_declaration_source(&declaration, command.await_promise)
} else {
declaration
}
}
pub(super) fn bidi_local_value_deserializer_source() -> &'static str {
"const __deserialize = (value) => {\n\
if (value && value.__moliBidiRemoteReference === true) { return __remoteReferences[value.index]; }\n\
if (value && value.__moliBidiChannelProxy === true) {\n\
const proxy = __remoteReferences[value.index];\n\
return proxy && typeof proxy.sendMessage === 'function'\n\
? proxy.sendMessage.bind(proxy)\n\
: undefined;\n\
}\n\
if (!value || value.__moliBidiLocalValue !== true) { return value; }\n\
switch (value.type) {\n\
case 'undefined': return undefined;\n\
case 'null': return null;\n\
case 'string':\n\
case 'boolean': return value.value;\n\
case 'number':\n\
if (value.value === 'NaN') { return NaN; }\n\
if (value.value === '-0') { return -0; }\n\
if (value.value === 'Infinity') { return Infinity; }\n\
if (value.value === '-Infinity') { return -Infinity; }\n\
return value.value;\n\
case 'bigint': return BigInt(value.value);\n\
case 'date': return new Date(value.value);\n\
case 'regexp': return new RegExp(value.value.pattern, value.value.flags || '');\n\
case 'array': return value.value.map(__deserialize);\n\
case 'object': return Object.fromEntries(value.value.map(([key, item]) => [key, __deserialize(item)]));\n\
case 'map': return new Map(value.value.map(([key, item]) => [__deserialize(key), __deserialize(item)]));\n\
case 'set': return new Set(value.value.map(__deserialize));\n\
case 'channel': return typeof __moliCreateBidiChannelDelegate === 'function'\n\
? __moliCreateBidiChannelDelegate(value.value)\n\
: undefined;\n\
default: return value.value;\n\
}\n\
};"
}
pub(super) fn bidi_window_remote_result_expression_source(
expression: &str,
await_promise: bool,
) -> String {
let serializer = bidi_window_remote_result_serializer_source();
let encoded_expression = serde_json::to_string(expression)
.expect("serializing a string expression to JSON should not fail");
if await_promise {
format!(
"Promise.resolve((0, eval)({encoded_expression})).then((__moliBidiResult) => {{\n\
{serializer}\n\
return __moliSerializeBidiWindowRemoteResult(__moliBidiResult);\n\
}})"
)
} else {
format!(
"((__moliBidiResult) => {{\n\
{serializer}\n\
return __moliSerializeBidiWindowRemoteResult(__moliBidiResult);\n\
}})(\n\
(0, eval)({encoded_expression})\n\
)"
)
}
}
pub(super) fn bidi_window_remote_result_function_declaration_source(
function_declaration: &str,
await_promise: bool,
) -> String {
let serializer = bidi_window_remote_result_serializer_source();
if await_promise {
format!(
"(...args) => {{\n\
{serializer}\n\
const __moliBidiFunction = ({function_declaration});\n\
const __moliBidiResult = Reflect.apply(__moliBidiFunction, this, args);\n\
return Promise.resolve(__moliBidiResult).then(__moliSerializeBidiWindowRemoteResult);\n\
}}"
)
} else {
format!(
"(...args) => {{\n\
{serializer}\n\
const __moliBidiFunction = ({function_declaration});\n\
const __moliBidiResult = Reflect.apply(__moliBidiFunction, this, args);\n\
return __moliSerializeBidiWindowRemoteResult(__moliBidiResult);\n\
}}"
)
}
}
pub(super) fn bidi_window_remote_result_serializer_source() -> &'static str {
"const __moliSerializeBidiWindowRemoteResult = (value) => {\n\
if (typeof __moliHostBidiWindowRemoteValue === 'function') {\n\
const windowRemoteValue = __moliHostBidiWindowRemoteValue(value);\n\
if (windowRemoteValue && windowRemoteValue.__moliBidiRemoteValue === true) {\n\
return windowRemoteValue;\n\
}\n\
}\n\
return value;\n\
};"
}
pub(super) fn bidi_script_message_ownership_from_value(
value: Option<&Value>,
) -> Result<DevToolsResultOwnership, String> {
match value {
None => Ok(DevToolsResultOwnership::None),
Some(Value::String(value)) if value == "root" => Ok(DevToolsResultOwnership::Root),
Some(Value::String(value)) if value == "none" => Ok(DevToolsResultOwnership::None),
Some(_) => Err("InvalidChannelArgument".to_owned()),
}
}
pub(super) fn bidi_script_message_ownership_from_str(
value: Option<&str>,
) -> Result<DevToolsResultOwnership, String> {
match value {
None => Ok(DevToolsResultOwnership::None),
Some("root") => Ok(DevToolsResultOwnership::Root),
Some("none") => Ok(DevToolsResultOwnership::None),
Some(_) => Err("InvalidChannelArgument".to_owned()),
}
}
pub(super) fn devtools_call_function_arguments(
command: &DevToolsCallFunctionCommand,
) -> Vec<Value> {
command
.this_parameter
.iter()
.cloned()
.chain(command.arguments.iter().cloned())
.collect()
}
pub(super) fn devtools_call_function_cdp_arguments(
command: &DevToolsCallFunctionCommand,
deserialize_bidi_local_values: bool,
) -> Vec<Value> {
let arguments = devtools_call_function_arguments(command);
if !deserialize_bidi_local_values {
return arguments
.into_iter()
.map(cdp_call_argument_from_devtools_argument)
.collect();
}
let mut remote_object_ids = Vec::new();
let mut context = BidiLocalValueDescriptorContext {
remote_object_ids: &mut remote_object_ids,
preload_channel_handoffs: None,
preload_channel_error: None,
};
let mut cdp_arguments = arguments
.into_iter()
.map(|argument| {
let descriptor =
bidi_local_value_descriptor_from_devtools_argument(&argument, &mut context)
.or_else(|| cdp_value_from_devtools_argument(&argument))
.or_else(|| {
argument
.as_object()
.and_then(|map| map.get("value"))
.cloned()
})
.unwrap_or(argument);
json!({ "value": descriptor })
})
.collect::<Vec<_>>();
drop(context);
cdp_arguments.extend(
remote_object_ids
.into_iter()
.map(|object_id| json!({ "objectId": object_id })),
);
cdp_arguments
}
pub(super) fn devtools_call_function_deserializes_bidi_local_values(
command: &DevToolsCallFunctionCommand,
) -> bool {
matches!(command.context.protocol, DevToolsProtocol::WebDriverBidi)
&& command
.this_parameter
.iter()
.chain(command.arguments.iter())
.any(bidi_local_value_needs_js_deserialization)
}
pub(super) fn bidi_local_value_needs_js_deserialization(argument: &Value) -> bool {
bidi_local_value_needs_js_deserialization_nested(argument, false)
}
pub(super) fn bidi_local_value_needs_js_deserialization_nested(
argument: &Value,
nested: bool,
) -> bool {
let Some(map) = argument.as_object() else {
return false;
};
if map.get("__moliBidiChannelProxy").and_then(Value::as_bool) == Some(true) {
return true;
}
if bidi_remote_reference_object_id(map).is_some() {
return nested;
}
match map.get("type").and_then(Value::as_str) {
Some("date" | "map" | "regexp" | "set") => true,
Some("channel") => true,
Some("bigint") => nested,
Some("number") => {
nested
&& map
.get("value")
.and_then(Value::as_str)
.is_some_and(is_bidi_unserializable_number)
}
Some("array") => map
.get("value")
.and_then(Value::as_array)
.is_some_and(|values| {
values
.iter()
.any(|value| bidi_local_value_needs_js_deserialization_nested(value, true))
}),
Some("object") => map
.get("value")
.and_then(Value::as_array)
.is_some_and(|entries| {
entries.iter().any(|entry| {
entry
.as_array()
.and_then(|pair| pair.get(1))
.is_some_and(|value| {
bidi_local_value_needs_js_deserialization_nested(value, true)
})
})
}),
_ => false,
}
}
pub(super) fn apply_devtools_serialization_options(
params: &mut Value,
serialization_options: Option<&DevToolsSerializationOptions>,
) {
let Some(serialization_options) = serialization_options else {
return;
};
let Some(map) = params.as_object_mut() else {
return;
};
map.insert(
"serializationOptions".to_owned(),
devtools_deep_serialization_options_json(serialization_options),
);
}
pub(super) fn devtools_result_ownership_returns_by_value(
ownership: DevToolsResultOwnership,
preserve_remote_metadata: bool,
) -> bool {
!preserve_remote_metadata && !matches!(ownership, DevToolsResultOwnership::Root)
}
pub(super) fn cdp_call_argument_from_devtools_argument(argument: Value) -> Value {
let Some(map) = argument.as_object() else {
return json!({ "value": argument });
};
if let Some(object_id) = bidi_remote_reference_object_id(map) {
return json!({ "objectId": object_id });
}
if map.get("type").and_then(Value::as_str) == Some("number")
&& let Some(unserializable_value) = map.get("value").and_then(Value::as_str)
&& is_bidi_unserializable_number(unserializable_value)
{
return json!({ "unserializableValue": unserializable_value });
}
if map.get("type").and_then(Value::as_str) == Some("bigint")
&& let Some(value) = map.get("value").and_then(Value::as_str)
{
let unserializable_value = if value.ends_with('n') {
value.to_owned()
} else {
format!("{value}n")
};
return json!({ "unserializableValue": unserializable_value });
}
if let Some(value) = cdp_value_from_devtools_argument(&argument) {
return json!({ "value": value });
}
if let Some(value) = map.get("value") {
return json!({ "value": value.clone() });
}
if map.get("type").and_then(Value::as_str) == Some("null") {
return json!({ "value": null });
}
argument
}
pub(super) fn is_bidi_unserializable_number(value: &str) -> bool {
matches!(value, "NaN" | "-0" | "Infinity" | "-Infinity")
}
pub(super) fn bidi_remote_reference_object_id(
map: &serde_json::Map<String, Value>,
) -> Option<&str> {
map.get("sharedId")
.or_else(|| map.get("handle"))
.and_then(Value::as_str)
}
pub(super) fn bidi_local_value_descriptor_from_devtools_argument(
argument: &Value,
context: &mut BidiLocalValueDescriptorContext<'_>,
) -> Option<Value> {
let Some(map) = argument.as_object() else {
return Some(argument.clone());
};
if map.get("__moliBidiChannelProxy").and_then(Value::as_bool) == Some(true) {
let object_id = map.get("handle")?.as_str()?;
let index = context.remote_object_ids.len();
context.remote_object_ids.push(object_id.to_owned());
return Some(json!({
"__moliBidiChannelProxy": true,
"index": index,
}));
}
if let Some(object_id) = bidi_remote_reference_object_id(map) {
let index = context.remote_object_ids.len();
context.remote_object_ids.push(object_id.to_owned());
return Some(json!({
"__moliBidiRemoteReference": true,
"index": index,
}));
}
let type_name = map.get("type").and_then(Value::as_str)?;
let descriptor_value = match type_name {
"undefined" | "null" => Value::Null,
"array" | "set" => Value::Array(
map.get("value")?
.as_array()?
.iter()
.map(|value| bidi_local_value_descriptor_from_devtools_argument(value, context))
.collect::<Option<Vec<_>>>()?,
),
"object" => {
let mut entries = Vec::new();
for entry in map.get("value")?.as_array()? {
let pair = entry.as_array()?;
let [key, value] = pair.as_slice() else {
return None;
};
entries.push(json!([
key.as_str()?,
bidi_local_value_descriptor_from_devtools_argument(value, context)?
]));
}
Value::Array(entries)
}
"map" => {
let mut entries = Vec::new();
for entry in map.get("value")?.as_array()? {
let pair = entry.as_array()?;
let [key, value] = pair.as_slice() else {
return None;
};
let key = key
.as_str()
.map(Value::from)
.or_else(|| bidi_local_value_descriptor_from_devtools_argument(key, context))?;
entries.push(json!([
key,
bidi_local_value_descriptor_from_devtools_argument(value, context)?
]));
}
Value::Array(entries)
}
"regexp" => {
let regexp = map.get("value")?.as_object()?;
json!({
"pattern": regexp.get("pattern")?.as_str()?,
"flags": regexp.get("flags").and_then(Value::as_str).unwrap_or_default(),
})
}
"channel" => {
let channel = map.get("value")?.as_object()?;
let mut descriptor = serde_json::Map::new();
descriptor.insert(
"channel".to_owned(),
json!(channel.get("channel")?.as_str()?),
);
if let Some(ownership) = channel.get("ownership").and_then(Value::as_str) {
descriptor.insert("ownership".to_owned(), json!(ownership));
}
if let Some(serialization_options) = channel.get("serializationOptions") {
descriptor.insert(
"serializationOptions".to_owned(),
serialization_options.clone(),
);
}
if let Some(handoffs) = context.preload_channel_handoffs.as_deref_mut() {
let handoff = match new_bidi_preload_channel_handoff(channel) {
Ok(handoff) => handoff,
Err(message) => {
context.preload_channel_error = Some(message);
return None;
}
};
descriptor.insert("handoffId".to_owned(), json!(handoff.handoff_id));
descriptor.insert("handoffToken".to_owned(), json!(handoff.token));
handoffs.push(handoff);
}
Value::Object(descriptor)
}
"string" | "number" | "boolean" | "bigint" | "date" => map.get("value")?.clone(),
_ => return None,
};
Some(json!({
"__moliBidiLocalValue": true,
"type": type_name,
"value": descriptor_value,
}))
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,905 @@
use super::*;
pub(super) async fn execute_devtools_locate_nodes_command_async(
conn: &mut CdpConnection,
mut command: DevToolsLocateNodesCommand,
) -> DevToolsCommandExecutionOutput {
if matches!(&command.locator, DevToolsLocateNodesLocator::Context(_)) {
return execute_devtools_locate_nodes_context_command_async(conn, command).await;
}
let target =
match devtools_runtime_target_async(conn, &DevToolsCommand::LocateNodes(command.clone()))
.await
{
Ok(target) => target,
Err(error) => return DevToolsCommandExecutionOutput::new(Err(error)),
};
let context = command.context.clone();
let locator = command.locator.clone();
let start_nodes = std::mem::take(&mut command.start_nodes);
let start_node_references = std::mem::take(&mut command.start_node_references);
let start_node_inputs = match locate_nodes_start_node_inputs_async(
conn,
&target,
start_nodes,
start_node_references,
)
.await
{
Ok(inputs) => inputs,
Err(error) => return DevToolsCommandExecutionOutput::new(Err(error)),
};
let (start_node_arguments, start_node_handles) =
match resolve_locate_nodes_start_node_inputs(conn, &context, start_node_inputs).await {
Ok(arguments) => arguments,
Err(error) => return DevToolsCommandExecutionOutput::new(Err(error)),
};
let call_function = locate_nodes_call_function_command(command, start_node_arguments);
let output = Box::pin(execute_devtools_runtime_command_async_with_protocol_events(
conn,
DevToolsCommand::CallFunction(call_function),
))
.await;
let (result, protocol_events, renderer_output_predecessor) = output.into_parts();
release_locate_nodes_start_node_handles(conn, context, start_node_handles).await;
let result = match result {
Ok(result) => {
locate_nodes_result_from_script_result_async(conn, &target, result, &locator).await
}
Err(error) => Err(error),
};
DevToolsCommandExecutionOutput::from_parts(result, protocol_events, renderer_output_predecessor)
}
pub(super) async fn execute_devtools_locate_nodes_context_command_async(
conn: &mut CdpConnection,
command: DevToolsLocateNodesCommand,
) -> DevToolsCommandExecutionOutput {
let DevToolsLocateNodesCommand {
context,
locator,
start_nodes,
start_node_references,
serialization_options,
..
} = command;
let DevToolsLocateNodesLocator::Context(ref frame_id) = locator else {
return DevToolsCommandExecutionOutput::new(Err(DevToolsError::new(
DevToolsErrorKind::Internal,
"UnexpectedLocateNodesContextLocator",
)));
};
if !start_nodes.is_empty() || !start_node_references.is_empty() {
return DevToolsCommandExecutionOutput::new(Err(DevToolsError::new(
DevToolsErrorKind::InvalidArgument,
"Start nodes are not supported",
)));
}
let owner_result = match crate::domains::dom::execute_devtools_dom_command_async(
conn,
DevToolsCommand::GetFrameOwner(DevToolsGetFrameOwnerCommand {
context: context.clone(),
frame_id: frame_id.clone(),
}),
)
.await
.map_err(locate_nodes_context_owner_error)
{
Ok(result) => result,
Err(error) => return DevToolsCommandExecutionOutput::new(Err(error)),
};
let DevToolsCommandResult::GetFrameOwner(owner_result) = owner_result else {
return DevToolsCommandExecutionOutput::new(Err(DevToolsError::new(
DevToolsErrorKind::Internal,
"UnexpectedLocateNodesFrameOwnerResult",
)));
};
let owner_node_id = owner_result.node_id;
let max_dom_depth = serialization_options
.as_ref()
.and_then(|options| options.max_dom_depth);
let (start_node_arguments, start_node_handles) =
match resolve_locate_nodes_start_node_reference_arguments(
conn,
&context,
vec![DevToolsDomNodeReference::BackendNodeId(
owner_result.backend_node_id,
)],
)
.await
.map_err(locate_nodes_context_owner_error)
{
Ok(arguments) => arguments,
Err(error) => return DevToolsCommandExecutionOutput::new(Err(error)),
};
let call_function_context = context.clone();
let call_function = DevToolsCallFunctionCommand {
context: call_function_context,
realm_id: None,
world_name: None,
object_id: None,
this_parameter: None,
function_declaration: r#"function(node) {
if (!node) {
throw new Error('Context does not exist');
}
return [node];
}"#
.to_owned(),
arguments: start_node_arguments,
await_promise: false,
user_gesture: false,
webdriver_bidi_file_prompt_handler: None,
result_ownership: DevToolsResultOwnership::None,
object_group: None,
preserve_remote_metadata: false,
materialize_bidi_script_result: false,
serialization_options: Some(DevToolsSerializationOptions {
max_object_depth: Some(1),
max_dom_depth,
include_shadow_tree: serialization_options
.as_ref()
.and_then(|options| options.include_shadow_tree.clone()),
}),
};
let output = Box::pin(execute_devtools_runtime_command_async_with_protocol_events(
conn,
DevToolsCommand::CallFunction(call_function),
))
.await;
let (result, protocol_events, renderer_output_predecessor) = output.into_parts();
release_locate_nodes_start_node_handles(conn, context, start_node_handles).await;
let result = result.and_then(|result| {
let mut result = locate_nodes_result_from_script_result(result, &locator)?;
if let DevToolsCommandResult::LocateNodes(result) = &mut result
&& result.node_ids.is_empty()
{
result.node_ids.push(owner_node_id);
if let Some(node) = result.nodes.first_mut() {
node.node_id = Some(owner_node_id);
}
}
Ok(result)
});
DevToolsCommandExecutionOutput::from_parts(result, protocol_events, renderer_output_predecessor)
}
pub(super) fn locate_nodes_context_owner_error(error: DevToolsError) -> DevToolsError {
match error.kind {
DevToolsErrorKind::NoSuchSession | DevToolsErrorKind::NoSuchTarget => error,
_ => DevToolsError::new(DevToolsErrorKind::InvalidArgument, "Context does not exist"),
}
}
pub(super) fn locate_nodes_call_function_command(
command: DevToolsLocateNodesCommand,
resolved_start_node_arguments: Vec<Value>,
) -> DevToolsCallFunctionCommand {
let DevToolsLocateNodesCommand {
context,
locator,
max_node_count,
start_nodes,
start_node_references: _,
serialization_options,
} = command;
let max_node_count = max_node_count.unwrap_or(0);
let mut arguments = locate_nodes_locator_arguments(&locator, max_node_count);
arguments.extend(resolved_start_node_arguments);
arguments.extend(start_nodes);
let max_dom_depth = serialization_options
.as_ref()
.and_then(|options| options.max_dom_depth);
DevToolsCallFunctionCommand {
context,
realm_id: None,
world_name: None,
object_id: None,
this_parameter: None,
function_declaration: locate_nodes_function_declaration(&locator).to_owned(),
arguments,
await_promise: false,
user_gesture: false,
webdriver_bidi_file_prompt_handler: None,
result_ownership: DevToolsResultOwnership::None,
object_group: None,
preserve_remote_metadata: false,
materialize_bidi_script_result: false,
serialization_options: Some(DevToolsSerializationOptions {
max_object_depth: Some(2),
max_dom_depth,
include_shadow_tree: serialization_options
.as_ref()
.and_then(|options| options.include_shadow_tree.clone()),
}),
}
}
pub(super) async fn locate_nodes_start_node_inputs_async(
conn: &mut CdpConnection,
target: &DevToolsRuntimeTarget,
start_nodes: Vec<Value>,
start_node_references: Vec<DevToolsDomNodeReference>,
) -> Result<Vec<LocateNodesStartNodeInput>, DevToolsError> {
let mut inputs = start_node_references
.into_iter()
.map(LocateNodesStartNodeInput::Reference)
.collect::<Vec<_>>();
for node in start_nodes {
let Some(object) = node.as_object() else {
return Err(DevToolsError::new(
DevToolsErrorKind::InvalidArgument,
"locateNodes startNodes entries must be node remote values",
));
};
if let Some(shared_id) = object.get("sharedId").and_then(Value::as_str) {
if let Some(reference) = renderer_locate_nodes_start_node_reference_for_shared_id_async(
conn, target, shared_id,
)
.await?
{
inputs.push(LocateNodesStartNodeInput::Reference(reference));
} else {
inputs.push(LocateNodesStartNodeInput::Raw(node));
}
continue;
}
if object.get("handle").and_then(Value::as_str).is_some() {
inputs.push(LocateNodesStartNodeInput::Raw(node));
continue;
}
return Err(DevToolsError::new(
DevToolsErrorKind::InvalidArgument,
"locateNodes startNodes entries must be node remote values",
));
}
Ok(inputs)
}
pub(super) async fn renderer_locate_nodes_start_node_reference_for_shared_id_async(
conn: &mut CdpConnection,
target: &DevToolsRuntimeTarget,
shared_id: &str,
) -> Result<Option<DevToolsDomNodeReference>, DevToolsError> {
let owner = CommandOwnerScope::for_route(target.route.clone());
let result = conn
.document_bidi_node_binding_for_owner_async(&owner, shared_id)
.await;
match result {
Ok(RendererDomBidiNodeBindingResolution::BackendNodeId(backend_node_id)) => Ok(Some(
DevToolsDomNodeReference::BackendNodeId(backend_node_id),
)),
Ok(RendererDomBidiNodeBindingResolution::NotFound) => Ok(None),
Err(error) => {
tracing::debug!(
%error,
%shared_id,
"failed to resolve locateNodes start node through renderer BiDi binding"
);
Ok(None)
}
}
}
pub(super) async fn resolve_locate_nodes_start_node_inputs(
conn: &mut CdpConnection,
context: &DevToolsCommandContext,
inputs: Vec<LocateNodesStartNodeInput>,
) -> Result<(Vec<Value>, Vec<DevToolsRemoteHandleId>), DevToolsError> {
let mut arguments = Vec::with_capacity(inputs.len());
let mut handles = Vec::new();
for input in inputs {
match input {
LocateNodesStartNodeInput::Raw(value) => arguments.push(value),
LocateNodesStartNodeInput::Reference(reference) => {
let resolved = resolve_locate_nodes_start_node_reference_arguments(
conn,
context,
vec![reference],
)
.await;
let (mut resolved_arguments, mut resolved_handles) = match resolved {
Ok(resolved) => resolved,
Err(error) => {
release_locate_nodes_start_node_handles(conn, context.clone(), handles)
.await;
return Err(error);
}
};
arguments.append(&mut resolved_arguments);
handles.append(&mut resolved_handles);
}
}
}
Ok((arguments, handles))
}
pub(super) async fn resolve_locate_nodes_start_node_reference_arguments(
conn: &mut CdpConnection,
context: &DevToolsCommandContext,
references: Vec<DevToolsDomNodeReference>,
) -> Result<(Vec<Value>, Vec<DevToolsRemoteHandleId>), DevToolsError> {
let mut arguments = Vec::with_capacity(references.len());
let mut handles = Vec::with_capacity(references.len());
for reference in references {
let result = crate::domains::dom::execute_devtools_dom_command_async(
conn,
DevToolsCommand::ResolveNode(DevToolsResolveNodeCommand {
context: context.clone(),
reference,
execution_context_id: None,
object_group: Some(LOCATE_NODES_START_NODE_OBJECT_GROUP.to_owned()),
}),
)
.await;
let result = match result {
Ok(result) => result,
Err(error) => {
release_locate_nodes_start_node_handles(conn, context.clone(), handles).await;
return Err(error);
}
};
let DevToolsCommandResult::ResolveNode(result) = result else {
release_locate_nodes_start_node_handles(conn, context.clone(), handles).await;
return Err(DevToolsError::new(
DevToolsErrorKind::Internal,
"LocateNodesStartNodeResolveReturnedUnexpectedResult",
));
};
let Some(object_id) = result
.object
.get("objectId")
.and_then(Value::as_str)
.map(DevToolsRemoteHandleId::from)
else {
release_locate_nodes_start_node_handles(conn, context.clone(), handles).await;
return Err(DevToolsError::new(
DevToolsErrorKind::NoSuchNode,
"start node is no longer attached to the DOM",
));
};
arguments.push(json!({ "handle": object_id.as_str() }));
handles.push(object_id);
}
Ok((arguments, handles))
}
pub(super) async fn release_locate_nodes_start_node_handles(
conn: &mut CdpConnection,
context: DevToolsCommandContext,
handles: Vec<DevToolsRemoteHandleId>,
) {
if handles.is_empty() {
return;
}
if let Err(error) = execute_devtools_release_objects_command_async(
conn,
DevToolsReleaseObjectsCommand {
context,
realm_id: None,
world_name: None,
handles,
},
)
.await
{
tracing::debug!(?error, "failed to release locateNodes start node handles");
}
}
pub(super) fn locate_nodes_locator_arguments(
locator: &DevToolsLocateNodesLocator,
max_node_count: u64,
) -> Vec<Value> {
match locator {
DevToolsLocateNodesLocator::Css(selector)
| DevToolsLocateNodesLocator::XPath(selector)
| DevToolsLocateNodesLocator::TagName(selector) => {
vec![json!(selector), json!(max_node_count)]
}
DevToolsLocateNodesLocator::LinkText { value, match_type } => {
vec![
json!(value),
json!(matches!(match_type, DevToolsLocateNodesTextMatch::Full)),
json!(max_node_count),
]
}
DevToolsLocateNodesLocator::Context(_) => {
unreachable!("context locator is handled before selector delegate")
}
DevToolsLocateNodesLocator::InnerText {
value,
ignore_case,
match_type,
max_depth,
} => vec![
json!(value),
json!(matches!(match_type, DevToolsLocateNodesTextMatch::Full)),
json!(*ignore_case),
json!(max_node_count),
json!(max_depth),
],
DevToolsLocateNodesLocator::Accessibility { role, name } => vec![
json!(name.as_deref().unwrap_or_default()),
json!(role.as_deref().unwrap_or_default()),
json!(max_node_count),
],
}
}
pub(super) fn locate_nodes_function_declaration(
locator: &DevToolsLocateNodesLocator,
) -> &'static str {
match locator {
DevToolsLocateNodesLocator::Context(_) => {
unreachable!("context locator is handled before selector delegate")
}
DevToolsLocateNodesLocator::Css(_) => {
r#"function(cssSelector, maxNodeCount, ...startNodes) {
const locatedNodeRecords = (nodes) => {
const returned = maxNodeCount === 0 ? nodes : nodes.slice(0, maxNodeCount);
return returned.map((node) => ({
backendNodeId: __moliHostResolveBackendNodeIdForObject(node),
node,
}));
};
const locateNodesUsingCss = (node) => {
if (!(node instanceof HTMLElement ||
node instanceof Document ||
node instanceof DocumentFragment ||
node instanceof SVGElement)) {
throw new Error('startNodes in css selector should be HTMLElement, SVGElement or Document or DocumentFragment');
}
return Array.from(node.querySelectorAll(cssSelector));
};
startNodes = startNodes.length > 0
? startNodes.filter(Boolean)
: [document];
const returnedNodes = startNodes.flatMap((startNode) => locateNodesUsingCss(startNode));
return locatedNodeRecords(returnedNodes);
}"#
}
DevToolsLocateNodesLocator::XPath(_) => {
r#"function(xPathSelector, maxNodeCount, ...startNodes) {
const locatedNodeRecords = (nodes) => {
const returned = maxNodeCount === 0 ? nodes : nodes.slice(0, maxNodeCount);
return returned.map((node) => ({
backendNodeId: __moliHostResolveBackendNodeIdForObject(node),
node,
}));
};
const locateNodesUsingXpath = (node) => {
const documentForNode = node.nodeType === Node.DOCUMENT_NODE ? node : node.ownerDocument;
const xPathResult = documentForNode.evaluate(
xPathSelector,
node,
null,
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
null
);
const returnedNodes = [];
for (let index = 0; index < xPathResult.snapshotLength; index += 1) {
returnedNodes.push(xPathResult.snapshotItem(index));
}
return returnedNodes;
};
startNodes = startNodes.length > 0
? startNodes.filter(Boolean)
: [document];
const returnedNodes = startNodes.flatMap((startNode) => locateNodesUsingXpath(startNode));
return locatedNodeRecords(returnedNodes);
}"#
}
DevToolsLocateNodesLocator::TagName(_) => {
r#"function(tagName, maxNodeCount, ...startNodes) {
const locatedNodeRecords = (nodes) => {
const returned = maxNodeCount === 0 ? nodes : nodes.slice(0, maxNodeCount);
return returned.map((node) => ({
backendNodeId: __moliHostResolveBackendNodeIdForObject(node),
node,
}));
};
if (tagName === '') {
throw new Error('Unable to locate an element with the tagName ""');
}
const locateNodesUsingTagName = (node) => {
if (!(node instanceof HTMLElement ||
node instanceof Document ||
node instanceof DocumentFragment ||
node instanceof SVGElement)) {
throw new Error('startNodes in tag name should be HTMLElement, SVGElement or Document or DocumentFragment');
}
return Array.from(node.getElementsByTagName(tagName));
};
startNodes = startNodes.length > 0
? startNodes.filter(Boolean)
: [document];
const returnedNodes = startNodes.flatMap((startNode) => locateNodesUsingTagName(startNode));
return locatedNodeRecords(returnedNodes);
}"#
}
DevToolsLocateNodesLocator::LinkText { .. } => {
r#"function(linkText, fullMatch, maxNodeCount, ...startNodes) {
const locatedNodeRecords = (nodes) => {
const returned = maxNodeCount === 0 ? nodes : nodes.slice(0, maxNodeCount);
return returned.map((node) => ({
backendNodeId: __moliHostResolveBackendNodeIdForObject(node),
node,
}));
};
const visibleLinkText = (element) => (element.innerText || element.textContent || '').trim();
const linkMatches = (element) => {
const text = visibleLinkText(element);
return fullMatch ? text === linkText : text.includes(linkText);
};
const locateNodesUsingLinkText = (node) => {
if (!(node instanceof HTMLElement ||
node instanceof Document ||
node instanceof DocumentFragment ||
node instanceof SVGElement)) {
throw new Error('startNodes in link text should be HTMLElement, SVGElement or Document or DocumentFragment');
}
return Array.from(node.getElementsByTagName('a')).filter(linkMatches);
};
startNodes = startNodes.length > 0
? startNodes.filter(Boolean)
: [document];
const returnedNodes = startNodes.flatMap((startNode) => locateNodesUsingLinkText(startNode));
return locatedNodeRecords(returnedNodes);
}"#
}
DevToolsLocateNodesLocator::InnerText { .. } => {
r#"function(innerTextSelector, fullMatch, ignoreCase, maxNodeCount, maxDepth, ...startNodes) {
const locatedNodeRecords = (nodes) => {
const returned = maxNodeCount === 0 ? nodes : nodes.slice(0, maxNodeCount);
return returned.map((node) => ({
backendNodeId: __moliHostResolveBackendNodeIdForObject(node),
node,
}));
};
const searchText = ignoreCase ? innerTextSelector.toUpperCase() : innerTextSelector;
const locateNodesUsingInnerText = (node, currentMaxDepth) => {
const returnedNodes = [];
if (node instanceof DocumentFragment || node instanceof Document) {
for (const child of node.children) {
returnedNodes.push(...locateNodesUsingInnerText(child, currentMaxDepth));
}
return returnedNodes;
}
if (!(node instanceof HTMLElement)) {
return [];
}
const nodeInnerText = ignoreCase ? node.innerText?.toUpperCase() : node.innerText;
if (!nodeInnerText || !nodeInnerText.includes(searchText)) {
return [];
}
const childNodes = Array.from(node.children).filter((child) => child instanceof HTMLElement);
if (childNodes.length === 0) {
if (!fullMatch || nodeInnerText === searchText) {
returnedNodes.push(node);
}
return returnedNodes;
}
const childNodeMatches = currentMaxDepth <= 0
? []
: childNodes.flatMap((child) => locateNodesUsingInnerText(child, currentMaxDepth - 1));
if (childNodeMatches.length === 0) {
if (!fullMatch || nodeInnerText === searchText) {
returnedNodes.push(node);
}
} else {
returnedNodes.push(...childNodeMatches);
}
return returnedNodes;
};
startNodes = startNodes.length > 0
? startNodes.filter(Boolean)
: [document];
const returnedNodes = startNodes.flatMap((startNode) => locateNodesUsingInnerText(startNode, maxDepth));
return locatedNodeRecords(returnedNodes);
}"#
}
DevToolsLocateNodesLocator::Accessibility { .. } => {
r#"function(name, role, maxNodeCount, ...startNodes) {
const locatedNodeRecords = (nodes) => {
const returned = maxNodeCount === 0 ? nodes : nodes.slice(0, maxNodeCount);
return returned.map((node) => ({
backendNodeId: __moliHostResolveBackendNodeIdForObject(node),
node,
}));
};
const implicitRole = (element) => {
const localName = element.localName;
if (/^h[1-6]$/.test(localName)) { return 'heading'; }
if (localName === 'article') { return 'article'; }
if (localName === 'button') { return 'button'; }
if (localName === 'input' && String(element.type || 'text') === 'text') { return 'textbox'; }
if (localName === 'input' && String(element.type || '') === 'search') { return 'searchbox'; }
if (localName === 'a' && element.hasAttribute('href')) { return 'link'; }
return '';
};
const accessibleRole = (element) => element.getAttribute('role') || implicitRole(element);
const accessibleName = (element) => {
if (element.hasAttribute('aria-label')) { return element.getAttribute('aria-label') || ''; }
return (element.innerText || element.textContent || '').trim();
};
const returnedNodes = [];
let aborted = false;
const collect = (contextNodes) => {
if (aborted) { return; }
for (const contextNode of contextNodes) {
if (!(contextNode instanceof HTMLElement || contextNode instanceof SVGElement)) {
continue;
}
let matches = true;
if (role && accessibleRole(contextNode) !== role) { matches = false; }
if (name && accessibleName(contextNode) !== name) { matches = false; }
if (matches) {
if (maxNodeCount !== 0 && returnedNodes.length === maxNodeCount) {
aborted = true;
break;
}
returnedNodes.push(contextNode);
}
collect(Array.from(contextNode.children));
}
};
startNodes = startNodes.length > 0
? startNodes.filter(Boolean)
: Array.from(document.documentElement.children).filter((child) => child instanceof HTMLElement || child instanceof SVGElement);
collect(startNodes);
return locatedNodeRecords(returnedNodes);
}"#
}
}
}
pub(super) async fn locate_nodes_result_from_script_result_async(
conn: &mut CdpConnection,
target: &DevToolsRuntimeTarget,
result: DevToolsCommandResult,
locator: &DevToolsLocateNodesLocator,
) -> Result<DevToolsCommandResult, DevToolsError> {
let mut result = locate_nodes_result_from_script_result(result, locator)?;
if let DevToolsCommandResult::LocateNodes(result) = &mut result {
materialize_locate_nodes_result_node_ids_async(conn, target, result).await;
}
Ok(result)
}
pub(super) async fn materialize_locate_nodes_result_node_ids_async(
conn: &mut CdpConnection,
target: &DevToolsRuntimeTarget,
result: &mut DevToolsLocateNodesResult,
) {
let owner = CommandOwnerScope::for_route(target.route.clone());
for index in 0..result.nodes.len() {
let shared_id = result.nodes[index]
.shared_id
.as_ref()
.map(|shared_id| shared_id.as_str().to_owned());
let mut materialized = false;
if let Some(shared_id) = shared_id {
let snapshot = conn
.document_node_snapshot_for_runtime_remote_object_id_for_owner_async(
&owner, &shared_id, 0, false,
)
.await;
if let Ok(Some(snapshot)) = snapshot {
result.nodes[index].node_id =
frontend_node_id_for_locate_node_snapshot(&snapshot.snapshot);
result.nodes[index].backend_node_id = snapshot
.snapshot
.backend_node_id
.or(result.nodes[index].backend_node_id);
materialized = true;
}
}
if !materialized
&& let Some(backend_node_id) = result.nodes[index].backend_node_id
&& let Ok(Some(snapshot)) = conn
.document_node_snapshot_for_backend_node_id_for_owner_async(
&owner,
backend_node_id,
0,
false,
)
.await
{
result.nodes[index].node_id =
frontend_node_id_for_locate_node_snapshot(&snapshot.snapshot);
result.nodes[index].backend_node_id =
snapshot.snapshot.backend_node_id.or(Some(backend_node_id));
}
}
result.node_ids = result
.nodes
.iter()
.filter_map(|node| node.node_id)
.collect();
}
pub(super) fn frontend_node_id_for_locate_node_snapshot(
snapshot: &DocumentNodeSnapshot,
) -> Option<u32> {
snapshot.frontend_node_id
}
pub(super) fn locate_nodes_result_from_script_result(
result: DevToolsCommandResult,
locator: &DevToolsLocateNodesLocator,
) -> Result<DevToolsCommandResult, DevToolsError> {
let DevToolsCommandResult::Script(result) = result else {
return Err(DevToolsError::new(
DevToolsErrorKind::Internal,
"UnexpectedLocateNodesResult",
));
};
match *result {
DevToolsScriptResult::Value(value) => {
let Some(deep_serialized_value) = value.deep_serialized_value else {
return Err(DevToolsError::new(
DevToolsErrorKind::Internal,
"LocateNodesMissingSerializedArray",
));
};
let Some(nodes) =
locate_nodes_remote_values_from_deep_serialized_array(&deep_serialized_value)
else {
return Err(DevToolsError::new(
DevToolsErrorKind::Internal,
"LocateNodesUnexpectedSerializedArray",
));
};
Ok(DevToolsCommandResult::LocateNodes(
DevToolsLocateNodesResult {
node_ids: nodes.iter().filter_map(|node| node.node_id).collect(),
nodes,
},
))
}
DevToolsScriptResult::Exception(exception) => {
Err(locate_nodes_error_from_exception(exception, locator))
}
}
}
pub(super) fn locate_nodes_error_from_exception(
exception: DevToolsScriptException,
locator: &DevToolsLocateNodesLocator,
) -> DevToolsError {
let text = locate_nodes_exception_text(&exception);
if locate_nodes_exception_is_invalid_selector(&text, locator) {
return DevToolsError::new(DevToolsErrorKind::InvalidSelector, text);
}
if text
== "Error: startNodes in css selector should be HTMLElement, SVGElement or Document or DocumentFragment"
|| text
== "Error: startNodes in tag name should be HTMLElement, SVGElement or Document or DocumentFragment"
{
return DevToolsError::new(DevToolsErrorKind::InvalidArgument, text);
}
if text == "Error: Context does not exist" {
return DevToolsError::new(DevToolsErrorKind::InvalidArgument, text);
}
DevToolsError::new(
DevToolsErrorKind::Internal,
format!("Unexpected error in selector script: {text}"),
)
}
pub(super) fn locate_nodes_exception_is_invalid_selector(
text: &str,
locator: &DevToolsLocateNodesLocator,
) -> bool {
(text.starts_with("SyntaxError: Failed to execute 'querySelectorAll' on '")
&& text.ends_with(" is not a valid selector."))
|| text.starts_with("SyntaxError: Failed to execute 'evaluate' on 'Document': ")
|| text.starts_with("DOMException: Failed to execute 'evaluate' on 'Document': ")
|| text.starts_with("DOMException: The string ")
&& text.ends_with(" is not a valid XPath expression.")
|| text == "DOMException" && matches!(locator, DevToolsLocateNodesLocator::XPath(_))
}
pub(super) fn locate_nodes_exception_text(exception: &DevToolsScriptException) -> String {
if exception.text != "Uncaught" {
return exception.text.clone();
}
if let Some(description) = exception
.value
.as_ref()
.and_then(|value| value.description.as_ref())
{
return description.clone();
}
if let Some(value) = exception
.value
.as_ref()
.and_then(|value| value.value.as_str())
{
return value.to_owned();
}
exception.text.clone()
}
pub(super) fn locate_nodes_remote_values_from_deep_serialized_array(
value: &Value,
) -> Option<Vec<DevToolsRemoteValue>> {
let object = value.as_object()?;
if object.get("type").and_then(Value::as_str) != Some("array") {
return None;
}
object
.get("value")?
.as_array()?
.iter()
.map(locate_node_remote_value_from_deep_serialized)
.collect()
}
pub(super) fn locate_node_remote_value_from_deep_serialized(
value: &Value,
) -> Option<DevToolsRemoteValue> {
if let Some((backend_node_id, node)) = locate_node_record_from_deep_serialized(value) {
let mut remote = locate_node_remote_value_from_deep_serialized_node(node)?;
remote.backend_node_id = backend_node_id;
return Some(remote);
}
locate_node_remote_value_from_deep_serialized_node(value)
}
pub(super) fn locate_node_record_from_deep_serialized(
value: &Value,
) -> Option<(Option<u32>, &Value)> {
let object = value.as_object()?;
if object.get("type").and_then(Value::as_str) != Some("object") {
return None;
}
let properties = object.get("value")?.as_array()?;
let node = deep_serialized_property(properties, "node")?;
let backend_node_id = deep_serialized_property(properties, "backendNodeId")
.and_then(deep_serialized_number_value)
.and_then(|id| u32::try_from(id).ok());
Some((backend_node_id, node))
}
pub(super) fn deep_serialized_number_value(value: &Value) -> Option<u64> {
let object = value.as_object()?;
if object.get("type").and_then(Value::as_str) != Some("number") {
return None;
}
object.get("value")?.as_u64()
}
pub(super) fn locate_node_remote_value_from_deep_serialized_node(
value: &Value,
) -> Option<DevToolsRemoteValue> {
let object = value.as_object()?;
if object.get("type").and_then(Value::as_str) != Some("node") {
return None;
}
let shared_id = object
.get("sharedId")
.and_then(Value::as_str)
.map(|shared_id| DevToolsRemoteHandleId::from(shared_id.to_owned()));
Some(DevToolsRemoteValue {
value: Value::Null,
handle: None,
shared_id,
node_id: None,
backend_node_id: None,
window_context: None,
realm: None,
remote_type: Some("object".to_owned()),
remote_subtype: Some("node".to_owned()),
unserializable_value: None,
description: None,
class_name: None,
deep_serialized_value: None,
node_value: object.get("value").cloned(),
})
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,556 @@
use super::*;
pub(super) fn cdp_value_from_devtools_argument(argument: &Value) -> Option<Value> {
let Some(map) = argument.as_object() else {
return Some(argument.clone());
};
if bidi_remote_reference_object_id(map).is_some() {
return None;
}
match map.get("type").and_then(Value::as_str) {
Some("null") => Some(Value::Null),
Some("array") => map.get("value")?.as_array().and_then(|values| {
values
.iter()
.map(cdp_value_from_devtools_argument)
.collect::<Option<Vec<_>>>()
.map(Value::Array)
}),
Some("object") => map.get("value")?.as_array().and_then(|entries| {
let mut object = serde_json::Map::new();
for entry in entries {
let pair = entry.as_array()?;
let [key, value] = pair.as_slice() else {
return None;
};
object.insert(
key.as_str()?.to_owned(),
cdp_value_from_devtools_argument(value)?,
);
}
Some(Value::Object(object))
}),
Some("string" | "number" | "boolean" | "bigint" | "date") => map.get("value").cloned(),
None => map.get("value").cloned(),
_ => None,
}
}
pub(super) fn devtools_call_function_remote_object_ids(
command: &DevToolsCallFunctionCommand,
) -> Vec<String> {
let mut object_ids = Vec::new();
if let Some(object_id) = command.object_id.as_ref() {
object_ids.push(object_id.as_str().to_owned());
}
if let Some(this_parameter) = &command.this_parameter {
collect_devtools_remote_object_ids(this_parameter, &mut object_ids);
}
for argument in &command.arguments {
collect_devtools_remote_object_ids(argument, &mut object_ids);
}
object_ids.sort();
object_ids.dedup();
object_ids
}
pub(super) fn devtools_call_function_remote_references(
command: &DevToolsCallFunctionCommand,
) -> Vec<RuntimeRemoteReference> {
let mut references = Vec::new();
if let Some(object_id) = command.object_id.as_ref() {
references.push(RuntimeRemoteReference {
object_id: object_id.as_str().to_owned(),
kind: RuntimeRemoteReferenceKind::Object,
});
}
if let Some(this_parameter) = &command.this_parameter {
collect_devtools_remote_references(this_parameter, &mut references);
}
for argument in &command.arguments {
collect_devtools_remote_references(argument, &mut references);
}
references.sort();
references.dedup();
references
}
pub(super) fn collect_devtools_remote_object_ids(value: &Value, out: &mut Vec<String>) {
let mut stack = vec![(value, MAX_INSPECTOR_PROTOCOL_VALUE_DEPTH)];
while let Some((value, remaining_tree_depth)) = stack.pop() {
let Some(next_tree_depth) = remaining_tree_depth.checked_sub(1) else {
continue;
};
match value {
Value::Object(map) => {
for key in ["objectId", "promiseObjectId"] {
if let Some(object_id) = map.get(key).and_then(Value::as_str) {
out.push(object_id.to_owned());
}
}
if let Some(object_id) = bidi_remote_reference_object_id(map) {
out.push(object_id.to_owned());
}
for child in map.values() {
stack.push((child, next_tree_depth));
}
}
Value::Array(values) => {
for child in values {
stack.push((child, next_tree_depth));
}
}
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
}
}
}
pub(super) fn collect_devtools_remote_references(
value: &Value,
out: &mut Vec<RuntimeRemoteReference>,
) {
let mut stack = vec![(value, MAX_INSPECTOR_PROTOCOL_VALUE_DEPTH)];
while let Some((value, remaining_tree_depth)) = stack.pop() {
let Some(next_tree_depth) = remaining_tree_depth.checked_sub(1) else {
continue;
};
match value {
Value::Object(map) => {
let mut found_cdp_remote_reference = false;
for key in ["objectId", "promiseObjectId"] {
if let Some(object_id) = map.get(key).and_then(Value::as_str) {
out.push(RuntimeRemoteReference {
object_id: object_id.to_owned(),
kind: RuntimeRemoteReferenceKind::Object,
});
found_cdp_remote_reference = true;
}
}
if found_cdp_remote_reference {
continue;
}
if let Some(shared_id) = map.get("sharedId").and_then(Value::as_str) {
out.push(RuntimeRemoteReference {
object_id: shared_id.to_owned(),
kind: if map.get("type").and_then(Value::as_str) == Some("node") {
RuntimeRemoteReferenceKind::Node
} else {
RuntimeRemoteReferenceKind::Object
},
});
continue;
} else if let Some(handle) = map.get("handle").and_then(Value::as_str) {
out.push(RuntimeRemoteReference {
object_id: handle.to_owned(),
kind: RuntimeRemoteReferenceKind::Object,
});
continue;
}
for child in map.values() {
stack.push((child, next_tree_depth));
}
}
Value::Array(values) => {
for child in values {
stack.push((child, next_tree_depth));
}
}
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
}
}
}
pub(super) fn devtools_script_result_from_response(
response: Value,
result_ownership: DevToolsResultOwnership,
target_realm: Option<DevToolsRealmId>,
) -> Result<DevToolsCommandResult, DevToolsError> {
if let Some(error) = response.get("error") {
return Err(devtools_error_from_cdp_error_value(error));
}
let result = response.get("result").unwrap_or(&Value::Null);
if let Some(exception_details) = result.get("exceptionDetails") {
let mut exception = devtools_script_exception_from_cdp(
exception_details,
matches!(result_ownership, DevToolsResultOwnership::Root),
);
if exception.realm.is_none() {
exception.realm = target_realm;
}
return Ok(DevToolsCommandResult::Script(Box::new(
DevToolsScriptResult::Exception(exception),
)));
}
let remote = result.get("result").unwrap_or(&Value::Null);
Ok(DevToolsCommandResult::Script(Box::new(
DevToolsScriptResult::Value(devtools_remote_value_from_cdp(
remote,
matches!(result_ownership, DevToolsResultOwnership::Root),
target_realm,
)),
)))
}
pub(super) fn devtools_empty_result_from_response(
response: Value,
) -> Result<DevToolsCommandResult, DevToolsError> {
if let Some(error) = response.get("error") {
return Err(devtools_error_from_cdp_error_value(error));
}
Ok(DevToolsCommandResult::Empty)
}
pub(super) fn validate_protocol_neutral_runtime_handle_realms(
conn: &CdpConnection,
owner: &CommandOwnerScope,
command: &DevToolsCommand,
target_realm: Option<&DevToolsRealmId>,
) -> Result<(), DevToolsError> {
let DevToolsCommand::CallFunction(command) = command else {
return Ok(());
};
if command.context.protocol != DevToolsProtocol::WebDriverBidi {
return Ok(());
}
let references = devtools_call_function_remote_references(command);
for reference in references {
if !conn.runtime_remote_object_id_known_for_owner(owner, &reference.object_id) {
return Err(match reference.kind {
RuntimeRemoteReferenceKind::Node => DevToolsError::new(
DevToolsErrorKind::NoSuchNode,
"Could not find node with given id",
),
RuntimeRemoteReferenceKind::Object => DevToolsError::new(
DevToolsErrorKind::NoSuchHandle,
"Cannot find object with given id",
),
});
}
if reference.kind == RuntimeRemoteReferenceKind::Object
&& let Some(target_realm) = target_realm
&& let Some(owner_realm) =
conn.runtime_remote_object_realm_for_owner(owner, &reference.object_id)
&& owner_realm != target_realm.as_str()
{
return Err(DevToolsError::new(
DevToolsErrorKind::NoSuchHandle,
"Cannot find object with given id",
));
}
}
Ok(())
}
pub(super) fn register_devtools_script_result_remote_object_realm(
conn: &mut CdpConnection,
owner: &CommandOwnerScope,
result: &DevToolsCommandResult,
realm_id: Option<&DevToolsRealmId>,
) {
let (DevToolsCommandResult::Script(result), Some(realm_id)) = (result, realm_id) else {
return;
};
let DevToolsScriptResult::Value(value) = result.as_ref() else {
return;
};
let Some(remote_object_id) = value.handle.as_ref().or(value.shared_id.as_ref()) else {
return;
};
if let Some(object_id) =
conn.runtime_remote_object_alias_for_owner(owner, remote_object_id.as_str())
{
conn.register_runtime_remote_object_alias_for_owner_with_realm(
owner,
remote_object_id.as_str().to_owned(),
object_id,
realm_id.as_str(),
);
return;
}
conn.register_runtime_remote_object_ids_for_owner_with_realm(
owner,
vec![remote_object_id.as_str().to_owned()],
realm_id.as_str(),
);
}
pub(super) fn register_devtools_script_result_remote_object(
conn: &mut CdpConnection,
owner: &CommandOwnerScope,
result: &DevToolsCommandResult,
) {
let Some(value) = devtools_script_result_remote_value(result) else {
return;
};
let Some(remote_object_id) = value.handle.as_ref().or(value.shared_id.as_ref()) else {
return;
};
conn.register_runtime_remote_object_ids_from_value_for_owner(
owner,
&json!({ "objectId": remote_object_id.as_str() }),
);
}
pub(super) fn materialize_devtools_script_window_remote_value(
result: &mut DevToolsCommandResult,
target: &DevToolsRuntimeTarget,
) {
let Some(target_window_context_id) = target.window_context_id.clone() else {
return;
};
let Some(candidate) = devtools_window_remote_candidate(result) else {
return;
};
let window_context_id = if let Some(window_remote) =
deep_serialized_bidi_window_remote_result(candidate.deep_serialized_value.as_ref())
{
match window_remote {
BidiWindowRemoteResult::TargetWindow => Some(target_window_context_id),
BidiWindowRemoteResult::Context(window_context_id) => Some(window_context_id),
}
} else {
None
};
let Some(window_context_id) = window_context_id else {
return;
};
if let Some(value) = devtools_script_result_remote_value_mut(result) {
value.window_context = Some(Box::new(window_context_id));
}
}
pub(super) fn deep_serialized_bidi_window_remote_result(
value: Option<&Value>,
) -> Option<BidiWindowRemoteResult> {
let value = value?;
if value.get("type").and_then(Value::as_str) == Some("window") {
return value
.get("value")
.and_then(|value| value.get("context"))
.and_then(Value::as_str)
.map(|context| BidiWindowRemoteResult::Context(DevToolsTargetId::from(context)));
}
let properties = value.get("value").and_then(Value::as_array)?;
let marker = deep_serialized_property(properties, "__moliBidiRemoteValue")?;
if deep_serialized_bool_value(marker) != Some(true) {
return None;
}
let type_value = deep_serialized_property(properties, "type")?;
if deep_serialized_string_value(type_value).as_deref() != Some("window") {
return None;
}
if let Some(target_window) = deep_serialized_property(properties, "targetWindow")
&& deep_serialized_bool_value(target_window) == Some(true)
{
return Some(BidiWindowRemoteResult::TargetWindow);
}
let context = deep_serialized_property(properties, "context")?;
deep_serialized_string_value(context)
.map(|context| BidiWindowRemoteResult::Context(DevToolsTargetId::from(context)))
}
pub(super) fn deep_serialized_bool_value(value: &Value) -> Option<bool> {
let object = value.as_object()?;
(object.get("type").and_then(Value::as_str) == Some("boolean"))
.then(|| object.get("value")?.as_bool())
.flatten()
}
pub(super) fn deep_serialized_string_value(value: &Value) -> Option<String> {
let object = value.as_object()?;
(object.get("type").and_then(Value::as_str) == Some("string"))
.then(|| object.get("value")?.as_str().map(str::to_owned))
.flatten()
}
pub(super) fn devtools_window_remote_candidate(
result: &DevToolsCommandResult,
) -> Option<DevToolsWindowRemoteCandidate> {
let value = devtools_script_result_remote_value(result)?;
Some(DevToolsWindowRemoteCandidate {
deep_serialized_value: value.deep_serialized_value.clone(),
})
}
pub(super) async fn devtools_probe_remote_value_async(
conn: &mut CdpConnection,
target: DevToolsRuntimeTarget,
command: DevToolsCommand,
) -> Result<Option<DevToolsRemoteValue>, DevToolsError> {
let result_ownership = devtools_runtime_result_ownership(&command);
let internal_command_id = conn.next_internal_runtime_command_id();
let mut step =
start_protocol_neutral_runtime_command(conn, target, command, internal_command_id).await;
loop {
match step {
RuntimeCommandTaskStep::Complete(plan) => {
let (response, _events) = plan
.into_runtime_inspector_response_and_background_events(
internal_command_id,
None,
);
let Some(response) = response else {
return Err(DevToolsError::new(
DevToolsErrorKind::Internal,
"MissingDevToolsCommandResult",
));
};
let result =
devtools_script_result_from_response(response, result_ownership, None)?;
let DevToolsCommandResult::Script(result) = result else {
return Ok(None);
};
let DevToolsScriptResult::Value(value) = *result else {
return Ok(None);
};
return Ok(Some(value));
}
RuntimeCommandTaskStep::Pending(pending) => {
let completed = pending.wait().await;
step = complete_pending_runtime_command(conn, completed).await;
}
}
}
}
pub(super) async fn materialize_devtools_script_deep_serialized_root_value_async(
conn: &mut CdpConnection,
result: &mut DevToolsCommandResult,
serialization_options: Option<&DevToolsSerializationOptions>,
target: &DevToolsRuntimeTarget,
) {
let Some(serialization_options) = serialization_options else {
return;
};
let Some(remote_value) = devtools_script_result_remote_value(result) else {
return;
};
if remote_value.deep_serialized_value.is_some() {
return;
}
if !matches!(remote_value.remote_type.as_deref(), Some("object")) {
return;
}
let Some(root_object_id) = remote_value
.shared_id
.as_ref()
.map(|shared_id| shared_id.as_str().to_owned())
else {
return;
};
let command = DevToolsCommand::CallFunction(DevToolsCallFunctionCommand {
context: DevToolsCommandContext {
protocol: DevToolsProtocol::WebDriverBidi,
session_id: None,
target_id: target.window_context_id.clone(),
browser_context_id: None,
},
realm_id: None,
world_name: None,
object_id: Some(DevToolsRemoteHandleId::from(root_object_id.clone())),
this_parameter: None,
function_declaration: "function() { return this; }".to_owned(),
arguments: Vec::new(),
await_promise: false,
user_gesture: false,
webdriver_bidi_file_prompt_handler: None,
result_ownership: DevToolsResultOwnership::ByValue,
object_group: None,
preserve_remote_metadata: false,
materialize_bidi_script_result: false,
serialization_options: Some(serialization_options.clone()),
});
let deep_serialized_value =
match devtools_probe_remote_value_async(conn, target.clone(), command).await {
Ok(Some(readback)) => readback.deep_serialized_value,
Ok(None) => None,
Err(error) => {
tracing::debug!(
%root_object_id,
?error,
"failed to materialize root BiDi deep serialized value"
);
None
}
};
let Some(deep_serialized_value) = deep_serialized_value else {
return;
};
if let Some(remote_value) = devtools_script_result_remote_value_mut(result) {
remote_value.deep_serialized_value = Some(deep_serialized_value);
}
}
pub(super) fn devtools_script_result_remote_value(
result: &DevToolsCommandResult,
) -> Option<&DevToolsRemoteValue> {
let DevToolsCommandResult::Script(result) = result else {
return None;
};
match result.as_ref() {
DevToolsScriptResult::Value(value) => Some(value),
DevToolsScriptResult::Exception(exception) => exception.value.as_ref(),
}
}
pub(super) fn devtools_script_result_remote_value_mut(
result: &mut DevToolsCommandResult,
) -> Option<&mut DevToolsRemoteValue> {
let DevToolsCommandResult::Script(result) = result else {
return None;
};
match result.as_mut() {
DevToolsScriptResult::Value(value) => Some(value),
DevToolsScriptResult::Exception(exception) => exception.value.as_mut(),
}
}
pub(super) fn bidi_script_message_serialization_options_from_value(
value: &Value,
) -> Result<DevToolsSerializationOptions, String> {
let object = value
.as_object()
.ok_or_else(|| "InvalidChannelArgument".to_owned())?;
let max_object_depth = match object.get("maxObjectDepth") {
Some(Value::Null) | None => None,
Some(value) => Some(
value
.as_u64()
.ok_or_else(|| "InvalidChannelArgument".to_owned())?,
),
};
let max_dom_depth = match object.get("maxDomDepth") {
Some(Value::Null) | None => None,
Some(value) => Some(
value
.as_u64()
.ok_or_else(|| "InvalidChannelArgument".to_owned())?,
),
};
let include_shadow_tree = match object.get("includeShadowTree") {
None => None,
Some(Value::String(value)) if matches!(value.as_str(), "none" | "open" | "all") => {
Some(value.clone())
}
Some(_) => return Err("InvalidChannelArgument".to_owned()),
};
Ok(DevToolsSerializationOptions {
max_object_depth,
max_dom_depth,
include_shadow_tree,
})
}
pub(super) async fn devtools_probe_value_async(
conn: &mut CdpConnection,
target: DevToolsRuntimeTarget,
command: DevToolsCommand,
) -> Result<Option<Value>, DevToolsError> {
let Some(value) = devtools_probe_remote_value_async(conn, target, command).await? else {
return Ok(None);
};
Ok(Some(value.value))
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,151 @@
use super::*;
pub(super) fn start_pending_runtime_disable_command(
conn: &mut CdpConnection,
cmd: &Cmd<'_>,
) -> RuntimeCommandTaskStep {
let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id);
let response_delivery = cmd.terminal_response_delivery();
start_runtime_disable_command_for_owner(conn, cmd, owner_scope, response_delivery)
}
pub(super) fn start_runtime_disable_command_for_owner(
conn: &mut CdpConnection,
cmd: &Cmd<'_>,
owner_scope: CommandOwnerScope,
response_delivery: RendererInspectorResponseDelivery,
) -> RuntimeCommandTaskStep {
if !conn
.runtime_session_owner_slot_for_owner(&owner_scope)
.is_ok_and(|slot| slot.has_loaded_page())
{
return RuntimeCommandTaskStep::Complete(disable_command_output_plan_sync_for_owner(
conn,
&owner_scope,
));
}
let pending = match start_pending_runtime_inspector_dispatch_with_delivery(
conn,
cmd,
&owner_scope,
cmd.json.to_owned(),
response_delivery,
) {
Ok(pending) => pending,
Err(message) => {
return RuntimeCommandTaskStep::Complete(runtime_inspector_error_plan(cmd.id, message));
}
};
RuntimeCommandTaskStep::Pending(Box::new(PendingRuntimeCommandDispatch {
command_id: cmd.id,
action: "disable",
owner_scope,
object_group: None,
release_object_ids: Vec::new(),
release_object_group: None,
await_promise: false,
wait_for_deferred_reply: false,
pending: PendingRuntimeCommandKind::Inspector { pending },
}))
}
pub(super) fn start_runtime_run_if_waiting_for_debugger_command(
conn: &mut CdpConnection,
cmd: &Cmd<'_>,
) -> RuntimeCommandTaskStep {
if !conn
.runtime_session_owner_slot(cmd.session_id)
.is_ok_and(|slot| slot.has_loaded_page())
{
return RuntimeCommandTaskStep::Complete(CommandOutputPlan::success());
}
let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id);
let pending = match start_pending_runtime_inspector_dispatch_with_delivery(
conn,
cmd,
&owner_scope,
cmd.json.to_owned(),
cmd.terminal_response_delivery(),
) {
Ok(pending) => pending,
Err(message) if message == "NoDocumentLoaded" => {
return RuntimeCommandTaskStep::Complete(CommandOutputPlan::success());
}
Err(message) => {
return RuntimeCommandTaskStep::Complete(runtime_inspector_error_plan(cmd.id, message));
}
};
RuntimeCommandTaskStep::Pending(Box::new(PendingRuntimeCommandDispatch {
command_id: cmd.id,
action: "runIfWaitingForDebugger",
owner_scope,
object_group: None,
release_object_ids: Vec::new(),
release_object_group: None,
await_promise: false,
wait_for_deferred_reply: false,
pending: PendingRuntimeCommandKind::Inspector { pending },
}))
}
pub(super) fn apply_runtime_disable_projection_after_success_for_owner(
conn: &mut CdpConnection,
owner: &CommandOwnerScope,
) -> Result<(), String> {
let was_enabled = conn
.target_runtime_session_state_for_owner(owner)
.is_some_and(|state| state.runtime_frontend_enabled);
match conn.set_runtime_frontend_enabled_for_owner(owner, false) {
SessionOwnerRuntimeFrontendEnableResult::Handled => {
advance_runtime_observable_cursors_to_current_for_owner(conn, owner);
if was_enabled {
clear_runtime_binding_definitions_for_owner(conn, owner)?;
}
Ok(())
}
SessionOwnerRuntimeFrontendEnableResult::UnknownSession => {
Err("Runtime.disable succeeded after session owner disappeared".to_owned())
}
}
}
pub(super) fn start_runtime_discard_console_entries_command(
conn: &mut CdpConnection,
cmd: &Cmd<'_>,
) -> RuntimeCommandTaskStep {
if !conn
.runtime_session_owner_slot(cmd.session_id)
.is_ok_and(|slot| slot.has_loaded_page())
{
advance_runtime_observable_cursors_to_current_for_session_owner(conn, cmd.session_id);
return RuntimeCommandTaskStep::Complete(CommandOutputPlan::success());
}
let owner_scope = CommandOwnerScope::capture(conn, cmd.session_id);
let pending = match start_pending_runtime_inspector_dispatch_with_delivery(
conn,
cmd,
&owner_scope,
cmd.json.to_owned(),
cmd.terminal_response_delivery(),
) {
Ok(pending) => pending,
Err(message) => {
return RuntimeCommandTaskStep::Complete(runtime_inspector_error_plan(cmd.id, message));
}
};
RuntimeCommandTaskStep::Pending(Box::new(PendingRuntimeCommandDispatch {
command_id: cmd.id,
action: "discardConsoleEntries",
owner_scope,
object_group: None,
release_object_ids: Vec::new(),
release_object_group: None,
await_promise: false,
wait_for_deferred_reply: false,
pending: PendingRuntimeCommandKind::Inspector { pending },
}))
}
@@ -0,0 +1,464 @@
use super::*;
pub(super) async fn execute_devtools_release_objects_command_async(
conn: &mut CdpConnection,
command: DevToolsReleaseObjectsCommand,
) -> Result<DevToolsCommandResult, DevToolsError> {
let target =
devtools_runtime_target_async(conn, &DevToolsCommand::ReleaseObjects(command.clone()))
.await?;
let target_realm = devtools_realm_id_for_runtime_target_async(conn, &target).await;
let owner = CommandOwnerScope::for_route(target.route);
release_devtools_objects_for_owner_async(conn, &owner, &command.handles, target_realm.as_ref())
.await?;
Ok(DevToolsCommandResult::Empty)
}
pub(super) async fn release_devtools_objects_for_owner_async(
conn: &mut CdpConnection,
owner: &CommandOwnerScope,
handles: &[DevToolsRemoteHandleId],
target_realm: Option<&DevToolsRealmId>,
) -> Result<(), DevToolsError> {
for handle in handles {
let object_id = handle.as_str().to_owned();
if !conn.runtime_remote_object_id_known_for_owner(owner, &object_id) {
continue;
}
if let Some(target_realm) = target_realm
&& let Some(owner_realm) = conn.runtime_remote_object_realm_for_owner(owner, &object_id)
&& owner_realm != target_realm.as_str()
{
continue;
}
let params = json!({ "objectId": object_id });
let command_id = conn.next_internal_runtime_command_id();
let raw_json = runtime_inspector_command_json(command_id, "Runtime.releaseObject", &params);
let response = dispatch_runtime_inspector_command_response_for_owner_async(
conn, owner, raw_json, command_id,
)
.await?;
if let BackgroundCommandResponsePayload::Error { code, message, .. } = response {
let error = devtools_error_from_cdp_error_parts(Some(i64::from(code)), &message);
if matches!(error.kind, DevToolsErrorKind::NoSuchHandle) {
conn.unregister_runtime_remote_object_ids_for_owner(owner, &[object_id]);
continue;
}
return Err(error);
}
conn.unregister_runtime_remote_object_ids_for_owner(owner, &[object_id]);
}
Ok(())
}
pub(super) async fn dispatch_runtime_inspector_command_response_for_owner_async(
conn: &mut CdpConnection,
owner: &CommandOwnerScope,
raw_json: String,
command_id: u64,
) -> Result<BackgroundCommandResponsePayload, DevToolsError> {
let descriptor = RendererCommandDescriptor::from_synthesized_payload(raw_json)
.map_err(|message| DevToolsError::new(DevToolsErrorKind::Internal, message))?;
let pending = conn
.start_runtime_protocol_message_for_owner_with_deferred_response(
owner, descriptor, command_id,
)
.map_err(|message| DevToolsError::new(DevToolsErrorKind::Internal, message))?;
let mut completed = pending
.wait()
.await
.map_err(|message| DevToolsError::new(DevToolsErrorKind::Internal, message))?;
let response_rx = completed.take_deferred_response_receiver().ok_or_else(|| {
DevToolsError::new(
DevToolsErrorKind::Internal,
"MissingRuntimeInspectorResponseReceiver",
)
})?;
let output = conn
.complete_runtime_protocol_message_async(completed)
.await
.map_err(|message| DevToolsError::new(DevToolsErrorKind::Internal, message))?;
if let Some(message) = output
.as_ref()
.and_then(|output| renderer_command_turn_frontend_protocol_response(output, command_id))
{
return Ok(BackgroundCommandResponsePayload::from_runtime_inspector_message(message));
}
let response = RuntimeInspectorResponseReady::for_owner(
command_id,
owner,
response_rx
.await
.map_err(|_| "RuntimeInspectorResponseCanceled".to_owned()),
);
let Some(mut response) = conn.resolve_runtime_inspector_response_ready(response) else {
return Err(DevToolsError::new(
DevToolsErrorKind::Internal,
"RuntimeInspectorResponseMissingCorrelation",
));
};
if response
.renderer_agent_attachment_id()
.is_some_and(|attachment_id| {
conn.current_renderer_agent_attachment_id_for_owner(owner) != Some(attachment_id)
})
{
response.replace_with_error("Execution context was destroyed by navigation");
}
Ok(response.into_command_response_payload())
}
pub(super) async fn execute_devtools_get_realms_command_async(
conn: &mut CdpConnection,
command: DevToolsGetRealmsCommand,
) -> Result<DevToolsCommandResult, DevToolsError> {
let requested_target_id = command
.context
.target_id
.as_ref()
.map(|target_id| target_id.as_str().to_owned());
let routes = devtools_get_realms_routes(conn, command.context.target_id.as_ref())?;
let requested_target_is_worker_target = routes.iter().any(|route| {
matches!(
route,
CdpSessionRoute::SharedWorkerTarget { .. }
| CdpSessionRoute::DedicatedWorkerTarget { .. }
| CdpSessionRoute::ServiceWorkerTarget { .. }
)
});
let mut realms = Vec::new();
for route in routes {
realms.extend(devtools_realms_for_route_async(conn, route).await?);
}
if let Some(requested_target_id) = requested_target_id.as_deref()
&& !requested_target_is_worker_target
{
realms.retain(|realm| {
realm
.frame_id
.as_ref()
.is_some_and(|frame_id| frame_id.as_str() == requested_target_id)
});
if realms.is_empty() {
return Err(DevToolsError::new(
DevToolsErrorKind::NoSuchTarget,
"NoSuchTarget",
));
}
}
if let Some(realm_type) = command.realm_type.as_deref() {
realms.retain(|realm| devtools_realm_type(realm.context_type.as_deref()) == realm_type);
}
dedup_devtools_realms(&mut realms);
Ok(DevToolsCommandResult::Realms(DevToolsGetRealmsResult {
realms,
}))
}
pub(super) fn dedup_devtools_realms(realms: &mut Vec<RuntimeExecutionContextEvent>) {
let mut seen = HashSet::new();
realms.retain(|realm| {
let key = if let Some(realm_id) = realm.realm_id.as_ref() {
format!(
"realm:{}:{}",
realm
.frame_id
.as_ref()
.map(|frame_id| frame_id.as_str())
.unwrap_or_default(),
realm_id.as_str()
)
} else {
format!(
"context:{}:{}:{}",
realm
.frame_id
.as_ref()
.map(|frame_id| frame_id.as_str())
.unwrap_or_default(),
realm
.context_id
.map(|context_id| context_id.to_string())
.unwrap_or_default(),
realm.context_type.as_deref().unwrap_or_default()
)
};
seen.insert(key)
});
}
pub(super) fn devtools_get_realms_routes(
conn: &CdpConnection,
target_id: Option<&crate::devtools_runtime::DevToolsTargetId>,
) -> Result<Vec<CdpSessionRoute>, DevToolsError> {
if let Some(target_id) = target_id {
if let Some(route) = conn
.target_session_route_for_target_id(target_id.as_str())
.or_else(|| conn.target_session_route_for_child_frame_id(target_id.as_str()))
{
return Ok(vec![route]);
}
return Err(DevToolsError::new(
DevToolsErrorKind::NoSuchTarget,
"NoSuchTarget",
));
}
let mut routes = Vec::new();
for target_info in conn
.browser_contexts()
.flat_map(crate::conn::BrowserContext::devtools_target_infos)
{
if !matches!(
target_info.kind,
crate::devtools_runtime::DevToolsTargetKind::Page
| crate::devtools_runtime::DevToolsTargetKind::Frame
| crate::devtools_runtime::DevToolsTargetKind::SharedWorker
| crate::devtools_runtime::DevToolsTargetKind::Worker
| crate::devtools_runtime::DevToolsTargetKind::ServiceWorker
) {
continue;
}
let Some(target_id) = target_info.target_id else {
continue;
};
if let Some(route) = conn.target_session_route_for_target_id(target_id.as_str()) {
routes.push(route);
}
}
if routes.is_empty() {
return Err(DevToolsError::new(
DevToolsErrorKind::NoSuchTarget,
"NoSuchTarget",
));
}
Ok(routes)
}
pub(super) async fn devtools_realms_for_route_async(
conn: &mut CdpConnection,
route: CdpSessionRoute,
) -> Result<Vec<RuntimeExecutionContextEvent>, DevToolsError> {
if let CdpSessionRoute::SharedWorkerTarget {
browser_context_id,
target_id,
} = &route
{
let target = conn
.browser_context_by_id(browser_context_id)
.and_then(|context| context.shared_worker_target(target_id))
.ok_or_else(|| DevToolsError::new(DevToolsErrorKind::NoSuchTarget, "NoSuchTarget"))?;
return Ok(shared_worker_target_runtime_realm(target)
.into_iter()
.collect());
}
if let CdpSessionRoute::DedicatedWorkerTarget {
browser_context_id,
target_id,
} = &route
{
let target = conn
.browser_context_by_id(browser_context_id)
.and_then(|context| context.dedicated_worker_target(target_id))
.ok_or_else(|| DevToolsError::new(DevToolsErrorKind::NoSuchTarget, "NoSuchTarget"))?;
return Ok(dedicated_worker_target_runtime_realm(target)
.into_iter()
.collect());
}
if let CdpSessionRoute::ServiceWorkerTarget {
browser_context_id,
target_id,
} = &route
{
let target = conn
.browser_context_by_id(browser_context_id)
.and_then(|context| context.service_worker_target(target_id))
.ok_or_else(|| DevToolsError::new(DevToolsErrorKind::NoSuchTarget, "NoSuchTarget"))?;
return Ok(service_worker_target_runtime_realm(target)
.into_iter()
.collect());
}
let owner = CommandOwnerScope::for_route(route);
conn.runtime_realm_inventory_for_owner_async(&owner)
.await
.map_err(|message| DevToolsError::new(DevToolsErrorKind::Internal, message))
}
pub(super) fn shared_worker_target_runtime_realm(
target: &crate::conn::SharedWorkerTargetState,
) -> Option<RuntimeExecutionContextEvent> {
let context_id = target.real_runtime_execution_context_id()?;
let origin = url::Url::parse(&target.url)
.ok()
.map(|url| moli_url::origin_ascii_serialization(&url))
.unwrap_or_else(|| "null".to_owned());
Some(RuntimeExecutionContextEvent {
target_id: Some(DevToolsTargetId::from(target.target_id.as_str())),
context_id: Some(context_id),
realm_id: Some(DevToolsRealmId::from(format!(
"shared-worker-{}",
target.target_id
))),
frame_id: None,
origin: Some(origin),
name: Some(target.name.clone()),
is_default: Some(true),
context_type: Some("shared-worker".to_owned()),
grant_universal_access: None,
})
}
pub(super) fn dedicated_worker_target_runtime_realm(
target: &crate::conn::DedicatedWorkerTargetState,
) -> Option<RuntimeExecutionContextEvent> {
let mut realm = shared_worker_target_runtime_realm(&target.inner)?;
realm.realm_id = Some(DevToolsRealmId::from(format!(
"dedicated-worker-{}",
target.target_id
)));
realm.context_type = Some("worker".to_owned());
Some(realm)
}
pub(super) fn service_worker_target_runtime_realm(
target: &crate::conn::ServiceWorkerTargetState,
) -> Option<RuntimeExecutionContextEvent> {
let context_id = target.real_runtime_execution_context_id()?;
let origin = url::Url::parse(&target.script_url)
.ok()
.map(|url| moli_url::origin_ascii_serialization(&url))
.unwrap_or_else(|| "null".to_owned());
Some(RuntimeExecutionContextEvent {
target_id: Some(DevToolsTargetId::from(target.target_id.as_str())),
context_id: Some(context_id),
realm_id: Some(DevToolsRealmId::from(format!(
"service-worker-{}",
target.target_id
))),
frame_id: None,
origin: Some(origin),
name: Some(String::new()),
is_default: Some(true),
context_type: Some("service-worker".to_owned()),
grant_universal_access: None,
})
}
pub(super) fn devtools_realm_type(context_type: Option<&str>) -> &'static str {
match context_type {
Some("dedicated-worker") => "dedicated-worker",
Some("shared-worker") => "shared-worker",
Some("service-worker") => "service-worker",
Some("paint-worklet") => "paint-worklet",
Some("audio-worklet") => "audio-worklet",
Some("worklet") => "worklet",
Some("worker") => "worker",
_ => "window",
}
}
pub(super) fn devtools_remote_value_from_cdp(
remote: &Value,
retain_handle: bool,
realm: Option<DevToolsRealmId>,
) -> DevToolsRemoteValue {
let object_id = remote.get("objectId").and_then(Value::as_str);
DevToolsRemoteValue {
value: cdp_remote_object_value(remote),
handle: retain_handle
.then(|| object_id.map(DevToolsRemoteHandleId::from))
.flatten(),
shared_id: object_id.map(DevToolsRemoteHandleId::from),
node_id: None,
backend_node_id: None,
window_context: None,
realm,
remote_type: remote
.get("type")
.and_then(Value::as_str)
.map(str::to_owned),
remote_subtype: remote
.get("subtype")
.and_then(Value::as_str)
.map(str::to_owned),
unserializable_value: remote
.get("unserializableValue")
.and_then(Value::as_str)
.map(str::to_owned),
description: remote
.get("description")
.and_then(Value::as_str)
.map(str::to_owned),
class_name: remote
.get("className")
.and_then(Value::as_str)
.map(str::to_owned),
deep_serialized_value: remote.get("deepSerializedValue").cloned(),
node_value: None,
}
}
pub(super) fn devtools_script_exception_from_cdp(
exception_details: &Value,
retain_handle: bool,
) -> DevToolsScriptException {
let exception = exception_details.get("exception");
let text = exception_details
.get("text")
.and_then(Value::as_str)
.or_else(|| {
exception.and_then(|exception| exception.get("description").and_then(Value::as_str))
})
.unwrap_or("JavaScript exception")
.to_owned();
DevToolsScriptException {
exception_id: exception_details.get("exceptionId").and_then(Value::as_u64),
script_id: exception_details
.get("scriptId")
.and_then(Value::as_str)
.map(str::to_owned),
text,
value: exception.map(|remote| devtools_remote_value_from_cdp(remote, retain_handle, None)),
realm: exception_details
.get("executionContextUniqueId")
.and_then(Value::as_str)
.map(DevToolsRealmId::from)
.or_else(|| {
exception_details
.get("executionContextId")
.and_then(Value::as_i64)
.map(|id| DevToolsRealmId::from(id.to_string()))
}),
line_number: exception_details.get("lineNumber").and_then(Value::as_u64),
column_number: exception_details
.get("columnNumber")
.and_then(Value::as_u64),
stack_trace: exception_details
.get("stackTrace")
.and_then(crate::devtools_runtime::DevToolsStackTrace::from_cdp_value),
}
}
pub(super) fn cdp_remote_object_value(remote: &Value) -> Value {
if let Some(value) = remote.get("value") {
return value.clone();
}
if remote
.get("subtype")
.and_then(Value::as_str)
.is_some_and(|subtype| subtype == "null")
{
return Value::Null;
}
if let Some(unserializable) = remote.get("unserializableValue").and_then(Value::as_str) {
return Value::String(unserializable.to_owned());
}
match remote.get("type").and_then(Value::as_str) {
Some("undefined") => Value::Null,
Some("boolean") => Value::Bool(false),
Some("number") => json!(0),
Some("string") => Value::String(String::new()),
Some("object") | Some("function") => json!({}),
_ => Value::Null,
}
}
@@ -0,0 +1,502 @@
use super::*;
pub(super) async fn devtools_realm_id_for_runtime_target_async(
conn: &mut CdpConnection,
target: &DevToolsRuntimeTarget,
) -> Option<DevToolsRealmId> {
devtools_runtime_realm_for_target_async(conn, target)
.await
.and_then(|realm| realm.realm_id)
}
pub(super) async fn devtools_runtime_realm_for_target_async(
conn: &mut CdpConnection,
target: &DevToolsRuntimeTarget,
) -> Option<RuntimeExecutionContextEvent> {
let mut realms = devtools_realms_for_route_async(conn, target.route.clone())
.await
.ok()?;
if let Some(execution_context_id) = target.execution_context_id {
return realms
.into_iter()
.find(|realm| realm.context_id == Some(execution_context_id));
}
if let Some(index) = realms
.iter()
.position(|realm| realm.is_default != Some(false))
{
return Some(realms.remove(index));
}
realms.into_iter().next()
}
pub(super) fn devtools_runtime_result_ownership(
command: &DevToolsCommand,
) -> DevToolsResultOwnership {
match command {
DevToolsCommand::EvaluateScript(command) => command.result_ownership,
DevToolsCommand::CallFunction(command) => command.result_ownership,
_ => DevToolsResultOwnership::None,
}
}
pub(super) fn devtools_runtime_command_result_kind(
command: &DevToolsCommand,
) -> DevToolsRuntimeCommandResultKind {
match command {
DevToolsCommand::TerminateExecution(_) => DevToolsRuntimeCommandResultKind::Empty,
_ => DevToolsRuntimeCommandResultKind::Script,
}
}
pub(super) fn devtools_runtime_serialization_options(
command: &DevToolsCommand,
) -> Option<DevToolsSerializationOptions> {
match command {
DevToolsCommand::EvaluateScript(command) => command.serialization_options.clone(),
DevToolsCommand::CallFunction(command) => command.serialization_options.clone(),
_ => None,
}
}
#[cfg(test)]
pub(super) fn devtools_command_has_bidi_script_channel_arguments(
command: &DevToolsCommand,
) -> bool {
let DevToolsCommand::CallFunction(command) = command else {
return false;
};
matches!(command.context.protocol, DevToolsProtocol::WebDriverBidi)
&& command
.this_parameter
.iter()
.chain(command.arguments.iter())
.any(bidi_local_value_contains_channel)
}
#[cfg(test)]
pub(super) fn bidi_local_value_contains_channel(value: &Value) -> bool {
let Some(map) = value.as_object() else {
return false;
};
match map.get("type").and_then(Value::as_str) {
Some("channel") => true,
Some("array" | "set") => map
.get("value")
.and_then(Value::as_array)
.is_some_and(|items| items.iter().any(bidi_local_value_contains_channel)),
Some("object" | "map") => {
map.get("value")
.and_then(Value::as_array)
.is_some_and(|entries| {
entries.iter().any(|entry| {
let Some(pair) = entry.as_array() else {
return false;
};
pair.iter().any(bidi_local_value_contains_channel)
})
})
}
_ => false,
}
}
pub(super) async fn devtools_runtime_target_async(
conn: &mut CdpConnection,
command: &DevToolsCommand,
) -> Result<DevToolsRuntimeTarget, DevToolsError> {
if let DevToolsCommand::TerminateExecution(command) = command {
let target_id =
command.context.target_id.as_ref().ok_or_else(|| {
DevToolsError::new(DevToolsErrorKind::NoSuchTarget, "NoSuchTarget")
})?;
return devtools_runtime_control_target(conn, target_id);
}
let (target_id, realm_id, world_name) = match command {
DevToolsCommand::EvaluateScript(command) => (
command.context.target_id.as_ref(),
command.realm_id.as_ref(),
command.world_name.as_deref(),
),
DevToolsCommand::CallFunction(command) => (
command.context.target_id.as_ref(),
command.realm_id.as_ref(),
command.world_name.as_deref(),
),
DevToolsCommand::LocateNodes(command) => (command.context.target_id.as_ref(), None, None),
DevToolsCommand::ReleaseObjects(command) => (
command.context.target_id.as_ref(),
command.realm_id.as_ref(),
command.world_name.as_deref(),
),
_ => (None, None, None),
};
if let Some(target_id) = target_id {
return devtools_runtime_context_target_async(conn, target_id, world_name).await;
}
if let Some(realm_id) = realm_id {
return devtools_runtime_realm_target_async(conn, realm_id).await;
}
Err(DevToolsError::new(
DevToolsErrorKind::NoSuchTarget,
"NoSuchTarget",
))
}
pub(super) fn devtools_runtime_control_target(
conn: &CdpConnection,
target_id: &DevToolsTargetId,
) -> Result<DevToolsRuntimeTarget, DevToolsError> {
// Do not fall back to realm discovery here: it is a MainThread operation
// and would put the escape hatch behind the work it needs to interrupt.
let route = conn
.target_session_route_for_target_id(target_id.as_str())
.or_else(|| conn.target_session_route_for_child_frame_id(target_id.as_str()))
.ok_or_else(|| DevToolsError::new(DevToolsErrorKind::NoSuchTarget, "NoSuchTarget"))?;
Ok(DevToolsRuntimeTarget {
route,
execution_context_id: None,
window_context_id: Some(target_id.clone()),
})
}
pub(super) async fn devtools_runtime_context_target_async(
conn: &mut CdpConnection,
target_id: &DevToolsTargetId,
world_name: Option<&str>,
) -> Result<DevToolsRuntimeTarget, DevToolsError> {
if let Some(route) = conn.target_session_route_for_target_id(target_id.as_str()) {
let execution_context_id = if let Some(world_name) = world_name {
Some(
devtools_ensure_runtime_world_async(conn, route.clone(), target_id, world_name)
.await?,
)
} else {
None
};
return Ok(DevToolsRuntimeTarget {
route,
execution_context_id,
window_context_id: Some(target_id.clone()),
});
}
if let Some(route) = conn.target_session_route_for_child_frame_id(target_id.as_str()) {
let execution_context_id = if let Some(world_name) = world_name {
Some(
devtools_ensure_runtime_world_async(conn, route.clone(), target_id, world_name)
.await?,
)
} else {
let owner = CommandOwnerScope::for_route(route.clone());
let result = conn
.child_default_execution_context_id_for_frame_id_for_owner_async(
&owner,
target_id.as_str(),
)
.await;
Some(
result
.map_err(|message| DevToolsError::new(DevToolsErrorKind::Internal, message))?
.ok_or_else(|| {
DevToolsError::new(
DevToolsErrorKind::Internal,
"NoExecutionContextForFrame",
)
})?,
)
};
return Ok(DevToolsRuntimeTarget {
route,
execution_context_id,
window_context_id: Some(target_id.clone()),
});
}
let routes = devtools_get_realms_routes(conn, None)?;
for route in routes {
let realms = devtools_realms_for_search_route_async(conn, route.clone()).await?;
let Some(default_realm) = realms.iter().find(|realm| {
realm
.frame_id
.as_ref()
.is_some_and(|frame_id| frame_id.as_str() == target_id.as_str())
&& realm.is_default != Some(false)
}) else {
continue;
};
let execution_context_id = if let Some(world_name) = world_name {
Some(
devtools_ensure_runtime_world_async(conn, route.clone(), target_id, world_name)
.await?,
)
} else {
Some(default_realm.context_id.ok_or_else(|| {
DevToolsError::new(DevToolsErrorKind::Internal, "NoExecutionContextForFrame")
})?)
};
return Ok(DevToolsRuntimeTarget {
route,
execution_context_id,
window_context_id: default_realm
.frame_id
.clone()
.map(|frame_id| DevToolsTargetId::from(frame_id.into_string())),
});
}
Err(DevToolsError::new(
DevToolsErrorKind::NoSuchTarget,
"NoSuchTarget",
))
}
pub(super) async fn devtools_ensure_runtime_world_async(
conn: &mut CdpConnection,
route: CdpSessionRoute,
frame_id: &DevToolsTargetId,
world_name: &str,
) -> Result<i64, DevToolsError> {
let owner = CommandOwnerScope::for_route(route);
let result = conn
.runtime_ensure_isolated_world_for_owner_async(&owner, Some(frame_id.as_str()), world_name)
.await;
result.map_err(|message| DevToolsError::new(DevToolsErrorKind::Internal, message))
}
pub(super) async fn devtools_runtime_realm_target_async(
conn: &mut CdpConnection,
realm_id: &DevToolsRealmId,
) -> Result<DevToolsRuntimeTarget, DevToolsError> {
let routes = devtools_get_realms_routes(conn, None)?;
for route in routes {
let realms = devtools_realms_for_search_route_async(conn, route.clone()).await?;
if let Some(realm) = realms
.into_iter()
.find(|realm| realm.realm_id.as_ref() == Some(realm_id))
{
let execution_context_id = realm.context_id.ok_or_else(|| {
DevToolsError::new(DevToolsErrorKind::Internal, "NoExecutionContextForRealm")
})?;
return Ok(DevToolsRuntimeTarget {
route,
execution_context_id: Some(execution_context_id),
window_context_id: realm
.frame_id
.clone()
.map(|frame_id| DevToolsTargetId::from(frame_id.into_string())),
});
}
}
Err(DevToolsError::new(
DevToolsErrorKind::NoSuchTarget,
"NoSuchRealm",
))
}
pub(super) async fn devtools_realms_for_search_route_async(
conn: &mut CdpConnection,
route: CdpSessionRoute,
) -> Result<Vec<RuntimeExecutionContextEvent>, DevToolsError> {
match devtools_realms_for_route_async(conn, route).await {
Ok(realms) => Ok(realms),
Err(error)
if matches!(error.kind, DevToolsErrorKind::Internal)
&& error.message == "NoDocumentLoaded" =>
{
Ok(Vec::new())
}
Err(error) => Err(error),
}
}
pub(super) async fn remap_bidi_node_shared_references_for_target_async(
conn: &mut CdpConnection,
target: &DevToolsRuntimeTarget,
command: &mut DevToolsCallFunctionCommand,
realm_id: Option<&DevToolsRealmId>,
) -> Result<(), DevToolsError> {
let owner = CommandOwnerScope::for_route(target.route.clone());
let mut references = Vec::new();
if let Some(this_parameter) = command.this_parameter.as_ref() {
collect_bidi_node_shared_reference_paths(
this_parameter,
BidiCallFunctionValueRoot::This,
&mut references,
);
}
for (index, argument) in command.arguments.iter().enumerate() {
collect_bidi_node_shared_reference_paths(
argument,
BidiCallFunctionValueRoot::Argument(index),
&mut references,
);
}
references.sort_by(|left, right| {
(left.root, &left.path, &left.shared_id).cmp(&(right.root, &right.path, &right.shared_id))
});
references.dedup();
if references.is_empty() {
return Ok(());
}
let execution_context_id = if let Some(execution_context_id) = target.execution_context_id {
Some(execution_context_id)
} else {
let result = conn
.runtime_default_or_initial_execution_context_id_for_owner_async(&owner)
.await;
Some(
result
.map_err(|message| DevToolsError::new(DevToolsErrorKind::Internal, message))?
.ok_or_else(|| {
DevToolsError::new(DevToolsErrorKind::Internal, "NoDefaultExecutionContext")
})?,
)
};
for reference in references {
let Some(target_shared_id) = remap_bidi_node_shared_id_for_target_async(
conn,
&owner,
execution_context_id,
&reference.shared_id,
realm_id,
)
.await?
else {
continue;
};
let Some(value) =
bidi_call_function_value_at_path_mut(command, reference.root, &reference.path)
else {
continue;
};
if let Some(map) = value.as_object_mut() {
map.insert("sharedId".to_owned(), json!(target_shared_id.into_string()));
}
}
Ok(())
}
pub(super) fn collect_bidi_node_shared_reference_paths(
value: &Value,
root: BidiCallFunctionValueRoot,
out: &mut Vec<BidiNodeSharedReferencePath>,
) {
let mut stack = vec![(
value,
Vec::<BidiValuePathSegment>::new(),
MAX_INSPECTOR_PROTOCOL_VALUE_DEPTH,
)];
while let Some((value, path, remaining_tree_depth)) = stack.pop() {
let Some(next_tree_depth) = remaining_tree_depth.checked_sub(1) else {
continue;
};
match value {
Value::Object(map) => {
if let Some(shared_id) = map.get("sharedId").and_then(Value::as_str) {
out.push(BidiNodeSharedReferencePath {
root,
path,
shared_id: shared_id.to_owned(),
});
continue;
}
if map.get("handle").and_then(Value::as_str).is_some() {
continue;
}
let children = map.iter().collect::<Vec<_>>();
for (key, child) in children.into_iter().rev() {
let mut child_path = path.clone();
child_path.push(BidiValuePathSegment::Key(key.clone()));
stack.push((child, child_path, next_tree_depth));
}
}
Value::Array(values) => {
for index in (0..values.len()).rev() {
let mut child_path = path.clone();
child_path.push(BidiValuePathSegment::Index(index));
stack.push((&values[index], child_path, next_tree_depth));
}
}
Value::Null | Value::Bool(_) | Value::Number(_) | Value::String(_) => {}
}
}
}
pub(super) async fn remap_bidi_node_shared_id_for_target_async(
conn: &mut CdpConnection,
owner: &CommandOwnerScope,
execution_context_id: Option<i64>,
shared_id: &str,
realm_id: Option<&DevToolsRealmId>,
) -> Result<Option<DevToolsRemoteHandleId>, DevToolsError> {
let is_internal_bidi_node_id = is_webdriver_bidi_node_shared_id(shared_id);
let snapshot = bidi_node_snapshot_for_shared_id_async(conn, owner, shared_id, 0, false).await?;
let Some(snapshot) = snapshot else {
return if is_internal_bidi_node_id {
Err(DevToolsError::new(
DevToolsErrorKind::NoSuchNode,
"Could not find node with given id",
))
} else {
Ok(None)
};
};
let Some(backend_node_id) = snapshot.snapshot.backend_node_id else {
return Err(DevToolsError::new(
DevToolsErrorKind::NoSuchNode,
"Could not find node with given id",
));
};
let Some(remote_object) = conn
.runtime_remote_object_for_backend_node_id_for_owner_async(
owner,
backend_node_id,
execution_context_id,
None,
)
.await
.map_err(|message| DevToolsError::new(DevToolsErrorKind::Internal, message))?
else {
return Err(DevToolsError::new(
DevToolsErrorKind::NoSuchNode,
"Could not find node with given id",
));
};
let Some(object_id) = remote_object
.get("objectId")
.and_then(Value::as_str)
.map(str::to_owned)
else {
return Err(DevToolsError::new(
DevToolsErrorKind::NoSuchNode,
"Could not find node with given id",
));
};
register_remapped_bidi_node_remote_object(conn, owner, &remote_object, &object_id, realm_id);
Ok(Some(DevToolsRemoteHandleId::from(object_id)))
}
pub(super) fn register_remapped_bidi_node_remote_object(
conn: &mut CdpConnection,
owner: &CommandOwnerScope,
remote_object: &Value,
object_id: &str,
realm_id: Option<&DevToolsRealmId>,
) {
if let Some(realm_id) = realm_id {
conn.register_runtime_remote_object_ids_for_owner_with_realm(
owner,
vec![object_id.to_owned()],
realm_id.as_str(),
);
} else {
conn.register_runtime_remote_object_ids_from_value_for_owner(owner, remote_object);
}
}
File diff suppressed because it is too large Load Diff