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