/* 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 base::id::{Index, PipelineId, PipelineNamespaceId}; use constellation_traits::ScriptToConstellationChan; use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, SourceInfo, WorkerId}; use dom_struct::dom_struct; use embedder_traits::resources::{self, Resource}; use embedder_traits::{JavaScriptEvaluationError, ScriptToEmbedderChan}; use ipc_channel::ipc::IpcSender; use js::jsval::UndefinedValue; use js::rust::wrappers::JS_DefineDebuggerObject; use net_traits::ResourceThreads; use profile_traits::{mem, time}; use script_bindings::codegen::GenericBindings::DebuggerGetPossibleBreakpointsEventBinding::RecommendedBreakpointLocation; use script_bindings::codegen::GenericBindings::DebuggerGlobalScopeBinding::{ DebuggerGlobalScopeMethods, NotifyNewSource, }; use script_bindings::realms::InRealm; use script_bindings::reflector::DomObject; use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; use crate::dom::bindings::codegen::Bindings::DebuggerGlobalScopeBinding; use crate::dom::bindings::error::report_pending_exception; use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::root::DomRoot; use crate::dom::bindings::utils::define_all_exposed_interfaces; use crate::dom::globalscope::GlobalScope; use crate::dom::types::{DebuggerAddDebuggeeEvent, DebuggerGetPossibleBreakpointsEvent, Event}; #[cfg(feature = "testbinding")] #[cfg(feature = "webgpu")] use crate::dom::webgpu::identityhub::IdentityHub; use crate::realms::enter_realm; use crate::script_module::ScriptFetchOptions; use crate::script_runtime::{CanGc, IntroductionType, JSContext}; #[dom_struct] /// Global scope for interacting with the devtools Debugger API. /// /// pub(crate) struct DebuggerGlobalScope { global_scope: GlobalScope, #[no_trace] devtools_to_script_sender: IpcSender, #[no_trace] get_possible_breakpoints_result_sender: RefCell>>>, } impl DebuggerGlobalScope { /// Create a new heap-allocated `DebuggerGlobalScope`. /// /// `debugger_pipeline_id` is the pipeline id to use when creating the debugger’s [`GlobalScope`]: /// - in normal script threads, it should be set to `PipelineId::new()`, because those threads can generate /// pipeline ids, and they may contain debuggees from more than one pipeline /// - in web worker threads, it should be set to the pipeline id of the page that created the thread, because /// those threads can’t generate pipeline ids, and they only contain one debuggee from one pipeline #[allow(unsafe_code, clippy::too_many_arguments)] pub(crate) fn new( debugger_pipeline_id: PipelineId, script_to_devtools_sender: Option>, devtools_to_script_sender: IpcSender, mem_profiler_chan: mem::ProfilerChan, time_profiler_chan: time::ProfilerChan, script_to_constellation_chan: ScriptToConstellationChan, script_to_embedder_chan: ScriptToEmbedderChan, resource_threads: ResourceThreads, #[cfg(feature = "webgpu")] gpu_id_hub: std::sync::Arc, can_gc: CanGc, ) -> DomRoot { let global = Box::new(Self { global_scope: GlobalScope::new_inherited( debugger_pipeline_id, script_to_devtools_sender, mem_profiler_chan, time_profiler_chan, script_to_constellation_chan, script_to_embedder_chan, resource_threads, MutableOrigin::new(ImmutableOrigin::new_opaque()), ServoUrl::parse_with_base(None, "about:internal/debugger") .expect("Guaranteed by argument"), None, Default::default(), #[cfg(feature = "webgpu")] gpu_id_hub, None, false, None, // font_context ), devtools_to_script_sender, get_possible_breakpoints_result_sender: RefCell::new(None), }); let global = DebuggerGlobalScopeBinding::Wrap::(GlobalScope::get_cx(), global); let realm = enter_realm(&*global); define_all_exposed_interfaces(global.upcast(), InRealm::entered(&realm), can_gc); assert!(unsafe { // Invariants: `cx` must be a non-null, valid JSContext pointer, // and `obj` must be a handle to a JS global object. JS_DefineDebuggerObject( *Self::get_cx(), global.global_scope.reflector().get_jsobject(), ) }); global } /// Get the JS context. pub(crate) fn get_cx() -> JSContext { GlobalScope::get_cx() } pub(crate) fn as_global_scope(&self) -> &GlobalScope { self.upcast::() } 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( script, rval.handle_mut(), ScriptFetchOptions::default_classic_script(&self.global_scope), self.global_scope.api_base_url(), can_gc, None, ) } pub(crate) fn execute(&self, can_gc: CanGc) { if self .evaluate_js(&resources::read_string(Resource::DebuggerJS), can_gc) .is_err() { let ar = enter_realm(self); report_pending_exception(Self::get_cx(), true, InRealm::Entered(&ar), can_gc); } } pub(crate) fn fire_add_debuggee( &self, can_gc: CanGc, debuggee_global: &GlobalScope, debuggee_pipeline_id: PipelineId, debuggee_worker_id: Option, ) { let debuggee_pipeline_id = crate::dom::pipelineid::PipelineId::new(self.upcast(), debuggee_pipeline_id, can_gc); let event = DomRoot::upcast::(DebuggerAddDebuggeeEvent::new( self.upcast(), debuggee_global, &debuggee_pipeline_id, debuggee_worker_id.map(|id| id.to_string().into()), can_gc, )); assert!( DomRoot::upcast::(event).fire(self.upcast(), can_gc), "Guaranteed by DebuggerAddDebuggeeEvent::new" ); } pub(crate) fn fire_get_possible_breakpoints( &self, can_gc: CanGc, spidermonkey_id: u32, result_sender: IpcSender>, ) { assert!( self.get_possible_breakpoints_result_sender .replace(Some(result_sender)) .is_none() ); let event = DomRoot::upcast::(DebuggerGetPossibleBreakpointsEvent::new( self.upcast(), spidermonkey_id, can_gc, )); assert!( DomRoot::upcast::(event).fire(self.upcast(), can_gc), "Guaranteed by DebuggerGetPossibleBreakpointsEvent::new" ); } } impl DebuggerGlobalScopeMethods 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() // - getSourceURL() // - resolveSourceURL() // - SourceActor#_isInlineSource // - SourceActor#url // 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 ) 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` won’t 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 `