mirror of
https://github.com/lexmount/moli.git
synced 2026-09-27 16:01:31 +00:00
refactor(protocol): split runtime dispatcher responsibilities
This commit is contained in:
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", ¶ms);
|
||||
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",
|
||||
¶ms,
|
||||
);
|
||||
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", ¶ms);
|
||||
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
Reference in New Issue
Block a user