487 lines
16 KiB
C++
487 lines
16 KiB
C++
|
/*
|
||
|
* 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 <jsinspector-modern/RuntimeTarget.h>
|
||
|
|
||
|
#include <concepts>
|
||
|
#include <deque>
|
||
|
#include <string>
|
||
|
|
||
|
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<std::string, int> countMap;
|
||
|
|
||
|
/**
|
||
|
* https://console.spec.whatwg.org/#timing
|
||
|
*/
|
||
|
std::unordered_map<std::string, double> 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::duration<double, std::milli>>(
|
||
|
std::chrono::system_clock::now().time_since_epoch())
|
||
|
.count();
|
||
|
}
|
||
|
|
||
|
template <typename T>
|
||
|
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> /*stackTrace*/>;
|
||
|
|
||
|
template <typename T>
|
||
|
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> 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<jsi::Value> 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> 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<jsi::Value> 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> 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<jsi::Value> 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> 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<jsi::Value> 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<jsi::Value> 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> 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<jsi::Value> 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<jsi::Value> 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> stackTrace) {
|
||
|
if (count >= 1 && toBoolean(runtime, args[0])) {
|
||
|
return;
|
||
|
}
|
||
|
std::deque<jsi::Value> 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<jsi::Value>(
|
||
|
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> stackTrace) { \
|
||
|
std::vector<jsi::Value> 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<jsi::Object> originalConsole;
|
||
|
if (originalConsoleVal.isObject()) {
|
||
|
originalConsole =
|
||
|
std::make_shared<jsi::Object>(originalConsoleVal.getObject(runtime));
|
||
|
consolePrototype = std::move(originalConsoleVal);
|
||
|
} else {
|
||
|
consolePrototype = jsi::Object(runtime);
|
||
|
}
|
||
|
auto console = objectCreate(runtime, std::move(consolePrototype));
|
||
|
auto state = std::make_shared<ConsoleState>();
|
||
|
|
||
|
/**
|
||
|
* 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<RuntimeTargetDelegate&> 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
|