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 #[allow(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.get_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#[allow(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(JS_ValueToSource(cx, value)) {
128 Some(js_str) => {
129 js_string.set(js_str.as_ptr());
130 DOMString::from_string(jsstr_to_string(cx, js_str))
131 },
132 None => "<error converting value to string>".into(),
133 }
134}
135
136#[allow(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#[allow(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 !GetBuiltinClass(cx, obj.handle(), &mut object_class as *mut _) {
178 return DOMString::from("/* invalid */");
179 }
180 let mut ids = IdVector::new(cx);
181 if !GetPropertyKeys(
182 cx,
183 obj.handle(),
184 jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS,
185 ids.handle_mut(),
186 ) {
187 return DOMString::from("/* invalid */");
188 }
189 let truncate = ids.len() > MAX_LOG_CHILDREN;
190 if object_class != ESClass::Array && object_class != ESClass::Object {
191 if truncate {
192 return DOMString::from("…");
193 } else {
194 return handle_value_to_string(cx, value);
195 }
196 }
197
198 let mut explicit_keys = object_class == ESClass::Object;
199 let mut props = Vec::with_capacity(ids.len());
200 for id in ids.iter().take(MAX_LOG_CHILDREN) {
201 rooted!(in(cx) let id = *id);
202 rooted!(in(cx) let mut desc = PropertyDescriptor::default());
203
204 let mut is_none = false;
205 if !JS_GetOwnPropertyDescriptorById(
206 cx,
207 obj.handle(),
208 id.handle(),
209 desc.handle_mut(),
210 &mut is_none,
211 ) {
212 return DOMString::from("/* invalid */");
213 }
214
215 rooted!(in(cx) let mut property = UndefinedValue());
216 if !JS_GetPropertyById(cx, obj.handle(), id.handle(), property.handle_mut()) {
217 return DOMString::from("/* invalid */");
218 }
219
220 if !explicit_keys {
221 if id.is_int() {
222 if let Ok(id_int) = usize::try_from(id.to_int()) {
223 explicit_keys = props.len() != id_int;
224 } else {
225 explicit_keys = false;
226 }
227 } else {
228 explicit_keys = false;
229 }
230 }
231 let value_string =
232 stringify_inner(JSContext::from_ptr(cx), property.handle(), parents.clone());
233 if explicit_keys {
234 let key = if id.is_string() || id.is_symbol() || id.is_int() {
235 rooted!(in(cx) let mut key_value = UndefinedValue());
236 let raw_id: jsapi::HandleId = id.handle().into();
237 if !JS_IdToValue(cx, *raw_id.ptr, key_value.handle_mut()) {
238 return DOMString::from("/* invalid */");
239 }
240 handle_value_to_string(cx, key_value.handle())
241 } else {
242 return DOMString::from("/* invalid */");
243 };
244 props.push(format!("{}: {}", key, value_string,));
245 } else {
246 props.push(value_string.to_string());
247 }
248 }
249 if truncate {
250 props.push("…".to_string());
251 }
252 if object_class == ESClass::Array {
253 DOMString::from(format!("[{}]", itertools::join(props, ", ")))
254 } else {
255 DOMString::from(format!("{{{}}}", itertools::join(props, ", ")))
256 }
257 }
258 fn stringify_inner(cx: JSContext, value: HandleValue, mut parents: Vec<u64>) -> DOMString {
259 if parents.len() >= MAX_LOG_DEPTH {
260 return DOMString::from("...");
261 }
262 let value_bits = value.asBits_;
263 if parents.contains(&value_bits) {
264 return DOMString::from("[circular]");
265 }
266 if value.is_undefined() {
267 return DOMString::from("undefined");
269 } else if !value.is_object() {
270 return unsafe { handle_value_to_string(*cx, value) };
271 }
272 parents.push(value_bits);
273
274 if value.is_object() {
275 if let Some(repr) = maybe_stringify_dom_object(cx, value) {
276 return repr;
277 }
278 }
279 unsafe { stringify_object_from_handle_value(*cx, value, parents) }
280 }
281 stringify_inner(cx, message, Vec::new())
282 }
283}
284
285#[allow(unsafe_code)]
286fn maybe_stringify_dom_object(cx: JSContext, value: HandleValue) -> Option<DOMString> {
287 rooted!(in(*cx) let obj = value.to_object());
292 let is_dom_class = unsafe { get_dom_class(obj.get()).is_ok() };
293 if !is_dom_class {
294 return None;
295 }
296 rooted!(in(*cx) let class_name = unsafe { ToString(*cx, value) });
297 let Some(class_name) = NonNull::new(class_name.get()) else {
298 return Some("<error converting DOM object to string>".into());
299 };
300 let class_name = unsafe {
301 jsstr_to_string(*cx, class_name)
302 .replace("[object ", "")
303 .replace("]", "")
304 };
305 let mut repr = format!("{} ", class_name);
306 rooted!(in(*cx) let mut value = value.get());
307
308 #[allow(unsafe_code)]
309 unsafe extern "C" fn stringified(
310 string: *const u16,
311 len: u32,
312 data: *mut std::ffi::c_void,
313 ) -> bool {
314 let s = data as *mut String;
315 let string_chars = slice::from_raw_parts(string, len as usize);
316 (*s).push_str(&String::from_utf16_lossy(string_chars));
317 true
318 }
319
320 rooted!(in(*cx) let space = Int32Value(2));
321 let stringify_result = unsafe {
322 JS_Stringify(
323 *cx,
324 value.handle_mut(),
325 HandleObject::null(),
326 space.handle(),
327 Some(stringified),
328 &mut repr as *mut String as *mut _,
329 )
330 };
331 if !stringify_result {
332 return Some("<error converting DOM object to string>".into());
333 }
334 Some(repr.into())
335}
336
337fn stringify_handle_values(messages: &[HandleValue]) -> DOMString {
338 DOMString::from(itertools::join(
339 messages.iter().copied().map(stringify_handle_value),
340 " ",
341 ))
342}
343
344#[allow(unused_variables)]
349fn console_message_to_stdout(global: &GlobalScope, message: &DOMString) {
350 #[cfg(not(any(target_os = "android", target_env = "ohos")))]
351 {
352 let prefix = global.current_group_label().unwrap_or_default();
353 let formatted_message = format!("{}{}", prefix, message);
354 with_stderr_lock(move || {
355 println!("{}", formatted_message);
356 });
357 }
358}
359
360#[derive(Debug, Eq, PartialEq)]
361enum IncludeStackTrace {
362 Yes,
363 No,
364}
365
366impl consoleMethods<crate::DomTypeHolder> for Console {
367 fn Log(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
369 Console::method(global, LogLevel::Log, messages, IncludeStackTrace::No);
370 }
371
372 fn Clear(global: &GlobalScope) {
374 let message = Console::build_message(LogLevel::Clear).finish();
375 Console::send_to_devtools(global, message);
376 }
377
378 fn Debug(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
380 Console::method(global, LogLevel::Debug, messages, IncludeStackTrace::No);
381 }
382
383 fn Info(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
385 Console::method(global, LogLevel::Info, messages, IncludeStackTrace::No);
386 }
387
388 fn Warn(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
390 Console::method(global, LogLevel::Warn, messages, IncludeStackTrace::No);
391 }
392
393 fn Error(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
395 Console::method(global, LogLevel::Error, messages, IncludeStackTrace::No);
396 }
397
398 fn Trace(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
400 Console::method(global, LogLevel::Trace, messages, IncludeStackTrace::Yes);
401 }
402
403 fn Assert(_cx: JSContext, global: &GlobalScope, condition: bool, messages: Vec<HandleValue>) {
405 if !condition {
406 let message = format!("Assertion failed: {}", stringify_handle_values(&messages));
407
408 Console::send_string_message(global, LogLevel::Log, message.clone());
409 }
410 }
411
412 fn Time(global: &GlobalScope, label: DOMString) {
414 if let Ok(()) = global.time(label.clone()) {
415 let message = format!("{label}: timer started");
416 Console::send_string_message(global, LogLevel::Log, message.clone());
417 }
418 }
419
420 fn TimeLog(_cx: JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
422 if let Ok(delta) = global.time_log(&label) {
423 let message = format!("{label}: {delta}ms {}", stringify_handle_values(&data));
424
425 Console::send_string_message(global, LogLevel::Log, message.clone());
426 }
427 }
428
429 fn TimeEnd(global: &GlobalScope, label: DOMString) {
431 if let Ok(delta) = global.time_end(&label) {
432 let message = format!("{label}: {delta}ms");
433
434 Console::send_string_message(global, LogLevel::Log, message.clone());
435 }
436 }
437
438 fn Group(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
440 global.push_console_group(stringify_handle_values(&messages));
441 }
442
443 fn GroupCollapsed(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
445 global.push_console_group(stringify_handle_values(&messages));
446 }
447
448 fn GroupEnd(global: &GlobalScope) {
450 global.pop_console_group();
451 }
452
453 fn Count(global: &GlobalScope, label: DOMString) {
455 let count = global.increment_console_count(&label);
456 let message = format!("{label}: {count}");
457
458 Console::send_string_message(global, LogLevel::Log, message.clone());
459 }
460
461 fn CountReset(global: &GlobalScope, label: DOMString) {
463 if global.reset_console_count(&label).is_err() {
464 Self::internal_warn(
465 global,
466 DOMString::from(format!("Counter “{label}” doesn’t exist.")),
467 )
468 }
469 }
470}
471
472#[allow(unsafe_code)]
473fn get_js_stack(cx: *mut jsapi::JSContext) -> Vec<StackFrame> {
474 const MAX_FRAME_COUNT: u32 = 128;
475
476 let mut frames = vec![];
477 rooted!(in(cx) let mut handle = ptr::null_mut());
478 let captured_js_stack = unsafe { CapturedJSStack::new(cx, handle, Some(MAX_FRAME_COUNT)) };
479 let Some(captured_js_stack) = captured_js_stack else {
480 return frames;
481 };
482
483 captured_js_stack.for_each_stack_frame(|frame| {
484 rooted!(in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
485
486 unsafe {
488 jsapi::GetSavedFrameFunctionDisplayName(
489 cx,
490 ptr::null_mut(),
491 frame.into(),
492 result.handle_mut().into(),
493 jsapi::SavedFrameSelfHosted::Include,
494 );
495 }
496 let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
497 unsafe { jsstr_to_string(cx, nonnull_result) }
498 } else {
499 "<anonymous>".into()
500 };
501
502 result.set(ptr::null_mut());
504 unsafe {
505 jsapi::GetSavedFrameSource(
506 cx,
507 ptr::null_mut(),
508 frame.into(),
509 result.handle_mut().into(),
510 jsapi::SavedFrameSelfHosted::Include,
511 );
512 }
513 let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
514 unsafe { jsstr_to_string(cx, nonnull_result) }
515 } else {
516 "<anonymous>".into()
517 };
518
519 let mut line_number = 0;
521 unsafe {
522 jsapi::GetSavedFrameLine(
523 cx,
524 ptr::null_mut(),
525 frame.into(),
526 &mut line_number,
527 jsapi::SavedFrameSelfHosted::Include,
528 );
529 }
530
531 let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
532 unsafe {
533 jsapi::GetSavedFrameColumn(
534 cx,
535 ptr::null_mut(),
536 frame.into(),
537 &mut column_number,
538 jsapi::SavedFrameSelfHosted::Include,
539 );
540 }
541 let frame = StackFrame {
542 filename,
543 function_name,
544 line_number,
545 column_number: column_number.value_,
546 };
547
548 frames.push(frame);
549 });
550
551 frames
552}