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