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::{
12    DevtoolScriptControlMsg, EvaluateJSReply, ScriptToDevtoolsControlMsg, SourceInfo, WorkerId,
13};
14use dom_struct::dom_struct;
15use embedder_traits::resources::{self, Resource};
16use embedder_traits::{JavaScriptEvaluationError, ScriptToEmbedderChan};
17use js::context::JSContext;
18use js::jsval::UndefinedValue;
19use js::rust::wrappers2::JS_DefineDebuggerObject;
20use net_traits::ResourceThreads;
21use profile_traits::{mem, time};
22use script_bindings::codegen::GenericBindings::DebuggerEvalEventBinding::EvalResultValue;
23use script_bindings::codegen::GenericBindings::DebuggerGetPossibleBreakpointsEventBinding::RecommendedBreakpointLocation;
24use script_bindings::codegen::GenericBindings::DebuggerGlobalScopeBinding::{
25    DebuggerGlobalScopeMethods, NotifyNewSource, PipelineIdInit,
26};
27use script_bindings::realms::InRealm;
28use script_bindings::reflector::DomObject;
29use script_bindings::str::DOMString;
30use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
31use storage_traits::StorageThreads;
32
33use crate::dom::bindings::codegen::Bindings::DebuggerGlobalScopeBinding;
34use crate::dom::bindings::codegen::Bindings::DebuggerPauseEventBinding::PauseFrameResult;
35use crate::dom::bindings::error::report_pending_exception;
36use crate::dom::bindings::inheritance::Castable;
37use crate::dom::bindings::root::DomRoot;
38use crate::dom::bindings::utils::define_all_exposed_interfaces;
39use crate::dom::debuggerclearbreakpointevent::DebuggerClearBreakpointEvent;
40use crate::dom::debuggerpauseevent::DebuggerPauseEvent;
41use crate::dom::debuggersetbreakpointevent::DebuggerSetBreakpointEvent;
42use crate::dom::globalscope::GlobalScope;
43use crate::dom::types::{
44    DebuggerAddDebuggeeEvent, DebuggerEvalEvent, DebuggerGetPossibleBreakpointsEvent, Event,
45};
46#[cfg(feature = "webgpu")]
47use crate::dom::webgpu::identityhub::IdentityHub;
48use crate::realms::{enter_auto_realm, enter_realm};
49use crate::script_runtime::{CanGc, IntroductionType};
50use crate::script_thread::with_script_thread;
51
52#[dom_struct]
53/// Global scope for interacting with the devtools Debugger API.
54///
55/// <https://firefox-source-docs.mozilla.org/js/Debugger/>
56pub(crate) struct DebuggerGlobalScope {
57    global_scope: GlobalScope,
58    #[no_trace]
59    devtools_to_script_sender: GenericSender<DevtoolScriptControlMsg>,
60    #[no_trace]
61    get_possible_breakpoints_result_sender:
62        RefCell<Option<GenericSender<Vec<devtools_traits::RecommendedBreakpointLocation>>>>,
63    #[no_trace]
64    get_frame_result_sender: RefCell<Option<GenericSender<devtools_traits::PauseFrameResult>>>,
65    #[no_trace]
66    eval_result_sender: RefCell<Option<GenericSender<EvaluateJSReply>>>,
67}
68
69impl DebuggerGlobalScope {
70    /// Create a new heap-allocated `DebuggerGlobalScope`.
71    ///
72    /// `debugger_pipeline_id` is the pipeline id to use when creating the debugger’s [`GlobalScope`]:
73    /// - in normal script threads, it should be set to `PipelineId::new()`, because those threads can generate
74    ///   pipeline ids, and they may contain debuggees from more than one pipeline
75    /// - in web worker threads, it should be set to the pipeline id of the page that created the thread, because
76    ///   those threads can’t generate pipeline ids, and they only contain one debuggee from one pipeline
77    #[expect(unsafe_code, clippy::too_many_arguments)]
78    pub(crate) fn new(
79        debugger_pipeline_id: PipelineId,
80        script_to_devtools_sender: Option<GenericCallback<ScriptToDevtoolsControlMsg>>,
81        devtools_to_script_sender: GenericSender<DevtoolScriptControlMsg>,
82        mem_profiler_chan: mem::ProfilerChan,
83        time_profiler_chan: time::ProfilerChan,
84        script_to_constellation_chan: ScriptToConstellationChan,
85        script_to_embedder_chan: ScriptToEmbedderChan,
86        resource_threads: ResourceThreads,
87        storage_threads: StorageThreads,
88        #[cfg(feature = "webgpu")] gpu_id_hub: std::sync::Arc<IdentityHub>,
89        cx: &mut JSContext,
90    ) -> DomRoot<Self> {
91        let global = Box::new(Self {
92            global_scope: GlobalScope::new_inherited(
93                debugger_pipeline_id,
94                script_to_devtools_sender,
95                mem_profiler_chan,
96                time_profiler_chan,
97                script_to_constellation_chan,
98                script_to_embedder_chan,
99                resource_threads,
100                storage_threads,
101                MutableOrigin::new(ImmutableOrigin::new_opaque()),
102                ServoUrl::parse_with_base(None, "about:internal/debugger")
103                    .expect("Guaranteed by argument"),
104                None,
105                #[cfg(feature = "webgpu")]
106                gpu_id_hub,
107                None,
108                false,
109                None, // font_context
110            ),
111            devtools_to_script_sender,
112            get_possible_breakpoints_result_sender: RefCell::new(None),
113            get_frame_result_sender: RefCell::new(None),
114            eval_result_sender: RefCell::new(None),
115        });
116        let global = DebuggerGlobalScopeBinding::Wrap::<crate::DomTypeHolder>(cx, global);
117
118        let mut realm = enter_auto_realm(cx, &*global);
119        let mut realm = realm.current_realm();
120        define_all_exposed_interfaces(&mut realm, global.upcast());
121        assert!(unsafe {
122            // Invariants: `obj` must be a handle to a JS global object.
123            JS_DefineDebuggerObject(&mut realm, global.global_scope.reflector().get_jsobject())
124        });
125
126        global
127    }
128
129    pub(crate) fn as_global_scope(&self) -> &GlobalScope {
130        self.upcast::<GlobalScope>()
131    }
132
133    fn evaluate_js(
134        &self,
135        script: Cow<'_, str>,
136        cx: &mut JSContext,
137    ) -> Result<(), JavaScriptEvaluationError> {
138        rooted!(&in(cx) let mut rval = UndefinedValue());
139        self.global_scope.evaluate_js_on_global(
140            script,
141            "",
142            None,
143            rval.handle_mut(),
144            CanGc::from_cx(cx),
145        )
146    }
147
148    pub(crate) fn execute(&self, cx: &mut JSContext) {
149        if self
150            .evaluate_js(resources::read_string(Resource::DebuggerJS).into(), cx)
151            .is_err()
152        {
153            let mut realm = enter_auto_realm(cx, self);
154            let mut realm = realm.current_realm();
155            let in_realm_proof = (&mut realm).into();
156            let in_realm = InRealm::Already(&in_realm_proof);
157
158            let cx = &mut realm;
159            report_pending_exception(cx.into(), true, in_realm, CanGc::from_cx(cx));
160        }
161    }
162
163    pub(crate) fn fire_add_debuggee(
164        &self,
165        can_gc: CanGc,
166        debuggee_global: &GlobalScope,
167        debuggee_pipeline_id: PipelineId,
168        debuggee_worker_id: Option<WorkerId>,
169    ) {
170        let _realm = enter_realm(self);
171        let debuggee_pipeline_id =
172            crate::dom::pipelineid::PipelineId::new(self.upcast(), debuggee_pipeline_id, can_gc);
173        let event = DomRoot::upcast::<Event>(DebuggerAddDebuggeeEvent::new(
174            self.upcast(),
175            debuggee_global,
176            &debuggee_pipeline_id,
177            debuggee_worker_id.map(|id| id.to_string().into()),
178            can_gc,
179        ));
180        assert!(
181            event.fire(self.upcast(), can_gc),
182            "Guaranteed by DebuggerAddDebuggeeEvent::new"
183        );
184    }
185
186    pub(crate) fn fire_eval(
187        &self,
188        can_gc: CanGc,
189        code: DOMString,
190        debuggee_pipeline_id: PipelineId,
191        debuggee_worker_id: Option<WorkerId>,
192        result_sender: GenericSender<EvaluateJSReply>,
193    ) {
194        assert!(
195            self.eval_result_sender
196                .replace(Some(result_sender))
197                .is_none()
198        );
199        let _realm = enter_realm(self);
200        let debuggee_pipeline_id =
201            crate::dom::pipelineid::PipelineId::new(self.upcast(), debuggee_pipeline_id, can_gc);
202        let event = DomRoot::upcast::<Event>(DebuggerEvalEvent::new(
203            self.upcast(),
204            code,
205            &debuggee_pipeline_id,
206            debuggee_worker_id.map(|id| id.to_string().into()),
207            can_gc,
208        ));
209        assert!(
210            event.fire(self.upcast(), can_gc),
211            "Guaranteed by DebuggerEvalEvent::new"
212        );
213    }
214
215    pub(crate) fn fire_get_possible_breakpoints(
216        &self,
217        can_gc: CanGc,
218        spidermonkey_id: u32,
219        result_sender: GenericSender<Vec<devtools_traits::RecommendedBreakpointLocation>>,
220    ) {
221        assert!(
222            self.get_possible_breakpoints_result_sender
223                .replace(Some(result_sender))
224                .is_none()
225        );
226        let _realm = enter_realm(self);
227        let event = DomRoot::upcast::<Event>(DebuggerGetPossibleBreakpointsEvent::new(
228            self.upcast(),
229            spidermonkey_id,
230            can_gc,
231        ));
232        assert!(
233            event.fire(self.upcast(), can_gc),
234            "Guaranteed by DebuggerGetPossibleBreakpointsEvent::new"
235        );
236    }
237
238    pub(crate) fn fire_set_breakpoint(
239        &self,
240        can_gc: CanGc,
241        spidermonkey_id: u32,
242        script_id: u32,
243        offset: u32,
244    ) {
245        let event = DomRoot::upcast::<Event>(DebuggerSetBreakpointEvent::new(
246            self.upcast(),
247            spidermonkey_id,
248            script_id,
249            offset,
250            can_gc,
251        ));
252        assert!(
253            event.fire(self.upcast(), can_gc),
254            "Guaranteed by DebuggerSetBreakpointEvent::new"
255        );
256    }
257
258    pub(crate) fn fire_pause(
259        &self,
260        can_gc: CanGc,
261        result_sender: GenericSender<devtools_traits::PauseFrameResult>,
262    ) {
263        assert!(
264            self.get_frame_result_sender
265                .replace(Some(result_sender))
266                .is_none()
267        );
268        let event = DomRoot::upcast::<Event>(DebuggerPauseEvent::new(self.upcast(), can_gc));
269        assert!(
270            event.fire(self.upcast(), can_gc),
271            "Guaranteed by DebuggerPauseEvent::new"
272        );
273    }
274
275    pub(crate) fn fire_clear_breakpoint(
276        &self,
277        can_gc: CanGc,
278        spidermonkey_id: u32,
279        script_id: u32,
280        offset: u32,
281    ) {
282        let event = DomRoot::upcast::<Event>(DebuggerClearBreakpointEvent::new(
283            self.upcast(),
284            spidermonkey_id,
285            script_id,
286            offset,
287            can_gc,
288        ));
289        assert!(
290            event.fire(self.upcast(), can_gc),
291            "Guaranteed by DebuggerClearBreakpointEvent::new"
292        );
293    }
294}
295
296impl DebuggerGlobalScopeMethods<crate::DomTypeHolder> for DebuggerGlobalScope {
297    // check-tidy: no specs after this line
298    fn NotifyNewSource(&self, args: &NotifyNewSource) {
299        let Some(devtools_chan) = self.as_global_scope().devtools_chan() else {
300            return;
301        };
302        let pipeline_id = PipelineId {
303            namespace_id: PipelineNamespaceId(args.pipelineId.namespaceId),
304            index: Index::new(args.pipelineId.index).expect("`pipelineId.index` must not be zero"),
305        };
306
307        if let Some(introduction_type) = args.introductionType.as_ref() {
308            // Check the `introductionType` and `url`, decide whether or not to create a source actor, and if so,
309            // tell the devtools server to create a source actor. Based on the Firefox impl in:
310            // - getDebuggerSourceURL() <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/utils/source-url.js#7-42>
311            // - getSourceURL() <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#67-109>
312            // - resolveSourceURL() <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#48-66>
313            // - SourceActor#_isInlineSource <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#130-143>
314            // - SourceActor#url <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#157-162>
315
316            // Firefox impl: getDebuggerSourceURL(), getSourceURL()
317            // TODO: handle `about:srcdoc` case (see Firefox getDebuggerSourceURL())
318            // TODO: remove trailing details that may have been appended by SpiderMonkey
319            // (currently impossible to do robustly due to <https://bugzilla.mozilla.org/show_bug.cgi?id=1982001>)
320            let url_original = args.url.str();
321            // FIXME: use page/worker url as base here
322            let url_original = ServoUrl::parse(&url_original).ok();
323
324            // If the source has a `urlOverride` (aka `displayURL` aka `//# sourceURL`), it should be a valid url,
325            // possibly relative to the page/worker url, and we should treat the source as coming from that url for
326            // devtools purposes, including the file tree in the Sources tab.
327            // Firefox impl: getSourceURL()
328            let url_override = args
329                .urlOverride
330                .as_ref()
331                .map(|url| url.str())
332                // FIXME: use page/worker url as base here, not `url_original`
333                .and_then(|url| ServoUrl::parse_with_base(url_original.as_ref(), &url).ok());
334
335            // If the `introductionType` is “eval or eval-like”, the `url` won’t be meaningful, so ignore these
336            // sources unless we have a `urlOverride` (aka `displayURL` aka `//# sourceURL`).
337            // Firefox impl: getDebuggerSourceURL(), getSourceURL()
338            if [
339                IntroductionType::INJECTED_SCRIPT_STR,
340                IntroductionType::EVAL_STR,
341                IntroductionType::DEBUGGER_EVAL_STR,
342                IntroductionType::FUNCTION_STR,
343                IntroductionType::JAVASCRIPT_URL_STR,
344                IntroductionType::EVENT_HANDLER_STR,
345                IntroductionType::DOM_TIMER_STR,
346            ]
347            .contains(&&*introduction_type.str()) &&
348                url_override.is_none()
349            {
350                debug!(
351                    "Not creating debuggee: `introductionType` is `{introduction_type}` but no valid url"
352                );
353                return;
354            }
355
356            // Sources with an `introductionType` of `inlineScript` are generally inline, meaning their contents
357            // are a substring of the page markup (hence not known to SpiderMonkey, requiring plumbing in Servo).
358            // But sources with a `urlOverride` are not inline, since they get their own place in the Sources tree.
359            // nor are sources created for `<iframe srcdoc>`, since they are not necessarily a substring of the
360            // page markup as originally sent by the server.
361            // Firefox impl: SourceActor#_isInlineSource
362            // TODO: handle `about:srcdoc` case (see Firefox SourceActor#_isInlineSource)
363            let inline = introduction_type.str() == "inlineScript" && url_override.is_none();
364            let Some(url) = url_override.or(url_original) else {
365                debug!("Not creating debuggee: no valid url");
366                return;
367            };
368
369            let worker_id = args.workerId.as_ref().map(|id| id.parse().unwrap());
370
371            let source_info = SourceInfo {
372                url,
373                introduction_type: introduction_type.str().to_owned(),
374                inline,
375                worker_id,
376                content: (!inline).then(|| args.text.to_string()),
377                content_type: None, // TODO
378                spidermonkey_id: args.spidermonkeyId,
379            };
380            if let Err(error) = devtools_chan.send(ScriptToDevtoolsControlMsg::CreateSourceActor(
381                self.devtools_to_script_sender.clone(),
382                pipeline_id,
383                source_info,
384            )) {
385                warn!("Failed to send to devtools server: {error:?}");
386            }
387        } else {
388            debug!("Not creating debuggee for script with no `introductionType`");
389        }
390    }
391
392    fn GetPossibleBreakpointsResult(
393        &self,
394        event: &DebuggerGetPossibleBreakpointsEvent,
395        result: Vec<RecommendedBreakpointLocation>,
396    ) {
397        info!("GetPossibleBreakpointsResult: {event:?} {result:?}");
398        let sender = self
399            .get_possible_breakpoints_result_sender
400            .take()
401            .expect("Guaranteed by Self::fire_get_possible_breakpoints()");
402        let _ = sender.send(
403            result
404                .into_iter()
405                .map(|entry| devtools_traits::RecommendedBreakpointLocation {
406                    script_id: entry.scriptId,
407                    offset: entry.offset,
408                    line_number: entry.lineNumber,
409                    column_number: entry.columnNumber,
410                    is_step_start: entry.isStepStart,
411                })
412                .collect(),
413        );
414    }
415
416    fn GetFrameResult(&self, event: &DebuggerPauseEvent, result: &PauseFrameResult) {
417        info!("GetFrameResult: {event:?} {result:?}");
418        let sender = self
419            .get_frame_result_sender
420            .take()
421            .expect("Guaranteed by Self::fire_get_frame()");
422        let _ = sender.send(devtools_traits::PauseFrameResult {
423            column: result.column,
424            display_name: result.displayName.clone().into(),
425            line: result.line,
426            on_stack: result.onStack,
427            oldest: result.oldest,
428            terminated: result.terminated,
429            type_: result.type_.clone().into(),
430            url: result.url.clone().into(),
431        });
432    }
433
434    /// Handle the result from debugger.js executeInGlobal() call.
435    ///
436    /// The result contains completion value information from the SpiderMonkey Debugger API:
437    /// <https://firefox-source-docs.mozilla.org/js/Debugger/Conventions.html#completion-values>
438    fn EvalResult(&self, _event: &DebuggerEvalEvent, result: &EvalResultValue) {
439        let sender = self
440            .eval_result_sender
441            .take()
442            .expect("Guaranteed by Self::fire_eval()");
443
444        let reply = if result.completionType.str() == "terminated" {
445            EvaluateJSReply::VoidValue
446        } else {
447            match &*result.valueType.str() {
448                "undefined" => EvaluateJSReply::VoidValue,
449                "null" => EvaluateJSReply::NullValue,
450                "boolean" => {
451                    EvaluateJSReply::BooleanValue(result.booleanValue.flatten().unwrap_or(false))
452                },
453                "number" => {
454                    let num = result.numberValue.flatten().map(|f| *f).unwrap_or(0.0);
455                    EvaluateJSReply::NumberValue(num)
456                },
457                "string" => EvaluateJSReply::StringValue(
458                    result
459                        .stringValue
460                        .as_ref()
461                        .and_then(|opt| opt.as_ref())
462                        .map(|s| s.to_string())
463                        .unwrap_or_default(),
464                ),
465                "object" => {
466                    let class = result
467                        .objectClass
468                        .as_ref()
469                        .and_then(|opt| opt.as_ref())
470                        .map(|s| s.to_string())
471                        .unwrap_or_else(|| "Object".to_string());
472                    EvaluateJSReply::ActorValue {
473                        class,
474                        uuid: uuid::Uuid::new_v4().to_string(),
475                    }
476                },
477                _ => unreachable!(),
478            }
479        };
480
481        let _ = sender.send(reply);
482    }
483
484    fn NotifyBreakpointHit(&self, pipeline_id: &PipelineIdInit, result: &PauseFrameResult) {
485        let pipeline_id = PipelineId {
486            namespace_id: PipelineNamespaceId(pipeline_id.namespaceId),
487            index: Index::new(pipeline_id.index).expect("`pipelineId.index` must not be zero"),
488        };
489
490        if let Some(chan) = self.upcast::<GlobalScope>().devtools_chan() {
491            let frame_result = devtools_traits::PauseFrameResult {
492                column: result.column,
493                display_name: result.displayName.clone().into(),
494                line: result.line,
495                on_stack: result.onStack,
496                oldest: result.oldest,
497                terminated: result.terminated,
498                type_: result.type_.clone().into(),
499                url: result.url.clone().into(),
500            };
501            let msg = ScriptToDevtoolsControlMsg::BreakpointHit(pipeline_id, frame_result);
502            let _ = chan.send(msg);
503        }
504
505        with_script_thread(|script_thread| {
506            script_thread.enter_debugger_pause_loop();
507        });
508    }
509}