From b4a1be8f600d60191570a3bbf42bc4c82af47b06 Mon Sep 17 00:00:00 2001 From: Michael FIG Date: Mon, 7 Sep 2020 23:03:08 -0600 Subject: [PATCH] feat: implement CapTP forwarding over a plugin device --- packages/SwingSet/src/devices/plugin-src.js | 84 +++++++++++++++++++ packages/SwingSet/src/devices/plugin.js | 10 +++ packages/SwingSet/src/index.js | 1 + packages/cosmic-swingset/lib/ag-solo/start.js | 3 + .../lib/ag-solo/vats/bootstrap.js | 8 +- .../lib/ag-solo/vats/plugin.js | 71 ++++++++++++++++ packages/eventual-send/test/test-hp.js | 8 +- 7 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 packages/SwingSet/src/devices/plugin-src.js create mode 100644 packages/SwingSet/src/devices/plugin.js create mode 100644 packages/cosmic-swingset/lib/ag-solo/vats/plugin.js diff --git a/packages/SwingSet/src/devices/plugin-src.js b/packages/SwingSet/src/devices/plugin-src.js new file mode 100644 index 00000000000..72bd38ba77e --- /dev/null +++ b/packages/SwingSet/src/devices/plugin-src.js @@ -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) => void} receive a message from the module + * @returns {(obj: Record) => 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; + }, + }); +} diff --git a/packages/SwingSet/src/devices/plugin.js b/packages/SwingSet/src/devices/plugin.js new file mode 100644 index 00000000000..a50ac0863cd --- /dev/null +++ b/packages/SwingSet/src/devices/plugin.js @@ -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 }, + }; +} diff --git a/packages/SwingSet/src/index.js b/packages/SwingSet/src/index.js index ec0bbc5d430..e41ca4606cc 100644 --- a/packages/SwingSet/src/index.js +++ b/packages/SwingSet/src/index.js @@ -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'; diff --git a/packages/cosmic-swingset/lib/ag-solo/start.js b/packages/cosmic-swingset/lib/ag-solo/start.js index 0a4f67bd263..6d6c9873f1d 100644 --- a/packages/cosmic-swingset/lib/ag-solo/start.js +++ b/packages/cosmic-swingset/lib/ag-solo/start.js @@ -17,6 +17,7 @@ import { buildVatController, buildMailboxStateMap, buildMailbox, + buildPlugin, buildTimer, } from '@agoric/swingset-vat'; import { getBestSwingStore } from '../check-lmdb'; @@ -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) { @@ -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'); diff --git a/packages/cosmic-swingset/lib/ag-solo/vats/bootstrap.js b/packages/cosmic-swingset/lib/ag-solo/vats/bootstrap.js index 6f001a778cb..a9cb8bcbc3f 100644 --- a/packages/cosmic-swingset/lib/ag-solo/vats/bootstrap.js +++ b/packages/cosmic-swingset/lib/ag-solo/vats/bootstrap.js @@ -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; @@ -207,7 +208,7 @@ 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(); @@ -215,6 +216,8 @@ export function buildRootObject(vatPowers, vatParameters) { // 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) { @@ -242,6 +245,7 @@ export function buildRootObject(vatPowers, vatParameters) { harden({ uploads, spawner, + plugin, network: vats.network, http: httpRegCallback, vattp: makeVattpFrom(vats), @@ -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, { diff --git a/packages/cosmic-swingset/lib/ag-solo/vats/plugin.js b/packages/cosmic-swingset/lib/ag-solo/vats/plugin.js new file mode 100644 index 00000000000..63fb47b2e22 --- /dev/null +++ b/packages/cosmic-swingset/lib/ag-solo/vats/plugin.js @@ -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} load + */ + +/** + * @typedef {Object} Receiver + * @property {(index: number, obj: Record) => void} receive + */ + +/** + * @typedef {Object} PluginDevice + * @property {(mod: string) => number} connect + * @property {(receiver: Receiver) => void} registerReceiver + * @property {(index: number, obj: Record) => void} send + */ + +/** + * Create a handler that manages a promise interface to external modules. + * + * @param {import('@agoric/eventual-send').EProxy} E The eventual sender + * @param {(target: Device) => T} D The device sender + * @param {Device} pluginDevice The bridge to manage + * @returns {PluginManager} admin facet for this handler + */ +export function makePluginManager(E, D, pluginDevice) { + /** + * @type {import('@agoric/store').Store) => 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(); + }, + }); +} diff --git a/packages/eventual-send/test/test-hp.js b/packages/eventual-send/test/test-hp.js index 5db1f9f8468..b63c043eb9a 100644 --- a/packages/eventual-send/test/test-hp.js +++ b/packages/eventual-send/test/test-hp.js @@ -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); }, @@ -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`, );