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
92 changes: 92 additions & 0 deletions packages/kernel-test/src/service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { Far } from '@endo/marshal';
import { makeSQLKernelDatabase } from '@metamask/kernel-store/sqlite/nodejs';
import { waitUntilQuiescent } from '@metamask/kernel-utils';
import { Kernel, krefOf } from '@metamask/ocap-kernel';
import type { SlotValue } from '@metamask/ocap-kernel';
import { describe, expect, it } from 'vitest';

import {
getBundleSpec,
makeKernel,
makeTestLogger,
runTestVats,
extractTestLogs,
} from './utils.ts';

const testSubcluster = {
bootstrap: 'main',
forceReset: true,
services: ['testService'],
vats: {
main: {
bundleSpec: getBundleSpec('service-vat'),
parameters: {
name: 'main',
},
},
},
};

describe('Kernel service object invocation', () => {
let kernel: Kernel;

const testService = Far('serviceObject', {
async getStuff(obj: SlotValue, tag: string): Promise<string> {
return `${tag} -- ${krefOf(obj)}`;
},
});

it('can invoke a kernel service and get an answer', async () => {
const kernelDatabase = await makeSQLKernelDatabase({
dbFilename: ':memory:',
});
const { logger, entries } = makeTestLogger();
kernel = await makeKernel(kernelDatabase, true, logger);
kernel.registerKernelServiceObject('testService', testService);

await runTestVats(kernel, testSubcluster);

// ko1 ::= the (test) service object
// ko2 ::= test vat root object
// ko3 ::= internal object generated inside test vat to have its kref extracted

await kernel.queueMessage('ko2', 'go', []);
await waitUntilQuiescent(100);
const testLogs = extractTestLogs(entries);
expect(testLogs).toStrictEqual(['kernel service returns hello -- ko3']);
});

it('configure subcluster with unknown service throws', async () => {
const kernelDatabase = await makeSQLKernelDatabase({
dbFilename: ':memory:',
});
const { logger } = makeTestLogger();
kernel = await makeKernel(kernelDatabase, true, logger);

await expect(runTestVats(kernel, testSubcluster)).rejects.toThrow(
`no registered kernel service 'testService'`,
);
});

it('invoking unknown service method throws', async () => {
const kernelDatabase = await makeSQLKernelDatabase({
dbFilename: ':memory:',
});
const { logger, entries } = makeTestLogger();
kernel = await makeKernel(kernelDatabase, true, logger);
kernel.registerKernelServiceObject('testService', testService);

await runTestVats(kernel, testSubcluster);

// ko1 ::= the (test) service object
// ko2 ::= test vat root object
// ko3 ::= internal object generated inside test vat to have its kref extracted

await kernel.queueMessage('ko2', 'goBadly', []);
await waitUntilQuiescent(100);
const testLogs = extractTestLogs(entries);
expect(testLogs).toStrictEqual([
`kernel service threw: unknown service method 'nonexistentMethod'`,
]);
});
});
47 changes: 47 additions & 0 deletions packages/kernel-test/src/vats/service-vat.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { E } from '@endo/eventual-send';
import { Far } from '@endo/marshal';

/**
* Build function for running a test of kernel service objects.
*
* @param {unknown} vatPowers - Special powers granted to this vat.
* @param {unknown} parameters - Initialization parameters from the vat's config object.
* @returns {unknown} The root object for the new vat.
*/
export function buildRootObject(vatPowers, parameters) {
const name = parameters?.name ?? 'anonymous';
const logger = vatPowers.logger.subLogger({ tags: ['test', name] });
const tlog = (...args) => logger.log(...args);
console.log(`buildRootObject "${name}"`);

const thing = Far('thing', {});
let testService;

const mainVatRoot = Far('root', {
async bootstrap(_vats, services) {
console.log(`vat ${name} is bootstrap`);
testService = services.testService;
},
async go() {
const serviceResult = await E(testService).getStuff(thing, 'hello');
tlog(`kernel service returns ${serviceResult}`);
await E(mainVatRoot).loopback();
},
async goBadly() {
try {
const serviceResult = await E(testService).nonexistentMethod(
thing,
'hello',
);
tlog(`kernel service returns ${serviceResult} and it shouldn't have`);
} catch (problem) {
tlog(`kernel service threw: ${problem.message}`);
}
await E(mainVatRoot).loopback();
},
loopback() {
return undefined;
},
});
return mainVatRoot;
}
83 changes: 80 additions & 3 deletions packages/ocap-kernel/src/Kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { KernelQueue } from './KernelQueue.ts';
import { KernelRouter } from './KernelRouter.ts';
import { kernelHandlers } from './rpc/index.ts';
import type { PingVatResult } from './rpc/index.ts';
import { kslot } from './services/kernel-marshal.ts';
import { kslot, kser, kunser } from './services/kernel-marshal.ts';
import type { SlotValue } from './services/kernel-marshal.ts';
import { makeKernelStore } from './store/index.ts';
import type { KernelStore } from './store/index.ts';
Expand All @@ -32,11 +32,18 @@ import type {
VatConfig,
KernelStatus,
Subcluster,
Message,
} from './types.ts';
import { ROOT_OBJECT_VREF, isClusterConfig } from './types.ts';
import { Fail } from './utils/assert.ts';
import { Fail, assert } from './utils/assert.ts';
import { VatHandle } from './VatHandle.ts';

type KernelService = {
name: string;
kref: string;
service: object;
};

export class Kernel {
/** Command channel from the controlling console/browser extension/test driver */
readonly #commandStream: DuplexStream<JsonRpcCall, JsonRpcResponse>;
Expand All @@ -61,6 +68,11 @@ export class Kernel {
/** The kernel's router */
readonly #kernelRouter: KernelRouter;

/** Objects providing custom or kernel-privileged services to vats. */
readonly #kernelServicesByName: Map<string, KernelService> = new Map();

readonly #kernelServicesByObject: Map<string, KernelService> = new Map();

/**
* Construct a new kernel instance.
*
Expand Down Expand Up @@ -98,6 +110,7 @@ export class Kernel {
this.#kernelStore,
this.#kernelQueue,
this.#getVat.bind(this),
this.#invokeKernelService.bind(this),
);
harden(this);
}
Expand Down Expand Up @@ -370,9 +383,21 @@ export class Kernel {
rootIds[vatName] = rootRef;
roots[vatName] = kslot(rootRef, 'vatRoot');
}
const services: Record<string, SlotValue> = {};
if (config.services) {
for (const name of config.services) {
const possibleService = this.#kernelServicesByName.get(name);
if (possibleService) {
const { kref } = possibleService;
services[name] = kslot(kref);
} else {
throw Error(`no registered kernel service '${name}'`);
}
}
}
const bootstrapRoot = rootIds[config.bootstrap];
if (bootstrapRoot) {
return this.queueMessage(bootstrapRoot, 'bootstrap', [roots]);
return this.queueMessage(bootstrapRoot, 'bootstrap', [roots, services]);
}
return undefined;
}
Expand Down Expand Up @@ -632,5 +657,57 @@ export class Kernel {
}
this.#kernelStore.collectGarbage();
}

registerKernelServiceObject(name: string, service: object): void {
const kref = this.#kernelStore.initKernelObject('kernel');
const kernelService = { name, kref, service };
this.#kernelServicesByName.set(name, kernelService);
this.#kernelServicesByObject.set(kref, kernelService);
}

async #invokeKernelService(target: KRef, message: Message): Promise<void> {
const kernelService = this.#kernelServicesByObject.get(target);
if (!kernelService) {
throw Error(`no registered service for ${target}`);
}
const { methargs, result } = message;
const [method, args] = kunser(methargs) as [string, unknown[]];
assert.typeof(method, 'string');
if (result) {
assert.typeof(result, 'string');
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
const service = kernelService.service as Record<string, Function>;
const methodFunction = service[method];
if (methodFunction === undefined) {
if (result) {
this.#kernelQueue.resolvePromises('kernel', [
[result, true, kser(Error(`unknown service method '${method}'`))],
]);
} else {
this.#logger.error(`unknown service method '${method}'`);
}
return;
}
assert.typeof(methodFunction, 'function');
assert(Array.isArray(args));
try {
// eslint-disable-next-line prefer-spread
const resultValue = await methodFunction.apply(null, args);
if (result) {
this.#kernelQueue.resolvePromises('kernel', [
[result, false, kser(resultValue)],
]);
}
} catch (problem) {
if (result) {
this.#kernelQueue.resolvePromises('kernel', [
[result, true, kser(problem)],
]);
} else {
this.#logger.error('error in kernel service method:', problem);
}
}
}
}
harden(Kernel);
2 changes: 1 addition & 1 deletion packages/ocap-kernel/src/KernelQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export class KernelQueue {
vatId: VatId | undefined,
resolutions: VatOneResolution[],
): void {
if (vatId) {
if (vatId && vatId !== 'kernel') {
insistVatId(vatId);
}
for (const resolution of resolutions) {
Expand Down
35 changes: 29 additions & 6 deletions packages/ocap-kernel/src/KernelRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { isPromiseRef } from './store/utils/promise-ref.ts';
import type {
VatId,
KRef,
Message,
RunQueueItem,
RunQueueItemSend,
RunQueueItemBringOutYourDead,
Expand Down Expand Up @@ -42,21 +43,30 @@ export class KernelRouter {
/** A function that returns a vat handle for a given vat id. */
readonly #getVat: (vatId: VatId) => VatHandle;

/** A function that invokes a method on a kernel service. */
readonly #invokeKernelService: (
target: KRef,
message: Message,
) => Promise<void>;

/**
* Construct a new KernelRouter.
*
* @param kernelStore - The kernel's store.
* @param kernelQueue - The kernel's queue.
* @param getVat - A function that returns a vat handle for a given vat id.
* @param invokeKernelService - A function that calls a method on a kernel service object.
*/
constructor(
kernelStore: KernelStore,
kernelQueue: KernelQueue,
getVat: (vatId: VatId) => VatHandle,
invokeKernelService: (target: KRef, message: Message) => Promise<void>,
) {
this.#kernelStore = kernelStore;
this.#kernelQueue = kernelQueue;
this.#getVat = getVat;
this.#invokeKernelService = invokeKernelService;
}

/**
Expand Down Expand Up @@ -201,8 +211,9 @@ export class KernelRouter {
`@@@@ deliver ${vatId} send ${target}<-${JSON.stringify(message)}`,
);
if (vatId) {
const vat = this.#getVat(vatId);
if (vat) {
const isKernelServiceMessage = vatId === 'kernel';
const vat = isKernelServiceMessage ? null : this.#getVat(vatId);
if (vat || isKernelServiceMessage) {
if (message.result) {
if (typeof message.result !== 'string') {
throw TypeError('message result must be a string');
Expand All @@ -213,6 +224,8 @@ export class KernelRouter {
'deliver|send|result',
);
}
}
if (vat) {
const vatTarget = this.#kernelStore.translateRefKtoV(
vatId,
target,
Expand All @@ -223,13 +236,15 @@ export class KernelRouter {
message,
);
crankResults = await vat.deliverMessage(vatTarget, vatMessage);
this.#kernelStore.decrementRefCount(target, 'deliver|send|target');
for (const slot of message.methargs.slots) {
this.#kernelStore.decrementRefCount(slot, 'deliver|send|slot');
}
} else if (isKernelServiceMessage) {
crankResults = await this.#deliverKernelServiceMessage(target, message);
} else {
Fail`no owner for kernel object ${target}`;
}
this.#kernelStore.decrementRefCount(target, 'deliver|send|target');
for (const slot of message.methargs.slots) {
this.#kernelStore.decrementRefCount(slot, 'deliver|send|slot');
}
} else {
this.#kernelStore.enqueuePromiseMessage(target, message);
}
Expand All @@ -240,6 +255,14 @@ export class KernelRouter {
return crankResults;
}

async #deliverKernelServiceMessage(
target: KRef,
message: Message,
): Promise<CrankResults> {
await this.#invokeKernelService(target, message);
return { didDelivery: 'kernel' };
}

/**
* Deliver a 'notify' run queue item.
*
Expand Down
2 changes: 2 additions & 0 deletions packages/ocap-kernel/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ describe('index', () => {
'VatSupervisor',
'isVatConfig',
'isVatId',
'krefOf',
'kser',
'kslot',
'kunser',
'makeKernelStore',
'parseRef',
Expand Down
3 changes: 2 additions & 1 deletion packages/ocap-kernel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ export {
KernelStatusStruct,
SubclusterStruct,
} from './types.ts';
export { kunser, kser } from './services/kernel-marshal.ts';
export { kunser, kser, kslot, krefOf } from './services/kernel-marshal.ts';
export type { SlotValue } from './services/kernel-marshal.ts';
export { makeKernelStore } from './store/index.ts';
export type { KernelStore } from './store/index.ts';
export { parseRef } from './store/utils/parse-ref.ts';
Loading
Loading