@remcp/runtime 0.2.200 → 0.2.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Linux exposes multiple independent hardware temperature channels through hwmon and
|
|
2
|
+
// thermal zones. Never stop at the first thermal zone: that hides real CPU/GPU/SSD data.
|
|
3
|
+
import { readFile as kernelRead, readdir as kernelList } from 'node:fs/promises';
|
|
4
|
+
import { nvidiaTemperatureReadings } from './nvidia-temperature.mjs';
|
|
5
|
+
|
|
6
|
+
const CHIP = /^[A-Za-z0-9_.+-]{1,32}$/;
|
|
7
|
+
const LABEL = /^[A-Za-z0-9_.:+#() -]{1,40}$/;
|
|
8
|
+
const SAFE_NAME = /^[A-Za-z0-9_.:+#() -]{1,60}$/;
|
|
9
|
+
const SENSOR_LIMIT = 64;
|
|
10
|
+
|
|
11
|
+
export function linuxTemperatureValue(raw) {
|
|
12
|
+
const input = String(raw || '').trim();
|
|
13
|
+
if (!/^-?\d{1,7}$/.test(input)) return null;
|
|
14
|
+
const celsius = Number(input) / 1000;
|
|
15
|
+
return Number.isFinite(celsius) && celsius >= -40 && celsius <= 125
|
|
16
|
+
? Number(celsius.toFixed(1)) : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const priority = name => /^(?:k10temp|coretemp|zenpower|cpu)/i.test(name) ? 0
|
|
20
|
+
: /^(?:amdgpu|nvidia|gpu)/i.test(name) ? 1
|
|
21
|
+
: /^nvme/i.test(name) ? 2 : 3;
|
|
22
|
+
|
|
23
|
+
export async function linuxTemperatureReadings({
|
|
24
|
+
list = kernelList, read = kernelRead, nvidia = nvidiaTemperatureReadings,
|
|
25
|
+
} = {}) {
|
|
26
|
+
const entries = [];
|
|
27
|
+
const chips = [];
|
|
28
|
+
const seen = new Set();
|
|
29
|
+
const boundedFile = async name => {
|
|
30
|
+
const value = String(await read(name, 'utf8'));
|
|
31
|
+
return value.length <= 128 ? value.trim() : '';
|
|
32
|
+
};
|
|
33
|
+
const add = (name, value, chip = '') => {
|
|
34
|
+
if (value === null || !SAFE_NAME.test(name) || seen.has(name)) return;
|
|
35
|
+
entries.push({ name, celsius: value, chip });
|
|
36
|
+
seen.add(name);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const dirs = (await list('/sys/class/hwmon'))
|
|
41
|
+
.filter(name => /^hwmon\d{1,3}$/.test(name)).slice(0, 32);
|
|
42
|
+
for (const dir of dirs) {
|
|
43
|
+
const base = '/sys/class/hwmon/' + dir;
|
|
44
|
+
let chip;
|
|
45
|
+
try { chip = await boundedFile(base + '/name'); } catch { continue; }
|
|
46
|
+
if (!CHIP.test(chip)) continue;
|
|
47
|
+
chips.push(chip);
|
|
48
|
+
let inputs = [];
|
|
49
|
+
try { inputs = (await list(base)).filter(name => /^temp(?:[1-9]|[1-9]\d)_input$/.test(name)).slice(0, 32); }
|
|
50
|
+
catch { continue; }
|
|
51
|
+
for (const file of inputs) {
|
|
52
|
+
const index = file.match(/^temp(\d+)_input$/)[1];
|
|
53
|
+
try {
|
|
54
|
+
const value = linuxTemperatureValue(await boundedFile(base + '/' + file));
|
|
55
|
+
let label = '';
|
|
56
|
+
try { label = await boundedFile(base + '/temp' + index + '_label'); } catch { /* Optional. */ }
|
|
57
|
+
if (!LABEL.test(label)) label = 'temp' + index;
|
|
58
|
+
const baseName = chip + ': ' + label;
|
|
59
|
+
// Multiple NVMe drives can share the same chip/label. Preserve each
|
|
60
|
+
// physical channel using its hwmon ID rather than dropping it.
|
|
61
|
+
add(seen.has(baseName) ? chip + '(' + dir + '): ' + label : baseName, value, chip);
|
|
62
|
+
} catch { /* Optional or inaccessible hardware channel. */ }
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch { /* No hwmon inventory (VMs, containers, hardened hosts). */ }
|
|
66
|
+
|
|
67
|
+
// Thermal zones can expose additional independent channels. Drop an exact
|
|
68
|
+
// ACPI duplicate when hwmon already reports the same chip and temperature.
|
|
69
|
+
try {
|
|
70
|
+
const zones = (await list('/sys/class/thermal'))
|
|
71
|
+
.filter(name => /^thermal_zone\d{1,3}$/.test(name)).slice(0, 32);
|
|
72
|
+
for (const zone of zones) {
|
|
73
|
+
const base = '/sys/class/thermal/' + zone;
|
|
74
|
+
try {
|
|
75
|
+
const value = linuxTemperatureValue(await boundedFile(base + '/temp'));
|
|
76
|
+
const type = await boundedFile(base + '/type').catch(() => '');
|
|
77
|
+
const duplicate = CHIP.test(type) && entries.some(row =>
|
|
78
|
+
row.chip === type && value !== null && Math.abs(row.celsius - value) <= 0.1);
|
|
79
|
+
if (!duplicate) add('Thermal ' + zone.slice('thermal_zone'.length), value);
|
|
80
|
+
} catch { /* Optional or inaccessible thermal zone. */ }
|
|
81
|
+
}
|
|
82
|
+
} catch { /* No thermal zones. */ }
|
|
83
|
+
// The proprietary NVIDIA driver may not expose GPU temperatures via hwmon.
|
|
84
|
+
// Query its read-only tool using fixed arguments; missing drivers mean unknown.
|
|
85
|
+
const vendorGpu = await nvidia().catch(() => []);
|
|
86
|
+
for (const item of Array.isArray(vendorGpu) ? vendorGpu.slice(0, 16) : []) {
|
|
87
|
+
if (!/^NVIDIA GPU (?:[0-9]|1[0-5])$/.test(item?.name)) continue;
|
|
88
|
+
const value = typeof item.celsius === 'number' && Number.isFinite(item.celsius) &&
|
|
89
|
+
item.celsius >= 0 && item.celsius <= 125 ? item.celsius : null;
|
|
90
|
+
add(item.name, value, 'nvidia');
|
|
91
|
+
}
|
|
92
|
+
// Prefer measured CPU and GPU channels for the compact overview widget.
|
|
93
|
+
return entries.sort((a, b) => priority(a.chip) - priority(b.chip))
|
|
94
|
+
.slice(0, SENSOR_LIMIT).map(({ name, celsius }) => ({ name, celsius }));
|
|
95
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// NVIDIA exposes GPU die temperatures through the read-only nvidia-smi CLI
|
|
2
|
+
// on Linux and Windows. Do not execute a shell or user-supplied arguments.
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
const exec = promisify(execFile);
|
|
6
|
+
|
|
7
|
+
export function parseNvidiaTemperatures(stdout) {
|
|
8
|
+
return String(stdout || '').split(/\r?\n/).slice(0, 16).flatMap((line, index) => {
|
|
9
|
+
const value = line.trim();
|
|
10
|
+
if (!/^\d{1,3}(?:\.\d)?$/.test(value)) return [];
|
|
11
|
+
const celsius = Number(value);
|
|
12
|
+
return celsius >= 0 && celsius <= 125
|
|
13
|
+
? [{ name: 'NVIDIA GPU ' + index, celsius }] : [];
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function nvidiaTemperatureReadings({ run = exec } = {}) {
|
|
18
|
+
try {
|
|
19
|
+
const result = await run('nvidia-smi',
|
|
20
|
+
['--query-gpu=temperature.gpu', '--format=csv,noheader,nounits'],
|
|
21
|
+
{ windowsHide: true, timeout: 2500, maxBuffer: 4096 });
|
|
22
|
+
return parseNvidiaTemperatures(result.stdout);
|
|
23
|
+
} catch { return []; }
|
|
24
|
+
}
|
|
@@ -5,6 +5,8 @@ import { execFile } from 'node:child_process';
|
|
|
5
5
|
import { promisify } from 'node:util';
|
|
6
6
|
import process from 'node:process';
|
|
7
7
|
import { darwinTemperatureReadings } from './darwin-temperature.mjs';
|
|
8
|
+
import { linuxTemperatureReadings } from './linux-temperature.mjs';
|
|
9
|
+
import { windowsTemperatureReadings } from './windows-temperature.mjs';
|
|
8
10
|
|
|
9
11
|
const run = promisify(execFile);
|
|
10
12
|
const numeric = value => Number.isFinite(value) && value >= 0 && value <= Number.MAX_SAFE_INTEGER;
|
|
@@ -62,35 +64,9 @@ export async function networkRates() {
|
|
|
62
64
|
|
|
63
65
|
export async function thermalReadings() {
|
|
64
66
|
if (process.platform === 'darwin') return darwinTemperatureReadings();
|
|
65
|
-
if (process.platform
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
const names = (await readdir('/sys/class/thermal')).filter(name => /^thermal_zone\d{1,3}$/.test(name)).slice(0, 16);
|
|
70
|
-
for (const name of names) {
|
|
71
|
-
try {
|
|
72
|
-
const value = Number((await readFile('/sys/class/thermal/' + name + '/temp', 'utf8')).trim()) / 1000;
|
|
73
|
-
if (Number.isFinite(value) && value >= -40 && value <= 125)
|
|
74
|
-
sensors.push({ name: 'Thermal ' + name.slice(12), celsius: Number(value.toFixed(1)) });
|
|
75
|
-
} catch { /* Sensor is optional or permissions deny reading it. */ }
|
|
76
|
-
}
|
|
77
|
-
} catch { /* No thermal zone inventory. */ }
|
|
78
|
-
// hwmon is common on desktop hardware without usable thermal zones.
|
|
79
|
-
if (!sensors.length) {
|
|
80
|
-
try {
|
|
81
|
-
const chips = (await readdir('/sys/class/hwmon')).filter(name => /^hwmon\d{1,3}$/.test(name)).slice(0, 12);
|
|
82
|
-
for (const chip of chips) {
|
|
83
|
-
for (let index = 1; index <= 4 && sensors.length < 12; index++) {
|
|
84
|
-
try {
|
|
85
|
-
const value = Number((await readFile('/sys/class/hwmon/' + chip + '/temp' + index + '_input', 'utf8')).trim()) / 1000;
|
|
86
|
-
if (Number.isFinite(value) && value >= -40 && value <= 125)
|
|
87
|
-
sensors.push({ name: 'Sensor ' + (sensors.length + 1), celsius: Number(value.toFixed(1)) });
|
|
88
|
-
} catch { /* Not every chip exposes every temperature index. */ }
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
} catch { /* No readable hwmon inventory. */ }
|
|
92
|
-
}
|
|
93
|
-
return sensors.slice(0, 12);
|
|
67
|
+
if (process.platform === 'linux') return linuxTemperatureReadings();
|
|
68
|
+
if (process.platform === 'win32') return windowsTemperatureReadings();
|
|
69
|
+
return [];
|
|
94
70
|
}
|
|
95
71
|
|
|
96
72
|
export function parseDiskVolumes(input) {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Optional read-only Windows temperature sources. Windows has no universal
|
|
2
|
+
// built-in API for CPU/GPU/SSD temperatures: report only exposed real sensors.
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { promisify } from 'node:util';
|
|
5
|
+
import { nvidiaTemperatureReadings } from './nvidia-temperature.mjs';
|
|
6
|
+
export { parseNvidiaTemperatures } from './nvidia-temperature.mjs';
|
|
7
|
+
|
|
8
|
+
const exec = promisify(execFile);
|
|
9
|
+
const MAX_SENSORS = 64;
|
|
10
|
+
const CLEAN_LABEL = /^[A-Za-z0-9_.:+#() -]{1,60}$/;
|
|
11
|
+
|
|
12
|
+
// Fixed PowerShell source, with no user-supplied script text or arguments.
|
|
13
|
+
export const WINDOWS_TEMPERATURE_SCRIPT = [
|
|
14
|
+
"$ErrorActionPreference = 'Stop'",
|
|
15
|
+
"$rows = [System.Collections.Generic.List[object]]::new()",
|
|
16
|
+
"foreach ($namespace in @('root\\LibreHardwareMonitor','root\\OpenHardwareMonitor')) {",
|
|
17
|
+
" try {",
|
|
18
|
+
" $sensors = @(Get-CimInstance -Namespace $namespace -ClassName Sensor -ErrorAction Stop)",
|
|
19
|
+
" foreach ($sensor in $sensors) {",
|
|
20
|
+
" if ($sensor.SensorType -ne 'Temperature') { continue }",
|
|
21
|
+
" $rows.Add(@{ name = ('Hardware: ' + [string]$sensor.Name); celsius = $sensor.Value })",
|
|
22
|
+
" if ($rows.Count -ge 64) { break }",
|
|
23
|
+
" }",
|
|
24
|
+
" if ($rows.Count -gt 0) { break }",
|
|
25
|
+
" } catch { }",
|
|
26
|
+
"}",
|
|
27
|
+
"try {",
|
|
28
|
+
" $zones = @(Get-CimInstance -Namespace 'root\\wmi' -ClassName MSAcpi_ThermalZoneTemperature -ErrorAction Stop)",
|
|
29
|
+
" $index = 0",
|
|
30
|
+
" foreach ($zone in $zones) {",
|
|
31
|
+
" $raw = [double]$zone.CurrentTemperature",
|
|
32
|
+
" $rows.Add(@{ name = ('ACPI Thermal ' + $index); celsius = (($raw / 10) - 273.15) })",
|
|
33
|
+
" $index += 1",
|
|
34
|
+
" if ($rows.Count -ge 64) { break }",
|
|
35
|
+
" }",
|
|
36
|
+
"} catch { }",
|
|
37
|
+
"ConvertTo-Json -InputObject @($rows.ToArray()) -Compress -Depth 3",
|
|
38
|
+
].join('\n');
|
|
39
|
+
|
|
40
|
+
export function parseWindowsTemperatureReadings(stdout) {
|
|
41
|
+
let values;
|
|
42
|
+
try { values = JSON.parse(String(stdout || '')); } catch { return []; }
|
|
43
|
+
if (!Array.isArray(values)) values = values && typeof values === 'object' ? [values] : [];
|
|
44
|
+
const sensors = [];
|
|
45
|
+
const names = new Set();
|
|
46
|
+
for (const raw of values.slice(0, 128)) {
|
|
47
|
+
const baseName = String(raw?.name ?? '').trim();
|
|
48
|
+
const value = raw?.celsius;
|
|
49
|
+
if (!CLEAN_LABEL.test(baseName) || typeof value !== 'number' ||
|
|
50
|
+
!Number.isFinite(value) || value < -40 || value > 125) continue;
|
|
51
|
+
// Separate GPUs/cores can expose identical labels through the same WMI
|
|
52
|
+
// provider; retain every valid channel with a deterministic index.
|
|
53
|
+
let name = baseName;
|
|
54
|
+
if (names.has(name)) name = baseName + ' (' + (sensors.length + 1) + ')';
|
|
55
|
+
if (!CLEAN_LABEL.test(name) || names.has(name)) continue;
|
|
56
|
+
names.add(name);
|
|
57
|
+
sensors.push({ name, celsius: Number(value.toFixed(1)) });
|
|
58
|
+
if (sensors.length >= MAX_SENSORS) break;
|
|
59
|
+
}
|
|
60
|
+
return sensors;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function windowsTemperatureReadings({ run = exec } = {}) {
|
|
64
|
+
const [cim, nvidia] = await Promise.all([
|
|
65
|
+
run('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive',
|
|
66
|
+
'-Command', WINDOWS_TEMPERATURE_SCRIPT], {
|
|
67
|
+
windowsHide: true, timeout: 5500, maxBuffer: 32768,
|
|
68
|
+
}).then(result => parseWindowsTemperatureReadings(result.stdout)).catch(() => []),
|
|
69
|
+
nvidiaTemperatureReadings({ run }),
|
|
70
|
+
]);
|
|
71
|
+
const names = new Set(cim.map(item => item.name));
|
|
72
|
+
for (const item of nvidia) if (!names.has(item.name) && cim.length < MAX_SENSORS) {
|
|
73
|
+
cim.push(item); names.add(item.name);
|
|
74
|
+
}
|
|
75
|
+
return cim;
|
|
76
|
+
}
|