143 lines
4.5 KiB
TypeScript
143 lines
4.5 KiB
TypeScript
|
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');
|
||
|
}
|
||
|
}
|