Create source actors from Debugger API notifications (#38334)

Co-authored-by: atbrakhi <atbrakhi@igalia.com>
Signed-off-by: Delan Azabani <dazabani@igalia.com>
This commit is contained in:
Delan Azabani 2025-07-29 18:45:20 +08:00
parent ad18638534
commit 4657f62f3b
9 changed files with 439 additions and 94 deletions

View file

@ -56,6 +56,7 @@ pub struct SourceActor {
pub content: Option<String>,
pub content_type: Option<String>,
pub spidermonkey_id: u32,
/// `introductionType` in SpiderMonkey `CompileOptionsWrapper`.
pub introduction_type: String,
}
@ -96,6 +97,7 @@ impl SourceActor {
url: ServoUrl,
content: Option<String>,
content_type: Option<String>,
spidermonkey_id: u32,
introduction_type: String,
) -> SourceActor {
SourceActor {
@ -104,6 +106,7 @@ impl SourceActor {
content,
content_type,
is_black_boxed: false,
spidermonkey_id,
introduction_type,
}
}
@ -114,6 +117,7 @@ impl SourceActor {
url: ServoUrl,
content: Option<String>,
content_type: Option<String>,
spidermonkey_id: u32,
introduction_type: String,
) -> &SourceActor {
let source_actor_name = actors.new_name("source");
@ -123,6 +127,7 @@ impl SourceActor {
url,
content,
content_type,
spidermonkey_id,
introduction_type,
);
actors.register(Box::new(source_actor));

View file

@ -552,6 +552,7 @@ impl DevtoolsInstance {
source_info.url,
source_content,
source_info.content_type,
source_info.spidermonkey_id,
source_info.introduction_type,
);
let source_actor_name = source_actor.name.clone();

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::resources::{self, Resource};
use ipc_channel::ipc::IpcSender;
@ -13,6 +13,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};
@ -104,6 +107,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) -> bool {
rooted!(in (*Self::get_cx()) let mut rval = UndefinedValue());
self.global_scope.evaluate_js_on_global_with_result(
@ -145,3 +152,89 @@ impl DebuggerGlobalScope {
);
}
}
impl DebuggerGlobalScopeMethods<crate::DomTypeHolder> for DebuggerGlobalScope {
// check-tidy: no specs after this line
fn NotifyNewSource(&self, args: &NotifyNewSource) {
info!(
"NotifyNewSource: ({},{}) {} {} {}",
args.pipelineId.namespaceId,
args.pipelineId.index,
args.spidermonkeyId,
args.url,
args.text
);
if let Some(devtools_chan) = self.as_global_scope().devtools_chan() {
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() {
// TODO: handle the other cases in
// <https://searchfox.org/mozilla-central/rev/f6a806c38c459e0e0d797d264ca0e8ad46005105/devtools/server/actors/utils/source-url.js#34-39>
// <https://searchfox.org/mozilla-central/rev/5446303cba9b19b9e88937be62936a96086dcf32/devtools/server/actors/source.js#65-98>
// TODO: remove trailing details that may have been appended by SpiderMonkey (currently buggy)
// <https://bugzilla.mozilla.org/show_bug.cgi?id=1982001>
let url_original = args.url.str();
let url_original = ServoUrl::parse(url_original).ok();
let url_override = args
.urlOverride
.as_ref()
.map(|url| url.str())
// TODO: do we need to use the page url as base here, say if url_original fails to parse?
.and_then(|url| ServoUrl::parse_with_base(url_original.as_ref(), url).ok());
// <https://searchfox.org/mozilla-central/rev/f6a806c38c459e0e0d797d264ca0e8ad46005105/devtools/server/actors/utils/source-url.js#21-33>
if [
"injectedScript",
"eval",
"debugger eval",
"Function",
"javascriptURL",
"eventHandler",
"domTimer",
]
.contains(&introduction_type.str()) &&
url_override.is_none()
{
debug!(
"Not creating debuggee: `introductionType` is `{introduction_type}` but no valid url"
);
return;
}
// TODO: handle the other cases in
// <https://searchfox.org/mozilla-central/rev/5446303cba9b19b9e88937be62936a96086dcf32/devtools/server/actors/source.js#126-133>
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| dbg!(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,
};
devtools_chan
.send(ScriptToDevtoolsControlMsg::CreateSourceActor(
pipeline_id,
source_info,
))
.expect("Failed to send to devtools server");
} 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;
@ -536,24 +536,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

@ -1024,59 +1024,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

@ -6,4 +6,27 @@
// web pages.
[Global=DebuggerGlobalScope, Exposed=DebuggerGlobalScope]
interface DebuggerGlobalScope: GlobalScope {
undefined notifyNewSource(NotifyNewSource args);
};
// http://dev.w3.org/csswg/cssom-view/#extensions-to-the-window-interface
dictionary NotifyNewSource {
required PipelineIdInit pipelineId;
required DOMString? workerId;
required unsigned long spidermonkeyId;
required DOMString url;
required DOMString? urlOverride;
required DOMString text;
required DOMString? introductionType;
// FIXME: error[E0599]: the method `trace` exists for reference `&Option<TypedArray<Uint8, *mut JSObject>>`, but
// its trait bounds were not satisfied
// Uint8Array binary;
// TODO: contentType
};
dictionary PipelineIdInit {
required unsigned long namespaceId;
required unsigned long index;
};

View file

@ -598,8 +598,9 @@ impl fmt::Display for ShadowRootMode {
pub struct SourceInfo {
pub url: ServoUrl,
pub introduction_type: String,
pub external: bool,
pub inline: bool,
pub worker_id: Option<WorkerId>,
pub content: Option<String>,
pub content_type: Option<String>,
pub spidermonkey_id: u32,
}

View file

@ -71,8 +71,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
[
Source("srcScript", f"{self.base_urls[0]}/classic.js"),
Source("inlineScript", f"{self.base_urls[0]}/test.html"),
Source("srcScript", f"{self.base_urls[1]}/classic.js"),
Source("inlineScript", f"{self.base_urls[0]}/test.html"),
Source("srcScript", f"{self.base_urls[1]}/classic.js"),
Source("importedModule", f"{self.base_urls[0]}/module.js"),
]
),
tuple([Source("Worker", f"{self.base_urls[0]}/classic_worker.js")]),
@ -116,7 +117,6 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
# Sources list for `introductionType` = `importedModule`
@unittest.expectedFailure
def test_sources_list_with_static_import_module(self):
self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources"))
self.run_servoshell(url=f"{self.base_urls[0]}/test_sources_list_with_static_import_module.html")
@ -135,7 +135,6 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
),
)
@unittest.expectedFailure
def test_sources_list_with_dynamic_import_module(self):
self.start_web_server(test_dir=os.path.join(DevtoolsTests.script_path, "devtools_tests/sources"))
self.run_servoshell(url=f"{self.base_urls[0]}/test_sources_list_with_dynamic_import_module.html")
@ -196,6 +195,302 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
),
)
# Sources list for `introductionType` set to values that require `displayURL` (`//# sourceURL`)
def test_sources_list_with_injected_script_write_and_display_url(self):
self.run_servoshell(
url='data:text/html,<script>document.write("<script>//%23 sourceURL=http://test</scr"+"ipt>")</script>'
)
self.assert_sources_list(
set(
[
tuple(
[
Source(
"inlineScript",
'data:text/html,<script>document.write("<script>//%23 sourceURL=http://test</scr"+"ipt>")</script>',
),
Source("injectedScript", "http://test/"),
]
)
]
)
)
def test_sources_list_with_injected_script_write_but_no_display_url(self):
self.run_servoshell(url='data:text/html,<script>document.write("<script>1</scr"+"ipt>")</script>')
self.assert_sources_list(
set(
[
tuple(
[
Source(
"inlineScript",
'data:text/html,<script>document.write("<script>1</scr"+"ipt>")</script>',
),
]
)
]
)
)
def test_sources_list_with_injected_script_append_and_display_url(self):
script = 's=document.createElement("script");s.append("//%23 sourceURL=http://test");document.body.append(s)'
self.run_servoshell(url=f"data:text/html,<body><script>{script}</script>")
self.assert_sources_list(
set(
[
tuple(
[
Source(
"inlineScript",
f"data:text/html,<body><script>{script}</script>",
),
Source("injectedScript", "http://test/"),
]
)
]
)
)
def test_sources_list_with_injected_script_append_but_no_display_url(self):
script = 's=document.createElement("script");s.append("1");document.body.append(s)'
self.run_servoshell(url=f"data:text/html,<body><script>{script}</script>")
self.assert_sources_list(
set(
[
tuple(
[
Source(
"inlineScript",
f"data:text/html,<body><script>{script}</script>",
),
]
)
]
)
)
def test_sources_list_with_eval_and_display_url(self):
self.run_servoshell(url='data:text/html,<script>eval("//%23 sourceURL=http://test")</script>')
self.assert_sources_list(
set(
[
tuple(
[
Source(
"inlineScript", 'data:text/html,<script>eval("//%23 sourceURL=http://test")</script>'
),
Source("eval", "http://test/"),
]
)
]
)
)
def test_sources_list_with_eval_but_no_display_url(self):
self.run_servoshell(url='data:text/html,<script>eval("1")</script>')
self.assert_sources_list(set([tuple([Source("inlineScript", 'data:text/html,<script>eval("1")</script>')])]))
def test_sources_list_with_debugger_eval_and_display_url(self):
pass
def test_sources_list_with_debugger_eval_but_no_display_url(self):
pass
def test_sources_list_with_function_and_display_url(self):
self.run_servoshell(url='data:text/html,<script>new Function("//%23 sourceURL=http://test")</script>')
self.assert_sources_list(
set(
[
tuple(
[
Source(
"inlineScript",
'data:text/html,<script>new Function("//%23 sourceURL=http://test")</script>',
),
Source("Function", "http://test/"),
]
)
]
)
)
def test_sources_list_with_function_but_no_display_url(self):
self.run_servoshell(url='data:text/html,<script>new Function("1")</script>')
self.assert_sources_list(
set(
[
tuple(
[
Source("inlineScript", 'data:text/html,<script>new Function("1")</script>'),
]
)
]
)
)
def test_sources_list_with_javascript_url_and_display_url(self):
# “1” prefix is a workaround for <https://github.com/servo/servo/issues/38547>
self.run_servoshell(
url='data:text/html,<a href="javascript:1//%23 sourceURL=http://test"></a><script>document.querySelector("a").click()</script>'
)
self.assert_sources_list(
set(
[
tuple(
[
Source(
"inlineScript",
'data:text/html,<a href="javascript:1//%23 sourceURL=http://test"></a><script>document.querySelector("a").click()</script>',
),
Source("javascriptURL", "http://test/"),
]
)
]
)
)
def test_sources_list_with_javascript_url_but_no_display_url(self):
self.run_servoshell(url='data:text/html,<a href="javascript:1"></a>')
self.assert_sources_list(set([tuple([])]))
@unittest.expectedFailure
def test_sources_list_with_event_handler_and_display_url(self):
self.run_servoshell(url='data:text/html,<a onclick="//%23 sourceURL=http://test"></a>')
self.assert_sources_list(
set(
[
tuple(
[
Source("eventHandler", "http://test/"),
]
)
]
)
)
def test_sources_list_with_event_handler_but_no_display_url(self):
self.run_servoshell(url='data:text/html,<a onclick="1"></a>')
self.assert_sources_list(set([tuple([])]))
@unittest.expectedFailure
def test_sources_list_with_dom_timer_and_display_url(self):
self.run_servoshell(url='data:text/html,<script>setTimeout("//%23 sourceURL=http://test",0)</script>')
self.assert_sources_list(
set(
[
tuple(
[
Source("domTimer", "http://test/"),
]
)
]
)
)
@unittest.expectedFailure
def test_sources_list_with_dom_timer_but_no_display_url(self):
self.run_servoshell(url='data:text/html,<script>setTimeout("1",0)</script>')
self.assert_sources_list(set([tuple([])]))
# Sources list for scripts with `displayURL` (`//# sourceURL`), despite not being required by `introductionType`
def test_sources_list_with_inline_script_and_display_url(self):
self.run_servoshell(url="data:text/html,<script>//%23 sourceURL=http://test</script>")
self.assert_sources_list(
set(
[
tuple(
[
Source("inlineScript", "http://test/"),
]
)
]
)
)
# Extra test case for situation where `//# sourceURL` cant be parsed with page url as base.
def test_sources_list_with_inline_script_but_invalid_display_url(self):
self.run_servoshell(url="data:text/html,<script>//%23 sourceURL=test</script>")
self.assert_sources_list(
set(
[
tuple(
[
Source("inlineScript", "data:text/html,<script>//%23 sourceURL=test</script>"),
]
)
]
)
)
def test_sources_list_with_inline_script_but_no_display_url(self):
self.run_servoshell(url="data:text/html,<script>1</script>")
self.assert_sources_list(
set(
[
tuple(
[
Source("inlineScript", "data:text/html,<script>1</script>"),
]
)
]
)
)
# Sources list for inline scripts in `<iframe srcdoc>`
@unittest.expectedFailure
def test_sources_list_with_iframe_srcdoc_and_display_url(self):
self.run_servoshell(url='data:text/html,<iframe srcdoc="<script>//%23 sourceURL=http://test</script>">')
self.assert_sources_list(
set(
[
tuple(
[
Source("inlineScript", "http://test/"),
]
)
]
)
)
@unittest.expectedFailure
def test_sources_list_with_iframe_srcdoc_but_no_display_url(self):
self.run_servoshell(url='data:text/html,<iframe srcdoc="<script>1</script>">')
self.assert_sources_list(
set(
[
tuple(
[
# FIXME: its not really gonna be 0
Source("inlineScript", "about:srcdoc#0"),
]
)
]
)
)
@unittest.expectedFailure
def test_sources_list_with_iframe_srcdoc_multiple_inline_scripts(self):
self.run_servoshell(
url='data:text/html,<iframe srcdoc="<script>//%23 sourceURL=http://test</script><script>2</script>">'
)
self.assert_sources_list(
set(
[
tuple(
[
Source("inlineScript", "http://test/"),
# FIXME: its not really gonna be 0
Source("inlineScript", "about:srcdoc#0"),
]
)
]
)
)
# Source contents
def test_source_content_inline_script(self):

View file

@ -6,24 +6,26 @@ const dbg = new Debugger;
const debuggeesToPipelineIds = new Map;
const debuggeesToWorkerIds = new Map;
dbg.onNewGlobalObject = function(global) {
dbg.uncaughtExceptionHook = function(error) {
console.error(`[debugger] Uncaught exception at ${error.fileName}:${error.lineNumber}:${error.columnNumber}: ${error.name}: ${error.message}`);
};
dbg.onNewScript = function(script, /* undefined; seems to be `script.global` now */ global) {
console.log("[debugger] onNewScript url=", script.url, "source id=", script.source.id, "introductionType=", script.source.introductionType, "displayURL=", script.source.displayURL);
try {
// TODO: notify script system about new source
/* notifyNewSource */({
pipelineId: debuggeesToPipelineIds.get(script.global),
workerId: debuggeesToWorkerIds.get(script.global),
spidermonkeyId: script.source.id,
url: script.source.url,
urlOverride: script.source.displayURL,
text: script.source.text,
introductionType: script.source.introductionType ?? null,
});
console.log("[debugger] source binary=", typeof script.source.binary);
} catch (error) {
logError(error);
// Do nothing; the source is not wasm
}
notifyNewSource({
pipelineId: debuggeesToPipelineIds.get(script.global),
workerId: debuggeesToWorkerIds.get(script.global),
spidermonkeyId: script.source.id,
url: script.source.url,
urlOverride: script.source.displayURL,
text: script.source.text,
introductionType: script.source.introductionType ?? null,
});
};
addEventListener("addDebuggee", event => {
@ -36,7 +38,3 @@ addEventListener("addDebuggee", event => {
});
debuggeesToWorkerIds.set(debuggerObject, workerId);
});
function logError(error) {
console.log(`[debugger] ERROR at ${error.fileName}:${error.lineNumber}:${error.columnNumber}: ${error.name}: ${error.message}`);
}