146 lines
4.7 KiB
TypeScript
146 lines
4.7 KiB
TypeScript
import process from 'node:process';
|
|
import * as fs from 'node:fs';
|
|
import { exec, execFile } from 'node:child_process';
|
|
import { promisify } from 'node:util';
|
|
import { logger } from './logger.ts';
|
|
|
|
const execAsync = promisify(exec);
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
interface IShutdownAlternative {
|
|
cmd: string;
|
|
args: string[];
|
|
}
|
|
|
|
interface IAlternativeLogConfig {
|
|
resolvedMessage: (commandPath: string, args: string[]) => string;
|
|
pathMessage: (command: string, args: string[]) => string;
|
|
failureMessage?: (command: string, error: unknown) => string;
|
|
}
|
|
|
|
export class ShutdownExecutor {
|
|
private readonly commonCommandDirs = ['/sbin', '/usr/sbin', '/bin', '/usr/bin'];
|
|
|
|
public async scheduleShutdown(delayMinutes: number): Promise<void> {
|
|
const shutdownMessage = `UPS battery critical, shutting down in ${delayMinutes} minutes`;
|
|
const shutdownCommandPath = this.findCommandPath('shutdown');
|
|
|
|
if (shutdownCommandPath) {
|
|
logger.log(`Found shutdown command at: ${shutdownCommandPath}`);
|
|
logger.log(`Executing: ${shutdownCommandPath} -h +${delayMinutes} "UPS battery critical..."`);
|
|
const { stdout } = await execFileAsync(shutdownCommandPath, [
|
|
'-h',
|
|
`+${delayMinutes}`,
|
|
shutdownMessage,
|
|
]);
|
|
logger.log(`Shutdown initiated: ${stdout}`);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
logger.log('Shutdown command not found in common paths, trying via PATH...');
|
|
const { stdout } = await execAsync(
|
|
`shutdown -h +${delayMinutes} "${shutdownMessage}"`,
|
|
{ env: process.env },
|
|
);
|
|
logger.log(`Shutdown initiated: ${stdout}`);
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Shutdown command not found: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
public async forceImmediateShutdown(): Promise<void> {
|
|
const shutdownMessage = 'EMERGENCY: UPS battery critically low, shutting down NOW';
|
|
const shutdownCommandPath = this.findCommandPath('shutdown');
|
|
|
|
if (shutdownCommandPath) {
|
|
logger.log(`Found shutdown command at: ${shutdownCommandPath}`);
|
|
logger.log(`Executing emergency shutdown: ${shutdownCommandPath} -h now`);
|
|
await execFileAsync(shutdownCommandPath, ['-h', 'now', shutdownMessage]);
|
|
return;
|
|
}
|
|
|
|
logger.log('Shutdown command not found in common paths, trying via PATH...');
|
|
await execAsync(`shutdown -h now "${shutdownMessage}"`, {
|
|
env: process.env,
|
|
});
|
|
}
|
|
|
|
public async tryScheduledAlternatives(): Promise<boolean> {
|
|
return await this.tryAlternatives(
|
|
[
|
|
{ cmd: 'poweroff', args: ['--force'] },
|
|
{ cmd: 'halt', args: ['-p'] },
|
|
{ cmd: 'systemctl', args: ['poweroff'] },
|
|
{ cmd: 'reboot', args: ['-p'] },
|
|
],
|
|
{
|
|
resolvedMessage: (commandPath, args) =>
|
|
`Trying alternative shutdown method: ${commandPath} ${args.join(' ')}`,
|
|
pathMessage: (command, args) => `Trying alternative via PATH: ${command} ${args.join(' ')}`,
|
|
failureMessage: (command, error) => `Alternative method ${command} failed: ${error}`,
|
|
},
|
|
);
|
|
}
|
|
|
|
public async tryEmergencyAlternatives(): Promise<boolean> {
|
|
return await this.tryAlternatives(
|
|
[
|
|
{ cmd: 'poweroff', args: ['--force'] },
|
|
{ cmd: 'halt', args: ['-p'] },
|
|
{ cmd: 'systemctl', args: ['poweroff'] },
|
|
],
|
|
{
|
|
resolvedMessage: (commandPath, args) => `Emergency: using ${commandPath} ${args.join(' ')}`,
|
|
pathMessage: (command) => `Emergency: trying ${command} via PATH`,
|
|
},
|
|
);
|
|
}
|
|
|
|
private findCommandPath(command: string): string | null {
|
|
for (const directory of this.commonCommandDirs) {
|
|
const commandPath = `${directory}/${command}`;
|
|
try {
|
|
if (fs.existsSync(commandPath)) {
|
|
return commandPath;
|
|
}
|
|
} catch (_error) {
|
|
// Continue checking other paths.
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private async tryAlternatives(
|
|
alternatives: IShutdownAlternative[],
|
|
logConfig: IAlternativeLogConfig,
|
|
): Promise<boolean> {
|
|
for (const alternative of alternatives) {
|
|
try {
|
|
const commandPath = this.findCommandPath(alternative.cmd);
|
|
|
|
if (commandPath) {
|
|
logger.log(logConfig.resolvedMessage(commandPath, alternative.args));
|
|
await execFileAsync(commandPath, alternative.args);
|
|
return true;
|
|
}
|
|
|
|
logger.log(logConfig.pathMessage(alternative.cmd, alternative.args));
|
|
await execAsync(`${alternative.cmd} ${alternative.args.join(' ')}`, {
|
|
env: process.env,
|
|
});
|
|
return true;
|
|
} catch (error) {
|
|
if (logConfig.failureMessage) {
|
|
logger.error(logConfig.failureMessage(alternative.cmd, error));
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|