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