blob: 91451af73a92dba92dd13007b5c598b4487582d8 [file]
import { randomUUID } from 'node:crypto';
import { link, mkdir, open, readFile, rm } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { requireClientInstanceId } from '../protocol/index.js';
const CLIENT_IDENTITY_SCHEMA_VERSION = 1;
const CLIENT_IDENTITY_MAX_BYTES = 512;
interface ClientIdentityDocument {
readonly schemaVersion: typeof CLIENT_IDENTITY_SCHEMA_VERSION;
readonly clientInstanceId: string;
}
export async function loadOrCreateRuntimeHostClientInstanceId(path: string): Promise<string> {
const existing = await readClientIdentity(path);
if (existing !== undefined) return existing.clientInstanceId;
const directory = dirname(path);
await mkdir(directory, { recursive: true, mode: 0o700 });
const document: ClientIdentityDocument = {
schemaVersion: CLIENT_IDENTITY_SCHEMA_VERSION,
clientInstanceId: randomUUID(),
};
const temporaryPath = join(directory, `.runtime-host-client-${randomUUID()}.tmp`);
const handle = await open(temporaryPath, 'wx', 0o600);
try {
try {
await handle.writeFile(`${JSON.stringify(document)}\n`, 'utf8');
await handle.sync();
} finally {
await handle.close();
}
await link(temporaryPath, path);
await syncDirectory(directory);
return document.clientInstanceId;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
const winner = await readClientIdentity(path);
if (winner === undefined) throw new Error('Runtime Host Client identity disappeared');
return winner.clientInstanceId;
} finally {
await rm(temporaryPath, { force: true });
}
}
async function readClientIdentity(path: string): Promise<ClientIdentityDocument | undefined> {
let bytes: Buffer;
try {
bytes = await readFile(path);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined;
throw error;
}
if (bytes.length > CLIENT_IDENTITY_MAX_BYTES) {
throw new Error('Runtime Host Client identity exceeds its size limit');
}
let value: unknown;
try {
value = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(bytes));
} catch (error) {
throw new Error('Runtime Host Client identity is not valid JSON', { cause: error });
}
if (!isRecord(value) || value.schemaVersion !== CLIENT_IDENTITY_SCHEMA_VERSION) {
throw new Error('Runtime Host Client identity has an unsupported schema');
}
if (Object.keys(value).some((key) => key !== 'schemaVersion' && key !== 'clientInstanceId')) {
throw new Error('Runtime Host Client identity contains unknown fields');
}
return {
schemaVersion: CLIENT_IDENTITY_SCHEMA_VERSION,
clientInstanceId: requireClientInstanceId(value.clientInstanceId),
};
}
async function syncDirectory(path: string): Promise<void> {
if (process.platform === 'win32') return;
const handle = await open(path, 'r');
try {
await handle.sync();
} finally {
await handle.close();
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}