neex 0.6.52 → 0.6.54
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 +92 -17
- package/dist/src/start-manager.js +150 -44
- package/package.json +1 -1
|
@@ -29,60 +29,118 @@ function addStartCommands(program) {
|
|
|
29
29
|
.option('--graceful-timeout <ms>', 'Graceful shutdown timeout (ms)', parseInt, 30000)
|
|
30
30
|
.option('--inspect', 'Enable Node.js inspector')
|
|
31
31
|
.option('--inspect-brk', 'Enable Node.js inspector with break')
|
|
32
|
+
.option('--node-args <args>', 'Additional Node.js arguments')
|
|
32
33
|
.action(async (file, options) => {
|
|
33
34
|
try {
|
|
34
35
|
const targetFile = file || 'dist/index.js';
|
|
35
|
-
|
|
36
|
+
let resolvedFile = path_1.default.resolve(options.dir, targetFile);
|
|
36
37
|
// Auto-detect main file if not found
|
|
37
38
|
if (!fs_1.default.existsSync(resolvedFile)) {
|
|
38
39
|
const packageJsonPath = path_1.default.join(options.dir, 'package.json');
|
|
39
40
|
if (fs_1.default.existsSync(packageJsonPath)) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
41
|
+
try {
|
|
42
|
+
const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
|
|
43
|
+
const mainFile = packageJson.main || 'index.js';
|
|
44
|
+
const alternativeFile = path_1.default.resolve(options.dir, mainFile);
|
|
45
|
+
if (fs_1.default.existsSync(alternativeFile)) {
|
|
46
|
+
resolvedFile = alternativeFile;
|
|
47
|
+
if (options.verbose) {
|
|
48
|
+
logger_manager_js_1.loggerManager.printLine(`Using main file: ${mainFile}`, 'info');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
// Try common locations
|
|
53
|
+
const commonLocations = [
|
|
54
|
+
'dist/server.js',
|
|
55
|
+
'dist/app.js',
|
|
56
|
+
'build/index.js',
|
|
57
|
+
'build/server.js',
|
|
58
|
+
'src/index.js',
|
|
59
|
+
'src/server.js',
|
|
60
|
+
'index.js',
|
|
61
|
+
'server.js'
|
|
62
|
+
];
|
|
63
|
+
let found = false;
|
|
64
|
+
for (const location of commonLocations) {
|
|
65
|
+
const testPath = path_1.default.resolve(options.dir, location);
|
|
66
|
+
if (fs_1.default.existsSync(testPath)) {
|
|
67
|
+
resolvedFile = testPath;
|
|
68
|
+
found = true;
|
|
69
|
+
if (options.verbose) {
|
|
70
|
+
logger_manager_js_1.loggerManager.printLine(`Found application file: ${location}`, 'info');
|
|
71
|
+
}
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!found) {
|
|
76
|
+
throw new Error(`Application file not found. Tried: ${targetFile}, ${mainFile}, and common locations.`);
|
|
77
|
+
}
|
|
46
78
|
}
|
|
47
79
|
}
|
|
48
|
-
|
|
49
|
-
throw new Error(`
|
|
80
|
+
catch (parseError) {
|
|
81
|
+
throw new Error(`Failed to parse package.json: ${parseError.message}`);
|
|
50
82
|
}
|
|
51
83
|
}
|
|
52
84
|
else {
|
|
53
85
|
throw new Error(`Application file not found: ${resolvedFile}`);
|
|
54
86
|
}
|
|
55
87
|
}
|
|
56
|
-
//
|
|
88
|
+
// Detect environment and set optimal configuration
|
|
57
89
|
const isDevelopment = options.watch || process.env.NODE_ENV === 'development';
|
|
58
90
|
const isProduction = !isDevelopment;
|
|
59
91
|
const cpuCount = os_1.default.cpus().length;
|
|
60
|
-
|
|
92
|
+
// Smart worker allocation
|
|
93
|
+
let workers = options.workers;
|
|
94
|
+
if (!workers) {
|
|
95
|
+
if (isDevelopment) {
|
|
96
|
+
workers = 1; // Single worker for development
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
// For production, use CPU count but cap at 8 for most applications
|
|
100
|
+
workers = Math.min(cpuCount, 8);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
61
103
|
const healthCheck = options.health !== false && isProduction;
|
|
104
|
+
const defaultPort = parseInt(process.env.PORT || '8000');
|
|
105
|
+
const port = options.port || defaultPort;
|
|
106
|
+
// Environment setup
|
|
107
|
+
if (!process.env.NODE_ENV) {
|
|
108
|
+
process.env.NODE_ENV = isProduction ? 'production' : 'development';
|
|
109
|
+
}
|
|
62
110
|
// Startup logging
|
|
63
111
|
const mode = isDevelopment ? 'development' : 'production';
|
|
64
112
|
const clusterInfo = workers > 1 ? ` (${workers} workers)` : '';
|
|
65
113
|
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green(figures_1.default.play)} Starting ${mode} server${clusterInfo}`, 'info');
|
|
114
|
+
if (options.verbose) {
|
|
115
|
+
logger_manager_js_1.loggerManager.printLine(`File: ${path_1.default.relative(process.cwd(), resolvedFile)}`);
|
|
116
|
+
logger_manager_js_1.loggerManager.printLine(`Working Directory: ${options.dir}`);
|
|
117
|
+
logger_manager_js_1.loggerManager.printLine(`Environment: ${process.env.NODE_ENV}`);
|
|
118
|
+
logger_manager_js_1.loggerManager.printLine(`Port: ${port}`);
|
|
119
|
+
logger_manager_js_1.loggerManager.printLine(`Workers: ${workers}`);
|
|
120
|
+
if (healthCheck) {
|
|
121
|
+
logger_manager_js_1.loggerManager.printLine(`Health Check: http://localhost:${options.healthPort}/health`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
66
124
|
startManager = new start_manager_js_1.StartManager({
|
|
67
125
|
file: resolvedFile,
|
|
68
126
|
workingDir: options.dir,
|
|
69
127
|
envFile: options.env,
|
|
70
|
-
port
|
|
128
|
+
port,
|
|
71
129
|
workers,
|
|
72
130
|
memoryLimit: isProduction ? '512M' : undefined,
|
|
73
131
|
logLevel: options.verbose ? 'debug' : 'info',
|
|
74
|
-
color:
|
|
132
|
+
color: process.stdout.isTTY,
|
|
75
133
|
verbose: options.verbose,
|
|
76
134
|
watch: options.watch,
|
|
77
135
|
maxMemory: options.maxMemory || (isProduction ? '1G' : undefined),
|
|
78
|
-
maxCrashes: isProduction ? 3 :
|
|
136
|
+
maxCrashes: isProduction ? 3 : 10,
|
|
79
137
|
restartDelay: isProduction ? 2000 : 1000,
|
|
80
138
|
healthCheck,
|
|
81
139
|
healthPort: options.healthPort,
|
|
82
140
|
gracefulTimeout: options.gracefulTimeout,
|
|
83
141
|
inspect: options.inspect,
|
|
84
142
|
inspectBrk: options.inspectBrk,
|
|
85
|
-
nodeArgs:
|
|
143
|
+
nodeArgs: options.nodeArgs
|
|
86
144
|
});
|
|
87
145
|
await startManager.start();
|
|
88
146
|
}
|
|
@@ -104,13 +162,17 @@ function addStartCommands(program) {
|
|
|
104
162
|
startManager = null;
|
|
105
163
|
}
|
|
106
164
|
catch (error) {
|
|
107
|
-
// Ignore cleanup errors
|
|
165
|
+
// Ignore cleanup errors but log in verbose mode
|
|
166
|
+
if (process.env.VERBOSE) {
|
|
167
|
+
console.error('Cleanup error:', error);
|
|
168
|
+
}
|
|
108
169
|
}
|
|
109
170
|
}
|
|
110
171
|
};
|
|
111
|
-
//
|
|
172
|
+
// Enhanced signal handling
|
|
112
173
|
const handleExit = (signal) => {
|
|
113
174
|
if (startManager) {
|
|
175
|
+
console.log(`\n${chalk_1.default.yellow(figures_1.default.warning)} Received ${signal}, shutting down gracefully...`);
|
|
114
176
|
cleanupStart().then(() => {
|
|
115
177
|
process.exit(0);
|
|
116
178
|
}).catch(() => {
|
|
@@ -125,6 +187,19 @@ function addStartCommands(program) {
|
|
|
125
187
|
process.on('SIGINT', () => handleExit('SIGINT'));
|
|
126
188
|
process.on('SIGTERM', () => handleExit('SIGTERM'));
|
|
127
189
|
process.on('SIGUSR2', () => handleExit('SIGUSR2'));
|
|
190
|
+
// Handle uncaught exceptions
|
|
191
|
+
process.on('uncaughtException', (error) => {
|
|
192
|
+
console.error(`${chalk_1.default.red(figures_1.default.cross)} Uncaught Exception:`, error);
|
|
193
|
+
cleanupStart().then(() => {
|
|
194
|
+
process.exit(1);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
198
|
+
console.error(`${chalk_1.default.red(figures_1.default.cross)} Unhandled Rejection at:`, promise, 'reason:', reason);
|
|
199
|
+
cleanupStart().then(() => {
|
|
200
|
+
process.exit(1);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
128
203
|
return { cleanupStart };
|
|
129
204
|
}
|
|
130
205
|
exports.addStartCommands = addStartCommands;
|
|
@@ -4,7 +4,7 @@ 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 -
|
|
7
|
+
// src/start-manager.ts - Fixed and optimized production start manager
|
|
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");
|
|
@@ -14,6 +14,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
14
14
|
const fs_1 = __importDefault(require("fs"));
|
|
15
15
|
const http_1 = __importDefault(require("http"));
|
|
16
16
|
const lodash_1 = require("lodash");
|
|
17
|
+
const os_1 = __importDefault(require("os"));
|
|
17
18
|
class StartManager {
|
|
18
19
|
constructor(options) {
|
|
19
20
|
this.workers = new Map();
|
|
@@ -21,6 +22,7 @@ class StartManager {
|
|
|
21
22
|
this.healthServer = null;
|
|
22
23
|
this.isShuttingDown = false;
|
|
23
24
|
this.totalRestarts = 0;
|
|
25
|
+
this.envLoaded = false;
|
|
24
26
|
this.options = options;
|
|
25
27
|
this.startTime = new Date();
|
|
26
28
|
this.debouncedRestart = (0, lodash_1.debounce)(this.restartAll.bind(this), options.restartDelay);
|
|
@@ -29,10 +31,13 @@ class StartManager {
|
|
|
29
31
|
logger_manager_js_1.loggerManager.printLine(message, level);
|
|
30
32
|
}
|
|
31
33
|
loadEnvFile() {
|
|
34
|
+
if (this.envLoaded)
|
|
35
|
+
return;
|
|
32
36
|
if (this.options.envFile && fs_1.default.existsSync(this.options.envFile)) {
|
|
33
37
|
try {
|
|
34
38
|
const envContent = fs_1.default.readFileSync(this.options.envFile, 'utf8');
|
|
35
39
|
const lines = envContent.split('\n');
|
|
40
|
+
let loadedCount = 0;
|
|
36
41
|
for (const line of lines) {
|
|
37
42
|
const trimmed = line.trim();
|
|
38
43
|
if (trimmed && !trimmed.startsWith('#')) {
|
|
@@ -40,10 +45,17 @@ class StartManager {
|
|
|
40
45
|
if (key && values.length > 0) {
|
|
41
46
|
const value = values.join('=').trim();
|
|
42
47
|
const cleanValue = value.replace(/^["']|["']$/g, '');
|
|
43
|
-
process.env[key.trim()]
|
|
48
|
+
if (!process.env[key.trim()]) {
|
|
49
|
+
process.env[key.trim()] = cleanValue;
|
|
50
|
+
loadedCount++;
|
|
51
|
+
}
|
|
44
52
|
}
|
|
45
53
|
}
|
|
46
54
|
}
|
|
55
|
+
if (this.options.verbose && loadedCount > 0) {
|
|
56
|
+
this.log(`Loaded ${loadedCount} environment variables from ${this.options.envFile}`);
|
|
57
|
+
}
|
|
58
|
+
this.envLoaded = true;
|
|
47
59
|
}
|
|
48
60
|
catch (error) {
|
|
49
61
|
if (this.options.verbose) {
|
|
@@ -67,8 +79,11 @@ class StartManager {
|
|
|
67
79
|
getNodeArgs() {
|
|
68
80
|
const args = [];
|
|
69
81
|
if (this.options.memoryLimit) {
|
|
70
|
-
const
|
|
71
|
-
|
|
82
|
+
const memoryBytes = this.parseMemoryLimit(this.options.memoryLimit);
|
|
83
|
+
if (memoryBytes) {
|
|
84
|
+
const memoryMB = Math.floor(memoryBytes / (1024 * 1024));
|
|
85
|
+
args.push(`--max-old-space-size=${memoryMB}`);
|
|
86
|
+
}
|
|
72
87
|
}
|
|
73
88
|
if (this.options.inspect) {
|
|
74
89
|
args.push('--inspect');
|
|
@@ -77,51 +92,63 @@ class StartManager {
|
|
|
77
92
|
args.push('--inspect-brk');
|
|
78
93
|
}
|
|
79
94
|
if (this.options.nodeArgs) {
|
|
80
|
-
args.push(...this.options.nodeArgs.split(' '));
|
|
95
|
+
args.push(...this.options.nodeArgs.split(' ').filter(arg => arg.trim()));
|
|
81
96
|
}
|
|
82
97
|
return args;
|
|
83
98
|
}
|
|
84
99
|
async startWorker(workerId) {
|
|
85
100
|
var _a, _b;
|
|
86
101
|
const nodeArgs = this.getNodeArgs();
|
|
102
|
+
const basePort = this.options.port || 8000;
|
|
103
|
+
const workerPort = this.options.workers > 1 ? basePort + workerId - 1 : basePort;
|
|
87
104
|
const env = {
|
|
88
105
|
...process.env,
|
|
89
106
|
NODE_ENV: process.env.NODE_ENV || 'production',
|
|
90
107
|
WORKER_ID: workerId.toString(),
|
|
108
|
+
PORT: workerPort.toString(),
|
|
91
109
|
CLUSTER_WORKER: 'true',
|
|
92
|
-
FORCE_COLOR: this.options.color ? '1' : '0'
|
|
110
|
+
FORCE_COLOR: this.options.color ? '1' : '0',
|
|
111
|
+
// Prevent dotenv from loading again in worker processes
|
|
112
|
+
DOTENV_CONFIG_PATH: '',
|
|
113
|
+
NODE_OPTIONS: '--no-deprecation'
|
|
93
114
|
};
|
|
94
|
-
if (this.options.port) {
|
|
95
|
-
env.PORT = this.options.port.toString();
|
|
96
|
-
}
|
|
97
115
|
const workerProcess = (0, child_process_1.fork)(this.options.file, [], {
|
|
98
116
|
cwd: this.options.workingDir,
|
|
99
117
|
env,
|
|
100
118
|
execArgv: nodeArgs,
|
|
101
|
-
silent:
|
|
119
|
+
silent: true // Capture output to avoid duplication
|
|
102
120
|
});
|
|
103
121
|
const workerInfo = {
|
|
104
122
|
process: workerProcess,
|
|
105
123
|
pid: workerProcess.pid,
|
|
106
124
|
restarts: 0,
|
|
107
|
-
startTime: new Date()
|
|
125
|
+
startTime: new Date(),
|
|
126
|
+
id: workerId
|
|
108
127
|
};
|
|
109
128
|
this.workers.set(workerId, workerInfo);
|
|
110
|
-
// Handle worker output
|
|
129
|
+
// Handle worker output with proper prefixing
|
|
111
130
|
(_a = workerProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
|
|
112
131
|
const message = data.toString().trim();
|
|
113
132
|
if (message) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
133
|
+
// Filter out repetitive dotenv messages
|
|
134
|
+
if (message.includes('[dotenv@') || message.includes('injecting env')) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const prefix = this.options.verbose ?
|
|
138
|
+
chalk_1.default.dim(`[Worker ${workerId}:${workerPort}] `) : '';
|
|
139
|
+
console.log(prefix + message);
|
|
117
140
|
}
|
|
118
141
|
});
|
|
119
142
|
(_b = workerProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
|
|
120
143
|
const message = data.toString().trim();
|
|
121
144
|
if (message) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
145
|
+
// Filter out non-critical warnings
|
|
146
|
+
if (message.includes('[dotenv@') || message.includes('injecting env')) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const prefix = this.options.verbose ?
|
|
150
|
+
chalk_1.default.dim(`[Worker ${workerId}:${workerPort}] `) : '';
|
|
151
|
+
console.error(prefix + chalk_1.default.red(message));
|
|
125
152
|
}
|
|
126
153
|
});
|
|
127
154
|
workerProcess.on('error', (error) => {
|
|
@@ -130,14 +157,41 @@ class StartManager {
|
|
|
130
157
|
workerProcess.on('exit', (code, signal) => {
|
|
131
158
|
this.workers.delete(workerId);
|
|
132
159
|
if (!this.isShuttingDown) {
|
|
133
|
-
if (code !== 0) {
|
|
134
|
-
this.log(`Worker ${workerId} crashed (code: ${code})`, 'error');
|
|
160
|
+
if (code !== 0 && signal !== 'SIGTERM') {
|
|
161
|
+
this.log(`Worker ${workerId} crashed (code: ${code}, signal: ${signal})`, 'error');
|
|
135
162
|
this.restartWorker(workerId);
|
|
136
163
|
}
|
|
137
164
|
}
|
|
138
165
|
});
|
|
166
|
+
// Wait for worker to be ready
|
|
167
|
+
await new Promise((resolve, reject) => {
|
|
168
|
+
const timeout = setTimeout(() => {
|
|
169
|
+
reject(new Error(`Worker ${workerId} failed to start within timeout`));
|
|
170
|
+
}, 10000);
|
|
171
|
+
workerProcess.on('message', (message) => {
|
|
172
|
+
if (message && message.type === 'ready') {
|
|
173
|
+
clearTimeout(timeout);
|
|
174
|
+
resolve();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
workerProcess.on('error', (error) => {
|
|
178
|
+
clearTimeout(timeout);
|
|
179
|
+
reject(error);
|
|
180
|
+
});
|
|
181
|
+
workerProcess.on('exit', (code) => {
|
|
182
|
+
if (code !== 0) {
|
|
183
|
+
clearTimeout(timeout);
|
|
184
|
+
reject(new Error(`Worker ${workerId} exited with code ${code}`));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
// Fallback - assume ready after 2 seconds if no message
|
|
188
|
+
setTimeout(() => {
|
|
189
|
+
clearTimeout(timeout);
|
|
190
|
+
resolve();
|
|
191
|
+
}, 2000);
|
|
192
|
+
});
|
|
139
193
|
if (this.options.verbose) {
|
|
140
|
-
this.log(`Worker ${workerId} started (PID: ${workerProcess.pid})`);
|
|
194
|
+
this.log(`Worker ${workerId} started (PID: ${workerProcess.pid}, Port: ${workerPort})`);
|
|
141
195
|
}
|
|
142
196
|
return workerInfo;
|
|
143
197
|
}
|
|
@@ -147,7 +201,7 @@ class StartManager {
|
|
|
147
201
|
workerInfo.restarts++;
|
|
148
202
|
this.totalRestarts++;
|
|
149
203
|
if (workerInfo.restarts >= this.options.maxCrashes) {
|
|
150
|
-
this.log(`Worker ${workerId} reached max crashes, not restarting`, 'error');
|
|
204
|
+
this.log(`Worker ${workerId} reached max crashes (${this.options.maxCrashes}), not restarting`, 'error');
|
|
151
205
|
return;
|
|
152
206
|
}
|
|
153
207
|
if (this.options.verbose) {
|
|
@@ -174,30 +228,57 @@ class StartManager {
|
|
|
174
228
|
clearTimeout(timer);
|
|
175
229
|
resolve();
|
|
176
230
|
});
|
|
231
|
+
if (process.killed) {
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
resolve();
|
|
234
|
+
}
|
|
177
235
|
});
|
|
178
236
|
}
|
|
179
237
|
async startCluster() {
|
|
238
|
+
const startPromises = [];
|
|
180
239
|
for (let i = 0; i < this.options.workers; i++) {
|
|
181
|
-
|
|
240
|
+
startPromises.push(this.startWorker(i + 1));
|
|
241
|
+
}
|
|
242
|
+
try {
|
|
243
|
+
await Promise.all(startPromises);
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
this.log(`Failed to start some workers: ${error.message}`, 'error');
|
|
247
|
+
// Continue with successfully started workers
|
|
182
248
|
}
|
|
183
249
|
}
|
|
184
250
|
setupHealthCheck() {
|
|
185
251
|
if (!this.options.healthCheck)
|
|
186
252
|
return;
|
|
187
253
|
this.healthServer = http_1.default.createServer((req, res) => {
|
|
254
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
255
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
256
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
257
|
+
if (req.method === 'OPTIONS') {
|
|
258
|
+
res.writeHead(200);
|
|
259
|
+
res.end();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
188
262
|
if (req.url === '/health') {
|
|
189
263
|
const stats = {
|
|
190
264
|
status: 'ok',
|
|
191
265
|
uptime: Date.now() - this.startTime.getTime(),
|
|
192
266
|
workers: this.workers.size,
|
|
267
|
+
activeWorkers: Array.from(this.workers.values()).map(w => ({
|
|
268
|
+
id: w.id,
|
|
269
|
+
pid: w.pid,
|
|
270
|
+
restarts: w.restarts,
|
|
271
|
+
uptime: Date.now() - w.startTime.getTime()
|
|
272
|
+
})),
|
|
193
273
|
totalRestarts: this.totalRestarts,
|
|
194
|
-
memory: process.memoryUsage()
|
|
274
|
+
memory: process.memoryUsage(),
|
|
275
|
+
cpu: os_1.default.loadavg()
|
|
195
276
|
};
|
|
196
277
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
197
|
-
res.end(JSON.stringify(stats));
|
|
278
|
+
res.end(JSON.stringify(stats, null, 2));
|
|
198
279
|
}
|
|
199
280
|
else {
|
|
200
|
-
res.writeHead(404);
|
|
281
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
201
282
|
res.end('Not Found');
|
|
202
283
|
}
|
|
203
284
|
});
|
|
@@ -216,9 +297,13 @@ class StartManager {
|
|
|
216
297
|
`${this.options.workingDir}/**/*.env*`
|
|
217
298
|
];
|
|
218
299
|
this.watcher = (0, chokidar_1.watch)(watchPatterns, {
|
|
219
|
-
ignored: ['**/node_modules/**', '**/.git/**', '**/logs/**'],
|
|
300
|
+
ignored: ['**/node_modules/**', '**/.git/**', '**/logs/**', '**/tmp/**'],
|
|
220
301
|
ignoreInitial: true,
|
|
221
|
-
atomic: 300
|
|
302
|
+
atomic: 300,
|
|
303
|
+
awaitWriteFinish: {
|
|
304
|
+
stabilityThreshold: 2000,
|
|
305
|
+
pollInterval: 100
|
|
306
|
+
}
|
|
222
307
|
});
|
|
223
308
|
this.watcher.on('change', (filePath) => {
|
|
224
309
|
if (this.options.verbose) {
|
|
@@ -226,6 +311,9 @@ class StartManager {
|
|
|
226
311
|
}
|
|
227
312
|
this.debouncedRestart();
|
|
228
313
|
});
|
|
314
|
+
this.watcher.on('error', (error) => {
|
|
315
|
+
this.log(`Watcher error: ${error.message}`, 'error');
|
|
316
|
+
});
|
|
229
317
|
if (this.options.verbose) {
|
|
230
318
|
this.log('File watching enabled');
|
|
231
319
|
}
|
|
@@ -233,19 +321,24 @@ class StartManager {
|
|
|
233
321
|
async restartAll() {
|
|
234
322
|
if (this.isShuttingDown)
|
|
235
323
|
return;
|
|
236
|
-
this.log('Restarting due to file changes...');
|
|
324
|
+
this.log('Restarting all workers due to file changes...');
|
|
325
|
+
const restartPromises = [];
|
|
237
326
|
for (const [workerId, workerInfo] of this.workers.entries()) {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}, this.options.restartDelay);
|
|
327
|
+
restartPromises.push((async () => {
|
|
328
|
+
try {
|
|
329
|
+
workerInfo.process.kill('SIGTERM');
|
|
330
|
+
await this.waitForProcessExit(workerInfo.process, 5000);
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
workerInfo.process.kill('SIGKILL');
|
|
334
|
+
}
|
|
335
|
+
})());
|
|
248
336
|
}
|
|
337
|
+
await Promise.allSettled(restartPromises);
|
|
338
|
+
// Wait a bit before restarting
|
|
339
|
+
setTimeout(() => {
|
|
340
|
+
this.startCluster();
|
|
341
|
+
}, this.options.restartDelay);
|
|
249
342
|
}
|
|
250
343
|
setupMemoryMonitoring() {
|
|
251
344
|
if (!this.options.maxMemory)
|
|
@@ -253,12 +346,17 @@ class StartManager {
|
|
|
253
346
|
const maxMemory = this.parseMemoryLimit(this.options.maxMemory);
|
|
254
347
|
if (!maxMemory)
|
|
255
348
|
return;
|
|
256
|
-
setInterval(() => {
|
|
349
|
+
const checkInterval = setInterval(() => {
|
|
350
|
+
if (this.isShuttingDown) {
|
|
351
|
+
clearInterval(checkInterval);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
257
354
|
this.workers.forEach((workerInfo, workerId) => {
|
|
258
355
|
try {
|
|
356
|
+
// This is a simplified check - in practice, you'd need to get actual worker memory usage
|
|
259
357
|
const usage = process.memoryUsage();
|
|
260
358
|
if (usage.heapUsed > maxMemory) {
|
|
261
|
-
this.log(`Worker ${workerId} exceeded memory limit, restarting`, 'warn');
|
|
359
|
+
this.log(`Worker ${workerId} may have exceeded memory limit, restarting`, 'warn');
|
|
262
360
|
this.restartWorker(workerId);
|
|
263
361
|
}
|
|
264
362
|
}
|
|
@@ -270,17 +368,23 @@ class StartManager {
|
|
|
270
368
|
}
|
|
271
369
|
async start() {
|
|
272
370
|
try {
|
|
371
|
+
// Load environment variables only once in the main process
|
|
273
372
|
this.loadEnvFile();
|
|
373
|
+
// Set up monitoring and health checks
|
|
274
374
|
this.setupHealthCheck();
|
|
275
375
|
this.setupWatcher();
|
|
276
376
|
this.setupMemoryMonitoring();
|
|
377
|
+
// Start worker cluster
|
|
277
378
|
await this.startCluster();
|
|
278
379
|
// Success message
|
|
279
|
-
const
|
|
280
|
-
const portInfo =
|
|
281
|
-
|
|
380
|
+
const basePort = this.options.port || 8000;
|
|
381
|
+
const portInfo = this.options.workers > 1 ?
|
|
382
|
+
` on ports ${basePort}-${basePort + this.options.workers - 1}` :
|
|
383
|
+
` on port ${basePort}`;
|
|
384
|
+
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green(figures_1.default.tick)} Server ready${portInfo} (${this.workers.size} workers)`, 'info');
|
|
282
385
|
}
|
|
283
386
|
catch (error) {
|
|
387
|
+
this.log(`Failed to start server: ${error.message}`, 'error');
|
|
284
388
|
throw error;
|
|
285
389
|
}
|
|
286
390
|
}
|
|
@@ -288,6 +392,7 @@ class StartManager {
|
|
|
288
392
|
if (this.isShuttingDown)
|
|
289
393
|
return;
|
|
290
394
|
this.isShuttingDown = true;
|
|
395
|
+
this.log('Shutting down gracefully...');
|
|
291
396
|
if (this.watcher) {
|
|
292
397
|
await this.watcher.close();
|
|
293
398
|
}
|
|
@@ -308,6 +413,7 @@ class StartManager {
|
|
|
308
413
|
}
|
|
309
414
|
await Promise.allSettled(shutdownPromises);
|
|
310
415
|
this.workers.clear();
|
|
416
|
+
this.log('Server stopped');
|
|
311
417
|
}
|
|
312
418
|
}
|
|
313
419
|
exports.StartManager = StartManager;
|