neex 0.6.50 → 0.6.51
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/dist/src/commands/start-commands.js +142 -153
- package/dist/src/start-manager.js +350 -304
- package/package.json +1 -1
|
@@ -8,63 +8,86 @@ const start_manager_js_1 = require("../start-manager.js");
|
|
|
8
8
|
const logger_manager_js_1 = require("../logger-manager.js");
|
|
9
9
|
const chalk_1 = __importDefault(require("chalk"));
|
|
10
10
|
const figures_1 = __importDefault(require("figures"));
|
|
11
|
-
const
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
12
13
|
function addStartCommands(program) {
|
|
13
14
|
let startManager = null;
|
|
14
|
-
//
|
|
15
|
+
// Start command for production execution
|
|
15
16
|
program
|
|
16
|
-
.command('start [
|
|
17
|
-
.description('Start
|
|
18
|
-
.option('-
|
|
19
|
-
.option('-
|
|
20
|
-
.option('-
|
|
21
|
-
.option('-w, --
|
|
17
|
+
.command('start [file]')
|
|
18
|
+
.description('Start production application (default: dist/index.js)')
|
|
19
|
+
.option('-d, --dir <directory>', 'Working directory', process.cwd())
|
|
20
|
+
.option('-e, --env <file>', 'Environment file to load', '.env')
|
|
21
|
+
.option('-p, --port <port>', 'Port number (if applicable)', parseInt)
|
|
22
|
+
.option('-w, --workers <count>', 'Number of worker processes', parseInt, 1)
|
|
22
23
|
.option('-c, --cluster', 'Enable cluster mode')
|
|
23
|
-
.option('--
|
|
24
|
-
.option('-
|
|
25
|
-
.option('-
|
|
26
|
-
.option('--graceful-timeout <ms>', 'Graceful shutdown timeout in milliseconds', '5000')
|
|
27
|
-
.option('-v, --verbose', 'Verbose output')
|
|
28
|
-
.option('-q, --quiet', 'Quiet output')
|
|
24
|
+
.option('-m, --memory <limit>', 'Memory limit (e.g., 512M, 1G)')
|
|
25
|
+
.option('-l, --log-level <level>', 'Log level (error, warn, info, debug)', 'info')
|
|
26
|
+
.option('-f, --log-file <file>', 'Log file path')
|
|
29
27
|
.option('--no-color', 'Disable colored output')
|
|
30
|
-
.option('
|
|
31
|
-
.option('
|
|
28
|
+
.option('-q, --quiet', 'Quiet mode')
|
|
29
|
+
.option('-v, --verbose', 'Verbose output')
|
|
30
|
+
.option('--watch', 'Watch for changes and restart')
|
|
31
|
+
.option('--max-memory <limit>', 'Maximum memory before restart (e.g., 1G)')
|
|
32
|
+
.option('--max-crashes <count>', 'Maximum crashes before giving up', parseInt, 5)
|
|
33
|
+
.option('--restart-delay <ms>', 'Delay between restarts (ms)', parseInt, 1000)
|
|
34
|
+
.option('--health-check', 'Enable health check monitoring')
|
|
35
|
+
.option('--health-port <port>', 'Health check port', parseInt, 3001)
|
|
36
|
+
.option('--graceful-timeout <ms>', 'Graceful shutdown timeout (ms)', parseInt, 30000)
|
|
32
37
|
.option('--inspect', 'Enable Node.js inspector')
|
|
33
|
-
.option('--inspect-
|
|
34
|
-
.option('--
|
|
35
|
-
.
|
|
36
|
-
.option('--restart-delay <ms>', 'Delay between restarts in milliseconds', '1000')
|
|
37
|
-
.option('--max-restarts <count>', 'Maximum restart attempts', '10')
|
|
38
|
-
.action(async (entry, options) => {
|
|
38
|
+
.option('--inspect-brk', 'Enable Node.js inspector with break')
|
|
39
|
+
.option('--node-args <args>', 'Additional Node.js arguments')
|
|
40
|
+
.action(async (file, options) => {
|
|
39
41
|
try {
|
|
40
|
-
const
|
|
42
|
+
const targetFile = file || 'dist/index.js';
|
|
43
|
+
const resolvedFile = path_1.default.resolve(options.dir, targetFile);
|
|
44
|
+
// Check if file exists
|
|
45
|
+
if (!fs_1.default.existsSync(resolvedFile)) {
|
|
46
|
+
// Try to find main file from package.json
|
|
47
|
+
const packageJsonPath = path_1.default.join(options.dir, 'package.json');
|
|
48
|
+
if (fs_1.default.existsSync(packageJsonPath)) {
|
|
49
|
+
const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
|
|
50
|
+
const mainFile = packageJson.main || 'index.js';
|
|
51
|
+
const alternativeFile = path_1.default.resolve(options.dir, mainFile);
|
|
52
|
+
if (fs_1.default.existsSync(alternativeFile)) {
|
|
53
|
+
if (options.verbose) {
|
|
54
|
+
logger_manager_js_1.loggerManager.printLine(`Using main file from package.json: ${mainFile}`, 'info');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
throw new Error(`Neither ${targetFile} nor ${mainFile} found. Please build your project first.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
throw new Error(`File not found: ${resolvedFile}. Please build your project first.`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
41
65
|
if (!options.quiet) {
|
|
42
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.blue(figures_1.default.info)} Starting production server
|
|
66
|
+
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.blue(figures_1.default.info)} Starting production server: ${chalk_1.default.cyan(targetFile)}`, 'info');
|
|
43
67
|
}
|
|
44
|
-
const nodeArgs = options.nodeArgs ?
|
|
45
|
-
options.nodeArgs.split(',').map((arg) => arg.trim()) : [];
|
|
46
68
|
startManager = new start_manager_js_1.StartManager({
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
file: resolvedFile,
|
|
70
|
+
workingDir: options.dir,
|
|
71
|
+
envFile: options.env,
|
|
72
|
+
port: options.port,
|
|
73
|
+
workers: options.workers,
|
|
52
74
|
cluster: options.cluster,
|
|
53
|
-
|
|
54
|
-
memory: parseInt(options.memory),
|
|
55
|
-
timeout: parseInt(options.timeout),
|
|
56
|
-
gracefulTimeout: parseInt(options.gracefulTimeout),
|
|
57
|
-
verbose: options.verbose,
|
|
58
|
-
quiet: options.quiet,
|
|
59
|
-
color: options.color,
|
|
75
|
+
memoryLimit: options.memory,
|
|
60
76
|
logLevel: options.logLevel,
|
|
61
|
-
|
|
77
|
+
logFile: options.logFile,
|
|
78
|
+
color: options.color,
|
|
79
|
+
quiet: options.quiet,
|
|
80
|
+
verbose: options.verbose,
|
|
81
|
+
watch: options.watch,
|
|
82
|
+
maxMemory: options.maxMemory,
|
|
83
|
+
maxCrashes: options.maxCrashes,
|
|
84
|
+
restartDelay: options.restartDelay,
|
|
85
|
+
healthCheck: options.healthCheck,
|
|
86
|
+
healthPort: options.healthPort,
|
|
87
|
+
gracefulTimeout: options.gracefulTimeout,
|
|
62
88
|
inspect: options.inspect,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
restart: options.restart,
|
|
66
|
-
restartDelay: parseInt(options.restartDelay),
|
|
67
|
-
maxRestarts: parseInt(options.maxRestarts)
|
|
89
|
+
inspectBrk: options.inspectBrk,
|
|
90
|
+
nodeArgs: options.nodeArgs
|
|
68
91
|
});
|
|
69
92
|
await startManager.start();
|
|
70
93
|
}
|
|
@@ -78,135 +101,109 @@ function addStartCommands(program) {
|
|
|
78
101
|
process.exit(1);
|
|
79
102
|
}
|
|
80
103
|
});
|
|
81
|
-
// Quick start command
|
|
104
|
+
// Quick start command
|
|
82
105
|
program
|
|
83
|
-
.command('
|
|
84
|
-
.description('Quick start
|
|
85
|
-
.
|
|
86
|
-
.option('-v, --verbose', 'Verbose output')
|
|
87
|
-
.action(async (entry, options) => {
|
|
106
|
+
.command('run [file]')
|
|
107
|
+
.description('Quick start without options (alias for start)')
|
|
108
|
+
.action(async (file) => {
|
|
88
109
|
try {
|
|
89
|
-
const
|
|
90
|
-
|
|
110
|
+
const targetFile = file || 'dist/index.js';
|
|
111
|
+
if (!fs_1.default.existsSync(targetFile)) {
|
|
112
|
+
throw new Error(`File not found: ${targetFile}. Please build your project first.`);
|
|
113
|
+
}
|
|
114
|
+
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.blue(figures_1.default.info)} Running: ${chalk_1.default.cyan(targetFile)}`, 'info');
|
|
91
115
|
startManager = new start_manager_js_1.StartManager({
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
watch: true,
|
|
97
|
-
cluster: false,
|
|
116
|
+
file: path_1.default.resolve(targetFile),
|
|
117
|
+
workingDir: process.cwd(),
|
|
118
|
+
envFile: '.env',
|
|
119
|
+
port: undefined,
|
|
98
120
|
workers: 1,
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
gracefulTimeout: 3000,
|
|
102
|
-
verbose: options.verbose,
|
|
103
|
-
quiet: false,
|
|
104
|
-
color: true,
|
|
121
|
+
cluster: false,
|
|
122
|
+
memoryLimit: undefined,
|
|
105
123
|
logLevel: 'info',
|
|
106
|
-
|
|
124
|
+
logFile: undefined,
|
|
125
|
+
color: true,
|
|
126
|
+
quiet: false,
|
|
127
|
+
verbose: false,
|
|
128
|
+
watch: false,
|
|
129
|
+
maxMemory: undefined,
|
|
130
|
+
maxCrashes: 5,
|
|
131
|
+
restartDelay: 1000,
|
|
132
|
+
healthCheck: false,
|
|
133
|
+
healthPort: 3001,
|
|
134
|
+
gracefulTimeout: 30000,
|
|
107
135
|
inspect: false,
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
restart: true,
|
|
111
|
-
restartDelay: 500,
|
|
112
|
-
maxRestarts: 50
|
|
136
|
+
inspectBrk: false,
|
|
137
|
+
nodeArgs: undefined
|
|
113
138
|
});
|
|
114
139
|
await startManager.start();
|
|
115
140
|
}
|
|
116
141
|
catch (error) {
|
|
117
142
|
if (error instanceof Error) {
|
|
118
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)}
|
|
143
|
+
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} Run failed: ${error.message}`, 'error');
|
|
119
144
|
}
|
|
120
145
|
else {
|
|
121
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} An unknown
|
|
146
|
+
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} An unknown run error occurred`, 'error');
|
|
122
147
|
}
|
|
123
148
|
process.exit(1);
|
|
124
149
|
}
|
|
125
150
|
});
|
|
126
|
-
// Production
|
|
151
|
+
// Production command with clustering
|
|
127
152
|
program
|
|
128
|
-
.command('
|
|
129
|
-
.description('
|
|
130
|
-
.option('-
|
|
131
|
-
.option('-
|
|
132
|
-
.option('-
|
|
133
|
-
.option('--
|
|
134
|
-
.option('-
|
|
135
|
-
.action(async (
|
|
153
|
+
.command('prod [file]')
|
|
154
|
+
.description('Start in production mode with clustering')
|
|
155
|
+
.option('-w, --workers <count>', 'Number of workers (default: CPU cores)', parseInt)
|
|
156
|
+
.option('-p, --port <port>', 'Port number', parseInt)
|
|
157
|
+
.option('-e, --env <file>', 'Environment file', '.env.production')
|
|
158
|
+
.option('-l, --log-file <file>', 'Log file path')
|
|
159
|
+
.option('-q, --quiet', 'Quiet mode')
|
|
160
|
+
.action(async (file, options) => {
|
|
136
161
|
try {
|
|
137
|
-
const
|
|
138
|
-
|
|
162
|
+
const targetFile = file || 'dist/index.js';
|
|
163
|
+
const resolvedFile = path_1.default.resolve(targetFile);
|
|
164
|
+
if (!fs_1.default.existsSync(resolvedFile)) {
|
|
165
|
+
throw new Error(`File not found: ${resolvedFile}. Please build your project first.`);
|
|
166
|
+
}
|
|
167
|
+
const workers = options.workers || require('os').cpus().length;
|
|
168
|
+
if (!options.quiet) {
|
|
169
|
+
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.blue(figures_1.default.info)} Starting production server with ${workers} workers`, 'info');
|
|
170
|
+
}
|
|
139
171
|
startManager = new start_manager_js_1.StartManager({
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
172
|
+
file: resolvedFile,
|
|
173
|
+
workingDir: process.cwd(),
|
|
174
|
+
envFile: options.env,
|
|
175
|
+
port: options.port,
|
|
176
|
+
workers,
|
|
145
177
|
cluster: true,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
gracefulTimeout: 10000,
|
|
150
|
-
verbose: options.verbose,
|
|
151
|
-
quiet: false,
|
|
178
|
+
memoryLimit: undefined,
|
|
179
|
+
logLevel: 'info',
|
|
180
|
+
logFile: options.logFile,
|
|
152
181
|
color: false,
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
restart: true,
|
|
182
|
+
quiet: options.quiet,
|
|
183
|
+
verbose: false,
|
|
184
|
+
watch: false,
|
|
185
|
+
maxMemory: '1G',
|
|
186
|
+
maxCrashes: 3,
|
|
159
187
|
restartDelay: 2000,
|
|
160
|
-
|
|
188
|
+
healthCheck: true,
|
|
189
|
+
healthPort: 3001,
|
|
190
|
+
gracefulTimeout: 30000,
|
|
191
|
+
inspect: false,
|
|
192
|
+
inspectBrk: false,
|
|
193
|
+
nodeArgs: undefined
|
|
161
194
|
});
|
|
162
195
|
await startManager.start();
|
|
163
196
|
}
|
|
164
197
|
catch (error) {
|
|
165
198
|
if (error instanceof Error) {
|
|
166
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)}
|
|
199
|
+
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} Production start failed: ${error.message}`, 'error');
|
|
167
200
|
}
|
|
168
201
|
else {
|
|
169
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} An unknown
|
|
202
|
+
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} An unknown production start error occurred`, 'error');
|
|
170
203
|
}
|
|
171
204
|
process.exit(1);
|
|
172
205
|
}
|
|
173
206
|
});
|
|
174
|
-
// Health check command
|
|
175
|
-
program
|
|
176
|
-
.command('health [url]')
|
|
177
|
-
.description('Check server health')
|
|
178
|
-
.option('-t, --timeout <ms>', 'Request timeout in milliseconds', '5000')
|
|
179
|
-
.option('-i, --interval <ms>', 'Check interval in milliseconds', '1000')
|
|
180
|
-
.option('-c, --count <number>', 'Number of checks', '1')
|
|
181
|
-
.action(async (url, options) => {
|
|
182
|
-
const checkUrl = url || 'http://localhost:3000/health';
|
|
183
|
-
const timeout = parseInt(options.timeout);
|
|
184
|
-
const interval = parseInt(options.interval);
|
|
185
|
-
const count = parseInt(options.count);
|
|
186
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.blue(figures_1.default.info)} Health check: ${checkUrl}`, 'info');
|
|
187
|
-
for (let i = 0; i < count; i++) {
|
|
188
|
-
try {
|
|
189
|
-
const controller = new AbortController();
|
|
190
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
191
|
-
const response = await fetch(checkUrl, {
|
|
192
|
-
signal: controller.signal
|
|
193
|
-
});
|
|
194
|
-
clearTimeout(timeoutId);
|
|
195
|
-
if (response.ok) {
|
|
196
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green(figures_1.default.tick)} Health check ${i + 1}/${count}: OK (${response.status})`, 'info');
|
|
197
|
-
}
|
|
198
|
-
else {
|
|
199
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} Health check ${i + 1}/${count}: Failed (${response.status})`, 'error');
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
catch (error) {
|
|
203
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} Health check ${i + 1}/${count}: Error (${error.message})`, 'error');
|
|
204
|
-
}
|
|
205
|
-
if (i < count - 1) {
|
|
206
|
-
await new Promise(resolve => setTimeout(resolve, interval));
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
207
|
// Cleanup function
|
|
211
208
|
const cleanupStart = async () => {
|
|
212
209
|
if (startManager) {
|
|
@@ -222,7 +219,7 @@ function addStartCommands(program) {
|
|
|
222
219
|
// Handle process termination
|
|
223
220
|
const handleExit = (signal) => {
|
|
224
221
|
if (startManager) {
|
|
225
|
-
logger_manager_js_1.loggerManager.printLine(`\n${chalk_1.default.yellow(figures_1.default.warning)} Received ${signal},
|
|
222
|
+
logger_manager_js_1.loggerManager.printLine(`\n${chalk_1.default.yellow(figures_1.default.warning)} Received ${signal}, stopping server...`, 'info');
|
|
226
223
|
cleanupStart().then(() => {
|
|
227
224
|
process.exit(0);
|
|
228
225
|
}).catch(() => {
|
|
@@ -236,19 +233,11 @@ function addStartCommands(program) {
|
|
|
236
233
|
// Register signal handlers
|
|
237
234
|
process.on('SIGINT', () => handleExit('SIGINT'));
|
|
238
235
|
process.on('SIGTERM', () => handleExit('SIGTERM'));
|
|
239
|
-
process.on('
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
process.exit(1);
|
|
245
|
-
});
|
|
246
|
-
});
|
|
247
|
-
process.on('unhandledRejection', (reason, promise) => {
|
|
248
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} Unhandled rejection at: ${promise} reason: ${reason}`, 'error');
|
|
249
|
-
cleanupStart().then(() => {
|
|
250
|
-
process.exit(1);
|
|
251
|
-
});
|
|
236
|
+
process.on('SIGUSR2', () => handleExit('SIGUSR2')); // PM2 restart
|
|
237
|
+
process.on('exit', () => {
|
|
238
|
+
if (startManager) {
|
|
239
|
+
cleanupStart();
|
|
240
|
+
}
|
|
252
241
|
});
|
|
253
242
|
return { cleanupStart };
|
|
254
243
|
}
|
|
@@ -4,407 +4,453 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.StartManager = void 0;
|
|
7
|
-
// src/start-manager.ts - Production
|
|
7
|
+
// src/start-manager.ts - Production start manager with clustering and monitoring
|
|
8
8
|
const child_process_1 = require("child_process");
|
|
9
9
|
const chokidar_1 = require("chokidar");
|
|
10
10
|
const logger_manager_js_1 = require("./logger-manager.js");
|
|
11
11
|
const chalk_1 = __importDefault(require("chalk"));
|
|
12
|
-
const figures_1 = __importDefault(require("figures"));
|
|
13
12
|
const path_1 = __importDefault(require("path"));
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const http_1 = __importDefault(require("http"));
|
|
15
|
+
const lodash_1 = require("lodash");
|
|
17
16
|
class StartManager {
|
|
18
17
|
constructor(options) {
|
|
19
|
-
this.
|
|
18
|
+
this.workers = new Map();
|
|
19
|
+
this.masterProcess = null;
|
|
20
20
|
this.watcher = null;
|
|
21
|
-
this.
|
|
22
|
-
this.restartCount = 0;
|
|
23
|
-
this.startTime = 0;
|
|
21
|
+
this.healthServer = null;
|
|
24
22
|
this.isShuttingDown = false;
|
|
25
|
-
this.
|
|
23
|
+
this.logStream = null;
|
|
24
|
+
this.totalRestarts = 0;
|
|
26
25
|
this.options = options;
|
|
26
|
+
this.startTime = new Date();
|
|
27
|
+
this.debouncedRestart = (0, lodash_1.debounce)(this.restartAll.bind(this), options.restartDelay);
|
|
28
|
+
this.setupLogging();
|
|
27
29
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
30
|
+
setupLogging() {
|
|
31
|
+
if (this.options.logFile) {
|
|
32
|
+
try {
|
|
33
|
+
const logDir = path_1.default.dirname(this.options.logFile);
|
|
34
|
+
if (!fs_1.default.existsSync(logDir)) {
|
|
35
|
+
fs_1.default.mkdirSync(logDir, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
this.logStream = fs_1.default.createWriteStream(this.options.logFile, { flags: 'a' });
|
|
38
|
+
if (this.options.verbose) {
|
|
39
|
+
logger_manager_js_1.loggerManager.printLine(`Logging to: ${this.options.logFile}`, 'info');
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
logger_manager_js_1.loggerManager.printLine(`Failed to setup logging: ${error.message}`, 'warn');
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
46
|
+
}
|
|
47
|
+
log(message, level = 'info') {
|
|
48
|
+
const timestamp = new Date().toISOString();
|
|
49
|
+
const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
|
|
50
|
+
if (this.logStream) {
|
|
51
|
+
this.logStream.write(logMessage + '\n');
|
|
45
52
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const distJsPath = path_1.default.join('dist', entryPath + '.js');
|
|
49
|
-
if ((0, fs_1.existsSync)(distJsPath)) {
|
|
50
|
-
return path_1.default.resolve(distJsPath);
|
|
51
|
-
}
|
|
53
|
+
if (!this.options.quiet) {
|
|
54
|
+
logger_manager_js_1.loggerManager.printLine(message, level);
|
|
52
55
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
56
|
+
}
|
|
57
|
+
loadEnvFile() {
|
|
58
|
+
if (this.options.envFile && fs_1.default.existsSync(this.options.envFile)) {
|
|
59
|
+
try {
|
|
60
|
+
const envContent = fs_1.default.readFileSync(this.options.envFile, 'utf8');
|
|
61
|
+
const lines = envContent.split('\n');
|
|
62
|
+
for (const line of lines) {
|
|
63
|
+
const trimmed = line.trim();
|
|
64
|
+
if (trimmed && !trimmed.startsWith('#')) {
|
|
65
|
+
const [key, ...values] = trimmed.split('=');
|
|
66
|
+
if (key && values.length > 0) {
|
|
67
|
+
const value = values.join('=').trim();
|
|
68
|
+
// Remove quotes if present
|
|
69
|
+
const cleanValue = value.replace(/^["']|["']$/g, '');
|
|
70
|
+
process.env[key.trim()] = cleanValue;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (this.options.verbose) {
|
|
75
|
+
this.log(`Loaded environment variables from ${this.options.envFile}`);
|
|
66
76
|
}
|
|
67
|
-
return path_1.default.resolve(entry);
|
|
68
77
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
async loadPackageJson() {
|
|
73
|
-
try {
|
|
74
|
-
const packageJsonPath = path_1.default.join(process.cwd(), 'package.json');
|
|
75
|
-
if ((0, fs_1.existsSync)(packageJsonPath)) {
|
|
76
|
-
const content = await promises_1.default.readFile(packageJsonPath, 'utf8');
|
|
77
|
-
return JSON.parse(content);
|
|
78
|
+
catch (error) {
|
|
79
|
+
this.log(`Failed to load environment file: ${error.message}`, 'warn');
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
}
|
|
83
|
+
parseMemoryLimit(limit) {
|
|
84
|
+
var _a;
|
|
85
|
+
if (!limit)
|
|
86
|
+
return undefined;
|
|
87
|
+
const match = limit.match(/^(\d+)([KMGT]?)$/i);
|
|
88
|
+
if (!match)
|
|
89
|
+
return undefined;
|
|
90
|
+
const value = parseInt(match[1]);
|
|
91
|
+
const unit = ((_a = match[2]) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || '';
|
|
92
|
+
const multipliers = { K: 1024, M: 1024 * 1024, G: 1024 * 1024 * 1024, T: 1024 * 1024 * 1024 * 1024 };
|
|
93
|
+
return value * (multipliers[unit] || 1);
|
|
84
94
|
}
|
|
85
95
|
getNodeArgs() {
|
|
86
96
|
const args = [];
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
args.push(...this.options.nodeArgs);
|
|
97
|
+
if (this.options.memoryLimit) {
|
|
98
|
+
args.push(`--max-old-space-size=${this.parseMemoryLimit(this.options.memoryLimit) / (1024 * 1024)}`);
|
|
90
99
|
}
|
|
91
|
-
// Memory limit
|
|
92
|
-
if (this.options.memory > 0) {
|
|
93
|
-
args.push(`--max-old-space-size=${this.options.memory}`);
|
|
94
|
-
}
|
|
95
|
-
// Debugging
|
|
96
100
|
if (this.options.inspect) {
|
|
97
|
-
args.push(
|
|
101
|
+
args.push('--inspect');
|
|
102
|
+
}
|
|
103
|
+
if (this.options.inspectBrk) {
|
|
104
|
+
args.push('--inspect-brk');
|
|
98
105
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
args.push('--optimize-for-size');
|
|
106
|
+
if (this.options.nodeArgs) {
|
|
107
|
+
args.push(...this.options.nodeArgs.split(' '));
|
|
102
108
|
}
|
|
103
109
|
return args;
|
|
104
110
|
}
|
|
105
|
-
|
|
111
|
+
async startWorker(workerId) {
|
|
112
|
+
var _a, _b;
|
|
113
|
+
const nodeArgs = this.getNodeArgs();
|
|
106
114
|
const env = {
|
|
107
115
|
...process.env,
|
|
108
|
-
NODE_ENV:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
NODE_ENV: process.env.NODE_ENV || 'production',
|
|
117
|
+
WORKER_ID: workerId.toString(),
|
|
118
|
+
CLUSTER_WORKER: 'true',
|
|
119
|
+
FORCE_COLOR: this.options.color ? '1' : '0'
|
|
112
120
|
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
env.FORCE_COLOR = '1';
|
|
121
|
+
if (this.options.port) {
|
|
122
|
+
env.PORT = this.options.port.toString();
|
|
116
123
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
const workerProcess = (0, child_process_1.fork)(this.options.file, [], {
|
|
125
|
+
cwd: this.options.workingDir,
|
|
126
|
+
env,
|
|
127
|
+
execArgv: nodeArgs,
|
|
128
|
+
silent: true
|
|
129
|
+
});
|
|
130
|
+
const workerInfo = {
|
|
131
|
+
process: workerProcess,
|
|
132
|
+
pid: workerProcess.pid,
|
|
133
|
+
restarts: 0,
|
|
134
|
+
startTime: new Date(),
|
|
135
|
+
memoryUsage: 0,
|
|
136
|
+
cpuUsage: 0
|
|
137
|
+
};
|
|
138
|
+
this.workers.set(workerId, workerInfo);
|
|
139
|
+
// Handle worker output
|
|
140
|
+
(_a = workerProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
|
|
141
|
+
if (!this.options.quiet) {
|
|
142
|
+
const message = data.toString().trim();
|
|
143
|
+
if (message) {
|
|
144
|
+
console.log(chalk_1.default.dim(`[Worker ${workerId}]`) + ' ' + message);
|
|
125
145
|
}
|
|
126
146
|
}
|
|
127
|
-
|
|
128
|
-
|
|
147
|
+
});
|
|
148
|
+
(_b = workerProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
|
|
149
|
+
if (!this.options.quiet) {
|
|
150
|
+
const message = data.toString().trim();
|
|
151
|
+
if (message) {
|
|
152
|
+
console.error(chalk_1.default.dim(`[Worker ${workerId}]`) + ' ' + chalk_1.default.red(message));
|
|
153
|
+
}
|
|
129
154
|
}
|
|
130
|
-
}
|
|
155
|
+
});
|
|
156
|
+
workerProcess.on('error', (error) => {
|
|
157
|
+
this.log(`Worker ${workerId} error: ${error.message}`, 'error');
|
|
158
|
+
});
|
|
159
|
+
workerProcess.on('exit', (code, signal) => {
|
|
160
|
+
this.workers.delete(workerId);
|
|
161
|
+
if (!this.isShuttingDown) {
|
|
162
|
+
if (code !== 0) {
|
|
163
|
+
this.log(`Worker ${workerId} exited with code ${code}`, 'error');
|
|
164
|
+
this.restartWorker(workerId);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
this.log(`Worker ${workerId} exited gracefully`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
workerProcess.on('message', (message) => {
|
|
172
|
+
if (this.options.verbose) {
|
|
173
|
+
this.log(`Worker ${workerId} message: ${JSON.stringify(message)}`);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
this.log(`Started worker ${workerId} (PID: ${workerProcess.pid})`);
|
|
177
|
+
return workerInfo;
|
|
131
178
|
}
|
|
132
|
-
async
|
|
133
|
-
|
|
179
|
+
async restartWorker(workerId) {
|
|
180
|
+
const workerInfo = this.workers.get(workerId);
|
|
181
|
+
if (workerInfo) {
|
|
182
|
+
workerInfo.restarts++;
|
|
183
|
+
this.totalRestarts++;
|
|
184
|
+
if (workerInfo.restarts >= this.options.maxCrashes) {
|
|
185
|
+
this.log(`Worker ${workerId} reached max crashes (${this.options.maxCrashes}), not restarting`, 'error');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.log(`Restarting worker ${workerId} (restart #${workerInfo.restarts})`);
|
|
189
|
+
// Graceful shutdown
|
|
134
190
|
try {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
logger_manager_js_1.loggerManager.printLine(`PID file removed: ${this.options.pid}`, 'info');
|
|
138
|
-
}
|
|
191
|
+
workerInfo.process.kill('SIGTERM');
|
|
192
|
+
await this.waitForProcessExit(workerInfo.process, 5000);
|
|
139
193
|
}
|
|
140
194
|
catch (error) {
|
|
141
|
-
|
|
195
|
+
workerInfo.process.kill('SIGKILL');
|
|
142
196
|
}
|
|
197
|
+
// Start new worker
|
|
198
|
+
setTimeout(() => {
|
|
199
|
+
this.startWorker(workerId);
|
|
200
|
+
}, this.options.restartDelay);
|
|
143
201
|
}
|
|
144
202
|
}
|
|
145
|
-
async
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const nodeArgs = this.getNodeArgs();
|
|
154
|
-
const env = this.getEnvironment();
|
|
155
|
-
if (!this.options.quiet) {
|
|
156
|
-
const restartInfo = this.restartCount > 0 ? ` (restart #${this.restartCount})` : '';
|
|
157
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.blue(figures_1.default.info)} Starting server${restartInfo}...`, 'info');
|
|
158
|
-
if (this.options.verbose) {
|
|
159
|
-
logger_manager_js_1.loggerManager.printLine(`Entry: ${entryPath}`, 'info');
|
|
160
|
-
logger_manager_js_1.loggerManager.printLine(`Environment: ${this.options.env}`, 'info');
|
|
161
|
-
logger_manager_js_1.loggerManager.printLine(`Host: ${this.options.host}:${this.options.port}`, 'info');
|
|
162
|
-
if (nodeArgs.length > 0) {
|
|
163
|
-
logger_manager_js_1.loggerManager.printLine(`Node args: ${nodeArgs.join(' ')}`, 'info');
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
const args = [...nodeArgs, entryPath];
|
|
168
|
-
this.serverProcess = (0, child_process_1.spawn)('node', args, {
|
|
169
|
-
stdio: this.options.verbose ? ['ignore', 'pipe', 'pipe'] : ['ignore', 'ignore', 'pipe'],
|
|
170
|
-
env,
|
|
171
|
-
detached: false,
|
|
172
|
-
cwd: process.cwd()
|
|
203
|
+
async waitForProcessExit(process, timeout) {
|
|
204
|
+
return new Promise((resolve, reject) => {
|
|
205
|
+
const timer = setTimeout(() => {
|
|
206
|
+
reject(new Error('Process exit timeout'));
|
|
207
|
+
}, timeout);
|
|
208
|
+
process.on('exit', () => {
|
|
209
|
+
clearTimeout(timer);
|
|
210
|
+
resolve();
|
|
173
211
|
});
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
async startSingleProcess() {
|
|
215
|
+
const nodeArgs = this.getNodeArgs();
|
|
216
|
+
const env = {
|
|
217
|
+
...process.env,
|
|
218
|
+
NODE_ENV: process.env.NODE_ENV || 'production',
|
|
219
|
+
FORCE_COLOR: this.options.color ? '1' : '0'
|
|
220
|
+
};
|
|
221
|
+
if (this.options.port) {
|
|
222
|
+
env.PORT = this.options.port.toString();
|
|
223
|
+
}
|
|
224
|
+
this.masterProcess = (0, child_process_1.spawn)('node', [...nodeArgs, this.options.file], {
|
|
225
|
+
cwd: this.options.workingDir,
|
|
226
|
+
env,
|
|
227
|
+
stdio: this.options.quiet ? 'ignore' : 'inherit'
|
|
228
|
+
});
|
|
229
|
+
this.masterProcess.on('error', (error) => {
|
|
230
|
+
this.log(`Process error: ${error.message}`, 'error');
|
|
231
|
+
});
|
|
232
|
+
this.masterProcess.on('exit', (code, signal) => {
|
|
233
|
+
if (!this.isShuttingDown) {
|
|
234
|
+
if (code !== 0) {
|
|
235
|
+
this.log(`Process exited with code ${code}`, 'error');
|
|
236
|
+
if (this.totalRestarts < this.options.maxCrashes) {
|
|
237
|
+
this.totalRestarts++;
|
|
238
|
+
this.log(`Restarting process (restart #${this.totalRestarts})`);
|
|
239
|
+
setTimeout(() => {
|
|
240
|
+
this.startSingleProcess();
|
|
241
|
+
}, this.options.restartDelay);
|
|
183
242
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (this.serverProcess.stderr) {
|
|
188
|
-
this.serverProcess.stderr.on('data', (data) => {
|
|
189
|
-
const error = data.toString().trim();
|
|
190
|
-
if (error && !this.options.quiet) {
|
|
191
|
-
logger_manager_js_1.loggerManager.printLine(`[SERVER] ${error}`, 'error');
|
|
243
|
+
else {
|
|
244
|
+
this.log(`Reached max crashes (${this.options.maxCrashes}), giving up`, 'error');
|
|
245
|
+
process.exit(1);
|
|
192
246
|
}
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
// Handle process events
|
|
196
|
-
this.serverProcess.on('error', (error) => {
|
|
197
|
-
this.isStarting = false;
|
|
198
|
-
if (!this.isShuttingDown) {
|
|
199
|
-
logger_manager_js_1.loggerManager.printLine(`Server process error: ${error.message}`, 'error');
|
|
200
|
-
this.handleServerExit(1);
|
|
201
247
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
this.isStarting = false;
|
|
205
|
-
if (!this.isShuttingDown) {
|
|
206
|
-
this.handleServerExit(code || 0, signal || undefined);
|
|
248
|
+
else {
|
|
249
|
+
this.log('Process exited gracefully');
|
|
207
250
|
}
|
|
208
|
-
});
|
|
209
|
-
// Wait for server to start
|
|
210
|
-
await this.waitForServerStart();
|
|
211
|
-
this.isStarting = false;
|
|
212
|
-
const duration = Math.round(perf_hooks_1.performance.now() - this.startTime);
|
|
213
|
-
if (!this.options.quiet) {
|
|
214
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green(figures_1.default.tick)} Server started successfully in ${duration}ms`, 'info');
|
|
215
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.blue(figures_1.default.info)} Running on ${chalk_1.default.cyan(`http://${this.options.host}:${this.options.port}`)}`, 'info');
|
|
216
251
|
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
252
|
+
});
|
|
253
|
+
this.log(`Started process (PID: ${this.masterProcess.pid})`);
|
|
254
|
+
}
|
|
255
|
+
async startCluster() {
|
|
256
|
+
this.log(`Starting cluster with ${this.options.workers} workers`);
|
|
257
|
+
for (let i = 0; i < this.options.workers; i++) {
|
|
258
|
+
await this.startWorker(i + 1);
|
|
221
259
|
}
|
|
222
260
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
else {
|
|
236
|
-
resolve();
|
|
237
|
-
}
|
|
261
|
+
setupHealthCheck() {
|
|
262
|
+
if (!this.options.healthCheck)
|
|
263
|
+
return;
|
|
264
|
+
this.healthServer = http_1.default.createServer((req, res) => {
|
|
265
|
+
if (req.url === '/health') {
|
|
266
|
+
const stats = {
|
|
267
|
+
status: 'ok',
|
|
268
|
+
uptime: Date.now() - this.startTime.getTime(),
|
|
269
|
+
workers: this.workers.size,
|
|
270
|
+
totalRestarts: this.totalRestarts,
|
|
271
|
+
memoryUsage: process.memoryUsage(),
|
|
272
|
+
cpuUsage: process.cpuUsage()
|
|
238
273
|
};
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
setTimeout(() => {
|
|
242
|
-
if (this.serverProcess && !this.serverProcess.killed) {
|
|
243
|
-
this.serverProcess.removeListener('exit', exitHandler);
|
|
244
|
-
clearTimeout(timeout);
|
|
245
|
-
resolve();
|
|
246
|
-
}
|
|
247
|
-
}, 1000);
|
|
274
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
275
|
+
res.end(JSON.stringify(stats, null, 2));
|
|
248
276
|
}
|
|
249
277
|
else {
|
|
250
|
-
|
|
251
|
-
|
|
278
|
+
res.writeHead(404);
|
|
279
|
+
res.end('Not Found');
|
|
252
280
|
}
|
|
253
281
|
});
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (this.isShuttingDown) {
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
const exitMessage = signal
|
|
261
|
-
? `Server process killed with signal ${signal}`
|
|
262
|
-
: `Server process exited with code ${code}`;
|
|
263
|
-
if (code !== 0 && !this.options.quiet) {
|
|
264
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} ${exitMessage}`, 'error');
|
|
265
|
-
}
|
|
266
|
-
// Handle restart logic
|
|
267
|
-
if (this.options.restart && code !== 0 && this.restartCount < this.options.maxRestarts) {
|
|
268
|
-
this.scheduleRestart();
|
|
269
|
-
}
|
|
270
|
-
else if (this.restartCount >= this.options.maxRestarts) {
|
|
271
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.red(figures_1.default.cross)} Maximum restart attempts reached (${this.options.maxRestarts})`, 'error');
|
|
272
|
-
process.exit(1);
|
|
273
|
-
}
|
|
274
|
-
else if (code !== 0) {
|
|
275
|
-
process.exit(code);
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
scheduleRestart() {
|
|
279
|
-
this.restartCount++;
|
|
280
|
-
const delay = this.options.restartDelay * Math.min(this.restartCount, 5); // Exponential backoff
|
|
281
|
-
if (!this.options.quiet) {
|
|
282
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.yellow(figures_1.default.warning)} Restarting server in ${delay}ms... (${this.restartCount}/${this.options.maxRestarts})`, 'info');
|
|
283
|
-
}
|
|
284
|
-
this.restartTimer = setTimeout(() => {
|
|
285
|
-
this.restartTimer = null;
|
|
286
|
-
this.startServer().catch(error => {
|
|
287
|
-
logger_manager_js_1.loggerManager.printLine(`Restart failed: ${error.message}`, 'error');
|
|
288
|
-
this.handleServerExit(1);
|
|
289
|
-
});
|
|
290
|
-
}, delay);
|
|
282
|
+
this.healthServer.listen(this.options.healthPort, () => {
|
|
283
|
+
this.log(`Health check server listening on port ${this.options.healthPort}`);
|
|
284
|
+
});
|
|
291
285
|
}
|
|
292
286
|
setupWatcher() {
|
|
293
287
|
if (!this.options.watch)
|
|
294
288
|
return;
|
|
295
289
|
const watchPatterns = [
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
290
|
+
`${this.options.workingDir}/**/*.js`,
|
|
291
|
+
`${this.options.workingDir}/**/*.json`,
|
|
292
|
+
`${this.options.workingDir}/**/*.env*`
|
|
299
293
|
];
|
|
300
294
|
this.watcher = (0, chokidar_1.watch)(watchPatterns, {
|
|
301
|
-
ignoreInitial: true,
|
|
302
|
-
followSymlinks: false,
|
|
303
|
-
usePolling: false,
|
|
304
|
-
atomic: 500,
|
|
305
295
|
ignored: [
|
|
306
296
|
'**/node_modules/**',
|
|
307
297
|
'**/.git/**',
|
|
308
|
-
'
|
|
309
|
-
'**/*.
|
|
310
|
-
]
|
|
298
|
+
'**/logs/**',
|
|
299
|
+
'**/*.log'
|
|
300
|
+
],
|
|
301
|
+
ignoreInitial: true,
|
|
302
|
+
followSymlinks: false,
|
|
303
|
+
usePolling: false,
|
|
304
|
+
atomic: 300
|
|
311
305
|
});
|
|
312
306
|
this.watcher.on('change', (filePath) => {
|
|
313
307
|
if (this.options.verbose) {
|
|
314
|
-
|
|
308
|
+
this.log(`File changed: ${path_1.default.relative(this.options.workingDir, filePath)}`);
|
|
315
309
|
}
|
|
316
|
-
this.
|
|
310
|
+
this.debouncedRestart();
|
|
317
311
|
});
|
|
318
312
|
this.watcher.on('error', (error) => {
|
|
319
|
-
|
|
313
|
+
this.log(`Watcher error: ${error.message}`, 'error');
|
|
320
314
|
});
|
|
321
|
-
|
|
322
|
-
logger_manager_js_1.loggerManager.printLine(`Watching: ${watchPatterns.join(', ')}`, 'info');
|
|
323
|
-
}
|
|
315
|
+
this.log('File watcher enabled');
|
|
324
316
|
}
|
|
325
|
-
async
|
|
317
|
+
async restartAll() {
|
|
326
318
|
if (this.isShuttingDown)
|
|
327
319
|
return;
|
|
328
|
-
|
|
329
|
-
|
|
320
|
+
this.log('Restarting all processes due to file changes');
|
|
321
|
+
if (this.options.cluster) {
|
|
322
|
+
// Graceful restart of workers
|
|
323
|
+
for (const [workerId, workerInfo] of this.workers.entries()) {
|
|
324
|
+
try {
|
|
325
|
+
workerInfo.process.kill('SIGTERM');
|
|
326
|
+
await this.waitForProcessExit(workerInfo.process, 5000);
|
|
327
|
+
}
|
|
328
|
+
catch (error) {
|
|
329
|
+
workerInfo.process.kill('SIGKILL');
|
|
330
|
+
}
|
|
331
|
+
// Start new worker
|
|
332
|
+
setTimeout(() => {
|
|
333
|
+
this.startWorker(workerId);
|
|
334
|
+
}, this.options.restartDelay);
|
|
335
|
+
}
|
|
330
336
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
process.exit(1);
|
|
337
|
-
});
|
|
338
|
-
}, 100);
|
|
339
|
-
}
|
|
340
|
-
async stopServer() {
|
|
341
|
-
if (!this.serverProcess)
|
|
342
|
-
return;
|
|
343
|
-
return new Promise((resolve) => {
|
|
344
|
-
if (!this.serverProcess) {
|
|
345
|
-
resolve();
|
|
346
|
-
return;
|
|
337
|
+
else if (this.masterProcess) {
|
|
338
|
+
// Restart single process
|
|
339
|
+
try {
|
|
340
|
+
this.masterProcess.kill('SIGTERM');
|
|
341
|
+
await this.waitForProcessExit(this.masterProcess, 5000);
|
|
347
342
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (this.restartTimer) {
|
|
351
|
-
clearTimeout(this.restartTimer);
|
|
352
|
-
this.restartTimer = null;
|
|
343
|
+
catch (error) {
|
|
344
|
+
this.masterProcess.kill('SIGKILL');
|
|
353
345
|
}
|
|
354
|
-
|
|
355
|
-
this.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// Ignore
|
|
346
|
+
setTimeout(() => {
|
|
347
|
+
this.startSingleProcess();
|
|
348
|
+
}, this.options.restartDelay);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
setupMonitoring() {
|
|
352
|
+
setInterval(() => {
|
|
353
|
+
if (this.options.verbose) {
|
|
354
|
+
this.workers.forEach((workerInfo, workerId) => {
|
|
355
|
+
try {
|
|
356
|
+
const usage = process.memoryUsage();
|
|
357
|
+
workerInfo.memoryUsage = usage.heapUsed;
|
|
358
|
+
if (this.options.maxMemory) {
|
|
359
|
+
const maxMemory = this.parseMemoryLimit(this.options.maxMemory);
|
|
360
|
+
if (maxMemory && usage.heapUsed > maxMemory) {
|
|
361
|
+
this.log(`Worker ${workerId} exceeded memory limit, restarting`, 'warn');
|
|
362
|
+
this.restartWorker(workerId);
|
|
372
363
|
}
|
|
373
364
|
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
// Ignore monitoring errors
|
|
368
|
+
}
|
|
369
|
+
});
|
|
379
370
|
}
|
|
380
|
-
});
|
|
371
|
+
}, 10000); // Check every 10 seconds
|
|
381
372
|
}
|
|
382
373
|
async start() {
|
|
383
374
|
try {
|
|
384
|
-
|
|
375
|
+
this.loadEnvFile();
|
|
376
|
+
this.setupHealthCheck();
|
|
385
377
|
this.setupWatcher();
|
|
386
|
-
|
|
387
|
-
|
|
378
|
+
this.setupMonitoring();
|
|
379
|
+
const runtime = Date.now() - this.startTime.getTime();
|
|
380
|
+
this.log(`Starting application in ${runtime}ms`);
|
|
381
|
+
if (this.options.cluster) {
|
|
382
|
+
await this.startCluster();
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
await this.startSingleProcess();
|
|
386
|
+
}
|
|
387
|
+
this.log(`Application started successfully`);
|
|
388
|
+
if (this.options.verbose) {
|
|
389
|
+
this.log(`Configuration: ${JSON.stringify({
|
|
390
|
+
workers: this.options.workers,
|
|
391
|
+
cluster: this.options.cluster,
|
|
392
|
+
watch: this.options.watch,
|
|
393
|
+
healthCheck: this.options.healthCheck,
|
|
394
|
+
port: this.options.port
|
|
395
|
+
})}`);
|
|
388
396
|
}
|
|
389
397
|
}
|
|
390
398
|
catch (error) {
|
|
391
|
-
|
|
392
|
-
|
|
399
|
+
this.log(`Failed to start application: ${error.message}`, 'error');
|
|
400
|
+
throw error;
|
|
393
401
|
}
|
|
394
402
|
}
|
|
395
403
|
async stop() {
|
|
404
|
+
if (this.isShuttingDown)
|
|
405
|
+
return;
|
|
396
406
|
this.isShuttingDown = true;
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
407
|
+
this.log('Stopping application...');
|
|
408
|
+
// Stop watcher
|
|
400
409
|
if (this.watcher) {
|
|
401
410
|
await this.watcher.close();
|
|
402
411
|
this.watcher = null;
|
|
403
412
|
}
|
|
404
|
-
|
|
405
|
-
if (
|
|
406
|
-
|
|
413
|
+
// Stop health server
|
|
414
|
+
if (this.healthServer) {
|
|
415
|
+
this.healthServer.close();
|
|
416
|
+
this.healthServer = null;
|
|
417
|
+
}
|
|
418
|
+
// Stop all workers
|
|
419
|
+
const shutdownPromises = [];
|
|
420
|
+
for (const [workerId, workerInfo] of this.workers.entries()) {
|
|
421
|
+
shutdownPromises.push((async () => {
|
|
422
|
+
try {
|
|
423
|
+
workerInfo.process.kill('SIGTERM');
|
|
424
|
+
await this.waitForProcessExit(workerInfo.process, this.options.gracefulTimeout);
|
|
425
|
+
}
|
|
426
|
+
catch (error) {
|
|
427
|
+
workerInfo.process.kill('SIGKILL');
|
|
428
|
+
}
|
|
429
|
+
})());
|
|
430
|
+
}
|
|
431
|
+
// Stop master process
|
|
432
|
+
if (this.masterProcess) {
|
|
433
|
+
shutdownPromises.push((async () => {
|
|
434
|
+
try {
|
|
435
|
+
this.masterProcess.kill('SIGTERM');
|
|
436
|
+
await this.waitForProcessExit(this.masterProcess, this.options.gracefulTimeout);
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
this.masterProcess.kill('SIGKILL');
|
|
440
|
+
}
|
|
441
|
+
})());
|
|
442
|
+
}
|
|
443
|
+
// Wait for all processes to shut down
|
|
444
|
+
await Promise.allSettled(shutdownPromises);
|
|
445
|
+
// Close log stream
|
|
446
|
+
if (this.logStream) {
|
|
447
|
+
this.logStream.end();
|
|
448
|
+
this.logStream = null;
|
|
407
449
|
}
|
|
450
|
+
const uptime = Date.now() - this.startTime.getTime();
|
|
451
|
+
this.log(`Application stopped after ${uptime}ms uptime`);
|
|
452
|
+
this.workers.clear();
|
|
453
|
+
this.masterProcess = null;
|
|
408
454
|
}
|
|
409
455
|
}
|
|
410
456
|
exports.StartManager = StartManager;
|