1use 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]
53pub(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 #[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, ),
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 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 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 let url_original = args.url.str();
321 let url_original = ServoUrl::parse(&url_original).ok();
323
324 let url_override = args
329 .urlOverride
330 .as_ref()
331 .map(|url| url.str())
332 .and_then(|url| ServoUrl::parse_with_base(url_original.as_ref(), &url).ok());
334
335 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 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, 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 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}