fix(core): Improve error handling, logging, and test suite; update dependency versions
This commit is contained in:
parent
5c4836fd68
commit
779593f73a
@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-03-10 - 1.5.1 - fix(core)
|
||||||
|
Improve error handling, logging, and test suite; update dependency versions
|
||||||
|
|
||||||
|
- Updated devDependencies versions in package.json (@git.zone/tsbuild, @push.rocks/tapbundle, and @push.rocks/smartdaemon)
|
||||||
|
- Refactored error handling and enhanced logging in ProcessMonitor and ProcessWrapper modules
|
||||||
|
- Improved test structure by adding clear module import tests and usage examples in test files
|
||||||
|
|
||||||
## 2025-03-04 - 1.5.0 - feat(cli)
|
## 2025-03-04 - 1.5.0 - feat(cli)
|
||||||
Enhance CLI with new process management commands
|
Enhance CLI with new process management commands
|
||||||
|
|
||||||
|
@ -18,18 +18,18 @@
|
|||||||
"tspm": "./cli.js"
|
"tspm": "./cli.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@git.zone/tsbuild": "^2.1.25",
|
"@git.zone/tsbuild": "^2.2.6",
|
||||||
"@git.zone/tsbundle": "^2.0.5",
|
"@git.zone/tsbundle": "^2.0.5",
|
||||||
"@git.zone/tsrun": "^1.2.46",
|
"@git.zone/tsrun": "^1.2.46",
|
||||||
"@git.zone/tstest": "^1.0.44",
|
"@git.zone/tstest": "^1.0.44",
|
||||||
"@push.rocks/tapbundle": "^5.0.15",
|
"@push.rocks/tapbundle": "^5.5.9",
|
||||||
"@types/node": "^22.13.8"
|
"@types/node": "^22.13.10"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@push.rocks/npmextra": "^5.1.2",
|
"@push.rocks/npmextra": "^5.1.2",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/smartcli": "^4.0.11",
|
"@push.rocks/smartcli": "^4.0.11",
|
||||||
"@push.rocks/smartdaemon": "^2.0.6",
|
"@push.rocks/smartdaemon": "^2.0.8",
|
||||||
"@push.rocks/smartpath": "^5.0.18",
|
"@push.rocks/smartpath": "^5.0.18",
|
||||||
"pidusage": "^4.0.0",
|
"pidusage": "^4.0.0",
|
||||||
"ps-tree": "^1.2.0"
|
"ps-tree": "^1.2.0"
|
||||||
|
989
pnpm-lock.yaml
generated
989
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
133
test/test.ts
133
test/test.ts
@ -1,27 +1,124 @@
|
|||||||
import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
|
import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
|
||||||
import * as tspm from '../ts/index.js';
|
import * as tspm from '../ts/index.js';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
tap.test('first test', async () => {
|
// Basic module import test
|
||||||
console.log(tspm);
|
tap.test('module import test', async () => {
|
||||||
|
console.log('Imported modules:', Object.keys(tspm));
|
||||||
|
expect(tspm.ProcessMonitor).toBeTypeOf('function');
|
||||||
|
expect(tspm.Tspm).toBeTypeOf('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ProcessMonitor test
|
||||||
|
tap.test('ProcessMonitor test', async () => {
|
||||||
|
const config: tspm.IMonitorConfig = {
|
||||||
|
name: 'Test Monitor',
|
||||||
|
projectDir: process.cwd(),
|
||||||
|
command: 'echo "Test process running"',
|
||||||
|
memoryLimitBytes: 50 * 1024 * 1024, // 50MB
|
||||||
|
monitorIntervalMs: 1000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const monitor = new tspm.ProcessMonitor(config);
|
||||||
|
|
||||||
|
// Test monitor creation
|
||||||
|
expect(monitor).toBeInstanceOf(tspm.ProcessMonitor);
|
||||||
|
|
||||||
|
// We won't actually start it in tests to avoid side effects
|
||||||
|
// but we can test the API
|
||||||
|
expect(monitor.start).toBeInstanceOf('function');
|
||||||
|
expect(monitor.stop).toBeInstanceOf('function');
|
||||||
|
expect(monitor.getLogs).toBeInstanceOf('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tspm class test
|
||||||
|
tap.test('Tspm class test', async () => {
|
||||||
|
const tspmInstance = new tspm.Tspm();
|
||||||
|
|
||||||
|
expect(tspmInstance).toBeInstanceOf(tspm.Tspm);
|
||||||
|
expect(tspmInstance.start).toBeInstanceOf('function');
|
||||||
|
expect(tspmInstance.stop).toBeInstanceOf('function');
|
||||||
|
expect(tspmInstance.restart).toBeInstanceOf('function');
|
||||||
|
expect(tspmInstance.list).toBeInstanceOf('function');
|
||||||
|
expect(tspmInstance.describe).toBeInstanceOf('function');
|
||||||
|
expect(tspmInstance.getLogs).toBeInstanceOf('function');
|
||||||
});
|
});
|
||||||
|
|
||||||
tap.start();
|
tap.start();
|
||||||
|
|
||||||
// Example usage:
|
// ====================================================
|
||||||
const config: tspm.IMonitorConfig = {
|
// Example usage (this part is not executed in tests)
|
||||||
name: 'Project XYZ Monitor', // Identifier for the instance
|
// ====================================================
|
||||||
projectDir: '/path/to/your/project', // Set the project directory here
|
|
||||||
command: 'npm run xyz', // Full command string (no need for args)
|
|
||||||
memoryLimitBytes: 500 * 1024 * 1024, // 500 MB memory limit
|
|
||||||
monitorIntervalMs: 5000, // Check memory usage every 5 seconds
|
|
||||||
};
|
|
||||||
|
|
||||||
const monitor = new tspm.ProcessMonitor(config);
|
// Example 1: Using ProcessMonitor directly
|
||||||
monitor.start();
|
function exampleUsingProcessMonitor() {
|
||||||
|
const config: tspm.IMonitorConfig = {
|
||||||
|
name: 'Project XYZ Monitor',
|
||||||
|
projectDir: '/path/to/your/project',
|
||||||
|
command: 'npm run xyz',
|
||||||
|
memoryLimitBytes: 500 * 1024 * 1024, // 500 MB memory limit
|
||||||
|
monitorIntervalMs: 5000, // Check memory usage every 5 seconds
|
||||||
|
logBufferSize: 200, // Keep last 200 log lines
|
||||||
|
};
|
||||||
|
|
||||||
// Ensure that on process exit (e.g. Ctrl+C) we clean up the child process and prevent respawns.
|
const monitor = new tspm.ProcessMonitor(config);
|
||||||
process.on('SIGINT', () => {
|
monitor.start();
|
||||||
console.log('Received SIGINT, stopping monitor...');
|
|
||||||
monitor.stop();
|
// Ensure that on process exit (e.g. Ctrl+C) we clean up the child process and prevent respawns.
|
||||||
process.exit();
|
process.on('SIGINT', () => {
|
||||||
});
|
console.log('Received SIGINT, stopping monitor...');
|
||||||
|
monitor.stop();
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get logs example
|
||||||
|
setTimeout(() => {
|
||||||
|
const logs = monitor.getLogs(10); // Get last 10 log lines
|
||||||
|
console.log('Latest logs:', logs);
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example 2: Using Tspm (higher-level process manager)
|
||||||
|
async function exampleUsingTspm() {
|
||||||
|
const tspmInstance = new tspm.Tspm();
|
||||||
|
|
||||||
|
// Start a process
|
||||||
|
await tspmInstance.start({
|
||||||
|
id: 'web-server',
|
||||||
|
name: 'Web Server',
|
||||||
|
projectDir: '/path/to/web/project',
|
||||||
|
command: 'npm run serve',
|
||||||
|
memoryLimitBytes: 300 * 1024 * 1024, // 300 MB
|
||||||
|
autorestart: true,
|
||||||
|
watch: true,
|
||||||
|
monitorIntervalMs: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start another process
|
||||||
|
await tspmInstance.start({
|
||||||
|
id: 'api-server',
|
||||||
|
name: 'API Server',
|
||||||
|
projectDir: '/path/to/api/project',
|
||||||
|
command: 'npm run api',
|
||||||
|
memoryLimitBytes: 400 * 1024 * 1024, // 400 MB
|
||||||
|
autorestart: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// List all processes
|
||||||
|
const processes = tspmInstance.list();
|
||||||
|
console.log('Running processes:', processes);
|
||||||
|
|
||||||
|
// Get logs from a process
|
||||||
|
const logs = tspmInstance.getLogs('web-server', 20);
|
||||||
|
console.log('Web server logs:', logs);
|
||||||
|
|
||||||
|
// Stop a process
|
||||||
|
await tspmInstance.stop('api-server');
|
||||||
|
|
||||||
|
// Handle graceful shutdown
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
console.log('Shutting down all processes...');
|
||||||
|
await tspmInstance.stopAll();
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
}
|
@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tspm',
|
name: '@git.zone/tspm',
|
||||||
version: '1.5.0',
|
version: '1.5.1',
|
||||||
description: 'a no fuzz process manager'
|
description: 'a no fuzz process manager'
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import { ProcessWrapper, type IProcessLog } from './classes.processwrapper.js';
|
import { ProcessWrapper, type IProcessLog } from './classes.processwrapper.js';
|
||||||
|
import { Logger, ProcessError, handleError } from './utils.errorhandler.js';
|
||||||
|
|
||||||
export interface IMonitorConfig {
|
export interface IMonitorConfig {
|
||||||
name?: string; // Optional name to identify the instance
|
name?: string; // Optional name to identify the instance
|
||||||
@ -18,9 +19,11 @@ export class ProcessMonitor {
|
|||||||
private intervalId: NodeJS.Timeout | null = null;
|
private intervalId: NodeJS.Timeout | null = null;
|
||||||
private stopped: boolean = true; // Initially stopped until start() is called
|
private stopped: boolean = true; // Initially stopped until start() is called
|
||||||
private restartCount: number = 0;
|
private restartCount: number = 0;
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(config: IMonitorConfig) {
|
constructor(config: IMonitorConfig) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
|
this.logger = new Logger(`ProcessMonitor:${config.name || 'unnamed'}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public start(): void {
|
public start(): void {
|
||||||
@ -31,7 +34,7 @@ export class ProcessMonitor {
|
|||||||
|
|
||||||
// Set the monitoring interval.
|
// Set the monitoring interval.
|
||||||
const interval = this.config.monitorIntervalMs || 5000;
|
const interval = this.config.monitorIntervalMs || 5000;
|
||||||
this.intervalId = setInterval(() => {
|
this.intervalId = setInterval((): void => {
|
||||||
if (this.processWrapper && this.processWrapper.getPid()) {
|
if (this.processWrapper && this.processWrapper.getPid()) {
|
||||||
this.monitorProcessGroup(this.processWrapper.getPid()!, this.config.memoryLimitBytes);
|
this.monitorProcessGroup(this.processWrapper.getPid()!, this.config.memoryLimitBytes);
|
||||||
}
|
}
|
||||||
@ -40,7 +43,12 @@ export class ProcessMonitor {
|
|||||||
|
|
||||||
private spawnProcess(): void {
|
private spawnProcess(): void {
|
||||||
// Don't spawn if the monitor has been stopped.
|
// Don't spawn if the monitor has been stopped.
|
||||||
if (this.stopped) return;
|
if (this.stopped) {
|
||||||
|
this.logger.debug('Not spawning process because monitor is stopped');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Spawning process: ${this.config.command}`);
|
||||||
|
|
||||||
// Create a new process wrapper
|
// Create a new process wrapper
|
||||||
this.processWrapper = new ProcessWrapper({
|
this.processWrapper = new ProcessWrapper({
|
||||||
@ -53,7 +61,7 @@ export class ProcessMonitor {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Set up event handlers
|
// Set up event handlers
|
||||||
this.processWrapper.on('log', (log) => {
|
this.processWrapper.on('log', (log: IProcessLog): void => {
|
||||||
// Here we could add handlers to send logs somewhere
|
// Here we could add handlers to send logs somewhere
|
||||||
// For now, we just log system messages to the console
|
// For now, we just log system messages to the console
|
||||||
if (log.type === 'system') {
|
if (log.type === 'system') {
|
||||||
@ -61,26 +69,47 @@ export class ProcessMonitor {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.processWrapper.on('exit', (code, signal) => {
|
this.processWrapper.on('exit', (code: number | null, signal: string | null): void => {
|
||||||
this.log(`Process exited with code ${code}, signal ${signal}.`);
|
const exitMsg = `Process exited with code ${code}, signal ${signal}.`;
|
||||||
|
this.logger.info(exitMsg);
|
||||||
|
this.log(exitMsg);
|
||||||
|
|
||||||
if (!this.stopped) {
|
if (!this.stopped) {
|
||||||
|
this.logger.info('Restarting process...');
|
||||||
this.log('Restarting process...');
|
this.log('Restarting process...');
|
||||||
this.restartCount++;
|
this.restartCount++;
|
||||||
this.spawnProcess();
|
this.spawnProcess();
|
||||||
|
} else {
|
||||||
|
this.logger.debug('Not restarting process because monitor is stopped');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.processWrapper.on('error', (error) => {
|
this.processWrapper.on('error', (error: Error | ProcessError): void => {
|
||||||
this.log(`Process error: ${error.message}`);
|
const errorMsg = error instanceof ProcessError
|
||||||
|
? `Process error: ${error.toString()}`
|
||||||
|
: `Process error: ${error.message}`;
|
||||||
|
|
||||||
|
this.logger.error(error);
|
||||||
|
this.log(errorMsg);
|
||||||
|
|
||||||
if (!this.stopped) {
|
if (!this.stopped) {
|
||||||
|
this.logger.info('Restarting process due to error...');
|
||||||
this.log('Restarting process due to error...');
|
this.log('Restarting process due to error...');
|
||||||
this.restartCount++;
|
this.restartCount++;
|
||||||
this.spawnProcess();
|
this.spawnProcess();
|
||||||
|
} else {
|
||||||
|
this.logger.debug('Not restarting process because monitor is stopped');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start the process
|
// Start the process
|
||||||
this.processWrapper.start();
|
try {
|
||||||
|
this.processWrapper.start();
|
||||||
|
} catch (error: Error | unknown) {
|
||||||
|
// The process wrapper will handle logging the error
|
||||||
|
// Just prevent it from bubbling up further
|
||||||
|
this.logger.error(`Failed to start process: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,24 +119,40 @@ export class ProcessMonitor {
|
|||||||
private async monitorProcessGroup(pid: number, memoryLimit: number): Promise<void> {
|
private async monitorProcessGroup(pid: number, memoryLimit: number): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const memoryUsage = await this.getProcessGroupMemory(pid);
|
const memoryUsage = await this.getProcessGroupMemory(pid);
|
||||||
|
|
||||||
|
this.logger.debug(
|
||||||
|
`Memory usage for PID ${pid}: ${this.humanReadableBytes(memoryUsage)} (${memoryUsage} bytes)`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only log to the process log at longer intervals to avoid spamming
|
||||||
this.log(
|
this.log(
|
||||||
`Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes(
|
`Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes(
|
||||||
memoryUsage
|
memoryUsage
|
||||||
)} (${memoryUsage} bytes)`
|
)} (${memoryUsage} bytes)`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (memoryUsage > memoryLimit) {
|
if (memoryUsage > memoryLimit) {
|
||||||
this.log(
|
const memoryLimitMsg = `Memory usage ${this.humanReadableBytes(
|
||||||
`Memory usage ${this.humanReadableBytes(
|
memoryUsage
|
||||||
memoryUsage
|
)} exceeds limit of ${this.humanReadableBytes(memoryLimit)}. Restarting process.`;
|
||||||
)} exceeds limit of ${this.humanReadableBytes(memoryLimit)}. Restarting process.`
|
|
||||||
);
|
this.logger.warn(memoryLimitMsg);
|
||||||
|
this.log(memoryLimitMsg);
|
||||||
|
|
||||||
// Stop the process wrapper, which will trigger the exit handler and restart
|
// Stop the process wrapper, which will trigger the exit handler and restart
|
||||||
if (this.processWrapper) {
|
if (this.processWrapper) {
|
||||||
this.processWrapper.stop();
|
this.processWrapper.stop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: Error | unknown) {
|
||||||
this.log('Error monitoring process group: ' + error);
|
const processError = new ProcessError(
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
'ERR_MEMORY_MONITORING_FAILED',
|
||||||
|
{ pid }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.error(processError);
|
||||||
|
this.log(`Error monitoring process group: ${processError.toString()}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,16 +161,40 @@ export class ProcessMonitor {
|
|||||||
*/
|
*/
|
||||||
private getProcessGroupMemory(pid: number): Promise<number> {
|
private getProcessGroupMemory(pid: number): Promise<number> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
plugins.psTree(pid, (err, children) => {
|
this.logger.debug(`Getting memory usage for process group with PID ${pid}`);
|
||||||
if (err) return reject(err);
|
|
||||||
|
plugins.psTree(pid, (err: Error | null, children: Array<{ PID: string }>) => {
|
||||||
|
if (err) {
|
||||||
|
const processError = new ProcessError(
|
||||||
|
`Failed to get process tree: ${err.message}`,
|
||||||
|
'ERR_PSTREE_FAILED',
|
||||||
|
{ pid }
|
||||||
|
);
|
||||||
|
this.logger.debug(`psTree error: ${err.message}`);
|
||||||
|
return reject(processError);
|
||||||
|
}
|
||||||
|
|
||||||
// Include the main process and its children.
|
// Include the main process and its children.
|
||||||
const pids: number[] = [pid, ...children.map(child => Number(child.PID))];
|
const pids: number[] = [pid, ...children.map(child => Number(child.PID))];
|
||||||
plugins.pidusage(pids, (err, stats) => {
|
this.logger.debug(`Found ${pids.length} processes in group with parent PID ${pid}`);
|
||||||
if (err) return reject(err);
|
|
||||||
|
plugins.pidusage(pids, (err: Error | null, stats: Record<string, { memory: number }>) => {
|
||||||
|
if (err) {
|
||||||
|
const processError = new ProcessError(
|
||||||
|
`Failed to get process usage stats: ${err.message}`,
|
||||||
|
'ERR_PIDUSAGE_FAILED',
|
||||||
|
{ pids }
|
||||||
|
);
|
||||||
|
this.logger.debug(`pidusage error: ${err.message}`);
|
||||||
|
return reject(processError);
|
||||||
|
}
|
||||||
|
|
||||||
let totalMemory = 0;
|
let totalMemory = 0;
|
||||||
for (const key in stats) {
|
for (const key in stats) {
|
||||||
totalMemory += stats[key].memory;
|
totalMemory += stats[key].memory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`Total memory for process group: ${this.humanReadableBytes(totalMemory)}`);
|
||||||
resolve(totalMemory);
|
resolve(totalMemory);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import { Logger, ProcessError, handleError } from './utils.errorhandler.js';
|
||||||
|
|
||||||
export interface IProcessWrapperOptions {
|
export interface IProcessWrapperOptions {
|
||||||
command: string;
|
command: string;
|
||||||
@ -22,11 +23,13 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
private logs: IProcessLog[] = [];
|
private logs: IProcessLog[] = [];
|
||||||
private logBufferSize: number;
|
private logBufferSize: number;
|
||||||
private startTime: Date | null = null;
|
private startTime: Date | null = null;
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
constructor(options: IProcessWrapperOptions) {
|
constructor(options: IProcessWrapperOptions) {
|
||||||
super();
|
super();
|
||||||
this.options = options;
|
this.options = options;
|
||||||
this.logBufferSize = options.logBuffer || 100;
|
this.logBufferSize = options.logBuffer || 100;
|
||||||
|
this.logger = new Logger(`ProcessWrapper:${options.name}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,6 +39,8 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
this.addSystemLog('Starting process...');
|
this.addSystemLog('Starting process...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
this.logger.debug(`Starting process: ${this.options.command}`);
|
||||||
|
|
||||||
if (this.options.args && this.options.args.length > 0) {
|
if (this.options.args && this.options.args.length > 0) {
|
||||||
this.process = plugins.childProcess.spawn(this.options.command, this.options.args, {
|
this.process = plugins.childProcess.spawn(this.options.command, this.options.args, {
|
||||||
cwd: this.options.cwd,
|
cwd: this.options.cwd,
|
||||||
@ -56,14 +61,22 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
|
|
||||||
// Handle process exit
|
// Handle process exit
|
||||||
this.process.on('exit', (code, signal) => {
|
this.process.on('exit', (code, signal) => {
|
||||||
this.addSystemLog(`Process exited with code ${code}, signal ${signal}`);
|
const exitMessage = `Process exited with code ${code}, signal ${signal}`;
|
||||||
|
this.logger.info(exitMessage);
|
||||||
|
this.addSystemLog(exitMessage);
|
||||||
this.emit('exit', code, signal);
|
this.emit('exit', code, signal);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle errors
|
// Handle errors
|
||||||
this.process.on('error', (error) => {
|
this.process.on('error', (error) => {
|
||||||
this.addSystemLog(`Process error: ${error.message}`);
|
const processError = new ProcessError(
|
||||||
this.emit('error', error);
|
error.message,
|
||||||
|
'ERR_PROCESS_EXECUTION',
|
||||||
|
{ command: this.options.command, pid: this.process?.pid }
|
||||||
|
);
|
||||||
|
this.logger.error(processError);
|
||||||
|
this.addSystemLog(`Process error: ${processError.toString()}`);
|
||||||
|
this.emit('error', processError);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Capture stdout
|
// Capture stdout
|
||||||
@ -91,12 +104,22 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.addSystemLog(`Process started with PID ${this.process.pid}`);
|
this.addSystemLog(`Process started with PID ${this.process.pid}`);
|
||||||
|
this.logger.info(`Process started with PID ${this.process.pid}`);
|
||||||
this.emit('start', this.process.pid);
|
this.emit('start', this.process.pid);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error: Error | unknown) {
|
||||||
this.addSystemLog(`Failed to start process: ${error.message}`);
|
const processError = error instanceof ProcessError
|
||||||
this.emit('error', error);
|
? error
|
||||||
throw error;
|
: new ProcessError(
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
'ERR_PROCESS_START_FAILED',
|
||||||
|
{ command: this.options.command }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logger.error(processError);
|
||||||
|
this.addSystemLog(`Failed to start process: ${processError.toString()}`);
|
||||||
|
this.emit('error', processError);
|
||||||
|
throw processError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,30 +128,43 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
public stop(): void {
|
public stop(): void {
|
||||||
if (!this.process) {
|
if (!this.process) {
|
||||||
|
this.logger.debug('Stop called but no process is running');
|
||||||
this.addSystemLog('No process running');
|
this.addSystemLog('No process running');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.info('Stopping process...');
|
||||||
this.addSystemLog('Stopping process...');
|
this.addSystemLog('Stopping process...');
|
||||||
|
|
||||||
// First try SIGTERM for graceful shutdown
|
// First try SIGTERM for graceful shutdown
|
||||||
if (this.process.pid) {
|
if (this.process.pid) {
|
||||||
try {
|
try {
|
||||||
|
this.logger.debug(`Sending SIGTERM to process ${this.process.pid}`);
|
||||||
process.kill(this.process.pid, 'SIGTERM');
|
process.kill(this.process.pid, 'SIGTERM');
|
||||||
|
|
||||||
// Give it 5 seconds to shut down gracefully
|
// Give it 5 seconds to shut down gracefully
|
||||||
setTimeout(() => {
|
setTimeout((): void => {
|
||||||
if (this.process && this.process.pid) {
|
if (this.process && this.process.pid) {
|
||||||
|
this.logger.warn(`Process ${this.process.pid} did not exit gracefully, force killing...`);
|
||||||
this.addSystemLog('Process did not exit gracefully, force killing...');
|
this.addSystemLog('Process did not exit gracefully, force killing...');
|
||||||
try {
|
try {
|
||||||
process.kill(this.process.pid, 'SIGKILL');
|
process.kill(this.process.pid, 'SIGKILL');
|
||||||
} catch (error) {
|
} catch (error: Error | unknown) {
|
||||||
// Process might have exited between checks
|
// Process might have exited between checks
|
||||||
|
this.logger.debug(`Failed to send SIGKILL, process probably already exited: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 5000);
|
}, 5000);
|
||||||
} catch (error) {
|
} catch (error: Error | unknown) {
|
||||||
this.addSystemLog(`Error stopping process: ${error.message}`);
|
const processError = new ProcessError(
|
||||||
|
error instanceof Error ? error.message : String(error),
|
||||||
|
'ERR_PROCESS_STOP_FAILED',
|
||||||
|
{ pid: this.process.pid }
|
||||||
|
);
|
||||||
|
this.logger.error(processError);
|
||||||
|
this.addSystemLog(`Error stopping process: ${processError.toString()}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,13 @@ import * as plugins from './plugins.js';
|
|||||||
import * as paths from './paths.js';
|
import * as paths from './paths.js';
|
||||||
import { ProcessMonitor, type IMonitorConfig } from './classes.processmonitor.js';
|
import { ProcessMonitor, type IMonitorConfig } from './classes.processmonitor.js';
|
||||||
import { TspmConfig } from './classes.config.js';
|
import { TspmConfig } from './classes.config.js';
|
||||||
|
import {
|
||||||
|
Logger,
|
||||||
|
ProcessError,
|
||||||
|
ConfigError,
|
||||||
|
ValidationError,
|
||||||
|
handleError
|
||||||
|
} from './utils.errorhandler.js';
|
||||||
|
|
||||||
export interface IProcessConfig extends IMonitorConfig {
|
export interface IProcessConfig extends IMonitorConfig {
|
||||||
id: string; // Unique identifier for the process
|
id: string; // Unique identifier for the process
|
||||||
@ -32,8 +39,10 @@ export class Tspm {
|
|||||||
private processInfo: Map<string, IProcessInfo> = new Map();
|
private processInfo: Map<string, IProcessInfo> = new Map();
|
||||||
private config: TspmConfig;
|
private config: TspmConfig;
|
||||||
private configStorageKey = 'processes';
|
private configStorageKey = 'processes';
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.logger = new Logger('Tspm');
|
||||||
this.config = new TspmConfig();
|
this.config = new TspmConfig();
|
||||||
this.loadProcessConfigs();
|
this.loadProcessConfigs();
|
||||||
}
|
}
|
||||||
@ -42,53 +51,113 @@ export class Tspm {
|
|||||||
* Start a new process with the given configuration
|
* Start a new process with the given configuration
|
||||||
*/
|
*/
|
||||||
public async start(config: IProcessConfig): Promise<void> {
|
public async start(config: IProcessConfig): Promise<void> {
|
||||||
// Check if process with this id already exists
|
this.logger.info(`Starting process with id '${config.id}'`);
|
||||||
if (this.processes.has(config.id)) {
|
|
||||||
throw new Error(`Process with id '${config.id}' already exists`);
|
// Validate config
|
||||||
|
if (!config.id || !config.command || !config.projectDir) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'Invalid process configuration: missing required fields',
|
||||||
|
'ERR_INVALID_CONFIG',
|
||||||
|
{ config }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and store process config
|
// Check if process with this id already exists
|
||||||
this.processConfigs.set(config.id, config);
|
if (this.processes.has(config.id)) {
|
||||||
|
throw new ValidationError(
|
||||||
|
`Process with id '${config.id}' already exists`,
|
||||||
|
'ERR_DUPLICATE_PROCESS'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize process info
|
try {
|
||||||
this.processInfo.set(config.id, {
|
// Create and store process config
|
||||||
id: config.id,
|
this.processConfigs.set(config.id, config);
|
||||||
status: 'stopped',
|
|
||||||
memory: 0,
|
// Initialize process info
|
||||||
restarts: 0
|
this.processInfo.set(config.id, {
|
||||||
});
|
id: config.id,
|
||||||
|
status: 'stopped',
|
||||||
// Create and start process monitor
|
memory: 0,
|
||||||
const monitor = new ProcessMonitor({
|
restarts: 0
|
||||||
name: config.name || config.id,
|
});
|
||||||
projectDir: config.projectDir,
|
|
||||||
command: config.command,
|
// Create and start process monitor
|
||||||
args: config.args,
|
const monitor = new ProcessMonitor({
|
||||||
memoryLimitBytes: config.memoryLimitBytes,
|
name: config.name || config.id,
|
||||||
monitorIntervalMs: config.monitorIntervalMs
|
projectDir: config.projectDir,
|
||||||
});
|
command: config.command,
|
||||||
|
args: config.args,
|
||||||
this.processes.set(config.id, monitor);
|
memoryLimitBytes: config.memoryLimitBytes,
|
||||||
monitor.start();
|
monitorIntervalMs: config.monitorIntervalMs,
|
||||||
|
env: config.env,
|
||||||
// Update process info
|
logBufferSize: config.logBufferSize
|
||||||
this.updateProcessInfo(config.id, { status: 'online' });
|
});
|
||||||
|
|
||||||
// Save updated configs
|
this.processes.set(config.id, monitor);
|
||||||
await this.saveProcessConfigs();
|
monitor.start();
|
||||||
|
|
||||||
|
// Update process info
|
||||||
|
this.updateProcessInfo(config.id, { status: 'online' });
|
||||||
|
|
||||||
|
// Save updated configs
|
||||||
|
await this.saveProcessConfigs();
|
||||||
|
|
||||||
|
this.logger.info(`Successfully started process with id '${config.id}'`);
|
||||||
|
} catch (error: Error | unknown) {
|
||||||
|
// Clean up in case of error
|
||||||
|
this.processConfigs.delete(config.id);
|
||||||
|
this.processInfo.delete(config.id);
|
||||||
|
this.processes.delete(config.id);
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
throw new ProcessError(
|
||||||
|
`Failed to start process: ${error.message}`,
|
||||||
|
'ERR_PROCESS_START_FAILED',
|
||||||
|
{ id: config.id, command: config.command }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const genericError = new ProcessError(
|
||||||
|
`Failed to start process: ${String(error)}`,
|
||||||
|
'ERR_PROCESS_START_FAILED',
|
||||||
|
{ id: config.id }
|
||||||
|
);
|
||||||
|
this.logger.error(genericError);
|
||||||
|
throw genericError;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop a process by id
|
* Stop a process by id
|
||||||
*/
|
*/
|
||||||
public async stop(id: string): Promise<void> {
|
public async stop(id: string): Promise<void> {
|
||||||
|
this.logger.info(`Stopping process with id '${id}'`);
|
||||||
|
|
||||||
const monitor = this.processes.get(id);
|
const monitor = this.processes.get(id);
|
||||||
if (!monitor) {
|
if (!monitor) {
|
||||||
throw new Error(`Process with id '${id}' not found`);
|
const error = new ValidationError(
|
||||||
|
`Process with id '${id}' not found`,
|
||||||
|
'ERR_PROCESS_NOT_FOUND'
|
||||||
|
);
|
||||||
|
this.logger.error(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
monitor.stop();
|
try {
|
||||||
this.updateProcessInfo(id, { status: 'stopped' });
|
monitor.stop();
|
||||||
|
this.updateProcessInfo(id, { status: 'stopped' });
|
||||||
|
this.logger.info(`Successfully stopped process with id '${id}'`);
|
||||||
|
} catch (error: Error | unknown) {
|
||||||
|
const processError = new ProcessError(
|
||||||
|
`Failed to stop process: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
'ERR_PROCESS_STOP_FAILED',
|
||||||
|
{ id }
|
||||||
|
);
|
||||||
|
this.logger.error(processError);
|
||||||
|
throw processError;
|
||||||
|
}
|
||||||
|
|
||||||
// Don't remove from the maps, just mark as stopped
|
// Don't remove from the maps, just mark as stopped
|
||||||
// This allows it to be restarted later
|
// This allows it to be restarted later
|
||||||
@ -98,36 +167,57 @@ export class Tspm {
|
|||||||
* Restart a process by id
|
* Restart a process by id
|
||||||
*/
|
*/
|
||||||
public async restart(id: string): Promise<void> {
|
public async restart(id: string): Promise<void> {
|
||||||
|
this.logger.info(`Restarting process with id '${id}'`);
|
||||||
|
|
||||||
const monitor = this.processes.get(id);
|
const monitor = this.processes.get(id);
|
||||||
const config = this.processConfigs.get(id);
|
const config = this.processConfigs.get(id);
|
||||||
|
|
||||||
if (!monitor || !config) {
|
if (!monitor || !config) {
|
||||||
throw new Error(`Process with id '${id}' not found`);
|
const error = new ValidationError(
|
||||||
|
`Process with id '${id}' not found`,
|
||||||
|
'ERR_PROCESS_NOT_FOUND'
|
||||||
|
);
|
||||||
|
this.logger.error(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop and then start the process
|
try {
|
||||||
monitor.stop();
|
// Stop and then start the process
|
||||||
|
monitor.stop();
|
||||||
// Create a new monitor instance
|
|
||||||
const newMonitor = new ProcessMonitor({
|
// Create a new monitor instance
|
||||||
name: config.name || config.id,
|
const newMonitor = new ProcessMonitor({
|
||||||
projectDir: config.projectDir,
|
name: config.name || config.id,
|
||||||
command: config.command,
|
projectDir: config.projectDir,
|
||||||
args: config.args,
|
command: config.command,
|
||||||
memoryLimitBytes: config.memoryLimitBytes,
|
args: config.args,
|
||||||
monitorIntervalMs: config.monitorIntervalMs
|
memoryLimitBytes: config.memoryLimitBytes,
|
||||||
});
|
monitorIntervalMs: config.monitorIntervalMs,
|
||||||
|
env: config.env,
|
||||||
this.processes.set(id, newMonitor);
|
logBufferSize: config.logBufferSize
|
||||||
newMonitor.start();
|
|
||||||
|
|
||||||
// Update restart count
|
|
||||||
const info = this.processInfo.get(id);
|
|
||||||
if (info) {
|
|
||||||
this.updateProcessInfo(id, {
|
|
||||||
status: 'online',
|
|
||||||
restarts: info.restarts + 1
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.processes.set(id, newMonitor);
|
||||||
|
newMonitor.start();
|
||||||
|
|
||||||
|
// Update restart count
|
||||||
|
const info = this.processInfo.get(id);
|
||||||
|
if (info) {
|
||||||
|
this.updateProcessInfo(id, {
|
||||||
|
status: 'online',
|
||||||
|
restarts: info.restarts + 1
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info(`Successfully restarted process with id '${id}'`);
|
||||||
|
} catch (error: Error | unknown) {
|
||||||
|
const processError = new ProcessError(
|
||||||
|
`Failed to restart process: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
'ERR_PROCESS_RESTART_FAILED',
|
||||||
|
{ id }
|
||||||
|
);
|
||||||
|
this.logger.error(processError);
|
||||||
|
throw processError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -135,20 +225,52 @@ export class Tspm {
|
|||||||
* Delete a process by id
|
* Delete a process by id
|
||||||
*/
|
*/
|
||||||
public async delete(id: string): Promise<void> {
|
public async delete(id: string): Promise<void> {
|
||||||
// Stop the process if it's running
|
this.logger.info(`Deleting process with id '${id}'`);
|
||||||
try {
|
|
||||||
await this.stop(id);
|
// Check if process exists
|
||||||
} catch (error) {
|
if (!this.processConfigs.has(id)) {
|
||||||
// Ignore errors if the process is not running
|
const error = new ValidationError(
|
||||||
|
`Process with id '${id}' not found`,
|
||||||
|
'ERR_PROCESS_NOT_FOUND'
|
||||||
|
);
|
||||||
|
this.logger.error(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove from all maps
|
// Stop the process if it's running
|
||||||
this.processes.delete(id);
|
try {
|
||||||
this.processConfigs.delete(id);
|
if (this.processes.has(id)) {
|
||||||
this.processInfo.delete(id);
|
await this.stop(id);
|
||||||
|
}
|
||||||
// Save updated configs
|
|
||||||
await this.saveProcessConfigs();
|
// Remove from all maps
|
||||||
|
this.processes.delete(id);
|
||||||
|
this.processConfigs.delete(id);
|
||||||
|
this.processInfo.delete(id);
|
||||||
|
|
||||||
|
// Save updated configs
|
||||||
|
await this.saveProcessConfigs();
|
||||||
|
|
||||||
|
this.logger.info(`Successfully deleted process with id '${id}'`);
|
||||||
|
} catch (error: Error | unknown) {
|
||||||
|
// Even if stop fails, we should still try to delete the configuration
|
||||||
|
try {
|
||||||
|
this.processes.delete(id);
|
||||||
|
this.processConfigs.delete(id);
|
||||||
|
this.processInfo.delete(id);
|
||||||
|
await this.saveProcessConfigs();
|
||||||
|
|
||||||
|
this.logger.info(`Successfully deleted process with id '${id}' after stopping failure`);
|
||||||
|
} catch (deleteError: Error | unknown) {
|
||||||
|
const configError = new ConfigError(
|
||||||
|
`Failed to delete process configuration: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`,
|
||||||
|
'ERR_CONFIG_DELETE_FAILED',
|
||||||
|
{ id }
|
||||||
|
);
|
||||||
|
this.logger.error(configError);
|
||||||
|
throw configError;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -227,33 +349,71 @@ export class Tspm {
|
|||||||
* Save all process configurations to config storage
|
* Save all process configurations to config storage
|
||||||
*/
|
*/
|
||||||
private async saveProcessConfigs(): Promise<void> {
|
private async saveProcessConfigs(): Promise<void> {
|
||||||
const configs = Array.from(this.processConfigs.values());
|
this.logger.debug('Saving process configurations to storage');
|
||||||
await this.config.writeKey(this.configStorageKey, JSON.stringify(configs));
|
|
||||||
|
try {
|
||||||
|
const configs = Array.from(this.processConfigs.values());
|
||||||
|
await this.config.writeKey(this.configStorageKey, JSON.stringify(configs));
|
||||||
|
this.logger.debug(`Saved ${configs.length} process configurations`);
|
||||||
|
} catch (error: Error | unknown) {
|
||||||
|
const configError = new ConfigError(
|
||||||
|
`Failed to save process configurations: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
'ERR_CONFIG_SAVE_FAILED'
|
||||||
|
);
|
||||||
|
this.logger.error(configError);
|
||||||
|
throw configError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load process configurations from config storage
|
* Load process configurations from config storage
|
||||||
*/
|
*/
|
||||||
private async loadProcessConfigs(): Promise<void> {
|
private async loadProcessConfigs(): Promise<void> {
|
||||||
|
this.logger.debug('Loading process configurations from storage');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const configsJson = await this.config.readKey(this.configStorageKey);
|
const configsJson = await this.config.readKey(this.configStorageKey);
|
||||||
if (configsJson) {
|
if (configsJson) {
|
||||||
const configs = JSON.parse(configsJson) as IProcessConfig[];
|
try {
|
||||||
for (const config of configs) {
|
const configs = JSON.parse(configsJson) as IProcessConfig[];
|
||||||
this.processConfigs.set(config.id, config);
|
this.logger.debug(`Loaded ${configs.length} process configurations`);
|
||||||
|
|
||||||
// Initialize process info
|
for (const config of configs) {
|
||||||
this.processInfo.set(config.id, {
|
// Validate config
|
||||||
id: config.id,
|
if (!config.id || !config.command || !config.projectDir) {
|
||||||
status: 'stopped',
|
this.logger.warn(`Skipping invalid process config for id '${config.id || 'unknown'}'`);
|
||||||
memory: 0,
|
continue;
|
||||||
restarts: 0
|
}
|
||||||
});
|
|
||||||
|
this.processConfigs.set(config.id, config);
|
||||||
|
|
||||||
|
// Initialize process info
|
||||||
|
this.processInfo.set(config.id, {
|
||||||
|
id: config.id,
|
||||||
|
status: 'stopped',
|
||||||
|
memory: 0,
|
||||||
|
restarts: 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (parseError: Error | unknown) {
|
||||||
|
const configError = new ConfigError(
|
||||||
|
`Failed to parse process configurations: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
||||||
|
'ERR_CONFIG_PARSE_FAILED'
|
||||||
|
);
|
||||||
|
this.logger.error(configError);
|
||||||
|
throw configError;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
this.logger.info('No saved process configurations found');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: Error | unknown) {
|
||||||
|
// Only throw if it's not the "no configs found" case
|
||||||
|
if (error instanceof ConfigError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
// If no configs found or error reading, just continue with empty configs
|
// If no configs found or error reading, just continue with empty configs
|
||||||
console.log('No saved process configurations found');
|
this.logger.info('No saved process configurations found or error reading them');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
109
ts/cli.ts
109
ts/cli.ts
@ -1,17 +1,40 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import * as paths from './paths.js';
|
import * as paths from './paths.js';
|
||||||
import { Tspm, IProcessConfig } from './classes.tspm.js';
|
import { Tspm, type IProcessConfig } from './classes.tspm.js';
|
||||||
|
import {
|
||||||
|
Logger,
|
||||||
|
LogLevel,
|
||||||
|
handleError,
|
||||||
|
TspmError,
|
||||||
|
ProcessError,
|
||||||
|
ConfigError,
|
||||||
|
ValidationError
|
||||||
|
} from './utils.errorhandler.js';
|
||||||
|
|
||||||
export const run = async () => {
|
// Define interface for CLI arguments
|
||||||
|
interface CliArguments {
|
||||||
|
_: (string | number)[];
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const run = async (): Promise<void> => {
|
||||||
|
const cliLogger = new Logger('CLI');
|
||||||
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||||
const tspm = new Tspm();
|
const tspm = new Tspm();
|
||||||
|
|
||||||
|
// Check if debug mode is enabled
|
||||||
|
const debugMode = process.env.TSPM_DEBUG === 'true';
|
||||||
|
if (debugMode) {
|
||||||
|
cliLogger.setLevel(LogLevel.DEBUG);
|
||||||
|
cliLogger.debug('Debug mode enabled');
|
||||||
|
}
|
||||||
|
|
||||||
const smartcliInstance = new plugins.smartcli.Smartcli();
|
const smartcliInstance = new plugins.smartcli.Smartcli();
|
||||||
smartcliInstance.addVersion(tspmProjectinfo.npm.version);
|
smartcliInstance.addVersion(tspmProjectinfo.npm.version);
|
||||||
|
|
||||||
// Default command - show help and list processes
|
// Default command - show help and list processes
|
||||||
smartcliInstance.standardCommand().subscribe({
|
smartcliInstance.standardCommand().subscribe({
|
||||||
next: async (argvArg) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
console.log(`TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`);
|
console.log(`TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`);
|
||||||
console.log('Usage: tspm [command] [options]');
|
console.log('Usage: tspm [command] [options]');
|
||||||
console.log('\nCommands:');
|
console.log('\nCommands:');
|
||||||
@ -46,7 +69,7 @@ export const run = async () => {
|
|||||||
|
|
||||||
// Start command - start a new process
|
// Start command - start a new process
|
||||||
smartcliInstance.addCommand('start').subscribe({
|
smartcliInstance.addCommand('start').subscribe({
|
||||||
next: async (argvArg) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
const script = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
const script = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
||||||
if (!script) {
|
if (!script) {
|
||||||
console.error('Error: Missing script argument. Usage: tspm start <script>');
|
console.error('Error: Missing script argument. Usage: tspm start <script>');
|
||||||
@ -59,6 +82,8 @@ export const run = async () => {
|
|||||||
const memLimit = parseMemoryString(argvArg.memory || '500MB');
|
const memLimit = parseMemoryString(argvArg.memory || '500MB');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
cliLogger.debug(`Starting process with script: ${script}`);
|
||||||
|
|
||||||
const processConfig: IProcessConfig = {
|
const processConfig: IProcessConfig = {
|
||||||
id: argvArg.id || name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase(),
|
id: argvArg.id || name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase(),
|
||||||
name: name,
|
name: name,
|
||||||
@ -71,17 +96,32 @@ export const run = async () => {
|
|||||||
watch: Boolean(argvArg.watch)
|
watch: Boolean(argvArg.watch)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cliLogger.debug(`Created process config: ${JSON.stringify(processConfig)}`);
|
||||||
|
|
||||||
await tspm.start(processConfig);
|
await tspm.start(processConfig);
|
||||||
console.log(`Process ${processConfig.id} started successfully.`);
|
console.log(`Process ${processConfig.id} started successfully.`);
|
||||||
} catch (error) {
|
} catch (error: Error | unknown) {
|
||||||
console.error(`Error starting process: ${error.message}`);
|
const tspmError = handleError(error);
|
||||||
|
|
||||||
|
if (tspmError instanceof ValidationError) {
|
||||||
|
console.error(`Validation error: ${tspmError.message}`);
|
||||||
|
} else if (tspmError instanceof ProcessError) {
|
||||||
|
console.error(`Process error: ${tspmError.message}`);
|
||||||
|
if (debugMode) {
|
||||||
|
console.error(`Error details: ${JSON.stringify(tspmError.details)}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(`Error starting process: ${tspmError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
cliLogger.error(tspmError);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start as daemon command
|
// Start as daemon command
|
||||||
smartcliInstance.addCommand('startAsDaemon').subscribe({
|
smartcliInstance.addCommand('startAsDaemon').subscribe({
|
||||||
next: async (argvArg) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
const script = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
const script = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
||||||
if (!script) {
|
if (!script) {
|
||||||
console.error('Error: Missing script argument. Usage: tspm startAsDaemon <script>');
|
console.error('Error: Missing script argument. Usage: tspm startAsDaemon <script>');
|
||||||
@ -114,7 +154,7 @@ export const run = async () => {
|
|||||||
|
|
||||||
// Stop command
|
// Stop command
|
||||||
smartcliInstance.addCommand('stop').subscribe({
|
smartcliInstance.addCommand('stop').subscribe({
|
||||||
next: async (argvArg) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@ -123,17 +163,26 @@ export const run = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
cliLogger.debug(`Stopping process: ${id}`);
|
||||||
await tspm.stop(id);
|
await tspm.stop(id);
|
||||||
console.log(`Process ${id} stopped.`);
|
console.log(`Process ${id} stopped.`);
|
||||||
} catch (error) {
|
} catch (error: Error | unknown) {
|
||||||
console.error(`Error stopping process: ${error.message}`);
|
const tspmError = handleError(error);
|
||||||
|
|
||||||
|
if (tspmError instanceof ValidationError) {
|
||||||
|
console.error(`Validation error: ${tspmError.message}`);
|
||||||
|
} else {
|
||||||
|
console.error(`Error stopping process: ${tspmError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
cliLogger.error(tspmError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Restart command
|
// Restart command
|
||||||
smartcliInstance.addCommand('restart').subscribe({
|
smartcliInstance.addCommand('restart').subscribe({
|
||||||
next: async (argvArg) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@ -142,17 +191,28 @@ export const run = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
cliLogger.debug(`Restarting process: ${id}`);
|
||||||
await tspm.restart(id);
|
await tspm.restart(id);
|
||||||
console.log(`Process ${id} restarted.`);
|
console.log(`Process ${id} restarted.`);
|
||||||
} catch (error) {
|
} catch (error: Error | unknown) {
|
||||||
console.error(`Error restarting process: ${error.message}`);
|
const tspmError = handleError(error);
|
||||||
|
|
||||||
|
if (tspmError instanceof ValidationError) {
|
||||||
|
console.error(`Validation error: ${tspmError.message}`);
|
||||||
|
} else if (tspmError instanceof ProcessError) {
|
||||||
|
console.error(`Process error: ${tspmError.message}`);
|
||||||
|
} else {
|
||||||
|
console.error(`Error restarting process: ${tspmError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
cliLogger.error(tspmError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete command
|
// Delete command
|
||||||
smartcliInstance.addCommand('delete').subscribe({
|
smartcliInstance.addCommand('delete').subscribe({
|
||||||
next: async (argvArg) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@ -161,17 +221,28 @@ export const run = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
cliLogger.debug(`Deleting process: ${id}`);
|
||||||
await tspm.delete(id);
|
await tspm.delete(id);
|
||||||
console.log(`Process ${id} deleted.`);
|
console.log(`Process ${id} deleted.`);
|
||||||
} catch (error) {
|
} catch (error: Error | unknown) {
|
||||||
console.error(`Error deleting process: ${error.message}`);
|
const tspmError = handleError(error);
|
||||||
|
|
||||||
|
if (tspmError instanceof ValidationError) {
|
||||||
|
console.error(`Validation error: ${tspmError.message}`);
|
||||||
|
} else if (tspmError instanceof ConfigError) {
|
||||||
|
console.error(`Configuration error: ${tspmError.message}`);
|
||||||
|
} else {
|
||||||
|
console.error(`Error deleting process: ${tspmError.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
cliLogger.error(tspmError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// List command
|
// List command
|
||||||
smartcliInstance.addCommand('list').subscribe({
|
smartcliInstance.addCommand('list').subscribe({
|
||||||
next: async (argvArg) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
const processes = tspm.list();
|
const processes = tspm.list();
|
||||||
|
|
||||||
if (processes.length === 0) {
|
if (processes.length === 0) {
|
||||||
@ -193,7 +264,7 @@ export const run = async () => {
|
|||||||
|
|
||||||
// Describe command
|
// Describe command
|
||||||
smartcliInstance.addCommand('describe').subscribe({
|
smartcliInstance.addCommand('describe').subscribe({
|
||||||
next: async (argvArg) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
@ -224,7 +295,7 @@ export const run = async () => {
|
|||||||
|
|
||||||
// Logs command
|
// Logs command
|
||||||
smartcliInstance.addCommand('logs').subscribe({
|
smartcliInstance.addCommand('logs').subscribe({
|
||||||
next: async (argvArg) => {
|
next: async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
|
||||||
export const packageDir = plugins.path.join(plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url), '..');
|
export const packageDir: string = plugins.path.join(plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url), '..');
|
||||||
export const cwd = process.cwd();
|
export const cwd: string = process.cwd();
|
@ -2,6 +2,7 @@
|
|||||||
import * as childProcess from 'child_process';
|
import * as childProcess from 'child_process';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
|
|
||||||
|
// Export with explicit module types
|
||||||
export {
|
export {
|
||||||
childProcess,
|
childProcess,
|
||||||
path,
|
path,
|
||||||
@ -14,6 +15,7 @@ import * as smartcli from '@push.rocks/smartcli';
|
|||||||
import * as smartdaemon from '@push.rocks/smartdaemon';
|
import * as smartdaemon from '@push.rocks/smartdaemon';
|
||||||
import * as smartpath from '@push.rocks/smartpath';
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
|
|
||||||
|
// Export with explicit module types
|
||||||
export {
|
export {
|
||||||
npmextra,
|
npmextra,
|
||||||
projectinfo,
|
projectinfo,
|
||||||
@ -26,6 +28,7 @@ export {
|
|||||||
import psTree from 'ps-tree';
|
import psTree from 'ps-tree';
|
||||||
import pidusage from 'pidusage';
|
import pidusage from 'pidusage';
|
||||||
|
|
||||||
|
// Add explicit types for third-party exports
|
||||||
export {
|
export {
|
||||||
psTree,
|
psTree,
|
||||||
pidusage,
|
pidusage,
|
||||||
|
138
ts/utils.errorhandler.ts
Normal file
138
ts/utils.errorhandler.ts
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
/**
|
||||||
|
* Centralized error handling utility for TSPM
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Define error types
|
||||||
|
export enum ErrorType {
|
||||||
|
CONFIG = 'ConfigError',
|
||||||
|
PROCESS = 'ProcessError',
|
||||||
|
RUNTIME = 'RuntimeError',
|
||||||
|
VALIDATION = 'ValidationError',
|
||||||
|
UNKNOWN = 'UnknownError'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Base error class with type and code support
|
||||||
|
export class TspmError extends Error {
|
||||||
|
type: ErrorType;
|
||||||
|
code: string;
|
||||||
|
details?: Record<string, any>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
type: ErrorType = ErrorType.UNKNOWN,
|
||||||
|
code: string = 'ERR_UNKNOWN',
|
||||||
|
details?: Record<string, any>
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
this.name = type;
|
||||||
|
this.type = type;
|
||||||
|
this.code = code;
|
||||||
|
this.details = details;
|
||||||
|
|
||||||
|
// Preserve proper stack trace
|
||||||
|
if (Error.captureStackTrace) {
|
||||||
|
Error.captureStackTrace(this, this.constructor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return `[${this.type}:${this.code}] ${this.message}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specific error classes
|
||||||
|
export class ConfigError extends TspmError {
|
||||||
|
constructor(message: string, code: string = 'ERR_CONFIG', details?: Record<string, any>) {
|
||||||
|
super(message, ErrorType.CONFIG, code, details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ProcessError extends TspmError {
|
||||||
|
constructor(message: string, code: string = 'ERR_PROCESS', details?: Record<string, any>) {
|
||||||
|
super(message, ErrorType.PROCESS, code, details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValidationError extends TspmError {
|
||||||
|
constructor(message: string, code: string = 'ERR_VALIDATION', details?: Record<string, any>) {
|
||||||
|
super(message, ErrorType.VALIDATION, code, details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility for handling any error type
|
||||||
|
export const handleError = (error: Error | unknown): TspmError => {
|
||||||
|
if (error instanceof TspmError) {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof Error) {
|
||||||
|
return new TspmError(error.message, ErrorType.UNKNOWN, 'ERR_UNKNOWN', { originalError: error });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TspmError(String(error), ErrorType.UNKNOWN, 'ERR_UNKNOWN');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Logger with different log levels
|
||||||
|
export enum LogLevel {
|
||||||
|
DEBUG = 0,
|
||||||
|
INFO = 1,
|
||||||
|
WARN = 2,
|
||||||
|
ERROR = 3,
|
||||||
|
NONE = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Logger {
|
||||||
|
private static instance: Logger;
|
||||||
|
private level: LogLevel = LogLevel.INFO;
|
||||||
|
private componentName: string;
|
||||||
|
|
||||||
|
constructor(componentName: string) {
|
||||||
|
this.componentName = componentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getInstance(componentName: string): Logger {
|
||||||
|
if (!Logger.instance) {
|
||||||
|
Logger.instance = new Logger(componentName);
|
||||||
|
}
|
||||||
|
return Logger.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLevel(level: LogLevel): void {
|
||||||
|
this.level = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatMessage(message: string): string {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
return `[${timestamp}] [${this.componentName}] ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug(message: string): void {
|
||||||
|
if (this.level <= LogLevel.DEBUG) {
|
||||||
|
console.log(this.formatMessage(`DEBUG: ${message}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
info(message: string): void {
|
||||||
|
if (this.level <= LogLevel.INFO) {
|
||||||
|
console.log(this.formatMessage(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
warn(message: string): void {
|
||||||
|
if (this.level <= LogLevel.WARN) {
|
||||||
|
console.warn(this.formatMessage(`WARNING: ${message}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
error(error: Error | unknown): void {
|
||||||
|
if (this.level <= LogLevel.ERROR) {
|
||||||
|
const tspmError = handleError(error);
|
||||||
|
console.error(this.formatMessage(`ERROR: ${tspmError.toString()}`));
|
||||||
|
|
||||||
|
// In debug mode, also log stack trace
|
||||||
|
if (this.level === LogLevel.DEBUG && tspmError.stack) {
|
||||||
|
console.error(tspmError.stack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user