neex 0.6.52 → 0.6.55
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 +96 -23
- package/dist/src/start-manager.js +219 -59
- package/package.json +1 -1
|
@@ -13,10 +13,10 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
13
13
|
const os_1 = __importDefault(require("os"));
|
|
14
14
|
function addStartCommands(program) {
|
|
15
15
|
let startManager = null;
|
|
16
|
-
//
|
|
16
|
+
// Production start command
|
|
17
17
|
program
|
|
18
18
|
.command('start [file]')
|
|
19
|
-
.description('Start production application
|
|
19
|
+
.description('Start production application')
|
|
20
20
|
.option('-d, --dir <directory>', 'Working directory', process.cwd())
|
|
21
21
|
.option('-e, --env <file>', 'Environment file to load', '.env')
|
|
22
22
|
.option('-p, --port <port>', 'Port number', parseInt)
|
|
@@ -29,60 +29,119 @@ 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
|
-
const targetFile = file || 'dist/
|
|
35
|
-
|
|
35
|
+
const targetFile = file || 'dist/server.js';
|
|
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
|
+
'dist/index.js',
|
|
57
|
+
'build/server.js',
|
|
58
|
+
'build/app.js',
|
|
59
|
+
'build/index.js',
|
|
60
|
+
'server.js',
|
|
61
|
+
'app.js',
|
|
62
|
+
'index.js'
|
|
63
|
+
];
|
|
64
|
+
let found = false;
|
|
65
|
+
for (const location of commonLocations) {
|
|
66
|
+
const testPath = path_1.default.resolve(options.dir, location);
|
|
67
|
+
if (fs_1.default.existsSync(testPath)) {
|
|
68
|
+
resolvedFile = testPath;
|
|
69
|
+
found = true;
|
|
70
|
+
if (options.verbose) {
|
|
71
|
+
logger_manager_js_1.loggerManager.printLine(`Found application file: ${location}`, 'info');
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (!found) {
|
|
77
|
+
throw new Error(`Application file not found. Tried: ${targetFile}, ${mainFile}, and common locations.`);
|
|
78
|
+
}
|
|
46
79
|
}
|
|
47
80
|
}
|
|
48
|
-
|
|
49
|
-
throw new Error(`
|
|
81
|
+
catch (parseError) {
|
|
82
|
+
throw new Error(`Failed to parse package.json: ${parseError.message}`);
|
|
50
83
|
}
|
|
51
84
|
}
|
|
52
85
|
else {
|
|
53
86
|
throw new Error(`Application file not found: ${resolvedFile}`);
|
|
54
87
|
}
|
|
55
88
|
}
|
|
56
|
-
//
|
|
89
|
+
// Environment detection
|
|
57
90
|
const isDevelopment = options.watch || process.env.NODE_ENV === 'development';
|
|
58
91
|
const isProduction = !isDevelopment;
|
|
59
92
|
const cpuCount = os_1.default.cpus().length;
|
|
60
|
-
|
|
61
|
-
|
|
93
|
+
// Smart worker allocation
|
|
94
|
+
let workers = options.workers;
|
|
95
|
+
if (!workers) {
|
|
96
|
+
if (isDevelopment) {
|
|
97
|
+
workers = 1;
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// For production, use CPU count but cap at reasonable limit
|
|
101
|
+
workers = Math.min(cpuCount, 4); // Reduced from 8 to 4 for better resource management
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const healthCheck = options.health !== false;
|
|
105
|
+
const defaultPort = parseInt(process.env.PORT || '8000');
|
|
106
|
+
const port = options.port || defaultPort;
|
|
107
|
+
// Set NODE_ENV if not already set
|
|
108
|
+
if (!process.env.NODE_ENV) {
|
|
109
|
+
process.env.NODE_ENV = isProduction ? 'production' : 'development';
|
|
110
|
+
}
|
|
62
111
|
// Startup logging
|
|
63
112
|
const mode = isDevelopment ? 'development' : 'production';
|
|
64
113
|
const clusterInfo = workers > 1 ? ` (${workers} workers)` : '';
|
|
65
114
|
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green(figures_1.default.play)} Starting ${mode} server${clusterInfo}`, 'info');
|
|
115
|
+
if (options.verbose) {
|
|
116
|
+
logger_manager_js_1.loggerManager.printLine(`File: ${path_1.default.relative(process.cwd(), resolvedFile)}`);
|
|
117
|
+
logger_manager_js_1.loggerManager.printLine(`Working Directory: ${options.dir}`);
|
|
118
|
+
logger_manager_js_1.loggerManager.printLine(`Environment: ${process.env.NODE_ENV}`);
|
|
119
|
+
logger_manager_js_1.loggerManager.printLine(`Port: ${port}`);
|
|
120
|
+
logger_manager_js_1.loggerManager.printLine(`Workers: ${workers}`);
|
|
121
|
+
if (healthCheck) {
|
|
122
|
+
logger_manager_js_1.loggerManager.printLine(`Health Check: http://localhost:${options.healthPort}/health`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
66
125
|
startManager = new start_manager_js_1.StartManager({
|
|
67
126
|
file: resolvedFile,
|
|
68
127
|
workingDir: options.dir,
|
|
69
128
|
envFile: options.env,
|
|
70
|
-
port
|
|
129
|
+
port,
|
|
71
130
|
workers,
|
|
72
131
|
memoryLimit: isProduction ? '512M' : undefined,
|
|
73
132
|
logLevel: options.verbose ? 'debug' : 'info',
|
|
74
|
-
color:
|
|
133
|
+
color: process.stdout.isTTY,
|
|
75
134
|
verbose: options.verbose,
|
|
76
135
|
watch: options.watch,
|
|
77
136
|
maxMemory: options.maxMemory || (isProduction ? '1G' : undefined),
|
|
78
|
-
maxCrashes: isProduction ? 3 :
|
|
79
|
-
restartDelay: isProduction ?
|
|
137
|
+
maxCrashes: isProduction ? 3 : 10,
|
|
138
|
+
restartDelay: isProduction ? 1000 : 500,
|
|
80
139
|
healthCheck,
|
|
81
140
|
healthPort: options.healthPort,
|
|
82
141
|
gracefulTimeout: options.gracefulTimeout,
|
|
83
142
|
inspect: options.inspect,
|
|
84
143
|
inspectBrk: options.inspectBrk,
|
|
85
|
-
nodeArgs:
|
|
144
|
+
nodeArgs: options.nodeArgs
|
|
86
145
|
});
|
|
87
146
|
await startManager.start();
|
|
88
147
|
}
|
|
@@ -104,13 +163,16 @@ function addStartCommands(program) {
|
|
|
104
163
|
startManager = null;
|
|
105
164
|
}
|
|
106
165
|
catch (error) {
|
|
107
|
-
|
|
166
|
+
if (process.env.VERBOSE) {
|
|
167
|
+
console.error('Cleanup error:', error);
|
|
168
|
+
}
|
|
108
169
|
}
|
|
109
170
|
}
|
|
110
171
|
};
|
|
111
|
-
//
|
|
172
|
+
// 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(() => {
|
|
@@ -121,10 +183,21 @@ function addStartCommands(program) {
|
|
|
121
183
|
process.exit(0);
|
|
122
184
|
}
|
|
123
185
|
};
|
|
124
|
-
// Register signal handlers
|
|
125
186
|
process.on('SIGINT', () => handleExit('SIGINT'));
|
|
126
187
|
process.on('SIGTERM', () => handleExit('SIGTERM'));
|
|
127
188
|
process.on('SIGUSR2', () => handleExit('SIGUSR2'));
|
|
189
|
+
process.on('uncaughtException', (error) => {
|
|
190
|
+
console.error(`${chalk_1.default.red(figures_1.default.cross)} Uncaught Exception:`, error);
|
|
191
|
+
cleanupStart().then(() => {
|
|
192
|
+
process.exit(1);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
196
|
+
console.error(`${chalk_1.default.red(figures_1.default.cross)} Unhandled Rejection at:`, promise, 'reason:', reason);
|
|
197
|
+
cleanupStart().then(() => {
|
|
198
|
+
process.exit(1);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
128
201
|
return { cleanupStart };
|
|
129
202
|
}
|
|
130
203
|
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 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,8 @@ class StartManager {
|
|
|
21
22
|
this.healthServer = null;
|
|
22
23
|
this.isShuttingDown = false;
|
|
23
24
|
this.totalRestarts = 0;
|
|
25
|
+
this.envLoaded = false;
|
|
26
|
+
this.masterProcess = null;
|
|
24
27
|
this.options = options;
|
|
25
28
|
this.startTime = new Date();
|
|
26
29
|
this.debouncedRestart = (0, lodash_1.debounce)(this.restartAll.bind(this), options.restartDelay);
|
|
@@ -29,10 +32,13 @@ class StartManager {
|
|
|
29
32
|
logger_manager_js_1.loggerManager.printLine(message, level);
|
|
30
33
|
}
|
|
31
34
|
loadEnvFile() {
|
|
35
|
+
if (this.envLoaded)
|
|
36
|
+
return;
|
|
32
37
|
if (this.options.envFile && fs_1.default.existsSync(this.options.envFile)) {
|
|
33
38
|
try {
|
|
34
39
|
const envContent = fs_1.default.readFileSync(this.options.envFile, 'utf8');
|
|
35
40
|
const lines = envContent.split('\n');
|
|
41
|
+
let loadedCount = 0;
|
|
36
42
|
for (const line of lines) {
|
|
37
43
|
const trimmed = line.trim();
|
|
38
44
|
if (trimmed && !trimmed.startsWith('#')) {
|
|
@@ -40,10 +46,17 @@ class StartManager {
|
|
|
40
46
|
if (key && values.length > 0) {
|
|
41
47
|
const value = values.join('=').trim();
|
|
42
48
|
const cleanValue = value.replace(/^["']|["']$/g, '');
|
|
43
|
-
process.env[key.trim()]
|
|
49
|
+
if (!process.env[key.trim()]) {
|
|
50
|
+
process.env[key.trim()] = cleanValue;
|
|
51
|
+
loadedCount++;
|
|
52
|
+
}
|
|
44
53
|
}
|
|
45
54
|
}
|
|
46
55
|
}
|
|
56
|
+
if (this.options.verbose && loadedCount > 0) {
|
|
57
|
+
this.log(`Loaded ${loadedCount} environment variables from ${this.options.envFile}`);
|
|
58
|
+
}
|
|
59
|
+
this.envLoaded = true;
|
|
47
60
|
}
|
|
48
61
|
catch (error) {
|
|
49
62
|
if (this.options.verbose) {
|
|
@@ -67,8 +80,11 @@ class StartManager {
|
|
|
67
80
|
getNodeArgs() {
|
|
68
81
|
const args = [];
|
|
69
82
|
if (this.options.memoryLimit) {
|
|
70
|
-
const
|
|
71
|
-
|
|
83
|
+
const memoryBytes = this.parseMemoryLimit(this.options.memoryLimit);
|
|
84
|
+
if (memoryBytes) {
|
|
85
|
+
const memoryMB = Math.floor(memoryBytes / (1024 * 1024));
|
|
86
|
+
args.push(`--max-old-space-size=${memoryMB}`);
|
|
87
|
+
}
|
|
72
88
|
}
|
|
73
89
|
if (this.options.inspect) {
|
|
74
90
|
args.push('--inspect');
|
|
@@ -77,51 +93,115 @@ class StartManager {
|
|
|
77
93
|
args.push('--inspect-brk');
|
|
78
94
|
}
|
|
79
95
|
if (this.options.nodeArgs) {
|
|
80
|
-
args.push(...this.options.nodeArgs.split(' '));
|
|
96
|
+
args.push(...this.options.nodeArgs.split(' ').filter(arg => arg.trim()));
|
|
81
97
|
}
|
|
82
98
|
return args;
|
|
83
99
|
}
|
|
100
|
+
async startSingleProcess() {
|
|
101
|
+
const nodeArgs = this.getNodeArgs();
|
|
102
|
+
const port = this.options.port || 8000;
|
|
103
|
+
const env = {
|
|
104
|
+
...process.env,
|
|
105
|
+
NODE_ENV: process.env.NODE_ENV || 'production',
|
|
106
|
+
PORT: port.toString(),
|
|
107
|
+
FORCE_COLOR: this.options.color ? '1' : '0',
|
|
108
|
+
NODE_OPTIONS: '--no-deprecation'
|
|
109
|
+
};
|
|
110
|
+
this.masterProcess = (0, child_process_1.fork)(this.options.file, [], {
|
|
111
|
+
cwd: this.options.workingDir,
|
|
112
|
+
env,
|
|
113
|
+
execArgv: nodeArgs,
|
|
114
|
+
silent: false // Let the process handle its own output
|
|
115
|
+
});
|
|
116
|
+
this.masterProcess.on('error', (error) => {
|
|
117
|
+
this.log(`Process error: ${error.message}`, 'error');
|
|
118
|
+
});
|
|
119
|
+
this.masterProcess.on('exit', (code, signal) => {
|
|
120
|
+
if (!this.isShuttingDown && code !== 0 && signal !== 'SIGTERM') {
|
|
121
|
+
this.log(`Process crashed (code: ${code}, signal: ${signal})`, 'error');
|
|
122
|
+
this.totalRestarts++;
|
|
123
|
+
if (this.totalRestarts < this.options.maxCrashes) {
|
|
124
|
+
setTimeout(() => {
|
|
125
|
+
this.startSingleProcess();
|
|
126
|
+
}, this.options.restartDelay);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
this.log(`Max crashes reached (${this.options.maxCrashes}), not restarting`, 'error');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
// Wait for process to be ready
|
|
134
|
+
await new Promise((resolve, reject) => {
|
|
135
|
+
const timeout = setTimeout(() => {
|
|
136
|
+
resolve(); // Don't reject, assume it's ready
|
|
137
|
+
}, 5000);
|
|
138
|
+
this.masterProcess.on('message', (message) => {
|
|
139
|
+
if (message && message.type === 'ready') {
|
|
140
|
+
clearTimeout(timeout);
|
|
141
|
+
resolve();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
this.masterProcess.on('error', (error) => {
|
|
145
|
+
clearTimeout(timeout);
|
|
146
|
+
reject(error);
|
|
147
|
+
});
|
|
148
|
+
this.masterProcess.on('exit', (code) => {
|
|
149
|
+
clearTimeout(timeout);
|
|
150
|
+
if (code !== 0) {
|
|
151
|
+
reject(new Error(`Process exited with code ${code}`));
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
resolve();
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
if (this.options.verbose) {
|
|
159
|
+
this.log(`Process started (PID: ${this.masterProcess.pid}, Port: ${port})`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
84
162
|
async startWorker(workerId) {
|
|
85
163
|
var _a, _b;
|
|
86
164
|
const nodeArgs = this.getNodeArgs();
|
|
165
|
+
const port = this.options.port || 8000;
|
|
87
166
|
const env = {
|
|
88
167
|
...process.env,
|
|
89
168
|
NODE_ENV: process.env.NODE_ENV || 'production',
|
|
90
169
|
WORKER_ID: workerId.toString(),
|
|
170
|
+
PORT: port.toString(),
|
|
91
171
|
CLUSTER_WORKER: 'true',
|
|
92
|
-
FORCE_COLOR: this.options.color ? '1' : '0'
|
|
172
|
+
FORCE_COLOR: this.options.color ? '1' : '0',
|
|
173
|
+
NODE_OPTIONS: '--no-deprecation'
|
|
93
174
|
};
|
|
94
|
-
if (this.options.port) {
|
|
95
|
-
env.PORT = this.options.port.toString();
|
|
96
|
-
}
|
|
97
175
|
const workerProcess = (0, child_process_1.fork)(this.options.file, [], {
|
|
98
176
|
cwd: this.options.workingDir,
|
|
99
177
|
env,
|
|
100
178
|
execArgv: nodeArgs,
|
|
101
|
-
silent:
|
|
179
|
+
silent: true
|
|
102
180
|
});
|
|
103
181
|
const workerInfo = {
|
|
104
182
|
process: workerProcess,
|
|
105
183
|
pid: workerProcess.pid,
|
|
106
184
|
restarts: 0,
|
|
107
|
-
startTime: new Date()
|
|
185
|
+
startTime: new Date(),
|
|
186
|
+
id: workerId,
|
|
187
|
+
port: port
|
|
108
188
|
};
|
|
109
189
|
this.workers.set(workerId, workerInfo);
|
|
110
190
|
// Handle worker output
|
|
111
191
|
(_a = workerProcess.stdout) === null || _a === void 0 ? void 0 : _a.on('data', (data) => {
|
|
112
192
|
const message = data.toString().trim();
|
|
113
|
-
if (message) {
|
|
114
|
-
|
|
115
|
-
chalk_1.default.dim(`[Worker ${workerId}]`)
|
|
116
|
-
|
|
193
|
+
if (message && !message.includes('[dotenv@') && !message.includes('injecting env')) {
|
|
194
|
+
const prefix = this.options.verbose ?
|
|
195
|
+
chalk_1.default.dim(`[Worker ${workerId}] `) : '';
|
|
196
|
+
console.log(prefix + message);
|
|
117
197
|
}
|
|
118
198
|
});
|
|
119
199
|
(_b = workerProcess.stderr) === null || _b === void 0 ? void 0 : _b.on('data', (data) => {
|
|
120
200
|
const message = data.toString().trim();
|
|
121
|
-
if (message) {
|
|
122
|
-
|
|
123
|
-
chalk_1.default.dim(`[Worker ${workerId}]`)
|
|
124
|
-
|
|
201
|
+
if (message && !message.includes('[dotenv@') && !message.includes('injecting env')) {
|
|
202
|
+
const prefix = this.options.verbose ?
|
|
203
|
+
chalk_1.default.dim(`[Worker ${workerId}] `) : '';
|
|
204
|
+
console.error(prefix + chalk_1.default.red(message));
|
|
125
205
|
}
|
|
126
206
|
});
|
|
127
207
|
workerProcess.on('error', (error) => {
|
|
@@ -129,12 +209,35 @@ class StartManager {
|
|
|
129
209
|
});
|
|
130
210
|
workerProcess.on('exit', (code, signal) => {
|
|
131
211
|
this.workers.delete(workerId);
|
|
132
|
-
if (!this.isShuttingDown) {
|
|
212
|
+
if (!this.isShuttingDown && code !== 0 && signal !== 'SIGTERM') {
|
|
213
|
+
this.log(`Worker ${workerId} crashed (code: ${code}, signal: ${signal})`, 'error');
|
|
214
|
+
this.restartWorker(workerId);
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
// Wait for worker to be ready
|
|
218
|
+
await new Promise((resolve, reject) => {
|
|
219
|
+
const timeout = setTimeout(() => {
|
|
220
|
+
resolve(); // Don't reject, assume it's ready
|
|
221
|
+
}, 5000);
|
|
222
|
+
workerProcess.on('message', (message) => {
|
|
223
|
+
if (message && message.type === 'ready') {
|
|
224
|
+
clearTimeout(timeout);
|
|
225
|
+
resolve();
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
workerProcess.on('error', (error) => {
|
|
229
|
+
clearTimeout(timeout);
|
|
230
|
+
reject(error);
|
|
231
|
+
});
|
|
232
|
+
workerProcess.on('exit', (code) => {
|
|
233
|
+
clearTimeout(timeout);
|
|
133
234
|
if (code !== 0) {
|
|
134
|
-
|
|
135
|
-
this.restartWorker(workerId);
|
|
235
|
+
reject(new Error(`Worker ${workerId} exited with code ${code}`));
|
|
136
236
|
}
|
|
137
|
-
|
|
237
|
+
else {
|
|
238
|
+
resolve();
|
|
239
|
+
}
|
|
240
|
+
});
|
|
138
241
|
});
|
|
139
242
|
if (this.options.verbose) {
|
|
140
243
|
this.log(`Worker ${workerId} started (PID: ${workerProcess.pid})`);
|
|
@@ -147,7 +250,7 @@ class StartManager {
|
|
|
147
250
|
workerInfo.restarts++;
|
|
148
251
|
this.totalRestarts++;
|
|
149
252
|
if (workerInfo.restarts >= this.options.maxCrashes) {
|
|
150
|
-
this.log(`Worker ${workerId} reached max crashes, not restarting`, 'error');
|
|
253
|
+
this.log(`Worker ${workerId} reached max crashes (${this.options.maxCrashes}), not restarting`, 'error');
|
|
151
254
|
return;
|
|
152
255
|
}
|
|
153
256
|
if (this.options.verbose) {
|
|
@@ -174,30 +277,64 @@ class StartManager {
|
|
|
174
277
|
clearTimeout(timer);
|
|
175
278
|
resolve();
|
|
176
279
|
});
|
|
280
|
+
if (process.killed) {
|
|
281
|
+
clearTimeout(timer);
|
|
282
|
+
resolve();
|
|
283
|
+
}
|
|
177
284
|
});
|
|
178
285
|
}
|
|
179
286
|
async startCluster() {
|
|
287
|
+
if (this.options.workers === 1) {
|
|
288
|
+
// Single process mode
|
|
289
|
+
await this.startSingleProcess();
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
// Multi-worker mode
|
|
293
|
+
const startPromises = [];
|
|
180
294
|
for (let i = 0; i < this.options.workers; i++) {
|
|
181
|
-
|
|
295
|
+
startPromises.push(this.startWorker(i + 1));
|
|
296
|
+
}
|
|
297
|
+
try {
|
|
298
|
+
await Promise.all(startPromises);
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
this.log(`Failed to start some workers: ${error.message}`, 'error');
|
|
302
|
+
// Continue with successfully started workers
|
|
182
303
|
}
|
|
183
304
|
}
|
|
184
305
|
setupHealthCheck() {
|
|
185
306
|
if (!this.options.healthCheck)
|
|
186
307
|
return;
|
|
187
308
|
this.healthServer = http_1.default.createServer((req, res) => {
|
|
309
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
310
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
311
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
312
|
+
if (req.method === 'OPTIONS') {
|
|
313
|
+
res.writeHead(200);
|
|
314
|
+
res.end();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
188
317
|
if (req.url === '/health') {
|
|
189
318
|
const stats = {
|
|
190
319
|
status: 'ok',
|
|
191
320
|
uptime: Date.now() - this.startTime.getTime(),
|
|
192
321
|
workers: this.workers.size,
|
|
322
|
+
activeWorkers: Array.from(this.workers.values()).map(w => ({
|
|
323
|
+
id: w.id,
|
|
324
|
+
pid: w.pid,
|
|
325
|
+
restarts: w.restarts,
|
|
326
|
+
uptime: Date.now() - w.startTime.getTime()
|
|
327
|
+
})),
|
|
193
328
|
totalRestarts: this.totalRestarts,
|
|
194
|
-
memory: process.memoryUsage()
|
|
329
|
+
memory: process.memoryUsage(),
|
|
330
|
+
cpu: os_1.default.loadavg(),
|
|
331
|
+
port: this.options.port || 8000
|
|
195
332
|
};
|
|
196
333
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
197
|
-
res.end(JSON.stringify(stats));
|
|
334
|
+
res.end(JSON.stringify(stats, null, 2));
|
|
198
335
|
}
|
|
199
336
|
else {
|
|
200
|
-
res.writeHead(404);
|
|
337
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
201
338
|
res.end('Not Found');
|
|
202
339
|
}
|
|
203
340
|
});
|
|
@@ -216,9 +353,13 @@ class StartManager {
|
|
|
216
353
|
`${this.options.workingDir}/**/*.env*`
|
|
217
354
|
];
|
|
218
355
|
this.watcher = (0, chokidar_1.watch)(watchPatterns, {
|
|
219
|
-
ignored: ['**/node_modules/**', '**/.git/**', '**/logs/**'],
|
|
356
|
+
ignored: ['**/node_modules/**', '**/.git/**', '**/logs/**', '**/tmp/**'],
|
|
220
357
|
ignoreInitial: true,
|
|
221
|
-
atomic: 300
|
|
358
|
+
atomic: 300,
|
|
359
|
+
awaitWriteFinish: {
|
|
360
|
+
stabilityThreshold: 2000,
|
|
361
|
+
pollInterval: 100
|
|
362
|
+
}
|
|
222
363
|
});
|
|
223
364
|
this.watcher.on('change', (filePath) => {
|
|
224
365
|
if (this.options.verbose) {
|
|
@@ -226,6 +367,9 @@ class StartManager {
|
|
|
226
367
|
}
|
|
227
368
|
this.debouncedRestart();
|
|
228
369
|
});
|
|
370
|
+
this.watcher.on('error', (error) => {
|
|
371
|
+
this.log(`Watcher error: ${error.message}`, 'error');
|
|
372
|
+
});
|
|
229
373
|
if (this.options.verbose) {
|
|
230
374
|
this.log('File watching enabled');
|
|
231
375
|
}
|
|
@@ -234,53 +378,56 @@ class StartManager {
|
|
|
234
378
|
if (this.isShuttingDown)
|
|
235
379
|
return;
|
|
236
380
|
this.log('Restarting due to file changes...');
|
|
237
|
-
|
|
381
|
+
if (this.options.workers === 1 && this.masterProcess) {
|
|
382
|
+
// Single process restart
|
|
238
383
|
try {
|
|
239
|
-
|
|
240
|
-
await this.waitForProcessExit(
|
|
384
|
+
this.masterProcess.kill('SIGTERM');
|
|
385
|
+
await this.waitForProcessExit(this.masterProcess, 5000);
|
|
241
386
|
}
|
|
242
387
|
catch (error) {
|
|
243
|
-
|
|
388
|
+
this.masterProcess.kill('SIGKILL');
|
|
244
389
|
}
|
|
245
390
|
setTimeout(() => {
|
|
246
|
-
this.
|
|
391
|
+
this.startSingleProcess();
|
|
247
392
|
}, this.options.restartDelay);
|
|
248
393
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
this.workers.forEach((workerInfo, workerId) => {
|
|
258
|
-
try {
|
|
259
|
-
const usage = process.memoryUsage();
|
|
260
|
-
if (usage.heapUsed > maxMemory) {
|
|
261
|
-
this.log(`Worker ${workerId} exceeded memory limit, restarting`, 'warn');
|
|
262
|
-
this.restartWorker(workerId);
|
|
394
|
+
else {
|
|
395
|
+
// Multi-worker restart
|
|
396
|
+
const restartPromises = [];
|
|
397
|
+
for (const [workerId, workerInfo] of this.workers.entries()) {
|
|
398
|
+
restartPromises.push((async () => {
|
|
399
|
+
try {
|
|
400
|
+
workerInfo.process.kill('SIGTERM');
|
|
401
|
+
await this.waitForProcessExit(workerInfo.process, 5000);
|
|
263
402
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
403
|
+
catch (error) {
|
|
404
|
+
workerInfo.process.kill('SIGKILL');
|
|
405
|
+
}
|
|
406
|
+
})());
|
|
407
|
+
}
|
|
408
|
+
await Promise.allSettled(restartPromises);
|
|
409
|
+
setTimeout(() => {
|
|
410
|
+
this.startCluster();
|
|
411
|
+
}, this.options.restartDelay);
|
|
412
|
+
}
|
|
270
413
|
}
|
|
271
414
|
async start() {
|
|
272
415
|
try {
|
|
416
|
+
// Load environment variables
|
|
273
417
|
this.loadEnvFile();
|
|
418
|
+
// Set up monitoring and health checks
|
|
274
419
|
this.setupHealthCheck();
|
|
275
420
|
this.setupWatcher();
|
|
276
|
-
|
|
421
|
+
// Start the application
|
|
277
422
|
await this.startCluster();
|
|
278
423
|
// Success message
|
|
279
|
-
const port = this.options.port ||
|
|
280
|
-
const
|
|
281
|
-
|
|
424
|
+
const port = this.options.port || 8000;
|
|
425
|
+
const workerInfo = this.options.workers === 1 ?
|
|
426
|
+
'' : ` (${this.workers.size} workers)`;
|
|
427
|
+
logger_manager_js_1.loggerManager.printLine(`${chalk_1.default.green(figures_1.default.tick)} Server ready on port ${port}${workerInfo}`, 'info');
|
|
282
428
|
}
|
|
283
429
|
catch (error) {
|
|
430
|
+
this.log(`Failed to start server: ${error.message}`, 'error');
|
|
284
431
|
throw error;
|
|
285
432
|
}
|
|
286
433
|
}
|
|
@@ -288,12 +435,24 @@ class StartManager {
|
|
|
288
435
|
if (this.isShuttingDown)
|
|
289
436
|
return;
|
|
290
437
|
this.isShuttingDown = true;
|
|
438
|
+
this.log('Shutting down gracefully...');
|
|
291
439
|
if (this.watcher) {
|
|
292
440
|
await this.watcher.close();
|
|
293
441
|
}
|
|
294
442
|
if (this.healthServer) {
|
|
295
443
|
this.healthServer.close();
|
|
296
444
|
}
|
|
445
|
+
// Stop single process
|
|
446
|
+
if (this.masterProcess) {
|
|
447
|
+
try {
|
|
448
|
+
this.masterProcess.kill('SIGTERM');
|
|
449
|
+
await this.waitForProcessExit(this.masterProcess, this.options.gracefulTimeout);
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
this.masterProcess.kill('SIGKILL');
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// Stop workers
|
|
297
456
|
const shutdownPromises = [];
|
|
298
457
|
for (const [workerId, workerInfo] of this.workers.entries()) {
|
|
299
458
|
shutdownPromises.push((async () => {
|
|
@@ -308,6 +467,7 @@ class StartManager {
|
|
|
308
467
|
}
|
|
309
468
|
await Promise.allSettled(shutdownPromises);
|
|
310
469
|
this.workers.clear();
|
|
470
|
+
this.log('Server stopped');
|
|
311
471
|
}
|
|
312
472
|
}
|
|
313
473
|
exports.StartManager = StartManager;
|