Skip to content

Commit

Permalink
feat(bundle): backend implementation for upload-contract, register-http
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelfig committed Oct 30, 2019
1 parent e8d2cde commit b10e244
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 167 deletions.
141 changes: 141 additions & 0 deletions lib/ag-solo/bundle.js
Original file line number Diff line number Diff line change
@@ -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;
}
8 changes: 6 additions & 2 deletions lib/ag-solo/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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`);
Expand Down
18 changes: 18 additions & 0 deletions lib/ag-solo/register-http.js
Original file line number Diff line number Diff line change
@@ -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));
}));
}
21 changes: 1 addition & 20 deletions lib/ag-solo/start.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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.
}
162 changes: 33 additions & 129 deletions lib/ag-solo/upload-contract.js
Original file line number Diff line number Diff line change
@@ -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()');
}
Loading

0 comments on commit b10e244

Please sign in to comment.