From ce7024af36fcde97b1da5b2731f6edc4a4c236b8 Mon Sep 17 00:00:00 2001 From: Douglas Gubert Date: Tue, 19 Nov 2024 23:13:01 -0300 Subject: [PATCH] feat: make Deno capture unhandled exceptions and rejections and report them to the server (#33997) --- .changeset/three-dragons-brush.md | 8 +++++ .../deno-runtime/error-handlers.ts | 33 +++++++++++++++++++ packages/apps-engine/deno-runtime/main.ts | 3 ++ .../src/definition/metadata/AppMethod.ts | 2 ++ .../runtime/deno/AppsEngineDenoRuntime.ts | 19 ++++++++++- 5 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 .changeset/three-dragons-brush.md create mode 100644 packages/apps-engine/deno-runtime/error-handlers.ts diff --git a/.changeset/three-dragons-brush.md b/.changeset/three-dragons-brush.md new file mode 100644 index 000000000000..d80c4dc83306 --- /dev/null +++ b/.changeset/three-dragons-brush.md @@ -0,0 +1,8 @@ +--- +'@rocket.chat/apps-engine': minor +'@rocket.chat/livechat': minor +'@rocket.chat/i18n': minor +'@rocket.chat/meteor': minor +--- + +Prevent apps' subprocesses from crashing on unhandled rejections or uncaught exceptions diff --git a/packages/apps-engine/deno-runtime/error-handlers.ts b/packages/apps-engine/deno-runtime/error-handlers.ts new file mode 100644 index 000000000000..1e042e0f2c62 --- /dev/null +++ b/packages/apps-engine/deno-runtime/error-handlers.ts @@ -0,0 +1,33 @@ +import * as Messenger from './lib/messenger.ts'; + +export function unhandledRejectionListener(event: PromiseRejectionEvent) { + event.preventDefault(); + + const { type, reason } = event; + + Messenger.sendNotification({ + method: 'unhandledRejection', + params: [ + { + type, + reason: reason instanceof Error ? reason.message : reason, + timestamp: new Date(), + }, + ], + }); +} + +export function unhandledExceptionListener(event: ErrorEvent) { + event.preventDefault(); + + const { type, message, filename, lineno, colno } = event; + Messenger.sendNotification({ + method: 'uncaughtException', + params: [{ type, message, filename, lineno, colno }], + }); +} + +export default function registerErrorListeners() { + addEventListener('unhandledrejection', unhandledRejectionListener); + addEventListener('error', unhandledExceptionListener); +} diff --git a/packages/apps-engine/deno-runtime/main.ts b/packages/apps-engine/deno-runtime/main.ts index 09be5258ecd0..fa2822908954 100644 --- a/packages/apps-engine/deno-runtime/main.ts +++ b/packages/apps-engine/deno-runtime/main.ts @@ -21,6 +21,7 @@ import videoConferenceHandler from './handlers/videoconference-handler.ts'; import apiHandler from './handlers/api-handler.ts'; import handleApp from './handlers/app/handler.ts'; import handleScheduler from './handlers/scheduler-handler.ts'; +import registerErrorListeners from './error-handlers.ts'; type Handlers = { app: typeof handleApp; @@ -126,4 +127,6 @@ async function main() { } } +registerErrorListeners(); + main(); diff --git a/packages/apps-engine/src/definition/metadata/AppMethod.ts b/packages/apps-engine/src/definition/metadata/AppMethod.ts index 8ec07f53e001..b7fcf306f6aa 100644 --- a/packages/apps-engine/src/definition/metadata/AppMethod.ts +++ b/packages/apps-engine/src/definition/metadata/AppMethod.ts @@ -101,4 +101,6 @@ export enum AppMethod { EXECUTE_POST_USER_STATUS_CHANGED = 'executePostUserStatusChanged', // Runtime specific methods RUNTIME_RESTART = 'runtime:restart', + RUNTIME_UNCAUGHT_EXCEPTION = 'runtime:uncaughtException', + RUNTIME_UNHANDLED_REJECTION = 'runtime:unhandledRejection', } diff --git a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts b/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts index d462098d8e65..458799286c83 100644 --- a/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts +++ b/packages/apps-engine/src/server/runtime/deno/AppsEngineDenoRuntime.ts @@ -16,6 +16,7 @@ import type { IParseAppPackageResult } from '../../compiler'; import { AppConsole, type ILoggerStorageEntry } from '../../logging'; import type { AppAccessorManager, AppApiManager } from '../../managers'; import type { AppLogStorage, IAppStorageItem } from '../../storage'; +import { AppMethod } from '../../../definition/metadata'; const baseDebug = debugFactory('appsEngine:runtime:deno'); @@ -367,7 +368,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter { this.deno.stderr.on('data', this.parseError.bind(this)); this.deno.on('error', (err) => { this.state = 'invalid'; - console.error('Failed to startup Deno subprocess', err); + console.error(`Failed to startup Deno subprocess for app ${this.getAppId()}`, err); }); this.once('ready', this.onReady.bind(this)); this.parseStdout(this.deno.stdout); @@ -544,12 +545,28 @@ export class DenoRuntimeSubprocessController extends EventEmitter { case 'log': console.log('SUBPROCESS LOG', message); break; + case 'unhandledRejection': + case 'uncaughtException': + await this.logUnhandledError(`runtime:${method}`, message); + break; default: console.warn('Unrecognized method from sub process'); break; } } + private async logUnhandledError( + method: `${AppMethod.RUNTIME_UNCAUGHT_EXCEPTION | AppMethod.RUNTIME_UNHANDLED_REJECTION}`, + message: jsonrpc.IParsedObjectRequest | jsonrpc.IParsedObjectNotification, + ) { + this.debug('Unhandled error of type "%s" caught in subprocess', method); + + const logger = new AppConsole(method); + logger.error(message.payload); + + await this.logStorage.storeEntries(AppConsole.toStorageEntry(this.getAppId(), logger)); + } + private async handleResultMessage(message: jsonrpc.IParsedObjectError | jsonrpc.IParsedObjectSuccess): Promise { const { id } = message.payload;