Skip to content
This repository was archived by the owner on Nov 5, 2025. It is now read-only.

Commit cc757c9

Browse files
Merge pull request #512 from RocketChat/improvement/vm2-poc
[NEW] Add runtime provider management and support VM2
2 parents 5fdd30d + 48a8228 commit cc757c9

26 files changed

Lines changed: 569 additions & 228 deletions

package-lock.json

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@
9191
"lodash.clonedeep": "^4.5.0",
9292
"semver": "^5.7.1",
9393
"stack-trace": "0.0.10",
94-
"uuid": "^3.4.0"
94+
"uuid": "^3.4.0",
95+
"vm2": "^3.9.11"
9596
},
9697
"nyc": {
9798
"include": [

src/definition/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@rocket.chat/apps-ts-definition",
3-
"version": "1.33.0-alpha",
3+
"version": "1.35.0-alpha",
44
"description": "Contains the TypeScript definitions for the Rocket.Chat Applications.",
55
"main": "index.js",
66
"typings": "index",

src/server/AppManager.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { IMarketplaceInfo } from './marketplace';
2525
import { DisabledApp } from './misc/DisabledApp';
2626
import { defaultPermissions } from './permissions/AppPermissions';
2727
import { ProxiedApp } from './ProxiedApp';
28+
import { AppsEngineEmptyRuntime } from './runtime/AppsEngineEmptyRuntime';
2829
import { AppLogStorage, AppMetadataStorage, IAppStorageItem } from './storage';
2930
import { AppSourceStorage } from './storage/AppSourceStorage';
3031

@@ -240,7 +241,7 @@ export class AppManager {
240241
app.getLogger().error(e);
241242
this.logStorage.storeEntries(app.getID(), app.getLogger());
242243

243-
const prl = new ProxiedApp(this, item, app, () => '');
244+
const prl = new ProxiedApp(this, item, app, new AppsEngineEmptyRuntime(app));
244245
this.apps.set(item.id, prl);
245246
aff.setApp(prl);
246247
}

src/server/ProxiedApp.ts

Lines changed: 15 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import * as vm from 'vm';
2-
31
import { IAppAccessors, ILogger } from '../definition/accessors';
42
import { App } from '../definition/App';
53
import { AppStatus } from '../definition/AppStatus';
@@ -10,23 +8,27 @@ import { AppManager } from './AppManager';
108
import { NotEnoughMethodArgumentsError } from './errors';
119
import { AppConsole } from './logging';
1210
import { AppLicenseValidationResult } from './marketplace/license';
13-
import { Utilities } from './misc/Utilities';
11+
import { AppsEngineRuntime } from './runtime/AppsEngineRuntime';
1412
import { IAppStorageItem } from './storage';
1513

16-
export const ROCKETCHAT_APP_EXECUTION_PREFIX = '$RocketChat_App$';
17-
1814
export class ProxiedApp implements IApp {
1915
private previousStatus: AppStatus;
2016

2117
private latestLicenseValidationResult: AppLicenseValidationResult;
2218

23-
constructor(private readonly manager: AppManager,
24-
private storageItem: IAppStorageItem,
25-
private readonly app: App,
26-
private readonly customRequire: (mod: string) => {}) {
19+
constructor(
20+
private readonly manager: AppManager,
21+
private storageItem: IAppStorageItem,
22+
private readonly app: App,
23+
private readonly runtime: AppsEngineRuntime,
24+
) {
2725
this.previousStatus = storageItem.status;
2826
}
2927

28+
public getRuntime(): AppsEngineRuntime {
29+
return this.runtime;
30+
}
31+
3032
public getApp(): App {
3133
return this.app;
3234
}
@@ -51,12 +53,6 @@ export class ProxiedApp implements IApp {
5153
return typeof (this.app as any)[method] === 'function';
5254
}
5355

54-
public makeContext(data: object): vm.Context {
55-
return Utilities.buildDefaultAppContext(Object.assign({}, {
56-
require: this.customRequire,
57-
}, data));
58-
}
59-
6056
public setupLogger(method: AppMethod): AppConsole {
6157
const logger = new AppConsole(method);
6258
// Set the logger to our new one
@@ -65,13 +61,6 @@ export class ProxiedApp implements IApp {
6561
return logger;
6662
}
6763

68-
public runInContext(codeToRun: string, context: vm.Context): any {
69-
return vm.runInContext(codeToRun, context, {
70-
timeout: 1000,
71-
filename: `${ ROCKETCHAT_APP_EXECUTION_PREFIX }_${ this.getName() }.ts`,
72-
});
73-
}
74-
7564
public async call(method: AppMethod, ...args: Array<any>): Promise<any> {
7665
if (typeof (this.app as any)[method] !== 'function') {
7766
throw new Error(`The App ${this.app.getName()} (${this.app.getID()}`
@@ -89,8 +78,10 @@ export class ProxiedApp implements IApp {
8978

9079
let result;
9180
try {
92-
// tslint:disable-next-line:max-line-length
93-
result = await this.runInContext(`app.${method}.apply(app, args)`, this.makeContext({ app: this.app, args })) as Promise<any>;
81+
result = await this.runtime.runInSandbox(
82+
`module.exports = app.${method}.apply(app, args)`,
83+
{ app: this.app, args },
84+
);
9485
logger.debug(`'${method}' was successfully called! The result is:`, result);
9586
} catch (e) {
9687
logger.error(e);

src/server/compiler/AppCompiler.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import * as path from 'path';
2-
import * as vm from 'vm';
32

43
import { App } from '../../definition/App';
54
import { AppMethod } from '../../definition/metadata';
65
import { AppAccessors } from '../accessors';
76
import { AppManager } from '../AppManager';
8-
import { MustContainFunctionError, MustExtendAppError } from '../errors';
7+
import { MustContainFunctionError } from '../errors';
98
import { AppConsole } from '../logging';
10-
import { Utilities } from '../misc/Utilities';
119
import { ProxiedApp } from '../ProxiedApp';
10+
import { getRuntime } from '../runtime';
11+
import { buildCustomRequire } from '../runtime/require';
1212
import { IAppStorageItem } from '../storage';
1313
import { IParseAppPackageResult } from './IParseAppPackageResult';
1414

@@ -29,31 +29,30 @@ export class AppCompiler {
2929
`Could not find the classFile (${ storage.info.classFile }) file.`);
3030
}
3131

32-
const exports = {};
33-
const customRequire = Utilities.buildCustomRequire(files, storage.info.id);
34-
const context = Utilities.buildDefaultAppContext({ require: customRequire, exports, process: {}, console });
32+
const Runtime = getRuntime();
3533

36-
const script = new vm.Script(files[path.normalize(storage.info.classFile)]);
37-
const result = script.runInContext(context);
34+
const customRequire = buildCustomRequire(files, storage.info.id);
35+
const result = Runtime.runCode(files[path.normalize(storage.info.classFile)], {
36+
require: customRequire,
37+
});
3838

3939
if (typeof result !== 'function') {
4040
// tslint:disable-next-line:max-line-length
4141
throw new Error(`The App's main class for ${ storage.info.name } is not valid ("${ storage.info.classFile }").`);
4242
}
43-
4443
const appAccessors = new AppAccessors(manager, storage.info.id);
4544
const logger = new AppConsole(AppMethod._CONSTRUCTOR);
46-
const rl = vm.runInNewContext('new App(info, rcLogger, appAccessors);', Utilities.buildDefaultAppContext({
45+
const rl = Runtime.runCode('exports.app = new App(info, rcLogger, appAccessors);', {
4746
rcLogger: logger,
4847
info: storage.info,
4948
App: result,
50-
process: {},
5149
appAccessors,
52-
}), { timeout: 1000, filename: `App_${ storage.info.nameSlug }.js` });
50+
}, { timeout: 1000, filename: `App_${ storage.info.nameSlug }.js` });
5351

54-
if (!(rl instanceof App)) {
55-
throw new MustExtendAppError();
56-
}
52+
// TODO: app is importing the Class App internally so it's not same object to compare. Need to find a way to make this test
53+
// if (!(rl instanceof App)) {
54+
// throw new MustExtendAppError();
55+
// }
5756

5857
if (typeof rl.getName !== 'function') {
5958
throw new MustContainFunctionError(storage.info.classFile, 'getName');
@@ -79,7 +78,8 @@ export class AppCompiler {
7978
throw new MustContainFunctionError(storage.info.classFile, 'getRequiredApiVersion');
8079
}
8180

82-
const app = new ProxiedApp(manager, storage, rl as App, customRequire);
81+
// TODO: Fix this type cast from to any to the right one
82+
const app = new ProxiedApp(manager, storage, rl as App, new Runtime(rl as App, customRequire as any));
8383

8484
manager.getLogStorage().storeEntries(app.getID(), logger);
8585

src/server/compiler/modules/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ const proxyHandlers = {
4444
querystring: defaultHandler,
4545
};
4646

47-
export function requireNativeModule(module: AllowedInternalModules, appId: string) {
48-
const requiredModule = require(module);
47+
export function requireNativeModule(module: AllowedInternalModules, appId: string, requirer: any) {
48+
const requiredModule = requirer(module);
4949

5050
return new Proxy(
5151
requiredModule,

src/server/managers/AppApi.ts

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -75,24 +75,22 @@ export class AppApi {
7575
hash: this.hash,
7676
};
7777

78-
const runContext = this.app.makeContext({
79-
endpoint: this.endpoint,
80-
args: [
81-
request,
82-
endpoint,
83-
accessors.getReader(this.app.getID()),
84-
accessors.getModifier(this.app.getID()),
85-
accessors.getHttp(this.app.getID()),
86-
accessors.getPersistence(this.app.getID()),
87-
],
88-
});
89-
9078
const logger = this.app.setupLogger(AppMethod._API_EXECUTOR);
9179
logger.debug(`${ path }'s ${ method } is being executed...`, request);
9280

93-
const runCode = `endpoint.${ method }.apply(endpoint, args)`;
81+
const runCode = `module.exports = endpoint.${ method }.apply(endpoint, args)`;
9482
try {
95-
const result: IApiResponse = await this.app.runInContext(runCode, runContext);
83+
const result: IApiResponse = await this.app.getRuntime().runInSandbox(runCode, {
84+
endpoint: this.endpoint,
85+
args: [
86+
request,
87+
endpoint,
88+
accessors.getReader(this.app.getID()),
89+
accessors.getModifier(this.app.getID()),
90+
accessors.getHttp(this.app.getID()),
91+
accessors.getPersistence(this.app.getID()),
92+
],
93+
});
9694
logger.debug(`${ path }'s ${ method } was successfully executed.`);
9795
logStorage.storeEntries(this.app.getID(), logger);
9896
return result;

src/server/managers/AppPermissionManager.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { IPermission } from '../../definition/permissions/IPermission';
22
import { getPermissionsByAppId } from '../AppManager';
33
import { PermissionDeniedError } from '../errors/PermissionDeniedError';
4-
import { ROCKETCHAT_APP_EXECUTION_PREFIX } from '../ProxiedApp';
4+
import { APPS_ENGINE_RUNTIME_FILE_PREFIX } from '../runtime/AppsEngineRuntime';
55

66
export class AppPermissionManager {
77
/**
@@ -33,7 +33,7 @@ export class AppPermissionManager {
3333

3434
private static getCallStack(): string {
3535
const stack = new Error().stack.toString().split('\n');
36-
const appStackIndex = stack.findIndex((position) => position.includes(ROCKETCHAT_APP_EXECUTION_PREFIX));
36+
const appStackIndex = stack.findIndex((position) => position.includes(APPS_ENGINE_RUNTIME_FILE_PREFIX));
3737

3838
return stack.slice(4, appStackIndex).join('\n');
3939
}

src/server/managers/AppSchedulerManager.ts

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -63,23 +63,21 @@ export class AppSchedulerManager {
6363
return;
6464
}
6565

66-
const context = app.makeContext({
67-
processor,
68-
args: [
69-
jobContext,
70-
this.accessors.getReader(appId),
71-
this.accessors.getModifier(appId),
72-
this.accessors.getHttp(appId),
73-
this.accessors.getPersistence(appId),
74-
],
75-
});
76-
7766
const logger = app.setupLogger(AppMethod._JOB_PROCESSOR);
7867
logger.debug(`Job processor ${processor.id} is being executed...`);
7968

8069
try {
81-
const codeToRun = `processor.processor.apply(null, args)`;
82-
await app.runInContext(codeToRun, context);
70+
const codeToRun = `module.exports = processor.processor.apply(null, args)`;
71+
await app.getRuntime().runInSandbox(codeToRun, {
72+
processor,
73+
args: [
74+
jobContext,
75+
this.accessors.getReader(appId),
76+
this.accessors.getModifier(appId),
77+
this.accessors.getHttp(appId),
78+
this.accessors.getPersistence(appId),
79+
],
80+
});
8381
logger.debug(`Job processor ${processor.id} was sucessfully executed`);
8482
} catch (e) {
8583
logger.error(e);

0 commit comments

Comments
 (0)