agoric 0.18.4 → 0.18.5-dev-800c2bb.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/deploy.js CHANGED
@@ -1,19 +1,14 @@
1
+ // @ts-check
1
2
  /* global process setTimeout setInterval clearInterval */
2
- /* eslint-disable no-await-in-loop */
3
3
 
4
4
  import { E, makeCapTP } from '@endo/captp';
5
5
  import { makePromiseKit } from '@endo/promise-kit';
6
- import bundleSource from '@endo/bundle-source';
7
- import { makeCache } from '@agoric/cache';
8
6
  import { makeLeaderFromRpcAddresses } from '@agoric/casting';
9
- import { search as readContainingPackageDescriptor } from '@endo/compartment-mapper';
10
- import url from 'url';
11
7
  import path from 'path';
12
8
  import http from 'http';
13
9
  import inquirer from 'inquirer';
14
- import createEsmRequire from 'esm';
15
- import { createRequire } from 'module';
16
10
  import { SigningStargateClient } from '@cosmjs/stargate';
11
+ import { whileTrue } from '@agoric/internal';
17
12
 
18
13
  import { getAccessToken } from '@agoric/access-token';
19
14
 
@@ -23,12 +18,10 @@ import {
23
18
  makeHttpBundlePublisher,
24
19
  } from './publish.js';
25
20
  import { makeJsonHttpClient } from './json-http-client-node.js';
21
+ import { makeScriptLoader } from './scripts.js';
26
22
 
27
23
  const { details: X } = assert;
28
24
 
29
- const require = createRequire(import.meta.url);
30
- const esmRequire = createEsmRequire({});
31
-
32
25
  // note: CapTP has its own HandledPromise instantiation, and the contract
33
26
  // must use the same one that CapTP uses. We achieve this by not bundling
34
27
  // captp, and doing a (non-isolated) dynamic import of the deploy script
@@ -43,14 +36,170 @@ const esmRequire = createEsmRequire({});
43
36
 
44
37
  const RETRY_DELAY_MS = 1000;
45
38
 
46
- const PATH_SEP_RE = new RegExp(`${path.sep.replace(/\\/g, '\\\\')}`, 'g');
39
+ const getPort = u =>
40
+ u.port || (['https:', 'wss:'].includes(u.protocol) ? '443' : '80');
41
+
42
+ const connectAndRun = async (
43
+ wsurl,
44
+ main,
45
+ { need, provide },
46
+ { makeWebSocket, now, console },
47
+ ) => {
48
+ let connected = false;
49
+ let progressDot = '.';
50
+ const exit = makePromiseKit();
51
+ let progressTimer = null;
52
+
53
+ const sendJSON = (ws, obj) => {
54
+ if (ws.readyState !== ws.OPEN) {
55
+ return;
56
+ }
57
+ const body = JSON.stringify(obj);
58
+ console.debug('sendJSON', body.slice(0, 200));
59
+ ws.send(body);
60
+ };
61
+
62
+ const retryWebsocket = async () => {
63
+ if (!progressTimer) {
64
+ process.stdout.write(`Open CapTP connection to ${wsurl}...`);
65
+ progressDot = '.';
66
+ progressTimer = setInterval(
67
+ () => process.stdout.write(progressDot),
68
+ 1000,
69
+ );
70
+ }
71
+
72
+ const accessToken = await getAccessToken(
73
+ `${wsurl.hostname}:${getPort(wsurl)}`,
74
+ );
75
+
76
+ // For a WebSocket we need to put the token in the query string.
77
+ assert.typeof(accessToken, 'string');
78
+ const wsWebkey = `${wsurl}?accessToken=${encodeURIComponent(accessToken)}`;
79
+
80
+ const ws = makeWebSocket(wsWebkey, { origin: wsurl.origin });
81
+ ws.on('open', () => {
82
+ const tryOnOpen = async () => {
83
+ connected = true;
84
+ console.debug('Connected to CapTP!');
85
+ // Help disambiguate connections.
86
+ const epoch = now();
87
+ const { dispatch, getBootstrap } = makeCapTP(
88
+ 'bundle',
89
+ obj => sendJSON(ws, obj),
90
+ undefined,
91
+ {
92
+ epoch,
93
+ },
94
+ );
95
+ ws.on('message', data => {
96
+ try {
97
+ const obj = JSON.parse(data);
98
+ console.debug('receiving', data.slice(0, 200));
99
+ if (obj.type === 'CTP_ERROR') {
100
+ throw obj.error;
101
+ }
102
+ dispatch(obj);
103
+ } catch (e) {
104
+ console.error('server error processing message', data, e);
105
+ exit.reject(e);
106
+ }
107
+ });
108
+
109
+ // Wait for the chain to become ready.
110
+ let bootP = getBootstrap();
111
+ let lastUpdateCount;
112
+ let stillLoading = [...need].sort();
113
+ progressDot = 'o';
114
+ for await (const _ of whileTrue(() => stillLoading.length)) {
115
+ // Wait for the notifier to report a new state.
116
+ process.stdout.write(progressDot);
117
+ console.debug('need:', stillLoading.join(', '));
118
+ const update = await E(E.get(bootP).loadingNotifier).getUpdateSince(
119
+ lastUpdateCount,
120
+ );
121
+ lastUpdateCount = update.updateCount;
122
+
123
+ // Skip the deploy if our provides are not needed.
124
+ let needsProvide = !provide.length;
125
+ const notNeeded = [];
126
+ for (const dep of provide) {
127
+ if (update.value.includes(dep)) {
128
+ needsProvide = true;
129
+ } else {
130
+ notNeeded.push(dep);
131
+ }
132
+ }
133
+ if (!needsProvide) {
134
+ console.info(`Don't need our provides: ${notNeeded.join(', ')}`);
135
+ exit.resolve(0);
136
+ return;
137
+ }
138
+
139
+ const nextLoading = [];
140
+ for (const dep of stillLoading) {
141
+ if (update.value.includes(dep)) {
142
+ // A dependency is still loading.
143
+ nextLoading.push(dep);
144
+ }
145
+ }
146
+ stillLoading = nextLoading;
147
+ }
148
+
149
+ clearInterval(progressTimer);
150
+ process.stdout.write('\n');
151
+ console.debug(JSON.stringify(need), 'loaded');
152
+ // Take a new copy, since the chain objects have been added to bootstrap.
153
+ bootP = getBootstrap();
154
+
155
+ await main({ home: bootP });
156
+
157
+ const doneLoading = async () => {
158
+ if (!provide.length) {
159
+ return;
160
+ }
161
+ console.debug('provide:', provide.join(', '));
162
+ await E(E.get(E.get(bootP).local).http).doneLoading(provide);
163
+ };
164
+ await doneLoading();
165
+
166
+ console.debug('Done!');
167
+ ws.close();
168
+ exit.resolve(0);
169
+ };
170
+ tryOnOpen().catch(e => {
171
+ exit.reject(e);
172
+ ws.close();
173
+ });
174
+ });
175
+ ws.on('close', (_code, _reason) => {
176
+ console.debug('connection closed');
177
+ if (connected) {
178
+ exit.resolve(1);
179
+ }
180
+ });
181
+ ws.on('error', e => {
182
+ if (e.code === 'ECONNREFUSED' && !connected) {
183
+ // Retry in a little bit.
184
+ setTimeout(() => retryWebsocket().catch(exit.reject), RETRY_DELAY_MS);
185
+ return;
186
+ }
187
+ exit.reject(e);
188
+ });
189
+ };
190
+ // Start the retry process.
191
+ return retryWebsocket().then(() => exit.promise);
192
+ };
47
193
 
48
194
  export default async function deployMain(progname, rawArgs, powers, opts) {
49
- const { anylogger, fs, makeWebSocket, now } = powers;
195
+ const { anylogger, now, fs } = powers;
50
196
  const console = anylogger('agoric:deploy');
51
197
 
52
198
  const allowUnsafePlugins = opts.allowUnsafePlugins;
53
- if (allowUnsafePlugins) {
199
+ const promptForUnsafePlugins = async () => {
200
+ if (!allowUnsafePlugins) {
201
+ return;
202
+ }
54
203
  const { yesReally } = await inquirer.prompt([
55
204
  {
56
205
  name: 'yesReally',
@@ -64,9 +213,10 @@ export default async function deployMain(progname, rawArgs, powers, opts) {
64
213
  );
65
214
  process.exit(1);
66
215
  }
67
- }
216
+ };
217
+ await promptForUnsafePlugins();
68
218
 
69
- const args = rawArgs.slice(1);
219
+ const scripts = rawArgs.slice(1);
70
220
  const provide = opts.provide
71
221
  .split(',')
72
222
  .map(dep => dep.trim())
@@ -87,15 +237,6 @@ export default async function deployMain(progname, rawArgs, powers, opts) {
87
237
  .filter(dep => dep && !provide.includes(dep))
88
238
  .sort();
89
239
 
90
- const sendJSON = (ws, obj) => {
91
- if (ws.readyState !== ws.OPEN) {
92
- return;
93
- }
94
- const body = JSON.stringify(obj);
95
- console.debug('sendJSON', body.slice(0, 200));
96
- ws.send(body);
97
- };
98
-
99
240
  const match = opts.hostport.match(/^(.*):(\d+)$/);
100
241
  const host = match ? match[1] : 'localhost';
101
242
  const port = match ? match[2] : '8000';
@@ -104,25 +245,15 @@ export default async function deployMain(progname, rawArgs, powers, opts) {
104
245
  ? new URL(opts.hostport, 'ws://localhost:8000')
105
246
  : new URL(`ws://${host}:${port}`);
106
247
 
107
- const myPort =
108
- wsurl.port || (['https:', 'wss:'].includes(wsurl.protocol) ? '443' : '80');
109
-
110
248
  assert.equal(wsurl.pathname, '/', X`${opts.hostport} cannot contain a path`);
111
249
  wsurl.pathname = '/private/captp';
112
250
 
113
- const exit = makePromiseKit();
114
- let connected = false;
115
- process.stdout.write(`Open CapTP connection to ${wsurl}...`);
116
- let progressDot = '.';
117
- const progressTimer = setInterval(
118
- () => process.stdout.write(progressDot),
119
- 1000,
120
- );
121
-
122
251
  const jsonHttpCall = makeJsonHttpClient({ http });
123
252
 
124
253
  const listConnections = async () => {
125
- const accessToken = await getAccessToken(`${wsurl.hostname}:${myPort}`);
254
+ const accessToken = await getAccessToken(
255
+ `${wsurl.hostname}:${getPort(wsurl)}`,
256
+ );
126
257
 
127
258
  const { ok, connections } = await jsonHttpCall({
128
259
  hostname: host,
@@ -181,7 +312,7 @@ export default async function deployMain(progname, rawArgs, powers, opts) {
181
312
  const { connectionNumber } = await inquirer.prompt({
182
313
  name: 'connectionNumber',
183
314
  message: 'Please choose a connection to publish to',
184
- choices: lookup.keys(),
315
+ choices: [...lookup.keys()],
185
316
  });
186
317
  const connection = lookup.get(connectionNumber);
187
318
  console.log({ connection });
@@ -191,7 +322,6 @@ export default async function deployMain(progname, rawArgs, powers, opts) {
191
322
  const publishBundleCosmos = makeCosmosBundlePublisher({
192
323
  connectWithSigner: SigningStargateClient.connectWithSigner,
193
324
  pathResolve: path.resolve,
194
- writeFile: fs.writeFile,
195
325
  readFile: fs.readFile,
196
326
  random: Math.random,
197
327
  });
@@ -205,301 +335,45 @@ export default async function deployMain(progname, rawArgs, powers, opts) {
205
335
  publishBundleHttp,
206
336
  });
207
337
 
208
- const retryWebsocket = async () => {
209
- const accessToken = await getAccessToken(`${wsurl.hostname}:${myPort}`);
210
-
211
- // For a WebSocket we need to put the token in the query string.
212
- const wsWebkey = `${wsurl}?accessToken=${encodeURIComponent(accessToken)}`;
213
-
214
- const ws = makeWebSocket(wsWebkey, { origin: wsurl.origin });
215
- ws.on('open', async () => {
216
- connected = true;
217
- try {
218
- console.debug('Connected to CapTP!');
219
- // Help disambiguate connections.
220
- const epoch = now();
221
- const { dispatch, getBootstrap } = makeCapTP(
222
- 'bundle',
223
- obj => sendJSON(ws, obj),
224
- undefined,
225
- {
226
- epoch,
227
- },
228
- );
229
- ws.on('message', data => {
230
- try {
231
- const obj = JSON.parse(data);
232
- console.debug('receiving', data.slice(0, 200));
233
- if (obj.type === 'CTP_ERROR') {
234
- throw obj.error;
235
- }
236
- dispatch(obj);
237
- } catch (e) {
238
- console.error('server error processing message', data, e);
239
- exit.reject(e);
240
- }
241
- });
242
-
243
- // Wait for the chain to become ready.
244
- let bootP = getBootstrap();
245
- let lastUpdateCount;
246
- let stillLoading = [...need].sort();
247
- progressDot = 'o';
248
- while (stillLoading.length) {
249
- // Wait for the notifier to report a new state.
250
- process.stdout.write(progressDot);
251
- console.debug('need:', stillLoading.join(', '));
252
- const update = await E(E.get(bootP).loadingNotifier).getUpdateSince(
253
- lastUpdateCount,
254
- );
255
- lastUpdateCount = update.updateCount;
256
-
257
- // Skip the deploy if our provides are not needed.
258
- let needsProvide = !provide.length;
259
- const notNeeded = [];
260
- for (const dep of provide) {
261
- if (update.value.includes(dep)) {
262
- needsProvide = true;
263
- } else {
264
- notNeeded.push(dep);
265
- }
266
- }
267
- if (!needsProvide) {
268
- console.info(`Don't need our provides: ${notNeeded.join(', ')}`);
269
- process.exit(0);
270
- }
271
-
272
- const nextLoading = [];
273
- for (const dep of stillLoading) {
274
- if (update.value.includes(dep)) {
275
- // A dependency is still loading.
276
- nextLoading.push(dep);
277
- }
278
- }
279
- stillLoading = nextLoading;
280
- }
281
-
282
- clearInterval(progressTimer);
283
- process.stdout.write('\n');
284
- console.debug(JSON.stringify(need), 'loaded');
285
- // Take a new copy, since the chain objects have been added to bootstrap.
286
- bootP = getBootstrap();
287
-
288
- const pluginManager = await E.get(E.get(bootP).local).plugin.catch(
289
- _ => {},
290
- );
291
- const pluginDir = await E(pluginManager)
292
- .getPluginDir()
293
- .catch(_ => {});
294
-
295
- if (allowUnsafePlugins && !pluginDir) {
296
- throw Error(
297
- `Installing unsafe plugins disabled; no pluginDir detected`,
298
- );
299
- }
300
-
301
- const cache = makeCache(
302
- E(E(E.get(bootP).wallet).getBridge()).getCacheCoordinator(),
303
- );
304
-
305
- let cachedLeader;
306
- const makeDefaultLeader = async leaderOptions => {
307
- if (cachedLeader === undefined) {
308
- const conn = await getDefaultConnection();
309
- const { type, rpcAddresses } = conn;
310
- assert.equal(
311
- type,
312
- 'chain-cosmos-sdk',
313
- X`${type} doesn't support casting followers`,
314
- );
315
- cachedLeader = makeLeaderFromRpcAddresses(
316
- rpcAddresses,
317
- leaderOptions,
318
- );
319
- }
320
- return cachedLeader;
321
- };
322
-
323
- for (const arg of args) {
324
- const moduleFile = path.resolve(process.cwd(), arg);
325
- const pathResolve = (...paths) => {
326
- const fileName = paths.pop();
327
- try {
328
- return require.resolve(fileName, {
329
- paths: [...paths, path.dirname(moduleFile)],
330
- });
331
- } catch (e) {
332
- return path.resolve(path.dirname(moduleFile), ...paths, fileName);
333
- }
334
- };
335
- console.warn('running', moduleFile);
336
-
337
- let installUnsafePlugin;
338
- if (!allowUnsafePlugins) {
339
- installUnsafePlugin = async (plugin, _opts = undefined) => {
340
- throw Error(
341
- `Installing unsafe plugin ${JSON.stringify(
342
- pathResolve(plugin),
343
- )} disabled; maybe you meant '--allow-unsafe-plugins'?`,
344
- );
345
- };
346
- } else {
347
- installUnsafePlugin = async (plugin, pluginOpts = undefined) => {
348
- try {
349
- const absPath = pathResolve(plugin);
350
- const pluginName = absPath.replace(PATH_SEP_RE, '_');
351
- const pluginFile = path.resolve(pluginDir, pluginName);
352
-
353
- // Just create a little redirector for that path.
354
- console.warn(
355
- `Installing unsafe plugin ${JSON.stringify(absPath)}`,
356
- );
357
- const content = `\
358
- // ${pluginFile}
359
- // AUTOMATICALLY GENERATED BY ${JSON.stringify(
360
- [progname, ...rawArgs].join(' '),
361
- )}
362
- export { bootPlugin } from ${JSON.stringify(absPath)};
363
- `;
364
- await fs.writeFile(pluginFile, content);
365
-
366
- // Return the bootstrap object for this plugin.
367
- console.info(`Loading plugin ${JSON.stringify(pluginFile)}`);
368
- return E.get(E(pluginManager).load(pluginName, pluginOpts))
369
- .pluginRoot;
370
- } catch (e) {
371
- throw Error(
372
- `Cannot install unsafe plugin: ${(e && e.stack) || e}`,
373
- );
374
- }
375
- };
376
- }
377
-
378
- // Use a dynamic import to load the deploy script.
379
- // It is unconfined.
380
-
381
- // Use Node.js ESM support if package.json of template says "type":
382
- // "module".
383
- const read = async location =>
384
- fs.readFile(url.fileURLToPath(location));
385
- const { packageDescriptorText } =
386
- await readContainingPackageDescriptor(
387
- read,
388
- url.pathToFileURL(moduleFile),
389
- ).catch(cause => {
390
- throw new Error(
391
- `Expected a package.json beside deploy script ${moduleFile}, ${cause}`,
392
- { cause },
393
- );
394
- });
395
- const packageDescriptor = JSON.parse(packageDescriptorText);
396
- const nativeEsm = packageDescriptor.type === 'module';
397
- console.log(
398
- `Deploy script will run with ${
399
- nativeEsm ? 'Node.js ESM' : 'standardthings/esm emulation'
400
- }`,
401
- );
402
-
403
- const modulePath = pathResolve(moduleFile);
404
- const mainNS = nativeEsm
405
- ? await import(modulePath)
406
- : esmRequire(modulePath);
407
- const main = mainNS.default;
408
- if (typeof main !== 'function') {
409
- console.error(
410
- `${moduleFile} does not have an export default function main`,
411
- );
412
- } else {
413
- await main(bootP, {
414
- bundleSource: (file, options = undefined) =>
415
- bundleSource(pathResolve(file), options),
416
- cache,
417
- makeDefaultLeader,
418
- publishBundle,
419
- listConnections,
420
- pathResolve,
421
- installUnsafePlugin,
422
- /**
423
- * Recursively look up names in the context of the bootstrap
424
- * promise, such as:
425
- *
426
- * ['agoricNames', 'oracleBrand', 'USD']
427
- * ['namesByAddress']
428
- * ['namesByAddress', 'agoric1...']
429
- * ['namesByAddress', 'agoric1...', 'depositFacet']
430
- * ['wallet', 'issuer', 'IST']
431
- *
432
- * @param {...string[]} namePath
433
- * @returns {Promise<any>}
434
- */
435
- lookup: (...namePath) => {
436
- if (namePath.length === 1 && Array.isArray(namePath[0])) {
437
- // Convert single array argument to a path.
438
- namePath = namePath[0];
439
- }
440
- if (namePath.length === 0) {
441
- return bootP;
442
- }
443
- const [first, ...remaining] = namePath;
444
-
445
- // The first part of the name path is a property on bootP.
446
- let nextValue = E.get(bootP)[first];
447
- if (remaining.length === 0) {
448
- return nextValue;
449
- }
450
-
451
- // Compatibility for agoricdev-8; use `.get` for the next part.
452
- // TODO: remove when agoricdev-9 is released.
453
- if (first === 'scratch') {
454
- const second = remaining.shift();
455
- const secondValue = E(nextValue).get(second);
456
- if (remaining.length === 0) {
457
- return secondValue;
458
- }
459
-
460
- // Fall through to the lookup below.
461
- nextValue = secondValue;
462
- }
463
-
464
- // Any remaining paths go through the lookup method of the found
465
- // object.
466
- return E(nextValue).lookup(...remaining);
467
- },
468
- host,
469
- port,
470
- args: opts.scriptArgs,
471
- });
472
- }
473
- }
474
-
475
- if (provide.length) {
476
- console.debug('provide:', provide.join(', '));
477
- await E(E.get(E.get(bootP).local).http).doneLoading(provide);
478
- }
338
+ let cachedLeader;
339
+ const makeDefaultLeader = async leaderOptions => {
340
+ if (cachedLeader) {
341
+ return cachedLeader;
342
+ }
343
+ const conn = await getDefaultConnection();
344
+ const { type, rpcAddresses } = conn;
345
+ assert.equal(
346
+ type,
347
+ 'chain-cosmos-sdk',
348
+ X`${type} doesn't support casting followers`,
349
+ );
350
+ cachedLeader = makeLeaderFromRpcAddresses(rpcAddresses, leaderOptions);
351
+ return cachedLeader;
352
+ };
479
353
 
480
- console.debug('Done!');
481
- ws.close();
482
- exit.resolve(0);
483
- } catch (e) {
484
- exit.reject(e);
485
- }
486
- });
487
- ws.on('close', (_code, _reason) => {
488
- console.debug('connection closed');
489
- if (connected) {
490
- exit.resolve(1);
491
- }
492
- });
493
- ws.on('error', e => {
494
- if (e.code === 'ECONNREFUSED' && !connected) {
495
- // Retry in a little bit.
496
- setTimeout(retryWebsocket, RETRY_DELAY_MS);
497
- return;
498
- }
499
- exit.reject(e);
500
- });
354
+ const endowments = {
355
+ now,
356
+ makeDefaultLeader,
357
+ publishBundle,
358
+ listConnections,
501
359
  };
502
- // Start the retry process.
503
- retryWebsocket();
504
- return exit.promise;
360
+ if (opts.scriptArgs) {
361
+ endowments.scriptArgs = opts.scriptArgs;
362
+ }
363
+ const runScripts = makeScriptLoader(
364
+ scripts,
365
+ { allowUnsafePlugins, progname, rawArgs, endowments },
366
+ { fs, console },
367
+ );
368
+
369
+ return connectAndRun(
370
+ wsurl,
371
+ runScripts,
372
+ { need, provide },
373
+ {
374
+ makeWebSocket: powers.makeWebSocket,
375
+ console,
376
+ now,
377
+ },
378
+ );
505
379
  }
package/src/entrypoint.js CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ // @jessie-check
2
3
 
3
4
  /* global process */
4
5
 
@@ -37,9 +38,15 @@ main(progname, rawArgs, {
37
38
  process,
38
39
  spawn,
39
40
  }).then(
40
- res => res === undefined || process.exit(res),
41
+ res => {
42
+ if (Number.isSafeInteger(res)) {
43
+ process.exitCode = res;
44
+ }
45
+ },
41
46
  rej => {
42
47
  log.error(rej);
43
- process.exit(2);
48
+ if (!process.exitCode) {
49
+ process.exitCode = 2;
50
+ }
44
51
  },
45
52
  );