-
Notifications
You must be signed in to change notification settings - Fork 237
/
Copy pathcontroller.js
346 lines (304 loc) · 11.7 KB
/
controller.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
/* global Compartment harden */
import fs from 'fs';
import path from 'path';
import re2 from 're2';
import { assert } from '@agoric/assert';
import { isTamed, tameMetering } from '@agoric/tame-metering';
import bundleSource from '@agoric/bundle-source';
import { importBundle } from '@agoric/import-bundle';
import { initSwingStore } from '@agoric/swing-store-simple';
import { HandledPromise } from '@agoric/eventual-send';
import { makeMeteringTransformer } from '@agoric/transform-metering';
import * as babelCore from '@babel/core';
import { makeTransform } from '@agoric/transform-eventual-send';
import * as babelParser from '@agoric/babel-parser';
import babelGenerate from '@babel/generator';
import anylogger from 'anylogger';
import { waitUntilQuiescent } from './waitUntilQuiescent';
import { insistStorageAPI } from './storageAPI';
import { insistCapData } from './capdata';
import { parseVatSlot } from './parseVatSlots';
function makeConsole(tag) {
const log = anylogger(tag);
const cons = {};
for (const level of ['debug', 'log', 'info', 'warn', 'error']) {
cons[level] = log[level];
}
return harden(cons);
}
const console = makeConsole('SwingSet:controller');
// FIXME: Put this somewhere better.
process.on('unhandledRejection', e =>
console.error('UnhandledPromiseRejectionWarning:', e),
);
const ADMIN_DEVICE_PATH = require.resolve('./kernel/vatAdmin/vatAdmin-src');
const ADMIN_VAT_PATH = require.resolve('./kernel/vatAdmin/vatAdminWrapper');
const KERNEL_SOURCE_PATH = require.resolve('./kernel/kernel.js');
function byName(a, b) {
if (a.name < b.name) {
return -1;
}
if (a.name > b.name) {
return 1;
}
return 0;
}
/**
* Scan a directory for files defining the vats to bootstrap for a swingset.
* Looks for files with names of the pattern `vat-NAME.js` as well as a file
* named 'bootstrap.js'.
*
* @param basedir The directory to scan
*
* @return an object {
* vats, // map from NAME to the full path to the corresponding .js file
* bootstrapIndexJS, // path to the bootstrap.js file, or undefined if none
* }
*
* TODO: bootstrapIndexJS is a terrible name. Rename to something like
* bootstrapSourcePath (renaming mildly complicated because it's referenced in
* lots of places).
*/
export function loadBasedir(basedir) {
console.debug(`= loading config from basedir ${basedir}`);
const vats = new Map(); // name -> { sourcepath, options }
const subs = fs.readdirSync(basedir, { withFileTypes: true });
subs.sort(byName);
subs.forEach(dirent => {
if (dirent.name.endsWith('~')) {
// Special case crap filter to ignore emacs backup files and the like.
// Note that the regular filename parsing below will ignore such files
// anyway, but this skips logging them so as to reduce log spam.
return;
}
if (
dirent.name.startsWith('vat-') &&
dirent.isFile() &&
dirent.name.endsWith('.js')
) {
const name = dirent.name.slice('vat-'.length, -'.js'.length);
const vatSourcePath = path.resolve(basedir, dirent.name);
vats.set(name, { sourcepath: vatSourcePath, options: {} });
} else {
console.debug('ignoring ', dirent.name);
}
});
let bootstrapIndexJS = path.resolve(basedir, 'bootstrap.js');
try {
fs.statSync(bootstrapIndexJS);
} catch (e) {
// TODO this will catch the case of the file not existing but doesn't check
// that it's a plain file and not a directory or something else unreadable.
// Consider putting in a more sophisticated check if this whole directory
// scanning thing is something we decide we want to have long term.
bootstrapIndexJS = undefined;
}
return { vats, bootstrapIndexJS };
}
export async function buildVatController(config, argv = []) {
if (typeof Compartment === 'undefined') {
throw Error('SES must be installed before calling buildVatController');
}
// todo: move argv into the config
// https://github.com/Agoric/SES-shim/issues/292
harden(Object.getPrototypeOf(console));
harden(console);
function kernelRequire(what) {
if (what === '@agoric/harden') {
return harden;
} else if (what === 're2') {
// The kernel imports @agoric/transform-metering to get makeMeter(),
// and transform-metering imports re2, to add it to the generated
// endowments. TODO Our transformers no longer traffic in endowments,
// so that could probably be removed, in which case we'd no longer need
// to provide it here. We should decide whether to let the kernel use
// the native RegExp or replace it with re2. TODO we also need to make
// sure vats get (and stick with) re2 for their 'RegExp'.
return re2;
} else {
throw Error(`kernelRequire unprepared to satisfy require(${what})`);
}
}
const kernelSource = await bundleSource(KERNEL_SOURCE_PATH);
const kernelNS = await importBundle(kernelSource, {
filePrefix: 'kernel',
endowments: {
console: makeConsole('SwingSet:kernel'),
require: kernelRequire,
HandledPromise,
},
});
const buildKernel = kernelNS.default;
// transformMetering() requires Babel, which imports 'fs' and 'path', so it
// cannot be implemented within a non-start-Compartment. We build it out
// here and pass it to the kernel, which then passes it to vats. This is
// intended to be powerless. TODO: when we remove metering within vats
// (leaving only vat-at-a-time metering), this function should only be used
// to build loadStaticVat and loadDynamicVat. It may still be passed to the
// kernel (for loadDynamicVat), but it should no longer be passed into the
// vats themselves. TODO: transformMetering() is sync because it is passed
// into c.evaluate (which of course cannot handle async), but in the
// future, this may live on the far side of a kernel/vatworker boundary, so
// we kind of want it to be async.
const mt = makeMeteringTransformer(babelCore);
function transformMetering(src, getMeter) {
// 'getMeter' provides the meter to which the transformation itself is
// billed (the COMPUTE meter is charged the length of the source string).
// The endowment must be present and truthy, otherwise the transformation
// is disabled. TODO: rethink that, and have @agoric/transform-metering
// export a simpler function (without 'endowments' or .rewrite).
const ss = mt.rewrite({ src, endowments: { getMeter } });
return ss.src;
}
harden(transformMetering);
// the same is true for the tildot transform
const transformTildot = harden(makeTransform(babelParser, babelGenerate));
// Allow vats to import certain modules, by providing them the same values
// we imported here in the kernel, which came themselves from
// kernelRequire() defined in the controller. This will go away once
// 'harden' is used as a global everywhere, and vats no longer need to
// import anything outside their bundle.
function vatRequire(what) {
throw Error(`vatRequire unprepared to satisfy require(${what})`);
}
function makeVatEndowments(consoleTag) {
return harden({
require: vatRequire,
console: makeConsole(`SwingSet:${consoleTag}`),
HandledPromise,
// re2 is a RegExp work-a-like that disables backtracking expressions for
// safer memory consumption
RegExp: re2,
});
}
async function loadStaticVat(sourceIndex, name) {
if (!(sourceIndex[0] === '.' || path.isAbsolute(sourceIndex))) {
throw Error(
'sourceIndex must be relative (./foo) or absolute (/foo) not bare (foo)',
);
}
const bundle = await bundleSource(sourceIndex);
const vatNS = await importBundle(bundle, {
filePrefix: name,
endowments: makeVatEndowments(name),
});
let setup;
if ('buildRootObject' in vatNS) {
setup = (syscall, state, helpers, vatPowers) => {
return helpers.makeLiveSlots(
syscall,
state,
vatP => vatNS.buildRootObject(vatP),
helpers.vatID,
vatPowers,
);
};
} else {
setup = vatNS.default;
}
return setup;
}
const hostStorage = config.hostStorage || initSwingStore().storage;
insistStorageAPI(hostStorage);
// It is important that tameMetering() was called by application startup,
// before install-ses. Rather than ask applications to capture the return
// value and pass it all the way through to here, we just run
// tameMetering() again (and rely upon its only-once behavior) to get the
// control facet (replaceGlobalMeter), and pass it in through
// kernelEndowments. If our enclosing application decided to not tame the
// globals, we detect that and refrain from touching it later.
const replaceGlobalMeter = isTamed() ? tameMetering() : undefined;
console.log(
`SwingSet global metering is ${
isTamed() ? 'enabled' : 'disabled (no replaceGlobalMeter)'
}`,
);
// we give the kernel a meteringTransform function, so it can offer it to
const kernelEndowments = {
waitUntilQuiescent,
hostStorage,
makeVatEndowments,
vatAdminDevSetup: await loadStaticVat(ADMIN_DEVICE_PATH, 'dev-vatAdmin'),
vatAdminVatSetup: await loadStaticVat(ADMIN_VAT_PATH, 'vat-vatAdmin'),
replaceGlobalMeter,
transformMetering,
transformTildot,
};
const kernel = buildKernel(kernelEndowments);
if (config.verbose) {
kernel.kdebugEnable(true);
}
async function addGenesisVat(name, sourceIndex, options = {}) {
console.debug(`= adding vat '${name}' from ${sourceIndex}`);
const setup = await loadStaticVat(sourceIndex, `vat-${name}`);
kernel.addGenesisVat(name, setup, options);
}
async function addGenesisDevice(name, sourceIndex, endowments) {
const setup = await loadStaticVat(sourceIndex, `dev-${name}`);
kernel.addGenesisDevice(name, setup, endowments);
}
if (config.devices) {
for (const [name, srcpath, endowments] of config.devices) {
// eslint-disable-next-line no-await-in-loop
await addGenesisDevice(name, srcpath, endowments);
}
}
if (config.vats) {
for (const name of config.vats.keys()) {
const v = config.vats.get(name);
// eslint-disable-next-line no-await-in-loop
await addGenesisVat(name, v.sourcepath, v.options || {});
}
}
let bootstrapVatName;
if (config.bootstrapIndexJS) {
bootstrapVatName = '_bootstrap';
await addGenesisVat(bootstrapVatName, config.bootstrapIndexJS, {});
}
// start() may queue bootstrap if state doesn't say we did it already. It
// also replays the transcripts from a previous run, if any, which will
// execute vat code (but all syscalls will be disabled)
const bootstrapResult = await kernel.start(
bootstrapVatName,
JSON.stringify(argv),
);
// the kernel won't leak our objects into the Vats, we must do
// the same in this wrapper
const controller = harden({
log(str) {
kernel.log(str);
},
dump() {
return JSON.parse(JSON.stringify(kernel.dump()));
},
verboseDebugMode(flag) {
kernel.kdebugEnable(flag);
},
async run() {
return kernel.run();
},
async step() {
return kernel.step();
},
getStats() {
return JSON.parse(JSON.stringify(kernel.getStats()));
},
// these are for tests
vatNameToID(vatName) {
return kernel.vatNameToID(vatName);
},
deviceNameToID(deviceName) {
return kernel.deviceNameToID(deviceName);
},
queueToVatExport(vatName, exportID, method, args, resultPolicy = 'ignore') {
const vatID = kernel.vatNameToID(vatName);
parseVatSlot(exportID);
assert.typeof(method, 'string');
insistCapData(args);
kernel.addExport(vatID, exportID);
return kernel.queueToExport(vatID, exportID, method, args, resultPolicy);
},
bootstrapResult,
});
return controller;
}