import process from 'node:process'; import { promises as fs } from 'node:fs'; import { execSync } from 'node:child_process'; import { NupstDaemon } from './daemon.ts'; import { logger } from './logger.ts'; import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts'; /** * Class for managing systemd service * Handles installation, removal, and control of the NUPST systemd service */ export class NupstSystemd { /** Path to the systemd service file */ private readonly serviceFilePath = '/etc/systemd/system/nupst.service'; private readonly daemon: NupstDaemon; /** Template for the systemd service file */ private readonly serviceTemplate = `[Unit] Description=NUPST - Deno-powered UPS Monitoring Tool After=network.target [Service] ExecStart=/usr/local/bin/nupst service start-daemon Restart=always RestartSec=10 User=root Group=root Environment=PATH=/usr/bin:/usr/local/bin WorkingDirectory=/opt/nupst [Install] WantedBy=multi-user.target `; /** * Create a new systemd manager * @param daemon The daemon instance to manage */ constructor(daemon: NupstDaemon) { this.daemon = daemon; } /** * Check if a configuration file exists * @private * @throws Error if configuration not found */ private async checkConfigExists(): Promise { const configPath = '/etc/nupst/config.json'; try { await fs.access(configPath); } catch (error) { const boxWidth = 50; logger.logBoxTitle('Configuration Error', boxWidth); logger.logBoxLine(`No configuration file found at ${configPath}`); logger.logBoxLine("Please run 'nupst add' first to create a UPS configuration."); logger.logBoxEnd(); throw new Error('Configuration not found'); } } /** * Install the systemd service file * @throws Error if installation fails */ public async install(): Promise { try { // Check if configuration exists before installing await this.checkConfigExists(); // Write the service file await fs.writeFile(this.serviceFilePath, this.serviceTemplate); const boxWidth = 50; logger.logBoxTitle('Service Installation', boxWidth); logger.logBoxLine(`Service file created at ${this.serviceFilePath}`); // Reload systemd daemon execSync('systemctl daemon-reload'); logger.logBoxLine('Systemd daemon reloaded'); // Enable the service execSync('systemctl enable nupst.service'); logger.logBoxLine('Service enabled to start on boot'); logger.logBoxEnd(); } catch (error) { if (error instanceof Error && error.message === 'Configuration not found') { // Just rethrow the error as the message has already been displayed throw error; } logger.error(`Failed to install systemd service: ${error}`); throw error; } } /** * Start the systemd service * @throws Error if start fails */ public async start(): Promise { try { // Check if configuration exists before starting await this.checkConfigExists(); execSync('systemctl start nupst.service'); const boxWidth = 45; logger.logBoxTitle('Service Status', boxWidth); logger.logBoxLine('NUPST service started successfully'); logger.logBoxEnd(); } catch (error) { if (error instanceof Error && error.message === 'Configuration not found') { // Exit with error code since configuration is required process.exit(1); } logger.error(`Failed to start service: ${error}`); throw error; } } /** * Stop the systemd service * @throws Error if stop fails */ public stop(): void { try { execSync('systemctl stop nupst.service'); logger.success('NUPST service stopped'); } catch (error) { logger.error(`Failed to stop service: ${error}`); throw error; } } /** * Get status of the systemd service and UPS * @param debugMode Whether to enable debug mode for SNMP */ public async getStatus(debugMode: boolean = false): Promise { try { // Enable debug mode if requested if (debugMode) { const boxWidth = 45; logger.logBoxTitle('Debug Mode', boxWidth); logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown'); logger.logBoxEnd(); this.daemon.getNupstSnmp().enableDebug(); } // Display version information this.daemon.getNupstSnmp().getNupst().logVersionInfo(); // Check if config exists first try { await this.checkConfigExists(); } catch (error) { // Error message already displayed by checkConfigExists return; } await this.displayServiceStatus(); await this.displayAllUpsStatus(); } catch (error) { logger.error( `Failed to get status: ${error instanceof Error ? error.message : String(error)}`, ); } } /** * Display the systemd service status * @private */ private displayServiceStatus(): void { try { const serviceStatus = execSync('systemctl status nupst.service').toString(); const lines = serviceStatus.split('\n'); // Parse key information from systemctl output let isActive = false; let pid = ''; let memory = ''; let cpu = ''; for (const line of lines) { if (line.includes('Active:')) { isActive = line.includes('active (running)'); } else if (line.includes('Main PID:')) { const match = line.match(/Main PID:\s+(\d+)/); if (match) pid = match[1]; } else if (line.includes('Memory:')) { const match = line.match(/Memory:\s+([\d.]+[A-Z])/); if (match) memory = match[1]; } else if (line.includes('CPU:')) { const match = line.match(/CPU:\s+([\d.]+(?:ms|s))/); if (match) cpu = match[1]; } } // Display beautiful status console.log(''); if (isActive) { console.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`); } else { console.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`); } if (pid || memory || cpu) { const details = []; if (pid) details.push(`PID: ${theme.dim(pid)}`); if (memory) details.push(`Memory: ${theme.dim(memory)}`); if (cpu) details.push(`CPU: ${theme.dim(cpu)}`); console.log(` ${details.join(' ')}`); } console.log(''); } catch (error) { console.log(''); console.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`); console.log(''); } } /** * Display all UPS statuses * @private */ private async displayAllUpsStatus(): Promise { try { // Explicitly load the configuration first to ensure it's up-to-date await this.daemon.loadConfig(); const config = this.daemon.getConfig(); const snmp = this.daemon.getNupstSnmp(); // Check if we have the new multi-UPS config format if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) { console.log(theme.info(`UPS Devices (${config.upsDevices.length}):`)); // Show status for each UPS for (const ups of config.upsDevices) { await this.displaySingleUpsStatus(ups, snmp); } } else if (config.snmp) { // Legacy single UPS configuration console.log(theme.info('UPS Devices (1):')); const legacyUps = { id: 'default', name: 'Default UPS', snmp: config.snmp, thresholds: config.thresholds, groups: [], }; await this.displaySingleUpsStatus(legacyUps, snmp); } else { console.log(''); console.log(`${symbols.warning} ${theme.warning('No UPS devices configured')}`); console.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`); console.log(''); } } catch (error) { console.log(''); console.log(`${symbols.error} ${theme.error('Failed to retrieve UPS status')}`); console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`); console.log(''); } } /** * Display status of a single UPS * @param ups UPS configuration * @param snmp SNMP manager */ private async displaySingleUpsStatus(ups: any, snmp: any): Promise { try { // Create a test config with a short timeout const testConfig = { ...ups.snmp, timeout: Math.min(ups.snmp.timeout, 10000), // Use at most 10 seconds for status check }; const status = await snmp.getUpsStatus(testConfig); // Determine status symbol based on power status let statusSymbol = symbols.unknown; if (status.powerStatus === 'online') { statusSymbol = symbols.running; } else if (status.powerStatus === 'onBattery') { statusSymbol = symbols.warning; } // Display UPS name and power status console.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`); // Display battery with color coding const batteryColor = getBatteryColor(status.batteryCapacity); const batterySymbol = status.batteryCapacity >= ups.thresholds.battery ? symbols.success : symbols.warning; console.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`); // Display host info console.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`); // Display groups if any if (ups.groups && ups.groups.length > 0) { const config = this.daemon.getConfig(); const groupNames = ups.groups.map((groupId: string) => { const group = config.groups?.find((g: { id: string }) => g.id === groupId); return group ? group.name : groupId; }); console.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`); } console.log(''); } catch (error) { // Display error for this UPS console.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`); console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`); console.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`); console.log(''); } } /** * Disable and uninstall the systemd service * @throws Error if disabling fails */ public async disable(): Promise { try { await this.stopService(); await this.disableService(); await this.removeServiceFile(); // Reload systemd daemon execSync('systemctl daemon-reload'); logger.log('Systemd daemon reloaded'); logger.success('NUPST service has been successfully uninstalled'); } catch (error) { logger.error(`Failed to disable and uninstall service: ${error}`); throw error; } } /** * Stop the service if it's running * @private */ private stopService(): void { try { logger.log('Stopping NUPST service...'); execSync('systemctl stop nupst.service'); } catch (error) { // Service might not be running, that's okay logger.log('Service was not running or could not be stopped'); } } /** * Disable the service * @private */ private disableService(): void { try { logger.log('Disabling NUPST service...'); execSync('systemctl disable nupst.service'); } catch (error) { logger.log('Service was not enabled or could not be disabled'); } } /** * Remove the service file if it exists * @private */ private async removeServiceFile(): Promise { if (await fs.stat(this.serviceFilePath).catch(() => null)) { logger.log(`Removing service file ${this.serviceFilePath}...`); await fs.unlink(this.serviceFilePath); logger.log('Service file removed'); } else { logger.log('Service file did not exist'); } } }