Removed the last remaining ugly ASCII boxes: - Version info box (┌─┐│└┘) that appeared at top - Async version check box that ended randomly in middle - Configuration error box Now status output is 100% clean and beautiful with just colored text: ● Service: active (running) PID: 9120 Memory: 45.7M CPU: 190ms UPS Devices (2): ⚠ Test UPS (SNMP v1) - On Battery Battery: 100% ✓ Runtime: 48 min Host: 192.168.187.140:161 ◯ Test UPS (SNMP v3) - Unknown Battery: 0% ⚠ Runtime: 0 min Host: 192.168.187.140:161 No boxes, just beautiful colored output with symbols! Bumped to v4.1.0 to mark completion of beautiful CLI feature.
379 lines
12 KiB
TypeScript
379 lines
12 KiB
TypeScript
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<void> {
|
|
const configPath = '/etc/nupst/config.json';
|
|
try {
|
|
await fs.access(configPath);
|
|
} catch (error) {
|
|
console.log('');
|
|
console.log(`${symbols.error} ${theme.error('No configuration found')}`);
|
|
console.log(` ${theme.dim('Config file:')} ${configPath}`);
|
|
console.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to create a configuration')}`);
|
|
console.log('');
|
|
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);
|
|
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<void> {
|
|
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<void> {
|
|
try {
|
|
// Enable debug mode if requested
|
|
if (debugMode) {
|
|
console.log('');
|
|
logger.info('Debug Mode: SNMP debugging enabled');
|
|
console.log('');
|
|
this.daemon.getNupstSnmp().enableDebug();
|
|
}
|
|
|
|
// 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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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');
|
|
}
|
|
}
|
|
}
|