feat(systemd): improve service status reporting with structured systemctl data
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
+111
-33
@@ -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<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 {
|
||||
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('');
|
||||
|
||||
Reference in New Issue
Block a user