| |
| import { randomInt } from "node:crypto"; |
| import { quoteCmdScriptArg } from "../../daemon/cmd-argv.js"; |
| import { |
| formatWindowsTaskSupervisorChildArgument, |
| isWindowsTaskSupervisorChildArgument, |
| WINDOWS_TASK_LAUNCHER_ACTIVE, |
| WINDOWS_TASK_LAUNCHER_ENV, |
| WINDOWS_TASK_SUPERVISOR_FLAG, |
| WINDOWS_TASK_SUPERVISOR_RESTART_EXIT_CODE_MAX, |
| WINDOWS_TASK_SUPERVISOR_RESTART_EXIT_CODE_MIN, |
| } from "../../daemon/windows-task-supervisor-contract.js"; |
| import { flushLogger } from "../../logging/logger.js"; |
| import { createSubsystemLogger } from "../../logging/subsystem.js"; |
| import { getProcessSupervisor, type ManagedRun } from "../../process/supervisor/index.js"; |
|
|
| const log = createSubsystemLogger("gateway/task-supervisor"); |
| const STDERR_TAIL_CHARS = 8192; |
|
|
| function renderGatewayTaskCommand(restartExitCode: number): string { |
| const childArgs = [...process.execArgv, ...process.argv.slice(1)].filter( |
| (argument) => |
| argument !== WINDOWS_TASK_SUPERVISOR_FLAG && !isWindowsTaskSupervisorChildArgument(argument), |
| ); |
| if (childArgs.length === 0) { |
| throw new Error("Windows task supervisor could not resolve the Gateway command"); |
| } |
| return [process.execPath, ...childArgs, formatWindowsTaskSupervisorChildArgument(restartExitCode)] |
| .map((argument) => quoteCmdScriptArg(argument)) |
| .join(" "); |
| } |
|
|
| |
| |
| |
| |
| |
| export async function runWindowsGatewayTaskSupervisor(): Promise<void> { |
| if (process.platform !== "win32") { |
| throw new Error("--task-supervisor is only available to the Windows Gateway service"); |
| } |
| let stderr = ""; |
| let managed: ManagedRun | null = null; |
| let cancelled = false; |
| const cancel = () => { |
| cancelled = true; |
| managed?.cancel("signal"); |
| }; |
| process.once("SIGINT", cancel); |
| process.once("SIGTERM", cancel); |
| try { |
| const launcher = process.env[WINDOWS_TASK_LAUNCHER_ENV]; |
| delete process.env[WINDOWS_TASK_LAUNCHER_ENV]; |
| if (launcher === WINDOWS_TASK_LAUNCHER_ACTIVE) { |
| const [{ default: koffi }, { bindWindowsTaskLauncher }] = await Promise.all([ |
| import("koffi"), |
| import("../../process/supervisor/service-child-windows-task-launcher.js"), |
| ]); |
| bindWindowsTaskLauncher(koffi); |
| } |
| while (true) { |
| stderr = ""; |
| |
| |
| const restartExitCode = randomInt( |
| WINDOWS_TASK_SUPERVISOR_RESTART_EXIT_CODE_MIN, |
| WINDOWS_TASK_SUPERVISOR_RESTART_EXIT_CODE_MAX + 1, |
| ); |
| managed = await getProcessSupervisor().spawn({ |
| mode: "anchored-shell", |
| command: renderGatewayTaskCommand(restartExitCode), |
| scopeKey: `gateway-task-supervisor:${process.pid}`, |
| captureOutput: false, |
| onStderr: (chunk) => { |
| stderr = (stderr + chunk).slice(-STDERR_TAIL_CHARS); |
| }, |
| }); |
| if (cancelled) { |
| managed.cancel("signal"); |
| } |
| const result = await managed.wait(); |
| |
| const diagnostic = { |
| exitCode: result.exitCode, |
| exitSignal: result.exitSignal, |
| reason: result.reason, |
| stderr, |
| }; |
| const restartRequested = result.exitCode === restartExitCode; |
| if (result.exitCode === 0) { |
| log.info("Gateway child exited", diagnostic); |
| } else if (restartRequested) { |
| log.info( |
| cancelled |
| ? "Gateway child restart suppressed by shutdown" |
| : "Gateway child requested restart", |
| diagnostic, |
| ); |
| } else { |
| process.exitCode = result.exitCode ?? 1; |
| log.error("Gateway child failed", diagnostic); |
| } |
| await managed.waitForExtinction?.(); |
| managed = null; |
| if (restartRequested && !cancelled) { |
| |
| |
| |
| continue; |
| } |
| return; |
| } |
| } catch (error) { |
| process.exitCode = 1; |
| log.error(`Gateway task supervisor failed: ${String(error)}`, { stderr }); |
| } finally { |
| process.removeListener("SIGINT", cancel); |
| process.removeListener("SIGTERM", cancel); |
| await flushLogger(); |
| } |
| } |
|
|