Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion Polyfills/Console/Include/Babylon/Polyfills/Console.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@ namespace Babylon::Polyfills::Console
Error,
};

using CallbackT = std::function<void BABYLON_API (const char*, LogLevel)>;
/**
* Called for each `console.log` / `console.warn` / `console.error` invocation.
*
* @param message Formatted message produced by joining the call arguments (like browsers do).
* @param logLevel Importance of the message.
* @param jsStack For `LogLevel::Error`, the JavaScript callstack captured at the
* `console.error` call site (raw `Error.stack` string -- the format is engine-
* defined and typically starts with the literal `Error\n`). Empty string for
* `Log` and `Warn` (capturing a stack on every `console.log` is too expensive).
Comment thread
bkaradzic-microsoft marked this conversation as resolved.
Outdated
*/
using CallbackT = std::function<void BABYLON_API (const char* message, LogLevel logLevel, const char* jsStack)>;
Comment thread
bkaradzic-microsoft marked this conversation as resolved.
Outdated

void BABYLON_API Initialize(Napi::Env env, CallbackT callback);
}
7 changes: 5 additions & 2 deletions Polyfills/Console/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ Currently supports:
* `warn()`
* `error()`

When initializing, you should provide a callback which takes a message and a log level and outputs the message in whatever way you like. For example, you could initialize it like so:
When initializing, you should provide a callback which takes a message, a log level, and a JS callstack and outputs the message in whatever way you like. The callstack is captured at the call site for `error()` calls (raw `Error.stack`, engine-defined format -- typically starting with a literal `Error\n` line); it is an empty string for `log()` and `warn()` calls (capturing a stack on every `console.log` would be too costly). For example, you could initialize it like so:
```c++
Babylon::Polyfills::Console::Initialize(env, [](const char* message, auto) {
Babylon::Polyfills::Console::Initialize(env, [](const char* message, auto, const char* jsStack) {
fprintf(stdout, "%s", message);
if (jsStack[0] != '\0') {
fprintf(stdout, "\n%s", jsStack);
}
Comment thread
bkaradzic-microsoft marked this conversation as resolved.
Outdated
fflush(stdout);
});
29 changes: 28 additions & 1 deletion Polyfills/Console/Source/Console.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,34 @@ namespace
}
}

callback(ss.str().c_str(), logLevel);
std::string jsStack{};
if (logLevel == Babylon::Polyfills::Console::LogLevel::Error)
{
// Capture the JS callstack at the `console.error` call site. We construct a JS `Error`
// object via N-API which fills its `stack` property using the engine's current JS frames;
// the topmost real frame is the user's `console.error(...)` callsite (the Napi-wrapped
// function we registered does not add a visible JS frame on V8/JSC/Chakra). Best effort;
// any failure leaves `jsStack` empty.
try
{
Napi::Env env = info.Env();
Napi::Value errorCtorValue = env.Global().Get("Error");
if (errorCtorValue.IsFunction())
{
Napi::Object errObj = errorCtorValue.As<Napi::Function>().New({}).As<Napi::Object>();
Napi::Value stackValue = errObj.Get("stack");
if (stackValue.IsString())
{
jsStack = stackValue.As<Napi::String>().Utf8Value();
}
}
}
catch (...)
{
}
}
Comment thread
bkaradzic-microsoft marked this conversation as resolved.
Outdated

callback(ss.str().c_str(), logLevel, jsStack.c_str());
}

void AddMethod(Napi::Object& console, const char* functionName, Babylon::Polyfills::Console::LogLevel logLevel, Babylon::Polyfills::Console::CallbackT callback)
Expand Down
45 changes: 42 additions & 3 deletions Tests/UnitTests/Shared/Shared.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,13 @@ TEST(JavaScript, All)
Babylon::AppRuntime runtime{options};

runtime.Dispatch([&exitCodePromise](Napi::Env env) mutable {
Babylon::Polyfills::Console::Initialize(env, [](const char* message, Babylon::Polyfills::Console::LogLevel logLevel) {
std::cout << "[" << EnumToString(logLevel) << "] " << message << std::endl;
Babylon::Polyfills::Console::Initialize(env, [](const char* message, Babylon::Polyfills::Console::LogLevel logLevel, const char* jsStack) {
std::cout << "[" << EnumToString(logLevel) << "] " << message;
if (logLevel == Babylon::Polyfills::Console::LogLevel::Error && jsStack != nullptr && jsStack[0] != '\0')
{
std::cout << std::endl << jsStack;
}
std::cout << std::endl;
std::cout.flush();
});

Expand Down Expand Up @@ -100,7 +105,7 @@ TEST(Console, Log)
Babylon::AppRuntime runtime{};

runtime.Dispatch([](Napi::Env env) mutable {
Babylon::Polyfills::Console::Initialize(env, [](const char* message, Babylon::Polyfills::Console::LogLevel logLevel) {
Babylon::Polyfills::Console::Initialize(env, [](const char* message, Babylon::Polyfills::Console::LogLevel logLevel, const char* /*jsStack*/) {
const char* test = "foo bar";
if (strcmp(message, test) != 0)
{
Expand All @@ -123,6 +128,40 @@ TEST(Console, Log)
done.get_future().get();
}

TEST(Console, ErrorProvidesJsStack)
{
// Regression: console.error must invoke the callback with a non-empty jsStack containing
// the JS call site. console.log / console.warn must be empty (stack capture is skipped for
// hot paths).
Babylon::AppRuntime runtime{};

std::promise<std::string> errorStackPromise;
std::promise<std::string> logStackPromise;

runtime.Dispatch([&errorStackPromise, &logStackPromise](Napi::Env env) mutable {
Babylon::Polyfills::Console::Initialize(env, [&](const char* /*message*/, Babylon::Polyfills::Console::LogLevel logLevel, const char* jsStack) {
if (logLevel == Babylon::Polyfills::Console::LogLevel::Error)
{
errorStackPromise.set_value(jsStack ? jsStack : "");
}
else if (logLevel == Babylon::Polyfills::Console::LogLevel::Log)
{
logStackPromise.set_value(jsStack ? jsStack : "");
}
});
});

Babylon::ScriptLoader loader{runtime};
loader.Eval("console.log('log message');", "");
loader.Eval("function inner() { console.error('error message'); } inner();", "");

std::string errorStack = errorStackPromise.get_future().get();
std::string logStack = logStackPromise.get_future().get();

Comment thread
bkaradzic-microsoft marked this conversation as resolved.
EXPECT_TRUE(logStack.empty()) << "Log path should not capture a stack; got: " << logStack;
EXPECT_FALSE(errorStack.empty()) << "Error path must provide a non-empty jsStack";
}

TEST(AppRuntime, DestroyDoesNotDeadlock)
{
// Regression test verifying AppRuntime destruction doesn't deadlock.
Expand Down
Loading