@agoric/cosmic-swingset 0.41.4-dev-3825031.0 → 0.41.4-dev-d1ef359.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/package.json +16 -12
- package/src/chain-main.js +10 -3
- package/src/helpers/bufferedStorage.js +24 -13
- package/src/launch-chain.js +24 -3
- package/tools/inquisitor.mjs +825 -0
- package/tools/test-kit.js +25 -3
- package/tsconfig.json +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agoric/cosmic-swingset",
|
|
3
|
-
"version": "0.41.4-dev-
|
|
3
|
+
"version": "0.41.4-dev-d1ef359.0+d1ef359",
|
|
4
4
|
"description": "Agoric's Cosmos blockchain integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,15 +22,15 @@
|
|
|
22
22
|
"author": "Agoric",
|
|
23
23
|
"license": "Apache-2.0",
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@agoric/builders": "0.1.1-dev-
|
|
26
|
-
"@agoric/cosmos": "0.34.2-dev-
|
|
27
|
-
"@agoric/deploy-script-support": "0.10.4-dev-
|
|
28
|
-
"@agoric/internal": "0.3.3-dev-
|
|
29
|
-
"@agoric/store": "0.9.3-dev-
|
|
30
|
-
"@agoric/swing-store": "0.9.2-dev-
|
|
31
|
-
"@agoric/swingset-vat": "0.32.3-dev-
|
|
32
|
-
"@agoric/telemetry": "0.6.3-dev-
|
|
33
|
-
"@agoric/vm-config": "0.1.1-dev-
|
|
25
|
+
"@agoric/builders": "0.1.1-dev-d1ef359.0+d1ef359",
|
|
26
|
+
"@agoric/cosmos": "0.34.2-dev-d1ef359.0+d1ef359",
|
|
27
|
+
"@agoric/deploy-script-support": "0.10.4-dev-d1ef359.0+d1ef359",
|
|
28
|
+
"@agoric/internal": "0.3.3-dev-d1ef359.0+d1ef359",
|
|
29
|
+
"@agoric/store": "0.9.3-dev-d1ef359.0+d1ef359",
|
|
30
|
+
"@agoric/swing-store": "0.9.2-dev-d1ef359.0+d1ef359",
|
|
31
|
+
"@agoric/swingset-vat": "0.32.3-dev-d1ef359.0+d1ef359",
|
|
32
|
+
"@agoric/telemetry": "0.6.3-dev-d1ef359.0+d1ef359",
|
|
33
|
+
"@agoric/vm-config": "0.1.1-dev-d1ef359.0+d1ef359",
|
|
34
34
|
"@endo/bundle-source": "^3.5.1",
|
|
35
35
|
"@endo/env-options": "^1.1.8",
|
|
36
36
|
"@endo/errors": "^1.2.9",
|
|
@@ -52,8 +52,12 @@
|
|
|
52
52
|
"tmp": "^0.2.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
+
"@agoric/kmarshal": "0.1.1-dev-d1ef359.0+d1ef359",
|
|
56
|
+
"@endo/eventual-send": "^1.2.8",
|
|
55
57
|
"ava": "^5.3.0",
|
|
56
|
-
"
|
|
58
|
+
"better-sqlite3": "^9.1.1",
|
|
59
|
+
"c8": "^10.1.2",
|
|
60
|
+
"ses": "^1.10.0"
|
|
57
61
|
},
|
|
58
62
|
"publishConfig": {
|
|
59
63
|
"access": "public"
|
|
@@ -73,5 +77,5 @@
|
|
|
73
77
|
"typeCoverage": {
|
|
74
78
|
"atLeast": 83.4
|
|
75
79
|
},
|
|
76
|
-
"gitHead": "
|
|
80
|
+
"gitHead": "d1ef35965047ef31f0fa5451d4cfb8bd06c59247"
|
|
77
81
|
}
|
package/src/chain-main.js
CHANGED
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
makeReadCachingStorage,
|
|
48
48
|
} from './helpers/bufferedStorage.js';
|
|
49
49
|
import stringify from './helpers/json-stable-stringify.js';
|
|
50
|
-
import { launch } from './launch-chain.js';
|
|
50
|
+
import { launch, launchAndShareInternals } from './launch-chain.js';
|
|
51
51
|
import { makeProcessValue } from './helpers/process-value.js';
|
|
52
52
|
import {
|
|
53
53
|
spawnSwingStoreExport,
|
|
@@ -228,7 +228,11 @@ export const makeQueueStorage = (call, queuePath) => {
|
|
|
228
228
|
* slogSender?: ERef<EReturn<typeof makeSlogSender>>,
|
|
229
229
|
* swingStore?: import('@agoric/swing-store').SwingStore,
|
|
230
230
|
* vatconfig?: Parameters<typeof launch>[0]['vatconfig'],
|
|
231
|
-
*
|
|
231
|
+
* withInternals?: boolean,
|
|
232
|
+
* }} [options.testingOverrides] Exposed only for testing purposes.
|
|
233
|
+
* `debugName`/`slogSender`/`swingStore`/`vatConfig` are pure overrides, while
|
|
234
|
+
* `withInternals` expands the return value to expose internal objects
|
|
235
|
+
* `controller`/`bridgeInbound`/`timer`.
|
|
232
236
|
*/
|
|
233
237
|
export const makeLaunchChain = (
|
|
234
238
|
agcc,
|
|
@@ -523,7 +527,10 @@ export const makeLaunchChain = (
|
|
|
523
527
|
? makeArchiveTranscript(vatTranscriptArchiveDir, fsPowers)
|
|
524
528
|
: undefined;
|
|
525
529
|
|
|
526
|
-
const
|
|
530
|
+
const launcher = testingOverrides.withInternals
|
|
531
|
+
? launchAndShareInternals
|
|
532
|
+
: launch;
|
|
533
|
+
const s = await launcher({
|
|
527
534
|
actionQueueStorage,
|
|
528
535
|
highPriorityQueueStorage,
|
|
529
536
|
swingStore: testingOverrides.swingStore,
|
|
@@ -76,10 +76,8 @@ export const makeKVStoreFromMap = map => {
|
|
|
76
76
|
let priorKeyIndex;
|
|
77
77
|
|
|
78
78
|
const ensureSorted = () => {
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
sortedKeys.sort(compareByCodePoints);
|
|
82
|
-
}
|
|
79
|
+
if (sortedKeys) return;
|
|
80
|
+
sortedKeys = [...map.keys()].sort(compareByCodePoints);
|
|
83
81
|
};
|
|
84
82
|
|
|
85
83
|
const clearGetNextKeyCache = () => {
|
|
@@ -101,9 +99,16 @@ export const makeKVStoreFromMap = map => {
|
|
|
101
99
|
assert.typeof(priorKey, 'string');
|
|
102
100
|
ensureSorted();
|
|
103
101
|
const start =
|
|
104
|
-
|
|
105
|
-
?
|
|
106
|
-
:
|
|
102
|
+
priorKeyReturned === undefined
|
|
103
|
+
? 0
|
|
104
|
+
: // If priorKeyReturned <= priorKey, start just after it.
|
|
105
|
+
(compareByCodePoints(priorKeyReturned, priorKey) <= 0 &&
|
|
106
|
+
priorKeyIndex + 1) ||
|
|
107
|
+
// Else if priorKeyReturned immediately follows priorKey, start at
|
|
108
|
+
// its index (and expect to return it again).
|
|
109
|
+
(sortedKeys.at(priorKeyIndex - 1) === priorKey && priorKeyIndex) ||
|
|
110
|
+
// Otherwise, start at the beginning.
|
|
111
|
+
0;
|
|
107
112
|
for (let i = start; i < sortedKeys.length; i += 1) {
|
|
108
113
|
const key = sortedKeys[i];
|
|
109
114
|
if (compareByCodePoints(key, priorKey) <= 0) continue;
|
|
@@ -226,7 +231,8 @@ export function makeBufferedStorage(kvStore, listeners = {}) {
|
|
|
226
231
|
|
|
227
232
|
// To avoid confusion, additions and deletions are prevented from sharing
|
|
228
233
|
// the same key at any given time.
|
|
229
|
-
|
|
234
|
+
/** @type {Map<string, T> & KVStore<T>} */
|
|
235
|
+
const additions = provideEnhancedKVStore(makeKVStoreFromMap(new Map()));
|
|
230
236
|
const deletions = new Set();
|
|
231
237
|
|
|
232
238
|
/** @type {KVStore<T>} */
|
|
@@ -257,13 +263,18 @@ export function makeBufferedStorage(kvStore, listeners = {}) {
|
|
|
257
263
|
deletions.add(key);
|
|
258
264
|
if (onPendingDelete !== undefined) onPendingDelete(key);
|
|
259
265
|
},
|
|
260
|
-
|
|
261
|
-
/**
|
|
262
|
-
* @param {string} previousKey
|
|
263
|
-
*/
|
|
264
266
|
getNextKey(previousKey) {
|
|
265
267
|
assert.typeof(previousKey, 'string');
|
|
266
|
-
|
|
268
|
+
const bufferedNextKey = additions.getNextKey(previousKey);
|
|
269
|
+
let nextKey = kvStore.getNextKey(previousKey);
|
|
270
|
+
while (nextKey !== undefined) {
|
|
271
|
+
if (bufferedNextKey !== undefined) {
|
|
272
|
+
if (compareByCodePoints(bufferedNextKey, nextKey) <= 0) break;
|
|
273
|
+
}
|
|
274
|
+
if (!deletions.has(nextKey)) return nextKey;
|
|
275
|
+
nextKey = kvStore.getNextKey(nextKey);
|
|
276
|
+
}
|
|
277
|
+
return bufferedNextKey;
|
|
267
278
|
},
|
|
268
279
|
};
|
|
269
280
|
function commit() {
|
package/src/launch-chain.js
CHANGED
|
@@ -26,8 +26,8 @@ import {
|
|
|
26
26
|
} from '@agoric/swingset-vat';
|
|
27
27
|
import { waitUntilQuiescent } from '@agoric/internal/src/lib-nodejs/waitUntilQuiescent.js';
|
|
28
28
|
import { openSwingStore } from '@agoric/swing-store';
|
|
29
|
-
import { BridgeId as BRIDGE_ID } from '@agoric/internal';
|
|
30
|
-
import { objectMapMutable } from '@agoric/internal/src/js-utils.js';
|
|
29
|
+
import { attenuate, BridgeId as BRIDGE_ID } from '@agoric/internal';
|
|
30
|
+
import { objectMapMutable, TRUE } from '@agoric/internal/src/js-utils.js';
|
|
31
31
|
import { makeWithQueue } from '@agoric/internal/src/queue.js';
|
|
32
32
|
import * as ActionType from '@agoric/internal/src/action-types.js';
|
|
33
33
|
|
|
@@ -405,7 +405,7 @@ export async function buildSwingset(
|
|
|
405
405
|
/**
|
|
406
406
|
* @param {LaunchOptions} options
|
|
407
407
|
*/
|
|
408
|
-
export async function
|
|
408
|
+
export async function launchAndShareInternals({
|
|
409
409
|
actionQueueStorage,
|
|
410
410
|
highPriorityQueueStorage,
|
|
411
411
|
kernelStateDBDir,
|
|
@@ -1408,5 +1408,26 @@ export async function launch({
|
|
|
1408
1408
|
writeSlogObject,
|
|
1409
1409
|
savedHeight,
|
|
1410
1410
|
savedChainSends: JSON.parse(kvStore.get(getHostKey('chainSends')) || '[]'),
|
|
1411
|
+
// NOTE: to be used only for testing purposes!
|
|
1412
|
+
internals: {
|
|
1413
|
+
controller,
|
|
1414
|
+
bridgeInbound,
|
|
1415
|
+
timer,
|
|
1416
|
+
},
|
|
1411
1417
|
};
|
|
1412
1418
|
}
|
|
1419
|
+
|
|
1420
|
+
/**
|
|
1421
|
+
* @param {LaunchOptions} options
|
|
1422
|
+
* @returns {Promise<Omit<Awaited<ReturnType<typeof launchAndShareInternals>>, 'internals'>>}
|
|
1423
|
+
*/
|
|
1424
|
+
export async function launch(options) {
|
|
1425
|
+
const launchResult = await launchAndShareInternals(options);
|
|
1426
|
+
return attenuate(launchResult, {
|
|
1427
|
+
blockingSend: TRUE,
|
|
1428
|
+
shutdown: TRUE,
|
|
1429
|
+
writeSlogObject: TRUE,
|
|
1430
|
+
savedHeight: TRUE,
|
|
1431
|
+
savedChainSends: TRUE,
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file Interact with the database and/or vats of a swingstore.sqlite in an
|
|
4
|
+
* ephemeral environment. This file functions as both an importable module and
|
|
5
|
+
* as a standalone interactive or non-interactive script. See
|
|
6
|
+
* "Check for CLI invocation" below for usage detail about the latter.
|
|
7
|
+
*/
|
|
8
|
+
/* eslint-env node */
|
|
9
|
+
/* global globalThis */
|
|
10
|
+
/* eslint-disable no-empty */
|
|
11
|
+
|
|
12
|
+
// Overwrite the global console for deeper inspection.
|
|
13
|
+
// @ts-expect-error TS2307 Cannot find module
|
|
14
|
+
import 'data:text/javascript,import { Console } from "node:console"; const { stdout, stderr, env } = process; const inspectOptions = { depth: Number(env.CONSOLE_INSPECT_DEPTH) || 6 }; globalThis.console = new Console({ stdout, stderr, inspectOptions });';
|
|
15
|
+
|
|
16
|
+
import 'ses';
|
|
17
|
+
import '@endo/eventual-send/shim.js';
|
|
18
|
+
import '@endo/init/pre.js';
|
|
19
|
+
// __hardenTaming__: "unsafe" is unfortunate, but without it, automatic
|
|
20
|
+
// hardening discovers EventEmitter.prototype and breaks creation of new event
|
|
21
|
+
// emitters (e.g., `Readable.from(...)`) because initialization is vulnerable to
|
|
22
|
+
// the property assignment override mistake w.r.t. _events/_eventsCount/etc.
|
|
23
|
+
// https://github.com/nodejs/node/blob/v22.12.0/lib/events.js#L347
|
|
24
|
+
// @ts-expect-error TS2307 Cannot find module
|
|
25
|
+
import 'data:text/javascript,try { lockdown({ domainTaming: "unsafe", errorTaming: "unsafe-debug", __hardenTaming__: "unsafe" }); } catch (_err) {}';
|
|
26
|
+
|
|
27
|
+
import { spawn } from 'node:child_process';
|
|
28
|
+
import fs from 'node:fs';
|
|
29
|
+
import os from 'node:os';
|
|
30
|
+
import pathlib from 'node:path';
|
|
31
|
+
import repl from 'node:repl';
|
|
32
|
+
import stream from 'node:stream';
|
|
33
|
+
import {
|
|
34
|
+
setImmediate as resolveImmediate,
|
|
35
|
+
setTimeout as delay,
|
|
36
|
+
} from 'node:timers/promises';
|
|
37
|
+
import { fileURLToPath } from 'node:url';
|
|
38
|
+
import { inspect, parseArgs } from 'node:util';
|
|
39
|
+
import { isMainThread } from 'node:worker_threads';
|
|
40
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
41
|
+
import sqlite3 from 'better-sqlite3';
|
|
42
|
+
import { Fail, b, q } from '@endo/errors';
|
|
43
|
+
import { makePromiseKit } from '@endo/promise-kit';
|
|
44
|
+
import { objectMap, BridgeId } from '@agoric/internal';
|
|
45
|
+
import { QueuedActionType } from '@agoric/internal/src/action-types.js';
|
|
46
|
+
import { defineName } from '@agoric/internal/src/js-utils.js';
|
|
47
|
+
import { makeFakeStorageKit } from '@agoric/internal/src/storage-test-utils.js';
|
|
48
|
+
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
49
|
+
import { krefOf, kser, kslot, kunser } from '@agoric/kmarshal';
|
|
50
|
+
import {
|
|
51
|
+
openSwingStore,
|
|
52
|
+
makeBundleStore,
|
|
53
|
+
bundleIDFromName,
|
|
54
|
+
} from '@agoric/swing-store';
|
|
55
|
+
import {
|
|
56
|
+
makeBufferedStorage,
|
|
57
|
+
provideEnhancedKVStore,
|
|
58
|
+
} from '../src/helpers/bufferedStorage.js';
|
|
59
|
+
import {
|
|
60
|
+
DEFAULT_SIM_SWINGSET_PARAMS,
|
|
61
|
+
makeVatCleanupBudgetFromKeywords,
|
|
62
|
+
} from '../src/sim-params.js';
|
|
63
|
+
import { makeCosmicSwingsetTestKit } from './test-kit.js';
|
|
64
|
+
|
|
65
|
+
/** @import { ManagerType, SwingSetConfig } from '@agoric/swingset-vat' */
|
|
66
|
+
/** @import { KVStore } from '../src/helpers/bufferedStorage.js' */
|
|
67
|
+
|
|
68
|
+
const useColors = process.stdout?.hasColors?.();
|
|
69
|
+
const inspectDepth = 6;
|
|
70
|
+
|
|
71
|
+
const dataProp = { writable: true, enumerable: true, configurable: true };
|
|
72
|
+
const empty = Object.create(null);
|
|
73
|
+
const noop = () => {};
|
|
74
|
+
const parseNumber = input => (input.match(/[0-9]/) ? Number(input) : NaN);
|
|
75
|
+
|
|
76
|
+
// cf. packages/swing-store/src/exporter.js
|
|
77
|
+
const storeExportAPI = ['getExportRecords', 'getArtifactNames'];
|
|
78
|
+
|
|
79
|
+
// TODO: getVatAdminNode('v112') # scan the vatAdmin vom v2.vs.vom.* vrefs for value matching /\b${vatID}\b/
|
|
80
|
+
export const makeHelpers = ({ db, EV }) => {
|
|
81
|
+
const sqlKVGet = db
|
|
82
|
+
.prepare('SELECT value FROM kvStore WHERE key = ?')
|
|
83
|
+
.pluck();
|
|
84
|
+
const kvGet = key => sqlKVGet.get(key);
|
|
85
|
+
const kvGetJSON = key => JSON.parse(kvGet(key));
|
|
86
|
+
|
|
87
|
+
const sqlKVByRange = db.prepare(
|
|
88
|
+
`SELECT key, value FROM kvStore WHERE key >= :a AND key < :b AND ${[
|
|
89
|
+
'(:keySuffix IS NULL OR substr(key, -length(:keySuffix)) = :keySuffix)',
|
|
90
|
+
'(:keyGlob IS NULL OR key GLOB :keyGlob)',
|
|
91
|
+
'(:valueGlob IS NULL OR value GLOB :valueGlob)',
|
|
92
|
+
].join(' AND ')}`,
|
|
93
|
+
);
|
|
94
|
+
const sqlKVByHalfRange = db.prepare(
|
|
95
|
+
`SELECT key, value FROM kvStore WHERE key >= :a AND ${[
|
|
96
|
+
'(:keySuffix IS NULL OR substr(key, -length(:keySuffix)) = :keySuffix)',
|
|
97
|
+
'(:keyGlob IS NULL OR key GLOB :keyGlob)',
|
|
98
|
+
'(:valueGlob IS NULL OR value GLOB :valueGlob)',
|
|
99
|
+
].join(' AND ')}`,
|
|
100
|
+
);
|
|
101
|
+
const kvGlob = (keyGlob, valueGlob = undefined, lazy = false) => {
|
|
102
|
+
const [_keyPattern, keyPrefix, keyTail, keySuffix] =
|
|
103
|
+
/** @type {string[]} */ (/^([^*?]*)((?:[*?]([^*?]*))*)$/.exec(keyGlob));
|
|
104
|
+
let sql = sqlKVByHalfRange;
|
|
105
|
+
/** @type {Record<'a' | 'b' | 'keySuffix' | 'keyGlob' | 'valueGlob', string | null>} */
|
|
106
|
+
const args = {
|
|
107
|
+
a: keyPrefix,
|
|
108
|
+
b: null,
|
|
109
|
+
keySuffix: keySuffix || null,
|
|
110
|
+
keyGlob: keyTail && keyTail !== '*' ? keyGlob : null,
|
|
111
|
+
valueGlob: valueGlob ?? null,
|
|
112
|
+
};
|
|
113
|
+
const chars = [...keyPrefix];
|
|
114
|
+
const i = chars.findLastIndex(ch => ch < '\u{10FFFF}');
|
|
115
|
+
if (i !== -1) {
|
|
116
|
+
sql = sqlKVByRange;
|
|
117
|
+
const newLastCP = /** @type {number} */ (chars[i].codePointAt(0)) + 1;
|
|
118
|
+
args.b = chars.slice(0, i).join('') + String.fromCodePoint(newLastCP);
|
|
119
|
+
} else {
|
|
120
|
+
console.warn('Warning: Unprefixed searches can be slow');
|
|
121
|
+
}
|
|
122
|
+
return lazy ? sql.iterate(args) : sql.all(args);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
let vatsByID = new Map();
|
|
126
|
+
let vatsByName = new Map();
|
|
127
|
+
try {
|
|
128
|
+
// @see {@link ../../SwingSet/src/kernel/state/kernelKeeper.js}
|
|
129
|
+
kvGetJSON('vat.names').every(
|
|
130
|
+
name =>
|
|
131
|
+
typeof name === 'string' ||
|
|
132
|
+
Fail`static vat name ${q(name)} must be a string`,
|
|
133
|
+
);
|
|
134
|
+
kvGetJSON('vat.dynamicIDs').every(
|
|
135
|
+
vatID =>
|
|
136
|
+
typeof vatID === 'string' ||
|
|
137
|
+
Fail`dynamic vatID ${q(vatID)} must be a string`,
|
|
138
|
+
);
|
|
139
|
+
const vatQuery = db.prepare(`
|
|
140
|
+
WITH vat AS (
|
|
141
|
+
SELECT 1 AS rank, nameJSON.key AS idx, vatNameToID.value AS vatID
|
|
142
|
+
FROM kvStore AS nameList
|
|
143
|
+
LEFT JOIN json_each(nameList.value) AS nameJSON
|
|
144
|
+
LEFT JOIN kvStore AS vatNameToID
|
|
145
|
+
ON vatNameToID.key = 'vat.name.' || nameJSON.atom
|
|
146
|
+
WHERE nameList.key='vat.names'
|
|
147
|
+
UNION SELECT 2 as rank, idJSON.key AS idx, idJSON.value AS vatID
|
|
148
|
+
FROM kvStore AS idList, json_each(idList.value) AS idJSON
|
|
149
|
+
WHERE idList.key='vat.dynamicIDs'
|
|
150
|
+
)
|
|
151
|
+
SELECT vat.vatID, rank, source.value AS sourceText, options.value AS optionsText
|
|
152
|
+
FROM vat
|
|
153
|
+
LEFT JOIN kvStore AS source ON source.key = vat.vatID || '.source'
|
|
154
|
+
LEFT JOIN kvStore AS options ON options.key = vat.vatID || '.options'
|
|
155
|
+
ORDER BY vat.rank, vat.idx
|
|
156
|
+
`);
|
|
157
|
+
for (const dbRecord of vatQuery.iterate()) {
|
|
158
|
+
const { vatID, rank, sourceText, optionsText } = dbRecord;
|
|
159
|
+
const isStatic = rank === 1;
|
|
160
|
+
const source = sourceText ? JSON.parse(sourceText) : undefined;
|
|
161
|
+
const options = optionsText ? JSON.parse(optionsText) : undefined;
|
|
162
|
+
const name = options?.name;
|
|
163
|
+
const vat = harden({ vatID, name, isStatic, source, options });
|
|
164
|
+
vatsByID.set(vatID, vat);
|
|
165
|
+
if (name) {
|
|
166
|
+
const conflict = vatsByName.get(name);
|
|
167
|
+
if (vat.isStatic || !conflict) {
|
|
168
|
+
// Static vats trump dynamic vats in vatsByName.
|
|
169
|
+
vatsByName.set(name, vat);
|
|
170
|
+
} else if (!Array.isArray(conflict)) {
|
|
171
|
+
// ...but dynamic vats with duplicate names get collected into arrays.
|
|
172
|
+
vatsByName.set(name, [conflict, vat]);
|
|
173
|
+
} else {
|
|
174
|
+
conflict.push(vat);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch (err) {
|
|
179
|
+
console.warn('Warning: Could not build vat maps', err);
|
|
180
|
+
// @ts-expect-error
|
|
181
|
+
vatsByID = undefined;
|
|
182
|
+
// @ts-expect-error
|
|
183
|
+
vatsByName = undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const vatIDPatt = /^v[1-9][0-9]*$/;
|
|
187
|
+
// @see {@link ../../SwingSet/docs/c-lists.md}
|
|
188
|
+
// @see {@link ../../swingset-liveslots/src/vatstore-usage.md}
|
|
189
|
+
const refPatt =
|
|
190
|
+
/(?<kref>^k[opd][1-9][0-9]*$)|(?<vref>^[opd][+-](?:0|[1-9][0-9]*)$|^(?<baseref>o[+][vd]?(?<kindID>[1-9][0-9]*)\/[1-9][0-9]*)(?<facetSuffix>:(?<facetID>0|[1-9][0-9]*))?)/;
|
|
191
|
+
const krefToVrefValuePatt = /^([R_]) ([^ ]+)$/;
|
|
192
|
+
const getKindMeta = (vatID, kindID) => {
|
|
193
|
+
const kindMetaJSON =
|
|
194
|
+
kvGet(`${vatID}.vs.vom.dkind.${kindID}.descriptor`) ||
|
|
195
|
+
kvGet(`${vatID}.vs.vom.vkind.${kindID}.descriptor`);
|
|
196
|
+
return JSON.parse(kindMetaJSON);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* @param {string} refID kref or vref
|
|
201
|
+
* @param {string} [contextVatID]
|
|
202
|
+
* @returns {Array<{vatID: string, kref: string, vref: string, kind?: string, facet?: string}>}
|
|
203
|
+
*/
|
|
204
|
+
const getRefs = (refID, contextVatID = undefined) => {
|
|
205
|
+
const refParts = refID.match(refPatt)?.groups;
|
|
206
|
+
if (!refParts) throw Fail`unknown kref or vref format in ${refID}`;
|
|
207
|
+
const isKref = !!refParts.kref;
|
|
208
|
+
contextVatID === undefined ||
|
|
209
|
+
contextVatID.match(vatIDPatt) ||
|
|
210
|
+
Fail`invalid contextVatID ${contextVatID}`;
|
|
211
|
+
|
|
212
|
+
// Search for rows like (`${vatID}.c.${kref}`, `${flag} ${vref}`), where
|
|
213
|
+
// kref might be exracted from rows like (`${vatID}.c.${vref}`, kref).
|
|
214
|
+
// @see {@link ../../SwingSet/docs/c-lists.md}
|
|
215
|
+
const krefs = [];
|
|
216
|
+
let kindMeta;
|
|
217
|
+
if (isKref) {
|
|
218
|
+
krefs.push(refID);
|
|
219
|
+
} else {
|
|
220
|
+
const maybeKref = kref => kref && krefs.push(kref);
|
|
221
|
+
maybeKref(kvGet(`${contextVatID}.c.${refID}`));
|
|
222
|
+
const { baseref, kindID, facetID } = refParts;
|
|
223
|
+
if (kindID && !facetID) {
|
|
224
|
+
// Each facet might have its own kref.
|
|
225
|
+
kindMeta = getKindMeta(contextVatID, kindID);
|
|
226
|
+
const facetNames = kindMeta?.facets;
|
|
227
|
+
for (let i = 0; i < (facetNames?.length ?? 0); i += 1) {
|
|
228
|
+
maybeKref(kvGet(`${contextVatID}.c.${baseref}:${i}`));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Don't scan when we can enumerate keys.
|
|
233
|
+
const results = [];
|
|
234
|
+
for (const vatID of vatsByID.keys()) {
|
|
235
|
+
for (const kref of krefs) {
|
|
236
|
+
const value = kvGet(`${vatID}.c.${kref}`);
|
|
237
|
+
if (!value) continue;
|
|
238
|
+
const [_value, _reachabilityFlag, vref] =
|
|
239
|
+
value.match(krefToVrefValuePatt) ||
|
|
240
|
+
Fail`unexpected c-list value ${value}`;
|
|
241
|
+
const result = { vatID, kref, vref };
|
|
242
|
+
const { kindID, facetID } = vref.match(refPatt)?.groups || empty;
|
|
243
|
+
if (kindID) {
|
|
244
|
+
// kindID appears only in vrefs for the exporting vat, where we either
|
|
245
|
+
// get metadata on the first try or not at all.
|
|
246
|
+
if (kindMeta !== null) {
|
|
247
|
+
kindMeta ||= getKindMeta(vatID, kindID) || null;
|
|
248
|
+
}
|
|
249
|
+
result.kind = kindMeta?.tag;
|
|
250
|
+
if (facetID) result.facet = kindMeta?.facets?.[facetID];
|
|
251
|
+
}
|
|
252
|
+
results.push(result);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return results;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Run a core-eval directly through the controller (i.e., without a block).
|
|
260
|
+
*
|
|
261
|
+
* @param {string} fnText must evaluate to a function that will be invoked in
|
|
262
|
+
* a core eval compartment with a "powers" argument as attenuated by
|
|
263
|
+
* `permits` (with no attenuation by default).
|
|
264
|
+
* @param {import('@agoric/vats/src/core/lib-boot.js').BootstrapManifestPermit} [permits]
|
|
265
|
+
*/
|
|
266
|
+
const runCoreEval = async (fnText, permits = true) => {
|
|
267
|
+
// Fail noisily if fnText does not evaluate to a function.
|
|
268
|
+
// This must be refactored if there is ever a need for such input.
|
|
269
|
+
const fn = new Compartment().evaluate(fnText);
|
|
270
|
+
typeof fn === 'function' || Fail`text must evaluate to a function`;
|
|
271
|
+
/** @type {import('@agoric/cosmic-proto/swingset/swingset.js').CoreEvalSDKType} */
|
|
272
|
+
const coreEvalDesc = {
|
|
273
|
+
json_permits: JSON.stringify(permits),
|
|
274
|
+
js_code: fnText,
|
|
275
|
+
};
|
|
276
|
+
const coreEvalAction = {
|
|
277
|
+
type: QueuedActionType.CORE_EVAL,
|
|
278
|
+
evals: [coreEvalDesc],
|
|
279
|
+
};
|
|
280
|
+
// Assume a path to the coreEvalBridgeHandler.
|
|
281
|
+
const coreEvalBridgeHandler = await EV.vat('bootstrap').consumeItem(
|
|
282
|
+
'coreEvalBridgeHandler',
|
|
283
|
+
);
|
|
284
|
+
return EV(coreEvalBridgeHandler).fromBridge(coreEvalAction);
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
return harden({
|
|
288
|
+
runCoreEval,
|
|
289
|
+
stable: { db, getRefs, kvGet, kvGetJSON, kvGlob, vatsByID, vatsByName },
|
|
290
|
+
});
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Wrap a swing-store sub-store (kvStore/transcriptStore/etc.) with a
|
|
295
|
+
* replacement whose functions log and/or track/respond to staleness.
|
|
296
|
+
*
|
|
297
|
+
* @template {object} Substore
|
|
298
|
+
* @param {string} storeName
|
|
299
|
+
* @param {Substore} store
|
|
300
|
+
* @param {object} [options]
|
|
301
|
+
* @param {(...args: unknown[]) => void} [options.log]
|
|
302
|
+
* @param {(...args: unknown[]) => void} [options.warn]
|
|
303
|
+
* @param {(key?: unknown) => boolean} [options.isClean]
|
|
304
|
+
* @param {(key?: unknown) => boolean} [options.isStale]
|
|
305
|
+
* @param {(key?: unknown) => void} [options.markStale]
|
|
306
|
+
* @param {string[]} [options.allow] functions to allow
|
|
307
|
+
* @param {string[]} [options.allowIfClean] functions to allow when `isClean(firstArg)` returns true
|
|
308
|
+
* @param {string[]} [options.allowAndMark] functions to augment with `markStale(firstArg)`
|
|
309
|
+
* @param {Array<string | [string, Function]>} [options.logAndMark] functions to replace with `log(storeName, functionName, firstArg, ...details)` and `markStale(firstArg)`
|
|
310
|
+
* @param {string[]} [options.warnIfStale] functions to augment with `warn(storeName, functionName, firstArg, ...details)` when `isStale(firstArg)` returns true
|
|
311
|
+
* @param {string[]} [options.disallow] functions to disallow
|
|
312
|
+
* @returns {Substore}
|
|
313
|
+
*/
|
|
314
|
+
export const wrapSubstore = (storeName, store, options = {}) => {
|
|
315
|
+
const {
|
|
316
|
+
log = console.log,
|
|
317
|
+
warn = console.warn,
|
|
318
|
+
isClean = () => Fail`[inquisitor] cannot check isClean in ${storeName}`,
|
|
319
|
+
isStale = () => Fail`[inquisitor] cannot check isStale in ${storeName}`,
|
|
320
|
+
markStale = () => Fail`[inquisitor] cannot markStale in ${storeName}`,
|
|
321
|
+
allow = [],
|
|
322
|
+
allowIfClean = [],
|
|
323
|
+
allowAndMark = [],
|
|
324
|
+
logAndMark: rawLogAndMark = [],
|
|
325
|
+
warnIfStale = [],
|
|
326
|
+
disallow = [],
|
|
327
|
+
} = options;
|
|
328
|
+
const logAndMarkMap = new Map(
|
|
329
|
+
rawLogAndMark.map(x => (Array.isArray(x) ? x : [x, noop])),
|
|
330
|
+
);
|
|
331
|
+
const flat = (...arrs) => [].concat(...arrs);
|
|
332
|
+
/** @type {Set<string>} */
|
|
333
|
+
const unseen = new Set(
|
|
334
|
+
flat(allow, allowIfClean, allowAndMark, warnIfStale, disallow),
|
|
335
|
+
);
|
|
336
|
+
for (const name of logAndMarkMap.keys()) unseen.add(name);
|
|
337
|
+
const wrapped = objectMap(
|
|
338
|
+
/** @type {Record<string, Function>} */ (store),
|
|
339
|
+
(fn, name) => {
|
|
340
|
+
if (typeof name !== 'string') {
|
|
341
|
+
throw Fail`[inquisitor] non-string property ${b(storeName)}[${q(name)}]`;
|
|
342
|
+
}
|
|
343
|
+
unseen.delete(name);
|
|
344
|
+
if (allow.includes(name)) {
|
|
345
|
+
return fn;
|
|
346
|
+
} else if (allowIfClean.includes(name)) {
|
|
347
|
+
return defineName(name, (key, ...rest) => {
|
|
348
|
+
isClean(key) ||
|
|
349
|
+
Fail`[inquisitor] ${b(storeName)}.${b(name)}(${b(key)}) after mutations`;
|
|
350
|
+
return fn(key, ...rest);
|
|
351
|
+
});
|
|
352
|
+
} else if (allowAndMark.includes(name)) {
|
|
353
|
+
return defineName(name, (key, ...rest) => {
|
|
354
|
+
markStale(key);
|
|
355
|
+
return fn(key, ...rest);
|
|
356
|
+
});
|
|
357
|
+
} else if (logAndMarkMap.has(name)) {
|
|
358
|
+
const makeResult = /** @type {Function} */ (logAndMarkMap.get(name));
|
|
359
|
+
return defineName(name, (key, ...rest) => {
|
|
360
|
+
markStale(key);
|
|
361
|
+
log(storeName, name, key, ...rest);
|
|
362
|
+
return makeResult(key, ...rest);
|
|
363
|
+
});
|
|
364
|
+
} else if (warnIfStale.includes(name)) {
|
|
365
|
+
return defineName(name, (key, ...rest) => {
|
|
366
|
+
if (isStale(key)) {
|
|
367
|
+
warn(storeName, name, key, 'returning stale data');
|
|
368
|
+
}
|
|
369
|
+
return fn(key, ...rest);
|
|
370
|
+
});
|
|
371
|
+
} else if (disallow.includes(name)) {
|
|
372
|
+
return defineName(
|
|
373
|
+
name,
|
|
374
|
+
() => Fail`[inquisitor] disallowed ${b(storeName)}.${b(name)}`,
|
|
375
|
+
);
|
|
376
|
+
} else {
|
|
377
|
+
throw Fail`[inquisitor] unknown ${b(storeName)} function ${b(name)}; time to update?`;
|
|
378
|
+
}
|
|
379
|
+
},
|
|
380
|
+
);
|
|
381
|
+
unseen.size === 0 ||
|
|
382
|
+
Fail`[inquisitor] ${b(storeName)} lacked ${q([...unseen])}; time to update?`;
|
|
383
|
+
// @ts-expect-error cast
|
|
384
|
+
return wrapped;
|
|
385
|
+
};
|
|
386
|
+
harden(wrapSubstore);
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Make an overlay-like swing-store that buffers all mutations over a read-only
|
|
390
|
+
* database.
|
|
391
|
+
* If this ever needs substantial refactoring, consider pushing the
|
|
392
|
+
* functionality into swing-store itself.
|
|
393
|
+
*
|
|
394
|
+
* @param {string} dbPath to a swingstore.sqlite file
|
|
395
|
+
* @param {typeof wrapSubstore} wrapStore a function to replace swing-store sub-stores (kvStore/transcriptStore/etc.)
|
|
396
|
+
*/
|
|
397
|
+
export const makeSwingStoreOverlay = (dbPath, wrapStore = wrapSubstore) => {
|
|
398
|
+
/** @type {Array<[storeName: string, operation: string, ...args: unknown[]]>} */
|
|
399
|
+
const mutations = [];
|
|
400
|
+
const recordCall = (storeName, operation, ...details) =>
|
|
401
|
+
mutations.push([storeName, operation, ...details]);
|
|
402
|
+
const makeWrapHelpers = () => {
|
|
403
|
+
const modifiedVats = new Set();
|
|
404
|
+
return {
|
|
405
|
+
log: recordCall,
|
|
406
|
+
isClean: () => modifiedVats.size === 0,
|
|
407
|
+
isStale: vatID => modifiedVats.has(vatID),
|
|
408
|
+
markStale: vatID => modifiedVats.add(vatID),
|
|
409
|
+
};
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const kvListeners = {
|
|
413
|
+
onPendingSet: (key, value) => recordCall('kvStore', 'set', key, value),
|
|
414
|
+
onPendingDelete: key => recordCall('kvStore', 'delete', key),
|
|
415
|
+
};
|
|
416
|
+
const swingStore = openSwingStore(dbPath, {
|
|
417
|
+
asFile: true,
|
|
418
|
+
readonly: true,
|
|
419
|
+
wrapKvStore: base => makeBufferedStorage(base, kvListeners).kvStore,
|
|
420
|
+
wrapTranscriptStore: transcriptStore => {
|
|
421
|
+
const wrapHelpers = makeWrapHelpers();
|
|
422
|
+
const pendingItemsByVat = new Map();
|
|
423
|
+
/** @type {ReturnType<import('@agoric/swing-store').makeTranscriptStore>} */
|
|
424
|
+
const transcriptStoreOverride = {
|
|
425
|
+
...transcriptStore,
|
|
426
|
+
addItem: (vatID, item) => {
|
|
427
|
+
recordCall('transcriptStore', 'addItem', vatID, item);
|
|
428
|
+
if (wrapHelpers.isStale(vatID)) return;
|
|
429
|
+
const pendingItems = pendingItemsByVat.get(vatID) || [];
|
|
430
|
+
const { startPos, endPos, hash, incarnation } =
|
|
431
|
+
pendingItems.at(-1) || transcriptStore.getCurrentSpanBounds(vatID);
|
|
432
|
+
pendingItems.push({
|
|
433
|
+
item,
|
|
434
|
+
startPos,
|
|
435
|
+
endPos: endPos + 1,
|
|
436
|
+
hash: hash && '<unknown>',
|
|
437
|
+
incarnation,
|
|
438
|
+
});
|
|
439
|
+
pendingItemsByVat.set(vatID, pendingItems);
|
|
440
|
+
},
|
|
441
|
+
getCurrentSpanBounds: vatID => {
|
|
442
|
+
const pendingItems = pendingItemsByVat.get(vatID) || [];
|
|
443
|
+
const { startPos, endPos, hash, incarnation } =
|
|
444
|
+
pendingItems.at(-1) || transcriptStore.getCurrentSpanBounds(vatID);
|
|
445
|
+
return { startPos, endPos, hash, incarnation };
|
|
446
|
+
},
|
|
447
|
+
readSpan: (vatID, startPos) => {
|
|
448
|
+
const reader = function* reader() {
|
|
449
|
+
try {
|
|
450
|
+
// Read from the base store.
|
|
451
|
+
yield* transcriptStore.readSpan(vatID, startPos);
|
|
452
|
+
} catch (_err) {}
|
|
453
|
+
// Read from the overlay, assuming that any transcripts of vatID
|
|
454
|
+
// are for the current span.
|
|
455
|
+
const pendingItems = pendingItemsByVat.get(vatID) || [];
|
|
456
|
+
for (const { item, startPos: itemStartPos } of pendingItems) {
|
|
457
|
+
if (startPos !== undefined && itemStartPos !== startPos) break;
|
|
458
|
+
yield item;
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
return reader();
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
return wrapStore('transcriptStore', transcriptStoreOverride, {
|
|
465
|
+
...wrapHelpers,
|
|
466
|
+
logAndMark: [
|
|
467
|
+
'initTranscript',
|
|
468
|
+
'rolloverSpan',
|
|
469
|
+
'rolloverIncarnation',
|
|
470
|
+
'stopUsingTranscript',
|
|
471
|
+
['deleteVatTranscripts', () => harden({ done: true, cleanups: 0 })],
|
|
472
|
+
],
|
|
473
|
+
warnIfStale: ['addItem', 'getCurrentSpanBounds', 'readSpan'],
|
|
474
|
+
allowIfClean: [
|
|
475
|
+
...storeExportAPI,
|
|
476
|
+
'exportSpan',
|
|
477
|
+
'dumpTranscripts',
|
|
478
|
+
'readFullVatTranscript',
|
|
479
|
+
],
|
|
480
|
+
disallow: [
|
|
481
|
+
'importTranscriptSpanRecord',
|
|
482
|
+
'populateTranscriptSpan',
|
|
483
|
+
'assertComplete',
|
|
484
|
+
'repairTranscriptSpanRecord',
|
|
485
|
+
],
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
wrapSnapStore: snapStore => {
|
|
489
|
+
const wrapHelpers = makeWrapHelpers();
|
|
490
|
+
/** @type {ReturnType<import('@agoric/swing-store').makeSnapStore>} */
|
|
491
|
+
const snapStoreOverride = {
|
|
492
|
+
...snapStore,
|
|
493
|
+
saveSnapshot: async (vatID, snapPos, dataStream) => {
|
|
494
|
+
const entryPrefix = ['snapStore', 'saveSnapshot', vatID, snapPos];
|
|
495
|
+
wrapHelpers.markStale(vatID);
|
|
496
|
+
await null;
|
|
497
|
+
let size = 0;
|
|
498
|
+
try {
|
|
499
|
+
for await (const chunk of dataStream) size += chunk.length;
|
|
500
|
+
recordCall(...entryPrefix, `<${size} bytes>`);
|
|
501
|
+
} catch (err) {
|
|
502
|
+
recordCall(...entryPrefix, `<error after ${size} bytes>`);
|
|
503
|
+
throw err;
|
|
504
|
+
}
|
|
505
|
+
return /** @type {import('@agoric/swing-store').SnapshotResult} */ (
|
|
506
|
+
harden({ uncompressedSize: size })
|
|
507
|
+
);
|
|
508
|
+
},
|
|
509
|
+
};
|
|
510
|
+
return wrapStore('snapStore', snapStoreOverride, {
|
|
511
|
+
...wrapHelpers,
|
|
512
|
+
allow: ['saveSnapshot'],
|
|
513
|
+
logAndMark: [
|
|
514
|
+
['deleteVatSnapshots', () => harden({ done: true, cleanups: 0 })],
|
|
515
|
+
'stopUsingLastSnapshot',
|
|
516
|
+
],
|
|
517
|
+
warnIfStale: ['loadSnapshot', 'getSnapshotInfo', 'hasHash'],
|
|
518
|
+
allowIfClean: [
|
|
519
|
+
...storeExportAPI,
|
|
520
|
+
'exportSnapshot',
|
|
521
|
+
'listAllSnapshots',
|
|
522
|
+
'dumpSnapshots',
|
|
523
|
+
],
|
|
524
|
+
disallow: [
|
|
525
|
+
'deleteAllUnusedSnapshots',
|
|
526
|
+
'importSnapshotRecord',
|
|
527
|
+
'populateSnapshot',
|
|
528
|
+
'assertComplete',
|
|
529
|
+
'repairSnapshotRecord',
|
|
530
|
+
'deleteSnapshotByHash',
|
|
531
|
+
],
|
|
532
|
+
});
|
|
533
|
+
},
|
|
534
|
+
wrapBundleStore: bundleStore => {
|
|
535
|
+
const overlayDB = sqlite3(':memory:');
|
|
536
|
+
const overlay = makeBundleStore(overlayDB, noop, noop);
|
|
537
|
+
let modified = false;
|
|
538
|
+
const onNewBundle = (operation, key, ...details) => {
|
|
539
|
+
modified = true;
|
|
540
|
+
recordCall('bundleStore', operation, key, ...details);
|
|
541
|
+
const bundleID = bundleIDFromName(key);
|
|
542
|
+
!bundleStore.hasBundle(bundleID) ||
|
|
543
|
+
Fail`base bundleStore already has ${bundleID}`;
|
|
544
|
+
};
|
|
545
|
+
/** @type {ReturnType<import('@agoric/swing-store').makeBundleStore>} */
|
|
546
|
+
const bundleStoreOverride = {
|
|
547
|
+
...bundleStore,
|
|
548
|
+
// writes
|
|
549
|
+
importBundleRecord: (key, value) => {
|
|
550
|
+
onNewBundle('importBundleRecord', key, value);
|
|
551
|
+
return overlay.importBundleRecord(key, value);
|
|
552
|
+
},
|
|
553
|
+
importBundle: async (name, dataProvider) => {
|
|
554
|
+
const data = await dataProvider();
|
|
555
|
+
onNewBundle('importBundle', name, `<${data.length} bytes>`);
|
|
556
|
+
return overlay.importBundle(name, () => Promise.resolve(data));
|
|
557
|
+
},
|
|
558
|
+
addBundle: (bundleID, bundle) => {
|
|
559
|
+
onNewBundle('addBundle', `bundle.${bundleID}`, bundle.moduleFormat);
|
|
560
|
+
return overlay.addBundle(bundleID, bundle);
|
|
561
|
+
},
|
|
562
|
+
deleteBundle: bundleID => {
|
|
563
|
+
modified = true;
|
|
564
|
+
recordCall('bundleStore', 'deleteBundle', bundleID);
|
|
565
|
+
if (overlay.hasBundle(bundleID)) overlay.deleteBundle(bundleID);
|
|
566
|
+
},
|
|
567
|
+
// reads
|
|
568
|
+
hasBundle: bundleID =>
|
|
569
|
+
overlay.hasBundle(bundleID) || bundleStore.hasBundle(bundleID),
|
|
570
|
+
getBundle: bundleID => {
|
|
571
|
+
if (overlay.hasBundle(bundleID)) return overlay.getBundle(bundleID);
|
|
572
|
+
return bundleStore.getBundle(bundleID);
|
|
573
|
+
},
|
|
574
|
+
};
|
|
575
|
+
return wrapStore('bundleStore', bundleStoreOverride, {
|
|
576
|
+
log: recordCall,
|
|
577
|
+
isClean: () => !modified,
|
|
578
|
+
allow: [
|
|
579
|
+
'importBundleRecord',
|
|
580
|
+
'importBundle',
|
|
581
|
+
'addBundle',
|
|
582
|
+
'hasBundle',
|
|
583
|
+
'getBundle',
|
|
584
|
+
'deleteBundle',
|
|
585
|
+
],
|
|
586
|
+
allowIfClean: [
|
|
587
|
+
...storeExportAPI,
|
|
588
|
+
'exportBundle',
|
|
589
|
+
'getBundleIDs',
|
|
590
|
+
'dumpBundles',
|
|
591
|
+
],
|
|
592
|
+
disallow: ['assertComplete', 'repairBundleRecord'],
|
|
593
|
+
});
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
return { swingStore, mutations };
|
|
598
|
+
};
|
|
599
|
+
harden(makeSwingStoreOverlay);
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Load a swing-store database for either REPL or scripted interactions.
|
|
603
|
+
*
|
|
604
|
+
* @param {[swingstoreDbPath: string]} argv
|
|
605
|
+
* @param {{ interactive?: boolean, historyFile?: string }} [options]
|
|
606
|
+
* @param {{ console?: typeof globalThis.console, process?: typeof globalThis.process }} [powers]
|
|
607
|
+
*/
|
|
608
|
+
const main = async (argv, options = {}, powers = {}) => {
|
|
609
|
+
const { interactive, historyFile } = options;
|
|
610
|
+
const { console = globalThis.console, process = globalThis.process } = powers;
|
|
611
|
+
const { env } = process;
|
|
612
|
+
const maxVatsOnline = parseNumber(env.INQUISITOR_MAX_VATS_ONLINE || '3');
|
|
613
|
+
|
|
614
|
+
const { swingStore, mutations } = makeSwingStoreOverlay(argv[0]);
|
|
615
|
+
const { db, kvStore } = swingStore.internal;
|
|
616
|
+
const fakeStorageKit = makeFakeStorageKit('');
|
|
617
|
+
const { toStorage: handleVstorage } = fakeStorageKit;
|
|
618
|
+
const receiveBridgeSend = (destPort, msg) => {
|
|
619
|
+
console.log('[bridge] received', msg);
|
|
620
|
+
switch (destPort) {
|
|
621
|
+
case BridgeId.STORAGE: {
|
|
622
|
+
return handleVstorage(msg);
|
|
623
|
+
}
|
|
624
|
+
default:
|
|
625
|
+
Fail`[inquisitor] bridge port ${q(destPort)} not implemented for message ${msg}`;
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
const config = {
|
|
629
|
+
swingsetConfig: { maxVatsOnline },
|
|
630
|
+
swingStore,
|
|
631
|
+
/** @type {Partial<SwingSetConfig>} */
|
|
632
|
+
configOverrides: {
|
|
633
|
+
// Default to XS workers with no GC or snapshots.
|
|
634
|
+
defaultManagerType: 'xsnap',
|
|
635
|
+
defaultReapGCKrefs: 'never',
|
|
636
|
+
defaultReapInterval: 'never',
|
|
637
|
+
snapshotInterval: Number.MAX_VALUE,
|
|
638
|
+
},
|
|
639
|
+
fixupInitMessage: msg => ({
|
|
640
|
+
...msg,
|
|
641
|
+
blockHeight: Number(swingStore.hostStorage.kvStore.get('host.height')),
|
|
642
|
+
blockTime: Math.floor(Date.now() / 1000 - 60),
|
|
643
|
+
// Default to no cleanup for terminated vats.
|
|
644
|
+
params: {
|
|
645
|
+
...DEFAULT_SIM_SWINGSET_PARAMS,
|
|
646
|
+
...msg.params,
|
|
647
|
+
vat_cleanup_budget: makeVatCleanupBudgetFromKeywords({ Default: 0 }),
|
|
648
|
+
},
|
|
649
|
+
}),
|
|
650
|
+
};
|
|
651
|
+
const testKit = await makeCosmicSwingsetTestKit(receiveBridgeSend, config);
|
|
652
|
+
|
|
653
|
+
const {
|
|
654
|
+
EV,
|
|
655
|
+
controller,
|
|
656
|
+
shutdown,
|
|
657
|
+
getLastBlockInfo,
|
|
658
|
+
pushQueueRecord,
|
|
659
|
+
pushCoreEval,
|
|
660
|
+
runNextBlock,
|
|
661
|
+
} = testKit;
|
|
662
|
+
const helpers = makeHelpers({ db, EV });
|
|
663
|
+
const endowments = {
|
|
664
|
+
// Raw access to overlay data.
|
|
665
|
+
...{ kvStore: provideEnhancedKVStore(kvStore), swingStore },
|
|
666
|
+
// Block interactions
|
|
667
|
+
...{ getLastBlockInfo, pushQueueRecord, pushCoreEval, runNextBlock },
|
|
668
|
+
// Vat interactions.
|
|
669
|
+
...{ EV, controller, krefOf, kser, kslot, kunser },
|
|
670
|
+
// Inquisitor API.
|
|
671
|
+
...{ mutations, ...helpers },
|
|
672
|
+
};
|
|
673
|
+
const contextDescriptors = objectMap(
|
|
674
|
+
{ console, endowments, ...endowments, shutdown },
|
|
675
|
+
(value, name) => {
|
|
676
|
+
// For final cleanup, `shutdown` must be preserved.
|
|
677
|
+
if (name === 'shutdown') {
|
|
678
|
+
return { ...dataProp, value, writable: false, configurable: false };
|
|
679
|
+
}
|
|
680
|
+
return { ...dataProp, value };
|
|
681
|
+
},
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
if (!interactive) {
|
|
685
|
+
Object.defineProperties(globalThis, contextDescriptors);
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const truthyKeys = obj =>
|
|
690
|
+
Object.entries(obj).flatMap(([key, value]) => (value ? [key] : []));
|
|
691
|
+
console.warn('endowments:', ...truthyKeys(endowments));
|
|
692
|
+
console.warn('endowments.stable:', ...truthyKeys(endowments.stable));
|
|
693
|
+
const replServer = repl.start({
|
|
694
|
+
useGlobal: true,
|
|
695
|
+
// @ts-expect-error TS2322 REPLWriter really is allowed to return an Error
|
|
696
|
+
writer: value => {
|
|
697
|
+
if (value instanceof Error) {
|
|
698
|
+
// Use the SES console.
|
|
699
|
+
console.error(value);
|
|
700
|
+
return Object.defineProperty(Error(value.message), 'name', {
|
|
701
|
+
value: value.name,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
return inspect(value, { colors: useColors, depth: inspectDepth });
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
if (historyFile) replServer.setupHistory(historyFile, _err => {});
|
|
708
|
+
Object.defineProperties(replServer.context, contextDescriptors);
|
|
709
|
+
const cleanup = () => shutdown().catch(noop);
|
|
710
|
+
replServer.on('exit', cleanup);
|
|
711
|
+
process.on('beforeExit', cleanup);
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
// Check for CLI invocation.
|
|
715
|
+
const isImport =
|
|
716
|
+
fs.realpathSync(process.argv[1]) !== fileURLToPath(import.meta.url);
|
|
717
|
+
const isCLIEntryPoint = !isImport && !process.send && isMainThread !== false;
|
|
718
|
+
const interactive = process.stdin.isTTY && !process.env.INQUISITOR_NO_REPL;
|
|
719
|
+
if (isCLIEntryPoint && !interactive) {
|
|
720
|
+
// When directly invoked with non-interactive stdin, defer to a child process
|
|
721
|
+
// that will read stdin as module statements in the global environment with
|
|
722
|
+
// `EV`/`controller`/`kvStore`/etc.
|
|
723
|
+
const args = [
|
|
724
|
+
'--input-type=module',
|
|
725
|
+
...['--import', process.argv[1]],
|
|
726
|
+
'',
|
|
727
|
+
...process.argv.slice(2),
|
|
728
|
+
];
|
|
729
|
+
const child = spawn(process.argv[0], args, {
|
|
730
|
+
env: { ...process.env, INQUISITOR_NO_REPL: '1' },
|
|
731
|
+
stdio: ['pipe', 'inherit', 'inherit', 'ipc'],
|
|
732
|
+
});
|
|
733
|
+
const { promise: childDoneP, resolve: finishChild } = makePromiseKit();
|
|
734
|
+
child.on('error', error => setImmediate(finishChild, { error }));
|
|
735
|
+
child.on('exit', (code, signal) => finishChild({ code, signal }));
|
|
736
|
+
void childDoneP.then(resolveImmediate).then(async result => {
|
|
737
|
+
await null;
|
|
738
|
+
if (result?.signal) {
|
|
739
|
+
process.kill(process.pid, result.signal);
|
|
740
|
+
await delay(100);
|
|
741
|
+
}
|
|
742
|
+
if (typeof result?.code === 'number') process.exit(result.code);
|
|
743
|
+
const { error } = result;
|
|
744
|
+
console.error(error);
|
|
745
|
+
process.exit(error.code || 1);
|
|
746
|
+
});
|
|
747
|
+
const childInput = child.stdin;
|
|
748
|
+
if (!childInput) throw Fail`[inquisitor] child must have stdin`;
|
|
749
|
+
process.stdin.pipe(childInput, { end: false });
|
|
750
|
+
process.stdin.on('end', () => {
|
|
751
|
+
const cleanup = `\n; try { await shutdown(); } catch (_err) {}`;
|
|
752
|
+
stream.Readable.from([cleanup]).pipe(childInput);
|
|
753
|
+
});
|
|
754
|
+
} else if (isCLIEntryPoint || process.env.INQUISITOR_NO_REPL) {
|
|
755
|
+
// When directly invoked with interactive stdin OR as a worker above, parse
|
|
756
|
+
// CLI arguments and use `main` to setup the environment for either a REPL or
|
|
757
|
+
// evaluating stdin as module statements (respectively).
|
|
758
|
+
const homedir = os.homedir();
|
|
759
|
+
const defaultHistFile = pathlib.join(
|
|
760
|
+
homedir,
|
|
761
|
+
'.agoric_inquisitor_repl_history',
|
|
762
|
+
);
|
|
763
|
+
/** @typedef {{type: 'string' | 'boolean', short?: string, multiple?: boolean, default?: string | boolean | string[] | boolean[]}} ParseArgsOptionConfig */
|
|
764
|
+
/** @type {Record<string, ParseArgsOptionConfig>} */
|
|
765
|
+
const cliOptions = {
|
|
766
|
+
help: { type: 'boolean' },
|
|
767
|
+
'history-file': {
|
|
768
|
+
type: 'string',
|
|
769
|
+
default: defaultHistFile,
|
|
770
|
+
},
|
|
771
|
+
};
|
|
772
|
+
const { values: options, positionals: args } = parseArgs({
|
|
773
|
+
options: cliOptions,
|
|
774
|
+
allowPositionals: true,
|
|
775
|
+
});
|
|
776
|
+
try {
|
|
777
|
+
if (options.help) throw Error();
|
|
778
|
+
args.length >= 1 || Fail`missing swingstore.sqlite`;
|
|
779
|
+
args.length === 1 || Fail`extra arguments`;
|
|
780
|
+
} catch (err) {
|
|
781
|
+
const log = options.help ? console.log : console.error;
|
|
782
|
+
if (!options.help) log(`Error: ${err.message}`);
|
|
783
|
+
const self = pathlib.relative(process.cwd(), process.argv[1]);
|
|
784
|
+
log(`Usage: ${self} swingstore.sqlite \\
|
|
785
|
+
[--history-file PATH (default ${cliOptions['history-file'].default})]
|
|
786
|
+
|
|
787
|
+
Loads an ephemeral environment in which one or more vats may be probed
|
|
788
|
+
via \`EV\`/\`controller\`/\`kvStore\`/\`mutations\`/etc. without persisting changes.
|
|
789
|
+
May be used interactively, or as a recipient of piped commands, or as a module.
|
|
790
|
+
Example commands:
|
|
791
|
+
* stable.db.prepare("SELECT name FROM sqlite_schema WHERE type='table'").pluck().all();
|
|
792
|
+
* stable.db.pragma("table_info(transcriptSpans)");
|
|
793
|
+
* [vatAdminNodeRow] = stable.db.kvGlob('v2.vs.*', '*v100*');
|
|
794
|
+
* stable.getRefs('o+10', 'v1');
|
|
795
|
+
* board = await EV.vat('bootstrap').consumeItem('board');
|
|
796
|
+
* obj = await EV(board).getValue('board02963');
|
|
797
|
+
* await runCoreEval(\`async powers => {
|
|
798
|
+
const ref = await E.get(powers.consume.auctioneerKit).governorAdminFacet;
|
|
799
|
+
console.log(ref);
|
|
800
|
+
powers.produce.ref.resolve(ref);
|
|
801
|
+
}\`);
|
|
802
|
+
* (await EV.vat('bootstrap').consumeItem('ref')).getKref()
|
|
803
|
+
|
|
804
|
+
ENVIRONMENT VARIABLES
|
|
805
|
+
CONSOLE_INSPECT_DEPTH
|
|
806
|
+
The number of times to recurse while formatting an object (default 6).
|
|
807
|
+
INQUISITOR_MAX_VATS_ONLINE
|
|
808
|
+
The maximum number of vats to have in memory at any given time (default 3).`);
|
|
809
|
+
process.exit(64);
|
|
810
|
+
}
|
|
811
|
+
const camelizedOptions = Object.fromEntries(
|
|
812
|
+
Object.entries(options).map(([name, value]) => [
|
|
813
|
+
name.replaceAll(/-([a-z])/g, (_, letter) => `${letter.toUpperCase()}`),
|
|
814
|
+
value,
|
|
815
|
+
]),
|
|
816
|
+
);
|
|
817
|
+
// eslint-disable-next-line @jessie.js/safe-await-separator
|
|
818
|
+
await main(/** @type {[string]} */ (args), {
|
|
819
|
+
...camelizedOptions,
|
|
820
|
+
interactive,
|
|
821
|
+
}).catch(err => {
|
|
822
|
+
console.error(err);
|
|
823
|
+
process.exit(err.code || 1);
|
|
824
|
+
});
|
|
825
|
+
}
|
package/tools/test-kit.js
CHANGED
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
} from '@agoric/internal/src/action-types.js';
|
|
14
14
|
import * as STORAGE_PATH from '@agoric/internal/src/chain-storage-paths.js';
|
|
15
15
|
import { deepCopyJsonable } from '@agoric/internal/src/js-utils.js';
|
|
16
|
+
import { makeRunUtils } from '@agoric/swingset-vat/tools/run-utils.js';
|
|
16
17
|
import { initSwingStore } from '@agoric/swing-store';
|
|
17
|
-
|
|
18
18
|
import {
|
|
19
19
|
extractPortNums,
|
|
20
20
|
makeLaunchChain,
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { DEFAULT_SIM_SWINGSET_PARAMS } from '../src/sim-params.js';
|
|
24
24
|
import { makeQueue } from '../src/helpers/make-queue.js';
|
|
25
25
|
|
|
26
|
+
/** @import {EReturn} from '@endo/far'; */
|
|
26
27
|
/** @import { BlockInfo, InitMsg } from '@agoric/internal/src/chain-utils.js' */
|
|
27
28
|
/** @import { ManagerType, SwingSetConfig } from '@agoric/swingset-vat' */
|
|
28
29
|
/** @import { InboundQueue } from '../src/launch-chain.js'; */
|
|
@@ -252,13 +253,25 @@ export const makeCosmicSwingsetTestKit = async (
|
|
|
252
253
|
env,
|
|
253
254
|
fs,
|
|
254
255
|
path: nativePath,
|
|
255
|
-
testingOverrides: {
|
|
256
|
+
testingOverrides: {
|
|
257
|
+
debugName,
|
|
258
|
+
slogSender,
|
|
259
|
+
swingStore,
|
|
260
|
+
vatconfig: config,
|
|
261
|
+
withInternals: true,
|
|
262
|
+
},
|
|
256
263
|
});
|
|
257
264
|
const launchResult = await launchChain({
|
|
258
265
|
...initMessage,
|
|
259
266
|
resolvedConfig: swingsetConfig,
|
|
260
267
|
});
|
|
261
|
-
const {
|
|
268
|
+
const {
|
|
269
|
+
blockingSend,
|
|
270
|
+
shutdown: shutdownKernel,
|
|
271
|
+
internals,
|
|
272
|
+
} = /** @type {EReturn<import('../src/launch-chain.js').launchAndShareInternals>} */ (
|
|
273
|
+
launchResult
|
|
274
|
+
);
|
|
262
275
|
/** @type {(options?: { kernelOnly?: boolean }) => Promise<void>} */
|
|
263
276
|
const shutdown = async ({ kernelOnly = false } = {}) => {
|
|
264
277
|
await shutdownKernel();
|
|
@@ -266,6 +279,8 @@ export const makeCosmicSwingsetTestKit = async (
|
|
|
266
279
|
await hostStorage.close();
|
|
267
280
|
await cleanupDB();
|
|
268
281
|
};
|
|
282
|
+
const { controller, bridgeInbound, timer } = internals;
|
|
283
|
+
const { queueAndRun, EV } = makeRunUtils(controller);
|
|
269
284
|
|
|
270
285
|
// Remember information about the current block, starting with the init
|
|
271
286
|
// message.
|
|
@@ -388,6 +403,13 @@ export const makeCosmicSwingsetTestKit = async (
|
|
|
388
403
|
shutdown,
|
|
389
404
|
swingStore,
|
|
390
405
|
|
|
406
|
+
// Controller-oriented helpers.
|
|
407
|
+
controller,
|
|
408
|
+
bridgeInbound,
|
|
409
|
+
timer,
|
|
410
|
+
queueAndRun,
|
|
411
|
+
EV,
|
|
412
|
+
|
|
391
413
|
// Functions specific to this kit.
|
|
392
414
|
getLastBlockInfo,
|
|
393
415
|
pushQueueRecord,
|