#include "Console.h" #include "Caches.h" #include #include #include "Helpers.h" #include "RuntimeConfig.h" // #include "v8-log-agent-impl.h" #include using namespace v8; namespace tns { void Console::Init(Local context) { Isolate* isolate = context->GetIsolate(); Context::Scope context_scope(context); Local console = Object::New(isolate); bool success = console->SetPrototype(context, Object::New(isolate)).FromMaybe(false); tns::Assert(success, isolate); Console::AttachLogFunction(context, console, "log"); Console::AttachLogFunction(context, console, "info"); Console::AttachLogFunction(context, console, "error"); Console::AttachLogFunction(context, console, "warn"); Console::AttachLogFunction(context, console, "trace"); Console::AttachLogFunction(context, console, "assert", AssertCallback); Console::AttachLogFunction(context, console, "dir", DirCallback); Console::AttachLogFunction(context, console, "time", TimeCallback); Console::AttachLogFunction(context, console, "timeEnd", TimeEndCallback); Local global = context->Global(); PropertyAttribute readOnlyFlags = static_cast(PropertyAttribute::DontDelete | PropertyAttribute::ReadOnly); if (!global->DefineOwnProperty(context, tns::ToV8String(isolate, "console"), console, readOnlyFlags).FromMaybe(false)) { tns::Assert(false, isolate); } } void Console::AttachInspectorClient(v8_inspector::JsV8InspectorClient* aInspector) { inspector = aInspector; } void Console::LogCallback(const FunctionCallbackInfo& args) { // TODO: implement 'forceLog' override option like android has, to force logs in prod if desired if (!RuntimeConfig.LogToSystemConsole) { return; } Isolate* isolate = args.GetIsolate(); std::string stringResult = BuildStringFromArgs(args); Local data = args.Data().As(); std::string verbosityLevel = tns::ToString(isolate, data); std::string verbosityLevelUpper = verbosityLevel; std::transform(verbosityLevelUpper.begin(), verbosityLevelUpper.end(), verbosityLevelUpper.begin(), ::toupper); std::stringstream ss; ss << stringResult; if (verbosityLevel == "trace") { std::string stacktrace = tns::GetStackTrace(isolate); ss << std::endl << stacktrace << std::endl; } std::string msgToLog = ss.str(); ConsoleAPIType method = VerbosityToInspectorMethod(verbosityLevel); SendToDevToolsFrontEnd(method, args); std::string msgWithVerbosity = "CONSOLE " + verbosityLevelUpper + ": " + msgToLog; Log("%s", msgWithVerbosity.c_str()); } void Console::AssertCallback(const FunctionCallbackInfo& args) { if (!RuntimeConfig.LogToSystemConsole) { return; } Isolate* isolate = args.GetIsolate(); int argsLength = args.Length(); bool expressionPasses = argsLength > 0 && args[0]->BooleanValue(isolate); if (!expressionPasses) { std::stringstream ss; ss << "Assertion failed: "; if (argsLength > 1) { ss << BuildStringFromArgs(args, 1); } else { ss << "console.assert"; } std::string log = ss.str(); SendToDevToolsFrontEnd(ConsoleAPIType::kAssert, args); Log("%s", log.c_str()); } } void Console::DirCallback(const FunctionCallbackInfo& args) { if (!RuntimeConfig.LogToSystemConsole) { return; } int argsLen = args.Length(); Isolate* isolate = args.GetIsolate(); Local context = isolate->GetCurrentContext(); std::stringstream ss; std::string scriptUrl = tns::GetCurrentScriptUrl(isolate); ss << scriptUrl << ":"; if (argsLen > 0) { if (!args[0]->IsObject()) { std::string logString = BuildStringFromArgs(args); ss << " " << logString; } else { ss << std::endl << "==== object dump start ====" << std::endl; Local argObject = args[0].As(); Local propNames; bool success = argObject->GetPropertyNames(context).ToLocal(&propNames); tns::Assert(success, isolate); uint32_t propertiesLength = propNames->Length(); for (uint32_t i = 0; i < propertiesLength; i++) { Local propertyName = propNames->Get(context, i).ToLocalChecked(); Local propertyValue; bool success = argObject->Get(context, propertyName).ToLocal(&propertyValue); if (!success || propertyValue.IsEmpty() || propertyValue->IsUndefined()) { continue; } bool propIsFunction = propertyValue->IsFunction(); ss << tns::ToString(isolate, propertyName->ToString(context).ToLocalChecked()) << ": "; if (propIsFunction) { ss << "()"; } else if (propertyValue->IsArray()) { Local stringResult = BuildStringFromArg(context, propertyValue); std::string jsonStringifiedArray = tns::ToString(isolate, stringResult); ss << jsonStringifiedArray; } else if (propertyValue->IsObject()) { Local obj = propertyValue->ToObject(context).ToLocalChecked(); Local objString = TransformJSObject(obj); std::string jsonStringifiedObject = tns::ToString(isolate, objString); // if object prints out as the error string for circular references, replace with #CR instead for brevity if (jsonStringifiedObject.find("circular structure") != std::string::npos) { jsonStringifiedObject = "#CR"; } ss << jsonStringifiedObject; } else { ss << "\"" << tns::ToString(isolate, propertyValue->ToDetailString(context).ToLocalChecked()) << "\""; } ss << std::endl; } ss << "==== object dump end ====" << std::endl; } } else { ss << ""; } std::string msgToLog = ss.str(); SendToDevToolsFrontEnd(ConsoleAPIType::kDir, args); Log("%s", msgToLog.c_str()); } void Console::TimeCallback(const FunctionCallbackInfo& args) { if (!RuntimeConfig.LogToSystemConsole) { return; } Isolate* isolate = args.GetIsolate(); Local context = isolate->GetCurrentContext(); std::string label = "default"; Local labelString; if (args.Length() > 0 && args[0]->ToString(context).ToLocal(&labelString)) { label = tns::ToString(isolate, labelString); } std::shared_ptr cache = Caches::Get(isolate); auto nano = std::chrono::time_point_cast(std::chrono::system_clock::now()); double timeStamp = nano.time_since_epoch().count(); cache->Timers.emplace(label, timeStamp); } void Console::TimeEndCallback(const FunctionCallbackInfo& args) { if (!RuntimeConfig.LogToSystemConsole) { return; } Isolate* isolate = args.GetIsolate(); Local context = isolate->GetCurrentContext(); std::string label = "default"; Local labelString; if (args.Length() > 0 && args[0]->ToString(context).ToLocal(&labelString)) { label = tns::ToString(isolate, labelString); } std::shared_ptr cache = Caches::Get(isolate); auto itTimersMap = cache->Timers.find(label); if (itTimersMap == cache->Timers.end()) { std::string warning = std::string("No such label '" + label + "' for console.timeEnd()"); Log("%s", warning.c_str()); return; } auto nano = std::chrono::time_point_cast(std::chrono::system_clock::now()); double endTimeStamp = nano.time_since_epoch().count(); double startTimeStamp = itTimersMap->second; cache->Timers.erase(label); double diffMicroseconds = endTimeStamp - startTimeStamp; double diffMilliseconds = diffMicroseconds / 1000.0; std::stringstream ss; ss << "CONSOLE INFO " << label << ": " << std::fixed << std::setprecision(3) << diffMilliseconds << "ms" ; std::string msgToLog = ss.str(); SendToDevToolsFrontEnd(isolate, ConsoleAPIType::kTimeEnd, msgToLog); Log("%s", msgToLog.c_str()); } void Console::AttachLogFunction(Local context, Local console, const std::string name, v8::FunctionCallback callback) { Isolate* isolate = context->GetIsolate(); Local func; if (!Function::New(context, callback, tns::ToV8String(isolate, name), 0, ConstructorBehavior::kThrow).ToLocal(&func)) { tns::Assert(false, isolate); } Local logFuncName = tns::ToV8String(isolate, name); func->SetName(logFuncName); if (!console->CreateDataProperty(context, logFuncName, func).FromMaybe(false)) { tns::Assert(false, isolate); } } std::string Console::BuildStringFromArgs(const FunctionCallbackInfo& args, int startingIndex) { Isolate* isolate = args.GetIsolate(); Local context = isolate->GetCurrentContext(); int argLen = args.Length(); std::stringstream ss; if (argLen > 0) { for (int i = startingIndex; i < argLen; i++) { Local argString; argString = BuildStringFromArg(context, args[i]); // separate args with a space if (i != startingIndex) { ss << " "; } ss << tns::ToString(isolate, argString); } } else { ss << std::endl; } std::string stringResult = ss.str(); return stringResult; } const Local Console::BuildStringFromArg(Local context, const Local& val) { Isolate* isolate = context->GetIsolate(); Local argString; if (val->IsFunction()) { bool success = val->ToDetailString(context).ToLocal(&argString); tns::Assert(success, isolate); } else if (val->IsArray()) { Local cachedSelf = val; Local array = val->ToObject(context).ToLocalChecked(); Local arrayEntryKeys = array->GetPropertyNames(context).ToLocalChecked(); uint32_t arrayLength = arrayEntryKeys->Length(); argString = tns::ToV8String(isolate, "["); for (int i = 0; i < arrayLength; i++) { Local propertyName = arrayEntryKeys->Get(context, i).ToLocalChecked(); Local propertyValue = array->Get(context, propertyName).ToLocalChecked(); // avoid bottomless recursion with cyclic reference to the same array if (propertyValue->StrictEquals(cachedSelf)) { argString = v8::String::Concat(isolate, argString, tns::ToV8String(isolate, "[Circular]")); continue; } Local objectString = BuildStringFromArg(context, propertyValue); argString = v8::String::Concat(isolate, argString, objectString); if (i != arrayLength - 1) { argString = v8::String::Concat(isolate, argString, tns::ToV8String(isolate, ", ")); } } argString = v8::String::Concat(isolate, argString, tns::ToV8String(isolate, "]")); } else if (val->IsObject()) { Local obj = val.As(); argString = TransformJSObject(obj); } else { bool success = val->ToDetailString(isolate->GetCurrentContext()).ToLocal(&argString); tns::Assert(success, isolate); } return argString; } const Local Console::TransformJSObject(Local object) { Local context; bool success = object->GetCreationContext().ToLocal(&context); tns::Assert(success); Isolate* isolate = context->GetIsolate(); Local value; { TryCatch tc(isolate); bool success = object->ToString(context).ToLocal(&value); if (!success) { return tns::ToV8String(isolate, ""); } } Local objToString = value.As(); Local resultString; bool hasCustomToStringImplementation = tns::ToString(isolate, objToString).find("[object Object]") == std::string::npos; if (hasCustomToStringImplementation) { resultString = objToString; } else { resultString = tns::JsonStringifyObject(context, object); } return resultString; } v8_inspector::ConsoleAPIType Console::VerbosityToInspectorMethod(const std::string level) { if (level == "error") { return ConsoleAPIType::kError; } else if (level == "warn") { return ConsoleAPIType::kWarning; } else if (level == "info") { return ConsoleAPIType::kInfo; } else if (level == "trace") { return ConsoleAPIType::kTrace; } assert(level == "log"); return ConsoleAPIType::kLog; } void Console::SendToDevToolsFrontEnd(ConsoleAPIType method, const v8::FunctionCallbackInfo& args) { if (!inspector) { return; } std::vector> arg_vector; unsigned nargs = args.Length(); arg_vector.reserve(nargs); for (unsigned ix = 0; ix < nargs; ix++) arg_vector.push_back(args[ix]); inspector->consoleLog(args.GetIsolate(), method, arg_vector); } void Console::SendToDevToolsFrontEnd(v8::Isolate* isolate, ConsoleAPIType method, const std::string& msg) { if (!inspector) { return; } v8::Local v8str = v8::String::NewFromUtf8( isolate, msg.c_str(), v8::NewStringType::kNormal, -1).ToLocalChecked(); std::vector> args{v8str}; inspector->consoleLog(isolate, method, args); } v8_inspector::JsV8InspectorClient* Console::inspector = nullptr; }