Files
nupst/ts/actions/shutdown-action.ts

143 lines
4.5 KiB
TypeScript
Raw Normal View History

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<void> {
// 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<void> {
// 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<void> {
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');
}
}