mirror of
https://github.com/servo/servo.git
synced 2025-09-30 00:29:14 +01:00
devtools clients query source actors to determine where the user can set breakpoints in a source. there are two relevant requests here: `getBreakableLines` controls which line numbers can be clicked in the margin, and once a line number is clicked, `getBreakpointPositionsCompressed` controls where to show breakpoint buttons within that line. this patch handles those requests by querying the [SpiderMonkey Debugger API](https://firefox-source-docs.mozilla.org/js/Debugger/) for that information: - devtools sends its script thread a GetPossibleBreakpoints message for the source’s [`spidermonkey_id`](https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Source.html#id) - the script thread fires a `getPossibleBreakpoints` event into its debugger global - the debugger script looks up the [root](https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.html#onnewscript-script-global) [Debugger.Script](https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Script.html#getpossiblebreakpoints-query) for that source, calls [getPossibleBreakpoints()](https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.Script.html#getpossiblebreakpoints-query), and returns the result via DebuggerGlobalScope#getPossibleBreakpointsResult() - that method takes the pending result sender, and sends the result back to devtools - devtools massages the result into the format required by the request, and replies to the client as a result, users of the Firefox devtools client can now set breakpoints, though they don’t have any effect. Testing: this patch adds new devtools tests Fixes: part of #36027 <img width="1433" height="1328" alt="image" src="https://github.com/user-attachments/assets/f0cd31e0-742f-44d3-8c5d-ceedd9a2706d" /> --------- Signed-off-by: Delan Azabani <dazabani@igalia.com> Co-authored-by: atbrakhi <atbrakhi@igalia.com>
263 lines
9.2 KiB
Rust
263 lines
9.2 KiB
Rust
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||
|
||
use std::cell::RefCell;
|
||
use std::collections::{BTreeMap, BTreeSet};
|
||
|
||
use base::id::PipelineId;
|
||
use devtools_traits::DevtoolScriptControlMsg;
|
||
use ipc_channel::ipc::{IpcSender, channel};
|
||
use serde::Serialize;
|
||
use serde_json::{Map, Value};
|
||
use servo_url::ServoUrl;
|
||
|
||
use crate::StreamId;
|
||
use crate::actor::{Actor, ActorError, ActorRegistry};
|
||
use crate::protocol::ClientRequest;
|
||
|
||
/// A `sourceForm` as used in responses to thread `sources` requests.
|
||
///
|
||
/// For now, we also use this for sources in watcher `resource-available-array` messages,
|
||
/// but in Firefox those have extra fields.
|
||
///
|
||
/// <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#loading-script-sources>
|
||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub(crate) struct SourceForm {
|
||
pub actor: String,
|
||
/// URL of the script, or URL of the page for inline scripts.
|
||
pub url: String,
|
||
pub is_black_boxed: bool,
|
||
/// `introductionType` in SpiderMonkey `CompileOptionsWrapper`.
|
||
pub introduction_type: String,
|
||
}
|
||
|
||
#[derive(Serialize)]
|
||
pub(crate) struct SourcesReply {
|
||
pub from: String,
|
||
pub sources: Vec<SourceForm>,
|
||
}
|
||
|
||
pub(crate) struct SourceManager {
|
||
source_actor_names: RefCell<BTreeSet<String>>,
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
pub struct SourceActor {
|
||
/// Actor name.
|
||
pub name: String,
|
||
|
||
/// URL of the script, or URL of the page for inline scripts.
|
||
pub url: ServoUrl,
|
||
|
||
/// The ‘black-boxed’ flag, which tells the debugger to avoid pausing inside this script.
|
||
/// <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#black-boxing-sources>
|
||
pub is_black_boxed: bool,
|
||
|
||
pub content: Option<String>,
|
||
pub content_type: Option<String>,
|
||
|
||
// TODO: use it in #37667, then remove this allow
|
||
#[allow(unused)]
|
||
pub spidermonkey_id: u32,
|
||
/// `introductionType` in SpiderMonkey `CompileOptionsWrapper`.
|
||
pub introduction_type: String,
|
||
|
||
script_sender: IpcSender<DevtoolScriptControlMsg>,
|
||
}
|
||
|
||
#[derive(Serialize)]
|
||
struct SourceContentReply {
|
||
from: String,
|
||
#[serde(rename = "contentType")]
|
||
content_type: Option<String>,
|
||
source: String,
|
||
}
|
||
|
||
#[derive(Serialize)]
|
||
struct GetBreakableLinesReply {
|
||
from: String,
|
||
// Line numbers are one-based.
|
||
// <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#source-locations>
|
||
lines: BTreeSet<u32>,
|
||
}
|
||
|
||
#[derive(Serialize)]
|
||
struct GetBreakpointPositionsCompressedReply {
|
||
from: String,
|
||
// Column numbers are in UTF-16 code units, not Unicode scalar values or grapheme clusters.
|
||
// Line number are one-based. Column numbers are zero-based.
|
||
// FIXME: the docs say column numbers are one-based, but this appears to be incorrect.
|
||
// <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#source-locations>
|
||
positions: BTreeMap<u32, BTreeSet<u32>>,
|
||
}
|
||
|
||
impl SourceManager {
|
||
pub fn new() -> Self {
|
||
Self {
|
||
source_actor_names: RefCell::new(BTreeSet::default()),
|
||
}
|
||
}
|
||
|
||
pub fn add_source(&self, actor_name: &str) {
|
||
self.source_actor_names
|
||
.borrow_mut()
|
||
.insert(actor_name.to_owned());
|
||
}
|
||
|
||
pub fn source_forms(&self, actors: &ActorRegistry) -> Vec<SourceForm> {
|
||
self.source_actor_names
|
||
.borrow()
|
||
.iter()
|
||
.map(|actor_name| actors.find::<SourceActor>(actor_name).source_form())
|
||
.collect()
|
||
}
|
||
}
|
||
|
||
impl SourceActor {
|
||
pub fn new(
|
||
name: String,
|
||
url: ServoUrl,
|
||
content: Option<String>,
|
||
content_type: Option<String>,
|
||
spidermonkey_id: u32,
|
||
introduction_type: String,
|
||
script_sender: IpcSender<DevtoolScriptControlMsg>,
|
||
) -> SourceActor {
|
||
SourceActor {
|
||
name,
|
||
url,
|
||
content,
|
||
content_type,
|
||
is_black_boxed: false,
|
||
spidermonkey_id,
|
||
introduction_type,
|
||
script_sender,
|
||
}
|
||
}
|
||
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn new_registered(
|
||
actors: &mut ActorRegistry,
|
||
pipeline_id: PipelineId,
|
||
url: ServoUrl,
|
||
content: Option<String>,
|
||
content_type: Option<String>,
|
||
spidermonkey_id: u32,
|
||
introduction_type: String,
|
||
script_sender: IpcSender<DevtoolScriptControlMsg>,
|
||
) -> &SourceActor {
|
||
let source_actor_name = actors.new_name("source");
|
||
|
||
let source_actor = SourceActor::new(
|
||
source_actor_name.clone(),
|
||
url,
|
||
content,
|
||
content_type,
|
||
spidermonkey_id,
|
||
introduction_type,
|
||
script_sender,
|
||
);
|
||
actors.register(Box::new(source_actor));
|
||
actors.register_source_actor(pipeline_id, &source_actor_name);
|
||
|
||
actors.find(&source_actor_name)
|
||
}
|
||
|
||
pub fn source_form(&self) -> SourceForm {
|
||
SourceForm {
|
||
actor: self.name.clone(),
|
||
url: self.url.to_string(),
|
||
is_black_boxed: self.is_black_boxed,
|
||
introduction_type: self.introduction_type.clone(),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Actor for SourceActor {
|
||
fn name(&self) -> String {
|
||
self.name.clone()
|
||
}
|
||
|
||
fn handle_message(
|
||
&self,
|
||
request: ClientRequest,
|
||
_registry: &ActorRegistry,
|
||
msg_type: &str,
|
||
_msg: &Map<String, Value>,
|
||
_id: StreamId,
|
||
) -> Result<(), ActorError> {
|
||
match msg_type {
|
||
// Client has requested contents of the source.
|
||
"source" => {
|
||
let reply = SourceContentReply {
|
||
from: self.name(),
|
||
content_type: self.content_type.clone(),
|
||
// TODO: if needed, fetch the page again, in the same way as in the original request.
|
||
// Fetch it from cache, even if the original request was non-idempotent (e.g. POST).
|
||
// If we can’t fetch it from cache, we should probably give up, because with a real
|
||
// fetch, the server could return a different response.
|
||
// TODO: do we want to wait instead of giving up immediately, in cases where the content could
|
||
// become available later (e.g. after a fetch)?
|
||
source: self
|
||
.content
|
||
.as_deref()
|
||
.unwrap_or("<!-- not available; please reload! -->")
|
||
.to_owned(),
|
||
};
|
||
request.reply_final(&reply)?
|
||
},
|
||
// Client wants to know which lines can have breakpoints.
|
||
// Sent when opening a source in the Sources panel, and controls whether the line numbers can be clicked.
|
||
"getBreakableLines" => {
|
||
let (tx, rx) = channel().map_err(|_| ActorError::Internal)?;
|
||
self.script_sender
|
||
.send(DevtoolScriptControlMsg::GetPossibleBreakpoints(
|
||
self.spidermonkey_id,
|
||
tx,
|
||
))
|
||
.map_err(|_| ActorError::Internal)?;
|
||
let result = rx.recv().map_err(|_| ActorError::Internal)?;
|
||
let lines = result
|
||
.into_iter()
|
||
.map(|entry| entry.line_number)
|
||
.collect::<BTreeSet<_>>();
|
||
let reply = GetBreakableLinesReply {
|
||
from: self.name(),
|
||
lines,
|
||
};
|
||
request.reply_final(&reply)?
|
||
},
|
||
// Client wants to know which columns in the line can have breakpoints.
|
||
// Sent when the user tries to set a breakpoint by clicking a line number in a source.
|
||
"getBreakpointPositionsCompressed" => {
|
||
let (tx, rx) = channel().map_err(|_| ActorError::Internal)?;
|
||
self.script_sender
|
||
.send(DevtoolScriptControlMsg::GetPossibleBreakpoints(
|
||
self.spidermonkey_id,
|
||
tx,
|
||
))
|
||
.map_err(|_| ActorError::Internal)?;
|
||
let result = rx.recv().map_err(|_| ActorError::Internal)?;
|
||
let mut positions: BTreeMap<u32, BTreeSet<u32>> = BTreeMap::default();
|
||
for entry in result {
|
||
// Line number are one-based. Column numbers are zero-based.
|
||
// FIXME: the docs say column numbers are one-based, but this appears to be incorrect.
|
||
// <https://firefox-source-docs.mozilla.org/devtools/backend/protocol.html#source-locations>
|
||
positions
|
||
.entry(entry.line_number)
|
||
.or_default()
|
||
.insert(entry.column_number - 1);
|
||
}
|
||
let reply = GetBreakpointPositionsCompressedReply {
|
||
from: self.name(),
|
||
positions,
|
||
};
|
||
request.reply_final(&reply)?
|
||
},
|
||
_ => return Err(ActorError::UnrecognizedPacketType),
|
||
};
|
||
Ok(())
|
||
}
|
||
}
|