neex 0.6.47 → 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/build-manager.js +69 -70
- package/dist/src/commands/build-commands.js +83 -13
- package/dist/src/commands/dev-commands.js +30 -134
- package/dist/src/commands/start-commands.js +219 -35
- package/dist/src/dev-manager.js +47 -151
- package/dist/src/start-manager.js +415 -80
- package/package.json +1 -1
|
@@ -4,118 +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
|
+
const chokidar_1 = require("chokidar");
|
|
9
10
|
const logger_manager_js_1 = require("./logger-manager.js");
|
|
10
11
|
const chalk_1 = __importDefault(require("chalk"));
|
|
11
|
-
const figures_1 = __importDefault(require("figures"));
|
|
12
|
-
const fs_1 = __importDefault(require("fs"));
|
|
13
12
|
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const fs_1 = __importDefault(require("fs"));
|
|
14
|
+
const http_1 = __importDefault(require("http"));
|
|
15
|
+
const lodash_1 = require("lodash");
|
|
14
16
|
class StartManager {
|
|
15
17
|
constructor(options) {
|
|
16
|
-
this.
|
|
17
|
-
this.
|
|
18
|
+
this.workers = new Map();
|
|
19
|
+
this.masterProcess = null;
|
|
20
|
+
this.watcher = null;
|
|
21
|
+
this.healthServer = null;
|
|
22
|
+
this.isShuttingDown = false;
|
|
23
|
+
this.logStream = null;
|
|
24
|
+
this.totalRestarts = 0;
|
|
18
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();
|
|
19
29
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
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');
|
|
24
52
|
}
|
|
25
|
-
const nodeArgs = this.options.nodeArgs || [];
|
|
26
|
-
const args = [...nodeArgs, this.options.entry];
|
|
27
|
-
if (this.options.verbose) {
|
|
28
|
-
logger_manager_js_1.loggerManager.printLine(`Executing: node ${args.join(' ')}`, 'info');
|
|
29
|
-
}
|
|
30
|
-
this.process = (0, child_process_1.spawn)('node', args, {
|
|
31
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
32
|
-
shell: false,
|
|
33
|
-
env: {
|
|
34
|
-
...process.env,
|
|
35
|
-
NODE_ENV: 'production',
|
|
36
|
-
FORCE_COLOR: this.options.color ? '1' : '0'
|
|
37
|
-
},
|
|
38
|
-
detached: true
|
|
39
|
-
});
|
|
40
|
-
const appName = this.options.name || path_1.default.basename(this.options.entry);
|
|
41
53
|
if (!this.options.quiet) {
|
|
42
|
-
logger_manager_js_1.loggerManager.printLine(
|
|
54
|
+
logger_manager_js_1.loggerManager.printLine(message, level);
|
|
55
|
+
}
|
|
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}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
this.log(`Failed to load environment file: ${error.message}`, 'warn');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
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);
|
|
94
|
+
}
|
|
95
|
+
getNodeArgs() {
|
|
96
|
+
const args = [];
|
|
97
|
+
if (this.options.memoryLimit) {
|
|
98
|
+
args.push(`--max-old-space-size=${this.parseMemoryLimit(this.options.memoryLimit) / (1024 * 1024)}`);
|
|
99
|
+
}
|
|
100
|
+
if (this.options.inspect) {
|
|
101
|
+
args.push('--inspect');
|
|
102
|
+
}
|
|
103
|
+
if (this.options.inspectBrk) {
|
|
104
|
+
args.push('--inspect-brk');
|
|
43
105
|
}
|
|
44
|
-
(
|
|
106
|
+
if (this.options.nodeArgs) {
|
|
107
|
+
args.push(...this.options.nodeArgs.split(' '));
|
|
108
|
+
}
|
|
109
|
+
return args;
|
|
110
|
+
}
|
|
111
|
+
async startWorker(workerId) {
|
|
112
|
+
var _a, _b;
|
|
113
|
+
const nodeArgs = this.getNodeArgs();
|
|
114
|
+
const env = {
|
|
115
|
+
...process.env,
|
|
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'
|
|
120
|
+
};
|
|
121
|
+
if (this.options.port) {
|
|
122
|
+
env.PORT = this.options.port.toString();
|
|
123
|
+
}
|
|
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) => {
|
|
45
141
|
if (!this.options.quiet) {
|
|
46
|
-
|
|
142
|
+
const message = data.toString().trim();
|
|
143
|
+
if (message) {
|
|
144
|
+
console.log(chalk_1.default.dim(`[Worker ${workerId}]`) + ' ' + message);
|
|
145
|
+
}
|
|
47
146
|
}
|
|
48
147
|
});
|
|
49
|
-
(_b =
|
|
148
|
+
(_b = workerProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
|
|
50
149
|
if (!this.options.quiet) {
|
|
51
|
-
|
|
150
|
+
const message = data.toString().trim();
|
|
151
|
+
if (message) {
|
|
152
|
+
console.error(chalk_1.default.dim(`[Worker ${workerId}]`) + ' ' + chalk_1.default.red(message));
|
|
153
|
+
}
|
|
52
154
|
}
|
|
53
155
|
});
|
|
54
|
-
|
|
55
|
-
|
|
156
|
+
workerProcess.on('error', (error) => {
|
|
157
|
+
this.log(`Worker ${workerId} error: ${error.message}`, 'error');
|
|
56
158
|
});
|
|
57
|
-
|
|
58
|
-
this.
|
|
59
|
-
if (!this.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this.startProcess();
|
|
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);
|
|
64
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)}`);
|
|
65
174
|
}
|
|
66
175
|
});
|
|
176
|
+
this.log(`Started worker ${workerId} (PID: ${workerProcess.pid})`);
|
|
177
|
+
return workerInfo;
|
|
67
178
|
}
|
|
68
|
-
async
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
190
|
+
try {
|
|
191
|
+
workerInfo.process.kill('SIGTERM');
|
|
192
|
+
await this.waitForProcessExit(workerInfo.process, 5000);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
workerInfo.process.kill('SIGKILL');
|
|
196
|
+
}
|
|
197
|
+
// Start new worker
|
|
198
|
+
setTimeout(() => {
|
|
199
|
+
this.startWorker(workerId);
|
|
200
|
+
}, this.options.restartDelay);
|
|
71
201
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
202
|
+
}
|
|
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();
|
|
211
|
+
});
|
|
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);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
this.log(`Reached max crashes (${this.options.maxCrashes}), giving up`, 'error');
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
this.log('Process exited gracefully');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
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);
|
|
75
259
|
}
|
|
76
260
|
}
|
|
77
|
-
|
|
78
|
-
this.
|
|
79
|
-
const proc = this.process;
|
|
80
|
-
if (!proc) {
|
|
261
|
+
setupHealthCheck() {
|
|
262
|
+
if (!this.options.healthCheck)
|
|
81
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()
|
|
273
|
+
};
|
|
274
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
275
|
+
res.end(JSON.stringify(stats, null, 2));
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
res.writeHead(404);
|
|
279
|
+
res.end('Not Found');
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
this.healthServer.listen(this.options.healthPort, () => {
|
|
283
|
+
this.log(`Health check server listening on port ${this.options.healthPort}`);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
setupWatcher() {
|
|
287
|
+
if (!this.options.watch)
|
|
288
|
+
return;
|
|
289
|
+
const watchPatterns = [
|
|
290
|
+
`${this.options.workingDir}/**/*.js`,
|
|
291
|
+
`${this.options.workingDir}/**/*.json`,
|
|
292
|
+
`${this.options.workingDir}/**/*.env*`
|
|
293
|
+
];
|
|
294
|
+
this.watcher = (0, chokidar_1.watch)(watchPatterns, {
|
|
295
|
+
ignored: [
|
|
296
|
+
'**/node_modules/**',
|
|
297
|
+
'**/.git/**',
|
|
298
|
+
'**/logs/**',
|
|
299
|
+
'**/*.log'
|
|
300
|
+
],
|
|
301
|
+
ignoreInitial: true,
|
|
302
|
+
followSymlinks: false,
|
|
303
|
+
usePolling: false,
|
|
304
|
+
atomic: 300
|
|
305
|
+
});
|
|
306
|
+
this.watcher.on('change', (filePath) => {
|
|
307
|
+
if (this.options.verbose) {
|
|
308
|
+
this.log(`File changed: ${path_1.default.relative(this.options.workingDir, filePath)}`);
|
|
309
|
+
}
|
|
310
|
+
this.debouncedRestart();
|
|
311
|
+
});
|
|
312
|
+
this.watcher.on('error', (error) => {
|
|
313
|
+
this.log(`Watcher error: ${error.message}`, 'error');
|
|
314
|
+
});
|
|
315
|
+
this.log('File watcher enabled');
|
|
316
|
+
}
|
|
317
|
+
async restartAll() {
|
|
318
|
+
if (this.isShuttingDown)
|
|
319
|
+
return;
|
|
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
|
+
}
|
|
82
336
|
}
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
const appName = this.options.name || path_1.default.basename(this.options.entry);
|
|
86
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.yellow(figures_1.default.warning)} Stopping application ${appName}...`, 'info');
|
|
87
|
-
proc.on('exit', () => {
|
|
88
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.yellow(figures_1.default.square)} Application stopped.`, 'info');
|
|
89
|
-
resolve();
|
|
90
|
-
});
|
|
91
|
-
proc.on('error', () => {
|
|
92
|
-
// Handle errors during shutdown, e.g., if the process is already gone
|
|
93
|
-
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.yellow(figures_1.default.square)} Application stopped.`, 'info');
|
|
94
|
-
resolve();
|
|
95
|
-
});
|
|
337
|
+
else if (this.masterProcess) {
|
|
338
|
+
// Restart single process
|
|
96
339
|
try {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
340
|
+
this.masterProcess.kill('SIGTERM');
|
|
341
|
+
await this.waitForProcessExit(this.masterProcess, 5000);
|
|
342
|
+
}
|
|
343
|
+
catch (error) {
|
|
344
|
+
this.masterProcess.kill('SIGKILL');
|
|
345
|
+
}
|
|
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);
|
|
109
363
|
}
|
|
110
364
|
}
|
|
111
|
-
}
|
|
112
|
-
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
// Ignore monitoring errors
|
|
368
|
+
}
|
|
369
|
+
});
|
|
113
370
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
371
|
+
}, 10000); // Check every 10 seconds
|
|
372
|
+
}
|
|
373
|
+
async start() {
|
|
374
|
+
try {
|
|
375
|
+
this.loadEnvFile();
|
|
376
|
+
this.setupHealthCheck();
|
|
377
|
+
this.setupWatcher();
|
|
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();
|
|
117
383
|
}
|
|
118
|
-
|
|
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
|
+
})}`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
catch (error) {
|
|
399
|
+
this.log(`Failed to start application: ${error.message}`, 'error');
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async stop() {
|
|
404
|
+
if (this.isShuttingDown)
|
|
405
|
+
return;
|
|
406
|
+
this.isShuttingDown = true;
|
|
407
|
+
this.log('Stopping application...');
|
|
408
|
+
// Stop watcher
|
|
409
|
+
if (this.watcher) {
|
|
410
|
+
await this.watcher.close();
|
|
411
|
+
this.watcher = null;
|
|
412
|
+
}
|
|
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;
|
|
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;
|
|
119
454
|
}
|
|
120
455
|
}
|
|
121
456
|
exports.StartManager = StartManager;
|