// Copyright 2016 the V8 project authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "src/inspector/v8-console-message.h" #include "src/debug/debug-interface.h" #include "src/inspector/inspected-context.h" #include "src/inspector/protocol/Protocol.h" #include "src/inspector/string-util.h" #include "src/inspector/v8-console-agent-impl.h" #include "src/inspector/v8-inspector-impl.h" #include "src/inspector/v8-inspector-session-impl.h" #include "src/inspector/v8-runtime-agent-impl.h" #include "src/inspector/v8-stack-trace-impl.h" #include "src/tracing/trace-event.h" #include "include/v8-inspector.h" namespace v8_inspector { namespace { String16 consoleAPITypeValue(ConsoleAPIType type) { switch (type) { case ConsoleAPIType::kLog: return protocol::Runtime::ConsoleAPICalled::TypeEnum::Log; case ConsoleAPIType::kDebug: return protocol::Runtime::ConsoleAPICalled::TypeEnum::Debug; case ConsoleAPIType::kInfo: return protocol::Runtime::ConsoleAPICalled::TypeEnum::Info; case ConsoleAPIType::kError: return protocol::Runtime::ConsoleAPICalled::TypeEnum::Error; case ConsoleAPIType::kWarning: return protocol::Runtime::ConsoleAPICalled::TypeEnum::Warning; case ConsoleAPIType::kClear: return protocol::Runtime::ConsoleAPICalled::TypeEnum::Clear; case ConsoleAPIType::kDir: return protocol::Runtime::ConsoleAPICalled::TypeEnum::Dir; case ConsoleAPIType::kDirXML: return protocol::Runtime::ConsoleAPICalled::TypeEnum::Dirxml; case ConsoleAPIType::kTable: return protocol::Runtime::ConsoleAPICalled::TypeEnum::Table; case ConsoleAPIType::kTrace: return protocol::Runtime::ConsoleAPICalled::TypeEnum::Trace; case ConsoleAPIType::kStartGroup: return protocol::Runtime::ConsoleAPICalled::TypeEnum::StartGroup; case ConsoleAPIType::kStartGroupCollapsed: return protocol::Runtime::ConsoleAPICalled::TypeEnum::StartGroupCollapsed; case ConsoleAPIType::kEndGroup: return protocol::Runtime::ConsoleAPICalled::TypeEnum::EndGroup; case ConsoleAPIType::kAssert: return protocol::Runtime::ConsoleAPICalled::TypeEnum::Assert; case ConsoleAPIType::kTimeEnd: return protocol::Runtime::ConsoleAPICalled::TypeEnum::TimeEnd; case ConsoleAPIType::kCount: return protocol::Runtime::ConsoleAPICalled::TypeEnum::Count; } return protocol::Runtime::ConsoleAPICalled::TypeEnum::Log; } const char kGlobalConsoleMessageHandleLabel[] = "DevTools console"; const unsigned maxConsoleMessageCount = 1000; const int maxConsoleMessageV8Size = 10 * 1024 * 1024; const unsigned maxArrayItemsLimit = 10000; const unsigned maxStackDepthLimit = 32; class V8ValueStringBuilder { public: static String16 toString(v8::Local<v8::Value> value, v8::Local<v8::Context> context) { V8ValueStringBuilder builder(context); if (!builder.append(value)) return String16(); return builder.toString(); } private: enum { IgnoreNull = 1 << 0, IgnoreUndefined = 1 << 1, }; explicit V8ValueStringBuilder(v8::Local<v8::Context> context) : m_arrayLimit(maxArrayItemsLimit), m_isolate(context->GetIsolate()), m_tryCatch(context->GetIsolate()), m_context(context) {} bool append(v8::Local<v8::Value> value, unsigned ignoreOptions = 0) { if (value.IsEmpty()) return true; if ((ignoreOptions & IgnoreNull) && value->IsNull()) return true; if ((ignoreOptions & IgnoreUndefined) && value->IsUndefined()) return true; if (value->IsString()) return append(v8::Local<v8::String>::Cast(value)); if (value->IsStringObject()) return append(v8::Local<v8::StringObject>::Cast(value)->ValueOf()); if (value->IsBigInt()) return append(v8::Local<v8::BigInt>::Cast(value)); if (value->IsBigIntObject()) return append(v8::Local<v8::BigIntObject>::Cast(value)->ValueOf()); if (value->IsSymbol()) return append(v8::Local<v8::Symbol>::Cast(value)); if (value->IsSymbolObject()) return append(v8::Local<v8::SymbolObject>::Cast(value)->ValueOf()); if (value->IsNumberObject()) { m_builder.append(String16::fromDouble( v8::Local<v8::NumberObject>::Cast(value)->ValueOf(), 6)); return true; } if (value->IsBooleanObject()) { m_builder.append(v8::Local<v8::BooleanObject>::Cast(value)->ValueOf() ? "true" : "false"); return true; } if (value->IsArray()) return append(v8::Local<v8::Array>::Cast(value)); if (value->IsProxy()) { m_builder.append("[object Proxy]"); return true; } if (value->IsObject() && !value->IsDate() && !value->IsFunction() && !value->IsNativeError() && !value->IsRegExp()) { v8::Local<v8::Object> object = v8::Local<v8::Object>::Cast(value); v8::Local<v8::String> stringValue; if (object->ObjectProtoToString(m_context).ToLocal(&stringValue)) return append(stringValue); } v8::Local<v8::String> stringValue; if (!value->ToString(m_context).ToLocal(&stringValue)) return false; return append(stringValue); } bool append(v8::Local<v8::Array> array) { for (const auto& it : m_visitedArrays) { if (it == array) return true; } uint32_t length = array->Length(); if (length > m_arrayLimit) return false; if (m_visitedArrays.size() > maxStackDepthLimit) return false; bool result = true; m_arrayLimit -= length; m_visitedArrays.push_back(array); for (uint32_t i = 0; i < length; ++i) { if (i) m_builder.append(','); v8::Local<v8::Value> value; if (!array->Get(m_context, i).ToLocal(&value)) continue; if (!append(value, IgnoreNull | IgnoreUndefined)) { result = false; break; } } m_visitedArrays.pop_back(); return result; } bool append(v8::Local<v8::Symbol> symbol) { m_builder.append("Symbol("); bool result = append(symbol->Description(), IgnoreUndefined); m_builder.append(')'); return result; } bool append(v8::Local<v8::BigInt> bigint) { v8::Local<v8::String> bigint_string; if (!bigint->ToString(m_context).ToLocal(&bigint_string)) return false; bool result = append(bigint_string); if (m_tryCatch.HasCaught()) return false; m_builder.append('n'); return result; } bool append(v8::Local<v8::String> string) { if (m_tryCatch.HasCaught()) return false; if (!string.IsEmpty()) { m_builder.append(toProtocolString(m_isolate, string)); } return true; } String16 toString() { if (m_tryCatch.HasCaught()) return String16(); return m_builder.toString(); } uint32_t m_arrayLimit; v8::Isolate* m_isolate; String16Builder m_builder; std::vector<v8::Local<v8::Array>> m_visitedArrays; v8::TryCatch m_tryCatch; v8::Local<v8::Context> m_context; }; } // namespace V8ConsoleMessage::V8ConsoleMessage(V8MessageOrigin origin, double timestamp, const String16& message) : m_origin(origin), m_timestamp(timestamp), m_message(message), m_lineNumber(0), m_columnNumber(0), m_scriptId(0), m_contextId(0), m_type(ConsoleAPIType::kLog), m_exceptionId(0), m_revokedExceptionId(0) {} V8ConsoleMessage::~V8ConsoleMessage() = default; void V8ConsoleMessage::setLocation(const String16& url, unsigned lineNumber, unsigned columnNumber, std::unique_ptr<V8StackTraceImpl> stackTrace, int scriptId) { m_url = url; m_lineNumber = lineNumber; m_columnNumber = columnNumber; m_stackTrace = std::move(stackTrace); m_scriptId = scriptId; } void V8ConsoleMessage::reportToFrontend( protocol::Console::Frontend* frontend) const { DCHECK_EQ(V8MessageOrigin::kConsole, m_origin); String16 level = protocol::Console::ConsoleMessage::LevelEnum::Log; if (m_type == ConsoleAPIType::kDebug || m_type == ConsoleAPIType::kCount || m_type == ConsoleAPIType::kTimeEnd) level = protocol::Console::ConsoleMessage::LevelEnum::Debug; else if (m_type == ConsoleAPIType::kError || m_type == ConsoleAPIType::kAssert) level = protocol::Console::ConsoleMessage::LevelEnum::Error; else if (m_type == ConsoleAPIType::kWarning) level = protocol::Console::ConsoleMessage::LevelEnum::Warning; else if (m_type == ConsoleAPIType::kInfo) level = protocol::Console::ConsoleMessage::LevelEnum::Info; std::unique_ptr<protocol::Console::ConsoleMessage> result = protocol::Console::ConsoleMessage::create() .setSource(protocol::Console::ConsoleMessage::SourceEnum::ConsoleApi) .setLevel(level) .setText(m_message) .build(); result->setLine(static_cast<int>(m_lineNumber)); result->setColumn(static_cast<int>(m_columnNumber)); result->setUrl(m_url); frontend->messageAdded(std::move(result)); } std::unique_ptr<protocol::Array<protocol::Runtime::RemoteObject>> V8ConsoleMessage::wrapArguments(V8InspectorSessionImpl* session, bool generatePreview) const { V8InspectorImpl* inspector = session->inspector(); int contextGroupId = session->contextGroupId(); int contextId = m_contextId; if (!m_arguments.size() || !contextId) return nullptr; InspectedContext* inspectedContext = inspector->getContext(contextGroupId, contextId); if (!inspectedContext) return nullptr; v8::Isolate* isolate = inspectedContext->isolate(); v8::HandleScope handles(isolate); v8::Local<v8::Context> context = inspectedContext->context(); auto args = std::make_unique<protocol::Array<protocol::Runtime::RemoteObject>>(); v8::Local<v8::Value> value = m_arguments[0]->Get(isolate); if (value->IsObject() && m_type == ConsoleAPIType::kTable && generatePreview) { v8::MaybeLocal<v8::Array> columns; if (m_arguments.size() > 1) { v8::Local<v8::Value> secondArgument = m_arguments[1]->Get(isolate); if (secondArgument->IsArray()) { columns = v8::Local<v8::Array>::Cast(secondArgument); } else if (secondArgument->IsString()) { v8::TryCatch tryCatch(isolate); v8::Local<v8::Array> array = v8::Array::New(isolate); if (array->Set(context, 0, secondArgument).IsJust()) { columns = array; } } } std::unique_ptr<protocol::Runtime::RemoteObject> wrapped = session->wrapTable(context, v8::Local<v8::Object>::Cast(value), columns); inspectedContext = inspector->getContext(contextGroupId, contextId); if (!inspectedContext) return nullptr; if (wrapped) { args->emplace_back(std::move(wrapped)); } else { args = nullptr; } } else { for (size_t i = 0; i < m_arguments.size(); ++i) { std::unique_ptr<protocol::Runtime::RemoteObject> wrapped = session->wrapObject(context, m_arguments[i]->Get(isolate), "console", generatePreview); inspectedContext = inspector->getContext(contextGroupId, contextId); if (!inspectedContext) return nullptr; if (!wrapped) { args = nullptr; break; } args->emplace_back(std::move(wrapped)); } } return args; } void V8ConsoleMessage::reportToFrontend(protocol::Runtime::Frontend* frontend, V8InspectorSessionImpl* session, bool generatePreview) const { int contextGroupId = session->contextGroupId(); V8InspectorImpl* inspector = session->inspector(); if (m_origin == V8MessageOrigin::kException) { std::unique_ptr<protocol::Runtime::RemoteObject> exception = wrapException(session, generatePreview); if (!inspector->hasConsoleMessageStorage(contextGroupId)) return; std::unique_ptr<protocol::Runtime::ExceptionDetails> exceptionDetails = protocol::Runtime::ExceptionDetails::create() .setExceptionId(m_exceptionId) .setText(exception ? m_message : m_detailedMessage) .setLineNumber(m_lineNumber ? m_lineNumber - 1 : 0) .setColumnNumber(m_columnNumber ? m_columnNumber - 1 : 0) .build(); if (m_scriptId) exceptionDetails->setScriptId(String16::fromInteger(m_scriptId)); if (!m_url.isEmpty()) exceptionDetails->setUrl(m_url); if (m_stackTrace) { exceptionDetails->setStackTrace( m_stackTrace->buildInspectorObjectImpl(inspector->debugger())); } if (m_contextId) exceptionDetails->setExecutionContextId(m_contextId); if (exception) exceptionDetails->setException(std::move(exception)); frontend->exceptionThrown(m_timestamp, std::move(exceptionDetails)); return; } if (m_origin == V8MessageOrigin::kRevokedException) { frontend->exceptionRevoked(m_message, m_revokedExceptionId); return; } if (m_origin == V8MessageOrigin::kConsole) { std::unique_ptr<protocol::Array<protocol::Runtime::RemoteObject>> arguments = wrapArguments(session, generatePreview); if (!inspector->hasConsoleMessageStorage(contextGroupId)) return; if (!arguments) { arguments = std::make_unique<protocol::Array<protocol::Runtime::RemoteObject>>(); if (!m_message.isEmpty()) { std::unique_ptr<protocol::Runtime::RemoteObject> messageArg = protocol::Runtime::RemoteObject::create() .setType(protocol::Runtime::RemoteObject::TypeEnum::String) .build(); messageArg->setValue(protocol::StringValue::create(m_message)); arguments->emplace_back(std::move(messageArg)); } } Maybe<String16> consoleContext; if (!m_consoleContext.isEmpty()) consoleContext = m_consoleContext; std::unique_ptr<protocol::Runtime::StackTrace> stackTrace; if (m_stackTrace) { switch (m_type) { case ConsoleAPIType::kAssert: case ConsoleAPIType::kError: case ConsoleAPIType::kTrace: case ConsoleAPIType::kWarning: stackTrace = m_stackTrace->buildInspectorObjectImpl(inspector->debugger()); break; default: stackTrace = m_stackTrace->buildInspectorObjectImpl(inspector->debugger(), 0); break; } } frontend->consoleAPICalled( consoleAPITypeValue(m_type), std::move(arguments), m_contextId, m_timestamp, std::move(stackTrace), std::move(consoleContext)); return; } UNREACHABLE(); } std::unique_ptr<protocol::Runtime::RemoteObject> V8ConsoleMessage::wrapException(V8InspectorSessionImpl* session, bool generatePreview) const { if (!m_arguments.size() || !m_contextId) return nullptr; DCHECK_EQ(1u, m_arguments.size()); InspectedContext* inspectedContext = session->inspector()->getContext(session->contextGroupId(), m_contextId); if (!inspectedContext) return nullptr; v8::Isolate* isolate = inspectedContext->isolate(); v8::HandleScope handles(isolate); // TODO(dgozman): should we use different object group? return session->wrapObject(inspectedContext->context(), m_arguments[0]->Get(isolate), "console", generatePreview); } V8MessageOrigin V8ConsoleMessage::origin() const { return m_origin; } ConsoleAPIType V8ConsoleMessage::type() const { return m_type; } // static std::unique_ptr<V8ConsoleMessage> V8ConsoleMessage::createForConsoleAPI( v8::Local<v8::Context> v8Context, int contextId, int groupId, V8InspectorImpl* inspector, double timestamp, ConsoleAPIType type, const std::vector<v8::Local<v8::Value>>& arguments, const String16& consoleContext, std::unique_ptr<V8StackTraceImpl> stackTrace) { v8::Isolate* isolate = v8Context->GetIsolate(); std::unique_ptr<V8ConsoleMessage> message( new V8ConsoleMessage(V8MessageOrigin::kConsole, timestamp, String16())); if (stackTrace && !stackTrace->isEmpty()) { message->m_url = toString16(stackTrace->topSourceURL()); message->m_lineNumber = stackTrace->topLineNumber(); message->m_columnNumber = stackTrace->topColumnNumber(); } message->m_stackTrace = std::move(stackTrace); message->m_consoleContext = consoleContext; message->m_type = type; message->m_contextId = contextId; for (size_t i = 0; i < arguments.size(); ++i) { std::unique_ptr<v8::Global<v8::Value>> argument( new v8::Global<v8::Value>(isolate, arguments.at(i))); argument->AnnotateStrongRetainer(kGlobalConsoleMessageHandleLabel); message->m_arguments.push_back(std::move(argument)); message->m_v8Size += v8::debug::EstimatedValueSize(isolate, arguments.at(i)); } for (size_t i = 0, num_args = arguments.size(); i < num_args; ++i) { if (i) message->m_message += String16(" "); message->m_message += V8ValueStringBuilder::toString(arguments[i], v8Context); } v8::Isolate::MessageErrorLevel clientLevel = v8::Isolate::kMessageInfo; if (type == ConsoleAPIType::kDebug || type == ConsoleAPIType::kCount || type == ConsoleAPIType::kTimeEnd) { clientLevel = v8::Isolate::kMessageDebug; } else if (type == ConsoleAPIType::kError || type == ConsoleAPIType::kAssert) { clientLevel = v8::Isolate::kMessageError; } else if (type == ConsoleAPIType::kWarning) { clientLevel = v8::Isolate::kMessageWarning; } else if (type == ConsoleAPIType::kInfo || type == ConsoleAPIType::kLog) { clientLevel = v8::Isolate::kMessageInfo; } if (type != ConsoleAPIType::kClear) { inspector->client()->consoleAPIMessage( groupId, clientLevel, toStringView(message->m_message), toStringView(message->m_url), message->m_lineNumber, message->m_columnNumber, message->m_stackTrace.get()); } return message; } // static std::unique_ptr<V8ConsoleMessage> V8ConsoleMessage::createForException( double timestamp, const String16& detailedMessage, const String16& url, unsigned lineNumber, unsigned columnNumber, std::unique_ptr<V8StackTraceImpl> stackTrace, int scriptId, v8::Isolate* isolate, const String16& message, int contextId, v8::Local<v8::Value> exception, unsigned exceptionId) { std::unique_ptr<V8ConsoleMessage> consoleMessage( new V8ConsoleMessage(V8MessageOrigin::kException, timestamp, message)); consoleMessage->setLocation(url, lineNumber, columnNumber, std::move(stackTrace), scriptId); consoleMessage->m_exceptionId = exceptionId; consoleMessage->m_detailedMessage = detailedMessage; if (contextId && !exception.IsEmpty()) { consoleMessage->m_contextId = contextId; consoleMessage->m_arguments.push_back( std::unique_ptr<v8::Global<v8::Value>>( new v8::Global<v8::Value>(isolate, exception))); consoleMessage->m_v8Size += v8::debug::EstimatedValueSize(isolate, exception); } return consoleMessage; } // static std::unique_ptr<V8ConsoleMessage> V8ConsoleMessage::createForRevokedException( double timestamp, const String16& messageText, unsigned revokedExceptionId) { std::unique_ptr<V8ConsoleMessage> message(new V8ConsoleMessage( V8MessageOrigin::kRevokedException, timestamp, messageText)); message->m_revokedExceptionId = revokedExceptionId; return message; } void V8ConsoleMessage::contextDestroyed(int contextId) { if (contextId != m_contextId) return; m_contextId = 0; if (m_message.isEmpty()) m_message = "<message collected>"; Arguments empty; m_arguments.swap(empty); m_v8Size = 0; } // ------------------------ V8ConsoleMessageStorage ---------------------------- V8ConsoleMessageStorage::V8ConsoleMessageStorage(V8InspectorImpl* inspector, int contextGroupId) : m_inspector(inspector), m_contextGroupId(contextGroupId) {} V8ConsoleMessageStorage::~V8ConsoleMessageStorage() { clear(); } namespace { void TraceV8ConsoleMessageEvent(V8MessageOrigin origin, ConsoleAPIType type) { // Change in this function requires adjustment of Catapult/Telemetry metric // tracing/tracing/metrics/console_error_metric.html. // See https://crbug.com/880432 if (origin == V8MessageOrigin::kException) { TRACE_EVENT_INSTANT0("v8.console", "V8ConsoleMessage::Exception", TRACE_EVENT_SCOPE_THREAD); } else if (type == ConsoleAPIType::kError) { TRACE_EVENT_INSTANT0("v8.console", "V8ConsoleMessage::Error", TRACE_EVENT_SCOPE_THREAD); } else if (type == ConsoleAPIType::kAssert) { TRACE_EVENT_INSTANT0("v8.console", "V8ConsoleMessage::Assert", TRACE_EVENT_SCOPE_THREAD); } } } // anonymous namespace void V8ConsoleMessageStorage::addMessage( std::unique_ptr<V8ConsoleMessage> message) { int contextGroupId = m_contextGroupId; V8InspectorImpl* inspector = m_inspector; if (message->type() == ConsoleAPIType::kClear) clear(); TraceV8ConsoleMessageEvent(message->origin(), message->type()); inspector->forEachSession( contextGroupId, [&message](V8InspectorSessionImpl* session) { if (message->origin() == V8MessageOrigin::kConsole) session->consoleAgent()->messageAdded(message.get()); session->runtimeAgent()->messageAdded(message.get()); }); if (!inspector->hasConsoleMessageStorage(contextGroupId)) return; DCHECK(m_messages.size() <= maxConsoleMessageCount); if (m_messages.size() == maxConsoleMessageCount) { m_estimatedSize -= m_messages.front()->estimatedSize(); m_messages.pop_front(); } while (m_estimatedSize + message->estimatedSize() > maxConsoleMessageV8Size && !m_messages.empty()) { m_estimatedSize -= m_messages.front()->estimatedSize(); m_messages.pop_front(); } m_messages.push_back(std::move(message)); m_estimatedSize += m_messages.back()->estimatedSize(); } void V8ConsoleMessageStorage::clear() { m_messages.clear(); m_estimatedSize = 0; m_inspector->forEachSession(m_contextGroupId, [](V8InspectorSessionImpl* session) { session->releaseObjectGroup("console"); }); m_data.clear(); } bool V8ConsoleMessageStorage::shouldReportDeprecationMessage( int contextId, const String16& method) { std::set<String16>& reportedDeprecationMessages = m_data[contextId].m_reportedDeprecationMessages; auto it = reportedDeprecationMessages.find(method); if (it != reportedDeprecationMessages.end()) return false; reportedDeprecationMessages.insert(it, method); return true; } int V8ConsoleMessageStorage::count(int contextId, const String16& id) { return ++m_data[contextId].m_count[id]; } void V8ConsoleMessageStorage::time(int contextId, const String16& id) { m_data[contextId].m_time[id] = m_inspector->client()->currentTimeMS(); } bool V8ConsoleMessageStorage::countReset(int contextId, const String16& id) { std::map<String16, int>& count_map = m_data[contextId].m_count; if (count_map.find(id) == count_map.end()) return false; count_map[id] = 0; return true; } double V8ConsoleMessageStorage::timeLog(int contextId, const String16& id) { std::map<String16, double>& time = m_data[contextId].m_time; auto it = time.find(id); if (it == time.end()) return 0.0; return m_inspector->client()->currentTimeMS() - it->second; } double V8ConsoleMessageStorage::timeEnd(int contextId, const String16& id) { std::map<String16, double>& time = m_data[contextId].m_time; auto it = time.find(id); if (it == time.end()) return 0.0; double elapsed = m_inspector->client()->currentTimeMS() - it->second; time.erase(it); return elapsed; } bool V8ConsoleMessageStorage::hasTimer(int contextId, const String16& id) { const std::map<String16, double>& time = m_data[contextId].m_time; return time.find(id) != time.end(); } void V8ConsoleMessageStorage::contextDestroyed(int contextId) { m_estimatedSize = 0; for (size_t i = 0; i < m_messages.size(); ++i) { m_messages[i]->contextDestroyed(contextId); m_estimatedSize += m_messages[i]->estimatedSize(); } auto it = m_data.find(contextId); if (it != m_data.end()) m_data.erase(contextId); } } // namespace v8_inspector