127 lines
4.4 KiB
TypeScript
127 lines
4.4 KiB
TypeScript
import * as net from 'node:net';
|
|
import * as os from 'node:os';
|
|
import * as path from 'node:path';
|
|
import * as fs from 'node:fs';
|
|
import * as crypto from 'node:crypto';
|
|
import {type ChildProcess, spawn} from 'node:child_process';
|
|
import {IPCCommon} from './common.js';
|
|
import {timeout} from './util.js';
|
|
|
|
const IPC_SOCKET_ARG = 'ipc-socket';
|
|
const ACCEPT_TIMEOUT_MS = 10000;
|
|
|
|
export class ParentIPC extends IPCCommon {
|
|
private readonly cmdPath: string;
|
|
private readonly cmdArgs: string[];
|
|
private cmd: ChildProcess | null = null;
|
|
private readonly listener: net.Server;
|
|
private cmdExitResult: { code: number | null, signal: string | null } | null = null;
|
|
private cmdExitCallbacks: ((result: { code: number | null, signal: string | null }) => void)[] = [];
|
|
|
|
constructor(cmdPath: string, cmdArgs: string[], ...localApis: object[]) {
|
|
const socketPath = path.join(os.tmpdir(), `kitten-ipc-${ process.pid }-${ crypto.randomInt(2**48 - 1) }.sock`);
|
|
super(localApis, socketPath);
|
|
|
|
this.cmdPath = cmdPath;
|
|
if (cmdArgs.includes(`--${ IPC_SOCKET_ARG }`)) {
|
|
throw new Error(`you should not use '--${ IPC_SOCKET_ARG }' argument in your command`);
|
|
}
|
|
this.cmdArgs = cmdArgs;
|
|
|
|
this.listener = net.createServer();
|
|
}
|
|
|
|
async start(): Promise<void> {
|
|
try {
|
|
fs.unlinkSync(this.socketPath);
|
|
} catch {
|
|
}
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
this.listener.listen(this.socketPath, () => {
|
|
resolve();
|
|
});
|
|
this.listener.on('error', reject);
|
|
});
|
|
|
|
const cmdArgs = [...this.cmdArgs, `--${ IPC_SOCKET_ARG }`, this.socketPath];
|
|
this.cmd = spawn(this.cmdPath, cmdArgs, {stdio: 'inherit'});
|
|
|
|
this.cmd.on('error', (err) => {
|
|
this.raiseErr(err);
|
|
});
|
|
|
|
this.cmd.on('close', (code, signal) => {
|
|
const result = { code, signal };
|
|
this.cmdExitResult = result;
|
|
for (const cb of this.cmdExitCallbacks) cb(result);
|
|
this.cmdExitCallbacks = [];
|
|
});
|
|
|
|
await this.acceptConn();
|
|
}
|
|
|
|
private async acceptConn(): Promise<void> {
|
|
const acceptPromise = new Promise<net.Socket>((resolve, reject) => {
|
|
this.listener.once('connection', (conn) => {
|
|
resolve(conn);
|
|
});
|
|
this.listener.once('error', reject);
|
|
});
|
|
|
|
const exitPromise = new Promise<net.Socket>((_, reject) => {
|
|
if (this.cmdExitResult) {
|
|
reject(new Error(`command exited before connection established`));
|
|
} else {
|
|
this.cmdExitCallbacks.push(() => {
|
|
reject(new Error(`command exited before connection established`));
|
|
});
|
|
}
|
|
});
|
|
|
|
try {
|
|
this.conn = await timeout(Promise.race([acceptPromise, exitPromise]), ACCEPT_TIMEOUT_MS);
|
|
this.readConn();
|
|
} catch (e) {
|
|
if (this.cmd) this.cmd.kill();
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async wait(): Promise<void> {
|
|
if (!this.cmd) {
|
|
throw new Error('Command is not started yet');
|
|
}
|
|
|
|
const exitPromise = new Promise<{ code: number | null, signal: string | null }>((resolve) => {
|
|
if (this.cmdExitResult) {
|
|
resolve(this.cmdExitResult);
|
|
} else {
|
|
this.cmdExitCallbacks.push(resolve);
|
|
}
|
|
});
|
|
|
|
try {
|
|
await Promise.race([
|
|
exitPromise.then(({ code, signal }) => {
|
|
if (signal || code) {
|
|
if (signal) throw new Error(`Process exited with signal ${ signal }`);
|
|
else throw new Error(`Process exited with code ${ code }`);
|
|
} else if (!this.ready) {
|
|
throw new Error('command exited before connection established');
|
|
}
|
|
}),
|
|
this.errorQueue.collect().then((errors) => {
|
|
if (errors.length === 1) {
|
|
throw errors[0];
|
|
} else if (errors.length > 1) {
|
|
throw new Error(errors.map(e => e.toString()).join(', '));
|
|
}
|
|
}),
|
|
]);
|
|
} finally {
|
|
try { fs.unlinkSync(this.socketPath); } catch {}
|
|
}
|
|
}
|
|
}
|