diff --git a/changelog.md b/changelog.md index 70b5893..1163481 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2025-03-26 - 2.6.5 - fix(daemon, setup) +Improve shutdown command detection and fallback logic; update setup script to use absolute Node/npm paths + +- Use execFileAsync to execute shutdown commands reliably +- Add multiple fallback alternatives for shutdown and emergency shutdown handling +- Update setup.sh to log the Node and NPM versions using absolute paths without modifying PATH + ## 2025-03-26 - 2.6.4 - fix(setup) Improve installation process in setup script by cleaning up package files and ensuring a minimal net-snmp dependency installation. diff --git a/setup.sh b/setup.sh index e25f991..2619081 100644 --- a/setup.sh +++ b/setup.sh @@ -239,10 +239,20 @@ echo "dist_ts directory successfully downloaded from npm registry." # Make launcher script executable chmod +x "$SCRIPT_DIR/bin/nupst" -# Add our Node.js bin directory to the PATH temporarily +# Set path to our Node.js binaries NODE_BIN_DIR="$SCRIPT_DIR/vendor/$NODE_DIR/bin" +NODE_BIN="$NODE_BIN_DIR/node" +NPM_BIN="$NODE_BIN_DIR/npm" + +# Ensure we have executable permissions +chmod +x "$NODE_BIN" "$NPM_BIN" + +# Save original PATH but don't modify it +# We'll use the full paths to binaries instead OLD_PATH="$PATH" -export PATH="$NODE_BIN_DIR:$PATH" + +echo "Using Node binary: $NODE_BIN" +echo "Using NPM binary: $NPM_BIN" # Remove existing node_modules directory and package files echo "Cleaning up existing installation..." @@ -278,12 +288,11 @@ echo '{ # Install ONLY net-snmp echo "Installing ONLY net-snmp dependency..." -echo "Using Node.js binary from: $NODE_BIN_DIR" -echo "Node version: $(node --version)" -echo "NPM version: $(npm --version)" +echo "Node version: $("$NODE_BIN" --version)" +echo "NPM version: $("$NPM_BIN" --version)" -# Use clean install to ensure only net-snmp is installed -npm --prefix "$SCRIPT_DIR" install --no-audit --no-fund +# Use absolute paths to binaries to ensure we use our Node.js +"$NPM_BIN" --prefix "$SCRIPT_DIR" install --no-audit --no-fund INSTALL_STATUS=$? if [ $INSTALL_STATUS -ne 0 ]; then @@ -301,8 +310,7 @@ else rm -f "$SCRIPT_DIR/package.json.bak" fi -# Restore the original PATH -export PATH="$OLD_PATH" +# We didn't modify PATH, so no need to restore it echo "NUPST setup completed successfully." echo "You can now run NUPST using: $SCRIPT_DIR/bin/nupst" diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 6f16b2e..121594a 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/nupst', - version: '2.6.4', + version: '2.6.5', description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices' } diff --git a/ts/daemon.ts b/ts/daemon.ts index 1372a10..204b6f7 100644 --- a/ts/daemon.ts +++ b/ts/daemon.ts @@ -1,11 +1,12 @@ import * as fs from 'fs'; import * as path from 'path'; -import { exec } from 'child_process'; +import { exec, execFile } from 'child_process'; import { promisify } from 'util'; import { NupstSnmp } from './snmp/manager.js'; import type { ISnmpConfig } from './snmp/types.js'; const execAsync = promisify(exec); +const execFileAsync = promisify(execFile); /** * Configuration interface for the daemon @@ -298,23 +299,101 @@ export class NupstDaemon { const shutdownDelayMinutes = 5; try { - // Execute shutdown command with delay to allow for VM graceful shutdown - const { stdout } = await execAsync(`shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`); - console.log('Shutdown initiated:', stdout); - console.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`); + // 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; + console.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 + console.log(`Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`); + const { stdout } = await execFileAsync(shutdownCmd, [ + '-h', + `+${shutdownDelayMinutes}`, + `UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes` + ]); + console.log('Shutdown initiated:', stdout); + console.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`); + } else { + // Try using the PATH to find shutdown + try { + console.log('Shutdown command not found in common paths, trying via PATH...'); + const { stdout } = await execAsync(`shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`, { + env: process.env // Pass the current environment + }); + console.log('Shutdown initiated:', stdout); + } catch (e) { + throw new Error(`Shutdown command not found: ${e.message}`); + } + } // Monitor UPS during shutdown and force immediate shutdown if battery gets too low console.log('Monitoring UPS during shutdown process...'); await this.monitorDuringShutdown(); } catch (error) { console.error('Failed to initiate shutdown:', error); - // Try a different method if first one fails - try { - console.log('Trying alternative shutdown method...'); - await execAsync('poweroff --force'); - } catch (innerError) { - console.error('All shutdown methods failed:', innerError); + + // Try 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) { + console.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`); + await execFileAsync(cmdPath, alt.args); + return; // Exit if successful + } else { + // Try using PATH environment + console.log(`Trying alternative via PATH: ${alt.cmd} ${alt.args.join(' ')}`); + await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, { + env: process.env // Pass the current environment + }); + return; // Exit if successful + } + } catch (altError) { + console.error(`Alternative method ${alt.cmd} failed:`, altError); + // Continue to next method + } } + + console.error('All shutdown methods failed'); } } @@ -346,10 +425,79 @@ export class NupstDaemon { console.log('└──────────────────────────────────────────┘'); try { - await execAsync('shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"'); + // 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) { + if (fs.existsSync(path)) { + shutdownCmd = path; + console.log(`Found shutdown command at: ${shutdownCmd}`); + break; + } + } + + if (shutdownCmd) { + console.log(`Executing emergency shutdown: ${shutdownCmd} -h now`); + await execFileAsync(shutdownCmd, ['-h', 'now', 'EMERGENCY: UPS battery critically low, shutting down NOW']); + } else { + // Try using the PATH to find shutdown + console.log('Shutdown command not found in common paths, trying via PATH...'); + await execAsync('shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"', { + env: process.env // Pass the current environment + }); + } } catch (error) { - console.error('Emergency shutdown failed, trying alternative method...'); - await execAsync('poweroff --force'); + console.error('Emergency shutdown failed, trying alternative methods...'); + + // Try alternative shutdown methods in sequence + const alternatives = [ + { cmd: 'poweroff', args: ['--force'] }, + { cmd: 'halt', args: ['-p'] }, + { cmd: 'systemctl', args: ['poweroff'] } + ]; + + for (const alt of alternatives) { + try { + // Check common 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) { + console.log(`Emergency: using ${cmdPath} ${alt.args.join(' ')}`); + await execFileAsync(cmdPath, alt.args); + return; // Exit if successful + } else { + // Try using PATH + console.log(`Emergency: trying ${alt.cmd} via PATH`); + await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, { + env: process.env + }); + return; // Exit if successful + } + } catch (altError) { + // Continue to next method + } + } + + console.error('All emergency shutdown methods failed'); } // Stop monitoring after initiating emergency shutdown