Files
nupst/ts/systemd.ts

421 lines
13 KiB
TypeScript
Raw Normal View History

2025-10-19 12:57:17 +00:00
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';
2025-03-25 09:06:23 +00:00
/**
* 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
2025-03-25 09:06:23 +00:00
After=network.target
[Service]
ExecStart=/usr/local/bin/nupst service start-daemon
2025-03-25 09:06:23 +00:00
Restart=always
RestartSec=10
2025-03-25 09:06:23 +00:00
User=root
Group=root
Environment=PATH=/usr/bin:/usr/local/bin
WorkingDirectory=/opt/nupst
2025-03-25 09:06:23 +00:00
[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) {
logger.log('');
logger.error('No configuration found');
logger.log(` ${theme.dim('Config file:')} ${configPath}`);
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to create a configuration')}`);
logger.log('');
2025-03-25 09:06:23 +00:00
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();
2025-03-25 09:06:23 +00:00
// 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}`);
2025-03-25 09:06:23 +00:00
// Reload systemd daemon
execSync('systemctl daemon-reload');
logger.logBoxLine('Systemd daemon reloaded');
2025-03-25 09:06:23 +00:00
// Enable the service
execSync('systemctl enable nupst.service');
logger.logBoxLine('Service enabled to start on boot');
logger.logBoxEnd();
2025-03-25 09:06:23 +00:00
} catch (error) {
fix: resolve all TypeScript type errors across codebase for Deno strict mode Comprehensive type safety improvements across all CLI handlers and daemon: **Error handling type fixes:** - Add 'error instanceof Error' checks before accessing error.message throughout - Fix all error/retryError/stdError/upsError type assertions - Replace direct error.message with proper type guards **Switch case improvements:** - Wrap case block declarations in braces to satisfy deno-lint - Fix no-case-declarations warnings in CLI command handlers **Null/undefined safety:** - Add checks for config.snmp and config.thresholds before access - Fix IUpsStatus lastStatusChange to handle undefined with default value - Add proper null checks in legacy configuration paths **Type annotations:** - Add explicit type annotations to lambda parameters (groupId, updateAvailable, etc.) - Add TUpsModel type cast for 'cyberpower' default - Import and use INupstConfig type where needed **Parameter type fixes:** - Fix implicit 'any' type errors in array callbacks - Add type annotations to filter/find/map parameters Files modified: - ts/cli.ts: config.snmp/thresholds null checks, unused error variable fixes - ts/cli/group-handler.ts: 4 error.message fixes + 2 parameter type annotations - ts/cli/service-handler.ts: 3 error.message fixes - ts/cli/ups-handler.ts: 5 error.message fixes + config checks + TUpsModel import - ts/daemon.ts: 8 error.message fixes + IUpsStatus lastStatusChange fix + updateAvailable type - ts/nupst.ts: 1 error.message fix - ts/systemd.ts: 5 error.message fixes + parameter type annotation All tests passing (3/3 SNMP tests + 10/10 logger tests) Type check: ✓ No errors
2025-10-18 21:07:57 +00:00
if (error instanceof Error && error.message === 'Configuration not found') {
2025-03-25 09:06:23 +00:00
// Just rethrow the error as the message has already been displayed
throw error;
}
logger.error(`Failed to install systemd service: ${error}`);
2025-03-25 09:06:23 +00:00
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();
2025-03-25 09:06:23 +00:00
execSync('systemctl start nupst.service');
const boxWidth = 45;
logger.logBoxTitle('Service Status', boxWidth);
logger.logBoxLine('NUPST service started successfully');
logger.logBoxEnd();
2025-03-25 09:06:23 +00:00
} catch (error) {
fix: resolve all TypeScript type errors across codebase for Deno strict mode Comprehensive type safety improvements across all CLI handlers and daemon: **Error handling type fixes:** - Add 'error instanceof Error' checks before accessing error.message throughout - Fix all error/retryError/stdError/upsError type assertions - Replace direct error.message with proper type guards **Switch case improvements:** - Wrap case block declarations in braces to satisfy deno-lint - Fix no-case-declarations warnings in CLI command handlers **Null/undefined safety:** - Add checks for config.snmp and config.thresholds before access - Fix IUpsStatus lastStatusChange to handle undefined with default value - Add proper null checks in legacy configuration paths **Type annotations:** - Add explicit type annotations to lambda parameters (groupId, updateAvailable, etc.) - Add TUpsModel type cast for 'cyberpower' default - Import and use INupstConfig type where needed **Parameter type fixes:** - Fix implicit 'any' type errors in array callbacks - Add type annotations to filter/find/map parameters Files modified: - ts/cli.ts: config.snmp/thresholds null checks, unused error variable fixes - ts/cli/group-handler.ts: 4 error.message fixes + 2 parameter type annotations - ts/cli/service-handler.ts: 3 error.message fixes - ts/cli/ups-handler.ts: 5 error.message fixes + config checks + TUpsModel import - ts/daemon.ts: 8 error.message fixes + IUpsStatus lastStatusChange fix + updateAvailable type - ts/nupst.ts: 1 error.message fix - ts/systemd.ts: 5 error.message fixes + parameter type annotation All tests passing (3/3 SNMP tests + 10/10 logger tests) Type check: ✓ No errors
2025-10-18 21:07:57 +00:00
if (error instanceof Error && error.message === 'Configuration not found') {
2025-03-25 09:06:23 +00:00
// Exit with error code since configuration is required
process.exit(1);
}
logger.error(`Failed to start service: ${error}`);
2025-03-25 09:06:23 +00:00
throw error;
}
}
/**
* Stop the systemd service
* @throws Error if stop fails
*/
public stop(): void {
2025-03-25 09:06:23 +00:00
try {
execSync('systemctl stop nupst.service');
logger.success('NUPST service stopped');
2025-03-25 09:06:23 +00:00
} catch (error) {
logger.error(`Failed to stop service: ${error}`);
2025-03-25 09:06:23 +00:00
throw error;
}
}
/**
* Get status of the systemd service and UPS
* @param debugMode Whether to enable debug mode for SNMP
2025-03-25 09:06:23 +00:00
*/
/**
* Display version information and update status
* @private
*/
private async displayVersionInfo(): Promise<void> {
try {
const nupst = this.daemon.getNupstSnmp().getNupst();
const version = nupst.getVersion();
// Check for updates
const updateAvailable = await nupst.checkForUpdates();
// Display version info
if (updateAvailable) {
const updateStatus = nupst.getUpdateStatus();
logger.log('');
logger.log(
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.warning} ${theme.statusWarning(`Update available: v${updateStatus.latestVersion}`)}`,
);
logger.log(` ${theme.dim('Run')} ${theme.command('sudo nupst update')} ${theme.dim('to upgrade')}`);
} else {
logger.log('');
logger.log(
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.success} ${theme.success('Up to date')}`,
);
}
} catch (error) {
// If version check fails, show at least the current version
try {
const nupst = this.daemon.getNupstSnmp().getNupst();
const version = nupst.getVersion();
logger.log('');
logger.log(`${theme.dim('NUPST')} ${theme.dim('v' + version)}`);
} catch (_innerError) {
// Silently fail if we can't even get the version
}
}
}
public async getStatus(debugMode: boolean = false): Promise<void> {
2025-03-25 09:06:23 +00:00
try {
// Enable debug mode if requested
if (debugMode) {
console.log('');
logger.info('Debug Mode: SNMP debugging enabled');
console.log('');
this.daemon.getNupstSnmp().enableDebug();
}
// Display version and update status first
await this.displayVersionInfo();
// Check if config exists
2025-03-25 09:06:23 +00:00
try {
await this.checkConfigExists();
} catch (error) {
// Error message already displayed by checkConfigExists
return;
}
2025-03-25 09:06:23 +00:00
await this.displayServiceStatus();
await this.displayAllUpsStatus();
2025-03-25 09:06:23 +00:00
} catch (error) {
logger.error(
`Failed to get status: ${error instanceof Error ? error.message : String(error)}`,
);
2025-03-25 09:06:23 +00:00
}
}
/**
* Display the systemd service status
* @private
*/
private displayServiceStatus(): void {
2025-03-25 09:06:23 +00:00
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
logger.log('');
if (isActive) {
logger.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
} else {
logger.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)}`);
logger.log(` ${details.join(' ')}`);
}
logger.log('');
2025-03-25 09:06:23 +00:00
} catch (error) {
logger.log('');
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
logger.log('');
2025-03-25 09:06:23 +00:00
}
}
/**
* Display all UPS statuses
2025-03-25 09:06:23 +00:00
* @private
*/
private async displayAllUpsStatus(): Promise<void> {
2025-03-25 09:06:23 +00:00
try {
// Explicitly load the configuration first to ensure it's up-to-date
await this.daemon.loadConfig();
const config = this.daemon.getConfig();
2025-03-25 09:06:23 +00:00
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.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
logger.info('UPS Devices (1):');
const legacyUps = {
id: 'default',
name: 'Default UPS',
snmp: config.snmp,
thresholds: config.thresholds,
groups: [],
};
await this.displaySingleUpsStatus(legacyUps, snmp);
} else {
logger.log('');
logger.warn('No UPS devices configured');
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
logger.log('');
}
} catch (error) {
logger.log('');
logger.error('Failed to retrieve UPS status');
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
logger.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
logger.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;
logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
// Display host info
logger.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;
});
logger.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
}
logger.log('');
2025-03-25 09:06:23 +00:00
} catch (error) {
// Display error for this UPS
logger.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
logger.log('');
2025-03-25 09:06:23 +00:00
}
}
/**
* 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();
2025-03-25 09:06:23 +00:00
// Reload systemd daemon
execSync('systemctl daemon-reload');
logger.log('Systemd daemon reloaded');
logger.success('NUPST service has been successfully uninstalled');
2025-03-25 09:06:23 +00:00
} catch (error) {
logger.error(`Failed to disable and uninstall service: ${error}`);
2025-03-25 09:06:23 +00:00
throw error;
}
}
/**
* Stop the service if it's running
* @private
*/
private stopService(): void {
2025-03-25 09:06:23 +00:00
try {
logger.log('Stopping NUPST service...');
2025-03-25 09:06:23 +00:00
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');
2025-03-25 09:06:23 +00:00
}
}
/**
* Disable the service
* @private
*/
private disableService(): void {
2025-03-25 09:06:23 +00:00
try {
logger.log('Disabling NUPST service...');
2025-03-25 09:06:23 +00:00
execSync('systemctl disable nupst.service');
} catch (error) {
logger.log('Service was not enabled or could not be disabled');
2025-03-25 09:06:23 +00:00
}
}
/**
* 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}...`);
2025-03-25 09:06:23 +00:00
await fs.unlink(this.serviceFilePath);
logger.log('Service file removed');
2025-03-25 09:06:23 +00:00
} else {
logger.log('Service file did not exist');
2025-03-25 09:06:23 +00:00
}
}
}