@comake/skl-js-engine
Version:
Standard Knowledge Language Javascript Engine
219 lines • 7.79 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProcessManager = void 0;
const node_events_1 = require("node:events");
const logger_1 = require("../../../logger");
const constants_1 = require("../../constants");
const denoUtils_1 = require("../../denoUtils");
const errors_1 = require("../../errors");
const PermissionBuilder_1 = require("../../PermissionBuilder");
const PollingUtils_1 = require("../utils/PollingUtils");
/**
* Manages the lifecycle of child processes for JavaScript execution
*/
class ProcessManager extends node_events_1.EventEmitter {
constructor() {
super();
this.logger = logger_1.Logger.getInstance();
}
/**
* Set a name for this process manager (used in logging)
*/
setName(name) {
this.name = name;
this.logger.setMetadata({ name, component: 'ProcessManager' });
}
/**
* Get the current child process
*/
getProcess() {
return this.childProcess;
}
/**
* Check if the process is running
*/
isRunning() {
return this.childProcess !== undefined && !this.childProcess.killed;
}
/**
* Spawn a new child process
*/
async spawn(config) {
if (this.childProcess) {
throw new Error('Process already spawned. Call terminate() first.');
}
try {
const normalizedOptions = this.normalizeExecutionOptions(config.executionOptions);
const permissionConfig = this.extractPermissionConfig(normalizedOptions);
// Validate permissions before proceeding
(0, PermissionBuilder_1.validatePermissionConfig)(permissionConfig);
const permissions = (0, PermissionBuilder_1.buildDenoPermissions)(permissionConfig);
// Build command arguments
const commandArgs = [...constants_1.EXECUTION_CONSTANTS.denoFlags, ...permissions, config.scriptPath];
this.logger.debug('Spawning process with args:', commandArgs);
// Spawn the Deno process
this.childProcess = (0, denoUtils_1.spawnDenoProcess)(commandArgs);
this.setupProcessHandlers();
// Wait for process to be ready
await this.waitForReady(config.startupTimeout ?? 60 * 60 * 1000);
this.emit('ready');
}
catch (error) {
this.childProcess = undefined;
throw error;
}
}
/**
* Terminate the child process
*/
async terminate(timeout = 3000) {
if (!this.childProcess || this.childProcess.killed) {
return;
}
this.logger.log('Terminating process');
// First try graceful termination
this.childProcess.kill(constants_1.EXECUTION_CONSTANTS.processSignals.term);
// Wait for process to exit gracefully
await new Promise(resolve => {
const timeoutId = setTimeout(() => {
if (this.childProcess && !this.childProcess.killed) {
this.logger.log('Force killing process after timeout');
this.childProcess.kill('SIGKILL');
}
resolve();
}, timeout);
this.childProcess.on('exit', () => {
clearTimeout(timeoutId);
resolve();
});
});
this.childProcess = undefined;
}
/**
* Send data to the process stdin
*/
write(data) {
if (!this.childProcess?.stdin) {
throw new Error('Process stdin not available');
}
return this.childProcess.stdin.write(data);
}
/**
* Get process stdout stream
*/
getStdout() {
return this.childProcess?.stdout ?? undefined;
}
/**
* Get process stdin stream
*/
getStdin() {
return this.childProcess?.stdin ?? undefined;
}
/**
* Register event listeners
*/
on(event, handler) {
return super.on(event, handler);
}
/**
* Remove event listeners
*/
off(event, handler) {
return super.off(event, handler);
}
/**
* Set up process event handlers
*/
setupProcessHandlers() {
if (!this.childProcess) {
return;
}
// Handle stdout data
this.childProcess.stdout?.on('data', (data) => {
this.emit('stdout', data);
});
// Handle stderr data
this.childProcess.stderr?.on('data', (data) => {
this.emit('stderr', data);
});
// Handle process exit
this.childProcess.on('exit', (code, signal) => {
this.logger.log(`Process exited with code: ${code}, signal: ${signal}`);
this.emit('exit', code, signal);
});
// Handle process errors
this.childProcess.on('error', error => {
this.logger.error('Process error:', error);
this.emit('error', new errors_1.ProcessSpawnError(error));
});
}
/**
* Wait for the process to be ready
*/
async waitForReady(timeout) {
await (0, PollingUtils_1.pollUntilTrue)(() => {
// Check if process is running and not killed
if (!this.childProcess) {
// Process not spawned yet
return false;
}
if (this.childProcess.killed) {
throw new Error('Process was killed during startup');
}
// Process is running
return !this.childProcess.killed;
}, {
timeout,
// Check every 100ms or 1/10th of timeout
interval: Math.min(100, timeout / 10),
// Let process start before first check
initialDelay: 50
}, 'Process startup timed out');
// Additional wait for process to be fully ready (like the original logic)
const stabilityWait = Math.min(500, timeout / 4);
await new Promise(resolve => setTimeout(resolve, stabilityWait));
// Final check that process is still running after stability wait
if (!this.childProcess || this.childProcess.killed) {
throw new Error('Process died during startup after initial success');
}
}
/**
* Normalize execution options with defaults
*/
normalizeExecutionOptions(options) {
const DEFAULT_EXECUTION_OPTIONS = {
timeout: 30000,
functionName: 'main',
allowNetwork: false,
allowedDomains: [],
allowEnv: false,
allowRead: false,
debugMode: false,
retries: 0
};
return {
timeout: options?.timeout ?? DEFAULT_EXECUTION_OPTIONS.timeout,
functionName: options?.functionName ?? DEFAULT_EXECUTION_OPTIONS.functionName,
allowNetwork: options?.allowNetwork ?? DEFAULT_EXECUTION_OPTIONS.allowNetwork,
allowedDomains: options?.allowedDomains ?? [...DEFAULT_EXECUTION_OPTIONS.allowedDomains],
allowEnv: options?.allowEnv ?? DEFAULT_EXECUTION_OPTIONS.allowEnv,
allowRead: options?.allowRead ?? DEFAULT_EXECUTION_OPTIONS.allowRead,
debugMode: options?.debugMode ?? DEFAULT_EXECUTION_OPTIONS.debugMode,
retries: options?.retries ?? DEFAULT_EXECUTION_OPTIONS.retries
};
}
/**
* Extract permission configuration from execution options
*/
extractPermissionConfig(options) {
return {
allowNetwork: options.allowNetwork,
allowedDomains: options.allowedDomains,
allowEnv: options.allowEnv,
allowRead: options.allowRead
};
}
}
exports.ProcessManager = ProcessManager;
//# sourceMappingURL=ProcessManager.js.map