mirror of
https://github.com/Tencent/BrowserSkill.git
synced 2026-09-10 13:01:23 +08:00
fix(windows): harden installation and parent cancellation
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
* map the CLI's JSON error envelope onto a thrown `BskError`.
|
||||
*/
|
||||
|
||||
import { type ChildProcess, spawn } from "node:child_process";
|
||||
import { type ChildProcess, type SpawnOptionsWithoutStdio, spawn } from "node:child_process";
|
||||
|
||||
/** Shape of the JSON error envelope `bsk --json` prints on failure. */
|
||||
export interface BskErrorBody {
|
||||
@@ -53,7 +53,11 @@ export interface BskRunOptions {
|
||||
}
|
||||
|
||||
/** Minimal spawn signature so tests can substitute a fake child process. */
|
||||
export type SpawnImpl = (command: string, args: string[]) => ChildProcess;
|
||||
export type SpawnImpl = (
|
||||
command: string,
|
||||
args: string[],
|
||||
options?: SpawnOptionsWithoutStdio,
|
||||
) => ChildProcess;
|
||||
|
||||
export interface BskRunner {
|
||||
/** Run `bsk <args...> --json` and collect its output. */
|
||||
@@ -64,29 +68,65 @@ export interface BskRunner {
|
||||
killFor(tag: string): number;
|
||||
}
|
||||
|
||||
// Business RPCs translate SIGINT into the daemon's cancel(rpc_id) protocol.
|
||||
// Give that bounded reconciliation path time to settle before the hard kill.
|
||||
// Business RPCs translate Ctrl-C / opt-in stdin EOF into cancel(rpc_id).
|
||||
// Allow reconciliation before hard-killing an old or unresponsive CLI.
|
||||
const KILL_GRACE_MS = 3000;
|
||||
// Windows IPC may spend 5s connecting, then 2s cancelling and 2s settling.
|
||||
const WINDOWS_KILL_GRACE_MS = 10_000;
|
||||
const SESSION_BUSY_RETRY_DELAY_MS = 100;
|
||||
|
||||
export function createBskRunner(bskPath: string, spawnImpl: SpawnImpl = spawn): BskRunner {
|
||||
const live = new Map<ChildProcess, string | undefined>();
|
||||
const windows = process.platform === "win32";
|
||||
const cancelling = new Set<ChildProcess>();
|
||||
|
||||
function killChild(child: ChildProcess): void {
|
||||
if (child.exitCode !== null || child.signalCode !== null) return;
|
||||
child.kill("SIGINT");
|
||||
const force = setTimeout(() => {
|
||||
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
||||
}, KILL_GRACE_MS);
|
||||
if (child.exitCode !== null || child.signalCode !== null || cancelling.has(child)) return;
|
||||
cancelling.add(child);
|
||||
// Node kills Windows children outright for SIGINT. EOF asks the CLI to
|
||||
// send its existing cancel RPC and wait for browser reconciliation.
|
||||
if (windows && child.stdin) child.stdin.end();
|
||||
else child.kill("SIGINT");
|
||||
const force = setTimeout(
|
||||
() => {
|
||||
if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
|
||||
},
|
||||
windows ? WINDOWS_KILL_GRACE_MS : KILL_GRACE_MS,
|
||||
);
|
||||
force.unref();
|
||||
child.once("close", () => {
|
||||
clearTimeout(force);
|
||||
cancelling.delete(child);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
run(args, options = {}) {
|
||||
if (options.signal?.aborted) {
|
||||
return Promise.resolve({
|
||||
code: null,
|
||||
stdout: "",
|
||||
stderr: "",
|
||||
timedOut: false,
|
||||
aborted: true,
|
||||
});
|
||||
}
|
||||
return new Promise<BskRunResult>((resolve, reject) => {
|
||||
let child: ChildProcess;
|
||||
try {
|
||||
child = spawnImpl(bskPath, [...args, "--json"]);
|
||||
child = spawnImpl(
|
||||
bskPath,
|
||||
[...args, "--json"],
|
||||
windows
|
||||
? {
|
||||
windowsHide: true,
|
||||
env: { ...process.env, BSK_CANCEL_ON_STDIN_CLOSE: "1" },
|
||||
}
|
||||
: undefined,
|
||||
);
|
||||
// A child exiting while cancellation closes stdin may report EPIPE.
|
||||
// Its close/error event remains the authority for the run result.
|
||||
child.stdin?.on("error", () => {});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user