From 8764332d2b5212656cddde0a9b841e622b28753a Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:12:10 -0500 Subject: [PATCH 1/9] feat(kernel): Add InitKernel command. --- packages/kernel/src/messages/kernel.test.ts | 32 +++++++++++++++++++++ packages/kernel/src/messages/kernel.ts | 13 ++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 packages/kernel/src/messages/kernel.test.ts diff --git a/packages/kernel/src/messages/kernel.test.ts b/packages/kernel/src/messages/kernel.test.ts new file mode 100644 index 000000000..259bae1e3 --- /dev/null +++ b/packages/kernel/src/messages/kernel.test.ts @@ -0,0 +1,32 @@ +import { describe, expect, it } from 'vitest'; + +import { + isKernelCommand, + isKernelCommandReply, + KernelCommandMethod, +} from './kernel.js'; + +describe('isKernelCommand', () => { + it.each` + value | expectedResult | description + ${{ method: KernelCommandMethod.InitKernel, params: null }} | ${true} | ${'valid command with null data'} + ${123} | ${false} | ${'invalid command: primitive number'} + ${{ method: true, params: 'data' }} | ${false} | ${'invalid command: invalid type'} + ${{ method: 123, params: null }} | ${false} | ${'invalid command: invalid type and valid data'} + ${{ method: 'some-type', params: true }} | ${false} | ${'invalid command: valid type and invalid data'} + `('returns $expectedResult for $description', ({ value, expectedResult }) => { + expect(isKernelCommand(value)).toBe(expectedResult); + }); +}); + +describe('isKernelCommandReply', () => { + it.each` + value | expectedResult | description + ${{ method: KernelCommandMethod.InitKernel, params: { initTime: 22, defaultVat: 'v0' } }} | ${true} | ${'valid command with object data'} + ${{ method: true, params: 'data' }} | ${false} | ${'invalid command reply: invalid type'} + ${{ method: 123, params: null }} | ${false} | ${'invalid command reply: invalid type and valid data'} + ${{ method: 'some-type', params: true }} | ${false} | ${'invalid command reply: valid type and invalid data'} + `('returns $expectedResult for $description', ({ value, expectedResult }) => { + expect(isKernelCommandReply(value)).toBe(expectedResult); + }); +}); diff --git a/packages/kernel/src/messages/kernel.ts b/packages/kernel/src/messages/kernel.ts index a04924364..2a05d1254 100644 --- a/packages/kernel/src/messages/kernel.ts +++ b/packages/kernel/src/messages/kernel.ts @@ -1,9 +1,20 @@ +import { isObject } from '@metamask/utils'; import type { TypeGuard } from '@ocap/utils'; import { kernelTestCommand } from './kernel-test.js'; -import { makeMessageKit } from './message-kit.js'; +import { makeMessageKit, messageType } from './message-kit.js'; +import type { VatId } from '../types.js'; +import { isVatId } from '../types.js'; export const kernelCommand = { + InitKernel: messageType( + (send) => send === null, + (reply) => + isObject(reply) && + typeof reply.initTime === 'number' && + isVatId(reply.defaultVat), + ), + ...kernelTestCommand, }; From 90d63eebda4b98911784fa513663ad6620abca14 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 4 Oct 2024 16:22:37 -0500 Subject: [PATCH 2/9] feat(extension): Use init kernel command. --- packages/extension/src/background.ts | 1 + packages/extension/src/offscreen.ts | 17 +++++++++++++++-- packages/kernel/src/messages/cluster.ts | 4 ++-- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/extension/src/background.ts b/packages/extension/src/background.ts index b11b2991a..bfdbf3250 100644 --- a/packages/extension/src/background.ts +++ b/packages/extension/src/background.ts @@ -81,6 +81,7 @@ async function main(): Promise { } switch (message.method) { + case ClusterCommandMethod.InitKernel: case ClusterCommandMethod.Evaluate: case ClusterCommandMethod.CapTpCall: case ClusterCommandMethod.Ping: diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index 7c18dfa59..192772912 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -29,6 +29,9 @@ async function main(): Promise { ChromeRuntimeTarget.Background, ); + const startTime = performance.now(); + const defaultVatId = 'v0'; + const kernelWorker = makeKernelWorker(); // Setup mock VatWorker service. @@ -58,7 +61,7 @@ async function main(): Promise { // Create kernel. const kernel = new Kernel(vatWorkerClient); - const iframeReadyP = kernel.launchVat({ id: 'v0' }); + const iframeReadyP = kernel.launchVat({ id: defaultVatId }); // Setup glue. @@ -96,6 +99,8 @@ async function main(): Promise { async function handleKernelCommand(command: KernelCommand): Promise { const { method, params } = command; switch (method) { + case KernelCommandMethod.InitKernel: + throw new Error('background should not call init kernel'); case KernelCommandMethod.Ping: await replyToBackground({ method, params: 'pong' }); break; @@ -223,7 +228,8 @@ async function main(): Promise { // the sole eventual recipient is a human eyeball, and even then it's questionable. result = `ERROR: ${possibleError.message}`; } else { - result = params; + // The InitKernel reply has an object params. + result = String(params); } const reply = { method, params: result ?? null }; if (!isKernelCommandReply(reply)) { @@ -239,6 +245,13 @@ async function main(): Promise { await workerStream.write(message); }; + iframeReadyP.then(async () => { + await replyToBackground({ + method: KernelCommandMethod.InitKernel, + params: { initTime: performance.now() - startTime, defaultVat: defaultVatId }, + }) + }); + return { sendMessage, receiveMessages, diff --git a/packages/kernel/src/messages/cluster.ts b/packages/kernel/src/messages/cluster.ts index ec76f633b..c7afd5d5f 100644 --- a/packages/kernel/src/messages/cluster.ts +++ b/packages/kernel/src/messages/cluster.ts @@ -1,10 +1,10 @@ import type { TypeGuard } from '@ocap/utils'; -import { kernelTestCommand } from './kernel-test.js'; +import { kernelCommand } from './kernel.js'; import { makeMessageKit } from './message-kit.js'; const clusterCommand = { - ...kernelTestCommand, + ...kernelCommand, }; const clusterCommandKit = makeMessageKit(clusterCommand); From 737d6bf3f795da2a8e2b5d623ec58895379a9923 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:52:27 -0500 Subject: [PATCH 3/9] refactor(extension): Extract kernel types from kernel-worker. --- packages/extension/src/kernel-types.ts | 82 +++++++++++++++++++++++++ packages/extension/src/kernel-worker.ts | 80 ------------------------ 2 files changed, 82 insertions(+), 80 deletions(-) create mode 100644 packages/extension/src/kernel-types.ts diff --git a/packages/extension/src/kernel-types.ts b/packages/extension/src/kernel-types.ts new file mode 100644 index 000000000..aed415d68 --- /dev/null +++ b/packages/extension/src/kernel-types.ts @@ -0,0 +1,82 @@ +/** + * A structured representation of an ocap kernel. + */ +type Queue = Type[]; + +type VatId = `v${number}`; +type RemoteId = `r${number}`; +type EndpointId = VatId | RemoteId; + +type RefTypeTag = 'o' | 'p'; +type RefDirectionTag = '+' | '-'; +type InnerKRef = `${RefTypeTag}${number}`; +type InnerERef = `${RefTypeTag}${RefDirectionTag}${number}`; + +type KRef = `k${InnerKRef}`; +type VRef = `v${InnerERef}`; +type RRef = `r${InnerERef}`; +type ERef = VRef | RRef; + +type CapData = { + body: string; + slots: string[]; +}; + +type Message = { + target: ERef | KRef; + method: string; + params: CapData; +}; + +// Per-endpoint persistent state +type EndpointState = { + name: string; + id: IdType; + nextExportObjectIdCounter: number; + nextExportPromiseIdCounter: number; + eRefToKRef: Map; + kRefToERef: Map; +}; + +type VatState = { + messagePort: MessagePort; + state: EndpointState; + source: string; + kvTable: Map; +}; + +type RemoteState = { + state: EndpointState; + connectToURL: string; + // more here about maintaining connection... +}; + +// Kernel persistent state +type KernelObject = { + owner: EndpointId; + reachableCount: number; + recognizableCount: number; +}; + +type PromiseState = 'unresolved' | 'fulfilled' | 'rejected'; + +type KernelPromise = { + decider: EndpointId; + state: PromiseState; + referenceCount: number; + messageQueue: Queue; + value: undefined | CapData; +}; + +// export temporarily to shut up lint whinges about unusedness +export type KernelState = { + runQueue: Queue; + nextVatIdCounter: number; + vats: Map; + nextRemoteIdCounter: number; + remotes: Map; + nextKernelObjectIdCounter: number; + kernelObjects: Map; + nextKernePromiseIdCounter: number; + kernelPromises: Map; +}; diff --git a/packages/extension/src/kernel-worker.ts b/packages/extension/src/kernel-worker.ts index a64c89bf7..896410a1f 100644 --- a/packages/extension/src/kernel-worker.ts +++ b/packages/extension/src/kernel-worker.ts @@ -19,86 +19,6 @@ const isKernelWorkerCommand = (value: unknown): value is KernelWorkerCommand => (value.method === KernelCommandMethod.KVSet || value.method === KernelCommandMethod.KVGet); -type Queue = Type[]; - -type VatId = `v${number}`; -type RemoteId = `r${number}`; -type EndpointId = VatId | RemoteId; - -type RefTypeTag = 'o' | 'p'; -type RefDirectionTag = '+' | '-'; -type InnerKRef = `${RefTypeTag}${number}`; -type InnerERef = `${RefTypeTag}${RefDirectionTag}${number}`; - -type KRef = `k${InnerKRef}`; -type VRef = `v${InnerERef}`; -type RRef = `r${InnerERef}`; -type ERef = VRef | RRef; - -type CapData = { - body: string; - slots: string[]; -}; - -type Message = { - target: ERef | KRef; - method: string; - params: CapData; -}; - -// Per-endpoint persistent state -type EndpointState = { - name: string; - id: IdType; - nextExportObjectIdCounter: number; - nextExportPromiseIdCounter: number; - eRefToKRef: Map; - kRefToERef: Map; -}; - -type VatState = { - messagePort: MessagePort; - state: EndpointState; - source: string; - kvTable: Map; -}; - -type RemoteState = { - state: EndpointState; - connectToURL: string; - // more here about maintaining connection... -}; - -// Kernel persistent state -type KernelObject = { - owner: EndpointId; - reachableCount: number; - recognizableCount: number; -}; - -type PromiseState = 'unresolved' | 'fulfilled' | 'rejected'; - -type KernelPromise = { - decider: EndpointId; - state: PromiseState; - referenceCount: number; - messageQueue: Queue; - value: undefined | CapData; -}; - -// export temporarily to shut up lint whinges about unusedness -export type KernelState = { - runQueue: Queue; - nextVatIdCounter: number; - vats: Map; - nextRemoteIdCounter: number; - remotes: Map; - nextKernelObjectIdCounter: number; - kernelObjects: Map; - nextKernePromiseIdCounter: number; - kernelPromises: Map; -}; - /** * Ensure that SQLite is initialized. * From 5b133e67cb8a77e11b8af3bbcc33a5c5abf2fa3f Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Wed, 9 Oct 2024 19:29:54 -0500 Subject: [PATCH 4/9] refactor(extension): Move kernel into kernel-worker. --- packages/extension/src/kernel-worker.ts | 132 ++++++++++++-- packages/extension/src/offscreen.ts | 219 +++++------------------- 2 files changed, 158 insertions(+), 193 deletions(-) diff --git a/packages/extension/src/kernel-worker.ts b/packages/extension/src/kernel-worker.ts index 896410a1f..ee2c7d90c 100644 --- a/packages/extension/src/kernel-worker.ts +++ b/packages/extension/src/kernel-worker.ts @@ -1,23 +1,18 @@ import './kernel-worker-trusted-prelude.js'; -import type { KernelCommand, KernelCommandReply } from '@ocap/kernel'; -import { isKernelCommand, KernelCommandMethod } from '@ocap/kernel'; -import { PostMessageDuplexStream } from '@ocap/streams'; +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'; -main().catch(console.error); +import { ExtensionVatWorkerClient } from './VatWorkerClient.js'; -// We temporarily have the kernel commands split between offscreen and kernel-worker -type KernelWorkerCommand = Extract< - KernelCommand, - | { method: typeof KernelCommandMethod.KVSet } - | { method: typeof KernelCommandMethod.KVGet } ->; +type MainArgs = { defaultVatId: VatId }; -const isKernelWorkerCommand = (value: unknown): value is KernelWorkerCommand => - isKernelCommand(value) && - (value.method === KernelCommandMethod.KVSet || - value.method === KernelCommandMethod.KVGet); +const logger = makeLogger('[kernel worker]'); + +main({ defaultVatId: 'v0' }).catch(console.error); /** * Ensure that SQLite is initialized. @@ -35,8 +30,27 @@ async function initDB(): Promise { /** * The main function for the offscreen script. + * + * @param options - The options bag. + * @param options.defaultVatId - The id to give the default vat. */ -async function main(): Promise { +async function main({ defaultVatId }: MainArgs): 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( + (listener) => globalThis.addEventListener('message', listener), + (listener) => globalThis.removeEventListener('message', listener), + ); + + const vatWorkerClient = new ExtensionVatWorkerClient( + (message) => clientPort.postMessage(message), + (listener) => { + clientPort.onmessage = listener; + }, + ); + + const start = performance.now(); + const kernelStream = new PostMessageDuplexStream< KernelCommand, KernelCommandReply @@ -46,14 +60,26 @@ async function main(): Promise { (listener) => globalThis.removeEventListener('message', listener), ); + // Initialize kernel store. + const { sqlKVGet, sqlKVSet } = await initDb(); + // Create kernel. + + const kernel = new Kernel(vatWorkerClient); + const iframeReadyP = kernel.launchVat({ id: defaultVatId }); + + await reply({ + method: KernelCommandMethod.InitKernel, + params: { defaultVat: defaultVatId, initTime: performance.now() - start }, + }); + // Handle messages from the console service worker for await (const message of kernelStream) { - if (isKernelWorkerCommand(message)) { + if (isKernelCommand(message)) { await handleKernelCommand(message); } else { - console.error('Received unexpected message', message); + logger.debug(`Received unexpected message ${stringify(message)}`); } } @@ -67,8 +93,19 @@ async function main(): Promise { async function handleKernelCommand({ method, params, - }: KernelWorkerCommand): Promise { + }: KernelCommand): Promise { 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: kvSet(params.key, params.value); await reply({ @@ -101,6 +138,43 @@ async function main(): Promise { } } + /** + * 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 iframeReadyP; + 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. * @@ -110,6 +184,28 @@ async function main(): 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.`; + } + } + /** * Coerce an unknown problem into an Error object. * diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index 192772912..6637e3b9a 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -1,22 +1,23 @@ +import { makePromiseKit } from '@endo/promise-kit'; import { - Kernel, - KernelCommandMethod, isKernelCommand, isKernelCommandReply, + KernelCommandMethod, } from '@ocap/kernel'; -import type { KernelCommandReply, KernelCommand, VatId } from '@ocap/kernel'; +import type { KernelCommandReply, KernelCommand } from '@ocap/kernel'; import { ChromeRuntimeTarget, initializeMessageChannel, ChromeRuntimeDuplexStream, PostMessageDuplexStream, } from '@ocap/streams'; -import { stringify } from '@ocap/utils'; +import { makeLogger } from '@ocap/utils'; import { makeIframeVatWorker } from './iframe-vat-worker.js'; -import { ExtensionVatWorkerClient } from './VatWorkerClient.js'; import { ExtensionVatWorkerServer } from './VatWorkerServer.js'; +const logger = makeLogger('[ocap glue]'); + main().catch(console.error); /** @@ -29,41 +30,14 @@ async function main(): Promise { ChromeRuntimeTarget.Background, ); - const startTime = performance.now(); - const defaultVatId = 'v0'; - - const kernelWorker = makeKernelWorker(); - - // Setup mock VatWorker service. - - const { port1: serverPort, port2: clientPort } = new MessageChannel(); - - const vatWorkerServer = new ExtensionVatWorkerServer( - (message: unknown, transfer?: Transferable[]) => - transfer - ? serverPort.postMessage(message, transfer) - : serverPort.postMessage(message), - (listener) => { - serverPort.onmessage = listener; - }, - (vatId: VatId) => makeIframeVatWorker(vatId, initializeMessageChannel), - ); - - vatWorkerServer.start(); - - const vatWorkerClient = new ExtensionVatWorkerClient( - (message: unknown) => clientPort.postMessage(message), - (listener) => { - clientPort.onmessage = listener; - }, - ); - - // Create kernel. - - const kernel = new Kernel(vatWorkerClient); - const iframeReadyP = kernel.launchVat({ id: defaultVatId }); - - // Setup glue. + const kernelWorker = await makeKernelWorker(); + const kernelInit = + makePromiseKit< + Extract< + KernelCommandReply, + { method: typeof KernelCommandMethod.InitKernel } + >['params'] + >(); /** * Reply to a command from the background script. @@ -80,119 +54,43 @@ async function main(): Promise { await Promise.all([ (async () => { for await (const message of backgroundStream) { - if (!isKernelCommand(message)) { - console.error('Offscreen received unexpected message', message); - continue; - } - - await handleKernelCommand(message); + await kernelInit.promise; + isKernelCommand(message) + ? await kernelWorker.sendMessage(message) + : logger.debug('Received unexpected message', message); } })(), kernelWorker.receiveMessages(), ]); - /** - * Handle a KernelCommand received from the background script. - * - * @param command - The command to handle. - */ - async function handleKernelCommand(command: KernelCommand): Promise { - const { method, params } = command; - switch (method) { - case KernelCommandMethod.InitKernel: - throw new Error('background should not call init kernel'); - case KernelCommandMethod.Ping: - await replyToBackground({ method, params: 'pong' }); - break; - case KernelCommandMethod.Evaluate: - await handleVatTestCommand({ method, params }); - break; - case KernelCommandMethod.CapTpCall: - await handleVatTestCommand({ method, params }); - break; - case KernelCommandMethod.KVGet: - await kernelWorker.sendMessage({ method, params }); - break; - case KernelCommandMethod.KVSet: - await kernelWorker.sendMessage({ method, params }); - break; - default: - console.error( - 'Offscreen received unexpected kernel 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 iframeReadyP; - switch (method) { - case KernelCommandMethod.Evaluate: - await replyToBackground({ - method, - params: await evaluate(vat.id, params), - }); - break; - case KernelCommandMethod.CapTpCall: - await replyToBackground({ - 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 }, - ); - } - } - - /** - * 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.`; - } - } - /** * Make the SQLite kernel worker. * * @returns An object with methods to send and receive messages from the kernel worker. */ - function makeKernelWorker(): { + async function makeKernelWorker(): Promise<{ sendMessage: (message: KernelCommand) => Promise; receiveMessages: () => Promise; - } { + }> { const worker = new Worker('kernel-worker.js', { type: 'module' }); + + // Note we must setup the worker MessageChannel before initializing the stream, + // because the stream will close if it receives an unrecognized message. + const workerPort = await initializeMessageChannel((message, transfer) => + worker.postMessage(message, transfer), + ); + + const vatWorkerServer = new ExtensionVatWorkerServer( + (message, transfer?) => + transfer + ? workerPort.postMessage(message, transfer) + : workerPort.postMessage(message), + (listener) => workerPort.addEventListener('message', listener), + (vatId) => makeIframeVatWorker(vatId, initializeMessageChannel), + ); + + vatWorkerServer.start(); + const workerStream = new PostMessageDuplexStream< KernelCommandReply, KernelCommand @@ -206,38 +104,16 @@ async function main(): Promise { // For the time being, the only messages that come from the kernel worker are replies to actions // initiated from the console, so just forward these replies to the console. This will need to // change once this offscreen script is providing services to the kernel worker that don't - // involve the user (e.g., for things the worker can't do for itself, such as create an - // offscreen iframe). - - // XXX TODO: Using the IframeMessage type here assumes that the set of response messages is the - // same as (and aligns perfectly with) the set of command messages, which is horribly, terribly, - // awfully wrong. Need to add types to account for the replies. + // involve the user. for await (const message of workerStream) { if (!isKernelCommandReply(message)) { - console.error('kernel received unexpected message', message); - return; + logger.debug('Received unexpected reply', message); } - const { method, params } = message; - let result: string; - const possibleError = params as unknown as Error; - if (possibleError?.message && possibleError?.stack) { - // XXX TODO: The following is an egregious hack which is barely good enough for manual testing - // but not acceptable for serious use. We should be passing some kind of proper error - // indication back so that the recipient will experience a thrown exception or rejected - // promise, instead of having to look for a magic string. This is tolerable only so long as - // the sole eventual recipient is a human eyeball, and even then it's questionable. - result = `ERROR: ${possibleError.message}`; - } else { - // The InitKernel reply has an object params. - result = String(params); + if (message.method === KernelCommandMethod.InitKernel) { + logger.debug('Kernel initialized.'); + kernelInit.resolve(message.params); } - const reply = { method, params: result ?? null }; - if (!isKernelCommandReply(reply)) { - // Internal error. - console.error('Malformed command reply', reply); - return; - } - await replyToBackground(reply); + await replyToBackground(message); } }; @@ -245,13 +121,6 @@ async function main(): Promise { await workerStream.write(message); }; - iframeReadyP.then(async () => { - await replyToBackground({ - method: KernelCommandMethod.InitKernel, - params: { initTime: performance.now() - startTime, defaultVat: defaultVatId }, - }) - }); - return { sendMessage, receiveMessages, From 916474f20f268f4ccbe56e8e7d7ffbea4e31b30f Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:21:03 -0700 Subject: [PATCH 5/9] Update offscreen.ts (#144) --- eslint.config.mjs | 10 ++++++---- packages/extension/src/offscreen.ts | 22 +++++++++------------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index b79f8dc1a..cbe6efdab 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -18,6 +18,7 @@ const config = [ ...options, files: ['**/*.test.{ts,js}'], })), + { ignores: [ 'yarn.config.cjs', @@ -29,6 +30,7 @@ const config = [ '**/coverage', ], }, + { languageOptions: { parserOptions: { @@ -47,15 +49,15 @@ const config = [ }, ], - // This prevents using Node.js and/or browser specific globals. We - // currently use both in our codebase, so this rule is disabled. - 'no-restricted-globals': 'off', - 'import-x/extensions': 'off', 'import-x/no-unassigned-import': 'off', // This prevents pretty formatting of comments with multi-line lists entries. 'jsdoc/check-indentation': 'off', + + // This prevents using Node.js and/or browser specific globals. We + // currently use both in our codebase, so this rule is disabled. + 'no-restricted-globals': 'off', }, }, diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index 6637e3b9a..bf32be35d 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -31,13 +31,7 @@ async function main(): Promise { ); const kernelWorker = await makeKernelWorker(); - const kernelInit = - makePromiseKit< - Extract< - KernelCommandReply, - { method: typeof KernelCommandMethod.InitKernel } - >['params'] - >(); + const kernelInitKit = makePromiseKit(); /** * Reply to a command from the background script. @@ -52,15 +46,15 @@ async function main(): Promise { // Handle messages from the background service worker and the kernel SQLite worker. await Promise.all([ - (async () => { + kernelWorker.receiveMessages(), + kernelInitKit.promise.then(async () => { for await (const message of backgroundStream) { - await kernelInit.promise; isKernelCommand(message) ? await kernelWorker.sendMessage(message) : logger.debug('Received unexpected message', message); } - })(), - kernelWorker.receiveMessages(), + return undefined; + }), ]); /** @@ -107,11 +101,13 @@ async function main(): Promise { // involve the user. for await (const message of workerStream) { if (!isKernelCommandReply(message)) { - logger.debug('Received unexpected reply', message); + logger.error('Received unexpected reply', message); + continue; } + if (message.method === KernelCommandMethod.InitKernel) { logger.debug('Kernel initialized.'); - kernelInit.resolve(message.params); + kernelInitKit.resolve(); } await replyToBackground(message); } From de34ebba21417138e120e0e12fdc1f28b79f16b7 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 10 Oct 2024 16:42:25 -0500 Subject: [PATCH 6/9] tidy up --- packages/extension/src/kernel-worker.ts | 25 ++++++++++--------------- packages/extension/src/offscreen.ts | 15 +++------------ 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/packages/extension/src/kernel-worker.ts b/packages/extension/src/kernel-worker.ts index ee2c7d90c..bed827aea 100644 --- a/packages/extension/src/kernel-worker.ts +++ b/packages/extension/src/kernel-worker.ts @@ -1,8 +1,8 @@ import './kernel-worker-trusted-prelude.js'; import type { KernelCommand, KernelCommandReply, VatId } from '@ocap/kernel'; -import { isKernelCommand, Kernel, KernelCommandMethod } from '@ocap/kernel'; +import { Kernel, KernelCommandMethod } from '@ocap/kernel'; import { PostMessageDuplexStream, receiveMessagePort } from '@ocap/streams'; -import { makeLogger, stringify } from '@ocap/utils'; +import { stringify } from '@ocap/utils'; import type { Database } from '@sqlite.org/sqlite-wasm'; import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; @@ -10,8 +10,6 @@ import { ExtensionVatWorkerClient } from './VatWorkerClient.js'; type MainArgs = { defaultVatId: VatId }; -const logger = makeLogger('[kernel worker]'); - main({ defaultVatId: 'v0' }).catch(console.error); /** @@ -49,7 +47,7 @@ async function main({ defaultVatId }: MainArgs): Promise { }, ); - const start = performance.now(); + const startTime = performance.now(); const kernelStream = new PostMessageDuplexStream< KernelCommand, @@ -67,21 +65,18 @@ async function main({ defaultVatId }: MainArgs): Promise { // Create kernel. const kernel = new Kernel(vatWorkerClient); - const iframeReadyP = kernel.launchVat({ id: defaultVatId }); + const vatReadyP = kernel.launchVat({ id: defaultVatId }); await reply({ method: KernelCommandMethod.InitKernel, - params: { defaultVat: defaultVatId, initTime: performance.now() - start }, + params: { + defaultVat: defaultVatId, + initTime: performance.now() - startTime, + }, }); // Handle messages from the console service worker - for await (const message of kernelStream) { - if (isKernelCommand(message)) { - await handleKernelCommand(message); - } else { - logger.debug(`Received unexpected message ${stringify(message)}`); - } - } + await kernelStream.drain(handleKernelCommand); /** * Handle a KernelCommand sent from the offscreen. @@ -151,7 +146,7 @@ async function main({ defaultVatId }: MainArgs): Promise { >, ): Promise { const { method, params } = command; - const vat = await iframeReadyP; + const vat = await vatReadyP; switch (method) { case KernelCommandMethod.Evaluate: await reply({ diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index bf32be35d..4bfdc87a7 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -1,9 +1,5 @@ import { makePromiseKit } from '@endo/promise-kit'; -import { - isKernelCommand, - isKernelCommandReply, - KernelCommandMethod, -} from '@ocap/kernel'; +import { isKernelCommand, KernelCommandMethod } from '@ocap/kernel'; import type { KernelCommandReply, KernelCommand } from '@ocap/kernel'; import { ChromeRuntimeTarget, @@ -51,7 +47,7 @@ async function main(): Promise { for await (const message of backgroundStream) { isKernelCommand(message) ? await kernelWorker.sendMessage(message) - : logger.debug('Received unexpected message', message); + : logger.info('Received unexpected message', message); } return undefined; }), @@ -100,13 +96,8 @@ async function main(): Promise { // change once this offscreen script is providing services to the kernel worker that don't // involve the user. for await (const message of workerStream) { - if (!isKernelCommandReply(message)) { - logger.error('Received unexpected reply', message); - continue; - } - if (message.method === KernelCommandMethod.InitKernel) { - logger.debug('Kernel initialized.'); + logger.info('Kernel initialized.'); kernelInitKit.resolve(); } await replyToBackground(message); From a9734dabcb97b48cbf90833290b098679186bcb3 Mon Sep 17 00:00:00 2001 From: Erik Marks <25517051+rekmarks@users.noreply.github.com> Date: Thu, 10 Oct 2024 15:09:22 -0700 Subject: [PATCH 7/9] feat(extension): Restore error logs in message handlers (#146) --- packages/extension/src/offscreen.ts | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index 4bfdc87a7..109a71146 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -1,5 +1,9 @@ import { makePromiseKit } from '@endo/promise-kit'; -import { isKernelCommand, KernelCommandMethod } from '@ocap/kernel'; +import { + isKernelCommand, + isKernelCommandReply, + KernelCommandMethod, +} from '@ocap/kernel'; import type { KernelCommandReply, KernelCommand } from '@ocap/kernel'; import { ChromeRuntimeTarget, @@ -14,7 +18,7 @@ import { ExtensionVatWorkerServer } from './VatWorkerServer.js'; const logger = makeLogger('[ocap glue]'); -main().catch(console.error); +main().catch(logger.error); /** * The main function for the offscreen script. @@ -45,9 +49,12 @@ async function main(): Promise { kernelWorker.receiveMessages(), kernelInitKit.promise.then(async () => { for await (const message of backgroundStream) { - isKernelCommand(message) - ? await kernelWorker.sendMessage(message) - : logger.info('Received unexpected message', message); + if (!isKernelCommand(message)) { + logger.error('Offscreen received unexpected message', message); + continue; + } + + await kernelWorker.sendMessage(message); } return undefined; }), @@ -96,6 +103,11 @@ async function main(): Promise { // change once this offscreen script is providing services to the kernel worker that don't // involve the user. for await (const message of workerStream) { + if (!isKernelCommandReply(message)) { + logger.error('Kernel received unexpected message', message); + continue; + } + if (message.method === KernelCommandMethod.InitKernel) { logger.info('Kernel initialized.'); kernelInitKit.resolve(); From 523a9485f33ae0e670532cf479609240166d7835 Mon Sep 17 00:00:00 2001 From: grypez <143971198+grypez@users.noreply.github.com> Date: Thu, 10 Oct 2024 17:17:06 -0500 Subject: [PATCH 8/9] guard drain handler --- packages/extension/src/kernel-worker.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/extension/src/kernel-worker.ts b/packages/extension/src/kernel-worker.ts index bed827aea..ccecff356 100644 --- a/packages/extension/src/kernel-worker.ts +++ b/packages/extension/src/kernel-worker.ts @@ -1,8 +1,8 @@ import './kernel-worker-trusted-prelude.js'; import type { KernelCommand, KernelCommandReply, VatId } from '@ocap/kernel'; -import { Kernel, KernelCommandMethod } from '@ocap/kernel'; +import { isKernelCommand, Kernel, KernelCommandMethod } from '@ocap/kernel'; import { PostMessageDuplexStream, receiveMessagePort } from '@ocap/streams'; -import { stringify } from '@ocap/utils'; +import { makeLogger, stringify } from '@ocap/utils'; import type { Database } from '@sqlite.org/sqlite-wasm'; import sqlite3InitModule from '@sqlite.org/sqlite-wasm'; @@ -10,6 +10,8 @@ import { ExtensionVatWorkerClient } from './VatWorkerClient.js'; type MainArgs = { defaultVatId: VatId }; +const logger = makeLogger('[kernel worker]'); + main({ defaultVatId: 'v0' }).catch(console.error); /** @@ -82,13 +84,15 @@ async function main({ defaultVatId }: MainArgs): Promise { * Handle a KernelCommand sent from the offscreen. * * @param command - The KernelCommand to handle. - * @param command.method - The command method. - * @param command.params - The command params. */ - async function handleKernelCommand({ - method, - params, - }: KernelCommand): Promise { + 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.'); @@ -164,7 +168,6 @@ async function main({ defaultVatId }: MainArgs): Promise { console.error( 'Offscreen received unexpected vat command', // @ts-expect-error Runtime does not respect "never". - { method: method.valueOf(), params }, ); } From cc274b8fb61ed3efd83f3f0eb08a06523e50cf9c Mon Sep 17 00:00:00 2001 From: Erik Marks Date: Thu, 10 Oct 2024 15:31:59 -0700 Subject: [PATCH 9/9] refactor(extension): Appease the gremlin in the CI linter --- packages/extension/src/offscreen.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/extension/src/offscreen.ts b/packages/extension/src/offscreen.ts index 109a71146..8210b89c5 100644 --- a/packages/extension/src/offscreen.ts +++ b/packages/extension/src/offscreen.ts @@ -18,7 +18,7 @@ import { ExtensionVatWorkerServer } from './VatWorkerServer.js'; const logger = makeLogger('[ocap glue]'); -main().catch(logger.error); +main().catch((error) => logger.error(error)); /** * The main function for the offscreen script.