BREAKING CHANGE(core): Add multi-UPS support and group management; update CLI, configuration and documentation to support multiple UPS devices with group modes
This commit is contained in:
637
ts/daemon.ts
637
ts/daemon.ts
@@ -10,9 +10,13 @@ const execAsync = promisify(exec);
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
/**
|
||||
* Configuration interface for the daemon
|
||||
* UPS configuration interface
|
||||
*/
|
||||
export interface INupstConfig {
|
||||
export interface IUpsConfig {
|
||||
/** Unique ID for the UPS */
|
||||
id: string;
|
||||
/** Friendly name for the UPS */
|
||||
name: string;
|
||||
/** SNMP configuration settings */
|
||||
snmp: ISnmpConfig;
|
||||
/** Threshold settings for initiating shutdown */
|
||||
@@ -22,8 +26,58 @@ export interface INupstConfig {
|
||||
/** Shutdown when runtime below this minutes */
|
||||
runtime: number;
|
||||
};
|
||||
/** 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 {
|
||||
/** UPS devices configuration */
|
||||
upsDevices: IUpsConfig[];
|
||||
/** Groups configuration */
|
||||
groups: IGroupConfig[];
|
||||
/** Check interval in milliseconds */
|
||||
checkInterval: number;
|
||||
|
||||
// Legacy fields for backward compatibility
|
||||
/** SNMP configuration settings (legacy) */
|
||||
snmp?: ISnmpConfig;
|
||||
/** Threshold settings (legacy) */
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,32 +90,41 @@ export class NupstDaemon {
|
||||
|
||||
/** Default configuration */
|
||||
private readonly DEFAULT_CONFIG: INupstConfig = {
|
||||
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
|
||||
upsModel: 'cyberpower'
|
||||
},
|
||||
thresholds: {
|
||||
battery: 60, // Shutdown when battery below 60%
|
||||
runtime: 20, // Shutdown when runtime below 20 minutes
|
||||
},
|
||||
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
|
||||
upsModel: 'cyberpower'
|
||||
},
|
||||
thresholds: {
|
||||
battery: 60, // Shutdown when battery below 60%
|
||||
runtime: 20, // Shutdown when runtime below 20 minutes
|
||||
},
|
||||
groups: []
|
||||
}
|
||||
],
|
||||
groups: [],
|
||||
checkInterval: 30000, // Check every 30 seconds
|
||||
};
|
||||
|
||||
private config: INupstConfig;
|
||||
private snmp: NupstSnmp;
|
||||
private isRunning: boolean = false;
|
||||
private upsStatus: Map<string, IUpsStatus> = new Map();
|
||||
|
||||
/**
|
||||
* Create a new daemon instance with the given SNMP manager
|
||||
@@ -87,10 +150,36 @@ export class NupstDaemon {
|
||||
|
||||
// Read and parse config
|
||||
const configData = fs.readFileSync(this.CONFIG_PATH, 'utf8');
|
||||
this.config = JSON.parse(configData);
|
||||
const parsedConfig = JSON.parse(configData);
|
||||
|
||||
// Handle legacy configuration format
|
||||
if (!parsedConfig.upsDevices && parsedConfig.snmp) {
|
||||
// Convert legacy format to new format
|
||||
this.config = {
|
||||
upsDevices: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default UPS',
|
||||
snmp: parsedConfig.snmp,
|
||||
thresholds: parsedConfig.thresholds,
|
||||
groups: []
|
||||
}
|
||||
],
|
||||
groups: [],
|
||||
checkInterval: parsedConfig.checkInterval
|
||||
};
|
||||
|
||||
logger.log('Legacy configuration format detected. Converting to multi-UPS format.');
|
||||
|
||||
// Save the new format
|
||||
await this.saveConfig(this.config);
|
||||
} else {
|
||||
this.config = parsedConfig;
|
||||
}
|
||||
|
||||
return this.config;
|
||||
} catch (error) {
|
||||
if (error.message.includes('No configuration found')) {
|
||||
if (error.message && error.message.includes('No configuration found')) {
|
||||
throw error; // Re-throw the no configuration error
|
||||
}
|
||||
|
||||
@@ -175,6 +264,9 @@ export class NupstDaemon {
|
||||
}
|
||||
}).catch(() => {}); // Ignore errors checking for updates
|
||||
|
||||
// Initialize UPS status tracking
|
||||
this.initializeUpsStatus();
|
||||
|
||||
// Start UPS monitoring
|
||||
this.isRunning = true;
|
||||
await this.monitor();
|
||||
@@ -185,19 +277,56 @@ export class NupstDaemon {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize UPS status tracking for all UPS devices
|
||||
*/
|
||||
private initializeUpsStatus(): void {
|
||||
this.upsStatus.clear();
|
||||
|
||||
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(),
|
||||
lastCheckTime: 0
|
||||
});
|
||||
}
|
||||
|
||||
logger.log(`Initialized status tracking for ${this.config.upsDevices.length} UPS devices`);
|
||||
} else {
|
||||
logger.error('No UPS devices found in configuration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the loaded configuration settings
|
||||
*/
|
||||
private logConfigLoaded(): void {
|
||||
const boxWidth = 50;
|
||||
logger.logBoxTitle('Configuration Loaded', boxWidth);
|
||||
logger.logBoxLine('SNMP Settings:');
|
||||
logger.logBoxLine(` Host: ${this.config.snmp.host}`);
|
||||
logger.logBoxLine(` Port: ${this.config.snmp.port}`);
|
||||
logger.logBoxLine(` Version: ${this.config.snmp.version}`);
|
||||
logger.logBoxLine('Thresholds:');
|
||||
logger.logBoxLine(` Battery: ${this.config.thresholds.battery}%`);
|
||||
logger.logBoxLine(` Runtime: ${this.config.thresholds.runtime} minutes`);
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`);
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
@@ -216,81 +345,239 @@ export class NupstDaemon {
|
||||
private async monitor(): Promise<void> {
|
||||
logger.log('Starting UPS monitoring...');
|
||||
|
||||
let lastStatus: 'online' | 'onBattery' | 'unknown' = 'unknown';
|
||||
if (!this.config.upsDevices || this.config.upsDevices.length === 0) {
|
||||
logger.error('No UPS devices found in configuration. Monitoring stopped.');
|
||||
this.isRunning = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let lastLogTime = 0; // Track when we last logged status
|
||||
const LOG_INTERVAL = 5 * 60 * 1000; // Log at least every 5 minutes (300000ms)
|
||||
|
||||
// Monitor continuously
|
||||
while (this.isRunning) {
|
||||
try {
|
||||
const status = await this.snmp.getUpsStatus(this.config.snmp);
|
||||
const currentTime = Date.now();
|
||||
const shouldLogStatus = (currentTime - lastLogTime) >= LOG_INTERVAL;
|
||||
// Check all UPS devices
|
||||
await this.checkAllUpsDevices();
|
||||
|
||||
// Log status changes
|
||||
if (status.powerStatus !== lastStatus) {
|
||||
const statusBoxWidth = 45;
|
||||
logger.logBoxTitle('Power Status Change', statusBoxWidth);
|
||||
logger.logBoxLine(`Status changed: ${lastStatus} → ${status.powerStatus}`);
|
||||
logger.logBoxEnd();
|
||||
lastStatus = status.powerStatus;
|
||||
lastLogTime = currentTime; // Reset log timer when status changes
|
||||
}
|
||||
// Log status periodically (at least every 5 minutes)
|
||||
else if (shouldLogStatus) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const periodicBoxWidth = 45;
|
||||
logger.logBoxTitle('Periodic Status Update', periodicBoxWidth);
|
||||
logger.logBoxLine(`Timestamp: ${timestamp}`);
|
||||
logger.logBoxLine(`Power Status: ${status.powerStatus}`);
|
||||
logger.logBoxLine(`Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
|
||||
logger.logBoxEnd();
|
||||
// Log periodic status update
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastLogTime >= LOG_INTERVAL) {
|
||||
this.logAllUpsStatus();
|
||||
lastLogTime = currentTime;
|
||||
}
|
||||
|
||||
// Handle battery power status
|
||||
if (status.powerStatus === 'onBattery') {
|
||||
await this.handleOnBatteryStatus(status);
|
||||
}
|
||||
// Check if shutdown is required based on group configurations
|
||||
await this.evaluateGroupShutdownConditions();
|
||||
|
||||
// Wait before next check
|
||||
await this.sleep(this.config.checkInterval);
|
||||
} catch (error) {
|
||||
console.error('Error during UPS monitoring:', error);
|
||||
logger.error(`Error during UPS monitoring: ${error.message}`);
|
||||
await this.sleep(this.config.checkInterval);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('UPS monitoring stopped');
|
||||
logger.log('UPS monitoring stopped');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle UPS status when running on battery
|
||||
* Check status of all UPS devices
|
||||
*/
|
||||
private async handleOnBatteryStatus(status: {
|
||||
powerStatus: string,
|
||||
batteryCapacity: number,
|
||||
batteryRuntime: number
|
||||
}): Promise<void> {
|
||||
console.log('┌─ UPS Status ─────────────────────────────┐');
|
||||
console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
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(),
|
||||
lastCheckTime: 0
|
||||
});
|
||||
}
|
||||
|
||||
// Check UPS status
|
||||
const status = await this.snmp.getUpsStatus(ups.snmp);
|
||||
const currentTime = Date.now();
|
||||
|
||||
// Get the current status from the map
|
||||
const currentStatus = this.upsStatus.get(ups.id);
|
||||
|
||||
// Update status with new values
|
||||
const updatedStatus = {
|
||||
...currentStatus,
|
||||
powerStatus: status.powerStatus,
|
||||
batteryCapacity: status.batteryCapacity,
|
||||
batteryRuntime: status.batteryRuntime,
|
||||
lastCheckTime: currentTime
|
||||
};
|
||||
|
||||
// Check if power status changed
|
||||
if (currentStatus.powerStatus !== status.powerStatus) {
|
||||
logger.logBoxTitle(`Power Status Change: ${ups.name}`, 50);
|
||||
logger.logBoxLine(`Status changed: ${currentStatus.powerStatus} → ${status.powerStatus}`);
|
||||
logger.logBoxEnd();
|
||||
|
||||
updatedStatus.lastStatusChange = currentTime;
|
||||
}
|
||||
|
||||
// Update the status in the map
|
||||
this.upsStatus.set(ups.id, updatedStatus);
|
||||
} catch (error) {
|
||||
logger.error(`Error checking UPS ${ups.name} (${ups.id}): ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log status of all UPS devices
|
||||
*/
|
||||
private logAllUpsStatus(): void {
|
||||
const timestamp = new Date().toISOString();
|
||||
const boxWidth = 60;
|
||||
logger.logBoxTitle('Periodic Status Update', boxWidth);
|
||||
logger.logBoxLine(`Timestamp: ${timestamp}`);
|
||||
logger.logBoxLine('');
|
||||
|
||||
// Check battery threshold
|
||||
if (status.batteryCapacity < this.config.thresholds.battery) {
|
||||
console.log('⚠️ WARNING: Battery capacity below threshold');
|
||||
console.log(`Current: ${status.batteryCapacity}% | Threshold: ${this.config.thresholds.battery}%`);
|
||||
await this.initiateShutdown('Battery capacity below threshold');
|
||||
for (const [id, status] of this.upsStatus.entries()) {
|
||||
logger.logBoxLine(`UPS: ${status.name} (${id})`);
|
||||
logger.logBoxLine(` Power Status: ${status.powerStatus}`);
|
||||
logger.logBoxLine(` Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
|
||||
logger.logBoxLine('');
|
||||
}
|
||||
|
||||
logger.logBoxEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
const ups = this.config.upsDevices.find(u => u.id === id);
|
||||
if (ups) {
|
||||
await this.evaluateUpsShutdownCondition(ups, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check runtime threshold
|
||||
if (status.batteryRuntime < this.config.thresholds.runtime) {
|
||||
console.log('⚠️ WARNING: Runtime below threshold');
|
||||
console.log(`Current: ${status.batteryRuntime} min | Threshold: ${this.config.thresholds.runtime} min`);
|
||||
await this.initiateShutdown('Runtime below threshold');
|
||||
// Evaluate each group
|
||||
for (const group of this.config.groups) {
|
||||
// Find all UPS devices in this group
|
||||
const upsDevicesInGroup = this.config.upsDevices.filter(ups =>
|
||||
ups.groups && ups.groups.includes(group.id)
|
||||
);
|
||||
|
||||
if (upsDevicesInGroup.length === 0) {
|
||||
// No UPS devices in this group
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a redundant group for shutdown conditions
|
||||
* In redundant mode, we only shut down if ALL UPS devices are in critical condition
|
||||
*/
|
||||
private async evaluateRedundantGroup(group: IGroupConfig, upsDevices: IUpsConfig[]): Promise<void> {
|
||||
// Count UPS devices on battery and in critical condition
|
||||
let upsOnBattery = 0;
|
||||
let upsInCriticalCondition = 0;
|
||||
|
||||
for (const ups of upsDevices) {
|
||||
const status = this.upsStatus.get(ups.id);
|
||||
if (!status) continue;
|
||||
|
||||
if (status.powerStatus === 'onBattery') {
|
||||
upsOnBattery++;
|
||||
|
||||
// Check if this UPS is in critical condition
|
||||
if (status.batteryCapacity < ups.thresholds.battery ||
|
||||
status.batteryRuntime < ups.thresholds.runtime) {
|
||||
upsInCriticalCondition++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All UPS devices must be online for a redundant group to be considered healthy
|
||||
const allUpsCount = upsDevices.length;
|
||||
|
||||
// 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();
|
||||
|
||||
await this.initiateShutdown(`All UPS devices in redundant group "${group.name}" in critical condition`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate a non-redundant group for shutdown conditions
|
||||
* In non-redundant mode, we shut down if ANY UPS device is in critical condition
|
||||
*/
|
||||
private async evaluateNonRedundantGroup(group: IGroupConfig, upsDevices: IUpsConfig[]): Promise<void> {
|
||||
for (const ups of upsDevices) {
|
||||
const status = this.upsStatus.get(ups.id);
|
||||
if (!status) continue;
|
||||
|
||||
if (status.powerStatus === 'onBattery') {
|
||||
// Check if this UPS is in critical condition
|
||||
if (status.batteryCapacity < ups.thresholds.battery ||
|
||||
status.batteryRuntime < ups.thresholds.runtime) {
|
||||
logger.logBoxTitle(`Group Shutdown Required: ${group.name}`, 50);
|
||||
logger.logBoxLine(`Mode: Non-Redundant`);
|
||||
logger.logBoxLine(`UPS ${ups.name} in critical condition`);
|
||||
logger.logBoxLine(`Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`);
|
||||
logger.logBoxLine(`Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`);
|
||||
logger.logBoxEnd();
|
||||
|
||||
await this.initiateShutdown(`UPS "${ups.name}" in non-redundant group "${group.name}" in critical condition`);
|
||||
return; // Exit after initiating shutdown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check threshold conditions
|
||||
if (status.batteryCapacity < ups.thresholds.battery ||
|
||||
status.batteryRuntime < ups.thresholds.runtime) {
|
||||
logger.logBoxTitle(`UPS Shutdown Required: ${ups.name}`, 50);
|
||||
logger.logBoxLine(`Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`);
|
||||
logger.logBoxLine(`Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`);
|
||||
logger.logBoxEnd();
|
||||
|
||||
await this.initiateShutdown(`UPS "${ups.name}" battery or runtime below threshold`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -404,7 +691,7 @@ export class NupstDaemon {
|
||||
|
||||
/**
|
||||
* Monitor UPS during system shutdown
|
||||
* Force immediate shutdown if battery gets critically low
|
||||
* Force immediate shutdown if any UPS gets critically low
|
||||
*/
|
||||
private async monitorDuringShutdown(): Promise<void> {
|
||||
const EMERGENCY_RUNTIME_THRESHOLD = 5; // 5 minutes remaining is critical
|
||||
@@ -412,112 +699,126 @@ export class NupstDaemon {
|
||||
const MAX_MONITORING_TIME = 5 * 60 * 1000; // Max 5 minutes of monitoring
|
||||
const startTime = Date.now();
|
||||
|
||||
console.log(`Emergency shutdown threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes remaining battery runtime`);
|
||||
logger.log(`Emergency shutdown threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes remaining battery runtime`);
|
||||
|
||||
// Continue monitoring until max monitoring time is reached
|
||||
while (Date.now() - startTime < MAX_MONITORING_TIME) {
|
||||
try {
|
||||
console.log('Checking UPS status during shutdown...');
|
||||
const status = await this.snmp.getUpsStatus(this.config.snmp);
|
||||
logger.log('Checking UPS status during shutdown...');
|
||||
|
||||
console.log(`Current battery: ${status.batteryCapacity}%, Runtime: ${status.batteryRuntime} minutes`);
|
||||
|
||||
// If battery runtime gets critically low, force immediate shutdown
|
||||
if (status.batteryRuntime < EMERGENCY_RUNTIME_THRESHOLD) {
|
||||
console.log('┌─ EMERGENCY SHUTDOWN ─────────────────────┐');
|
||||
console.log(`│ Battery runtime critically low: ${status.batteryRuntime} minutes`);
|
||||
console.log('│ Forcing immediate shutdown!');
|
||||
console.log('└──────────────────────────────────────────┘');
|
||||
|
||||
// Check all UPS devices
|
||||
for (const ups of this.config.upsDevices) {
|
||||
try {
|
||||
// Find shutdown command in common system paths
|
||||
const shutdownPaths = [
|
||||
'/sbin/shutdown',
|
||||
'/usr/sbin/shutdown',
|
||||
'/bin/shutdown',
|
||||
'/usr/bin/shutdown'
|
||||
];
|
||||
const status = await this.snmp.getUpsStatus(ups.snmp);
|
||||
|
||||
let shutdownCmd = '';
|
||||
for (const path of shutdownPaths) {
|
||||
if (fs.existsSync(path)) {
|
||||
shutdownCmd = path;
|
||||
console.log(`Found shutdown command at: ${shutdownCmd}`);
|
||||
break;
|
||||
}
|
||||
logger.log(`UPS ${ups.name}: Battery ${status.batteryCapacity}%, Runtime: ${status.batteryRuntime} minutes`);
|
||||
|
||||
// If any UPS battery runtime gets critically low, force immediate shutdown
|
||||
if (status.batteryRuntime < EMERGENCY_RUNTIME_THRESHOLD) {
|
||||
logger.logBoxTitle('EMERGENCY SHUTDOWN', 50);
|
||||
logger.logBoxLine(`UPS ${ups.name} runtime critically low: ${status.batteryRuntime} minutes`);
|
||||
logger.logBoxLine('Forcing immediate shutdown!');
|
||||
logger.logBoxEnd();
|
||||
|
||||
// Force immediate shutdown
|
||||
await this.forceImmediateShutdown();
|
||||
return;
|
||||
}
|
||||
|
||||
if (shutdownCmd) {
|
||||
console.log(`Executing emergency shutdown: ${shutdownCmd} -h now`);
|
||||
await execFileAsync(shutdownCmd, ['-h', 'now', 'EMERGENCY: UPS battery critically low, shutting down NOW']);
|
||||
} else {
|
||||
// Try using the PATH to find shutdown
|
||||
console.log('Shutdown command not found in common paths, trying via PATH...');
|
||||
await execAsync('shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"', {
|
||||
env: process.env // Pass the current environment
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Emergency shutdown failed, trying alternative methods...');
|
||||
|
||||
// Try alternative shutdown methods in sequence
|
||||
const alternatives = [
|
||||
{ cmd: 'poweroff', args: ['--force'] },
|
||||
{ cmd: 'halt', args: ['-p'] },
|
||||
{ cmd: 'systemctl', args: ['poweroff'] }
|
||||
];
|
||||
|
||||
for (const alt of alternatives) {
|
||||
try {
|
||||
// Check common paths
|
||||
const paths = [
|
||||
`/sbin/${alt.cmd}`,
|
||||
`/usr/sbin/${alt.cmd}`,
|
||||
`/bin/${alt.cmd}`,
|
||||
`/usr/bin/${alt.cmd}`
|
||||
];
|
||||
|
||||
let cmdPath = '';
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
cmdPath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (cmdPath) {
|
||||
console.log(`Emergency: using ${cmdPath} ${alt.args.join(' ')}`);
|
||||
await execFileAsync(cmdPath, alt.args);
|
||||
return; // Exit if successful
|
||||
} else {
|
||||
// Try using PATH
|
||||
console.log(`Emergency: trying ${alt.cmd} via PATH`);
|
||||
await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
|
||||
env: process.env
|
||||
});
|
||||
return; // Exit if successful
|
||||
}
|
||||
} catch (altError) {
|
||||
// Continue to next method
|
||||
}
|
||||
}
|
||||
|
||||
console.error('All emergency shutdown methods failed');
|
||||
} catch (upsError) {
|
||||
logger.error(`Error checking UPS ${ups.name} during shutdown: ${upsError.message}`);
|
||||
}
|
||||
|
||||
// Stop monitoring after initiating emergency shutdown
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait before checking again
|
||||
await this.sleep(CHECK_INTERVAL);
|
||||
} catch (error) {
|
||||
console.error('Error monitoring UPS during shutdown:', error);
|
||||
logger.error(`Error monitoring UPS during shutdown: ${error.message}`);
|
||||
await this.sleep(CHECK_INTERVAL);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('UPS monitoring during shutdown completed');
|
||||
logger.log('UPS monitoring during shutdown completed');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
'/usr/bin/shutdown'
|
||||
];
|
||||
|
||||
let shutdownCmd = '';
|
||||
for (const path of shutdownPaths) {
|
||||
if (fs.existsSync(path)) {
|
||||
shutdownCmd = path;
|
||||
logger.log(`Found shutdown command at: ${shutdownCmd}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shutdownCmd) {
|
||||
logger.log(`Executing emergency shutdown: ${shutdownCmd} -h now`);
|
||||
await execFileAsync(shutdownCmd, ['-h', 'now', 'EMERGENCY: UPS battery critically low, shutting down NOW']);
|
||||
} else {
|
||||
// Try using the PATH to find shutdown
|
||||
logger.log('Shutdown command not found in common paths, trying via PATH...');
|
||||
await execAsync('shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"', {
|
||||
env: process.env // Pass the current environment
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Emergency shutdown failed, trying alternative methods...');
|
||||
|
||||
// Try alternative shutdown methods in sequence
|
||||
const alternatives = [
|
||||
{ cmd: 'poweroff', args: ['--force'] },
|
||||
{ cmd: 'halt', args: ['-p'] },
|
||||
{ cmd: 'systemctl', args: ['poweroff'] }
|
||||
];
|
||||
|
||||
for (const alt of alternatives) {
|
||||
try {
|
||||
// Check common paths
|
||||
const paths = [
|
||||
`/sbin/${alt.cmd}`,
|
||||
`/usr/sbin/${alt.cmd}`,
|
||||
`/bin/${alt.cmd}`,
|
||||
`/usr/bin/${alt.cmd}`
|
||||
];
|
||||
|
||||
let cmdPath = '';
|
||||
for (const path of paths) {
|
||||
if (fs.existsSync(path)) {
|
||||
cmdPath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
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(' ')}`, {
|
||||
env: process.env
|
||||
});
|
||||
return; // Exit if successful
|
||||
}
|
||||
} catch (altError) {
|
||||
// Continue to next method
|
||||
}
|
||||
}
|
||||
|
||||
logger.error('All emergency shutdown methods failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user