import process from 'node:process'; import * as fs from 'node:fs'; import * as path from 'node:path'; import { exec, execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { NupstSnmp } from './snmp/manager.ts'; import type { ISnmpConfig, IUpsStatus as ISnmpUpsStatus } from './snmp/types.ts'; import { logger, type ITableColumn } from './logger.ts'; import { MigrationRunner } from './migrations/index.ts'; import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts'; import type { IActionConfig } from './actions/base-action.ts'; import { ActionManager, type IActionContext, type TPowerStatus } from './actions/index.ts'; const execAsync = promisify(exec); const execFileAsync = promisify(execFile); /** * UPS configuration interface */ export interface IUpsConfig { /** Unique ID for the UPS */ id: string; /** Friendly name for the UPS */ name: string; /** SNMP configuration settings */ snmp: ISnmpConfig; /** Group IDs this UPS belongs to */ groups: string[]; /** Actions to trigger on power status changes and threshold violations */ actions?: IActionConfig[]; } /** * 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; /** Actions to trigger on power status changes and threshold violations */ actions?: IActionConfig[]; } /** * Configuration interface for the daemon */ export interface INupstConfig { /** Configuration format version */ version?: string; /** UPS devices configuration */ upsDevices: IUpsConfig[]; /** Groups configuration */ groups: IGroupConfig[]; /** Check interval in milliseconds */ checkInterval: number; // Legacy fields for backward compatibility (will be migrated away) /** UPS list (v3 format - legacy) */ upsList?: IUpsConfig[]; /** SNMP configuration settings (v1 format - legacy) */ snmp?: ISnmpConfig; /** Threshold settings (v1 format - legacy) */ thresholds?: { /** Shutdown when battery below this percentage */ battery: number; /** Shutdown when runtime below this minutes */ runtime: number; }; } /** * UPS status tracking interface */ export interface IUpsStatus { id: string; name: string; powerStatus: 'online' | 'onBattery' | 'unknown'; batteryCapacity: number; batteryRuntime: number; lastStatusChange: number; lastCheckTime: number; } /** * 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 */ private readonly DEFAULT_CONFIG: INupstConfig = { version: '4.2', 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', }, groups: [], actions: [ { type: 'shutdown', triggerMode: 'onlyThresholds', thresholds: { battery: 60, // Shutdown when battery below 60% runtime: 20, // Shutdown when runtime below 20 minutes }, shutdownDelay: 5, }, ], }, ], groups: [], checkInterval: 30000, // Check every 30 seconds } private config: INupstConfig; private snmp: NupstSnmp; private isRunning: boolean = false; private upsStatus: Map = new Map(); /** * 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 */ public async loadConfig(): Promise { 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); } // Read and parse config const configData = fs.readFileSync(this.CONFIG_PATH, 'utf8'); const parsedConfig = JSON.parse(configData); // 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 // Cast to INupstConfig since migrations ensure the output is valid const validConfig = migratedConfig as unknown as INupstConfig; if (migrated) { this.config = validConfig; await this.saveConfig(this.config); } else { this.config = validConfig; } return this.config; } catch (error) { if ( error instanceof Error && error.message && error.message.includes('No configuration found') ) { throw error; // Re-throw the no configuration error } this.logConfigError( `Error loading configuration: ${error instanceof Error ? error.message : String(error)}`, ); throw new Error('Failed to load configuration'); } } /** * Save configuration to file */ public saveConfig(config: INupstConfig): void { try { const configDir = path.dirname(this.CONFIG_PATH); if (!fs.existsSync(configDir)) { fs.mkdirSync(configDir, { recursive: true }); } // Ensure version is always set and remove legacy fields before saving const configToSave: INupstConfig = { version: '4.1', upsDevices: config.upsDevices, groups: config.groups, checkInterval: config.checkInterval, }; fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(configToSave, null, 2)); this.config = configToSave; logger.logBox('Configuration Saved', [`Location: ${this.CONFIG_PATH}`], 45, 'success'); } catch (error) { logger.error(`Error saving configuration: ${error}`); } } /** * Helper method to log configuration errors consistently */ private logConfigError(message: string): void { logger.logBox('Configuration Error', [message, "Please run 'nupst setup' first to create a configuration."], 45, 'error'); } /** * Get the current configuration */ public getConfig(): INupstConfig { return this.config; } /** * Get the SNMP instance */ public getNupstSnmp(): NupstSnmp { return this.snmp; } /** * Start the monitoring daemon */ public async start(): Promise { if (this.isRunning) { logger.log('Daemon is already running'); return; } logger.log('Starting NUPST daemon...'); try { // Load configuration - this will throw an error if config doesn't exist await this.loadConfig(); this.logConfigLoaded(); // Log version information this.snmp.getNupst().logVersionInfo(false); // Don't check for updates immediately on startup // Check for updates in the background this.snmp.getNupst().checkForUpdates().then((updateAvailable: boolean) => { if (updateAvailable) { const updateStatus = this.snmp.getNupst().getUpdateStatus(); 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(); } }).catch(() => {}); // Ignore errors checking for updates // Initialize UPS status tracking this.initializeUpsStatus(); // Start UPS monitoring this.isRunning = true; await this.monitor(); } catch (error) { this.isRunning = false; logger.error( `Daemon failed to start: ${error instanceof Error ? error.message : String(error)}`, ); process.exit(1); // Exit with error } } /** * 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 { logger.log(''); logger.logBoxTitle('Configuration Loaded', 70, 'success'); logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`); logger.logBoxEnd(); logger.log(''); // Display UPS devices in a table if (this.config.upsDevices && this.config.upsDevices.length > 0) { logger.info(`UPS Devices (${this.config.upsDevices.length}):`); const upsColumns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [ { header: 'Name', key: 'name', align: 'left', color: theme.highlight }, { header: 'ID', key: 'id', align: 'left', color: theme.dim }, { header: 'Host:Port', key: 'host', align: 'left', color: theme.info }, { header: 'Actions', key: 'actions', align: 'left' }, ]; const upsRows: Array> = this.config.upsDevices.map((ups) => ({ name: ups.name, id: ups.id, host: `${ups.snmp.host}:${ups.snmp.port}`, actions: `${(ups.actions || []).length} configured`, })); logger.logTable(upsColumns, upsRows); logger.log(''); } else { logger.warn('No UPS devices configured'); logger.log(''); } // Display groups in a table if (this.config.groups && this.config.groups.length > 0) { logger.info(`Groups (${this.config.groups.length}):`); const groupColumns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [ { header: 'Name', key: 'name', align: 'left', color: theme.highlight }, { header: 'ID', key: 'id', align: 'left', color: theme.dim }, { header: 'Mode', key: 'mode', align: 'left', color: theme.info }, ]; const groupRows: Array> = this.config.groups.map((group) => ({ name: group.name, id: group.id, mode: group.mode, })); logger.logTable(groupColumns, groupRows); logger.log(''); } } /** * Stop the monitoring daemon */ public stop(): void { logger.log('Stopping NUPST daemon...'); this.isRunning = false; } /** * Monitor the UPS status and trigger shutdown when necessary */ private async monitor(): Promise { logger.log('Starting UPS monitoring...'); if (!this.config.upsDevices || this.config.upsDevices.length === 0) { logger.warn('No UPS devices found in configuration. Daemon will remain idle...'); // Don't exit - enter idle monitoring mode instead await this.idleMonitoring(); 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 { // Check all UPS devices await this.checkAllUpsDevices(); // Log periodic status update const currentTime = Date.now(); if (currentTime - lastLogTime >= LOG_INTERVAL) { this.logAllUpsStatus(); lastLogTime = currentTime; } // Wait before next check await this.sleep(this.config.checkInterval); } catch (error) { logger.error( `Error during UPS monitoring: ${error instanceof Error ? error.message : String(error)}`, ); await this.sleep(this.config.checkInterval); } } logger.log('UPS monitoring stopped'); } /** * Check status of all UPS devices */ private async checkAllUpsDevices(): Promise { 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: IUpsStatus = { id: ups.id, name: ups.name, powerStatus: status.powerStatus, batteryCapacity: status.batteryCapacity, batteryRuntime: status.batteryRuntime, lastCheckTime: currentTime, lastStatusChange: currentStatus?.lastStatusChange || currentTime, }; // Check if power status changed if (currentStatus && currentStatus.powerStatus !== status.powerStatus) { logger.log(''); logger.logBoxTitle(`Power Status Change: ${ups.name}`, 60, 'warning'); logger.logBoxLine(`Previous: ${formatPowerStatus(currentStatus.powerStatus)}`); logger.logBoxLine(`Current: ${formatPowerStatus(status.powerStatus)}`); logger.logBoxLine(`Time: ${new Date().toISOString()}`); logger.logBoxEnd(); logger.log(''); updatedStatus.lastStatusChange = currentTime; // Trigger actions for power status change await this.triggerUpsActions(ups, updatedStatus, currentStatus, 'powerStatusChange'); } // Check if any action's thresholds are exceeded (for threshold violation triggers) // Only check when on battery power if (status.powerStatus === 'onBattery' && ups.actions && ups.actions.length > 0) { let anyThresholdExceeded = false; for (const actionConfig of ups.actions) { if (actionConfig.thresholds) { if ( status.batteryCapacity < actionConfig.thresholds.battery || status.batteryRuntime < actionConfig.thresholds.runtime ) { anyThresholdExceeded = true; break; } } } // Trigger actions with threshold violation reason if any threshold is exceeded // Actions will individually check their own thresholds in shouldExecute() if (anyThresholdExceeded) { await this.triggerUpsActions(ups, updatedStatus, currentStatus, 'thresholdViolation'); } } // Update the status in the map this.upsStatus.set(ups.id, updatedStatus); } catch (error) { logger.error( `Error checking UPS ${ups.name} (${ups.id}): ${ error instanceof Error ? error.message : String(error) }`, ); } } } /** * Log status of all UPS devices */ private logAllUpsStatus(): void { const timestamp = new Date().toISOString(); logger.log(''); logger.logBoxTitle('Periodic Status Update', 70, 'info'); logger.logBoxLine(`Timestamp: ${timestamp}`); logger.logBoxEnd(); logger.log(''); // Build table data const columns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [ { header: 'UPS Name', key: 'name', align: 'left', color: theme.highlight }, { header: 'ID', key: 'id', align: 'left', color: theme.dim }, { header: 'Power Status', key: 'powerStatus', align: 'left' }, { header: 'Battery', key: 'battery', align: 'right' }, { header: 'Runtime', key: 'runtime', align: 'right' }, ]; const rows: Array> = []; for (const [id, status] of this.upsStatus.entries()) { const batteryColor = getBatteryColor(status.batteryCapacity); const runtimeColor = getRuntimeColor(status.batteryRuntime); rows.push({ name: status.name, id: id, powerStatus: formatPowerStatus(status.powerStatus), battery: batteryColor(status.batteryCapacity + '%'), runtime: runtimeColor(status.batteryRuntime + ' min'), }); } logger.logTable(columns, rows); logger.log(''); } /** * Build action context from UPS state * @param ups UPS configuration * @param status Current UPS status * @param triggerReason Why this action is being triggered * @returns Action context */ private buildActionContext( ups: IUpsConfig, status: IUpsStatus, triggerReason: 'powerStatusChange' | 'thresholdViolation', ): IActionContext { return { upsId: ups.id, upsName: ups.name, powerStatus: status.powerStatus as TPowerStatus, batteryCapacity: status.batteryCapacity, batteryRuntime: status.batteryRuntime, previousPowerStatus: 'unknown' as TPowerStatus, // Will be set from map in calling code timestamp: Date.now(), triggerReason, }; } /** * Trigger actions for a UPS device * @param ups UPS configuration * @param status Current UPS status * @param previousStatus Previous UPS status (for determining previousPowerStatus) * @param triggerReason Why actions are being triggered */ private async triggerUpsActions( ups: IUpsConfig, status: IUpsStatus, previousStatus: IUpsStatus | undefined, triggerReason: 'powerStatusChange' | 'thresholdViolation', ): Promise { const actions = ups.actions || []; // Backward compatibility: if no actions configured, use default shutdown behavior if (actions.length === 0 && triggerReason === 'thresholdViolation') { // Fall back to old shutdown logic for backward compatibility await this.initiateShutdown(`UPS "${ups.name}" battery or runtime below threshold`); return; } if (actions.length === 0) { return; // No actions to execute } // Build action context const context = this.buildActionContext(ups, status, triggerReason); context.previousPowerStatus = (previousStatus?.powerStatus || 'unknown') as TPowerStatus; // Execute actions await ActionManager.executeActions(actions, context); } /** * Initiate system shutdown with UPS monitoring during shutdown * @param reason Reason for shutdown */ public async initiateShutdown(reason: string): Promise { logger.log(`Initiating system shutdown due to: ${reason}`); // Set a longer delay for shutdown to allow VMs and services to close const shutdownDelayMinutes = 5; 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) { try { if (fs.existsSync(path)) { shutdownCmd = path; logger.log(`Found shutdown command at: ${shutdownCmd}`); break; } } catch (e) { // Continue checking other paths } } if (shutdownCmd) { // Execute shutdown command with delay to allow for VM graceful shutdown logger.log( `Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`, ); const { stdout } = await execFileAsync(shutdownCmd, [ '-h', `+${shutdownDelayMinutes}`, `UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes`, ]); logger.log(`Shutdown initiated: ${stdout}`); logger.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`); } else { // Try using the PATH to find shutdown try { logger.log('Shutdown command not found in common paths, trying via PATH...'); const { stdout } = await execAsync( `shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`, { env: process.env, // Pass the current environment }, ); logger.log(`Shutdown initiated: ${stdout}`); } catch (e) { throw new Error( `Shutdown command not found: ${e instanceof Error ? e.message : String(e)}`, ); } } // Monitor UPS during shutdown and force immediate shutdown if battery gets too low logger.log('Monitoring UPS during shutdown process...'); await this.monitorDuringShutdown(); } catch (error) { logger.error(`Failed to initiate shutdown: ${error}`); // Try alternative shutdown methods const alternatives = [ { cmd: 'poweroff', args: ['--force'] }, { cmd: 'halt', args: ['-p'] }, { cmd: 'systemctl', args: ['poweroff'] }, { cmd: 'reboot', args: ['-p'] }, // Some systems allow reboot -p for power off ]; 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}`, `/usr/bin/${alt.cmd}`, ]; let cmdPath = ''; for (const path of paths) { if (fs.existsSync(path)) { cmdPath = path; break; } } if (cmdPath) { logger.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`); await execFileAsync(cmdPath, alt.args); return; // Exit if successful } else { // Try using PATH environment logger.log(`Trying alternative via PATH: ${alt.cmd} ${alt.args.join(' ')}`); await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, { env: process.env, // Pass the current environment }); return; // Exit if successful } } catch (altError) { logger.error(`Alternative method ${alt.cmd} failed: ${altError}`); // Continue to next method } } logger.error('All shutdown methods failed'); } } /** * Monitor UPS during system shutdown * Force immediate shutdown if any UPS gets critically low */ private async monitorDuringShutdown(): Promise { 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(); logger.log(''); logger.logBoxTitle('Shutdown Monitoring Active', 60, 'warning'); logger.logBoxLine(`Emergency threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes runtime`); logger.logBoxLine(`Check interval: ${CHECK_INTERVAL / 1000} seconds`); logger.logBoxLine(`Max monitoring time: ${MAX_MONITORING_TIME / 1000} seconds`); logger.logBoxEnd(); logger.log(''); // Continue monitoring until max monitoring time is reached while (Date.now() - startTime < MAX_MONITORING_TIME) { try { logger.info('Checking UPS status during shutdown...'); // Build table for UPS status during shutdown const columns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [ { header: 'UPS Name', key: 'name', align: 'left', color: theme.highlight }, { header: 'Battery', key: 'battery', align: 'right' }, { header: 'Runtime', key: 'runtime', align: 'right' }, { header: 'Status', key: 'status', align: 'left' }, ]; const rows: Array> = []; let emergencyDetected = false; let emergencyUps: { ups: IUpsConfig; status: ISnmpUpsStatus } | null = null; // Check all UPS devices for (const ups of this.config.upsDevices) { try { const status = await this.snmp.getUpsStatus(ups.snmp); const batteryColor = getBatteryColor(status.batteryCapacity); const runtimeColor = getRuntimeColor(status.batteryRuntime); const isCritical = status.batteryRuntime < EMERGENCY_RUNTIME_THRESHOLD; rows.push({ name: ups.name, battery: batteryColor(status.batteryCapacity + '%'), runtime: runtimeColor(status.batteryRuntime + ' min'), status: isCritical ? theme.error('CRITICAL!') : theme.success('OK'), }); // If any UPS battery runtime gets critically low, flag for immediate shutdown if (isCritical && !emergencyDetected) { emergencyDetected = true; emergencyUps = { ups, status }; } } catch (upsError) { rows.push({ name: ups.name, battery: theme.error('N/A'), runtime: theme.error('N/A'), status: theme.error('ERROR'), }); logger.error( `Error checking UPS ${ups.name} during shutdown: ${ upsError instanceof Error ? upsError.message : String(upsError) }`, ); } } // Display the table logger.logTable(columns, rows); logger.log(''); // If emergency detected, trigger immediate shutdown if (emergencyDetected && emergencyUps) { logger.log(''); logger.logBoxTitle('EMERGENCY SHUTDOWN', 60, 'error'); logger.logBoxLine( `UPS ${emergencyUps.ups.name} runtime critically low: ${emergencyUps.status.batteryRuntime} minutes`, ); logger.logBoxLine(`Emergency threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes`); logger.logBoxLine('Forcing immediate shutdown!'); logger.logBoxEnd(); logger.log(''); // Force immediate shutdown await this.forceImmediateShutdown(); return; } // Wait before checking again await this.sleep(CHECK_INTERVAL); } catch (error) { logger.error( `Error monitoring UPS during shutdown: ${ error instanceof Error ? error.message : String(error) }`, ); await this.sleep(CHECK_INTERVAL); } } logger.log(''); logger.success('UPS monitoring during shutdown completed'); logger.log(''); } /** * Force an immediate system shutdown */ private async forceImmediateShutdown(): Promise { 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'); } } /** * Idle monitoring loop when no UPS devices are configured * Watches for config changes and reloads when detected */ private async idleMonitoring(): Promise { 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 { 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)}`, ); } } /** * Sleep for the specified milliseconds */ private sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } }