feat(core): Introduce ProcessMonitor class and integrate native and external plugins
This commit is contained in:
		@@ -1,5 +1,12 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## 2025-03-01 - 1.1.0 - feat(core)
 | 
			
		||||
Introduce ProcessMonitor class and integrate native and external plugins
 | 
			
		||||
 | 
			
		||||
- Added a new ProcessMonitor class to manage and monitor child processes with memory constraints.
 | 
			
		||||
- Integrated native 'path' and external '@push.rocks/smartpath' packages in a unified plugins file.
 | 
			
		||||
- Adjusted index and related files for improved modular structure.
 | 
			
		||||
 | 
			
		||||
## 2025-02-24 - 1.0.3 - fix(core)
 | 
			
		||||
Corrected description in package.json and readme.md from 'task manager' to 'process manager'.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,9 @@
 | 
			
		||||
    "@push.rocks/tapbundle": "^5.0.15",
 | 
			
		||||
    "@types/node": "^20.8.7"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {},
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@push.rocks/smartpath": "^5.0.18"
 | 
			
		||||
  },
 | 
			
		||||
  "repository": {
 | 
			
		||||
    "type": "git",
 | 
			
		||||
    "url": "https://code.foss.global/git.zone/tspm.git"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										4
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -7,6 +7,10 @@ settings:
 | 
			
		||||
importers:
 | 
			
		||||
 | 
			
		||||
  .:
 | 
			
		||||
    dependencies:
 | 
			
		||||
      '@push.rocks/smartpath':
 | 
			
		||||
        specifier: ^5.0.18
 | 
			
		||||
        version: 5.0.18
 | 
			
		||||
    devDependencies:
 | 
			
		||||
      '@git.zone/tsbuild':
 | 
			
		||||
        specifier: ^2.1.25
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,6 @@
 | 
			
		||||
 */
 | 
			
		||||
export const commitinfo = {
 | 
			
		||||
  name: '@git.zone/tspm',
 | 
			
		||||
  version: '1.0.3',
 | 
			
		||||
  version: '1.1.0',
 | 
			
		||||
  description: 'a no fuzz process manager'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										178
									
								
								ts/classes.processmonitor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								ts/classes.processmonitor.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,178 @@
 | 
			
		||||
import { spawn, ChildProcess } from 'child_process';
 | 
			
		||||
import psTree from 'ps-tree';
 | 
			
		||||
import pidusage from 'pidusage';
 | 
			
		||||
 | 
			
		||||
interface IMonitorConfig {
 | 
			
		||||
  name?: string;                 // Optional name to identify the instance
 | 
			
		||||
  projectDir: string;            // Directory where the command will run
 | 
			
		||||
  command: string;               // Full command to run (e.g., "npm run xyz")
 | 
			
		||||
  args?: string[];               // Optional: arguments for the command
 | 
			
		||||
  memoryLimitBytes: number;      // Maximum allowed memory (in bytes) for the process group
 | 
			
		||||
  monitorIntervalMs?: number;    // Interval (in ms) at which memory is checked (default: 5000)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ProcessMonitor {
 | 
			
		||||
  private child: ChildProcess | null = null;
 | 
			
		||||
  private config: IMonitorConfig;
 | 
			
		||||
  private intervalId: NodeJS.Timeout | null = null;
 | 
			
		||||
  private stopped: boolean = true; // Initially stopped until start() is called
 | 
			
		||||
 | 
			
		||||
  constructor(config: IMonitorConfig) {
 | 
			
		||||
    this.config = config;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public start(): void {
 | 
			
		||||
    // Reset the stopped flag so that new processes can spawn.
 | 
			
		||||
    this.stopped = false;
 | 
			
		||||
    this.log(`Starting process monitor.`);
 | 
			
		||||
    this.spawnChild();
 | 
			
		||||
 | 
			
		||||
    // Set the monitoring interval.
 | 
			
		||||
    const interval = this.config.monitorIntervalMs || 5000;
 | 
			
		||||
    this.intervalId = setInterval(() => {
 | 
			
		||||
      if (this.child && this.child.pid) {
 | 
			
		||||
        this.monitorProcessGroup(this.child.pid, this.config.memoryLimitBytes);
 | 
			
		||||
      }
 | 
			
		||||
    }, interval);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private spawnChild(): void {
 | 
			
		||||
    // Don't spawn if the monitor has been stopped.
 | 
			
		||||
    if (this.stopped) return;
 | 
			
		||||
 | 
			
		||||
    if (this.config.args && this.config.args.length > 0) {
 | 
			
		||||
      this.log(
 | 
			
		||||
        `Spawning command "${this.config.command}" with args [${this.config.args.join(
 | 
			
		||||
          ', '
 | 
			
		||||
        )}] in directory: ${this.config.projectDir}`
 | 
			
		||||
      );
 | 
			
		||||
      this.child = spawn(this.config.command, this.config.args, {
 | 
			
		||||
        cwd: this.config.projectDir,
 | 
			
		||||
        detached: true,
 | 
			
		||||
        stdio: 'inherit',
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      this.log(
 | 
			
		||||
        `Spawning command "${this.config.command}" in directory: ${this.config.projectDir}`
 | 
			
		||||
      );
 | 
			
		||||
      // Use shell mode to allow a full command string.
 | 
			
		||||
      this.child = spawn(this.config.command, {
 | 
			
		||||
        cwd: this.config.projectDir,
 | 
			
		||||
        detached: true,
 | 
			
		||||
        stdio: 'inherit',
 | 
			
		||||
        shell: true,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.log(`Spawned process with PID ${this.child.pid}`);
 | 
			
		||||
 | 
			
		||||
    // When the child process exits, restart it if the monitor isn't stopped.
 | 
			
		||||
    this.child.on('exit', (code, signal) => {
 | 
			
		||||
      this.log(`Child process exited with code ${code}, signal ${signal}.`);
 | 
			
		||||
      if (!this.stopped) {
 | 
			
		||||
        this.log('Restarting process...');
 | 
			
		||||
        this.spawnChild();
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Monitor the process group’s memory usage. If the total memory exceeds the limit,
 | 
			
		||||
   * kill the process group so that the 'exit' handler can restart it.
 | 
			
		||||
   */
 | 
			
		||||
  private async monitorProcessGroup(pid: number, memoryLimit: number): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const memoryUsage = await this.getProcessGroupMemory(pid);
 | 
			
		||||
      this.log(
 | 
			
		||||
        `Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes(
 | 
			
		||||
          memoryUsage
 | 
			
		||||
        )} (${memoryUsage} bytes)`
 | 
			
		||||
      );
 | 
			
		||||
      if (memoryUsage > memoryLimit) {
 | 
			
		||||
        this.log(
 | 
			
		||||
          `Memory usage ${this.humanReadableBytes(
 | 
			
		||||
            memoryUsage
 | 
			
		||||
          )} exceeds limit of ${this.humanReadableBytes(memoryLimit)}. Restarting process.`
 | 
			
		||||
        );
 | 
			
		||||
        // Kill the entire process group by sending a signal to -PID.
 | 
			
		||||
        process.kill(-pid, 'SIGKILL');
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.log('Error monitoring process group: ' + error);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the total memory usage (in bytes) for the process group (the main process and its children).
 | 
			
		||||
   */
 | 
			
		||||
  private getProcessGroupMemory(pid: number): Promise<number> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      psTree(pid, (err, children) => {
 | 
			
		||||
        if (err) return reject(err);
 | 
			
		||||
        // Include the main process and its children.
 | 
			
		||||
        const pids: number[] = [pid, ...children.map(child => Number(child.PID))];
 | 
			
		||||
        pidusage(pids, (err, stats) => {
 | 
			
		||||
          if (err) return reject(err);
 | 
			
		||||
          let totalMemory = 0;
 | 
			
		||||
          for (const key in stats) {
 | 
			
		||||
            totalMemory += stats[key].memory;
 | 
			
		||||
          }
 | 
			
		||||
          resolve(totalMemory);
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Convert a number of bytes into a human-readable string (e.g. "1.23 MB").
 | 
			
		||||
   */
 | 
			
		||||
  private humanReadableBytes(bytes: number, decimals: number = 2): string {
 | 
			
		||||
    if (bytes === 0) return '0 Bytes';
 | 
			
		||||
    const k = 1024;
 | 
			
		||||
    const dm = decimals < 0 ? 0 : decimals;
 | 
			
		||||
    const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
 | 
			
		||||
    const i = Math.floor(Math.log(bytes) / Math.log(k));
 | 
			
		||||
    return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Stop the monitor and prevent any further respawns.
 | 
			
		||||
   */
 | 
			
		||||
  public stop(): void {
 | 
			
		||||
    this.log('Stopping process monitor.');
 | 
			
		||||
    this.stopped = true;
 | 
			
		||||
    if (this.intervalId) {
 | 
			
		||||
      clearInterval(this.intervalId);
 | 
			
		||||
    }
 | 
			
		||||
    if (this.child && this.child.pid) {
 | 
			
		||||
      process.kill(-this.child.pid, 'SIGKILL');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Helper method for logging messages with the instance name.
 | 
			
		||||
   */
 | 
			
		||||
  private log(message: string): void {
 | 
			
		||||
    const prefix = this.config.name ? `[${this.config.name}] ` : '';
 | 
			
		||||
    console.log(prefix + message);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Example usage:
 | 
			
		||||
const config: IMonitorConfig = {
 | 
			
		||||
  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 ProcessMonitor(config);
 | 
			
		||||
monitor.start();
 | 
			
		||||
 | 
			
		||||
// Ensure that on process exit (e.g. Ctrl+C) we clean up the child process and prevent respawns.
 | 
			
		||||
process.on('SIGINT', () => {
 | 
			
		||||
  monitor.log('Received SIGINT, stopping monitor...');
 | 
			
		||||
  monitor.stop();
 | 
			
		||||
  process.exit();
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										6
									
								
								ts/classes.tspm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								ts/classes.tspm.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
import * as plugins from './plugins.js';
 | 
			
		||||
import * as paths from './paths.js';
 | 
			
		||||
 | 
			
		||||
export class Tspm {
 | 
			
		||||
   
 | 
			
		||||
}
 | 
			
		||||
@@ -1,3 +1,3 @@
 | 
			
		||||
import * as plugins from './tspm.plugins.js';
 | 
			
		||||
import * as plugins from './plugins.js';
 | 
			
		||||
 | 
			
		||||
export let demoExport = 'Hi there! :) This is an exported string';
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								ts/plugins.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								ts/plugins.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
// native scope
 | 
			
		||||
import * as path from 'node:path';
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  path,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// @push.rocks scope
 | 
			
		||||
import * as smartpath from '@push.rocks/smartpath';
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  smartpath,
 | 
			
		||||
}
 | 
			
		||||
@@ -1,2 +0,0 @@
 | 
			
		||||
const removeme = {};
 | 
			
		||||
export { removeme };
 | 
			
		||||
		Reference in New Issue
	
	Block a user