1use std::convert::TryFrom;
6use std::ptr::{self, NonNull};
7use std::slice;
8
9use devtools_traits::{
10 ConsoleMessage, ConsoleMessageArgument, ConsoleMessageBuilder, LogLevel,
11 ScriptToDevtoolsControlMsg, StackFrame,
12};
13use js::conversions::jsstr_to_string;
14use js::jsapi::{self, ESClass, PropertyDescriptor};
15use js::jsval::{Int32Value, UndefinedValue};
16use js::rust::wrappers::{
17 GetBuiltinClass, GetPropertyKeys, JS_GetOwnPropertyDescriptorById, JS_GetPropertyById,
18 JS_IdToValue, JS_Stringify, JS_ValueToSource,
19};
20use js::rust::{
21 CapturedJSStack, HandleObject, HandleValue, IdVector, ToString, describe_scripted_caller,
22};
23use script_bindings::conversions::get_dom_class;
24
25use crate::dom::bindings::codegen::Bindings::ConsoleBinding::consoleMethods;
26use crate::dom::bindings::inheritance::Castable;
27use crate::dom::bindings::str::DOMString;
28use crate::dom::globalscope::GlobalScope;
29use crate::dom::workerglobalscope::WorkerGlobalScope;
30use crate::script_runtime::JSContext;
31
32const MAX_LOG_DEPTH: usize = 10;
34const MAX_LOG_CHILDREN: usize = 15;
36
37#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
39pub(crate) struct Console;
40
41impl Console {
42 #[expect(unsafe_code)]
43 fn build_message(level: LogLevel) -> ConsoleMessageBuilder {
44 let cx = GlobalScope::get_cx();
45 let caller = unsafe { describe_scripted_caller(*cx) }.unwrap_or_default();
46
47 ConsoleMessageBuilder::new(level, caller.filename, caller.line, caller.col)
48 }
49
50 fn send_string_message(global: &GlobalScope, level: LogLevel, message: String) {
53 let s = DOMString::from(message.clone());
54 log!(level.clone().into(), "{}", &s);
55 console_message_to_stdout(global, &s);
56
57 let mut builder = Self::build_message(level);
58 builder.add_argument(message.into());
59 let log_message = builder.finish();
60
61 Self::send_to_devtools(global, log_message);
62 }
63
64 fn method(
65 global: &GlobalScope,
66 level: LogLevel,
67 messages: Vec<HandleValue>,
68 include_stacktrace: IncludeStackTrace,
69 ) {
70 let cx = GlobalScope::get_cx();
71
72 let mut log: ConsoleMessageBuilder = Console::build_message(level.clone());
73 for message in &messages {
74 log.add_argument(console_argument_from_handle_value(cx, *message));
75 }
76
77 if include_stacktrace == IncludeStackTrace::Yes {
78 log.attach_stack_trace(get_js_stack(*GlobalScope::get_cx()));
79 }
80
81 Console::send_to_devtools(global, log.finish());
82
83 let msgs = stringify_handle_values(&messages);
84 console_message_to_stdout(global, &msgs);
86
87 log!(level.into(), "{}", &msgs);
89 }
90
91 fn send_to_devtools(global: &GlobalScope, message: ConsoleMessage) {
92 if let Some(chan) = global.devtools_chan() {
93 let worker_id = global
94 .downcast::<WorkerGlobalScope>()
95 .map(|worker| worker.worker_id());
96 let devtools_message =
97 ScriptToDevtoolsControlMsg::ConsoleAPI(global.pipeline_id(), message, worker_id);
98 chan.send(devtools_message).unwrap();
99 }
100 }
101
102 pub(crate) fn internal_warn(global: &GlobalScope, message: DOMString) {
104 Console::send_string_message(global, LogLevel::Warn, String::from(message.clone()));
105 }
106}
107
108#[cfg(not(any(target_os = "android", target_env = "ohos")))]
114fn with_stderr_lock<F>(f: F)
115where
116 F: FnOnce(),
117{
118 use std::io;
119 let stderr = io::stderr();
120 let _handle = stderr.lock();
121 f()
122}
123
124#[expect(unsafe_code)]
125unsafe fn handle_value_to_string(cx: *mut jsapi::JSContext, value: HandleValue) -> DOMString {
126 rooted!(in(cx) let mut js_string = std::ptr::null_mut::<jsapi::JSString>());
127 match std::ptr::NonNull::new(unsafe { JS_ValueToSource(cx, value) }) {
128 Some(js_str) => {
129 js_string.set(js_str.as_ptr());
130 DOMString::from_string(unsafe { jsstr_to_string(cx, js_str) })
131 },
132 None => "<error converting value to string>".into(),
133 }
134}
135
136#[expect(unsafe_code)]
137fn console_argument_from_handle_value(
138 cx: JSContext,
139 handle_value: HandleValue,
140) -> ConsoleMessageArgument {
141 if handle_value.is_string() {
142 let js_string = ptr::NonNull::new(handle_value.to_string()).unwrap();
143 let dom_string = unsafe { jsstr_to_string(*cx, js_string) };
144 return ConsoleMessageArgument::String(dom_string);
145 }
146
147 if handle_value.is_int32() {
148 let integer = handle_value.to_int32();
149 return ConsoleMessageArgument::Integer(integer);
150 }
151
152 if handle_value.is_number() {
153 let number = handle_value.to_number();
154 return ConsoleMessageArgument::Number(number);
155 }
156
157 let stringified_value = stringify_handle_value(handle_value);
159 ConsoleMessageArgument::String(stringified_value.into())
160}
161
162#[expect(unsafe_code)]
163fn stringify_handle_value(message: HandleValue) -> DOMString {
164 let cx = GlobalScope::get_cx();
165 unsafe {
166 if message.is_string() {
167 let jsstr = std::ptr::NonNull::new(message.to_string()).unwrap();
168 return DOMString::from_string(jsstr_to_string(*cx, jsstr));
169 }
170 unsafe fn stringify_object_from_handle_value(
171 cx: *mut jsapi::JSContext,
172 value: HandleValue,
173 parents: Vec<u64>,
174 ) -> DOMString {
175 rooted!(in(cx) let mut obj = value.to_object());
176 let mut object_class = ESClass::Other;
177 if !unsafe { GetBuiltinClass(cx, obj.handle(), &mut object_class as *mut _) } {
178 return DOMString::from("/* invalid */");
179 }
180 let mut ids = unsafe { IdVector::new(cx) };
181 if !unsafe {
182 GetPropertyKeys(
183 cx,
184 obj.handle(),
185 jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS,
186 ids.handle_mut(),
187 )
188 } {
189 return DOMString::from("/* invalid */");
190 }
191 let truncate = ids.len() > MAX_LOG_CHILDREN;
192 if object_class != ESClass::Array && object_class != ESClass::Object {
193 if truncate {
194 return DOMString::from("…");
195 } else {
196 return unsafe { handle_value_to_string(cx, value) };
197 }
198 }
199
200 let mut explicit_keys = object_class == ESClass::Object;
201 let mut props = Vec::with_capacity(ids.len());
202 for id in ids.iter().take(MAX_LOG_CHILDREN) {
203 rooted!(in(cx) let id = *id);
204 rooted!(in(cx) let mut desc = PropertyDescriptor::default());
205
206 let mut is_none = false;
207 if !unsafe {
208 JS_GetOwnPropertyDescriptorById(
209 cx,
210 obj.handle(),
211 id.handle(),
212 desc.handle_mut(),
213 &mut is_none,
214 )
215 } {
216 return DOMString::from("/* invalid */");
217 }
218
219 rooted!(in(cx) let mut property = UndefinedValue());
220 if !unsafe {
221 JS_GetPropertyById(cx, obj.handle(), id.handle(), property.handle_mut())
222 } {
223 return DOMString::from("/* invalid */");
224 }
225
226 if !explicit_keys {
227 if id.is_int() {
228 if let Ok(id_int) = usize::try_from(id.to_int()) {
229 explicit_keys = props.len() != id_int;
230 } else {
231 explicit_keys = false;
232 }
233 } else {
234 explicit_keys = false;
235 }
236 }
237 let value_string = stringify_inner(
238 unsafe { JSContext::from_ptr(cx) },
239 property.handle(),
240 parents.clone(),
241 );
242 if explicit_keys {
243 let key = if id.is_string() || id.is_symbol() || id.is_int() {
244 rooted!(in(cx) let mut key_value = UndefinedValue());
245 let raw_id: jsapi::HandleId = id.handle().into();
246 if !unsafe { JS_IdToValue(cx, *raw_id.ptr, key_value.handle_mut()) } {
247 return DOMString::from("/* invalid */");
248 }
249 unsafe { handle_value_to_string(cx, key_value.handle()) }
250 } else {
251 return DOMString::from("/* invalid */");
252 };
253 props.push(format!("{}: {}", key, value_string,));
254 } else {
255 props.push(value_string.to_string());
256 }
257 }
258 if truncate {
259 props.push("…".to_string());
260 }
261 if object_class == ESClass::Array {
262 DOMString::from(format!("[{}]", itertools::join(props, ", ")))
263 } else {
264 DOMString::from(format!("{{{}}}", itertools::join(props, ", ")))
265 }
266 }
267 fn stringify_inner(cx: JSContext, value: HandleValue, mut parents: Vec<u64>) -> DOMString {
268 if parents.len() >= MAX_LOG_DEPTH {
269 return DOMString::from("...");
270 }
271 let value_bits = value.asBits_;
272 if parents.contains(&value_bits) {
273 return DOMString::from("[circular]");
274 }
275 if value.is_undefined() {
276 return DOMString::from("undefined");
278 } else if !value.is_object() {
279 return unsafe { handle_value_to_string(*cx, value) };
280 }
281 parents.push(value_bits);
282
283 if value.is_object() {
284 if let Some(repr) = maybe_stringify_dom_object(cx, value) {
285 return repr;
286 }
287 }
288 unsafe { stringify_object_from_handle_value(*cx, value, parents) }
289 }
290 stringify_inner(cx, message, Vec::new())
291 }
292}
293
294#[expect(unsafe_code)]
295fn maybe_stringify_dom_object(cx: JSContext, value: HandleValue) -> Option<DOMString> {
296 rooted!(in(*cx) let obj = value.to_object());
301 let is_dom_class = unsafe { get_dom_class(obj.get()).is_ok() };
302 if !is_dom_class {
303 return None;
304 }
305 rooted!(in(*cx) let class_name = unsafe { ToString(*cx, value) });
306 let Some(class_name) = NonNull::new(class_name.get()) else {
307 return Some("<error converting DOM object to string>".into());
308 };
309 let class_name = unsafe {
310 jsstr_to_string(*cx, class_name)
311 .replace("[object ", "")
312 .replace("]", "")
313 };
314 let mut repr = format!("{} ", class_name);
315 rooted!(in(*cx) let mut value = value.get());
316
317 #[expect(unsafe_code)]
318 unsafe extern "C" fn stringified(
319 string: *const u16,
320 len: u32,
321 data: *mut std::ffi::c_void,
322 ) -> bool {
323 let s = data as *mut String;
324 let string_chars = unsafe { slice::from_raw_parts(string, len as usize) };
325 unsafe { (*s).push_str(&String::from_utf16_lossy(string_chars)) };
326 true
327 }
328
329 rooted!(in(*cx) let space = Int32Value(2));
330 let stringify_result = unsafe {
331 JS_Stringify(
332 *cx,
333 value.handle_mut(),
334 HandleObject::null(),
335 space.handle(),
336 Some(stringified),
337 &mut repr as *mut String as *mut _,
338 )
339 };
340 if !stringify_result {
341 return Some("<error converting DOM object to string>".into());
342 }
343 Some(repr.into())
344}
345
346fn stringify_handle_values(messages: &[HandleValue]) -> DOMString {
347 DOMString::from(itertools::join(
348 messages.iter().copied().map(stringify_handle_value),
349 " ",
350 ))
351}
352
353fn console_message_to_stdout(global: &GlobalScope, message: &DOMString) {
358 #[cfg(not(any(target_os = "android", target_env = "ohos")))]
359 {
360 let prefix = global.current_group_label().unwrap_or_default();
361 let formatted_message = format!("{}{}", prefix, message);
362 with_stderr_lock(move || {
363 println!("{}", formatted_message);
364 });
365 }
366}
367
368#[derive(Debug, Eq, PartialEq)]
369enum IncludeStackTrace {
370 Yes,
371 No,
372}
373
374impl consoleMethods<crate::DomTypeHolder> for Console {
375 fn Log(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
377 Console::method(global, LogLevel::Log, messages, IncludeStackTrace::No);
378 }
379
380 fn Clear(global: &GlobalScope) {
382 let message = Console::build_message(LogLevel::Clear).finish();
383 Console::send_to_devtools(global, message);
384 }
385
386 fn Debug(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
388 Console::method(global, LogLevel::Debug, messages, IncludeStackTrace::No);
389 }
390
391 fn Info(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
393 Console::method(global, LogLevel::Info, messages, IncludeStackTrace::No);
394 }
395
396 fn Warn(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
398 Console::method(global, LogLevel::Warn, messages, IncludeStackTrace::No);
399 }
400
401 fn Error(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
403 Console::method(global, LogLevel::Error, messages, IncludeStackTrace::No);
404 }
405
406 fn Trace(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
408 Console::method(global, LogLevel::Trace, messages, IncludeStackTrace::Yes);
409 }
410
411 fn Assert(_cx: JSContext, global: &GlobalScope, condition: bool, messages: Vec<HandleValue>) {
413 if !condition {
414 let message = format!("Assertion failed: {}", stringify_handle_values(&messages));
415
416 Console::send_string_message(global, LogLevel::Log, message.clone());
417 }
418 }
419
420 fn Time(global: &GlobalScope, label: DOMString) {
422 if let Ok(()) = global.time(label.clone()) {
423 let message = format!("{label}: timer started");
424 Console::send_string_message(global, LogLevel::Log, message.clone());
425 }
426 }
427
428 fn TimeLog(_cx: JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
430 if let Ok(delta) = global.time_log(&label) {
431 let message = format!("{label}: {delta}ms {}", stringify_handle_values(&data));
432
433 Console::send_string_message(global, LogLevel::Log, message.clone());
434 }
435 }
436
437 fn TimeEnd(global: &GlobalScope, label: DOMString) {
439 if let Ok(delta) = global.time_end(&label) {
440 let message = format!("{label}: {delta}ms");
441
442 Console::send_string_message(global, LogLevel::Log, message.clone());
443 }
444 }
445
446 fn Group(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
448 global.push_console_group(stringify_handle_values(&messages));
449 }
450
451 fn GroupCollapsed(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
453 global.push_console_group(stringify_handle_values(&messages));
454 }
455
456 fn GroupEnd(global: &GlobalScope) {
458 global.pop_console_group();
459 }
460
461 fn Count(global: &GlobalScope, label: DOMString) {
463 let count = global.increment_console_count(&label);
464 let message = format!("{label}: {count}");
465
466 Console::send_string_message(global, LogLevel::Log, message.clone());
467 }
468
469 fn CountReset(global: &GlobalScope, label: DOMString) {
471 if global.reset_console_count(&label).is_err() {
472 Self::internal_warn(
473 global,
474 DOMString::from(format!("Counter “{label}” doesn’t exist.")),
475 )
476 }
477 }
478}
479
480#[expect(unsafe_code)]
481fn get_js_stack(cx: *mut jsapi::JSContext) -> Vec<StackFrame> {
482 const MAX_FRAME_COUNT: u32 = 128;
483
484 let mut frames = vec![];
485 rooted!(in(cx) let mut handle = ptr::null_mut());
486 let captured_js_stack = unsafe { CapturedJSStack::new(cx, handle, Some(MAX_FRAME_COUNT)) };
487 let Some(captured_js_stack) = captured_js_stack else {
488 return frames;
489 };
490
491 captured_js_stack.for_each_stack_frame(|frame| {
492 rooted!(in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
493
494 unsafe {
496 jsapi::GetSavedFrameFunctionDisplayName(
497 cx,
498 ptr::null_mut(),
499 frame.into(),
500 result.handle_mut().into(),
501 jsapi::SavedFrameSelfHosted::Include,
502 );
503 }
504 let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
505 unsafe { jsstr_to_string(cx, nonnull_result) }
506 } else {
507 "<anonymous>".into()
508 };
509
510 result.set(ptr::null_mut());
512 unsafe {
513 jsapi::GetSavedFrameSource(
514 cx,
515 ptr::null_mut(),
516 frame.into(),
517 result.handle_mut().into(),
518 jsapi::SavedFrameSelfHosted::Include,
519 );
520 }
521 let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
522 unsafe { jsstr_to_string(cx, nonnull_result) }
523 } else {
524 "<anonymous>".into()
525 };
526
527 let mut line_number = 0;
529 unsafe {
530 jsapi::GetSavedFrameLine(
531 cx,
532 ptr::null_mut(),
533 frame.into(),
534 &mut line_number,
535 jsapi::SavedFrameSelfHosted::Include,
536 );
537 }
538
539 let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
540 unsafe {
541 jsapi::GetSavedFrameColumn(
542 cx,
543 ptr::null_mut(),
544 frame.into(),
545 &mut column_number,
546 jsapi::SavedFrameSelfHosted::Include,
547 );
548 }
549 let frame = StackFrame {
550 filename,
551 function_name,
552 line_number,
553 column_number: column_number.value_,
554 };
555
556 frames.push(frame);
557 });
558
559 frames
560}