Compare commits

..

21 Commits
v1.0.6 ... main

Author SHA1 Message Date
github-actions[bot]
de13e2329b
chore: update changelog to v1.0.7 (#298)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-08-23 01:31:02 +02:00
Meliox
6f78347a0b
fix:(nag_screen): register dpkg trigger interest for proxmox-widget-toolkit for automatic patching (#317)
Co-authored-by: Meliox <na>
2026-08-23 01:23:53 +02:00
Meliox
8d70194b8a
fix(nag_screen): Patch typo (#318)
Co-authored-by: Meliox <na>
2026-08-23 01:23:42 +02:00
Meliox
2a122ef3eb
feat(node_info) Add configurable minimum-temperature ignore threshold (#316)
Co-authored-by: Meliox <na>
2026-08-23 00:30:51 +02:00
Meliox
0476410de5
fix(node_info): re-show sensor widgets when data becomes valid again after being hidden (#312)
Co-authored-by: Meliox <na>
2026-08-23 00:25:27 +02:00
Meliox
972c9ae956
fix(node_info): reap zombie pve_mod_worker and orphaned collector processes (#307)
* fix(node_info): reap zombie pve_mod_worker and orphaned collector processes

Double-forks pve_mod_worker so it is reparented to init instead of the calling process, which reaps it immediately on exit and prevents it lingering as a <defunct> zombie. Also adds startup-time reaping of orphaned collector-* processes (ppid==1) left behind if a worker was previously killed with SIGKILL, and an opportunistic waitpid(WNOHANG) in _worker_lock_file_exists as a cheap supplemental reap.

* fix compile error and reap orphans

---------

Co-authored-by: Meliox <na>
2026-08-22 23:48:59 +02:00
Evan
69e2d78b0a
feat(node_info): UPS widget: redesign status panel with battery-charge progress bar (#310) 2026-08-22 23:33:04 +02:00
Meliox
bdb3f9f3df
fix(node_info): normalize UPS data — derive ups.realpower fallback and clean invalid battery.mfr.date (#309)
Co-authored-by: Meliox <na>
2026-08-22 23:17:59 +02:00
Meliox
c768eac6e3
fix(nag_screen): Fix nag screen patch webui line offsets (#315)
Co-authored-by: Meliox <na>
2026-08-22 21:20:25 +02:00
Meliox
7cb9aefced
feat(ci): Capture and report Proxmox package versions in weekly patch test issues (#314)
Co-authored-by: Meliox <na>
2026-08-22 20:50:22 +02:00
Meliox
d99d08cf5f
fix(ci): pass mod argument to single patch in test-patches.sh (#313)
Co-authored-by: Meliox <na>
2026-08-22 20:49:58 +02:00
Evan
8a9b7568a7
fix(node_info): hide GPU Details and GPU Fans when no GPU is present (#311)
Empty graphics payloads currently leave the labelled rows visible with
no content. Hide those widgets unless they can render at least one GPU.
2026-08-22 17:49:56 +02:00
Meliox
f754331c3a
fix(node_info): fix stale worker lock detection for zombie processes (#306)
Co-authored-by: Meliox <na>
2026-08-19 21:58:49 +02:00
Meliox
e46734cd37
fix(node_info): return average and core temps for intel (#302)
Co-authored-by: Meliox <na>
2026-08-17 20:51:43 +02:00
Meliox
bc87579d77
fix(ci): stop force-pushing empty diff on version-bump branch to prevent PR churn (#303)
Co-authored-by: Meliox <na>
2026-08-17 20:47:42 +02:00
Meliox
e59d09a788
fix(ci): run weekly patch tests in debian:trixie container to fix missing dependencies(#304)
Co-authored-by: Meliox <na>
2026-08-17 20:47:01 +02:00
Meliox
cee8d04e36
feat/fix(node_info): expose temp_unit in graphics API response & fix temperature conversion (#295)
Co-authored-by: Meliox <na>
2026-08-14 20:32:10 +02:00
Meliox
8ebc2b4753
feat(node_info): add GPU utilization display with color-coded thresholds for NVIDIA GPUs (#296)
Co-authored-by: Meliox <na>
2026-08-14 20:15:43 +02:00
Meliox
795b0ebf39
build(ci): fix weekly patch test build steps and align with test-release workflow (#293)
Co-authored-by: Meliox <na>
2026-08-10 18:47:22 +02:00
Meliox
43514fc949
fix readme dl link (#290)
Co-authored-by: Meliox <na>
2026-08-09 14:28:48 +02:00
Meliox
c1c26aff56
Readme update for version 1.0 (#225)
* Readme update for next version

* update readme

* link fix

* language

* more changes

* typo

* more cleanup

---------

Co-authored-by: Meliox <na>
2026-08-09 14:25:14 +02:00
16 changed files with 719 additions and 249 deletions

View File

@ -58,7 +58,6 @@ jobs:
if [[ "${{ github.event_name }}" == "push" ]]; then if [[ "${{ github.event_name }}" == "push" ]]; then
# Always recreate from main — avoids rebase conflicts after PR merge # Always recreate from main — avoids rebase conflicts after PR merge
git checkout -b chore/version-bump origin/main git checkout -b chore/version-bump origin/main
git push --force origin chore/version-bump
else else
# Label change: check out the existing branch # Label change: check out the existing branch
git fetch origin chore/version-bump git fetch origin chore/version-bump
@ -133,8 +132,8 @@ jobs:
echo "$MAIN_CHANGELOG" echo "$MAIN_CHANGELOG"
} > debian/changelog } > debian/changelog
git add debian/changelog git add debian/changelog
git diff --cached --quiet || git commit -m "chore: update changelog to v${NEW_VERSION}" git commit -m "chore: update changelog to v${NEW_VERSION}"
git push --force-with-lease origin chore/version-bump git push --force origin chore/version-bump
- name: Build PR body - name: Build PR body
env: env:

View File

@ -36,11 +36,20 @@ jobs:
test: test:
needs: discover needs: discover
runs-on: ubuntu-latest runs-on: ubuntu-latest
container:
image: debian:trixie
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
mod: ${{ fromJSON(needs.discover.outputs.mods) }} mod: ${{ fromJSON(needs.discover.outputs.mods) }}
steps: steps:
# ── Base tools (mirrors validate-mods.yml) ──────────────────────────────
- name: Install base tools
run: |
apt-get update
apt-get install -y --no-install-recommends \
ca-certificates git sudo wget xz-utils dpkg nodejs perl
- uses: actions/checkout@v7 - uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@ -48,22 +57,32 @@ jobs:
# ── Build (mirrors test-release.yml) ─────────────────────────────────── # ── Build (mirrors test-release.yml) ───────────────────────────────────
- name: Install build deps - name: Install build deps
run: | run: |
sudo apt-get update -qq apt-get update -qq
sudo apt-get install -y devscripts debhelper build-essential apt-get install -y devscripts debhelper build-essential
- name: Compute build version
id: version
run: |
BASE_VERSION=$(grep -m1 '(' debian/changelog | sed 's/.*(\(.*\)).*/\1/')
SHORT_SHA=$(git rev-parse --short HEAD)
BUILD_VERSION="${BASE_VERSION}+weekly.${SHORT_SHA}"
echo "build_version=$BUILD_VERSION" >> "$GITHUB_OUTPUT"
echo "Building version: $BUILD_VERSION"
- name: Inject version into Config.pm - name: Inject version into Config.pm
run: | run: |
VERSION=$(grep -m1 '(' debian/changelog | sed 's/.*(\(.*\)).*/\1/') BUILD_VERSION="${{ steps.version.outputs.build_version }}"
SHORT_SHA=$(git rev-parse --short HEAD) sed -i -E "s/^our \\\$VERSION\s*=.*/our \\\$VERSION = '${BUILD_VERSION}';/" \
export BUILD_VERSION="${VERSION}-test-${SHORT_SHA}" src/modules/node_info/files/Config.pm
perl -i -pe 'BEGIN { $v = $ENV{BUILD_VERSION} }
s/(our \$VERSION\s*=\s*'"'"')[^'"'"']*'"'"';/$1'"'"'$v'"'"';/ - name: Set package version in changelog
' src/modules/node_info/files/Config.pm run: |
BUILD_VERSION="${{ steps.version.outputs.build_version }}"
sed -i -E "0,/\(([^)]*)\)/s//(${BUILD_VERSION})/" debian/changelog
- name: Generate dynamic debian rules - name: Generate dynamic debian rules
run: | run: |
bash build/gen-rules.sh >> debian/rules bash build/gen-rules.sh >> debian/rules
bash build/gen-rules.sh conffiles >> debian/pve-mod.conffiles
- name: Build package - name: Build package
run: dpkg-buildpackage -us -uc -b run: dpkg-buildpackage -us -uc -b
@ -72,13 +91,26 @@ jobs:
- name: Fetch upstream Proxmox files - name: Fetch upstream Proxmox files
run: bash build/test/fetch-proxmox-files.sh run: bash build/test/fetch-proxmox-files.sh
- name: Capture upstream package versions
id: pve_versions
run: |
PVE_MANAGER_VERSION=$(dpkg-query -W -f='${Version}' pve-manager 2>/dev/null || echo "unknown")
PWT_VERSION=$(dpkg-query -W -f='${Version}' proxmox-widget-toolkit 2>/dev/null || echo "unknown")
echo "pve_manager=${PVE_MANAGER_VERSION}" >> "$GITHUB_OUTPUT"
echo "proxmox_widget_toolkit=${PWT_VERSION}" >> "$GITHUB_OUTPUT"
echo "pve-manager: ${PVE_MANAGER_VERSION}"
echo "proxmox-widget-toolkit: ${PWT_VERSION}"
- name: Install built package - name: Install built package
run: sudo apt-get install -y ../pve-mod_*.deb run: apt-get install -y ../pve-mod_*.deb
- name: Test mod - name: Test mod
run: | run: |
set -o pipefail set -o pipefail
sudo bash build/test/test-patches.sh "${{ matrix.mod }}" 2>&1 | tee test-output.log bash build/test/test-patches.sh "${{ matrix.mod }}" 2>&1 | tee test-output.log
- name: Validate source syntax
run: bash build/test/test-syntax.sh "${{ matrix.mod }}"
- name: Report failure as issue - name: Report failure as issue
if: failure() if: failure()
@ -90,6 +122,10 @@ jobs:
{ {
echo "The weekly patch test failed for mod \`${MOD}\` against the latest Proxmox VE files." echo "The weekly patch test failed for mod \`${MOD}\` against the latest Proxmox VE files."
echo "" echo ""
echo "**Proxmox versions tested:**"
echo "- pve-manager: \`${{ steps.pve_versions.outputs.pve_manager }}\`"
echo "- proxmox-widget-toolkit: \`${{ steps.pve_versions.outputs.proxmox_widget_toolkit }}\`"
echo ""
echo "Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" echo "Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
echo "" echo ""
echo "<details><summary>Test log</summary>" echo "<details><summary>Test log</summary>"
@ -109,8 +145,9 @@ jobs:
--jq ".[] | select(.title==\"$TITLE\") | .number" | head -n1) --jq ".[] | select(.title==\"$TITLE\") | .number" | head -n1)
if [[ -n "$EXISTING" ]]; then if [[ -n "$EXISTING" ]]; then
gh issue edit "$EXISTING" --body-file issue-body.md
gh issue comment "$EXISTING" --body-file issue-body.md gh issue comment "$EXISTING" --body-file issue-body.md
echo "Commented on existing issue #$EXISTING" echo "Updated and commented on existing issue #$EXISTING"
else else
gh issue create --title "$TITLE" --label patch-break --body-file issue-body.md gh issue create --title "$TITLE" --label patch-break --body-file issue-body.md
echo "Opened new issue for mod $MOD" echo "Opened new issue for mod $MOD"

View File

@ -121,7 +121,7 @@ test_one_mod() {
if [[ $ok -eq 0 ]]; then if [[ $ok -eq 0 ]]; then
info "Reverting patches for '$mod'..." info "Reverting patches for '$mod'..."
local revert_log local revert_log
revert_log="$("$REVERT" 2>&1)" revert_log="$("$REVERT" "$mod" 2>&1)"
echo "$revert_log" echo "$revert_log"
if echo "$revert_log" | grep -qE "could not be reverted cleanly|reported an error"; then if echo "$revert_log" | grep -qE "could not be reverted cleanly|reported an error"; then
warn "revert-patches.sh reported an unclean revert for mod '$mod'" warn "revert-patches.sh reported an unclean revert for mod '$mod'"

17
debian/changelog vendored
View File

@ -1,3 +1,20 @@
pve-mod (1.0.7) stable; urgency=low
* 8d70194 fix(nag_screen): Patch typo (#318)
* 2a122ef feat(node_info) Add configurable minimum-temperature ignore threshold (#316)
* 0476410 fix(node_info): re-show sensor widgets when data becomes valid again after being hidden (#312)
* 972c9ae fix(node_info): reap zombie pve_mod_worker and orphaned collector processes (#307)
* 69e2d78 feat(node_info): UPS widget: redesign status panel with battery-charge progress bar (#310)
* bdb3f9f fix(node_info): normalize UPS data — derive ups.realpower fallback and clean invalid battery.mfr.date (#309)
* c768eac fix(nag_screen): Fix nag screen patch webui line offsets (#315)
* 8a9b756 fix(node_info): hide GPU Details and GPU Fans when no GPU is present (#311)
* f754331 fix(node_info): fix stale worker lock detection for zombie processes (#306)
* e46734c fix(node_info): return average and core temps for intel (#302)
* cee8d04 feat/fix(node_info): expose temp_unit in graphics API response & fix temperature conversion (#295)
* 8ebc2b4 feat(node_info): add GPU utilization display with color-coded thresholds for NVIDIA GPUs (#296)
-- github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Sat, 22 Aug 2026 23:24:00 +0000
pve-mod (1.0.6) stable; urgency=low pve-mod (1.0.6) stable; urgency=low
* c21b973 remove buildtest file (#288) * c21b973 remove buildtest file (#288)

View File

@ -1,3 +1,4 @@
interest-noawait /usr/share/pve-manager/js interest-noawait /usr/share/pve-manager/js
interest-noawait /usr/share/perl5/PVE interest-noawait /usr/share/perl5/PVE
interest-noawait /usr/share/pve-yew-mobile-gui interest-noawait /usr/share/pve-yew-mobile-gui
interest-noawait /usr/share/javascript/proxmox-widget-toolkit

View File

@ -3,11 +3,80 @@ A small collection of scripts and mods for Proxmox Virtual Environment (PVE)
If you find this helpful, a small donation is appreciated, [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=K8XPMSEBERH3W). If you find this helpful, a small donation is appreciated, [![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=K8XPMSEBERH3W).
## Node sensor readings view | Version | Description | Status |
|---------|-------------|--------|
| **v2** (current) | Debian package (`pve-mod`) with interactive `pve-mod-configure` wizard | Recommended |
| v1 (legacy) | Standalone bash scripts, manual install | Archived — see [Legacy section](#legacy-v1-shell-scripts) |
---
## Version 2 (beta) — Debian package
Compatibility: PVE 9.0+
TBD PICTURE
`pve-mod` is a Debian package that installs UI patches and an interactive configuration wizard.
### Mods
| Mod | Description | Dependencies |
|--------|-------------|--------------|
| [`node_info`](src/modules/node_info/readme.md) | Displays sensor readings in the node summary panel: CPU, NVMe/HDD/SSD temperatures (°C/°F), fan speeds, RAM temperatures, GPU stats (Intel/NVIDIA), UPS status, and system/motherboard info. <br> Can optionally run as background sensor daemon | - General sensors: `lm-sensors`<br>- HDD/SSD: Kernal module `drivetemp`<br>- UPS: `upsc`<br>- GPU: INTEL `intel-gpu-tools` and/or NVIDIA `nvidia-driver-* `
| [`nag_screen`](src/modules/nag_screen/readme.md) | Removes the subscription nag screen from the PVE web UI. | - |
### How it works
1. The `pve-mod` package installs the mods installation, patch files under `/usr/lib/pve-mod/`.
2. The mods main configuration file and mods can be found under `/etc/pve-mod/`.
2. Running `pve-mod-configure` prompts for which modules to enable and configuration of mods.
3. The wizard applies the selected patches to the PVE system files and restarts `pveproxy`.
### Install
Commands and mod configuration must be run as `root`.
```bash
curl -sL https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/install.sh | bash
```
Install respective mod dependencies - see Mod table.
Run `pve-mod-configure` to install/uninstall and configure mods.
#### Manual configuration changes
Configuration files are located in `/etc/pve-mod/`. After any manual edits, restart `pveproxy` to apply the changes:
```bash
systemctl restart pveproxy
```
### Uninstall
Commands and mod configuration must be run as `root`.
```bash
apt-get remove pve-mod
```
And remove dependencies needed by respective mods.
### Notes
Multiple node support requires application to be installed on all nodes with identical configuration. (untested)
---
## Legacy / v1 (shell scripts)
> **These scripts are archived.** For new installations, use the [v2 Debian package](#version-2--debian-package) above.
### Node sensor readings view
![Proxmox temp mod](https://github.com/Meliox/PVE-mods/blob/main/pve-mod-sensors.png?raw=true)
Compatibility: Compatibility:
- 9.0-9.2. Newer versions may often work - 9.0-9.2. Newer versions may often work
- Older version (7.x-8.x), use git version from Apr 6th 2025 - Older version (7.x-8.x), use git version from Apr 6th 2025
![Promxox temp mod](https://github.com/Meliox/PVE-mods/blob/main/pve-mod-sensors.png?raw=true)
This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) to display sensor readings in a flexible and readable manner. This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) to display sensor readings in a flexible and readable manner.
The following readings are possible: The following readings are possible:
@ -15,7 +84,7 @@ The following readings are possible:
- UPS information via Network Monitoring Tool - UPS information via Network Monitoring Tool
- Motherboard information or system information via dmidecode - Motherboard information or system information via dmidecode
### How it works #### How it works
The modification involves the following steps: The modification involves the following steps:
1. Backup original files in the home directory/backup 1. Backup original files in the home directory/backup
- `/usr/share/pve-manager/js/pvemanagerlib.js` - `/usr/share/pve-manager/js/pvemanagerlib.js`
@ -38,7 +107,7 @@ Notes:
- UPS support in multi-node setups require identical login credentials across nodes. This has not been fully tested. - UPS support in multi-node setups require identical login credentials across nodes. This has not been fully tested.
- Proxmox upgrades may overwrite modified files; reinstallation of this mod could be required. - Proxmox upgrades may overwrite modified files; reinstallation of this mod could be required.
### Install #### Install
Instructions be performed as 'root', as normal users do not have access to the files. Instructions be performed as 'root', as normal users do not have access to the files.
``` ```
@ -51,7 +120,7 @@ bash pve-mod-gui-sensors.sh install
``` ```
Additionally, adjustments are available in the first part of the script, where paths can be edited, cpucore offset and display information. Additionally, adjustments are available in the first part of the script, where paths can be edited, cpucore offset and display information.
## Nag screen deactivation ### Nag screen deactivation
(Tested compatibility: 7.x - 8.3.5) (Tested compatibility: 7.x - 8.3.5)
This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) which deactivates the subscription nag screen. This bash script installs a modification to the Proxmox Virtual Environment (PVE) web user interface (UI) which deactivates the subscription nag screen.
@ -65,21 +134,21 @@ The script provides three options:
| `install` | Installs the modification by applying the necessary changes. | | `install` | Installs the modification by applying the necessary changes. |
| `uninstall` | Removes the modification by restoring the original files from backups. | | `uninstall` | Removes the modification by restoring the original files from backups. |
### Install #### Install
Instructions be performed as 'root', as normal users do not have access to the files. Instructions be performed as 'root', as normal users do not have access to the files.
``` ```
wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/legacy-scripts/pve-mod-nag-screen.sh wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/legacy-scripts/pve-mod-nag-screen.sh
bash pve-mod-nag-screen.sh install bash pve-mod-nag-screen.sh install
``` ```
## Script to update all containers ### Script to update all containers
(Tested compatibility: 7.x - 8.3.5) (Tested compatibility: 7.x - 8.3.5)
This script updates all running Proxmox containers, skipping specified excluded containers, and generates a separate log file for each container. This script updates all running Proxmox containers, skipping specified excluded containers, and generates a separate log file for each container.
The script first updates the Proxmox host system, then iterates through each container, updates the container, and reboots it if necessary. The script first updates the Proxmox host system, then iterates through each container, updates the container, and reboots it if necessary.
Each container's log file is stored in $log_path and the main script log file is named container-upgrade-main.log. Each container's log file is stored in $log_path and the main script log file is named container-upgrade-main.log.
### Install #### Install
``` ```
wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/legacy-scripts/updateallcontainers.sh wget https://raw.githubusercontent.com/Meliox/PVE-mods/refs/heads/main/legacy-scripts/updateallcontainers.sh
``` ```

View File

@ -1,7 +1,7 @@
--- a/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js --- /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js.orig 2026-08-23 01:17:11.563403975 +0200
+++ b/usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js +++ /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js 2026-08-23 01:19:00.972084488 +0200
@@ -599,37 +599,8 @@ @@ -612,37 +612,8 @@
Ext.Msg.alert(gettext('Error'), res.htmlStatus || res.result.message), ),
checked_command: function (orig_cmd) { checked_command: function (orig_cmd) {
- Proxmox.Utils.API2Request({ - Proxmox.Utils.API2Request({

View File

@ -97,7 +97,9 @@ sub _get_temperature_sensors {
fans => $config{lm_sensors}{enable_fan_speed} ? \1 : \0, fans => $config{lm_sensors}{enable_fan_speed} ? \1 : \0,
hdd => $config{lm_sensors}{enable_hdd_temp} ? \1 : \0, hdd => $config{lm_sensors}{enable_hdd_temp} ? \1 : \0,
other => $config{lm_sensors}{enable_other_temp} ? \1 : \0, other => $config{lm_sensors}{enable_other_temp} ? \1 : \0,
temp_unit => $config{lm_sensors}{temp_unit}, temp_unit => $config{system_info}{temp_unit},
cpu_temp_target => $config{lm_sensors}{cpu_temp_target},
ignore_temp_below => $config{system_info}{ignore_temp_below} + 0,
data => { 'PVE MOD lm-sensors Enhanced' => $sensors_json }, data => { 'PVE MOD lm-sensors Enhanced' => $sensors_json },
}); });

View File

@ -112,6 +112,8 @@ sub _parse_upsc_output {
} }
} }
} }
_normalize_ups_data($ups_data);
}; };
if ($@) { if ($@) {
debug(__LINE__, "Error parsing upsc output: $@"); debug(__LINE__, "Error parsing upsc output: $@");
@ -122,4 +124,28 @@ sub _parse_upsc_output {
return $ups_data; return $ups_data;
} }
# ============================================================================
# UPS — data normalization
# ============================================================================
sub _normalize_ups_data {
my ($ups_data) = @_;
# Some devices report a placeholder (e.g. "OPEN") instead of a real date
if (defined $ups_data->{'battery.mfr.date'} && $ups_data->{'battery.mfr.date'} !~ m{^\d{4}[/-]\d{1,2}[/-]\d{1,2}$}) {
delete $ups_data->{'battery.mfr.date'};
}
# Derive real power draw for devices that don't report ups.realpower directly
if (!defined $ups_data->{'ups.realpower'}) {
if (defined $ups_data->{'ups.load'} && defined $ups_data->{'ups.realpower.nominal'}) {
$ups_data->{'ups.realpower'} = int((($ups_data->{'ups.load'} / 100) * $ups_data->{'ups.realpower.nominal'}) + 0.5);
} elsif (defined $ups_data->{'output.current'} && defined $ups_data->{'output.voltage'}) {
$ups_data->{'ups.realpower'} = int(($ups_data->{'output.current'} * $ups_data->{'output.voltage'}) + 0.5);
}
}
return;
}
1; 1;

View File

@ -72,7 +72,6 @@ our %config = (
enable_other_temp => 0, enable_other_temp => 0,
enable_fan_speed => 0, enable_fan_speed => 0,
display_zero_speed_fans => 0, display_zero_speed_fans => 0,
temp_unit => 'C',
}, },
ups => { ups => {
enabled => 0, enabled => 0,
@ -81,6 +80,8 @@ our %config = (
system_info => { system_info => {
enabled => 0, enabled => 0,
type => 1, # 1 = System (dmidecode -t 1), 2 = Baseboard/Motherboard (dmidecode -t 2) type => 1, # 1 = System (dmidecode -t 1), 2 = Baseboard/Motherboard (dmidecode -t 2)
temp_unit => 'C',
ignore_temp_below => 5,
}, },
paths => { paths => {
working_dir => '/run/pveproxy/pve-mod', working_dir => '/run/pveproxy/pve-mod',

View File

@ -4,7 +4,7 @@ use strict;
use warnings; use warnings;
use Exporter 'import'; use Exporter 'import';
use POSIX qw(WNOHANG); use POSIX qw(WNOHANG setsid);
use File::Path qw(remove_tree); use File::Path qw(remove_tree);
use PVE::PVEMod::Config qw( use PVE::PVEMod::Config qw(
@ -13,7 +13,7 @@ use PVE::PVEMod::Config qw(
$pve_mod_worker_lock $startup_lock $pve_mod_worker_lock $startup_lock
); );
use PVE::PVEMod::Utils qw( use PVE::PVEMod::Utils qw(
debug is_process_alive read_lock_pid debug is_process_alive get_process_ppid read_lock_pid
acquire_exclusive_lock ensure_pve_mod_directory_exists acquire_exclusive_lock ensure_pve_mod_directory_exists
check_executable startup_message check_executable startup_message
); );
@ -112,10 +112,37 @@ sub notify_pve_mod_worker {
# ============================================================================ # ============================================================================
sub _worker_lock_file_exists { sub _worker_lock_file_exists {
return -f $pve_mod_worker_lock; return 0 unless -f $pve_mod_worker_lock;
my $pid = read_lock_pid($pve_mod_worker_lock);
if (!defined $pid || $pid !~ /^(\d+)$/) {
debug(__LINE__, "Worker lock is invalid (PID: " . ($pid // 'undefined') . "), removing");
unlink($pve_mod_worker_lock);
return 0;
}
my $worker_pid = $1;
if (is_process_alive($worker_pid)) {
return 1;
}
debug(__LINE__, "Stale or zombie worker lock found for PID $worker_pid, reaping if needed");
my $reaped_pid = waitpid($worker_pid, WNOHANG);
if ($reaped_pid == $worker_pid) {
debug(__LINE__, "Reaped stale worker PID $worker_pid");
} elsif ($reaped_pid == -1 && $!{ECHILD}) {
# Expected once the worker is double-forked: we're not its parent, init is.
debug(__LINE__, "PID $worker_pid is not a child of this process; treating lock as stale");
} else {
debug(__LINE__, "waitpid on PID $worker_pid returned $reaped_pid: $!");
}
unlink($pve_mod_worker_lock);
return 0;
} }
# Forks the worker process and records its PID in the lock file. # Double-forks so the worker is reparented to init (PID 1) instead of the
# caller; init reaps it on exit, so it never lingers as a <defunct> zombie.
sub _pve_mod_worker { sub _pve_mod_worker {
debug(__LINE__, "_pve_mod_worker called"); debug(__LINE__, "_pve_mod_worker called");
@ -125,34 +152,57 @@ sub _pve_mod_worker {
print $pve_mod_worker_fh "$$\n"; print $pve_mod_worker_fh "$$\n";
close($pve_mod_worker_fh); close($pve_mod_worker_fh);
debug(__LINE__, "Forking new pve_mod_worker process"); debug(__LINE__, "Forking intermediate process for pve_mod_worker");
my $pve_mod_worker_pid = fork(); my $intermediate_pid = fork();
unless (defined $pve_mod_worker_pid) { unless (defined $intermediate_pid) {
debug(__LINE__, "Failed to fork pve_mod_worker process: $!"); debug(__LINE__, "Failed to fork intermediate pve_mod_worker process: $!");
unlink($pve_mod_worker_lock);
return; return;
} }
if ($pve_mod_worker_pid == 0) { if ($intermediate_pid == 0) {
# Child # Intermediate child: detach into its own session, fork the real
$0 = "pve_mod_worker_controller"; # worker, then exit immediately so the worker is reparented to init.
debug(__LINE__, "Child process forked, calling _pve_mod_keep_alive"); setsid();
_pve_mod_keep_alive();
exit(0); my $worker_pid = fork();
unless (defined $worker_pid) {
debug(__LINE__, "Failed to fork pve_mod_worker process: $!");
unlink($pve_mod_worker_lock);
POSIX::_exit(1);
} }
# Parent — update lock file with real child PID if ($worker_pid == 0) {
debug(__LINE__, "Forked pve_mod_worker process with PID $pve_mod_worker_pid"); # Grandchild — the actual worker
$0 = "pve_mod_worker_controller";
if (open my $fh, '>', $pve_mod_worker_lock) { if (open my $fh, '>', $pve_mod_worker_lock) {
print $fh "$pve_mod_worker_pid\n"; print $fh "$$\n";
close $fh; close $fh;
debug(__LINE__, "Wrote pve_mod_worker PID to lock file: $pve_mod_worker_lock"); debug(__LINE__, "Wrote pve_mod_worker PID to lock file: $pve_mod_worker_lock");
} else { } else {
debug(__LINE__, "Failed to write pve_mod_worker lock file: $!"); debug(__LINE__, "Failed to write pve_mod_worker lock file: $!");
kill('TERM', $pve_mod_worker_pid); }
debug(__LINE__, "Worker process forked, calling _pve_mod_keep_alive");
_pve_mod_keep_alive();
exit(0);
} }
debug(__LINE__, "Intermediate process exiting, worker PID $worker_pid reparented to init");
# _exit (not exit) so we skip END blocks/global destruction — this
# process only ever existed to perform the double fork.
POSIX::_exit(0);
}
# Parent — reap the short-lived intermediate process immediately, so it
# never has a chance to become a zombie under us either.
waitpid($intermediate_pid, 0);
my $status = $? >> 8;
if ($status == 0) {
debug(__LINE__, "pve_mod_worker process started successfully"); debug(__LINE__, "pve_mod_worker process started successfully");
} else {
debug(__LINE__, "Intermediate pve_mod_worker process exited with status $status");
}
} }
# ============================================================================ # ============================================================================
@ -199,6 +249,8 @@ sub _pve_mod_keep_alive {
exit(0); exit(0);
}; };
_reap_orphaned_collectors();
debug(__LINE__, "Worker starting all collectors"); debug(__LINE__, "Worker starting all collectors");
_initialise_sensors_collector(); _initialise_sensors_collector();
_initialise_graphics_collectors(); _initialise_graphics_collectors();
@ -451,6 +503,74 @@ sub _stop_child_collectors {
debug(__LINE__, "Cleanup complete"); debug(__LINE__, "Cleanup complete");
} }
# Finds and terminates collector processes left behind by a previous worker
# that never got to run _stop_child_collectors() (e.g. killed with SIGKILL).
# Only targets processes reparented to init, never a running worker's own.
sub _reap_orphaned_collectors {
debug(__LINE__, "Scanning for orphaned collector processes from a previous worker");
my $dh;
unless (opendir($dh, '/proc')) {
debug(__LINE__, "Failed to open /proc: $!");
return;
}
my @orphans;
while (my $entry = readdir($dh)) {
next unless $entry =~ /^(\d+)$/;
my $pid = $1;
next if $pid == $$;
next unless open(my $fh, '<', "/proc/$pid/cmdline");
my $cmdline = <$fh>;
close($fh);
next unless defined $cmdline;
my ($name) = split /\0/, $cmdline;
next unless defined $name && $name =~ /^collector-/;
my $ppid = get_process_ppid($pid);
next unless defined $ppid && $ppid == 1;
push @orphans, $pid;
}
closedir($dh);
unless (@orphans) {
debug(__LINE__, "No orphaned collectors found");
return;
}
debug(__LINE__, "Found " . scalar(@orphans) . " orphaned collector(s), terminating");
foreach my $pid (@orphans) {
kill('TERM', $pid);
debug(__LINE__, "Sent SIGTERM to orphaned collector PID $pid");
}
# Orphans are already zombies as soon as init reaps them, and kill(0,...)
# keeps reporting a zombie's PID as present - use is_process_alive() so we
# don't spin the full timeout (or send a pointless KILL) on a dead PID.
my $timeout = 2;
my $start = time();
while (time() - $start < $timeout) {
my $any_alive = 0;
foreach my $pid (@orphans) {
if (is_process_alive($pid)) { $any_alive = 1; last; }
}
last unless $any_alive;
select(undef, undef, undef, 0.1);
}
foreach my $pid (@orphans) {
if (is_process_alive($pid)) {
debug(__LINE__, "Force killing orphaned collector process $pid");
kill('KILL', $pid);
}
}
debug(__LINE__, "Orphan cleanup complete");
}
# ============================================================================ # ============================================================================
# END block — only the worker process performs cleanup # END block — only the worker process performs cleanup
# ============================================================================ # ============================================================================

View File

@ -71,6 +71,8 @@ sub _merge_graphics_files {
Intel => {}, Intel => {},
NVIDIA => {}, NVIDIA => {},
AMD => {}, AMD => {},
temp_unit => $config{system_info}{temp_unit},
ignore_temp_below => $config{system_info}{ignore_temp_below} + 0,
} }
}; };
@ -134,7 +136,7 @@ sub _load_graphics_data {
\@filepaths, \@filepaths,
$graphics_cache, $graphics_cache,
\&_merge_graphics_files, \&_merge_graphics_files,
{ Graphics => { Intel => {}, NVIDIA => {}, AMD => {} } } { Graphics => { Intel => {}, NVIDIA => {}, AMD => {}, temp_unit => $config{system_info}{temp_unit}, ignore_temp_below => $config{system_info}{ignore_temp_below} + 0 } }
); );
my $intel_count = scalar(keys %{$data->{Graphics}{Intel} // {}}); my $intel_count = scalar(keys %{$data->{Graphics}{Intel} // {}});

View File

@ -223,15 +223,16 @@ Ext.define('PVE.node.StatusView', {
} catch(e) { } catch(e) {
objValue = {}; objValue = {};
} }
this.show();
// sensors configuration // sensors configuration
const cpuTempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: objValue.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS}); const cpuTempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: value.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS});
const cpuIgnoreThreshold = cpuTempHelper.getTemp(parseFloat(value.ignore_temp_below));
const cpuKeysI = Object.keys(objValue).filter(item => String(item).startsWith('coretemp-isa-')).sort(); const cpuKeysI = Object.keys(objValue).filter(item => String(item).startsWith('coretemp-isa-')).sort();
const cpuKeysA = Object.keys(objValue).filter(item => String(item).startsWith('k10temp-pci-')).sort(); const cpuKeysA = Object.keys(objValue).filter(item => String(item).startsWith('k10temp-pci-')).sort();
const cpuKeysRpi = Object.keys(objValue).filter(item => String(item).startsWith('cpu_thermal-virtual-')).sort(); const cpuKeysRpi = Object.keys(objValue).filter(item => String(item).startsWith('cpu_thermal-virtual-')).sort();
const bINTEL = cpuKeysI.length > 0 ? true : false; const bINTEL = cpuKeysI.length > 0 ? true : false;
const INTELPackagePrefix = 'Core' == 'Core' ? 'Core ' : 'Package id'; const INTELPackagePrefix = value.cpu_temp_target == 'Core' ? 'Core ' : 'Package id';
const INTELPackageCaption = 'Core' == 'Core' ? 'Core' : 'Package'; const INTELPackageCaption = value.cpu_temp_target == 'Core' ? 'Core' : 'Package';
let AMDPackagePrefix = 'Tccd'; let AMDPackagePrefix = 'Tccd';
let AMDPackageCaption = 'CCD'; let AMDPackageCaption = 'CCD';
@ -305,7 +306,7 @@ Ext.define('PVE.node.StatusView', {
} }
}); });
if (!isNaN(tempVal)) { if (!isNaN(tempVal) && tempVal >= cpuIgnoreThreshold) {
let tempStyle = ''; let tempStyle = '';
if (!isNaN(tempMax) && tempVal >= tempMax) { if (!isNaN(tempMax) && tempVal >= tempMax) {
tempStyle = 'color: #FFC300; font-weight: bold;'; tempStyle = 'color: #FFC300; font-weight: bold;';
@ -392,9 +393,16 @@ Ext.define('PVE.node.StatusView', {
this.hide(); this.hide();
return ''; return '';
} else if (!gpuStats || !gpuStats.Graphics) { } else if (!gpuStats || !gpuStats.Graphics) {
this.hide();
return ''; return '';
} }
// Create temperature helper for GPU temperature conversion
const gpuTempHelper = Ext.create('PVE.mod.TempHelper', {
srcUnit: PVE.mod.TempHelper.CELSIUS,
dstUnit: gpuStats.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS
});
let html = '<table style="width: 100%; border-collapse: collapse; table-layout: fixed;">'; let html = '<table style="width: 100%; border-collapse: collapse; table-layout: fixed;">';
// Intel GPUs - Secondary details // Intel GPUs - Secondary details
@ -445,6 +453,15 @@ Ext.define('PVE.node.StatusView', {
let details = []; let details = [];
// GPU Utilization
if (stats.utilization && stats.utilization.gpu) {
const gpuUsage = parseInt(stats.utilization.gpu);
let gpuStyle = '';
if (gpuUsage >= 90) gpuStyle = 'color: #d9534f; font-weight: bold;';
else if (gpuUsage >= 70) gpuStyle = 'color: #f0ad4e; font-weight: bold;';
details.push(`<span style="${gpuStyle}">GPU: ${stats.utilization.gpu}%</span>`);
}
// Memory Utilization // Memory Utilization
if (stats.utilization && stats.utilization.memory) { if (stats.utilization && stats.utilization.memory) {
const memUsage = parseInt(stats.utilization.memory); const memUsage = parseInt(stats.utilization.memory);
@ -467,13 +484,21 @@ Ext.define('PVE.node.StatusView', {
// Temperature // Temperature
if (stats.temperature) { if (stats.temperature) {
const gpuTemp = gpuTempHelper.getTemp(parseFloat(stats.temperature.gpu));
const gpuIgnoreThreshold = gpuTempHelper.getTemp(parseFloat(gpuStats.ignore_temp_below));
if (gpuTemp >= gpuIgnoreThreshold) {
const tempUnit = gpuTempHelper.getUnit();
// Convert thresholds to target unit for comparison
const tempHigh = gpuTempHelper.getTemp(80);
const tempWarn = gpuTempHelper.getTemp(70);
let tempStyle = ''; let tempStyle = '';
if (stats.temperature.gpu >= 80) { if (gpuTemp >= tempHigh) {
tempStyle = 'color: red; font-weight: bold;'; tempStyle = 'color: red; font-weight: bold;';
} else if (stats.temperature.gpu >= 70) { } else if (gpuTemp >= tempWarn) {
tempStyle = 'color: #FFC300; font-weight: bold;'; tempStyle = 'color: #FFC300; font-weight: bold;';
} }
details.push(`Temp: <span style="${tempStyle}">${stats.temperature.gpu}${stats.temperature.unit}</span>`); details.push(`Temp: <span style="${tempStyle}">${Ext.util.Format.number(gpuTemp, '0')}${tempUnit}</span>`);
}
} }
// Power // Power
@ -489,9 +514,12 @@ Ext.define('PVE.node.StatusView', {
} }
html += '</table>'; html += '</table>';
return html.indexOf('<tr>') > 0 if (html.indexOf('<tr>') <= 0) {
? '<div style="padding-left: 20px; box-sizing: border-box;">' + html + '</div>' this.hide();
: ''; return '';
}
this.show();
return '<div style="padding-left: 20px; box-sizing: border-box;">' + html + '</div>';
}, },
}, },
{ {
@ -520,7 +548,9 @@ Ext.define('PVE.node.StatusView', {
} catch(e) { } catch(e) {
objValue = {}; objValue = {};
} }
const tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: objValue.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS}); this.show();
const tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: value.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS});
const ignoreThreshold = tempHelper.getTemp(parseFloat(value.ignore_temp_below));
const drvKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' })); const drvKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
let drvData = []; let drvData = [];
drvKeys.forEach((drvKey) => { drvKeys.forEach((drvKey) => {
@ -536,7 +566,7 @@ Ext.define('PVE.node.StatusView', {
tempCrit = tempHelper.getTemp(parseFloat(drv[sensorName][secondLevelKey])); tempCrit = tempHelper.getTemp(parseFloat(drv[sensorName][secondLevelKey]));
} }
}); });
if (!isNaN(tempVal)) { if (!isNaN(tempVal) && tempVal >= ignoreThreshold) {
let tempStyle = ''; let tempStyle = '';
if (!isNaN(tempMax) && tempVal >= tempMax) { if (!isNaN(tempMax) && tempVal >= tempMax) {
tempStyle = 'color: #FFC300; font-weight: bold;'; tempStyle = 'color: #FFC300; font-weight: bold;';
@ -600,7 +630,9 @@ Ext.define('PVE.node.StatusView', {
} catch(e) { } catch(e) {
objValue = {}; objValue = {};
} }
const tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: objValue.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS}); this.show();
const tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: value.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS});
const ignoreThreshold = tempHelper.getTemp(parseFloat(value.ignore_temp_below));
const nvmeKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort(); const nvmeKeys = Object.keys(objValue).filter(item => String(item).startsWith(addressPrefix)).sort();
let nvmeData = []; let nvmeData = [];
nvmeKeys.forEach((nvmeKey, index) => { nvmeKeys.forEach((nvmeKey, index) => {
@ -618,7 +650,7 @@ Ext.define('PVE.node.StatusView', {
model = objValue[nvmeKey]['model'] || 'Unknown'; model = objValue[nvmeKey]['model'] || 'Unknown';
serial = objValue[nvmeKey]['serial'] || ''; serial = objValue[nvmeKey]['serial'] || '';
if (!isNaN(tempVal)) { if (!isNaN(tempVal) && tempVal >= ignoreThreshold) {
let tempStyle = ''; let tempStyle = '';
if (!isNaN(tempMax) && tempVal >= tempMax) { if (!isNaN(tempMax) && tempVal >= tempMax) {
tempStyle = 'color: #FFC300; font-weight: bold;'; tempStyle = 'color: #FFC300; font-weight: bold;';
@ -688,7 +720,9 @@ Ext.define('PVE.node.StatusView', {
} catch(e) { } catch(e) {
objValue = {}; objValue = {};
} }
const tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: objValue.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS}); this.show();
const tempHelper = Ext.create('PVE.mod.TempHelper', {srcUnit: PVE.mod.TempHelper.CELSIUS, dstUnit: value.temp_unit === 'F' ? PVE.mod.TempHelper.FAHRENHEIT : PVE.mod.TempHelper.CELSIUS});
const ignoreThreshold = tempHelper.getTemp(parseFloat(value.ignore_temp_below));
// Keep only keys that do not belong to known categories // Keep only keys that do not belong to known categories
const otherKeys = Object.keys(objValue).filter(key => const otherKeys = Object.keys(objValue).filter(key =>
@ -723,7 +757,7 @@ Ext.define('PVE.node.StatusView', {
} }
}); });
if (!isNaN(tempVal)) { if (!isNaN(tempVal) && tempVal >= ignoreThreshold) {
let tempStyle = ''; let tempStyle = '';
if (!isNaN(tempMax) && tempVal >= tempMax) { if (!isNaN(tempMax) && tempVal >= tempMax) {
tempStyle = 'color: #FFC300; font-weight: bold;'; tempStyle = 'color: #FFC300; font-weight: bold;';
@ -788,6 +822,7 @@ Ext.define('PVE.node.StatusView', {
} catch(e) { } catch(e) {
objValue = {}; objValue = {};
} }
this.show();
// Recursive function to find fan keys and values // Recursive function to find fan keys and values
function findFanKeys(obj, fanKeys, parentKey = null) { function findFanKeys(obj, fanKeys, parentKey = null) {
@ -846,6 +881,7 @@ Ext.define('PVE.node.StatusView', {
this.hide(); this.hide();
return ''; return '';
} else if (!gpuStats || !gpuStats.Graphics || !gpuStats.Graphics.NVIDIA) { } else if (!gpuStats || !gpuStats.Graphics || !gpuStats.Graphics.NVIDIA) {
this.hide();
return ''; return '';
} }
@ -873,8 +909,10 @@ Ext.define('PVE.node.StatusView', {
}); });
if (rows.length === 0) { if (rows.length === 0) {
this.hide();
return ''; return '';
} }
this.show();
return '<div style="padding-left: 20px; box-sizing: border-box;"><table style="width: 100%; border-collapse: collapse; table-layout: fixed;">' + rows.join('') + '</table></div>'; return '<div style="padding-left: 20px; box-sizing: border-box;"><table style="width: 100%; border-collapse: collapse; table-layout: fixed;">' + rows.join('') + '</table></div>';
}, },
@ -882,187 +920,294 @@ Ext.define('PVE.node.StatusView', {
{ {
itemId: 'upsc', itemId: 'upsc',
colspan: 2, colspan: 2,
printBar: false,
title: gettext('UPS Status'), title: gettext('UPS Status'),
iconCls: 'fa fa-fw fa-battery-three-quarters', iconCls: 'fa fa-fw fa-battery-three-quarters',
textField: 'PveMod_upsInfo', valueField: 'PveMod_upsInfo',
printBar: true,
warningThreshold: 1.1,
criticalThreshold: 1.2,
layout: {
type: 'vbox',
align: 'stretch',
},
items: [
{
xtype: 'component',
itemId: 'label',
data: {
title: '',
usage: '',
iconCls: undefined,
},
tpl: [
'<div class="left-aligned">',
'<tpl if="iconCls">',
'<i class="{iconCls}"></i> ',
'</tpl>',
'{title}</div>',
],
},
{
xtype: 'container',
layout: {
type: 'hbox',
align: 'middle',
},
items: [
{
xtype: 'component',
itemId: 'usageText',
flex: 1,
minWidth: 0,
margin: '0 16 0 0',
},
{
xtype: 'container',
flex: 1,
minWidth: 0,
layout: {
type: 'vbox',
align: 'stretch',
},
items: [
{
xtype: 'component',
itemId: 'loadText',
margin: '0 0 2 0',
},
{
xtype: 'progressbar',
itemId: 'progress',
height: 5,
value: 0,
animate: true,
},
],
},
],
},
],
initComponent: function() {
var me = this;
if (!me.title) {
throw 'no title defined';
}
Ext.container.Container.prototype.initComponent.call(me);
var progress = me.down('#progress');
if (progress) {
progress.setVisible(!!me.printBar);
}
me.updateValue(me.text, me.value);
me.setIconCls(me.iconCls);
},
setPrintBar: function(enable) {
var me = this;
me.printBar = enable;
var progress = me.down('#progress');
if (progress) {
progress.setVisible(enable);
}
},
// Single source of truth for all DOM writes — called once per refresh
// cycle, right after renderer() returns. renderer() only computes data;
// it never touches the DOM, so there's no race/flash between the two.
updateValue: function(text, usage) {
var me = this;
var loadText = me._pendingLoadText || '';
if (me.lastText === text && me.lastUsage === usage && me.lastLoadText === loadText) {
return;
}
me.lastText = text;
me.lastUsage = usage;
me.lastLoadText = loadText;
var label = me.getComponent('label');
if (label) {
label.update(Ext.apply(label.data, { title: me.title, usage: '' }));
}
var usageText = me.down('#usageText');
if (usageText) {
if (usageText.setHtml) {
usageText.setHtml(text || '');
} else {
usageText.update(text || '');
}
}
var loadTextCmp = me.down('#loadText');
if (loadTextCmp) {
if (loadTextCmp.setHtml) {
loadTextCmp.setHtml(loadText);
} else {
loadTextCmp.update(loadText);
}
}
var progressBar = me.down('#progress');
if (usage !== undefined && me.printBar && Ext.isNumeric(usage) && usage >= 0 && progressBar) {
progressBar.updateProgress(usage, '');
if (usage > me.criticalThreshold) {
progressBar.removeCls('warning');
progressBar.addCls('critical');
} else if (usage > me.warningThreshold) {
progressBar.removeCls('critical');
progressBar.addCls('warning');
} else {
progressBar.removeCls('warning');
progressBar.removeCls('critical');
}
}
},
// Drives the progress bar's fill — based on battery.charge.
calculate: function(used) {
if (!used || used.disabled === true || typeof used !== 'object') {
return 0;
}
let charge = NaN;
Object.keys(used).forEach(function(k) {
const row = used[k];
if (row && typeof row === 'object' && row['battery.charge'] != null) {
charge = parseFloat(row['battery.charge']);
}
});
if (isNaN(charge)) {
return 0;
}
return Math.max(0, Math.min(1, charge / 100));
},
// Pure computation — no DOM writes. Stashes the "Battery capacity ...
// X% (Runtime: ...)" line for updateValue to place above the bar, and
// returns a 30/70 table (model | other info incl. Load) wrapped in the
// standard indent div, matching the other widgets in this panel.
renderer: function(value) { renderer: function(value) {
let objValue; let objValue;
try { try {
objValue = value || {}; objValue = value || {};
} catch(e) { } catch (e) {
objValue = {}; objValue = {};
} }
if (objValue.disabled === true) { if (objValue.disabled === true) {
this.hide(); this.hide();
this.setPrintBar(false);
return ''; return '';
} else if (!objValue || Object.keys(objValue).length === 0) {
return 'N/A';
} }
this.show();
// Helper function to get status color const upsKeys = Object.keys(objValue).filter(function(k) {
function getStatusColor(status) { return objValue[k] && typeof objValue[k] === 'object' && !Array.isArray(objValue[k]);
if (!status) return '#999'; });
const statusUpper = status.toUpperCase(); if (!upsKeys.length) {
if (statusUpper.includes('OL')) return null; this.hide();
if (statusUpper.includes('OB')) return '#d9534f'; this.setPrintBar(false);
if (statusUpper.includes('LB')) return '#d9534f'; return '';
return '#f0ad4e';
} }
this.show();
this.setPrintBar(true);
// Helper function to get load/charge color
function getPercentageColor(value, isLoad = false) {
if (!value || isNaN(value)) return '#999';
const num = parseFloat(value);
if (isLoad) {
if (num >= 80) return '#d9534f';
if (num >= 60) return '#f0ad4e';
return null;
} else {
if (num <= 20) return '#d9534f';
if (num <= 50) return '#f0ad4e';
return null;
}
}
// Helper function to format runtime
function formatRuntime(seconds) { function formatRuntime(seconds) {
if (!seconds || isNaN(seconds)) return 'N/A'; const s = parseInt(seconds, 10);
const mins = Math.floor(seconds / 60); if (!s || isNaN(s)) {
const secs = seconds % 60; return null;
return `${mins}m ${secs}s`; }
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
if (h > 0) {
return h + 'h ' + m + 'm';
}
return m + 'm';
} }
// Process each UPS in the data function statusText(upsStatus) {
let allDisplayItems = []; const u = String(upsStatus || '').toUpperCase();
if (u.indexOf('LB') >= 0) {
return { text: 'Low Battery', color: '#d9534f' };
}
if (u.indexOf('OB') >= 0) {
return { text: 'On Battery', color: '#d9534f' };
}
if (u.indexOf('FSD') >= 0) {
return { text: 'Shutdown', color: '#d9534f' };
}
if (u.indexOf('OL') >= 0) {
return {
text: u.indexOf('CHRG') >= 0 ? 'Online, charging' : 'Online',
color: null,
};
}
return { text: upsStatus || 'Unknown', color: '#f0ad4e' };
}
Object.keys(objValue).forEach(upsKey => { function colorize(label, color) {
const upsData = objValue[upsKey]; if (!color) {
return label;
}
return '<span style="color:' + color + ';">' + label + '</span>';
}
// Extract key UPS information let aboveBarText = '';
const batteryCharge = upsData['battery.charge']; const rows = [];
const batteryRuntime = upsData['battery.runtime'];
const inputVoltage = upsData['input.voltage']; upsKeys.forEach(function(upsKey) {
const upsLoad = upsData['ups.load']; const upsData = objValue[upsKey] || {};
const upsStatus = upsData['ups.status']; const charge = parseFloat(upsData['battery.charge']);
const upsModel = upsData['ups.model'] || upsData['device.model']; const runtime = formatRuntime(upsData['battery.runtime']);
const inputVoltage = parseFloat(upsData['input.voltage']);
const load = parseFloat(upsData['ups.load']);
const watts = parseFloat(upsData['ups.realpower']);
const model = upsData['ups.model'] || upsData['device.model'] || upsKey;
const st = statusText(upsData['ups.status']);
const testResult = upsData['ups.test.result']; const testResult = upsData['ups.test.result'];
const batteryChargeLow = upsData['battery.charge.low']; const manufacturingDate = upsData['battery.mfr.date'];
const batteryRuntimeLow = upsData['battery.runtime.low'];
const upsRealPowerNominal = upsData['ups.realpower.nominal'];
const batteryMfrDate = upsData['battery.mfr.date'];
// Main status line with all metrics // Above the bar: "Battery capacity" on the left, charge% (Runtime: ...) on the right.
let statusLine = ''; let rightSide = !isNaN(charge) ? (Math.round(charge) + '%') : '';
if (runtime) {
rightSide += (rightSide ? ' ' : '') + '(Runtime: ' + runtime + ' left)';
}
aboveBarText =
'<div style="display: flex; justify-content: space-between; gap: 8px;">' +
'<span>Battery capacity</span>' +
'<span style="text-align: right;">' + rightSide + '</span>' +
'</div>';
// Status // General information table: Status, Output, Input, Load, Test.
if (upsStatus) { const infoBits = [];
const statusUpper = upsStatus.toUpperCase(); if (st.text) {
let statusText = 'Unknown'; infoBits.push('Status: ' + colorize(st.text, st.color));
let statusColor = '#f0ad4e'; }
if (!isNaN(watts)) {
if (statusUpper.includes('OL')) { infoBits.push('Output: ' + Math.round(watts) + 'W');
statusText = 'Online'; }
statusColor = null; if (!isNaN(inputVoltage)) {
} else if (statusUpper.includes('OB')) { const places = inputVoltage >= 50 ? 0 : 1;
statusText = 'On Battery'; infoBits.push('Input: ' + inputVoltage.toFixed(places) + ' V');
statusColor = '#d9534f'; }
} else if (statusUpper.includes('LB')) { if (!isNaN(load)) {
statusText = 'Low Battery'; infoBits.push('Load: ' + Math.round(load) + '%');
statusColor = '#d9534f'; }
} else { if (manufacturingDate) {
statusText = upsStatus; infoBits.push('Manufacturing Date: ' + manufacturingDate);
statusColor = '#f0ad4e'; }
if (testResult) {
infoBits.push('Test: ' + testResult);
} }
let statusStyle = statusColor ? ('color: ' + statusColor + ';') : ''; rows.push(
statusLine += 'Status: <span style="' + statusStyle + '">' + statusText + '</span>'; '<tr>' +
} else { '<td style="padding: 2px 10px 2px 0; text-align: left; width: 30%; vertical-align: top; overflow-wrap: anywhere; word-break: break-word;">' + model + '</td>' +
statusLine += 'Status: <span>N/A</span>'; '<td style="padding: 2px 0 2px 10px; text-align: right; width: 70%; vertical-align: top; overflow-wrap: anywhere; word-break: break-word; white-space: normal;">' + infoBits.join(' | ') + '</td>' +
} '</tr>'
);
// Battery charge
if (statusLine) statusLine += ' | ';
if (batteryCharge) {
const chargeColor = getPercentageColor(batteryCharge, false);
let chargeStyle = chargeColor ? ('color: ' + chargeColor + ';') : '';
statusLine += 'Battery: <span style="' + chargeStyle + '">' + batteryCharge + '%</span>';
} else {
statusLine += 'Battery: <span>N/A</span>';
}
// Load percentage
if (statusLine) statusLine += ' | ';
if (upsLoad) {
const loadColor = getPercentageColor(upsLoad, true);
let loadStyle = loadColor ? ('color: ' + loadColor + ';') : '';
statusLine += 'Load: <span style="' + loadStyle + '">' + upsLoad + '%</span>';
} else {
statusLine += 'Load: <span>N/A</span>';
}
// Runtime
if (statusLine) statusLine += ' | ';
if (batteryRuntime) {
const runtime = parseInt(batteryRuntime);
const runtimeLowThreshold = batteryRuntimeLow ? parseInt(batteryRuntimeLow) : 600;
let runtimeColor = null;
if (runtime <= runtimeLowThreshold / 2) runtimeColor = '#d9534f';
else if (runtime <= runtimeLowThreshold) runtimeColor = '#f0ad4e';
let runtimeStyle = runtimeColor ? ('color: ' + runtimeColor + ';') : '';
statusLine += 'Runtime: <span style="' + runtimeStyle + '">' + formatRuntime(runtime) + '</span>';
} else {
statusLine += 'Runtime: <span>N/A</span>';
}
// Input voltage
if (statusLine) statusLine += ' | ';
if (inputVoltage) {
statusLine += 'Input: <span>' + parseFloat(inputVoltage).toFixed(0) + 'V</span>';
} else {
statusLine += 'Input: <span>N/A</span>';
}
// Calculate actual watt usage
if (statusLine) statusLine += ' | ';
let actualWattage = null;
if (upsLoad && upsRealPowerNominal) {
const load = parseFloat(upsLoad);
const nominal = parseFloat(upsRealPowerNominal);
if (!isNaN(load) && !isNaN(nominal)) {
actualWattage = Math.round((load / 100) * nominal);
}
}
// Real power (calculated watt usage)
if (actualWattage !== null) {
statusLine += 'Output: <span>' + actualWattage + 'W</span>';
} else {
statusLine += 'Output: <span>N/A</span>';
}
// Append battery MFD + last test to the same line (single-line UPS summary)
statusLine += ' | Battery MFD: ' + (batteryMfrDate || 'N/A');
if (testResult && !testResult.toLowerCase().includes('no test')) {
const testColor = testResult.toLowerCase().includes('passed') ? null : '#d9534f';
let testStyle = testColor ? ('color: ' + testColor + ';') : '';
statusLine += ' | <span style="' + testStyle + '">Test: ' + testResult + '</span>';
} else {
statusLine += ' | Test: N/A';
}
// Build UPS display with model on left, details on right
let upsHtml = '<tr>';
upsHtml += '<td style="padding: 2px 10px 2px 0; text-align: left; width: 30%; vertical-align: top; overflow-wrap: anywhere; word-break: break-word;">' + (upsModel || upsKey) + '</td>';
upsHtml += '<td style="padding: 2px 0 2px 10px; text-align: right; width: 70%; vertical-align: top; overflow-wrap: anywhere; word-break: break-word; white-space: normal;">' + statusLine + '</td>';
upsHtml += '</tr>';
allDisplayItems.push(upsHtml);
}); });
// Format the final output for all UPS devices // Stash for updateValue to consume — no DOM writes here.
return '<div style="padding-left: 20px; box-sizing: border-box;"><table style="width: 100%; border-collapse: collapse; table-layout: fixed;">' + allDisplayItems.join('') + '</table></div>'; this._pendingLoadText = aboveBarText;
// Becomes the `text` argument passed to updateValue (left column table).
return '<div style="padding-left: 20px; box-sizing: border-box;"><table style="width: 100%; border-collapse: collapse; table-layout: fixed;">' + rows.join('') + '</table></div>';
} }
}, },
{ {
@ -1132,6 +1277,7 @@ Ext.define('PVE.node.StatusView', {
this.hide(); this.hide();
return ''; return '';
} }
this.show();
const titleMap = { const titleMap = {
manufacturer: 'Manufacturer', manufacturer: 'Manufacturer',

View File

@ -15,6 +15,7 @@ our @EXPORT_OK = qw(
debug debug
read_sysfs read_sysfs
is_process_alive is_process_alive
get_process_ppid
read_lock_pid read_lock_pid
acquire_exclusive_lock acquire_exclusive_lock
ensure_pve_mod_directory_exists ensure_pve_mod_directory_exists
@ -91,7 +92,33 @@ sub read_sysfs {
sub is_process_alive { sub is_process_alive {
my ($pid) = @_; my ($pid) = @_;
return -d "/proc/$pid"; return 0 unless -d "/proc/$pid";
# A zombie still has /proc/<pid> but is dead - otherwise a stale
# worker lock is never cleaned up.
if (open my $fh, '<', "/proc/$pid/stat") {
my $line = <$fh>;
close $fh;
return 0 if defined $line && $line =~ /\)\s+Z\s/;
}
return 1;
}
# Returns the parent PID of $pid, or undef if it can't be determined.
sub get_process_ppid {
my ($pid) = @_;
return undef unless open my $fh, '<', "/proc/$pid/stat";
my $line = <$fh>;
close $fh;
return undef unless defined $line;
# comm field (2nd, in parens) can itself contain ')', so split on the
# last one before parsing the remaining space-separated fields.
my $last_paren = rindex($line, ')');
return undef if $last_paren < 0;
my @fields = split ' ', substr($line, $last_paren + 1);
return undef unless defined $fields[1] && $fields[1] =~ /^(\d+)$/;
return $1;
} }
sub read_lock_pid { sub read_lock_pid {

View File

@ -17,7 +17,6 @@ enable_hdd_temp=0
enable_nvme_temp=0 enable_nvme_temp=0
enable_fan_speed=0 enable_fan_speed=0
display_zero_speed_fans=0 display_zero_speed_fans=0
temp_unit=C
[ups] [ups]
enabled=0 enabled=0
@ -26,6 +25,8 @@ device_name=ups@localhost
[system_info] [system_info]
enabled=0 enabled=0
type=1 type=1
temp_unit=C
ignore_temp_below=5
# Debug mode: when a collector's mode is 1, the real tool is not required. # Debug mode: when a collector's mode is 1, the real tool is not required.
# Data is read from the file path instead. Useful for development/testing. # Data is read from the file path instead. Useful for development/testing.

View File

@ -65,11 +65,11 @@ node_info_defaults() {
LM_SENSORS_ENABLED=0 LM_SENSORS_ENABLED=0
ENABLE_CPU=0; CPU_TEMP_TARGET="Core" ENABLE_CPU=0; CPU_TEMP_TARGET="Core"
ENABLE_RAM_TEMP=0; ENABLE_HDD_TEMP=0; ENABLE_NVME_TEMP=0; ENABLE_OTHER_TEMP=0 ENABLE_RAM_TEMP=0; ENABLE_HDD_TEMP=0; ENABLE_NVME_TEMP=0; ENABLE_OTHER_TEMP=0
ENABLE_FAN_SPEED=0; DISPLAY_ZERO_SPEED_FANS=0; TEMP_UNIT="C" ENABLE_FAN_SPEED=0; DISPLAY_ZERO_SPEED_FANS=0
ENABLE_INTEL_GPU_INFO=0; ENABLE_NVIDIA_GPU_INFO=0; ENABLE_AMD_GPU_INFO=0 ENABLE_INTEL_GPU_INFO=0; ENABLE_NVIDIA_GPU_INFO=0; ENABLE_AMD_GPU_INFO=0
ENABLE_GPU_HISTORY=0 ENABLE_GPU_HISTORY=0
ENABLE_UPS=0; UPS_DEVICE_NAME="ups@localhost" ENABLE_UPS=0; UPS_DEVICE_NAME="ups@localhost"
ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1 ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1; TEMP_UNIT="C"; IGNORE_TEMP_BELOW=5
DEBUG_LM_SENSORS=0; DEBUG_LM_SENSORS_FILE="/tmp/sensors-output.json" DEBUG_LM_SENSORS=0; DEBUG_LM_SENSORS_FILE="/tmp/sensors-output.json"
DEBUG_INTEL=0; DEBUG_INTEL_FILE="/tmp/intel-gpu-devices.txt" DEBUG_INTEL=0; DEBUG_INTEL_FILE="/tmp/intel-gpu-devices.txt"
DEBUG_INTEL_OUTPUT_FILE="/tmp/intel-gpu-top-output.txt" DEBUG_INTEL_OUTPUT_FILE="/tmp/intel-gpu-top-output.txt"
@ -104,11 +104,12 @@ node_info_load_conf() {
lm_sensors.enable_other_temp) ENABLE_OTHER_TEMP="$val" ;; lm_sensors.enable_other_temp) ENABLE_OTHER_TEMP="$val" ;;
lm_sensors.enable_fan_speed) ENABLE_FAN_SPEED="$val" ;; lm_sensors.enable_fan_speed) ENABLE_FAN_SPEED="$val" ;;
lm_sensors.display_zero_speed_fans) DISPLAY_ZERO_SPEED_FANS="$val" ;; lm_sensors.display_zero_speed_fans) DISPLAY_ZERO_SPEED_FANS="$val" ;;
lm_sensors.temp_unit) TEMP_UNIT="$val" ;;
ups.enabled) ENABLE_UPS="$val" ;; ups.enabled) ENABLE_UPS="$val" ;;
ups.device_name) UPS_DEVICE_NAME="$val" ;; ups.device_name) UPS_DEVICE_NAME="$val" ;;
system_info.enabled) ENABLE_SYSTEM_INFO="$val" ;; system_info.enabled) ENABLE_SYSTEM_INFO="$val" ;;
system_info.type) SYSTEM_INFO_TYPE="$val" ;; system_info.type) SYSTEM_INFO_TYPE="$val" ;;
system_info.temp_unit) TEMP_UNIT="$val" ;;
system_info.ignore_temp_below) IGNORE_TEMP_BELOW="$val" ;;
debug.lm_sensors_mode) DEBUG_LM_SENSORS="$val" ;; debug.lm_sensors_mode) DEBUG_LM_SENSORS="$val" ;;
debug.lm_sensors_output_file) DEBUG_LM_SENSORS_FILE="$val" ;; debug.lm_sensors_output_file) DEBUG_LM_SENSORS_FILE="$val" ;;
debug.intel_mode) DEBUG_INTEL="$val" ;; debug.intel_mode) DEBUG_INTEL="$val" ;;
@ -133,11 +134,11 @@ node_info_configure() {
LM_SENSORS_ENABLED=0 LM_SENSORS_ENABLED=0
ENABLE_CPU=0; CPU_TEMP_TARGET="Core" ENABLE_CPU=0; CPU_TEMP_TARGET="Core"
ENABLE_RAM_TEMP=0; ENABLE_HDD_TEMP=0; ENABLE_NVME_TEMP=0; ENABLE_OTHER_TEMP=0 ENABLE_RAM_TEMP=0; ENABLE_HDD_TEMP=0; ENABLE_NVME_TEMP=0; ENABLE_OTHER_TEMP=0
ENABLE_FAN_SPEED=0; DISPLAY_ZERO_SPEED_FANS=0; TEMP_UNIT="C" ENABLE_FAN_SPEED=0; DISPLAY_ZERO_SPEED_FANS=0
ENABLE_INTEL_GPU_INFO=0; ENABLE_NVIDIA_GPU_INFO=0; ENABLE_AMD_GPU_INFO=0 ENABLE_INTEL_GPU_INFO=0; ENABLE_NVIDIA_GPU_INFO=0; ENABLE_AMD_GPU_INFO=0
ENABLE_GPU_HISTORY=0 ENABLE_GPU_HISTORY=0
ENABLE_UPS=0; UPS_DEVICE_NAME="ups@localhost" ENABLE_UPS=0; UPS_DEVICE_NAME="ups@localhost"
ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1 ENABLE_SYSTEM_INFO=0; SYSTEM_INFO_TYPE=1; TEMP_UNIT="C"; IGNORE_TEMP_BELOW=5
local lm_sensors_ok=false local lm_sensors_ok=false
local sensors_detected=false local sensors_detected=false
@ -294,6 +295,26 @@ node_info_configure() {
[fF]) TEMP_UNIT="F"; info "Using Fahrenheit." ;; [fF]) TEMP_UNIT="F"; info "Using Fahrenheit." ;;
*) TEMP_UNIT="C"; info "Using Celsius." ;; *) TEMP_UNIT="C"; info "Using Celsius." ;;
esac esac
msgb "\n=== Ignore threshold ==="
local default_c=5 default_display entered
if [[ "$TEMP_UNIT" == "F" ]]; then
default_display=$(awk -v c="$default_c" 'BEGIN{printf "%.0f", c*9/5+32}')
else
default_display="$default_c"
fi
while true; do
entered=$(ask "Hide temperature readings below this value (°${TEMP_UNIT}) [${default_display}]")
[[ -z "$entered" ]] && entered="$default_display"
[[ "$entered" =~ ^-?[0-9]+(\.[0-9]+)?$ ]] && break
warn "Invalid number, please try again."
done
if [[ "$TEMP_UNIT" == "F" ]]; then
IGNORE_TEMP_BELOW=$(awk -v f="$entered" 'BEGIN{printf "%.1f", (f-32)*5/9}')
else
IGNORE_TEMP_BELOW="$entered"
fi
info "Temperature readings below ${entered}°${TEMP_UNIT} will be hidden."
fi fi
#endregion Temperature unit #endregion Temperature unit
fi fi
@ -478,7 +499,6 @@ enable_nvme_temp=${ENABLE_NVME_TEMP}
enable_other_temp=${ENABLE_OTHER_TEMP} enable_other_temp=${ENABLE_OTHER_TEMP}
enable_fan_speed=${ENABLE_FAN_SPEED} enable_fan_speed=${ENABLE_FAN_SPEED}
display_zero_speed_fans=${DISPLAY_ZERO_SPEED_FANS} display_zero_speed_fans=${DISPLAY_ZERO_SPEED_FANS}
temp_unit=${TEMP_UNIT}
[ups] [ups]
enabled=${ENABLE_UPS} enabled=${ENABLE_UPS}
@ -487,6 +507,8 @@ device_name=${UPS_DEVICE_NAME}
[system_info] [system_info]
enabled=${ENABLE_SYSTEM_INFO} enabled=${ENABLE_SYSTEM_INFO}
type=${SYSTEM_INFO_TYPE} type=${SYSTEM_INFO_TYPE}
temp_unit=${TEMP_UNIT}
ignore_temp_below=${IGNORE_TEMP_BELOW}
# Debug mode: when a collector's mode is 1, the real tool is not required. # Debug mode: when a collector's mode is 1, the real tool is not required.
# Data is read from the file path instead. Useful for development/testing. # Data is read from the file path instead. Useful for development/testing.