From 563e486ef54c0567f7e7bc64bccad65e64b91689 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:06:41 -0500 Subject: [PATCH 1/4] refactor(extension): Extract kernel store functionality from kernel-worker. --- packages/extension/src/kernel-worker.ts | 84 +---------------- packages/extension/src/offscreen.ts | 2 +- packages/extension/src/sqlite-kernel-store.ts | 93 +++++++++++++++++++ 3 files changed, 96 insertions(+), 83 deletions(-) create mode 100644 packages/extension/src/sqlite-kernel-store.ts diff --git a/packages/extension/src/kernel-worker.ts b/packages/extension/src/kernel-worker.ts index ccecff356..01c80dff4 100644 --- a/packages/extension/src/kernel-worker.ts +++ b/packages/extension/src/kernel-worker.ts @@ -3,9 +3,8 @@ import type { KernelCommand, KernelCommandReply, VatId } from '@ocap/kernel'; import { isKernelCommand, Kernel, KernelCommandMethod } from '@ocap/kernel'; import { PostMessageDuplexStream, receiveMessagePort } from '@ocap/streams'; import { makeLogger, stringify } from '@ocap/utils'; -import type { Database } from '@sqlite.org/sqlite-wasm'; -import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; +import { makeKernelStore } from './sqlite-kernel-store.js'; import { ExtensionVatWorkerClient } from './VatWorkerClient.js'; type MainArgs = { defaultVatId: VatId }; @@ -14,20 +13,6 @@ const logger = makeLogger('[kernel worker]'); main({ defaultVatId: 'v0' }).catch(console.error); -/** - * Ensure that SQLite is initialized. - * - * @returns The SQLite database object. - */ -async function initDB(): Promise { - const sqlite3 = await sqlite3InitModule(); - if (sqlite3.oo1.OpfsDb) { - return new sqlite3.oo1.OpfsDb('/testdb.sqlite', 'cwt'); - } - console.warn(`OPFS not enabled, database will be ephemeral`); - return new sqlite3.oo1.DB('/testdb.sqlite', 'cwt'); -} - /** * The main function for the offscreen script. * @@ -62,7 +47,7 @@ async function main({ defaultVatId }: MainArgs): Promise { // Initialize kernel store. - const { sqlKVGet, sqlKVSet } = await initDb(); + const { kvGet, kvSet } = await makeKernelStore(); // Create kernel. @@ -215,69 +200,4 @@ async function main({ defaultVatId }: MainArgs): Promise { ? problem : new Error('Unknown', { cause: problem }); } - - /** - * Initialize the database and some prepared statements. - * - * @returns The prepared database statements. - */ - async function initDb(): Promise<{ - sqlKVGet: ReturnType; - sqlKVSet: ReturnType; - }> { - const db = await initDB(); - db.exec(` - CREATE TABLE IF NOT EXISTS kv ( - key TEXT, - value TEXT, - PRIMARY KEY(key) - ) - `); - - return { - sqlKVGet: db.prepare(` - SELECT value - FROM kv - WHERE key = ? - `), - sqlKVSet: db.prepare(` - INSERT INTO kv (key, value) - VALUES (?, ?) - ON CONFLICT DO UPDATE SET value = excluded.value - `), - }; - } - - /** - * Exercise reading from the database. - * - * @param key - A key to fetch. - * @returns The value at that key. - */ - function kvGet(key: string): string { - sqlKVGet.bind([key]); - if (sqlKVGet.step()) { - const result = sqlKVGet.getString(0); - if (result) { - sqlKVGet.reset(); - console.log(`kernel get '${key}' as '${result}'`); - return result; - } - } - sqlKVGet.reset(); - throw Error(`no record matching key '${key}'`); - } - - /** - * Exercise writing to the database. - * - * @param key - A key to assign. - * @param value - The value to assign to it. - */ - function kvSet(key: string, value: string): void { - console.log(`kernel set '${key}' to '${value}'`); - sqlKVSet.bind([key, value]); - sqlKVSet.step(); - sqlKVSet.reset(); - } } diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index 8210b89c5..dd5be0136 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -104,7 +104,7 @@ async function main(): Promise { // involve the user. for await (const message of workerStream) { if (!isKernelCommandReply(message)) { - logger.error('Kernel received unexpected message', message); + logger.error('Kernel sent unexpected reply', message); continue; } diff --git a/packages/extension/src/sqlite-kernel-store.ts b/packages/extension/src/sqlite-kernel-store.ts new file mode 100644 index 000000000..5e589a2b3 --- /dev/null +++ b/packages/extension/src/sqlite-kernel-store.ts @@ -0,0 +1,93 @@ +import { makeLogger } from '@ocap/utils'; +import type { Database } from '@sqlite.org/sqlite-wasm'; +import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; + +/** + * Ensure that SQLite is initialized. + * + * @returns The SQLite database object. + */ +async function initDB(): Promise { + const sqlite3 = await sqlite3InitModule(); + if (sqlite3.oo1.OpfsDb) { + return new sqlite3.oo1.OpfsDb('/testdb.sqlite', 'cwt'); + } + console.warn(`OPFS not enabled, database will be ephemeral`); + return new sqlite3.oo1.DB('/testdb.sqlite', 'cwt'); +} + +export type KernelStore = { + kvGet: (key: string) => string; + kvSet: (key: string, value: string) => void; +}; + +/** + * Makes a {@link KernelStore} for persistent storage. + * + * @param label - A logger prefix label. Defaults to '[sqlite]'. + * @returns The kernel store. + */ +export async function makeKernelStore( + label: string = '[sqlite]', +): Promise { + const logger = makeLogger(label); + const db = await initDB(); + + db.exec(` + CREATE TABLE IF NOT EXISTS kv ( + key TEXT, + value TEXT, + PRIMARY KEY(key) + ) + `); + + const sqlKVGet = db.prepare(` + SELECT value + FROM kv + WHERE key = ? + `); + + /** + * Exercise reading from the database. + * + * @param key - A key to fetch. + * @returns The value at that key. + */ + function kvGet(key: string): string { + sqlKVGet.bind([key]); + if (sqlKVGet.step()) { + const result = sqlKVGet.getString(0); + if (result) { + sqlKVGet.reset(); + logger.debug(`kernel get '${key}' as '${result}'`); + return result; + } + } + sqlKVGet.reset(); + throw Error(`no record matching key '${key}'`); + } + + const sqlKVSet = db.prepare(` + INSERT INTO kv (key, value) + VALUES (?, ?) + ON CONFLICT DO UPDATE SET value = excluded.value + `); + + /** + * Exercise writing to the database. + * + * @param key - A key to assign. + * @param value - The value to assign to it. + */ + function kvSet(key: string, value: string): void { + logger.debug(`kernel set '${key}' to '${value}'`); + sqlKVSet.bind([key, value]); + sqlKVSet.step(); + sqlKVSet.reset(); + } + + return { + kvGet, + kvSet, + }; +} From 95eacf827b7be51a93ec1d8a03cc647d2fd9e5e1 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:58:31 -0500 Subject: [PATCH 2/4] feat(kernel): Add a KernelStore interface. --- packages/extension/src/kernel-worker.ts | 8 ++--- packages/extension/src/sqlite-kernel-store.ts | 6 +--- packages/kernel/src/Kernel.test.ts | 32 ++++++++++++------- packages/kernel/src/Kernel.ts | 15 ++++++++- packages/kernel/src/index.ts | 1 + packages/kernel/src/kernel-store.ts | 4 +++ packages/kernel/test/storage.ts | 20 ++++++++++++ packages/kernel/tsconfig.json | 2 +- 8 files changed, 65 insertions(+), 23 deletions(-) create mode 100644 packages/kernel/src/kernel-store.ts create mode 100644 packages/kernel/test/storage.ts diff --git a/packages/extension/src/kernel-worker.ts b/packages/extension/src/kernel-worker.ts index 01c80dff4..8056eb9e3 100644 --- a/packages/extension/src/kernel-worker.ts +++ b/packages/extension/src/kernel-worker.ts @@ -47,11 +47,11 @@ async function main({ defaultVatId }: MainArgs): Promise { // Initialize kernel store. - const { kvGet, kvSet } = await makeKernelStore(); + const kernelStore = await makeKernelStore(); // Create kernel. - const kernel = new Kernel(vatWorkerClient); + const kernel = new Kernel(vatWorkerClient, kernelStore); const vatReadyP = kernel.launchVat({ id: defaultVatId }); await reply({ @@ -91,7 +91,7 @@ async function main({ defaultVatId }: MainArgs): Promise { await handleVatTestCommand({ method, params }); break; case KernelCommandMethod.KVSet: - kvSet(params.key, params.value); + kernel.kvSet(params.key, params.value); await reply({ method, params: `~~~ set "${params.key}" to "${params.value}" ~~~`, @@ -99,7 +99,7 @@ async function main({ defaultVatId }: MainArgs): Promise { break; case KernelCommandMethod.KVGet: { try { - const result = kvGet(params); + const result = kernel.kvGet(params); await reply({ method, params: result, diff --git a/packages/extension/src/sqlite-kernel-store.ts b/packages/extension/src/sqlite-kernel-store.ts index 5e589a2b3..ddfb69f16 100644 --- a/packages/extension/src/sqlite-kernel-store.ts +++ b/packages/extension/src/sqlite-kernel-store.ts @@ -1,3 +1,4 @@ +import type { KernelStore } from '@ocap/kernel'; import { makeLogger } from '@ocap/utils'; import type { Database } from '@sqlite.org/sqlite-wasm'; import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; @@ -16,11 +17,6 @@ async function initDB(): Promise { return new sqlite3.oo1.DB('/testdb.sqlite', 'cwt'); } -export type KernelStore = { - kvGet: (key: string) => string; - kvSet: (key: string, value: string) => void; -}; - /** * Makes a {@link KernelStore} for persistent storage. * diff --git a/packages/kernel/src/Kernel.test.ts b/packages/kernel/src/Kernel.test.ts index 22f08d7bd..8ad841405 100644 --- a/packages/kernel/src/Kernel.test.ts +++ b/packages/kernel/src/Kernel.test.ts @@ -2,12 +2,14 @@ import type { DuplexStream } from '@ocap/streams'; import type { MockInstance } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { KernelStore } from './kernel-store.js'; import { Kernel } from './Kernel.js'; import type { VatCommand } from './messages.js'; import type { StreamEnvelope, StreamEnvelopeReply } from './stream-envelope.js'; import type { VatId } from './types.js'; import { Vat } from './Vat.js'; import type { VatWorkerClient } from './VatWorkerClient.js'; +import { makeMapKernelStore } from '../test/storage.js'; describe('Kernel', () => { let mockWorkerService: VatWorkerClient; @@ -16,6 +18,8 @@ describe('Kernel', () => { let initMock: MockInstance; let terminateMock: MockInstance; + let mockKernelStore: KernelStore; + beforeEach(() => { mockWorkerService = { initWorker: async () => ({}), @@ -35,22 +39,24 @@ describe('Kernel', () => { terminateMock = vi .spyOn(Vat.prototype, 'terminate') .mockImplementation(vi.fn()); + + mockKernelStore = makeMapKernelStore(); }); describe('getVatIds()', () => { it('returns an empty array when no vats are added', () => { - const kernel = new Kernel(mockWorkerService); + const kernel = new Kernel(mockWorkerService, mockKernelStore); expect(kernel.getVatIds()).toStrictEqual([]); }); it('returns the vat IDs after adding a vat', async () => { - const kernel = new Kernel(mockWorkerService); + const kernel = new Kernel(mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); expect(kernel.getVatIds()).toStrictEqual(['v0']); }); it('returns multiple vat IDs after adding multiple vats', async () => { - const kernel = new Kernel(mockWorkerService); + const kernel = new Kernel(mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); await kernel.launchVat({ id: 'v1' }); expect(kernel.getVatIds()).toStrictEqual(['v0', 'v1']); @@ -59,7 +65,7 @@ describe('Kernel', () => { describe('launchVat()', () => { it('adds a vat to the kernel without errors when no vat with the same ID exists', async () => { - const kernel = new Kernel(mockWorkerService); + const kernel = new Kernel(mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); expect(initMock).toHaveBeenCalledOnce(); expect(mockGetWorkerStreams).toHaveBeenCalled(); @@ -67,7 +73,7 @@ describe('Kernel', () => { }); it('throws an error when launching a vat that already exists in the kernel', async () => { - const kernel = new Kernel(mockWorkerService); + const kernel = new Kernel(mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); expect(kernel.getVatIds()).toStrictEqual(['v0']); await expect( @@ -81,7 +87,7 @@ describe('Kernel', () => { describe('deleteVat()', () => { it('deletes a vat from the kernel without errors when the vat exists', async () => { - const kernel = new Kernel(mockWorkerService); + const kernel = new Kernel(mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); expect(kernel.getVatIds()).toStrictEqual(['v0']); await kernel.deleteVat('v0'); @@ -91,7 +97,7 @@ describe('Kernel', () => { }); it('throws an error when deleting a vat that does not exist in the kernel', async () => { - const kernel = new Kernel(mockWorkerService); + const kernel = new Kernel(mockWorkerService, mockKernelStore); const nonExistentVatId: VatId = 'v9'; await expect(async () => kernel.deleteVat(nonExistentVatId), @@ -100,7 +106,7 @@ describe('Kernel', () => { }); it('throws an error when a vat terminate method throws', async () => { - const kernel = new Kernel(mockWorkerService); + const kernel = new Kernel(mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); vi.spyOn(Vat.prototype, 'terminate').mockRejectedValueOnce('Test error'); await expect(async () => kernel.deleteVat('v0')).rejects.toThrow( @@ -111,7 +117,7 @@ describe('Kernel', () => { describe('sendMessage()', () => { it('sends a message to the vat without errors when the vat exists', async () => { - const kernel = new Kernel(mockWorkerService); + const kernel = new Kernel(mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); vi.spyOn(Vat.prototype, 'sendMessage').mockResolvedValueOnce('test'); expect( @@ -123,7 +129,7 @@ describe('Kernel', () => { }); it('throws an error when sending a message to the vat that does not exist in the kernel', async () => { - const kernel = new Kernel(mockWorkerService); + const kernel = new Kernel(mockWorkerService, mockKernelStore); const nonExistentVatId: VatId = 'v9'; await expect(async () => kernel.sendMessage(nonExistentVatId, {} as VatCommand['payload']), @@ -131,7 +137,7 @@ describe('Kernel', () => { }); it('throws an error when sending a message to the vat throws', async () => { - const kernel = new Kernel(mockWorkerService); + const kernel = new Kernel(mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); vi.spyOn(Vat.prototype, 'sendMessage').mockRejectedValueOnce('error'); await expect(async () => @@ -142,7 +148,9 @@ describe('Kernel', () => { describe('constructor()', () => { it('initializes the kernel without errors', () => { - expect(async () => new Kernel(mockWorkerService)).not.toThrow(); + expect( + async () => new Kernel(mockWorkerService, mockKernelStore), + ).not.toThrow(); }); }); }); diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index 7fe14486c..5d499bd09 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -1,4 +1,5 @@ import '@ocap/shims/endoify'; +import type { KernelStore } from './kernel-store.js'; import type { VatCommand } from './messages.js'; import type { VatId, VatWorkerService } from './types.js'; import { Vat } from './Vat.js'; @@ -8,9 +9,20 @@ export class Kernel { readonly #vatWorkerService: VatWorkerService; - constructor(vatWorkerService: VatWorkerService) { + readonly #storage: KernelStore; + + constructor(vatWorkerService: VatWorkerService, storage: KernelStore) { this.#vats = new Map(); this.#vatWorkerService = vatWorkerService; + this.#storage = storage; + } + + kvGet(key: string): string { + return this.#storage.kvGet(key); + } + + kvSet(key: string, value: string): void { + this.#storage.kvSet(key, value); } /** @@ -81,3 +93,4 @@ export class Kernel { return vat; } } +harden(Kernel); diff --git a/packages/kernel/src/index.ts b/packages/kernel/src/index.ts index 04c3d0287..f6c4c0f37 100644 --- a/packages/kernel/src/index.ts +++ b/packages/kernel/src/index.ts @@ -1,5 +1,6 @@ export * from './messages.js'; export { Kernel } from './Kernel.js'; +export type { KernelStore } from './kernel-store.js'; export { Vat } from './Vat.js'; export { Supervisor } from './Supervisor.js'; export type { StreamEnvelope, StreamEnvelopeReply } from './stream-envelope.js'; diff --git a/packages/kernel/src/kernel-store.ts b/packages/kernel/src/kernel-store.ts new file mode 100644 index 000000000..af4da6b64 --- /dev/null +++ b/packages/kernel/src/kernel-store.ts @@ -0,0 +1,4 @@ +export type KernelStore = { + kvGet: (key: string) => string; + kvSet: (key: string, value: string) => void; +}; diff --git a/packages/kernel/test/storage.ts b/packages/kernel/test/storage.ts new file mode 100644 index 000000000..46c38e5da --- /dev/null +++ b/packages/kernel/test/storage.ts @@ -0,0 +1,20 @@ +import type { KernelStore } from '../src/kernel-store.js'; + +/** + * A mock kernel store realized as a Map. + * + * @returns The mock {@link KernelStore}. + */ +export function makeMapKernelStore(): KernelStore { + const map = new Map(); + return { + kvGet: (key) => { + const value = map.get(key); + if (value === undefined) { + throw new Error(`No value found for key ${key}.`); + } + return value; + }, + kvSet: map.set.bind(map), + }; +} diff --git a/packages/kernel/tsconfig.json b/packages/kernel/tsconfig.json index 2e5a27d22..be7af40bd 100644 --- a/packages/kernel/tsconfig.json +++ b/packages/kernel/tsconfig.json @@ -4,5 +4,5 @@ "baseUrl": "./" }, "references": [{ "path": "../streams" }, { "path": "../utils" }], - "include": ["./src"] + "include": ["./src", "./test"] } From 7ae6a7f81dcf28e61ab33dbaeaf82f430b6331db Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Wed, 9 Oct 2024 21:24:10 -0500 Subject: [PATCH 3/4] refactor(extension,kernel): Move kernel message handling to Kernel. --- packages/extension/src/kernel-worker.ts | 171 ++---------------------- packages/kernel/src/Kernel.test.ts | 59 +++++--- packages/kernel/src/Kernel.ts | 151 ++++++++++++++++++++- 3 files changed, 198 insertions(+), 183 deletions(-) diff --git a/packages/extension/src/kernel-worker.ts b/packages/extension/src/kernel-worker.ts index 8056eb9e3..8b8c080c8 100644 --- a/packages/extension/src/kernel-worker.ts +++ b/packages/extension/src/kernel-worker.ts @@ -1,25 +1,19 @@ import './kernel-worker-trusted-prelude.js'; import type { KernelCommand, KernelCommandReply, VatId } from '@ocap/kernel'; -import { isKernelCommand, Kernel, KernelCommandMethod } from '@ocap/kernel'; +import { Kernel } from '@ocap/kernel'; import { PostMessageDuplexStream, receiveMessagePort } from '@ocap/streams'; -import { makeLogger, stringify } from '@ocap/utils'; import { makeKernelStore } from './sqlite-kernel-store.js'; import { ExtensionVatWorkerClient } from './VatWorkerClient.js'; -type MainArgs = { defaultVatId: VatId }; - -const logger = makeLogger('[kernel worker]'); - -main({ defaultVatId: 'v0' }).catch(console.error); +main('v0').catch(console.error); /** - * The main function for the offscreen script. + * The main function for the kernel worker. * - * @param options - The options bag. - * @param options.defaultVatId - The id to give the default vat. + * @param defaultVatId - The id to give the default vat. */ -async function main({ defaultVatId }: MainArgs): Promise { +async function main(defaultVatId: VatId): Promise { // Note we must setup the worker MessageChannel before initializing the stream, // because the stream will close if it receives an unrecognized message. const clientPort = await receiveMessagePort( @@ -34,8 +28,6 @@ async function main({ defaultVatId }: MainArgs): Promise { }, ); - const startTime = performance.now(); - const kernelStream = new PostMessageDuplexStream< KernelCommand, KernelCommandReply @@ -49,155 +41,8 @@ async function main({ defaultVatId }: MainArgs): Promise { const kernelStore = await makeKernelStore(); - // Create kernel. - - const kernel = new Kernel(vatWorkerClient, kernelStore); - const vatReadyP = kernel.launchVat({ id: defaultVatId }); - - await reply({ - method: KernelCommandMethod.InitKernel, - params: { - defaultVat: defaultVatId, - initTime: performance.now() - startTime, - }, - }); - - // Handle messages from the console service worker - await kernelStream.drain(handleKernelCommand); - - /** - * Handle a KernelCommand sent from the offscreen. - * - * @param command - The KernelCommand to handle. - */ - async function handleKernelCommand(command: KernelCommand): Promise { - if (!isKernelCommand(command)) { - logger.error('Received unexpected message', command); - return; - } - - const { method, params } = command; - - switch (method) { - case KernelCommandMethod.InitKernel: - throw new Error('The kernel starts itself.'); - case KernelCommandMethod.Ping: - await reply({ method, params: 'pong' }); - break; - case KernelCommandMethod.Evaluate: - await handleVatTestCommand({ method, params }); - break; - case KernelCommandMethod.CapTpCall: - await handleVatTestCommand({ method, params }); - break; - case KernelCommandMethod.KVSet: - kernel.kvSet(params.key, params.value); - await reply({ - method, - params: `~~~ set "${params.key}" to "${params.value}" ~~~`, - }); - break; - case KernelCommandMethod.KVGet: { - try { - const result = kernel.kvGet(params); - await reply({ - method, - params: result, - }); - } catch (problem) { - // TODO: marshal - await reply({ - method, - params: String(asError(problem)), - }); - } - break; - } - default: - console.error( - 'kernel worker received unexpected command', - // @ts-expect-error Runtime does not respect "never". - { method: method.valueOf(), params }, - ); - } - } - - /** - * Handle a command implemented by the test vat. - * - * @param command - The command to handle. - */ - async function handleVatTestCommand( - command: Extract< - KernelCommand, - | { method: typeof KernelCommandMethod.Evaluate } - | { method: typeof KernelCommandMethod.CapTpCall } - >, - ): Promise { - const { method, params } = command; - const vat = await vatReadyP; - switch (method) { - case KernelCommandMethod.Evaluate: - await reply({ - method, - params: await evaluate(vat.id, params), - }); - break; - case KernelCommandMethod.CapTpCall: - await reply({ - method, - params: stringify(await vat.callCapTp(params)), - }); - break; - default: - console.error( - 'Offscreen received unexpected vat command', - // @ts-expect-error Runtime does not respect "never". - { method: method.valueOf(), params }, - ); - } - } - - /** - * Reply to the background script. - * - * @param payload - The payload to reply with. - */ - async function reply(payload: KernelCommandReply): Promise { - await kernelStream.write(payload); - } - - /** - * Evaluate a string in the default iframe. - * - * @param vatId - The ID of the vat to send the message to. - * @param source - The source string to evaluate. - * @returns The result of the evaluation, or an error message. - */ - async function evaluate(vatId: VatId, source: string): Promise { - try { - const result = await kernel.sendMessage(vatId, { - method: KernelCommandMethod.Evaluate, - params: source, - }); - return String(result); - } catch (error) { - if (error instanceof Error) { - return `Error: ${error.message}`; - } - return `Error: Unknown error during evaluation.`; - } - } + // Create and start kernel. - /** - * Coerce an unknown problem into an Error object. - * - * @param problem - Whatever was caught. - * @returns The problem if it is an Error, or a new Error with the problem as the cause. - */ - function asError(problem: unknown): Error { - return problem instanceof Error - ? problem - : new Error('Unknown', { cause: problem }); - } + const kernel = new Kernel(kernelStream, vatWorkerClient, kernelStore); + await kernel.init({ defaultVatId }); } diff --git a/packages/kernel/src/Kernel.test.ts b/packages/kernel/src/Kernel.test.ts index 8ad841405..94c17c229 100644 --- a/packages/kernel/src/Kernel.test.ts +++ b/packages/kernel/src/Kernel.test.ts @@ -1,18 +1,24 @@ -import type { DuplexStream } from '@ocap/streams'; +import '@ocap/shims/endoify'; + +import type { MessagePortDuplexStream, DuplexStream } from '@ocap/streams'; import type { MockInstance } from 'vitest'; import { describe, it, expect, vi, beforeEach } from 'vitest'; import type { KernelStore } from './kernel-store.js'; import { Kernel } from './Kernel.js'; -import type { VatCommand } from './messages.js'; +import { + type KernelCommand, + type KernelCommandReply, + type VatCommand, +} from './messages.js'; import type { StreamEnvelope, StreamEnvelopeReply } from './stream-envelope.js'; -import type { VatId } from './types.js'; +import type { VatId, VatWorkerService } from './types.js'; import { Vat } from './Vat.js'; -import type { VatWorkerClient } from './VatWorkerClient.js'; import { makeMapKernelStore } from '../test/storage.js'; describe('Kernel', () => { - let mockWorkerService: VatWorkerClient; + let mockStream: DuplexStream; + let mockWorkerService: VatWorkerService; let mockGetWorkerStreams: MockInstance; let mockDeleteWorker: MockInstance; let initMock: MockInstance; @@ -21,10 +27,19 @@ describe('Kernel', () => { let mockKernelStore: KernelStore; beforeEach(() => { + mockStream = { + write: vi.fn(), + next: vi.fn(), + return: vi.fn(), + drain: vi.fn(), + throw: vi.fn(), + [Symbol.asyncIterator]: vi.fn(() => mockStream), + } as unknown as MessagePortDuplexStream; + mockWorkerService = { initWorker: async () => ({}), deleteWorker: async () => undefined, - } as unknown as VatWorkerClient; + } as unknown as VatWorkerService; mockGetWorkerStreams = vi .spyOn(mockWorkerService, 'initWorker') @@ -45,18 +60,18 @@ describe('Kernel', () => { describe('getVatIds()', () => { it('returns an empty array when no vats are added', () => { - const kernel = new Kernel(mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); expect(kernel.getVatIds()).toStrictEqual([]); }); it('returns the vat IDs after adding a vat', async () => { - const kernel = new Kernel(mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); expect(kernel.getVatIds()).toStrictEqual(['v0']); }); it('returns multiple vat IDs after adding multiple vats', async () => { - const kernel = new Kernel(mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); await kernel.launchVat({ id: 'v1' }); expect(kernel.getVatIds()).toStrictEqual(['v0', 'v1']); @@ -65,7 +80,7 @@ describe('Kernel', () => { describe('launchVat()', () => { it('adds a vat to the kernel without errors when no vat with the same ID exists', async () => { - const kernel = new Kernel(mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); expect(initMock).toHaveBeenCalledOnce(); expect(mockGetWorkerStreams).toHaveBeenCalled(); @@ -73,7 +88,7 @@ describe('Kernel', () => { }); it('throws an error when launching a vat that already exists in the kernel', async () => { - const kernel = new Kernel(mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); expect(kernel.getVatIds()).toStrictEqual(['v0']); await expect( @@ -87,7 +102,7 @@ describe('Kernel', () => { describe('deleteVat()', () => { it('deletes a vat from the kernel without errors when the vat exists', async () => { - const kernel = new Kernel(mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); expect(kernel.getVatIds()).toStrictEqual(['v0']); await kernel.deleteVat('v0'); @@ -97,7 +112,7 @@ describe('Kernel', () => { }); it('throws an error when deleting a vat that does not exist in the kernel', async () => { - const kernel = new Kernel(mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); const nonExistentVatId: VatId = 'v9'; await expect(async () => kernel.deleteVat(nonExistentVatId), @@ -106,7 +121,7 @@ describe('Kernel', () => { }); it('throws an error when a vat terminate method throws', async () => { - const kernel = new Kernel(mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); vi.spyOn(Vat.prototype, 'terminate').mockRejectedValueOnce('Test error'); await expect(async () => kernel.deleteVat('v0')).rejects.toThrow( @@ -117,7 +132,7 @@ describe('Kernel', () => { describe('sendMessage()', () => { it('sends a message to the vat without errors when the vat exists', async () => { - const kernel = new Kernel(mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); vi.spyOn(Vat.prototype, 'sendMessage').mockResolvedValueOnce('test'); expect( @@ -129,7 +144,7 @@ describe('Kernel', () => { }); it('throws an error when sending a message to the vat that does not exist in the kernel', async () => { - const kernel = new Kernel(mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); const nonExistentVatId: VatId = 'v9'; await expect(async () => kernel.sendMessage(nonExistentVatId, {} as VatCommand['payload']), @@ -137,7 +152,7 @@ describe('Kernel', () => { }); it('throws an error when sending a message to the vat throws', async () => { - const kernel = new Kernel(mockWorkerService, mockKernelStore); + const kernel = new Kernel(mockStream, mockWorkerService, mockKernelStore); await kernel.launchVat({ id: 'v0' }); vi.spyOn(Vat.prototype, 'sendMessage').mockRejectedValueOnce('error'); await expect(async () => @@ -149,8 +164,16 @@ describe('Kernel', () => { describe('constructor()', () => { it('initializes the kernel without errors', () => { expect( - async () => new Kernel(mockWorkerService, mockKernelStore), + async () => new Kernel(mockStream, mockWorkerService, mockKernelStore), ).not.toThrow(); }); }); + + describe('init()', () => { + it.todo('initializes the kernel store'); + + it.todo('sends an InitKernel reply to the console'); + + it.todo('starts receiving messages'); + }); }); diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index 5d499bd09..2f001e366 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -1,20 +1,155 @@ import '@ocap/shims/endoify'; +import type { PromiseKit } from '@endo/promise-kit'; +import { makePromiseKit } from '@endo/promise-kit'; +import type { DuplexStream } from '@ocap/streams'; +import type { Logger } from '@ocap/utils'; +import { makeLogger, stringify } from '@ocap/utils'; + import type { KernelStore } from './kernel-store.js'; -import type { VatCommand } from './messages.js'; +import { + isKernelCommand, + KernelCommandMethod, + VatCommandMethod, + type KernelCommand, + type KernelCommandReply, + type VatCommand, +} from './messages.js'; import type { VatId, VatWorkerService } from './types.js'; import { Vat } from './Vat.js'; export class Kernel { + readonly #stream: DuplexStream; + readonly #vats: Map; readonly #vatWorkerService: VatWorkerService; readonly #storage: KernelStore; - constructor(vatWorkerService: VatWorkerService, storage: KernelStore) { + // Hopefully removed when we get to n+1 vats. + readonly #defaultVat: PromiseKit; + + readonly #logger: Logger; + + constructor( + stream: DuplexStream, + vatWorkerService: VatWorkerService, + storage: KernelStore, + logger?: Logger, + ) { + this.#stream = stream; this.#vats = new Map(); this.#vatWorkerService = vatWorkerService; this.#storage = storage; + this.#defaultVat = makePromiseKit(); + this.#logger = logger ?? makeLogger('[ocap kernel]'); + } + + async init({ defaultVatId }: { defaultVatId: VatId }): Promise { + const start = performance.now(); + + await this.launchVat({ id: defaultVatId }) + .then(this.#defaultVat.resolve) + .catch(this.#defaultVat.reject); + + await this.#stream.write({ + method: KernelCommandMethod.InitKernel, + params: { defaultVat: defaultVatId, initTime: performance.now() - start }, + }); + + // This would be a good place to use the void operator. + + return this.#receiveMessages(); + } + + async #receiveMessages(): Promise { + for await (const message of this.#stream) { + if (!isKernelCommand(message)) { + this.#logger.debug('Received unexpected message', message); + continue; + } + + const { method, params } = message; + + let vat: Vat; + + switch (method) { + case KernelCommandMethod.InitKernel: + throw new Error('The kernel initializes itself.'); + case KernelCommandMethod.Ping: + await this.#reply({ method, params: 'pong' }); + break; + case KernelCommandMethod.Evaluate: + vat = await this.#defaultVat.promise; + await this.#reply({ + method, + params: await this.evaluate(vat.id, params), + }); + break; + case KernelCommandMethod.CapTpCall: + vat = await this.#defaultVat.promise; + await this.#reply({ + method, + params: stringify(await vat.callCapTp(params)), + }); + break; + case KernelCommandMethod.KVSet: + this.kvSet(params.key, params.value); + await this.#reply({ + method, + params: `~~~ set "${params.key}" to "${params.value}" ~~~`, + }); + break; + case KernelCommandMethod.KVGet: { + try { + const result = this.kvGet(params); + await this.#reply({ + method, + params: result, + }); + } catch (problem) { + // TODO: marshal + await this.#reply({ + method, + params: String(asError(problem)), + }); + } + break; + } + default: + console.error( + 'kernel worker received unexpected command', + // @ts-expect-error Runtime does not respect "never". + { method: method.valueOf(), params }, + ); + } + } + } + + async #reply(message: KernelCommandReply): Promise { + await this.#stream.write(message); + } + + /** + * Evaluate a string in the default iframe. + * + * @param vatId - The ID of the vat to send the message to. + * @param source - The source string to evaluate. + * @returns The result of the evaluation, or an error message. + */ + async evaluate(vatId: VatId, source: string): Promise { + try { + const result = await this.sendMessage(vatId, { + method: VatCommandMethod.Evaluate, + params: source, + }); + return String(result); + } catch (error) { + if (error instanceof Error) { + return `Error: ${error.message}`; + } + return `Error: Unknown error during evaluation.`; + } } kvGet(key: string): string { @@ -94,3 +229,15 @@ export class Kernel { } } harden(Kernel); + +/** + * Coerce an unknown problem into an Error object. + * + * @param problem - Whatever was caught. + * @returns The problem if it is an Error, or a new Error with the problem as the cause. + */ +function asError(problem: unknown): Error { + return problem instanceof Error + ? problem + : new Error('Unknown', { cause: problem }); +} From af6ee52b3592d20748a604472b04508cf0926f72 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 10 Oct 2024 18:34:14 -0500 Subject: [PATCH 4/4] defaultVat -> defaultVatKit --- packages/kernel/src/Kernel.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/packages/kernel/src/Kernel.ts b/packages/kernel/src/Kernel.ts index 2f001e366..ee1b01202 100644 --- a/packages/kernel/src/Kernel.ts +++ b/packages/kernel/src/Kernel.ts @@ -27,7 +27,7 @@ export class Kernel { readonly #storage: KernelStore; // Hopefully removed when we get to n+1 vats. - readonly #defaultVat: PromiseKit; + readonly #defaultVatKit: PromiseKit; readonly #logger: Logger; @@ -41,7 +41,7 @@ export class Kernel { this.#vats = new Map(); this.#vatWorkerService = vatWorkerService; this.#storage = storage; - this.#defaultVat = makePromiseKit(); + this.#defaultVatKit = makePromiseKit(); this.#logger = logger ?? makeLogger('[ocap kernel]'); } @@ -49,16 +49,14 @@ export class Kernel { const start = performance.now(); await this.launchVat({ id: defaultVatId }) - .then(this.#defaultVat.resolve) - .catch(this.#defaultVat.reject); + .then(this.#defaultVatKit.resolve) + .catch(this.#defaultVatKit.reject); await this.#stream.write({ method: KernelCommandMethod.InitKernel, params: { defaultVat: defaultVatId, initTime: performance.now() - start }, }); - // This would be a good place to use the void operator. - return this.#receiveMessages(); } @@ -80,14 +78,14 @@ export class Kernel { await this.#reply({ method, params: 'pong' }); break; case KernelCommandMethod.Evaluate: - vat = await this.#defaultVat.promise; + vat = await this.#defaultVatKit.promise; await this.#reply({ method, params: await this.evaluate(vat.id, params), }); break; case KernelCommandMethod.CapTpCall: - vat = await this.#defaultVat.promise; + vat = await this.#defaultVatKit.promise; await this.#reply({ method, params: stringify(await vat.callCapTp(params)),