Compare commits

...

30 Commits

Author SHA1 Message Date
f860f39e59 2.6.14 2025-03-26 18:15:17 +00:00
fa4516de3b fix(systemd): Shorten closing log divider in systemd service installation output for consistent formatting. 2025-03-26 18:15:17 +00:00
539547beb8 2.6.13 2025-03-26 18:13:12 +00:00
6eb92959ec fix(cli): Fix CLI update output box formatting 2025-03-26 18:13:12 +00:00
4af9af0845 2.6.12 2025-03-26 18:10:49 +00:00
f7e12cdcbb fix(systemd): Adjust logging border in systemd service installation output 2025-03-26 18:10:49 +00:00
002498b91b 2.6.11 2025-03-26 18:08:43 +00:00
459911fe5f fix(cli, systemd): Adjust log formatting for consistent output in CLI and systemd commands 2025-03-26 18:08:43 +00:00
9859a02ea2 2.6.10 2025-03-26 18:04:12 +00:00
65444b6d25 fix(daemon): Adjust console log box formatting for consistent output in daemon status messages 2025-03-26 18:04:12 +00:00
d049e8741f 2.6.9 2025-03-26 18:00:55 +00:00
1123a99aea fix(cli): Improve console output formatting for status banners and logging messages 2025-03-26 18:00:54 +00:00
d01e878310 2.6.8 2025-03-26 17:49:50 +00:00
588aeabf4b fix(cli): Improve CLI formatting, refine debug option filtering, and remove unused dgram import in SNMP manager 2025-03-26 17:49:50 +00:00
87005e72f1 2.6.7 2025-03-26 15:56:31 +00:00
f799c2ee66 fix(setup.sh): Clarify net-snmp dependency installation message in setup.sh 2025-03-26 15:56:31 +00:00
1a029ba493 2.6.6 2025-03-26 15:53:38 +00:00
5b756dd223 fix(setup.sh): Improve setup script to detect and execute npm-cli.js directly using the Node.js binary 2025-03-26 15:53:38 +00:00
4cac599a58 2.6.5 2025-03-26 15:49:54 +00:00
be6a7314c3 fix(daemon, setup): Improve shutdown command detection and fallback logic; update setup script to use absolute Node/npm paths 2025-03-26 15:49:54 +00:00
83ba9c2611 2.6.4 2025-03-26 14:09:01 +00:00
22ab472e58 fix(setup): Improve installation process in setup script by cleaning up package files and ensuring a minimal net-snmp dependency installation. 2025-03-26 14:09:01 +00:00
9a77030377 2.6.3 2025-03-26 14:05:44 +00:00
ceff285ff5 fix(setup): Update setup script to install only net-snmp dependency and create a minimal package-lock.json for better dependency control. 2025-03-26 14:05:44 +00:00
d8bfbf0be3 2.6.2 2025-03-26 13:54:49 +00:00
3e6b883b38 fix(setup/readme): Improve force update instructions and dependency installation process in setup.sh and readme.md 2025-03-26 13:54:49 +00:00
47ef918128 2.6.1 2025-03-26 13:51:45 +00:00
5951638967 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. 2025-03-26 13:51:45 +00:00
b06e2b2273 2.6.0 2025-03-26 13:49:47 +00:00
cc1cfe894c feat(setup): Add --force update flag to setup script and update installation instructions 2025-03-26 13:49:47 +00:00
11 changed files with 659 additions and 238 deletions

View File

@ -1,5 +1,104 @@
# Changelog
## 2025-03-26 - 2.6.14 - fix(systemd)
Shorten closing log divider in systemd service installation output for consistent formatting.
- Replaced the overly long footer with a shorter one in ts/systemd.ts.
- This change improves log readability without affecting functionality.
## 2025-03-26 - 2.6.13 - fix(cli)
Fix CLI update output box formatting
- Adjusted the closing box line in the update process log messages for consistent visual formatting
## 2025-03-26 - 2.6.12 - fix(systemd)
Adjust logging border in systemd service installation output
- Updated the closing border line for consistent output formatting in ts/systemd.ts
## 2025-03-26 - 2.6.11 - fix(cli, systemd)
Adjust log formatting for consistent output in CLI and systemd commands
- Fixed spacing issues in service installation and status log messages in the systemd module.
- Revised output formatting in the CLI to improve message clarity.
## 2025-03-26 - 2.6.10 - fix(daemon)
Adjust console log box formatting for consistent output in daemon status messages
- Updated closing box borders to align properly in configuration error, periodic updates, and UPS status logs
- Improved visual consistency in log messages
## 2025-03-26 - 2.6.9 - fix(cli)
Improve console output formatting for status banners and logging messages
- Standardize banner messages in daemon status updates
- Refine version information banner in nupst logging
- Update UPS connection and status banners in systemd
## 2025-03-26 - 2.6.8 - fix(cli)
Improve CLI formatting, refine debug option filtering, and remove unused dgram import in SNMP manager
- Standardize whitespace and formatting in ts/cli.ts for consistency
- Refine argument filtering for debug mode and prompt messages
- Remove unused 'dgram' import from ts/snmp/manager.ts
## 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)
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.
- Temporarily prepend vendor Node.js binary directory to PATH to ensure proper npm execution.
- Log Node.js and npm versions for debugging purposes.
- Restore the original PATH after installing dependencies.
## 2025-03-26 - 2.6.0 - feat(setup)
Add --force update flag to setup script and update installation instructions
- Implemented --force option in setup.sh to force-update Node.js binary and dependencies
- Updated readme.md to document the --force flag and revised update steps
- Modified ts/cli.ts update command to pass the --force flag to setup.sh
## 2025-03-26 - 2.5.2 - fix(installer)
Improve Node.js binary detection, dependency management, and SNMPv3 fallback logic

View File

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

2
pnpm-lock.yaml generated
View File

@ -9,7 +9,7 @@ importers:
.:
dependencies:
net-snmp:
specifier: ^3.20.0
specifier: 3.20.0
version: 3.20.0
devDependencies:
'@git.zone/tsbuild':

View File

@ -227,8 +227,19 @@ sudo nupst update
This will:
1. Pull the latest changes from the git repository
2. Run the installation scripts
3. Refresh the systemd service configuration
4. Restart the service if it was running
3. Force-update Node.js and all dependencies, even if they already exist
4. Refresh the systemd service configuration
5. Restart the service if it was running
You can also manually run the setup script with the force flag to update Node.js and dependencies without updating the application code:
```bash
# 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

110
setup.sh
View File

@ -2,6 +2,22 @@
# NUPST Setup Script
# Downloads the appropriate Node.js binary for the current platform
# and installs production dependencies
# Parse command line arguments
FORCE_UPDATE=0
for arg in "$@"; do
case $arg in
--force|-f)
FORCE_UPDATE=1
shift
;;
*)
# Unknown option
;;
esac
done
# Find the directory where this script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
@ -74,8 +90,9 @@ case "$OS" in
esac
# Check if we already have the Node.js binary
if [ -f "$SCRIPT_DIR/vendor/$NODE_DIR/bin/node" ]; then
if [ -f "$SCRIPT_DIR/vendor/$NODE_DIR/bin/node" ] && [ $FORCE_UPDATE -eq 0 ]; then
echo "Node.js binary already exists for $OS-$ARCH. Skipping download."
echo "Use --force or -f to force update Node.js."
else
echo "Downloading Node.js v$NODE_VERSION for $OS-$ARCH..."
@ -222,25 +239,90 @@ echo "dist_ts directory successfully downloaded from npm registry."
# Make launcher script executable
chmod +x "$SCRIPT_DIR/bin/nupst"
# Install production dependencies
echo "Installing production dependencies..."
"$SCRIPT_DIR/vendor/$NODE_DIR/bin/npm" --prefix "$SCRIPT_DIR" ci --only=production --no-audit --no-fund
# Set up Node.js binary path
NODE_BIN_DIR="$SCRIPT_DIR/vendor/$NODE_DIR/bin"
NODE_BIN="$NODE_BIN_DIR/node"
NPM_CLI_JS="$NODE_BIN_DIR/../lib/node_modules/npm/bin/npm-cli.js"
if [ $? -ne 0 ]; then
echo "Warning: Failed to install dependencies with 'npm ci'. Trying 'npm install'..."
"$SCRIPT_DIR/vendor/$NODE_DIR/bin/npm" --prefix "$SCRIPT_DIR" install --only=production --no-audit --no-fund
# Ensure we have executable permissions
chmod +x "$NODE_BIN"
# Make sure the npm-cli.js exists
if [ ! -f "$NPM_CLI_JS" ]; then
# Try to find npm-cli.js
NPM_CLI_JS=$(find "$NODE_BIN_DIR/.." -name "npm-cli.js" | head -1)
if [ $? -ne 0 ]; then
echo "Error: Failed to install dependencies. NUPST may not function correctly."
echo "You can try to install dependencies manually by running:"
echo "cd $SCRIPT_DIR && npm install --only=production"
if [ -z "$NPM_CLI_JS" ]; then
echo "Warning: Could not find npm-cli.js, npm commands may fail"
# Set to a fallback value so code can continue
NPM_CLI_JS="$NODE_BIN_DIR/npm"
else
echo "Dependencies installed successfully with 'npm install'."
echo "Found npm-cli.js at: $NPM_CLI_JS"
fi
else
echo "Dependencies installed successfully with 'npm ci'."
fi
# Display which binaries we're using
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 "You can now run NUPST using: $SCRIPT_DIR/bin/nupst"
echo "To install NUPST globally, run: sudo ln -s $SCRIPT_DIR/bin/nupst /usr/local/bin/nupst"

View File

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

452
ts/cli.ts
View File

@ -30,7 +30,7 @@ export class NupstCli {
// Enable debug mode in the SNMP client
this.nupst.getSnmp().enableDebug();
}
// Get the command (default to help if none provided)
const command = args[2] || 'help';
@ -46,8 +46,8 @@ export class NupstCli {
private extractDebugOptions(args: string[]): { debugMode: boolean; cleanedArgs: string[] } {
const debugMode = args.includes('--debug') || args.includes('-d');
// Remove debug flags from args
const cleanedArgs = args.filter(arg => arg !== '--debug' && arg !== '-d');
const cleanedArgs = args.filter((arg) => arg !== '--debug' && arg !== '-d');
return { debugMode, cleanedArgs };
}
@ -85,23 +85,23 @@ export class NupstCli {
case 'disable':
await this.disable();
break;
case 'setup':
await this.setup();
break;
case 'test':
await this.test(debugMode);
break;
case 'update':
await this.update();
break;
case 'uninstall':
await this.uninstall();
break;
case 'config':
await this.showConfig();
break;
@ -149,17 +149,17 @@ export class NupstCli {
// Use exec with spawn to properly follow logs in real-time
const { spawn } = await import('child_process');
console.log('Tailing nupst service logs (Ctrl+C to exit)...\n');
const journalctl = spawn('journalctl', ['-u', 'nupst.service', '-n', '50', '-f'], {
stdio: ['ignore', 'inherit', 'inherit']
stdio: ['ignore', 'inherit', 'inherit'],
});
// Forward signals to child process
process.on('SIGINT', () => {
journalctl.kill('SIGINT');
process.exit(0);
});
// Wait for process to exit
await new Promise<void>((resolve) => {
journalctl.on('exit', () => resolve());
@ -229,21 +229,21 @@ export class NupstCli {
console.log('│ SNMP debugging enabled - detailed logs will be shown');
console.log('└──────────────────────────────────────────┘');
}
// Try to load the configuration
try {
await this.nupst.getDaemon().loadConfig();
} catch (error) {
console.error('┌─ Configuration Error ─────────────────────┐');
console.error('│ No configuration found.');
console.error('│ Please run \'nupst setup\' first to create a configuration.');
console.error("│ Please run 'nupst setup' first to create a configuration.");
console.error('└──────────────────────────────────────────┘');
return;
}
// Get current configuration
const config = this.nupst.getDaemon().getConfig();
this.displayTestConfig(config);
await this.testConnection(config);
} catch (error) {
@ -262,26 +262,26 @@ export class NupstCli {
console.log(`│ Port: ${config.snmp.port}`);
console.log(`│ Version: ${config.snmp.version}`);
console.log(`│ UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
if (config.snmp.version === 1 || config.snmp.version === 2) {
console.log(`│ Community: ${config.snmp.community}`);
} else if (config.snmp.version === 3) {
console.log(`│ Security Level: ${config.snmp.securityLevel}`);
console.log(`│ Username: ${config.snmp.username}`);
// Show auth and privacy details based on security level
if (config.snmp.securityLevel === 'authNoPriv' || config.snmp.securityLevel === 'authPriv') {
console.log(`│ Auth Protocol: ${config.snmp.authProtocol || 'None'}`);
}
if (config.snmp.securityLevel === 'authPriv') {
console.log(`│ Privacy Protocol: ${config.snmp.privProtocol || 'None'}`);
}
// Show timeout value
console.log(`│ Timeout: ${config.snmp.timeout / 1000} seconds`);
}
// Show OIDs if custom model is selected
if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) {
console.log('│ Custom OIDs:');
@ -304,20 +304,20 @@ export class NupstCli {
console.log('\nTesting connection to UPS...');
try {
// Create a test config with a short timeout
const testConfig = {
const testConfig = {
...config.snmp,
timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for testing
timeout: Math.min(config.snmp.timeout, 10000), // Use at most 10 seconds for testing
};
const status = await this.nupst.getSnmp().getUpsStatus(testConfig);
console.log('┌─ Connection Successful! ─────────────────┐');
console.log('│ UPS Status:');
console.log(`│ Power Status: ${status.powerStatus}`);
console.log(`│ Battery Capacity: ${status.batteryCapacity}%`);
console.log(`│ Runtime Remaining: ${status.batteryRuntime} minutes`);
console.log('└──────────────────────────────────────────┘');
// Check status against thresholds if on battery
if (status.powerStatus === 'onBattery') {
this.analyzeThresholds(status, config);
@ -326,7 +326,7 @@ export class NupstCli {
console.error('┌─ Connection Failed! ───────────────────────┐');
console.error(`│ Error: ${error.message}`);
console.error('└──────────────────────────────────────────┘');
console.log('\nPlease check your settings and run \'nupst setup\' to reconfigure.');
console.log("\nPlease check your settings and run 'nupst setup' to reconfigure.");
}
}
@ -337,25 +337,33 @@ export class NupstCli {
*/
private analyzeThresholds(status: any, config: any): void {
console.log('┌─ Threshold Analysis ───────────────────────┐');
if (status.batteryCapacity < config.thresholds.battery) {
console.log('│ ⚠️ WARNING: Battery capacity below threshold');
console.log(`│ Current: ${status.batteryCapacity}% | Threshold: ${config.thresholds.battery}%`);
console.log(
`│ Current: ${status.batteryCapacity}% | Threshold: ${config.thresholds.battery}%`
);
console.log('│ System would initiate shutdown');
} else {
console.log('│ ✓ Battery capacity above threshold');
console.log(`│ Current: ${status.batteryCapacity}% | Threshold: ${config.thresholds.battery}%`);
console.log(
`│ Current: ${status.batteryCapacity}% | Threshold: ${config.thresholds.battery}%`
);
}
if (status.batteryRuntime < config.thresholds.runtime) {
console.log('│ ⚠️ WARNING: Runtime below threshold');
console.log(`│ Current: ${status.batteryRuntime} min | Threshold: ${config.thresholds.runtime} min`);
console.log(
`│ Current: ${status.batteryRuntime} min | Threshold: ${config.thresholds.runtime} min`
);
console.log('│ System would initiate shutdown');
} else {
console.log('│ ✓ Runtime above threshold');
console.log(`│ Current: ${status.batteryRuntime} min | Threshold: ${config.thresholds.runtime} min`);
console.log(
`│ Current: ${status.batteryRuntime} min | Threshold: ${config.thresholds.runtime} min`
);
}
console.log('└──────────────────────────────────────────┘');
}
@ -386,60 +394,72 @@ Options:
(Example: nupst test --debug)
`);
}
/**
* Update NUPST from repository and refresh systemd service
*/
private async update(): Promise<void> {
try {
// Check if running as root
this.checkRootAccess('This command must be run as root to update NUPST and refresh the systemd service.');
this.checkRootAccess(
'This command must be run as root to update NUPST and refresh the systemd service.'
);
console.log('┌─ NUPST Update Process ──────────────────┐');
console.log('│ Updating NUPST from repository...');
// Determine the installation directory (assuming it's either /opt/nupst or the current directory)
const { existsSync } = await import('fs');
let installDir = '/opt/nupst';
if (!existsSync(installDir)) {
// If not installed in /opt/nupst, use the current directory
const { dirname } = await import('path');
installDir = dirname(dirname(process.argv[1])); // Go up two levels from the executable
console.log(`│ Using local installation directory: ${installDir}`);
}
try {
// 1. Update the repository
console.log('│ Pulling latest changes from git repository...');
execSync(`cd ${installDir} && git fetch origin && git reset --hard origin/main`, { stdio: 'pipe' });
execSync(`cd ${installDir} && git fetch origin && git reset --hard origin/main`, {
stdio: 'pipe',
});
// 2. Run the install.sh script
console.log('│ Running install.sh to update NUPST...');
execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' });
// 3. Run the setup.sh script
console.log('│ Running setup.sh to update dependencies...');
execSync(`cd ${installDir} && bash ./setup.sh`, { stdio: 'pipe' });
// 3. Run the setup.sh script with force flag to update Node.js and dependencies
console.log('│ Running setup.sh to update Node.js and dependencies...');
execSync(`cd ${installDir} && bash ./setup.sh --force`, { stdio: 'pipe' });
// 4. Refresh the systemd service
console.log('│ Refreshing systemd service...');
// First check if service exists
const serviceExists = execSync('systemctl list-unit-files | grep nupst.service').toString().includes('nupst.service');
let serviceExists = false;
try {
const output = execSync('systemctl list-unit-files | grep nupst.service').toString();
serviceExists = output.includes('nupst.service');
} catch (error) {
// If grep fails (service not found), serviceExists remains false
serviceExists = false;
}
if (serviceExists) {
// Stop the service if it's running
const isRunning = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
const isRunning =
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
if (isRunning) {
console.log('│ Stopping nupst service...');
execSync('systemctl stop nupst.service');
}
// Reinstall the service
console.log('│ Reinstalling systemd service...');
await this.nupst.getSystemd().install();
// Restart the service if it was running
if (isRunning) {
console.log('│ Restarting nupst service...');
@ -449,13 +469,13 @@ Options:
console.log('│ Systemd service not installed, skipping service refresh.');
console.log('│ Run "nupst enable" to install the service.');
}
console.log('│ Update completed successfully!');
console.log('└──────────────────────────────────────────┘');
console.log('└─────────────────────────────────────────────┘');
} catch (error) {
console.error('│ Error during update process:');
console.error(`${error.message}`);
console.error('└──────────────────────────────────────────┘');
console.error('└─────────────────────────────────────────────┘');
process.exit(1);
}
} catch (error) {
@ -463,7 +483,7 @@ Options:
process.exit(1);
}
}
/**
* Interactive setup for configuring SNMP settings
*/
@ -471,12 +491,12 @@ Options:
try {
// Import readline module (ESM style)
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
output: process.stdout,
});
// Helper function to prompt for input
const prompt = (question: string): Promise<string> => {
return new Promise((resolve) => {
@ -485,7 +505,7 @@ Options:
});
});
};
try {
await this.runSetupProcess(prompt);
} finally {
@ -504,7 +524,7 @@ Options:
console.log('\nNUPST Interactive Setup');
console.log('======================\n');
console.log('This will guide you through configuring your UPS SNMP settings.\n');
// Try to load existing config if available
let config;
try {
@ -518,24 +538,24 @@ Options:
// Gather SNMP settings
config = await this.gatherSnmpSettings(config, prompt);
// Gather threshold settings
config = await this.gatherThresholdSettings(config, prompt);
// Gather UPS model settings
config = await this.gatherUpsModelSettings(config, prompt);
// Save the configuration
await this.nupst.getDaemon().saveConfig(config);
this.displayConfigSummary(config);
// Test the connection if requested
await this.optionallyTestConnection(config, prompt);
// Check if service is running and restart it if needed
await this.restartServiceIfRunning();
console.log('\nSetup complete!');
await this.optionallyEnableService(prompt);
}
@ -546,18 +566,21 @@ Options:
* @param prompt Function to prompt for user input
* @returns Updated configuration
*/
private async gatherSnmpSettings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
private async gatherSnmpSettings(
config: any,
prompt: (question: string) => Promise<string>
): Promise<any> {
// SNMP IP Address
const defaultHost = config.snmp.host;
const host = await prompt(`UPS IP Address [${defaultHost}]: `);
config.snmp.host = host.trim() || defaultHost;
// SNMP Port
const defaultPort = config.snmp.port;
const portInput = await prompt(`SNMP Port [${defaultPort}]: `);
const port = parseInt(portInput, 10);
config.snmp.port = (portInput.trim() && !isNaN(port)) ? port : defaultPort;
config.snmp.port = portInput.trim() && !isNaN(port) ? port : defaultPort;
// SNMP Version
const defaultVersion = config.snmp.version;
console.log('\nSNMP Version:');
@ -566,8 +589,11 @@ Options:
console.log(' 3) SNMPv3 (with security features)');
const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `);
const version = parseInt(versionInput, 10);
config.snmp.version = (versionInput.trim() && (version === 1 || version === 2 || version === 3)) ? version : defaultVersion;
config.snmp.version =
versionInput.trim() && (version === 1 || version === 2 || version === 3)
? version
: defaultVersion;
if (config.snmp.version === 1 || config.snmp.version === 2) {
// SNMP Community String (for v1/v2c)
const defaultCommunity = config.snmp.community || 'public';
@ -577,7 +603,7 @@ Options:
// SNMP v3 settings
config = await this.gatherSnmpV3Settings(config, prompt);
}
return config;
}
@ -587,20 +613,27 @@ Options:
* @param prompt Function to prompt for user input
* @returns Updated configuration
*/
private async gatherSnmpV3Settings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
private async gatherSnmpV3Settings(
config: any,
prompt: (question: string) => Promise<string>
): Promise<any> {
console.log('\nSNMPv3 Security Settings:');
// Security Level
console.log('\nSecurity Level:');
console.log(' 1) noAuthNoPriv (No Authentication, No Privacy)');
console.log(' 2) authNoPriv (Authentication, No Privacy)');
console.log(' 3) authPriv (Authentication and Privacy)');
const defaultSecLevel = config.snmp.securityLevel ?
(config.snmp.securityLevel === 'noAuthNoPriv' ? 1 :
config.snmp.securityLevel === 'authNoPriv' ? 2 : 3) : 3;
const defaultSecLevel = config.snmp.securityLevel
? config.snmp.securityLevel === 'noAuthNoPriv'
? 1
: config.snmp.securityLevel === 'authNoPriv'
? 2
: 3
: 3;
const secLevelInput = await prompt(`Select Security Level [${defaultSecLevel}]: `);
const secLevel = parseInt(secLevelInput, 10) || defaultSecLevel;
if (secLevel === 1) {
config.snmp.securityLevel = 'noAuthNoPriv';
// No auth, no priv - clear out authentication and privacy settings
@ -622,31 +655,33 @@ Options:
// Set appropriate timeout for security level
config.snmp.timeout = 15000; // 15 seconds for full encryption
}
// Username
const defaultUsername = config.snmp.username || '';
const username = await prompt(`SNMPv3 Username [${defaultUsername}]: `);
config.snmp.username = username.trim() || defaultUsername;
if (secLevel >= 2) {
// Authentication settings
config = await this.gatherAuthenticationSettings(config, prompt);
if (secLevel === 3) {
// Privacy settings
config = await this.gatherPrivacySettings(config, prompt);
}
// Allow customizing the timeout value
const defaultTimeout = config.snmp.timeout / 1000; // Convert from ms to seconds for display
console.log('\nSNMPv3 operations with authentication and privacy may require longer timeouts.');
console.log(
'\nSNMPv3 operations with authentication and privacy may require longer timeouts.'
);
const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `);
const timeout = parseInt(timeoutInput, 10);
if (timeoutInput.trim() && !isNaN(timeout)) {
config.snmp.timeout = timeout * 1000; // Convert to ms
}
}
return config;
}
@ -656,21 +691,26 @@ Options:
* @param prompt Function to prompt for user input
* @returns Updated configuration
*/
private async gatherAuthenticationSettings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
private async gatherAuthenticationSettings(
config: any,
prompt: (question: string) => Promise<string>
): Promise<any> {
// Authentication protocol
console.log('\nAuthentication Protocol:');
console.log(' 1) MD5');
console.log(' 2) SHA');
const defaultAuthProtocol = config.snmp.authProtocol === 'SHA' ? 2 : 1;
const authProtocolInput = await prompt(`Select Authentication Protocol [${defaultAuthProtocol}]: `);
const authProtocolInput = await prompt(
`Select Authentication Protocol [${defaultAuthProtocol}]: `
);
const authProtocol = parseInt(authProtocolInput, 10) || defaultAuthProtocol;
config.snmp.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5';
// Authentication Key/Password
const defaultAuthKey = config.snmp.authKey || '';
const authKey = await prompt(`Authentication Password ${defaultAuthKey ? '[*****]' : ''}: `);
config.snmp.authKey = authKey.trim() || defaultAuthKey;
return config;
}
@ -680,7 +720,10 @@ Options:
* @param prompt Function to prompt for user input
* @returns Updated configuration
*/
private async gatherPrivacySettings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
private async gatherPrivacySettings(
config: any,
prompt: (question: string) => Promise<string>
): Promise<any> {
// Privacy protocol
console.log('\nPrivacy Protocol:');
console.log(' 1) DES');
@ -689,12 +732,12 @@ Options:
const privProtocolInput = await prompt(`Select Privacy Protocol [${defaultPrivProtocol}]: `);
const privProtocol = parseInt(privProtocolInput, 10) || defaultPrivProtocol;
config.snmp.privProtocol = privProtocol === 2 ? 'AES' : 'DES';
// Privacy Key/Password
const defaultPrivKey = config.snmp.privKey || '';
const privKey = await prompt(`Privacy Password ${defaultPrivKey ? '[*****]' : ''}: `);
config.snmp.privKey = privKey.trim() || defaultPrivKey;
return config;
}
@ -704,33 +747,43 @@ Options:
* @param prompt Function to prompt for user input
* @returns Updated configuration
*/
private async gatherThresholdSettings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
private async gatherThresholdSettings(
config: any,
prompt: (question: string) => Promise<string>
): Promise<any> {
console.log('\nShutdown Thresholds:');
// Battery threshold
const defaultBatteryThreshold = config.thresholds.battery;
const batteryThresholdInput = await prompt(`Battery percentage threshold [${defaultBatteryThreshold}%]: `);
const batteryThresholdInput = await prompt(
`Battery percentage threshold [${defaultBatteryThreshold}%]: `
);
const batteryThreshold = parseInt(batteryThresholdInput, 10);
config.thresholds.battery = (batteryThresholdInput.trim() && !isNaN(batteryThreshold))
? batteryThreshold
: defaultBatteryThreshold;
config.thresholds.battery =
batteryThresholdInput.trim() && !isNaN(batteryThreshold)
? batteryThreshold
: defaultBatteryThreshold;
// Runtime threshold
const defaultRuntimeThreshold = config.thresholds.runtime;
const runtimeThresholdInput = await prompt(`Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: `);
const runtimeThresholdInput = await prompt(
`Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: `
);
const runtimeThreshold = parseInt(runtimeThresholdInput, 10);
config.thresholds.runtime = (runtimeThresholdInput.trim() && !isNaN(runtimeThreshold))
? runtimeThreshold
: defaultRuntimeThreshold;
config.thresholds.runtime =
runtimeThresholdInput.trim() && !isNaN(runtimeThreshold)
? runtimeThreshold
: defaultRuntimeThreshold;
// Check interval
const defaultInterval = config.checkInterval / 1000; // Convert from ms to seconds for display
const intervalInput = await prompt(`Check interval in seconds [${defaultInterval}]: `);
const interval = parseInt(intervalInput, 10);
config.checkInterval = (intervalInput.trim() && !isNaN(interval))
? interval * 1000 // Convert to ms
: defaultInterval * 1000;
config.checkInterval =
intervalInput.trim() && !isNaN(interval)
? interval * 1000 // Convert to ms
: defaultInterval * 1000;
return config;
}
@ -740,7 +793,10 @@ Options:
* @param prompt Function to prompt for user input
* @returns Updated configuration
*/
private async gatherUpsModelSettings(config: any, prompt: (question: string) => Promise<string>): Promise<any> {
private async gatherUpsModelSettings(
config: any,
prompt: (question: string) => Promise<string>
): Promise<any> {
console.log('\nUPS Model Selection:');
console.log(' 1) CyberPower');
console.log(' 2) APC');
@ -748,17 +804,25 @@ Options:
console.log(' 4) TrippLite');
console.log(' 5) Liebert/Vertiv');
console.log(' 6) Custom (Advanced)');
const defaultModelValue = config.snmp.upsModel === 'cyberpower' ? 1 :
config.snmp.upsModel === 'apc' ? 2 :
config.snmp.upsModel === 'eaton' ? 3 :
config.snmp.upsModel === 'tripplite' ? 4 :
config.snmp.upsModel === 'liebert' ? 5 :
config.snmp.upsModel === 'custom' ? 6 : 1;
const defaultModelValue =
config.snmp.upsModel === 'cyberpower'
? 1
: config.snmp.upsModel === 'apc'
? 2
: config.snmp.upsModel === 'eaton'
? 3
: config.snmp.upsModel === 'tripplite'
? 4
: config.snmp.upsModel === 'liebert'
? 5
: config.snmp.upsModel === 'custom'
? 6
: 1;
const modelInput = await prompt(`Select UPS model [${defaultModelValue}]: `);
const modelValue = parseInt(modelInput, 10) || defaultModelValue;
if (modelValue === 1) {
config.snmp.upsModel = 'cyberpower';
} else if (modelValue === 2) {
@ -773,20 +837,20 @@ Options:
config.snmp.upsModel = 'custom';
console.log('\nEnter custom OIDs for your UPS:');
console.log('(Leave blank to use standard RFC 1628 OIDs as fallback)');
// Custom OIDs
const powerStatusOID = await prompt('Power Status OID: ');
const batteryCapacityOID = await prompt('Battery Capacity OID: ');
const batteryRuntimeOID = await prompt('Battery Runtime OID: ');
// Create custom OIDs object
config.snmp.customOIDs = {
POWER_STATUS: powerStatusOID.trim(),
BATTERY_CAPACITY: batteryCapacityOID.trim(),
BATTERY_RUNTIME: batteryRuntimeOID.trim()
BATTERY_RUNTIME: batteryRuntimeOID.trim(),
};
}
return config;
}
@ -799,8 +863,10 @@ Options:
console.log(`│ SNMP Host: ${config.snmp.host}:${config.snmp.port}`);
console.log(`│ SNMP Version: ${config.snmp.version}`);
console.log(`│ UPS Model: ${config.snmp.upsModel}`);
console.log(`│ Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`);
console.log(`│ Check Interval: ${config.checkInterval/1000} seconds`);
console.log(
`│ Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`
);
console.log(`│ Check Interval: ${config.checkInterval / 1000} seconds`);
console.log('└──────────────────────────────────────────┘\n');
}
@ -809,17 +875,22 @@ Options:
* @param config Current configuration
* @param prompt Function to prompt for user input
*/
private async optionallyTestConnection(config: any, prompt: (question: string) => Promise<string>): Promise<void> {
const testConnection = await prompt('Would you like to test the connection to your UPS? (y/N): ');
private async optionallyTestConnection(
config: any,
prompt: (question: string) => Promise<string>
): Promise<void> {
const testConnection = await prompt(
'Would you like to test the connection to your UPS? (y/N): '
);
if (testConnection.toLowerCase() === 'y') {
console.log('\nTesting connection to UPS...');
try {
// Create a test config with a short timeout
const testConfig = {
const testConfig = {
...config.snmp,
timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for testing
timeout: Math.min(config.snmp.timeout, 10000), // Use at most 10 seconds for testing
};
const status = await this.nupst.getSnmp().getUpsStatus(testConfig);
console.log('\n┌─ Connection Successful! ─────────────────┐');
console.log('│ UPS Status:');
@ -843,14 +914,15 @@ Options:
private async restartServiceIfRunning(): Promise<void> {
try {
// Check if the service is active
const isActive = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
const isActive =
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
if (isActive) {
// Service is running, restart it
console.log('┌─ Service Update ─────────────────────────┐');
console.log('┌─ Service Update ─────────────────────────┐');
console.log('│ Configuration has changed.');
console.log('│ Restarting NUPST service to apply changes...');
try {
if (process.getuid && process.getuid() === 0) {
// We have root access, restart directly
@ -866,8 +938,8 @@ Options:
console.log('│ You may need to restart the service manually:');
console.log('│ sudo systemctl restart nupst.service');
}
console.log('└──────────────────────────────────────────┘');
console.log('└──────────────────────────────────────────┘');
}
} catch (error) {
// Ignore errors checking service status
@ -878,18 +950,24 @@ Options:
* Optionally enable and start systemd service
* @param prompt Function to prompt for user input
*/
private async optionallyEnableService(prompt: (question: string) => Promise<string>): Promise<void> {
private async optionallyEnableService(
prompt: (question: string) => Promise<string>
): Promise<void> {
if (process.getuid && process.getuid() !== 0) {
console.log('\nNote: Run "sudo nupst enable" to set up NUPST as a system service.');
} else {
const setupService = await prompt('Would you like to enable NUPST as a system service? (y/N): ');
const setupService = await prompt(
'Would you like to enable NUPST as a system service? (y/N): '
);
if (setupService.toLowerCase() === 'y') {
try {
await this.nupst.getSystemd().install();
console.log('Service installed and enabled to start on boot.');
// Ask if the user wants to start the service now
const startService = await prompt('Would you like to start the NUPST service now? (Y/n): ');
const startService = await prompt(
'Would you like to start the NUPST service now? (Y/n): '
);
if (startService.toLowerCase() !== 'n') {
await this.nupst.getSystemd().start();
console.log('NUPST service started successfully.');
@ -902,7 +980,7 @@ Options:
}
}
}
/**
* Display the current configuration
*/
@ -914,68 +992,75 @@ Options:
} catch (error) {
console.error('┌─ Configuration Error ─────────────────────┐');
console.error('│ No configuration found.');
console.error('│ Please run \'nupst setup\' first to create a configuration.');
console.error("│ Please run 'nupst setup' first to create a configuration.");
console.error('└──────────────────────────────────────────┘');
return;
}
// Get current configuration
const config = this.nupst.getDaemon().getConfig();
console.log('┌─ NUPST Configuration ──────────────────────┐');
// SNMP Settings
console.log('│ SNMP Settings:');
console.log(`│ Host: ${config.snmp.host}`);
console.log(`│ Port: ${config.snmp.port}`);
console.log(`│ Version: ${config.snmp.version}`);
console.log(`│ UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
if (config.snmp.version === 1 || config.snmp.version === 2) {
console.log(`│ Community: ${config.snmp.community}`);
} else if (config.snmp.version === 3) {
console.log(`│ Security Level: ${config.snmp.securityLevel}`);
console.log(`│ Username: ${config.snmp.username}`);
// Show auth and privacy details based on security level
if (config.snmp.securityLevel === 'authNoPriv' || config.snmp.securityLevel === 'authPriv') {
if (
config.snmp.securityLevel === 'authNoPriv' ||
config.snmp.securityLevel === 'authPriv'
) {
console.log(`│ Auth Protocol: ${config.snmp.authProtocol || 'None'}`);
}
if (config.snmp.securityLevel === 'authPriv') {
console.log(`│ Privacy Protocol: ${config.snmp.privProtocol || 'None'}`);
}
// Show timeout value
console.log(`│ Timeout: ${config.snmp.timeout / 1000} seconds`);
}
// Show OIDs if custom model is selected
if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) {
console.log('│ Custom OIDs:');
console.log(`│ Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`);
console.log(`│ Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`);
console.log(
`│ Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`
);
console.log(`│ Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
}
// Thresholds
console.log('│ Thresholds:');
console.log(`│ Battery: ${config.thresholds.battery}%`);
console.log(`│ Runtime: ${config.thresholds.runtime} minutes`);
console.log(`│ Check Interval: ${config.checkInterval / 1000} seconds`);
// Configuration file location
console.log('│');
console.log('│ Configuration File Location:');
console.log('│ /etc/nupst/config.json');
console.log('└──────────────────────────────────────────┘');
// Show service status
try {
const isActive = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
const isEnabled = execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled';
const isActive =
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
const isEnabled =
execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled';
console.log('┌─ Service Status ─────────────────────────┐');
console.log(`│ Service Active: ${isActive ? 'Yes' : 'No'}`);
console.log(`│ Service Enabled: ${isEnabled ? 'Yes' : 'No'}`);
@ -983,7 +1068,6 @@ Options:
} catch (error) {
// Ignore errors checking service status
}
} catch (error) {
console.error(`Failed to display configuration: ${error.message}`);
}
@ -999,12 +1083,12 @@ Options:
try {
// Import readline module for user input
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
output: process.stdout,
});
// Helper function to prompt for input
const prompt = (question: string): Promise<string> => {
return new Promise((resolve) => {
@ -1019,11 +1103,13 @@ Options:
console.log('This will completely remove NUPST from your system.\n');
// Ask about removing configuration
const removeConfig = await prompt('Do you want to remove the NUPST configuration files? (y/N): ');
const removeConfig = await prompt(
'Do you want to remove the NUPST configuration files? (y/N): '
);
// Find the uninstall.sh script location
let uninstallScriptPath: string;
// Try to determine script location based on executable path
try {
// For ESM, we can use import.meta.url, but since we might be in CJS
@ -1031,16 +1117,13 @@ Options:
const binPath = process.argv[1];
const modulePath = dirname(dirname(binPath));
uninstallScriptPath = join(modulePath, 'uninstall.sh');
// Check if the script exists
await fs.access(uninstallScriptPath);
} catch (error) {
// If we can't find it in the expected location, try common installation paths
const commonPaths = [
'/opt/nupst/uninstall.sh',
join(process.cwd(), 'uninstall.sh')
];
const commonPaths = ['/opt/nupst/uninstall.sh', join(process.cwd(), 'uninstall.sh')];
for (const path of commonPaths) {
try {
await fs.access(path);
@ -1050,37 +1133,36 @@ Options:
// Continue to next path
}
}
if (!uninstallScriptPath) {
console.error('Could not locate uninstall.sh script. Aborting uninstall.');
rl.close();
process.exit(1);
}
}
// Close readline before executing script
rl.close();
// Execute uninstall.sh with the appropriate option
console.log(`\nRunning uninstaller from ${uninstallScriptPath}...`);
// Pass the configuration removal option as an environment variable
const env = {
...process.env,
REMOVE_CONFIG: removeConfig.toLowerCase() === 'y' ? 'yes' : 'no',
REMOVE_REPO: 'yes', // Always remove repo as requested
NUPST_CLI_CALL: 'true' // Flag to indicate this is being called from CLI
REMOVE_REPO: 'yes', // Always remove repo as requested
NUPST_CLI_CALL: 'true', // Flag to indicate this is being called from CLI
};
// Run the uninstall script with sudo
execSync(`sudo bash ${uninstallScriptPath}`, {
execSync(`sudo bash ${uninstallScriptPath}`, {
env,
stdio: 'inherit' // Show output in the terminal
stdio: 'inherit', // Show output in the terminal
});
} catch (error) {
console.error(`Uninstall failed: ${error.message}`);
process.exit(1);
}
}
}
}

View File

@ -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
@ -124,7 +125,7 @@ export class NupstDaemon {
console.error('┌─ Configuration Error ─────────────────────┐');
console.error(`${message}`);
console.error('│ Please run \'nupst setup\' first to create a configuration.');
console.error('└──────────────────────────────────────────┘');
console.error('└──────────────────────────────────────────┘');
}
/**
@ -195,7 +196,7 @@ export class NupstDaemon {
console.log(`│ Battery: ${this.config.thresholds.battery}%`);
console.log(`│ Runtime: ${this.config.thresholds.runtime} minutes`);
console.log(`│ Check Interval: ${this.config.checkInterval / 1000} seconds`);
console.log('└──────────────────────────────────────────┘');
console.log('└────────────────────────────────────────────┘');
}
/**
@ -225,20 +226,20 @@ export class NupstDaemon {
// Log status changes
if (status.powerStatus !== lastStatus) {
console.log('┌──────────────────────────────────────────┐');
console.log(`Power status changed: ${lastStatus}${status.powerStatus}`);
console.log('└──────────────────────────────────────────┘');
console.log('┌─ Power Status Change ─────────────────────┐');
console.log(`Status changed: ${lastStatus}${status.powerStatus}`);
console.log('└──────────────────────────────────────────┘');
lastStatus = status.powerStatus;
lastLogTime = currentTime; // Reset log timer when status changes
}
// Log status periodically (at least every 5 minutes)
else if (shouldLogStatus) {
const timestamp = new Date().toISOString();
console.log('┌──────────────────────────────────────────┐');
console.log(`[${timestamp}] Periodic Status Update`);
console.log('┌─ Periodic Status Update ──────────────────┐');
console.log(`Timestamp: ${timestamp}`);
console.log(`│ Power Status: ${status.powerStatus}`);
console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
console.log('└──────────────────────────────────────────┘');
console.log('└──────────────────────────────────────────┘');
lastLogTime = currentTime;
}
@ -266,8 +267,8 @@ export class NupstDaemon {
batteryCapacity: number,
batteryRuntime: number
}): Promise<void> {
console.log('┌─ UPS Status ───────────────────────────────┐');
console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
console.log('┌─ UPS Status ─────────────────────────────┐');
console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
console.log('└──────────────────────────────────────────┘');
// Check battery threshold
@ -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

View File

@ -162,7 +162,7 @@ export class Nupst {
*/
public logVersionInfo(checkForUpdates: boolean = true): void {
const version = this.getVersion();
console.log('┌─ NUPST Version ────────────────────────┐');
console.log('┌─ NUPST Version ────────────────────────────┐');
console.log(`│ Current Version: ${version}`);
if (this.updateAvailable && this.latestVersion) {

View File

@ -1,4 +1,3 @@
import * as dgram from 'dgram';
import * as snmp from 'net-snmp';
import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.js';
import { UpsOidSets } from './oid-sets.js';

View File

@ -66,7 +66,7 @@ WantedBy=multi-user.target
// Write the service file
await fs.writeFile(this.serviceFilePath, this.serviceTemplate);
console.log('┌─ Service Installation ─────────────────────┐');
console.log('┌─ Service Installation ─────────────────────┐');
console.log(`│ Service file created at ${this.serviceFilePath}`);
// Reload systemd daemon
@ -76,7 +76,7 @@ WantedBy=multi-user.target
// Enable the service
execSync('systemctl enable nupst.service');
console.log('│ Service enabled to start on boot');
console.log('└──────────────────────────────────────────┘');
console.log('└─────────────────────────────────────────────┘');
} catch (error) {
if (error.message === 'Configuration not found') {
// Just rethrow the error as the message has already been displayed
@ -97,9 +97,9 @@ WantedBy=multi-user.target
await this.checkConfigExists();
execSync('systemctl start nupst.service');
console.log('┌─ Service Status ─────────────────────────┐');
console.log('┌─ Service Status ───────────────────────────┐');
console.log('│ NUPST service started successfully');
console.log('└──────────────────────────────────────────┘');
console.log('└────────────────────────────────────────────┘');
} catch (error) {
if (error.message === 'Configuration not found') {
// Exit with error code since configuration is required
@ -190,20 +190,20 @@ WantedBy=multi-user.target
timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for status check
};
console.log('┌─ Connecting to UPS... ────────────────────┐');
console.log('┌─ Connecting to UPS... ────────────────────┐');
console.log(`│ Host: ${config.snmp.host}:${config.snmp.port}`);
console.log(`│ UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
console.log('└──────────────────────────────────────────┘');
console.log('└────────────────────────────────────────────┘');
const status = await snmp.getUpsStatus(snmpConfig);
console.log('┌─ UPS Status ───────────────────────────────┐');
console.log('┌─ UPS Status ─────────────────────────────┐');
console.log(`│ Power Status: ${status.powerStatus}`);
console.log(`│ Battery Capacity: ${status.batteryCapacity}%`);
console.log(`│ Runtime Remaining: ${status.batteryRuntime} minutes`);
console.log('└──────────────────────────────────────────┘');
} catch (error) {
console.error('┌─ UPS Status ───────────────────────────────┐');
console.error('┌─ UPS Status ─────────────────────────────┐');
console.error(`│ Failed to retrieve UPS status: ${error.message}`);
console.error('└──────────────────────────────────────────┘');
}