Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | import '@metamask/kernel-shims/endoify-node';
import { makeKernel } from '@metamask/kernel-node-runtime';
import { startDaemon } from '@metamask/kernel-node-runtime/daemon';
import type { DaemonHandle } from '@metamask/kernel-node-runtime/daemon';
import type { LogEntry } from '@metamask/logger';
import { Logger } from '@metamask/logger';
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { getOcapHome } from '../ocap-home.ts';
import { isProcessAlive } from '../utils.ts';
main().catch((error) => {
process.stderr.write(`Daemon fatal: ${String(error)}\n`);
process.exitCode = 1;
});
/**
* Main daemon entry point. Starts the daemon process and keeps it running.
*/
async function main(): Promise<void> {
const ocapDir = getOcapHome();
await mkdir(ocapDir, { recursive: true });
const logPath = join(ocapDir, 'daemon.log');
const logger = new Logger({
tags: ['daemon'],
transports: [makeFileTransport(logPath)],
});
const socketPath =
process.env.OCAP_SOCKET_PATH ?? join(ocapDir, 'daemon.sock');
const dbFilename = join(ocapDir, 'kernel.sqlite');
const { kernel, kernelDatabase } = await makeKernel({
resetStorage: false,
dbFilename,
logger,
});
const pidPath = join(ocapDir, 'daemon.pid');
// Interlock: refuse to start a second daemon under the same OCAP_HOME.
// The socket-binding interlock in startDaemon handles the live-socket
// case; this catches the rarer case where an orphan still holds the
// kernel.sqlite locks but its socket file has already been unlinked.
const existingPid = await readDaemonPid(pidPath);
if (existingPid !== undefined && isProcessAlive(existingPid)) {
throw new Error(
`Daemon is already running (pid ${existingPid}) under ${ocapDir}. ` +
`Use 'ocap daemon stop' first.`,
);
}
if (existingPid !== undefined) {
// Stale pid file — owner is dead, take over.
await rm(pidPath, { force: true });
}
let handle: DaemonHandle;
try {
await kernel.initIdentity();
await writeFile(pidPath, String(process.pid));
handle = await startDaemon({
socketPath,
kernel,
kernelDatabase,
onShutdown: async () => shutdown('RPC shutdown'),
});
} catch (error) {
try {
kernel.stop().catch(() => undefined);
kernelDatabase.close();
} catch {
// Best-effort cleanup.
}
rm(pidPath, { force: true }).catch(() => undefined);
throw error;
}
logger.info(`Daemon started. Socket: ${handle.socketPath}`);
let shutdownPromise: Promise<void> | undefined;
/**
* Shut down the daemon idempotently. Concurrent calls coalesce.
*
* @param reason - A label describing why shutdown was triggered.
* @returns A promise that resolves when shutdown completes.
*/
async function shutdown(reason: string): Promise<void> {
if (shutdownPromise === undefined) {
logger.info(`Shutting down (${reason})...`);
shutdownPromise = handle.close().finally(() => {
rm(pidPath, { force: true }).catch(() => undefined);
});
}
return shutdownPromise;
}
process.on('SIGTERM', () => {
shutdown('SIGTERM').catch(() => (process.exitCode = 1));
});
process.on('SIGINT', () => {
shutdown('SIGINT').catch(() => (process.exitCode = 1));
});
}
/**
* Read the PID from `daemon.pid`, returning `undefined` if missing or
* unparseable.
*
* @param pidPath - Path to the pid file.
* @returns The parsed pid, or `undefined`.
*/
async function readDaemonPid(pidPath: string): Promise<number | undefined> {
let raw: string;
try {
raw = await readFile(pidPath, 'utf-8');
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
return undefined;
}
throw error;
}
const pid = Number(raw.trim());
return Number.isFinite(pid) && pid > 0 ? pid : undefined;
}
/**
* Create a file transport that writes logs to a file.
*
* @param logPath - The log file path.
* @returns A log transport function.
*/
function makeFileTransport(logPath: string) {
// eslint-disable-next-line @typescript-eslint/no-require-imports, n/global-require -- need sync fs for log transport
const fs = require('node:fs') as typeof import('node:fs');
return (entry: LogEntry): void => {
const line = `[${new Date().toISOString()}] [${entry.level}] ${entry.message ?? ''} ${(entry.data ?? []).map(String).join(' ')}\n`;
// eslint-disable-next-line n/no-sync -- synchronous write needed for log transport reliability
fs.appendFileSync(logPath, line);
};
}
|