






















@@ -42,6 +42,14 @@ import type { QaTransportAdapter } from "./qa-transport.js";
42424343export type { QaCliBackendAuthMode } from "./providers/env.js";
4444const QA_GATEWAY_CHILD_STARTUP_MAX_ATTEMPTS = 5;
45+46+export type QaGatewayChildStateMutationContext = {
47+configPath: string;
48+runtimeEnv: NodeJS.ProcessEnv;
49+stateDir: string;
50+tempRoot: string;
51+};
52+4553async function getFreePort() {
4654return await new Promise<number>((resolve, reject) => {
4755const server = net.createServer();
@@ -694,10 +702,95 @@ export async function startQaGatewayChild(params: {
694702if (!child || !cfg || !baseUrl || !wsUrl || !rpcClient || !env) {
695703throw new Error("qa gateway child failed to start");
696704}
697-const runningChild = child;
698-const runningRpcClient = rpcClient;
705+let activeChild = child;
706+let activeRpcClient = rpcClient;
699707const runningEnv = env;
700708709+const spawnReplacementGatewayChild = async () => {
710+const nextChild = spawn(
711+nodeExecPath,
712+[
713+distEntryPath,
714+"gateway",
715+"run",
716+"--port",
717+String(gatewayPort),
718+"--bind",
719+"loopback",
720+"--allow-unconfigured",
721+],
722+{
723+cwd: runtimeCwd,
724+env: runningEnv,
725+detached: process.platform !== "win32",
726+stdio: ["ignore", "pipe", "pipe"],
727+},
728+);
729+nextChild.stdout.on("data", (chunk) => {
730+const buffer = Buffer.from(chunk);
731+stdout.push(buffer);
732+stdoutLog.write(buffer);
733+});
734+nextChild.stderr.on("data", (chunk) => {
735+const buffer = Buffer.from(chunk);
736+stderr.push(buffer);
737+stderrLog.write(buffer);
738+});
739+740+try {
741+await waitForGatewayReady({
742+ baseUrl,
743+ logs,
744+child: nextChild,
745+timeoutMs: 120_000,
746+});
747+const nextRpcClient = await startQaGatewayRpcClient({
748+ wsUrl,
749+token: gatewayToken,
750+ logs,
751+});
752+try {
753+let rpcReady = false;
754+let lastRpcStartupError: unknown = null;
755+for (let rpcAttempt = 1; rpcAttempt <= 4; rpcAttempt += 1) {
756+try {
757+await nextRpcClient.request("config.get", {}, { timeoutMs: 10_000 });
758+rpcReady = true;
759+break;
760+} catch (error) {
761+lastRpcStartupError = error;
762+if (rpcAttempt >= 4 || !isRetryableRpcStartupError(error)) {
763+throw error;
764+}
765+await sleep(500 * rpcAttempt);
766+await waitForGatewayReady({
767+ baseUrl,
768+ logs,
769+child: nextChild,
770+timeoutMs: 15_000,
771+});
772+}
773+}
774+if (!rpcReady) {
775+throw lastRpcStartupError ?? new Error("qa gateway rpc client failed to start");
776+}
777+} catch (error) {
778+await nextRpcClient.stop().catch(() => {});
779+throw error;
780+}
781+return {
782+child: nextChild,
783+rpcClient: nextRpcClient,
784+};
785+} catch (error) {
786+await stopQaGatewayChildProcessTree(nextChild, {
787+gracefulTimeoutMs: 1_500,
788+forceTimeoutMs: 1_500,
789+});
790+throw error;
791+}
792+};
793+701794return {
702795 cfg,
703796 baseUrl,
@@ -710,10 +803,27 @@ export async function startQaGatewayChild(params: {
710803runtimeEnv: runningEnv,
711804 logs,
712805async restart(signal: NodeJS.Signals = "SIGUSR1") {
713-if (!runningChild.pid) {
806+if (!activeChild.pid) {
714807throw new Error("qa gateway child has no pid");
715808}
716-process.kill(runningChild.pid, signal);
809+process.kill(activeChild.pid, signal);
810+},
811+async restartAfterStateMutation(
812+mutateState: (context: QaGatewayChildStateMutationContext) => Promise<void>,
813+) {
814+await activeRpcClient.stop().catch(() => {});
815+await stopQaGatewayChildProcessTree(activeChild);
816+await mutateState({
817+ configPath,
818+runtimeEnv: runningEnv,
819+ stateDir,
820+ tempRoot,
821+});
822+const restarted = await spawnReplacementGatewayChild();
823+activeChild = restarted.child;
824+activeRpcClient = restarted.rpcClient;
825+child = activeChild;
826+rpcClient = activeRpcClient;
717827},
718828async call(
719829method: string,
@@ -724,7 +834,7 @@ export async function startQaGatewayChild(params: {
724834let lastDetails = "";
725835for (let attempt = 1; attempt <= 3; attempt += 1) {
726836try {
727-return await runningRpcClient.request(method, rpcParams, {
837+return await activeRpcClient.request(method, rpcParams, {
728838 ...opts,
729839 timeoutMs,
730840});
@@ -737,16 +847,16 @@ export async function startQaGatewayChild(params: {
737847await waitForGatewayReady({
738848 baseUrl,
739849 logs,
740-child: runningChild,
850+child: activeChild,
741851timeoutMs: Math.max(10_000, timeoutMs),
742852});
743853}
744854}
745855throw new Error(`${lastDetails}${formatQaGatewayLogsForError(logs())}`);
746856},
747857async stop(opts?: { keepTemp?: boolean; preserveToDir?: string }) {
748-await runningRpcClient.stop().catch(() => {});
749-await stopQaGatewayChildProcessTree(runningChild);
858+await activeRpcClient.stop().catch(() => {});
859+await stopQaGatewayChildProcessTree(activeChild);
750860await closeWriteStream(stdoutLog);
751861await closeWriteStream(stderrLog);
752862if (opts?.preserveToDir && !(opts?.keepTemp ?? keepTemp)) {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。