fix(cli,daemon,snmp): normalize CLI argument parsing and extract daemon monitoring helpers with stronger SNMP typing
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user