import * as xmlrpc from 'xmlrpc'; import { createLogger, formatError } from '../utils/logger'; import { ProcessInfo, ProcessInfoSchema, SupervisorStateInfo, SupervisorStateInfoSchema, ConfigInfo, ConfigInfoSchema, ReloadConfigResult, ReloadConfigResultSchema, ProcessActionResult, LogTailResult, SystemInfo, } from './types'; export interface SupervisorClientConfig { host: string; port: number; username?: string; password?: string; } export class SupervisorClient { private client: xmlrpc.Client; private config: SupervisorClientConfig; private logger: ReturnType; private cachedLogfilePath?: string; constructor(config: SupervisorClientConfig) { this.config = config; // Create logger with supervisor context this.logger = createLogger({ component: 'SupervisorClient', host: config.host, port: config.port, }); const clientOptions: any = { host: config.host, port: config.port, path: '/RPC2', }; // Add basic auth if credentials provided if (config.username && config.password) { clientOptions.basic_auth = { user: config.username, pass: config.password, }; this.logger.debug('Basic auth configured'); } this.client = xmlrpc.createClient(clientOptions); this.logger.info({ config: { host: config.host, port: config.port } }, 'Supervisor client initialized'); } /** * Generic method call wrapper with error handling and logging */ private async call(method: string, params: any[] = []): Promise { const startTime = Date.now(); // Log the method call this.logger.debug({ method, params }, `Calling XML-RPC method: ${method}`); return new Promise((resolve, reject) => { this.client.methodCall(method, params, (error: any, value: any) => { const duration = Date.now() - startTime; if (error) { const errorInfo = formatError(error); this.logger.error({ method, params, duration, error: errorInfo, }, `XML-RPC call failed: ${method} (${duration}ms) - ${errorInfo.message}`); reject(new Error(`XML-RPC Error: ${error?.message || 'Unknown error'}`)); } else { this.logger.debug({ method, duration, resultSize: JSON.stringify(value).length, }, `XML-RPC call successful: ${method} (${duration}ms)`); resolve(value); } }); }); } // ===== System Methods ===== async getAPIVersion(): Promise { return this.call('supervisor.getAPIVersion'); } async getSupervisorVersion(): Promise { return this.call('supervisor.getSupervisorVersion'); } /** * Get logfile path from API version metadata * Caches the result after first call */ private async getLogfilePath(): Promise { if (this.cachedLogfilePath) { return this.cachedLogfilePath; } // Get API version which should contain logfile path in metadata const apiVersionData = await this.call('supervisor.getAPIVersion'); // Parse the response to extract logfile path // The exact structure depends on your supervisor API implementation // Common formats might be: // - { version: "3.0", logfile: "/path/to/log" } // - Just a string with embedded metadata let logfilePath: string; if (typeof apiVersionData === 'object' && apiVersionData.logfile) { logfilePath = apiVersionData.logfile; } else if (typeof apiVersionData === 'object' && apiVersionData.logfile_path) { logfilePath = apiVersionData.logfile_path; } else { // Fallback to default supervisord log location if not found in metadata this.logger.warn('Could not extract logfile path from getAPIVersion, using default'); logfilePath = '/var/log/supervisor/supervisord.log'; } this.cachedLogfilePath = logfilePath; this.logger.debug({ logfilePath }, 'Retrieved and cached logfile path'); return logfilePath; } async getIdentification(): Promise { return this.call('supervisor.getIdentification'); } async getState(): Promise { const result = await this.call('supervisor.getState'); return SupervisorStateInfoSchema.parse(result); } async getPID(): Promise { return this.call('supervisor.getPID'); } async getSystemInfo(): Promise { const [apiVersion, supervisorVersion, identification, state, pid] = await Promise.all([ this.getAPIVersion(), this.getSupervisorVersion(), this.getIdentification(), this.getState(), this.getPID(), ]); return { apiVersion, supervisorVersion, identification, state, pid, }; } // ===== Process Info Methods ===== async getAllProcessInfo(): Promise { const result = await this.call('supervisor.getAllProcessInfo'); return result.map((item) => ProcessInfoSchema.parse(item)); } async getProcessInfo(name: string): Promise { const result = await this.call('supervisor.getProcessInfo', [name]); return ProcessInfoSchema.parse(result); } async getAllConfigInfo(): Promise { const result = await this.call('supervisor.getAllConfigInfo'); return result.map((item) => ConfigInfoSchema.parse(item)); } // ===== Process Control Methods ===== async startProcess(name: string, wait: boolean = true): Promise { return this.call('supervisor.startProcess', [name, wait]); } async startProcessGroup(name: string, wait: boolean = true): Promise { return this.call('supervisor.startProcessGroup', [name, wait]); } async startAllProcesses(wait: boolean = true): Promise { return this.call('supervisor.startAllProcesses', [wait]); } async stopProcess(name: string, wait: boolean = true): Promise { return this.call('supervisor.stopProcess', [name, wait]); } async stopProcessGroup(name: string, wait: boolean = true): Promise { return this.call('supervisor.stopProcessGroup', [name, wait]); } async stopAllProcesses(wait: boolean = true): Promise { return this.call('supervisor.stopAllProcesses', [wait]); } async restartProcess(name: string): Promise { await this.stopProcess(name, true); return this.startProcess(name, true); } async signalProcess(name: string, signal: string): Promise { return this.call('supervisor.signalProcess', [name, signal]); } async signalProcessGroup(name: string, signal: string): Promise { return this.call('supervisor.signalProcessGroup', [name, signal]); } async signalAllProcesses(signal: string): Promise { return this.call('supervisor.signalAllProcesses', [signal]); } // ===== Log Methods ===== async readProcessStdoutLog( name: string, offset: number, length: number ): Promise { return this.call('supervisor.readProcessStdoutLog', [name, offset, length]); } async readProcessStderrLog( name: string, offset: number, length: number ): Promise { return this.call('supervisor.readProcessStderrLog', [name, offset, length]); } async tailProcessStdoutLog( name: string, offset: number, length: number ): Promise { const result = await this.call<[string, number, boolean]>('supervisor.tailProcessStdoutLog', [ name, offset, length, ]); return { bytes: result[0], offset: result[1], overflow: result[2], }; } async tailProcessStderrLog( name: string, offset: number, length: number ): Promise { const result = await this.call<[string, number, boolean]>('supervisor.tailProcessStderrLog', [ name, offset, length, ]); return { bytes: result[0], offset: result[1], overflow: result[2], }; } async clearProcessLogs(name: string): Promise { return this.call('supervisor.clearProcessLogs', [name]); } async clearAllProcessLogs(): Promise { return this.call('supervisor.clearAllProcessLogs'); } async readLog(offset: number, length: number, logfilePath?: string): Promise { // If logfilePath not provided, get it from API version metadata const path = logfilePath || await this.getLogfilePath(); return this.call('supervisor.readLog', [offset, length, path]); } async clearLog(): Promise { return this.call('supervisor.clearLog'); } // ===== Configuration Methods ===== async reloadConfig(): Promise { const result = await this.call('supervisor.reloadConfig'); return ReloadConfigResultSchema.parse({ added: result[0], changed: result[1], removed: result[2], }); } async addProcessGroup(name: string): Promise { return this.call('supervisor.addProcessGroup', [name]); } async removeProcessGroup(name: string): Promise { return this.call('supervisor.removeProcessGroup', [name]); } // ===== Supervisor Control Methods ===== async shutdown(): Promise { return this.call('supervisor.shutdown'); } async restart(): Promise { return this.call('supervisor.restart'); } async sendProcessStdin(name: string, chars: string): Promise { return this.call('supervisor.sendProcessStdin', [name, chars]); } async sendRemoteCommEvent(type: string, data: string): Promise { return this.call('supervisor.sendRemoteCommEvent', [type, data]); } } /** * Factory function to create a supervisor client from environment variables */ export function createSupervisorClient(config?: Partial): SupervisorClient { const defaultConfig: SupervisorClientConfig = { host: process.env.SUPERVISOR_HOST || 'localhost', port: parseInt(process.env.SUPERVISOR_PORT || '9001', 10), username: process.env.SUPERVISOR_USERNAME, password: process.env.SUPERVISOR_PASSWORD, }; return new SupervisorClient({ ...defaultConfig, ...config }); }