devtools: Create source actors from Debugger API notifications (#38334)

currently our devtools impl creates source actors in script, when
executing scripts in HTMLScriptElement or DedicatedWorkerGlobalScope.
this approach is cumbersome, and it means that many pathways to running
scripts are missed, such as imported ES modules.

with the [SpiderMonkey Debugger
API](https://firefox-source-docs.mozilla.org/js/Debugger/), we can pick
up all of the scripts and all of their sources without any extra code,
as long as we tell it about every global we create (#38333, #38551).
this patch adds a [Debugger#onNewScript()
hook](https://firefox-source-docs.mozilla.org/js/Debugger/Debugger.html#onnewscript-script-global)
to the debugger script, which calls
DebuggerGlobalScope#notifyNewSource() to notify our script system when a
new script runs. if the source is relevant to the file tree in the
Sources tab, script tells devtools to create a source actor.

Testing: adds several new automated devtools tests
Fixes: part of #36027

Signed-off-by: Delan Azabani <dazabani@igalia.com>
Co-authored-by: atbrakhi <atbrakhi@igalia.com>
This commit is contained in:
shuppy 2025-08-11 14:04:51 +08:00 committed by GitHub
parent de73d4a25c
commit 4784668fa9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 522 additions and 118 deletions

View file

@ -2,9 +2,9 @@
* 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 base::id::PipelineId;
use base::id::{Index, PipelineId, PipelineNamespaceId};
use constellation_traits::ScriptToConstellationChan;
use devtools_traits::{ScriptToDevtoolsControlMsg, WorkerId};
use devtools_traits::{ScriptToDevtoolsControlMsg, SourceInfo, WorkerId};
use dom_struct::dom_struct;
use embedder_traits::JavaScriptEvaluationError;
use embedder_traits::resources::{self, Resource};
@ -14,6 +14,9 @@ use js::rust::Runtime;
use js::rust::wrappers::JS_DefineDebuggerObject;
use net_traits::ResourceThreads;
use profile_traits::{mem, time};
use script_bindings::codegen::GenericBindings::DebuggerGlobalScopeBinding::{
DebuggerGlobalScopeMethods, NotifyNewSource,
};
use script_bindings::realms::InRealm;
use script_bindings::reflector::DomObject;
use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
@ -30,7 +33,7 @@ use crate::dom::types::{DebuggerAddDebuggeeEvent, Event};
use crate::dom::webgpu::identityhub::IdentityHub;
use crate::realms::enter_realm;
use crate::script_module::ScriptFetchOptions;
use crate::script_runtime::{CanGc, JSContext};
use crate::script_runtime::{CanGc, IntroductionType, JSContext};
#[dom_struct]
/// Global scope for interacting with the devtools Debugger API.
@ -105,6 +108,10 @@ impl DebuggerGlobalScope {
GlobalScope::get_cx()
}
pub(crate) fn as_global_scope(&self) -> &GlobalScope {
self.upcast::<GlobalScope>()
}
fn evaluate_js(&self, script: &str, can_gc: CanGc) -> Result<(), JavaScriptEvaluationError> {
rooted!(in (*Self::get_cx()) let mut rval = UndefinedValue());
self.global_scope.evaluate_js_on_global_with_result(
@ -149,3 +156,99 @@ impl DebuggerGlobalScope {
);
}
}
impl DebuggerGlobalScopeMethods<crate::DomTypeHolder> for DebuggerGlobalScope {
// check-tidy: no specs after this line
fn NotifyNewSource(&self, args: &NotifyNewSource) {
let Some(devtools_chan) = self.as_global_scope().devtools_chan() else {
return;
};
let pipeline_id = PipelineId {
namespace_id: PipelineNamespaceId(args.pipelineId.namespaceId),
index: Index::new(args.pipelineId.index).expect("`pipelineId.index` must not be zero"),
};
if let Some(introduction_type) = args.introductionType.as_ref() {
// Check the `introductionType` and `url`, decide whether or not to create a source actor, and if so,
// tell the devtools server to create a source actor. Based on the Firefox impl in:
// - getDebuggerSourceURL() <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/utils/source-url.js#7-42>
// - getSourceURL() <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#67-109>
// - resolveSourceURL() <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#48-66>
// - SourceActor#_isInlineSource <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#130-143>
// - SourceActor#url <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#157-162>
// Firefox impl: getDebuggerSourceURL(), getSourceURL()
// TODO: handle `about:srcdoc` case (see Firefox getDebuggerSourceURL())
// TODO: remove trailing details that may have been appended by SpiderMonkey
// (currently impossible to do robustly due to <https://bugzilla.mozilla.org/show_bug.cgi?id=1982001>)
let url_original = args.url.str();
// FIXME: use page/worker url as base here
let url_original = ServoUrl::parse(url_original).ok();
// If the source has a `urlOverride` (aka `displayURL` aka `//# sourceURL`), it should be a valid url,
// possibly relative to the page/worker url, and we should treat the source as coming from that url for
// devtools purposes, including the file tree in the Sources tab.
// Firefox impl: getSourceURL()
let url_override = args
.urlOverride
.as_ref()
.map(|url| url.str())
// FIXME: use page/worker url as base here, not `url_original`
.and_then(|url| ServoUrl::parse_with_base(url_original.as_ref(), url).ok());
// If the `introductionType` is “eval or eval-like”, the `url` wont be meaningful, so ignore these
// sources unless we have a `urlOverride` (aka `displayURL` aka `//# sourceURL`).
// Firefox impl: getDebuggerSourceURL(), getSourceURL()
if [
IntroductionType::INJECTED_SCRIPT_STR,
IntroductionType::EVAL_STR,
IntroductionType::DEBUGGER_EVAL_STR,
IntroductionType::FUNCTION_STR,
IntroductionType::JAVASCRIPT_URL_STR,
IntroductionType::EVENT_HANDLER_STR,
IntroductionType::DOM_TIMER_STR,
]
.contains(&introduction_type.str()) &&
url_override.is_none()
{
debug!(
"Not creating debuggee: `introductionType` is `{introduction_type}` but no valid url"
);
return;
}
// Sources with an `introductionType` of `inlineScript` are generally inline, meaning their contents
// are a substring of the page markup (hence not known to SpiderMonkey, requiring plumbing in Servo).
// But sources with a `urlOverride` are not inline, since they get their own place in the Sources tree.
// nor are sources created for `<iframe srcdoc>`, since they are not necessarily a substring of the
// page markup as originally sent by the server.
// Firefox impl: SourceActor#_isInlineSource
// TODO: handle `about:srcdoc` case (see Firefox SourceActor#_isInlineSource)
let inline = introduction_type.str() == "inlineScript" && url_override.is_none();
let Some(url) = url_override.or(url_original) else {
debug!("Not creating debuggee: no valid url");
return;
};
let worker_id = args.workerId.as_ref().map(|id| id.parse().unwrap());
let source_info = SourceInfo {
url,
introduction_type: introduction_type.str().to_owned(),
inline,
worker_id,
content: (!inline).then(|| args.text.to_string()),
content_type: None, // TODO
spidermonkey_id: args.spidermonkeyId,
};
if let Err(error) = devtools_chan.send(ScriptToDevtoolsControlMsg::CreateSourceActor(
pipeline_id,
source_info,
)) {
warn!("Failed to send to devtools server: {error:?}");
}
} else {
debug!("Not creating debuggee for script with no `introductionType`");
}
}
}

View file

@ -9,7 +9,7 @@ use std::thread::{self, JoinHandle};
use base::id::{BrowsingContextId, PipelineId, WebViewId};
use constellation_traits::{WorkerGlobalScopeInit, WorkerScriptLoadOrigin};
use crossbeam_channel::{Receiver, Sender, unbounded};
use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, SourceInfo};
use devtools_traits::DevtoolScriptControlMsg;
use dom_struct::dom_struct;
use headers::{HeaderMapExt, ReferrerPolicy as ReferrerPolicyHeader};
use ipc_channel::ipc::IpcReceiver;
@ -60,9 +60,7 @@ use crate::fetch::{CspViolationsProcessor, load_whole_resource};
use crate::messaging::{CommonScriptMsg, ScriptEventLoopReceiver, ScriptEventLoopSender};
use crate::realms::{AlreadyInRealm, InRealm, enter_realm};
use crate::script_runtime::ScriptThreadEventCategory::WorkerEvent;
use crate::script_runtime::{
CanGc, IntroductionType, JSContext as SafeJSContext, Runtime, ThreadSafeJSContext,
};
use crate::script_runtime::{CanGc, JSContext as SafeJSContext, Runtime, ThreadSafeJSContext};
use crate::task_queue::{QueuedTask, QueuedTaskConversion, TaskQueue};
use crate::task_source::{SendableTaskSource, TaskSourceName};
@ -536,24 +534,6 @@ impl DedicatedWorkerGlobalScope {
));
global_scope.set_https_state(metadata.https_state);
let source = String::from_utf8_lossy(&bytes);
if let Some(chan) = global_scope.devtools_chan() {
let pipeline_id = global_scope.pipeline_id();
let source_info = SourceInfo {
url: metadata.final_url,
introduction_type: IntroductionType::WORKER
.to_str()
.expect("Guaranteed by definition")
.to_owned(),
external: true, // Worker scripts are always external.
worker_id: Some(global.upcast::<WorkerGlobalScope>().get_worker_id()),
content: Some(source.to_string()),
content_type: metadata.content_type.map(|c_type| c_type.0.to_string()),
};
let _ = chan.send(ScriptToDevtoolsControlMsg::CreateSourceActor(
pipeline_id,
source_info,
));
}
unsafe {
// Handle interrupt requests

View file

@ -8,7 +8,6 @@ use std::path::PathBuf;
use std::rc::Rc;
use base::id::{PipelineId, WebViewId};
use devtools_traits::{ScriptToDevtoolsControlMsg, SourceInfo};
use dom_struct::dom_struct;
use encoding_rs::Encoding;
use html5ever::{LocalName, Prefix, local_name, ns};
@ -1024,59 +1023,6 @@ impl HTMLScriptElement {
Ok(script) => script,
};
if let Some(chan) = self.global().devtools_chan() {
let pipeline_id = self.global().pipeline_id();
let (url, content, content_type, introduction_type, is_external) = if script.external {
let content = match &script.code {
SourceCode::Text(text) => text.to_string(),
SourceCode::Compiled(compiled) => compiled.original_text.to_string(),
};
// content_type: https://html.spec.whatwg.org/multipage/#scriptingLanguages
(
script.url.clone(),
Some(content),
"text/javascript",
IntroductionType::SRC_SCRIPT
.to_str()
.expect("Guaranteed by definition")
.to_owned(),
true,
)
} else {
// 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 cant fetch it from cache, we should probably give up, because with a real
// fetch, the server could return a different response.
// TODO: handle cases where Content-Type is not text/html.
(
doc.url(),
None,
"text/html",
IntroductionType::INLINE_SCRIPT
.to_str()
.expect("Guaranteed by definition")
.to_owned(),
false,
)
};
let source_info = SourceInfo {
url,
introduction_type: introduction_type.to_owned(),
external: is_external,
worker_id: None,
content,
content_type: Some(content_type.to_string()),
};
let _ = chan.send(ScriptToDevtoolsControlMsg::CreateSourceActor(
pipeline_id,
source_info,
));
}
if script.type_ == ScriptType::Classic {
unminify_js(&mut script);
self.substitute_with_local_script(&mut script);

View file

@ -1254,20 +1254,36 @@ pub(crate) use script_bindings::script_runtime::CanGc;
// TODO: squish `scriptElement` <https://searchfox.org/mozilla-central/rev/202069c4c5113a1a9052d84fa4679d4c1b22113e/devtools/server/actors/source.js#199-201>
pub(crate) struct IntroductionType;
impl IntroductionType {
/// `introductionType` for code passed to `eval`.
pub const EVAL: &CStr = c"eval";
pub const EVAL_STR: &str = "eval";
/// `introductionType` for code evaluated by debugger.
/// This includes code run via the devtools repl, even if the thread is not paused.
pub const DEBUGGER_EVAL: &CStr = c"debugger eval";
pub const DEBUGGER_EVAL_STR: &str = "debugger eval";
/// `introductionType` for code passed to the `Function` constructor.
pub const FUNCTION: &CStr = c"Function";
pub const FUNCTION_STR: &str = "Function";
/// `introductionType` for code loaded by worklet.
pub const WORKLET: &CStr = c"Worklet";
pub const WORKLET_STR: &str = "Worklet";
/// `introductionType` for code assigned to DOM elements event handler IDL attributes as a string.
pub const EVENT_HANDLER: &CStr = c"eventHandler";
pub const EVENT_HANDLER_STR: &str = "eventHandler";
/// `introductionType` for code belonging to `<script src="file.js">` elements.
/// This includes `<script type="module" src="...">`.
pub const SRC_SCRIPT: &CStr = c"srcScript";
pub const SRC_SCRIPT_STR: &str = "srcScript";
/// `introductionType` for code belonging to `<script>code;</script>` elements.
/// This includes `<script type="module" src="...">`.
pub const INLINE_SCRIPT: &CStr = c"inlineScript";
pub const INLINE_SCRIPT_STR: &str = "inlineScript";
/// `introductionType` for code belonging to scripts that *would* be `"inlineScript"` except that they were not
/// part of the initial file itself.
@ -1275,18 +1291,24 @@ impl IntroductionType {
/// - `document.write("<script>code;</script>")`
/// - `var s = document.createElement("script"); s.text = "code";`
pub const INJECTED_SCRIPT: &CStr = c"injectedScript";
pub const INJECTED_SCRIPT_STR: &str = "injectedScript";
/// `introductionType` for code that was loaded indirectly by being imported by another script
/// using ESM static or dynamic imports.
pub const IMPORTED_MODULE: &CStr = c"importedModule";
pub const IMPORTED_MODULE_STR: &str = "importedModule";
/// `introductionType` for code presented in `javascript:` URLs.
pub const JAVASCRIPT_URL: &CStr = c"javascriptURL";
pub const JAVASCRIPT_URL_STR: &str = "javascriptURL";
/// `introductionType` for code passed to `setTimeout`/`setInterval` as a string.
pub const DOM_TIMER: &CStr = c"domTimer";
pub const DOM_TIMER_STR: &str = "domTimer";
/// `introductionType` for web workers.
/// <https://searchfox.org/mozilla-central/rev/202069c4c5113a1a9052d84fa4679d4c1b22113e/devtools/docs/user/debugger-api/debugger.source/index.rst#96>
/// FIXME: only documented in older(?) devtools user docs
/// <https://firefox-source-docs.mozilla.org/devtools-user/debugger-api/debugger.source/index.html>
pub const WORKER: &CStr = c"Worker";
pub const WORKER_STR: &str = "Worker";
}