Skip to content

Commit

Permalink
Refactor app initialization (#684)
Browse files Browse the repository at this point in the history
  • Loading branch information
d-gubert authored Dec 15, 2023
1 parent 1e4839b commit 6f9da6f
Show file tree
Hide file tree
Showing 15 changed files with 191 additions and 225 deletions.
51 changes: 30 additions & 21 deletions deno-runtime/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,23 @@ if (!Deno.args.includes('--subprocess')) {
}

import { createRequire } from 'node:module';
import { sanitizeDeprecatedUsage } from "./lib/sanitizeDeprecatedUsage.ts";
import { AppAccessorsInstance } from "./lib/accessors/mod.ts";
import * as Messenger from "./lib/messenger.ts";
import { AppObjectRegistry } from "./AppObjectRegistry.ts";
import { sanitizeDeprecatedUsage } from './lib/sanitizeDeprecatedUsage.ts';
import { AppAccessorsInstance } from './lib/accessors/mod.ts';
import * as Messenger from './lib/messenger.ts';
import { AppObjectRegistry } from './AppObjectRegistry.ts';
import { Logger } from "./lib/logger.ts";

const require = createRequire(import.meta.url);
import type { IParseAppPackageResult } from '@rocket.chat/apps-engine/server/compiler/IParseAppPackageResult.ts';

// @deno-types='../definition/App.d.ts'
const { App } = require('../definition/App');
const require = createRequire(import.meta.url);

const ALLOWED_NATIVE_MODULES = ['path', 'url', 'crypto', 'buffer', 'stream', 'net', 'http', 'https', 'zlib', 'util', 'punycode', 'os', 'querystring'];
const ALLOWED_EXTERNAL_MODULES = ['uuid'];

function buildRequire(): (module: string) => unknown {
return (module: string): unknown => {
if (ALLOWED_NATIVE_MODULES.includes(module)) {
return require(`node:${module}`)
return require(`node:${module}`);
}

if (ALLOWED_EXTERNAL_MODULES.includes(module)) {
Expand Down Expand Up @@ -55,14 +54,19 @@ function wrapAppCode(code: string): (require: (module: string) => unknown) => Pr
) as (require: (module: string) => unknown) => Promise<Record<string, unknown>>;
}

async function handlInitializeApp({ id, source }: { id: string; source: string }): Promise<void> {
source = sanitizeDeprecatedUsage(source);
async function handlInitializeApp(appPackage: IParseAppPackageResult): Promise<void> {
const source = sanitizeDeprecatedUsage(appPackage.files[appPackage.info.classFile]);

const require = buildRequire();
const exports = await wrapAppCode(source)(require);

// This is the same naive logic we've been using in the App Compiler
const appClass = Object.values(exports)[0] as typeof App;
// Applying the correct type here is quite difficult because of the dynamic nature of the code
// deno-lint-ignore no-explicit-any
const appClass = Object.values(exports)[0] as any;
const logger = AppObjectRegistry.get('logger');
const app = new appClass({ author: {} }, logger, AppAccessorsInstance.getDefaultAppAccessors());

const app = new appClass(appPackage.info, logger, AppAccessorsInstance.getDefaultAppAccessors());

if (typeof app.getName !== 'function') {
throw new Error('App must contain a getName function');
Expand All @@ -89,7 +93,7 @@ async function handlInitializeApp({ id, source }: { id: string; source: string }
}

AppObjectRegistry.set('app', app);
AppObjectRegistry.set('id', id);
AppObjectRegistry.set('id', appPackage.info.id);
}

async function handleRequest({ type, payload }: Messenger.JsonRpcRequest): Promise<void> {
Expand All @@ -106,16 +110,16 @@ async function handleRequest({ type, payload }: Messenger.JsonRpcRequest): Promi
AppObjectRegistry.set('logger', logger);

switch (method) {
case 'construct': {
const [appId, source] = params as [string, string];
case 'app:construct': {
const [appPackage] = params as [IParseAppPackageResult];

if (!appId || !source) {
if (!appPackage?.info?.id || !appPackage?.info?.classFile || !appPackage?.files) {
return Messenger.sendInvalidParamsError(id);
}

await handlInitializeApp({ id: appId, source })
await handlInitializeApp(appPackage);

Messenger.successResponse({ id, result: 'hooray' });
Messenger.successResponse({ id, result: 'logs should go here as a response' });
break;
}
default: {
Expand All @@ -132,21 +136,26 @@ function handleResponse(response: Messenger.JsonRpcResponse): void {
let event: Event;

if (response.type === 'error') {
event = new ErrorEvent(`response:${response.payload.id}`, { error: response.payload.error });
event = new ErrorEvent(`response:${response.payload.id}`, {
error: response.payload.error,
});
} else {
event = new CustomEvent(`response:${response.payload.id}`, { detail: response.payload.result });
event = new CustomEvent(`response:${response.payload.id}`, {
detail: response.payload.result,
});
}

Messenger.RPCResponseObserver.dispatchEvent(event);
}

async function main() {
setTimeout(() => Messenger.sendNotification({ method: 'ready' }), 1_780);
Messenger.sendNotification({ method: 'ready' });

const decoder = new TextDecoder();

for await (const chunk of Deno.stdin.readable) {
const message = decoder.decode(chunk);

let JSONRPCMessage;

try {
Expand Down
2 changes: 1 addition & 1 deletion src/definition/App.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export abstract class App implements IApp {
* Also, please use the `initialize()` method to do items instead of the constructor as the constructor
* *might* be called more than once but the `initialize()` will only be called once.
*/
protected constructor(private readonly info: IAppInfo, private readonly logger: ILogger, private readonly accessors?: IAppAccessors) {
public constructor(private readonly info: IAppInfo, private readonly logger: ILogger, private readonly accessors?: IAppAccessors) {
this.logger.debug(
`Constructed the App ${this.info.name} (${this.info.id})`,
`v${this.info.version} which depends on the API v${this.info.requiredApiVersion}!`,
Expand Down
2 changes: 1 addition & 1 deletion src/definition/metadata/IAppInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ export interface IAppInfo {
/** Base64 string of the App's icon. */
iconFileContent?: string;
essentials?: Array<AppInterface>;
permissions: Array<IPermission>;
permissions?: Array<IPermission>;
}
12 changes: 10 additions & 2 deletions src/server/AppManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,12 @@ import type { IMarketplaceInfo } from './marketplace';
import { DisabledApp } from './misc/DisabledApp';
import { defaultPermissions } from './permissions/AppPermissions';
import { ProxiedApp } from './ProxiedApp';
import { AppsEngineEmptyRuntime } from './runtime/AppsEngineEmptyRuntime';
import type { IAppStorageItem } from './storage';
import { AppLogStorage, AppMetadataStorage } from './storage';
import { AppSourceStorage } from './storage/AppSourceStorage';
import { AppInstallationSource } from './storage/IAppStorageItem';
import { AppRuntimeManager } from './managers/AppRuntimeManager';
import type { DenoRuntimeSubprocessController } from './runtime/AppsEngineDenoRuntime';
import { AppConsole } from './logging';

export interface IAppInstallParameters {
Expand Down Expand Up @@ -100,6 +101,8 @@ export class AppManager {

private readonly signatureManager: AppSignatureManager;

private readonly runtime: AppRuntimeManager;

private isLoaded: boolean;

constructor({ metadataStorage, logStorage, bridges, sourceStorage }: IAppManagerDeps) {
Expand Down Expand Up @@ -147,6 +150,7 @@ export class AppManager {
this.uiActionButtonManager = new UIActionButtonManager(this);
this.videoConfProviderManager = new AppVideoConfProviderManager(this);
this.signatureManager = new AppSignatureManager(this);
this.runtime = new AppRuntimeManager(this);

this.isLoaded = false;
AppManager.Instance = this;
Expand Down Expand Up @@ -227,6 +231,10 @@ export class AppManager {
return this.signatureManager;
}

public getRuntime(): AppRuntimeManager {
return this.runtime;
}

/** Gets whether the Apps have been loaded or not. */
public areAppsLoaded(): boolean {
return this.isLoaded;
Expand Down Expand Up @@ -266,7 +274,7 @@ export class AppManager {
app.getLogger().error(e);
await this.logStorage.storeEntries(AppConsole.toStorageEntry(app.getID(), app.getLogger()));

const prl = new ProxiedApp(this, item, app, new AppsEngineEmptyRuntime(app));
const prl = new ProxiedApp(this, item, {} as DenoRuntimeSubprocessController);
this.apps.set(item.id, prl);
}
}
Expand Down
79 changes: 28 additions & 51 deletions src/server/ProxiedApp.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import type { IAppAccessors, ILogger } from '../definition/accessors';
import type { App } from '../definition/App';
import type { AppStatus } from '../definition/AppStatus';
import { AppsEngineException } from '../definition/exceptions';
import { AppStatus } from '../definition/AppStatus';
import type { IApp } from '../definition/IApp';
import type { IAppAuthorInfo, IAppInfo } from '../definition/metadata';
import { AppMethod } from '../definition/metadata';
import type { AppManager } from './AppManager';
import { NotEnoughMethodArgumentsError } from './errors';
import { InvalidInstallationError } from './errors/InvalidInstallationError';
import { AppConsole } from './logging';
import { AppLicenseValidationResult } from './marketplace/license';
import type { DenoRuntimeSubprocessController } from './runtime/AppsEngineDenoRuntime';
import type { AppsEngineRuntime } from './runtime/AppsEngineRuntime';
import type { IAppStorageItem } from './storage';

Expand All @@ -18,21 +16,12 @@ export class ProxiedApp implements IApp {

private latestLicenseValidationResult: AppLicenseValidationResult;

constructor(
private readonly manager: AppManager,
private storageItem: IAppStorageItem,
private readonly app: App,
private readonly runtime: AppsEngineRuntime,
) {
constructor(private readonly manager: AppManager, private storageItem: IAppStorageItem, private readonly appRuntime: DenoRuntimeSubprocessController) {
this.previousStatus = storageItem.status;
}

public getRuntime(): AppsEngineRuntime {
return this.runtime;
}

public getApp(): App {
return this.app;
return this.manager.getRuntime();
}

public getStorageItem(): IAppStorageItem {
Expand All @@ -52,51 +41,36 @@ export class ProxiedApp implements IApp {
}

public hasMethod(method: AppMethod): boolean {
return typeof (this.app as any)[method] === 'function';
return true; // TODO: needs refactor, remove usages
}

public setupLogger(method: `${AppMethod}`): AppConsole {
const logger = new AppConsole(method);
// Set the logger to our new one
(this.app as any).logger = logger;

return logger;
}

public async call(method: `${AppMethod}`, ...args: Array<any>): Promise<any> {
if (typeof (this.app as any)[method] !== 'function') {
throw new Error(`The App ${this.app.getName()} (${this.app.getID()} does not have the method: "${method}"`);
}

const methodDeclartion = (this.app as any)[method] as (...args: any[]) => any;
if (args.length < methodDeclartion.length) {
throw new NotEnoughMethodArgumentsError(method, methodDeclartion.length, args.length);
}

const logger = this.setupLogger(method);
logger.debug(`${method} is being called...`);

let result;
try {
result = await this.runtime.runInSandbox(`module.exports = app.${method}.apply(app, args)`, { app: this.app, args });
logger.debug(`'${method}' was successfully called! The result is:`, result);
const result = await this.appRuntime.sendRequest({ method, params: args });

logger.debug('Result:', result);

return result;
} catch (e) {
logger.error(e);
logger.debug(`'${method}' was unsuccessful.`);
logger.error('Error:', e);

const errorInfo = new AppsEngineException(e.message).getErrorInfo();
if (e.name === errorInfo.name) {
throw e;
}
throw e;
} finally {
await this.manager.getLogStorage().storeEntries(AppConsole.toStorageEntry(this.getID(), logger));
}

return result;
}

public getStatus(): AppStatus {
return this.app.getStatus();
// return this.appRuntime.getStatus();
return AppStatus.UNKNOWN; // TODO: need to circle back on this one
}

public async setStatus(status: AppStatus, silent?: boolean): Promise<void> {
Expand All @@ -108,47 +82,50 @@ export class ProxiedApp implements IApp {
}

public getName(): string {
return this.app.getName();
return this.storageItem.info.name;
}

public getNameSlug(): string {
return this.app.getNameSlug();
return this.storageItem.info.nameSlug;
}

public getAppUserUsername(): string {
return this.app.getAppUserUsername();
// return this.app.getAppUserUsername();
return 'some-username'; // TODO: need to circle back on this one
}

public getID(): string {
return this.app.getID();
return this.storageItem.id;
}

public getVersion(): string {
return this.app.getVersion();
return this.storageItem.info.version;
}

public getDescription(): string {
return this.app.getDescription();
return this.storageItem.info.description;
}

public getRequiredApiVersion(): string {
return this.app.getRequiredApiVersion();
return this.storageItem.info.requiredApiVersion;
}

public getAuthorInfo(): IAppAuthorInfo {
return this.app.getAuthorInfo();
return this.storageItem.info.author;
}

public getInfo(): IAppInfo {
return this.app.getInfo();
return this.storageItem.info;
}

public getLogger(): ILogger {
return this.app.getLogger();
// return this.app.getLogger();
return new AppConsole('constructor'); // TODO: need to circle back on this one
}

public getAccessors(): IAppAccessors {
return this.app.getAccessors();
// return this.app.getAccessors();
return {} as IAppAccessors; // TODO: need to circle back on this one
}

public getEssentials(): IAppInfo['essentials'] {
Expand Down
Loading

0 comments on commit 6f9da6f

Please sign in to comment.