import * as fs from 'node:fs'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; import { Action, type IActionConfig, type IActionContext } from './base-action.ts'; import { logger } from '../logger.ts'; const execFileAsync = promisify(execFile); /** * ShutdownAction - Initiates system shutdown * * This action triggers a system shutdown using the standard shutdown command. * It includes a configurable delay to allow VMs and services to gracefully terminate. */ export class ShutdownAction extends Action { readonly type = 'shutdown'; /** * Execute the shutdown action * @param context Action context with UPS state */ async execute(context: IActionContext): Promise { // Check if we should execute based on trigger mode and thresholds if (!this.shouldExecute(context)) { logger.info(`Shutdown action skipped (trigger mode: ${this.config.triggerMode || 'powerChangesAndThresholds'})`); return; } const shutdownDelay = this.config.shutdownDelay || 5; // Default 5 minutes logger.log(''); logger.logBoxTitle('Initiating System Shutdown', 60, 'error'); logger.logBoxLine(`UPS: ${context.upsName} (${context.upsId})`); logger.logBoxLine(`Power Status: ${context.powerStatus}`); logger.logBoxLine(`Battery: ${context.batteryCapacity}%`); logger.logBoxLine(`Runtime: ${context.batteryRuntime} minutes`); logger.logBoxLine(`Trigger: ${context.triggerReason}`); logger.logBoxLine(`Shutdown delay: ${shutdownDelay} minutes`); logger.logBoxEnd(); logger.log(''); try { await this.executeShutdownCommand(shutdownDelay); } catch (error) { logger.error( `Shutdown command failed: ${error instanceof Error ? error.message : String(error)}`, ); // Try alternative methods await this.tryAlternativeShutdownMethods(); } } /** * Execute the primary shutdown command * @param delayMinutes Minutes to delay before shutdown */ private async executeShutdownCommand(delayMinutes: number): Promise { // 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 const message = `UPS battery critical, shutting down in ${delayMinutes} minutes`; logger.log(`Executing: ${shutdownCmd} -h +${delayMinutes} "${message}"`); const { stdout } = await execFileAsync(shutdownCmd, [ '-h', `+${delayMinutes}`, message, ]); logger.log(`Shutdown initiated: ${stdout}`); logger.log(`Allowing ${delayMinutes} minutes for VMs to shut down safely`); } else { throw new Error('Shutdown command not found in common paths'); } } /** * Try alternative shutdown methods if primary command fails */ private async tryAlternativeShutdownMethods(): Promise { logger.error('Trying 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); logger.log(`Alternative method ${alt.cmd} succeeded`); return; // Exit if successful } } catch (_altError) { logger.error(`Alternative method ${alt.cmd} failed`); // Continue to next method } } logger.error('All shutdown methods failed'); } }