Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions packages/extension/src/kernel-integration/kernel-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ import { ExtensionVatWorkerClient } from './VatWorkerClient.ts';
const logger = makeLogger('[kernel worker]');
const DB_FILENAME = 'store.db';

// XXX Warning: Setting this flag to true causes persistent storage to be
// cleared on extension load. This is a hack to aid development debugging,
// wherein extension reloads are almost exclusively used for retrying from
// scratch after tweaking the code to fix something. Setting the flag will
// prevent the accumulation of long term persistent state, so it should be
// cleared (or simply removed) prior to release.
const ALWAYS_RESET_STORAGE = true;

main().catch(logger.error);

/**
Expand All @@ -44,23 +52,21 @@ async function main(): Promise<void> {
const kernelDatabase = await makeSQLKernelDatabase({
dbFilename: DB_FILENAME,
});
const firstTime = !kernelDatabase.kernelKVStore.get('initialized');

const kernel = await Kernel.make(
kernelStream,
vatWorkerClient,
kernelDatabase,
{
// XXX Warning: Clearing storage here is a hack to aid development
// debugging, wherein extension reloads are almost exclusively used for
// retrying after tweaking some fix. The following line will prevent
// the accumulation of long term kernel state.
resetStorage: true,
resetStorage: ALWAYS_RESET_STORAGE,
},
);
const kernelEngine = new JsonRpcEngine();
kernelEngine.push(loggingMiddleware);
kernelEngine.push(createPanelMessageMiddleware(kernel, kernelDatabase));
receiveUiConnections(async (request) => kernelEngine.handle(request), logger);
const launchDefaultSubcluster = firstTime || ALWAYS_RESET_STORAGE;

const defaultSubcluster = await fetchValidatedJson<ClusterConfig>(
new URL('../vats/default-cluster.json', import.meta.url).href,
Expand All @@ -74,14 +80,18 @@ async function main(): Promise<void> {
// here of `launchSubcluster` turns out to depend on aspects of the IPC
// setup completing successfully but those pieces aren't ready in time, then
// it could get stuck. Current experience suggests this is not a problem,
// but as yet have only an intuitive sense (i.e., promises, yay) why this
// but as yet we have only an intuitive sense (i.e., promises, yay) why this
// might be true rather than a principled explanation that it is necessarily
// true. Hence this comment to serve as a marker if some problem crops up
// with startup wedging and some poor soul is reading through the code
// trying to diagnose it.
(async () => {
const result = await kernel.launchSubcluster(defaultSubcluster);
console.log(`Subcluster launched: ${JSON.stringify(result)}`);
if (launchDefaultSubcluster) {
const result = await kernel.launchSubcluster(defaultSubcluster);
console.log(`Subcluster launched: ${JSON.stringify(result)}`);
} else {
console.log(`Resuming kernel execution`);
}
})(),
]);
}
19 changes: 19 additions & 0 deletions packages/kernel-test/src/liveslots.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,23 @@ describe('liveslots promise handling', () => {
];
expect(vatLogs).toStrictEqual(reference);
}, 30000);

it('messageToPromise: send to promise before resolution', async () => {
const [bootstrapResult, vatLogs] = await runTestVats(
'message-to-promise-vat',
'messageToPromise',
);
expect(bootstrapResult).toBe('p2succ');
const reference = [
`Alice: running test messageToPromise`,
`Alice: invoking loopback`,
`Alice: second result resolved to 'deferred something'`,
`Alice: loopback done`,
`Bob: setup`,
`Bob: doResolve`,
`Bob: thing.doSomething`,
`Bob: loopback`,
];
expect(vatLogs).toStrictEqual(reference);
}, 30000);
});
89 changes: 89 additions & 0 deletions packages/kernel-test/src/message-to-promise-vat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { E } from '@endo/eventual-send';
import { Far } from '@endo/marshal';
import { makePromiseKit } from '@endo/promise-kit';
/**
* Build function for vats that will run various tests.
*
* @param {*} _vatPowers - Special powers granted to this vat (not used here).
* @param {*} parameters - Initialization parameters from the vat's config object.
* @param {*} _baggage - Root of vat's persistent state (not used here).
* @returns {*} The root object for the new vat.
*/
export function buildRootObject(_vatPowers, parameters, _baggage) {
const name = parameters?.name ?? 'anonymous';
const test = parameters?.test ?? 'unspecified';

/**
* Print a message to the log.
*
* @param {string} message - The message to print.
*/
function log(message) {
console.log(`${name}: ${message}`);
}

/**
* Print a message to the log, tagged as part of the test output.
*
* @param {string} message - The message to print.
*/
function tlog(message) {
console.log(`::> ${name}: ${message}`);
}

log(`buildRootObject`);
log(`configuration parameters: ${JSON.stringify(parameters)}`);

const thing = Far('thing', {
doSomething() {
tlog(`thing.doSomething`);
return `deferred something`;
},
});

let resolveDeferred;

return Far('root', {
async bootstrap(vats) {
log(`bootstrap start`);
tlog(`running test ${test}`);
const promise1 = E(vats.bob).setup();
const promise2 = E(promise1).doSomething();
const doneP = promise2.then(
(res) => {
tlog(`second result resolved to '${res}'`);
return 'p2succ';
},
(rej) => {
tlog(`second result rejected with '${rej}'`);
return 'p2fail';
},
);
await E(vats.bob).doResolve();
tlog(`invoking loopback`);
await E(vats.bob).loopback();
tlog(`loopback done`);
return doneP;
},

// This is a hack that effectively does the job of stdout.flush() even
// though we don't have access to stdout itself here. It makes sure we
// capture all the log output prior to the return value from `bootstrap`
// resolving.
loopback() {
tlog(`loopback`);
return undefined;
},

setup() {
tlog(`setup`);
const { promise, resolve } = makePromiseKit();
resolveDeferred = resolve;
return promise;
},
doResolve() {
tlog(`doResolve`);
resolveDeferred(thing);
},
});
}
117 changes: 117 additions & 0 deletions packages/kernel-test/src/resume-vat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/* global harden */
import { E } from '@endo/eventual-send';
import { Far } from '@endo/marshal';

/**
* Build function for generic test vat.
*
* @param {unknown} _vatPowers - Special powers granted to this vat (not used here).
* @param {unknown} parameters - Initialization parameters from the vat's config object.
* @param {unknown} baggage - Root of vat's persistent state (not used here).
* @returns {unknown} The root object for the new vat.
*/
export function buildRootObject(_vatPowers, parameters, baggage) {
const name = parameters?.name ?? 'anonymous';

/**
* Print a message to the log.
*
* @param {string} message - The message to print.
*/
function log(message) {
console.log(`${name}: ${message}`);
}

/**
* Print a message to the log, tagged as part of the test output.
*
* @param {string} message - The message to print.
*/
function tlog(message) {
console.log(`::> ${name}: ${message}`);
}

log(`buildRootObject`);

let startCount;
if (baggage.has('name')) {
const savedName = baggage.get('name');
tlog(`saved name is ${savedName}`);

startCount = baggage.get('startCount') + 1;
baggage.set('startCount', startCount);
} else {
baggage.init('name', name);
tlog(`saving name`);

baggage.init('startCount', 1);
startCount = 1;
}
tlog(`start count: ${startCount}`);

const me = Far('root', {
async bootstrap(vats) {
tlog(`bootstrap()`);
// Explanation for the following bit of gymnastics: we'd like to save
// `vats` itself in the baggage, but we can't because the entry for our
// own root is a local reference and thus not durable, and we can't remove
// this entry from `vats` directly because, being a parameter object, it
// arrived hardened. So instead we have to copy it sans the unwritable element.
const writeVats = {};
for (const [prop, value] of Object.entries(vats)) {
if (value !== me) {
writeVats[prop] = value;
}
}
baggage.init('vats', harden(writeVats));

const pIntroB = E(vats.bob).intro(me);
const pIntroC = E(vats.carol).intro(me);
const pGreetB = E(vats.bob).greet(`hello from ${name}`);
const pGreetC = E(vats.carol).greet(`hello from ${name}`);
const results = await Promise.all([pIntroB, pIntroC, pGreetB, pGreetC]);
const [, , greetB, greetC] = results;
tlog(`Bob answers greeting: '${greetB}'`);
tlog(`Carol answers greeting: '${greetC}'`);
tlog(`end bootstrap`);
await E(vats.bob).loopback();
return `bootstrap ${name}`;
},
intro(bootVat) {
tlog(`intro()`);
baggage.init('bootVat', bootVat);
},
greet(greeting) {
tlog(`greet('${greeting}')`);
return `${name} returns your greeting '${greeting}'`;
},
async resume() {
tlog(`resume()`);
if (baggage.has('vats')) {
// I am the bootstrap vat
tlog(`resumed vat is bootstrap`);
const vats = baggage.get('vats');
const pGreetB = E(vats.bob).greet(`hello again from ${name}`);
const pGreetC = E(vats.carol).greet(`hello again from ${name}`);
const [greetB, greetC] = await Promise.all([pGreetB, pGreetC]);
tlog(`Bob answers greeting: '${greetB}'`);
tlog(`Carol answers greeting: '${greetC}'`);
await E(vats.bob).loopback();
}
if (baggage.has('bootVat')) {
// I am Bob or Carol
tlog(`resumed vat is not bootstrap`);
const bootVat = baggage.get('bootVat');
const greetBack = await E(bootVat).greet(`hello boot vat from ${name}`);
tlog(`boot vat returns greeting with '${greetBack}'`);
await E(bootVat).loopback();
}
tlog(`end resume`);
return `resume ${name}`;
},
loopback() {
return undefined;
},
});
return me;
}
Loading