Compare commits

...

12 Commits

6 changed files with 287 additions and 39 deletions

View File

@ -1,5 +1,48 @@
# Changelog # Changelog
## 2025-03-26 - 2.6.7 - fix(setup.sh)
Clarify net-snmp dependency installation message in setup.sh
- Updated echo statement to indicate installation of net-snmp along with 2 subdependencies
- Improves clarity on dependency installation during setup
## 2025-03-26 - 2.6.6 - fix(setup.sh)
Improve setup script to detect and execute npm-cli.js directly using the Node.js binary
- Replace use of the npm binary with direct execution of npm-cli.js
- Add fallback logic to locate npm-cli.js when not found at the expected path
- Simplify cleanup by removing unnecessary PATH modifications
## 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.
- Remove existing package-lock.json along with node_modules to prevent stale artifacts.
- Back up the original package.json before modifying it.
- Create a minimal package.json with only the net-snmp dependency based on the backed-up version.
- Use a clean install to guarantee that only net-snmp is installed.
- Restore the original package.json if the installation fails.
## 2025-03-26 - 2.6.3 - fix(setup)
Update setup script to install only net-snmp dependency and create a minimal package-lock.json for better dependency control.
- Removed full production dependency install in favor of installing only net-snmp@3.20.0
- Added verification step to confirm net-snmp installation
- Generate a minimal package-lock.json if one does not exist
## 2025-03-26 - 2.6.2 - fix(setup/readme)
Improve force update instructions and dependency installation process in setup.sh and readme.md
- Clarify force update commands with explicit paths in readme.md
- Remove existing node_modules before installing dependencies in setup.sh
- Switch from 'npm ci --only=production' to 'npm install --omit=dev' with updated error instructions
## 2025-03-26 - 2.6.1 - fix(setup) ## 2025-03-26 - 2.6.1 - fix(setup)
Update setup.sh to temporarily add vendor Node.js binary to PATH for dependency installation, log Node and npm versions, and restore the original PATH afterwards. Update setup.sh to temporarily add vendor Node.js binary to PATH for dependency installation, log Node and npm versions, and restore the original PATH afterwards.

View File

@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/nupst", "name": "@serve.zone/nupst",
"version": "2.6.1", "version": "2.6.7",
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices", "description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
"main": "dist/index.js", "main": "dist/index.js",
"bin": { "bin": {

View File

@ -234,7 +234,11 @@ This will:
You can also manually run the setup script with the force flag to update Node.js and dependencies without updating the application code: You can also manually run the setup script with the force flag to update Node.js and dependencies without updating the application code:
```bash ```bash
bash setup.sh --force # If you're in the nupst directory:
bash ./setup.sh --force
# If you're in another directory, specify the full path:
bash /opt/nupst/setup.sh --force
``` ```
## Security ## Security

View File

@ -239,36 +239,89 @@ echo "dist_ts directory successfully downloaded from npm registry."
# Make launcher script executable # Make launcher script executable
chmod +x "$SCRIPT_DIR/bin/nupst" chmod +x "$SCRIPT_DIR/bin/nupst"
# Add our Node.js bin directory to the PATH temporarily # Set up Node.js binary path
NODE_BIN_DIR="$SCRIPT_DIR/vendor/$NODE_DIR/bin" NODE_BIN_DIR="$SCRIPT_DIR/vendor/$NODE_DIR/bin"
OLD_PATH="$PATH" NODE_BIN="$NODE_BIN_DIR/node"
export PATH="$NODE_BIN_DIR:$PATH" NPM_CLI_JS="$NODE_BIN_DIR/../lib/node_modules/npm/bin/npm-cli.js"
# Install production dependencies # Ensure we have executable permissions
echo "Installing production dependencies..." chmod +x "$NODE_BIN"
echo "Using Node.js binary from: $NODE_BIN_DIR"
echo "Node version: $(node --version)"
echo "NPM version: $(npm --version)"
npm --prefix "$SCRIPT_DIR" ci --only=production --no-audit --no-fund # Make sure the npm-cli.js exists
if [ ! -f "$NPM_CLI_JS" ]; then
if [ $? -ne 0 ]; then # Try to find npm-cli.js
echo "Warning: Failed to install dependencies with 'npm ci'. Trying 'npm install'..." NPM_CLI_JS=$(find "$NODE_BIN_DIR/.." -name "npm-cli.js" | head -1)
npm --prefix "$SCRIPT_DIR" install --only=production --no-audit --no-fund
if [ $? -ne 0 ]; then if [ -z "$NPM_CLI_JS" ]; then
echo "Error: Failed to install dependencies. NUPST may not function correctly." echo "Warning: Could not find npm-cli.js, npm commands may fail"
echo "You can try to install dependencies manually by running:" # Set to a fallback value so code can continue
echo "cd $SCRIPT_DIR && npm install --only=production" NPM_CLI_JS="$NODE_BIN_DIR/npm"
else else
echo "Dependencies installed successfully with 'npm install'." echo "Found npm-cli.js at: $NPM_CLI_JS"
fi fi
else
echo "Dependencies installed successfully with 'npm ci'."
fi fi
# Restore the original PATH # Display which binaries we're using
export PATH="$OLD_PATH" echo "Using Node binary: $NODE_BIN"
echo "Using NPM CLI JS: $NPM_CLI_JS"
# Remove existing node_modules directory and package files
echo "Cleaning up existing installation..."
rm -rf "$SCRIPT_DIR/node_modules"
rm -f "$SCRIPT_DIR/package-lock.json"
# Back up existing package.json if it exists
if [ -f "$SCRIPT_DIR/package.json" ]; then
echo "Backing up existing package.json..."
cp "$SCRIPT_DIR/package.json" "$SCRIPT_DIR/package.json.bak"
fi
# Create a clean minimal package.json with ONLY net-snmp dependency
echo "Creating minimal package.json with only net-snmp dependency..."
VERSION=$(grep -o '"version": "[^"]*"' "$SCRIPT_DIR/package.json.bak" | head -1 | cut -d'"' -f4 || echo "2.6.3")
echo '{
"name": "@serve.zone/nupst",
"version": "'$VERSION'",
"description": "Node.js UPS Shutdown Tool for SNMP-enabled UPS devices",
"main": "dist_ts/index.js",
"type": "module",
"bin": {
"nupst": "bin/nupst"
},
"dependencies": {
"net-snmp": "3.20.0"
},
"engines": {
"node": ">=16.0.0"
},
"private": true
}' > "$SCRIPT_DIR/package.json"
# Install ONLY net-snmp
echo "Installing ONLY net-snmp dependency (+ 2 subdependencies)..."
echo "Node version: $("$NODE_BIN" --version)"
echo "Executing NPM directly with Node.js"
# Execute npm-cli.js directly with our Node.js binary
"$NODE_BIN" "$NPM_CLI_JS" --prefix "$SCRIPT_DIR" install --no-audit --no-fund
INSTALL_STATUS=$?
if [ $INSTALL_STATUS -ne 0 ]; then
echo "Error: Failed to install net-snmp dependency. NUPST may not function correctly."
echo "Restoring original package.json..."
mv "$SCRIPT_DIR/package.json.bak" "$SCRIPT_DIR/package.json"
exit 1
else
echo "net-snmp dependency installed successfully."
# Show what's actually installed
echo "Installed modules:"
find "$SCRIPT_DIR/node_modules" -maxdepth 1 -type d | grep -v "^$SCRIPT_DIR/node_modules$" | sort
# Remove backup if successful
rm -f "$SCRIPT_DIR/package.json.bak"
fi
# No temporary files to clean up
echo "NUPST setup completed successfully." echo "NUPST setup completed successfully."
echo "You can now run NUPST using: $SCRIPT_DIR/bin/nupst" echo "You can now run NUPST using: $SCRIPT_DIR/bin/nupst"

View File

@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/nupst', name: '@serve.zone/nupst',
version: '2.6.1', version: '2.6.7',
description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices' description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices'
} }

View File

@ -1,11 +1,12 @@
import * as fs from 'fs'; import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { exec } from 'child_process'; import { exec, execFile } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import { NupstSnmp } from './snmp/manager.js'; import { NupstSnmp } from './snmp/manager.js';
import type { ISnmpConfig } from './snmp/types.js'; import type { ISnmpConfig } from './snmp/types.js';
const execAsync = promisify(exec); const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
/** /**
* Configuration interface for the daemon * Configuration interface for the daemon
@ -298,23 +299,101 @@ export class NupstDaemon {
const shutdownDelayMinutes = 5; const shutdownDelayMinutes = 5;
try { try {
// Execute shutdown command with delay to allow for VM graceful shutdown // Find shutdown command in common system paths
const { stdout } = await execAsync(`shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`); const shutdownPaths = [
console.log('Shutdown initiated:', stdout); '/sbin/shutdown',
console.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`); '/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 // Monitor UPS during shutdown and force immediate shutdown if battery gets too low
console.log('Monitoring UPS during shutdown process...'); console.log('Monitoring UPS during shutdown process...');
await this.monitorDuringShutdown(); await this.monitorDuringShutdown();
} catch (error) { } catch (error) {
console.error('Failed to initiate shutdown:', error); console.error('Failed to initiate shutdown:', error);
// Try a different method if first one fails
try { // Try alternative shutdown methods
console.log('Trying alternative shutdown method...'); const alternatives = [
await execAsync('poweroff --force'); { cmd: 'poweroff', args: ['--force'] },
} catch (innerError) { { cmd: 'halt', args: ['-p'] },
console.error('All shutdown methods failed:', innerError); { 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('└──────────────────────────────────────────┘'); console.log('└──────────────────────────────────────────┘');
try { 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) { } catch (error) {
console.error('Emergency shutdown failed, trying alternative method...'); console.error('Emergency shutdown failed, trying alternative methods...');
await execAsync('poweroff --force');
// 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 // Stop monitoring after initiating emergency shutdown