diff --git a/deno-runtime/error-handlers.ts b/deno-runtime/error-handlers.ts new file mode 100644 index 000000000..1e042e0f2 --- /dev/null +++ b/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/deno-runtime/main.ts b/deno-runtime/main.ts index 09be5258e..fa2822908 100644 --- a/deno-runtime/main.ts +++ b/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/package-lock.json b/package-lock.json index 541d72628..0327e5dc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps-engine", - "version": "1.43.2", + "version": "1.43.3-rc.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4397a093b..03b80d9d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rocket.chat/apps-engine", - "version": "1.43.2", + "version": "1.43.3-rc.0", "description": "The engine code for the Rocket.Chat Apps which manages, runs, translates, coordinates and all of that.", "main": "index", "typings": "index", diff --git a/src/definition/metadata/AppMethod.ts b/src/definition/metadata/AppMethod.ts index 007ef7d0c..65e4d5b8d 100644 --- a/src/definition/metadata/AppMethod.ts +++ b/src/definition/metadata/AppMethod.ts @@ -100,4 +100,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/src/server/ProxiedApp.ts b/src/server/ProxiedApp.ts index adc98e26c..1af91b3ce 100644 --- a/src/server/ProxiedApp.ts +++ b/src/server/ProxiedApp.ts @@ -54,9 +54,9 @@ export class ProxiedApp { let options; // Pre events need to be fast as they block the user - if (method.startsWith('checkPre') || method.startsWith('executePre')) { - options = { timeout: 1000 }; - } + // if (method.startsWith('checkPre') || method.startsWith('executePre')) { + // options = { timeout: 1000 }; + // } try { return await this.appRuntime.sendRequest({ method: `app:${method}`, params: args }, options); diff --git a/src/server/runtime/deno/AppsEngineDenoRuntime.ts b/src/server/runtime/deno/AppsEngineDenoRuntime.ts index 00c07aa8a..6f9e55ae3 100644 --- a/src/server/runtime/deno/AppsEngineDenoRuntime.ts +++ b/src/server/runtime/deno/AppsEngineDenoRuntime.ts @@ -13,6 +13,7 @@ import type { IParseAppPackageResult } from '../../compiler'; import { AppConsole, type ILoggerStorageEntry } from '../../logging'; import type { AppAccessorManager, AppApiManager } from '../../managers'; import { AppStatus } from '../../../definition/AppStatus'; +import type { AppMethod } from '../../../definition/metadata'; import { bundleLegacyApp } from './bundler'; import { ProcessMessenger } from './ProcessMessenger'; import { LivenessManager } from './LivenessManager'; @@ -52,6 +53,18 @@ const COMMAND_PONG = '_zPONG'; export const JSONRPC_METHOD_NOT_FOUND = -32601; +export function getRuntimeTimeout() { + const defaultTimeout = 30000; + const envValue = isFinite(process.env.APPS_ENGINE_RUNTIME_TIMEOUT as any) ? Number(process.env.APPS_ENGINE_RUNTIME_TIMEOUT) : defaultTimeout; + + if (envValue < 0) { + console.log('Environment variable APPS_ENGINE_RUNTIME_TIMEOUT has a negative value, ignoring...'); + return defaultTimeout; + } + + return envValue; +} + export function isValidOrigin(accessor: string): accessor is typeof ALLOWED_ACCESSOR_METHODS[number] { return ALLOWED_ACCESSOR_METHODS.includes(accessor as any); } @@ -87,7 +100,7 @@ export class DenoRuntimeSubprocessController extends EventEmitter { private readonly debug: debug.Debugger; private readonly options = { - timeout: 10000, + timeout: getRuntimeTimeout(), }; private readonly accessors: AppAccessorManager; @@ -535,12 +548,29 @@ 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;