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

@ -56,6 +56,9 @@ pub struct SourceActor {
pub content: Option<String>, pub content: Option<String>,
pub content_type: 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`. /// `introductionType` in SpiderMonkey `CompileOptionsWrapper`.
pub introduction_type: String, pub introduction_type: String,
} }
@ -96,6 +99,7 @@ impl SourceActor {
url: ServoUrl, url: ServoUrl,
content: Option<String>, content: Option<String>,
content_type: Option<String>, content_type: Option<String>,
spidermonkey_id: u32,
introduction_type: String, introduction_type: String,
) -> SourceActor { ) -> SourceActor {
SourceActor { SourceActor {
@ -104,6 +108,7 @@ impl SourceActor {
content, content,
content_type, content_type,
is_black_boxed: false, is_black_boxed: false,
spidermonkey_id,
introduction_type, introduction_type,
} }
} }
@ -114,6 +119,7 @@ impl SourceActor {
url: ServoUrl, url: ServoUrl,
content: Option<String>, content: Option<String>,
content_type: Option<String>, content_type: Option<String>,
spidermonkey_id: u32,
introduction_type: String, introduction_type: String,
) -> &SourceActor { ) -> &SourceActor {
let source_actor_name = actors.new_name("source"); let source_actor_name = actors.new_name("source");
@ -123,6 +129,7 @@ impl SourceActor {
url, url,
content, content,
content_type, content_type,
spidermonkey_id,
introduction_type, introduction_type,
); );
actors.register(Box::new(source_actor)); actors.register(Box::new(source_actor));
@ -160,6 +167,10 @@ impl Actor for SourceActor {
let reply = SourceContentReply { let reply = SourceContentReply {
from: self.name(), from: self.name(),
content_type: self.content_type.clone(), 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 cant 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 // 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)? // become available later (e.g. after a fetch)?
source: self source: self

View file

@ -552,6 +552,7 @@ impl DevtoolsInstance {
source_info.url, source_info.url,
source_content, source_content,
source_info.content_type, source_info.content_type,
source_info.spidermonkey_id,
source_info.introduction_type, source_info.introduction_type,
); );
let source_actor_name = source_actor.name.clone(); 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 * 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/. */ * 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 constellation_traits::ScriptToConstellationChan;
use devtools_traits::{ScriptToDevtoolsControlMsg, WorkerId}; use devtools_traits::{ScriptToDevtoolsControlMsg, SourceInfo, WorkerId};
use dom_struct::dom_struct; use dom_struct::dom_struct;
use embedder_traits::JavaScriptEvaluationError; use embedder_traits::JavaScriptEvaluationError;
use embedder_traits::resources::{self, Resource}; use embedder_traits::resources::{self, Resource};
@ -14,6 +14,9 @@ use js::rust::Runtime;
use js::rust::wrappers::JS_DefineDebuggerObject; use js::rust::wrappers::JS_DefineDebuggerObject;
use net_traits::ResourceThreads; use net_traits::ResourceThreads;
use profile_traits::{mem, time}; use profile_traits::{mem, time};
use script_bindings::codegen::GenericBindings::DebuggerGlobalScopeBinding::{
DebuggerGlobalScopeMethods, NotifyNewSource,
};
use script_bindings::realms::InRealm; use script_bindings::realms::InRealm;
use script_bindings::reflector::DomObject; use script_bindings::reflector::DomObject;
use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
@ -30,7 +33,7 @@ use crate::dom::types::{DebuggerAddDebuggeeEvent, Event};
use crate::dom::webgpu::identityhub::IdentityHub; use crate::dom::webgpu::identityhub::IdentityHub;
use crate::realms::enter_realm; use crate::realms::enter_realm;
use crate::script_module::ScriptFetchOptions; use crate::script_module::ScriptFetchOptions;
use crate::script_runtime::{CanGc, JSContext}; use crate::script_runtime::{CanGc, IntroductionType, JSContext};
#[dom_struct] #[dom_struct]
/// Global scope for interacting with the devtools Debugger API. /// Global scope for interacting with the devtools Debugger API.
@ -105,6 +108,10 @@ impl DebuggerGlobalScope {
GlobalScope::get_cx() 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> { fn evaluate_js(&self, script: &str, can_gc: CanGc) -> Result<(), JavaScriptEvaluationError> {
rooted!(in (*Self::get_cx()) let mut rval = UndefinedValue()); rooted!(in (*Self::get_cx()) let mut rval = UndefinedValue());
self.global_scope.evaluate_js_on_global_with_result( 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 base::id::{BrowsingContextId, PipelineId, WebViewId};
use constellation_traits::{WorkerGlobalScopeInit, WorkerScriptLoadOrigin}; use constellation_traits::{WorkerGlobalScopeInit, WorkerScriptLoadOrigin};
use crossbeam_channel::{Receiver, Sender, unbounded}; use crossbeam_channel::{Receiver, Sender, unbounded};
use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, SourceInfo}; use devtools_traits::DevtoolScriptControlMsg;
use dom_struct::dom_struct; use dom_struct::dom_struct;
use headers::{HeaderMapExt, ReferrerPolicy as ReferrerPolicyHeader}; use headers::{HeaderMapExt, ReferrerPolicy as ReferrerPolicyHeader};
use ipc_channel::ipc::IpcReceiver; use ipc_channel::ipc::IpcReceiver;
@ -60,9 +60,7 @@ use crate::fetch::{CspViolationsProcessor, load_whole_resource};
use crate::messaging::{CommonScriptMsg, ScriptEventLoopReceiver, ScriptEventLoopSender}; use crate::messaging::{CommonScriptMsg, ScriptEventLoopReceiver, ScriptEventLoopSender};
use crate::realms::{AlreadyInRealm, InRealm, enter_realm}; use crate::realms::{AlreadyInRealm, InRealm, enter_realm};
use crate::script_runtime::ScriptThreadEventCategory::WorkerEvent; use crate::script_runtime::ScriptThreadEventCategory::WorkerEvent;
use crate::script_runtime::{ use crate::script_runtime::{CanGc, JSContext as SafeJSContext, Runtime, ThreadSafeJSContext};
CanGc, IntroductionType, JSContext as SafeJSContext, Runtime, ThreadSafeJSContext,
};
use crate::task_queue::{QueuedTask, QueuedTaskConversion, TaskQueue}; use crate::task_queue::{QueuedTask, QueuedTaskConversion, TaskQueue};
use crate::task_source::{SendableTaskSource, TaskSourceName}; use crate::task_source::{SendableTaskSource, TaskSourceName};
@ -536,24 +534,6 @@ impl DedicatedWorkerGlobalScope {
)); ));
global_scope.set_https_state(metadata.https_state); global_scope.set_https_state(metadata.https_state);
let source = String::from_utf8_lossy(&bytes); 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 { unsafe {
// Handle interrupt requests // Handle interrupt requests

View file

@ -8,7 +8,6 @@ use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use base::id::{PipelineId, WebViewId}; use base::id::{PipelineId, WebViewId};
use devtools_traits::{ScriptToDevtoolsControlMsg, SourceInfo};
use dom_struct::dom_struct; use dom_struct::dom_struct;
use encoding_rs::Encoding; use encoding_rs::Encoding;
use html5ever::{LocalName, Prefix, local_name, ns}; use html5ever::{LocalName, Prefix, local_name, ns};
@ -1024,59 +1023,6 @@ impl HTMLScriptElement {
Ok(script) => script, 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 { if script.type_ == ScriptType::Classic {
unminify_js(&mut script); unminify_js(&mut script);
self.substitute_with_local_script(&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> // TODO: squish `scriptElement` <https://searchfox.org/mozilla-central/rev/202069c4c5113a1a9052d84fa4679d4c1b22113e/devtools/server/actors/source.js#199-201>
pub(crate) struct IntroductionType; pub(crate) struct IntroductionType;
impl 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. /// `introductionType` for code evaluated by debugger.
/// This includes code run via the devtools repl, even if the thread is not paused. /// 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: &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. /// `introductionType` for code loaded by worklet.
pub const WORKLET: &CStr = c"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. /// `introductionType` for code belonging to `<script src="file.js">` elements.
/// This includes `<script type="module" src="...">`. /// This includes `<script type="module" src="...">`.
pub const SRC_SCRIPT: &CStr = c"srcScript"; pub const SRC_SCRIPT: &CStr = c"srcScript";
pub const SRC_SCRIPT_STR: &str = "srcScript";
/// `introductionType` for code belonging to `<script>code;</script>` elements. /// `introductionType` for code belonging to `<script>code;</script>` elements.
/// This includes `<script type="module" src="...">`. /// This includes `<script type="module" src="...">`.
pub const INLINE_SCRIPT: &CStr = c"inlineScript"; 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 /// `introductionType` for code belonging to scripts that *would* be `"inlineScript"` except that they were not
/// part of the initial file itself. /// part of the initial file itself.
@ -1275,18 +1291,24 @@ impl IntroductionType {
/// - `document.write("<script>code;</script>")` /// - `document.write("<script>code;</script>")`
/// - `var s = document.createElement("script"); s.text = "code";` /// - `var s = document.createElement("script"); s.text = "code";`
pub const INJECTED_SCRIPT: &CStr = c"injectedScript"; 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 /// `introductionType` for code that was loaded indirectly by being imported by another script
/// using ESM static or dynamic imports. /// using ESM static or dynamic imports.
pub const IMPORTED_MODULE: &CStr = c"importedModule"; pub const IMPORTED_MODULE: &CStr = c"importedModule";
pub const IMPORTED_MODULE_STR: &str = "importedModule";
/// `introductionType` for code presented in `javascript:` URLs. /// `introductionType` for code presented in `javascript:` URLs.
pub const JAVASCRIPT_URL: &CStr = c"javascriptURL"; pub const JAVASCRIPT_URL: &CStr = c"javascriptURL";
pub const JAVASCRIPT_URL_STR: &str = "javascriptURL";
/// `introductionType` for code passed to `setTimeout`/`setInterval` as a string. /// `introductionType` for code passed to `setTimeout`/`setInterval` as a string.
pub const DOM_TIMER: &CStr = c"domTimer"; pub const DOM_TIMER: &CStr = c"domTimer";
pub const DOM_TIMER_STR: &str = "domTimer";
/// `introductionType` for web workers. /// `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: &CStr = c"Worker";
pub const WORKER_STR: &str = "Worker";
} }

View file

@ -6,4 +6,20 @@
// web pages. // web pages.
[Global=DebuggerGlobalScope, Exposed=DebuggerGlobalScope] [Global=DebuggerGlobalScope, Exposed=DebuggerGlobalScope]
interface DebuggerGlobalScope: GlobalScope { interface DebuggerGlobalScope: GlobalScope {
undefined notifyNewSource(NotifyNewSource args);
};
dictionary NotifyNewSource {
required PipelineIdInit pipelineId;
required DOMString? workerId;
required unsigned long spidermonkeyId;
required DOMString url;
required DOMString? urlOverride;
required DOMString text;
required DOMString? introductionType;
};
dictionary PipelineIdInit {
required unsigned long namespaceId;
required unsigned long index;
}; };

View file

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

View file

@ -13,6 +13,7 @@ import logging
from geckordp.actors.root import RootActor from geckordp.actors.root import RootActor
from geckordp.actors.descriptors.tab import TabActor from geckordp.actors.descriptors.tab import TabActor
from geckordp.actors.watcher import WatcherActor from geckordp.actors.watcher import WatcherActor
from geckordp.actors.web_console import WebConsoleActor
from geckordp.actors.resources import Resources from geckordp.actors.resources import Resources
from geckordp.actors.events import Events from geckordp.actors.events import Events
from geckordp.rdp_client import RDPClient from geckordp.rdp_client import RDPClient
@ -37,6 +38,13 @@ class Source:
url: str url: str
@dataclass
class Devtools:
client: RDPClient
watcher: WatcherActor
targets: list
class DevtoolsTests(unittest.IsolatedAsyncioTestCase): class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
# /path/to/servo/python/servo # /path/to/servo/python/servo
script_path = None script_path = None
@ -71,8 +79,9 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
[ [
Source("srcScript", f"{self.base_urls[0]}/classic.js"), Source("srcScript", f"{self.base_urls[0]}/classic.js"),
Source("inlineScript", f"{self.base_urls[0]}/test.html"), 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("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")]), tuple([Source("Worker", f"{self.base_urls[0]}/classic_worker.js")]),
@ -116,7 +125,6 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
# Sources list for `introductionType` = `importedModule` # Sources list for `introductionType` = `importedModule`
@unittest.expectedFailure
def test_sources_list_with_static_import_module(self): 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.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") self.run_servoshell(url=f"{self.base_urls[0]}/test_sources_list_with_static_import_module.html")
@ -135,7 +143,6 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
), ),
) )
@unittest.expectedFailure
def test_sources_list_with_dynamic_import_module(self): 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.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") self.run_servoshell(url=f"{self.base_urls[0]}/test_sources_list_with_dynamic_import_module.html")
@ -196,6 +203,324 @@ 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):
self.run_servoshell(url="data:text/html,")
devtools = self._setup_devtools_client()
console = WebConsoleActor(devtools.client, devtools.targets[0]["consoleActor"])
evaluation_result = Future()
async def on_evaluation_result(data: dict):
evaluation_result.set_result(data)
devtools.client.add_event_listener(console.actor_id, Events.WebConsole.EVALUATION_RESULT, on_evaluation_result)
console.evaluate_js_async("//# sourceURL=http://test")
evaluation_result.result(1)
self.assert_sources_list(set([tuple([Source("debugger eval", "http://test/")])]))
def test_sources_list_with_debugger_eval_but_no_display_url(self):
self.run_servoshell(url="data:text/html,")
devtools = self._setup_devtools_client()
console = WebConsoleActor(devtools.client, devtools.targets[0]["consoleActor"])
evaluation_result = Future()
async def on_evaluation_result(data: dict):
evaluation_result.set_result(data)
devtools.client.add_event_listener(console.actor_id, Events.WebConsole.EVALUATION_RESULT, on_evaluation_result)
console.evaluate_js_async("1")
evaluation_result.result(1)
self.assert_sources_list(set([tuple([])]))
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 # Source contents
def test_source_content_inline_script(self): def test_source_content_inline_script(self):
@ -320,7 +645,7 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
if self.base_urls is not None: if self.base_urls is not None:
self.base_urls = None self.base_urls = None
def _setup_devtools_client(self, expected_targets=1): def _setup_devtools_client(self, *, expected_targets=1) -> Devtools:
client = RDPClient() client = RDPClient()
client.connect("127.0.0.1", 6080) client.connect("127.0.0.1", 6080)
root = RootActor(client) root = RootActor(client)
@ -355,11 +680,14 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
if result: if result:
raise result raise result
return client, watcher, targets return Devtools(client, watcher, targets)
def assert_sources_list(self, expected_sources_by_target: set[tuple[Source]]): def assert_sources_list(
self, expected_sources_by_target: set[tuple[Source]], *, devtools: Optional[Devtools] = None
):
expected_targets = len(expected_sources_by_target) expected_targets = len(expected_sources_by_target)
client, watcher, targets = self._setup_devtools_client(expected_targets) if devtools is None:
devtools = self._setup_devtools_client(expected_targets=expected_targets)
done = Future() done = Future()
# NOTE: breaks if two targets have the same list of source urls. # NOTE: breaks if two targets have the same list of source urls.
# This should really be a multiset, but Python does not have multisets. # This should really be a multiset, but Python does not have multisets.
@ -379,22 +707,25 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
# Send the exception back so it can be raised. # Send the exception back so it can be raised.
done.set_result(e) done.set_result(e)
for target in targets: for target in devtools.targets:
client.add_event_listener( devtools.client.add_event_listener(
target["actor"], target["actor"],
Events.Watcher.RESOURCES_AVAILABLE_ARRAY, Events.Watcher.RESOURCES_AVAILABLE_ARRAY,
on_source_resource, on_source_resource,
) )
watcher.watch_resources([Resources.SOURCE]) devtools.watcher.watch_resources([Resources.SOURCE])
result: Optional[Exception] = done.result(1) result: Optional[Exception] = done.result(1)
if result: if result:
raise result raise result
self.assertEqual(actual_sources_by_target, expected_sources_by_target) self.assertEqual(actual_sources_by_target, expected_sources_by_target)
client.disconnect() devtools.client.disconnect()
def assert_source_content(self, expected_source: Source, expected_content: str): def assert_source_content(
client, watcher, targets = self._setup_devtools_client() self, expected_source: Source, expected_content: str, *, devtools: Optional[Devtools] = None
):
if devtools is None:
devtools = self._setup_devtools_client()
done = Future() done = Future()
source_actors = {} source_actors = {}
@ -410,13 +741,13 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
except Exception as e: except Exception as e:
done.set_result(e) done.set_result(e)
for target in targets: for target in devtools.targets:
client.add_event_listener( devtools.client.add_event_listener(
target["actor"], target["actor"],
Events.Watcher.RESOURCES_AVAILABLE_ARRAY, Events.Watcher.RESOURCES_AVAILABLE_ARRAY,
on_source_resource, on_source_resource,
) )
watcher.watch_resources([Resources.SOURCE]) devtools.watcher.watch_resources([Resources.SOURCE])
result: Optional[Exception] = done.result(1) result: Optional[Exception] = done.result(1)
if result: if result:
@ -426,11 +757,11 @@ class DevtoolsTests(unittest.IsolatedAsyncioTestCase):
self.assertIn(expected_source, source_actors) self.assertIn(expected_source, source_actors)
source_actor = source_actors[expected_source] source_actor = source_actors[expected_source]
response = client.send_receive({"to": source_actor, "type": "source"}) response = devtools.client.send_receive({"to": source_actor, "type": "source"})
self.assertEqual(response["source"], expected_content) self.assertEqual(response["source"], expected_content)
client.disconnect() devtools.client.disconnect()
def get_test_path(self, path: str) -> str: def get_test_path(self, path: str) -> str:
return os.path.join(DevtoolsTests.script_path, os.path.join("devtools_tests", path)) return os.path.join(DevtoolsTests.script_path, os.path.join("devtools_tests", path))

View file

@ -6,13 +6,13 @@ const dbg = new Debugger;
const debuggeesToPipelineIds = new Map; const debuggeesToPipelineIds = new Map;
const debuggeesToWorkerIds = 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) { dbg.onNewScript = function(script, /* undefined; seems to be `script.global` now */ global) {
try { // TODO: handle wasm (`script.source.introductionType == wasm`)
// TODO: notify script system about new source notifyNewSource({
/* notifyNewSource */({
pipelineId: debuggeesToPipelineIds.get(script.global), pipelineId: debuggeesToPipelineIds.get(script.global),
workerId: debuggeesToWorkerIds.get(script.global), workerId: debuggeesToWorkerIds.get(script.global),
spidermonkeyId: script.source.id, spidermonkeyId: script.source.id,
@ -21,9 +21,6 @@ dbg.onNewScript = function(script, /* undefined; seems to be `script.global` now
text: script.source.text, text: script.source.text,
introductionType: script.source.introductionType ?? null, introductionType: script.source.introductionType ?? null,
}); });
} catch (error) {
logError(error);
}
}; };
addEventListener("addDebuggee", event => { addEventListener("addDebuggee", event => {
@ -36,7 +33,3 @@ addEventListener("addDebuggee", event => {
}); });
debuggeesToWorkerIds.set(debuggerObject, workerId); debuggeesToWorkerIds.set(debuggerObject, workerId);
}); });
function logError(error) {
console.log(`[debugger] ERROR at ${error.fileName}:${error.lineNumber}:${error.columnNumber}: ${error.name}: ${error.message}`);
}