Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor plugin + worker interface #19

Merged
merged 11 commits into from
Jun 16, 2021
2 changes: 1 addition & 1 deletion packages/controllers/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from './workers/WorkerController';
export * from './services/WorkerExecutionEnvironment';
export * from './plugins/PluginController';
export * from './resource/ExternalResourceController';
46 changes: 29 additions & 17 deletions packages/controllers/src/plugins/PluginController.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from 'fs';
import { ControllerMessenger } from '@metamask/controllers/dist/ControllerMessenger';
import { WorkerController } from '../workers/WorkerController';
import { WorkerExecutionEnvironment } from '../services/WorkerExecutionEnvironment';
import { PluginController } from './PluginController';

const workerCode = fs.readFileSync(
Expand All @@ -10,20 +10,26 @@ const workerCode = fs.readFileSync(

describe('PluginController Controller', () => {
it('can create a worker and plugin controller', async () => {
const workerController = new WorkerController({
const workerExecutionEnvironment = new WorkerExecutionEnvironment({
setupWorkerConnection: jest.fn(),
workerUrl: new URL(URL.createObjectURL(new Blob([workerCode]))),
});
const pluginController = new PluginController({
command: workerController.command.bind(workerController),
createPluginWorker: workerController.createPluginWorker.bind(
workerController,
command: workerExecutionEnvironment.command.bind(
workerExecutionEnvironment,
),
terminateAll: workerController.terminateAll.bind(workerController),
terminateWorkerOf: workerController.terminateWorkerOf.bind(
workerController,
createPluginEnvironment: workerExecutionEnvironment.createPluginEnvironment.bind(
workerExecutionEnvironment,
),
terminateAll: workerExecutionEnvironment.terminateAllPlugins.bind(
shanejonas marked this conversation as resolved.
Show resolved Hide resolved
workerExecutionEnvironment,
),
terminatePlugin: workerExecutionEnvironment.terminatePlugin.bind(
workerExecutionEnvironment,
),
startPlugin: workerExecutionEnvironment.startPlugin.bind(
workerExecutionEnvironment,
),
startPlugin: workerController.startPlugin.bind(workerController),
removeAllPermissionsFor: jest.fn(),
getPermissions: jest.fn(),
hasPermission: jest.fn(),
Expand All @@ -42,20 +48,26 @@ describe('PluginController Controller', () => {
});

it('can add a plugin and use its JSON-RPC api', async () => {
const workerController = new WorkerController({
const workerExecutionEnvironment = new WorkerExecutionEnvironment({
setupWorkerConnection: jest.fn(),
workerUrl: new URL(URL.createObjectURL(new Blob([workerCode]))),
});
const pluginController = new PluginController({
command: workerController.command.bind(workerController),
createPluginWorker: workerController.createPluginWorker.bind(
workerController,
command: workerExecutionEnvironment.command.bind(
workerExecutionEnvironment,
),
createPluginEnvironment: workerExecutionEnvironment.createPluginEnvironment.bind(
workerExecutionEnvironment,
),
terminateAll: workerExecutionEnvironment.terminateAllPlugins.bind(
shanejonas marked this conversation as resolved.
Show resolved Hide resolved
workerExecutionEnvironment,
),
terminatePlugin: workerExecutionEnvironment.terminatePlugin.bind(
workerExecutionEnvironment,
),
terminateAll: workerController.terminateAll.bind(workerController),
terminateWorkerOf: workerController.terminateWorkerOf.bind(
workerController,
startPlugin: workerExecutionEnvironment.startPlugin.bind(
workerExecutionEnvironment,
),
startPlugin: workerController.startPlugin.bind(workerController),
removeAllPermissionsFor: jest.fn(),
getPermissions: jest.fn(),
hasPermission: jest.fn(),
Expand Down
70 changes: 36 additions & 34 deletions packages/controllers/src/plugins/PluginController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import {
BaseControllerV2 as BaseController,
RestrictedControllerMessenger,
} from '@metamask/controllers';
import { Json, JsonRpcRequest } from 'json-rpc-engine';
import { PluginData } from '@mm-snap/types';
import { PluginWorkerMetadata } from '../workers/WorkerController';
import { Json } from 'json-rpc-engine';
import {
Command,
CreatePluginEnvironment,
StartPlugin,
TerminateAll,
TerminatePlugin,
} from '../services/PluginExecutionEnvironmentService';
import { INLINE_PLUGINS } from './inlinePlugins';

export const PLUGIN_PREFIX = 'wallet_plugin_';
Expand Down Expand Up @@ -59,18 +64,6 @@ type HasPermissionFunction = (
permissionName: string,
) => boolean;
type GetPermissionsFunction = (domain: string) => IOcapLdCapability[];
type TerminateWorkerOf = (pluginName: string) => void;
type Command = (
workerId: string,
message: JsonRpcRequest<unknown>,
) => Promise<unknown>;
type TerminateAll = () => void;
type CreatePluginWorker = (metadate: PluginWorkerMetadata) => Promise<string>;
type StartPlugin = (
workerId: string,
pluginData: PluginData,
) => Promise<unknown>;

type PluginId = string;
type StoredPlugins = Record<PluginId, Plugin>;

Expand All @@ -90,10 +83,10 @@ interface PluginControllerArgs {
requestPermissions: RequestPermissionsFunction;
getPermissions: GetPermissionsFunction;
hasPermission: HasPermissionFunction;
terminateWorkerOf: TerminateWorkerOf;
terminatePlugin: TerminatePlugin;
command: Command;
terminateAll: TerminateAll;
shanejonas marked this conversation as resolved.
Show resolved Hide resolved
createPluginWorker: CreatePluginWorker;
createPluginEnvironment: CreatePluginEnvironment;
startPlugin: StartPlugin;
}

Expand Down Expand Up @@ -149,13 +142,13 @@ export class PluginController extends BaseController<

private _hasPermission: HasPermissionFunction;

private _terminateWorkerOf: TerminateWorkerOf;
private _terminatePlugin: TerminatePlugin;

private _command: Command;

private _terminateAll: TerminateAll;
shanejonas marked this conversation as resolved.
Show resolved Hide resolved

private _createPluginWorker: CreatePluginWorker;
private _createPluginEnvironment: CreatePluginEnvironment;

private _startPlugin: StartPlugin;

Expand All @@ -166,10 +159,10 @@ export class PluginController extends BaseController<
closeAllConnections,
requestPermissions,
getPermissions,
terminateWorkerOf,
terminatePlugin,
terminateAll,
hasPermission,
createPluginWorker,
createPluginEnvironment,
startPlugin,
command,
messenger,
Expand Down Expand Up @@ -207,9 +200,9 @@ export class PluginController extends BaseController<
this._getPermissions = getPermissions;
this._hasPermission = hasPermission;

this._terminateWorkerOf = terminateWorkerOf;
this._terminatePlugin = terminatePlugin;
this._terminateAll = terminateAll;
this._createPluginWorker = createPluginWorker;
this._createPluginEnvironment = createPluginEnvironment;
this._startPlugin = startPlugin;
this._command = command;

Expand All @@ -236,7 +229,7 @@ export class PluginController extends BaseController<
console.log(`Starting: ${pluginName}`);

try {
await this._startPluginInWorker(pluginName, sourceCode);
await this._startPluginInExecutionEnvironment(pluginName, sourceCode);
} catch (err) {
console.warn(`Failed to start "${pluginName}", deleting it.`, err);
// Clean up failed plugins:
Expand All @@ -262,7 +255,10 @@ export class PluginController extends BaseController<
}

try {
await this._startPluginInWorker(pluginName, plugin.sourceCode);
await this._startPluginInExecutionEnvironment(
pluginName,
plugin.sourceCode,
);
} catch (err) {
console.error(`Failed to start "${pluginName}".`, err);
}
Expand Down Expand Up @@ -298,7 +294,7 @@ export class PluginController extends BaseController<
private _stopPlugin(pluginName: string, setNotRunning = true): void {
this._removePluginHooks(pluginName);
this._closeAllConnections(pluginName);
this._terminateWorkerOf(pluginName);
this._terminatePlugin(pluginName);
if (setNotRunning) {
this._setPluginToNotRunning(pluginName);
}
Expand Down Expand Up @@ -525,7 +521,7 @@ export class PluginController extends BaseController<

await this.authorize(pluginName);

await this._startPluginInWorker(pluginName, sourceCode);
await this._startPluginInExecutionEnvironment(pluginName, sourceCode);

return this.getSerializable(pluginName) as SerializablePlugin;
} catch (err) {
Expand Down Expand Up @@ -691,7 +687,10 @@ export class PluginController extends BaseController<
* Test method.
*/
runInlinePlugin(inlinePluginName: keyof typeof INLINE_PLUGINS = 'IDLE') {
this._startPluginInWorker('inlinePlugin', INLINE_PLUGINS[inlinePluginName]);
this._startPluginInExecutionEnvironment(
'inlinePlugin',
INLINE_PLUGINS[inlinePluginName],
);
this.update((state: any) => {
state.inlinePluginIsRunning = true;
});
Expand All @@ -707,12 +706,15 @@ export class PluginController extends BaseController<
this.removePlugin('inlinePlugin');
}

private async _startPluginInWorker(pluginName: string, sourceCode: string) {
const workerId = await this._createPluginWorker({
private async _startPluginInExecutionEnvironment(
pluginName: string,
sourceCode: string,
) {
shanejonas marked this conversation as resolved.
Show resolved Hide resolved
await this._createPluginEnvironment({
hostname: pluginName,
});
this._createPluginHooks(pluginName, workerId);
await this._startPlugin(workerId, {
this._createPluginHooks(pluginName);
await this._startPlugin({
pluginName,
sourceCode,
});
Expand All @@ -728,12 +730,12 @@ export class PluginController extends BaseController<
return this._pluginRpcHooks.get(pluginName);
}

private _createPluginHooks(pluginName: string, workerId: string) {
private _createPluginHooks(pluginName: string) {
shanejonas marked this conversation as resolved.
Show resolved Hide resolved
const rpcHook = async (
origin: string,
request: Record<string, unknown>,
) => {
return await this._command(workerId, {
return await this._command(pluginName, {
id: nanoid(),
jsonrpc: '2.0',
method: 'pluginRpc',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { JsonRpcRequest } from 'json-rpc-engine';
import { PluginData } from '@mm-snap/types';

export interface PluginMetadata {
hostname: string;
}

export type TerminatePlugin = (pluginName: string) => void;
export type Command = (
pluginName: string,
message: JsonRpcRequest<unknown>,
) => Promise<unknown>;
export type TerminateAll = () => void;
export type CreatePluginEnvironment = (
metadata: PluginMetadata,
) => Promise<string>;
export type StartPlugin = (pluginData: PluginData) => Promise<unknown>;

export interface PluginExecutionEnvironmentService {
terminatePlugin: TerminatePlugin;
terminateAllPlugins: TerminateAll;
createPluginEnvironment: CreatePluginEnvironment;
command: Command;
shanejonas marked this conversation as resolved.
Show resolved Hide resolved
startPlugin: StartPlugin;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fs from 'fs';
import { WorkerController } from './WorkerController';
import { WorkerExecutionEnvironment } from './WorkerExecutionEnvironment';

const workerCode = fs.readFileSync(
require.resolve('@mm-snap/workers/dist/PluginWorker.js'),
Expand All @@ -8,7 +8,7 @@ const workerCode = fs.readFileSync(

describe('Worker Controller', () => {
it('can boot', async () => {
const workerController = new WorkerController({
const workerController = new WorkerExecutionEnvironment({
shanejonas marked this conversation as resolved.
Show resolved Hide resolved
setupWorkerConnection: () => {
// do nothing
},
Expand All @@ -17,30 +17,30 @@ describe('Worker Controller', () => {
expect(workerController).toBeDefined();
});
it('can create a plugin worker and get the workerId back', async () => {
const workerController = new WorkerController({
const workerController = new WorkerExecutionEnvironment({
setupWorkerConnection: () => {
// do nothing
},
workerUrl: new URL(URL.createObjectURL(new Blob([workerCode]))),
});
expect(
typeof (await workerController.createPluginWorker({
typeof (await workerController.createPluginEnvironment({
hostname: 'foobarbaz',
})),
).toEqual('string');
});
it('can create a plugin worker and start the plugin', async () => {
const workerController = new WorkerController({
const workerController = new WorkerExecutionEnvironment({
setupWorkerConnection: () => {
// do nothing
},
workerUrl: new URL(URL.createObjectURL(new Blob([workerCode]))),
});
const pluginName = 'foo.bar.baz';
const workerId = await workerController.createPluginWorker({
await workerController.createPluginEnvironment({
hostname: pluginName,
});
const response = await workerController.startPlugin(workerId, {
const response = await workerController.startPlugin({
pluginName,
sourceCode: `
console.log('foo');
Expand Down
Loading