script/dom/
debuggerglobalscope.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::borrow::Cow;
6use std::cell::RefCell;
7
8use base::id::{Index, PipelineId, PipelineNamespaceId};
9use constellation_traits::ScriptToConstellationChan;
10use devtools_traits::{DevtoolScriptControlMsg, ScriptToDevtoolsControlMsg, SourceInfo, WorkerId};
11use dom_struct::dom_struct;
12use embedder_traits::resources::{self, Resource};
13use embedder_traits::{JavaScriptEvaluationError, ScriptToEmbedderChan};
14use ipc_channel::ipc::IpcSender;
15use js::jsval::UndefinedValue;
16use js::rust::wrappers::JS_DefineDebuggerObject;
17use net_traits::ResourceThreads;
18use profile_traits::{mem, time};
19use script_bindings::codegen::GenericBindings::DebuggerGetPossibleBreakpointsEventBinding::RecommendedBreakpointLocation;
20use script_bindings::codegen::GenericBindings::DebuggerGlobalScopeBinding::{
21    DebuggerGlobalScopeMethods, NotifyNewSource,
22};
23use script_bindings::realms::InRealm;
24use script_bindings::reflector::DomObject;
25use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
26use storage_traits::StorageThreads;
27
28use crate::dom::bindings::codegen::Bindings::DebuggerGlobalScopeBinding;
29use crate::dom::bindings::error::report_pending_exception;
30use crate::dom::bindings::inheritance::Castable;
31use crate::dom::bindings::root::DomRoot;
32use crate::dom::bindings::utils::define_all_exposed_interfaces;
33use crate::dom::globalscope::GlobalScope;
34use crate::dom::types::{DebuggerAddDebuggeeEvent, DebuggerGetPossibleBreakpointsEvent, Event};
35#[cfg(feature = "testbinding")]
36#[cfg(feature = "webgpu")]
37use crate::dom::webgpu::identityhub::IdentityHub;
38use crate::realms::enter_realm;
39use crate::script_module::ScriptFetchOptions;
40use crate::script_runtime::{CanGc, IntroductionType, JSContext};
41
42#[dom_struct]
43/// Global scope for interacting with the devtools Debugger API.
44///
45/// <https://firefox-source-docs.mozilla.org/js/Debugger/>
46pub(crate) struct DebuggerGlobalScope {
47    global_scope: GlobalScope,
48    #[no_trace]
49    devtools_to_script_sender: IpcSender<DevtoolScriptControlMsg>,
50    #[no_trace]
51    get_possible_breakpoints_result_sender:
52        RefCell<Option<IpcSender<Vec<devtools_traits::RecommendedBreakpointLocation>>>>,
53}
54
55impl DebuggerGlobalScope {
56    /// Create a new heap-allocated `DebuggerGlobalScope`.
57    ///
58    /// `debugger_pipeline_id` is the pipeline id to use when creating the debugger’s [`GlobalScope`]:
59    /// - in normal script threads, it should be set to `PipelineId::new()`, because those threads can generate
60    ///   pipeline ids, and they may contain debuggees from more than one pipeline
61    /// - in web worker threads, it should be set to the pipeline id of the page that created the thread, because
62    ///   those threads can’t generate pipeline ids, and they only contain one debuggee from one pipeline
63    #[expect(unsafe_code, clippy::too_many_arguments)]
64    pub(crate) fn new(
65        debugger_pipeline_id: PipelineId,
66        script_to_devtools_sender: Option<IpcSender<ScriptToDevtoolsControlMsg>>,
67        devtools_to_script_sender: IpcSender<DevtoolScriptControlMsg>,
68        mem_profiler_chan: mem::ProfilerChan,
69        time_profiler_chan: time::ProfilerChan,
70        script_to_constellation_chan: ScriptToConstellationChan,
71        script_to_embedder_chan: ScriptToEmbedderChan,
72        resource_threads: ResourceThreads,
73        storage_threads: StorageThreads,
74        #[cfg(feature = "webgpu")] gpu_id_hub: std::sync::Arc<IdentityHub>,
75        can_gc: CanGc,
76    ) -> DomRoot<Self> {
77        let global = Box::new(Self {
78            global_scope: GlobalScope::new_inherited(
79                debugger_pipeline_id,
80                script_to_devtools_sender,
81                mem_profiler_chan,
82                time_profiler_chan,
83                script_to_constellation_chan,
84                script_to_embedder_chan,
85                resource_threads,
86                storage_threads,
87                MutableOrigin::new(ImmutableOrigin::new_opaque()),
88                ServoUrl::parse_with_base(None, "about:internal/debugger")
89                    .expect("Guaranteed by argument"),
90                None,
91                #[cfg(feature = "webgpu")]
92                gpu_id_hub,
93                None,
94                false,
95                None, // font_context
96            ),
97            devtools_to_script_sender,
98            get_possible_breakpoints_result_sender: RefCell::new(None),
99        });
100        let global =
101            DebuggerGlobalScopeBinding::Wrap::<crate::DomTypeHolder>(GlobalScope::get_cx(), global);
102
103        let realm = enter_realm(&*global);
104        define_all_exposed_interfaces(global.upcast(), InRealm::entered(&realm), can_gc);
105        assert!(unsafe {
106            // Invariants: `cx` must be a non-null, valid JSContext pointer,
107            // and `obj` must be a handle to a JS global object.
108            JS_DefineDebuggerObject(
109                *Self::get_cx(),
110                global.global_scope.reflector().get_jsobject(),
111            )
112        });
113
114        global
115    }
116
117    /// Get the JS context.
118    pub(crate) fn get_cx() -> JSContext {
119        GlobalScope::get_cx()
120    }
121
122    pub(crate) fn as_global_scope(&self) -> &GlobalScope {
123        self.upcast::<GlobalScope>()
124    }
125
126    fn evaluate_js(
127        &self,
128        script: Cow<'_, str>,
129        can_gc: CanGc,
130    ) -> Result<(), JavaScriptEvaluationError> {
131        rooted!(in (*Self::get_cx()) let mut rval = UndefinedValue());
132        self.global_scope.evaluate_js_on_global_with_result(
133            script,
134            rval.handle_mut(),
135            ScriptFetchOptions::default_classic_script(&self.global_scope),
136            self.global_scope.api_base_url(),
137            can_gc,
138            None,
139        )
140    }
141
142    pub(crate) fn execute(&self, can_gc: CanGc) {
143        if self
144            .evaluate_js(resources::read_string(Resource::DebuggerJS).into(), can_gc)
145            .is_err()
146        {
147            let ar = enter_realm(self);
148            report_pending_exception(Self::get_cx(), true, InRealm::Entered(&ar), can_gc);
149        }
150    }
151
152    pub(crate) fn fire_add_debuggee(
153        &self,
154        can_gc: CanGc,
155        debuggee_global: &GlobalScope,
156        debuggee_pipeline_id: PipelineId,
157        debuggee_worker_id: Option<WorkerId>,
158    ) {
159        let _realm = enter_realm(self);
160        let debuggee_pipeline_id =
161            crate::dom::pipelineid::PipelineId::new(self.upcast(), debuggee_pipeline_id, can_gc);
162        let event = DomRoot::upcast::<Event>(DebuggerAddDebuggeeEvent::new(
163            self.upcast(),
164            debuggee_global,
165            &debuggee_pipeline_id,
166            debuggee_worker_id.map(|id| id.to_string().into()),
167            can_gc,
168        ));
169        assert!(
170            DomRoot::upcast::<Event>(event).fire(self.upcast(), can_gc),
171            "Guaranteed by DebuggerAddDebuggeeEvent::new"
172        );
173    }
174
175    pub(crate) fn fire_get_possible_breakpoints(
176        &self,
177        can_gc: CanGc,
178        spidermonkey_id: u32,
179        result_sender: IpcSender<Vec<devtools_traits::RecommendedBreakpointLocation>>,
180    ) {
181        assert!(
182            self.get_possible_breakpoints_result_sender
183                .replace(Some(result_sender))
184                .is_none()
185        );
186        let _realm = enter_realm(self);
187        let event = DomRoot::upcast::<Event>(DebuggerGetPossibleBreakpointsEvent::new(
188            self.upcast(),
189            spidermonkey_id,
190            can_gc,
191        ));
192        assert!(
193            DomRoot::upcast::<Event>(event).fire(self.upcast(), can_gc),
194            "Guaranteed by DebuggerGetPossibleBreakpointsEvent::new"
195        );
196    }
197}
198
199impl DebuggerGlobalScopeMethods<crate::DomTypeHolder> for DebuggerGlobalScope {
200    // check-tidy: no specs after this line
201    fn NotifyNewSource(&self, args: &NotifyNewSource) {
202        let Some(devtools_chan) = self.as_global_scope().devtools_chan() else {
203            return;
204        };
205        let pipeline_id = PipelineId {
206            namespace_id: PipelineNamespaceId(args.pipelineId.namespaceId),
207            index: Index::new(args.pipelineId.index).expect("`pipelineId.index` must not be zero"),
208        };
209
210        if let Some(introduction_type) = args.introductionType.as_ref() {
211            // Check the `introductionType` and `url`, decide whether or not to create a source actor, and if so,
212            // tell the devtools server to create a source actor. Based on the Firefox impl in:
213            // - getDebuggerSourceURL() <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/utils/source-url.js#7-42>
214            // - getSourceURL() <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#67-109>
215            // - resolveSourceURL() <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#48-66>
216            // - SourceActor#_isInlineSource <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#130-143>
217            // - SourceActor#url <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#157-162>
218
219            // Firefox impl: getDebuggerSourceURL(), getSourceURL()
220            // TODO: handle `about:srcdoc` case (see Firefox getDebuggerSourceURL())
221            // TODO: remove trailing details that may have been appended by SpiderMonkey
222            // (currently impossible to do robustly due to <https://bugzilla.mozilla.org/show_bug.cgi?id=1982001>)
223            let url_original = args.url.str();
224            // FIXME: use page/worker url as base here
225            let url_original = ServoUrl::parse(&url_original).ok();
226
227            // If the source has a `urlOverride` (aka `displayURL` aka `//# sourceURL`), it should be a valid url,
228            // possibly relative to the page/worker url, and we should treat the source as coming from that url for
229            // devtools purposes, including the file tree in the Sources tab.
230            // Firefox impl: getSourceURL()
231            let url_override = args
232                .urlOverride
233                .as_ref()
234                .map(|url| url.str())
235                // FIXME: use page/worker url as base here, not `url_original`
236                .and_then(|url| ServoUrl::parse_with_base(url_original.as_ref(), &url).ok());
237
238            // If the `introductionType` is “eval or eval-like”, the `url` won’t be meaningful, so ignore these
239            // sources unless we have a `urlOverride` (aka `displayURL` aka `//# sourceURL`).
240            // Firefox impl: getDebuggerSourceURL(), getSourceURL()
241            if [
242                IntroductionType::INJECTED_SCRIPT_STR,
243                IntroductionType::EVAL_STR,
244                IntroductionType::DEBUGGER_EVAL_STR,
245                IntroductionType::FUNCTION_STR,
246                IntroductionType::JAVASCRIPT_URL_STR,
247                IntroductionType::EVENT_HANDLER_STR,
248                IntroductionType::DOM_TIMER_STR,
249            ]
250            .contains(&&*introduction_type.str()) &&
251                url_override.is_none()
252            {
253                debug!(
254                    "Not creating debuggee: `introductionType` is `{introduction_type}` but no valid url"
255                );
256                return;
257            }
258
259            // Sources with an `introductionType` of `inlineScript` are generally inline, meaning their contents
260            // are a substring of the page markup (hence not known to SpiderMonkey, requiring plumbing in Servo).
261            // But sources with a `urlOverride` are not inline, since they get their own place in the Sources tree.
262            // nor are sources created for `<iframe srcdoc>`, since they are not necessarily a substring of the
263            // page markup as originally sent by the server.
264            // Firefox impl: SourceActor#_isInlineSource
265            // TODO: handle `about:srcdoc` case (see Firefox SourceActor#_isInlineSource)
266            let inline = introduction_type.str() == "inlineScript" && url_override.is_none();
267            let Some(url) = url_override.or(url_original) else {
268                debug!("Not creating debuggee: no valid url");
269                return;
270            };
271
272            let worker_id = args.workerId.as_ref().map(|id| id.parse().unwrap());
273
274            let source_info = SourceInfo {
275                url,
276                introduction_type: introduction_type.str().to_owned(),
277                inline,
278                worker_id,
279                content: (!inline).then(|| args.text.to_string()),
280                content_type: None, // TODO
281                spidermonkey_id: args.spidermonkeyId,
282            };
283            if let Err(error) = devtools_chan.send(ScriptToDevtoolsControlMsg::CreateSourceActor(
284                self.devtools_to_script_sender.clone(),
285                pipeline_id,
286                source_info,
287            )) {
288                warn!("Failed to send to devtools server: {error:?}");
289            }
290        } else {
291            debug!("Not creating debuggee for script with no `introductionType`");
292        }
293    }
294
295    fn GetPossibleBreakpointsResult(
296        &self,
297        event: &DebuggerGetPossibleBreakpointsEvent,
298        result: Vec<RecommendedBreakpointLocation>,
299    ) {
300        info!("GetPossibleBreakpointsResult: {event:?} {result:?}");
301        let sender = self
302            .get_possible_breakpoints_result_sender
303            .take()
304            .expect("Guaranteed by Self::fire_get_possible_breakpoints()");
305        let _ = sender.send(
306            result
307                .into_iter()
308                .map(|entry| devtools_traits::RecommendedBreakpointLocation {
309                    offset: entry.offset,
310                    line_number: entry.lineNumber,
311                    column_number: entry.columnNumber,
312                    is_step_start: entry.isStepStart,
313                })
314                .collect(),
315        );
316    }
317}