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 { 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 { 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 { 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 { 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 { 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; } }