webpack.config.js requires terser-webpack-plugin, but the package was
never declared in package.json. It only resolved through npm hoisting,
because webpack itself depended on it up to 5.107.2.
webpack 5.110.3 replaced that internal dependency with
minimizer-webpack-plugin, so the phantom dependency disappeared from
node_modules and `make webui` failed before the build even started:
Cannot find module 'terser-webpack-plugin'
at Object.<anonymous> (glances/outputs/static/webpack.config.js:5:29)
Declare it in devDependencies so the config no longer depends on
another package's transitive dependency tree.
Also includes the JS dependency bumps from `make webui-update` and the
regenerated bundles.
get_ip_address() returned the address of the first up, non-loopback
interface psutil happened to enumerate. On hosts running Docker, VMware,
libvirt or similar, a virtual bridge (docker0, vmnet8, br-*...) can occupy
that position and silently win, so the IP plugin reports a bridge address
as the primary IP. Which interface wins depends on interface creation
order, so the symptom can appear after something as routine as a
NetworkManager restart recreating a bond.
Instead, ask the OS routing table which source address it would use for
default-route traffic: connect() on a SOCK_DGRAM socket resolves the route
without sending any packet, honouring metrics, policy routing and
per-route source hints, and works on every platform Glances supports. The
probe address is from the documentation range (RFC 5737 / RFC 3849) and is
never contacted. A loopback or unspecified probe result (hosts that
locally blackhole bogon ranges) is rejected. The previous interface scan
is kept as the fallback, and the existing tests now pin exactly that
fallback behaviour.
This also improves servers_list_dynamic.py, which uses get_ip_address()
for the zeroconf bind address and could previously announce a virtual
bridge address to LAN peers.
Verified on a host with 22 interfaces (bond, 3 docker bridges, 2 vmware
vmnets, 10 veths): returns the bond address for IPv4 and the global
address for IPv6, independent of enumeration order.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
getIOWaitAlert passed cpu.system to the iowait threshold lookup, so the
iowait column was decorated from a completely different field. The curses
UI colours each per-CPU stat with self.get_alert(cpu[stat], header=stat) —
every stat by its own value — so the web UI disagreed with it.
With the shipped defaults (iowait 50/70/90) that means a CPU pinned at 95%
iowait with low system time shows no alert at all, and a CPU at 95% system
with 2% iowait paints the iowait cell red.
Also fixes the column header, which read "steel" instead of "steal".
Bundle rebuilt: exactly 7 bytes change in public/glances.js, the six of
system -> iowait and the one of steel -> steal.
`plugin-vms.vue` decided the status colour inline:
:class="vm.status == 'stopped' ? 'careful' : 'ok'"
`VmsPlugin.vm_alert()` — which the TUI calls directly at
`vms/__init__.py:271` — classifies three states the WebUI had never heard of:
running -> OK
starting, restarting, delayed shutdown -> WARNING
everything else -> INFO
So the only status the WebUI recognised was `stopped`, and **every other
non-running state fell through to "ok"**. A VM that is starting, restarting,
or in delayed shutdown rendered green in the browser while the TUI showed
WARNING — a VM stuck restarting looked healthy. `stopped` was also
over-reported as CAREFUL where the plugin says INFO.
`getStatusClass` now mirrors `vm_alert`, and follows the shape
`plugin-containers.vue` already uses for its equivalent (`container_alert`),
including returning `info` for the fallback.
Evidence, since the WebUI has no JS test runner: the string
`delayed shutdown` goes 0 -> 1 occurrences in the rebuilt bundle. It was
absent entirely before, which is the point — that state had no representation
in the WebUI at all.
`npx eslint` on the component: 0 errors. Its 5 warnings sit at lines 9/15/19/24
(header attributes) and 69 (the props block); this change touches line 35 and
adds a methods block, so none of them are new.
getMemoryPercentAlert passed process.cpu_percent to the memory threshold
lookup, so the MEM% column in the process list was decorated from CPU
usage. The curses UI colours it correctly, from p['memory_percent'] with
header="mem", so the two UIs disagreed about the same process table.
With the shipped defaults (mem 50/70/90) a process holding 94% of RAM
while idle shows no alert, and a busy loop with a 0.4% footprint paints
its MEM% cell red. Both the list and the tree render path go through this
one helper.
Bundle rebuilt. Undoing just this identifier in the rebuilt public/glances.js
makes it byte-identical to the committed one, so nothing else rode along.
`plugin-amps.vue` disagreed with `AmpsPlugin.get_alert()`, which is the
authority — the TUI calls it directly at `amps/__init__.py:114`.
Two divergences:
1. An out-of-range process count rendered "careful"; the plugin returns
WARNING. Those are different levels with different colours in style.scss
(`.careful` #295183, `.warning` #5D4062), so an AMP outside its configured
bounds was under-reported in the WebUI relative to the TUI.
2. With a count of 0 the WebUI returned "critical" for any non-null
`countmin`, including `countmin=0`. The plugin defaults an unset bound to
the observed count and then returns OK when `int(countmin) == 0`, so a
configured minimum of zero means zero processes is fine. The plugin
supports that value explicitly; the WebUI flagged it red.
The range check itself already matched and is unchanged in behaviour — it is
only rewritten to default the bounds the way the plugin does, rather than
short-circuiting on null, so the two read alike.
Not mirrored: the plugin's `nbprocess is None -> OK` guard. `count` is
initialised to 0 in `amps/amp.py` and only ever assigned a real count, so that
branch is unreachable from the API and adding it would be dead code.
Evidence, since the WebUI has no JS test runner: in the rebuilt bundle the
`careful` occurrences drop 18 -> 17. `npx eslint` on the component reports
0 errors (the two warnings, `vue/require-default-prop` and `vue/no-v-html`,
are on untouched code and predate this change).
`plugin-ports.vue` disagreed with the ports plugin on two counts. Both were
verified against a live `/api/4/ports` response rather than inferred.
1. URL rows were decorated by `getPortDecoration`. `getWebDecoration` was
defined in the same file and never called.
The port form compares `status` against an rtt threshold, but a URL's
status is an HTTP code or the string 'Error'. With the default
`rtt_warning: null` every URL therefore fell through to "ok": an
unreachable URL rendered green while the TUI rendered it CRITICAL. With a
threshold configured it was still wrong the other way -- `200 > 1` is true,
so a healthy URL rendered "warning", and a 404 rendered "warning" instead
of "critical".
2. The status text compared against the strings 'null', 'false' and 'true'.
The API sends real JSON values. Captured from a running server:
{"description": "ClosedPort", "status": false, "rtt_warning": null, ...}
{"description": "UnreachableWeb", "status": "Error", "elapsed": 0, ...}
`false == 'false'` is `0 == NaN` in JavaScript, so none of the three
branches could ever match. A closed or timed-out port fell through to
`false * 1000` and displayed "0ms" -- which reads as an instant, perfect
response for a port that is down. "Scanning" and "Timeout" never rendered
at all, and the 'true' branch was doubly dead: the scanner only ever sets
`status` to None, a float, or False.
Both now mirror the plugin, which is the authority: `set_status_if_host`,
`set_status_if_url` and `get_conds_if_url`. The URL branch falls through to
the raw status string as `set_status_if_url` does, rather than special-casing
'Error'.
Evidence, since the WebUI has no JS test runner -- counted in the rebuilt
bundle:
getPortDecoration 3 -> 2 (definition + two call sites -> + one)
getWebDecoration 1 -> 2 (definition only -> definition + call site)
"null" literals 4 -> 2
"false" literals 4 -> 3
`npx eslint` on the component: 0 errors (the one `vue/require-default-prop`
warning is on the untouched `props` block and predates this change).
socket.setdefaulttimeout() is process-wide and was never restored, so
once a port had been scanned every socket created afterwards anywhere in
Glances that does not set its own timeout inherited the last scanned
port's timeout -- the hddtemp grabber among them.
_socket.settimeout() sets the same value on the socket the scan actually
uses and leaves the rest of the process alone. Scan results are
unchanged.
Also return when the socket cannot be created: the code fell through
with _socket unbound, so connect_ex raised UnboundLocalError into the
'Error while scanning port' handler and the finally clause raised it
again with nothing left to catch it.
load_limits split a config list on ',' and kept the spaces, so 'list=cpu, mem,
load' became ['cpu', ' mem', ' load']. glances.conf writes lists that way in its
own comments, so following the shipped documentation produced a config that did
not work.
The loudest symptom is show/hide: every item is used as a re.fullmatch pattern,
and a leading space makes the pattern match nothing. 'hide=sda2, loop.*' hid
sda2 and silently kept showing every loop device.
Quicklook also answered a bad list by falling back to AVAILABLE_STATS_LIST -
more than the user asked for, and its two GPU entries flip the gpu_stats polling
flags, so a typo started polling the GPU. It now falls back to the documented
DEFAULT_STATS_LIST and names the offending entries in the warning.
`exclamation` mapped `undefined` and `""` to `?` but not `null`, and the
API sends JSON: a process whose nice value psutil cannot read arrives at
the WebUI as `null`. Vue renders `null` as an empty string, so those rows
showed a blank NI cell while the TUI showed `?` for the same process.
On this Windows box that is 144 of 513 processes.
Explicit comparisons rather than a falsy test: `0` is a real nice value on
every POSIX system and has to keep rendering as `0`.
The filter has exactly two call sites, both the NI column, so nothing else
changes shape.
`update_views` skipped the whole interface when either direction measured
0 bytes/s:
if not i.get('bytes_recv_rate_per_sec') or not i.get('bytes_sent_rate_per_sec'):
continue
The guard is `or`, so one idle direction suppressed the alert for *both*. A
send-only interface saturating its uplink went undecorated because its rx rate
happened to be exactly 0, and a receive-only one — a monitor/SPAN port — the
same way. 0 bytes/s is a real measurement, not a missing one.
The rates now default to 0 instead of skipping, which is what the diskio plugin
already does: it dropped this same guard in #3684 and reads the rate through
`or 0` for the same reason.
The WebUI needs no change — `plugin-network.vue` renders `getDecoration(...)`
straight from the server-supplied view, so it inherits this.
Tests: four cases in `TestNetworkPluginZeroRateAlerts`. Three fail before this
change (saturated tx with idle rx, saturated rx with idle tx, and a fully idle
interface reading DEFAULT instead of OK); the fourth — both directions busy —
passes before and after, which is the path that already worked.
`tests/test_plugin_network.py` 39 passed.
sort_stats wraps the specific sort helpers in try/except and falls back to
cpu_percent for the whole list. _sort_cpu_times indexed cpu_times['user'] and
_sort_io_counters indexed io_counters[0..3], so one row with an empty value
raised and silently reordered every other row while the header still read TIME
or IOR/IOW.
Those empty values are expected rather than corrupt: programs.py builds a
program with p['cpu_times'] or {} and list(p['io_counters'] or NO_IO_COUNTERS),
and its own comment says some values can be None on macOS system processes.
Each helper now reads a missing value as zero, so the degenerate row sorts last
and the rest keep their order.
Codacy's Prospector/pydocstyle profile flagged 23 issues on the new file: the
multi-line docstrings all put a description straight under the summary line
(D205, D209, D213, D400, D415). Keeping the summary to one line and putting the
reasoning in comments below says the same thing and leaves the file clean.
No test logic changed: still 7 red on origin/develop, 9 green with the fix.
io_counters is a fixed five-slot list -
[read_bytes, write_bytes, read_bytes_old, write_bytes_old, io_tag] - and
update_program_dict merged it with '+=', which concatenates lists rather than
adding them. Three things followed:
- Readers index the list, so a program showed only its first process's disk
IO. Two processes reading 100 and 1000 bytes displayed 90 B/s where the
sum is 990 B/s.
- '+=' extends in place, and create_program_dict stored the process's own
list, so aggregating wrote back into the process list.
- Re-aggregating the same processes - which a re-sort does - appended again
every time, growing the list without bound.
Sum the four byte slots and OR the io_tag: processlist only displays a rate
when the tag is exactly 1, so adding it would blank the columns instead.
WifiPlugin.get_alert chained its three comparisons, so the first undefined level
was compared against None, raised TypeError, and the blanket handler dropped the
result to DEFAULT. Because 'critical' is tested first, leaving it out silenced
the other two: a config defining only wifi_careful produced no alert at all, and
even a strong signal came back undecorated instead of OK.
GlancesPluginModel.get_alert already guards each level separately, and the
sensors plugin has tests pinning that partial thresholds work. This brings wifi
in line. `is not None` rather than truthiness because these limits are negative
dBm values, where `if threshold` would discard a 0.
The TypeError handler is kept for a non-numeric signal level (issue #1373) but
narrowed to that case, which is now the only way it can fire.
The WebUI decided degradation with `diskData.used < diskData.available` and
nothing else, while the authoritative RaidPlugin.raid_alert applies three rules
before that comparison: raid0 is always OK, an inactive array is CRITICAL, and
an unknown device count is left undecorated.
Two of those omissions are visible:
- A raid0 array reporting fewer used than available devices was drawn as
"Degraded mode" in warning colour. raid0 has no redundancy, so that comparison
carries no meaning there — msg_curse excludes raid0 from the same banner.
- `used` is None whenever the parser cannot read the device count, and in
JavaScript `null < 5` is true, so an unreadable array was reported as degraded.
The TUI leaves it undecorated.
Comparing both implementations across seven arrays, four disagreed before and
none after.
One asymmetry is preserved rather than fixed: raid_alert returns OK for an
inactive raid0 because the raid0 branch runs first. This change matches that so
the two views agree; whether the Python ordering itself is right is a separate
question, and the inactive line is rendered regardless.
`update_views` guarded the alert loop with `if not i['value']: continue`, which
skips a genuine reading of 0 along with the absent ones.
The battery alert is computed on `100 - value`, so an empty battery is the most
critical reading there is — yet a battery at 0% was left undecorated while one at
3%, a strictly better state, was flagged CRITICAL. A fan reporting 0 RPM was
likewise never evaluated.
Tests for a usable number instead. Sensors reporting a placeholder — no battery
is an empty list, hddtemp uses b'ERR'/b'SLP'/b'UNK'/b'NOS' — keep the DEFAULT
decoration the parent update_views() already assigns, so nothing that was skipped
before starts being decorated now. It also keeps non-numbers away from the
`100 - value` subtraction, which raises TypeError on them today.
`summarize_all_cpus_not_displayed` sliced `percpu_list[0 : max_cpu_display]` —
the exact slice `msg_curse` has just printed one line above. So the CPU* row
averaged the cores already on screen instead of the ones that did not fit.
`manage_max_cpu_to_display` sorts by total descending whenever the list
overflows, so the displayed slice is the busiest cores. On an 8-core box showing
4, with four cores at ~87% and four idle at 2%, the CPU* row read 87.0% — a
figure for cores the user can already see, and 43x the load of the group it
claims to represent. The busier the top cores, the more misleading the summary.
Slices the tail instead, so the displayed slice and the summarized slice
partition the list.