File size: 4,755 Bytes
f778c12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// Windows Task Scheduler bridge: retain the Job Object owner until the Gateway exits.
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(" ");
}

/**
 * Runs the real Gateway inside the Windows Job Object owned by ProcessSupervisor.
 * The hidden task launcher owns an outer Job containing this supervisor. The
 * command anchor owns the inner Job used for cancellation and extinction joins.
 */
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 = "";
      // A fresh high-entropy outcome correlates this child-to-supervisor handoff.
      // Common dependency exit codes must terminate the task instead of respawning forever.
      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();
      // Persist the child outcome before joining cleanup, which can fail independently.
      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) {
        // The child has released its Gateway lock and extinguished descendants.
        // Recheck cancellation after the asynchronous extinction join so shutdown
        // cannot admit a replacement; stop and update handoffs exit 0.
        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();
  }
}