419 lines
12 KiB
TypeScript
419 lines
12 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
import * as paths from './paths.js';
|
|
import { ProcessMonitor, type IMonitorConfig } from './classes.processmonitor.js';
|
|
import { TspmConfig } from './classes.config.js';
|
|
import {
|
|
Logger,
|
|
ProcessError,
|
|
ConfigError,
|
|
ValidationError,
|
|
handleError
|
|
} from './utils.errorhandler.js';
|
|
|
|
export interface IProcessConfig extends IMonitorConfig {
|
|
id: string; // Unique identifier for the process
|
|
autorestart: boolean; // Whether to restart the process automatically on crash
|
|
watch?: boolean; // Whether to watch for file changes and restart
|
|
watchPaths?: string[]; // Paths to watch for changes
|
|
}
|
|
|
|
export interface IProcessInfo {
|
|
id: string;
|
|
pid?: number;
|
|
status: 'online' | 'stopped' | 'errored';
|
|
memory: number;
|
|
cpu?: number;
|
|
uptime?: number;
|
|
restarts: number;
|
|
}
|
|
|
|
export interface IProcessLog {
|
|
timestamp: Date;
|
|
type: 'stdout' | 'stderr' | 'system';
|
|
message: string;
|
|
}
|
|
|
|
export class Tspm {
|
|
private processes: Map<string, ProcessMonitor> = new Map();
|
|
private processConfigs: Map<string, IProcessConfig> = new Map();
|
|
private processInfo: Map<string, IProcessInfo> = new Map();
|
|
private config: TspmConfig;
|
|
private configStorageKey = 'processes';
|
|
private logger: Logger;
|
|
|
|
constructor() {
|
|
this.logger = new Logger('Tspm');
|
|
this.config = new TspmConfig();
|
|
this.loadProcessConfigs();
|
|
}
|
|
|
|
/**
|
|
* Start a new process with the given configuration
|
|
*/
|
|
public async start(config: IProcessConfig): Promise<void> {
|
|
this.logger.info(`Starting process with id '${config.id}'`);
|
|
|
|
// Validate config
|
|
if (!config.id || !config.command || !config.projectDir) {
|
|
throw new ValidationError(
|
|
'Invalid process configuration: missing required fields',
|
|
'ERR_INVALID_CONFIG',
|
|
{ config }
|
|
);
|
|
}
|
|
|
|
// Check if process with this id already exists
|
|
if (this.processes.has(config.id)) {
|
|
throw new ValidationError(
|
|
`Process with id '${config.id}' already exists`,
|
|
'ERR_DUPLICATE_PROCESS'
|
|
);
|
|
}
|
|
|
|
try {
|
|
// Create and store process config
|
|
this.processConfigs.set(config.id, config);
|
|
|
|
// Initialize process info
|
|
this.processInfo.set(config.id, {
|
|
id: config.id,
|
|
status: 'stopped',
|
|
memory: 0,
|
|
restarts: 0
|
|
});
|
|
|
|
// Create and start process monitor
|
|
const monitor = new ProcessMonitor({
|
|
name: config.name || config.id,
|
|
projectDir: config.projectDir,
|
|
command: config.command,
|
|
args: config.args,
|
|
memoryLimitBytes: config.memoryLimitBytes,
|
|
monitorIntervalMs: config.monitorIntervalMs,
|
|
env: config.env,
|
|
logBufferSize: config.logBufferSize
|
|
});
|
|
|
|
this.processes.set(config.id, monitor);
|
|
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
|
|
*/
|
|
public async stop(id: string): Promise<void> {
|
|
this.logger.info(`Stopping process with id '${id}'`);
|
|
|
|
const monitor = this.processes.get(id);
|
|
if (!monitor) {
|
|
const error = new ValidationError(
|
|
`Process with id '${id}' not found`,
|
|
'ERR_PROCESS_NOT_FOUND'
|
|
);
|
|
this.logger.error(error);
|
|
throw error;
|
|
}
|
|
|
|
try {
|
|
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
|
|
// This allows it to be restarted later
|
|
}
|
|
|
|
/**
|
|
* Restart a process by id
|
|
*/
|
|
public async restart(id: string): Promise<void> {
|
|
this.logger.info(`Restarting process with id '${id}'`);
|
|
|
|
const monitor = this.processes.get(id);
|
|
const config = this.processConfigs.get(id);
|
|
|
|
if (!monitor || !config) {
|
|
const error = new ValidationError(
|
|
`Process with id '${id}' not found`,
|
|
'ERR_PROCESS_NOT_FOUND'
|
|
);
|
|
this.logger.error(error);
|
|
throw error;
|
|
}
|
|
|
|
try {
|
|
// Stop and then start the process
|
|
monitor.stop();
|
|
|
|
// Create a new monitor instance
|
|
const newMonitor = new ProcessMonitor({
|
|
name: config.name || config.id,
|
|
projectDir: config.projectDir,
|
|
command: config.command,
|
|
args: config.args,
|
|
memoryLimitBytes: config.memoryLimitBytes,
|
|
monitorIntervalMs: config.monitorIntervalMs,
|
|
env: config.env,
|
|
logBufferSize: config.logBufferSize
|
|
});
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete a process by id
|
|
*/
|
|
public async delete(id: string): Promise<void> {
|
|
this.logger.info(`Deleting process with id '${id}'`);
|
|
|
|
// Check if process exists
|
|
if (!this.processConfigs.has(id)) {
|
|
const error = new ValidationError(
|
|
`Process with id '${id}' not found`,
|
|
'ERR_PROCESS_NOT_FOUND'
|
|
);
|
|
this.logger.error(error);
|
|
throw error;
|
|
}
|
|
|
|
// Stop the process if it's running
|
|
try {
|
|
if (this.processes.has(id)) {
|
|
await this.stop(id);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a list of all process infos
|
|
*/
|
|
public list(): IProcessInfo[] {
|
|
return Array.from(this.processInfo.values());
|
|
}
|
|
|
|
/**
|
|
* Get detailed info for a specific process
|
|
*/
|
|
public describe(id: string): { config: IProcessConfig; info: IProcessInfo } | null {
|
|
const config = this.processConfigs.get(id);
|
|
const info = this.processInfo.get(id);
|
|
|
|
if (!config || !info) {
|
|
return null;
|
|
}
|
|
|
|
return { config, info };
|
|
}
|
|
|
|
/**
|
|
* Get process logs
|
|
*/
|
|
public getLogs(id: string, limit?: number): IProcessLog[] {
|
|
const monitor = this.processes.get(id);
|
|
if (!monitor) {
|
|
return [];
|
|
}
|
|
|
|
return monitor.getLogs(limit);
|
|
}
|
|
|
|
/**
|
|
* Start all saved processes
|
|
*/
|
|
public async startAll(): Promise<void> {
|
|
for (const [id, config] of this.processConfigs.entries()) {
|
|
if (!this.processes.has(id)) {
|
|
await this.start(config);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Stop all running processes
|
|
*/
|
|
public async stopAll(): Promise<void> {
|
|
for (const id of this.processes.keys()) {
|
|
await this.stop(id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Restart all processes
|
|
*/
|
|
public async restartAll(): Promise<void> {
|
|
for (const id of this.processes.keys()) {
|
|
await this.restart(id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update the info for a process
|
|
*/
|
|
private updateProcessInfo(id: string, update: Partial<IProcessInfo>): void {
|
|
const info = this.processInfo.get(id);
|
|
if (info) {
|
|
this.processInfo.set(id, { ...info, ...update });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save all process configurations to config storage
|
|
*/
|
|
private async saveProcessConfigs(): Promise<void> {
|
|
this.logger.debug('Saving process configurations to storage');
|
|
|
|
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
|
|
*/
|
|
private async loadProcessConfigs(): Promise<void> {
|
|
this.logger.debug('Loading process configurations from storage');
|
|
|
|
try {
|
|
const configsJson = await this.config.readKey(this.configStorageKey);
|
|
if (configsJson) {
|
|
try {
|
|
const configs = JSON.parse(configsJson) as IProcessConfig[];
|
|
this.logger.debug(`Loaded ${configs.length} process configurations`);
|
|
|
|
for (const config of configs) {
|
|
// Validate config
|
|
if (!config.id || !config.command || !config.projectDir) {
|
|
this.logger.warn(`Skipping invalid process config for id '${config.id || 'unknown'}'`);
|
|
continue;
|
|
}
|
|
|
|
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: 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
|
|
this.logger.info('No saved process configurations found or error reading them');
|
|
}
|
|
}
|
|
} |