Skip to content

Commit

Permalink
feat: make Deno capture unhandled exceptions and rejections and repor…
Browse files Browse the repository at this point in the history
…t them to the server (#34150)

Co-authored-by: Douglas Gubert <[email protected]>
  • Loading branch information
dionisio-bot[bot] and d-gubert authored Dec 9, 2024
1 parent bef79cd commit 58e1c80
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 1 deletion.
8 changes: 8 additions & 0 deletions .changeset/three-dragons-brush.md
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions packages/apps-engine/deno-runtime/error-handlers.ts
Original file line number Diff line number Diff line change
@@ -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);
}
3 changes: 3 additions & 0 deletions packages/apps-engine/deno-runtime/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -126,4 +127,6 @@ async function main() {
}
}

registerErrorListeners();

main();
2 changes: 2 additions & 0 deletions packages/apps-engine/src/definition/metadata/AppMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import debugFactory from 'debug';
import * as jsonrpc from 'jsonrpc-lite';

import { AppStatus } from '../../../definition/AppStatus';
import type { AppMethod } from '../../../definition/metadata';
import type { AppManager } from '../../AppManager';
import type { AppBridges } from '../../bridges';
import type { IParseAppPackageResult } from '../../compiler';
Expand Down Expand Up @@ -366,7 +367,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);
Expand Down Expand Up @@ -543,12 +544,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<void> {
const { id } = message.payload;

Expand Down

0 comments on commit 58e1c80

Please sign in to comment.