Skip to content

Commit

Permalink
feat: implement CapTP forwarding over a plugin device
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfig committed Sep 16, 2020
1 parent 6ba171f commit b4a1be8
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 6 deletions.
84 changes: 84 additions & 0 deletions packages/SwingSet/src/devices/plugin-src.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/* global harden */

import { makeCapTP } from '@agoric/captp';

export function buildRootDeviceNode(tools) {
const { SO, getDeviceState, setDeviceState, endowments } = tools;
const restart = getDeviceState();

let registeredReceiver;

const connectedMods = [];
const senders = [];
const connectedState = restart ? [...restart.connectedState] : [];

function saveState() {
setDeviceState(
harden({
connectedMods,
connectedState: [...connectedState],
}),
);
}

/**
* Load a module and connect to it.
* @param {string} mod module with an exported `bootPlugin(state = undefined)`
* @param {(obj: Record<string, any>) => void} receive a message from the module
* @returns {(obj: Record<string, any>) => void} send a message to the module
*/
function connect(mod) {
try {
const modNS = endowments.require(mod);
const index = connectedMods.length;
connectedMods.push(mod);
const receiver = obj => {
console.info('receiver', index, obj);
switch (obj.type) {
case 'PLUGIN_SAVE_STATE':
connectedState[index] = obj.data;
saveState();
break;
default:
SO(registeredReceiver).receive(index, obj);
}
};
// Create a bootstrap reference from the module.
const bootstrap = modNS.bootPlugin(connectedState[index]);

// Establish a CapTP connection.
const { dispatch } = makeCapTP(mod, receiver, bootstrap);

// Save the dispatch function for later.
senders[index] = dispatch;
return index;
} catch (e) {
console.error(`Cannot connect to ${mod}:`, e);
return `${(e && e.stack) || e}`;
}
}

function send(index, obj) {
const sender = senders[index];
console.error('send', obj);
sender(obj);
}

// Connect to all existing modules.
const preload = restart ? restart.connectedMods : [];
preload.forEach(mod => {
try {
connect(mod);
} catch (e) {
console.error(`Cannot connect to ${mod}:`, e);
}
});

return harden({
connect,
send,
registerReceiver(receiver) {
registeredReceiver = receiver;
},
});
}
10 changes: 10 additions & 0 deletions packages/SwingSet/src/devices/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function buildPlugin(pluginRequire) {
const srcPath = require.resolve('./plugin-src');

// srcPath and endowments are provided to buildRootDeviceNode() for use
// during configuration.
return {
srcPath,
endowments: { require: pluginRequire },
};
}
1 change: 1 addition & 0 deletions packages/SwingSet/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { buildMailboxStateMap, buildMailbox } from './devices/mailbox';
export { buildTimer } from './devices/timer';
export { buildBridge } from './devices/bridge';
export { default as buildCommand } from './devices/command';
export { buildPlugin } from './devices/plugin';
3 changes: 3 additions & 0 deletions packages/cosmic-swingset/lib/ag-solo/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
buildVatController,
buildMailboxStateMap,
buildMailbox,
buildPlugin,
buildTimer,
} from '@agoric/swingset-vat';
import { getBestSwingStore } from '../check-lmdb';
Expand Down Expand Up @@ -87,6 +88,7 @@ async function buildSwingset(
const mb = buildMailbox(mbs);
const cm = buildCommand(broadcast);
const timer = buildTimer();
const plugin = buildPlugin(require);

let config = loadSwingsetConfigFile(`${vatsDir}/solo-config.json`);
if (config === null) {
Expand All @@ -96,6 +98,7 @@ async function buildSwingset(
['mailbox', mb.srcPath, mb.endowments],
['command', cm.srcPath, cm.endowments],
['timer', timer.srcPath, timer.endowments],
['plugin', plugin.srcPath, plugin.endowments],
];

const tempdir = path.resolve(kernelStateDBDir, 'check-lmdb-tempdir');
Expand Down
8 changes: 6 additions & 2 deletions packages/cosmic-swingset/lib/ag-solo/vats/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { E } from '@agoric/eventual-send';
// has been run to update gci.js
import { GCI } from './gci';
import { makeBridgeManager } from './bridge';
import { makePluginManager } from './plugin';

const NUM_IBC_PORTS = 3;

Expand Down Expand Up @@ -207,14 +208,16 @@ export function buildRootObject(vatPowers, vatParameters) {
// objects that live in the client's solo vat. Some services should only
// be in the DApp environment (or only in end-user), but we're not yet
// making a distinction, so the user also gets them.
async function createLocalBundle(vats) {
async function createLocalBundle(vats, devices) {
// This will eventually be a vat spawning service. Only needed by dev
// environments.
const spawner = E(vats.host).makeHost();

// Needed for DApps, maybe for user clients.
const uploads = E(vats.uploads).getUploads();

const plugin = makePluginManager(E, D, devices.plugin);

// This will allow dApp developers to register in their api/deploy.js
const httpRegCallback = {
doneLoading(subsystems) {
Expand Down Expand Up @@ -242,6 +245,7 @@ export function buildRootObject(vatPowers, vatParameters) {
harden({
uploads,
spawner,
plugin,
network: vats.network,
http: httpRegCallback,
vattp: makeVattpFrom(vats),
Expand Down Expand Up @@ -314,7 +318,7 @@ export function buildRootObject(vatPowers, vatParameters) {
GCI,
PROVISIONER_INDEX,
);
const localBundle = await createLocalBundle(vats);
const localBundle = await createLocalBundle(vats, devices);
await E(vats.http).setPresences(localBundle);
const bundle = await E(demoProvider).getDemoBundle();
await E(vats.http).setPresences(localBundle, bundle, {
Expand Down
71 changes: 71 additions & 0 deletions packages/cosmic-swingset/lib/ag-solo/vats/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/* global harden */
// @ts-check

import makeStore from '@agoric/store';
import { makeCapTP } from '@agoric/captp';

/**
* @template T
* @typedef {T} Device
*/

/**
* @typedef {Object} PluginManager
* @property {(mod: string) => ERef<any>} load
*/

/**
* @typedef {Object} Receiver
* @property {(index: number, obj: Record<string, any>) => void} receive
*/

/**
* @typedef {Object} PluginDevice
* @property {(mod: string) => number} connect
* @property {(receiver: Receiver) => void} registerReceiver
* @property {(index: number, obj: Record<string, any>) => void} send
*/

/**
* Create a handler that manages a promise interface to external modules.
*
* @param {import('@agoric/eventual-send').EProxy} E The eventual sender
* @param {<T>(target: Device<T>) => T} D The device sender
* @param {Device<PluginDevice>} pluginDevice The bridge to manage
* @returns {PluginManager} admin facet for this handler
*/
export function makePluginManager(E, D, pluginDevice) {
/**
* @type {import('@agoric/store').Store<number, (obj: Record<string,any>) => void>}
*/
const modReceivers = makeStore('moduleIndex');

// Dispatch object to the right index.
D(pluginDevice).registerReceiver(
harden({
receive(index, obj) {
console.info('receive', index, obj);
modReceivers.get(index)(obj);
},
}),
);

return harden({
load(mod) {
// Start CapTP on the plugin module's side.
const index = D(pluginDevice).connect(mod);
if (typeof index === 'string') {
throw Error(index);
}
// Create a CapTP channel.
const { getBootstrap, dispatch } = makeCapTP(mod, obj =>
D(pluginDevice).send(index, obj),
);
// Register our dispatcher for this connect index.
modReceivers.init(index, dispatch);

// Give up our bootstrap object for the caller to use.
return getBootstrap();
},
});
}
8 changes: 4 additions & 4 deletions packages/eventual-send/test/test-hp.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ test('chained properties', async t => {
const data = {};
const queue = [];
const handler = {
applyMethod(_o, prop, args, target) {
applyMethod(_o, prop, args) {
// Support: o~.[prop](...args) remote method invocation
queue.push([0, prop, args, target]);
queue.push([0, prop, args]);
return data;
// return queueMessage(slot, prop, args);
},
Expand All @@ -33,8 +33,8 @@ test('chained properties', async t => {
t.deepEqual(
queue,
[
[0, 'cont0', [], queue[0][3]], // FIXME: Actually use the target of this call
[0, 'cont1', [], queue[1][3]], // FIXME: Actually use the target of this call
[0, 'cont0', []],
[0, 'cont1', []],
],
`first turn`,
);
Expand Down

0 comments on commit b4a1be8

Please sign in to comment.