@zintrust/core 1.5.4 → 1.5.5

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.
@@ -11,7 +11,7 @@ import { Logger } from '../../config/logger.js';
11
11
  import notificationConfig from '../../config/notification.js';
12
12
  import { StartupConfigValidator } from '../../config/StartupConfigValidator.js';
13
13
  import { ErrorFactory } from '../../exceptions/ZintrustError.js';
14
- import { isNonEmptyString } from '../../helper/index.js';
14
+ import { isNonEmptyString, ShutdownTrace } from '../../helper/index.js';
15
15
  import { existsSync } from '../../node-singletons/fs.js';
16
16
  import * as path from '../../node-singletons/path.js';
17
17
  import { pathToFileURL } from '../../node-singletons/url.js';
@@ -23,6 +23,7 @@ import { SocketFeature } from '../../sockets/SocketRuntime.js';
23
23
  import { SocketRuntimeRegistry } from '../../sockets/SocketRuntimeRegistry.js';
24
24
  import { registerBroadcastersFromRuntimeConfig } from '../../tools/broadcast/BroadcastRuntimeRegistration.js';
25
25
  import { registerNotificationChannelsFromRuntimeConfig } from '../../tools/notification/NotificationRuntimeRegistration.js';
26
+ import { QueueReliabilityOrchestrator } from '../../tools/queue/QueueReliabilityOrchestrator.js';
26
27
  import { registerQueuesFromRuntimeConfig } from '../../tools/queue/QueueRuntimeRegistration.js';
27
28
  import { registerDisksFromRuntimeConfig } from '../../tools/storage/StorageRuntimeRegistration.js';
28
29
  const importFromExistingCandidates = async (moduleCandidates) => {
@@ -332,6 +333,10 @@ const initializeQueueMonitor = async (router) => {
332
333
  knownQueues: resolveKnownQueues,
333
334
  redis: redisConfig,
334
335
  });
336
+ runtimeQueueMonitor = monitor;
337
+ ShutdownTrace.logHandles('runtime.queue-monitor.created', {
338
+ basePath: monitorConfig.basePath ?? '',
339
+ });
335
340
  try {
336
341
  monitor.registerRoutes(router);
337
342
  }
@@ -341,6 +346,7 @@ const initializeQueueMonitor = async (router) => {
341
346
  Logger.info(`Queue Monitor routes registered at http://127.0.0.1:${appConfig.port}${monitorConfig.basePath ?? ''}`);
342
347
  Logger.info(`Queue Monitor enqueue endpoint at http://127.0.0.1:${appConfig.port}/test/enqueue`);
343
348
  };
349
+ let runtimeQueueMonitor = null;
344
350
  const initializeWorkers = async (router) => {
345
351
  const workers = await loadWorkersModule({ allowWhenDisabled: true });
346
352
  if (workers?.WorkerInit !== undefined && typeof workers.registerWorkerRoutes === 'function') {
@@ -528,7 +534,12 @@ export const createLifecycle = (params) => {
528
534
  initializeSockets(params.router);
529
535
  await initializeSystemTrace(params.router);
530
536
  if (Cloudflare.getWorkersEnv() === null && appConfig.dockerWorker === false) {
531
- await initializeWorkers(params.router);
537
+ if (appConfig.worker === true) {
538
+ await initializeWorkers(params.router);
539
+ }
540
+ else {
541
+ Logger.info('Skipping worker route registration (WORKER_ENABLED=false).');
542
+ }
532
543
  await initializeQueueMonitor(params.router);
533
544
  if (appConfig.worker === true) {
534
545
  await initializeQueueHttpGateway(params.router);
@@ -548,11 +559,34 @@ export const createLifecycle = (params) => {
548
559
  };
549
560
  const shutdown = async () => {
550
561
  Logger.info('🛑 Shutting down application...');
562
+ ShutdownTrace.logHandles('runtime.lifecycle.shutdown.start');
563
+ QueueReliabilityOrchestrator.stop();
564
+ ShutdownTrace.logHandles('runtime.lifecycle.shutdown.after-queue-reliability-stop');
565
+ if (runtimeQueueMonitor !== null && typeof runtimeQueueMonitor.close === 'function') {
566
+ try {
567
+ ShutdownTrace.log('runtime.lifecycle.shutdown.queue-monitor-close.start');
568
+ await runtimeQueueMonitor.close();
569
+ ShutdownTrace.logHandles('runtime.lifecycle.shutdown.queue-monitor-close.complete');
570
+ }
571
+ catch (error) {
572
+ Logger.warn('Queue Monitor shutdown failed', error);
573
+ ShutdownTrace.logHandles('runtime.lifecycle.shutdown.queue-monitor-close.failed', {
574
+ error: error instanceof Error ? error.message : String(error),
575
+ });
576
+ }
577
+ finally {
578
+ runtimeQueueMonitor = null;
579
+ }
580
+ }
551
581
  try {
552
582
  await params.shutdownManager.run();
583
+ ShutdownTrace.logHandles('runtime.lifecycle.shutdown.after-hooks');
553
584
  }
554
585
  catch (error) {
555
586
  Logger.error('Shutdown hook failed:', error);
587
+ ShutdownTrace.logHandles('runtime.lifecycle.shutdown.hooks-failed', {
588
+ error: error instanceof Error ? error.message : String(error),
589
+ });
556
590
  }
557
591
  // Ensure FileLogWriter.flush is attempted even if dynamic registration failed.
558
592
  try {
@@ -563,6 +597,7 @@ export const createLifecycle = (params) => {
563
597
  /* best-effort */
564
598
  }
565
599
  params.setBooted(false);
600
+ ShutdownTrace.logHandles('runtime.lifecycle.shutdown.complete');
566
601
  };
567
602
  return { boot, shutdown };
568
603
  };
@@ -1 +1 @@
1
- {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../../../src/boot/registry/worker.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGvD;;GAEG;AACH,eAAO,MAAM,0BAA0B,GACrC,iBAAiB,gBAAgB,KAChC,OAAO,CAAC,IAAI,CA0Cd,CAAC"}
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../../../../src/boot/registry/worker.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAGvD;;GAEG;AACH,eAAO,MAAM,0BAA0B,GACrC,iBAAiB,gBAAgB,KAChC,OAAO,CAAC,IAAI,CA2Cd,CAAC"}
@@ -5,7 +5,8 @@ import { loadWorkersModule } from '../../runtime/WorkersModule.js';
5
5
  * Helper: Register Worker management system shutdown hook
6
6
  */
7
7
  export const registerWorkerShutdownHook = async (shutdownManager) => {
8
- if (Env.getBool('WORKER_SHUTDOWN_ON_APP_EXIT', true) === false ||
8
+ if (appConfig.worker !== true ||
9
+ Env.getBool('WORKER_SHUTDOWN_ON_APP_EXIT', true) === false ||
9
10
  appConfig.dockerWorker === true) {
10
11
  return Promise.resolve(); // NOSONAR - Skip worker shutdown hook registration
11
12
  }
@@ -17,7 +18,7 @@ export const registerWorkerShutdownHook = async (shutdownManager) => {
17
18
  const mod = (await loadWorkersModule());
18
19
  const isShuttingDown = typeof mod.WorkerShutdown.isShuttingDown === 'function'
19
20
  ? mod.WorkerShutdown.isShuttingDown()
20
- : mod.WorkerShutdown.getShutdownState?.().isShuttingDown ?? false;
21
+ : (mod.WorkerShutdown.getShutdownState?.().isShuttingDown ?? false);
21
22
  const completedAt = mod.WorkerShutdown.getShutdownState?.().completedAt ?? null;
22
23
  if (isShuttingDown || completedAt !== null)
23
24
  return;
@@ -1 +1 @@
1
- {"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../../../../src/cli/utils/spawn.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AA8HD,eAAO,MAAM,SAAS;wBACM,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC;EA2F7D,CAAC"}
1
+ {"version":3,"file":"spawn.d.ts","sourceRoot":"","sources":["../../../../src/cli/utils/spawn.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAyYD,eAAO,MAAM,SAAS;wBACM,iBAAiB,GAAG,OAAO,CAAC,MAAM,CAAC;EA8E7D,CAAC"}
@@ -4,6 +4,36 @@ import { spawn } from '../../node-singletons/child-process.js';
4
4
  import { existsSync } from '../../node-singletons/fs.js';
5
5
  import * as path from '../../node-singletons/path.js';
6
6
  import { fileURLToPath } from '../../node-singletons/url.js';
7
+ const CLI_SPAWN_TRACE_ENV_KEYS = ['CLI_SPAWN_TRACE', 'ZIN_SPAWN_TRACE'];
8
+ const getCliSpawnTracePid = () => {
9
+ if (typeof process === 'undefined')
10
+ return undefined;
11
+ return process.pid;
12
+ };
13
+ const isCliSpawnTraceEnabled = () => {
14
+ if (typeof process === 'undefined')
15
+ return false;
16
+ return CLI_SPAWN_TRACE_ENV_KEYS.some((key) => {
17
+ const raw = process.env[key];
18
+ if (typeof raw !== 'string')
19
+ return false;
20
+ const normalized = raw.trim().toLowerCase();
21
+ return (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on');
22
+ });
23
+ };
24
+ const writeCliSpawnTrace = (label, details = {}) => {
25
+ if (!isCliSpawnTraceEnabled())
26
+ return;
27
+ const line = JSON.stringify({
28
+ trace: 'cli-spawn',
29
+ label,
30
+ pid: getCliSpawnTracePid(),
31
+ details,
32
+ });
33
+ if (typeof process !== 'undefined' && typeof process.stderr?.write === 'function') {
34
+ process.stderr.write(`${line}\n`);
35
+ }
36
+ };
7
37
  const getExitCode = (exitCode, signal) => {
8
38
  if (typeof exitCode === 'number')
9
39
  return exitCode;
@@ -48,35 +78,210 @@ const buildCommandNotFoundMessage = (command) => {
48
78
  const waitForChildExit = async (child, onExit) => {
49
79
  return new Promise((resolve, reject) => {
50
80
  let settled = false;
51
- const finish = (result) => {
81
+ let childResult = {
82
+ exitCode: null,
83
+ signal: null,
84
+ };
85
+ const finish = () => {
52
86
  if (settled)
53
87
  return;
54
88
  settled = true;
89
+ writeCliSpawnTrace('spawn.wait.finish', {
90
+ childPid: child.pid,
91
+ exitCode: childResult.exitCode,
92
+ signal: childResult.signal,
93
+ });
55
94
  child.off?.('exit', onExitEvent);
56
95
  child.off?.('close', onCloseEvent);
57
96
  onExit();
58
- resolve(result);
97
+ resolve(childResult);
59
98
  };
60
99
  const onExitEvent = (code, signal) => {
61
- finish({ exitCode: code, signal });
100
+ writeCliSpawnTrace('spawn.child.exit', {
101
+ childPid: child.pid,
102
+ exitCode: code,
103
+ signal,
104
+ });
105
+ childResult = { exitCode: code, signal };
62
106
  };
63
107
  const onCloseEvent = (code, signal) => {
64
- finish({ exitCode: code, signal });
108
+ writeCliSpawnTrace('spawn.child.close', {
109
+ childPid: child.pid,
110
+ exitCode: code,
111
+ signal,
112
+ });
113
+ childResult = {
114
+ exitCode: childResult.exitCode ?? code,
115
+ signal: childResult.signal ?? signal,
116
+ };
117
+ finish();
65
118
  };
66
119
  child.once('error', (error) => {
120
+ writeCliSpawnTrace('spawn.child.error', {
121
+ childPid: child.pid,
122
+ error: error instanceof Error ? error.message : String(error),
123
+ });
67
124
  reject(error);
68
125
  });
69
126
  child.once('exit', onExitEvent);
70
127
  child.once('close', onCloseEvent);
71
128
  });
72
129
  };
130
+ const resolveSignalHandling = (input) => {
131
+ const forwardSignals = typeof input.forwardSignals === 'boolean' ? input.forwardSignals : !process.stdin.isTTY;
132
+ const ttySignalForwardDelayMs = process.stdin.isTTY === true && forwardSignals === false
133
+ ? Math.max(0, input.ttySignalForwardDelayMs ?? 0)
134
+ : 0;
135
+ return {
136
+ forwardSignals,
137
+ ttySignalForwardDelayMs,
138
+ };
139
+ };
140
+ const spawnChildProcess = (input) => {
141
+ writeCliSpawnTrace('spawn.child.start', {
142
+ command: input.command,
143
+ args: input.args,
144
+ cwd: input.cwd,
145
+ shell: input.shell,
146
+ });
147
+ const child = spawn(input.command, input.args, {
148
+ cwd: input.cwd,
149
+ env: input.env,
150
+ stdio: ['inherit', 'pipe', 'pipe'],
151
+ shell: input.shell,
152
+ });
153
+ child.stdout?.on('data', (chunk) => {
154
+ writeCliSpawnTrace('spawn.child.stdout.data', {
155
+ childPid: child.pid,
156
+ bytes: typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length,
157
+ });
158
+ process.stdout.write(chunk);
159
+ });
160
+ child.stdout?.on('end', () => {
161
+ writeCliSpawnTrace('spawn.child.stdout.end', {
162
+ childPid: child.pid,
163
+ });
164
+ });
165
+ child.stdout?.on('close', () => {
166
+ writeCliSpawnTrace('spawn.child.stdout.close', {
167
+ childPid: child.pid,
168
+ });
169
+ });
170
+ child.stderr?.on('data', (chunk) => {
171
+ writeCliSpawnTrace('spawn.child.stderr.data', {
172
+ childPid: child.pid,
173
+ bytes: typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length,
174
+ });
175
+ process.stderr.write(chunk);
176
+ });
177
+ child.stderr?.on('end', () => {
178
+ writeCliSpawnTrace('spawn.child.stderr.end', {
179
+ childPid: child.pid,
180
+ });
181
+ });
182
+ child.stderr?.on('close', () => {
183
+ writeCliSpawnTrace('spawn.child.stderr.close', {
184
+ childPid: child.pid,
185
+ });
186
+ });
187
+ writeCliSpawnTrace('spawn.child.started', {
188
+ childPid: child.pid,
189
+ command: input.command,
190
+ });
191
+ return child;
192
+ };
193
+ const createForwardSignal = (input) => {
194
+ return (signal) => {
195
+ writeCliSpawnTrace('spawn.signal.forward.attempt', {
196
+ childPid: input.child.pid,
197
+ signal,
198
+ });
199
+ try {
200
+ input.child.kill(signal);
201
+ writeCliSpawnTrace('spawn.signal.forward.complete', {
202
+ childPid: input.child.pid,
203
+ signal,
204
+ });
205
+ }
206
+ catch (error) {
207
+ writeCliSpawnTrace('spawn.signal.forward.failed', {
208
+ childPid: input.child.pid,
209
+ signal,
210
+ error: error instanceof Error ? error.message : String(error),
211
+ });
212
+ const wrapped = ErrorFactory.createTryCatchError('Failed to forward signal to child process', error);
213
+ try {
214
+ process.stderr.write(`${String(wrapped.message)}\n`);
215
+ }
216
+ catch {
217
+ // ignore
218
+ }
219
+ throw wrapped;
220
+ }
221
+ };
222
+ };
223
+ const createSignalHandlers = (input) => {
224
+ const onSigint = () => {
225
+ writeCliSpawnTrace('spawn.signal.received', {
226
+ signal: 'SIGINT',
227
+ forwardSignals: input.forwardSignals,
228
+ });
229
+ if (input.forwardSignals) {
230
+ input.forwardSignal('SIGINT');
231
+ return;
232
+ }
233
+ input.delayedSignalForwarder.schedule('SIGINT');
234
+ };
235
+ const onSigterm = () => {
236
+ writeCliSpawnTrace('spawn.signal.received', {
237
+ signal: 'SIGTERM',
238
+ forwardSignals: input.forwardSignals,
239
+ });
240
+ if (input.forwardSignals) {
241
+ input.forwardSignal('SIGTERM');
242
+ return;
243
+ }
244
+ input.delayedSignalForwarder.schedule('SIGTERM');
245
+ };
246
+ return { onSigint, onSigterm };
247
+ };
73
248
  const createDelayedSignalForwarder = (input) => {
74
249
  let delayedSignalTimer;
250
+ let escalationTimer;
251
+ const clearEscalation = () => {
252
+ if (escalationTimer === undefined)
253
+ return;
254
+ clearTimeout(escalationTimer);
255
+ escalationTimer = undefined;
256
+ };
75
257
  const clear = () => {
76
- if (delayedSignalTimer === undefined)
258
+ if (delayedSignalTimer !== undefined) {
259
+ clearTimeout(delayedSignalTimer);
260
+ delayedSignalTimer = undefined;
261
+ }
262
+ clearEscalation();
263
+ writeCliSpawnTrace('spawn.signal.delay.clear');
264
+ };
265
+ const scheduleEscalation = (signal) => {
266
+ if (escalationTimer !== undefined || input.isChildClosed())
77
267
  return;
78
- clearTimeout(delayedSignalTimer);
79
- delayedSignalTimer = undefined;
268
+ const nextSignal = signal === 'SIGINT' ? 'SIGTERM' : signal;
269
+ const escalationDelayMs = Math.max(250, Math.min(1000, input.ttySignalForwardDelayMs));
270
+ escalationTimer = globalThis.setTimeout(() => {
271
+ escalationTimer = undefined;
272
+ if (input.isChildClosed())
273
+ return;
274
+ try {
275
+ writeCliSpawnTrace('spawn.signal.escalation.fire', {
276
+ signal: nextSignal,
277
+ });
278
+ input.forwardSignal(nextSignal);
279
+ }
280
+ catch {
281
+ // best-effort fallback for interactive watch processes
282
+ }
283
+ }, escalationDelayMs);
284
+ escalationTimer.unref?.();
80
285
  };
81
286
  const schedule = (signal) => {
82
287
  if (input.ttySignalForwardDelayMs <= 0 ||
@@ -89,12 +294,20 @@ const createDelayedSignalForwarder = (input) => {
89
294
  if (input.isChildClosed())
90
295
  return;
91
296
  try {
297
+ writeCliSpawnTrace('spawn.signal.delay.fire', {
298
+ signal,
299
+ });
92
300
  input.forwardSignal(signal);
301
+ scheduleEscalation(signal);
93
302
  }
94
303
  catch {
95
304
  // best-effort fallback for interactive watch processes
96
305
  }
97
306
  }, input.ttySignalForwardDelayMs);
307
+ writeCliSpawnTrace('spawn.signal.delay.schedule', {
308
+ signal,
309
+ delayMs: input.ttySignalForwardDelayMs,
310
+ });
98
311
  delayedSignalTimer.unref?.();
99
312
  };
100
313
  return { clear, schedule };
@@ -103,64 +316,57 @@ export const SpawnUtil = Object.freeze({
103
316
  async spawnAndWait(input) {
104
317
  const cwd = input.cwd ?? process.cwd();
105
318
  const resolvedCommand = input.shell === true ? input.command : resolveLocalBin(input.command, cwd);
106
- const child = spawn(resolvedCommand, input.args, {
319
+ const signalHandling = resolveSignalHandling(input);
320
+ writeCliSpawnTrace('spawn.and-wait.start', {
321
+ command: resolvedCommand,
322
+ args: input.args,
323
+ cwd,
324
+ shell: input.shell === true,
325
+ forwardSignals: signalHandling.forwardSignals,
326
+ ttySignalForwardDelayMs: signalHandling.ttySignalForwardDelayMs,
327
+ });
328
+ const child = spawnChildProcess({
329
+ command: resolvedCommand,
330
+ args: input.args,
107
331
  cwd,
108
332
  env: input.env ?? appConfig.getSafeEnv(),
109
- stdio: 'inherit',
110
333
  shell: input.shell === true,
111
334
  });
112
335
  // In interactive shells, the foreground process group already receives SIGINT
113
336
  // (and often SIGTERM) so forwarding can cause duplicates. `tsx watch` is
114
337
  // especially sensitive here and can print "Previous process hasn't exited yet. Force killing...".
115
- const forwardSignals = typeof input.forwardSignals === 'boolean' ? input.forwardSignals : !process.stdin.isTTY;
116
- const ttySignalForwardDelayMs = process.stdin.isTTY === true && forwardSignals === false
117
- ? Math.max(0, input.ttySignalForwardDelayMs ?? 0)
118
- : 0;
338
+ const forwardSignals = signalHandling.forwardSignals;
339
+ const ttySignalForwardDelayMs = signalHandling.ttySignalForwardDelayMs;
119
340
  let childClosed = false;
120
- const forwardSignal = (signal) => {
121
- try {
122
- child.kill(signal);
123
- }
124
- catch (error) {
125
- const wrapped = ErrorFactory.createTryCatchError('Failed to forward signal to child process', error);
126
- // Best-effort logging; then rethrow (tests/assertions rely on this behavior).
127
- try {
128
- process.stderr.write(`${String(wrapped.message)}\n`);
129
- }
130
- catch {
131
- // ignore
132
- }
133
- throw wrapped;
134
- }
135
- };
341
+ const forwardSignal = createForwardSignal({ child });
136
342
  const delayedSignalForwarder = createDelayedSignalForwarder({
137
343
  ttySignalForwardDelayMs,
138
344
  isChildClosed: () => childClosed,
139
345
  forwardSignal,
140
346
  });
141
- const onSigint = () => {
142
- if (forwardSignals) {
143
- forwardSignal('SIGINT');
144
- return;
145
- }
146
- // In interactive TTY mode, let the child receive the terminal SIGINT directly first.
147
- // If it is still alive after a short grace period, send one fallback signal so the
148
- // watcher exits without requiring a second Ctrl+C from the user.
149
- delayedSignalForwarder.schedule('SIGINT');
150
- };
151
- const onSigterm = () => {
152
- if (forwardSignals) {
153
- forwardSignal('SIGTERM');
154
- return;
155
- }
156
- delayedSignalForwarder.schedule('SIGTERM');
157
- };
347
+ const { onSigint, onSigterm } = createSignalHandlers({
348
+ forwardSignals,
349
+ delayedSignalForwarder,
350
+ forwardSignal,
351
+ });
158
352
  process.on('SIGINT', onSigint);
159
353
  process.on('SIGTERM', onSigterm);
354
+ writeCliSpawnTrace('spawn.signal.handlers.registered', {
355
+ childPid: child.pid,
356
+ });
160
357
  try {
161
358
  const result = await waitForChildExit(child, () => {
162
359
  childClosed = true;
163
360
  delayedSignalForwarder.clear();
361
+ writeCliSpawnTrace('spawn.child.mark-closed', {
362
+ childPid: child.pid,
363
+ });
364
+ });
365
+ writeCliSpawnTrace('spawn.and-wait.result', {
366
+ childPid: child.pid,
367
+ exitCode: result.exitCode,
368
+ signal: result.signal,
369
+ normalizedExitCode: getExitCode(result.exitCode, result.signal),
164
370
  });
165
371
  return getExitCode(result.exitCode, result.signal);
166
372
  }
@@ -175,6 +381,9 @@ export const SpawnUtil = Object.freeze({
175
381
  delayedSignalForwarder.clear();
176
382
  process.off('SIGINT', onSigint);
177
383
  process.off('SIGTERM', onSigterm);
384
+ writeCliSpawnTrace('spawn.signal.handlers.removed', {
385
+ childPid: child.pid,
386
+ });
178
387
  }
179
388
  },
180
389
  });
@@ -0,0 +1,9 @@
1
+ type ShutdownTraceDetails = Record<string, unknown>;
2
+ export declare const ShutdownTrace: Readonly<{
3
+ isEnabled: () => boolean;
4
+ log: (label: string, details?: ShutdownTraceDetails) => void;
5
+ logHandles: (label: string, details?: ShutdownTraceDetails) => void;
6
+ logBullMQWorker: (label: string, worker: unknown, details?: ShutdownTraceDetails) => void;
7
+ }>;
8
+ export {};
9
+ //# sourceMappingURL=ShutdownTrace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ShutdownTrace.d.ts","sourceRoot":"","sources":["../../../src/helper/ShutdownTrace.ts"],"names":[],"mappings":"AAKA,KAAK,oBAAoB,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAwMpD,eAAO,MAAM,aAAa;qBAlLJ,OAAO;iBAwFT,MAAM,YAAW,oBAAoB,KAAQ,IAAI;wBAY1C,MAAM,YAAW,oBAAoB,KAAQ,IAAI;6BA6CnE,MAAM,UACL,OAAO,YACN,oBAAoB,KAC5B,IAAI;EAmCL,CAAC"}
@@ -0,0 +1,165 @@
1
+ const TRACE_ENV_KEYS = ['SHUTDOWN_TRACE', 'DEBUG_SHUTDOWN_TRACE', 'WORKER_SHUTDOWN_TRACE'];
2
+ const MAX_HANDLE_DETAILS = 20;
3
+ const writeLine = (line) => {
4
+ const nodeProcess = getNodeProcess();
5
+ if (nodeProcess?.stderr && typeof nodeProcess.stderr.write === 'function') {
6
+ nodeProcess.stderr.write(`${line}\n`);
7
+ return;
8
+ }
9
+ if (typeof console !== 'undefined' && typeof console.info === 'function') {
10
+ console.info(line);
11
+ }
12
+ };
13
+ const getNodeProcess = () => {
14
+ if (typeof process === 'undefined')
15
+ return null;
16
+ return process;
17
+ };
18
+ const isEnabled = () => {
19
+ const nodeProcess = getNodeProcess();
20
+ if (nodeProcess === null)
21
+ return false;
22
+ return TRACE_ENV_KEYS.some((key) => {
23
+ const raw = nodeProcess.env[key];
24
+ if (typeof raw !== 'string')
25
+ return false;
26
+ const normalized = raw.trim().toLowerCase();
27
+ return (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on');
28
+ });
29
+ };
30
+ const getConstructorName = (value) => {
31
+ if (typeof value !== 'object' || value === null)
32
+ return typeof value;
33
+ const constructorValue = value.constructor;
34
+ return typeof constructorValue?.name === 'string' ? constructorValue.name : 'Unknown';
35
+ };
36
+ const hasFunction = (value, key) => {
37
+ return (typeof value === 'object' &&
38
+ value !== null &&
39
+ key in value &&
40
+ typeof value[key] === 'function');
41
+ };
42
+ const getOptionalValue = (value, key) => {
43
+ if (typeof value !== 'object' || value === null || !(key in value))
44
+ return undefined;
45
+ return value[key];
46
+ };
47
+ const summarizeHandle = (handle) => {
48
+ const constructorName = getConstructorName(handle);
49
+ const summary = {
50
+ type: constructorName,
51
+ };
52
+ const fd = getOptionalValue(handle, 'fd');
53
+ if (typeof fd === 'number') {
54
+ summary['fd'] = fd;
55
+ }
56
+ const localPort = getOptionalValue(handle, 'localPort');
57
+ if (typeof localPort === 'number') {
58
+ summary['localPort'] = localPort;
59
+ }
60
+ const remotePort = getOptionalValue(handle, 'remotePort');
61
+ if (typeof remotePort === 'number') {
62
+ summary['remotePort'] = remotePort;
63
+ }
64
+ const repeat = getOptionalValue(handle, '_repeat');
65
+ if (typeof repeat === 'number') {
66
+ summary['repeatMs'] = repeat;
67
+ }
68
+ const destroyed = getOptionalValue(handle, 'destroyed');
69
+ if (typeof destroyed === 'boolean') {
70
+ summary['destroyed'] = destroyed;
71
+ }
72
+ if (hasFunction(handle, 'hasRef')) {
73
+ try {
74
+ summary['hasRef'] = handle.hasRef();
75
+ }
76
+ catch {
77
+ summary['hasRef'] = 'error';
78
+ }
79
+ }
80
+ return summary;
81
+ };
82
+ const countTypes = (items) => {
83
+ return items.reduce((counts, item) => {
84
+ const type = getConstructorName(item);
85
+ counts[type] = (counts[type] ?? 0) + 1;
86
+ return counts;
87
+ }, {});
88
+ };
89
+ const log = (label, details = {}) => {
90
+ if (!isEnabled())
91
+ return;
92
+ writeLine(JSON.stringify({
93
+ level: 'info',
94
+ trace: 'shutdown',
95
+ label,
96
+ details,
97
+ }));
98
+ };
99
+ const logHandles = (label, details = {}) => {
100
+ if (!isEnabled())
101
+ return;
102
+ const nodeProcess = getNodeProcess();
103
+ if (nodeProcess === null) {
104
+ writeLine(JSON.stringify({
105
+ level: 'info',
106
+ trace: 'shutdown',
107
+ label,
108
+ details: {
109
+ ...details,
110
+ available: false,
111
+ reason: 'process unavailable',
112
+ },
113
+ }));
114
+ return;
115
+ }
116
+ const handles = typeof nodeProcess._getActiveHandles === 'function' ? nodeProcess._getActiveHandles() : [];
117
+ const requests = typeof nodeProcess._getActiveRequests === 'function' ? nodeProcess._getActiveRequests() : [];
118
+ writeLine(JSON.stringify({
119
+ level: 'info',
120
+ trace: 'shutdown',
121
+ label,
122
+ details: {
123
+ ...details,
124
+ available: true,
125
+ handleCount: handles.length,
126
+ requestCount: requests.length,
127
+ handleTypes: countTypes(handles),
128
+ requestTypes: countTypes(requests),
129
+ handles: handles.slice(0, MAX_HANDLE_DETAILS).map((handle) => summarizeHandle(handle)),
130
+ requests: requests.slice(0, MAX_HANDLE_DETAILS).map((request) => summarizeHandle(request)),
131
+ },
132
+ }));
133
+ };
134
+ const logBullMQWorker = (label, worker, details = {}) => {
135
+ if (!isEnabled())
136
+ return;
137
+ const name = getOptionalValue(worker, 'name');
138
+ const opts = getOptionalValue(worker, 'opts');
139
+ const connection = getOptionalValue(opts, 'connection');
140
+ const prefix = getOptionalValue(opts, 'prefix');
141
+ const concurrency = getOptionalValue(opts, 'concurrency');
142
+ const autorun = getOptionalValue(opts, 'autorun');
143
+ const closing = getOptionalValue(worker, 'closing');
144
+ writeLine(JSON.stringify({
145
+ level: 'info',
146
+ trace: 'shutdown',
147
+ label,
148
+ details: {
149
+ ...details,
150
+ workerType: getConstructorName(worker),
151
+ queueName: typeof name === 'string' ? name : undefined,
152
+ prefix: typeof prefix === 'string' ? prefix : undefined,
153
+ concurrency: typeof concurrency === 'number' ? concurrency : undefined,
154
+ autorun: typeof autorun === 'boolean' ? autorun : undefined,
155
+ connectionType: getConstructorName(connection),
156
+ closingState: closing === undefined ? 'idle' : getConstructorName(closing),
157
+ },
158
+ }));
159
+ };
160
+ export const ShutdownTrace = Object.freeze({
161
+ isEnabled,
162
+ log,
163
+ logHandles,
164
+ logBullMQWorker,
165
+ });