|
4 | 4 |
|
5 | 5 | #include <logging/logger.h> |
6 | 6 |
|
| 7 | +#include <algorithm> |
7 | 8 | #include <filesystem> |
8 | 9 |
|
9 | 10 |
|
@@ -175,13 +176,51 @@ namespace Framework::Scripting { |
175 | 176 | v8::HandleScope handle_scope(_isolate); |
176 | 177 | v8::Context::Scope context_scope(_setup->context()); |
177 | 178 |
|
178 | | - // Load Node.js internals with require setup |
| 179 | + // Load Node.js internals with require setup and uncaught exception/rejection |
| 180 | + // handlers. These prevent async errors (timers, promises) from crashing |
| 181 | + // the host process. |
| 182 | + // |
| 183 | + // setUncaughtExceptionCaptureCallback is preferred over process.on('uncaughtException') |
| 184 | + // because it: |
| 185 | + // - Cannot be removed by user scripts (process.removeAllListeners) |
| 186 | + // - Prevents abort even with --abort-on-uncaught-exception |
| 187 | + // - Is designed for embedder use cases |
| 188 | + // |
| 189 | + // For unhandled promise rejections, process.on('unhandledRejection') is used |
| 190 | + // as there is no capture callback equivalent. |
| 191 | + // |
| 192 | + // Both route to __fw_handleUncaughtError if installed later via |
| 193 | + // InstallUncaughtExceptionHandler(), otherwise log to stderr. |
179 | 194 | v8::MaybeLocal<v8::Value> loadResult = node::LoadEnvironment( |
180 | 195 | _env, |
181 | 196 | "const publicRequire = require('node:module').createRequire(process.cwd() + '/');" |
182 | 197 | "globalThis.require = publicRequire;" |
183 | 198 | "globalThis.Framework = {};" |
184 | 199 | "globalThis.Core = {};" |
| 200 | + "process.setUncaughtExceptionCaptureCallback((err) => {" |
| 201 | + " try {" |
| 202 | + " const msg = err instanceof Error ? (err.stack || err.message) : String(err);" |
| 203 | + " if (typeof globalThis.__fw_handleUncaughtError === 'function') {" |
| 204 | + " globalThis.__fw_handleUncaughtError(msg, 'uncaughtException');" |
| 205 | + " } else {" |
| 206 | + " console.error('[uncaughtException]', msg);" |
| 207 | + " }" |
| 208 | + " } catch(e) {" |
| 209 | + " console.error('Error in uncaught exception handler:', e);" |
| 210 | + " }" |
| 211 | + "});" |
| 212 | + "process.on('unhandledRejection', (reason) => {" |
| 213 | + " try {" |
| 214 | + " const msg = reason instanceof Error ? (reason.stack || reason.message) : String(reason);" |
| 215 | + " if (typeof globalThis.__fw_handleUncaughtError === 'function') {" |
| 216 | + " globalThis.__fw_handleUncaughtError(msg, 'unhandledRejection');" |
| 217 | + " } else {" |
| 218 | + " console.error('[unhandledRejection]', msg);" |
| 219 | + " }" |
| 220 | + " } catch(e) {" |
| 221 | + " console.error('Error in unhandled rejection handler:', e);" |
| 222 | + " }" |
| 223 | + "});" |
185 | 224 | ); |
186 | 225 |
|
187 | 226 | if (loadResult.IsEmpty()) { |
@@ -280,6 +319,78 @@ namespace Framework::Scripting { |
280 | 319 | return v8::Local<v8::Context>(); |
281 | 320 | } |
282 | 321 |
|
| 322 | + void NodeEngine::InstallUncaughtExceptionHandler(const std::string &resourcesPath) { |
| 323 | + // Store canonical resources path for extracting resource names from error stacks |
| 324 | + std::error_code ec; |
| 325 | + auto canonicalPath = std::filesystem::weakly_canonical(resourcesPath, ec); |
| 326 | + _resourcesPath = ec ? resourcesPath : canonicalPath.string(); |
| 327 | + |
| 328 | + // Create C++ handler function accessible from JS |
| 329 | + v8::Local<v8::Context> context = _setup->context(); |
| 330 | + v8::Local<v8::External> data = v8::External::New(_isolate, this); |
| 331 | + v8::Local<v8::FunctionTemplate> tmpl = v8::FunctionTemplate::New( |
| 332 | + _isolate, OnUncaughtError, data); |
| 333 | + v8::Local<v8::Function> fn = tmpl->GetFunction(context).ToLocalChecked(); |
| 334 | + |
| 335 | + v8::Local<v8::String> key = v8::String::NewFromUtf8( |
| 336 | + _isolate, "__fw_handleUncaughtError").ToLocalChecked(); |
| 337 | + context->Global()->Set(context, key, fn).Check(); |
| 338 | + } |
| 339 | + |
| 340 | + void NodeEngine::OnUncaughtError(const v8::FunctionCallbackInfo<v8::Value> &info) { |
| 341 | + v8::Isolate *isolate = info.GetIsolate(); |
| 342 | + auto *engine = static_cast<NodeEngine *>( |
| 343 | + v8::Local<v8::External>::Cast(info.Data())->Value()); |
| 344 | + |
| 345 | + std::string errorMsg = "Unknown error"; |
| 346 | + std::string origin = "uncaughtException"; |
| 347 | + |
| 348 | + if (info.Length() > 0) { |
| 349 | + v8::String::Utf8Value msg(isolate, info[0]); |
| 350 | + if (*msg) errorMsg = *msg; |
| 351 | + } |
| 352 | + if (info.Length() > 1) { |
| 353 | + v8::String::Utf8Value orig(isolate, info[1]); |
| 354 | + if (*orig) origin = *orig; |
| 355 | + } |
| 356 | + |
| 357 | + // Try to extract resource name from the error stack trace by matching |
| 358 | + // file paths against the configured resources directory |
| 359 | + std::string resourceName; |
| 360 | + if (!engine->_resourcesPath.empty()) { |
| 361 | + // Normalize path separators for cross-platform matching |
| 362 | + std::string normalizedError = errorMsg; |
| 363 | + std::string normalizedResPath = engine->_resourcesPath; |
| 364 | + std::replace(normalizedError.begin(), normalizedError.end(), '\\', '/'); |
| 365 | + std::replace(normalizedResPath.begin(), normalizedResPath.end(), '\\', '/'); |
| 366 | + |
| 367 | + if (!normalizedResPath.empty() && normalizedResPath.back() != '/') { |
| 368 | + normalizedResPath += '/'; |
| 369 | + } |
| 370 | + |
| 371 | + size_t pos = normalizedError.find(normalizedResPath); |
| 372 | + if (pos != std::string::npos) { |
| 373 | + size_t nameStart = pos + normalizedResPath.size(); |
| 374 | + size_t nameEnd = normalizedError.find('/', nameStart); |
| 375 | + if (nameEnd != std::string::npos) { |
| 376 | + resourceName = normalizedError.substr(nameStart, nameEnd - nameStart); |
| 377 | + } |
| 378 | + } |
| 379 | + } |
| 380 | + |
| 381 | + // Queue for processing outside of Tick() |
| 382 | + engine->_pendingErrors.push_back({ |
| 383 | + resourceName.empty() ? "unknown" : resourceName, |
| 384 | + "[" + origin + "] " + errorMsg |
| 385 | + }); |
| 386 | + } |
| 387 | + |
| 388 | + std::vector<NodeEngine::PendingUncaughtError> NodeEngine::DrainPendingErrors() { |
| 389 | + std::vector<PendingUncaughtError> errors; |
| 390 | + errors.swap(_pendingErrors); |
| 391 | + return errors; |
| 392 | + } |
| 393 | + |
283 | 394 | bool NodeEngine::ApplySandbox() { |
284 | 395 | // This function disables dangerous Node.js APIs for client-side sandboxing. |
285 | 396 | // We override require() to block dangerous modules and remove dangerous |
|
0 commit comments