feat(systemd): improve service status reporting with structured systemctl data

This commit is contained in:
2026-04-16 03:51:24 +00:00
parent e916ccf3ae
commit e2cfa67fee
3 changed files with 119 additions and 34 deletions
+7
View File
@@ -1,5 +1,12 @@
# Changelog # 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) ## 2026-04-16 - 5.7.0 - feat(monitoring)
add edge-triggered threshold handling with group action orchestration and HA-aware Proxmox shutdowns add edge-triggered threshold handling with group action orchestration and HA-aware Proxmox shutdowns
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/nupst', 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' description: 'Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies'
} }
+111 -33
View File
@@ -1,12 +1,72 @@
import process from 'node:process'; import process from 'node:process';
import { promises as fs } from 'node:fs'; 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 { type IUpsConfig, NupstDaemon } from './daemon.ts';
import { NupstSnmp } from './snmp/manager.ts'; import { NupstSnmp } from './snmp/manager.ts';
import { logger } from './logger.ts'; import { logger } from './logger.ts';
import { formatPowerStatus, getBatteryColor, getRuntimeColor, symbols, theme } from './colors.ts'; import { formatPowerStatus, getBatteryColor, getRuntimeColor, symbols, theme } from './colors.ts';
import { SHUTDOWN } from './constants.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 * Class for managing systemd service
* Handles installation, removal, and control of the NUPST 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 * Display the systemd service status
* @private * @private
*/ */
private getServiceStatusSnapshot(): IServiceStatusSnapshot {
const output = execFileSync(
'systemctl',
[
'show',
'nupst.service',
'--property=LoadState,ActiveState,SubState,MainPID,MemoryCurrent,CPUUsageNSec',
],
{ encoding: 'utf8' },
);
const properties = new Map<string, string>();
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 { private displayServiceStatus(): void {
try { try {
const serviceStatus = execSync('systemctl status nupst.service').toString(); const snapshot = this.getServiceStatusSnapshot();
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];
}
}
// Display beautiful status // Display beautiful status
logger.log(''); logger.log('');
if (isActive) { if (snapshot.loadState === 'not-found') {
logger.log( logger.log(
`${symbols.running} ${theme.success('Service:')} ${ `${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`,
theme.statusActive('active (running)') );
}`, } 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 { } else {
const serviceState = snapshot.subState && snapshot.subState !== snapshot.activeState
? `${snapshot.activeState} (${snapshot.subState})`
: snapshot.activeState || 'inactive';
logger.log( 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 = []; const details = [];
if (pid) details.push(`PID: ${theme.dim(pid)}`); if (snapshot.pid) details.push(`PID: ${theme.dim(snapshot.pid)}`);
if (memory) details.push(`Memory: ${theme.dim(memory)}`); if (snapshot.memory) details.push(`Memory: ${theme.dim(snapshot.memory)}`);
if (cpu) details.push(`CPU: ${theme.dim(cpu)}`); if (snapshot.cpu) details.push(`CPU: ${theme.dim(snapshot.cpu)}`);
logger.log(` ${details.join(' ')}`); logger.log(` ${details.join(' ')}`);
} }
logger.log(''); logger.log('');