diff --git a/lib/ag-solo/bundle.js b/lib/ag-solo/bundle.js new file mode 100644 index 00000000000..0104bfdc449 --- /dev/null +++ b/lib/ag-solo/bundle.js @@ -0,0 +1,141 @@ +/* eslint-disable no-await-in-loop */ +import parseArgs from 'minimist'; +import WebSocket from 'ws'; +import { E } from '@agoric/eventual-send'; +import { evaluateProgram } from '@agoric/evaluate'; +import { makeCapTP } from '@agoric/captp'; +import fs from 'fs'; +import path from 'path'; + +import buildSourceBundle from '@agoric/bundle-source'; + +const makePromise = () => { + const pr = {}; + pr.p = new Promise((resolve, reject) => { + pr.res = resolve; + pr.rej = reject; + }); + return pr; +}; + +const sendJSON = (ws, obj) => { + if (ws.readyState !== ws.OPEN) { + return; + } + // console.log('sending', obj); + ws.send(JSON.stringify(obj)); +}; + +export default async function bundle(insistIsBasedir, args) { + const { _: a, evaluate, once, output, 'ag-solo': agSolo } = parseArgs(args, { + boolean: ['once', 'evaluate'], + alias: {o: 'output', e: 'evaluate'}, + stopEarly: true, + }); + + const [mainModule, ...namePaths] = a; + if (!mainModule) { + console.error('You must specify a main module to bundle'); + return 1; + } + + if (!output && !evaluate) { + console.error(`You must specify at least one of '--output' or '--evaluate'`); + return 1; + } + + const bundled = {}; + + const moduleFile = `${__dirname}/${mainModule}.js`; + await Promise.all([`main=${moduleFile}`, ...namePaths].map(async namePath => { + const match = namePath.match(/^([^=]+)=(.+)$/); + if (!match) { + throw Error(`${namePath} isn't NAME=PATH`); + } + const name = match[1]; + const filepath = match[2]; + bundled[name] = await buildSourceBundle(filepath); + })); + + if (output) { + await fs.promises.writeFile(output, JSON.stringify(bundled)); + } + + if (!evaluate) { + return 0; + } + const actualSources = `(${bundled.main.source}\n)\n${bundled.main.sourceMap}`; + // console.log(actualSources); + const mainNS = evaluateProgram(actualSources, { require })(); + const main = mainNS.default; + if (typeof main !== 'function') { + console.error(`Bundle main does not have an export default function`); + return 1; + } + + let wsurl = agSolo; + if (!agSolo) { + const basedir = insistIsBasedir(); + const cjson = await fs.promises.readFile( + path.join(basedir, 'connections.json'), + ); + for (const conn of JSON.parse(cjson)) { + if (conn.type === 'http') { + wsurl = `ws://${conn.host}:${conn.port}/captp`; + } + } + } + + const ws = new WebSocket(wsurl, { origin: 'http://127.0.0.1' }); + const exit = makePromise(); + ws.on('open', async () => { + try { + const { dispatch, getBootstrap } = makeCapTP('bundle', obj => + sendJSON(ws, obj), + ); + ws.on('message', data => { + // console.log(data); + try { + const obj = JSON.parse(data); + if (obj.type === 'CTP_ERROR') { + throw obj.error; + } + dispatch(obj); + } catch (e) { + console.error('server error processing message', data, e); + exit.rej(e); + } + }); + + // Wait for the chain to become ready. + let bootC = E.C(getBootstrap()); + console.error('Chain loaded:', await bootC.G.LOADING.P); + // Take a new copy, since the chain objects have been added to bootstrap. + bootC = E.C(getBootstrap()); + if (once) { + if (await bootC.G.READY.M.isReady().P) { + console.error('Singleton bundle already installed'); + ws.close(); + exit.res(0); + return; + } + } + + console.error(`Running bundle main entry point...`); + await main({ bundle: bundled, home: bootC.P }); + console.error('Success!'); + if (once) { + await bootC.G.READY.M.resolve('initialized').P; + } + ws.close(); + exit.res(0); + } catch (e) { + exit.rej(e); + } + }); + ws.on('close', (_code, _reason) => { + // console.log('connection closed'); + exit.res(1); + }); + return exit.p; +} diff --git a/lib/ag-solo/main.js b/lib/ag-solo/main.js index d84d38a0668..e147261b3ca 100644 --- a/lib/ag-solo/main.js +++ b/lib/ag-solo/main.js @@ -5,10 +5,10 @@ import process from 'process'; import { insist } from './insist'; // Start a network service +import bundle from './bundle'; import initBasedir from './init-basedir'; import setGCIIngress from './set-gci-ingress'; import start from './start'; -import uploadContract from './upload-contract'; // As we add more egress types, put the default types in a comma-separated // string below. @@ -75,8 +75,12 @@ start const basedir = insistIsBasedir(); const withSES = true; await start(basedir, withSES, argv.slice(1)); + } else if (argv[0] === 'bundle') { + await bundle(insistIsBasedir, argv.slice(1)); } else if (argv[0] === 'upload-contract') { - await uploadContract(insistIsBasedir, argv.slice(1)); + await bundle(insistIsBasedir, [`--evaluate`, ...argv]); + } else if (argv[0] === 'register-http') { + await bundle(insistIsBasedir, [`--evaluate`, ...argv]); } else { console.error(`unrecognized command ${argv[0]}`); console.error(`try one of: init, set-gci-ingress, start`); diff --git a/lib/ag-solo/register-http.js b/lib/ag-solo/register-http.js new file mode 100644 index 00000000000..507b27511bd --- /dev/null +++ b/lib/ag-solo/register-http.js @@ -0,0 +1,18 @@ +export default async function registerHttp({ home, bundle }) { + console.error(`Upgrading Dapp handlers...`); + await register(home, bundle, Object.keys(bundle).filter(k => k !== 'main').sort()); +} + +export async function register(homeP, bundle, keys) { + const targetObj = await homeP~.http; + if (!targetObj) { + throw Error(`HTTP registration object not available`); + } + await Promise.all(keys.map(key => { + const { source, moduleFormat } = bundle[key]; + // console.error(`Uploading ${source}`); + + // Register the HTTP handler. + contractsAP.push(targetObj~.register(key, source, moduleFormat)); + })); +} diff --git a/lib/ag-solo/start.js b/lib/ag-solo/start.js index 6ba7aa5af9b..c17263587fa 100644 --- a/lib/ag-solo/start.js +++ b/lib/ag-solo/start.js @@ -23,7 +23,6 @@ import { import { buildStorageInMemory } from '@agoric/swingset-vat/src/hostStorage'; import buildCommand from '@agoric/swingset-vat/src/devices/command'; -import uploadContract from './upload-contract'; import { deliver, addDeliveryTarget } from './outbound'; import { makeHTTPListener } from './web'; @@ -237,23 +236,5 @@ export default async function start(basedir, withSES, argv) { console.log(`swingset running`); - // Install the contracts, if given a client role. - // FIXME: Don't do this for now. - if (false && argv.find(value => value.match(/^--role=.*client/)) !== undefined) { - const contractsDir = path.join(basedir, 'contracts'); - const pairs = (await fs.promises.readdir(contractsDir)) - .sort() - .reduce((prior, name) => { - const match = name.match(CONTRACT_REGEXP); - if (match) { - prior.push(`${match[1]}=${contractsDir}/${name}`); - } - return prior; - }, []); - - if (pairs.length > 0) { - // eslint-disable-next-line no-await-in-loop - await uploadContract(basedir, ['--once', ...pairs]); - } - } + // FIXME: Install the bundles as specified. } diff --git a/lib/ag-solo/upload-contract.js b/lib/ag-solo/upload-contract.js index 858525f3203..980ff758aad 100644 --- a/lib/ag-solo/upload-contract.js +++ b/lib/ag-solo/upload-contract.js @@ -1,135 +1,39 @@ -/* eslint-disable no-await-in-loop */ -import parseArgs from 'minimist'; -import WebSocket from 'ws'; -import { E } from '@agoric/eventual-send'; -import { makeCapTP } from '@agoric/captp'; -import fs from 'fs'; -import path from 'path'; - -import buildSourceBundle from '@agoric/bundle-source'; - -const makePromise = () => { - const pr = {}; - pr.p = new Promise((resolve, reject) => { - pr.res = resolve; - pr.rej = reject; - }); - return pr; -}; - -const sendJSON = (ws, obj) => { - if (ws.readyState !== ws.OPEN) { - return; - } - // console.log('sending', obj); - ws.send(JSON.stringify(obj)); -}; - -export default async function upload(insistIsBasedir, args) { - const { _: namePaths, once, 'ag-solo': agSolo } = parseArgs(args, { - boolean: ['once'], - stopEarly: true, - }); - if (namePaths.length === 0) { - console.error('You must specify TARGET-NAME=PATH arguments to upload'); - return 1; - } +export default async function installContracts({ home, bundle }) { + console.error(`Installing targeted contracts...`); + await install(home, bundle, Object.keys(bundle).filter(k => k !== 'main').sort()); +} - let wsurl = agSolo; - if (!agSolo) { - const basedir = insistIsBasedir(); - const cjson = await fs.promises.readFile( - path.join(basedir, 'connections.json'), - ); - for (const conn of JSON.parse(cjson)) { - if (conn.type === 'http') { - wsurl = `ws://${conn.host}:${conn.port}/captp`; - } +export async function install(homeP, bundle, keys) { + const names = []; + const contractsAP = []; + for (const key of keys) { + const match = key.match(/^(([^:]+):[^=]+)$/); + if (!match) { + throw Error(`${key} isn't TARGET:NAME`); } - } - - const ws = new WebSocket(wsurl, { origin: 'http://127.0.0.1' }); - const exit = makePromise(); - ws.on('open', async () => { - try { - const { dispatch, getBootstrap } = makeCapTP('upload', obj => - sendJSON(ws, obj), + const name = match[1]; + const target = match[2]; + const { source, moduleFormat } = bundle[key]; + // console.error(`Uploading ${source}`); + + const targetObj = await homeP~.[target]; + if (!targetObj) { + console.error( + `Contract installation target object ${target} is not available for ${name}; skipping...`, ); - ws.on('message', data => { - // console.log(data); - try { - const obj = JSON.parse(data); - if (obj.type === 'CTP_ERROR') { - throw obj.error; - } - dispatch(obj); - } catch (e) { - console.error('server error processing message', data, e); - exit.rej(e); - } - }); - - // Wait for the chain to become ready. - let bootC = E.C(getBootstrap()); - console.error('Chain loaded:', await bootC.G.LOADING.P); - // Take a new copy, since the contract targets should exist. - bootC = E.C(getBootstrap()); - if (once) { - if (await bootC.G.READY.M.isReady().P) { - console.error('Contracts already uploaded'); - ws.close(); - exit.res(0); - return; - } - } - const uploadsC = bootC.G.uploads; - - console.error(`Uploading contracts...`); - - const names = []; - const contractsAP = []; - for (const namePath of namePaths) { - const match = namePath.match(/^(([^\W-]+)-[^=]+)=(.+)$/); - if (!match) { - throw Error(`${namePath} isn't TARGET-NAME=PATH`); - } - const name = match[1]; - const target = match[2]; - const filepath = match[3]; - const { source, moduleFormat } = await buildSourceBundle(filepath); - // console.error(`Uploading ${source}`); - - const targetObj = await bootC.G[target].P; - if (!targetObj) { - console.error( - `Contract installation target object ${target} is not available for ${name}; skipping...`, - ); - } else { - // Install the contract, then save it in home.uploads. - console.log(name) - contractsAP.push(E(targetObj).install(source, moduleFormat)); - names.push(name); - } - } + } else { + // Install the contract, then save it in home.uploads. + console.log(name) + contractsAP.push(targetObj~.install(source, moduleFormat)); + names.push(name); + } + } - const contracts = await Promise.all(contractsAP); - for (let i = 0; i < contracts.length; i ++) { - await uploadsC.M.set(names[i], contracts[i]).P; - } + const uploadsP = homeP~.uploads; + const contracts = await Promise.all(contractsAP); + for (let i = 0; i < contracts.length; i ++) { + await uploadsP~.set(names[i], contracts[i]); + } - console.error('Success! See home.uploads~.list()'); - if (once) { - await bootC.G.READY.M.resolve('contracts uploaded').P; - } - ws.close(); - exit.res(0); - } catch (e) { - exit.rej(e); - } - }); - ws.on('close', (_code, _reason) => { - // console.log('connection closed'); - exit.res(1); - }); - return exit.p; + console.error('See home.uploads~.list()'); } diff --git a/package-lock.json b/package-lock.json index 0e5257660b3..20493991f34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "@agoric/acorn-eventual-send": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@agoric/acorn-eventual-send/-/acorn-eventual-send-1.0.1.tgz", - "integrity": "sha512-V0d3n2A4HqaVWFxw5fLkb837iaEbtVn83e8PLlyGNUDFjudPtxqnG0fGFFHbY/sWPSqsLhJ3zunRirmTGI8HKQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@agoric/acorn-eventual-send/-/acorn-eventual-send-1.0.2.tgz", + "integrity": "sha512-zLEM1EDtjGuCjzWgZH/MNUqlP8bQ/0R6cxiV40WH3onAlE122p/u8eRsTQL8JMoX5q8dv+JqjCHb46gsm7vfyA==", "requires": { "acorn": "^6.2.0" }, diff --git a/package.json b/package.json index 666766113b5..6b928452dc7 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "author": "Agoric", "license": "Apache-2.0", "dependencies": { - "@agoric/acorn-eventual-send": "^1.0.1", + "@agoric/acorn-eventual-send": "^1.0.2", "@agoric/bundle-source": "^0.1.0", "@agoric/captp": "^1.0.0", "@agoric/ertp": "^0.1.8", diff --git a/setup/main.js b/setup/main.js index 58cca808b46..572a0e50b0a 100644 --- a/setup/main.js +++ b/setup/main.js @@ -188,15 +188,17 @@ show-config display the client connection parameters break; } case 'bootstrap': { - const { _: subArgs, ...subOpts } = parseArgs(args.slice(1), { + const { + _: subArgs, + 'boot-tokens': bootTokens, + ...subOpts + } = parseArgs(args.slice(1), { + default: { + 'boot-tokens': DEFAULT_BOOT_TOKENS, + }, stopEarly: true, }); - let [bootTokens] = subArgs; - if (!bootTokens) { - bootTokens = DEFAULT_BOOT_TOKENS; - } - const dir = SETUP_HOME; if (await exists(`${dir}/network.txt`)) { // Change to directory. @@ -212,8 +214,6 @@ show-config display the client connection parameters ]); } - guardFile('boot-tokens.txt', makeFile => makeFile(bootTokens)); - await guardFile(`${PROVISION_DIR}/hosts`, async makeFile => { await needReMain(['provision', '-auto-approve']); const hosts = await needBacktick(`${shellEscape(progname)} show-hosts`); @@ -236,11 +236,26 @@ show-config display the client connection parameters await guardFile(`${PROVISION_DIR}/prepare.stamp`, () => needReMain(['play', 'prepare-machine']), ); - const bootOpts = []; - if (subOpts.bump) { - bootOpts.push(`--bump=${subOpts.bump}`); + + switch (subArgs[0]) { + case 'dapp': { + await needReMain(['bootstrap-cosmos-dapp', ...subArgs.slice(1)]); + break; + } + + case undefined: { + guardFile('boot-tokens.txt', makeFile => makeFile(bootTokens)); + const bootOpts = []; + if (subOpts.bump) { + bootOpts.push(`--bump=${subOpts.bump}`); + } + await needReMain(['bootstrap-cosmos', ...bootOpts]); + break; + } + default: { + throw Error(`Unrecognized bootstrap argument ${subArgs[0]}`); + } } - await needReMain(['bootstrap-cosmos', ...bootOpts]); break; }