@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.
- package/bin/z.js +74 -7
- package/bin/zin.js +164 -7
- package/bin/zintrust.d.ts.map +1 -1
- package/bin/zintrust.js +74 -7
- package/bin/zt.js +81 -4
- package/package.json +1 -1
- package/src/boot/bootstrap.d.ts.map +1 -1
- package/src/boot/bootstrap.js +105 -40
- package/src/boot/registry/runtime.d.ts.map +1 -1
- package/src/boot/registry/runtime.js +37 -2
- package/src/boot/registry/worker.d.ts.map +1 -1
- package/src/boot/registry/worker.js +3 -2
- package/src/cli/utils/spawn.d.ts.map +1 -1
- package/src/cli/utils/spawn.js +255 -46
- package/src/helper/ShutdownTrace.d.ts +9 -0
- package/src/helper/ShutdownTrace.d.ts.map +1 -0
- package/src/helper/ShutdownTrace.js +165 -0
- package/src/helper/index.d.ts +1 -0
- package/src/helper/index.d.ts.map +1 -1
- package/src/helper/index.js +1 -0
- package/src/index.js +3 -3
- package/src/tools/queue/QueueReliabilityOrchestrator.d.ts.map +1 -1
- package/src/tools/queue/QueueReliabilityOrchestrator.js +11 -0
|
@@ -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
|
-
|
|
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,
|
|
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 (
|
|
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;
|
|
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"}
|
package/src/cli/utils/spawn.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
97
|
+
resolve(childResult);
|
|
59
98
|
};
|
|
60
99
|
const onExitEvent = (code, signal) => {
|
|
61
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
79
|
-
|
|
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
|
|
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 =
|
|
116
|
-
const ttySignalForwardDelayMs =
|
|
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 = (
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
+
});
|