|
|
|
@@ -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('');
|
|
|
|
|