diff --git a/changelog.md b/changelog.md index 584c9c0..80aada9 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,12 @@ # Changelog +## 2026-04-16 - 5.8.0 - feat(systemd) +improve service status reporting with structured systemctl data + +- switch status collection from parsing `systemctl status` output to `systemctl show` properties for more reliable service state detection +- display a distinct "not installed" status when the unit is missing +- format systemd memory and CPU usage values into readable output for status details + ## 2026-04-16 - 5.7.0 - feat(monitoring) add edge-triggered threshold handling with group action orchestration and HA-aware Proxmox shutdowns diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index f5096de..5bbd29f 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/nupst', - version: '5.7.0', + version: '5.8.0', description: 'Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies' } diff --git a/ts/systemd.ts b/ts/systemd.ts index d67d73a..faca645 100644 --- a/ts/systemd.ts +++ b/ts/systemd.ts @@ -1,12 +1,72 @@ import process from 'node:process'; import { promises as fs } from 'node:fs'; -import { execSync } from 'node:child_process'; +import { execFileSync, execSync } from 'node:child_process'; import { type IUpsConfig, NupstDaemon } from './daemon.ts'; import { NupstSnmp } from './snmp/manager.ts'; import { logger } from './logger.ts'; import { formatPowerStatus, getBatteryColor, getRuntimeColor, symbols, theme } from './colors.ts'; import { SHUTDOWN } from './constants.ts'; +interface IServiceStatusSnapshot { + loadState: string; + activeState: string; + subState: string; + pid: string; + memory: string; + cpu: string; +} + +function formatSystemdMemory(memoryBytes: string): string { + const bytes = Number(memoryBytes); + if (!Number.isFinite(bytes) || bytes <= 0) { + return ''; + } + + const units = ['B', 'K', 'M', 'G', 'T', 'P']; + let value = bytes; + let unitIndex = 0; + + while (value >= 1024 && unitIndex < units.length - 1) { + value /= 1024; + unitIndex++; + } + + if (unitIndex === 0) { + return `${Math.round(value)}B`; + } + + return `${value.toFixed(1).replace(/\.0$/, '')}${units[unitIndex]}`; +} + +function formatSystemdCpu(cpuNanoseconds: string): string { + const nanoseconds = Number(cpuNanoseconds); + if (!Number.isFinite(nanoseconds) || nanoseconds <= 0) { + return ''; + } + + const milliseconds = nanoseconds / 1_000_000; + if (milliseconds < 1000) { + return `${Math.round(milliseconds)}ms`; + } + + const seconds = milliseconds / 1000; + if (seconds < 60) { + return `${seconds.toFixed(seconds >= 10 ? 1 : 3).replace(/\.?0+$/, '')}s`; + } + + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + if (minutes < 60) { + return `${minutes}min ${ + remainingSeconds.toFixed(remainingSeconds >= 10 ? 1 : 3).replace(/\.?0+$/, '') + }s`; + } + + const hours = Math.floor(minutes / 60); + const remainingMinutes = minutes % 60; + return `${hours}h ${remainingMinutes}min`; +} + /** * Class for managing systemd service * Handles installation, removal, and control of the NUPST systemd service @@ -224,51 +284,69 @@ WantedBy=multi-user.target * Display the systemd service status * @private */ + private getServiceStatusSnapshot(): IServiceStatusSnapshot { + const output = execFileSync( + 'systemctl', + [ + 'show', + 'nupst.service', + '--property=LoadState,ActiveState,SubState,MainPID,MemoryCurrent,CPUUsageNSec', + ], + { encoding: 'utf8' }, + ); + + const properties = new Map(); + for (const line of output.split('\n')) { + const separatorIndex = line.indexOf('='); + if (separatorIndex === -1) { + continue; + } + + properties.set(line.slice(0, separatorIndex), line.slice(separatorIndex + 1)); + } + + const pid = properties.get('MainPID') || ''; + return { + loadState: properties.get('LoadState') || '', + activeState: properties.get('ActiveState') || '', + subState: properties.get('SubState') || '', + pid: pid !== '0' ? pid : '', + memory: formatSystemdMemory(properties.get('MemoryCurrent') || ''), + cpu: formatSystemdCpu(properties.get('CPUUsageNSec') || ''), + }; + } + private displayServiceStatus(): void { try { - const serviceStatus = execSync('systemctl status nupst.service').toString(); - const lines = serviceStatus.split('\n'); - - // Parse key information from systemctl output - let isActive = false; - let pid = ''; - let memory = ''; - let cpu = ''; - - for (const line of lines) { - if (line.includes('Active:')) { - isActive = line.includes('active (running)'); - } else if (line.includes('Main PID:')) { - const match = line.match(/Main PID:\s+(\d+)/); - if (match) pid = match[1]; - } else if (line.includes('Memory:')) { - const match = line.match(/Memory:\s+([\d.]+[A-Z])/); - if (match) memory = match[1]; - } else if (line.includes('CPU:')) { - const match = line.match(/CPU:\s+([\d.]+(?:ms|s))/); - if (match) cpu = match[1]; - } - } + const snapshot = this.getServiceStatusSnapshot(); // Display beautiful status logger.log(''); - if (isActive) { + if (snapshot.loadState === 'not-found') { logger.log( - `${symbols.running} ${theme.success('Service:')} ${ - theme.statusActive('active (running)') - }`, + `${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`, + ); + } else if (snapshot.activeState === 'active') { + const serviceState = snapshot.subState + ? `${snapshot.activeState} (${snapshot.subState})` + : snapshot.activeState; + logger.log( + `${symbols.running} ${theme.success('Service:')} ${theme.statusActive(serviceState)}`, ); } else { + const serviceState = snapshot.subState && snapshot.subState !== snapshot.activeState + ? `${snapshot.activeState} (${snapshot.subState})` + : snapshot.activeState || 'inactive'; logger.log( - `${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`, + `${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive(serviceState)}`, ); } - if (pid || memory || cpu) { + if (snapshot.pid || snapshot.memory || snapshot.cpu) { const details = []; - if (pid) details.push(`PID: ${theme.dim(pid)}`); - if (memory) details.push(`Memory: ${theme.dim(memory)}`); - if (cpu) details.push(`CPU: ${theme.dim(cpu)}`); + if (snapshot.pid) details.push(`PID: ${theme.dim(snapshot.pid)}`); + if (snapshot.memory) details.push(`Memory: ${theme.dim(snapshot.memory)}`); + if (snapshot.cpu) details.push(`CPU: ${theme.dim(snapshot.cpu)}`); logger.log(` ${details.join(' ')}`); } logger.log('');