neex 0.7.20 → 0.7.31
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/README.md +13 -31
- package/dist/bin/neex.d.ts +26 -0
- package/dist/bin/neex.d.ts.map +1 -0
- package/dist/src/build-manager.d.ts +41 -0
- package/dist/src/build-manager.d.ts.map +1 -0
- package/dist/src/build-manager.js +26 -24
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/commands/build-commands.d.ts +5 -0
- package/dist/src/commands/build-commands.d.ts.map +1 -0
- package/dist/src/commands/build-commands.js +2 -2
- package/dist/src/commands/dev-commands.d.ts +5 -0
- package/dist/src/commands/dev-commands.d.ts.map +1 -0
- package/dist/src/commands/dev-commands.js +5 -3
- package/dist/src/commands/index.d.ts +7 -0
- package/dist/src/commands/index.d.ts.map +1 -0
- package/dist/src/commands/init-commands.d.ts +2 -0
- package/dist/src/commands/init-commands.d.ts.map +1 -0
- package/dist/src/commands/init-commands.js +4 -4
- package/dist/src/commands/run-commands.d.ts +3 -0
- package/dist/src/commands/run-commands.d.ts.map +1 -0
- package/dist/src/commands/run-commands.js +26 -26
- package/dist/src/commands/server-commands.d.ts +3 -0
- package/dist/src/commands/server-commands.d.ts.map +1 -0
- package/dist/src/commands/server-commands.js +4 -2
- package/dist/src/commands/start-commands.d.ts +5 -0
- package/dist/src/commands/start-commands.d.ts.map +1 -0
- package/dist/src/commands/start-commands.js +2 -2
- package/dist/src/dev-manager.d.ts +51 -0
- package/dist/src/dev-manager.d.ts.map +1 -0
- package/dist/src/dev-manager.js +29 -21
- package/dist/src/index.d.ts +41 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +15 -16
- package/dist/src/logger-manager.d.ts +4 -0
- package/dist/src/logger-manager.d.ts.map +1 -0
- package/dist/src/logger-manager.js +4 -4
- package/dist/src/logger.d.ts +34 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +21 -15
- package/dist/src/runner.d.ts +21 -0
- package/dist/src/runner.d.ts.map +1 -0
- package/dist/src/runner.js +38 -25
- package/dist/src/start-manager.d.ts +49 -0
- package/dist/src/start-manager.d.ts.map +1 -0
- package/dist/src/start-manager.js +34 -29
- package/dist/src/types.d.ts +41 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils.d.ts +2 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/package.json +14 -9
- package/dist/src/cli-init.js +0 -1
- package/dist/src/commands/process-commands.js +0 -759
- package/dist/src/config.js +0 -59
- package/dist/src/dev-runner.js +0 -234
- package/dist/src/logger-process.js +0 -17
- package/dist/src/process-manager.js +0 -669
- package/dist/src/watcher.js +0 -245
|
@@ -1,669 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
-
if (mod && mod.__esModule) return mod;
|
|
20
|
-
var result = {};
|
|
21
|
-
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
-
__setModuleDefault(result, mod);
|
|
23
|
-
return result;
|
|
24
|
-
};
|
|
25
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
-
};
|
|
28
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.ProcessManager = void 0;
|
|
30
|
-
// src/process-manager.ts - Enhanced PM2-like process manager
|
|
31
|
-
const child_process_1 = require("child_process");
|
|
32
|
-
const fs = __importStar(require("fs"));
|
|
33
|
-
const fs_1 = require("fs");
|
|
34
|
-
const path = __importStar(require("path"));
|
|
35
|
-
const events_1 = require("events");
|
|
36
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
37
|
-
const figures_1 = __importDefault(require("figures"));
|
|
38
|
-
const logger_js_1 = __importDefault(require("./logger.js"));
|
|
39
|
-
class ProcessManager extends events_1.EventEmitter {
|
|
40
|
-
constructor(configPath) {
|
|
41
|
-
super();
|
|
42
|
-
this.processes = new Map();
|
|
43
|
-
this.logStreams = new Map();
|
|
44
|
-
this.isMonitoring = false;
|
|
45
|
-
this.isDisposed = false;
|
|
46
|
-
const baseDir = path.join(process.cwd(), '.neex');
|
|
47
|
-
this.configPath = configPath || path.join(baseDir, 'processes.json');
|
|
48
|
-
this.logDir = path.join(baseDir, 'logs');
|
|
49
|
-
this.pidDir = path.join(baseDir, 'pids');
|
|
50
|
-
this.ensureDirectories();
|
|
51
|
-
this.startMonitoring();
|
|
52
|
-
}
|
|
53
|
-
async ensureDirectories() {
|
|
54
|
-
try {
|
|
55
|
-
const dirs = [
|
|
56
|
-
path.dirname(this.configPath),
|
|
57
|
-
this.logDir,
|
|
58
|
-
this.pidDir
|
|
59
|
-
];
|
|
60
|
-
for (const dir of dirs) {
|
|
61
|
-
await fs_1.promises.mkdir(dir, { recursive: true });
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
catch (error) {
|
|
65
|
-
logger_js_1.default.printLine(`Failed to create directories: ${error.message}`, 'error');
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
async saveConfig() {
|
|
69
|
-
try {
|
|
70
|
-
const config = Array.from(this.processes.values()).map(proc => {
|
|
71
|
-
var _a, _b;
|
|
72
|
-
return ({
|
|
73
|
-
...proc.config,
|
|
74
|
-
status: proc.status,
|
|
75
|
-
created_at: proc.created_at.toISOString(),
|
|
76
|
-
started_at: (_a = proc.started_at) === null || _a === void 0 ? void 0 : _a.toISOString(),
|
|
77
|
-
restarts: proc.restarts,
|
|
78
|
-
last_restart: (_b = proc.last_restart) === null || _b === void 0 ? void 0 : _b.toISOString()
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
await fs_1.promises.writeFile(this.configPath, JSON.stringify(config, null, 2));
|
|
82
|
-
}
|
|
83
|
-
catch (error) {
|
|
84
|
-
logger_js_1.default.printLine(`Failed to save config: ${error.message}`, 'error');
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
async load() {
|
|
88
|
-
try {
|
|
89
|
-
const data = await fs_1.promises.readFile(this.configPath, 'utf-8');
|
|
90
|
-
const configs = JSON.parse(data);
|
|
91
|
-
for (const config of configs) {
|
|
92
|
-
if (config.id && !this.processes.has(config.id)) {
|
|
93
|
-
const processInfo = {
|
|
94
|
-
id: config.id,
|
|
95
|
-
name: config.name,
|
|
96
|
-
status: 'stopped',
|
|
97
|
-
uptime: 0,
|
|
98
|
-
restarts: config.restarts || 0,
|
|
99
|
-
memory: 0,
|
|
100
|
-
cpu: 0,
|
|
101
|
-
created_at: config.created_at ? new Date(config.created_at) : new Date(),
|
|
102
|
-
started_at: config.started_at ? new Date(config.started_at) : undefined,
|
|
103
|
-
last_restart: config.last_restart ? new Date(config.last_restart) : undefined,
|
|
104
|
-
config: {
|
|
105
|
-
...config,
|
|
106
|
-
// Ensure required fields have defaults
|
|
107
|
-
autorestart: config.autorestart !== false,
|
|
108
|
-
max_restarts: config.max_restarts || 10,
|
|
109
|
-
restart_delay: config.restart_delay || 1000,
|
|
110
|
-
instances: config.instances || 1
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
this.processes.set(config.id, processInfo);
|
|
114
|
-
// Check if process is actually running
|
|
115
|
-
if (config.pid) {
|
|
116
|
-
const isRunning = await this.isProcessRunning(config.pid);
|
|
117
|
-
if (isRunning) {
|
|
118
|
-
processInfo.status = 'online';
|
|
119
|
-
processInfo.pid = config.pid;
|
|
120
|
-
processInfo.started_at = processInfo.started_at || new Date();
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
catch (error) {
|
|
127
|
-
// Config file might not exist yet, that's fine
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
async isProcessRunning(pid) {
|
|
131
|
-
try {
|
|
132
|
-
process.kill(pid, 0);
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
catch (error) {
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
generateId(name) {
|
|
140
|
-
let id = name.toLowerCase().replace(/[^a-z0-9-_]/g, '-');
|
|
141
|
-
let counter = 0;
|
|
142
|
-
while (this.processes.has(id)) {
|
|
143
|
-
counter++;
|
|
144
|
-
id = `${name.toLowerCase().replace(/[^a-z0-9-_]/g, '-')}-${counter}`;
|
|
145
|
-
}
|
|
146
|
-
return id;
|
|
147
|
-
}
|
|
148
|
-
setupLogStreams(processInfo) {
|
|
149
|
-
const { config } = processInfo;
|
|
150
|
-
// Close existing streams
|
|
151
|
-
this.closeLogStreams(processInfo.id);
|
|
152
|
-
// Setup default log paths
|
|
153
|
-
if (!config.out_file) {
|
|
154
|
-
config.out_file = path.join(this.logDir, `${processInfo.id}-out.log`);
|
|
155
|
-
}
|
|
156
|
-
if (!config.error_file) {
|
|
157
|
-
config.error_file = path.join(this.logDir, `${processInfo.id}-error.log`);
|
|
158
|
-
}
|
|
159
|
-
try {
|
|
160
|
-
const outStream = fs.createWriteStream(config.out_file, { flags: 'a' });
|
|
161
|
-
const errStream = fs.createWriteStream(config.error_file, { flags: 'a' });
|
|
162
|
-
this.logStreams.set(processInfo.id, { out: outStream, err: errStream });
|
|
163
|
-
}
|
|
164
|
-
catch (error) {
|
|
165
|
-
logger_js_1.default.printLine(`Failed to create log streams for ${processInfo.id}: ${error.message}`, 'error');
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
closeLogStreams(id) {
|
|
169
|
-
var _a, _b;
|
|
170
|
-
const streams = this.logStreams.get(id);
|
|
171
|
-
if (streams) {
|
|
172
|
-
(_a = streams.out) === null || _a === void 0 ? void 0 : _a.end();
|
|
173
|
-
(_b = streams.err) === null || _b === void 0 ? void 0 : _b.end();
|
|
174
|
-
this.logStreams.delete(id);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
async writePidFile(processInfo) {
|
|
178
|
-
if (!processInfo.pid)
|
|
179
|
-
return;
|
|
180
|
-
const pidFile = path.join(this.pidDir, `${processInfo.id}.pid`);
|
|
181
|
-
try {
|
|
182
|
-
await fs_1.promises.writeFile(pidFile, processInfo.pid.toString());
|
|
183
|
-
}
|
|
184
|
-
catch (error) {
|
|
185
|
-
logger_js_1.default.printLine(`Failed to write PID file: ${error.message}`, 'error');
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
async removePidFile(id) {
|
|
189
|
-
const pidFile = path.join(this.pidDir, `${id}.pid`);
|
|
190
|
-
try {
|
|
191
|
-
await fs_1.promises.unlink(pidFile);
|
|
192
|
-
}
|
|
193
|
-
catch (error) {
|
|
194
|
-
// PID file might not exist, ignore
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
parseCommand(script) {
|
|
198
|
-
// Handle different script formats
|
|
199
|
-
if (script.includes(' ')) {
|
|
200
|
-
const parts = script.split(' ');
|
|
201
|
-
return {
|
|
202
|
-
command: parts[0],
|
|
203
|
-
args: parts.slice(1)
|
|
204
|
-
};
|
|
205
|
-
}
|
|
206
|
-
// Handle file extensions
|
|
207
|
-
if (script.endsWith('.js') || script.endsWith('.mjs') || script.endsWith('.cjs')) {
|
|
208
|
-
return {
|
|
209
|
-
command: 'node',
|
|
210
|
-
args: [script]
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
if (script.endsWith('.ts') || script.endsWith('.mts') || script.endsWith('.cts')) {
|
|
214
|
-
return {
|
|
215
|
-
command: 'npx',
|
|
216
|
-
args: ['ts-node', script]
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
return {
|
|
220
|
-
command: script,
|
|
221
|
-
args: []
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
async startProcess(processInfo) {
|
|
225
|
-
const { config } = processInfo;
|
|
226
|
-
try {
|
|
227
|
-
processInfo.status = 'launching';
|
|
228
|
-
processInfo.started_at = new Date();
|
|
229
|
-
// Setup logging
|
|
230
|
-
this.setupLogStreams(processInfo);
|
|
231
|
-
// Parse command
|
|
232
|
-
const { command, args } = this.parseCommand(config.script);
|
|
233
|
-
const finalArgs = [...args, ...(config.args || [])];
|
|
234
|
-
// Setup environment
|
|
235
|
-
const env = {
|
|
236
|
-
...process.env,
|
|
237
|
-
...config.env,
|
|
238
|
-
NEEX_PROCESS_ID: processInfo.id,
|
|
239
|
-
NEEX_PROCESS_NAME: processInfo.name,
|
|
240
|
-
NODE_ENV: process.env.NODE_ENV || 'production'
|
|
241
|
-
};
|
|
242
|
-
// Spawn process
|
|
243
|
-
const childProcess = (0, child_process_1.spawn)(command, finalArgs, {
|
|
244
|
-
cwd: config.cwd || process.cwd(),
|
|
245
|
-
env,
|
|
246
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
247
|
-
detached: true // Detach the process to run independently
|
|
248
|
-
});
|
|
249
|
-
if (!childProcess.pid) {
|
|
250
|
-
throw new Error(`Failed to start process: ${config.script}`);
|
|
251
|
-
}
|
|
252
|
-
processInfo.process = childProcess;
|
|
253
|
-
processInfo.pid = childProcess.pid;
|
|
254
|
-
processInfo.status = 'online';
|
|
255
|
-
// Allow the parent process to exit independently of the child
|
|
256
|
-
childProcess.unref();
|
|
257
|
-
// Write PID file
|
|
258
|
-
await this.writePidFile(processInfo);
|
|
259
|
-
console.log(chalk_1.default.green(`${figures_1.default.tick} Process ${processInfo.name} (${processInfo.id}) started with PID ${processInfo.pid}`));
|
|
260
|
-
this.emit('process:start', processInfo);
|
|
261
|
-
// Setup logging
|
|
262
|
-
const streams = this.logStreams.get(processInfo.id);
|
|
263
|
-
if (childProcess.stdout && (streams === null || streams === void 0 ? void 0 : streams.out)) {
|
|
264
|
-
childProcess.stdout.pipe(streams.out);
|
|
265
|
-
childProcess.stdout.on('data', (data) => {
|
|
266
|
-
var _a;
|
|
267
|
-
const message = data.toString();
|
|
268
|
-
if (config.time) {
|
|
269
|
-
const timestamp = new Date().toISOString();
|
|
270
|
-
(_a = streams.out) === null || _a === void 0 ? void 0 : _a.write(`[${timestamp}] ${message}`);
|
|
271
|
-
}
|
|
272
|
-
this.emit('process:log', {
|
|
273
|
-
id: processInfo.id,
|
|
274
|
-
type: 'stdout',
|
|
275
|
-
data: message
|
|
276
|
-
});
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
if (childProcess.stderr && (streams === null || streams === void 0 ? void 0 : streams.err)) {
|
|
280
|
-
childProcess.stderr.pipe(streams.err);
|
|
281
|
-
childProcess.stderr.on('data', (data) => {
|
|
282
|
-
var _a;
|
|
283
|
-
const message = data.toString();
|
|
284
|
-
if (config.time) {
|
|
285
|
-
const timestamp = new Date().toISOString();
|
|
286
|
-
(_a = streams.err) === null || _a === void 0 ? void 0 : _a.write(`[${timestamp}] ${message}`);
|
|
287
|
-
}
|
|
288
|
-
this.emit('process:log', {
|
|
289
|
-
id: processInfo.id,
|
|
290
|
-
type: 'stderr',
|
|
291
|
-
data: message
|
|
292
|
-
});
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
// Handle process exit
|
|
296
|
-
childProcess.on('exit', async (code, signal) => {
|
|
297
|
-
processInfo.exit_code = code || undefined;
|
|
298
|
-
processInfo.exit_signal = signal || undefined;
|
|
299
|
-
processInfo.status = code === 0 ? 'stopped' : 'errored';
|
|
300
|
-
processInfo.process = undefined;
|
|
301
|
-
processInfo.pid = undefined;
|
|
302
|
-
await this.removePidFile(processInfo.id);
|
|
303
|
-
const exitMsg = signal
|
|
304
|
-
? `Process ${processInfo.name} killed by signal ${signal}`
|
|
305
|
-
: `Process ${processInfo.name} exited with code ${code}`;
|
|
306
|
-
console.log(chalk_1.default.yellow(`${figures_1.default.warning} ${exitMsg}`));
|
|
307
|
-
this.emit('process:exit', { processInfo, code, signal });
|
|
308
|
-
// Auto-restart if enabled and not manually stopped
|
|
309
|
-
if (config.autorestart &&
|
|
310
|
-
processInfo.status === 'errored' &&
|
|
311
|
-
processInfo.restarts < (config.max_restarts || 10)) {
|
|
312
|
-
const delay = config.restart_delay || 1000;
|
|
313
|
-
console.log(chalk_1.default.blue(`${figures_1.default.info} Restarting ${processInfo.name} in ${delay}ms...`));
|
|
314
|
-
setTimeout(async () => {
|
|
315
|
-
try {
|
|
316
|
-
await this.restart(processInfo.id);
|
|
317
|
-
}
|
|
318
|
-
catch (error) {
|
|
319
|
-
console.error(chalk_1.default.red(`${figures_1.default.cross} Failed to restart ${processInfo.name}: ${error.message}`));
|
|
320
|
-
}
|
|
321
|
-
}, delay);
|
|
322
|
-
}
|
|
323
|
-
await this.saveConfig();
|
|
324
|
-
});
|
|
325
|
-
// Handle process error
|
|
326
|
-
childProcess.on('error', (error) => {
|
|
327
|
-
processInfo.status = 'errored';
|
|
328
|
-
console.error(chalk_1.default.red(`${figures_1.default.cross} Process ${processInfo.name} error: ${error.message}`));
|
|
329
|
-
this.emit('process:error', { processInfo, error });
|
|
330
|
-
});
|
|
331
|
-
await this.saveConfig();
|
|
332
|
-
}
|
|
333
|
-
catch (error) {
|
|
334
|
-
processInfo.status = 'errored';
|
|
335
|
-
console.error(chalk_1.default.red(`${figures_1.default.cross} Failed to start ${processInfo.name}: ${error.message}`));
|
|
336
|
-
this.emit('process:error', { processInfo, error });
|
|
337
|
-
throw error;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
startMonitoring() {
|
|
341
|
-
if (this.isMonitoring || this.isDisposed)
|
|
342
|
-
return;
|
|
343
|
-
this.isMonitoring = true;
|
|
344
|
-
this.monitoringInterval = setInterval(async () => {
|
|
345
|
-
await this.updateProcessStats();
|
|
346
|
-
}, 5000);
|
|
347
|
-
}
|
|
348
|
-
stopMonitoring() {
|
|
349
|
-
if (this.monitoringInterval) {
|
|
350
|
-
clearInterval(this.monitoringInterval);
|
|
351
|
-
this.monitoringInterval = undefined;
|
|
352
|
-
}
|
|
353
|
-
this.isMonitoring = false;
|
|
354
|
-
}
|
|
355
|
-
async updateProcessStats() {
|
|
356
|
-
for (const processInfo of this.processes.values()) {
|
|
357
|
-
if (processInfo.status === 'online' && processInfo.pid) {
|
|
358
|
-
try {
|
|
359
|
-
// Check if process is still running
|
|
360
|
-
const isRunning = await this.isProcessRunning(processInfo.pid);
|
|
361
|
-
if (!isRunning) {
|
|
362
|
-
processInfo.status = 'stopped';
|
|
363
|
-
processInfo.process = undefined;
|
|
364
|
-
processInfo.pid = undefined;
|
|
365
|
-
await this.removePidFile(processInfo.id);
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
// Update uptime
|
|
369
|
-
if (processInfo.started_at) {
|
|
370
|
-
processInfo.uptime = Math.floor((Date.now() - processInfo.started_at.getTime()) / 1000);
|
|
371
|
-
}
|
|
372
|
-
// Get memory and CPU usage (Unix/Linux only)
|
|
373
|
-
if (process.platform !== 'win32') {
|
|
374
|
-
try {
|
|
375
|
-
const stats = await this.getProcessStats(processInfo.pid);
|
|
376
|
-
processInfo.memory = stats.memory;
|
|
377
|
-
processInfo.cpu = stats.cpu;
|
|
378
|
-
}
|
|
379
|
-
catch (error) {
|
|
380
|
-
// Process might have died
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
catch (error) {
|
|
385
|
-
// Process monitoring error
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
}
|
|
390
|
-
async getProcessStats(pid) {
|
|
391
|
-
return new Promise((resolve, reject) => {
|
|
392
|
-
(0, child_process_1.exec)(`ps -o pid,rss,pcpu -p ${pid}`, (error, stdout) => {
|
|
393
|
-
if (error) {
|
|
394
|
-
resolve({ memory: 0, cpu: 0 });
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
const lines = stdout.trim().split('\n');
|
|
398
|
-
if (lines.length < 2) {
|
|
399
|
-
resolve({ memory: 0, cpu: 0 });
|
|
400
|
-
return;
|
|
401
|
-
}
|
|
402
|
-
const stats = lines[1].trim().split(/\s+/);
|
|
403
|
-
const memory = parseInt(stats[1]) * 1024; // Convert KB to bytes
|
|
404
|
-
const cpu = parseFloat(stats[2]);
|
|
405
|
-
resolve({ memory, cpu });
|
|
406
|
-
});
|
|
407
|
-
});
|
|
408
|
-
}
|
|
409
|
-
// Public API methods
|
|
410
|
-
async start(config) {
|
|
411
|
-
const id = this.generateId(config.name);
|
|
412
|
-
const processInfo = {
|
|
413
|
-
id,
|
|
414
|
-
name: config.name,
|
|
415
|
-
status: 'stopped',
|
|
416
|
-
uptime: 0,
|
|
417
|
-
restarts: 0,
|
|
418
|
-
memory: 0,
|
|
419
|
-
cpu: 0,
|
|
420
|
-
created_at: new Date(),
|
|
421
|
-
config: { ...config, id }
|
|
422
|
-
};
|
|
423
|
-
this.processes.set(id, processInfo);
|
|
424
|
-
await this.startProcess(processInfo);
|
|
425
|
-
return id;
|
|
426
|
-
}
|
|
427
|
-
async stop(id) {
|
|
428
|
-
const processInfo = this.processes.get(id);
|
|
429
|
-
if (!processInfo) {
|
|
430
|
-
throw new Error(`Process ${id} not found`);
|
|
431
|
-
}
|
|
432
|
-
if (processInfo.status === 'stopped') {
|
|
433
|
-
console.log(chalk_1.default.yellow(`${figures_1.default.info} Process ${id} is already stopped`));
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
processInfo.status = 'stopping';
|
|
437
|
-
console.log(chalk_1.default.blue(`${figures_1.default.info} Stopping process ${processInfo.name} (${id})...`));
|
|
438
|
-
if (processInfo.process && processInfo.pid) {
|
|
439
|
-
try {
|
|
440
|
-
// Try graceful shutdown first
|
|
441
|
-
processInfo.process.kill('SIGTERM');
|
|
442
|
-
// Wait for graceful shutdown
|
|
443
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
444
|
-
// Force kill if still running
|
|
445
|
-
if (processInfo.process && !processInfo.process.killed) {
|
|
446
|
-
processInfo.process.kill('SIGKILL');
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
catch (error) {
|
|
450
|
-
// Process might already be dead
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
processInfo.status = 'stopped';
|
|
454
|
-
processInfo.process = undefined;
|
|
455
|
-
processInfo.pid = undefined;
|
|
456
|
-
await this.removePidFile(id);
|
|
457
|
-
this.closeLogStreams(id);
|
|
458
|
-
await this.saveConfig();
|
|
459
|
-
console.log(chalk_1.default.green(`${figures_1.default.tick} Process ${processInfo.name} (${id}) stopped`));
|
|
460
|
-
this.emit('process:stop', processInfo);
|
|
461
|
-
}
|
|
462
|
-
async restart(id) {
|
|
463
|
-
const processInfo = this.processes.get(id);
|
|
464
|
-
if (!processInfo) {
|
|
465
|
-
throw new Error(`Process ${id} not found`);
|
|
466
|
-
}
|
|
467
|
-
console.log(chalk_1.default.blue(`${figures_1.default.info} Restarting process ${processInfo.name} (${id})...`));
|
|
468
|
-
if (processInfo.status === 'online') {
|
|
469
|
-
await this.stop(id);
|
|
470
|
-
// Wait a bit before restarting
|
|
471
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
472
|
-
}
|
|
473
|
-
processInfo.restarts++;
|
|
474
|
-
processInfo.last_restart = new Date();
|
|
475
|
-
await this.startProcess(processInfo);
|
|
476
|
-
}
|
|
477
|
-
async delete(id) {
|
|
478
|
-
const processInfo = this.processes.get(id);
|
|
479
|
-
if (!processInfo) {
|
|
480
|
-
throw new Error(`Process ${id} not found`);
|
|
481
|
-
}
|
|
482
|
-
if (processInfo.status === 'online') {
|
|
483
|
-
await this.stop(id);
|
|
484
|
-
}
|
|
485
|
-
// Remove log files
|
|
486
|
-
try {
|
|
487
|
-
if (processInfo.config.out_file) {
|
|
488
|
-
await fs_1.promises.unlink(processInfo.config.out_file);
|
|
489
|
-
}
|
|
490
|
-
if (processInfo.config.error_file) {
|
|
491
|
-
await fs_1.promises.unlink(processInfo.config.error_file);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
catch (error) {
|
|
495
|
-
// Log files might not exist
|
|
496
|
-
}
|
|
497
|
-
this.closeLogStreams(id);
|
|
498
|
-
this.processes.delete(id);
|
|
499
|
-
await this.saveConfig();
|
|
500
|
-
console.log(chalk_1.default.green(`${figures_1.default.tick} Process ${processInfo.name} (${id}) deleted`));
|
|
501
|
-
this.emit('process:delete', processInfo);
|
|
502
|
-
}
|
|
503
|
-
async list() {
|
|
504
|
-
return Array.from(this.processes.values());
|
|
505
|
-
}
|
|
506
|
-
async logs(id, lines = 15) {
|
|
507
|
-
const processInfo = this.processes.get(id);
|
|
508
|
-
if (!processInfo) {
|
|
509
|
-
throw new Error(`Process ${id} not found`);
|
|
510
|
-
}
|
|
511
|
-
return this.readLogFiles(processInfo.config, lines);
|
|
512
|
-
}
|
|
513
|
-
async readLogFiles(config, lines) {
|
|
514
|
-
const logs = [];
|
|
515
|
-
const readLastLines = async (filePath) => {
|
|
516
|
-
try {
|
|
517
|
-
const content = await fs_1.promises.readFile(filePath, 'utf-8');
|
|
518
|
-
const fileLines = content.split('\n').filter(line => line.trim() !== '');
|
|
519
|
-
return fileLines.slice(-lines);
|
|
520
|
-
}
|
|
521
|
-
catch (error) {
|
|
522
|
-
return [];
|
|
523
|
-
}
|
|
524
|
-
};
|
|
525
|
-
if (config.out_file) {
|
|
526
|
-
const outLogs = await readLastLines(config.out_file);
|
|
527
|
-
logs.push(...outLogs.map(line => `[OUT] ${line}`));
|
|
528
|
-
}
|
|
529
|
-
if (config.error_file) {
|
|
530
|
-
const errLogs = await readLastLines(config.error_file);
|
|
531
|
-
logs.push(...errLogs.map(line => `[ERR] ${line}`));
|
|
532
|
-
}
|
|
533
|
-
return logs.slice(-lines);
|
|
534
|
-
}
|
|
535
|
-
followLogs(id, onLogEntry) {
|
|
536
|
-
const processInfo = this.processes.get(id);
|
|
537
|
-
if (!processInfo) {
|
|
538
|
-
logger_js_1.default.printLine(`Process ${id} not found for log following.`, 'error');
|
|
539
|
-
return undefined;
|
|
540
|
-
}
|
|
541
|
-
const { config } = processInfo;
|
|
542
|
-
const logFilesToWatch = [];
|
|
543
|
-
// Resolve log file paths, defaulting if not specified (consistent with setupLogStreams)
|
|
544
|
-
const outFile = config.out_file || path.join(this.logDir, `${processInfo.id}-out.log`);
|
|
545
|
-
const errFile = config.error_file || path.join(this.logDir, `${processInfo.id}-error.log`);
|
|
546
|
-
logFilesToWatch.push({ type: 'OUT', path: outFile });
|
|
547
|
-
if (errFile !== outFile) { // Avoid watching the same file path twice
|
|
548
|
-
logFilesToWatch.push({ type: 'ERR', path: errFile });
|
|
549
|
-
}
|
|
550
|
-
const lastReadSizes = {};
|
|
551
|
-
const setupWatcherForFile = (filePath, type) => {
|
|
552
|
-
try {
|
|
553
|
-
const stats = fs.existsSync(filePath) ? fs.statSync(filePath) : { size: 0 };
|
|
554
|
-
lastReadSizes[filePath] = stats.size;
|
|
555
|
-
}
|
|
556
|
-
catch (err) {
|
|
557
|
-
lastReadSizes[filePath] = 0;
|
|
558
|
-
// logger.printLine(`Could not get initial stats for ${filePath}: ${(err as Error).message}. Assuming size 0.`, 'debug');
|
|
559
|
-
}
|
|
560
|
-
const listener = (curr, prev) => {
|
|
561
|
-
if (curr.mtimeMs <= prev.mtimeMs && curr.size === prev.size) {
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
const newSize = curr.size;
|
|
565
|
-
let oldSize = lastReadSizes[filePath];
|
|
566
|
-
// Fallback for oldSize, though it should be initialized
|
|
567
|
-
if (typeof oldSize !== 'number')
|
|
568
|
-
oldSize = 0;
|
|
569
|
-
if (newSize > oldSize) {
|
|
570
|
-
const stream = fs.createReadStream(filePath, {
|
|
571
|
-
start: oldSize,
|
|
572
|
-
end: newSize - 1,
|
|
573
|
-
encoding: 'utf-8',
|
|
574
|
-
});
|
|
575
|
-
stream.on('data', (chunk) => {
|
|
576
|
-
chunk.split('\n').forEach(line => {
|
|
577
|
-
if (line.trim() !== '') {
|
|
578
|
-
onLogEntry(`[${type}] ${line}`);
|
|
579
|
-
}
|
|
580
|
-
});
|
|
581
|
-
});
|
|
582
|
-
stream.on('error', (streamErr) => {
|
|
583
|
-
logger_js_1.default.printLine(`Error reading log chunk for ${filePath}: ${streamErr.message}`, 'error');
|
|
584
|
-
});
|
|
585
|
-
lastReadSizes[filePath] = newSize;
|
|
586
|
-
}
|
|
587
|
-
else if (newSize < oldSize) {
|
|
588
|
-
// File was truncated or replaced
|
|
589
|
-
// logger.printLine(`Log file ${filePath} was truncated. Reading from start.`, 'debug');
|
|
590
|
-
lastReadSizes[filePath] = 0;
|
|
591
|
-
const stream = fs.createReadStream(filePath, {
|
|
592
|
-
start: 0,
|
|
593
|
-
end: newSize - 1,
|
|
594
|
-
encoding: 'utf-8',
|
|
595
|
-
});
|
|
596
|
-
stream.on('data', (chunk) => {
|
|
597
|
-
chunk.split('\n').forEach(line => {
|
|
598
|
-
if (line.trim() !== '') {
|
|
599
|
-
onLogEntry(`[${type}] ${line}`);
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
});
|
|
603
|
-
stream.on('error', (streamErr) => {
|
|
604
|
-
logger_js_1.default.printLine(`Error reading truncated log file ${filePath}: ${streamErr.message}`, 'error');
|
|
605
|
-
});
|
|
606
|
-
lastReadSizes[filePath] = newSize;
|
|
607
|
-
}
|
|
608
|
-
};
|
|
609
|
-
try {
|
|
610
|
-
fs.watchFile(filePath, { persistent: true, interval: 500 }, listener);
|
|
611
|
-
}
|
|
612
|
-
catch (watchError) {
|
|
613
|
-
logger_js_1.default.printLine(`Failed to watch log file ${filePath}: ${watchError.message}`, 'error');
|
|
614
|
-
}
|
|
615
|
-
};
|
|
616
|
-
logFilesToWatch.forEach(fileToWatch => {
|
|
617
|
-
if (fileToWatch.path) { // Ensure path is defined
|
|
618
|
-
setupWatcherForFile(fileToWatch.path, fileToWatch.type);
|
|
619
|
-
}
|
|
620
|
-
});
|
|
621
|
-
const stop = () => {
|
|
622
|
-
logFilesToWatch.forEach(fileToWatch => {
|
|
623
|
-
if (fileToWatch.path) { // Ensure path is defined before unwatching
|
|
624
|
-
try {
|
|
625
|
-
fs.unwatchFile(fileToWatch.path);
|
|
626
|
-
}
|
|
627
|
-
catch (unwatchError) {
|
|
628
|
-
// logger.printLine(`Error unwatching ${fileToWatch.path}: ${(unwatchError as Error).message}`, 'debug');
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
});
|
|
632
|
-
};
|
|
633
|
-
return { stop };
|
|
634
|
-
}
|
|
635
|
-
async save() {
|
|
636
|
-
await this.saveConfig();
|
|
637
|
-
console.log(chalk_1.default.green(`${figures_1.default.tick} Process configuration saved`));
|
|
638
|
-
}
|
|
639
|
-
async stopAll() {
|
|
640
|
-
const promises = Array.from(this.processes.keys()).map(id => this.stop(id));
|
|
641
|
-
await Promise.all(promises);
|
|
642
|
-
}
|
|
643
|
-
async restartAll() {
|
|
644
|
-
const promises = Array.from(this.processes.keys()).map(id => this.restart(id));
|
|
645
|
-
await Promise.all(promises);
|
|
646
|
-
}
|
|
647
|
-
async deleteAll() {
|
|
648
|
-
await this.stopAll();
|
|
649
|
-
this.processes.clear();
|
|
650
|
-
await this.saveConfig();
|
|
651
|
-
console.log(chalk_1.default.green(`${figures_1.default.tick} All processes deleted`));
|
|
652
|
-
}
|
|
653
|
-
getProcess(id) {
|
|
654
|
-
return this.processes.get(id);
|
|
655
|
-
}
|
|
656
|
-
async dispose() {
|
|
657
|
-
if (this.isDisposed)
|
|
658
|
-
return;
|
|
659
|
-
this.isDisposed = true;
|
|
660
|
-
this.stopMonitoring();
|
|
661
|
-
// Close all log streams
|
|
662
|
-
for (const id of this.logStreams.keys()) {
|
|
663
|
-
this.closeLogStreams(id);
|
|
664
|
-
}
|
|
665
|
-
// Stop all processes
|
|
666
|
-
await this.stopAll();
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
exports.ProcessManager = ProcessManager;
|