nupst/ts/systemd.ts
2025-03-25 09:06:23 +00:00

246 lines
8.3 KiB
TypeScript

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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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');
}
}
}