/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include #include #include #include using namespace facebook::jsi; using namespace std::string_literals; namespace facebook::react::jsinspector_modern { namespace { struct ConsoleState { /** * https://console.spec.whatwg.org/#counting */ std::unordered_map countMap; /** * https://console.spec.whatwg.org/#timing */ std::unordered_map timerTable; ConsoleState() = default; ConsoleState(const ConsoleState&) = delete; ConsoleState& operator=(const ConsoleState&) = delete; ConsoleState(ConsoleState&&) = delete; ConsoleState& operator=(ConsoleState&&) = delete; ~ConsoleState() = default; }; /** * JS `Object.create()` */ jsi::Object objectCreate(jsi::Runtime& runtime, jsi::Value prototype) { auto objectGlobal = runtime.global().getPropertyAsObject(runtime, "Object"); auto createFn = objectGlobal.getPropertyAsFunction(runtime, "create"); return createFn.callWithThis(runtime, objectGlobal, prototype) .getObject(runtime); } bool toBoolean(jsi::Runtime& runtime, const jsi::Value& val) { // Based on Operations.cpp:toBoolean in the Hermes VM. if (val.isUndefined() || val.isNull()) { return false; } if (val.isBool()) { return val.getBool(); } if (val.isNumber()) { double m = val.getNumber(); return m != 0 && !std::isnan(m); } if (val.isSymbol() || val.isObject()) { return true; } if (val.isString()) { std::string s = val.getString(runtime).utf8(runtime); return !s.empty(); } assert(false && "All cases should be covered"); return false; } /** * Get the current time in milliseconds as a double. */ double getTimestampMs() { return std::chrono::duration_cast>( std::chrono::system_clock::now().time_since_epoch()) .count(); } template concept ConsoleMethodBody = std::invocable< T, jsi::Runtime& /*runtime*/, const jsi::Value* /*args*/, size_t /*count*/, RuntimeTargetDelegate& /*runtimeTargetDelegate*/, ConsoleState& /*state*/, double /*timestampMs*/, std::unique_ptr /*stackTrace*/>; template concept CallableAsHostFunction = std::invocable< T, jsi::Runtime& /*runtime*/, const jsi::Value& /*thisVal*/, const jsi::Value* /*args*/, size_t /*count*/>; void consoleCount( jsi::Runtime& runtime, const jsi::Value* args, size_t count, RuntimeTargetDelegate& runtimeTargetDelegate, ConsoleState& state, double timestampMs, std::unique_ptr stackTrace) { std::string label = "default"; if (count > 0 && !args[0].isUndefined()) { label = args[0].toString(runtime).utf8(runtime); } auto it = state.countMap.find(label); if (it == state.countMap.end()) { it = state.countMap.insert({label, 1}).first; } else { it->second++; } std::vector vec; vec.emplace_back(jsi::String::createFromUtf8( runtime, label + ": "s + std::to_string(it->second))); runtimeTargetDelegate.addConsoleMessage( runtime, {timestampMs, ConsoleAPIType::kCount, std::move(vec), std::move(stackTrace)}); } void consoleCountReset( jsi::Runtime& runtime, const jsi::Value* args, size_t count, RuntimeTargetDelegate& runtimeTargetDelegate, ConsoleState& state, double timestampMs, std::unique_ptr stackTrace) { std::string label = "default"; if (count > 0 && !args[0].isUndefined()) { label = args[0].toString(runtime).utf8(runtime); } auto it = state.countMap.find(label); if (it == state.countMap.end()) { std::vector vec; vec.emplace_back(jsi::String::createFromUtf8( runtime, "Count for '"s + label + "' does not exist")); runtimeTargetDelegate.addConsoleMessage( runtime, {timestampMs, ConsoleAPIType::kWarning, std::move(vec), std::move(stackTrace)}); } else { it->second = 0; } } void consoleTime( jsi::Runtime& runtime, const jsi::Value* args, size_t count, RuntimeTargetDelegate& runtimeTargetDelegate, ConsoleState& state, double timestampMs, std::unique_ptr stackTrace) { std::string label = "default"; if (count > 0 && !args[0].isUndefined()) { label = args[0].toString(runtime).utf8(runtime); } auto it = state.timerTable.find(label); if (it == state.timerTable.end()) { state.timerTable.insert({label, timestampMs}); } else { std::vector vec; vec.emplace_back(jsi::String::createFromUtf8( runtime, "Timer '"s + label + "' already exists")); runtimeTargetDelegate.addConsoleMessage( runtime, {timestampMs, ConsoleAPIType::kWarning, std::move(vec), std::move(stackTrace)}); } } void consoleTimeEnd( jsi::Runtime& runtime, const jsi::Value* args, size_t count, RuntimeTargetDelegate& runtimeTargetDelegate, ConsoleState& state, double timestampMs, std::unique_ptr stackTrace) { std::string label = "default"; if (count > 0 && !args[0].isUndefined()) { label = args[0].toString(runtime).utf8(runtime); } auto it = state.timerTable.find(label); if (it == state.timerTable.end()) { std::vector vec; vec.emplace_back(jsi::String::createFromUtf8( runtime, "Timer '"s + label + "' does not exist")); runtimeTargetDelegate.addConsoleMessage( runtime, {timestampMs, ConsoleAPIType::kWarning, std::move(vec), std::move(stackTrace)}); } else { std::vector vec; vec.emplace_back(jsi::String::createFromUtf8( runtime, label + ": "s + std::to_string(timestampMs - it->second) + " ms")); state.timerTable.erase(it); runtimeTargetDelegate.addConsoleMessage( runtime, {timestampMs, ConsoleAPIType::kTimeEnd, std::move(vec), std::move(stackTrace)}); } } void consoleTimeLog( jsi::Runtime& runtime, const jsi::Value* args, size_t count, RuntimeTargetDelegate& runtimeTargetDelegate, ConsoleState& state, double timestampMs, std::unique_ptr stackTrace) { std::string label = "default"; if (count > 0 && !args[0].isUndefined()) { label = args[0].toString(runtime).utf8(runtime); } auto it = state.timerTable.find(label); if (it == state.timerTable.end()) { std::vector vec; vec.emplace_back(jsi::String::createFromUtf8( runtime, "Timer '"s + label + "' does not exist")); runtimeTargetDelegate.addConsoleMessage( runtime, {timestampMs, ConsoleAPIType::kWarning, std::move(vec), std::move(stackTrace)}); } else { std::vector vec; vec.emplace_back(jsi::String::createFromUtf8( runtime, label + ": "s + std::to_string(timestampMs - it->second) + " ms")); if (count > 1) { for (size_t i = 1; i != count; ++i) { vec.emplace_back(runtime, args[i]); } } runtimeTargetDelegate.addConsoleMessage( runtime, {timestampMs, ConsoleAPIType::kLog, std::move(vec), std::move(stackTrace)}); } } void consoleAssert( jsi::Runtime& runtime, const jsi::Value* args, size_t count, RuntimeTargetDelegate& runtimeTargetDelegate, ConsoleState& /*state*/, double timestampMs, std::unique_ptr stackTrace) { if (count >= 1 && toBoolean(runtime, args[0])) { return; } std::deque data; if (count > 1) { for (size_t i = 1; i != count; ++i) { data.emplace_back(runtime, args[i]); } } if (data.empty()) { data.emplace_back(jsi::String::createFromUtf8(runtime, "Assertion failed")); } else if (data.front().isString()) { data.front() = jsi::String::createFromUtf8( runtime, "Assertion failed: "s + data.front().asString(runtime).utf8(runtime)); } else { data.emplace_front( jsi::String::createFromUtf8(runtime, "Assertion failed")); } runtimeTargetDelegate.addConsoleMessage( runtime, {timestampMs, ConsoleAPIType::kAssert, std::vector( make_move_iterator(data.begin()), make_move_iterator(data.end())), std::move(stackTrace)}); } /** * `console` methods that have no behaviour other than emitting a * Runtime.consoleAPICalled message. */ #define FORWARDING_CONSOLE_METHOD(name, type) \ void console_##name( \ jsi::Runtime& runtime, \ const jsi::Value* args, \ size_t count, \ RuntimeTargetDelegate& runtimeTargetDelegate, \ ConsoleState& state, \ double timestampMs, \ std::unique_ptr stackTrace) { \ std::vector argsVec; \ for (size_t i = 0; i != count; ++i) { \ argsVec.emplace_back(runtime, args[i]); \ } \ runtimeTargetDelegate.addConsoleMessage( \ runtime, \ {timestampMs, type, std::move(argsVec), std::move(stackTrace)}); \ } #include "ForwardingConsoleMethods.def" #undef FORWARDING_CONSOLE_METHOD } // namespace void RuntimeTarget::installConsoleHandler() { auto delegateSupportsConsole = delegate_.supportsConsole(); jsExecutor_([selfWeak = weak_from_this(), selfExecutor = executorFromThis(), delegateSupportsConsole](jsi::Runtime& runtime) { jsi::Value consolePrototype = jsi::Value::null(); auto originalConsoleVal = runtime.global().getProperty(runtime, "console"); std::shared_ptr originalConsole; if (originalConsoleVal.isObject()) { originalConsole = std::make_shared(originalConsoleVal.getObject(runtime)); consolePrototype = std::move(originalConsoleVal); } else { consolePrototype = jsi::Object(runtime); } auto console = objectCreate(runtime, std::move(consolePrototype)); auto state = std::make_shared(); /** * An executor that runs synchronously and provides a safe reference to our * RuntimeTargetDelegate for use on the JS thread. * \see RuntimeTargetDelegate for information on which methods are safe to * call on the JS thread. * \warning The callback will not run if the RuntimeTarget has been * destroyed. */ auto delegateExecutorSync = [selfWeak, selfExecutor](std::invocable auto func) { if (auto self = selfWeak.lock()) { // Q: Why is it safe to use self->delegate_ here? // A: Because the caller of InspectorTarget::registerRuntime // is explicitly required to guarantee that the delegate not // only outlives the target, but also outlives all JS code // execution that occurs on the JS thread. func(self->delegate_); // To ensure we never destroy `self` on the JS thread, send // our shared_ptr back to the inspector thread. selfExecutor([self = std::move(self)](auto&) { (void)self; }); } }; /** * Call innerFn and forward any arguments to the original console method * named methodName, if possible. */ auto forwardToOriginalConsole = [originalConsole]( const char* methodName, CallableAsHostFunction auto innerFn) { return [originalConsole, innerFn = std::move(innerFn), methodName]( jsi::Runtime& runtime, const jsi::Value& thisVal, const jsi::Value* args, size_t count) { jsi::Value retVal = innerFn(runtime, thisVal, args, count); if (originalConsole) { auto val = originalConsole->getProperty(runtime, methodName); if (val.isObject()) { auto obj = val.getObject(runtime); if (obj.isFunction(runtime)) { auto func = obj.getFunction(runtime); func.callWithThis(runtime, *originalConsole, args, count); } } } return retVal; }; }; /** * Install a console method with the given name and body. The body receives * the usual JSI host function parameters plus a ConsoleState reference, a * reference to the RuntimeTargetDelegate for sending messages to the * client, and the timestamp of the call. After the body runs (or is skipped * due to RuntimeTarget having been destroyed), the method of the same name * is also called on originalConsole (if it exists). */ auto installConsoleMethod = [&](const char* methodName, ConsoleMethodBody auto body) { console.setProperty( runtime, methodName, jsi::Function::createFromHostFunction( runtime, jsi::PropNameID::forAscii(runtime, methodName), 0, forwardToOriginalConsole( methodName, [body = std::move(body), state, delegateExecutorSync]( jsi::Runtime& runtime, const jsi::Value& /*thisVal*/, const jsi::Value* args, size_t count) { auto timestampMs = getTimestampMs(); delegateExecutorSync([&](auto& runtimeTargetDelegate) { auto stackTrace = runtimeTargetDelegate.captureStackTrace( runtime, /* framesToSkip */ 1); body( runtime, args, count, runtimeTargetDelegate, *state, timestampMs, std::move(stackTrace)); }); return jsi::Value::undefined(); }))); }; /** * console.count */ installConsoleMethod("count", consoleCount); /** * console.countReset */ installConsoleMethod("countReset", consoleCountReset); /** * console.time */ installConsoleMethod("time", consoleTime); /** * console.timeEnd */ installConsoleMethod("timeEnd", consoleTimeEnd); /** * console.timeLog */ installConsoleMethod("timeLog", consoleTimeLog); /** * console.assert */ installConsoleMethod("assert", consoleAssert); // Install forwarding console methods. #define FORWARDING_CONSOLE_METHOD(name, type) \ installConsoleMethod(#name, console_##name); #include "ForwardingConsoleMethods.def" #undef FORWARDING_CONSOLE_METHOD runtime.global().setProperty(runtime, "console", console); if (delegateSupportsConsole) { // NOTE: If the delegate doesn't report console support, we'll still // install the console handler for consistency of the runtime environment, // but not claim that it has full console support. runtime.global().setProperty( runtime, "__FUSEBOX_HAS_FULL_CONSOLE_SUPPORT__", true); } }); } } // namespace facebook::react::jsinspector_modern