import { promises as fs } from 'fs'; import { execSync } from 'child_process'; import { NupstDaemon } from './daemon.js'; import { logger } from './logger.js'; /** * 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=Node.js UPS Shutdown Tool for Multiple UPS Devices After=network.target [Service] ExecStart=/opt/nupst/bin/nupst daemon-start Restart=always User=root Group=root Environment=PATH=/usr/bin:/usr/local/bin Environment=NODE_ENV=production WorkingDirectory=/tmp [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.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.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 async stop(): Promise { 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.message}`); } } /** * Display the systemd service status * @private */ private async displayServiceStatus(): Promise { try { const serviceStatus = execSync('systemctl status nupst.service').toString(); const boxWidth = 45; logger.logBoxTitle('Service Status', boxWidth); // Process each line of the status output serviceStatus.split('\n').forEach(line => { logger.logBoxLine(line); }); logger.logBoxEnd(); } catch (error) { const boxWidth = 45; logger.logBoxTitle('Service Status', boxWidth); logger.logBoxLine('Service is not running'); logger.logBoxEnd(); } } /** * 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) { logger.log(`Found ${config.upsDevices.length} UPS device(s) in configuration`); // Show status for each UPS for (const ups of config.upsDevices) { await this.displaySingleUpsStatus(ups, snmp); } } else if (config.snmp) { // Legacy single UPS configuration const legacyUps = { id: 'default', name: 'Default UPS', snmp: config.snmp, thresholds: config.thresholds, groups: [] }; await this.displaySingleUpsStatus(legacyUps, snmp); } else { logger.error('No UPS devices found in configuration'); } } catch (error) { const boxWidth = 45; logger.logBoxTitle('UPS Status', boxWidth); logger.logBoxLine(`Failed to retrieve UPS status: ${error.message}`); logger.logBoxEnd(); } } /** * Display status of a single UPS * @param ups UPS configuration * @param snmp SNMP manager */ private async displaySingleUpsStatus(ups: any, snmp: any): Promise { const boxWidth = 45; logger.logBoxTitle(`Connecting to UPS: ${ups.name}`, boxWidth); logger.logBoxLine(`ID: ${ups.id}`); logger.logBoxLine(`Host: ${ups.snmp.host}:${ups.snmp.port}`); logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel || 'cyberpower'}`); if (ups.groups && ups.groups.length > 0) { // Get group names if available const config = this.daemon.getConfig(); const groupNames = ups.groups.map(groupId => { const group = config.groups?.find(g => g.id === groupId); return group ? group.name : groupId; }); logger.logBoxLine(`Groups: ${groupNames.join(', ')}`); } logger.logBoxEnd(); 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); logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth); logger.logBoxLine(`Power Status: ${status.powerStatus}`); logger.logBoxLine(`Battery Capacity: ${status.batteryCapacity}%`); logger.logBoxLine(`Runtime Remaining: ${status.batteryRuntime} minutes`); // Show threshold status logger.logBoxLine(''); logger.logBoxLine('Thresholds:'); logger.logBoxLine(` Battery: ${status.batteryCapacity}% / ${ups.thresholds.battery}% ${ status.batteryCapacity < ups.thresholds.battery ? '⚠️' : '✓' }`); logger.logBoxLine(` Runtime: ${status.batteryRuntime} min / ${ups.thresholds.runtime} min ${ status.batteryRuntime < ups.thresholds.runtime ? '⚠️' : '✓' }`); logger.logBoxEnd(); } catch (error) { logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth); logger.logBoxLine(`Failed to retrieve UPS status: ${error.message}`); logger.logBoxEnd(); } } /** * 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 async stopService(): Promise { 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 async disableService(): Promise { 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'); } } }