import { promises as fs } from 'fs'; import { execSync } from 'child_process'; import { NupstDaemon } from './daemon.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 After=network.target [Service] ExecStart=/usr/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) { console.error('┌─ Configuration Error ─────────────────────┐'); console.error(`│ No configuration file found at ${configPath}`); console.error('│ Please run \'nupst setup\' first to create a configuration.'); console.error('└──────────────────────────────────────────┘'); 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); console.log('┌─ Service Installation ─────────────────────┐'); console.log(`│ Service file created at ${this.serviceFilePath}`); // Reload systemd daemon execSync('systemctl daemon-reload'); console.log('│ Systemd daemon reloaded'); // Enable the service execSync('systemctl enable nupst.service'); console.log('│ Service enabled to start on boot'); console.log('└──────────────────────────────────────────┘'); } catch (error) { if (error.message === 'Configuration not found') { // Just rethrow the error as the message has already been displayed throw error; } console.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'); console.log('┌─ Service Status ─────────────────────────┐'); console.log('│ NUPST service started successfully'); console.log('└──────────────────────────────────────────┘'); } catch (error) { if (error.message === 'Configuration not found') { // Exit with error code since configuration is required process.exit(1); } console.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'); console.log('NUPST service stopped'); } catch (error) { console.error('Failed to stop service:', error); throw error; } } /** * Get status of the systemd service and UPS */ public async getStatus(): Promise { try { // Check if config exists first try { await this.checkConfigExists(); } catch (error) { // Error message already displayed by checkConfigExists return; } await this.displayServiceStatus(); await this.displayUpsStatus(); } catch (error) { console.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(); console.log('┌─ Service Status ─────────────────────────┐'); console.log(serviceStatus.split('\n').map(line => `│ ${line}`).join('\n')); console.log('└──────────────────────────────────────────┘'); } catch (error) { console.error('┌─ Service Status ─────────────────────────┐'); console.error('│ Service is not running'); console.error('└──────────────────────────────────────────┘'); } } /** * Display the UPS status * @private */ private async displayUpsStatus(): Promise { try { const upsStatus = await this.daemon.getConfig().snmp; const snmp = this.daemon.getNupstSnmp(); const status = await snmp.getUpsStatus(upsStatus); console.log('┌─ UPS Status ───────────────────────────────┐'); console.log(`│ Power Status: ${status.powerStatus}`); console.log(`│ Battery Capacity: ${status.batteryCapacity}%`); console.log(`│ Runtime Remaining: ${status.batteryRuntime} minutes`); console.log('└──────────────────────────────────────────┘'); } catch (error) { console.error('┌─ UPS Status ───────────────────────────────┐'); console.error(`│ Failed to retrieve UPS status: ${error.message}`); console.error('└──────────────────────────────────────────┘'); } } /** * 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'); console.log('Systemd daemon reloaded'); console.log('NUPST service has been successfully uninstalled'); } catch (error) { console.error('Failed to disable and uninstall service:', error); throw error; } } /** * Stop the service if it's running * @private */ private async stopService(): Promise { try { console.log('Stopping NUPST service...'); execSync('systemctl stop nupst.service'); } catch (error) { // Service might not be running, that's okay console.log('Service was not running or could not be stopped'); } } /** * Disable the service * @private */ private async disableService(): Promise { try { console.log('Disabling NUPST service...'); execSync('systemctl disable nupst.service'); } catch (error) { console.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)) { console.log(`Removing service file ${this.serviceFilePath}...`); await fs.unlink(this.serviceFilePath); console.log('Service file removed'); } else { console.log('Service file did not exist'); } } }