blob: 14f4f4a0a51b1680d735e5a57bae8ea493a63d31 [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
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);
}