style: configure deno fmt to use single quotes
All checks were successful
CI / Build All Platforms (Tag/Main only) (push) Has been skipped
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 6s

- Add singleQuote: true to deno.json fmt configuration
- Reformat all files with single quotes using deno fmt
This commit is contained in:
2025-10-19 13:14:18 +00:00
parent b935087d50
commit 071ded9c41
24 changed files with 1094 additions and 672 deletions

View File

@@ -4,5 +4,5 @@
export const commitinfo = {
name: '@serve.zone/nupst',
version: '3.1.2',
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,4 +1,4 @@
import { execSync } from "node:child_process";
import { execSync } from 'node:child_process';
import { Nupst } from './nupst.ts';
import { logger } from './logger.ts';
@@ -62,7 +62,11 @@ export class NupstCli {
* @param commandArgs Additional command arguments
* @param debugMode Whether debug mode is enabled
*/
private async executeCommand(command: string, commandArgs: string[], debugMode: boolean): Promise<void> {
private async executeCommand(
command: string,
commandArgs: string[],
debugMode: boolean,
): Promise<void> {
// Get access to the handlers
const upsHandler = this.nupst.getUpsHandler();
const groupHandler = this.nupst.getGroupHandler();
@@ -87,7 +91,7 @@ export class NupstCli {
break;
case 'restart':
await serviceHandler.stop();
await new Promise(resolve => setTimeout(resolve, 2000)); // Wait 2s
await new Promise((resolve) => setTimeout(resolve, 2000)); // Wait 2s
await serviceHandler.start();
break;
case 'status':
@@ -263,7 +267,9 @@ export class NupstCli {
await serviceHandler.logs();
break;
case 'daemon-start':
logger.log("Note: 'nupst daemon-start' is deprecated. Use 'nupst service start-daemon' instead.");
logger.log(
"Note: 'nupst daemon-start' is deprecated. Use 'nupst service start-daemon' instead.",
);
await serviceHandler.daemonStart(debugMode);
break;
@@ -328,8 +334,12 @@ export class NupstCli {
logger.logBoxLine(`${ups.name} (${ups.id}):`);
logger.logBoxLine(` Host: ${ups.snmp.host}:${ups.snmp.port}`);
logger.logBoxLine(` Model: ${ups.snmp.upsModel}`);
logger.logBoxLine(` Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`);
logger.logBoxLine(` Groups: ${ups.groups.length > 0 ? ups.groups.join(', ') : 'None'}`);
logger.logBoxLine(
` Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`,
);
logger.logBoxLine(
` Groups: ${ups.groups.length > 0 ? ups.groups.join(', ') : 'None'}`,
);
logger.logBoxLine('');
}
logger.logBoxEnd();
@@ -344,11 +354,16 @@ export class NupstCli {
if (group.description) {
logger.logBoxLine(` Description: ${group.description}`);
}
// List UPS devices in this group
const upsInGroup = config.upsDevices.filter(ups => ups.groups && ups.groups.includes(group.id));
logger.logBoxLine(` UPS Devices: ${upsInGroup.length > 0 ?
upsInGroup.map(ups => ups.name).join(', ') : 'None'}`);
const upsInGroup = config.upsDevices.filter((ups) =>
ups.groups && ups.groups.includes(group.id)
);
logger.logBoxLine(
` UPS Devices: ${
upsInGroup.length > 0 ? upsInGroup.map((ups) => ups.name).join(', ') : 'None'
}`,
);
logger.logBoxLine('');
}
logger.logBoxEnd();
@@ -390,11 +405,15 @@ export class NupstCli {
// Show OIDs if custom model is selected
if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) {
logger.logBoxLine('Custom OIDs:');
logger.logBoxLine(` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`);
logger.logBoxLine(
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`
` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`,
);
logger.logBoxLine(
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
);
logger.logBoxLine(
` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`,
);
logger.logBoxLine(` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
}
}
@@ -435,7 +454,11 @@ export class NupstCli {
// Ignore errors checking service status
}
} catch (error) {
logger.error(`Failed to display configuration: ${error instanceof Error ? error.message : String(error)}`);
logger.error(
`Failed to display configuration: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
@@ -584,4 +607,4 @@ Examples:
nupst group remove dc-1 - Remove group with ID 'dc-1'
`);
}
}
}

View File

@@ -35,10 +35,10 @@ export class GroupHandler {
logger.logBoxEnd();
return;
}
// Get current configuration
const config = this.nupst.getDaemon().getConfig();
// Check if multi-UPS config
if (!config.groups || !Array.isArray(config.groups)) {
// Legacy or missing groups configuration
@@ -49,11 +49,11 @@ export class GroupHandler {
logger.logBoxEnd();
return;
}
// Display group list
const boxWidth = 60;
logger.logBoxTitle('UPS Groups', boxWidth);
if (config.groups.length === 0) {
logger.logBoxLine('No UPS groups configured.');
logger.logBoxLine('Use "nupst group add" to add a UPS group.');
@@ -62,27 +62,29 @@ export class GroupHandler {
logger.logBoxLine('');
logger.logBoxLine('ID | Name | Mode | UPS Devices');
logger.logBoxLine('-----------+----------------------+--------------+----------------');
for (const group of config.groups) {
const id = group.id.padEnd(10, ' ').substring(0, 10);
const name = (group.name || '').padEnd(20, ' ').substring(0, 20);
const mode = (group.mode || 'unknown').padEnd(12, ' ').substring(0, 12);
// Count UPS devices in this group
const upsInGroup = config.upsDevices.filter(ups => ups.groups.includes(group.id));
const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id));
const upsCount = upsInGroup.length;
const upsNames = upsInGroup.map(ups => ups.name).join(', ');
const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`);
}
}
logger.logBoxEnd();
} catch (error) {
logger.error(`Failed to list UPS groups: ${error instanceof Error ? error.message : String(error)}`);
logger.error(
`Failed to list UPS groups: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Add a new UPS group
*/
@@ -110,54 +112,56 @@ export class GroupHandler {
try {
await this.nupst.getDaemon().loadConfig();
} catch (error) {
logger.error('No configuration found. Please run "nupst setup" first to create a configuration.');
logger.error(
'No configuration found. Please run "nupst setup" first to create a configuration.',
);
return;
}
// Get current configuration
const config = this.nupst.getDaemon().getConfig();
// Initialize groups array if not exists
if (!config.groups) {
config.groups = [];
}
// Check if upsDevices is initialized
if (!config.upsDevices) {
config.upsDevices = [];
}
logger.log('\nNUPST Add Group');
logger.log('==============\n');
logger.log('This will guide you through creating a new UPS group.\n');
// Generate a new unique group ID
const groupId = helpers.shortId();
// Get group name
const name = await prompt('Group Name: ');
// Get group mode
const modeInput = await prompt('Group Mode (redundant/nonRedundant) [redundant]: ');
const mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
// Get optional description
const description = await prompt('Group Description (optional): ');
// Create the new group
const newGroup: IGroupConfig = {
id: groupId,
name: name || `Group-${groupId}`,
mode,
description: description || undefined
description: description || undefined,
};
// Add the group to the configuration
config.groups.push(newGroup);
// Save the configuration
await this.nupst.getDaemon().saveConfig(config);
// Display summary
const boxWidth = 45;
logger.logBoxTitle('Group Created', boxWidth);
@@ -168,21 +172,23 @@ export class GroupHandler {
logger.logBoxLine(`Description: ${newGroup.description}`);
}
logger.logBoxEnd();
// Check if there are UPS devices to assign to this group
if (config.upsDevices.length > 0) {
const assignUps = await prompt('Would you like to assign UPS devices to this group now? (y/N): ');
const assignUps = await prompt(
'Would you like to assign UPS devices to this group now? (y/N): ',
);
if (assignUps.toLowerCase() === 'y') {
await this.assignUpsToGroup(newGroup.id, config, prompt);
// Save again after assigning UPS devices
await this.nupst.getDaemon().saveConfig(config);
}
}
// Check if service is running and restart it if needed
this.nupst.getUpsHandler().restartServiceIfRunning();
logger.log('\nGroup setup complete!');
} finally {
rl.close();
@@ -191,7 +197,7 @@ export class GroupHandler {
logger.error(`Add group error: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Edit an existing UPS group
* @param groupId ID of the group to edit
@@ -220,57 +226,61 @@ export class GroupHandler {
try {
await this.nupst.getDaemon().loadConfig();
} catch (error) {
logger.error('No configuration found. Please run "nupst setup" first to create a configuration.');
logger.error(
'No configuration found. Please run "nupst setup" first to create a configuration.',
);
return;
}
// Get current configuration
const config = this.nupst.getDaemon().getConfig();
// Check if groups are initialized
if (!config.groups || !Array.isArray(config.groups)) {
logger.error('No groups configured. Please run "nupst group add" first to create a group.');
logger.error(
'No groups configured. Please run "nupst group add" first to create a group.',
);
return;
}
// Find the group to edit
const groupIndex = config.groups.findIndex(group => group.id === groupId);
const groupIndex = config.groups.findIndex((group) => group.id === groupId);
if (groupIndex === -1) {
logger.error(`Group with ID "${groupId}" not found.`);
return;
}
const group = config.groups[groupIndex];
logger.log(`\nNUPST Edit Group: ${group.name} (${group.id})`);
logger.log('==============================================\n');
// Edit group name
const newName = await prompt(`Group Name [${group.name}]: `);
if (newName.trim()) {
group.name = newName;
}
// Edit group mode
const currentMode = group.mode || 'redundant';
const modeInput = await prompt(`Group Mode (redundant/nonRedundant) [${currentMode}]: `);
if (modeInput.trim()) {
group.mode = modeInput.toLowerCase() === 'nonredundant' ? 'nonRedundant' : 'redundant';
}
// Edit description
const currentDesc = group.description || '';
const newDesc = await prompt(`Group Description [${currentDesc}]: `);
if (newDesc.trim() || newDesc === '') {
group.description = newDesc.trim() || undefined;
}
// Update the group in the configuration
config.groups[groupIndex] = group;
// Save the configuration
await this.nupst.getDaemon().saveConfig(config);
// Display summary
const boxWidth = 45;
logger.logBoxTitle('Group Updated', boxWidth);
@@ -281,19 +291,21 @@ export class GroupHandler {
logger.logBoxLine(`Description: ${group.description}`);
}
logger.logBoxEnd();
// Edit UPS assignments if requested
const editAssignments = await prompt('Would you like to edit UPS assignments for this group? (y/N): ');
const editAssignments = await prompt(
'Would you like to edit UPS assignments for this group? (y/N): ',
);
if (editAssignments.toLowerCase() === 'y') {
await this.assignUpsToGroup(group.id, config, prompt);
// Save again after editing assignments
await this.nupst.getDaemon().saveConfig(config);
}
// Check if service is running and restart it if needed
this.nupst.getUpsHandler().restartServiceIfRunning();
logger.log('\nGroup edit complete!');
} finally {
rl.close();
@@ -302,7 +314,7 @@ export class GroupHandler {
logger.error(`Edit group error: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Delete an existing UPS group
* @param groupId ID of the group to delete
@@ -313,48 +325,53 @@ export class GroupHandler {
try {
await this.nupst.getDaemon().loadConfig();
} catch (error) {
logger.error('No configuration found. Please run "nupst setup" first to create a configuration.');
logger.error(
'No configuration found. Please run "nupst setup" first to create a configuration.',
);
return;
}
// Get current configuration
const config = this.nupst.getDaemon().getConfig();
// Check if groups are initialized
if (!config.groups || !Array.isArray(config.groups)) {
logger.error('No groups configured.');
return;
}
// Find the group to delete
const groupIndex = config.groups.findIndex(group => group.id === groupId);
const groupIndex = config.groups.findIndex((group) => group.id === groupId);
if (groupIndex === -1) {
logger.error(`Group with ID "${groupId}" not found.`);
return;
}
const groupToDelete = config.groups[groupIndex];
// Get confirmation before deleting
const readline = await import('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const confirm = await new Promise<string>(resolve => {
rl.question(`Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `, answer => {
resolve(answer.toLowerCase());
});
const confirm = await new Promise<string>((resolve) => {
rl.question(
`Are you sure you want to delete group "${groupToDelete.name}" (${groupId})? [y/N]: `,
(answer) => {
resolve(answer.toLowerCase());
},
);
});
rl.close();
if (confirm !== 'y' && confirm !== 'yes') {
logger.log('Deletion cancelled.');
return;
}
// Remove this group from all UPS device group assignments
if (config.upsDevices && Array.isArray(config.upsDevices)) {
for (const ups of config.upsDevices) {
@@ -364,19 +381,21 @@ export class GroupHandler {
}
}
}
// Remove the group from the array
config.groups.splice(groupIndex, 1);
// Save the configuration
await this.nupst.getDaemon().saveConfig(config);
logger.log(`Group "${groupToDelete.name}" (${groupId}) has been deleted.`);
// Check if service is running and restart it if needed
this.nupst.getUpsHandler().restartServiceIfRunning();
} catch (error) {
logger.error(`Failed to delete group: ${error instanceof Error ? error.message : String(error)}`);
logger.error(
`Failed to delete group: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
@@ -389,18 +408,18 @@ export class GroupHandler {
public async assignUpsToGroups(
ups: any,
groups: any[],
prompt: (question: string) => Promise<string>
prompt: (question: string) => Promise<string>,
): Promise<void> {
// Initialize groups array if it doesn't exist
if (!ups.groups) {
ups.groups = [];
}
// Show current group assignments
logger.log('\nCurrent Group Assignments:');
if (ups.groups && ups.groups.length > 0) {
for (const groupId of ups.groups) {
const group = groups.find(g => g.id === groupId);
const group = groups.find((g) => g.id === groupId);
if (group) {
logger.log(`- ${group.name} (${group.id})`);
} else {
@@ -410,52 +429,56 @@ export class GroupHandler {
} else {
logger.log('- None');
}
// Show available groups
logger.log('\nAvailable Groups:');
if (groups.length === 0) {
logger.log('- No groups available. Use "nupst group add" to create groups.');
return;
}
for (let i = 0; i < groups.length; i++) {
const group = groups[i];
const assigned = ups.groups && ups.groups.includes(group.id);
logger.log(`${i + 1}) ${group.name} (${group.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`);
logger.log(
`${i + 1}) ${group.name} (${group.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`,
);
}
// Prompt for group selection
const selection = await prompt('\nSelect groups to assign/unassign (comma-separated numbers, or "clear" to remove all): ');
const selection = await prompt(
'\nSelect groups to assign/unassign (comma-separated numbers, or "clear" to remove all): ',
);
if (selection.toLowerCase() === 'clear') {
// Clear all group assignments
ups.groups = [];
logger.log('All group assignments cleared.');
return;
}
if (!selection.trim()) {
// No change if empty input
return;
}
// Process selections
const selections = selection.split(',').map(s => s.trim());
const selections = selection.split(',').map((s) => s.trim());
for (const sel of selections) {
const index = parseInt(sel, 10) - 1;
if (isNaN(index) || index < 0 || index >= groups.length) {
logger.error(`Invalid selection: ${sel}`);
continue;
}
const group = groups[index];
// Initialize groups array if needed (should already be done above)
if (!ups.groups) {
ups.groups = [];
}
// Toggle assignment
const groupIndex = ups.groups.indexOf(group.id);
if (groupIndex === -1) {
@@ -469,7 +492,7 @@ export class GroupHandler {
}
}
}
/**
* Assign UPS devices to a specific group
* @param groupId Group ID to assign UPS devices to
@@ -479,22 +502,24 @@ export class GroupHandler {
public async assignUpsToGroup(
groupId: string,
config: any,
prompt: (question: string) => Promise<string>
prompt: (question: string) => Promise<string>,
): Promise<void> {
if (!config.upsDevices || config.upsDevices.length === 0) {
logger.log('No UPS devices available. Use "nupst add" to add UPS devices.');
return;
}
const group = config.groups.find((g: { id: string }) => g.id === groupId);
if (!group) {
logger.error(`Group with ID "${groupId}" not found.`);
return;
}
// Show current assignments
logger.log(`\nUPS devices in group "${group.name}" (${group.id}):`);
const upsInGroup = config.upsDevices.filter((ups: { groups?: string[] }) => ups.groups && ups.groups.includes(groupId));
const upsInGroup = config.upsDevices.filter((ups: { groups?: string[] }) =>
ups.groups && ups.groups.includes(groupId)
);
if (upsInGroup.length === 0) {
logger.log('- None');
} else {
@@ -502,7 +527,7 @@ export class GroupHandler {
logger.log(`- ${ups.name} (${ups.id})`);
}
}
// Show all UPS devices
logger.log('\nAvailable UPS devices:');
for (let i = 0; i < config.upsDevices.length; i++) {
@@ -510,10 +535,12 @@ export class GroupHandler {
const assigned = ups.groups && ups.groups.includes(groupId);
logger.log(`${i + 1}) ${ups.name} (${ups.id}) [${assigned ? 'Assigned' : 'Not Assigned'}]`);
}
// Prompt for UPS selection
const selection = await prompt('\nSelect UPS devices to assign/unassign (comma-separated numbers, or "clear" to remove all): ');
const selection = await prompt(
'\nSelect UPS devices to assign/unassign (comma-separated numbers, or "clear" to remove all): ',
);
if (selection.toLowerCase() === 'clear') {
// Clear all UPS from this group
for (const ups of config.upsDevices) {
@@ -527,29 +554,29 @@ export class GroupHandler {
logger.log(`All UPS devices removed from group "${group.name}".`);
return;
}
if (!selection.trim()) {
// No change if empty input
return;
}
// Process selections
const selections = selection.split(',').map(s => s.trim());
const selections = selection.split(',').map((s) => s.trim());
for (const sel of selections) {
const index = parseInt(sel, 10) - 1;
if (isNaN(index) || index < 0 || index >= config.upsDevices.length) {
logger.error(`Invalid selection: ${sel}`);
continue;
}
const ups = config.upsDevices[index];
// Initialize groups array if needed
if (!ups.groups) {
ups.groups = [];
}
// Toggle assignment
const groupIndex = ups.groups.indexOf(groupId);
if (groupIndex === -1) {
@@ -563,4 +590,4 @@ export class GroupHandler {
}
}
}
}
}

View File

@@ -1,5 +1,5 @@
import process from 'node:process';
import { execSync } from "node:child_process";
import { execSync } from 'node:child_process';
import { Nupst } from '../nupst.ts';
import { logger } from '../logger.ts';
@@ -129,7 +129,7 @@ export class ServiceHandler {
try {
// Check if running as root
this.checkRootAccess(
'This command must be run as root to update NUPST and refresh the systemd service.'
'This command must be run as root to update NUPST and refresh the systemd service.',
);
const boxWidth = 45;
@@ -243,7 +243,7 @@ export class ServiceHandler {
// Ask about removing configuration
const removeConfig = await prompt(
'Do you want to remove the NUPST configuration files? (y/N): '
'Do you want to remove the NUPST configuration files? (y/N): ',
);
// Find the uninstall.sh script location
@@ -318,4 +318,4 @@ export class ServiceHandler {
return { debugMode, cleanedArgs };
}
}
}

View File

@@ -1,5 +1,5 @@
import process from 'node:process';
import { execSync } from "node:child_process";
import { execSync } from 'node:child_process';
import { Nupst } from '../nupst.ts';
import { logger } from '../logger.ts';
import * as helpers from '../helpers/index.ts';
@@ -67,7 +67,7 @@ export class UpsHandler {
try {
await this.nupst.getDaemon().loadConfig();
config = this.nupst.getDaemon().getConfig();
// Convert old format to new format if needed
if (!config.upsDevices) {
// Initialize with the current config as the first UPS
@@ -78,9 +78,9 @@ export class UpsHandler {
name: 'Default UPS',
snmp: config.snmp,
thresholds: config.thresholds,
groups: []
groups: [],
}],
groups: []
groups: [],
};
logger.log('Converting existing configuration to multi-UPS format.');
}
@@ -89,7 +89,7 @@ export class UpsHandler {
config = {
checkInterval: 30000, // Default check interval
upsDevices: [],
groups: []
groups: [],
};
logger.log('No existing configuration found. Creating a new configuration.');
}
@@ -108,13 +108,13 @@ export class UpsHandler {
community: 'public',
version: 1,
timeout: 5000,
upsModel: 'cyberpower' as TUpsModel
upsModel: 'cyberpower' as TUpsModel,
},
thresholds: {
battery: 60,
runtime: 20
runtime: 20,
},
groups: []
groups: [],
};
// Gather SNMP settings
@@ -144,10 +144,10 @@ export class UpsHandler {
// Test the connection if requested
await this.optionallyTestConnection(newUps.snmp, prompt);
// Check if service is running and restart it if needed
await this.restartServiceIfRunning();
logger.log('\nSetup complete!');
}
@@ -189,10 +189,13 @@ export class UpsHandler {
* @param upsId ID of the UPS to edit (undefined for default UPS)
* @param prompt Function to prompt for user input
*/
public async runEditProcess(upsId: string | undefined, prompt: (question: string) => Promise<string>): Promise<void> {
public async runEditProcess(
upsId: string | undefined,
prompt: (question: string) => Promise<string>,
): Promise<void> {
logger.log('\nNUPST Edit UPS');
logger.log('=============\n');
// Try to load existing config
try {
await this.nupst.getDaemon().loadConfig();
@@ -208,10 +211,10 @@ export class UpsHandler {
return;
}
}
// Get the config
const config = this.nupst.getDaemon().getConfig();
// Convert old format to new format if needed
if (!config.upsDevices) {
// Initialize with the current config as the first UPS
@@ -224,17 +227,17 @@ export class UpsHandler {
name: 'Default UPS',
snmp: config.snmp,
thresholds: config.thresholds,
groups: []
groups: [],
}];
config.groups = [];
logger.log('Converting existing configuration to multi-UPS format.');
}
// Find the UPS to edit
let upsToEdit;
if (upsId) {
// Find specific UPS by ID
upsToEdit = config.upsDevices.find(ups => ups.id === upsId);
upsToEdit = config.upsDevices.find((ups) => ups.id === upsId);
if (!upsToEdit) {
logger.error(`UPS with ID "${upsId}" not found.`);
return;
@@ -249,41 +252,41 @@ export class UpsHandler {
upsToEdit = config.upsDevices[0];
logger.log(`Editing default UPS: ${upsToEdit.name} (${upsToEdit.id})\n`);
}
// Allow editing UPS name
const newName = await prompt(`UPS Name [${upsToEdit.name}]: `);
if (newName.trim()) {
upsToEdit.name = newName;
}
// Edit SNMP settings
await this.gatherSnmpSettings(upsToEdit.snmp, prompt);
// Edit threshold settings
await this.gatherThresholdSettings(upsToEdit.thresholds, prompt);
// Edit UPS model settings
await this.gatherUpsModelSettings(upsToEdit.snmp, prompt);
// Get access to GroupHandler for group assignments
const groupHandler = this.nupst.getGroupHandler();
// Edit group assignments
if (config.groups && config.groups.length > 0) {
await groupHandler.assignUpsToGroups(upsToEdit, config.groups, prompt);
}
// Save the configuration
await this.nupst.getDaemon().saveConfig(config);
this.displayUpsConfigSummary(upsToEdit);
// Test the connection if requested
await this.optionallyTestConnection(upsToEdit.snmp, prompt);
// Check if service is running and restart it if needed
await this.restartServiceIfRunning();
logger.log('\nEdit complete!');
}
@@ -304,58 +307,63 @@ export class UpsHandler {
logger.logBoxEnd();
return;
}
// Get current configuration
const config = this.nupst.getDaemon().getConfig();
// Check if multi-UPS config
if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
logger.error('Legacy single-UPS configuration detected. Cannot delete UPS.');
logger.log('Use "nupst add" to migrate to multi-UPS configuration format first.');
return;
}
// Find the UPS to delete
const upsIndex = config.upsDevices.findIndex(ups => ups.id === upsId);
const upsIndex = config.upsDevices.findIndex((ups) => ups.id === upsId);
if (upsIndex === -1) {
logger.error(`UPS with ID "${upsId}" not found.`);
return;
}
const upsToDelete = config.upsDevices[upsIndex];
// Get confirmation before deleting
const readline = await import('node:readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const confirm = await new Promise<string>(resolve => {
rl.question(`Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `, answer => {
resolve(answer.toLowerCase());
});
const confirm = await new Promise<string>((resolve) => {
rl.question(
`Are you sure you want to delete UPS "${upsToDelete.name}" (${upsId})? [y/N]: `,
(answer) => {
resolve(answer.toLowerCase());
},
);
});
rl.close();
if (confirm !== 'y' && confirm !== 'yes') {
logger.log('Deletion cancelled.');
return;
}
// Remove the UPS from the array
config.upsDevices.splice(upsIndex, 1);
// Save the configuration
await this.nupst.getDaemon().saveConfig(config);
logger.log(`UPS "${upsToDelete.name}" (${upsId}) has been deleted.`);
// Check if service is running and restart it if needed
await this.restartServiceIfRunning();
} catch (error) {
logger.error(`Failed to delete UPS: ${error instanceof Error ? error.message : String(error)}`);
logger.error(
`Failed to delete UPS: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
@@ -375,10 +383,10 @@ export class UpsHandler {
logger.logBoxEnd();
return;
}
// Get current configuration
const config = this.nupst.getDaemon().getConfig();
// Check if multi-UPS config
if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
// Legacy single UPS configuration
@@ -395,41 +403,49 @@ export class UpsHandler {
logger.logBoxLine('Default UPS:');
logger.logBoxLine(` Host: ${config.snmp.host}:${config.snmp.port}`);
logger.logBoxLine(` Model: ${config.snmp.upsModel || 'cyberpower'}`);
logger.logBoxLine(` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`);
logger.logBoxLine(
` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`,
);
logger.logBoxLine('');
logger.logBoxLine('Use "nupst add" to add more UPS devices and migrate');
logger.logBoxLine('to the multi-UPS configuration format.');
logger.logBoxEnd();
return;
}
// Display UPS list
const boxWidth = 60;
logger.logBoxTitle('UPS Devices', boxWidth);
if (config.upsDevices.length === 0) {
logger.logBoxLine('No UPS devices configured.');
logger.logBoxLine('Use "nupst add" to add a UPS device.');
} else {
logger.logBoxLine(`Found ${config.upsDevices.length} UPS device(s)`);
logger.logBoxLine('');
logger.logBoxLine('ID | Name | Host | Mode | Groups');
logger.logBoxLine('-----------+----------------------+-----------------+--------------+----------------');
logger.logBoxLine(
'ID | Name | Host | Mode | Groups',
);
logger.logBoxLine(
'-----------+----------------------+-----------------+--------------+----------------',
);
for (const ups of config.upsDevices) {
const id = ups.id.padEnd(10, ' ').substring(0, 10);
const name = (ups.name || '').padEnd(20, ' ').substring(0, 20);
const host = `${ups.snmp.host}:${ups.snmp.port}`.padEnd(15, ' ').substring(0, 15);
const model = (ups.snmp.upsModel || 'cyberpower').padEnd(12, ' ').substring(0, 12);
const groups = ups.groups.length > 0 ? ups.groups.join(', ') : 'None';
logger.logBoxLine(`${id} | ${name} | ${host} | ${model} | ${groups}`);
}
}
logger.logBoxEnd();
} catch (error) {
logger.error(`Failed to list UPS devices: ${error instanceof Error ? error.message : String(error)}`);
logger.error(
`Failed to list UPS devices: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
@@ -465,7 +481,7 @@ export class UpsHandler {
// Handle new multi-UPS configuration format
if (config.upsDevices && config.upsDevices.length > 0) {
logger.log(`Found ${config.upsDevices.length} UPS devices in configuration.`);
for (let i = 0; i < config.upsDevices.length; i++) {
const ups = config.upsDevices[i];
logger.log(`\nTesting UPS: ${ups.name} (${ups.id})`);
@@ -492,11 +508,11 @@ export class UpsHandler {
const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {};
const thresholds = isUpsConfig ? config.thresholds : config.thresholds || {};
const checkInterval = config.checkInterval || 30000;
// Get UPS name and ID if available
const upsName = config.name ? config.name : 'Default UPS';
const upsId = config.id ? config.id : 'default';
const boxWidth = 45;
logger.logBoxTitle(`Testing Configuration: ${upsName}`, boxWidth);
logger.logBoxLine(`UPS ID: ${upsId}`);
@@ -529,18 +545,22 @@ export class UpsHandler {
if (snmpConfig.upsModel === 'custom' && snmpConfig.customOIDs) {
logger.logBoxLine('Custom OIDs:');
logger.logBoxLine(` Power Status: ${snmpConfig.customOIDs.POWER_STATUS || 'Not set'}`);
logger.logBoxLine(` Battery Capacity: ${snmpConfig.customOIDs.BATTERY_CAPACITY || 'Not set'}`);
logger.logBoxLine(
` Battery Capacity: ${snmpConfig.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
);
logger.logBoxLine(` Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
}
logger.logBoxLine('Thresholds:');
logger.logBoxLine(` Battery: ${thresholds.battery}%`);
logger.logBoxLine(` Runtime: ${thresholds.runtime} minutes`);
// Show group assignments if this is a UPS config
if (config.groups && Array.isArray(config.groups)) {
logger.logBoxLine(`Group Assignments: ${config.groups.length === 0 ? 'None' : config.groups.join(', ')}`);
logger.logBoxLine(
`Group Assignments: ${config.groups.length === 0 ? 'None' : config.groups.join(', ')}`,
);
}
logger.logBoxLine(`Check Interval: ${checkInterval / 1000} seconds`);
logger.logBoxEnd();
}
@@ -553,12 +573,12 @@ export class UpsHandler {
const upsId = config.id || 'default';
const upsName = config.name || 'Default UPS';
logger.log(`\nTesting connection to UPS: ${upsName} (${upsId})...`);
try {
// Create a test config with a short timeout
const snmpConfig = config.snmp ? config.snmp : config.snmp;
const thresholds = config.thresholds ? config.thresholds : config.thresholds;
const testConfig = {
...snmpConfig,
timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing
@@ -599,26 +619,26 @@ export class UpsHandler {
if (status.batteryCapacity < thresholds.battery) {
logger.logBoxLine('⚠️ WARNING: Battery capacity below threshold');
logger.logBoxLine(
` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`
` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`,
);
logger.logBoxLine(' System would initiate shutdown');
} else {
logger.logBoxLine('✓ Battery capacity above threshold');
logger.logBoxLine(
` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`
` Current: ${status.batteryCapacity}% | Threshold: ${thresholds.battery}%`,
);
}
if (status.batteryRuntime < thresholds.runtime) {
logger.logBoxLine('⚠️ WARNING: Runtime below threshold');
logger.logBoxLine(
` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`
` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`,
);
logger.logBoxLine(' System would initiate shutdown');
} else {
logger.logBoxLine('✓ Runtime above threshold');
logger.logBoxLine(
` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`
` Current: ${status.batteryRuntime} min | Threshold: ${thresholds.runtime} min`,
);
}
@@ -632,7 +652,7 @@ export class UpsHandler {
*/
private async gatherSnmpSettings(
snmpConfig: any,
prompt: (question: string) => Promise<string>
prompt: (question: string) => Promise<string>,
): Promise<void> {
// SNMP IP Address
const defaultHost = snmpConfig.host || '127.0.0.1';
@@ -653,10 +673,9 @@ export class UpsHandler {
console.log(' 3) SNMPv3 (with security features)');
const versionInput = await prompt(`Select SNMP version [${defaultVersion}]: `);
const version = parseInt(versionInput, 10);
snmpConfig.version =
versionInput.trim() && (version === 1 || version === 2 || version === 3)
? version
: defaultVersion;
snmpConfig.version = versionInput.trim() && (version === 1 || version === 2 || version === 3)
? version
: defaultVersion;
if (snmpConfig.version === 1 || snmpConfig.version === 2) {
// SNMP Community String (for v1/v2c)
@@ -676,7 +695,7 @@ export class UpsHandler {
*/
private async gatherSnmpV3Settings(
snmpConfig: any,
prompt: (question: string) => Promise<string>
prompt: (question: string) => Promise<string>,
): Promise<void> {
console.log('\nSNMPv3 Security Settings:');
@@ -734,7 +753,7 @@ export class UpsHandler {
// Allow customizing the timeout value
const defaultTimeout = snmpConfig.timeout / 1000; // Convert from ms to seconds for display
console.log(
'\nSNMPv3 operations with authentication and privacy may require longer timeouts.'
'\nSNMPv3 operations with authentication and privacy may require longer timeouts.',
);
const timeoutInput = await prompt(`SNMP Timeout in seconds [${defaultTimeout}]: `);
const timeout = parseInt(timeoutInput, 10);
@@ -751,7 +770,7 @@ export class UpsHandler {
*/
private async gatherAuthenticationSettings(
snmpConfig: any,
prompt: (question: string) => Promise<string>
prompt: (question: string) => Promise<string>,
): Promise<void> {
// Authentication protocol
console.log('\nAuthentication Protocol:');
@@ -759,7 +778,7 @@ export class UpsHandler {
console.log(' 2) SHA');
const defaultAuthProtocol = snmpConfig.authProtocol === 'SHA' ? 2 : 1;
const authProtocolInput = await prompt(
`Select Authentication Protocol [${defaultAuthProtocol}]: `
`Select Authentication Protocol [${defaultAuthProtocol}]: `,
);
const authProtocol = parseInt(authProtocolInput, 10) || defaultAuthProtocol;
snmpConfig.authProtocol = authProtocol === 2 ? 'SHA' : 'MD5';
@@ -777,7 +796,7 @@ export class UpsHandler {
*/
private async gatherPrivacySettings(
snmpConfig: any,
prompt: (question: string) => Promise<string>
prompt: (question: string) => Promise<string>,
): Promise<void> {
// Privacy protocol
console.log('\nPrivacy Protocol:');
@@ -801,31 +820,29 @@ export class UpsHandler {
*/
private async gatherThresholdSettings(
thresholds: any,
prompt: (question: string) => Promise<string>
prompt: (question: string) => Promise<string>,
): Promise<void> {
console.log('\nShutdown Thresholds:');
// Battery threshold
const defaultBatteryThreshold = thresholds.battery || 60;
const batteryThresholdInput = await prompt(
`Battery percentage threshold [${defaultBatteryThreshold}%]: `
`Battery percentage threshold [${defaultBatteryThreshold}%]: `,
);
const batteryThreshold = parseInt(batteryThresholdInput, 10);
thresholds.battery =
batteryThresholdInput.trim() && !isNaN(batteryThreshold)
? batteryThreshold
: defaultBatteryThreshold;
thresholds.battery = batteryThresholdInput.trim() && !isNaN(batteryThreshold)
? batteryThreshold
: defaultBatteryThreshold;
// Runtime threshold
const defaultRuntimeThreshold = thresholds.runtime || 20;
const runtimeThresholdInput = await prompt(
`Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: `
`Runtime minutes threshold [${defaultRuntimeThreshold} minutes]: `,
);
const runtimeThreshold = parseInt(runtimeThresholdInput, 10);
thresholds.runtime =
runtimeThresholdInput.trim() && !isNaN(runtimeThreshold)
? runtimeThreshold
: defaultRuntimeThreshold;
thresholds.runtime = runtimeThresholdInput.trim() && !isNaN(runtimeThreshold)
? runtimeThreshold
: defaultRuntimeThreshold;
}
/**
@@ -835,7 +852,7 @@ export class UpsHandler {
*/
private async gatherUpsModelSettings(
snmpConfig: any,
prompt: (question: string) => Promise<string>
prompt: (question: string) => Promise<string>,
): Promise<void> {
console.log('\nUPS Model Selection:');
console.log(' 1) CyberPower');
@@ -845,20 +862,19 @@ export class UpsHandler {
console.log(' 5) Liebert/Vertiv');
console.log(' 6) Custom (Advanced)');
const defaultModelValue =
snmpConfig.upsModel === 'cyberpower'
? 1
: snmpConfig.upsModel === 'apc'
? 2
: snmpConfig.upsModel === 'eaton'
? 3
: snmpConfig.upsModel === 'tripplite'
? 4
: snmpConfig.upsModel === 'liebert'
? 5
: snmpConfig.upsModel === 'custom'
? 6
: 1;
const defaultModelValue = snmpConfig.upsModel === 'cyberpower'
? 1
: snmpConfig.upsModel === 'apc'
? 2
: snmpConfig.upsModel === 'eaton'
? 3
: snmpConfig.upsModel === 'tripplite'
? 4
: snmpConfig.upsModel === 'liebert'
? 5
: snmpConfig.upsModel === 'custom'
? 6
: 1;
const modelInput = await prompt(`Select UPS model [${defaultModelValue}]: `);
const modelValue = parseInt(modelInput, 10) || defaultModelValue;
@@ -905,7 +921,7 @@ export class UpsHandler {
logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`);
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`);
logger.logBoxLine(
`Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`
`Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`,
);
if (ups.groups && ups.groups.length > 0) {
logger.logBoxLine(`Groups: ${ups.groups.join(', ')}`);
@@ -923,10 +939,10 @@ export class UpsHandler {
*/
private async optionallyTestConnection(
snmpConfig: any,
prompt: (question: string) => Promise<string>
prompt: (question: string) => Promise<string>,
): Promise<void> {
const testConnection = await prompt(
'Would you like to test the connection to your UPS? (y/N): '
'Would you like to test the connection to your UPS? (y/N): ',
);
if (testConnection.toLowerCase() === 'y') {
logger.log('\nTesting connection to UPS...');
@@ -985,7 +1001,9 @@ export class UpsHandler {
logger.logBoxLine(' sudo systemctl restart nupst.service');
}
} catch (error) {
logger.logBoxLine(`Error restarting service: ${error instanceof Error ? error.message : String(error)}`);
logger.logBoxLine(
`Error restarting service: ${error instanceof Error ? error.message : String(error)}`,
);
logger.logBoxLine('You may need to restart the service manually:');
logger.logBoxLine(' sudo systemctl restart nupst.service');
}
@@ -996,4 +1014,4 @@ export class UpsHandler {
// Ignore errors checking service status
}
}
}
}

View File

@@ -1,8 +1,8 @@
import process from 'node:process';
import * as fs from "node:fs";
import * as path from "node:path";
import { exec, execFile } from "node:child_process";
import { promisify } from "node:util";
import * as fs from 'node:fs';
import * as path from 'node:path';
import { exec, execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { NupstSnmp } from './snmp/manager.ts';
import type { ISnmpConfig } from './snmp/types.ts';
import { logger } from './logger.ts';
@@ -55,7 +55,7 @@ export interface INupstConfig {
groups: IGroupConfig[];
/** Check interval in milliseconds */
checkInterval: number;
// Legacy fields for backward compatibility
/** SNMP configuration settings (legacy) */
snmp?: ISnmpConfig;
@@ -109,14 +109,14 @@ export class NupstDaemon {
privProtocol: 'AES',
privKey: '',
// UPS model for OID selection
upsModel: 'cyberpower'
upsModel: 'cyberpower',
},
thresholds: {
battery: 60, // Shutdown when battery below 60%
runtime: 20, // Shutdown when runtime below 20 minutes
},
groups: []
}
groups: [],
},
],
groups: [],
checkInterval: 30000, // Check every 30 seconds
@@ -126,7 +126,7 @@ export class NupstDaemon {
private snmp: NupstSnmp;
private isRunning: boolean = false;
private upsStatus: Map<string, IUpsStatus> = new Map();
/**
* Create a new daemon instance with the given SNMP manager
*/
@@ -148,11 +148,11 @@ export class NupstDaemon {
this.logConfigError(errorMsg);
throw new Error(errorMsg);
}
// Read and parse config
const configData = fs.readFileSync(this.CONFIG_PATH, 'utf8');
const parsedConfig = JSON.parse(configData);
// Handle legacy configuration format
if (!parsedConfig.upsDevices && parsedConfig.snmp) {
// Convert legacy format to new format
@@ -163,28 +163,32 @@ export class NupstDaemon {
name: 'Default UPS',
snmp: parsedConfig.snmp,
thresholds: parsedConfig.thresholds,
groups: []
}
groups: [],
},
],
groups: [],
checkInterval: parsedConfig.checkInterval
checkInterval: parsedConfig.checkInterval,
};
logger.log('Legacy configuration format detected. Converting to multi-UPS format.');
// Save the new format
await this.saveConfig(this.config);
} else {
this.config = parsedConfig;
}
return this.config;
} catch (error) {
if (error instanceof Error && error.message && error.message.includes('No configuration found')) {
if (
error instanceof Error && error.message && error.message.includes('No configuration found')
) {
throw error; // Re-throw the no configuration error
}
this.logConfigError(`Error loading configuration: ${error instanceof Error ? error.message : String(error)}`);
this.logConfigError(
`Error loading configuration: ${error instanceof Error ? error.message : String(error)}`,
);
throw new Error('Failed to load configuration');
}
}
@@ -200,7 +204,7 @@ export class NupstDaemon {
}
fs.writeFileSync(this.CONFIG_PATH, JSON.stringify(config, null, 2));
this.config = config;
console.log('┌─ Configuration Saved ─────────────────────┐');
console.log(`│ Location: ${this.CONFIG_PATH}`);
console.log('└──────────────────────────────────────────┘');
@@ -215,7 +219,7 @@ export class NupstDaemon {
private logConfigError(message: string): void {
console.error('┌─ Configuration Error ─────────────────────┐');
console.error(`${message}`);
console.error('│ Please run \'nupst setup\' first to create a configuration.');
console.error("│ Please run 'nupst setup' first to create a configuration.");
console.error('└───────────────────────────────────────────┘');
}
@@ -225,7 +229,7 @@ export class NupstDaemon {
public getConfig(): INupstConfig {
return this.config;
}
/**
* Get the SNMP instance
*/
@@ -243,15 +247,15 @@ export class NupstDaemon {
}
logger.log('Starting NUPST daemon...');
try {
// Load configuration - this will throw an error if config doesn't exist
await this.loadConfig();
this.logConfigLoaded();
// Log version information
this.snmp.getNupst().logVersionInfo(false); // Don't check for updates immediately on startup
// Check for updates in the background
this.snmp.getNupst().checkForUpdates().then((updateAvailable: boolean) => {
if (updateAvailable) {
@@ -264,16 +268,18 @@ export class NupstDaemon {
logger.logBoxEnd();
}
}).catch(() => {}); // Ignore errors checking for updates
// Initialize UPS status tracking
this.initializeUpsStatus();
// Start UPS monitoring
this.isRunning = true;
await this.monitor();
} catch (error) {
this.isRunning = false;
logger.error(`Daemon failed to start: ${error instanceof Error ? error.message : String(error)}`);
logger.error(
`Daemon failed to start: ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1); // Exit with error
}
}
@@ -283,7 +289,7 @@ export class NupstDaemon {
*/
private initializeUpsStatus(): void {
this.upsStatus.clear();
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
for (const ups of this.config.upsDevices) {
this.upsStatus.set(ups.id, {
@@ -293,10 +299,10 @@ export class NupstDaemon {
batteryCapacity: 100,
batteryRuntime: 999, // High value as default
lastStatusChange: Date.now(),
lastCheckTime: 0
lastCheckTime: 0,
});
}
logger.log(`Initialized status tracking for ${this.config.upsDevices.length} UPS devices`);
} else {
logger.error('No UPS devices found in configuration');
@@ -309,7 +315,7 @@ export class NupstDaemon {
private logConfigLoaded(): void {
const boxWidth = 50;
logger.logBoxTitle('Configuration Loaded', boxWidth);
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
logger.logBoxLine(`UPS Devices: ${this.config.upsDevices.length}`);
for (const ups of this.config.upsDevices) {
@@ -318,7 +324,7 @@ export class NupstDaemon {
} else {
logger.logBoxLine('No UPS devices configured');
}
if (this.config.groups && this.config.groups.length > 0) {
logger.logBoxLine(`Groups: ${this.config.groups.length}`);
for (const group of this.config.groups) {
@@ -327,7 +333,7 @@ export class NupstDaemon {
} else {
logger.logBoxLine('No Groups configured');
}
logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`);
logger.logBoxEnd();
}
@@ -345,43 +351,45 @@ export class NupstDaemon {
*/
private async monitor(): Promise<void> {
logger.log('Starting UPS monitoring...');
if (!this.config.upsDevices || this.config.upsDevices.length === 0) {
logger.error('No UPS devices found in configuration. Monitoring stopped.');
this.isRunning = false;
return;
}
let lastLogTime = 0; // Track when we last logged status
const LOG_INTERVAL = 5 * 60 * 1000; // Log at least every 5 minutes (300000ms)
// Monitor continuously
while (this.isRunning) {
try {
// Check all UPS devices
await this.checkAllUpsDevices();
// Log periodic status update
const currentTime = Date.now();
if (currentTime - lastLogTime >= LOG_INTERVAL) {
this.logAllUpsStatus();
lastLogTime = currentTime;
}
// Check if shutdown is required based on group configurations
await this.evaluateGroupShutdownConditions();
// Wait before next check
await this.sleep(this.config.checkInterval);
} catch (error) {
logger.error(`Error during UPS monitoring: ${error instanceof Error ? error.message : String(error)}`);
logger.error(
`Error during UPS monitoring: ${error instanceof Error ? error.message : String(error)}`,
);
await this.sleep(this.config.checkInterval);
}
}
logger.log('UPS monitoring stopped');
}
/**
* Check status of all UPS devices
*/
@@ -398,14 +406,14 @@ export class NupstDaemon {
batteryCapacity: 100,
batteryRuntime: 999,
lastStatusChange: Date.now(),
lastCheckTime: 0
lastCheckTime: 0,
});
}
// Check UPS status
const status = await this.snmp.getUpsStatus(ups.snmp);
const currentTime = Date.now();
// Get the current status from the map
const currentStatus = this.upsStatus.get(ups.id);
@@ -417,7 +425,7 @@ export class NupstDaemon {
batteryCapacity: status.batteryCapacity,
batteryRuntime: status.batteryRuntime,
lastCheckTime: currentTime,
lastStatusChange: currentStatus?.lastStatusChange || currentTime
lastStatusChange: currentStatus?.lastStatusChange || currentTime,
};
// Check if power status changed
@@ -432,11 +440,15 @@ export class NupstDaemon {
// Update the status in the map
this.upsStatus.set(ups.id, updatedStatus);
} catch (error) {
logger.error(`Error checking UPS ${ups.name} (${ups.id}): ${error instanceof Error ? error.message : String(error)}`);
logger.error(
`Error checking UPS ${ups.name} (${ups.id}): ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
}
/**
* Log status of all UPS devices
*/
@@ -446,17 +458,19 @@ export class NupstDaemon {
logger.logBoxTitle('Periodic Status Update', boxWidth);
logger.logBoxLine(`Timestamp: ${timestamp}`);
logger.logBoxLine('');
for (const [id, status] of this.upsStatus.entries()) {
logger.logBoxLine(`UPS: ${status.name} (${id})`);
logger.logBoxLine(` Power Status: ${status.powerStatus}`);
logger.logBoxLine(` Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`);
logger.logBoxLine(
` Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`,
);
logger.logBoxLine('');
}
logger.logBoxEnd();
}
/**
* Evaluate if shutdown is required based on group configurations
*/
@@ -466,7 +480,7 @@ export class NupstDaemon {
for (const [id, status] of this.upsStatus.entries()) {
if (status.powerStatus === 'onBattery') {
// Find the UPS config
const ups = this.config.upsDevices.find(u => u.id === id);
const ups = this.config.upsDevices.find((u) => u.id === id);
if (ups) {
await this.evaluateUpsShutdownCondition(ups, status);
}
@@ -474,19 +488,19 @@ export class NupstDaemon {
}
return;
}
// Evaluate each group
for (const group of this.config.groups) {
// Find all UPS devices in this group
const upsDevicesInGroup = this.config.upsDevices.filter(ups =>
const upsDevicesInGroup = this.config.upsDevices.filter((ups) =>
ups.groups && ups.groups.includes(group.id)
);
if (upsDevicesInGroup.length === 0) {
// No UPS devices in this group
continue;
}
if (group.mode === 'redundant') {
// Redundant mode: only shutdown if ALL UPS devices in the group are in critical condition
await this.evaluateRedundantGroup(group, upsDevicesInGroup);
@@ -496,72 +510,90 @@ export class NupstDaemon {
}
}
}
/**
* Evaluate a redundant group for shutdown conditions
* In redundant mode, we only shut down if ALL UPS devices are in critical condition
*/
private async evaluateRedundantGroup(group: IGroupConfig, upsDevices: IUpsConfig[]): Promise<void> {
private async evaluateRedundantGroup(
group: IGroupConfig,
upsDevices: IUpsConfig[],
): Promise<void> {
// Count UPS devices on battery and in critical condition
let upsOnBattery = 0;
let upsInCriticalCondition = 0;
for (const ups of upsDevices) {
const status = this.upsStatus.get(ups.id);
if (!status) continue;
if (status.powerStatus === 'onBattery') {
upsOnBattery++;
// Check if this UPS is in critical condition
if (status.batteryCapacity < ups.thresholds.battery ||
status.batteryRuntime < ups.thresholds.runtime) {
if (
status.batteryCapacity < ups.thresholds.battery ||
status.batteryRuntime < ups.thresholds.runtime
) {
upsInCriticalCondition++;
}
}
}
// All UPS devices must be online for a redundant group to be considered healthy
const allUpsCount = upsDevices.length;
// If all UPS are on battery and in critical condition, shutdown
if (upsOnBattery === allUpsCount && upsInCriticalCondition === allUpsCount) {
logger.logBoxTitle(`Group Shutdown Required: ${group.name}`, 50);
logger.logBoxLine(`Mode: Redundant`);
logger.logBoxLine(`All ${allUpsCount} UPS devices in critical condition`);
logger.logBoxEnd();
await this.initiateShutdown(`All UPS devices in redundant group "${group.name}" in critical condition`);
await this.initiateShutdown(
`All UPS devices in redundant group "${group.name}" in critical condition`,
);
}
}
/**
* Evaluate a non-redundant group for shutdown conditions
* In non-redundant mode, we shut down if ANY UPS device is in critical condition
*/
private async evaluateNonRedundantGroup(group: IGroupConfig, upsDevices: IUpsConfig[]): Promise<void> {
private async evaluateNonRedundantGroup(
group: IGroupConfig,
upsDevices: IUpsConfig[],
): Promise<void> {
for (const ups of upsDevices) {
const status = this.upsStatus.get(ups.id);
if (!status) continue;
if (status.powerStatus === 'onBattery') {
// Check if this UPS is in critical condition
if (status.batteryCapacity < ups.thresholds.battery ||
status.batteryRuntime < ups.thresholds.runtime) {
if (
status.batteryCapacity < ups.thresholds.battery ||
status.batteryRuntime < ups.thresholds.runtime
) {
logger.logBoxTitle(`Group Shutdown Required: ${group.name}`, 50);
logger.logBoxLine(`Mode: Non-Redundant`);
logger.logBoxLine(`UPS ${ups.name} in critical condition`);
logger.logBoxLine(`Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`);
logger.logBoxLine(`Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`);
logger.logBoxLine(
`Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`,
);
logger.logBoxLine(
`Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`,
);
logger.logBoxEnd();
await this.initiateShutdown(`UPS "${ups.name}" in non-redundant group "${group.name}" in critical condition`);
await this.initiateShutdown(
`UPS "${ups.name}" in non-redundant group "${group.name}" in critical condition`,
);
return; // Exit after initiating shutdown
}
}
}
}
/**
* Evaluate an individual UPS for shutdown conditions
*/
@@ -570,38 +602,44 @@ export class NupstDaemon {
if (ups.groups && ups.groups.length > 0) {
return;
}
// Check threshold conditions
if (status.batteryCapacity < ups.thresholds.battery ||
status.batteryRuntime < ups.thresholds.runtime) {
if (
status.batteryCapacity < ups.thresholds.battery ||
status.batteryRuntime < ups.thresholds.runtime
) {
logger.logBoxTitle(`UPS Shutdown Required: ${ups.name}`, 50);
logger.logBoxLine(`Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`);
logger.logBoxLine(`Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`);
logger.logBoxLine(
`Battery: ${status.batteryCapacity}% (threshold: ${ups.thresholds.battery}%)`,
);
logger.logBoxLine(
`Runtime: ${status.batteryRuntime} min (threshold: ${ups.thresholds.runtime} min)`,
);
logger.logBoxEnd();
await this.initiateShutdown(`UPS "${ups.name}" battery or runtime below threshold`);
}
}
/**
* Initiate system shutdown with UPS monitoring during shutdown
* @param reason Reason for shutdown
*/
public async initiateShutdown(reason: string): Promise<void> {
logger.log(`Initiating system shutdown due to: ${reason}`);
// Set a longer delay for shutdown to allow VMs and services to close
const shutdownDelayMinutes = 5;
try {
// Find shutdown command in common system paths
const shutdownPaths = [
'/sbin/shutdown',
'/usr/sbin/shutdown',
'/bin/shutdown',
'/usr/bin/shutdown'
'/usr/bin/shutdown',
];
let shutdownCmd = '';
for (const path of shutdownPaths) {
try {
@@ -614,14 +652,16 @@ export class NupstDaemon {
// Continue checking other paths
}
}
if (shutdownCmd) {
// Execute shutdown command with delay to allow for VM graceful shutdown
logger.log(`Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`);
logger.log(
`Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`,
);
const { stdout } = await execFileAsync(shutdownCmd, [
'-h',
`+${shutdownDelayMinutes}`,
`UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes`
'-h',
`+${shutdownDelayMinutes}`,
`UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes`,
]);
logger.log(`Shutdown initiated: ${stdout}`);
logger.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`);
@@ -629,29 +669,34 @@ export class NupstDaemon {
// Try using the PATH to find shutdown
try {
logger.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
});
const { stdout } = await execAsync(
`shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`,
{
env: process.env, // Pass the current environment
},
);
logger.log(`Shutdown initiated: ${stdout}`);
} catch (e) {
throw new Error(`Shutdown command not found: ${e instanceof Error ? e.message : String(e)}`);
throw new Error(
`Shutdown command not found: ${e instanceof Error ? e.message : String(e)}`,
);
}
}
// Monitor UPS during shutdown and force immediate shutdown if battery gets too low
logger.log('Monitoring UPS during shutdown process...');
await this.monitorDuringShutdown();
} catch (error) {
logger.error(`Failed to initiate shutdown: ${error}`);
// 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
{ 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
@@ -659,9 +704,9 @@ export class NupstDaemon {
`/sbin/${alt.cmd}`,
`/usr/sbin/${alt.cmd}`,
`/bin/${alt.cmd}`,
`/usr/bin/${alt.cmd}`
`/usr/bin/${alt.cmd}`,
];
let cmdPath = '';
for (const path of paths) {
if (fs.existsSync(path)) {
@@ -669,7 +714,7 @@ export class NupstDaemon {
break;
}
}
if (cmdPath) {
logger.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`);
await execFileAsync(cmdPath, alt.args);
@@ -678,7 +723,7 @@ export class NupstDaemon {
// Try using PATH environment
logger.log(`Trying alternative via PATH: ${alt.cmd} ${alt.args.join(' ')}`);
await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
env: process.env // Pass the current environment
env: process.env, // Pass the current environment
});
return; // Exit if successful
}
@@ -687,11 +732,11 @@ export class NupstDaemon {
// Continue to next method
}
}
logger.error('All shutdown methods failed');
}
}
/**
* Monitor UPS during system shutdown
* Force immediate shutdown if any UPS gets critically low
@@ -701,48 +746,62 @@ export class NupstDaemon {
const CHECK_INTERVAL = 30000; // Check every 30 seconds during shutdown
const MAX_MONITORING_TIME = 5 * 60 * 1000; // Max 5 minutes of monitoring
const startTime = Date.now();
logger.log(`Emergency shutdown threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes remaining battery runtime`);
logger.log(
`Emergency shutdown threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes remaining battery runtime`,
);
// Continue monitoring until max monitoring time is reached
while (Date.now() - startTime < MAX_MONITORING_TIME) {
try {
logger.log('Checking UPS status during shutdown...');
// Check all UPS devices
for (const ups of this.config.upsDevices) {
try {
const status = await this.snmp.getUpsStatus(ups.snmp);
logger.log(`UPS ${ups.name}: Battery ${status.batteryCapacity}%, Runtime: ${status.batteryRuntime} minutes`);
logger.log(
`UPS ${ups.name}: Battery ${status.batteryCapacity}%, Runtime: ${status.batteryRuntime} minutes`,
);
// If any UPS battery runtime gets critically low, force immediate shutdown
if (status.batteryRuntime < EMERGENCY_RUNTIME_THRESHOLD) {
logger.logBoxTitle('EMERGENCY SHUTDOWN', 50);
logger.logBoxLine(`UPS ${ups.name} runtime critically low: ${status.batteryRuntime} minutes`);
logger.logBoxLine(
`UPS ${ups.name} runtime critically low: ${status.batteryRuntime} minutes`,
);
logger.logBoxLine('Forcing immediate shutdown!');
logger.logBoxEnd();
// Force immediate shutdown
await this.forceImmediateShutdown();
return;
}
} catch (upsError) {
logger.error(`Error checking UPS ${ups.name} during shutdown: ${upsError instanceof Error ? upsError.message : String(upsError)}`);
logger.error(
`Error checking UPS ${ups.name} during shutdown: ${
upsError instanceof Error ? upsError.message : String(upsError)
}`,
);
}
}
// Wait before checking again
await this.sleep(CHECK_INTERVAL);
} catch (error) {
logger.error(`Error monitoring UPS during shutdown: ${error instanceof Error ? error.message : String(error)}`);
logger.error(
`Error monitoring UPS during shutdown: ${
error instanceof Error ? error.message : String(error)
}`,
);
await this.sleep(CHECK_INTERVAL);
}
}
logger.log('UPS monitoring during shutdown completed');
}
/**
* Force an immediate system shutdown
*/
@@ -753,9 +812,9 @@ export class NupstDaemon {
'/sbin/shutdown',
'/usr/sbin/shutdown',
'/bin/shutdown',
'/usr/bin/shutdown'
'/usr/bin/shutdown',
];
let shutdownCmd = '';
for (const path of shutdownPaths) {
if (fs.existsSync(path)) {
@@ -764,27 +823,34 @@ export class NupstDaemon {
break;
}
}
if (shutdownCmd) {
logger.log(`Executing emergency shutdown: ${shutdownCmd} -h now`);
await execFileAsync(shutdownCmd, ['-h', 'now', 'EMERGENCY: UPS battery critically low, shutting down NOW']);
await execFileAsync(shutdownCmd, [
'-h',
'now',
'EMERGENCY: UPS battery critically low, shutting down NOW',
]);
} else {
// Try using the PATH to find shutdown
logger.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
});
await execAsync(
'shutdown -h now "EMERGENCY: UPS battery critically low, shutting down NOW"',
{
env: process.env, // Pass the current environment
},
);
}
} catch (error) {
logger.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'] }
{ cmd: 'systemctl', args: ['poweroff'] },
];
for (const alt of alternatives) {
try {
// Check common paths
@@ -792,9 +858,9 @@ export class NupstDaemon {
`/sbin/${alt.cmd}`,
`/usr/sbin/${alt.cmd}`,
`/bin/${alt.cmd}`,
`/usr/bin/${alt.cmd}`
`/usr/bin/${alt.cmd}`,
];
let cmdPath = '';
for (const path of paths) {
if (fs.existsSync(path)) {
@@ -802,7 +868,7 @@ export class NupstDaemon {
break;
}
}
if (cmdPath) {
logger.log(`Emergency: using ${cmdPath} ${alt.args.join(' ')}`);
await execFileAsync(cmdPath, alt.args);
@@ -811,7 +877,7 @@ export class NupstDaemon {
// Try using PATH
logger.log(`Emergency: trying ${alt.cmd} via PATH`);
await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, {
env: process.env
env: process.env,
});
return; // Exit if successful
}
@@ -819,7 +885,7 @@ export class NupstDaemon {
// Continue to next method
}
}
logger.error('All emergency shutdown methods failed');
}
}
@@ -828,6 +894,6 @@ export class NupstDaemon {
* Sleep for the specified milliseconds
*/
private sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
}

View File

@@ -1 +1 @@
export * from './shortid.ts';
export * from './shortid.ts';

View File

@@ -5,11 +5,11 @@
export function shortId(): string {
// Define the character set: a-z, A-Z, 0-9
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
// Generate cryptographically secure random values
const randomValues = new Uint8Array(6);
crypto.getRandomValues(randomValues);
// Map each random value to a character in our set
let result = '';
for (let i = 0; i < 6; i++) {
@@ -17,6 +17,6 @@ export function shortId(): string {
const index = randomValues[i] % chars.length;
result += chars[index];
}
return result;
}
}

View File

@@ -2,6 +2,7 @@
import { NupstCli } from './cli.ts';
import { logger } from './logger.ts';
import process from 'node:process';
/**
* Main entry point for NUPST
@@ -13,7 +14,7 @@ async function main() {
}
// Run the main function and handle any errors
main().catch(error => {
main().catch((error) => {
logger.error(`Error: ${error}`);
process.exit(1);
});

View File

@@ -5,7 +5,7 @@
export class Logger {
private currentBoxWidth: number | null = null;
private static instance: Logger;
/** Default width to use when no width is specified */
private readonly DEFAULT_WIDTH = 60;
@@ -66,14 +66,14 @@ export class Logger {
*/
public logBoxTitle(title: string, width?: number): void {
this.currentBoxWidth = width || this.DEFAULT_WIDTH;
// Create the title line with appropriate padding
const paddedTitle = ` ${title} `;
const remainingSpace = this.currentBoxWidth - 3 - paddedTitle.length;
// Title line: ┌─ Title ───┐
const titleLine = `┌─${paddedTitle}${'─'.repeat(Math.max(0, remainingSpace))}`;
console.log(titleLine);
}
@@ -87,12 +87,12 @@ export class Logger {
// No current width and no width provided, use default width
this.logBoxTitle('', this.DEFAULT_WIDTH);
}
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
// Calculate the available space for content
const availableSpace = boxWidth - 2; // Account for left and right borders
if (content.length <= availableSpace - 1) {
// If content fits with at least one space for the right border stripe
const padding = availableSpace - content.length - 1;
@@ -109,10 +109,10 @@ export class Logger {
*/
public logBoxEnd(width?: number): void {
const boxWidth = width || this.currentBoxWidth || this.DEFAULT_WIDTH;
// Create the bottom border: └────────┘
console.log(`${'─'.repeat(boxWidth - 2)}`);
// Reset the current box width
this.currentBoxWidth = null;
}
@@ -125,11 +125,11 @@ export class Logger {
*/
public logBox(title: string, lines: string[], width?: number): void {
this.logBoxTitle(title, width || this.DEFAULT_WIDTH);
for (const line of lines) {
this.logBoxLine(line);
}
this.logBoxEnd();
}
@@ -144,4 +144,4 @@ export class Logger {
}
// Export a singleton instance for easy use
export const logger = Logger.getInstance();
export const logger = Logger.getInstance();

View File

@@ -6,7 +6,7 @@ import { logger } from './logger.ts';
import { UpsHandler } from './cli/ups-handler.ts';
import { GroupHandler } from './cli/group-handler.ts';
import { ServiceHandler } from './cli/service-handler.ts';
import * as https from "node:https";
import * as https from 'node:https';
/**
* Main Nupst class that coordinates all components
@@ -31,7 +31,7 @@ export class Nupst {
this.snmp.setNupst(this); // Set up bidirectional reference
this.daemon = new NupstDaemon(this.snmp);
this.systemd = new NupstSystemd(this.daemon);
// Initialize handlers
this.upsHandler = new UpsHandler(this);
this.groupHandler = new GroupHandler(this);
@@ -58,28 +58,28 @@ export class Nupst {
public getSystemd(): NupstSystemd {
return this.systemd;
}
/**
* Get the UPS handler for UPS management
*/
public getUpsHandler(): UpsHandler {
return this.upsHandler;
}
/**
* Get the Group handler for group management
*/
public getGroupHandler(): GroupHandler {
return this.groupHandler;
}
/**
* Get the Service handler for service management
*/
public getServiceHandler(): ServiceHandler {
return this.serviceHandler;
}
/**
* Get the current version of NUPST
* @returns The current version string
@@ -87,7 +87,7 @@ export class Nupst {
public getVersion(): string {
return commitinfo.version;
}
/**
* Check if an update is available
* @returns Promise resolving to true if an update is available
@@ -96,34 +96,36 @@ export class Nupst {
try {
const latestVersion = await this.getLatestVersion();
const currentVersion = this.getVersion();
// Compare versions
this.updateAvailable = this.compareVersions(latestVersion, currentVersion) > 0;
this.latestVersion = latestVersion;
return this.updateAvailable;
} catch (error) {
logger.error(`Error checking for updates: ${error instanceof Error ? error.message : String(error)}`);
logger.error(
`Error checking for updates: ${error instanceof Error ? error.message : String(error)}`,
);
return false;
}
}
/**
* Get update status information
* @returns Object with update status information
*/
public getUpdateStatus(): {
currentVersion: string,
latestVersion: string,
updateAvailable: boolean
public getUpdateStatus(): {
currentVersion: string;
latestVersion: string;
updateAvailable: boolean;
} {
return {
currentVersion: this.getVersion(),
latestVersion: this.latestVersion || this.getVersion(),
updateAvailable: this.updateAvailable
updateAvailable: this.updateAvailable,
};
}
/**
* Get the latest version from npm registry
* @returns Promise resolving to the latest version string
@@ -136,17 +138,17 @@ export class Nupst {
method: 'GET',
headers: {
'Accept': 'application/json',
'User-Agent': `nupst/${this.getVersion()}`
}
'User-Agent': `nupst/${this.getVersion()}`,
},
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const response = JSON.parse(data);
@@ -160,15 +162,15 @@ export class Nupst {
}
});
});
req.on('error', (error) => {
reject(error);
});
req.end();
});
}
/**
* Compare two semantic version strings
* @param versionA First version
@@ -176,39 +178,39 @@ export class Nupst {
* @returns -1 if versionA < versionB, 0 if equal, 1 if versionA > versionB
*/
private compareVersions(versionA: string, versionB: string): number {
const partsA = versionA.split('.').map(part => parseInt(part, 10));
const partsB = versionB.split('.').map(part => parseInt(part, 10));
const partsA = versionA.split('.').map((part) => parseInt(part, 10));
const partsB = versionB.split('.').map((part) => parseInt(part, 10));
for (let i = 0; i < Math.max(partsA.length, partsB.length); i++) {
const partA = i < partsA.length ? partsA[i] : 0;
const partB = i < partsB.length ? partsB[i] : 0;
if (partA > partB) return 1;
if (partA < partB) return -1;
}
return 0; // Versions are equal
}
/**
* Log the current version and update status
*/
public logVersionInfo(checkForUpdates: boolean = true): void {
const version = this.getVersion();
const boxWidth = 45;
logger.logBoxTitle('NUPST Version', boxWidth);
logger.logBoxLine(`Current Version: ${version}`);
if (this.updateAvailable && this.latestVersion) {
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
logger.logBoxLine('Run "sudo nupst update" to update');
logger.logBoxEnd();
} else if (checkForUpdates) {
logger.logBoxLine('Checking for updates...');
// We can't end the box yet since we're in an async operation
this.checkForUpdates().then(updateAvailable => {
this.checkForUpdates().then((updateAvailable) => {
if (updateAvailable) {
logger.logBoxLine(`Update Available: ${this.latestVersion}`);
logger.logBoxLine('Run "sudo nupst update" to update');
@@ -224,4 +226,4 @@ export class Nupst {
logger.logBoxEnd();
}
}
}
}

View File

@@ -4,7 +4,7 @@
*/
// Re-export all public types
export type { IUpsStatus, IOidSet, TUpsModel, ISnmpConfig } from './types.ts';
export type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts';
// Re-export the SNMP manager class
export { NupstSnmp } from './manager.ts';
export { NupstSnmp } from './manager.ts';

View File

@@ -1,6 +1,6 @@
import * as snmp from "npm:net-snmp@3.20.0";
import { Buffer } from "node:buffer";
import type { IOidSet, ISnmpConfig, TUpsModel, IUpsStatus } from './types.ts';
import * as snmp from 'npm:net-snmp@3.20.0';
import { Buffer } from 'node:buffer';
import type { IOidSet, ISnmpConfig, IUpsStatus, TUpsModel } from './types.ts';
import { UpsOidSets } from './oid-sets.ts';
/**
@@ -34,7 +34,7 @@ export class NupstSnmp {
// Set default OID set
this.activeOIDs = UpsOidSets.getOidSet('cyberpower');
}
/**
* Set reference to the main Nupst instance
* @param nupst Reference to the main Nupst instance
@@ -42,14 +42,14 @@ export class NupstSnmp {
public setNupst(nupst: any): void {
this.nupst = nupst;
}
/**
* Get reference to the main Nupst instance
*/
public getNupst(): any {
return this.nupst;
}
/**
* Enable debug mode
*/
@@ -71,11 +71,11 @@ export class NupstSnmp {
}
return;
}
// Use OIDs for the specified UPS model or default to Cyberpower
const model = config.upsModel || 'cyberpower';
this.activeOIDs = UpsOidSets.getOidSet(model);
if (this.debug) {
console.log(`Using OIDs for UPS model: ${model}`);
}
@@ -89,13 +89,15 @@ export class NupstSnmp {
* @returns Promise resolving to the SNMP response value
*/
public async snmpGet(
oid: string,
config = this.DEFAULT_CONFIG,
retryCount = 0
oid: string,
config = this.DEFAULT_CONFIG,
retryCount = 0,
): Promise<any> {
return new Promise((resolve, reject) => {
if (this.debug) {
console.log(`Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`);
console.log(
`Sending SNMP v${config.version} GET request for OID ${oid} to ${config.host}:${config.port}`,
);
console.log('Using community:', config.community);
}
@@ -106,7 +108,7 @@ export class NupstSnmp {
timeout: config.timeout,
transport: 'udp4',
idBitsSize: 32,
context: config.context || ''
context: config.context || '',
};
// Set version based on config
@@ -120,23 +122,23 @@ export class NupstSnmp {
// Create appropriate session based on SNMP version
let session;
if (config.version === 3) {
// For SNMPv3, we need to set up authentication and privacy
// For SNMPv3, we need a valid security level
const securityLevel = config.securityLevel || 'noAuthNoPriv';
// Create the user object with required structure for net-snmp
const user: any = {
name: config.username || ''
name: config.username || '',
};
// Set security level
if (securityLevel === 'noAuthNoPriv') {
user.level = snmp.SecurityLevel.noAuthNoPriv;
} else if (securityLevel === 'authNoPriv') {
user.level = snmp.SecurityLevel.authNoPriv;
// Set auth protocol - must provide both protocol and key
if (config.authProtocol && config.authKey) {
if (config.authProtocol === 'MD5') {
@@ -154,7 +156,7 @@ export class NupstSnmp {
}
} else if (securityLevel === 'authPriv') {
user.level = snmp.SecurityLevel.authPriv;
// Set auth protocol - must provide both protocol and key
if (config.authProtocol && config.authKey) {
if (config.authProtocol === 'MD5') {
@@ -163,7 +165,7 @@ export class NupstSnmp {
user.authProtocol = snmp.AuthProtocols.sha;
}
user.authKey = config.authKey;
// Set privacy protocol - must provide both protocol and key
if (config.privProtocol && config.privKey) {
if (config.privProtocol === 'DES') {
@@ -187,18 +189,20 @@ export class NupstSnmp {
}
}
}
if (this.debug) {
console.log('SNMPv3 user configuration:', {
name: user.name,
level: Object.keys(snmp.SecurityLevel).find(key => snmp.SecurityLevel[key] === user.level),
level: Object.keys(snmp.SecurityLevel).find((key) =>
snmp.SecurityLevel[key] === user.level
),
authProtocol: user.authProtocol ? 'Set' : 'Not Set',
authKey: user.authKey ? 'Set' : 'Not Set',
privProtocol: user.privProtocol ? 'Set' : 'Not Set',
privKey: user.privKey ? 'Set' : 'Not Set'
privKey: user.privKey ? 'Set' : 'Not Set',
});
}
session = snmp.createV3Session(config.host, user, options);
} else {
// For SNMPv1/v2c, we use the community string
@@ -230,9 +234,11 @@ export class NupstSnmp {
}
// Check for SNMP errors in the response
if (varbinds[0].type === snmp.ObjectType.NoSuchObject ||
varbinds[0].type === snmp.ObjectType.NoSuchInstance ||
varbinds[0].type === snmp.ObjectType.EndOfMibView) {
if (
varbinds[0].type === snmp.ObjectType.NoSuchObject ||
varbinds[0].type === snmp.ObjectType.NoSuchInstance ||
varbinds[0].type === snmp.ObjectType.EndOfMibView
) {
if (this.debug) {
console.error('SNMP error:', snmp.ObjectType[varbinds[0].type]);
}
@@ -259,7 +265,7 @@ export class NupstSnmp {
console.log('SNMP response:', {
oid: varbinds[0].oid,
type: varbinds[0].type,
value: value
value: value,
});
}
@@ -277,7 +283,7 @@ export class NupstSnmp {
try {
// Set active OID set based on UPS model in config
this.setActiveOIDs(config);
if (this.debug) {
console.log('---------------------------------------');
console.log('Getting UPS status with config:');
@@ -300,18 +306,30 @@ export class NupstSnmp {
console.log(' Battery Runtime:', this.activeOIDs.BATTERY_RUNTIME);
console.log('---------------------------------------');
}
// Get all values with independent retry logic
const powerStatusValue = await this.getSNMPValueWithRetry(this.activeOIDs.POWER_STATUS, 'power status', config);
const batteryCapacity = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_CAPACITY, 'battery capacity', config) || 0;
const batteryRuntime = await this.getSNMPValueWithRetry(this.activeOIDs.BATTERY_RUNTIME, 'battery runtime', config) || 0;
const powerStatusValue = await this.getSNMPValueWithRetry(
this.activeOIDs.POWER_STATUS,
'power status',
config,
);
const batteryCapacity = await this.getSNMPValueWithRetry(
this.activeOIDs.BATTERY_CAPACITY,
'battery capacity',
config,
) || 0;
const batteryRuntime = await this.getSNMPValueWithRetry(
this.activeOIDs.BATTERY_RUNTIME,
'battery runtime',
config,
) || 0;
// Determine power status - handle different values for different UPS models
const powerStatus = this.determinePowerStatus(config.upsModel, powerStatusValue);
// Convert to minutes for UPS models with different time units
const processedRuntime = this.processRuntimeValue(config.upsModel, batteryRuntime);
const result = {
powerStatus,
batteryCapacity,
@@ -322,7 +340,7 @@ export class NupstSnmp {
batteryRuntime,
},
};
if (this.debug) {
console.log('---------------------------------------');
console.log('UPS status result:');
@@ -331,15 +349,20 @@ export class NupstSnmp {
console.log(' Battery Runtime:', result.batteryRuntime, 'minutes');
console.log('---------------------------------------');
}
return result;
} catch (error) {
if (this.debug) {
console.error('---------------------------------------');
console.error('Error getting UPS status:', error instanceof Error ? error.message : String(error));
console.error(
'Error getting UPS status:',
error instanceof Error ? error.message : String(error),
);
console.error('---------------------------------------');
}
throw new Error(`Failed to get UPS status: ${error instanceof Error ? error.message : String(error)}`);
throw new Error(
`Failed to get UPS status: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
@@ -351,9 +374,9 @@ export class NupstSnmp {
* @returns Promise resolving to the SNMP value
*/
private async getSNMPValueWithRetry(
oid: string,
description: string,
config: ISnmpConfig
oid: string,
description: string,
config: ISnmpConfig,
): Promise<any> {
if (oid === '') {
if (this.debug) {
@@ -361,11 +384,11 @@ export class NupstSnmp {
}
return 0;
}
if (this.debug) {
console.log(`Getting ${description} OID: ${oid}`);
}
try {
const value = await this.snmpGet(oid, config);
if (this.debug) {
@@ -374,19 +397,22 @@ export class NupstSnmp {
return value;
} catch (error) {
if (this.debug) {
console.error(`Error getting ${description}:`, error instanceof Error ? error.message : String(error));
console.error(
`Error getting ${description}:`,
error instanceof Error ? error.message : String(error),
);
}
// If we're using SNMPv3, try with different security levels
if (config.version === 3) {
return await this.tryFallbackSecurityLevels(oid, description, config);
}
// Try with standard OIDs as fallback
if (config.upsModel !== 'custom') {
return await this.tryStandardOids(oid, description, config);
}
// Return a default value if all attempts fail
if (this.debug) {
console.log(`Using default value 0 for ${description}`);
@@ -403,14 +429,14 @@ export class NupstSnmp {
* @returns Promise resolving to the SNMP value
*/
private async tryFallbackSecurityLevels(
oid: string,
description: string,
config: ISnmpConfig
oid: string,
description: string,
config: ISnmpConfig,
): Promise<any> {
if (this.debug) {
console.log(`Retrying ${description} with fallback security level...`);
}
// Try with authNoPriv if current level is authPriv
if (config.securityLevel === 'authPriv') {
const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' };
@@ -425,7 +451,10 @@ export class NupstSnmp {
return value;
} catch (retryError) {
if (this.debug) {
console.error(`Retry failed for ${description}:`, retryError instanceof Error ? retryError.message : String(retryError));
console.error(
`Retry failed for ${description}:`,
retryError instanceof Error ? retryError.message : String(retryError),
);
}
}
}
@@ -444,7 +473,10 @@ export class NupstSnmp {
return value;
} catch (retryError) {
if (this.debug) {
console.error(`Retry failed for ${description}:`, retryError instanceof Error ? retryError.message : String(retryError));
console.error(
`Retry failed for ${description}:`,
retryError instanceof Error ? retryError.message : String(retryError),
);
}
}
}
@@ -460,18 +492,20 @@ export class NupstSnmp {
* @returns Promise resolving to the SNMP value
*/
private async tryStandardOids(
oid: string,
description: string,
config: ISnmpConfig
oid: string,
description: string,
config: ISnmpConfig,
): Promise<any> {
try {
// Try RFC 1628 standard UPS MIB OIDs
const standardOIDs = UpsOidSets.getStandardOids();
if (this.debug) {
console.log(`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`);
console.log(
`Trying standard RFC 1628 OID for ${description}: ${standardOIDs[description]}`,
);
}
const standardValue = await this.snmpGet(standardOIDs[description], config);
if (this.debug) {
console.log(`${description} standard OID value:`, standardValue);
@@ -479,10 +513,13 @@ export class NupstSnmp {
return standardValue;
} catch (stdError) {
if (this.debug) {
console.error(`Standard OID retry failed for ${description}:`, stdError instanceof Error ? stdError.message : String(stdError));
console.error(
`Standard OID retry failed for ${description}:`,
stdError instanceof Error ? stdError.message : String(stdError),
);
}
}
return 0;
}
@@ -493,8 +530,8 @@ export class NupstSnmp {
* @returns Standardized power status
*/
private determinePowerStatus(
upsModel: TUpsModel | undefined,
powerStatusValue: number
upsModel: TUpsModel | undefined,
powerStatusValue: number,
): 'online' | 'onBattery' | 'unknown' {
if (upsModel === 'cyberpower') {
// CyberPower RMCARD205: upsBaseOutputStatus values
@@ -528,7 +565,7 @@ export class NupstSnmp {
return 'onBattery';
}
}
return 'unknown';
}
@@ -539,25 +576,29 @@ export class NupstSnmp {
* @returns Processed runtime in minutes
*/
private processRuntimeValue(
upsModel: TUpsModel | undefined,
batteryRuntime: number
upsModel: TUpsModel | undefined,
batteryRuntime: number,
): number {
if (this.debug) {
console.log('Raw runtime value:', batteryRuntime);
}
if (upsModel === 'cyberpower' && batteryRuntime > 0) {
// CyberPower: TimeTicks is in 1/100 seconds, convert to minutes
const minutes = Math.floor(batteryRuntime / 6000); // 6000 ticks = 1 minute
if (this.debug) {
console.log(`Converting CyberPower runtime from ${batteryRuntime} ticks to ${minutes} minutes`);
console.log(
`Converting CyberPower runtime from ${batteryRuntime} ticks to ${minutes} minutes`,
);
}
return minutes;
} else if (upsModel === 'eaton' && batteryRuntime > 0) {
// Eaton: Runtime is in seconds, convert to minutes
const minutes = Math.floor(batteryRuntime / 60);
if (this.debug) {
console.log(`Converting Eaton runtime from ${batteryRuntime} seconds to ${minutes} minutes`);
console.log(
`Converting Eaton runtime from ${batteryRuntime} seconds to ${minutes} minutes`,
);
}
return minutes;
} else if (batteryRuntime > 10000) {
@@ -568,7 +609,7 @@ export class NupstSnmp {
}
return minutes;
}
return batteryRuntime;
}
}
}

View File

@@ -15,41 +15,41 @@ export class UpsOidSets {
BATTERY_CAPACITY: '1.3.6.1.4.1.3808.1.1.1.2.2.1.0', // upsAdvanceBatteryCapacity (percentage)
BATTERY_RUNTIME: '1.3.6.1.4.1.3808.1.1.1.2.2.4.0', // upsAdvanceBatteryRunTimeRemaining (TimeTicks)
},
// APC OIDs
apc: {
POWER_STATUS: '1.3.6.1.4.1.318.1.1.1.4.1.1.0', // Power status (1=online, 2=on battery)
BATTERY_CAPACITY: '1.3.6.1.4.1.318.1.1.1.2.2.1.0', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.318.1.1.1.2.2.3.0', // Remaining runtime in minutes
},
// Eaton OIDs
eaton: {
POWER_STATUS: '1.3.6.1.4.1.534.1.4.4.0', // xupsOutputSource (3=normal/mains, 5=battery)
BATTERY_CAPACITY: '1.3.6.1.4.1.534.1.2.4.0', // xupsBatCapacity (percentage)
BATTERY_RUNTIME: '1.3.6.1.4.1.534.1.2.1.0', // xupsBatTimeRemaining (seconds)
},
// TrippLite OIDs
tripplite: {
POWER_STATUS: '1.3.6.1.4.1.850.1.1.3.1.1.1.0', // Power status
BATTERY_CAPACITY: '1.3.6.1.4.1.850.1.1.3.2.4.1.0', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.850.1.1.3.2.2.1.0', // Remaining runtime in minutes
},
// Liebert/Vertiv OIDs
liebert: {
POWER_STATUS: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.2.1', // Power status
BATTERY_CAPACITY: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.4.1', // Battery capacity in percentage
BATTERY_RUNTIME: '1.3.6.1.4.1.476.1.42.3.9.20.1.20.1.2.1.5.1', // Remaining runtime in minutes
},
// Custom OIDs (to be provided by the user)
custom: {
POWER_STATUS: '',
BATTERY_CAPACITY: '',
BATTERY_RUNTIME: '',
}
},
};
/**
@@ -67,9 +67,9 @@ export class UpsOidSets {
*/
public static getStandardOids(): Record<string, string> {
return {
'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource
'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining
'battery runtime': '1.3.6.1.2.1.33.1.2.3.0' // upsEstimatedMinutesRemaining
'power status': '1.3.6.1.2.1.33.1.4.1.0', // upsOutputSource
'battery capacity': '1.3.6.1.2.1.33.1.2.4.0', // upsEstimatedChargeRemaining
'battery runtime': '1.3.6.1.2.1.33.1.2.3.0', // upsEstimatedMinutesRemaining
};
}
}
}

View File

@@ -2,7 +2,7 @@
* Type definitions for SNMP module
*/
import { Buffer } from "node:buffer";
import { Buffer } from 'node:buffer';
/**
* UPS status interface
@@ -49,11 +49,11 @@ export interface ISnmpConfig {
timeout: number;
context?: string;
// SNMPv1/v2c
/** Community string for SNMPv1/v2c */
community?: string;
// SNMPv3
/** Security level for SNMPv3 */
securityLevel?: 'noAuthNoPriv' | 'authNoPriv' | 'authPriv';
@@ -67,7 +67,7 @@ export interface ISnmpConfig {
privProtocol?: 'DES' | 'AES';
/** Privacy key for SNMPv3 */
privKey?: string;
// UPS model and custom OIDs
/** UPS model for OID selection */
upsModel?: TUpsModel;
@@ -91,4 +91,4 @@ export interface ISnmpV3SecurityParams {
msgAuthenticationParameters: Buffer;
/** Privacy parameters */
msgPrivacyParameters: Buffer;
}
}

View File

@@ -1,6 +1,6 @@
import process from 'node:process';
import { promises as fs } from "node:fs";
import { execSync } from "node:child_process";
import { promises as fs } from 'node:fs';
import { execSync } from 'node:child_process';
import { NupstDaemon } from './daemon.ts';
import { logger } from './logger.ts';
@@ -66,7 +66,7 @@ WantedBy=multi-user.target
try {
// Check if configuration exists before installing
await this.checkConfigExists();
// Write the service file
await fs.writeFile(this.serviceFilePath, this.serviceTemplate);
const boxWidth = 50;
@@ -99,7 +99,7 @@ WantedBy=multi-user.target
try {
// Check if configuration exists before starting
await this.checkConfigExists();
execSync('systemctl start nupst.service');
const boxWidth = 45;
logger.logBoxTitle('Service Status', boxWidth);
@@ -143,10 +143,10 @@ WantedBy=multi-user.target
logger.logBoxEnd();
this.daemon.getNupstSnmp().enableDebug();
}
// Display version information
this.daemon.getNupstSnmp().getNupst().logVersionInfo();
// Check if config exists first
try {
await this.checkConfigExists();
@@ -154,11 +154,13 @@ WantedBy=multi-user.target
// Error message already displayed by checkConfigExists
return;
}
await this.displayServiceStatus();
await this.displayAllUpsStatus();
} catch (error) {
logger.error(`Failed to get status: ${error instanceof Error ? error.message : String(error)}`);
logger.error(
`Failed to get status: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
@@ -172,7 +174,7 @@ WantedBy=multi-user.target
const boxWidth = 45;
logger.logBoxTitle('Service Status', boxWidth);
// Process each line of the status output
serviceStatus.split('\n').forEach(line => {
serviceStatus.split('\n').forEach((line) => {
logger.logBoxLine(line);
});
logger.logBoxEnd();
@@ -194,11 +196,11 @@ WantedBy=multi-user.target
await this.daemon.loadConfig();
const config = this.daemon.getConfig();
const snmp = this.daemon.getNupstSnmp();
// Check if we have the new multi-UPS config format
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
logger.log(`Found ${config.upsDevices.length} UPS device(s) in configuration`);
// Show status for each UPS
for (const ups of config.upsDevices) {
await this.displaySingleUpsStatus(ups, snmp);
@@ -210,9 +212,9 @@ WantedBy=multi-user.target
name: 'Default UPS',
snmp: config.snmp,
thresholds: config.thresholds,
groups: []
groups: [],
};
await this.displaySingleUpsStatus(legacyUps, snmp);
} else {
logger.error('No UPS devices found in configuration');
@@ -220,11 +222,13 @@ WantedBy=multi-user.target
} catch (error) {
const boxWidth = 45;
logger.logBoxTitle('UPS Status', boxWidth);
logger.logBoxLine(`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`);
logger.logBoxLine(
`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`,
);
logger.logBoxEnd();
}
}
/**
* Display status of a single UPS
* @param ups UPS configuration
@@ -236,7 +240,7 @@ WantedBy=multi-user.target
logger.logBoxLine(`ID: ${ups.id}`);
logger.logBoxLine(`Host: ${ups.snmp.host}:${ups.snmp.port}`);
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel || 'cyberpower'}`);
if (ups.groups && ups.groups.length > 0) {
// Get group names if available
const config = this.daemon.getConfig();
@@ -246,37 +250,43 @@ WantedBy=multi-user.target
});
logger.logBoxLine(`Groups: ${groupNames.join(', ')}`);
}
logger.logBoxEnd();
try {
// Create a test config with a short timeout
const testConfig = {
...ups.snmp,
timeout: Math.min(ups.snmp.timeout, 10000) // Use at most 10 seconds for status check
timeout: Math.min(ups.snmp.timeout, 10000), // Use at most 10 seconds for status check
};
const status = await snmp.getUpsStatus(testConfig);
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
logger.logBoxLine(`Power Status: ${status.powerStatus}`);
logger.logBoxLine(`Battery Capacity: ${status.batteryCapacity}%`);
logger.logBoxLine(`Runtime Remaining: ${status.batteryRuntime} minutes`);
// Show threshold status
logger.logBoxLine('');
logger.logBoxLine('Thresholds:');
logger.logBoxLine(` Battery: ${status.batteryCapacity}% / ${ups.thresholds.battery}% ${
status.batteryCapacity < ups.thresholds.battery ? '⚠️' : '✓'
}`);
logger.logBoxLine(` Runtime: ${status.batteryRuntime} min / ${ups.thresholds.runtime} min ${
status.batteryRuntime < ups.thresholds.runtime ? '⚠️' : '✓'
}`);
logger.logBoxLine(
` Battery: ${status.batteryCapacity}% / ${ups.thresholds.battery}% ${
status.batteryCapacity < ups.thresholds.battery ? '⚠️' : '✓'
}`,
);
logger.logBoxLine(
` Runtime: ${status.batteryRuntime} min / ${ups.thresholds.runtime} min ${
status.batteryRuntime < ups.thresholds.runtime ? '⚠️' : '✓'
}`,
);
logger.logBoxEnd();
} catch (error) {
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
logger.logBoxLine(`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`);
logger.logBoxLine(
`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`,
);
logger.logBoxEnd();
}
}
@@ -290,7 +300,7 @@ WantedBy=multi-user.target
await this.stopService();
await this.disableService();
await this.removeServiceFile();
// Reload systemd daemon
execSync('systemctl daemon-reload');
logger.log('Systemd daemon reloaded');
@@ -341,4 +351,4 @@ WantedBy=multi-user.target
logger.log('Service file did not exist');
}
}
}
}