Skip to content

Commit f6482ac

Browse files
committed
fix: clear up and solve the races around ag-solo initialisation
1 parent 0bf7eab commit f6482ac

File tree

7 files changed

+132
-112
lines changed

7 files changed

+132
-112
lines changed

packages/agoric-cli/lib/deploy.js

+37-5
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,18 @@ export default async function deployMain(progname, rawArgs, powers, opts) {
2323
const console = anylogger('agoric:deploy');
2424

2525
const args = rawArgs.slice(1);
26+
const deps = opts.need
27+
.split(',')
28+
.map(dep => dep.trim())
29+
.filter(dep => dep);
2630

27-
if (args.length === 0) {
28-
console.error('you must specify at least one deploy.js to run');
31+
const init = opts.provide
32+
.split(',')
33+
.map(dep => dep.trim())
34+
.filter(dep => dep);
35+
36+
if (args.length === 0 && !init.length) {
37+
console.error('you must specify at least one deploy.js (or --init=XXX)');
2938
return 1;
3039
}
3140

@@ -69,16 +78,34 @@ export default async function deployMain(progname, rawArgs, powers, opts) {
6978

7079
// Wait for the chain to become ready.
7180
let bootP = getBootstrap();
72-
const loaded = await E.G(bootP).LOADING;
73-
console.debug('Chain loaded:', loaded);
81+
let lastUpdateCount;
82+
let stillLoading = [...deps].sort();
83+
while (stillLoading.length) {
84+
// Wait for the notifier to report a new state.
85+
console.warn('need:', stillLoading.join(', '));
86+
const update = await E(E.G(bootP).loadingNotifier).getUpdateSince(
87+
lastUpdateCount,
88+
);
89+
lastUpdateCount = update.updateCount;
90+
const nextLoading = [];
91+
for (const dep of stillLoading) {
92+
if (update.value.includes(dep)) {
93+
// A dependency is still loading.
94+
nextLoading.push(dep);
95+
}
96+
}
97+
stillLoading = nextLoading;
98+
}
99+
100+
console.debug(JSON.stringify(deps), 'loaded');
74101
// Take a new copy, since the chain objects have been added to bootstrap.
75102
bootP = getBootstrap();
76103

77104
for (const arg of args) {
78105
const moduleFile = path.resolve(process.cwd(), arg);
79106
const pathResolve = (...resArgs) =>
80107
path.resolve(path.dirname(moduleFile), ...resArgs);
81-
console.log('running', moduleFile);
108+
console.warn('running', moduleFile);
82109

83110
// use a dynamic import to load the deploy script, it is unconfined
84111
// eslint-disable-next-line import/no-dynamic-require,global-require
@@ -96,6 +123,11 @@ export default async function deployMain(progname, rawArgs, powers, opts) {
96123
}
97124
}
98125

126+
if (init.length) {
127+
console.warn('provide:', init.join(', '));
128+
await E(E.G(E.G(bootP).local).http).doneLoading(init);
129+
}
130+
99131
console.debug('Done!');
100132
ws.close();
101133
exit.resolve(0);

packages/agoric-cli/lib/main.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,23 @@ const main = async (progname, rawArgs, powers) => {
107107
});
108108

109109
program
110-
.command('deploy <script...>')
110+
.command('deploy [script...]')
111111
.description('run a deployment script against the local Agoric VM')
112112
.option(
113113
'--hostport <HOST:PORT>',
114114
'host and port to connect to VM',
115115
'127.0.0.1:8000',
116116
)
117+
.option(
118+
'--need <DEPENDENCIES>',
119+
'comma-separated names of subsystems to wait for',
120+
'agoric,wallet',
121+
)
122+
.option(
123+
'--provide <DEPENDENCIES>',
124+
'comma-separated names of subsystems this script initializes',
125+
'',
126+
)
117127
.action(async (scripts, cmd) => {
118128
const opts = { ...program.opts(), ...cmd.opts() };
119129
return subMain(deployMain, ['deploy', ...scripts], opts);

packages/cosmic-swingset/lib/ag-solo/start.js

+27-22
Original file line numberDiff line numberDiff line change
@@ -305,29 +305,34 @@ export default async function start(basedir, argv) {
305305
swingSetRunning = true;
306306
deliverOutbound();
307307

308-
if (hostport && fs.existsSync('./wallet-deploy.js')) {
309-
// Install the wallet.
310-
let agoricCli;
311-
try {
312-
agoricCli = require.resolve('.bin/agoric');
313-
} catch (e) {
314-
// do nothing
315-
console.log(`Cannot find agoric CLI:`, e);
316-
}
317-
// Launch the agoric deploy, letting it synchronize with the chain.
318-
if (agoricCli) {
308+
if (hostport) {
309+
const agoricCli = require.resolve('.bin/agoric');
310+
311+
const makeHandler = (onSuccess = undefined) => (err, _stdout, stderr) => {
312+
if (err) {
313+
console.error(err);
314+
return;
315+
}
316+
if (stderr) {
317+
// Report the error.
318+
process.stderr.write(stderr);
319+
}
320+
onSuccess && onSuccess();
321+
};
322+
323+
if (fs.existsSync('./wallet-deploy.js')) {
324+
// Install the wallet.
325+
// Launch the agoric deploy, letting it synchronize with the chain but not wait
326+
// until open for business.
319327
exec(
320-
`${agoricCli} deploy --hostport=${hostport} ./wallet-deploy.js`,
321-
(err, stdout, stderr) => {
322-
if (err) {
323-
console.warn(err);
324-
return;
325-
}
326-
if (stderr) {
327-
// Report the error.
328-
console.error(stderr);
329-
}
330-
},
328+
`${agoricCli} deploy --need=agoric --provide=wallet --hostport=${hostport} ./wallet-deploy.js`,
329+
makeHandler(),
330+
);
331+
} else {
332+
// No need to wait for the wallet, just open for business.
333+
exec(
334+
`${agoricCli} deploy --need= --provide=wallet --hostport=${hostport}`,
335+
makeHandler(),
331336
);
332337
}
333338
}

packages/cosmic-swingset/lib/ag-solo/vats/bootstrap.js

+3
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,9 @@ export function buildRootObject(vatPowers) {
245245
// This will allow dApp developers to register in their api/deploy.js
246246
let walletRegistered = false;
247247
const httpRegCallback = {
248+
doneLoading(subsystems) {
249+
return E(vats.http).doneLoading(subsystems);
250+
},
248251
send(obj, connectionHandles) {
249252
return E(vats.http).send(obj, connectionHandles);
250253
},

packages/cosmic-swingset/lib/ag-solo/vats/vat-http.js

+44-83
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
/* global harden */
2+
import { makeNotifierKit } from '@agoric/notifier';
23
import { E } from '@agoric/eventual-send';
34
import { getReplHandler } from './repl';
45
import { getCapTPHandler } from './captp';
@@ -7,34 +8,32 @@ import { getCapTPHandler } from './captp';
78
export function buildRootObject(vatPowers) {
89
const { D } = vatPowers;
910
let commandDevice;
10-
let provisioner;
1111
const channelIdToHandle = new Map();
1212
const channelHandleToId = new WeakMap();
13-
const loaded = {};
14-
loaded.p = new Promise((resolve, reject) => {
15-
loaded.res = resolve;
16-
loaded.rej = reject;
17-
});
18-
harden(loaded);
13+
let LOADING = harden(['agoric', 'wallet', 'local']);
14+
const {
15+
notifier: loadingNotifier,
16+
updater: loadingUpdater,
17+
} = makeNotifierKit(LOADING);
18+
1919
const replObjects = {
20-
home: { LOADING: loaded.p }, // TODO: Remove
21-
agoric: { LOADING: loaded.p },
20+
home: { LOADING },
21+
agoric: {},
2222
local: {},
2323
};
24-
let isReady = false;
25-
const readyForClient = {};
24+
2625
let exportedToCapTP = {
27-
LOADING: loaded.p,
28-
READY: {
29-
resolve(value) {
30-
isReady = true;
31-
readyForClient.res(value);
32-
},
33-
isReady() {
34-
return isReady;
35-
},
36-
},
26+
loadingNotifier,
3727
};
28+
function doneLoading(subsystems) {
29+
LOADING = LOADING.filter(subsys => !subsystems.includes(subsys));
30+
loadingUpdater.updateState(LOADING);
31+
if (LOADING.length) {
32+
replObjects.home.LOADING = LOADING;
33+
} else {
34+
delete replObjects.home.LOADING;
35+
}
36+
}
3837

3938
const send = (obj, channelHandles) => {
4039
// TODO: Make this sane by adding support for multicast to the commandDevice.
@@ -47,12 +46,6 @@ export function buildRootObject(vatPowers) {
4746
}
4847
};
4948

50-
readyForClient.p = new Promise((resolve, reject) => {
51-
readyForClient.res = resolve;
52-
readyForClient.rej = reject;
53-
});
54-
harden(readyForClient);
55-
5649
const handler = {};
5750
const registeredURLHandlers = new Map();
5851

@@ -68,65 +61,29 @@ export function buildRootObject(vatPowers) {
6861
}
6962

7063
return harden({
71-
setCommandDevice(d, ROLES) {
64+
setCommandDevice(d) {
7265
commandDevice = d;
73-
if (ROLES.client) {
74-
handler.readyForClient = () => readyForClient.p;
75-
76-
const replHandler = getReplHandler(replObjects, send, vatPowers);
77-
registerURLHandler(replHandler, '/private/repl');
78-
79-
// Assign the captp handler.
80-
// TODO: Break this out into a separate vat.
81-
const captpHandler = getCapTPHandler(
82-
send,
83-
// Harden only our exported objects.
84-
() => harden(exportedToCapTP),
85-
{ E, harden, ...vatPowers },
86-
);
87-
registerURLHandler(captpHandler, '/private/captp');
88-
}
8966

90-
if (ROLES.controller) {
91-
handler.pleaseProvision = obj => {
92-
const { nickname, pubkey } = obj;
93-
// FIXME: There's a race here. We return from the call
94-
// before the outbound messages have been committed to
95-
// a block. This means the provisioning-server must
96-
// retry transactions as they might have the wrong sequence
97-
// number.
98-
return E(provisioner).pleaseProvision(nickname, pubkey);
99-
};
100-
handler.pleaseProvisionMany = obj => {
101-
const { applies } = obj;
102-
return Promise.all(
103-
applies.map(args =>
104-
// Emulate allSettled.
105-
E(provisioner)
106-
.pleaseProvision(...args)
107-
.then(
108-
value => ({ status: 'fulfilled', value }),
109-
reason => ({ status: 'rejected', reason }),
110-
),
111-
),
112-
);
113-
};
114-
}
67+
const replHandler = getReplHandler(replObjects, send, vatPowers);
68+
registerURLHandler(replHandler, '/private/repl');
69+
70+
// Assign the captp handler.
71+
// TODO: Break this out into a separate vat.
72+
const captpHandler = getCapTPHandler(
73+
send,
74+
// Harden only our exported objects.
75+
() => harden(exportedToCapTP),
76+
{ E, harden, ...vatPowers },
77+
);
78+
registerURLHandler(captpHandler, '/private/captp');
11579
},
11680

11781
registerURLHandler,
11882
registerAPIHandler: h => registerURLHandler(h, '/api'),
11983
send,
120-
121-
setProvisioner(p) {
122-
provisioner = p;
123-
},
84+
doneLoading,
12485

12586
setWallet(wallet) {
126-
// This must happen only after the local and agoric objects have been
127-
// installed in setPresences.
128-
// We're guaranteed that because the deployment script that installs
129-
// the wallet only runs after the chain has provisioned us.
13087
exportedToCapTP = {
13188
...exportedToCapTP,
13289
local: { ...exportedToCapTP.local, wallet },
@@ -142,23 +99,23 @@ export function buildRootObject(vatPowers) {
14299
handyObjects = undefined,
143100
) {
144101
exportedToCapTP = {
102+
...exportedToCapTP,
145103
...decentralObjects, // TODO: Remove; replaced by .agoric
146104
...privateObjects, // TODO: Remove; replaced by .local
147105
...handyObjects,
148-
LOADING: loaded.p, // TODO: Remove; replaced by .agoric.LOADING
149-
agoric: { ...decentralObjects, LOADING: loaded.p },
150-
local: privateObjects,
106+
agoric: { ...decentralObjects },
107+
local: { ...privateObjects },
151108
};
152109

153110
// We need to mutate the repl subobjects instead of replacing them.
154111
if (privateObjects) {
155112
Object.assign(replObjects.local, privateObjects);
113+
doneLoading(['local']);
156114
}
157115

158116
if (decentralObjects) {
159-
loaded.res('chain bundle loaded');
160117
Object.assign(replObjects.agoric, decentralObjects);
161-
delete replObjects.agoric.LOADING;
118+
doneLoading(['agoric']);
162119
}
163120

164121
// TODO: Remove; home object is deprecated.
@@ -169,7 +126,6 @@ export function buildRootObject(vatPowers) {
169126
privateObjects,
170127
handyObjects,
171128
);
172-
delete replObjects.home.LOADING;
173129
}
174130
},
175131

@@ -240,6 +196,11 @@ export function buildRootObject(vatPowers) {
240196
}
241197

242198
if (dispatcher === 'onMessage') {
199+
D(commandDevice).sendResponse(
200+
count,
201+
false,
202+
harden({ type: 'doesNotUnderstand', obj }),
203+
);
243204
throw Error(`No handler for ${url} ${type}`);
244205
}
245206
D(commandDevice).sendResponse(count, false, harden(true));

packages/cosmic-swingset/lib/ag-solo/web.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,15 @@ export async function makeHTTPListener(basedir, port, host, rawInboundCommand) {
192192
).finally(() => channels.delete(channelID));
193193
});
194194

195-
inboundCommand({ type: 'ws/meta' }, { ...meta, dispatcher: 'onOpen' }, id);
195+
// Close and throw if the open handler gives an error.
196+
inboundCommand(
197+
{ type: 'ws/meta' },
198+
{ ...meta, dispatcher: 'onOpen' },
199+
id,
200+
).catch(e => {
201+
log(id, 'error opening connection:', e);
202+
ws.close();
203+
});
196204

197205
ws.on('message', async message => {
198206
let obj = {};

packages/cosmic-swingset/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@agoric/install-ses": "^0.2.0",
3131
"@agoric/marshal": "^0.2.3",
3232
"@agoric/nat": "2.0.1",
33+
"@agoric/notifier": "^0.1.3",
3334
"@agoric/promise-kit": "^0.1.3",
3435
"@agoric/registrar": "^0.1.3",
3536
"@agoric/same-structure": "^0.0.8",

0 commit comments

Comments
 (0)