2025-10-19 12:57:17 +00:00
|
|
|
import process from 'node:process';
|
2025-10-19 13:14:18 +00:00
|
|
|
import * as fs from 'node:fs';
|
|
|
|
import * as path from 'node:path';
|
|
|
|
import { exec, execFile } from 'node:child_process';
|
|
|
|
import { promisify } from 'node:util';
|
2025-10-18 11:59:55 +00:00
|
|
|
import { NupstSnmp } from './snmp/manager.ts';
|
|
|
|
import type { ISnmpConfig } from './snmp/types.ts';
|
|
|
|
import { logger } from './logger.ts';
|
2025-10-19 20:41:09 +00:00
|
|
|
import { MigrationRunner } from './migrations/index.ts';
|
2025-03-25 09:06:23 +00:00
|
|
|
|
2025-03-25 11:49:50 +00:00
|
|
|
const execAsync = promisify(exec);
|
2025-03-26 15:49:54 +00:00
|
|
|
const execFileAsync = promisify(execFile);
|
2025-03-25 11:49:50 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
/**
|
2025-03-28 16:19:43 +00:00
|
|
|
* UPS configuration interface
|
2025-03-25 09:06:23 +00:00
|
|
|
*/
|
2025-03-28 16:19:43 +00:00
|
|
|
export interface IUpsConfig {
|
|
|
|
/** Unique ID for the UPS */
|
|
|
|
id: string;
|
|
|
|
/** Friendly name for the UPS */
|
|
|
|
name: string;
|
2025-03-25 09:06:23 +00:00
|
|
|
/** SNMP configuration settings */
|
2025-03-25 10:21:21 +00:00
|
|
|
snmp: ISnmpConfig;
|
2025-03-25 09:06:23 +00:00
|
|
|
/** Threshold settings for initiating shutdown */
|
|
|
|
thresholds: {
|
|
|
|
/** Shutdown when battery below this percentage */
|
|
|
|
battery: number;
|
|
|
|
/** Shutdown when runtime below this minutes */
|
|
|
|
runtime: number;
|
|
|
|
};
|
2025-03-28 16:19:43 +00:00
|
|
|
/** Group IDs this UPS belongs to */
|
|
|
|
groups: string[];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Group configuration interface
|
|
|
|
*/
|
|
|
|
export interface IGroupConfig {
|
|
|
|
/** Unique ID for the group */
|
|
|
|
id: string;
|
|
|
|
/** Friendly name for the group */
|
|
|
|
name: string;
|
|
|
|
/** Group operation mode */
|
|
|
|
mode: 'redundant' | 'nonRedundant';
|
|
|
|
/** Optional description */
|
|
|
|
description?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Configuration interface for the daemon
|
|
|
|
*/
|
|
|
|
export interface INupstConfig {
|
2025-10-19 20:41:09 +00:00
|
|
|
/** Configuration format version */
|
|
|
|
version?: string;
|
2025-03-28 16:19:43 +00:00
|
|
|
/** UPS devices configuration */
|
|
|
|
upsDevices: IUpsConfig[];
|
|
|
|
/** Groups configuration */
|
|
|
|
groups: IGroupConfig[];
|
2025-03-25 09:06:23 +00:00
|
|
|
/** Check interval in milliseconds */
|
|
|
|
checkInterval: number;
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-10-19 20:41:09 +00:00
|
|
|
// Legacy fields for backward compatibility (will be migrated away)
|
|
|
|
/** UPS list (v3 format - legacy) */
|
|
|
|
upsList?: IUpsConfig[];
|
|
|
|
/** SNMP configuration settings (v1 format - legacy) */
|
2025-03-28 16:19:43 +00:00
|
|
|
snmp?: ISnmpConfig;
|
2025-10-19 20:41:09 +00:00
|
|
|
/** Threshold settings (v1 format - legacy) */
|
2025-03-28 16:19:43 +00:00
|
|
|
thresholds?: {
|
|
|
|
/** Shutdown when battery below this percentage */
|
|
|
|
battery: number;
|
|
|
|
/** Shutdown when runtime below this minutes */
|
|
|
|
runtime: number;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* UPS status tracking interface
|
|
|
|
*/
|
|
|
|
interface IUpsStatus {
|
|
|
|
id: string;
|
|
|
|
name: string;
|
|
|
|
powerStatus: 'online' | 'onBattery' | 'unknown';
|
|
|
|
batteryCapacity: number;
|
|
|
|
batteryRuntime: number;
|
|
|
|
lastStatusChange: number;
|
|
|
|
lastCheckTime: number;
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Daemon class for monitoring UPS and handling shutdown
|
|
|
|
* Responsible for loading/saving config and monitoring the UPS status
|
|
|
|
*/
|
|
|
|
export class NupstDaemon {
|
|
|
|
/** Default configuration path */
|
|
|
|
private readonly CONFIG_PATH = '/etc/nupst/config.json';
|
|
|
|
|
|
|
|
/** Default configuration */
|
2025-03-25 10:21:21 +00:00
|
|
|
private readonly DEFAULT_CONFIG: INupstConfig = {
|
2025-10-19 20:41:09 +00:00
|
|
|
version: '4.0',
|
2025-03-28 16:19:43 +00:00
|
|
|
upsDevices: [
|
|
|
|
{
|
|
|
|
id: 'default',
|
|
|
|
name: 'Default UPS',
|
|
|
|
snmp: {
|
|
|
|
host: '127.0.0.1',
|
|
|
|
port: 161,
|
|
|
|
community: 'public',
|
|
|
|
version: 1,
|
|
|
|
timeout: 5000,
|
|
|
|
// SNMPv3 defaults (used only if version === 3)
|
|
|
|
securityLevel: 'authPriv',
|
|
|
|
username: '',
|
|
|
|
authProtocol: 'SHA',
|
|
|
|
authKey: '',
|
|
|
|
privProtocol: 'AES',
|
|
|
|
privKey: '',
|
|
|
|
// UPS model for OID selection
|
2025-10-19 13:14:18 +00:00
|
|
|
upsModel: 'cyberpower',
|
2025-03-28 16:19:43 +00:00
|
|
|
},
|
|
|
|
thresholds: {
|
|
|
|
battery: 60, // Shutdown when battery below 60%
|
|
|
|
runtime: 20, // Shutdown when runtime below 20 minutes
|
|
|
|
},
|
2025-10-19 13:14:18 +00:00
|
|
|
groups: [],
|
|
|
|
},
|
2025-03-28 16:19:43 +00:00
|
|
|
],
|
|
|
|
groups: [],
|
2025-03-25 09:06:23 +00:00
|
|
|
checkInterval: 30000, // Check every 30 seconds
|
|
|
|
};
|
|
|
|
|
2025-03-25 10:21:21 +00:00
|
|
|
private config: INupstConfig;
|
2025-03-25 09:06:23 +00:00
|
|
|
private snmp: NupstSnmp;
|
|
|
|
private isRunning: boolean = false;
|
2025-03-28 16:19:43 +00:00
|
|
|
private upsStatus: Map<string, IUpsStatus> = new Map();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
/**
|
|
|
|
* Create a new daemon instance with the given SNMP manager
|
|
|
|
*/
|
|
|
|
constructor(snmp: NupstSnmp) {
|
|
|
|
this.snmp = snmp;
|
|
|
|
this.config = this.DEFAULT_CONFIG;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Load configuration from file
|
|
|
|
* @throws Error if configuration file doesn't exist
|
|
|
|
*/
|
2025-03-25 10:21:21 +00:00
|
|
|
public async loadConfig(): Promise<INupstConfig> {
|
2025-03-25 09:06:23 +00:00
|
|
|
try {
|
|
|
|
// Check if config file exists
|
|
|
|
const configExists = fs.existsSync(this.CONFIG_PATH);
|
|
|
|
if (!configExists) {
|
|
|
|
const errorMsg = `No configuration found at ${this.CONFIG_PATH}`;
|
|
|
|
this.logConfigError(errorMsg);
|
|
|
|
throw new Error(errorMsg);
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
// Read and parse config
|
|
|
|
const configData = fs.readFileSync(this.CONFIG_PATH, 'utf8');
|
2025-03-28 16:19:43 +00:00
|
|
|
const parsedConfig = JSON.parse(configData);
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-10-19 20:41:09 +00:00
|
|
|
// Run migrations to upgrade config format if needed
|
|
|
|
const migrationRunner = new MigrationRunner();
|
|
|
|
const { config: migratedConfig, migrated } = await migrationRunner.run(parsedConfig);
|
|
|
|
|
|
|
|
// Save migrated config back to disk if any migrations ran
|
|
|
|
if (migrated) {
|
|
|
|
this.config = migratedConfig;
|
2025-03-28 16:19:43 +00:00
|
|
|
await this.saveConfig(this.config);
|
|
|
|
} else {
|
2025-10-19 20:41:09 +00:00
|
|
|
this.config = migratedConfig;
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
return this.config;
|
|
|
|
} catch (error) {
|
2025-10-19 13:14:18 +00:00
|
|
|
if (
|
|
|
|
error instanceof Error && error.message && error.message.includes('No configuration found')
|
|
|
|
) {
|
2025-03-25 09:06:23 +00:00
|
|
|
throw error; // Re-throw the no configuration error
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
|
|
|
this.logConfigError(
|
|
|
|
`Error loading configuration: ${error instanceof Error ? error.message : String(error)}`,
|
|
|
|
);
|
2025-03-25 09:06:23 +00:00
|
|
|
throw new Error('Failed to load configuration');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Save configuration to file
|
|
|
|
*/
|
2025-10-19 13:25:01 +00:00
|
|
|
public saveConfig(config: INupstConfig): void {
|
2025-03-25 09:06:23 +00:00
|
|
|
try {
|
|
|
|
const configDir = path.dirname(this.CONFIG_PATH);
|
|
|
|
if (!fs.existsSync(configDir)) {
|
|
|
|
fs.mkdirSync(configDir, { recursive: true });
|
|
|
|
}
|
2025-10-19 20:41:09 +00:00
|
|
|
|
|
|
|
// Ensure version is always set and remove legacy fields before saving
|
|
|
|
const configToSave: INupstConfig = {
|
|
|
|
version: '4.0',
|
|
|
|
upsDevices: config.upsDevices,
|
|
|
|
groups: config.groups,
|
|
|
|
checkInterval: config.checkInterval,
|
|
|
|
};
|
|
|
|
|
|
|
|
fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(configToSave, null, 2));
|
|
|
|
this.config = configToSave;
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-10-20 00:32:06 +00:00
|
|
|
logger.logBox('Configuration Saved', [`Location: ${this.CONFIG_PATH}`], 45, 'success');
|
2025-03-25 09:06:23 +00:00
|
|
|
} catch (error) {
|
2025-10-20 00:32:06 +00:00
|
|
|
logger.error(`Error saving configuration: ${error}`);
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Helper method to log configuration errors consistently
|
|
|
|
*/
|
|
|
|
private logConfigError(message: string): void {
|
2025-10-20 00:32:06 +00:00
|
|
|
logger.logBox('Configuration Error', [message, "Please run 'nupst setup' first to create a configuration."], 45, 'error');
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the current configuration
|
|
|
|
*/
|
2025-03-25 10:21:21 +00:00
|
|
|
public getConfig(): INupstConfig {
|
2025-03-25 09:06:23 +00:00
|
|
|
return this.config;
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
/**
|
|
|
|
* Get the SNMP instance
|
|
|
|
*/
|
|
|
|
public getNupstSnmp(): NupstSnmp {
|
|
|
|
return this.snmp;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Start the monitoring daemon
|
|
|
|
*/
|
|
|
|
public async start(): Promise<void> {
|
|
|
|
if (this.isRunning) {
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log('Daemon is already running');
|
2025-03-25 09:06:23 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log('Starting NUPST daemon...');
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
try {
|
|
|
|
// Load configuration - this will throw an error if config doesn't exist
|
|
|
|
await this.loadConfig();
|
|
|
|
this.logConfigLoaded();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:27:44 +00:00
|
|
|
// Log version information
|
|
|
|
this.snmp.getNupst().logVersionInfo(false); // Don't check for updates immediately on startup
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:27:44 +00:00
|
|
|
// Check for updates in the background
|
2025-10-18 21:07:57 +00:00
|
|
|
this.snmp.getNupst().checkForUpdates().then((updateAvailable: boolean) => {
|
2025-03-25 09:27:44 +00:00
|
|
|
if (updateAvailable) {
|
|
|
|
const updateStatus = this.snmp.getNupst().getUpdateStatus();
|
2025-03-26 22:28:38 +00:00
|
|
|
const boxWidth = 45;
|
|
|
|
logger.logBoxTitle('Update Available', boxWidth);
|
|
|
|
logger.logBoxLine(`Current Version: ${updateStatus.currentVersion}`);
|
|
|
|
logger.logBoxLine(`Latest Version: ${updateStatus.latestVersion}`);
|
|
|
|
logger.logBoxLine('Run "sudo nupst update" to update');
|
|
|
|
logger.logBoxEnd();
|
2025-03-25 09:27:44 +00:00
|
|
|
}
|
|
|
|
}).catch(() => {}); // Ignore errors checking for updates
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Initialize UPS status tracking
|
|
|
|
this.initializeUpsStatus();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
// Start UPS monitoring
|
|
|
|
this.isRunning = true;
|
|
|
|
await this.monitor();
|
|
|
|
} catch (error) {
|
|
|
|
this.isRunning = false;
|
2025-10-19 13:14:18 +00:00
|
|
|
logger.error(
|
|
|
|
`Daemon failed to start: ${error instanceof Error ? error.message : String(error)}`,
|
|
|
|
);
|
2025-03-25 09:06:23 +00:00
|
|
|
process.exit(1); // Exit with error
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
/**
|
|
|
|
* Initialize UPS status tracking for all UPS devices
|
|
|
|
*/
|
|
|
|
private initializeUpsStatus(): void {
|
|
|
|
this.upsStatus.clear();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
|
|
|
|
for (const ups of this.config.upsDevices) {
|
|
|
|
this.upsStatus.set(ups.id, {
|
|
|
|
id: ups.id,
|
|
|
|
name: ups.name,
|
|
|
|
powerStatus: 'unknown',
|
|
|
|
batteryCapacity: 100,
|
|
|
|
batteryRuntime: 999, // High value as default
|
|
|
|
lastStatusChange: Date.now(),
|
2025-10-19 13:14:18 +00:00
|
|
|
lastCheckTime: 0,
|
2025-03-28 16:19:43 +00:00
|
|
|
});
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.log(`Initialized status tracking for ${this.config.upsDevices.length} UPS devices`);
|
|
|
|
} else {
|
|
|
|
logger.error('No UPS devices found in configuration');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
/**
|
|
|
|
* Log the loaded configuration settings
|
|
|
|
*/
|
|
|
|
private logConfigLoaded(): void {
|
2025-03-26 22:28:38 +00:00
|
|
|
const boxWidth = 50;
|
|
|
|
logger.logBoxTitle('Configuration Loaded', boxWidth);
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
|
|
|
|
logger.logBoxLine(`UPS Devices: ${this.config.upsDevices.length}`);
|
|
|
|
for (const ups of this.config.upsDevices) {
|
|
|
|
logger.logBoxLine(` - ${ups.name} (${ups.id}): ${ups.snmp.host}:${ups.snmp.port}`);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
logger.logBoxLine('No UPS devices configured');
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
if (this.config.groups && this.config.groups.length > 0) {
|
|
|
|
logger.logBoxLine(`Groups: ${this.config.groups.length}`);
|
|
|
|
for (const group of this.config.groups) {
|
|
|
|
logger.logBoxLine(` - ${group.name} (${group.id}): ${group.mode} mode`);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
logger.logBoxLine('No Groups configured');
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`);
|
|
|
|
logger.logBoxEnd();
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Stop the monitoring daemon
|
|
|
|
*/
|
|
|
|
public stop(): void {
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log('Stopping NUPST daemon...');
|
2025-03-25 09:06:23 +00:00
|
|
|
this.isRunning = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Monitor the UPS status and trigger shutdown when necessary
|
|
|
|
*/
|
|
|
|
private async monitor(): Promise<void> {
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log('Starting UPS monitoring...');
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
if (!this.config.upsDevices || this.config.upsDevices.length === 0) {
|
feat(cli): add beautiful colored output and fix daemon exit bug
Major improvements:
- Created color theme system (ts/colors.ts) with ANSI colors
- Enhanced logger with colors, table formatting, and styled boxes
- Fixed daemon exit bug - now stays running when no UPS configured
- Added config hot-reload with file watcher for live updates
- Beautified CLI help output with color-coded commands
- Added showcase test demonstrating all output features
- Fixed ANSI code handling for perfect table/box alignment
Features:
- Color-coded messages (success=green, error=red, warning=yellow, info=cyan)
- Status symbols (●○◐◯ for running/stopped/starting/unknown)
- Battery level colors (green>60%, yellow 30-60%, red<30%)
- Table formatting with auto-sizing and column alignment
- Styled boxes (success, error, warning, info styles)
- Hot-reload: daemon watches config file and reloads automatically
- Idle mode: daemon stays alive when no devices, checks periodically
Daemon improvements:
- No longer exits when no UPS devices configured
- Enters idle monitoring loop waiting for config
- File watcher detects config changes in real-time
- Auto-reloads and starts monitoring when devices added
- Logs warnings instead of errors for missing devices
Technical fixes:
- Strip ANSI codes when calculating text width for alignment
- Use visible length for padding calculations in tables and boxes
- Properly handle colored text in table cells and box lines
Breaking changes: None (backward compatible)
2025-10-19 15:08:30 +00:00
|
|
|
logger.warn('No UPS devices found in configuration. Daemon will remain idle...');
|
|
|
|
// Don't exit - enter idle monitoring mode instead
|
|
|
|
await this.idleMonitoring();
|
2025-03-28 16:19:43 +00:00
|
|
|
return;
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:20:55 +00:00
|
|
|
let lastLogTime = 0; // Track when we last logged status
|
|
|
|
const LOG_INTERVAL = 5 * 60 * 1000; // Log at least every 5 minutes (300000ms)
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
// Monitor continuously
|
|
|
|
while (this.isRunning) {
|
|
|
|
try {
|
2025-03-28 16:19:43 +00:00
|
|
|
// Check all UPS devices
|
|
|
|
await this.checkAllUpsDevices();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Log periodic status update
|
|
|
|
const currentTime = Date.now();
|
|
|
|
if (currentTime - lastLogTime >= LOG_INTERVAL) {
|
|
|
|
this.logAllUpsStatus();
|
2025-03-25 09:20:55 +00:00
|
|
|
lastLogTime = currentTime;
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Check if shutdown is required based on group configurations
|
|
|
|
await this.evaluateGroupShutdownConditions();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
// Wait before next check
|
|
|
|
await this.sleep(this.config.checkInterval);
|
|
|
|
} catch (error) {
|
2025-10-19 13:14:18 +00:00
|
|
|
logger.error(
|
|
|
|
`Error during UPS monitoring: ${error instanceof Error ? error.message : String(error)}`,
|
|
|
|
);
|
2025-03-25 09:06:23 +00:00
|
|
|
await this.sleep(this.config.checkInterval);
|
|
|
|
}
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.log('UPS monitoring stopped');
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
/**
|
|
|
|
* Check status of all UPS devices
|
|
|
|
*/
|
|
|
|
private async checkAllUpsDevices(): Promise<void> {
|
|
|
|
for (const ups of this.config.upsDevices) {
|
|
|
|
try {
|
|
|
|
const upsStatus = this.upsStatus.get(ups.id);
|
|
|
|
if (!upsStatus) {
|
|
|
|
// Initialize status for this UPS if not exists
|
|
|
|
this.upsStatus.set(ups.id, {
|
|
|
|
id: ups.id,
|
|
|
|
name: ups.name,
|
|
|
|
powerStatus: 'unknown',
|
|
|
|
batteryCapacity: 100,
|
|
|
|
batteryRuntime: 999,
|
|
|
|
lastStatusChange: Date.now(),
|
2025-10-19 13:14:18 +00:00
|
|
|
lastCheckTime: 0,
|
2025-03-28 16:19:43 +00:00
|
|
|
});
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Check UPS status
|
|
|
|
const status = await this.snmp.getUpsStatus(ups.snmp);
|
|
|
|
const currentTime = Date.now();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Get the current status from the map
|
|
|
|
const currentStatus = this.upsStatus.get(ups.id);
|
2025-10-18 21:07:57 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Update status with new values
|
2025-10-18 21:07:57 +00:00
|
|
|
const updatedStatus: IUpsStatus = {
|
|
|
|
id: ups.id,
|
|
|
|
name: ups.name,
|
2025-03-28 16:19:43 +00:00
|
|
|
powerStatus: status.powerStatus,
|
|
|
|
batteryCapacity: status.batteryCapacity,
|
|
|
|
batteryRuntime: status.batteryRuntime,
|
2025-10-18 21:07:57 +00:00
|
|
|
lastCheckTime: currentTime,
|
2025-10-19 13:14:18 +00:00
|
|
|
lastStatusChange: currentStatus?.lastStatusChange || currentTime,
|
2025-03-28 16:19:43 +00:00
|
|
|
};
|
2025-10-18 21:07:57 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Check if power status changed
|
2025-10-18 21:07:57 +00:00
|
|
|
if (currentStatus && currentStatus.powerStatus !== status.powerStatus) {
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.logBoxTitle(`Power Status Change: ${ups.name}`, 50);
|
|
|
|
logger.logBoxLine(`Status changed: ${currentStatus.powerStatus} → ${status.powerStatus}`);
|
|
|
|
logger.logBoxEnd();
|
2025-10-18 21:07:57 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
updatedStatus.lastStatusChange = currentTime;
|
|
|
|
}
|
2025-10-18 21:07:57 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Update the status in the map
|
|
|
|
this.upsStatus.set(ups.id, updatedStatus);
|
|
|
|
} catch (error) {
|
2025-10-19 13:14:18 +00:00
|
|
|
logger.error(
|
|
|
|
`Error checking UPS ${ups.name} (${ups.id}): ${
|
|
|
|
error instanceof Error ? error.message : String(error)
|
|
|
|
}`,
|
|
|
|
);
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
/**
|
2025-03-28 16:19:43 +00:00
|
|
|
* Log status of all UPS devices
|
2025-03-25 09:06:23 +00:00
|
|
|
*/
|
2025-03-28 16:19:43 +00:00
|
|
|
private logAllUpsStatus(): void {
|
|
|
|
const timestamp = new Date().toISOString();
|
|
|
|
const boxWidth = 60;
|
|
|
|
logger.logBoxTitle('Periodic Status Update', boxWidth);
|
|
|
|
logger.logBoxLine(`Timestamp: ${timestamp}`);
|
|
|
|
logger.logBoxLine('');
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
for (const [id, status] of this.upsStatus.entries()) {
|
|
|
|
logger.logBoxLine(`UPS: ${status.name} (${id})`);
|
|
|
|
logger.logBoxLine(` Power Status: ${status.powerStatus}`);
|
2025-10-19 13:14:18 +00:00
|
|
|
logger.logBoxLine(
|
|
|
|
` Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`,
|
|
|
|
);
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.logBoxLine('');
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.logBoxEnd();
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
/**
|
|
|
|
* Evaluate if shutdown is required based on group configurations
|
|
|
|
*/
|
|
|
|
private async evaluateGroupShutdownConditions(): Promise<void> {
|
|
|
|
if (!this.config.groups || this.config.groups.length === 0) {
|
|
|
|
// No groups defined, check individual UPS conditions
|
|
|
|
for (const [id, status] of this.upsStatus.entries()) {
|
|
|
|
if (status.powerStatus === 'onBattery') {
|
|
|
|
// Find the UPS config
|
2025-10-19 13:14:18 +00:00
|
|
|
const ups = this.config.upsDevices.find((u) => u.id === id);
|
2025-03-28 16:19:43 +00:00
|
|
|
if (ups) {
|
|
|
|
await this.evaluateUpsShutdownCondition(ups, status);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-03-25 09:06:23 +00:00
|
|
|
return;
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Evaluate each group
|
|
|
|
for (const group of this.config.groups) {
|
|
|
|
// Find all UPS devices in this group
|
2025-10-19 13:14:18 +00:00
|
|
|
const upsDevicesInGroup = this.config.upsDevices.filter((ups) =>
|
2025-03-28 16:19:43 +00:00
|
|
|
ups.groups && ups.groups.includes(group.id)
|
|
|
|
);
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
if (upsDevicesInGroup.length === 0) {
|
|
|
|
// No UPS devices in this group
|
|
|
|
continue;
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
if (group.mode === 'redundant') {
|
|
|
|
// Redundant mode: only shutdown if ALL UPS devices in the group are in critical condition
|
|
|
|
await this.evaluateRedundantGroup(group, upsDevicesInGroup);
|
|
|
|
} else {
|
|
|
|
// Non-redundant mode: shutdown if ANY UPS device in the group is in critical condition
|
|
|
|
await this.evaluateNonRedundantGroup(group, upsDevicesInGroup);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
/**
|
|
|
|
* Evaluate a redundant group for shutdown conditions
|
|
|
|
* In redundant mode, we only shut down if ALL UPS devices are in critical condition
|
|
|
|
*/
|
2025-10-19 13:14:18 +00:00
|
|
|
private async evaluateRedundantGroup(
|
|
|
|
group: IGroupConfig,
|
|
|
|
upsDevices: IUpsConfig[],
|
|
|
|
): Promise<void> {
|
2025-03-28 16:19:43 +00:00
|
|
|
// Count UPS devices on battery and in critical condition
|
|
|
|
let upsOnBattery = 0;
|
|
|
|
let upsInCriticalCondition = 0;
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
for (const ups of upsDevices) {
|
|
|
|
const status = this.upsStatus.get(ups.id);
|
|
|
|
if (!status) continue;
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
if (status.powerStatus === 'onBattery') {
|
|
|
|
upsOnBattery++;
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Check if this UPS is in critical condition
|
2025-10-19 13:14:18 +00:00
|
|
|
if (
|
|
|
|
status.batteryCapacity < ups.thresholds.battery ||
|
|
|
|
status.batteryRuntime < ups.thresholds.runtime
|
|
|
|
) {
|
2025-03-28 16:19:43 +00:00
|
|
|
upsInCriticalCondition++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// All UPS devices must be online for a redundant group to be considered healthy
|
|
|
|
const allUpsCount = upsDevices.length;
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// If all UPS are on battery and in critical condition, shutdown
|
|
|
|
if (upsOnBattery === allUpsCount && upsInCriticalCondition === allUpsCount) {
|
|
|
|
logger.logBoxTitle(`Group Shutdown Required: ${group.name}`, 50);
|
|
|
|
logger.logBoxLine(`Mode: Redundant`);
|
|
|
|
logger.logBoxLine(`All ${allUpsCount} UPS devices in critical condition`);
|
|
|
|
logger.logBoxEnd();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
|
|
|
await this.initiateShutdown(
|
|
|
|
`All UPS devices in redundant group "${group.name}" in critical condition`,
|
|
|
|
);
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
/**
|
|
|
|
* Evaluate a non-redundant group for shutdown conditions
|
|
|
|
* In non-redundant mode, we shut down if ANY UPS device is in critical condition
|
|
|
|
*/
|
2025-10-19 13:14:18 +00:00
|
|
|
private async evaluateNonRedundantGroup(
|
|
|
|
group: IGroupConfig,
|
|
|
|
upsDevices: IUpsConfig[],
|
|
|
|
): Promise<void> {
|
2025-03-28 16:19:43 +00:00
|
|
|
for (const ups of upsDevices) {
|
|
|
|
const status = this.upsStatus.get(ups.id);
|
|
|
|
if (!status) continue;
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
if (status.powerStatus === 'onBattery') {
|
|
|
|
// Check if this UPS is in critical condition
|
2025-10-19 13:14:18 +00:00
|
|
|
if (
|
|
|
|
status.batteryCapacity < ups.thresholds.battery ||
|
|
|
|
status.batteryRuntime < ups.thresholds.runtime
|
|
|
|
) {
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.logBoxTitle(`Group Shutdown Required: ${group.name}`, 50);
|
|
|
|
logger.logBoxLine(`Mode: Non-Redundant`);
|
|
|
|
logger.logBoxLine(`UPS ${ups.name} in critical condition`);
|
2025-10-19 13:14:18 +00:00
|
|
|
logger.logBoxLine(
|
|
|
|
`Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`,
|
|
|
|
);
|
|
|
|
logger.logBoxLine(
|
|
|
|
`Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`,
|
|
|
|
);
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.logBoxEnd();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
|
|
|
await this.initiateShutdown(
|
|
|
|
`UPS "${ups.name}" in non-redundant group "${group.name}" in critical condition`,
|
|
|
|
);
|
2025-03-28 16:19:43 +00:00
|
|
|
return; // Exit after initiating shutdown
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
/**
|
|
|
|
* Evaluate an individual UPS for shutdown conditions
|
|
|
|
*/
|
|
|
|
private async evaluateUpsShutdownCondition(ups: IUpsConfig, status: IUpsStatus): Promise<void> {
|
|
|
|
// Only evaluate UPS devices not in any group
|
|
|
|
if (ups.groups && ups.groups.length > 0) {
|
2025-03-25 09:06:23 +00:00
|
|
|
return;
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Check threshold conditions
|
2025-10-19 13:14:18 +00:00
|
|
|
if (
|
|
|
|
status.batteryCapacity < ups.thresholds.battery ||
|
|
|
|
status.batteryRuntime < ups.thresholds.runtime
|
|
|
|
) {
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.logBoxTitle(`UPS Shutdown Required: ${ups.name}`, 50);
|
2025-10-19 13:14:18 +00:00
|
|
|
logger.logBoxLine(
|
|
|
|
`Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`,
|
|
|
|
);
|
|
|
|
logger.logBoxLine(
|
|
|
|
`Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`,
|
|
|
|
);
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.logBoxEnd();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
await this.initiateShutdown(`UPS "${ups.name}" battery or runtime below threshold`);
|
|
|
|
}
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 11:49:50 +00:00
|
|
|
/**
|
|
|
|
* Initiate system shutdown with UPS monitoring during shutdown
|
|
|
|
* @param reason Reason for shutdown
|
|
|
|
*/
|
|
|
|
public async initiateShutdown(reason: string): Promise<void> {
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log(`Initiating system shutdown due to: ${reason}`);
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 11:49:50 +00:00
|
|
|
// Set a longer delay for shutdown to allow VMs and services to close
|
|
|
|
const shutdownDelayMinutes = 5;
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 11:49:50 +00:00
|
|
|
try {
|
2025-03-26 15:49:54 +00:00
|
|
|
// Find shutdown command in common system paths
|
|
|
|
const shutdownPaths = [
|
|
|
|
'/sbin/shutdown',
|
|
|
|
'/usr/sbin/shutdown',
|
|
|
|
'/bin/shutdown',
|
2025-10-19 13:14:18 +00:00
|
|
|
'/usr/bin/shutdown',
|
2025-03-26 15:49:54 +00:00
|
|
|
];
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-26 15:49:54 +00:00
|
|
|
let shutdownCmd = '';
|
|
|
|
for (const path of shutdownPaths) {
|
|
|
|
try {
|
|
|
|
if (fs.existsSync(path)) {
|
|
|
|
shutdownCmd = path;
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log(`Found shutdown command at: ${shutdownCmd}`);
|
2025-03-26 15:49:54 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
} catch (e) {
|
|
|
|
// Continue checking other paths
|
|
|
|
}
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-26 15:49:54 +00:00
|
|
|
if (shutdownCmd) {
|
|
|
|
// Execute shutdown command with delay to allow for VM graceful shutdown
|
2025-10-19 13:14:18 +00:00
|
|
|
logger.log(
|
|
|
|
`Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`,
|
|
|
|
);
|
2025-03-26 15:49:54 +00:00
|
|
|
const { stdout } = await execFileAsync(shutdownCmd, [
|
2025-10-19 13:14:18 +00:00
|
|
|
'-h',
|
|
|
|
`+${shutdownDelayMinutes}`,
|
|
|
|
`UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes`,
|
2025-03-26 15:49:54 +00:00
|
|
|
]);
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log(`Shutdown initiated: ${stdout}`);
|
|
|
|
logger.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`);
|
2025-03-26 15:49:54 +00:00
|
|
|
} else {
|
|
|
|
// Try using the PATH to find shutdown
|
|
|
|
try {
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log('Shutdown command not found in common paths, trying via PATH...');
|
2025-10-19 13:14:18 +00:00
|
|
|
const { stdout } = await execAsync(
|
|
|
|
`shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`,
|
|
|
|
{
|
|
|
|
env: process.env, // Pass the current environment
|
|
|
|
},
|
|
|
|
);
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log(`Shutdown initiated: ${stdout}`);
|
2025-03-26 15:49:54 +00:00
|
|
|
} catch (e) {
|
2025-10-19 13:14:18 +00:00
|
|
|
throw new Error(
|
|
|
|
`Shutdown command not found: ${e instanceof Error ? e.message : String(e)}`,
|
|
|
|
);
|
2025-03-26 15:49:54 +00:00
|
|
|
}
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 11:49:50 +00:00
|
|
|
// Monitor UPS during shutdown and force immediate shutdown if battery gets too low
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log('Monitoring UPS during shutdown process...');
|
2025-03-25 11:49:50 +00:00
|
|
|
await this.monitorDuringShutdown();
|
|
|
|
} catch (error) {
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.error(`Failed to initiate shutdown: ${error}`);
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-26 15:49:54 +00:00
|
|
|
// Try alternative shutdown methods
|
|
|
|
const alternatives = [
|
|
|
|
{ cmd: 'poweroff', args: ['--force'] },
|
|
|
|
{ cmd: 'halt', args: ['-p'] },
|
|
|
|
{ cmd: 'systemctl', args: ['poweroff'] },
|
2025-10-19 13:14:18 +00:00
|
|
|
{ cmd: 'reboot', args: ['-p'] }, // Some systems allow reboot -p for power off
|
2025-03-26 15:49:54 +00:00
|
|
|
];
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-26 15:49:54 +00:00
|
|
|
for (const alt of alternatives) {
|
|
|
|
try {
|
|
|
|
// First check if command exists in common system paths
|
|
|
|
const paths = [
|
|
|
|
`/sbin/${alt.cmd}`,
|
|
|
|
`/usr/sbin/${alt.cmd}`,
|
|
|
|
`/bin/${alt.cmd}`,
|
2025-10-19 13:14:18 +00:00
|
|
|
`/usr/bin/${alt.cmd}`,
|
2025-03-26 15:49:54 +00:00
|
|
|
];
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-26 15:49:54 +00:00
|
|
|
let cmdPath = '';
|
|
|
|
for (const path of paths) {
|
|
|
|
if (fs.existsSync(path)) {
|
|
|
|
cmdPath = path;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-26 15:49:54 +00:00
|
|
|
if (cmdPath) {
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`);
|
2025-03-26 15:49:54 +00:00
|
|
|
await execFileAsync(cmdPath, alt.args);
|
|
|
|
return; // Exit if successful
|
|
|
|
} else {
|
|
|
|
// Try using PATH environment
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.log(`Trying alternative via PATH: ${alt.cmd} ${alt.args.join(' ')}`);
|
2025-03-26 15:49:54 +00:00
|
|
|
await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
|
2025-10-19 13:14:18 +00:00
|
|
|
env: process.env, // Pass the current environment
|
2025-03-26 15:49:54 +00:00
|
|
|
});
|
|
|
|
return; // Exit if successful
|
|
|
|
}
|
|
|
|
} catch (altError) {
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.error(`Alternative method ${alt.cmd} failed: ${altError}`);
|
2025-03-26 15:49:54 +00:00
|
|
|
// Continue to next method
|
|
|
|
}
|
2025-03-25 11:49:50 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-26 22:28:38 +00:00
|
|
|
logger.error('All shutdown methods failed');
|
2025-03-25 11:49:50 +00:00
|
|
|
}
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 11:49:50 +00:00
|
|
|
/**
|
|
|
|
* Monitor UPS during system shutdown
|
2025-03-28 16:19:43 +00:00
|
|
|
* Force immediate shutdown if any UPS gets critically low
|
2025-03-25 11:49:50 +00:00
|
|
|
*/
|
|
|
|
private async monitorDuringShutdown(): Promise<void> {
|
|
|
|
const EMERGENCY_RUNTIME_THRESHOLD = 5; // 5 minutes remaining is critical
|
|
|
|
const CHECK_INTERVAL = 30000; // Check every 30 seconds during shutdown
|
|
|
|
const MAX_MONITORING_TIME = 5 * 60 * 1000; // Max 5 minutes of monitoring
|
|
|
|
const startTime = Date.now();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
|
|
|
logger.log(
|
|
|
|
`Emergency shutdown threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes remaining battery runtime`,
|
|
|
|
);
|
|
|
|
|
2025-03-25 11:49:50 +00:00
|
|
|
// Continue monitoring until max monitoring time is reached
|
|
|
|
while (Date.now() - startTime < MAX_MONITORING_TIME) {
|
|
|
|
try {
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.log('Checking UPS status during shutdown...');
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Check all UPS devices
|
|
|
|
for (const ups of this.config.upsDevices) {
|
2025-03-25 11:49:50 +00:00
|
|
|
try {
|
2025-03-28 16:19:43 +00:00
|
|
|
const status = await this.snmp.getUpsStatus(ups.snmp);
|
2025-10-19 13:14:18 +00:00
|
|
|
|
|
|
|
logger.log(
|
|
|
|
`UPS ${ups.name}: Battery ${status.batteryCapacity}%, Runtime: ${status.batteryRuntime} minutes`,
|
|
|
|
);
|
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// If any UPS battery runtime gets critically low, force immediate shutdown
|
|
|
|
if (status.batteryRuntime < EMERGENCY_RUNTIME_THRESHOLD) {
|
|
|
|
logger.logBoxTitle('EMERGENCY SHUTDOWN', 50);
|
2025-10-19 13:14:18 +00:00
|
|
|
logger.logBoxLine(
|
|
|
|
`UPS ${ups.name} runtime critically low: ${status.batteryRuntime} minutes`,
|
|
|
|
);
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.logBoxLine('Forcing immediate shutdown!');
|
|
|
|
logger.logBoxEnd();
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Force immediate shutdown
|
|
|
|
await this.forceImmediateShutdown();
|
|
|
|
return;
|
2025-03-26 15:49:54 +00:00
|
|
|
}
|
2025-03-28 16:19:43 +00:00
|
|
|
} catch (upsError) {
|
2025-10-19 13:14:18 +00:00
|
|
|
logger.error(
|
|
|
|
`Error checking UPS ${ups.name} during shutdown: ${
|
|
|
|
upsError instanceof Error ? upsError.message : String(upsError)
|
|
|
|
}`,
|
|
|
|
);
|
2025-03-25 11:49:50 +00:00
|
|
|
}
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-25 11:49:50 +00:00
|
|
|
// Wait before checking again
|
|
|
|
await this.sleep(CHECK_INTERVAL);
|
|
|
|
} catch (error) {
|
2025-10-19 13:14:18 +00:00
|
|
|
logger.error(
|
|
|
|
`Error monitoring UPS during shutdown: ${
|
|
|
|
error instanceof Error ? error.message : String(error)
|
|
|
|
}`,
|
|
|
|
);
|
2025-03-25 11:49:50 +00:00
|
|
|
await this.sleep(CHECK_INTERVAL);
|
|
|
|
}
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.log('UPS monitoring during shutdown completed');
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
/**
|
|
|
|
* Force an immediate system shutdown
|
|
|
|
*/
|
|
|
|
private async forceImmediateShutdown(): Promise<void> {
|
|
|
|
try {
|
|
|
|
// Find shutdown command in common system paths
|
|
|
|
const shutdownPaths = [
|
|
|
|
'/sbin/shutdown',
|
|
|
|
'/usr/sbin/shutdown',
|
|
|
|
'/bin/shutdown',
|
2025-10-19 13:14:18 +00:00
|
|
|
'/usr/bin/shutdown',
|
2025-03-28 16:19:43 +00:00
|
|
|
];
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
let shutdownCmd = '';
|
|
|
|
for (const path of shutdownPaths) {
|
|
|
|
if (fs.existsSync(path)) {
|
|
|
|
shutdownCmd = path;
|
|
|
|
logger.log(`Found shutdown command at: ${shutdownCmd}`);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
if (shutdownCmd) {
|
|
|
|
logger.log(`Executing emergency shutdown: ${shutdownCmd} -h now`);
|
2025-10-19 13:14:18 +00:00
|
|
|
await execFileAsync(shutdownCmd, [
|
|
|
|
'-h',
|
|
|
|
'now',
|
|
|
|
'EMERGENCY: UPS battery critically low, shutting down NOW',
|
|
|
|
]);
|
2025-03-28 16:19:43 +00:00
|
|
|
} else {
|
|
|
|
// Try using the PATH to find shutdown
|
|
|
|
logger.log('Shutdown command not found in common paths, trying via PATH...');
|
2025-10-19 13:14:18 +00:00
|
|
|
await execAsync(
|
|
|
|
'shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"',
|
|
|
|
{
|
|
|
|
env: process.env, // Pass the current environment
|
|
|
|
},
|
|
|
|
);
|
2025-03-28 16:19:43 +00:00
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
logger.error('Emergency shutdown failed, trying alternative methods...');
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
// Try alternative shutdown methods in sequence
|
|
|
|
const alternatives = [
|
|
|
|
{ cmd: 'poweroff', args: ['--force'] },
|
|
|
|
{ cmd: 'halt', args: ['-p'] },
|
2025-10-19 13:14:18 +00:00
|
|
|
{ cmd: 'systemctl', args: ['poweroff'] },
|
2025-03-28 16:19:43 +00:00
|
|
|
];
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
for (const alt of alternatives) {
|
|
|
|
try {
|
|
|
|
// Check common paths
|
|
|
|
const paths = [
|
|
|
|
`/sbin/${alt.cmd}`,
|
|
|
|
`/usr/sbin/${alt.cmd}`,
|
|
|
|
`/bin/${alt.cmd}`,
|
2025-10-19 13:14:18 +00:00
|
|
|
`/usr/bin/${alt.cmd}`,
|
2025-03-28 16:19:43 +00:00
|
|
|
];
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
let cmdPath = '';
|
|
|
|
for (const path of paths) {
|
|
|
|
if (fs.existsSync(path)) {
|
|
|
|
cmdPath = path;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
if (cmdPath) {
|
|
|
|
logger.log(`Emergency: using ${cmdPath} ${alt.args.join(' ')}`);
|
|
|
|
await execFileAsync(cmdPath, alt.args);
|
|
|
|
return; // Exit if successful
|
|
|
|
} else {
|
|
|
|
// Try using PATH
|
|
|
|
logger.log(`Emergency: trying ${alt.cmd} via PATH`);
|
|
|
|
await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
|
2025-10-19 13:14:18 +00:00
|
|
|
env: process.env,
|
2025-03-28 16:19:43 +00:00
|
|
|
});
|
|
|
|
return; // Exit if successful
|
|
|
|
}
|
|
|
|
} catch (altError) {
|
|
|
|
// Continue to next method
|
|
|
|
}
|
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
|
2025-03-28 16:19:43 +00:00
|
|
|
logger.error('All emergency shutdown methods failed');
|
|
|
|
}
|
2025-03-25 11:49:50 +00:00
|
|
|
}
|
2025-03-25 09:06:23 +00:00
|
|
|
|
feat(cli): add beautiful colored output and fix daemon exit bug
Major improvements:
- Created color theme system (ts/colors.ts) with ANSI colors
- Enhanced logger with colors, table formatting, and styled boxes
- Fixed daemon exit bug - now stays running when no UPS configured
- Added config hot-reload with file watcher for live updates
- Beautified CLI help output with color-coded commands
- Added showcase test demonstrating all output features
- Fixed ANSI code handling for perfect table/box alignment
Features:
- Color-coded messages (success=green, error=red, warning=yellow, info=cyan)
- Status symbols (●○◐◯ for running/stopped/starting/unknown)
- Battery level colors (green>60%, yellow 30-60%, red<30%)
- Table formatting with auto-sizing and column alignment
- Styled boxes (success, error, warning, info styles)
- Hot-reload: daemon watches config file and reloads automatically
- Idle mode: daemon stays alive when no devices, checks periodically
Daemon improvements:
- No longer exits when no UPS devices configured
- Enters idle monitoring loop waiting for config
- File watcher detects config changes in real-time
- Auto-reloads and starts monitoring when devices added
- Logs warnings instead of errors for missing devices
Technical fixes:
- Strip ANSI codes when calculating text width for alignment
- Use visible length for padding calculations in tables and boxes
- Properly handle colored text in table cells and box lines
Breaking changes: None (backward compatible)
2025-10-19 15:08:30 +00:00
|
|
|
/**
|
|
|
|
* Idle monitoring loop when no UPS devices are configured
|
|
|
|
* Watches for config changes and reloads when detected
|
|
|
|
*/
|
|
|
|
private async idleMonitoring(): Promise<void> {
|
|
|
|
const IDLE_CHECK_INTERVAL = 60000; // Check every 60 seconds
|
|
|
|
let lastConfigCheck = Date.now();
|
|
|
|
const CONFIG_CHECK_INTERVAL = 60000; // Check config every minute
|
|
|
|
|
|
|
|
logger.log('Entering idle monitoring mode...');
|
|
|
|
logger.log('Daemon will check for config changes every 60 seconds');
|
|
|
|
|
|
|
|
// Start file watcher for hot-reload
|
|
|
|
this.watchConfigFile();
|
|
|
|
|
|
|
|
while (this.isRunning) {
|
|
|
|
try {
|
|
|
|
const currentTime = Date.now();
|
|
|
|
|
|
|
|
// Periodically check if config has been updated
|
|
|
|
if (currentTime - lastConfigCheck >= CONFIG_CHECK_INTERVAL) {
|
|
|
|
try {
|
|
|
|
// Try to load config
|
|
|
|
const newConfig = await this.loadConfig();
|
|
|
|
|
|
|
|
// Check if we now have UPS devices configured
|
|
|
|
if (newConfig.upsDevices && newConfig.upsDevices.length > 0) {
|
|
|
|
logger.success('Configuration updated! UPS devices found. Starting monitoring...');
|
|
|
|
this.initializeUpsStatus();
|
|
|
|
// Exit idle mode and start monitoring
|
|
|
|
await this.monitor();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
// Config still doesn't exist or invalid, continue waiting
|
|
|
|
}
|
|
|
|
|
|
|
|
lastConfigCheck = currentTime;
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.sleep(IDLE_CHECK_INTERVAL);
|
|
|
|
} catch (error) {
|
|
|
|
logger.error(
|
|
|
|
`Error during idle monitoring: ${error instanceof Error ? error.message : String(error)}`,
|
|
|
|
);
|
|
|
|
await this.sleep(IDLE_CHECK_INTERVAL);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
logger.log('Idle monitoring stopped');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Watch config file for changes and reload automatically
|
|
|
|
*/
|
|
|
|
private watchConfigFile(): void {
|
|
|
|
try {
|
|
|
|
// Use Deno's file watcher to monitor config file
|
|
|
|
const configDir = path.dirname(this.CONFIG_PATH);
|
|
|
|
|
|
|
|
// Spawn a background watcher (non-blocking)
|
|
|
|
(async () => {
|
|
|
|
try {
|
|
|
|
const watcher = Deno.watchFs(configDir);
|
|
|
|
|
|
|
|
logger.log('Config file watcher started');
|
|
|
|
|
|
|
|
for await (const event of watcher) {
|
|
|
|
// Only respond to modify events on the config file
|
|
|
|
if (
|
|
|
|
event.kind === 'modify' &&
|
|
|
|
event.paths.some((p) => p.includes('config.json'))
|
|
|
|
) {
|
|
|
|
logger.info('Config file changed, reloading...');
|
|
|
|
await this.reloadConfig();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Stop watching if daemon stopped
|
|
|
|
if (!this.isRunning) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
// Watcher error - not critical, just log it
|
|
|
|
logger.dim(
|
|
|
|
`Config watcher stopped: ${error instanceof Error ? error.message : String(error)}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
})();
|
|
|
|
} catch (error) {
|
|
|
|
// If we can't start the watcher, just log and continue
|
|
|
|
// The periodic check will still work
|
|
|
|
logger.dim('Could not start config file watcher, using periodic checks only');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Reload configuration and restart monitoring if needed
|
|
|
|
*/
|
|
|
|
private async reloadConfig(): Promise<void> {
|
|
|
|
try {
|
|
|
|
const oldDeviceCount = this.config.upsDevices?.length || 0;
|
|
|
|
|
|
|
|
// Load the new configuration
|
|
|
|
await this.loadConfig();
|
|
|
|
const newDeviceCount = this.config.upsDevices?.length || 0;
|
|
|
|
|
|
|
|
if (newDeviceCount > 0 && oldDeviceCount === 0) {
|
|
|
|
logger.success(`Configuration reloaded! Found ${newDeviceCount} UPS device(s)`);
|
|
|
|
logger.info('Monitoring will start automatically...');
|
|
|
|
} else if (newDeviceCount !== oldDeviceCount) {
|
|
|
|
logger.success(
|
|
|
|
`Configuration reloaded! UPS devices: ${oldDeviceCount} → ${newDeviceCount}`,
|
|
|
|
);
|
|
|
|
|
|
|
|
// Reinitialize UPS status tracking
|
|
|
|
this.initializeUpsStatus();
|
|
|
|
} else {
|
|
|
|
logger.success('Configuration reloaded successfully');
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
logger.warn(
|
|
|
|
`Failed to reload config: ${error instanceof Error ? error.message : String(error)}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-25 09:06:23 +00:00
|
|
|
/**
|
|
|
|
* Sleep for the specified milliseconds
|
|
|
|
*/
|
|
|
|
private sleep(ms: number): Promise<void> {
|
2025-10-19 13:14:18 +00:00
|
|
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
2025-03-25 09:06:23 +00:00
|
|
|
}
|
2025-10-19 13:14:18 +00:00
|
|
|
}
|