0.16.1-4 Hardware Monitoring

This commit is contained in:
2026-04-29 11:14:27 +00:00
parent 044ece0387
commit f9cf3cef09
8 changed files with 82 additions and 26 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.16.1-3", "version": "0.16.1-4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.16.1-3", "version": "0.16.1-4",
"dependencies": { "dependencies": {
"archiver": "^7.0.1", "archiver": "^7.0.1",
"cors": "^2.8.5", "cors": "^2.8.5",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ripster-backend", "name": "ripster-backend",
"version": "0.16.1-3", "version": "0.16.1-4",
"private": true, "private": true,
"type": "commonjs", "type": "commonjs",
"scripts": { "scripts": {
+60 -17
View File
@@ -34,6 +34,13 @@ function nowIso() {
return new Date().toISOString(); return new Date().toISOString();
} }
function isAbortError(error) {
if (!error || typeof error !== 'object') {
return false;
}
return error.name === 'AbortError' || error.code === 'ABORT_ERR';
}
function toBoolean(value) { function toBoolean(value) {
if (typeof value === 'boolean') { if (typeof value === 'boolean') {
return value; return value;
@@ -337,6 +344,8 @@ class HardwareMonitorService {
this.running = false; this.running = false;
this.timer = null; this.timer = null;
this.pollInFlight = false; this.pollInFlight = false;
this.activePollAbortController = null;
this.reloadSettingsSeq = 0;
this.lastCpuTimes = null; this.lastCpuTimes = null;
this.sensorsCommandAvailable = null; this.sensorsCommandAvailable = null;
this.nvidiaSmiAvailable = null; this.nvidiaSmiAvailable = null;
@@ -390,6 +399,8 @@ class HardwareMonitorService {
} }
async reloadFromSettings(options = {}) { async reloadFromSettings(options = {}) {
const requestSeq = this.reloadSettingsSeq + 1;
this.reloadSettingsSeq = requestSeq;
const forceBroadcast = Boolean(options?.forceBroadcast); const forceBroadcast = Boolean(options?.forceBroadcast);
const forceImmediatePoll = Boolean(options?.forceImmediatePoll); const forceImmediatePoll = Boolean(options?.forceImmediatePoll);
let settingsMap = {}; let settingsMap = {};
@@ -399,6 +410,9 @@ class HardwareMonitorService {
logger.warn('settings:load:failed', { error: errorToMeta(error) }); logger.warn('settings:load:failed', { error: errorToMeta(error) });
return this.getSnapshot(); return this.getSnapshot();
} }
if (requestSeq !== this.reloadSettingsSeq) {
return this.getSnapshot();
}
const nextEnabled = toBoolean(settingsMap.hardware_monitoring_enabled); const nextEnabled = toBoolean(settingsMap.hardware_monitoring_enabled);
const nextIntervalMs = clampIntervalMs(settingsMap.hardware_monitoring_interval_ms); const nextIntervalMs = clampIntervalMs(settingsMap.hardware_monitoring_interval_ms);
@@ -521,6 +535,14 @@ class HardwareMonitorService {
this.running = false; this.running = false;
this.pollInFlight = false; this.pollInFlight = false;
this.lastCpuTimes = null; this.lastCpuTimes = null;
if (this.activePollAbortController) {
try {
this.activePollAbortController.abort();
} catch (_error) {
// ignore abort errors
}
this.activePollAbortController = null;
}
if (this.timer) { if (this.timer) {
clearTimeout(this.timer); clearTimeout(this.timer);
this.timer = null; this.timer = null;
@@ -553,8 +575,10 @@ class HardwareMonitorService {
return; return;
} }
this.pollInFlight = true; this.pollInFlight = true;
const pollAbortController = new AbortController();
this.activePollAbortController = pollAbortController;
try { try {
const sample = await this.collectSample(); const sample = await this.collectSample(pollAbortController.signal);
this.lastSnapshot = { this.lastSnapshot = {
enabled: true, enabled: true,
intervalMs: this.intervalMs, intervalMs: this.intervalMs,
@@ -564,6 +588,10 @@ class HardwareMonitorService {
}; };
this.broadcastUpdate(); this.broadcastUpdate();
} catch (error) { } catch (error) {
if (isAbortError(error)) {
logger.debug('poll:aborted');
return;
}
logger.warn('poll:failed', { error: errorToMeta(error) }); logger.warn('poll:failed', { error: errorToMeta(error) });
this.lastSnapshot = { this.lastSnapshot = {
...this.lastSnapshot, ...this.lastSnapshot,
@@ -574,6 +602,9 @@ class HardwareMonitorService {
}; };
this.broadcastUpdate(); this.broadcastUpdate();
} finally { } finally {
if (this.activePollAbortController === pollAbortController) {
this.activePollAbortController = null;
}
this.pollInFlight = false; this.pollInFlight = false;
if (this.running && this.enabled) { if (this.running && this.enabled) {
this.scheduleNext(this.intervalMs); this.scheduleNext(this.intervalMs);
@@ -585,12 +616,12 @@ class HardwareMonitorService {
wsService.broadcast('HARDWARE_MONITOR_UPDATE', this.getSnapshot()); wsService.broadcast('HARDWARE_MONITOR_UPDATE', this.getSnapshot());
} }
async collectSample() { async collectSample(signal) {
const memory = this.collectMemoryMetrics(); const memory = this.collectMemoryMetrics();
const [cpu, gpu, storage] = await Promise.all([ const [cpu, gpu, storage] = await Promise.all([
this.collectCpuMetrics(), this.collectCpuMetrics(signal),
this.collectGpuMetrics(), this.collectGpuMetrics(signal),
this.collectStorageMetrics() this.collectStorageMetrics(signal)
]); ]);
return { return {
@@ -665,13 +696,13 @@ class HardwareMonitorService {
}; };
} }
async collectCpuMetrics() { async collectCpuMetrics(signal) {
const cpus = os.cpus() || []; const cpus = os.cpus() || [];
const currentTimes = this.getCpuTimes(); const currentTimes = this.getCpuTimes();
const usage = this.calculateCpuUsage(currentTimes, this.lastCpuTimes || []); const usage = this.calculateCpuUsage(currentTimes, this.lastCpuTimes || []);
this.lastCpuTimes = currentTimes; this.lastCpuTimes = currentTimes;
const tempMetrics = await this.collectCpuTemperatures(); const tempMetrics = await this.collectCpuTemperatures(signal);
const tempByCoreIndex = new Map( const tempByCoreIndex = new Map(
(tempMetrics.perCore || []).map((item) => [Number(item.index), item.temperatureC]) (tempMetrics.perCore || []).map((item) => [Number(item.index), item.temperatureC])
); );
@@ -708,8 +739,8 @@ class HardwareMonitorService {
}; };
} }
async collectCpuTemperatures() { async collectCpuTemperatures(signal) {
const sensors = await this.collectTempsViaSensors(); const sensors = await this.collectTempsViaSensors(signal);
if (sensors.available) { if (sensors.available) {
return sensors; return sensors;
} }
@@ -732,7 +763,7 @@ class HardwareMonitorService {
}; };
} }
async collectTempsViaSensors() { async collectTempsViaSensors(signal) {
if (this.sensorsCommandAvailable === false) { if (this.sensorsCommandAvailable === false) {
return { return {
source: 'sensors', source: 'sensors',
@@ -745,7 +776,8 @@ class HardwareMonitorService {
try { try {
const { stdout } = await execFileAsync('sensors', ['-j'], { const { stdout } = await execFileAsync('sensors', ['-j'], {
timeout: SENSORS_TIMEOUT_MS, timeout: SENSORS_TIMEOUT_MS,
maxBuffer: 2 * 1024 * 1024 maxBuffer: 2 * 1024 * 1024,
signal
}); });
this.sensorsCommandAvailable = true; this.sensorsCommandAvailable = true;
const parsed = JSON.parse(String(stdout || '{}')); const parsed = JSON.parse(String(stdout || '{}'));
@@ -756,6 +788,9 @@ class HardwareMonitorService {
...mapTemperatureCandidates(preferred) ...mapTemperatureCandidates(preferred)
}; };
} catch (error) { } catch (error) {
if (isAbortError(error)) {
throw error;
}
if (isCommandMissingError(error)) { if (isCommandMissingError(error)) {
this.sensorsCommandAvailable = false; this.sensorsCommandAvailable = false;
} }
@@ -863,7 +898,7 @@ class HardwareMonitorService {
}; };
} }
async collectGpuMetrics() { async collectGpuMetrics(signal) {
if (this.nvidiaSmiAvailable === false) { if (this.nvidiaSmiAvailable === false) {
return { return {
source: 'nvidia-smi', source: 'nvidia-smi',
@@ -882,7 +917,8 @@ class HardwareMonitorService {
], ],
{ {
timeout: NVIDIA_SMI_TIMEOUT_MS, timeout: NVIDIA_SMI_TIMEOUT_MS,
maxBuffer: 1024 * 1024 maxBuffer: 1024 * 1024,
signal
} }
); );
@@ -910,6 +946,9 @@ class HardwareMonitorService {
message: null message: null
}; };
} catch (error) { } catch (error) {
if (isAbortError(error)) {
throw error;
}
const commandMissing = isCommandMissingError(error); const commandMissing = isCommandMissingError(error);
if (commandMissing) { if (commandMissing) {
this.nvidiaSmiAvailable = false; this.nvidiaSmiAvailable = false;
@@ -926,10 +965,10 @@ class HardwareMonitorService {
} }
} }
async collectStorageMetrics() { async collectStorageMetrics(signal) {
const list = []; const list = [];
for (const entry of this.monitoredPaths) { for (const entry of this.monitoredPaths) {
const metric = await this.collectStorageForPath(entry); const metric = await this.collectStorageForPath(entry, signal);
if (metric && !metric.hidden) { if (metric && !metric.hidden) {
list.push(metric); list.push(metric);
} }
@@ -959,7 +998,7 @@ class HardwareMonitorService {
return null; return null;
} }
async collectStorageForPath(entry) { async collectStorageForPath(entry, signal) {
const key = String(entry?.key || ''); const key = String(entry?.key || '');
const label = String(entry?.label || key || 'Pfad'); const label = String(entry?.label || key || 'Pfad');
const rawPath = String(entry?.path || '').trim(); const rawPath = String(entry?.path || '').trim();
@@ -1028,7 +1067,8 @@ class HardwareMonitorService {
try { try {
const { stdout } = await execFileAsync('df', ['-Pk', queryPath], { const { stdout } = await execFileAsync('df', ['-Pk', queryPath], {
timeout: DF_TIMEOUT_MS, timeout: DF_TIMEOUT_MS,
maxBuffer: 256 * 1024 maxBuffer: 256 * 1024,
signal
}); });
const parsed = parseDfStats(stdout); const parsed = parseDfStats(stdout);
if (!parsed) { if (!parsed) {
@@ -1077,6 +1117,9 @@ class HardwareMonitorService {
error: null error: null
}; };
} catch (error) { } catch (error) {
if (isAbortError(error)) {
throw error;
}
if (hideWhenUnavailable) { if (hideWhenUnavailable) {
return hiddenResult({ return hiddenResult({
path: resolvedPath, path: resolvedPath,
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.16.1-3", "version": "0.16.1-4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.16.1-3", "version": "0.16.1-4",
"dependencies": { "dependencies": {
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primereact": "^10.9.2", "primereact": "^10.9.2",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "ripster-frontend", "name": "ripster-frontend",
"version": "0.16.1-3", "version": "0.16.1-4",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+13
View File
@@ -129,6 +129,11 @@ function getDownloadIndicatorMeta(summary) {
}; };
} }
const HARDWARE_MONITOR_SETTING_KEYS = new Set([
'hardware_monitoring_enabled',
'hardware_monitoring_interval_ms'
]);
function App() { function App() {
const appVersion = __APP_VERSION__; const appVersion = __APP_VERSION__;
const [pipeline, setPipeline] = useState({ state: 'IDLE', progress: 0, context: {} }); const [pipeline, setPipeline] = useState({ state: 'IDLE', progress: 0, context: {} });
@@ -511,10 +516,15 @@ function App() {
const val = setting?.value; const val = setting?.value;
setExpertMode(val === 'true' || val === true); setExpertMode(val === 'true' || val === true);
} }
const normalizedKey = String(setting?.key || '').trim().toLowerCase();
if (HARDWARE_MONITOR_SETTING_KEYS.has(normalizedKey)) {
refreshPipeline().catch(() => null);
}
} }
if (message.type === 'SETTINGS_BULK_UPDATED') { if (message.type === 'SETTINGS_BULK_UPDATED') {
const keys = message.payload?.keys || []; const keys = message.payload?.keys || [];
const normalizedKeys = keys.map((key) => String(key || '').trim().toLowerCase());
if (keys.includes('ui_expert_mode')) { if (keys.includes('ui_expert_mode')) {
api.getSettings({ forceRefresh: true }) api.getSettings({ forceRefresh: true })
.then((response) => { .then((response) => {
@@ -524,6 +534,9 @@ function App() {
}) })
.catch(() => null); .catch(() => null);
} }
if (normalizedKeys.some((key) => HARDWARE_MONITOR_SETTING_KEYS.has(key))) {
refreshPipeline().catch(() => null);
}
} }
if (message.type === 'DOWNLOADS_UPDATED') { if (message.type === 'DOWNLOADS_UPDATED') {
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "ripster", "name": "ripster",
"version": "0.16.1-3", "version": "0.16.1-4",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ripster", "name": "ripster",
"version": "0.16.1-3", "version": "0.16.1-4",
"devDependencies": { "devDependencies": {
"concurrently": "^9.1.2" "concurrently": "^9.1.2"
} }
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "ripster", "name": "ripster",
"private": true, "private": true,
"version": "0.16.1-3", "version": "0.16.1-4",
"scripts": { "scripts": {
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"", "dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
"dev:backend": "npm run dev --prefix backend", "dev:backend": "npm run dev --prefix backend",