@agoric/swingset-xsnap-supervisor 0.9.1-dev-6e51de2.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/LICENSE +201 -0
- package/README.md +56 -0
- package/dist/supervisor.bundle +1 -0
- package/dist/supervisor.bundle.sha256 +1 -0
- package/lib/capdata.js +20 -0
- package/lib/entry.js +1 -0
- package/lib/gc-and-finalize.js +91 -0
- package/lib/supervisor-helper.js +169 -0
- package/lib/supervisor-subprocess-xsnap.js +333 -0
- package/lib/waitUntilQuiescent.js +18 -0
- package/package.json +52 -0
- package/src/index.js +31 -0
- package/src/paths.js +15 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
9ed047832309a39356fd383c3e74e8b0973c7230f29b7b02d71a2e3555e4d60f
|
package/lib/capdata.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Fail } from '@agoric/assert';
|
|
2
|
+
|
|
3
|
+
/* eslint-disable jsdoc/require-returns-check */
|
|
4
|
+
/**
|
|
5
|
+
* Assert function to ensure that something expected to be a capdata object
|
|
6
|
+
* actually is. A capdata object should have a .body property that's a string
|
|
7
|
+
* and a .slots property that's an array of strings.
|
|
8
|
+
*
|
|
9
|
+
* @param {any} capdata The object to be tested
|
|
10
|
+
* @throws {Error} if, upon inspection, the parameter does not satisfy the above
|
|
11
|
+
* criteria.
|
|
12
|
+
* @returns {asserts capdata is import('@endo/marshal').CapData<string>}
|
|
13
|
+
*/
|
|
14
|
+
export function insistCapData(capdata) {
|
|
15
|
+
typeof capdata.body === 'string' ||
|
|
16
|
+
Fail`capdata has non-string .body ${capdata.body}`;
|
|
17
|
+
Array.isArray(capdata.slots) ||
|
|
18
|
+
Fail`capdata has non-Array slots ${capdata.slots}`;
|
|
19
|
+
// TODO check that the .slots array elements are actually strings
|
|
20
|
+
}
|
package/lib/entry.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './supervisor-subprocess-xsnap.js';
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/* global setImmediate */
|
|
2
|
+
|
|
3
|
+
/* A note on our GC terminology:
|
|
4
|
+
*
|
|
5
|
+
* We define four states for any JS object to be in:
|
|
6
|
+
*
|
|
7
|
+
* REACHABLE: There exists a path from some root (live export or top-level
|
|
8
|
+
* global) to this object, making it ineligible for collection. Userspace vat
|
|
9
|
+
* code has a strong reference to it (and userspace is not given access to
|
|
10
|
+
* WeakRef, so it has no weak reference that might be used to get access).
|
|
11
|
+
*
|
|
12
|
+
* UNREACHABLE: There is no strong reference from a root to the object.
|
|
13
|
+
* Userspace vat code has no means to access the object, although liveslots
|
|
14
|
+
* might (via a WeakRef). The object is eligible for collection, but that
|
|
15
|
+
* collection has not yet happened. The liveslots WeakRef is still alive: if
|
|
16
|
+
* it were to call `.deref()`, it would get the object.
|
|
17
|
+
*
|
|
18
|
+
* COLLECTED: The JS engine has performed enough GC to notice the
|
|
19
|
+
* unreachability of the object, and has collected it. The liveslots WeakRef
|
|
20
|
+
* is dead: `wr.deref() === undefined`. Neither liveslots nor userspace has
|
|
21
|
+
* any way to reach the object, and never will again. A finalizer callback
|
|
22
|
+
* has been queued, but not yet executed.
|
|
23
|
+
*
|
|
24
|
+
* FINALIZED: The JS engine has run the finalizer callback. Once the
|
|
25
|
+
* callback completes, the object is thoroughly dead and unremembered,
|
|
26
|
+
* and no longer exists in one of these four states.
|
|
27
|
+
*
|
|
28
|
+
* The transition from REACHABLE to UNREACHABLE always happens as a result of
|
|
29
|
+
* a message delivery or resolution notification (e.g when userspace
|
|
30
|
+
* overwrites a variable, deletes a Map entry, or a callback on the promise
|
|
31
|
+
* queue which closed over some objects is retired and deleted).
|
|
32
|
+
*
|
|
33
|
+
* The transition from UNREACHABLE to COLLECTED can happen spontaneously, as
|
|
34
|
+
* the JS engine decides it wants to perform GC. It will also happen
|
|
35
|
+
* deliberately if we provoke a GC call with a magic function like `gc()`
|
|
36
|
+
* (when Node.js imports `engine-gc`, which is morally-equivalent to
|
|
37
|
+
* running with `--expose-gc`, or when XS is configured to provide it as a
|
|
38
|
+
* C-level callback). We can force GC, but we cannot prevent it from happening
|
|
39
|
+
* at other times.
|
|
40
|
+
*
|
|
41
|
+
* FinalizationRegistry callbacks are defined to run on their own turn, so
|
|
42
|
+
* the transition from COLLECTED to FINALIZED occurs at a turn boundary.
|
|
43
|
+
* Node.js appears to schedule these finalizers on the timer/IO queue, not
|
|
44
|
+
* the promise/microtask queue. So under Node.js, you need a `setImmediate()`
|
|
45
|
+
* or two to ensure that finalizers have had a chance to run. XS is different
|
|
46
|
+
* but responds well to similar techniques.
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
* `gcAndFinalize` must be defined in the start compartment. It uses
|
|
51
|
+
* platform-specific features to provide a function which provokes a full GC
|
|
52
|
+
* operation: all "UNREACHABLE" objects should transition to "COLLECTED"
|
|
53
|
+
* before it returns. In addition, `gcAndFinalize()` returns a Promise. This
|
|
54
|
+
* Promise will resolve (with `undefined`) after all FinalizationRegistry
|
|
55
|
+
* callbacks have executed, causing all COLLECTED objects to transition to
|
|
56
|
+
* FINALIZED. If the caller can manage call gcAndFinalize with an empty
|
|
57
|
+
* promise queue, then their .then callback will also start with an empty
|
|
58
|
+
* promise queue, and there will be minimal uncollected unreachable objects
|
|
59
|
+
* in the heap when it begins.
|
|
60
|
+
*
|
|
61
|
+
* `gcAndFinalize` depends upon platform-specific tools to provoke a GC sweep
|
|
62
|
+
* and wait for finalizers to run: a `gc()` function, and `setImmediate`. If
|
|
63
|
+
* these tools do not exist, this function will do nothing, and return a
|
|
64
|
+
* dummy pre-resolved Promise.
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
export function makeGcAndFinalize(gcPower) {
|
|
68
|
+
if (typeof gcPower !== 'function') {
|
|
69
|
+
if (gcPower !== false) {
|
|
70
|
+
// We weren't explicitly disabled, so warn.
|
|
71
|
+
console.warn(
|
|
72
|
+
Error(`no gcPower() function; skipping finalizer provocation`),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return async function gcAndFinalize() {
|
|
77
|
+
if (typeof gcPower !== 'function') {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// on Node.js, GC seems to work better if the promise queue is empty first
|
|
82
|
+
await new Promise(setImmediate);
|
|
83
|
+
// on xsnap, we must do it twice for some reason
|
|
84
|
+
await new Promise(setImmediate);
|
|
85
|
+
gcPower();
|
|
86
|
+
// this gives finalizers a chance to run
|
|
87
|
+
await new Promise(setImmediate);
|
|
88
|
+
// Node.js seems to need another for promises to get cleared out
|
|
89
|
+
await new Promise(setImmediate);
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { assert } from '@agoric/assert';
|
|
2
|
+
import {
|
|
3
|
+
insistVatSyscallObject,
|
|
4
|
+
insistVatSyscallResult,
|
|
5
|
+
} from '@agoric/swingset-liveslots';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {import('@agoric/swingset-liveslots').VatDeliveryObject} VatDeliveryObject
|
|
9
|
+
* @typedef {import('@agoric/swingset-liveslots').VatDeliveryResult} VatDeliveryResult
|
|
10
|
+
* @typedef {import('@agoric/swingset-liveslots').VatSyscallObject} VatSyscallObject
|
|
11
|
+
* @typedef {import('@agoric/swingset-liveslots').VatSyscaller} VatSyscaller
|
|
12
|
+
* @typedef {import('@endo/marshal').CapData<string>} SwingSetCapData
|
|
13
|
+
* @typedef { (delivery: VatDeliveryObject) => (VatDeliveryResult | Promise<VatDeliveryResult>) } VatDispatcherSyncAsync
|
|
14
|
+
* @typedef { (delivery: VatDeliveryObject) => Promise<VatDeliveryResult> } VatDispatcher
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Given the liveslots 'dispatch' function, return a version that never
|
|
19
|
+
* rejects. It will always return a VatDeliveryResult, even if liveslots
|
|
20
|
+
* throws or rejects. All supervisors should wrap the liveslots `dispatch`
|
|
21
|
+
* function with this one, and call it in response to messages from the
|
|
22
|
+
* manager process.
|
|
23
|
+
*
|
|
24
|
+
* @param {VatDispatcherSyncAsync} dispatch
|
|
25
|
+
* @returns {VatDispatcher}
|
|
26
|
+
*/
|
|
27
|
+
function makeSupervisorDispatch(dispatch) {
|
|
28
|
+
/**
|
|
29
|
+
* @param {VatDeliveryObject} delivery
|
|
30
|
+
* @returns {Promise<VatDeliveryResult>}
|
|
31
|
+
*/
|
|
32
|
+
async function dispatchToVat(delivery) {
|
|
33
|
+
// the (low-level) vat is responsible for giving up agency, but we still
|
|
34
|
+
// protect against exceptions
|
|
35
|
+
return Promise.resolve(delivery)
|
|
36
|
+
.then(dispatch)
|
|
37
|
+
.then(
|
|
38
|
+
() => harden(['ok', null, null]),
|
|
39
|
+
err => {
|
|
40
|
+
// TODO react more thoughtfully, maybe terminate the vat
|
|
41
|
+
console.warn(`error during vat dispatch() of ${delivery}`, err);
|
|
42
|
+
return harden(['error', `${err}`, null]);
|
|
43
|
+
},
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return harden(dispatchToVat);
|
|
48
|
+
}
|
|
49
|
+
harden(makeSupervisorDispatch);
|
|
50
|
+
export { makeSupervisorDispatch };
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* This returns a function that is provided to liveslots as the 'syscall'
|
|
54
|
+
* argument: an object with one method per syscall type. These methods return
|
|
55
|
+
* data, or nothing. If the kernel experiences a problem executing the syscall,
|
|
56
|
+
* the method will throw, or the kernel will kill the vat, or both.
|
|
57
|
+
*
|
|
58
|
+
* I should be given a `syscallToManager` function that accepts a
|
|
59
|
+
* VatSyscallObject and (synchronously) returns a VatSyscallResult.
|
|
60
|
+
*
|
|
61
|
+
* @param {VatSyscaller} syscallToManager
|
|
62
|
+
* @param {boolean} workerCanBlock
|
|
63
|
+
* @typedef { unknown } TheSyscallObjectWithMethodsThatLiveslotsWants
|
|
64
|
+
* @returns {TheSyscallObjectWithMethodsThatLiveslotsWants}
|
|
65
|
+
*/
|
|
66
|
+
function makeSupervisorSyscall(syscallToManager, workerCanBlock) {
|
|
67
|
+
function doSyscall(fields) {
|
|
68
|
+
insistVatSyscallObject(fields);
|
|
69
|
+
/** @type { VatSyscallObject } */
|
|
70
|
+
const vso = harden(fields);
|
|
71
|
+
let r;
|
|
72
|
+
try {
|
|
73
|
+
r = syscallToManager(vso);
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.warn(`worker got error during syscall:`, err);
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
if (!workerCanBlock) {
|
|
79
|
+
// we don't expect an answer
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const vsr = r;
|
|
83
|
+
insistVatSyscallResult(vsr);
|
|
84
|
+
const [type, ...rest] = vsr;
|
|
85
|
+
switch (type) {
|
|
86
|
+
case 'ok': {
|
|
87
|
+
const [data] = rest;
|
|
88
|
+
return data;
|
|
89
|
+
}
|
|
90
|
+
case 'error': {
|
|
91
|
+
const [err] = rest;
|
|
92
|
+
throw Error(`syscall.${fields[0]} failed: ${err}`);
|
|
93
|
+
}
|
|
94
|
+
default:
|
|
95
|
+
throw Error(`unknown result type ${type}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// this will be given to liveslots, it should have distinct methods that
|
|
100
|
+
// return immediate results or throw errors
|
|
101
|
+
const syscallForVat = {
|
|
102
|
+
/** @type {(target: string, method: string, args: SwingSetCapData, result?: string) => unknown } */
|
|
103
|
+
send: (target, methargs, result) =>
|
|
104
|
+
doSyscall(['send', target, { methargs, result }]),
|
|
105
|
+
subscribe: vpid => doSyscall(['subscribe', vpid]),
|
|
106
|
+
resolve: resolutions => doSyscall(['resolve', resolutions]),
|
|
107
|
+
exit: (isFailure, data) => doSyscall(['exit', isFailure, data]),
|
|
108
|
+
dropImports: vrefs => doSyscall(['dropImports', vrefs]),
|
|
109
|
+
retireImports: vrefs => doSyscall(['retireImports', vrefs]),
|
|
110
|
+
retireExports: vrefs => doSyscall(['retireExports', vrefs]),
|
|
111
|
+
abandonExports: vrefs => doSyscall(['abandonExports', vrefs]),
|
|
112
|
+
|
|
113
|
+
// These syscalls should be omitted if the worker cannot get a
|
|
114
|
+
// synchronous return value back from the kernel, such as when the worker
|
|
115
|
+
// is in a child process or thread, and cannot be blocked until the
|
|
116
|
+
// result gets back. vatstoreSet and vatstoreDelete are included because
|
|
117
|
+
// vatstoreSet requires a result, and we offer them as a group.
|
|
118
|
+
callNow: (target, method, args) =>
|
|
119
|
+
doSyscall(['callNow', target, method, args]),
|
|
120
|
+
vatstoreGet: key => {
|
|
121
|
+
const result = doSyscall(['vatstoreGet', key]);
|
|
122
|
+
return result === null ? undefined : result;
|
|
123
|
+
},
|
|
124
|
+
vatstoreGetNextKey: priorKey => doSyscall(['vatstoreGetNextKey', priorKey]),
|
|
125
|
+
vatstoreSet: (key, value) => doSyscall(['vatstoreSet', key, value]),
|
|
126
|
+
vatstoreDelete: key => doSyscall(['vatstoreDelete', key]),
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const blocking = [
|
|
130
|
+
'callNow',
|
|
131
|
+
'vatstoreGet',
|
|
132
|
+
'vatstoreGetNextKey',
|
|
133
|
+
'vatstoreSet',
|
|
134
|
+
'vatstoreDelete',
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
if (!workerCanBlock) {
|
|
138
|
+
for (const name of blocking) {
|
|
139
|
+
const err = `this non-blocking worker transport cannot syscall.${name}`;
|
|
140
|
+
syscallForVat[name] = () => assert.fail(err);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return harden(syscallForVat);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
harden(makeSupervisorSyscall);
|
|
148
|
+
export { makeSupervisorSyscall };
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create a vat console from a log stream maker.
|
|
152
|
+
*
|
|
153
|
+
* TODO: consider other methods per SES VirtualConsole.
|
|
154
|
+
* See https://github.com/Agoric/agoric-sdk/issues/2146
|
|
155
|
+
*
|
|
156
|
+
* @param {(level: string) => (...args: any[]) => void} makeLog
|
|
157
|
+
*/
|
|
158
|
+
function makeVatConsole(makeLog) {
|
|
159
|
+
return harden({
|
|
160
|
+
debug: makeLog('debug'),
|
|
161
|
+
log: makeLog('log'),
|
|
162
|
+
info: makeLog('info'),
|
|
163
|
+
warn: makeLog('warn'),
|
|
164
|
+
error: makeLog('error'),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
harden(makeVatConsole);
|
|
169
|
+
export { makeVatConsole };
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/* global globalThis WeakRef FinalizationRegistry */
|
|
2
|
+
import { assert, Fail } from '@agoric/assert';
|
|
3
|
+
import { importBundle } from '@endo/import-bundle';
|
|
4
|
+
import { makeMarshal } from '@endo/marshal';
|
|
5
|
+
import {
|
|
6
|
+
makeLiveSlots,
|
|
7
|
+
insistVatDeliveryObject,
|
|
8
|
+
insistVatSyscallResult,
|
|
9
|
+
} from '@agoric/swingset-liveslots';
|
|
10
|
+
// import '../../types-ambient.js';
|
|
11
|
+
// grumble... waitUntilQuiescent is exported and closes over ambient authority
|
|
12
|
+
import { waitUntilQuiescent } from './waitUntilQuiescent.js';
|
|
13
|
+
import { makeGcAndFinalize } from './gc-and-finalize.js';
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
makeSupervisorDispatch,
|
|
17
|
+
makeSupervisorSyscall,
|
|
18
|
+
makeVatConsole,
|
|
19
|
+
} from './supervisor-helper.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {import('@agoric/swingset-liveslots').VatDeliveryObject} VatDeliveryObject
|
|
23
|
+
* @typedef {import('@agoric/swingset-liveslots').VatDeliveryResult} VatDeliveryResult
|
|
24
|
+
* @typedef {import('@agoric/swingset-liveslots').VatSyscallObject} VatSyscallObject
|
|
25
|
+
* @typedef {import('@agoric/swingset-liveslots').VatSyscallResult} VatSyscallResult
|
|
26
|
+
* @typedef {import('@agoric/swingset-liveslots').VatSyscaller} VatSyscaller
|
|
27
|
+
* @typedef {import('@agoric/swingset-liveslots').LiveSlotsOptions} LiveSlotsOptions
|
|
28
|
+
* @typedef {import('@agoric/swingset-liveslots').MeterControl} MeterControl
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
const encoder = new TextEncoder();
|
|
32
|
+
const decoder = new TextDecoder();
|
|
33
|
+
|
|
34
|
+
// eslint-disable-next-line no-unused-vars
|
|
35
|
+
function workerLog(first, ...args) {
|
|
36
|
+
// eslint-disable-next-line
|
|
37
|
+
// console.log(`---worker: ${first}`, ...args);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
workerLog(`supervisor started`);
|
|
41
|
+
|
|
42
|
+
function makeMeterControl() {
|
|
43
|
+
let meteringDisabled = 0;
|
|
44
|
+
|
|
45
|
+
function isMeteringDisabled() {
|
|
46
|
+
return !!meteringDisabled;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function assertIsMetered(msg) {
|
|
50
|
+
assert(!meteringDisabled, msg);
|
|
51
|
+
}
|
|
52
|
+
function assertNotMetered(msg) {
|
|
53
|
+
assert(!!meteringDisabled, msg);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function runWithoutMetering(thunk) {
|
|
57
|
+
const limit = globalThis.currentMeterLimit();
|
|
58
|
+
const before = globalThis.resetMeter(0, 0);
|
|
59
|
+
meteringDisabled += 1;
|
|
60
|
+
try {
|
|
61
|
+
return thunk();
|
|
62
|
+
} finally {
|
|
63
|
+
globalThis.resetMeter(limit, before);
|
|
64
|
+
meteringDisabled -= 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function runWithoutMeteringAsync(thunk) {
|
|
69
|
+
const limit = globalThis.currentMeterLimit();
|
|
70
|
+
const before = globalThis.resetMeter(0, 0);
|
|
71
|
+
meteringDisabled += 1;
|
|
72
|
+
return Promise.resolve()
|
|
73
|
+
.then(() => thunk())
|
|
74
|
+
.finally(() => {
|
|
75
|
+
globalThis.resetMeter(limit, before);
|
|
76
|
+
meteringDisabled -= 1;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// return a version of f that runs outside metering
|
|
81
|
+
function unmetered(f) {
|
|
82
|
+
function wrapped(...args) {
|
|
83
|
+
return runWithoutMetering(() => f(...args));
|
|
84
|
+
}
|
|
85
|
+
return harden(wrapped);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @type { MeterControl } */
|
|
89
|
+
const meterControl = {
|
|
90
|
+
isMeteringDisabled,
|
|
91
|
+
assertIsMetered,
|
|
92
|
+
assertNotMetered,
|
|
93
|
+
runWithoutMetering,
|
|
94
|
+
runWithoutMeteringAsync,
|
|
95
|
+
unmetered,
|
|
96
|
+
};
|
|
97
|
+
return harden(meterControl);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const meterControl = makeMeterControl();
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Wrap byte-level protocols with tagged array codec.
|
|
104
|
+
*
|
|
105
|
+
* @param {(cmd: ArrayBuffer) => ArrayBuffer} issueCommand as from xsnap
|
|
106
|
+
* @typedef { [unknown, ...unknown[]] } Tagged tagged array
|
|
107
|
+
*/
|
|
108
|
+
function managerPort(issueCommand) {
|
|
109
|
+
/** @type { (item: Tagged) => ArrayBuffer } */
|
|
110
|
+
const encode = item => {
|
|
111
|
+
let txt;
|
|
112
|
+
try {
|
|
113
|
+
txt = JSON.stringify(item);
|
|
114
|
+
} catch (nope) {
|
|
115
|
+
workerLog(nope.message, item);
|
|
116
|
+
throw nope;
|
|
117
|
+
}
|
|
118
|
+
return encoder.encode(txt).buffer;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
/** @type { (msg: ArrayBuffer) => any } */
|
|
122
|
+
const decodeData = msg => JSON.parse(decoder.decode(msg) || 'null');
|
|
123
|
+
|
|
124
|
+
/** @type { (msg: ArrayBuffer) => Tagged } */
|
|
125
|
+
function decode(msg) {
|
|
126
|
+
/** @type { Tagged } */
|
|
127
|
+
const item = decodeData(msg);
|
|
128
|
+
Array.isArray(item) || Fail`expected array`;
|
|
129
|
+
item.length > 0 || Fail`empty array lacks tag`;
|
|
130
|
+
return item;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return harden({
|
|
134
|
+
/** @type { (item: Tagged) => void } */
|
|
135
|
+
send: item => {
|
|
136
|
+
issueCommand(encode(item));
|
|
137
|
+
},
|
|
138
|
+
/** @type { (item: Tagged) => unknown } */
|
|
139
|
+
call: item => decodeData(issueCommand(encode(item))),
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Wrap an async Tagged handler in the xsnap async reporting idiom.
|
|
143
|
+
*
|
|
144
|
+
* @param {(item: Tagged) => Promise<Tagged>} f async Tagged handler
|
|
145
|
+
* @returns {(msg: ArrayBuffer) => Report<ArrayBuffer>} xsnap style handleCommand
|
|
146
|
+
*
|
|
147
|
+
* @typedef { { result?: T } } Report<T> report T when idle
|
|
148
|
+
* @template T
|
|
149
|
+
*/
|
|
150
|
+
handlerFrom(f) {
|
|
151
|
+
const lastResort = encoder.encode(`exception from ${f.name}`).buffer;
|
|
152
|
+
return msg => {
|
|
153
|
+
const report = {};
|
|
154
|
+
f(decode(msg))
|
|
155
|
+
.then(item => {
|
|
156
|
+
workerLog('result', item);
|
|
157
|
+
report.result = encode(item);
|
|
158
|
+
})
|
|
159
|
+
.catch(err => {
|
|
160
|
+
report.result = encode(['err', err.name, err.message]);
|
|
161
|
+
})
|
|
162
|
+
.catch(_err => {
|
|
163
|
+
report.result = lastResort;
|
|
164
|
+
});
|
|
165
|
+
return report;
|
|
166
|
+
};
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// please excuse copy-and-paste from kernel.js
|
|
172
|
+
function abbreviateReplacer(_, arg) {
|
|
173
|
+
if (typeof arg === 'bigint') {
|
|
174
|
+
// since testLog is only for testing, 2^53 is enough.
|
|
175
|
+
// precedent: 32a1dd3
|
|
176
|
+
return Number(arg);
|
|
177
|
+
}
|
|
178
|
+
if (typeof arg === 'string' && arg.length >= 40) {
|
|
179
|
+
// truncate long strings
|
|
180
|
+
return `${arg.slice(0, 15)}...${arg.slice(arg.length - 15)}`;
|
|
181
|
+
}
|
|
182
|
+
return arg;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {ReturnType<typeof managerPort>} port
|
|
187
|
+
*/
|
|
188
|
+
function makeWorker(port) {
|
|
189
|
+
/** @type { ((delivery: VatDeliveryObject) => Promise<VatDeliveryResult>) | null } */
|
|
190
|
+
let dispatch = null;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {unknown} vatID
|
|
194
|
+
* @param {unknown} bundle
|
|
195
|
+
* @param {LiveSlotsOptions} liveSlotsOptions
|
|
196
|
+
* @returns {Promise<Tagged>}
|
|
197
|
+
*/
|
|
198
|
+
async function setBundle(vatID, bundle, liveSlotsOptions) {
|
|
199
|
+
/** @type { VatSyscaller } */
|
|
200
|
+
function syscallToManager(vatSyscallObject) {
|
|
201
|
+
workerLog('doSyscall', vatSyscallObject);
|
|
202
|
+
const result = port.call(['syscall', vatSyscallObject]);
|
|
203
|
+
workerLog(' ... syscall result:', result);
|
|
204
|
+
insistVatSyscallResult(result);
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const syscall = makeSupervisorSyscall(syscallToManager, true);
|
|
209
|
+
|
|
210
|
+
const vatPowers = {
|
|
211
|
+
makeMarshal,
|
|
212
|
+
testLog: (...args) =>
|
|
213
|
+
port.send([
|
|
214
|
+
'testLog',
|
|
215
|
+
...args.map(arg =>
|
|
216
|
+
typeof arg === 'string'
|
|
217
|
+
? arg
|
|
218
|
+
: JSON.stringify(arg, abbreviateReplacer),
|
|
219
|
+
),
|
|
220
|
+
]),
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const gcTools = harden({
|
|
224
|
+
WeakRef,
|
|
225
|
+
FinalizationRegistry,
|
|
226
|
+
waitUntilQuiescent,
|
|
227
|
+
gcAndFinalize: makeGcAndFinalize(globalThis.gc),
|
|
228
|
+
meterControl,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
/** @param {string} source */
|
|
232
|
+
const makeLogMaker = source => {
|
|
233
|
+
/** @param {string} level */
|
|
234
|
+
const makeLog = level => {
|
|
235
|
+
// Capture the `console.log`, etc.'s `printAll` function.
|
|
236
|
+
const printAll = console[level];
|
|
237
|
+
assert.typeof(printAll, 'function');
|
|
238
|
+
const portSendingPrinter = (...args) => {
|
|
239
|
+
port.send(['sourcedConsole', source, level, ...args]);
|
|
240
|
+
};
|
|
241
|
+
return (...args) => {
|
|
242
|
+
// Use the causal console, but output to the port.
|
|
243
|
+
//
|
|
244
|
+
// FIXME: This is a hack until the start compartment can create
|
|
245
|
+
// Console objects that log to a different destination than
|
|
246
|
+
// `globalThis.console`.
|
|
247
|
+
const { print: savePrinter } = globalThis;
|
|
248
|
+
try {
|
|
249
|
+
globalThis.print = portSendingPrinter;
|
|
250
|
+
printAll(...args);
|
|
251
|
+
} finally {
|
|
252
|
+
globalThis.print = savePrinter;
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
return makeLog;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const workerEndowments = {
|
|
260
|
+
console: makeVatConsole(makeLogMaker('vat')),
|
|
261
|
+
assert,
|
|
262
|
+
// bootstrap provides HandledPromise
|
|
263
|
+
HandledPromise: globalThis.HandledPromise,
|
|
264
|
+
TextEncoder,
|
|
265
|
+
TextDecoder,
|
|
266
|
+
Base64: globalThis.Base64, // Present only in XSnap
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
async function buildVatNamespace(
|
|
270
|
+
lsEndowments,
|
|
271
|
+
inescapableGlobalProperties,
|
|
272
|
+
) {
|
|
273
|
+
const vatNS = await importBundle(bundle, {
|
|
274
|
+
endowments: { ...workerEndowments, ...lsEndowments },
|
|
275
|
+
inescapableGlobalProperties,
|
|
276
|
+
});
|
|
277
|
+
workerLog(`got vatNS:`, Object.keys(vatNS).join(','));
|
|
278
|
+
return vatNS;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const ls = makeLiveSlots(
|
|
282
|
+
syscall,
|
|
283
|
+
vatID,
|
|
284
|
+
vatPowers,
|
|
285
|
+
liveSlotsOptions,
|
|
286
|
+
gcTools,
|
|
287
|
+
makeVatConsole(makeLogMaker('ls')),
|
|
288
|
+
buildVatNamespace,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
assert(ls.dispatch);
|
|
292
|
+
dispatch = makeSupervisorDispatch(ls.dispatch);
|
|
293
|
+
workerLog(`got dispatch`);
|
|
294
|
+
return ['dispatchReady'];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** @type { (item: Tagged) => Promise<Tagged> } */
|
|
298
|
+
async function handleItem([tag, ...args]) {
|
|
299
|
+
workerLog('handleItem', tag, args.length);
|
|
300
|
+
switch (tag) {
|
|
301
|
+
case 'setBundle': {
|
|
302
|
+
assert(!dispatch, 'cannot setBundle again');
|
|
303
|
+
const liveSlotsOptions = /** @type LiveSlotsOptions */ (args[2]);
|
|
304
|
+
return setBundle(args[0], args[1], liveSlotsOptions);
|
|
305
|
+
}
|
|
306
|
+
case 'deliver': {
|
|
307
|
+
assert(dispatch, 'cannot deliver before setBundle');
|
|
308
|
+
const [vatDeliveryObject] = args;
|
|
309
|
+
harden(vatDeliveryObject);
|
|
310
|
+
insistVatDeliveryObject(vatDeliveryObject);
|
|
311
|
+
return dispatch(vatDeliveryObject);
|
|
312
|
+
}
|
|
313
|
+
default:
|
|
314
|
+
workerLog('handleItem: bad tag', tag, args.length);
|
|
315
|
+
return ['bad tag', tag];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return harden({
|
|
320
|
+
handleItem,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// xsnap provides issueCommand global
|
|
325
|
+
const port = managerPort(globalThis.issueCommand);
|
|
326
|
+
const worker = makeWorker(port);
|
|
327
|
+
|
|
328
|
+
// Send unexpected console messages to the manager port.
|
|
329
|
+
globalThis.print = (...args) => {
|
|
330
|
+
port.send(['sourcedConsole', 'xsnap', 'error', ...args]);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
globalThis.handleCommand = port.handlerFrom(worker.handleItem);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* global setImmediate */
|
|
2
|
+
import { makePromiseKit } from '@endo/promise-kit';
|
|
3
|
+
|
|
4
|
+
// This can only be imported from the Start Compartment, where 'setImmediate'
|
|
5
|
+
// is available.
|
|
6
|
+
|
|
7
|
+
export function waitUntilQuiescent() {
|
|
8
|
+
// the delivery might cause some number of (native) Promises to be
|
|
9
|
+
// created and resolved, so we use the IO queue to detect when the
|
|
10
|
+
// Promise queue is empty. The IO queue (setImmediate and setTimeout) is
|
|
11
|
+
// lower-priority than the Promise queue on browsers and Node 11, but on
|
|
12
|
+
// Node 10 it is higher. So this trick requires Node 11.
|
|
13
|
+
// https://jsblog.insiderattack.net/new-changes-to-timers-and-microtasks-from-node-v11-0-0-and-above-68d112743eb3
|
|
14
|
+
/** @type {import('@endo/promise-kit').PromiseKit<void>} */
|
|
15
|
+
const { promise: queueEmptyP, resolve } = makePromiseKit();
|
|
16
|
+
setImmediate(() => resolve());
|
|
17
|
+
return queueEmptyP;
|
|
18
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agoric/swingset-xsnap-supervisor",
|
|
3
|
+
"version": "0.9.1-dev-6e51de2.0+6e51de2",
|
|
4
|
+
"description": "Supervisor/Liveslots bundle for swingset xsnap workers",
|
|
5
|
+
"author": "Agoric",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./src/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./src/index.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build:bundle": "node scripts/build-bundle.js",
|
|
15
|
+
"build": "yarn build:bundle",
|
|
16
|
+
"clean": "rm -rf dist",
|
|
17
|
+
"lint": "run-s --continue-on-error lint:*",
|
|
18
|
+
"lint:js": "eslint 'lib/**/*.js' 'src/**/*.js' 'scripts/**/*.js' 'test/**/*.js'",
|
|
19
|
+
"lint:types": "tsc -p jsconfig.json",
|
|
20
|
+
"lint-fix": "eslint --fix 'lib/**/*.js' 'src/**/*.js' 'scripts/**/*.js' 'test/**/*.js'",
|
|
21
|
+
"test": "ava",
|
|
22
|
+
"test:c8": "c8 $C8_OPTIONS ava --config=ava-nesm.config.js",
|
|
23
|
+
"test:xs": "exit 0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@agoric/assert": "0.5.2-dev-6e51de2.0+6e51de2",
|
|
27
|
+
"@agoric/swingset-liveslots": "0.9.1-dev-6e51de2.0+6e51de2",
|
|
28
|
+
"@endo/bundle-source": "^2.4.2",
|
|
29
|
+
"@endo/import-bundle": "^0.3.0",
|
|
30
|
+
"@endo/init": "^0.5.52",
|
|
31
|
+
"@endo/marshal": "^0.8.1",
|
|
32
|
+
"ava": "^5.2.0",
|
|
33
|
+
"c8": "^7.13.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"LICENSE*",
|
|
37
|
+
"lib",
|
|
38
|
+
"dist",
|
|
39
|
+
"src"
|
|
40
|
+
],
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
},
|
|
44
|
+
"ava": {
|
|
45
|
+
"files": [
|
|
46
|
+
"test/**/test-*.js"
|
|
47
|
+
],
|
|
48
|
+
"timeout": "2m",
|
|
49
|
+
"workerThreads": false
|
|
50
|
+
},
|
|
51
|
+
"gitHead": "6e51de2dd0b371a2e52355a23be623a25d6390ff"
|
|
52
|
+
}
|