diff --git a/package.json b/package.json index 48faff4f999b..b132dda6c3a2 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "packages/cosmic-swingset", "packages/agoric-cli", "packages/deployment", - "packages/notifier" + "packages/notifier", + "packages/xs-vat-worker" ], "devDependencies": { "eslint": "^6.8.0", diff --git a/packages/xs-vat-worker/.eslintignore b/packages/xs-vat-worker/.eslintignore new file mode 100644 index 000000000000..ea65306b6ba6 --- /dev/null +++ b/packages/xs-vat-worker/.eslintignore @@ -0,0 +1,4 @@ +/dist/ +/scripts/ +/xs_modules/ +/swingset/ diff --git a/packages/xs-vat-worker/.eslintrc.js b/packages/xs-vat-worker/.eslintrc.js new file mode 100644 index 000000000000..3f42e1d90c74 --- /dev/null +++ b/packages/xs-vat-worker/.eslintrc.js @@ -0,0 +1,27 @@ +/* global module */ +module.exports = { + // parser: "babel-eslint", + extends: ['airbnb-base', 'plugin:prettier/recommended'], + env: { + es6: true, // supports new ES6 globals (e.g., new types such as Set) + }, + globals: { + "harden": "readonly", + }, + rules: { + 'implicit-arrow-linebreak': 'off', + 'function-paren-newline': 'off', + 'arrow-parens': 'off', + strict: 'off', + 'prefer-destructuring': 'off', + 'no-else-return': 'off', + 'no-console': 'off', + 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + 'no-return-assign': 'off', + 'no-param-reassign': 'off', + 'no-restricted-syntax': ['off', 'ForOfStatement'], + 'no-unused-expressions': 'off', + 'no-loop-func': 'off', + 'import/prefer-default-export': 'off', // contrary to Agoric standard + }, +}; diff --git a/packages/xs-vat-worker/.npmignore b/packages/xs-vat-worker/.npmignore new file mode 100644 index 000000000000..cd75c6170bfa --- /dev/null +++ b/packages/xs-vat-worker/.npmignore @@ -0,0 +1,11 @@ +# Demo +demo + +# scripts +scripts + +# test +test + +# Travis CI +.travis.yml diff --git a/packages/xs-vat-worker/.prettierrc.json b/packages/xs-vat-worker/.prettierrc.json new file mode 100644 index 000000000000..6e778b4fb9c5 --- /dev/null +++ b/packages/xs-vat-worker/.prettierrc.json @@ -0,0 +1,4 @@ +{ + "trailingComma": "all", + "singleQuote": true +} diff --git a/packages/xs-vat-worker/bin/node-vat-worker b/packages/xs-vat-worker/bin/node-vat-worker new file mode 100755 index 000000000000..9461d5dbf477 --- /dev/null +++ b/packages/xs-vat-worker/bin/node-vat-worker @@ -0,0 +1,2 @@ +#!/bin/sh +node -r esm ./src/index.js diff --git a/packages/xs-vat-worker/manifest.json b/packages/xs-vat-worker/manifest.json new file mode 100644 index 000000000000..411a331daf31 --- /dev/null +++ b/packages/xs-vat-worker/manifest.json @@ -0,0 +1,48 @@ +{ + "include": [ + "$(MODDABLE)/examples/manifest_base.json" + ], + "$note": [ + "horrible @agoric/E KLUDGE below", + "liveSlots is a bit KLUDGy too" + ], + "creation": { + "keys": { + "available": 4096 + }, + "stack": 4096, + "parser": { + "buffer": 32768 + } + }, + "strip": [], + "modules": { + "files": [ "$(MODULES)/files/file/*", "$(MODULES)/files/file/lin/*" ], + "fdchan": [ "./fdchan" ], + "timer": [ + "$(MODULES)/base/timer/timer", + "$(MODULES)/base/timer/lin/*", + ], + "@agoric/nat": "xs_modules/@agoric/nat/nat.esm", + "@agoric/install-ses": "xs_modules/@agoric/install-ses/install-ses", + "@agoric/import-bundle": "xs_modules/@agoric/import-bundle/index", + "@agoric/compartment-wrapper": "xs_modules/@agoric/import-bundle/compartment-wrapper", + "@agoric/eventual-send": [ + "xs_modules/@agoric/eventual-send/index" + ], + "@agoric/E": "xs_modules/@agoric/eventual-send/E", + "@agoric/marshal": "xs_modules/@agoric/marshal/marshal", + "@agoric/produce-promise": "xs_modules/@agoric/produce-promise/producePromise", + "@agoric/assert": "xs_modules/@agoric/assert/assert", + "@agoric/types": "xs_modules/@agoric/assert/types", + "swingset/capdata": "./swingset/capdata", + "swingset/parseVatSlots": "./swingset/parseVatSlots", + "swingset/kernel/liveSlots": "./swingset/kernel/liveSlots", + "*": [ + "./main", + "./console", + "./timer-ticks", + "./vatWorker" + ] + } +} diff --git a/packages/xs-vat-worker/package.json b/packages/xs-vat-worker/package.json new file mode 100644 index 000000000000..66624812561c --- /dev/null +++ b/packages/xs-vat-worker/package.json @@ -0,0 +1,57 @@ +{ + "name": "@agoric/xs-vat-worker", + "version": "0.0.1", + "description": "???", + "main": "dist/vat-worker.cjs.js", + "module": "dist/vat-worker.esm.js", + "browser": "dist/vat-worker.umd.js", + "scripts": { + "xs-build": "mkdir -p build; mcconfig -o build -p x-cli-lin -m -d", + "xs-run": "yarn xs-build && ./build/bin/lin/debug/make-vat-transcript", + "xs-build-release": "mkdir -p build; mcconfig -o build -p x-cli-lin -m", + "xs-run-release": "yarn xs-build-release && ./build/bin/lin/release/make-vat-transcript", + "test": "tape -r esm 'test/**/test-*.js'", + "build": "exit 0", + "lint-fix": "eslint --fix '**/*.{js,jsx}'", + "lint-check": "eslint '**/*.{js,jsx}'", + "lint-fix-jessie": "eslint -c '.eslintrc-jessie.js' --fix '**/*.{js,jsx}'", + "lint-check-jessie": "eslint -c '.eslintrc-jessie.js' '**/*.{js,jsx}'" + }, + "devDependencies": { + "eslint": "^6.1.0", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-config-jessie": "0.0.3", + "eslint-config-prettier": "^6.0.0", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-prettier": "^3.1.0", + "prettier": "^1.18.2", + "tap-spec": "^5.0.0", + "tape": "^4.11.0", + "tape-promise": "^4.0.0" + }, + "dependencies": { + "@agoric/assert": "^0.0.8", + "@agoric/bundle-source": "^1.1.6", + "@agoric/eventual-send": "^0.9.3", + "@agoric/import-bundle": "^0.0.8", + "@agoric/install-ses": "^0.2.0", + "@agoric/produce-promise": "^0.1.3", + "@agoric/swingset-vat": "^0.6.0", + "anylogger": "^1.0.4", + "esm": "^3.2.5" + }, + "keywords": [], + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "https://gist.github.com/f8e0b5d838079a994784d599c282cce7.git" + }, + "author": "Agoric", + "license": "Apache-2.0", + "bugs": { + "url": "https://gist.github.com/dckc/f8e0b5d838079a994784d599c282cce7" + }, + "homepage": "https://gist.github.com/dckc/f8e0b5d838079a994784d599c282cce7" +} diff --git a/packages/xs-vat-worker/src-native/fdchan.c b/packages/xs-vat-worker/src-native/fdchan.c new file mode 100644 index 000000000000..6ee2dd1bc55b --- /dev/null +++ b/packages/xs-vat-worker/src-native/fdchan.c @@ -0,0 +1,74 @@ +/** + * ref Netstrings 19970201 by Bernstein + * https://cr.yp.to/proto/netstrings.txt + * + * Moddable in C + * https://github.com/Moddable-OpenSource/moddable/blob/public/documentation/xs/XS%20in%20C.md + * + * stdio + * https://pubs.opengroup.org/onlinepubs/009695399/basedefs/stdio.h.html + */ + +#include +#include + +#include "xsAll.h" +#include "xs.h" + +void xs_Reader(xsMachine *the) { + int argc = xsToInteger(xsArgc); + if (argc < 1) { + mxTypeError("expected fd"); + } + int fd = xsToInteger(xsArg(0)); + FILE *inStream = fdopen(fd, "rb"); + if (!inStream) { + mxUnknownError("fdopen failed"); + } + xsSetHostData(xsThis, (void *)((uintptr_t)inStream)); + + // modInstrumentationAdjust(Files, +1); +} + +void xs_Writer(xsMachine *the) { + int argc = xsToInteger(xsArgc); + if (argc < 1) { + mxTypeError("expected fd"); + } + int fd = xsToInteger(xsArg(0)); + FILE *outStream = fdopen(fd, "wb"); + if (!outStream) { + mxUnknownError("fdopen failed"); + } + xsSetHostData(xsThis, (void *)((uintptr_t)outStream)); + + // modInstrumentationAdjust(Files, +1); +} + +void xs_read_netstring(xsMachine *the) { + size_t len; + char* buf = NULL; + FILE *inStream = xsGetHostData(xsThis); + assert(inStream); + + if (fscanf(inStream, "%9lu", &len) < 1) { goto BARF; } /* >999999999 bytes is bad */ + // fprintf(stderr, "xs_stdin_read_netstring len %lu\n", len); + if (fgetc(inStream) != ':') { goto BARF; } + buf = malloc(len + 1); /* malloc(0) is not portable */ + if (!buf) { goto BARF; } + if (fread(buf, 1, len, inStream) < len) { goto BARF; } + if (fgetc(inStream) != ',') { goto BARF; } + + xsResult = xsStringBuffer(buf, len); + free(buf); + // fprintf(stderr, "xs_stdin_read_nestring return\n"); + return; + +BARF: + free(buf); + xsUnknownError("getline failed"); +} + +void xs_fdchan_destructor() { + +} diff --git a/packages/xs-vat-worker/src-native/fdchan.js b/packages/xs-vat-worker/src-native/fdchan.js new file mode 100644 index 000000000000..f7d42f380d65 --- /dev/null +++ b/packages/xs-vat-worker/src-native/fdchan.js @@ -0,0 +1,9 @@ +export class Reader @ "xs_fdchan_destructor" { + constructor(fd) @ "xs_Reader" + read_netstring() @ "xs_read_netstring"; +} + +export class Writer @ "xs_fdchan_destructor" { + constructor(fd) @ "xs_Writer" + write(...items) @ "xs_file_write"; +} diff --git a/packages/xs-vat-worker/src/console.js b/packages/xs-vat-worker/src/console.js new file mode 100644 index 000000000000..ac8ba1bf457b --- /dev/null +++ b/packages/xs-vat-worker/src/console.js @@ -0,0 +1,25 @@ +/** console for xs platform */ +/* global trace, globalThis */ +const harden = x => Object.freeze(x, true); + +const text = it => (typeof it === 'object' ? JSON.stringify(it) : `${it}`); +const combine = (...things) => `${things.map(text).join(' ')}\n`; + +export function makeConsole(write_) { + const write = write_ || trace; // note ocap exception for tracing / logging + return harden({ + log(...things) { + write(combine(...things)); + }, + // node.js docs say this is just an alias for error + warn(...things) { + write(combine('WARNING: ', ...things)); + }, + // node docs say this goes to stderr + error(...things) { + write(combine('ERROR: ', ...things)); + }, + }); +} + +globalThis.console = makeConsole(); diff --git a/packages/xs-vat-worker/src/index.js b/packages/xs-vat-worker/src/index.js new file mode 100755 index 000000000000..1cde573ea1d2 --- /dev/null +++ b/packages/xs-vat-worker/src/index.js @@ -0,0 +1,111 @@ +// vatWorker driver for node.js +// contrast with main.js for xs +import '@agoric/install-ses'; + +import { main as vatWorker } from './vatWorker'; + +const INFD = 3; +const OUTFD = 4; + +function makePipe(io, sleep) { + function write(data) { + let done = 0; + for (;;) { + try { + done += io.writeSync(OUTFD, data.slice(done)); + if (done >= data.length) { + return done; + } + } catch (writeFailed) { + if (writeFailed.code === 'EAGAIN') { + sleep(0.1); + // try again + } else { + throw writeFailed; + } + } + } + } + + return harden({ + writeMessage(msg) { + write(`${msg.length}:`); + write(msg); + write(','); + }, + readMessage(EOF) { + let buf = Buffer.from('999999999:', 'utf-8'); + let len = null; + let colonPos = null; + let offset = 0; + + for (;;) { + // console.error('readMessage', { length: buf.length, len, colonPos, offset }); + try { + offset += io.readSync(INFD, buf, { + offset, + length: buf.length - offset, + }); + } catch (err) { + if (err.code === 'EAGAIN') { + sleep(0.1); + // eslint-disable-next-line no-continue + continue; + } else if (err.code === 'EOF') { + throw EOF; + } else { + throw err; + } + } + if (len === null) { + colonPos = buf.indexOf(':'); + if (colonPos > 0) { + const digits = buf.slice(0, colonPos).toString('utf-8'); + len = parseInt(digits, 10); + const rest = Buffer.alloc(len + 1); + // console.error('parsed len. copy', { digits, len, targetStart: 0, sourceStart: colonPos + 1, sourceEnd: offset }); + buf.copy(rest, 0, colonPos + 1, offset); + buf = rest; + offset -= colonPos + 1; + } + } else if (offset === len + 1) { + const delim = buf.slice(-1).toString('utf-8'); + if (delim !== ',') { + throw new Error( + `bad netstring: length ${len} expected , found [${delim}]`, + ); + } + const result = buf.slice(0, -1).toString('utf-8'); + // console.error({ colon: colonPos, len, result: result.slice(0, 20) }); + return result; + } + } + }, + }); +} + +async function main({ setImmediate, fs, spawnSync }) { + const sleep = secs => spawnSync('sleep', [secs]); + const pipe = makePipe(fs, sleep); + return vatWorker({ + readMessage: pipe.readMessage, + writeMessage: pipe.writeMessage, + setImmediate, + }); +} + +main({ + setImmediate, + // eslint-disable-next-line global-require + spawnSync: require('child_process').spawnSync, + fs: { + // eslint-disable-next-line global-require + readSync: require('fs').readSync, + // eslint-disable-next-line global-require + writeSync: require('fs').writeSync, + }, +}) + .catch(err => { + console.error(err); + }) + .then(() => process.exit(0)); diff --git a/packages/xs-vat-worker/src/main.js b/packages/xs-vat-worker/src/main.js new file mode 100644 index 000000000000..eb03ae228d34 --- /dev/null +++ b/packages/xs-vat-worker/src/main.js @@ -0,0 +1,26 @@ +// add harden; align xs's Compartment with Agoric's +// ISSUE: use a different module name? +import '@agoric/install-ses'; + +import './console'; // sets globalThis.console. ew. +import './timer-ticks'; // globalThis.setTimeout. ew. + +import { main as vatWorker } from './vatWorker'; +import { Reader, Writer } from './fdchan'; + +const INFD = 3; +const OUTFD = 4; + +export default async function main() { + const inStream = new Reader(INFD); + const outStream = new Writer(OUTFD); + + return vatWorker({ + setImmediate, + readMessage: () => inStream.read_netstring(), + writeMessage: message => { + // ISSUE: should be byte length + outStream.write(`${message.length}:`, message, ','); + }, + }); +} diff --git a/packages/xs-vat-worker/src/netstring.js b/packages/xs-vat-worker/src/netstring.js new file mode 100644 index 000000000000..3bd8edc1bdc8 --- /dev/null +++ b/packages/xs-vat-worker/src/netstring.js @@ -0,0 +1,51 @@ +export async function readNetstring(input) { + let prefix = Buffer.from([]); + let colonPos = -1; + + const nextChunk = () => + new Promise((resolve, _reject) => { + const rx = data => { + input.pause(); + input.removeListener('data', rx); + resolve(data); + }; + input.on('data', rx); + input.resume(); + }); + + while (colonPos < 0) { + // eslint-disable-next-line no-await-in-loop + const more = await nextChunk(); + prefix = Buffer.concat([prefix, more]); + colonPos = prefix.indexOf(':'); + } + let len; + const digits = prefix.slice(0, colonPos).toString('utf-8'); + try { + len = parseInt(digits, 10); + } catch (badLen) { + throw new Error(`bad netstring length ${digits}`); + } + // console.error('readNetstring parsed len', { digits, len }); + let data = prefix.slice(colonPos + 1); + while (data.length <= len) { + // console.log('netstring: looking for payload', data.length, len); + // eslint-disable-next-line no-await-in-loop + const more = await nextChunk(input); + data = Buffer.concat([data, more]); + } + if (data.slice(len).toString('utf-8') !== ',') { + throw new Error( + `bad netstring: expected , got ${data.slice(-1)} [${data.slice(-20)}]`, + ); + } + return data.slice(0, len).toString('utf-8'); +} + +export async function writeNetstring(out, payload) { + // ISSUE: non-ASCII length + // console.log('kernelSimulator send size', content.length); + await out.write(`${payload.length}:`); + await out.write(payload); + await out.write(','); +} diff --git a/packages/xs-vat-worker/src/timer-ticks.js b/packages/xs-vat-worker/src/timer-ticks.js new file mode 100644 index 000000000000..39c96d0580eb --- /dev/null +++ b/packages/xs-vat-worker/src/timer-ticks.js @@ -0,0 +1,17 @@ +// ref moddable/examples/base/timers/main.js +/* global globalThis */ + +// eslint-disable-next-line import/no-unresolved +import Timer from 'timer'; // moddable timer + +globalThis.setImmediate = callback => { + Timer.set(callback); +}; + +globalThis.setTimeout = (callback, delay) => { + Timer.set(callback, delay); +}; + +globalThis.setInterval = (callback, delay) => { + Timer.repeat(callback, delay); +}; diff --git a/packages/xs-vat-worker/src/vatWorker.js b/packages/xs-vat-worker/src/vatWorker.js new file mode 100644 index 000000000000..a7e741379eb5 --- /dev/null +++ b/packages/xs-vat-worker/src/vatWorker.js @@ -0,0 +1,159 @@ +import { importBundle } from '@agoric/import-bundle'; +import { HandledPromise } from '@agoric/eventual-send'; +// TODO? import anylogger from 'anylogger'; +import { makeLiveSlots } from '@agoric/swingset-vat/src/kernel/liveSlots'; + +const EOF = new Error('EOF'); + +// from SwingSet/src/controller.js +function makeConsole(_tag) { + const log = console; // TODO? anylogger(tag); + const cons = {}; + for (const level of ['debug', 'log', 'info', 'warn', 'error']) { + cons[level] = log[level]; + } + return harden(cons); +} + +function makeVatEndowments(consoleTag) { + return harden({ + console: makeConsole(`SwingSet:${consoleTag}`), + HandledPromise, + // TODO: re2 is a RegExp work-a-like that disables backtracking expressions for + // safer memory consumption + RegExp, + }); +} + +// see also: detecting an empty vat promise queue (end of "crank") +// https://github.com/Agoric/agoric-sdk/issues/45 +function endOfCrank(setImmediate) { + return new Promise((resolve, _reject) => { + setImmediate(() => { + // console.log('hello from setImmediate callback. The promise queue is presumably empty.'); + resolve(); + }); + }); +} + +function makeWorker(io, setImmediate) { + let vatNS = null; + let dispatch; + let state; + + const format = msg => JSON.stringify(msg); + const sync = (method, args) => { + io.writeMessage(format({ msgtype: 'syscall', method, args })); + return JSON.parse(io.readMessage()); + }; + const syscall = harden({ + subscribe(...args) { + return sync('subscribe', args); + }, + send(...args) { + return sync('send', args); + }, + fulfillToData(...args) { + return sync('fulfillToData', args); + }, + fulfillToPresence(...args) { + return sync('fulfillToPresence', args); + }, + reject(...args) { + return sync('reject', args); + }, + }); + + async function loadBundle(name, bundle) { + if (vatNS !== null) { + throw new Error('bundle already loaded'); + } + + vatNS = await importBundle(bundle, { + filePrefix: name, + endowments: makeVatEndowments(name), + }); + // TODO: be sure console.log isn't mixed with protocol stream + // console.log('loaded bundle with methods', Object.keys(vatNS)); + + state = {}; // ?? + dispatch = makeLiveSlots(syscall, state, vatNS.buildRootObject); + } + + function turnCrank(dispatchType, args) { + return new Promise((resolve, reject) => { + try { + dispatch[dispatchType](...args); + } catch (error) { + // console.log({ dispatchError: error }); + reject(error); + return; + } + endOfCrank(setImmediate).then(resolve); + }); + } + + const name = 'WORKER'; // TODO? + async function handle(message) { + switch (message.msgtype) { + case 'load-bundle': + try { + await loadBundle(name, message.bundle); + } catch (error) { + // console.log('load-bundle failed:', error); + io.writeMessage( + format({ msgtype: 'load-bundle-nak', error: error.message }), + ); + break; + } + io.writeMessage(format({ msgtype: 'load-bundle-ack' })); + break; + case 'dispatch': + try { + await turnCrank(message.type, message.args); + } catch (error) { + io.writeMessage( + format({ + msgtype: 'dispatch-nak', + error: error instanceof Error ? error.message : error, + }), + ); + break; + } + io.writeMessage(format({ msgtype: 'dispatch-ack' })); + break; + case 'finish': + io.writeMessage(format({ msgtype: 'finish-ack' })); + break; + default: + console.warn('unexpected msgtype', message.msgtype); + } + return message.msgtype; + } + + return harden({ handle }); +} + +export async function main({ readMessage, writeMessage, setImmediate }) { + const worker = makeWorker({ readMessage, writeMessage }, setImmediate); + + for (;;) { + let message; + try { + // eslint-disable-next-line no-await-in-loop + message = JSON.parse(readMessage(EOF)); + } catch (noMessage) { + if (noMessage === EOF) { + return; + } + console.warn('problem getting message:', noMessage); + // eslint-disable-next-line no-continue + continue; + } + // eslint-disable-next-line no-await-in-loop + const msgtype = await worker.handle(message); + if (msgtype === 'finish') { + break; + } + } +} diff --git a/packages/xs-vat-worker/test/Makefile b/packages/xs-vat-worker/test/Makefile new file mode 100644 index 000000000000..36ca5b3b3f82 --- /dev/null +++ b/packages/xs-vat-worker/test/Makefile @@ -0,0 +1,29 @@ +NODE_ESM=node -r esm + +transcript.txt: swingset-kernel-state + $(NODE_ESM) ../swingset-runner/bin/kerneldump --filedb ./swingset-kernel-state | grep v1.t |sort >$@ + +swingset-kernel-state: bootstrap.js vat-target.js + $(NODE_ESM) ../swingset-runner/bin/runner --filedb run + +ZOE1=../zoe/test/swingsetTests/zoe + + +zoe: transcript-zoe.txt ./node-vat-worker index.js vatWorker.js kernelSimulator.js + VAT1=$(ZOE1)/vat-zoe.js TRANSCRIPT=transcript-zoe.txt node -r esm kernelSimulator.js + +zoe-node: transcript-zoe.txt ./node-vat-worker index.js vatWorker.js kernelSimulator.js + VAT1=$(ZOE1)/vat-zoe.js TRANSCRIPT=transcript-zoe.txt WORKERBIN=./node-vat-worker node -r esm kernelSimulator.js + + +transcript-zoe.txt: $(ZOE1)/swingset-kernel-state + $(NODE_ESM) ../swingset-runner/bin/kerneldump --filedb $(ZOE1)/swingset-kernel-state | grep ^v5.t | grep -v nextID >,zoe-all + python sort_transcript.py <,zoe-all >$@ + + +$(ZOE1)/swingset-kernel-state: ,zoe-patched + (cd $(ZOE1) && $(NODE_ESM) ../../../../swingset-runner/bin/runner --filedb run) + +,zoe-patched: $(ZOE1)/bootstrap.js zoe-test.patch + cd ../.. && patch -p1 0, + () => 0, + ); +} + +export function buildRootObject() { + const callbackObj = harden({ + callback(arg1, arg2) { + console.log(`callback`, arg1, arg2); + return ['data', callbackObj]; // four, resolves pF + }, + }); + + const precD = producePromise(); + const precE = producePromise(); + + return harden({ + bootstrap(_argv, vats) { + const pA = E(vats.target).zero(callbackObj, precD.promise, precE.promise); + E(vats.target).one(); + precD.resolve(callbackObj); // two + precE.reject(Error('four')); // three + pA.then(([pB, pC]) => { + ignore(pB); + ignore(pC); + }); + }, + }); +} diff --git a/packages/xs-vat-worker/test/kernelSimulator.js b/packages/xs-vat-worker/test/kernelSimulator.js new file mode 100644 index 000000000000..4bb07c6c05b4 --- /dev/null +++ b/packages/xs-vat-worker/test/kernelSimulator.js @@ -0,0 +1,134 @@ +// Usage: node -r esm kernelSimulator.js +// context: https://github.com/Agoric/agoric-sdk/issues/1299 +import '@agoric/install-ses'; + +import { readNetstring, writeNetstring } from '../src/netstring'; + +const INFD = 3; +const OUTFD = 4; + +function options(env) { + return { + vat1: env.VAT1 || 'vat-target.js', + transcript: env.TRANSCRIPT || 'transcript.txt', + workerBin: env.WORKERBIN || './build/bin/lin/release/xs-vat-worker', + }; +} + +function makeWorker(child) { + const format = obj => JSON.stringify(obj); + const send = obj => writeNetstring(child.stdio[INFD], format(obj)); + + child.stdio[OUTFD].pause(); + + const expect = async msgtype => { + const txt = await readNetstring(child.stdio[OUTFD]); + let msg; + try { + msg = JSON.parse(txt); + } catch (badJSON) { + console.error('bad JSON ', txt.length, ' chars: [', txt, ']', badJSON); + throw badJSON; + } + if (msg.msgtype !== msgtype) { + throw new Error(`expected ${msgtype}; found: ${msg.msgtype}; error: ${msg.error} [${JSON.stringify(msg)}]`); + } + return msg; + }; + + return harden({ + async loadVat(bundle) { + await send({ msgtype: 'load-bundle', bundle }); + return expect('load-bundle-ack'); + }, + async dispatch({ d, syscalls, _crankNumber }) { + await send({ msgtype: 'dispatch', type: d[0], args: d.slice(1) }); + for (const syscall of syscalls) { + // eslint-disable-next-line no-await-in-loop + const request = await expect('syscall'); + console.log('syscall request', request); + // eslint-disable-next-line no-await-in-loop + await send({ msgtype: 'syscall-ack', response: syscall.response }); + } + return expect('dispatch-ack'); + }, + async finish() { + await send({ msgtype: 'finish' }); + await expect('finish-ack'); + }, + }); +} + +async function runTranscript(w1, bundle, transcript) { + await w1.loadVat(bundle); + console.log('loadVat done.'); + + const events = transcript.split('\n'); + + while (events.length > 0) { + const event = events.shift(); + if (!event) { + // eslint-disable-next-line no-continue + continue; + } + const found = event.match(/^(?[^ ]+) :: (?.*)/); + if (!found) { + console.log('unexpected transcript format', { line: event }); + // eslint-disable-next-line no-continue + continue; + } + const { id, payload } = found.groups; + + const obj = JSON.parse(payload); + if (typeof obj !== 'object') { + console.log('not a dispatch event', id, obj); + // eslint-disable-next-line no-continue + continue; + } + console.log('dispatching:', id); + // eslint-disable-next-line no-await-in-loop + await w1.dispatch(obj); + console.log('dispatch done:', id); + } + + await w1.finish(); + console.log('END OF TRANSCRIPT.'); +} + +// eslint-disable-next-line no-shadow +export async function main(argv, { env, io, bundleSource, spawn }) { + const { vat1, transcript, workerBin } = options(env); + + const bundle = await bundleSource(vat1); + + console.log('spawning', { workerBin }); + const child = await spawn(workerBin, [], { + stdio: ['inherit', 'inherit', 'inherit', 'pipe', 'pipe'], + }); + child.on('exit', (code, signal) => { + if (code !== 0) { + console.error('unexpected exit:', { code, signal }); + } + }); + const w1 = makeWorker(child); + + const text = await io.readFile(transcript, 'utf-8'); + await runTranscript(w1, bundle, text); +} + +if (require.main === module) { + main(process.argv, { + env: process.env, + // eslint-disable-next-line global-require + io: { + // eslint-disable-next-line global-require + readFile: require('fs').promises.readFile, + }, + // eslint-disable-next-line global-require + bundleSource: require('@agoric/bundle-source').default, + // eslint-disable-next-line global-require + spawn: require('child_process').spawn, + }).catch(err => { + console.error(err); + }); +} diff --git a/packages/xs-vat-worker/test/sort_transcript.py b/packages/xs-vat-worker/test/sort_transcript.py new file mode 100644 index 000000000000..c6c478a0c5c8 --- /dev/null +++ b/packages/xs-vat-worker/test/sort_transcript.py @@ -0,0 +1,5 @@ +import sys +lines = sys.stdin.readlines() +lines.sort(key=lambda line: int(line.split(" ", 1)[0].split(".")[2])) +for line in lines: + sys.stdout.write(line) diff --git a/packages/xs-vat-worker/test/test-vat1.js b/packages/xs-vat-worker/test/test-vat1.js new file mode 100644 index 000000000000..6899d27efcc5 --- /dev/null +++ b/packages/xs-vat-worker/test/test-vat1.js @@ -0,0 +1,28 @@ +import { test } from 'tape-promise/tape'; + +import { main } from './kernelSimulator'; + +const resolve = p => new URL(p, import.meta.url).toString().slice('file://'.length); + +test('replay simple transcript with node vatWorker', async t => { + process.env.WORKERBIN = resolve('../bin/node-vat-worker'); + process.env.VAT1 = resolve('./vat-target.js'); + process.env.TRANSCRIPT = resolve('./transcript.txt'); + + process.chdir(resolve('..')); // so node-vat-worker can find stuff in src/ + await main(process.argv, { + env: process.env, + // eslint-disable-next-line global-require + io: { + // eslint-disable-next-line global-require + readFile: require('fs').promises.readFile, + }, + // eslint-disable-next-line global-require + bundleSource: require('@agoric/bundle-source').default, + // eslint-disable-next-line global-require + spawn: require('child_process').spawn, + }); + + t.ok('did not crash'); + t.end(); +}) diff --git a/packages/xs-vat-worker/test/transcript.txt b/packages/xs-vat-worker/test/transcript.txt new file mode 100644 index 000000000000..4f6443500d82 --- /dev/null +++ b/packages/xs-vat-worker/test/transcript.txt @@ -0,0 +1,6 @@ +v1.t.0 :: {"d":["deliver","o+0","zero",{"body":"[{\"@qclass\":\"slot\",\"index\":0},{\"@qclass\":\"slot\",\"index\":1},{\"@qclass\":\"slot\",\"index\":2}]","slots":["o-50","p-60","p-61"]},"p-62"],"syscalls":[{"d":["subscribe","p-60"],"response":null},{"d":["subscribe","p-61"],"response":null},{"d":["send","o-50","callback",{"body":"[11,12]","slots":[]},"p+5"],"response":null},{"d":["subscribe","p+5"],"response":null},{"d":["fulfillToData","p-62",{"body":"[{\"@qclass\":\"slot\",\"index\":0},{\"@qclass\":\"slot\",\"index\":1}]","slots":["p+6","p+7"]}],"response":null}],"crankNumber":2} +v1.t.1 :: {"d":["deliver","o+0","one",{"body":"[]","slots":[]},"p-63"],"syscalls":[{"d":["fulfillToPresence","p+6","o-50"],"response":null},{"d":["reject","p+7",{"body":"{\"@qclass\":\"error\",\"name\":\"Error\",\"message\":\"oops\"}","slots":[]}],"response":null},{"d":["fulfillToData","p-63",{"body":"1","slots":[]}],"response":null}],"crankNumber":3} +v1.t.2 :: {"d":["notifyFulfillToPresence","p-60","o-50"],"syscalls":[],"crankNumber":4} +v1.t.3 :: {"d":["notifyReject","p-61",{"body":"{\"@qclass\":\"error\",\"name\":\"Error\",\"message\":\"four\"}","slots":[]}],"syscalls":[],"crankNumber":5} +v1.t.4 :: {"d":["notifyFulfillToData","p+5",{"body":"[\"data\",{\"@qclass\":\"slot\",\"index\":0}]","slots":["o-50"]}],"syscalls":[],"crankNumber":9} +v1.t.nextID :: 5 diff --git a/packages/xs-vat-worker/test/vat-target.js b/packages/xs-vat-worker/test/vat-target.js new file mode 100644 index 000000000000..a8ceee3b7237 --- /dev/null +++ b/packages/xs-vat-worker/test/vat-target.js @@ -0,0 +1,55 @@ +import { E } from '@agoric/eventual-send'; +import { producePromise } from '@agoric/produce-promise'; + +function ignore(p) { + p.then( + () => 0, + () => 0, + ); +} + +// We arrange for this vat, 'vat-target', to receive a specific set of +// inbound events ('dispatch'), which will provoke a set of outbound events +// ('syscall'), that cover the full range of the dispatch/syscall interface + +export function buildRootObject() { + const precB = producePromise(); + const precC = producePromise(); + let callbackObj; + + // zero: dispatch.deliver(target, method="one", result=pA, args=[callbackObj, pD, pE]) + // syscall.subscribe(pD) + // syscall.subscribe(pE) + // syscall.send(callbackObj, method="callback", result=rp2, args=[11, 12]); + // syscall.subscribe(rp2) + // syscall.fulfillToData(pA, [pB, pC]); + function zero(obj, pD, pE) { + callbackObj = obj; + E(callbackObj).callback(11, 12); // syscall.send + ignore(pD); + ignore(pE); + return [precB.promise, precC.promise]; // syscall.fulfillToData + } + + // one: dispatch.deliver(target, method="two", result=rp3, args=[]) + // syscall.fulfillToPresence(pB, callbackObj) + // syscall.reject(pC, Error('oops')) + // syscall.fulfillToData(rp3, 1) + function one() { + precB.resolve(callbackObj); // syscall.fulfillToPresence + precC.reject(Error('oops')); // syscall.reject + return 1; + } + + // two: dispatch.notifyFulfillToPresence(pD, callbackObj) + // three: dispatch.notifyReject(pE, Error('four')) + + // four: dispatch.notifyFulfillToData(pF, ['data', callbackObj]) + + const target = harden({ + zero, + one, + }); + + return target; +} diff --git a/packages/xs-vat-worker/xs_modules/@agoric/install-ses/install-ses.js b/packages/xs-vat-worker/xs_modules/@agoric/install-ses/install-ses.js new file mode 100644 index 000000000000..a218e5a39ef6 --- /dev/null +++ b/packages/xs-vat-worker/xs_modules/@agoric/install-ses/install-ses.js @@ -0,0 +1,17 @@ +/* global globalThis, Compartment */ + +function harden(x) { + return Object.freeze(x, true); +} + +harden(harden); + +globalThis.harden = harden; + +function tweakCompartmentAPI(C) { + return function Compartment(endowments, cmap, _options) { + return new C({ harden, ...endowments }, { '*': cmap }); + }; +} + +globalThis.Compartment = tweakCompartmentAPI(Compartment);