@sudobility/testomniac_runner 0.0.132 → 0.0.134

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sudobility/testomniac_runner",
3
- "version": "0.0.132",
3
+ "version": "0.0.134",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/index.ts CHANGED
@@ -15,6 +15,10 @@ const config = loadConfig();
15
15
  const pollIntervalMs = Number(process.env.SCAN_POLL_INTERVAL_MS ?? 10_000);
16
16
  const maxConcurrentRunners = config.maxConcurrentRunners;
17
17
  const runnerManager = new RunnerManager(pollIntervalMs, maxConcurrentRunners);
18
+ const runnerInstanceId =
19
+ process.env.TESTOMNIAC_RUNNER_INSTANCE_ID ??
20
+ process.env.TESTOMNIAC_RUNNER_PROCESS_INSTANCE_ID;
21
+ const runnerInstanceName = process.env.TESTOMNIAC_RUNNER_INSTANCE_NAME;
18
22
 
19
23
  const app = new Hono();
20
24
 
@@ -66,8 +70,8 @@ if (import.meta.main) {
66
70
  baseUrl,
67
71
  sizeClass,
68
72
  scanMode,
69
- runnerInstanceId: crypto.randomUUID(),
70
- runnerInstanceName: "mcp-runner",
73
+ runnerInstanceId: runnerInstanceId ?? crypto.randomUUID(),
74
+ runnerInstanceName: runnerInstanceName ?? "mcp-runner",
71
75
  });
72
76
  logger.info({ scanId }, "one-shot run completed");
73
77
  process.exit(0);
@@ -94,12 +98,65 @@ if (import.meta.main) {
94
98
  }
95
99
  } else {
96
100
  // Default: polling mode
97
- setInterval(() => {
101
+ const pollInterval = setInterval(() => {
98
102
  void runnerManager.tick();
99
103
  }, pollIntervalMs);
100
104
 
101
105
  void runnerManager.tick();
102
106
 
107
+ // IPC listener: parent process can send JSON messages on stdin
108
+ if (process.stdin.isTTY === false || process.stdin.readable) {
109
+ process.stdin.setEncoding("utf-8");
110
+ let ipcBuffer = "";
111
+ process.stdin.on("data", (chunk: string) => {
112
+ ipcBuffer += chunk;
113
+ let newlineIdx;
114
+ while ((newlineIdx = ipcBuffer.indexOf("\n")) !== -1) {
115
+ const line = ipcBuffer.slice(0, newlineIdx).trim();
116
+ ipcBuffer = ipcBuffer.slice(newlineIdx + 1);
117
+ if (!line) continue;
118
+ try {
119
+ const msg = JSON.parse(line);
120
+ if (msg.type === "stop_run" && typeof msg.runId === "number") {
121
+ logger.info({ runId: msg.runId }, "IPC: stop_run received");
122
+ const stopped = runnerManager.stopRun(msg.runId);
123
+ logger.info(
124
+ { runId: msg.runId, stopped },
125
+ "IPC: stop_run result"
126
+ );
127
+ }
128
+ } catch {
129
+ logger.warn({ line }, "IPC: ignoring malformed message");
130
+ }
131
+ }
132
+ });
133
+ process.stdin.on("end", () => {
134
+ logger.info("IPC: stdin closed");
135
+ });
136
+ }
137
+
138
+ // Graceful shutdown
139
+ let shuttingDown = false;
140
+ for (const sig of ["SIGTERM", "SIGINT"] as const) {
141
+ process.on(sig, () => {
142
+ if (shuttingDown) return;
143
+ shuttingDown = true;
144
+ logger.info({ signal: sig }, "graceful shutdown initiated");
145
+ clearInterval(pollInterval);
146
+ runnerManager.stopAllRuns();
147
+ const check = setInterval(() => {
148
+ if (runnerManager.getActiveRunCount() === 0) {
149
+ clearInterval(check);
150
+ process.exit(0);
151
+ }
152
+ }, 500);
153
+ setTimeout(() => {
154
+ logger.warn("graceful shutdown timed out, forcing exit");
155
+ process.exit(1);
156
+ }, 30_000);
157
+ });
158
+ }
159
+
103
160
  logger.warn(
104
161
  { port, pollIntervalMs, maxConcurrentRunners },
105
162
  "starting scanner service"
@@ -41,6 +41,7 @@ export interface RunOptions {
41
41
  entityCredentialId?: number;
42
42
  quickScan?: boolean;
43
43
  scanMode?: "full" | "partial" | "minimum";
44
+ signal?: AbortSignal;
44
45
  }
45
46
 
46
47
  export async function runFullScan(options: RunOptions): Promise<void> {
@@ -121,6 +122,7 @@ export async function runFullScan(options: RunOptions): Promise<void> {
121
122
  credentials: options.credentials,
122
123
  quickScan: options.quickScan,
123
124
  scanMode: options.scanMode,
125
+ signal: options.signal,
124
126
  },
125
127
  api,
126
128
  expertises,
@@ -163,6 +165,8 @@ export interface SequenceRunOptions {
163
165
  sequenceRunId: number;
164
166
  runnerId: number;
165
167
  sizeClass?: string;
168
+ runnerInstanceId?: string;
169
+ runnerInstanceName?: string;
166
170
  }
167
171
 
168
172
  export async function runSequenceScan(
@@ -211,8 +215,15 @@ export async function runSequenceScan(
211
215
  sequenceRunId: options.sequenceRunId,
212
216
  runnerId: options.runnerId,
213
217
  sizeClass,
214
- runnerInstanceId: crypto.randomUUID(),
215
- runnerInstanceName: "mcp-runner",
218
+ runnerInstanceId:
219
+ options.runnerInstanceId ??
220
+ process.env.TESTOMNIAC_RUNNER_INSTANCE_ID ??
221
+ process.env.TESTOMNIAC_RUNNER_PROCESS_INSTANCE_ID ??
222
+ crypto.randomUUID(),
223
+ runnerInstanceName:
224
+ options.runnerInstanceName ??
225
+ process.env.TESTOMNIAC_RUNNER_INSTANCE_NAME ??
226
+ "mcp-runner",
216
227
  },
217
228
  api,
218
229
  expertises,
@@ -11,10 +11,12 @@ const logger = pino({
11
11
  type ActiveRun = {
12
12
  runId: number;
13
13
  startedAtMs: number;
14
+ abortController: AbortController;
14
15
  };
15
16
 
16
17
  export class RunnerManager {
17
- private readonly processInstanceId = crypto.randomUUID();
18
+ private readonly processInstanceId =
19
+ process.env.TESTOMNIAC_RUNNER_PROCESS_INSTANCE_ID ?? crypto.randomUUID();
18
20
  private readonly activeRuns = new Map<number, ActiveRun>();
19
21
  private tickInFlight = false;
20
22
 
@@ -27,6 +29,21 @@ export class RunnerManager {
27
29
  return this.activeRuns.size;
28
30
  }
29
31
 
32
+ stopRun(runId: number): boolean {
33
+ const run = this.activeRuns.get(runId);
34
+ if (!run) return false;
35
+ logger.info({ runId }, "stopping run via abort signal");
36
+ run.abortController.abort();
37
+ return true;
38
+ }
39
+
40
+ stopAllRuns(): void {
41
+ for (const [runId, run] of this.activeRuns) {
42
+ logger.info({ runId }, "stopping run (shutdown)");
43
+ run.abortController.abort();
44
+ }
45
+ }
46
+
30
47
  async tick(): Promise<void> {
31
48
  if (this.tickInFlight) return;
32
49
 
@@ -106,9 +123,11 @@ export class RunnerManager {
106
123
  continue;
107
124
  }
108
125
 
126
+ const abortController = new AbortController();
109
127
  this.activeRuns.set(pendingRun.id, {
110
128
  runId: pendingRun.id,
111
129
  startedAtMs: Date.now(),
130
+ abortController,
112
131
  });
113
132
 
114
133
  logger.info(
@@ -131,6 +150,7 @@ export class RunnerManager {
131
150
  runnerInstanceName,
132
151
  quickScan: pendingRun.quickScan ?? false,
133
152
  scanMode: readScanMode(pendingRun),
153
+ signal: abortController.signal,
134
154
  });
135
155
  }
136
156
  } catch (err) {
@@ -150,6 +170,7 @@ export class RunnerManager {
150
170
  runnerInstanceName: string;
151
171
  quickScan: boolean;
152
172
  scanMode?: "full" | "partial" | "minimum";
173
+ signal: AbortSignal;
153
174
  }): Promise<void> {
154
175
  const config = loadConfig();
155
176
  const api = getApiClient(
@@ -169,6 +190,7 @@ export class RunnerManager {
169
190
  runnerInstanceName: params.runnerInstanceName,
170
191
  quickScan: params.quickScan,
171
192
  scanMode: params.scanMode,
193
+ signal: params.signal,
172
194
  });
173
195
 
174
196
  logger.info({ runId: params.runId }, "run completed successfully");