refactor: adapter 구현중..
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
|
||||
export type ProviderProcessInput = Readonly<{
|
||||
executable: string;
|
||||
arguments: readonly string[];
|
||||
environment: NodeJS.ProcessEnv;
|
||||
timeoutMs: number;
|
||||
}>;
|
||||
|
||||
type ProviderChild = Pick<ChildProcess, "kill" | "once" | "pid">;
|
||||
|
||||
export async function runProviderProcess(
|
||||
input: ProviderProcessInput,
|
||||
dependencies: Readonly<{
|
||||
spawnChild?: (input: ProviderProcessInput) => ProviderChild;
|
||||
setTimer?: (callback: () => void, milliseconds: number) => ReturnType<typeof setTimeout>;
|
||||
clearTimer?: (timer: ReturnType<typeof setTimeout>) => void;
|
||||
killProcessGroup?: (child: ProviderChild) => void;
|
||||
}> = {},
|
||||
): Promise<void> {
|
||||
if (!Number.isSafeInteger(input.timeoutMs) || input.timeoutMs <= 0) {
|
||||
throw new TypeError("provider process timeout must be a positive integer");
|
||||
}
|
||||
const child = (dependencies.spawnChild ?? defaultSpawn)(input);
|
||||
const setTimer = dependencies.setTimer ?? setTimeout;
|
||||
const clearTimer = dependencies.clearTimer ?? clearTimeout;
|
||||
const killProcessGroup = dependencies.killProcessGroup ?? defaultKillProcessGroup;
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
let settled = false;
|
||||
let timedOut = false;
|
||||
const killFailures: unknown[] = [];
|
||||
const settle = (error?: Error): void => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimer(timer);
|
||||
error ? reject(error) : resolve();
|
||||
};
|
||||
const timer = setTimer(() => {
|
||||
timedOut = true;
|
||||
try {
|
||||
killProcessGroup(child);
|
||||
} catch (groupError) {
|
||||
killFailures.push(groupError);
|
||||
try {
|
||||
child.kill("SIGKILL");
|
||||
} catch (fallbackError) {
|
||||
killFailures.push(fallbackError);
|
||||
}
|
||||
}
|
||||
}, input.timeoutMs);
|
||||
child.once("error", (error: Error) => {
|
||||
if (!timedOut) settle(error);
|
||||
});
|
||||
child.once("close", (code: number | null, signal: NodeJS.Signals | null) => {
|
||||
if (timedOut) {
|
||||
const timeoutError = new Error(
|
||||
"sandboxed external provider command timed out after process close",
|
||||
);
|
||||
settle(
|
||||
killFailures.length === 0
|
||||
? timeoutError
|
||||
: new AggregateError(
|
||||
[timeoutError, ...killFailures],
|
||||
"sandboxed external provider timed out and process-group kill failed before close",
|
||||
{ cause: killFailures.at(-1) },
|
||||
),
|
||||
);
|
||||
} else if (code === 0 && signal === null) {
|
||||
settle();
|
||||
} else {
|
||||
settle(
|
||||
new Error(
|
||||
`sandboxed external provider failed: exit=${code ?? "none"}, signal=${signal ?? "none"}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function defaultSpawn(input: ProviderProcessInput): ProviderChild {
|
||||
return spawn(input.executable, [...input.arguments], {
|
||||
env: input.environment,
|
||||
stdio: "inherit",
|
||||
detached: true,
|
||||
});
|
||||
}
|
||||
|
||||
function defaultKillProcessGroup(child: ProviderChild): void {
|
||||
if (child.pid && child.pid > 0) {
|
||||
try {
|
||||
process.kill(-child.pid, "SIGKILL");
|
||||
return;
|
||||
} catch (error) {
|
||||
if (!hasErrorCode(error, "ESRCH")) throw error;
|
||||
}
|
||||
}
|
||||
child.kill("SIGKILL");
|
||||
}
|
||||
|
||||
function hasErrorCode(error: unknown, code: string): boolean {
|
||||
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
|
||||
}
|
||||
Reference in New Issue
Block a user