@sudobility/testomniac_runner 0.0.133 → 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.133",
3
+ "version": "0.0.134",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/index.ts CHANGED
@@ -98,12 +98,65 @@ if (import.meta.main) {
98
98
  }
99
99
  } else {
100
100
  // Default: polling mode
101
- setInterval(() => {
101
+ const pollInterval = setInterval(() => {
102
102
  void runnerManager.tick();
103
103
  }, pollIntervalMs);
104
104
 
105
105
  void runnerManager.tick();
106
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
+
107
160
  logger.warn(
108
161
  { port, pollIntervalMs, maxConcurrentRunners },
109
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,
@@ -11,6 +11,7 @@ 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 {
@@ -28,6 +29,21 @@ export class RunnerManager {
28
29
  return this.activeRuns.size;
29
30
  }
30
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
+
31
47
  async tick(): Promise<void> {
32
48
  if (this.tickInFlight) return;
33
49
 
@@ -107,9 +123,11 @@ export class RunnerManager {
107
123
  continue;
108
124
  }
109
125
 
126
+ const abortController = new AbortController();
110
127
  this.activeRuns.set(pendingRun.id, {
111
128
  runId: pendingRun.id,
112
129
  startedAtMs: Date.now(),
130
+ abortController,
113
131
  });
114
132
 
115
133
  logger.info(
@@ -132,6 +150,7 @@ export class RunnerManager {
132
150
  runnerInstanceName,
133
151
  quickScan: pendingRun.quickScan ?? false,
134
152
  scanMode: readScanMode(pendingRun),
153
+ signal: abortController.signal,
135
154
  });
136
155
  }
137
156
  } catch (err) {
@@ -151,6 +170,7 @@ export class RunnerManager {
151
170
  runnerInstanceName: string;
152
171
  quickScan: boolean;
153
172
  scanMode?: "full" | "partial" | "minimum";
173
+ signal: AbortSignal;
154
174
  }): Promise<void> {
155
175
  const config = loadConfig();
156
176
  const api = getApiClient(
@@ -170,6 +190,7 @@ export class RunnerManager {
170
190
  runnerInstanceName: params.runnerInstanceName,
171
191
  quickScan: params.quickScan,
172
192
  scanMode: params.scanMode,
193
+ signal: params.signal,
173
194
  });
174
195
 
175
196
  logger.info({ runId: params.runId }, "run completed successfully");