@nodedaemon/core
Version:
Production-ready Node.js process manager with zero external dependencies
1,339 lines (1,329 loc) • 181 kB
JavaScript
#!/usr/bin/env node
#!/usr/bin/env node
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeDaemonCLI = void 0;
const child_process_1 = require("child_process");
const path_1 = require("path");
const NodeDaemonCore_1 = (function() {
const module = { exports: {} };
const exports = module.exports;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NodeDaemonCore = void 0;
const net_1 = require("net");
const fs_1 = require("fs");
const util_1 = require("util");
const events_1 = require("events");
const ProcessOrchestrator_1 = (function() {
const module = { exports: {} };
const exports = module.exports;
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProcessOrchestrator = void 0;
const child_process_1 = require("child_process");
const cluster_1 = __importDefault(require("cluster"));
const os_1 = require("os");
const events_1 = require("events");
const helpers_1 = (function() {
const module = { exports: {} };
const exports = module.exports;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateId = generateId;
exports.ensureDir = ensureDir;
exports.ensureFileDir = ensureFileDir;
exports.isFile = isFile;
exports.isDirectory = isDirectory;
exports.parseMemoryString = parseMemoryString;
exports.formatMemory = formatMemory;
exports.formatUptime = formatUptime;
exports.calculateExponentialBackoff = calculateExponentialBackoff;
exports.debounce = debounce;
exports.throttle = throttle;
exports.sanitizeProcessName = sanitizeProcessName;
exports.validateProcessConfig = validateProcessConfig;
const crypto_1 = require("crypto");
const fs_1 = require("fs");
const path_1 = require("path");
function generateId() {
return (0, crypto_1.randomUUID)();
}
function ensureDir(dirPath) {
if (!(0, fs_1.existsSync)(dirPath)) {
(0, fs_1.mkdirSync)(dirPath, { recursive: true });
}
}
function ensureFileDir(filePath) {
ensureDir((0, path_1.dirname)(filePath));
}
function isFile(path) {
try {
return (0, fs_1.existsSync)(path) && (0, fs_1.statSync)(path).isFile();
}
catch {
return false;
}
}
function isDirectory(path) {
try {
return (0, fs_1.existsSync)(path) && (0, fs_1.statSync)(path).isDirectory();
}
catch {
return false;
}
}
function parseMemoryString(memory) {
const units = {
'B': 1,
'KB': 1024,
'MB': 1024 * 1024,
'GB': 1024 * 1024 * 1024
};
const match = memory.match(/^(\d+(?:\.\d+)?)\s*(B|KB|MB|GB)$/i);
if (!match || !match[1] || !match[2]) {
throw new Error(`Invalid memory format: ${memory}`);
}
const [, value, unit] = match;
return Math.floor(parseFloat(value) * units[unit.toUpperCase()]);
}
function formatMemory(bytes) {
if (bytes === 0)
return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
}
function formatUptime(ms) {
const seconds = Math.floor(ms / 1000) % 60;
const minutes = Math.floor(ms / (1000 * 60)) % 60;
const hours = Math.floor(ms / (1000 * 60 * 60)) % 24;
const days = Math.floor(ms / (1000 * 60 * 60 * 24));
if (days > 0) {
return `${days}d ${hours}h ${minutes}m ${seconds}s`;
}
if (hours > 0) {
return `${hours}h ${minutes}m ${seconds}s`;
}
if (minutes > 0) {
return `${minutes}m ${seconds}s`;
}
return `${seconds}s`;
}
function calculateExponentialBackoff(restartCount, baseDelay, maxDelay) {
const delay = baseDelay * Math.pow(2, restartCount);
return Math.min(delay, maxDelay);
}
function debounce(func, wait) {
let timeout = null;
return (...args) => {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
timeout = null;
func.apply(null, args);
}, wait);
};
}
function throttle(func, limit) {
let inThrottle = false;
return (...args) => {
if (!inThrottle) {
func.apply(null, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
function sanitizeProcessName(name) {
return name.replace(/[^a-zA-Z0-9-_]/g, '_');
}
function validateProcessConfig(config) {
if (!config || typeof config !== 'object') {
throw new Error('Process config must be an object');
}
if (!config.script || typeof config.script !== 'string') {
throw new Error('Process config must have a script property');
}
if (!isFile(config.script)) {
throw new Error(`Script file does not exist: ${config.script}`);
}
if (config.instances !== undefined) {
if (config.instances !== 'max' &&
(!Number.isInteger(config.instances) || config.instances < 1)) {
throw new Error('instances must be a positive integer or "max"');
}
}
if (config.maxRestarts !== undefined) {
if (!Number.isInteger(config.maxRestarts) || config.maxRestarts < 0) {
throw new Error('maxRestarts must be a non-negative integer');
}
}
}
return module.exports;
})();
const env_1 = (function() {
const module = { exports: {} };
const exports = module.exports;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.loadEnvFile = loadEnvFile;
exports.findEnvFile = findEnvFile;
exports.mergeEnvConfigs = mergeEnvConfigs;
const fs_1 = require("fs");
const path_1 = require("path");
function loadEnvFile(filePath) {
const envConfig = {};
if (!(0, fs_1.existsSync)(filePath)) {
return envConfig;
}
try {
const content = (0, fs_1.readFileSync)(filePath, 'utf8');
const lines = content.split('\n');
for (const line of lines) {
// Skip empty lines and comments
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith('#')) {
continue;
}
// Parse KEY=VALUE format
const separatorIndex = trimmedLine.indexOf('=');
if (separatorIndex > 0) {
const key = trimmedLine.substring(0, separatorIndex).trim();
const value = trimmedLine.substring(separatorIndex + 1).trim();
// Remove surrounding quotes if present
const unquotedValue = value.replace(/^["']|["']$/g, '');
envConfig[key] = unquotedValue;
}
}
}
catch (error) {
// Silently fail if env file cannot be read
}
return envConfig;
}
function findEnvFile(scriptPath, envFile) {
const scriptDir = (0, path_1.dirname)(scriptPath);
// If specific env file is provided
if (envFile) {
const envPath = (0, path_1.join)(scriptDir, envFile);
return (0, fs_1.existsSync)(envPath) ? envPath : null;
}
// Look for common env file names in order of priority
const envFiles = [
'.env.local',
'.env.development',
'.env.production',
'.env'
];
// Check current directory first
for (const file of envFiles) {
const envPath = (0, path_1.join)(process.cwd(), file);
if ((0, fs_1.existsSync)(envPath)) {
return envPath;
}
}
// Then check script directory
for (const file of envFiles) {
const envPath = (0, path_1.join)(scriptDir, file);
if ((0, fs_1.existsSync)(envPath)) {
return envPath;
}
}
return null;
}
function mergeEnvConfigs(...configs) {
const merged = {};
for (const config of configs) {
Object.assign(merged, config);
}
return merged;
}
return module.exports;
})();
const constants_1 = (function() {
const module = { exports: {} };
const exports = module.exports;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.WEB_UI_STATIC_DIR = exports.WEB_UI_DIR = exports.DEFAULT_WEB_UI_CONFIG = exports.SIGNALS = exports.FILE_WATCH_IGNORE = exports.FILE_WATCH_DEBOUNCE = exports.FORCE_KILL_TIMEOUT = exports.GRACEFUL_SHUTDOWN_TIMEOUT = exports.CPU_THRESHOLD = exports.MEMORY_THRESHOLD = exports.HEALTH_CHECK_INTERVAL = exports.LOG_BUFFER_SIZE = exports.MAX_LOG_FILES = exports.MAX_LOG_SIZE = exports.LOG_LEVELS = exports.PROCESS_EVENTS = exports.RESTART_STRATEGIES = exports.DEFAULT_CONFIG = exports.IPC_SOCKET_PATH = exports.DAEMON_LOG = exports.LOG_DIR = exports.STATE_FILE = exports.NODEDAEMON_DIR = void 0;
const path_1 = require("path");
const os_1 = require("os");
exports.NODEDAEMON_DIR = (0, path_1.join)((0, os_1.homedir)(), '.nodedaemon');
exports.STATE_FILE = (0, path_1.join)(exports.NODEDAEMON_DIR, 'state.json');
exports.LOG_DIR = (0, path_1.join)(exports.NODEDAEMON_DIR, 'logs');
exports.DAEMON_LOG = (0, path_1.join)(exports.LOG_DIR, 'daemon.log');
exports.IPC_SOCKET_PATH = process.platform === 'win32'
? '\\\\.\\pipe\\nodedaemon'
: (0, path_1.join)(exports.NODEDAEMON_DIR, 'daemon.sock');
exports.DEFAULT_CONFIG = {
instances: 1,
maxRestarts: 10,
restartDelay: 1000,
maxRestartDelay: 30000,
minUptime: 10000, // 10 seconds - minimum uptime to reset restart counter
autoRestartOnCrash: true,
autoRestartOnHighMemory: false,
autoRestartOnHighCpu: false,
memoryThreshold: '512MB',
cpuThreshold: 80
};
exports.RESTART_STRATEGIES = {
EXPONENTIAL_BACKOFF: 'exponential',
FIXED_DELAY: 'fixed',
LINEAR_BACKOFF: 'linear'
};
exports.PROCESS_EVENTS = {
START: 'start',
STOP: 'stop',
RESTART: 'restart',
CRASH: 'crash',
EXIT: 'exit'
};
exports.LOG_LEVELS = {
DEBUG: 'debug',
INFO: 'info',
WARN: 'warn',
ERROR: 'error'
};
exports.MAX_LOG_SIZE = 10 * 1024 * 1024; // 10MB
exports.MAX_LOG_FILES = 5;
exports.LOG_BUFFER_SIZE = 1000;
exports.HEALTH_CHECK_INTERVAL = 30000; // 30 seconds
exports.MEMORY_THRESHOLD = 512 * 1024 * 1024; // 512MB
exports.CPU_THRESHOLD = 80; // 80%
exports.GRACEFUL_SHUTDOWN_TIMEOUT = 30000; // 30 seconds
exports.FORCE_KILL_TIMEOUT = 5000; // 5 seconds
exports.FILE_WATCH_DEBOUNCE = 100; // 100ms
exports.FILE_WATCH_IGNORE = [
'node_modules/**',
'.git/**',
'*.log',
'*.tmp',
'.DS_Store',
'Thumbs.db'
];
exports.SIGNALS = {
SIGTERM: 'SIGTERM',
SIGINT: 'SIGINT',
SIGKILL: 'SIGKILL',
SIGHUP: 'SIGHUP'
};
exports.DEFAULT_WEB_UI_CONFIG = {
enabled: false,
port: 8080,
host: '127.0.0.1',
auth: null
};
exports.WEB_UI_DIR = (0, path_1.join)(exports.NODEDAEMON_DIR, 'web');
exports.WEB_UI_STATIC_DIR = (0, path_1.join)(exports.WEB_UI_DIR, 'static');
return module.exports;
})();
class ProcessOrchestrator extends events_1.EventEmitter {
processes = new Map();
childProcesses = new Map();
restartTimers = new Map();
logger;
isShuttingDown = false;
constructor(logger) {
super();
this.logger = logger;
this.setupClusterEventHandlers();
this.setupProcessEventHandlers();
}
setupClusterEventHandlers() {
if (cluster_1.default.isPrimary) {
cluster_1.default.on('exit', (worker, code, signal) => {
this.handleWorkerExit(worker, code, signal);
});
cluster_1.default.on('disconnect', (worker) => {
this.logger.warn(`Cluster worker ${worker.id} disconnected`, { workerId: worker.id });
});
}
}
setupProcessEventHandlers() {
process.on('SIGTERM', () => this.gracefulShutdown());
process.on('SIGINT', () => this.gracefulShutdown());
process.on('SIGHUP', () => this.reloadAllProcesses());
}
async startProcess(config) {
if (this.isShuttingDown) {
throw new Error('Cannot start process during shutdown');
}
(0, helpers_1.validateProcessConfig)(config);
const processId = (0, helpers_1.generateId)();
const processName = config.name || (0, helpers_1.sanitizeProcessName)(config.script);
const instanceCount = this.resolveInstanceCount(config.instances);
const strategy = this.determineStrategy(config);
// Load environment from .env files
let envConfig = {};
const envFilePath = (0, env_1.findEnvFile)(config.script, config.envFile);
if (envFilePath) {
envConfig = (0, env_1.loadEnvFile)(envFilePath);
this.logger.info(`Loaded environment from ${envFilePath}`, { processName });
}
// Merge with provided env vars (provided vars take precedence)
const finalEnv = (0, env_1.mergeEnvConfigs)(envConfig, config.env || {});
const processInfo = {
id: processId,
name: processName,
script: config.script,
status: 'starting',
restarts: 0,
instances: [],
config: { ...constants_1.DEFAULT_CONFIG, ...config, env: finalEnv },
createdAt: Date.now(),
updatedAt: Date.now()
};
this.processes.set(processId, processInfo);
this.logger.info(`Starting process: ${processName}`, { processId, strategy, instances: instanceCount });
try {
if (strategy === 'cluster' && instanceCount > 1) {
await this.startClusterProcess(processInfo, instanceCount);
}
else {
await this.startSingleProcess(processInfo, strategy);
}
processInfo.status = 'running';
processInfo.updatedAt = Date.now();
this.emit('processStarted', processInfo);
return processId;
}
catch (error) {
processInfo.status = 'errored';
processInfo.updatedAt = Date.now();
this.logger.error(`Failed to start process ${processName}`, { error: error.message, processId });
throw error;
}
}
resolveInstanceCount(instances) {
if (instances === 'max') {
return (0, os_1.cpus)().length;
}
return (typeof instances === 'number') ? instances : constants_1.DEFAULT_CONFIG.instances;
}
determineStrategy(config) {
if (config.instances && (config.instances === 'max' || config.instances > 1)) {
return 'cluster';
}
if (config.script.endsWith('.js') || config.script.endsWith('.mjs')) {
return 'fork';
}
return 'spawn';
}
async startClusterProcess(processInfo, instanceCount) {
if (!cluster_1.default.isPrimary) {
throw new Error('Cluster processes can only be started from primary process');
}
const promises = [];
for (let i = 0; i < instanceCount; i++) {
promises.push(this.startClusterInstance(processInfo, i));
}
await Promise.all(promises);
}
startClusterInstance(processInfo, instanceIndex) {
return new Promise((resolve, reject) => {
const instanceId = (0, helpers_1.generateId)();
const instance = {
id: instanceId,
status: 'starting',
restarts: 0
};
processInfo.instances.push(instance);
cluster_1.default.setupPrimary({
exec: processInfo.script,
args: processInfo.config.args || [],
cwd: processInfo.config.cwd
});
const worker = cluster_1.default.fork({ ...process.env, ...processInfo.config.env });
this.childProcesses.set(instanceId, worker);
worker.on('online', () => {
instance.pid = worker.process.pid;
instance.status = 'running';
instance.uptime = Date.now();
this.logger.info(`Cluster instance ${instanceIndex} started`, {
processId: processInfo.id,
instanceId,
pid: worker.process.pid
});
resolve();
});
worker.on('exit', (code, signal) => {
this.handleInstanceExit(processInfo, instance, code, signal);
});
worker.on('error', (error) => {
this.logger.error(`Cluster instance error`, {
processId: processInfo.id,
instanceId,
error: error.message
});
reject(error);
});
setTimeout(() => {
if (instance.status === 'starting') {
reject(new Error(`Cluster instance ${instanceIndex} failed to start within timeout`));
}
}, 30000);
});
}
async startSingleProcess(processInfo, strategy) {
const instanceId = (0, helpers_1.generateId)();
const instance = {
id: instanceId,
status: 'starting',
restarts: 0
};
processInfo.instances.push(instance);
let childProcess;
if (strategy === 'fork') {
childProcess = (0, child_process_1.fork)(processInfo.script, processInfo.config.args || [], {
cwd: processInfo.config.cwd,
env: { ...process.env, ...processInfo.config.env },
silent: false
});
}
else {
const interpreter = processInfo.config.interpreter || 'node';
childProcess = (0, child_process_1.spawn)(interpreter, [processInfo.script, ...(processInfo.config.args || [])], {
cwd: processInfo.config.cwd,
env: { ...process.env, ...processInfo.config.env },
stdio: ['inherit', 'inherit', 'inherit']
});
}
this.childProcesses.set(instanceId, childProcess);
return new Promise((resolve, reject) => {
childProcess.on('spawn', () => {
instance.pid = childProcess.pid;
instance.status = 'running';
instance.uptime = Date.now();
this.logger.info(`Process started`, {
processId: processInfo.id,
instanceId,
pid: childProcess.pid,
strategy
});
resolve();
});
childProcess.on('exit', (code, signal) => {
this.handleInstanceExit(processInfo, instance, code, signal);
});
childProcess.on('error', (error) => {
this.logger.error(`Process error`, {
processId: processInfo.id,
instanceId,
error: error.message
});
reject(error);
});
setTimeout(() => {
if (instance.status === 'starting') {
reject(new Error('Process failed to start within timeout'));
}
}, 30000);
});
}
handleWorkerExit(worker, code, signal) {
const instanceId = this.findInstanceByPid(worker.process.pid);
if (instanceId) {
const processInfo = this.findProcessByInstanceId(instanceId);
const instance = processInfo?.instances.find(i => i.id === instanceId);
if (processInfo && instance) {
this.handleInstanceExit(processInfo, instance, code, signal);
}
}
}
handleInstanceExit(processInfo, instance, code, signal) {
instance.status = code === 0 ? 'stopped' : 'crashed';
this.childProcesses.delete(instance.id);
// Calculate uptime
const uptime = instance.uptime ? Date.now() - instance.uptime : 0;
// Reset restart counter if process ran successfully for minimum uptime
if (uptime >= (processInfo.config.minUptime || constants_1.DEFAULT_CONFIG.minUptime)) {
instance.restarts = 0;
this.logger.info(`Process ran successfully for ${(0, helpers_1.formatUptime)(uptime)}, resetting restart counter`, {
processId: processInfo.id,
instanceId: instance.id
});
}
this.logger.info(`Process instance exited`, {
processId: processInfo.id,
instanceId: instance.id,
pid: instance.pid,
code,
signal,
uptime: (0, helpers_1.formatUptime)(uptime),
restarts: instance.restarts
});
if (processInfo.status !== 'stopping' && !this.isShuttingDown) {
if (instance.restarts < processInfo.config.maxRestarts) {
this.scheduleRestart(processInfo, instance);
}
else {
this.logger.error(`Process instance reached max restarts, stopping permanently`, {
processId: processInfo.id,
instanceId: instance.id,
maxRestarts: processInfo.config.maxRestarts,
totalRestarts: instance.restarts
});
instance.status = 'errored';
processInfo.status = 'errored';
// Emit event for max restarts reached
this.emit('maxRestartsReached', processInfo, instance);
}
}
this.updateProcessStatus(processInfo);
this.emit('instanceExit', processInfo, instance, code, signal);
}
scheduleRestart(processInfo, instance) {
const delay = (0, helpers_1.calculateExponentialBackoff)(instance.restarts, processInfo.config.restartDelay, processInfo.config.maxRestartDelay);
this.logger.warn(`Process crashed too quickly, scheduling restart in ${delay}ms`, {
processId: processInfo.id,
instanceId: instance.id,
attempt: instance.restarts + 1,
maxRestarts: processInfo.config.maxRestarts,
backoffDelay: `${delay}ms`
});
const timer = setTimeout(() => {
this.restartInstance(processInfo, instance);
}, delay);
this.restartTimers.set(instance.id, timer);
}
async restartInstance(processInfo, instance) {
try {
this.restartTimers.delete(instance.id);
instance.restarts++;
instance.lastRestart = Date.now();
instance.status = 'starting';
this.logger.info(`Restarting process instance`, {
processId: processInfo.id,
instanceId: instance.id,
attempt: instance.restarts
});
const strategy = this.determineStrategy(processInfo.config);
if (strategy === 'cluster') {
await this.startClusterInstance(processInfo, 0);
}
else {
await this.startSingleProcess(processInfo, strategy);
}
this.emit('instanceRestarted', processInfo, instance);
}
catch (error) {
this.logger.error(`Failed to restart process instance`, {
processId: processInfo.id,
instanceId: instance.id,
error: error.message
});
instance.status = 'errored';
this.updateProcessStatus(processInfo);
}
}
updateProcessStatus(processInfo) {
const runningInstances = processInfo.instances.filter(i => i.status === 'running');
const stoppedInstances = processInfo.instances.filter(i => i.status === 'stopped');
const erroredInstances = processInfo.instances.filter(i => i.status === 'errored' || i.status === 'crashed');
if (runningInstances.length > 0) {
processInfo.status = 'running';
}
else if (erroredInstances.length > 0) {
processInfo.status = 'errored';
}
else if (stoppedInstances.length === processInfo.instances.length) {
processInfo.status = 'stopped';
}
else {
processInfo.status = 'starting';
}
processInfo.updatedAt = Date.now();
processInfo.restarts = processInfo.instances.reduce((sum, instance) => sum + instance.restarts, 0);
}
async stopProcess(processId, force = false) {
const processInfo = this.processes.get(processId);
if (!processInfo) {
throw new Error(`Process not found: ${processId}`);
}
processInfo.status = 'stopping';
processInfo.updatedAt = Date.now();
this.logger.info(`Stopping process: ${processInfo.name}`, { processId, force });
const stopPromises = processInfo.instances.map(instance => this.stopInstance(processInfo, instance, force));
await Promise.all(stopPromises);
processInfo.status = 'stopped';
processInfo.updatedAt = Date.now();
this.emit('processStopped', processInfo);
}
async stopInstance(processInfo, instance, force) {
const childProcess = this.childProcesses.get(instance.id);
if (!childProcess)
return;
return new Promise((resolve) => {
const cleanup = () => {
this.childProcesses.delete(instance.id);
instance.status = 'stopped';
resolve();
};
if (force) {
childProcess.kill('SIGKILL');
setTimeout(cleanup, 1000);
return;
}
let killed = false;
const killTimer = setTimeout(() => {
if (!killed) {
killed = true;
childProcess.kill('SIGKILL');
setTimeout(cleanup, constants_1.FORCE_KILL_TIMEOUT);
}
}, constants_1.GRACEFUL_SHUTDOWN_TIMEOUT);
childProcess.once('exit', () => {
if (!killed) {
killed = true;
clearTimeout(killTimer);
cleanup();
}
});
childProcess.kill('SIGTERM');
});
}
async restartProcess(processId, graceful = false) {
const processInfo = this.processes.get(processId);
if (!processInfo) {
throw new Error(`Process not found: ${processId}`);
}
const strategy = this.determineStrategy(processInfo.config);
const instanceCount = this.resolveInstanceCount(processInfo.config.instances);
// For cluster mode with multiple instances, do graceful reload
if (graceful && strategy === 'cluster' && instanceCount > 1) {
await this.gracefulReload(processInfo);
}
else {
// Standard restart
await this.stopProcess(processId);
const updatedProcessInfo = this.processes.get(processId);
if (!updatedProcessInfo) {
throw new Error(`Process not found after stop: ${processId}`);
}
updatedProcessInfo.instances = [];
updatedProcessInfo.status = 'starting';
updatedProcessInfo.updatedAt = Date.now();
if (strategy === 'cluster' && instanceCount > 1) {
await this.startClusterProcess(updatedProcessInfo, instanceCount);
}
else {
await this.startSingleProcess(updatedProcessInfo, strategy);
}
updatedProcessInfo.status = 'running';
updatedProcessInfo.updatedAt = Date.now();
this.emit('processRestarted', updatedProcessInfo);
}
}
async gracefulReload(processInfo) {
this.logger.info(`Starting graceful reload for ${processInfo.name}`, { processId: processInfo.id });
const instanceCount = this.resolveInstanceCount(processInfo.config.instances);
const oldInstances = [...processInfo.instances];
const newInstances = [];
processInfo.status = 'reloading';
processInfo.updatedAt = Date.now();
// Start new instances first
for (let i = 0; i < instanceCount; i++) {
const instanceId = (0, helpers_1.generateId)();
const instance = {
id: instanceId,
status: 'starting',
restarts: 0
};
newInstances.push(instance);
processInfo.instances.push(instance);
try {
await this.startClusterInstance(processInfo, i);
// Wait a bit for the new instance to be ready
await new Promise(resolve => setTimeout(resolve, 2000));
}
catch (error) {
this.logger.error(`Failed to start new instance during graceful reload`, {
processId: processInfo.id,
instanceId,
error: error.message
});
}
}
// Now stop old instances one by one
for (const oldInstance of oldInstances) {
await this.stopInstance(processInfo, oldInstance, false);
// Remove old instance from the list
const index = processInfo.instances.findIndex(i => i.id === oldInstance.id);
if (index >= 0) {
processInfo.instances.splice(index, 1);
}
// Wait a bit between stopping instances
await new Promise(resolve => setTimeout(resolve, 1000));
}
processInfo.status = 'running';
processInfo.updatedAt = Date.now();
this.logger.info(`Graceful reload completed for ${processInfo.name}`, {
processId: processInfo.id,
oldInstances: oldInstances.length,
newInstances: newInstances.length
});
this.emit('processReloaded', processInfo);
}
deleteProcess(processId) {
const processInfo = this.processes.get(processId);
if (!processInfo) {
throw new Error(`Process not found: ${processId}`);
}
if (processInfo.status === 'running' || processInfo.status === 'starting') {
throw new Error('Cannot delete running process. Stop it first.');
}
this.processes.delete(processId);
this.logger.info(`Process deleted: ${processInfo.name}`, { processId });
this.emit('processDeleted', processInfo);
}
getProcesses() {
return Array.from(this.processes.values());
}
getProcess(processId) {
return this.processes.get(processId);
}
getProcessByName(name) {
return Array.from(this.processes.values()).find(p => p.name === name);
}
findInstanceByPid(pid) {
for (const processInfo of this.processes.values()) {
for (const instance of processInfo.instances) {
if (instance.pid === pid) {
return instance.id;
}
}
}
return undefined;
}
findProcessByInstanceId(instanceId) {
for (const processInfo of this.processes.values()) {
if (processInfo.instances.some(i => i.id === instanceId)) {
return processInfo;
}
}
return undefined;
}
async reloadAllProcesses() {
const runningProcesses = Array.from(this.processes.values())
.filter(p => p.status === 'running');
this.logger.info(`Reloading ${runningProcesses.length} processes`);
const reloadPromises = runningProcesses.map(async (processInfo) => {
try {
await this.restartProcess(processInfo.id);
}
catch (error) {
this.logger.error(`Failed to reload process ${processInfo.name}`, {
processId: processInfo.id,
error: error.message
});
}
});
await Promise.all(reloadPromises);
}
async gracefulShutdown() {
if (this.isShuttingDown)
return;
this.isShuttingDown = true;
this.logger.info('Starting graceful shutdown');
const runningProcesses = Array.from(this.processes.values())
.filter(p => p.status === 'running' || p.status === 'starting');
if (runningProcesses.length === 0) {
this.logger.info('No running processes to stop');
return;
}
this.logger.info(`Stopping ${runningProcesses.length} processes`);
const stopPromises = runningProcesses.map(async (processInfo) => {
try {
await this.stopProcess(processInfo.id);
}
catch (error) {
this.logger.error(`Failed to stop process ${processInfo.name}`, {
processId: processInfo.id,
error: error.message
});
}
});
await Promise.all(stopPromises);
this.restartTimers.forEach(timer => clearTimeout(timer));
this.restartTimers.clear();
this.logger.info('Graceful shutdown completed');
}
getHealthCheck() {
const results = [];
for (const processInfo of this.processes.values()) {
for (const instance of processInfo.instances) {
if (instance.status === 'running' && instance.pid) {
try {
const memUsage = process.memoryUsage();
const uptime = instance.uptime ? Date.now() - instance.uptime : 0;
results.push({
processId: processInfo.id,
memory: memUsage.rss,
cpu: 0, // TODO: Implement CPU monitoring
uptime,
healthy: instance.status === 'running'
});
}
catch (error) {
results.push({
processId: processInfo.id,
memory: 0,
cpu: 0,
uptime: 0,
healthy: false,
issues: [`Health check failed: ${error.message}`]
});
}
}
}
}
return results;
}
}
exports.ProcessOrchestrator = ProcessOrchestrator;
return module.exports;
})();
const FileWatcher_1 = (function() {
const module = { exports: {} };
const exports = module.exports;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.FileWatcher = void 0;
const fs_1 = require("fs");
const path_1 = require("path");
const crypto_1 = require("crypto");
const events_1 = require("events");
const helpers_1 = (function() {
const module = { exports: {} };
const exports = module.exports;
return module.exports;
})();
const constants_1 = (function() {
const module = { exports: {} };
const exports = module.exports;
return module.exports;
})();
class FileWatcher extends events_1.EventEmitter {
watchers = new Map();
watchedFiles = new Map();
debouncedHandlers = new Map();
ignorePatterns = [];
isWatching = false;
constructor() {
super();
this.setupIgnorePatterns();
}
setupIgnorePatterns() {
this.ignorePatterns = constants_1.FILE_WATCH_IGNORE.map(pattern => {
const regexPattern = pattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
return new RegExp(regexPattern);
});
}
watch(paths, options = {}) {
// Don't unwatch existing paths, just add new ones
// This was the bug - it was unwatching all previous paths!
const pathsArray = Array.isArray(paths) ? paths : [paths];
const { ignoreInitial = true, recursive = true, ignored = [] } = options;
if (ignored.length > 0) {
const additionalPatterns = ignored.map(pattern => {
const regexPattern = pattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
return new RegExp(regexPattern);
});
this.ignorePatterns.push(...additionalPatterns);
}
pathsArray.forEach(watchPath => {
this.watchPath((0, path_1.resolve)(watchPath), recursive);
});
if (!ignoreInitial) {
this.scanInitialFiles(pathsArray);
}
this.isWatching = true;
}
watchPath(path, recursive) {
try {
if (!(0, helpers_1.isDirectory)(path) && !(0, helpers_1.isFile)(path)) {
return;
}
console.log(`[FileWatcher] Watching path: ${path} (recursive: ${recursive})`);
const watcher = (0, fs_1.watch)(path, { recursive }, (eventType, filename) => {
if (!filename)
return;
const fullPath = (0, path_1.resolve)(path, filename);
console.log(`[FileWatcher] Event: ${eventType} on ${fullPath}`);
this.handleFileEvent(eventType, fullPath);
});
watcher.on('error', (error) => {
this.emit('error', error);
});
this.watchers.set(path, watcher);
}
catch (error) {
this.emit('error', new Error(`Failed to watch path ${path}: ${error}`));
}
}
handleFileEvent(eventType, filePath) {
if (this.shouldIgnore(filePath)) {
return;
}
const debouncedHandler = this.getDebouncedHandler(filePath);
debouncedHandler(eventType, filePath);
}
getDebouncedHandler(filePath) {
if (!this.debouncedHandlers.has(filePath)) {
const handler = (0, helpers_1.debounce)((eventType, path) => {
this.processFileChange(eventType, path);
}, constants_1.FILE_WATCH_DEBOUNCE);
this.debouncedHandlers.set(filePath, handler);
}
return this.debouncedHandlers.get(filePath);
}
processFileChange(eventType, filePath) {
try {
const existingFile = this.watchedFiles.get(filePath);
if (!(0, helpers_1.isFile)(filePath)) {
if (existingFile) {
this.watchedFiles.delete(filePath);
this.emitFileEvent('unlink', filePath);
}
return;
}
const stats = (0, fs_1.statSync)(filePath);
const currentHash = this.calculateFileHash(filePath);
const currentSize = stats.size;
const currentMtime = stats.mtime.getTime();
if (!existingFile) {
this.watchedFiles.set(filePath, {
path: filePath,
hash: currentHash,
size: currentSize,
mtime: currentMtime
});
this.emitFileEvent('add', filePath, stats);
}
else {
const hasChanged = existingFile.hash !== currentHash ||
existingFile.size !== currentSize ||
existingFile.mtime !== currentMtime;
if (hasChanged) {
this.watchedFiles.set(filePath, {
path: filePath,
hash: currentHash,
size: currentSize,
mtime: currentMtime
});
this.emitFileEvent('change', filePath, stats);
}
}
}
catch (error) {
this.emit('error', new Error(`Failed to process file change for ${filePath}: ${error}`));
}
}
calculateFileHash(filePath) {
try {
const content = (0, fs_1.readFileSync)(filePath);
return (0, crypto_1.createHash)('md5').update(content).digest('hex');
}
catch (error) {
return '';
}
}
shouldIgnore(filePath) {
const relativePath = (0, path_1.relative)(process.cwd(), filePath);
return this.ignorePatterns.some(pattern => pattern.test(relativePath));
}
scanInitialFiles(paths) {
paths.forEach(path => {
this.scanDirectory((0, path_1.resolve)(path));
});
}
scanDirectory(dirPath) {
try {
if (!(0, helpers_1.isDirectory)(dirPath)) {
if ((0, helpers_1.isFile)(dirPath) && !this.shouldIgnore(dirPath)) {
this.processFileChange('add', dirPath);
}
return;
}
const fs = require('fs');
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
entries.forEach((entry) => {
const entryPath = (0, path_1.join)(dirPath, entry.name);
if (this.shouldIgnore(entryPath)) {
return;
}
if (entry.isDirectory()) {
this.scanDirectory(entryPath);
}
else if (entry.isFile()) {
this.processFileChange('add', entryPath);
}
});
}
catch (error) {
this.emit('error', new Error(`Failed to scan directory ${dirPath}: ${error}`));
}
}
emitFileEvent(type, filePath, stats) {
const event = {
type,
filename: require('path').basename(filePath),
path: filePath,
stats
};
this.emit('fileChange', event);
this.emit(type, filePath, stats);
}
unwatch() {
this.watchers.forEach(watcher => {
try {
watcher.close();
}
catch (error) {
this.emit('error', error);
}
});
this.watchers.clear();
this.watchedFiles.clear();
this.debouncedHandlers.clear();
this.isWatching = false;
}
getWatchedFiles() {
return Array.from(this.watchedFiles.keys());
}
isFileWatched(filePath) {
return this.watchedFiles.has((0, path_1.resolve)(filePath));
}
getFileInfo(filePath) {
return this.watchedFiles.get((0, path_1.resolve)(filePath));
}
addIgnorePattern(pattern) {
const regexPattern = pattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
this.ignorePatterns.push(new RegExp(regexPattern));
}
removeIgnorePattern(pattern) {
const regexPattern = pattern
.replace(/\./g, '\\.')
.replace(/\*/g, '.*')
.replace(/\?/g, '.');
const regex = new RegExp(regexPattern);
this.ignorePatterns = this.ignorePatterns.filter(existingPattern => existingPattern.source !== regex.source);
}
clearIgnorePatterns() {
this.ignorePatterns = [];
this.setupIgnorePatterns();
}
getIgnorePatterns() {
return this.ignorePatterns.map(pattern => pattern.source);
}
pause() {
this.watchers.forEach(watcher => {
try {
watcher.close();
}
catch (error) {
this.emit('error', error);
}
});
this.isWatching = false;
}
resume() {
if (!this.isWatching) {
const watchedPaths = Array.from(this.watchers.keys());
this.watchers.clear();
watchedPaths.forEach(path => {
this.watchPath(path, true);
});
this.isWatching = true;
}
}
getStats() {
return {
watchedPaths: this.watchers.size,
watchedFiles: this.watchedFiles.size,
isWatching: this.isWatching,
ignorePatterns: this.ignorePatterns.length
};
}
}
exports.FileWatcher = FileWatcher;
return module.exports;
})();
const LogManager_1 = (function() {
const module = { exports: {} };
const exports = module.exports;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.LogManager = void 0;
const fs_1 = require("fs");
const path_1 = require("path");
const zlib_1 = require("zlib");
const events_1 = require("events");
const helpers_1 = (function() {
const module = { exports: {} };
const exports = module.exports;
return module.exports;
})();
const constants_1 = (function() {
const module = { exports: {} };
const exports = module.exports;
return module.exports;
})();
class LogManager extends events_1.EventEmitter {
logStreams = new Map();
logBuffer = [];
bufferIndex = 0;
isShuttingDown = false;
constructor() {
super();
(0, helpers_1.ensureDir)(constants_1.LOG_DIR);
this.setupMainLogStream();
}
setupMainLogStream() {
const mainLogPath = (0, path_1.join)(constants_1.LOG_DIR, 'daemon.log');
this.createLogStream('daemon', mainLogPath);
}
createLogStream(name, filePath) {
if (this.logStreams.has(name)) {
this.logStreams.get(name)?.end();
}
const stream = (0, fs_1.createWriteStream)(filePath, { flags: 'a' });
stream.on('error', (error) => {
console.error(`Log stream error for ${name}:`, error);
});
this.logStreams.set(name, stream);
return stream;
}
log(entry) {
if (this.isShuttingDown)
return;
this.addToBuffer(entry);
// Emit log event for WebUI
this.emit('log', entry);
const logLine = this.formatLogEntry(entry);
const streamName = entry.processId || 'daemon';
let stream = this.logStreams.get(streamName);
if (!stream) {
const logPath = (0, path_1.join)(constants_1.LOG_DIR, `${streamName}.log`);
stream = this.createLogStream(streamName, logPath);
}
stream.write(logLine + '\n', (error) => {
if (error) {
console.error(`Failed to write log for ${streamName}:`, error);
}
});
this.checkLogRotation(streamName);
}
info(message, data, processId) {
this.log({
timestamp: Date.now(),
level: 'info',
processId,
message,
data
});
}
warn(message, data, processId) {
this.log({
timestamp: Date.now(),
level: 'warn',
processId,
message,
data
});
}
error(message, data, processId) {
this.log({
timestamp: Date.now(),
level: 'error',
processId,
message,
data
});
}
debug(message, data, processId) {
this.log({
timestamp: Date.now(),
level: 'debug',
processId,
message,
data
});
}
addToBuffer(entry) {
if (this.logBuffer.length < constants_1.LOG_BUFFER_SIZE) {
this.logBuffer.push(entry);
}
else {
this.logBuffer[this.bufferIndex] = entry;
this.bufferIndex = (this.bufferIndex + 1) % constants_1.LOG_BUFFER_SIZE;
}
}
getRecentLogs(count = 100, processId) {
let logs = [...this.logBuffer];
if (processId) {
logs = logs.filter(log => log.processId === processId);
}
return logs
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, count);
}
formatLogEntry(entry) {
const timestamp = new Date(entry.timestamp).toISOString();
const level = entry.level.toUpperCase().padEnd(5);
const processInfo = entry.processId ? `[${entry.processId}] ` : '';
const dataInfo = entry.data ? ` ${JSON.stringify(entry.data)}` : '';
return `${timestamp} ${level} ${processInfo}${entry.message}${dataInfo}`;
}
checkLogRotation(streamName) {
const logPath = (0, path_1.join)(constants_1.LOG_DIR, `${streamName}.log`);
if (!(0, fs_1.existsSync)(logPath))
return;
try {
const stats = (0, fs_1.statSync)(logPath);
if (stats.size > constants_1.MAX_LOG_SIZE) {
this.rotateLog(streamName, logPath);
}
}
catch (error) {
console.error(`Failed to check log size for ${streamName}:`, error);
}
}
rotateLog(streamName, currentLogPath) {
try {
const stream = this.logStreams.get(streamName);
if (stream) {
stream.end();
this.logStreams.delete(streamName);
}
this.rotateLogFiles(currentLogPath);
this.createLogStream(streamName, currentLogPath);
this.info(`Log rotated for ${streamName}`);
}
catch (error) {
console.error(`Failed to rotate log for ${streamName}:`, error);
}
}
rotateLogFiles(logPath) {
const basePath = logPath.replace('.log', '');
for (let i = constants_1.MAX_LOG_FILES - 1; i > 0; i--) {
const oldPath = i === 1 ? logPath : `${basePath}.${i}.log.gz`;
const newPath = `${basePath}.${i + 1}.log.gz`;
if ((0, fs_1.existsSync)(oldPath)) {
if (i === constants_1.MAX_LOG_FILES - 1)