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