@zintrust/core 1.5.4 → 1.6.0
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/launcher.d.ts +6 -0
- package/bin/launcher.d.ts.map +1 -0
- package/bin/launcher.js +167 -0
- package/bin/z.js +2 -35
- package/bin/zin.js +2 -35
- package/bin/zintrust.d.ts +1 -1
- package/bin/zintrust.d.ts.map +1 -1
- package/bin/zintrust.js +3 -36
- package/bin/zt.d.ts +1 -1
- package/bin/zt.js +3 -23
- 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 +4 -2
- package/src/boot/registry/runtime.d.ts.map +1 -1
- package/src/boot/registry/runtime.js +67 -26
- 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 +148 -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
- package/src/zintrust.plugins.d.ts +3 -6
- package/src/zintrust.plugins.d.ts.map +1 -1
- package/src/zintrust.plugins.js +3 -6
|
@@ -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') {
|
|
@@ -489,8 +495,34 @@ const initializeSockets = (router) => {
|
|
|
489
495
|
Logger.info(`Transport: ${diagnostics.transport}`);
|
|
490
496
|
Logger.info(`Path: ${diagnostics.path}`);
|
|
491
497
|
};
|
|
492
|
-
|
|
493
|
-
|
|
498
|
+
const initializeRuntimeRoutes = async (params) => {
|
|
499
|
+
await initializeArtifactDirectories(params.resolvedBasePath);
|
|
500
|
+
await registerMasterRoutes(params.resolvedBasePath, params.router);
|
|
501
|
+
initializeSockets(params.router);
|
|
502
|
+
await initializeSystemTrace(params.router);
|
|
503
|
+
if (Cloudflare.getWorkersEnv() === null && appConfig.dockerWorker === false) {
|
|
504
|
+
if (appConfig.worker === true) {
|
|
505
|
+
await initializeWorkers(params.router);
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
Logger.info('Skipping worker route registration (WORKER_ENABLED=false).');
|
|
509
|
+
}
|
|
510
|
+
await initializeQueueMonitor(params.router);
|
|
511
|
+
if (appConfig.worker === true) {
|
|
512
|
+
await initializeQueueHttpGateway(params.router);
|
|
513
|
+
await initializeScheduleHttpGateway(params.router);
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
Logger.info('Skipping worker execution/gateway initialization (WORKER_ENABLED=false).');
|
|
517
|
+
}
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (!appConfig.dockerWorker) {
|
|
521
|
+
Logger.info('Skipping local worker dashboards in Cloudflare Workers runtime.');
|
|
522
|
+
}
|
|
523
|
+
};
|
|
524
|
+
const createBootLifecycle = (params) => {
|
|
525
|
+
return async () => {
|
|
494
526
|
if (params.getBooted())
|
|
495
527
|
return;
|
|
496
528
|
Logger.info(`🚀 Booting ZinTrust Application in ${params.environment} mode...`);
|
|
@@ -509,7 +541,6 @@ export const createLifecycle = (params) => {
|
|
|
509
541
|
warnings: startupConfigValidation.warnings,
|
|
510
542
|
});
|
|
511
543
|
}
|
|
512
|
-
// Preload project-owned config overrides that must be available synchronously.
|
|
513
544
|
await StartupConfigFileRegistry.preload([
|
|
514
545
|
StartupConfigFile.Middleware,
|
|
515
546
|
StartupConfigFile.Cache,
|
|
@@ -523,38 +554,43 @@ export const createLifecycle = (params) => {
|
|
|
523
554
|
FeatureFlags.initialize();
|
|
524
555
|
await StartupHealthChecks.assertHealthy();
|
|
525
556
|
await registerFromRuntimeConfig();
|
|
526
|
-
await
|
|
527
|
-
await registerMasterRoutes(params.resolvedBasePath, params.router);
|
|
528
|
-
initializeSockets(params.router);
|
|
529
|
-
await initializeSystemTrace(params.router);
|
|
530
|
-
if (Cloudflare.getWorkersEnv() === null && appConfig.dockerWorker === false) {
|
|
531
|
-
await initializeWorkers(params.router);
|
|
532
|
-
await initializeQueueMonitor(params.router);
|
|
533
|
-
if (appConfig.worker === true) {
|
|
534
|
-
await initializeQueueHttpGateway(params.router);
|
|
535
|
-
await initializeScheduleHttpGateway(params.router);
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
Logger.info('Skipping worker execution/gateway initialization (WORKER_ENABLED=false).');
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
else if (!appConfig.dockerWorker) {
|
|
542
|
-
Logger.info('Skipping local worker dashboards in Cloudflare Workers runtime.');
|
|
543
|
-
}
|
|
544
|
-
// Register service providers
|
|
545
|
-
// Bootstrap services
|
|
557
|
+
await initializeRuntimeRoutes(params);
|
|
546
558
|
Logger.info('✅ Application booted successfully');
|
|
547
559
|
params.setBooted(true);
|
|
548
560
|
};
|
|
549
|
-
|
|
561
|
+
};
|
|
562
|
+
const createShutdownLifecycle = (params) => {
|
|
563
|
+
return async () => {
|
|
550
564
|
Logger.info('🛑 Shutting down application...');
|
|
565
|
+
ShutdownTrace.logHandles('runtime.lifecycle.shutdown.start');
|
|
566
|
+
QueueReliabilityOrchestrator.stop();
|
|
567
|
+
ShutdownTrace.logHandles('runtime.lifecycle.shutdown.after-queue-reliability-stop');
|
|
568
|
+
if (runtimeQueueMonitor !== null && typeof runtimeQueueMonitor.close === 'function') {
|
|
569
|
+
try {
|
|
570
|
+
ShutdownTrace.log('runtime.lifecycle.shutdown.queue-monitor-close.start');
|
|
571
|
+
await runtimeQueueMonitor.close();
|
|
572
|
+
ShutdownTrace.logHandles('runtime.lifecycle.shutdown.queue-monitor-close.complete');
|
|
573
|
+
}
|
|
574
|
+
catch (error) {
|
|
575
|
+
Logger.warn('Queue Monitor shutdown failed', error);
|
|
576
|
+
ShutdownTrace.logHandles('runtime.lifecycle.shutdown.queue-monitor-close.failed', {
|
|
577
|
+
error: error instanceof Error ? error.message : String(error),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
finally {
|
|
581
|
+
runtimeQueueMonitor = null;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
551
584
|
try {
|
|
552
585
|
await params.shutdownManager.run();
|
|
586
|
+
ShutdownTrace.logHandles('runtime.lifecycle.shutdown.after-hooks');
|
|
553
587
|
}
|
|
554
588
|
catch (error) {
|
|
555
589
|
Logger.error('Shutdown hook failed:', error);
|
|
590
|
+
ShutdownTrace.logHandles('runtime.lifecycle.shutdown.hooks-failed', {
|
|
591
|
+
error: error instanceof Error ? error.message : String(error),
|
|
592
|
+
});
|
|
556
593
|
}
|
|
557
|
-
// Ensure FileLogWriter.flush is attempted even if dynamic registration failed.
|
|
558
594
|
try {
|
|
559
595
|
const fileLogWriter = await tryImportOptional('@config/FileLogWriter');
|
|
560
596
|
fileLogWriter?.FileLogWriter?.flush?.();
|
|
@@ -563,6 +599,11 @@ export const createLifecycle = (params) => {
|
|
|
563
599
|
/* best-effort */
|
|
564
600
|
}
|
|
565
601
|
params.setBooted(false);
|
|
602
|
+
ShutdownTrace.logHandles('runtime.lifecycle.shutdown.complete');
|
|
566
603
|
};
|
|
604
|
+
};
|
|
605
|
+
export const createLifecycle = (params) => {
|
|
606
|
+
const boot = createBootLifecycle(params);
|
|
607
|
+
const shutdown = createShutdownLifecycle(params);
|
|
567
608
|
return { boot, shutdown };
|
|
568
609
|
};
|
|
@@ -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;AAoLpD,eAAO,MAAM,aAAa;qBAnKJ,OAAO;iBAwFT,MAAM,YAAW,oBAAoB,KAAQ,IAAI;wBAY1C,MAAM,YAAW,oBAAoB,KAAQ,IAAI;6BA8BnE,MAAM,UACL,OAAO,YACN,oBAAoB,KAC5B,IAAI;EAmCL,CAAC"}
|