From ec2e993f53f168531010b8ad09a197109d33a425 Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Mon, 19 Apr 2021 16:56:59 -0700 Subject: [PATCH] fix(swingset): refactor dispatch() This changes the raw vat API: instead of an object named `dispatch` with methods like `dispatch.deliver` and `dispatch.notify`, vats export a `dispatch()` *function* which always takes a `VatDeliveryObject`. This VDO contains a `type: 'message'` or `type: 'notify'` (among others). This paves the way for low-level vats to get control when their userspace code becomes quiescent, so they can perform GC activity at end-of-crank. The comms vat (our only non-liveslots "raw" vat) was changed to match. The various vat managers and supervisors were refactored around a "manager kit" and "supervisor kit", to centralize transcript replay and error handling. Each different type of manager is responsible for launching the worker and delivering VatDeliveryObjects / VatDeliveryResults over whatever form of "wire" they use, and likewise for VatSyscallObject / VatSyscallResults. But the `dispatch()` presented to the kernel (on one side), and the `dispatch`/`syscall` attached to the low-level vat (on the other) are both handled by common code, regardless of the type of manager. This removes a lot of boilerplate and should fix some of the accidental non-feature-parity between manager types. It should also help with the return of meter-consumed data from the worker, eventually. The manager-type -specific code no longer attempts to inspect/dissect/reassemble VatDeliveryObjects or VatSyscallObjects. The manager types are split into two categories: those for which syscalls can return data, and those which cannot. Local and XS workers can block until the kernel returns the syscall results, so they can use `syscall.invoke` and `syscall.vatstoreGet`, which are necessary for device calls and virtual objects, respectively. NodeWorker (i.e. threads) cannot block, and Node subprocess workers do not block (we could make them block, but it's more work than we care to invest). The creation of Vat Managers was shuffled to provide the `vatSyscallHandler` as an argument up-front, rather than passing it into a callback function later. We needed the old (and annoying) arrangement to deal with a cyclic dependency, but that was removed some time ago, so now we can finally clean it up. The transcript format was changed to include the VatDeliveryObject, VatSyscallObject, and VatSyscallResult objects more directly. Several syscall-handling paths were rewritten to do the same. This causes test-kernel.js to change slightly, in the parts which examine the transcript directly. A lot of typescript annotations were added (mostly in `src/types.js`), and many type-asserting functions were added to `src/message.js`. All unit tests that interacted with liveslots or the comms vat directly (using `dispatch.deliver()`) were changed to use `dispatch()`, along with helper functions to create the VatDeliveryObjects it now takes. --- packages/SwingSet/jsconfig.json | 1 - packages/SwingSet/src/initializeSwingset.js | 1 + packages/SwingSet/src/kernel/kernel.js | 18 +- packages/SwingSet/src/kernel/liveSlots.js | 29 ++- packages/SwingSet/src/kernel/loadVat.js | 22 +- .../SwingSet/src/kernel/vatManager/deliver.js | 153 ------------ .../SwingSet/src/kernel/vatManager/factory.js | 36 ++- .../src/kernel/vatManager/manager-helper.js | 220 ++++++++++++++++++ .../src/kernel/vatManager/manager-local.js | 112 +++++---- .../kernel/vatManager/manager-nodeworker.js | 94 +++----- .../vatManager/manager-subprocess-node.js | 113 +++------ .../vatManager/manager-subprocess-xsnap.js | 85 +++---- .../kernel/vatManager/supervisor-helper.js | 187 +++++++++++++++ .../vatManager/supervisor-nodeworker.js | 89 +++---- .../vatManager/supervisor-subprocess-node.js | 82 ++----- .../vatManager/supervisor-subprocess-xsnap.js | 103 ++------ .../SwingSet/src/kernel/vatManager/syscall.js | 110 --------- packages/SwingSet/src/kernel/vatTranslator.js | 10 +- packages/SwingSet/src/message.js | 160 ++++++++++++- packages/SwingSet/src/parseVatSlots.js | 2 +- packages/SwingSet/src/types.js | 68 +++++- packages/SwingSet/src/vats/comms/dispatch.js | 27 ++- packages/SwingSet/test/commsVatDriver.js | 13 +- .../test/files-devices/bootstrap-0.js | 13 +- .../test/files-devices/bootstrap-1.js | 30 +-- .../test/files-devices/bootstrap-4.js | 48 ++-- packages/SwingSet/test/liveslots-helpers.js | 46 ++++ packages/SwingSet/test/test-comms.js | 107 +++++---- packages/SwingSet/test/test-kernel.js | 150 +++++++----- packages/SwingSet/test/test-liveslots.js | 172 +++++--------- packages/SwingSet/test/test-vpid-liveslots.js | 139 ++++------- packages/SwingSet/test/util.js | 83 ++++++- packages/SwingSet/test/vat-controller-1 | 8 +- packages/SwingSet/test/vat-syscall-failure.js | 7 +- 34 files changed, 1409 insertions(+), 1129 deletions(-) delete mode 100644 packages/SwingSet/src/kernel/vatManager/deliver.js create mode 100644 packages/SwingSet/src/kernel/vatManager/manager-helper.js create mode 100644 packages/SwingSet/src/kernel/vatManager/supervisor-helper.js delete mode 100644 packages/SwingSet/src/kernel/vatManager/syscall.js create mode 100644 packages/SwingSet/test/liveslots-helpers.js diff --git a/packages/SwingSet/jsconfig.json b/packages/SwingSet/jsconfig.json index 6aa8279c4c4..8bb67b37ef4 100644 --- a/packages/SwingSet/jsconfig.json +++ b/packages/SwingSet/jsconfig.json @@ -10,7 +10,6 @@ "declaration": true, "emitDeclarationOnly": true, */ - "downlevelIteration": true, "strictNullChecks": true, "moduleResolution": "node", }, diff --git a/packages/SwingSet/src/initializeSwingset.js b/packages/SwingSet/src/initializeSwingset.js index 561379d3797..8952f5e8845 100644 --- a/packages/SwingSet/src/initializeSwingset.js +++ b/packages/SwingSet/src/initializeSwingset.js @@ -7,6 +7,7 @@ import { assert, details as X } from '@agoric/assert'; import bundleSource from '@agoric/bundle-source'; import { initSwingStore } from '@agoric/swing-store-simple'; +import './types'; import { insistStorageAPI } from './storageAPI'; import { initializeKernel } from './kernel/initializeKernel'; diff --git a/packages/SwingSet/src/kernel/kernel.js b/packages/SwingSet/src/kernel/kernel.js index 550bca38300..cd09f91213f 100644 --- a/packages/SwingSet/src/kernel/kernel.js +++ b/packages/SwingSet/src/kernel/kernel.js @@ -10,7 +10,7 @@ import { insistKernelType, parseKernelSlot } from './parseKernelSlots'; import { parseVatSlot } from '../parseVatSlots'; import { insistStorageAPI } from '../storageAPI'; import { insistCapData } from '../capdata'; -import { insistMessage } from '../message'; +import { insistMessage, insistVatDeliveryResult } from '../message'; import { insistDeviceID, insistVatID } from './id'; import { makeMeterManager } from './metering'; import { makeKernelSyscallHandler, doSend } from './kernelSyscall'; @@ -18,7 +18,6 @@ import { makeSlogger, makeDummySlogger } from './slogger'; import { getKpidsToRetire } from './cleanup'; import { makeVatLoader } from './loadVat'; -import { makeVatTranslators } from './vatTranslator'; import { makeDeviceTranslators } from './deviceTranslator'; function abbreviateReplacer(_, arg) { @@ -33,6 +32,7 @@ function abbreviateReplacer(_, arg) { } function makeError(message, name = 'Error') { + assert.typeof(message, 'string'); const err = { '@qclass': 'error', name, message }; return harden({ body: JSON.stringify(err), slots: [] }); } @@ -369,6 +369,7 @@ export default function buildKernel( ); try { const deliveryResult = await vat.manager.deliver(vatDelivery); + insistVatDeliveryResult(deliveryResult); finish(deliveryResult); const [status, problem] = deliveryResult; if (status !== 'ok') { @@ -572,7 +573,7 @@ export default function buildKernel( } } - const gcTools = harden({ WeakRef, FinalizationRegistry }); + const gcTools = harden({ WeakRef, FinalizationRegistry, waitUntilQuiescent }); const vatManagerFactory = makeVatManagerFactory({ allVatPowers, kernelKeeper, @@ -580,7 +581,6 @@ export default function buildKernel( meterManager, testLog, transformMetering, - waitUntilQuiescent, makeNodeWorker, startSubprocessWorkerNode, startXSnap, @@ -656,18 +656,16 @@ export default function buildKernel( * might tell the manager to replay the transcript later, if it notices * we're reloading a saved state vector. */ - function addVatManager(vatID, manager, managerOptions) { + function addVatManager(vatID, manager, translators, managerOptions) { // addVatManager takes a manager, not a promise for one assert( - manager.deliver && manager.setVatSyscallHandler, + manager.deliver, `manager lacks .deliver, isPromise=${manager instanceof Promise}`, ); const { enablePipelining = false, notifyTermination = () => {}, } = managerOptions; - kernelKeeper.getVatKeeper(vatID); - const translators = makeVatTranslators(vatID, kernelKeeper); ephemeral.vats.set( vatID, @@ -678,9 +676,6 @@ export default function buildKernel( enablePipelining: Boolean(enablePipelining), }), ); - - const vatSyscallHandler = buildVatSyscallHandler(vatID, translators); - manager.setVatSyscallHandler(vatSyscallHandler); } const { @@ -699,6 +694,7 @@ export default function buildKernel( queueToExport, kernelKeeper, panic, + buildVatSyscallHandler, }); /** diff --git a/packages/SwingSet/src/kernel/liveSlots.js b/packages/SwingSet/src/kernel/liveSlots.js index 330db794f5a..7642d91beb8 100644 --- a/packages/SwingSet/src/kernel/liveSlots.js +++ b/packages/SwingSet/src/kernel/liveSlots.js @@ -10,6 +10,7 @@ import { assert, details as X } from '@agoric/assert'; import { isPromise } from '@agoric/promise-kit'; import { insistVatType, makeVatSlot, parseVatSlot } from '../parseVatSlots'; import { insistCapData } from '../capdata'; +import { insistMessage } from '../message'; import { makeVirtualObjectManager } from './virtualObjectManager'; const DEFAULT_VIRTUAL_OBJECT_CACHE_SIZE = 3; // XXX ridiculously small value to force churn for testing @@ -679,12 +680,36 @@ function build( const rootObject = buildRootObject(harden(vpow), harden(vatParameters)); assert.equal(passStyleOf(rootObject), REMOTE_STYLE); - const rootSlot = makeVatSlot('object', true, 0n); + const rootSlot = makeVatSlot('object', true, BigInt(0)); valToSlot.set(rootObject, rootSlot); slotToVal.set(rootSlot, rootObject); } - const dispatch = harden({ deliver, notify, dropExports }); + function dispatch(vatDeliveryObject) { + const [type, ...args] = vatDeliveryObject; + switch (type) { + case 'message': { + const [targetSlot, msg] = args; + insistMessage(msg); + deliver(targetSlot, msg.method, msg.args, msg.result); + break; + } + case 'notify': { + const [resolutions] = args; + notify(resolutions); + break; + } + case 'dropExports': { + const [vrefs] = args; + dropExports(vrefs); + break; + } + default: + assert.fail(X`unknown delivery type ${type}`); + } + } + harden(dispatch); + return harden({ vatGlobals, setBuildRootObject, dispatch, m }); } diff --git a/packages/SwingSet/src/kernel/loadVat.js b/packages/SwingSet/src/kernel/loadVat.js index cda197e7a9a..dc03ffe9cea 100644 --- a/packages/SwingSet/src/kernel/loadVat.js +++ b/packages/SwingSet/src/kernel/loadVat.js @@ -2,6 +2,7 @@ import { assert, details as X } from '@agoric/assert'; import { assertKnownOptions } from '../assertOptions'; import { makeVatSlot } from '../parseVatSlots'; import { insistCapData } from '../capdata'; +import { makeVatTranslators } from './vatTranslator'; export function makeVatRootObjectSlot() { return makeVatSlot('object', true, 0n); @@ -19,6 +20,7 @@ export function makeVatLoader(stuff) { queueToExport, kernelKeeper, panic, + buildVatSyscallHandler, } = stuff; /** @@ -229,11 +231,17 @@ export function makeVatLoader(stuff) { // object) and for the spawner vat (not so easy). To avoid deeper changes, // we enable it for *all* static vats here. Once #1343 is fixed, remove // this addition and all support for internal metering. + const translators = makeVatTranslators(vatID, kernelKeeper); + const vatSyscallHandler = buildVatSyscallHandler(vatID, translators); const finish = kernelSlog.startup(vatID); - const manager = await vatManagerFactory(vatID, managerOptions); + const manager = await vatManagerFactory( + vatID, + managerOptions, + vatSyscallHandler, + ); finish(); - addVatManager(vatID, manager, managerOptions); + addVatManager(vatID, manager, translators, managerOptions); } function makeSuccessResponse() { @@ -304,8 +312,14 @@ export function makeVatLoader(stuff) { enableSetup: true, managerType: 'local', }; - const manager = await vatManagerFactory(vatID, managerOptions); - addVatManager(vatID, manager, managerOptions); + const translators = makeVatTranslators(vatID, kernelKeeper); + const vatSyscallHandler = buildVatSyscallHandler(vatID, translators); + const manager = await vatManagerFactory( + vatID, + managerOptions, + vatSyscallHandler, + ); + addVatManager(vatID, manager, translators, managerOptions); } return harden({ diff --git a/packages/SwingSet/src/kernel/vatManager/deliver.js b/packages/SwingSet/src/kernel/vatManager/deliver.js deleted file mode 100644 index 20e706e686b..00000000000 --- a/packages/SwingSet/src/kernel/vatManager/deliver.js +++ /dev/null @@ -1,153 +0,0 @@ -import { assert, details as X } from '@agoric/assert'; -import { insistMessage } from '../../message'; - -export function makeDeliver(tools, dispatch) { - const { - meterRecord, - refillAllMeters, - stopGlobalMeter, - transcriptManager, - vatID, - vatKeeper, - waitUntilQuiescent, - kernelSlog, - } = tools; - - /** - * Run a function, returning a promise that waits for the promise queue to be - * empty before resolving. - * - * @param {*} f The function to run - * @param {string} errmsg Tag string to be associated with the error message that gets - * logged if `f` rejects. - * - * The kernel uses `runAndWait` to wait for the vat to become quiescent (that - * is, with nothing remaining on the promise queue) after a dispatch call. - * Since the vat is never given direct access to the timer or IO queues (i.e., - * it can't call setImmediate, setInterval, or setTimeout), once the promise - * queue is empty, the vat has lost "agency" (the ability to initiate further - * execution). The kernel *does not* wait for the dispatch handler's return - * promise to resolve, since a malicious or erroneous vat might fail to do - * so, and the kernel must be defensive against this. - */ - function runAndWait(f, errmsg) { - // prettier-ignore - Promise.resolve() - .then(f) - .then( - undefined, - err => console.log(`doProcess: ${errmsg}: ${err.message}`), - ); - return waitUntilQuiescent(); - } - - function updateStats(_used) { - // TODO: accumulate used.allocate and used.compute into vatStats - } - - async function doProcess(dispatchRecord, errmsg) { - const dispatchOp = dispatchRecord[0]; - const dispatchArgs = dispatchRecord.slice(1); - transcriptManager.startDispatch(dispatchRecord); - await runAndWait(() => dispatch[dispatchOp](...dispatchArgs), errmsg); - stopGlobalMeter(); - - let status = ['ok', null, null]; - // refill this vat's meter, if any, accumulating its usage for stats - if (meterRecord) { - // note that refill() won't actually refill an exhausted meter - const meterUsage = meterRecord.refill(); - const exhaustionError = meterRecord.isExhausted(); - if (exhaustionError) { - status = ['error', exhaustionError.message, meterUsage]; - } else { - // We will have ['ok', null, meterUsage] - status[2] = meterUsage; - updateStats(status[2]); - } - } - - // refill all within-vat -created meters - refillAllMeters(); - - // TODO: if the dispatch failed, and we choose to destroy the vat, change - // what we do with the transcript here. - transcriptManager.finishDispatch(); - return status; - } - - async function deliverOneMessage(targetSlot, msg) { - insistMessage(msg); - const errmsg = `vat[${vatID}][${targetSlot}].${msg.method} dispatch failed`; - return doProcess( - ['deliver', targetSlot, msg.method, msg.args, msg.result], - errmsg, - ); - } - - async function deliverOneNotification(resolutions) { - const errmsg = `vat[${vatID}].notify failed`; - return doProcess(['notify', resolutions], errmsg); - } - - async function deliverOneDropExports(vrefs) { - const errmsg = `vat[${vatID}].dropExports failed`; - return doProcess(['dropExports', vrefs], errmsg); - } - - // vatDeliverObject is: - // ['message', target, msg] - // target is vid - // msg is: { method, args (capdata), result } - // ['notify', resolutions] - // resolutions is an array of triplets: [vpid, rejected, value] - // vpid is the id of the primary promise being resolved - // rejected is a boolean flag indicating if vpid is being fulfilled or rejected - // value is capdata describing the value the promise is being resolved to - // The first entry in the resolutions array is the primary promise being - // resolved, while the remainder (if any) are collateral promises it - // references whose resolution was newly discovered at the time the - // notification delivery was being generated - async function deliver(vatDeliverObject) { - const [type, ...args] = vatDeliverObject; - switch (type) { - case 'message': - return deliverOneMessage(...args); - case 'notify': - return deliverOneNotification(...args); - case 'dropExports': - return deliverOneDropExports(...args); - default: - assert.fail(X`unknown delivery type ${type}`); - } - } - - async function replayTranscript() { - const total = vatKeeper.vatStats().transcriptCount; - kernelSlog.write({ type: 'start-replay', vatID, deliveries: total }); - transcriptManager.startReplay(); - let deliveryNum = 0; - for (const t of vatKeeper.getTranscript()) { - deliveryNum += 1; // TODO probably off by one - // if (deliveryNum % 100 === 0) { - // console.debug(`replay vatID:${vatID} deliveryNum:${deliveryNum} / ${total}`); - // } - transcriptManager.checkReplayError(); - transcriptManager.startReplayDelivery(t.syscalls); - kernelSlog.write({ - type: 'start-replay-delivery', - vatID, - delivery: t.d, - deliveryNum, - }); - // eslint-disable-next-line no-await-in-loop - await doProcess(t.d, null); - kernelSlog.write({ type: 'finish-replay-delivery', vatID, deliveryNum }); - } - transcriptManager.checkReplayError(); - transcriptManager.finishReplay(); - kernelSlog.write({ type: 'finish-replay', vatID }); - } - - return harden({ deliver, replayTranscript }); -} diff --git a/packages/SwingSet/src/kernel/vatManager/factory.js b/packages/SwingSet/src/kernel/vatManager/factory.js index fad557e70e6..b687c4de9a5 100644 --- a/packages/SwingSet/src/kernel/vatManager/factory.js +++ b/packages/SwingSet/src/kernel/vatManager/factory.js @@ -11,7 +11,6 @@ export function makeVatManagerFactory({ vatEndowments, meterManager, transformMetering, - waitUntilQuiescent, makeNodeWorker, startSubprocessWorkerNode, startXSnap, @@ -25,7 +24,6 @@ export function makeVatManagerFactory({ vatEndowments, meterManager, transformMetering, - waitUntilQuiescent, gcTools, kernelSlog, }); @@ -33,18 +31,21 @@ export function makeVatManagerFactory({ const nodeWorkerFactory = makeNodeWorkerVatManagerFactory({ makeNodeWorker, kernelKeeper, + kernelSlog, testLog: allVatPowers.testLog, }); const nodeSubprocessFactory = makeNodeSubprocessFactory({ startSubprocessWorker: startSubprocessWorkerNode, kernelKeeper, + kernelSlog, testLog: allVatPowers.testLog, }); const xsWorkerFactory = makeXsSubprocessFactory({ startXSnap, kernelKeeper, + kernelSlog, allVatPowers, testLog: allVatPowers.testLog, }); @@ -77,7 +78,7 @@ export function makeVatManagerFactory({ } // returns promise for new vatManager - function vatManagerFactory(vatID, managerOptions) { + function vatManagerFactory(vatID, managerOptions, vatSyscallHandler) { validateManagerOptions(managerOptions); const { managerType = defaultManagerType, @@ -99,13 +100,28 @@ export function makeVatManagerFactory({ } if (managerType === 'local' || metered || enableSetup) { if (setup) { - return localFactory.createFromSetup(vatID, setup, managerOptions); + return localFactory.createFromSetup( + vatID, + setup, + managerOptions, + vatSyscallHandler, + ); } - return localFactory.createFromBundle(vatID, bundle, managerOptions); + return localFactory.createFromBundle( + vatID, + bundle, + managerOptions, + vatSyscallHandler, + ); } if (managerType === 'nodeWorker') { - return nodeWorkerFactory.createFromBundle(vatID, bundle, managerOptions); + return nodeWorkerFactory.createFromBundle( + vatID, + bundle, + managerOptions, + vatSyscallHandler, + ); } if (managerType === 'node-subprocess') { @@ -113,11 +129,17 @@ export function makeVatManagerFactory({ vatID, bundle, managerOptions, + vatSyscallHandler, ); } if (managerType === 'xs-worker') { - return xsWorkerFactory.createFromBundle(vatID, bundle, managerOptions); + return xsWorkerFactory.createFromBundle( + vatID, + bundle, + managerOptions, + vatSyscallHandler, + ); } throw Error( diff --git a/packages/SwingSet/src/kernel/vatManager/manager-helper.js b/packages/SwingSet/src/kernel/vatManager/manager-helper.js new file mode 100644 index 00000000000..e9edb830dd3 --- /dev/null +++ b/packages/SwingSet/src/kernel/vatManager/manager-helper.js @@ -0,0 +1,220 @@ +// @ts-check +import { assert } from '@agoric/assert'; +import '../../types'; +import { insistVatDeliveryResult } from '../../message'; +import { makeTranscriptManager } from './transcript'; + +// We use vat-centric terminology here, so "inbound" means "into a vat", +// always from the kernel. Conversely "outbound" means "out of a vat", into +// the local kernel. + +// The mapVatSlotToKernelSlot() function is used to translate slot references +// on the vat->kernel pathway. mapKernelToVatSlot() is used for kernel->vat. + +// The terms "import" and "export" are also vat-centric. "import" means +// something a Vat has imported (from the kernel). Imports are tracked in a +// kernel-side table for each Vat, which is populated by the kernel as a +// message is delivered. Each import is represented inside the Vat as a +// Presence (at least when using liveSlots). + +// "exports" are callable objects inside the Vat which it has made +// available to the kernel (so that other vats can invoke it). The exports +// table is managed by userspace code inside the vat. The kernel tables map +// one vat's import IDs (o-NN) to a kernel object ID (koNN) in the +// vatKeeper's state.vatSlotToKernelSlot table. To make sure we use the +// same importID each time, we also need to keep a reverse table: +// kernelSlotToVatSlot maps them back. + +// The vat sees "vat slots" (object references) as the arguments of +// syscall/dispatch functions. These take on the following forms (where +// "NN" is an integer): + +// o+NN : an object ref allocated by this Vat, hence an export +// o-NN : an object ref allocated by the kernel, an imported object +// p-NN : a promise ref allocated by the kernel +// p+NN : a promise ref allocated by this vat +// d-NN : a device ref allocated by the kernel, imported + +// Within the kernel, we use "kernel slots", with the following forms: + +// koNN : an object reference +// kpNN : a promise reference +// kdNN : a device reference + +// The vatManager is responsible for translating vat slots into kernel +// slots on the outbound (syscall) path, and kernel slots back into vat +// slots on the inbound (dispatch) path. + +/** + * + * @typedef { { getManager: (shutdown: () => Promise) => VatManager, + * syscallFromWorker: (vso: VatSyscallObject) => VatSyscallResult, + * setDeliverToWorker: (dtw: unknown) => void, + * } } ManagerKit + * + */ + +/** + * This generic helper runs on the manager side. It handles transcript + * record/replay, and errors in the manager-specific code. + * + * Create me at the beginning of the manager factory, with a vatKeeper and + * vatSyscallHandler. + * + * When you build the handler that accepts syscall requests from the worker, + * that handler should call my { syscallFromWorker } function, so I can + * forward those requests to vatSyscallHandler (after going through the + * transcript). + * + * At some point, you must give me a way to send deliveries to the worker, by + * calling setDeliverToWorker. This usually happens after the worker + * connection is established. + * + * At the end of the factory, use my getManager(shutdown) method to create, + * harden, and return the VatManager object. Give me a manager-specific + * shutdown function which I can include in the VatManager. + * + * + * deliverToWorker is expected to accept a VatDeliveryObject and return a + * VatDeliveryResult, or a promise for one. It is allowed to throw or reject, + * in which case the caller gets a error-bearing VDR, which will probably + * kill the vat. For remote (subprocess/thread) workers, deliverToWorker + * should be a function that serializes the VDO and sends it over the wire, + * and waits for a response, so it might reject if the child process has + * died. For a local worker, deliverToWorker can be the liveslots 'dispatch' + * method (which runs synchronously and throws if liveslots has an error). + * + * vatSyscallHandler should be the same 'vatSyscallHandler' given to a + * managerFactory. It is expected to accept a VatSyscallObject and return a + * (synchronous) VatSyscallResult, never throwing. + * + * The returned syscallFromWorker function should be called when the worker + * wants to make a syscall. It accepts a VatSyscallObject and will return a + * (synchronous) VatSyscallResult, never throwing. For remote workers, this + * should be called from a handler that receives a syscall message from the + * child process. + * + * The returned getManager() function will return a VatManager suitable for + * handing to the kernel, which can use it to send deliveries to the vat. + * + * @param { string } vatID + * @param { KernelKeeper } kernelKeeper + * @param { KernelSlog } kernelSlog + * @param { (vso: VatSyscallObject) => VatSyscallResult } vatSyscallHandler + * @param { boolean } workerCanBlock + * @returns { ManagerKit } + */ + +function makeManagerKit( + vatID, + kernelSlog, + kernelKeeper, + vatSyscallHandler, + workerCanBlock, +) { + assert(kernelSlog); + const vatKeeper = kernelKeeper.getVatKeeper(vatID); + const transcriptManager = makeTranscriptManager(vatKeeper, vatID); + + /** @type { (delivery: VatDeliveryObject) => Promise } */ + let deliverToWorker; + + /** + * @param { (delivery: VatDeliveryObject) => Promise } dtw + */ + function setDeliverToWorker(dtw) { + assert(!deliverToWorker, `setDeliverToWorker called twice`); + deliverToWorker = dtw; + } + + /** + * + * @param { VatDeliveryObject } delivery + * @returns { Promise } + */ + async function deliver(delivery) { + transcriptManager.startDispatch(delivery); + /** @type { VatDeliveryResult } */ + const status = await deliverToWorker(delivery).catch(err => + harden(['error', err.message, null]), + ); + insistVatDeliveryResult(status); + // TODO: if the dispatch failed for whatever reason, and we choose to + // destroy the vat, change what we do with the transcript here. + transcriptManager.finishDispatch(); + return status; + } + + async function replayTranscript() { + const total = vatKeeper.vatStats().transcriptCount; + kernelSlog.write({ type: 'start-replay', vatID, deliveries: total }); + transcriptManager.startReplay(); + let deliveryNum = 0; + for (const t of vatKeeper.getTranscript()) { + // if (deliveryNum % 100 === 0) { + // console.debug(`replay vatID:${vatID} deliveryNum:${deliveryNum} / ${total}`); + // } + transcriptManager.checkReplayError(); + transcriptManager.startReplayDelivery(t.syscalls); + kernelSlog.write({ + type: 'start-replay-delivery', + vatID, + delivery: t.d, + deliveryNum, + }); + // eslint-disable-next-line no-await-in-loop + await deliver(t.d); + kernelSlog.write({ type: 'finish-replay-delivery', vatID, deliveryNum }); + deliveryNum += 1; + } + transcriptManager.checkReplayError(); + transcriptManager.finishReplay(); + kernelSlog.write({ type: 'finish-replay', vatID }); + } + + /** + * vatSyscallObject is an array that starts with the syscall name ('send', + * 'subscribe', etc) followed by all the positional arguments of the + * syscall, designed for transport across a manager-worker link (serialized + * bytes over a socket or pipe, postMessage to an in-process Worker, or + * just direct). + * + * @param { VatSyscallObject } vso + * @returns { VatSyscallResult } + */ + function syscallFromWorker(vso) { + if (transcriptManager.inReplay()) { + // We're replaying old messages to bring the vat's internal state + // up-to-date. It will make syscalls like a puppy chasing rabbits in + // its sleep. Gently prevent their twitching paws from doing anything. + + // but if the puppy deviates one inch from previous twitches, explode + const data = transcriptManager.simulateSyscall(vso); + return harden(['ok', data]); + } + + const vres = vatSyscallHandler(vso); + // vres is ['error', reason] or ['ok', null] or ['ok', capdata] or ['ok', string] + const [successFlag, data] = vres; + if (successFlag === 'ok') { + if (data && !workerCanBlock) { + console.log(`warning: syscall returns data, but worker cannot get it`); + } + transcriptManager.addSyscall(vso, data); + } + return vres; + } + + /** + * + * @param { () => Promise} shutdown + * @returns { VatManager } + */ + function getManager(shutdown) { + return harden({ replayTranscript, deliver, shutdown }); + } + + return harden({ getManager, syscallFromWorker, setDeliverToWorker }); +} +harden(makeManagerKit); +export { makeManagerKit }; diff --git a/packages/SwingSet/src/kernel/vatManager/manager-local.js b/packages/SwingSet/src/kernel/vatManager/manager-local.js index f2d2370ce1c..3e51a093fc5 100644 --- a/packages/SwingSet/src/kernel/vatManager/manager-local.js +++ b/packages/SwingSet/src/kernel/vatManager/manager-local.js @@ -1,9 +1,13 @@ +// @ts-check import { assert, details as X } from '@agoric/assert'; import { importBundle } from '@agoric/import-bundle'; import { makeLiveSlots } from '../liveSlots'; -import { createSyscall } from './syscall'; -import { makeDeliver } from './deliver'; -import { makeTranscriptManager } from './transcript'; +import { makeManagerKit } from './manager-helper'; +import { + makeSupervisorDispatch, + makeMeteredDispatch, + makeSupervisorSyscall, +} from './supervisor-helper'; export function makeLocalVatManagerFactory(tools) { const { @@ -12,11 +16,11 @@ export function makeLocalVatManagerFactory(tools) { vatEndowments, meterManager, transformMetering, - waitUntilQuiescent, gcTools, kernelSlog, } = tools; + const { waitUntilQuiescent } = gcTools; const { makeGetMeter, refillAllMeters, stopGlobalMeter } = meterManager; const baseVP = { makeMarshal: allVatPowers.makeMarshal, @@ -28,62 +32,57 @@ export function makeLocalVatManagerFactory(tools) { }; // testLog is also a vatPower, only for unit tests - function prepare(vatID) { - const vatKeeper = kernelKeeper.getVatKeeper(vatID); - const transcriptManager = makeTranscriptManager(vatKeeper, vatID); - const { syscall, setVatSyscallHandler } = createSyscall(transcriptManager); - function finish(dispatch, meterRecord) { - assert( - dispatch && dispatch.deliver, - `vat failed to return a 'dispatch' with .deliver: ${dispatch}`, - ); - const { deliver, replayTranscript } = makeDeliver( - { - vatID, - stopGlobalMeter, - meterRecord, - refillAllMeters, - transcriptManager, - vatKeeper, - waitUntilQuiescent, - kernelSlog, - }, - dispatch, + function prepare(vatID, vatSyscallHandler, meterRecord) { + const mtools = harden({ stopGlobalMeter, meterRecord, refillAllMeters }); + const mk = makeManagerKit( + vatID, + kernelSlog, + kernelKeeper, + vatSyscallHandler, + true, + ); + + function finish(dispatch) { + assert.typeof(dispatch, 'function'); + // this 'deliverToWorker' never throws, even if liveslots has an internal error + const deliverToWorker = makeMeteredDispatch( + makeSupervisorDispatch(dispatch, waitUntilQuiescent), + mtools, ); + mk.setDeliverToWorker(deliverToWorker); async function shutdown() { // local workers don't need anything special to shut down between turns } - const manager = harden({ - replayTranscript, - setVatSyscallHandler, - deliver, - shutdown, - }); - return manager; + return mk.getManager(shutdown); } + const syscall = makeSupervisorSyscall(mk.syscallFromWorker, true); return { syscall, finish }; } - function createFromSetup(vatID, setup, managerOptions) { + function createFromSetup(vatID, setup, managerOptions, vatSyscallHandler) { assert(!managerOptions.metered, X`unsupported`); assert(!managerOptions.enableInternalMetering, X`unsupported`); assert(setup instanceof Function, 'setup is not an in-realm function'); - const { syscall, finish } = prepare(vatID, managerOptions); + const { syscall, finish } = prepare(vatID, vatSyscallHandler, null); const { vatParameters } = managerOptions; const { testLog } = allVatPowers; const helpers = harden({}); // DEPRECATED, todo remove from setup() const state = null; // TODO remove from setup() const vatPowers = harden({ ...baseVP, testLog }); + const dispatch = setup(syscall, state, helpers, vatPowers, vatParameters); - const meterRecord = null; - const manager = finish(dispatch, meterRecord); - return manager; + return finish(dispatch); } - async function createFromBundle(vatID, bundle, managerOptions) { + async function createFromBundle( + vatID, + bundle, + managerOptions, + vatSyscallHandler, + ) { const { metered = false, enableDisavow = false, @@ -96,7 +95,21 @@ export function makeLocalVatManagerFactory(tools) { } = managerOptions; assert(vatConsole, 'vats need managerOptions.vatConsole'); - const { syscall, finish } = prepare(vatID, managerOptions); + let meterRecord = null; + if (metered) { + // fail-stop: we refill the meter after each crank (in vatManager + // doProcess()), but if the vat exhausts its meter within a single + // crank, it will never run again. We set refillEachCrank:false because + // we want doProcess to do the refilling itself, so it can count the + // usage + meterRecord = makeGetMeter({ + refillEachCrank: false, + refillIfExhausted: false, + }); + } + + const { syscall, finish } = prepare(vatID, vatSyscallHandler, meterRecord); + const imVP = enableInternalMetering ? internalMeteringVP : {}; const vatPowers = harden({ ...baseVP, @@ -118,19 +131,6 @@ export function makeLocalVatManagerFactory(tools) { liveSlotsConsole, ); - let meterRecord = null; - if (metered) { - // fail-stop: we refill the meter after each crank (in vatManager - // doProcess()), but if the vat exhausts its meter within a single - // crank, it will never run again. We set refillEachCrank:false because - // we want doProcess to do the refilling itself, so it can count the - // usage - meterRecord = makeGetMeter({ - refillEachCrank: false, - refillIfExhausted: false, - }); - } - const endowments = harden({ ...vatEndowments, ...ls.vatGlobals, @@ -160,10 +160,7 @@ export function makeLocalVatManagerFactory(tools) { } else if (enableSetup) { const setup = vatNS.default; assert(setup, X`vat source bundle lacks (default) setup() function`); - assert( - setup instanceof Function, - `vat source bundle default export is not a function`, - ); + assert.typeof(setup, 'function'); const helpers = harden({}); // DEPRECATED, todo remove from setup() const state = null; // TODO remove from setup() dispatch = setup(syscall, state, helpers, vatPowers, vatParameters); @@ -171,8 +168,7 @@ export function makeLocalVatManagerFactory(tools) { assert.fail(X`vat source bundle lacks buildRootObject() function`); } - const manager = finish(dispatch, meterRecord); - return manager; + return finish(dispatch); } const localVatManagerFactory = harden({ diff --git a/packages/SwingSet/src/kernel/vatManager/manager-nodeworker.js b/packages/SwingSet/src/kernel/vatManager/manager-nodeworker.js index 9ce8f3675c3..39f18411fd5 100644 --- a/packages/SwingSet/src/kernel/vatManager/manager-nodeworker.js +++ b/packages/SwingSet/src/kernel/vatManager/manager-nodeworker.js @@ -1,9 +1,8 @@ +// @ts-check // import { Worker } from 'worker_threads'; // not from a Compartment import { assert, details as X } from '@agoric/assert'; import { makePromiseKit } from '@agoric/promise-kit'; -import { makeTranscriptManager } from './transcript'; - -import { createSyscall } from './syscall'; +import { makeManagerKit } from './manager-helper'; // start a "Worker" (Node's tool for starting new threads) and load a bundle // into it @@ -22,10 +21,21 @@ function parentLog(first, ...args) { // console.error(`--parent: ${first}`, ...args); } +/** @typedef { import ('worker_threads').Worker } Worker */ + +/** + * @param {{ + * makeNodeWorker: () => Worker, + * kernelKeeper: KernelKeeper, + * kernelSlog: KernelSlog, + * testLog: (...args: unknown[]) => void, + * }} tools + * @returns { VatManagerFactory } + */ export function makeNodeWorkerVatManagerFactory(tools) { - const { makeNodeWorker, kernelKeeper, testLog } = tools; + const { makeNodeWorker, kernelKeeper, kernelSlog, testLog } = tools; - function createFromBundle(vatID, bundle, managerOptions) { + function createFromBundle(vatID, bundle, managerOptions, vatSyscallHandler) { const { vatParameters, virtualObjectCacheSize, @@ -41,35 +51,17 @@ export function makeNodeWorkerVatManagerFactory(tools) { `node-worker does not support enableInternalMetering, ignoring`, ); } - const vatKeeper = kernelKeeper.getVatKeeper(vatID); - const transcriptManager = makeTranscriptManager(vatKeeper, vatID); - - // prepare to accept syscalls from the worker - - // TODO: make the worker responsible for checking themselves: we send - // both the delivery and the expected syscalls, and the supervisor - // compares what the bundle does with what it was told to expect. - // Modulo flow control, we just stream transcript entries at the - // worker and eventually get back an "ok" or an error. When we do - // that, doSyscall won't even see replayed syscalls from the worker. - const { doSyscall, setVatSyscallHandler } = createSyscall( - transcriptManager, + // We use workerCanBlock=false because we get syscalls via an async + // postMessage from the worker thread, whose vat code has moved on (it + // really wants a synchronous/immediate syscall) + const mk = makeManagerKit( + vatID, + kernelSlog, + kernelKeeper, + vatSyscallHandler, + false, ); - function handleSyscall(vatSyscallObject) { - // we are invoked by an async postMessage from the worker thread, whose - // vat code has moved on (it really wants a synchronous/immediate - // syscall) - const type = vatSyscallObject[0]; - assert( - type !== 'callNow', - X`nodeWorker cannot block, cannot use syscall.callNow`, - ); - // This might throw an Error if the syscall was faulty, in which case - // the vat will be terminated soon. It returns a vatSyscallResults, - // which we discard because there is nobody to send it to. - doSyscall(vatSyscallObject); - } // start the worker and establish a connection @@ -95,11 +87,11 @@ export function makeNodeWorkerVatManagerFactory(tools) { } else if (type === 'dispatchReady') { parentLog(`dispatch() ready`); // wait10ms().then(dispatchIsReady); // stall to let logs get printed - dispatchIsReady(); + dispatchIsReady(undefined); } else if (type === 'syscall') { parentLog(`syscall`, args); - const vatSyscallObject = args; - handleSyscall(vatSyscallObject); + const [vatSyscallObject] = args; + mk.syscallFromWorker(vatSyscallObject); } else if (type === 'testLog') { testLog(...args); } else if (type === 'deliverDone') { @@ -107,8 +99,8 @@ export function makeNodeWorkerVatManagerFactory(tools) { if (waiting) { const resolve = waiting; waiting = null; - const deliveryResult = args; - resolve(deliveryResult); + const [vatDeliveryResults] = args; + resolve(vatDeliveryResults); } } else { parentLog(`unrecognized uplink message ${type}`); @@ -128,41 +120,23 @@ export function makeNodeWorkerVatManagerFactory(tools) { enableDisavow, ]); - function deliver(delivery) { + function deliverToWorker(delivery) { parentLog(`sending delivery`, delivery); assert(!waiting, X`already waiting for delivery`); const pr = makePromiseKit(); waiting = pr.resolve; - sendToWorker(['deliver', ...delivery]); + sendToWorker(['deliver', delivery]); return pr.promise; } - - async function replayTranscript() { - transcriptManager.startReplay(); - for (const t of vatKeeper.getTranscript()) { - transcriptManager.checkReplayError(); - transcriptManager.startReplayDelivery(t.syscalls); - // eslint-disable-next-line no-await-in-loop - await deliver(t.d); - } - transcriptManager.checkReplayError(); - transcriptManager.finishReplay(); - } + mk.setDeliverToWorker(deliverToWorker); function shutdown() { // this returns a Promise that fulfills with 1 if we used // worker.terminate(), otherwise with the `exitCode` passed to // `process.exit(exitCode)` within the worker. - return worker.terminate(); + return worker.terminate().then(_ => undefined); } - - const manager = harden({ - replayTranscript, - setVatSyscallHandler, - deliver, - shutdown, - }); - + const manager = mk.getManager(shutdown); return dispatchReadyP.then(() => manager); } diff --git a/packages/SwingSet/src/kernel/vatManager/manager-subprocess-node.js b/packages/SwingSet/src/kernel/vatManager/manager-subprocess-node.js index f3063d3ae37..ff4f6e98eac 100644 --- a/packages/SwingSet/src/kernel/vatManager/manager-subprocess-node.js +++ b/packages/SwingSet/src/kernel/vatManager/manager-subprocess-node.js @@ -2,31 +2,20 @@ import { assert, details as X } from '@agoric/assert'; import { makePromiseKit } from '@agoric/promise-kit'; -import { makeTranscriptManager } from './transcript'; - -import { createSyscall } from './syscall'; +import { makeManagerKit } from './manager-helper'; // start a "Worker" (Node's tool for starting new threads) and load a bundle // into it -/* -import { waitUntilQuiescent } from '../../waitUntilQuiescent'; -function wait10ms() { - const { promise: queueEmptyP, resolve } = makePromiseKit(); - setTimeout(() => resolve(), 10); - return queueEmptyP; -} -*/ - // eslint-disable-next-line no-unused-vars function parentLog(first, ...args) { // console.error(`--parent: ${first}`, ...args); } export function makeNodeSubprocessFactory(tools) { - const { startSubprocessWorker, kernelKeeper, testLog } = tools; + const { startSubprocessWorker, kernelKeeper, kernelSlog, testLog } = tools; - function createFromBundle(vatID, bundle, managerOptions) { + function createFromBundle(vatID, bundle, managerOptions, vatSyscallHandler) { const { vatParameters, virtualObjectCacheSize, @@ -42,37 +31,14 @@ export function makeNodeSubprocessFactory(tools) { `node-worker does not support enableInternalMetering, ignoring`, ); } - const vatKeeper = kernelKeeper.getVatKeeper(vatID); - const transcriptManager = makeTranscriptManager(vatKeeper, vatID); - - // prepare to accept syscalls from the worker - - // TODO: make the worker responsible for checking themselves: we send - // both the delivery and the expected syscalls, and the supervisor - // compares what the bundle does with what it was told to expect. - // Modulo flow control, we just stream transcript entries at the - // worker and eventually get back an "ok" or an error. When we do - // that, doSyscall won't even see replayed syscalls from the worker. - const { doSyscall, setVatSyscallHandler } = createSyscall( - transcriptManager, + const mk = makeManagerKit( + vatID, + kernelSlog, + kernelKeeper, + vatSyscallHandler, + false, ); - function handleSyscall(vatSyscallObject) { - // We are currently invoked by an async piped from the worker thread, - // whose vat code has moved on (it really wants a synchronous/immediate - // syscall). TODO: unlike threads, subprocesses could be made to wait - // by doing a blocking read from the pipe, so we could fix this, and - // re-enable syscall.callNow - const type = vatSyscallObject[0]; - assert( - type !== 'callNow', - X`nodeWorker cannot block, cannot use syscall.callNow`, - ); - // This might throw an Error if the syscall was faulty, in which case - // the vat will be terminated soon. It returns a vatSyscallResults, - // which we discard because there is currently nobody to send it to. - doSyscall(vatSyscallObject); - } // start the worker and establish a connection const { fromChild, toChild, terminate, done } = startSubprocessWorker(); @@ -82,12 +48,33 @@ export function makeNodeSubprocessFactory(tools) { toChild.write(msg); } + // TODO: make the worker responsible for checking themselves: we send + // both the delivery and the expected syscalls, and the supervisor + // compares what the bundle does with what it was told to expect. + // Modulo flow control, we just stream transcript entries at the + // worker and eventually get back an "ok" or an error. When we do + // that, doSyscall won't even see replayed syscalls from the worker. + const { promise: dispatchReadyP, resolve: dispatchIsReady, } = makePromiseKit(); let waiting; + /** + * @param { VatDeliveryObject } delivery + * @returns { Promise } + */ + function deliverToWorker(delivery) { + parentLog(`sending delivery`, delivery); + assert(!waiting, X`already waiting for delivery`); + const pr = makePromiseKit(); + waiting = pr.resolve; + sendToWorker(['deliver', delivery]); + return pr.promise; + } + mk.setDeliverToWorker(deliverToWorker); + function handleUpstream([type, ...args]) { parentLog(`received`, type); if (type === 'setUplinkAck') { @@ -100,8 +87,8 @@ export function makeNodeSubprocessFactory(tools) { dispatchIsReady(); } else if (type === 'syscall') { parentLog(`syscall`, args); - const vatSyscallObject = args; - handleSyscall(vatSyscallObject); + const [vatSyscallObject] = args; + mk.syscallFromWorker(vatSyscallObject); } else if (type === 'testLog') { testLog(...args); } else if (type === 'deliverDone') { @@ -109,8 +96,8 @@ export function makeNodeSubprocessFactory(tools) { if (waiting) { const resolve = waiting; waiting = null; - const deliveryResult = args; - resolve(deliveryResult); + const [vatDeliveryResults] = args; + resolve(vatDeliveryResults); } } else { parentLog(`unrecognized uplink message ${type}`); @@ -128,39 +115,11 @@ export function makeNodeSubprocessFactory(tools) { enableDisavow, ]); - function deliver(delivery) { - parentLog(`sending delivery`, delivery); - assert(!waiting, X`already waiting for delivery`); - const pr = makePromiseKit(); - waiting = pr.resolve; - sendToWorker(['deliver', ...delivery]); - return pr.promise; - } - - async function replayTranscript() { - transcriptManager.startReplay(); - for (const t of vatKeeper.getTranscript()) { - transcriptManager.checkReplayError(); - transcriptManager.startReplayDelivery(t.syscalls); - // eslint-disable-next-line no-await-in-loop - await deliver(t.d); - } - transcriptManager.checkReplayError(); - transcriptManager.finishReplay(); - } - function shutdown() { terminate(); - return done; + return done.then(_ => undefined); } - - const manager = harden({ - replayTranscript, - setVatSyscallHandler, - deliver, - shutdown, - }); - + const manager = mk.getManager(shutdown); return dispatchReadyP.then(() => manager); } diff --git a/packages/SwingSet/src/kernel/vatManager/manager-subprocess-xsnap.js b/packages/SwingSet/src/kernel/vatManager/manager-subprocess-xsnap.js index c12ed69ead4..1aa9f492528 100644 --- a/packages/SwingSet/src/kernel/vatManager/manager-subprocess-xsnap.js +++ b/packages/SwingSet/src/kernel/vatManager/manager-subprocess-xsnap.js @@ -1,8 +1,8 @@ // @ts-check import { assert, details as X } from '@agoric/assert'; -import { makeTranscriptManager } from './transcript'; -import { createSyscall } from './syscall'; +import { makeManagerKit } from './manager-helper'; +import { insistVatSyscallObject, insistVatDeliveryResult } from '../../message'; import '../../types'; import './types'; @@ -18,6 +18,7 @@ const decoder = new TextDecoder(); * @param {{ * allVatPowers: VatPowers, * kernelKeeper: KernelKeeper, + * kernelSlog: KernelSlog, * startXSnap: (name: string, handleCommand: SyncHandler) => Promise, * testLog: (...args: unknown[]) => void, * }} tools @@ -26,21 +27,26 @@ const decoder = new TextDecoder(); * @typedef { { moduleFormat: 'getExport', source: string } } ExportBundle * @typedef { (msg: Uint8Array) => Uint8Array } SyncHandler * @typedef { ReturnType } XSnap - * @typedef { ReturnType } KernelKeeper - * @typedef { ReturnType } VatManagerFactory */ export function makeXsSubprocessFactory({ allVatPowers: { transformTildot }, kernelKeeper, + kernelSlog, startXSnap, testLog, }) { /** - * @param { unknown } vatID + * @param { string } vatID * @param { unknown } bundle * @param { ManagerOptions } managerOptions + * @param { (vso: VatSyscallObject) => VatSyscallResult } vatSyscallHandler */ - async function createFromBundle(vatID, bundle, managerOptions) { + async function createFromBundle( + vatID, + bundle, + managerOptions, + vatSyscallHandler, + ) { parentLog(vatID, 'createFromBundle', { vatID }); const { vatParameters, @@ -59,26 +65,23 @@ export function makeXsSubprocessFactory({ // stops doing that, turn this into a regular assert console.log(`xsnap worker does not support enableInternalMetering`); } - const vatKeeper = kernelKeeper.getVatKeeper(vatID); - const transcriptManager = makeTranscriptManager(vatKeeper, vatID); - - const { doSyscall, setVatSyscallHandler } = createSyscall( - transcriptManager, + const mk = makeManagerKit( + vatID, + kernelSlog, + kernelKeeper, + vatSyscallHandler, + true, ); - /** @type { (vatSyscallObject: Tagged) => unknown } */ - function handleSyscall(vatSyscallObject) { - return doSyscall(vatSyscallObject); - } - /** @type { (item: Tagged) => unknown } */ function handleUpstream([type, ...args]) { parentLog(vatID, `handleUpstream`, type, args.length); switch (type) { case 'syscall': { parentLog(vatID, `syscall`, args[0], args.length); - const [scTag, ...vatSyscallArgs] = args; - return handleSyscall([scTag, ...vatSyscallArgs]); + const vso = args[0]; + insistVatSyscallObject(vso); + return mk.syscallFromWorker(vso); } case 'console': { const [level, tag, ...rest] = args; @@ -142,49 +145,29 @@ export function makeXsSubprocessFactory({ assert.fail(X`failed to setBundle: ${bundleReply}`); } - /** @type { (item: Tagged) => Promise } */ - async function deliver(delivery) { + /** + * @param { VatDeliveryObject} delivery + * @returns { Promise } + */ + async function deliverToWorker(delivery) { parentLog(vatID, `sending delivery`, delivery); - transcriptManager.startDispatch(delivery); - const result = await issueTagged(['deliver', ...delivery]); + const result = await issueTagged(['deliver', delivery]); parentLog(vatID, `deliverDone`, result.reply[0], result.reply.length); - transcriptManager.finishDispatch(); - // Attach the meterUsage to the deliver result. - /** @type {Tagged} */ - const deliverResult = [ + const deliverResult = harden([ result.reply[0], // 'ok' or 'error' result.reply[1] || null, // problem or null result.meterUsage || null, // meter usage statistics or null - ]; - return harden(deliverResult); - } - - async function replayTranscript() { - transcriptManager.startReplay(); - for (const t of vatKeeper.getTranscript()) { - transcriptManager.checkReplayError(); - transcriptManager.startReplayDelivery(t.syscalls); - // eslint-disable-next-line no-await-in-loop - await deliver(t.d); - } - transcriptManager.checkReplayError(); - transcriptManager.finishReplay(); + ]); + insistVatDeliveryResult(deliverResult); + return deliverResult; } + mk.setDeliverToWorker(deliverToWorker); function shutdown() { - return worker.close(); + return worker.close().then(_ => undefined); } - - const manager = harden({ - replayTranscript, - setVatSyscallHandler, - deliver, - shutdown, - }); - - parentLog(vatID, 'manager', Object.keys(manager)); - return manager; + return mk.getManager(shutdown); } return harden({ createFromBundle }); diff --git a/packages/SwingSet/src/kernel/vatManager/supervisor-helper.js b/packages/SwingSet/src/kernel/vatManager/supervisor-helper.js new file mode 100644 index 00000000000..ea4b9aae40d --- /dev/null +++ b/packages/SwingSet/src/kernel/vatManager/supervisor-helper.js @@ -0,0 +1,187 @@ +// @ts-check +import { assert } from '@agoric/assert'; +import { insistVatSyscallObject, insistVatSyscallResult } from '../../message'; +import '../../types'; + +/** + * @typedef { (delivery: VatDeliveryObject) => (VatDeliveryResult | Promise) } VatDispatcherSyncAsync + * @typedef { (delivery: VatDeliveryObject) => Promise } VatDispatcher + * @typedef { { refill: () => unknown, isExhausted: () => null | Error } } MeterRecord + * @typedef { { stopGlobalMeter: () => void, meterRecord: MeterRecord, refillAllMeters: () => void } } DispatchMeteringTools + */ + +/** + * Given the liveslots 'dispatch' function, return a version that never + * rejects. It will always return a VatDeliveryResult, even if liveslots + * throws or rejects. All supervisors should wrap the liveslots `dispatch` + * function with this one, and call it in response to messages from the + * manager process. + * + * @param { VatDispatcherSyncAsync } dispatch + * @param { WaitUntilQuiescent } waitUntilQuiescent + * @returns { VatDispatcher } + */ +function makeSupervisorDispatch(dispatch, waitUntilQuiescent) { + assert.typeof(waitUntilQuiescent, 'function'); + /** + * @param { VatDeliveryObject } delivery + * @returns { Promise } + * + */ + async function dispatchToVat(delivery) { + // the (low-level) vat is not currently responsible for giving up agency: + // we enforce that ourselves for now + Promise.resolve(delivery) + .then(dispatch) + .catch(err => + console.log(`error ${err} during vat dispatch of ${delivery}`), + ); + await waitUntilQuiescent(); + return harden(['ok', null, null]); + } + + return harden(dispatchToVat); +} +harden(makeSupervisorDispatch); +export { makeSupervisorDispatch }; + +/** + * @param { VatDeliveryResult } status + * @param { DispatchMeteringTools } mtools + * @returns { VatDeliveryResult } + */ + +function processLocalMeters(status, mtools) { + const { stopGlobalMeter, meterRecord, refillAllMeters } = mtools; + stopGlobalMeter(); + /** @type VatDeliveryResult */ + status = [...status]; // mutable copy, to add usage + if (status[0] === 'ok') { + // refill this vat's meter, if any, accumulating its usage for stats + if (meterRecord) { + // note that refill() won't actually refill an exhausted meter + const meterUsage = meterRecord.refill(); + const exhaustionError = meterRecord.isExhausted(); + if (exhaustionError) { + status = ['error', exhaustionError.message, meterUsage]; + } else { + // We will have ['ok', null, meterUsage] + status[2] = meterUsage; + // TODO: accumulate used.allocate and used.compute into vatStats + // updateStats(status[2]); + } + } + // refill all within-vat -created meters + refillAllMeters(); + } + return harden(status); +} + +/** + * Given an async 'dispatch' function (like the return value of + * makeSupervisorDispatch), return a version that manages the + * '@agoric/tame-metering' -style meters. + * + * @param { VatDispatcher } dispatchToVat + * @param { DispatchMeteringTools } mtools + * @returns { VatDispatcher } + */ + +function makeMeteredDispatch(dispatchToVat, mtools) { + async function deliver(delivery) { + const status = await dispatchToVat(delivery); + // TODO: is there any chance the confined code could trigger a metering + // fault in a global after it loses agency but before we get to + // stopGlobalMeter() ? + return processLocalMeters(status, mtools); + } + return harden(deliver); +} +harden(makeMeteredDispatch); +export { makeMeteredDispatch }; + +/** + * This returns a function that is provided to liveslots as the 'syscall' + * argument: an object with one method per syscall type. These methods return + * data, or nothing. If the kernel experiences a problem executing the syscall, + * the method will throw, or the kernel will kill the vat, or both. + * + * I should be given a `syscallToManager` function that accepts a + * VatSyscallObject and (synchronously) returns a VatSyscallResult. + * + * @param { VatSyscaller } syscallToManager + * @param { boolean } workerCanBlock + * @typedef { unknown } TheSyscallObjectWithMethodsThatLiveslotsWants + * @returns { TheSyscallObjectWithMethodsThatLiveslotsWants } + */ +function makeSupervisorSyscall(syscallToManager, workerCanBlock) { + /** @type { (fields: unknown[]) => (null | string | SwingSetCapData) } */ + function doSyscall(fields) { + insistVatSyscallObject(fields); + /** @type { VatSyscallObject } */ + const vso = harden(fields); + let r; + try { + r = syscallToManager(vso); + } catch (err) { + console.log(`worker got error during syscall:`, err); + throw err; + } + if (!workerCanBlock) { + // we don't expect an answer + return null; + } + const vsr = r; + insistVatSyscallResult(vsr); + const [type, ...rest] = vsr; + switch (type) { + case 'ok': { + const [data] = rest; + return data; + } + case 'error': { + const [err] = rest; + throw Error(`syscall.${fields[0]} failed, prepare to die: ${err}`); + } + default: + throw Error(`unknown result type ${type}`); + } + } + + // this will be given to liveslots, it should have distinct methods that + // return immediate results or throw errors + const syscallForVat = { + /** @type {(target: string, method: string, args: SwingSetCapData, result?: string) => unknown } */ + send: (target, method, args, result) => + doSyscall(['send', target, { method, args, result }]), + subscribe: vpid => doSyscall(['subscribe', vpid]), + resolve: resolutions => doSyscall(['resolve', resolutions]), + exit: (isFailure, data) => doSyscall(['exit', isFailure, data]), + dropImports: vrefs => doSyscall(['dropImports', vrefs]), + + // These syscalls should be omitted if the worker cannot get a + // synchronous return value back from the kernel, such as when the worker + // is in a child process or thread, and cannot be blocked until the + // result gets back. vatstoreSet and vatstoreDelete are included because + // vatstoreSet requires a result, and we offer them as a group. + callNow: (target, method, args) => + doSyscall(['callNow', target, method, args]), + vatstoreGet: key => doSyscall(['vatstoreGet', key]), + vatstoreSet: (key, value) => doSyscall(['vatstoreSet', key, value]), + vatstoreDelete: key => doSyscall(['vatstoreDelete', key]), + }; + + const blocking = ['callNow', 'vatstoreGet', 'vatstoreSet', 'vatstoreDelete']; + + if (!workerCanBlock) { + for (const name of blocking) { + const err = `this non-blocking worker transport cannot syscall.${name}`; + syscallForVat[name] = () => assert.fail(err); + } + } + + return harden(syscallForVat); +} + +harden(makeSupervisorSyscall); +export { makeSupervisorSyscall }; diff --git a/packages/SwingSet/src/kernel/vatManager/supervisor-nodeworker.js b/packages/SwingSet/src/kernel/vatManager/supervisor-nodeworker.js index ea21d14eeac..8ad836eeacf 100644 --- a/packages/SwingSet/src/kernel/vatManager/supervisor-nodeworker.js +++ b/packages/SwingSet/src/kernel/vatManager/supervisor-nodeworker.js @@ -1,15 +1,23 @@ +// @ts-check // this file is loaded at the start of a new Worker, which makes it a new JS // environment (with it's own Realm), so we must install-ses too. import '@agoric/install-ses'; import { parentPort } from 'worker_threads'; import anylogger from 'anylogger'; +import '../../types'; import { assert, details as X } from '@agoric/assert'; import { importBundle } from '@agoric/import-bundle'; import { makeMarshal } from '@agoric/marshal'; import { WeakRef, FinalizationRegistry } from '../../weakref'; import { waitUntilQuiescent } from '../../waitUntilQuiescent'; import { makeLiveSlots } from '../liveSlots'; +import { + makeSupervisorDispatch, + makeSupervisorSyscall, +} from './supervisor-helper'; + +assert(parentPort, 'parentPort somehow missing, am I not a Worker?'); // eslint-disable-next-line no-unused-vars function workerLog(first, ...args) { @@ -27,48 +35,14 @@ function makeConsole(tag) { return harden(cons); } -function runAndWait(f, errmsg) { - Promise.resolve() - .then(f) - .then(undefined, err => workerLog(`doProcess: ${errmsg}:`, err)); - return waitUntilQuiescent(); -} - function sendUplink(msg) { assert(msg instanceof Array, X`msg must be an Array`); + assert(parentPort, 'parentPort somehow missing, am I not a Worker?'); parentPort.postMessage(msg); } let dispatch; -async function doProcess(dispatchRecord, errmsg) { - const dispatchOp = dispatchRecord[0]; - const dispatchArgs = dispatchRecord.slice(1); - workerLog(`runAndWait`); - await runAndWait(() => dispatch[dispatchOp](...dispatchArgs), errmsg); - workerLog(`doProcess done`); - const vatDeliveryResults = harden(['ok']); - return vatDeliveryResults; -} - -function doMessage(targetSlot, msg) { - const errmsg = `vat[${targetSlot}].${msg.method} dispatch failed`; - return doProcess( - ['deliver', targetSlot, msg.method, msg.args, msg.result], - errmsg, - ); -} - -function doNotify(resolutions) { - const errmsg = `vat.notify failed`; - return doProcess(['notify', resolutions], errmsg); -} - -function doDropExports(vrefs) { - const errmsg = `vat.doDropExport failed`; - return doProcess(['dropExports', vrefs], errmsg); -} - parentPort.on('message', ([type, ...margs]) => { workerLog(`received`, type); if (type === 'start') { @@ -87,23 +61,14 @@ parentPort.on('message', ([type, ...margs]) => { sendUplink(['testLog', ...args]); } - function doSyscall(vatSyscallObject) { - sendUplink(['syscall', ...vatSyscallObject]); + /** @type VatSyscaller */ + function syscallToManager(vatSyscallObject) { + sendUplink(['syscall', vatSyscallObject]); + // we can't block for a result, so we always tell the vat that the + // syscall was successful, and never has any result data + return ['ok', null]; } - const syscall = harden({ - send: (...args) => doSyscall(['send', ...args]), - callNow: (..._args) => { - assert.fail(X`nodeWorker cannot syscall.callNow`); - }, - subscribe: (...args) => doSyscall(['subscribe', ...args]), - resolve: (...args) => doSyscall(['resolve', ...args]), - exit: (...args) => doSyscall(['exit', ...args]), - vatstoreGet: (...args) => doSyscall(['vatstoreGet', ...args]), - vatstoreSet: (...args) => doSyscall(['vatstoreSet', ...args]), - vatstoreDelete: (...args) => doSyscall(['vatstoreDelete', ...args]), - dropImports: (...args) => doSyscall(['dropImports', ...args]), - }); - + const syscall = makeSupervisorSyscall(syscallToManager, false); const vatID = 'demo-vatID'; // todo: maybe add transformTildot, makeGetMeter/transformMetering to // vatPowers, but only if options tell us they're wanted. Maybe @@ -113,7 +78,11 @@ parentPort.on('message', ([type, ...margs]) => { makeMarshal, testLog, }; - const gcTools = harden({ WeakRef, FinalizationRegistry }); + const gcTools = harden({ + WeakRef, + FinalizationRegistry, + waitUntilQuiescent, + }); const ls = makeLiveSlots( syscall, vatID, @@ -134,7 +103,7 @@ parentPort.on('message', ([type, ...margs]) => { workerLog(`got vatNS:`, Object.keys(vatNS).join(',')); sendUplink(['gotBundle']); ls.setBuildRootObject(vatNS.buildRootObject); - dispatch = ls.dispatch; + dispatch = makeSupervisorDispatch(ls.dispatch, waitUntilQuiescent); workerLog(`got dispatch:`, Object.keys(dispatch).join(',')); sendUplink(['dispatchReady']); }); @@ -143,16 +112,10 @@ parentPort.on('message', ([type, ...margs]) => { workerLog(`error: deliver before dispatchReady`); return; } - const [dtype, ...dargs] = margs; - if (dtype === 'message') { - doMessage(...dargs).then(res => sendUplink(['deliverDone', ...res])); - } else if (dtype === 'notify') { - doNotify(...dargs).then(res => sendUplink(['deliverDone', ...res])); - } else if (dtype === 'dropExports') { - doDropExports(...dargs).then(res => sendUplink(['deliverDone', ...res])); - } else { - assert.fail(X`bad delivery type ${dtype}`); - } + const [vatDeliveryObject] = margs; + dispatch(vatDeliveryObject).then(vatDeliveryResults => + sendUplink(['deliverDone', vatDeliveryResults]), + ); } else { workerLog(`unrecognized downlink message ${type}`); } diff --git a/packages/SwingSet/src/kernel/vatManager/supervisor-subprocess-node.js b/packages/SwingSet/src/kernel/vatManager/supervisor-subprocess-node.js index fddfe8d1902..8a51e21b57f 100644 --- a/packages/SwingSet/src/kernel/vatManager/supervisor-subprocess-node.js +++ b/packages/SwingSet/src/kernel/vatManager/supervisor-subprocess-node.js @@ -15,6 +15,10 @@ import { } from '../../netstring'; import { waitUntilQuiescent } from '../../waitUntilQuiescent'; import { makeLiveSlots } from '../liveSlots'; +import { + makeSupervisorDispatch, + makeSupervisorSyscall, +} from './supervisor-helper'; // eslint-disable-next-line no-unused-vars function workerLog(first, ...args) { @@ -32,43 +36,8 @@ function makeConsole(tag) { return harden(cons); } -function runAndWait(f, errmsg) { - Promise.resolve() - .then(f) - .then(undefined, err => workerLog(`doProcess: ${errmsg}:`, err)); - return waitUntilQuiescent(); -} - let dispatch; -async function doProcess(dispatchRecord, errmsg) { - const dispatchOp = dispatchRecord[0]; - const dispatchArgs = dispatchRecord.slice(1); - workerLog(`runAndWait`); - await runAndWait(() => dispatch[dispatchOp](...dispatchArgs), errmsg); - workerLog(`doProcess done`); - const vatDeliveryResults = harden(['ok']); - return vatDeliveryResults; -} - -function doMessage(targetSlot, msg) { - const errmsg = `vat[${targetSlot}].${msg.method} dispatch failed`; - return doProcess( - ['deliver', targetSlot, msg.method, msg.args, msg.result], - errmsg, - ); -} - -function doNotify(resolutions) { - const errmsg = `vat.notify failed`; - return doProcess(['notify', resolutions], errmsg); -} - -function doDropExports(vrefs) { - const errmsg = `vat.doDropExport failed`; - return doProcess(['dropExports', vrefs], errmsg); -} - const toParent = arrayEncoderStream(); toParent .pipe(netstringEncoderStream()) @@ -107,23 +76,12 @@ fromParent.on('data', ([type, ...margs]) => { sendUplink(['testLog', ...args]); } - function doSyscall(vatSyscallObject) { - sendUplink(['syscall', ...vatSyscallObject]); + // syscallToManager can throw or return OK/ERR + function syscallToManager(vatSyscallObject) { + sendUplink(['syscall', vatSyscallObject]); } - const syscall = harden({ - send: (...args) => doSyscall(['send', ...args]), - callNow: (..._args) => { - assert.fail(X`nodeWorker cannot syscall.callNow`); - }, - subscribe: (...args) => doSyscall(['subscribe', ...args]), - resolve: (...args) => doSyscall(['resolve', ...args]), - exit: (...args) => doSyscall(['exit', ...args]), - vatstoreGet: (...args) => doSyscall(['vatstoreGet', ...args]), - vatstoreSet: (...args) => doSyscall(['vatstoreSet', ...args]), - vatstoreDelete: (...args) => doSyscall(['vatstoreDelete', ...args]), - dropImports: (...args) => doSyscall(['dropImports', ...args]), - }); - + // this 'syscall' throws or returns data + const syscall = makeSupervisorSyscall(syscallToManager, false); const vatID = 'demo-vatID'; // todo: maybe add transformTildot, makeGetMeter/transformMetering to // vatPowers, but only if options tell us they're wanted. Maybe @@ -133,7 +91,11 @@ fromParent.on('data', ([type, ...margs]) => { makeMarshal, testLog, }; - const gcTools = harden({ WeakRef, FinalizationRegistry }); + const gcTools = harden({ + WeakRef, + FinalizationRegistry, + waitUntilQuiescent, + }); const ls = makeLiveSlots( syscall, vatID, @@ -154,7 +116,7 @@ fromParent.on('data', ([type, ...margs]) => { workerLog(`got vatNS:`, Object.keys(vatNS).join(',')); sendUplink(['gotBundle']); ls.setBuildRootObject(vatNS.buildRootObject); - dispatch = ls.dispatch; + dispatch = makeSupervisorDispatch(ls.dispatch, waitUntilQuiescent); workerLog(`got dispatch:`, Object.keys(dispatch).join(',')); sendUplink(['dispatchReady']); }); @@ -163,16 +125,10 @@ fromParent.on('data', ([type, ...margs]) => { workerLog(`error: deliver before dispatchReady`); return; } - const [dtype, ...dargs] = margs; - if (dtype === 'message') { - doMessage(...dargs).then(res => sendUplink(['deliverDone', ...res])); - } else if (dtype === 'notify') { - doNotify(...dargs).then(res => sendUplink(['deliverDone', ...res])); - } else if (dtype === 'dropExports') { - doDropExports(...dargs).then(res => sendUplink(['deliverDone', ...res])); - } else { - assert.fail(X`bad delivery type ${dtype}`); - } + const [vatDeliveryObject] = margs; + dispatch(vatDeliveryObject).then(vatDeliveryResults => + sendUplink(['deliverDone', vatDeliveryResults]), + ); } else { workerLog(`unrecognized downlink message ${type}`); } diff --git a/packages/SwingSet/src/kernel/vatManager/supervisor-subprocess-xsnap.js b/packages/SwingSet/src/kernel/vatManager/supervisor-subprocess-xsnap.js index b5df65c5956..bf426ce16d5 100644 --- a/packages/SwingSet/src/kernel/vatManager/supervisor-subprocess-xsnap.js +++ b/packages/SwingSet/src/kernel/vatManager/supervisor-subprocess-xsnap.js @@ -1,12 +1,18 @@ -/* global globalThis */ +/* global globalThis WeakRef FinalizationRegistry */ // @ts-check import { assert, details as X } from '@agoric/assert'; import { importBundle } from '@agoric/import-bundle'; import { makeMarshal } from '@agoric/marshal'; +import '../../types'; // grumble... waitUntilQuiescent is exported and closes over ambient authority import { waitUntilQuiescent } from '../../waitUntilQuiescent'; +import { insistVatDeliveryObject, insistVatSyscallResult } from '../../message'; import { makeLiveSlots } from '../liveSlots'; +import { + makeSupervisorDispatch, + makeSupervisorSyscall, +} from './supervisor-helper'; const encoder = new TextEncoder(); const decoder = new TextDecoder(); @@ -102,61 +108,13 @@ function abbreviateReplacer(_, arg) { return arg; } -/** - * @param { (value: void) => void } f - * @param { string } errmsg - */ -function runAndWait(f, errmsg) { - Promise.resolve() - .then(f) - .catch(err => { - workerLog(`doProcess: ${errmsg}:`, err.message); - }); - return waitUntilQuiescent(); -} - /** * @param { ReturnType } port */ function makeWorker(port) { - /** @type { Record void> | null } */ + /** @type { ((delivery: VatDeliveryObject) => Promise) | null } */ let dispatch = null; - /** @type { (dr: Tagged, errmsg: string) => Promise } */ - async function doProcess(dispatchRecord, errmsg) { - assert(dispatch); - const theDispatch = dispatch; - const [dispatchOp, ...dispatchArgs] = dispatchRecord; - assert(typeof dispatchOp === 'string'); - workerLog(`runAndWait`); - await runAndWait(() => theDispatch[dispatchOp](...dispatchArgs), errmsg); - workerLog(`doProcess done`); - /** @type { Tagged } */ - const vatDeliveryResults = harden(['ok']); - return vatDeliveryResults; - } - - /** @type { (ts: unknown, msg: any) => Promise } */ - function doMessage(targetSlot, msg) { - const errmsg = `vat[${targetSlot}].${msg.method} dispatch failed`; - return doProcess( - ['deliver', targetSlot, msg.method, msg.args, msg.result], - errmsg, - ); - } - - /** @type { (rs: unknown) => Promise } */ - function doNotify(resolutions) { - const errmsg = `vat.notify failed`; - return doProcess(['notify', resolutions], errmsg); - } - - /** @type { (rs: unknown) => Promise } */ - function doDropExports(vrefs) { - const errmsg = `vat.dropExports failed`; - return doProcess(['dropExports', vrefs], errmsg); - } - /** * TODO: consider other methods per SES VirtualConsole. * See https://github.com/Agoric/agoric-sdk/issues/2146 @@ -199,25 +157,16 @@ function makeWorker(port) { virtualObjectCacheSize, enableDisavow, ) { - /** @type { (item: Tagged) => unknown } */ - function doSyscall(vatSyscallObject) { + /** @type { (vso: VatSyscallObject) => VatSyscallResult } */ + function syscallToManager(vatSyscallObject) { workerLog('doSyscall', vatSyscallObject); - const result = port.call(['syscall', ...vatSyscallObject]); + const result = port.call(['syscall', vatSyscallObject]); workerLog(' ... syscall result:', result); + insistVatSyscallResult(result); return result; } - const syscall = harden({ - send: (...args) => doSyscall(['send', ...args]), - callNow: (...args) => doSyscall(['callNow', ...args]), - subscribe: (...args) => doSyscall(['subscribe', ...args]), - resolve: (...args) => doSyscall(['resolve', ...args]), - exit: (...args) => doSyscall(['exit', ...args]), - vatstoreGet: (...args) => doSyscall(['vatstoreGet', ...args]), - vatstoreSet: (...args) => doSyscall(['vatstoreSet', ...args]), - vatstoreDelete: (...args) => doSyscall(['vatstoreDelete', ...args]), - dropImports: (...args) => doSyscall(['dropImports', ...args]), - }); + const syscall = makeSupervisorSyscall(syscallToManager, true); const vatPowers = { makeMarshal, @@ -238,7 +187,11 @@ function makeWorker(port) { ? virtualObjectCacheSize : undefined; - const gcTools = {}; // future expansion + const gcTools = harden({ + WeakRef, + FinalizationRegistry, + waitUntilQuiescent, + }); const ls = makeLiveSlots( syscall, @@ -260,9 +213,9 @@ function makeWorker(port) { const vatNS = await importBundle(bundle, { endowments }); workerLog(`got vatNS:`, Object.keys(vatNS).join(',')); ls.setBuildRootObject(vatNS.buildRootObject); - dispatch = ls.dispatch; - assert(dispatch); - workerLog(`got dispatch:`, Object.keys(dispatch).join(',')); + assert(ls.dispatch); + dispatch = makeSupervisorDispatch(ls.dispatch, waitUntilQuiescent); + workerLog(`got dispatch`); return ['dispatchReady']; } @@ -277,17 +230,9 @@ function makeWorker(port) { } case 'deliver': { assert(dispatch, 'cannot deliver before setBundle'); - const [dtype, ...dargs] = args; - switch (dtype) { - case 'message': - return doMessage(dargs[0], dargs[1]); - case 'notify': - return doNotify(dargs[0]); - case 'dropExports': - return doDropExports(dargs[0]); - default: - assert.fail(X`bad delivery type ${dtype}`); - } + const [vatDeliveryObject] = args; + insistVatDeliveryObject(vatDeliveryObject); + return dispatch(vatDeliveryObject); } default: workerLog('handleItem: bad tag', tag, args.length); diff --git a/packages/SwingSet/src/kernel/vatManager/syscall.js b/packages/SwingSet/src/kernel/vatManager/syscall.js deleted file mode 100644 index 58e0c9e4f2c..00000000000 --- a/packages/SwingSet/src/kernel/vatManager/syscall.js +++ /dev/null @@ -1,110 +0,0 @@ -// We use vat-centric terminology here, so "inbound" means "into a vat", -// generally from the kernel. We also have "comms vats" which use special -// device access to interact with remote machines: messages from those -// remote machines are "inbound" into the comms vats. Conversely "outbound" -// means "out of a vat", usually into the local kernel, but we also use -// "outbound" to describe the messages a comms vat is sending over a socket -// or other communications channel. - -// The mapVatSlotToKernelSlot() function is used to translate slot references -// on the vat->kernel pathway. mapKernelToVatSlot() is used for kernel->vat. - -// The terms "import" and "export" are also vat-centric. "import" means -// something a Vat has imported (from the kernel). Imports are tracked in a -// kernel-side table for each Vat, which is populated by the kernel as a -// message is delivered. Each import is represented inside the Vat as a -// Presence (at least when using liveSlots). - -// "exports" are callable objects inside the Vat which it has made -// available to the kernel (so that other vats can invoke it). The exports -// table is managed by userspace code inside the vat. The kernel tables map -// one vat's import IDs (o-NN) to a kernel object ID (koNN) in the -// vatKeeper's state.vatSlotToKernelSlot table. To make sure we use the -// same importID each time, we also need to keep a reverse table: -// kernelSlotToVatSlot maps them back. - -// Comms vats will have their own internal tables to track references -// shared with other machines. These will have mapInbound/mapOutbound too. -// A message arriving on a communication channel will pass through the -// comms vat's mapInbound to figure out which "machine export" is the -// target, which maps to a "vat import" (coming from the kernel). The -// arguments might also be machine exports (for arguments that are "coming -// home"), or more commonly will be new machine imports (for arguments that -// point to other machines, usually the sending machine). The machine -// imports will be presented to the kernel as exports of the comms vat. - -// The vat sees "vat slots" (object references) as the arguments of -// syscall/dispatch functions. These take on the following forms (where -// "NN" is an integer): - -// o+NN : an object ref allocated by this Vat, hence an export -// o-NN : an object ref allocated by the kernel, an imported object -// p-NN : a promise ref allocated by the kernel -// p+NN : (todo) a promise ref allocated by this vat -// d-NN : a device ref allocated by the kernel, imported - -// Within the kernel, we use "kernel slots", with the following forms: - -// koNN : an object reference -// kpNN : a promise reference -// kdNN : a device reference - -// The vatManager is responsible for translating vat slots into kernel -// slots on the outbound (syscall) path, and kernel slots back into vat -// slots on the inbound (dispatch) path. - -export function createSyscall(transcriptManager) { - let vatSyscallHandler; - function setVatSyscallHandler(handler) { - vatSyscallHandler = handler; - } - - /* vatSyscallObject is an array that starts with the syscall name ('send', - * 'subscribe', etc) followed by all the positional arguments of the - * syscall, designed for transport across a manager-worker link (serialized - * bytes over a socket or pipe, postMessage to an in-process Worker, or - * just direct). - */ - function doSyscall(vatSyscallObject) { - if (transcriptManager.inReplay()) { - // We're replaying old messages to bring the vat's internal state - // up-to-date. It will make syscalls like a puppy chasing rabbits in - // its sleep. Gently prevent their twitching paws from doing anything. - - // but if the puppy deviates one inch from previous twitches, explode - return transcriptManager.simulateSyscall(vatSyscallObject); - } - - const vres = vatSyscallHandler(vatSyscallObject); - // vres is ['error', reason] or ['ok', null] or ['ok', capdata] - const [successFlag, data] = vres; - if (successFlag === 'error') { - // Something went wrong, and we about to die. Either the kernel - // suffered a fault (and we'll be shut down momentarily along with - // everything else), or we reached for a clist entry that wasn't there - // (and we'll be terminated, but the kernel and all other vats will - // continue). Emit enough of an error message to explain the errors - // that are about to ensue on our way down. - throw Error( - `syscall ${vatSyscallObject[0]} suffered error, shutdown commencing`, - ); - } - // otherwise vres is ['ok', null] or ['ok', capdata] - transcriptManager.addSyscall(vatSyscallObject, data); - return data; - } - - const syscall = harden({ - send: (...args) => doSyscall(['send', ...args]), - callNow: (...args) => doSyscall(['callNow', ...args]), - subscribe: (...args) => doSyscall(['subscribe', ...args]), - resolve: (...args) => doSyscall(['resolve', ...args]), - exit: (...args) => doSyscall(['exit', ...args]), - vatstoreGet: (...args) => doSyscall(['vatstoreGet', ...args]), - vatstoreSet: (...args) => doSyscall(['vatstoreSet', ...args]), - vatstoreDelete: (...args) => doSyscall(['vatstoreDelete', ...args]), - dropImports: (...args) => doSyscall(['dropImports', ...args]), - }); - - return harden({ syscall, doSyscall, setVatSyscallHandler }); -} diff --git a/packages/SwingSet/src/kernel/vatTranslator.js b/packages/SwingSet/src/kernel/vatTranslator.js index a9343dd725a..b9de1d21bb1 100644 --- a/packages/SwingSet/src/kernel/vatTranslator.js +++ b/packages/SwingSet/src/kernel/vatTranslator.js @@ -108,8 +108,10 @@ function makeTranslateVatSyscallToKernelSyscall(vatID, kernelKeeper) { const vatKeeper = kernelKeeper.getVatKeeper(vatID); const { mapVatSlotToKernelSlot } = vatKeeper; - function translateSend(targetSlot, method, args, resultSlot) { + function translateSend(targetSlot, msg) { assert.typeof(targetSlot, 'string', 'non-string targetSlot'); + insistMessage(msg); + const { method, args, result: resultSlot } = msg; insistCapData(args); // TODO: disable send-to-self for now, qv issue #43 const target = mapVatSlotToKernelSlot(targetSlot); @@ -140,13 +142,13 @@ function makeTranslateVatSyscallToKernelSyscall(vatID, kernelKeeper) { // resolution authority now held by run-queue } - const msg = harden({ + const kmsg = harden({ method, args: kernelArgs, result, }); - insistMessage(msg); - const ks = harden(['send', target, msg]); + insistMessage(kmsg); + const ks = harden(['send', target, kmsg]); return ks; } diff --git a/packages/SwingSet/src/message.js b/packages/SwingSet/src/message.js index 810e495863f..dc9cba7a48c 100644 --- a/packages/SwingSet/src/message.js +++ b/packages/SwingSet/src/message.js @@ -7,12 +7,12 @@ import { insistCapData } from './capdata'; * string, a .args property that's a capdata object, and optionally a .result * property that, if present, must be a string. * - * @param {any} message The object to be tested + * @param {unknown} message The object to be tested * * @throws {Error} if, upon inspection, the parameter does not satisfy the above * criteria. * - * @returns {void} + * @returns { asserts message is Message } */ export function insistMessage(message) { assert.typeof( @@ -28,4 +28,160 @@ export function insistMessage(message) { X`message has non-string non-null .result ${message.result}`, ); } + return undefined; +} + +/** + * @param {unknown} vdo + * @returns { asserts vdo is VatDeliveryObject } + */ +export function insistVatDeliveryObject(vdo) { + assert(Array.isArray(vdo)); + const [type, ...rest] = vdo; + switch (type) { + case 'message': { + const [target, msg] = rest; + assert.typeof(target, 'string'); + insistMessage(msg); + break; + } + case 'notify': { + const [resolutions] = rest; + assert(Array.isArray(resolutions)); + for (const [vpid, rejected, data] of resolutions) { + assert.typeof(vpid, 'string'); + assert.typeof(rejected, 'boolean'); + insistCapData(data); + } + break; + } + case 'dropExports': { + const [slots] = rest; + assert(Array.isArray(slots)); + for (const slot of slots) { + assert.typeof(slot, 'string'); + } + break; + } + default: + assert.fail(`unknown delivery type ${type}`); + } + return undefined; +} + +/** + * @param {unknown} vdr + * @returns { asserts vdr is VatDeliveryResult } + */ +export function insistVatDeliveryResult(vdr) { + assert(Array.isArray(vdr)); + const [type, problem, _usage] = vdr; + switch (type) { + case 'ok': { + assert.equal(problem, null); + break; + } + case 'error': { + assert.typeof(problem, 'string'); + break; + } + default: + assert.fail(`unknown delivery result type ${type}`); + } + return undefined; +} + +/** + * + * @param {unknown} vso + * @returns { asserts vso is VatSyscallObject } + */ +export function insistVatSyscallObject(vso) { + assert(Array.isArray(vso)); + const [type, ...rest] = vso; + switch (type) { + case 'send': { + const [target, msg] = rest; + assert.typeof(target, 'string'); + insistMessage(msg); + break; + } + case 'callNow': { + const [target, method, args] = rest; + assert.typeof(target, 'string'); + assert.typeof(method, 'string'); + insistCapData(args); + break; + } + case 'subscribe': { + const [vpid] = rest; + assert.typeof(vpid, 'string'); + break; + } + case 'resolve': { + const [resolutions] = rest; + assert(Array.isArray(resolutions)); + for (const [vpid, rejected, data] of resolutions) { + assert.typeof(vpid, 'string'); + assert.typeof(rejected, 'boolean'); + insistCapData(data); + } + break; + } + case 'exit': { + const [isFailure, info] = rest; + assert.typeof(isFailure, 'boolean'); + insistCapData(info); + break; + } + case 'vatstoreGet': { + const [key] = rest; + assert.typeof(key, 'string'); + break; + } + case 'vatstoreSet': { + const [key, data] = rest; + assert.typeof(key, 'string'); + assert.typeof(data, 'string'); + break; + } + case 'vatstoreDelete': { + const [key] = rest; + assert.typeof(key, 'string'); + break; + } + case 'dropImports': { + const [slots] = rest; + assert(Array.isArray(slots)); + for (const slot of slots) { + assert.typeof(slot, 'string'); + } + break; + } + default: + assert.fail(`unknown syscall type ${type}`); + } + return undefined; +} + +/** + * @param { unknown } vsr + * @returns { asserts vsr is VatSyscallResult } + */ +export function insistVatSyscallResult(vsr) { + assert(Array.isArray(vsr)); + const [type, ...rest] = vsr; + switch (type) { + case 'ok': { + break; + } + case 'error': { + const [err] = rest; + assert.typeof(err, 'string'); + break; + } + default: + assert.fail(`unknown syscall result type ${type}`); + } + return undefined; } diff --git a/packages/SwingSet/src/parseVatSlots.js b/packages/SwingSet/src/parseVatSlots.js index 026b39b4b47..9bd3a459189 100644 --- a/packages/SwingSet/src/parseVatSlots.js +++ b/packages/SwingSet/src/parseVatSlots.js @@ -80,7 +80,7 @@ export function parseVatSlot(s) { * * @param {'object'|'device'|'promise'} type The type * @param {boolean} allocatedByVat Flag: true=>vat allocated, false=>kernel allocated - * @param {number} id The id, a Nat. + * @param {number | bigint} id The id, a Nat. * * @returns {string} the corresponding vat slot reference string. * diff --git a/packages/SwingSet/src/types.js b/packages/SwingSet/src/types.js index ae5ac3e794d..4826717765e 100644 --- a/packages/SwingSet/src/types.js +++ b/packages/SwingSet/src/types.js @@ -1,7 +1,9 @@ // @ts-check +// import '@agoric/marshal/src/types'; + /** - * @typedef {CapData} CapDataS + * @typedef {CapData} SwingSetCapData */ /** @@ -25,7 +27,7 @@ * enableDisavow?: boolean, * vatParameters: Record, * virtualObjectCacheSize: number, - * name?: string, + * name: string, * } & (HasBundle | HasSetup)} ManagerOptions */ @@ -55,3 +57,65 @@ * exitVatWithFailure: unknown, * }} TerminationVatPowers */ + +/* + * `['message', targetSlot, msg]` + * msg is `{ method, args, result }` + * `['notify', resolutions]` + * `['dropExports', vrefs]` + */ + +/** + * @typedef {{ + * method: string, + * args: SwingSetCapData, + * result?: string, + * }} Message + * + * @typedef { [tag: 'message', target: string, msg: Message]} VatDeliveryMessage + * @typedef { [tag: 'notify', resolutions: string[] ]} VatDeliveryNotify + * @typedef { [tag: 'dropExports', vrefs: string[] ]} VatDeliveryDropExports + * @typedef { VatDeliveryMessage | VatDeliveryNotify | VatDeliveryDropExports } VatDeliveryObject + * @typedef { [tag: 'ok', message: null, usage: unknown] | [tag: 'error', message: string, usage: unknown | null] } VatDeliveryResult + * + * @typedef { [tag: 'send', target: string, msg: Message] } VatSyscallSend + * @typedef { [tag: 'callNow', target: string, method: string, args: SwingSetCapData]} VatSyscallCallNow + * @typedef { [tag: 'subscribe', vpid: string ]} VatSyscallSubscribe + * @typedef { [ vpid: string, rejected: boolean, data: SwingSetCapData ]} Resolutions + * @typedef { [tag: 'resolve', resolutions: Resolutions ]} VatSyscallResolve + * @typedef { [tag: 'vatstoreGet', key: string ]} VatSyscallVatstoreGet + * @typedef { [tag: 'vatstoreSet', key: string, data: string ]} VatSyscallVatstoreSet + * @typedef { [tag: 'vatstoreDelete', key: string ]} VatSyscallVatstoreDelete + * @typedef { [tag: 'dropImports', slots: string[] ]} VatSyscallDropImports + * + * @typedef { VatSyscallSend | VatSyscallCallNow | VatSyscallSubscribe + * | VatSyscallResolve | VatSyscallVatstoreGet | VatSyscallVatstoreSet + * | VatSyscallVatstoreDelete | VatSyscallDropImports + * } VatSyscallObject + * + * @typedef { [tag: 'ok', data: SwingSetCapData | string | null ]} VatSyscallResultOk + * @typedef { [tag: 'error', err: string ] } VatSyscallResultError + * @typedef { VatSyscallResultOk | VatSyscallResultError } VatSyscallResult + * @typedef { (vso: VatSyscallObject) => VatSyscallResult } VatSyscaller + * + * @typedef { { d: VatDeliveryObject, syscalls: VatSyscallObject[] } } TranscriptEntry + * @typedef { { transcriptCount: number } } VatStats + * @typedef { { getTranscript: () => TranscriptEntry[], + * vatStats: () => VatStats, + * } } VatKeeper + * @typedef { { getVatKeeper: (vatID: string) => VatKeeper } } KernelKeeper + * @typedef { { write: ({}) => void, + * } } KernelSlog + * + * @typedef { { createFromBundle: (vatID: string, + * bundle: unknown, + * managerOptions: unknown, + * vatSyscallHandler: unknown) => Promise, + * } } VatManagerFactory + * @typedef { { deliver: (delivery: VatDeliveryObject) => Promise, + * replayTranscript: () => void, + * shutdown: () => Promise, + * } } VatManager + * @typedef { () => Promise } WaitUntilQuiescent + * + */ diff --git a/packages/SwingSet/src/vats/comms/dispatch.js b/packages/SwingSet/src/vats/comms/dispatch.js index d3c83d6009b..b653b00781c 100644 --- a/packages/SwingSet/src/vats/comms/dispatch.js +++ b/packages/SwingSet/src/vats/comms/dispatch.js @@ -1,5 +1,6 @@ import { assert, details as X } from '@agoric/assert'; import { makeVatSlot, insistVatType, parseVatSlot } from '../../parseVatSlots'; +import { insistMessage } from '../../message'; import { makeState } from './state'; import { deliverToController } from './controller'; import { insistCapData } from '../../capdata'; @@ -127,7 +128,31 @@ export function buildCommsDispatch( console.log(`-- comms ignoring dropExports`); } - const dispatch = harden({ deliver, notify, dropExports }); + function dispatch(vatDeliveryObject) { + const [type, ...args] = vatDeliveryObject; + switch (type) { + case 'message': { + const [targetSlot, msg] = args; + insistMessage(msg); + deliver(targetSlot, msg.method, msg.args, msg.result); + return; + } + case 'notify': { + const [resolutions] = args; + notify(resolutions); + return; + } + case 'dropExports': { + const [vrefs] = args; + dropExports(vrefs); + return; + } + default: + assert.fail(X`unknown delivery type ${type}`); + } + } + harden(dispatch); + debugState.set(dispatch, { state, clistKit }); return dispatch; diff --git a/packages/SwingSet/test/commsVatDriver.js b/packages/SwingSet/test/commsVatDriver.js index 0189982c827..5abbb5b6f2d 100644 --- a/packages/SwingSet/test/commsVatDriver.js +++ b/packages/SwingSet/test/commsVatDriver.js @@ -2,6 +2,7 @@ import { assert, details as X } from '@agoric/assert'; import buildCommsDispatch from '../src/vats/comms'; import { debugState } from '../src/vats/comms/dispatch'; import { flipRemoteSlot } from '../src/vats/comms/parseRemoteSlot'; +import { makeMessage, makeResolutions } from './util'; // This module provides a power tool for testing the comms vat implementation. // It provides support for injecting events into the comms vat and observing @@ -348,8 +349,8 @@ function remoteResolutions(doFlip, resolutions) { export function commsVatDriver(t, verbose = false) { const log = []; const syscall = loggingSyscall(log); - const d = buildCommsDispatch(syscall, 'fakestate', 'fakehelpers'); - const { state } = debugState.get(d); + const dispatch = buildCommsDispatch(syscall, 'fakestate', 'fakehelpers'); + const { state } = debugState.get(dispatch); const remotes = new Map(); @@ -423,14 +424,14 @@ export function commsVatDriver(t, verbose = false) { function injectSend(who, target, method, args, result) { t.deepEqual(log, []); if (who === 'k') { - d.deliver(target, method, args, result); + dispatch(makeMessage(target, method, args, result)); } else { const remote = remotes.get(who); const msg = prepareReceive( remote, remoteMessage(false, target, method, args, result), ); - d.deliver(remote.receiver, 'receive', msg); + dispatch(makeMessage(remote.receiver, 'receive', msg)); } } @@ -476,11 +477,11 @@ export function commsVatDriver(t, verbose = false) { function injectResolutions(who, resolutions) { t.deepEqual(log, []); if (who === 'k') { - d.notify(resolutions); + dispatch(makeResolutions(resolutions)); } else { const remote = remotes.get(who); const msg = prepareReceive(remote, remoteResolutions(false, resolutions)); - d.deliver(remote.receiver, 'receive', msg); + dispatch(makeMessage(remote.receiver, 'receive', msg)); } } diff --git a/packages/SwingSet/test/files-devices/bootstrap-0.js b/packages/SwingSet/test/files-devices/bootstrap-0.js index cabcea7f03f..e1da465802c 100644 --- a/packages/SwingSet/test/files-devices/bootstrap-0.js +++ b/packages/SwingSet/test/files-devices/bootstrap-0.js @@ -1,9 +1,10 @@ +import { extractMessage } from '../util'; + export default function setup(syscall, state, _helpers, vatPowers) { - const dispatch = harden({ - deliver(facetid, method, args, _result) { - vatPowers.testLog(args.body); - vatPowers.testLog(JSON.stringify(args.slots)); - }, - }); + function dispatch(vatDeliverObject) { + const { args } = extractMessage(vatDeliverObject); + vatPowers.testLog(args.body); + vatPowers.testLog(JSON.stringify(args.slots)); + } return dispatch; } diff --git a/packages/SwingSet/test/files-devices/bootstrap-1.js b/packages/SwingSet/test/files-devices/bootstrap-1.js index 5c569d65948..d11d8489ad3 100644 --- a/packages/SwingSet/test/files-devices/bootstrap-1.js +++ b/packages/SwingSet/test/files-devices/bootstrap-1.js @@ -1,22 +1,22 @@ import { assert, details as X } from '@agoric/assert'; +import { extractMessage } from '../util'; export default function setup(syscall, state, _helpers, vatPowers) { const { testLog } = vatPowers; let deviceRef; - const dispatch = harden({ - deliver(facetid, method, args, _result) { - if (method === 'bootstrap') { - const argb = JSON.parse(args.body); - const deviceIndex = argb[1].d1.index; - deviceRef = args.slots[deviceIndex]; - assert(deviceRef === 'd-70', X`bad deviceRef ${deviceRef}`); - } else if (method === 'step1') { - testLog(`callNow`); - const setArgs = harden({ body: JSON.stringify([1, 2]), slots: [] }); - const ret = syscall.callNow(deviceRef, 'set', setArgs); - testLog(JSON.stringify(ret)); - } - }, - }); + function dispatch(vatDeliverObject) { + const { method, args } = extractMessage(vatDeliverObject); + if (method === 'bootstrap') { + const argb = JSON.parse(args.body); + const deviceIndex = argb[1].d1.index; + deviceRef = args.slots[deviceIndex]; + assert(deviceRef === 'd-70', X`bad deviceRef ${deviceRef}`); + } else if (method === 'step1') { + testLog(`callNow`); + const setArgs = harden({ body: JSON.stringify([1, 2]), slots: [] }); + const ret = syscall.callNow(deviceRef, 'set', setArgs); + testLog(JSON.stringify(ret)); + } + } return dispatch; } diff --git a/packages/SwingSet/test/files-devices/bootstrap-4.js b/packages/SwingSet/test/files-devices/bootstrap-4.js index 24ad248c268..446743e430b 100644 --- a/packages/SwingSet/test/files-devices/bootstrap-4.js +++ b/packages/SwingSet/test/files-devices/bootstrap-4.js @@ -1,6 +1,7 @@ import { assert } from '@agoric/assert'; import { QCLASS } from '@agoric/marshal'; import { insistVatType } from '../../src/parseVatSlots'; +import { extractMessage } from '../util'; // to exercise the error we get when syscall.callNow() is given a promise // identifier, we must bypass liveslots, which would otherwise protect us @@ -17,32 +18,31 @@ function capargs(args, slots = []) { export default function setup(syscall, state, _helpers, vatPowers) { const { callNow } = syscall; const { testLog } = vatPowers; - const dispatch = harden({ - deliver(facetid, method, args, _result) { - if (method === 'bootstrap') { - // find the device slot - const [_vats, devices] = JSON.parse(args.body); - const qnode = devices.d0; - assert.equal(qnode[QCLASS], 'slot'); - const slot = args.slots[qnode.index]; - insistVatType('device', slot); + function dispatch(vatDeliverObject) { + const { method, args } = extractMessage(vatDeliverObject); + if (method === 'bootstrap') { + // find the device slot + const [_vats, devices] = JSON.parse(args.body); + const qnode = devices.d0; + assert.equal(qnode[QCLASS], 'slot'); + const slot = args.slots[qnode.index]; + insistVatType('device', slot); - const vpid = 'p+1'; // pretend we're exporting a promise - const pnode = { [QCLASS]: 'slot', index: 0 }; - const callNowArgs = capargs([pnode], [vpid]); + const vpid = 'p+1'; // pretend we're exporting a promise + const pnode = { [QCLASS]: 'slot', index: 0 }; + const callNowArgs = capargs([pnode], [vpid]); - testLog('sending Promise'); - try { - // this will throw an exception, but is also (eventually) vat-fatal - callNow(slot, 'send', callNowArgs); - testLog('oops: survived sending Promise'); - } catch (e) { - testLog('good: callNow failed'); - } - } else if (method === 'ping') { - testLog('oops: still alive'); + testLog('sending Promise'); + try { + // this will throw an exception, but is also (eventually) vat-fatal + callNow(slot, 'send', callNowArgs); + testLog('oops: survived sending Promise'); + } catch (e) { + testLog('good: callNow failed'); } - }, - }); + } else if (method === 'ping') { + testLog('oops: still alive'); + } + } return dispatch; } diff --git a/packages/SwingSet/test/liveslots-helpers.js b/packages/SwingSet/test/liveslots-helpers.js new file mode 100644 index 00000000000..99d32403d07 --- /dev/null +++ b/packages/SwingSet/test/liveslots-helpers.js @@ -0,0 +1,46 @@ +import { WeakRef, FinalizationRegistry } from '../src/weakref'; +import { makeLiveSlots } from '../src/kernel/liveSlots'; + +export function buildSyscall() { + const log = []; + + const syscall = { + send(targetSlot, method, args, resultSlot) { + log.push({ type: 'send', targetSlot, method, args, resultSlot }); + }, + subscribe(target) { + log.push({ type: 'subscribe', target }); + }, + resolve(resolutions) { + log.push({ type: 'resolve', resolutions }); + }, + dropImports(slots) { + log.push({ type: 'dropImports', slots }); + }, + exit(isFailure, info) { + log.push({ type: 'exit', isFailure, info }); + }, + }; + + return { log, syscall }; +} + +export function makeDispatch( + syscall, + build, + vatID = 'vatA', + enableDisavow = false, +) { + const gcTools = harden({ WeakRef, FinalizationRegistry }); + const { setBuildRootObject, dispatch } = makeLiveSlots( + syscall, + vatID, + {}, + {}, + undefined, + enableDisavow, + gcTools, + ); + setBuildRootObject(build); + return dispatch; +} diff --git a/packages/SwingSet/test/test-comms.js b/packages/SwingSet/test/test-comms.js index a00d4c2a3bf..92a31fecb00 100644 --- a/packages/SwingSet/test/test-comms.js +++ b/packages/SwingSet/test/test-comms.js @@ -5,6 +5,7 @@ import { flipRemoteSlot } from '../src/vats/comms/parseRemoteSlot'; import { makeState } from '../src/vats/comms/state'; import { makeCListKit } from '../src/vats/comms/clist'; import { debugState } from '../src/vats/comms/dispatch'; +import { makeMessage, makeDropExports } from './util'; import { commsVatDriver } from './commsVatDriver'; test('provideRemoteForLocal', t => { @@ -56,8 +57,8 @@ test('transmit', t => { // look at machine A, on which some local vat is sending messages to a // remote 'bob' on machine B const { syscall, sends } = mockSyscall(); - const d = buildCommsDispatch(syscall, 'fakestate', 'fakehelpers'); - const { state, clistKit } = debugState.get(d); + const dispatch = buildCommsDispatch(syscall, 'fakestate', 'fakehelpers'); + const { state, clistKit } = debugState.get(dispatch); state.initialize(); const { provideKernelForLocal, @@ -75,7 +76,7 @@ test('transmit', t => { // now tell the comms vat to send a message to a remote machine, the // equivalent of bob!foo() - d.deliver(bobKernel, 'foo', capdata('argsbytes', []), null); + dispatch(makeMessage(bobKernel, 'foo', capdata('argsbytes', []))); t.deepEqual(sends.shift(), [ transmitterID, 'transmit', @@ -83,11 +84,12 @@ test('transmit', t => { ]); // bob!bar(alice, bob) - d.deliver( - bobKernel, - 'bar', - capdata('argsbytes', [aliceKernel, bobKernel]), - null, + dispatch( + makeMessage( + bobKernel, + 'bar', + capdata('argsbytes', [aliceKernel, bobKernel]), + ), ); t.deepEqual(sends.shift(), [ transmitterID, @@ -97,11 +99,12 @@ test('transmit', t => { // the outbound ro-20 should match an inbound ro+20, both represent 'alice' t.is(getLocalForRemote(remoteID, 'ro+20'), aliceLocal); // do it again, should use same values - d.deliver( - bobKernel, - 'bar', - capdata('argsbytes', [aliceKernel, bobKernel]), - null, + dispatch( + makeMessage( + bobKernel, + 'bar', + capdata('argsbytes', [aliceKernel, bobKernel]), + ), ); t.deepEqual(sends.shift(), [ transmitterID, @@ -110,12 +113,13 @@ test('transmit', t => { ]); // bob!cat(alice, bob, ayana) - const ayana = 'o-11'; - d.deliver( - bobKernel, - 'cat', - capdata('argsbytes', [aliceKernel, bobKernel, ayana]), - null, + const ayanaKernel = 'o-11'; + dispatch( + makeMessage( + bobKernel, + 'cat', + capdata('argsbytes', [aliceKernel, bobKernel, ayanaKernel]), + ), ); t.deepEqual(sends.shift(), [ transmitterID, @@ -128,8 +132,8 @@ test('receive', t => { // look at machine B, which is receiving remote messages aimed at a local // vat's object 'bob' const { syscall, sends } = mockSyscall(); - const d = buildCommsDispatch(syscall, 'fakestate', 'fakehelpers'); - const { state, clistKit } = debugState.get(d); + const dispatch = buildCommsDispatch(syscall, 'fakestate', 'fakehelpers'); + const { state, clistKit } = debugState.get(dispatch); state.initialize(); const { provideLocalForKernel, @@ -149,20 +153,22 @@ test('receive', t => { // now pretend the transport layer received a message from remote1, as if // the remote machine had performed bob!foo() - d.deliver( - receiverID, - 'receive', - encodeArgs(`1:0:deliver:${bobRemote}:foo:;argsbytes`), - null, + dispatch( + makeMessage( + receiverID, + 'receive', + encodeArgs(`1:0:deliver:${bobRemote}:foo:;argsbytes`), + ), ); t.deepEqual(sends.shift(), [bobKernel, 'foo', capdata('argsbytes')]); // bob!bar(alice, bob) - d.deliver( - receiverID, - 'receive', - encodeArgs(`2:0:deliver:${bobRemote}:bar::ro-20:${bobRemote};argsbytes`), - null, + dispatch( + makeMessage( + receiverID, + 'receive', + encodeArgs(`2:0:deliver:${bobRemote}:bar::ro-20:${bobRemote};argsbytes`), + ), ); const expectedAliceKernel = 'o+31'; t.deepEqual(sends.shift(), [ @@ -178,11 +184,12 @@ test('receive', t => { // bob!bar(alice, bob), again, to test stability // also test absent sequence number - d.deliver( - receiverID, - 'receive', - encodeArgs(`:0:deliver:${bobRemote}:bar::ro-20:${bobRemote};argsbytes`), - null, + dispatch( + makeMessage( + receiverID, + 'receive', + encodeArgs(`:0:deliver:${bobRemote}:bar::ro-20:${bobRemote};argsbytes`), + ), ); t.deepEqual(sends.shift(), [ bobKernel, @@ -192,13 +199,14 @@ test('receive', t => { // bob!cat(alice, bob, ayana) const expectedAyanaKernel = 'o+32'; - d.deliver( - receiverID, - 'receive', - encodeArgs( - `4:0:deliver:${bobRemote}:cat::ro-20:${bobRemote}:ro-21;argsbytes`, + dispatch( + makeMessage( + receiverID, + 'receive', + encodeArgs( + `4:0:deliver:${bobRemote}:cat::ro-20:${bobRemote}:ro-21;argsbytes`, + ), ), - null, ); t.deepEqual(sends.shift(), [ bobKernel, @@ -209,17 +217,20 @@ test('receive', t => { // react to bad sequence number t.throws( () => - d.deliver( - receiverID, - 'receive', - encodeArgs(`47:deliver:${bobRemote}:bar::ro-20:${bobRemote};argsbytes`), - null, + dispatch( + makeMessage( + receiverID, + 'receive', + encodeArgs( + `47:deliver:${bobRemote}:bar::ro-20:${bobRemote};argsbytes`, + ), + ), ), { message: /unexpected recv seqNum .*/ }, ); // make sure comms can tolerate dropExports, even if it's a no-op - d.dropExports([expectedAliceKernel, expectedAyanaKernel]); + dispatch(makeDropExports(expectedAliceKernel, expectedAyanaKernel)); }); // This tests the various pathways through the comms vat driver. This has the diff --git a/packages/SwingSet/test/test-kernel.js b/packages/SwingSet/test/test-kernel.js index 39beeb9aa09..f36cf5f5eb6 100644 --- a/packages/SwingSet/test/test-kernel.js +++ b/packages/SwingSet/test/test-kernel.js @@ -10,7 +10,7 @@ import { waitUntilQuiescent } from '../src/waitUntilQuiescent'; import buildKernel from '../src/kernel/index'; import { initializeKernel } from '../src/kernel/initializeKernel'; import { makeVatSlot } from '../src/parseVatSlots'; -import { checkKT } from './util'; +import { checkKT, extractMessage } from './util'; function capdata(body, slots = []) { return harden({ body, slots }); @@ -41,8 +41,8 @@ function checkPromises(t, kernel, expected) { } function emptySetup(_syscall) { - function deliver() {} - return { deliver }; + function dispatch() {} + return dispatch; } function makeConsole(tag) { @@ -84,11 +84,13 @@ test('simple call', async t => { await kernel.start(); const log = []; function setup1(syscall, state, _helpers, vatPowers) { - function deliver(facetID, method, args) { + function dispatch(vatDeliverObject) { + // TODO: just push the vatDeliverObject + const { facetID, method, args } = extractMessage(vatDeliverObject); log.push([facetID, method, args]); vatPowers.testLog(JSON.stringify({ facetID, method, args })); } - return { deliver }; + return dispatch; } await kernel.createTestVat('vat1', setup1); const vat1 = kernel.vatNameToID('vat1'); @@ -128,7 +130,8 @@ test('vat store', async t => { await kernel.start(); const log = []; function setup(syscall, _state, _helpers, _vatPowers) { - function deliver(facetID, method, args) { + function dispatch(vatDeliverObject) { + const { method, args } = extractMessage(vatDeliverObject); switch (method) { case 'get': { const v = syscall.vatstoreGet('zot'); @@ -149,7 +152,7 @@ test('vat store', async t => { assert.fail(X`this can't happen`); } } - return { deliver }; + return dispatch; } await kernel.createTestVat('vat', setup); const vat = kernel.vatNameToID('vat'); @@ -179,10 +182,11 @@ test('map inbound', async t => { await kernel.start(); const log = []; function setup1(_syscall) { - function deliver(facetID, method, args) { + function dispatch(vatDeliverObject) { + const { facetID, method, args } = extractMessage(vatDeliverObject); log.push([facetID, method, args]); } - return { deliver }; + return dispatch; } await kernel.createTestVat('vat1', setup1); await kernel.createTestVat('vat2', setup1); @@ -226,8 +230,8 @@ test('addImport', async t => { const kernel = makeKernel(); await kernel.start(); function setup(_syscall) { - function deliver(_facetID, _method, _args) {} - return { deliver }; + function dispatch() {} + return dispatch; } await kernel.createTestVat('vat1', setup); await kernel.createTestVat('vat2', setup); @@ -256,7 +260,8 @@ test('outbound call', async t => { nextPromiseIndex += 1; return makeVatSlot('promise', true, index); } - function deliver(facetID, method, args) { + function dispatch(vatDeliverObject) { + const { facetID, method, args } = extractMessage(vatDeliverObject); // console.log(`d1/${facetID} called`); log.push(['d1', facetID, method, args]); const pid = allocatePromise(); @@ -267,17 +272,18 @@ test('outbound call', async t => { pid, ); } - return { deliver }; + return dispatch; } await kernel.createTestVat('vat1', setup1); function setup2(_syscall) { - function deliver(facetID, method, args) { + function dispatch(vatDeliverObject) { + const { facetID, method, args } = extractMessage(vatDeliverObject); // console.log(`d2/${facetID} called`); log.push(['d2', facetID, method, args]); log.push(['d2 promises', kernel.dump().promises]); } - return { deliver }; + return dispatch; } await kernel.createTestVat('vat2', setup2); const vat1 = kernel.vatNameToID('vat1'); @@ -462,31 +468,34 @@ test('three-party', async t => { nextPromiseIndex += 1; return makeVatSlot('promise', true, index); } - function deliver(facetID, method, args) { - console.log(`vatA/${facetID} called`); + function dispatch(vatDeliverObject) { + const { facetID, method, args } = extractMessage(vatDeliverObject); + // console.log(`vatA/${facetID} called`); log.push(['vatA', facetID, method, args]); const pid = allocatePromise(); syscall.send(bobForA, 'intro', capdata('bargs', [carolForA]), pid); log.push(['vatA', 'promiseID', pid]); } - return { deliver }; + return dispatch; } await kernel.createTestVat('vatA', setupA); function setupB(_syscall) { - function deliver(facetID, method, args) { - console.log(`vatB/${facetID} called`); + function dispatch(vatDeliverObject) { + const { facetID, method, args } = extractMessage(vatDeliverObject); + // console.log(`vatB/${facetID} called`); log.push(['vatB', facetID, method, args]); } - return { deliver }; + return dispatch; } await kernel.createTestVat('vatB', setupB); function setupC(_syscall) { - function deliver(facetID, method, args) { + function dispatch(vatDeliverObject) { + const { facetID, method, args } = extractMessage(vatDeliverObject); log.push(['vatC', facetID, method, args]); } - return { deliver }; + return dispatch; } await kernel.createTestVat('vatC', setupC); @@ -587,10 +596,11 @@ test('transfer promise', async t => { const logA = []; function setupA(syscall) { syscallA = syscall; - function deliver(facetID, method, args) { + function dispatch(vatDeliverObject) { + const { facetID, method, args } = extractMessage(vatDeliverObject); logA.push([facetID, method, args]); } - return { deliver }; + return dispatch; } await kernel.createTestVat('vatA', setupA); @@ -598,10 +608,11 @@ test('transfer promise', async t => { const logB = []; function setupB(syscall) { syscallB = syscall; - function deliver(facetID, method, args) { + function dispatch(vatDeliverObject) { + const { facetID, method, args } = extractMessage(vatDeliverObject); logB.push([facetID, method, args]); } - return { deliver }; + return dispatch; } await kernel.createTestVat('vatB', setupB); @@ -690,10 +701,11 @@ test('subscribe to promise', async t => { const log = []; function setup(s) { syscall = s; - function deliver(facetID, method, args) { + function dispatch(vatDeliverObject) { + const { facetID, method, args } = extractMessage(vatDeliverObject); log.push(['deliver', facetID, method, args]); } - return { deliver }; + return dispatch; } await kernel.createTestVat('vat1', setup); await kernel.createTestVat('vat2', emptySetup); @@ -733,19 +745,18 @@ test('promise resolveToData', async t => { let syscallA; function setupA(s) { syscallA = s; - function deliver() {} - function notify(resolutions) { - log.push(['notify', resolutions]); + function dispatch(vatDeliverObject) { + log.push(vatDeliverObject); } - return { deliver, notify }; + return dispatch; } await kernel.createTestVat('vatA', setupA); let syscallB; function setupB(s) { syscallB = s; - function deliver() {} - return { deliver }; + function dispatch() {} + return dispatch; } await kernel.createTestVat('vatB', setupB); @@ -803,19 +814,18 @@ test('promise resolveToPresence', async t => { let syscallA; function setupA(s) { syscallA = s; - function deliver() {} - function notify(resolutions) { - log.push(['notify', resolutions]); + function dispatch(vatDeliverObject) { + log.push(vatDeliverObject); } - return { deliver, notify }; + return dispatch; } await kernel.createTestVat('vatA', setupA); let syscallB; function setupB(s) { syscallB = s; - function deliver() {} - return { deliver }; + function dispatch() {} + return dispatch; } await kernel.createTestVat('vatB', setupB); @@ -880,19 +890,18 @@ test('promise reject', async t => { let syscallA; function setupA(s) { syscallA = s; - function deliver() {} - function notify(resolutions) { - log.push(['notify', resolutions]); + function dispatch(vatDeliverObject) { + log.push(vatDeliverObject); } - return { deliver, notify }; + return dispatch; } await kernel.createTestVat('vatA', setupA); let syscallB; function setupB(s) { syscallB = s; - function deliver() {} - return { deliver }; + function dispatch() {} + return dispatch; } await kernel.createTestVat('vatB', setupB); @@ -948,12 +957,13 @@ test('transcript', async t => { await kernel.start(); function setup(syscall, _state) { - function deliver(facetID, _method, args) { + function dispatch(vatDeliverObject) { + const { facetID, args } = extractMessage(vatDeliverObject); if (facetID === aliceForAlice) { syscall.send(args.slots[1], 'foo', capdata('fooarg'), 'p+5'); } } - return { deliver }; + return dispatch; } await kernel.createTestVat('vatA', setup); await kernel.createTestVat('vatB', emptySetup); @@ -978,15 +988,21 @@ test('transcript', async t => { t.is(tr.length, 1); t.deepEqual(tr[0], { d: [ - 'deliver', + 'message', aliceForAlice, - 'store', - capdata('args string', [aliceForAlice, bobForAlice]), - 'p-60', + { + method: 'store', + args: capdata('args string', [aliceForAlice, bobForAlice]), + result: 'p-60', + }, ], syscalls: [ { - d: ['send', bobForAlice, 'foo', capdata('fooarg'), 'p+5'], + d: [ + 'send', + bobForAlice, + { method: 'foo', args: capdata('fooarg'), result: 'p+5' }, + ], response: null, }, ], @@ -1005,16 +1021,19 @@ test('non-pipelined promise queueing', async t => { let syscall; function setupA(s) { syscall = s; - function deliver() {} - return { deliver }; + function dispatch() {} + return dispatch; } await kernel.createTestVat('vatA', setupA); function setupB(_s) { - function deliver(target, method, args, result) { - log.push([target, method, args, result]); + function dispatch(vatDeliverObject) { + const { facetID, method, args, result } = extractMessage( + vatDeliverObject, + ); + log.push([facetID, method, args, result]); } - return { deliver }; + return dispatch; } await kernel.createTestVat('vatB', setupB); @@ -1127,16 +1146,19 @@ test('pipelined promise queueing', async t => { let syscall; function setupA(s) { syscall = s; - function deliver() {} - return { deliver }; + function dispatch() {} + return dispatch; } await kernel.createTestVat('vatA', setupA); function setupB(_s) { - function deliver(target, method, args, result) { - log.push([target, method, args, result]); + function dispatch(vatDeliverObject) { + const { facetID, method, args, result } = extractMessage( + vatDeliverObject, + ); + log.push([facetID, method, args, result]); } - return { deliver }; + return dispatch; } await kernel.createTestVat('vatB', setupB, {}, { enablePipelining: true }); diff --git a/packages/SwingSet/test/test-liveslots.js b/packages/SwingSet/test/test-liveslots.js index 1fcfc3bc5f2..4d27fac5602 100644 --- a/packages/SwingSet/test/test-liveslots.js +++ b/packages/SwingSet/test/test-liveslots.js @@ -4,73 +4,17 @@ import { test } from '../tools/prepare-test-env-ava'; import { E } from '@agoric/eventual-send'; import { Far } from '@agoric/marshal'; import { assert, details as X } from '@agoric/assert'; -import { WeakRef, FinalizationRegistry } from '../src/weakref'; import { waitUntilQuiescent } from '../src/waitUntilQuiescent'; -import { makeLiveSlots } from '../src/kernel/liveSlots'; - -function capdata(body, slots = []) { - return harden({ body, slots }); -} - -function capargs(args, slots = []) { - return capdata(JSON.stringify(args), slots); -} - -function capdataOneSlot(slot) { - return capargs({ '@qclass': 'slot', iface: 'Alleged: export', index: 0 }, [ - slot, - ]); -} - -function capargsOneSlot(slot) { - return capargs( - [{ '@qclass': 'slot', iface: 'Alleged: export', index: 0 }], - [slot], - ); -} - -function oneResolution(promiseID, rejected, data) { - return [[promiseID, rejected, data]]; -} - -function buildSyscall() { - const log = []; - - const syscall = { - send(targetSlot, method, args, resultSlot) { - log.push({ type: 'send', targetSlot, method, args, resultSlot }); - }, - subscribe(target) { - log.push({ type: 'subscribe', target }); - }, - resolve(resolutions) { - log.push({ type: 'resolve', resolutions }); - }, - dropImports(slots) { - log.push({ type: 'dropImports', slots }); - }, - exit(isFailure, info) { - log.push({ type: 'exit', isFailure, info }); - }, - }; - - return { log, syscall }; -} - -function makeDispatch(syscall, build, enableDisavow = false) { - const gcTools = harden({ WeakRef, FinalizationRegistry }); - const { setBuildRootObject, dispatch } = makeLiveSlots( - syscall, - 'vatA', - {}, - {}, - undefined, - enableDisavow, - gcTools, - ); - setBuildRootObject(build); - return dispatch; -} +import { buildSyscall, makeDispatch } from './liveslots-helpers'; +import { + capargs, + capargsOneSlot, + capdataOneSlot, + makeMessage, + makeResolve, + makeReject, + makeDropExports, +} from './util'; test('calls', async t => { const { log, syscall } = buildSyscall(); @@ -94,24 +38,25 @@ test('calls', async t => { const rootA = 'o+0'; // root!one() // sendOnly - dispatch.deliver(rootA, 'one', capargs(['args']), undefined); + dispatch(makeMessage(rootA, 'one', capargs(['args']))); await waitUntilQuiescent(); t.deepEqual(log.shift(), 'one'); // pr = makePromise() // root!two(pr.promise) // pr.resolve('result') - dispatch.deliver( - rootA, - 'two', - capargs([{ '@qclass': 'slot', index: 0 }], ['p-1']), - undefined, + dispatch( + makeMessage( + rootA, + 'two', + capargs([{ '@qclass': 'slot', index: 0 }], ['p-1']), + ), ); await waitUntilQuiescent(); t.deepEqual(log.shift(), { type: 'subscribe', target: 'p-1' }); t.deepEqual(log.shift(), 'two true'); - dispatch.notify(oneResolution('p-1', false, capargs('result'))); + dispatch(makeResolve('p-1', capargs('result'))); await waitUntilQuiescent(); t.deepEqual(log.shift(), ['res', 'result']); @@ -119,17 +64,18 @@ test('calls', async t => { // root!two(pr.promise) // pr.reject('rejection') - dispatch.deliver( - rootA, - 'two', - capargs([{ '@qclass': 'slot', index: 0 }], ['p-2']), - undefined, + dispatch( + makeMessage( + rootA, + 'two', + capargs([{ '@qclass': 'slot', index: 0 }], ['p-2']), + ), ); await waitUntilQuiescent(); t.deepEqual(log.shift(), { type: 'subscribe', target: 'p-2' }); t.deepEqual(log.shift(), 'two true'); - dispatch.notify(oneResolution('p-2', true, capargs('rejection'))); + dispatch(makeReject('p-2', capargs('rejection'))); await waitUntilQuiescent(); t.deepEqual(log.shift(), ['rej', 'rejection']); @@ -158,11 +104,8 @@ test('liveslots pipelines to syscall.send', async t => { const p3 = 'p+7'; // root!one(x) // sendOnly - dispatch.deliver( - rootA, - 'one', - capargs([{ '@qclass': 'slot', index: 0 }], [x]), - undefined, + dispatch( + makeMessage(rootA, 'one', capargs([{ '@qclass': 'slot', index: 0 }], [x])), ); await waitUntilQuiescent(); @@ -223,7 +166,7 @@ test('liveslots pipeline/non-pipeline calls', async t => { const slot0arg = { '@qclass': 'slot', index: 0 }; // function deliver(target, method, argsdata, result) { - dispatch.deliver(rootA, 'one', capargs([slot0arg], [p1])); + dispatch(makeMessage(rootA, 'one', capargs([slot0arg], [p1]))); await waitUntilQuiescent(); // the vat should subscribe to the inbound p1 during deserialization t.deepEqual(log.shift(), { type: 'subscribe', target: p1 }); @@ -240,7 +183,7 @@ test('liveslots pipeline/non-pipeline calls', async t => { t.deepEqual(log, []); // now we tell it the promise has resolved, to object 'o2' - dispatch.notify(oneResolution(p1, false, capargs(slot0arg, [o2]))); + dispatch(makeResolve(p1, capargs(slot0arg, [o2]))); await waitUntilQuiescent(); // this allows E(o2).nonpipe2() to go out, which was not pipelined t.deepEqual(log.shift(), { @@ -256,7 +199,7 @@ test('liveslots pipeline/non-pipeline calls', async t => { // now call two(), which should send nonpipe3 to o2, not p1, since p1 has // been resolved - dispatch.deliver(rootA, 'two', capargs([], [])); + dispatch(makeMessage(rootA, 'two', capargs([], []))); await waitUntilQuiescent(); t.deepEqual(log.shift(), { type: 'send', @@ -333,7 +276,9 @@ async function doOutboundPromise(t, mode) { } // function deliver(target, method, argsdata, result) { - dispatch.deliver(rootA, 'run', capargs([slot0arg, resolution], [target])); + dispatch( + makeMessage(rootA, 'run', capargs([slot0arg, resolution], [target])), + ); await waitUntilQuiescent(); // The vat should send 'one' and mention the promise for the first time. It @@ -419,7 +364,7 @@ async function doResultPromise(t, mode) { const target2 = 'o-2'; // if it returns data or a rejection, two() results in an error - dispatch.deliver(rootA, 'run', capargs([slot0arg], [target1])); + dispatch(makeMessage(rootA, 'run', capargs([slot0arg], [target1]))); await waitUntilQuiescent(); // The vat should send 'getTarget2' and subscribe to the result promise @@ -448,13 +393,11 @@ async function doResultPromise(t, mode) { // resolve p1 first. The one() call was already pipelined, so this // should not trigger any new syscalls. if (mode === 'to presence') { - dispatch.notify( - oneResolution(expectedP1, false, capargs(slot0arg, [target2])), - ); + dispatch(makeResolve(expectedP1, capargs(slot0arg, [target2]))); } else if (mode === 'to data') { - dispatch.notify(oneResolution(expectedP1, false, capargs(4, []))); + dispatch(makeResolve(expectedP1, capargs(4, []))); } else if (mode === 'reject') { - dispatch.notify(oneResolution(expectedP1, true, capargs('error', []))); + dispatch(makeReject(expectedP1, capargs('error', []))); } else { assert.fail(X`unknown mode ${mode}`); } @@ -462,7 +405,7 @@ async function doResultPromise(t, mode) { t.deepEqual(log, []); // Now we resolve p2, allowing the second two() to proceed - dispatch.notify(oneResolution(expectedP2, false, capargs(4, []))); + dispatch(makeResolve(expectedP2, capargs(4, []))); await waitUntilQuiescent(); if (mode === 'to presence') { @@ -527,7 +470,7 @@ test('liveslots vs symbols', async t => { // E(root)[Symbol.asyncIterator]('one') const rp1 = 'p-1'; - dispatch.deliver(rootA, 'Symbol.asyncIterator', capargs(['one']), 'p-1'); + dispatch(makeMessage(rootA, 'Symbol.asyncIterator', capargs(['one']), 'p-1')); await waitUntilQuiescent(); t.deepEqual(log.shift(), { type: 'resolve', @@ -536,11 +479,12 @@ test('liveslots vs symbols', async t => { t.deepEqual(log, []); // root~.good(target) -> send(methodname=Symbol.asyncIterator) - dispatch.deliver( - rootA, - 'good', - capargs([{ '@qclass': 'slot', index: 0 }], [target]), - undefined, + dispatch( + makeMessage( + rootA, + 'good', + capargs([{ '@qclass': 'slot', index: 0 }], [target]), + ), ); await waitUntilQuiescent(); t.deepEqual(log.shift(), { @@ -561,11 +505,13 @@ test('liveslots vs symbols', async t => { message: 'arbitrary Symbols cannot be used as method names', name: 'Error', }; - dispatch.deliver( - rootA, - 'bad', - capargs([{ '@qclass': 'slot', index: 0 }], [target]), - rp2, + dispatch( + makeMessage( + rootA, + 'bad', + capargs([{ '@qclass': 'slot', index: 0 }], [target]), + rp2, + ), ); await waitUntilQuiescent(); t.deepEqual(log.shift(), { @@ -585,12 +531,12 @@ test('disable disavow', async t => { }, }); } - const dispatch = makeDispatch(syscall, build, false); + const dispatch = makeDispatch(syscall, build, 'vatA', false); t.deepEqual(log, []); const rootA = 'o+0'; // root~.one() // sendOnly - dispatch.deliver(rootA, 'one', capargs([]), undefined); + dispatch(makeMessage(rootA, 'one', capargs([]))); await waitUntilQuiescent(); t.deepEqual(log.shift(), false); t.deepEqual(log, []); @@ -643,13 +589,13 @@ test('disavow', async t => { }); return root; } - const dispatch = makeDispatch(syscall, build, true); + const dispatch = makeDispatch(syscall, build, 'vatA', true); t.deepEqual(log, []); const rootA = 'o+0'; const import1 = 'o-1'; // root~.one(import1) // sendOnly - dispatch.deliver(rootA, 'one', capargsOneSlot(import1), undefined); + dispatch(makeMessage(rootA, 'one', capargsOneSlot(import1))); await waitUntilQuiescent(); t.deepEqual(log.shift(), { type: 'dropImports', slots: [import1] }); t.deepEqual(log.shift(), 'disavowed pres1'); @@ -695,14 +641,14 @@ test('dropExports', async t => { }); return root; } - const dispatch = makeDispatch(syscall, build, true); + const dispatch = makeDispatch(syscall, build, 'vatA', true); t.deepEqual(log, []); const rootA = 'o+0'; // rp1 = root~.one() // ex1 = await rp1 const rp1 = 'p-1'; - dispatch.deliver(rootA, 'one', capargs([]), rp1); + dispatch(makeMessage(rootA, 'one', capargs([]), rp1)); await waitUntilQuiescent(); const l1 = log.shift(); const ex1 = l1.resolutions[0][2].slots[0]; @@ -713,6 +659,6 @@ test('dropExports', async t => { t.deepEqual(log, []); // now tell the vat to drop that export - dispatch.dropExports([ex1]); + dispatch(makeDropExports(ex1)); // for now, all that we care about is that liveslots doesn't crash }); diff --git a/packages/SwingSet/test/test-vpid-liveslots.js b/packages/SwingSet/test/test-vpid-liveslots.js index 9b29be9d531..252a58d24d8 100644 --- a/packages/SwingSet/test/test-vpid-liveslots.js +++ b/packages/SwingSet/test/test-vpid-liveslots.js @@ -1,5 +1,3 @@ -// eslint-disable-next-line no-redeclare -/* global setImmediate */ // eslint-disable-next-line import/order import { test } from '../tools/prepare-test-env-ava'; @@ -7,42 +5,9 @@ import { E } from '@agoric/eventual-send'; import { makePromiseKit } from '@agoric/promise-kit'; import { assert, details as X } from '@agoric/assert'; import { Far } from '@agoric/marshal'; -import { WeakRef, FinalizationRegistry } from '../src/weakref'; -import { makeLiveSlots } from '../src/kernel/liveSlots'; - -function capdata(body, slots = []) { - return harden({ body, slots }); -} - -function capargs(args, slots = []) { - return capdata(JSON.stringify(args), slots); -} - -function oneResolution(promiseID, rejected, data) { - return [[promiseID, rejected, data]]; -} - -function buildSyscall() { - const log = []; - - const syscall = { - send(targetSlot, method, args, resultSlot) { - log.push({ type: 'send', targetSlot, method, args, resultSlot }); - }, - subscribe(target) { - log.push({ type: 'subscribe', target }); - }, - resolve(resolutions) { - log.push({ type: 'resolve', resolutions }); - }, - }; - - return { log, syscall }; -} - -function endOfCrank() { - return new Promise(resolve => setImmediate(() => resolve())); -} +import { waitUntilQuiescent } from '../src/waitUntilQuiescent'; +import { buildSyscall, makeDispatch } from './liveslots-helpers'; +import { makeMessage, makeResolve, makeReject, capargs } from './util'; function hush(p) { p.then( @@ -187,22 +152,6 @@ function resolutionOf(vpid, mode, targets) { return resolution; } -function makeDispatch(syscall, build, vatID = 'vatA') { - function vatDecref() {} - const gcTools = harden({ WeakRef, FinalizationRegistry, vatDecref }); - const { setBuildRootObject, dispatch } = makeLiveSlots( - syscall, - vatID, - {}, - {}, - undefined, - false, - gcTools, - ); - setBuildRootObject(build); - return dispatch; -} - async function doVatResolveCase1(t, mode) { // case 1 const { log, syscall } = buildSyscall(); @@ -233,12 +182,14 @@ async function doVatResolveCase1(t, mode) { const expectedP3 = 'p+7'; const expectedP4 = 'p+8'; - dispatch.deliver( - rootA, - 'run', - capargs([slot0arg, slot1arg], [target1, target2]), + dispatch( + makeMessage( + rootA, + 'run', + capargs([slot0arg, slot1arg], [target1, target2]), + ), ); - await endOfCrank(); + await waitUntilQuiescent(); // The vat should send 'one' and subscribe to the result promise t.deepEqual(log.shift(), { @@ -389,24 +340,26 @@ async function doVatResolveCase23(t, which, mode, stalls) { const target2 = 'o-2'; if (which === 2) { - dispatch.deliver(rootA, 'result', capargs([], []), p1); - dispatch.deliver(rootA, 'promise', capargs([slot0arg], [p1])); + dispatch(makeMessage(rootA, 'result', capargs([], []), p1)); + dispatch(makeMessage(rootA, 'promise', capargs([slot0arg], [p1]))); } else if (which === 3) { - dispatch.deliver(rootA, 'promise', capargs([slot0arg], [p1])); - dispatch.deliver(rootA, 'result', capargs([], []), p1); + dispatch(makeMessage(rootA, 'promise', capargs([slot0arg], [p1]))); + dispatch(makeMessage(rootA, 'result', capargs([], []), p1)); } else { assert.fail(X`bad which=${which}`); } - await endOfCrank(); + await waitUntilQuiescent(); t.deepEqual(log.shift(), { type: 'subscribe', target: p1 }); t.deepEqual(log, []); - dispatch.deliver( - rootA, - 'run', - capargs([slot0arg, slot1arg], [target1, target2]), + dispatch( + makeMessage( + rootA, + 'run', + capargs([slot0arg, slot1arg], [target1, target2]), + ), ); - await endOfCrank(); + await waitUntilQuiescent(); // At the end of the turn in which run() is executed, the promise queue // will contain deliveries one() and two() (specifically invocations of the @@ -463,8 +416,8 @@ async function doVatResolveCase23(t, which, mode, stalls) { // `syscall.resolve(vpid1, stuff)` into the kernel, notifying any remote // subscribers that p1 has been resolved. Since the vat is also a subscriber, // thenResolve's callback must also invoke p1's resolver (which was stashed in - // importedPromisesByPromiseID), as if the kernel had call the vat's - // dispatch.notify. This causes the p1.then callback to be pushed to the back + // importedPromisesByPromiseID), as if the kernel had called the vat's + // dispatch(notify). This causes the p1.then callback to be pushed to the back // of the promise queue, which will set resolutionOfP1 after all the syscalls // have been made. @@ -606,13 +559,13 @@ async function doVatResolveCase4(t, mode) { } const target2 = 'o-2'; - dispatch.deliver(rootA, 'get', capargs([slot0arg], [p1])); - await endOfCrank(); + dispatch(makeMessage(rootA, 'get', capargs([slot0arg], [p1]))); + await waitUntilQuiescent(); t.deepEqual(log.shift(), { type: 'subscribe', target: p1 }); t.deepEqual(log, []); - dispatch.deliver(rootA, 'first', capargs([slot0arg], [target1])); - await endOfCrank(); + dispatch(makeMessage(rootA, 'first', capargs([slot0arg], [target1]))); + await waitUntilQuiescent(); const expectedP2 = nextP(); t.deepEqual(log.shift(), { @@ -635,26 +588,28 @@ async function doVatResolveCase4(t, mode) { t.deepEqual(log.shift(), { type: 'subscribe', target: expectedP3 }); t.deepEqual(log, []); + let r; if (mode === 'presence') { - dispatch.notify(oneResolution(p1, false, capargs(slot0arg, [target2]))); + r = makeResolve(p1, capargs(slot0arg, [target2])); } else if (mode === 'local-object') { - dispatch.notify(oneResolution(p1, false, capargs(slot0arg, [rootA]))); + r = makeResolve(p1, capargs(slot0arg, [rootA])); } else if (mode === 'data') { - dispatch.notify(oneResolution(p1, false, capargs(4, []))); + r = makeResolve(p1, capargs(4, [])); } else if (mode === 'promise-data') { - dispatch.notify(oneResolution(p1, false, capargs([slot0arg], [p1]))); + r = makeResolve(p1, capargs([slot0arg], [p1])); } else if (mode === 'reject') { - dispatch.notify(oneResolution(p1, true, capargs('error', []))); + r = makeReject(p1, capargs('error', [])); } else if (mode === 'promise-reject') { - dispatch.notify(oneResolution(p1, true, capargs([slot0arg], [p1]))); + r = makeReject(p1, capargs([slot0arg], [p1])); } else { assert.fail(X`unknown mode ${mode}`); } - await endOfCrank(); + dispatch(r); + await waitUntilQuiescent(); t.deepEqual(log, []); - dispatch.deliver(rootA, 'second', capargs([slot0arg], [target1])); - await endOfCrank(); + dispatch(makeMessage(rootA, 'second', capargs([slot0arg], [target1]))); + await waitUntilQuiescent(); const expectedP4 = nextP(); const expectedP5 = nextP(); @@ -733,16 +688,16 @@ test('inter-vat circular promise references', async t => { // const pbB = 'p-18'; // const paB = 'p-19'; - dispatchA.deliver(rootA, 'genPromise', capargs([], []), paA); - await endOfCrank(); + dispatchA(makeMessage(rootA, 'genPromise', capargs([], []), paA)); + await waitUntilQuiescent(); t.deepEqual(log, []); - // dispatchB.deliver(rootB, 'genPromise', capargs([], []), pbB); - // await endOfCrank(); + // dispatchB(makeMessage(rootB, 'genPromise', capargs([], []), pbB)); + // await waitUntilQuiescent(); // t.deepEqual(log, []); - dispatchA.deliver(rootA, 'usePromise', capargs([[slot0arg]], [pbA])); - await endOfCrank(); + dispatchA(makeMessage(rootA, 'usePromise', capargs([[slot0arg]], [pbA]))); + await waitUntilQuiescent(); t.deepEqual(log.shift(), { type: 'subscribe', target: pbA }); t.deepEqual(log.shift(), { type: 'resolve', @@ -750,8 +705,8 @@ test('inter-vat circular promise references', async t => { }); t.deepEqual(log, []); - // dispatchB.deliver(rootB, 'usePromise', capargs([[slot0arg]], [paB])); - // await endOfCrank(); + // dispatchB(makeMessage(rootB, 'usePromise', capargs([[slot0arg]], [paB]))); + // await waitUntilQuiescent(); // t.deepEqual(log.shift(), { type: 'subscribe', target: paB }); // t.deepEqual(log.shift(), { // type: 'resolve', diff --git a/packages/SwingSet/test/util.js b/packages/SwingSet/test/util.js index 32844981d15..fbb59b9ca8a 100644 --- a/packages/SwingSet/test/util.js +++ b/packages/SwingSet/test/util.js @@ -1,3 +1,5 @@ +import { assert } from '@agoric/assert'; + function compareArraysOfStrings(a, b) { a = a.join(' '); b = b.join(' '); @@ -36,22 +38,33 @@ export function dumpKT(kernel) { export function buildDispatch(onDispatchCallback = undefined) { const log = []; - const dispatch = { - deliver(targetSlot, method, args, resultSlot) { - const d = { type: 'deliver', targetSlot, method, args, resultSlot }; + function dispatch(vatDeliverObject) { + const [type, ...vdoargs] = vatDeliverObject; + if (type === 'message') { + const [target, msg] = vdoargs; + const { method, args, result } = msg; + const d = { + type: 'deliver', + targetSlot: target, + method, + args, + resultSlot: result, + }; log.push(d); if (onDispatchCallback) { onDispatchCallback(d); } - }, - notify(resolutions) { + } else if (type === 'notify') { + const [resolutions] = vdoargs; const d = { type: 'notify', resolutions }; log.push(d); if (onDispatchCallback) { onDispatchCallback(d); } - }, - }; + } else { + throw Error(`unknown vatDeliverObject type ${type}`); + } + } return { log, dispatch }; } @@ -62,3 +75,59 @@ export function ignore(p) { () => 0, ); } + +export function extractMessage(vatDeliverObject) { + const [type, ...vdoargs] = vatDeliverObject; + assert.equal(type, 'message'); + const [facetID, msg] = vdoargs; + const { method, args, result } = msg; + return { facetID, method, args, result }; +} + +function capdata(body, slots = []) { + return harden({ body, slots }); +} + +export function capargs(args, slots = []) { + return capdata(JSON.stringify(args), slots); +} + +export function capdataOneSlot(slot) { + return capargs({ '@qclass': 'slot', iface: 'Alleged: export', index: 0 }, [ + slot, + ]); +} + +export function capargsOneSlot(slot) { + return capargs( + [{ '@qclass': 'slot', iface: 'Alleged: export', index: 0 }], + [slot], + ); +} + +export function makeMessage(target, method, args, result = undefined) { + const msg = { method, args, result }; + const vatDeliverObject = harden(['message', target, msg]); + return vatDeliverObject; +} + +export function makeResolutions(resolutions) { + const vatDeliverObject = harden(['notify', resolutions]); + return vatDeliverObject; +} + +export function makeResolve(target, result) { + const resolutions = [[target, false, result]]; + return makeResolutions(resolutions); +} + +export function makeReject(target, result) { + const resolutions = [[target, true, result]]; + const vatDeliverObject = harden(['notify', resolutions]); + return vatDeliverObject; +} + +export function makeDropExports(...vrefs) { + const vatDeliverObject = harden(['dropExports', vrefs]); + return vatDeliverObject; +} diff --git a/packages/SwingSet/test/vat-controller-1 b/packages/SwingSet/test/vat-controller-1 index 9bfd9417a9c..a192d623cd1 100644 --- a/packages/SwingSet/test/vat-controller-1 +++ b/packages/SwingSet/test/vat-controller-1 @@ -1,7 +1,9 @@ // -*- js -*- +import { extractMessage } from './util'; export default function setup(syscall, _state, _helpers, vatPowers) { - function deliver(target, method, args) { - vatPowers.testLog(JSON.stringify({ target, method, args })); + function dispatch(vatDeliverObject) { + const { facetID, method, args } = extractMessage(vatDeliverObject); + vatPowers.testLog(JSON.stringify({ target: facetID, method, args })); } - return { deliver }; + return dispatch; } diff --git a/packages/SwingSet/test/vat-syscall-failure.js b/packages/SwingSet/test/vat-syscall-failure.js index 533be174bec..a761e80040e 100644 --- a/packages/SwingSet/test/vat-syscall-failure.js +++ b/packages/SwingSet/test/vat-syscall-failure.js @@ -1,3 +1,5 @@ +import { extractMessage } from './util'; + function capdata(body, slots = []) { return harden({ body, slots }); } @@ -7,10 +9,11 @@ function capargs(args, slots = []) { } export default function setup(syscall, _state, _helpers, vatPowers) { - function deliver(target, method, args) { + function dispatch(vatDeliverObject) { + const { method, args } = extractMessage(vatDeliverObject); vatPowers.testLog(`${method}`); const thing = method === 'begood' ? args.slots[0] : 'o-3414159'; syscall.send(thing, 'pretendToBeAThing', capargs([method])); } - return { deliver }; + return dispatch; }