Skip to main content

script/dom/debugger/
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 devtools_traits::{
8    DevtoolScriptControlMsg, EvaluateJSReply, ScriptToDevtoolsControlMsg, SourceInfo, WorkerId,
9};
10use dom_struct::dom_struct;
11use embedder_traits::ScriptToEmbedderChan;
12use embedder_traits::resources::{self, Resource};
13use js::context::JSContext;
14use js::rust::wrappers2::JS_DefineDebuggerObject;
15use net_traits::ResourceThreads;
16use profile_traits::{mem, time};
17use script_bindings::reflector::DomObject;
18use servo_base::generic_channel::{GenericCallback, GenericSender, channel};
19use servo_base::id::{Index, PipelineId, PipelineNamespaceId};
20use servo_constellation_traits::ScriptToConstellationChan;
21use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
22use storage_traits::StorageThreads;
23
24use crate::dom::bindings::codegen::Bindings::DebuggerEvalEventBinding::DebuggerValue;
25use crate::dom::bindings::codegen::Bindings::DebuggerEvalEventBinding::GenericBindings::ObjectPreview;
26use crate::dom::bindings::codegen::Bindings::DebuggerGetEnvironmentEventBinding::{
27    EnvironmentInfo, EnvironmentVariable,
28};
29use crate::dom::bindings::codegen::Bindings::DebuggerGlobalScopeBinding;
30use crate::dom::bindings::codegen::Bindings::DebuggerInterruptEventBinding::{
31    FrameInfo, FrameOffset, PauseReason,
32};
33use crate::dom::bindings::codegen::GenericBindings::DebuggerEvalEventBinding::{
34    EvalResult, PropertyDescriptor,
35};
36use crate::dom::bindings::codegen::GenericBindings::DebuggerGetPossibleBreakpointsEventBinding::RecommendedBreakpointLocation;
37use crate::dom::bindings::codegen::GenericBindings::DebuggerGlobalScopeBinding::{
38    DebuggerGlobalScopeMethods, NotifyNewSource, PipelineIdInit,
39};
40use crate::dom::bindings::inheritance::Castable;
41use crate::dom::bindings::root::DomRoot;
42use crate::dom::bindings::str::DOMString;
43use crate::dom::bindings::utils::define_all_exposed_interfaces;
44use crate::dom::debugger::debuggerclearbreakpointevent::DebuggerClearBreakpointEvent;
45use crate::dom::debugger::debuggerframeevent::DebuggerFrameEvent;
46use crate::dom::debugger::debuggergetenvironmentevent::DebuggerGetEnvironmentEvent;
47use crate::dom::debugger::debuggerinterruptevent::DebuggerInterruptEvent;
48use crate::dom::debugger::debuggerresumeevent::DebuggerResumeEvent;
49use crate::dom::debugger::debuggersetbreakpointevent::DebuggerSetBreakpointEvent;
50use crate::dom::globalscope::GlobalScope;
51use crate::dom::types::{
52    DebuggerAddDebuggeeEvent, DebuggerEvalEvent, DebuggerGetPossibleBreakpointsEvent, Event,
53};
54#[cfg(feature = "webgpu")]
55use crate::dom::webgpu::identityhub::IdentityHub;
56use crate::realms::{enter_auto_realm, enter_realm};
57use crate::script_runtime::{CanGc, IntroductionType};
58use crate::script_thread::with_script_thread;
59
60#[dom_struct]
61/// Global scope for interacting with the devtools Debugger API.
62///
63/// <https://firefox-source-docs.mozilla.org/js/Debugger/>
64pub(crate) struct DebuggerGlobalScope {
65    global_scope: GlobalScope,
66    #[no_trace]
67    devtools_to_script_sender: GenericSender<DevtoolScriptControlMsg>,
68    #[no_trace]
69    get_possible_breakpoints_result_sender:
70        RefCell<Option<GenericSender<Vec<devtools_traits::RecommendedBreakpointLocation>>>>,
71    #[no_trace]
72    eval_result_sender: RefCell<Option<GenericSender<EvaluateJSReply>>>,
73    #[no_trace]
74    get_list_frame_result_sender: RefCell<Option<GenericSender<Vec<String>>>>,
75    #[no_trace]
76    get_environment_result_sender: RefCell<Option<GenericSender<String>>>,
77}
78
79impl DebuggerGlobalScope {
80    /// Create a new heap-allocated `DebuggerGlobalScope`.
81    ///
82    /// `debugger_pipeline_id` is the pipeline id to use when creating the debugger’s [`GlobalScope`]:
83    /// - in normal script threads, it should be set to `PipelineId::new()`, because those threads can generate
84    ///   pipeline ids, and they may contain debuggees from more than one pipeline
85    /// - in web worker threads, it should be set to the pipeline id of the page that created the thread, because
86    ///   those threads can’t generate pipeline ids, and they only contain one debuggee from one pipeline
87    #[expect(unsafe_code, clippy::too_many_arguments)]
88    pub(crate) fn new(
89        debugger_pipeline_id: PipelineId,
90        script_to_devtools_sender: Option<GenericCallback<ScriptToDevtoolsControlMsg>>,
91        devtools_to_script_sender: GenericSender<DevtoolScriptControlMsg>,
92        mem_profiler_chan: mem::ProfilerChan,
93        time_profiler_chan: time::ProfilerChan,
94        script_to_constellation_chan: ScriptToConstellationChan,
95        script_to_embedder_chan: ScriptToEmbedderChan,
96        resource_threads: ResourceThreads,
97        storage_threads: StorageThreads,
98        #[cfg(feature = "webgpu")] gpu_id_hub: std::sync::Arc<IdentityHub>,
99        cx: &mut JSContext,
100    ) -> DomRoot<Self> {
101        let global = Box::new(Self {
102            global_scope: GlobalScope::new_inherited(
103                debugger_pipeline_id,
104                script_to_devtools_sender,
105                mem_profiler_chan,
106                time_profiler_chan,
107                script_to_constellation_chan,
108                script_to_embedder_chan,
109                resource_threads,
110                storage_threads,
111                MutableOrigin::new(ImmutableOrigin::new_opaque()),
112                ServoUrl::parse_with_base(None, "about:internal/debugger")
113                    .expect("Guaranteed by argument"),
114                None,
115                #[cfg(feature = "webgpu")]
116                gpu_id_hub,
117                None,
118                false,
119                None, // font_context
120            ),
121            devtools_to_script_sender,
122            get_possible_breakpoints_result_sender: RefCell::new(None),
123            get_list_frame_result_sender: RefCell::new(None),
124            get_environment_result_sender: RefCell::new(None),
125            eval_result_sender: RefCell::new(None),
126        });
127        let global = DebuggerGlobalScopeBinding::Wrap::<crate::DomTypeHolder>(cx, global);
128
129        let mut realm = enter_auto_realm(cx, &*global);
130        let mut realm = realm.current_realm();
131        define_all_exposed_interfaces(&mut realm, global.upcast());
132        assert!(unsafe {
133            // Invariants: `obj` must be a handle to a JS global object.
134            JS_DefineDebuggerObject(&mut realm, global.global_scope.reflector().get_jsobject())
135        });
136
137        global
138    }
139
140    pub(crate) fn as_global_scope(&self) -> &GlobalScope {
141        self.upcast::<GlobalScope>()
142    }
143
144    pub(crate) fn execute(&self, cx: &mut JSContext) {
145        let mut realm = enter_auto_realm(cx, self);
146        let cx = &mut realm.current_realm();
147
148        let _ = self.global_scope.evaluate_js_on_global(
149            cx,
150            resources::read_string(Resource::DebuggerJS).into(),
151            "",
152            None,
153            None,
154        );
155    }
156
157    pub(crate) fn fire_add_debuggee(
158        &self,
159        cx: &mut JSContext,
160        debuggee_global: &GlobalScope,
161        debuggee_pipeline_id: PipelineId,
162        debuggee_worker_id: Option<WorkerId>,
163    ) {
164        let mut realm = enter_auto_realm(cx, self);
165        let cx = &mut realm;
166        let debuggee_pipeline_id = crate::dom::pipelineid::PipelineId::new(
167            self.upcast(),
168            debuggee_pipeline_id,
169            CanGc::from_cx(cx),
170        );
171        let event = DomRoot::upcast::<Event>(DebuggerAddDebuggeeEvent::new(
172            self.upcast(),
173            debuggee_global,
174            &debuggee_pipeline_id,
175            debuggee_worker_id.map(|id| id.to_string().into()),
176            CanGc::from_cx(cx),
177        ));
178        assert!(
179            event.fire(self.upcast(), CanGc::from_cx(cx)),
180            "Guaranteed by DebuggerAddDebuggeeEvent::new"
181        );
182    }
183
184    pub(crate) fn fire_eval(
185        &self,
186        cx: &mut JSContext,
187        code: DOMString,
188        debuggee_pipeline_id: PipelineId,
189        debuggee_worker_id: Option<WorkerId>,
190        frame_actor_id: Option<String>,
191        result_sender: GenericSender<EvaluateJSReply>,
192    ) {
193        assert!(
194            self.eval_result_sender
195                .replace(Some(result_sender))
196                .is_none()
197        );
198        let mut realm = enter_auto_realm(cx, self);
199        let cx = &mut realm;
200        let debuggee_pipeline_id = crate::dom::pipelineid::PipelineId::new(
201            self.upcast(),
202            debuggee_pipeline_id,
203            CanGc::from_cx(cx),
204        );
205        let event = DomRoot::upcast::<Event>(DebuggerEvalEvent::new(
206            self.upcast(),
207            code,
208            &debuggee_pipeline_id,
209            debuggee_worker_id.map(|id| id.to_string().into()),
210            frame_actor_id.map(|id| id.into()),
211            CanGc::from_cx(cx),
212        ));
213        assert!(
214            event.fire(self.upcast(), CanGc::from_cx(cx)),
215            "Guaranteed by DebuggerEvalEvent::new"
216        );
217    }
218
219    pub(crate) fn fire_get_possible_breakpoints(
220        &self,
221        cx: &mut JSContext,
222        spidermonkey_id: u32,
223        result_sender: GenericSender<Vec<devtools_traits::RecommendedBreakpointLocation>>,
224    ) {
225        assert!(
226            self.get_possible_breakpoints_result_sender
227                .replace(Some(result_sender))
228                .is_none()
229        );
230        let _realm = enter_realm(self);
231        let event = DomRoot::upcast::<Event>(DebuggerGetPossibleBreakpointsEvent::new(
232            self.upcast(),
233            spidermonkey_id,
234            CanGc::from_cx(cx),
235        ));
236        assert!(
237            event.fire(self.upcast(), CanGc::from_cx(cx)),
238            "Guaranteed by DebuggerGetPossibleBreakpointsEvent::new"
239        );
240    }
241
242    pub(crate) fn fire_set_breakpoint(
243        &self,
244        cx: &mut JSContext,
245        spidermonkey_id: u32,
246        script_id: u32,
247        offset: u32,
248    ) {
249        let event = DomRoot::upcast::<Event>(DebuggerSetBreakpointEvent::new(
250            self.upcast(),
251            spidermonkey_id,
252            script_id,
253            offset,
254            CanGc::from_cx(cx),
255        ));
256        assert!(
257            event.fire(self.upcast(), CanGc::from_cx(cx)),
258            "Guaranteed by DebuggerSetBreakpointEvent::new"
259        );
260    }
261
262    pub(crate) fn fire_interrupt(&self, cx: &mut js::context::JSContext) {
263        let event = DomRoot::upcast::<Event>(DebuggerInterruptEvent::new(
264            self.upcast(),
265            CanGc::from_cx(cx),
266        ));
267        assert!(
268            event.fire(self.upcast(), CanGc::from_cx(cx)),
269            "Guaranteed by DebuggerInterruptEvent::new"
270        );
271    }
272
273    pub(crate) fn fire_list_frames(
274        &self,
275        cx: &mut js::context::JSContext,
276        pipeline_id: PipelineId,
277        start: u32,
278        count: u32,
279        result_sender: GenericSender<Vec<String>>,
280    ) {
281        assert!(
282            self.get_list_frame_result_sender
283                .replace(Some(result_sender))
284                .is_none()
285        );
286        let _realm = enter_realm(self);
287        let pipeline_id =
288            crate::dom::pipelineid::PipelineId::new(self.upcast(), pipeline_id, CanGc::from_cx(cx));
289        let event = DomRoot::upcast::<Event>(DebuggerFrameEvent::new(
290            self.upcast(),
291            &pipeline_id,
292            start,
293            count,
294            CanGc::from_cx(cx),
295        ));
296        assert!(
297            event.fire(self.upcast(), CanGc::from_cx(cx)),
298            "Guaranteed by DebuggerFrameEvent::new"
299        );
300    }
301
302    pub(crate) fn fire_get_environment(
303        &self,
304        cx: &mut JSContext,
305        frame_actor_id: String,
306        result_sender: GenericSender<String>,
307    ) {
308        assert!(
309            self.get_environment_result_sender
310                .replace(Some(result_sender))
311                .is_none()
312        );
313        let _realm = enter_realm(self);
314        let event = DomRoot::upcast::<Event>(DebuggerGetEnvironmentEvent::new(
315            self.upcast(),
316            frame_actor_id.into(),
317            CanGc::from_cx(cx),
318        ));
319        assert!(
320            event.fire(self.upcast(), CanGc::from_cx(cx)),
321            "Guaranteed by DebuggerGetEnvironmentEvent::new"
322        );
323    }
324
325    pub(crate) fn fire_resume(
326        &self,
327        cx: &mut JSContext,
328        resume_limit_type: Option<String>,
329        frame_actor_id: Option<String>,
330    ) {
331        let event = DomRoot::upcast::<Event>(DebuggerResumeEvent::new(
332            self.upcast(),
333            resume_limit_type.map(DOMString::from),
334            frame_actor_id.map(DOMString::from),
335            CanGc::from_cx(cx),
336        ));
337        assert!(
338            event.fire(self.upcast(), CanGc::from_cx(cx)),
339            "Guaranteed by DebuggerResumeEvent::new"
340        );
341    }
342
343    pub(crate) fn fire_clear_breakpoint(
344        &self,
345        cx: &mut JSContext,
346        spidermonkey_id: u32,
347        script_id: u32,
348        offset: u32,
349    ) {
350        let event = DomRoot::upcast::<Event>(DebuggerClearBreakpointEvent::new(
351            self.upcast(),
352            spidermonkey_id,
353            script_id,
354            offset,
355            CanGc::from_cx(cx),
356        ));
357        assert!(
358            event.fire(self.upcast(), CanGc::from_cx(cx)),
359            "Guaranteed by DebuggerClearBreakpointEvent::new"
360        );
361    }
362}
363
364impl DebuggerGlobalScopeMethods<crate::DomTypeHolder> for DebuggerGlobalScope {
365    // check-tidy: no specs after this line
366    fn NotifyNewSource(&self, args: &NotifyNewSource) {
367        let Some(devtools_chan) = self.as_global_scope().devtools_chan() else {
368            return;
369        };
370        let pipeline_id = PipelineId {
371            namespace_id: PipelineNamespaceId(args.pipelineId.namespaceId),
372            index: Index::new(args.pipelineId.index).expect("`pipelineId.index` must not be zero"),
373        };
374
375        if let Some(introduction_type) = args.introductionType.as_ref() {
376            // Check the `introductionType` and `url`, decide whether or not to create a source actor, and if so,
377            // tell the devtools server to create a source actor. Based on the Firefox impl in:
378            // - getDebuggerSourceURL() <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/utils/source-url.js#7-42>
379            // - getSourceURL() <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#67-109>
380            // - resolveSourceURL() <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#48-66>
381            // - SourceActor#_isInlineSource <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#130-143>
382            // - SourceActor#url <https://searchfox.org/mozilla-central/rev/85667ab51e4b2a3352f7077a9ee43513049ed2d6/devtools/server/actors/source.js#157-162>
383
384            // Firefox impl: getDebuggerSourceURL(), getSourceURL()
385            // TODO: handle `about:srcdoc` case (see Firefox getDebuggerSourceURL())
386            // TODO: remove trailing details that may have been appended by SpiderMonkey
387            // (currently impossible to do robustly due to <https://bugzilla.mozilla.org/show_bug.cgi?id=1982001>)
388            let url_original = args.url.str();
389            // FIXME: use page/worker url as base here
390            let url_original = ServoUrl::parse(&url_original).ok();
391
392            // If the source has a `urlOverride` (aka `displayURL` aka `//# sourceURL`), it should be a valid url,
393            // possibly relative to the page/worker url, and we should treat the source as coming from that url for
394            // devtools purposes, including the file tree in the Sources tab.
395            // Firefox impl: getSourceURL()
396            let url_override = args
397                .urlOverride
398                .as_ref()
399                .map(|url| url.str())
400                // FIXME: use page/worker url as base here, not `url_original`
401                .and_then(|url| ServoUrl::parse_with_base(url_original.as_ref(), &url).ok());
402
403            // If the `introductionType` is “eval or eval-like”, the `url` won’t be meaningful, so ignore these
404            // sources unless we have a `urlOverride` (aka `displayURL` aka `//# sourceURL`).
405            // Firefox impl: getDebuggerSourceURL(), getSourceURL()
406            if [
407                IntroductionType::INJECTED_SCRIPT_STR,
408                IntroductionType::EVAL_STR,
409                IntroductionType::DEBUGGER_EVAL_STR,
410                IntroductionType::FUNCTION_STR,
411                IntroductionType::JAVASCRIPT_URL_STR,
412                IntroductionType::EVENT_HANDLER_STR,
413                IntroductionType::DOM_TIMER_STR,
414            ]
415            .contains(&&*introduction_type.str()) &&
416                url_override.is_none()
417            {
418                debug!(
419                    "Not creating debuggee: `introductionType` is `{introduction_type}` but no valid url"
420                );
421                return;
422            }
423
424            // Sources with an `introductionType` of `inlineScript` are generally inline, meaning their contents
425            // are a substring of the page markup (hence not known to SpiderMonkey, requiring plumbing in Servo).
426            // But sources with a `urlOverride` are not inline, since they get their own place in the Sources tree.
427            // nor are sources created for `<iframe srcdoc>`, since they are not necessarily a substring of the
428            // page markup as originally sent by the server.
429            // Firefox impl: SourceActor#_isInlineSource
430            // TODO: handle `about:srcdoc` case (see Firefox SourceActor#_isInlineSource)
431            let inline = introduction_type.str() == "inlineScript" && url_override.is_none();
432            let Some(url) = url_override.or(url_original) else {
433                debug!("Not creating debuggee: no valid url");
434                return;
435            };
436
437            let worker_id = args.workerId.as_ref().map(|id| id.parse().unwrap());
438
439            let source_info = SourceInfo {
440                url,
441                introduction_type: introduction_type.str().to_owned(),
442                inline,
443                worker_id,
444                content: (!inline).then(|| args.text.to_string()),
445                content_type: None, // TODO
446                spidermonkey_id: args.spidermonkeyId,
447            };
448            if let Err(error) = devtools_chan.send(ScriptToDevtoolsControlMsg::CreateSourceActor(
449                self.devtools_to_script_sender.clone(),
450                pipeline_id,
451                source_info,
452            )) {
453                warn!("Failed to send to devtools server: {error:?}");
454            }
455        } else {
456            debug!("Not creating debuggee for script with no `introductionType`");
457        }
458    }
459
460    fn GetPossibleBreakpointsResult(
461        &self,
462        event: &DebuggerGetPossibleBreakpointsEvent,
463        result: Vec<RecommendedBreakpointLocation>,
464    ) {
465        info!("GetPossibleBreakpointsResult: {event:?} {result:?}");
466        let sender = self
467            .get_possible_breakpoints_result_sender
468            .take()
469            .expect("Guaranteed by Self::fire_get_possible_breakpoints()");
470        let _ = sender.send(
471            result
472                .into_iter()
473                .map(|entry| devtools_traits::RecommendedBreakpointLocation {
474                    script_id: entry.scriptId,
475                    offset: entry.offset,
476                    line_number: entry.lineNumber,
477                    column_number: entry.columnNumber,
478                    is_step_start: entry.isStepStart,
479                })
480                .collect(),
481        );
482    }
483
484    /// Handle the result from debugger.js executeInGlobal() call.
485    ///
486    /// The result contains completion value information from the SpiderMonkey Debugger API:
487    /// <https://firefox-source-docs.mozilla.org/js/Debugger/Conventions.html#completion-values>
488    fn EvalResult(&self, _event: &DebuggerEvalEvent, result: &EvalResult) {
489        let sender = self
490            .eval_result_sender
491            .take()
492            .expect("Guaranteed by Self::fire_eval()");
493
494        let reply = EvaluateJSReply {
495            value: parse_debugger_value(&result.value, result.preview.as_ref()),
496            has_exception: result.hasException.unwrap_or(false),
497        };
498
499        let _ = sender.send(reply);
500    }
501
502    fn PauseAndRespond(
503        &self,
504        pipeline_id: &PipelineIdInit,
505        frame_offset: &FrameOffset,
506        pause_reason: &PauseReason,
507    ) {
508        let pipeline_id = PipelineId {
509            namespace_id: PipelineNamespaceId(pipeline_id.namespaceId),
510            index: Index::new(pipeline_id.index).expect("`pipelineId.index` must not be zero"),
511        };
512
513        let frame_offset = devtools_traits::FrameOffset {
514            actor: frame_offset.frameActorId.clone().into(),
515            column: frame_offset.column,
516            line: frame_offset.line,
517        };
518
519        let pause_reason = devtools_traits::PauseReason {
520            type_: pause_reason.type_.clone().into(),
521            on_next: pause_reason.onNext,
522        };
523
524        if let Some(chan) = self.upcast::<GlobalScope>().devtools_chan() {
525            let msg =
526                ScriptToDevtoolsControlMsg::DebuggerPause(pipeline_id, frame_offset, pause_reason);
527            let _ = chan.send(msg);
528        }
529
530        with_script_thread(|script_thread| {
531            script_thread.enter_debugger_pause_loop();
532        });
533    }
534
535    fn RegisterFrameActor(
536        &self,
537        pipeline_id: &PipelineIdInit,
538        result: &FrameInfo,
539    ) -> Option<DOMString> {
540        let pipeline_id = PipelineId {
541            namespace_id: PipelineNamespaceId(pipeline_id.namespaceId),
542            index: Index::new(pipeline_id.index).expect("`pipelineId.index` must not be zero"),
543        };
544
545        let chan = self.upcast::<GlobalScope>().devtools_chan()?;
546        let (tx, rx) = channel::<String>().unwrap();
547
548        let frame = devtools_traits::FrameInfo {
549            display_name: result.displayName.clone().into(),
550            on_stack: result.onStack,
551            oldest: result.oldest,
552            terminated: result.terminated,
553            type_: result.type_.clone().into(),
554            url: result.url.clone().into(),
555        };
556        let msg = ScriptToDevtoolsControlMsg::CreateFrameActor(tx, pipeline_id, frame);
557        let _ = chan.send(msg);
558
559        rx.recv().ok().map(DOMString::from)
560    }
561
562    fn ListFramesResult(&self, frame_actor_ids: Vec<DOMString>) {
563        info!("ListFramesResult: {frame_actor_ids:?}");
564        let sender = self
565            .get_list_frame_result_sender
566            .take()
567            .expect("Guaranteed by Self::fire_list_frames()");
568
569        let _ = sender.send(frame_actor_ids.into_iter().map(|i| i.into()).collect());
570    }
571
572    fn RegisterEnvironmentActor(
573        &self,
574        environment: &EnvironmentInfo,
575        parent: Option<DOMString>,
576        actor: Option<DOMString>,
577    ) -> Option<DOMString> {
578        let chan = self.upcast::<GlobalScope>().devtools_chan()?;
579        let (tx, rx) = channel::<String>().unwrap();
580
581        let environment = devtools_traits::EnvironmentInfo {
582            type_: environment.type_.clone().map(String::from),
583            scope_kind: environment.scopeKind.clone().map(String::from),
584            function_display_name: environment.functionDisplayName.clone().map(String::from),
585            binding_variables: environment
586                .bindingVariables
587                .as_deref()
588                .into_iter()
589                .flatten()
590                .map(|EnvironmentVariable { property, preview }| {
591                    parse_property_descriptor(property, preview.as_ref())
592                })
593                .collect(),
594        };
595
596        let msg = ScriptToDevtoolsControlMsg::CreateEnvironmentActor(
597            tx,
598            environment,
599            parent.map(String::from),
600            actor.map(String::from),
601        );
602        let _ = chan.send(msg);
603
604        rx.recv().ok().map(DOMString::from)
605    }
606
607    fn GetEnvironmentResult(&self, environment_actor_id: DOMString) {
608        let sender = self
609            .get_environment_result_sender
610            .take()
611            .expect("Guaranteed by Self::fire_get_environment()");
612
613        let _ = sender.send(environment_actor_id.into());
614    }
615}
616
617fn parse_property_descriptor(
618    property: &PropertyDescriptor,
619    preview: Option<&ObjectPreview>,
620) -> devtools_traits::PropertyDescriptor {
621    devtools_traits::PropertyDescriptor {
622        name: property.name.to_string(),
623        value: parse_debugger_value(&property.value, preview),
624        configurable: property.configurable,
625        enumerable: property.enumerable,
626        writable: property.writable,
627        is_accessor: property.isAccessor,
628    }
629}
630
631fn parse_object_preview(preview: &ObjectPreview) -> devtools_traits::ObjectPreview {
632    devtools_traits::ObjectPreview {
633        kind: preview.kind.clone().into(),
634        own_properties: preview.ownProperties.as_ref().map(|properties| {
635            properties
636                .iter()
637                .map(|property| parse_property_descriptor(property, None))
638                .collect()
639        }),
640        own_properties_length: preview.ownPropertiesLength,
641        function: preview
642            .function
643            .as_ref()
644            .map(|fields| devtools_traits::FunctionPreview {
645                name: fields.name.as_ref().map(|s| s.to_string()),
646                display_name: fields.displayName.as_ref().map(|s| s.to_string()),
647                parameter_names: fields
648                    .parameterNames
649                    .iter()
650                    .map(|p| p.to_string())
651                    .collect(),
652                is_async: fields.isAsync,
653                is_generator: fields.isGenerator,
654            }),
655        array_length: preview.arrayLength,
656        items: preview.items.as_ref().map(|items| {
657            items
658                .iter()
659                .map(|item| parse_debugger_value(item, None))
660                .collect()
661        }),
662    }
663}
664
665fn parse_debugger_value(
666    value: &DebuggerValue,
667    preview: Option<&ObjectPreview>,
668) -> devtools_traits::DebuggerValue {
669    use devtools_traits::DebuggerValue::*;
670    match &*value.valueType.str() {
671        "undefined" => VoidValue,
672        "null" => NullValue,
673        "boolean" => BooleanValue(value.booleanValue.unwrap_or(false)),
674        "Infinity" => NumberValue(f64::INFINITY),
675        "-Infinity" => NumberValue(f64::NEG_INFINITY),
676        "NaN" => NumberValue(f64::NAN),
677        "-0" => NumberValue(-0.0),
678        "number" => {
679            let num = value.numberValue.map(|f| *f).unwrap_or(0.0);
680            NumberValue(num)
681        },
682        "string" => StringValue(
683            value
684                .stringValue
685                .as_ref()
686                .map(|s| s.to_string())
687                .unwrap_or_default(),
688        ),
689        "object" => {
690            let class = value
691                .objectClass
692                .as_ref()
693                .map(|s| s.to_string())
694                .unwrap_or_else(|| "Object".to_string());
695
696            ObjectValue {
697                uuid: uuid::Uuid::new_v4().to_string(),
698                class,
699                preview: preview.map(parse_object_preview),
700            }
701        },
702        _ => unreachable!(),
703    }
704}