Commit Graph

193 Commits

Author SHA1 Message Date
github-actions[bot]
82c25fb74b chore(v5): weekly merge from develop (2026-06-01) 2026-06-01 07:52:40 +00:00
nicolargo
e68e9f4452 Add unit test to containers/docker plugin 2026-05-31 17:34:25 +02:00
nicolargo
04419de50a Correct display issue on start with all perCPU = 100% 2026-05-30 15:56:23 +02:00
nicolargo
4dbc015214 Correct display issue on start with CPU = 100% 2026-05-30 15:37:04 +02:00
nicolargo
963e5445f0 Add first batch of hotkey management in the TUI 2026-05-30 10:59:57 +02:00
nicolargo
23ffad1b64 Merge branch 'develop' into develop-v5 2026-05-23 19:45:17 +02:00
nicolargo
e52e84f354 Add a short-cut to not generate alert for some plugin (first candidate is processlist) 2026-05-23 19:44:52 +02:00
nicolargo
2afc533d67 Merge branch 'Issue-3555_load_additional_plugins' of github.com:20086080/glances into 20086080-Issue-3555_load_additional_plugins 2026-05-23 15:48:42 +02:00
nicolargo
ff3eec3295 Command Injection via KVM/QEMU VM Domain Names in glances/plugins/vms/engines/virsh.py - CVE-2026-46606 2026-05-23 12:27:19 +02:00
nicolargo
cf14166fbe test(outdated): json round-trip and graceful migration from legacy pickle cache
Cover the non-RCE behaviour of the new JSON cache:
- round-trip: written file is valid JSON, re-read produces equivalent dict
- legacy pickle: a pre-fix pickle cache is treated as a cache miss, not
  a crash (upgrade path)
- expiry: caches older than 7 days are invalidated
- version skew: caches written by a different installed version are
  invalidated
- first run: a missing file is not an error
2026-05-23 11:52:53 +02:00
nicolargo
7098478c39 test(outdated): failing test — malicious pickle cache must not execute (CVE-2026-46607)
Regression test for GHSA-9837-48hr-q32j: glances/outdated.py reads its
version-check cache file via pickle.load(), a deserialization format
that executes arbitrary callables embedded via __reduce__.

The test plants a poisoned pickle at the cache path and asserts that
_load_cache() does NOT trigger the embedded callable. Against the
current (vulnerable) code this fails because the payload fires before
the TypeError is raised on the unrelated dict subscript.

The fix in the next commit replaces pickle with json, which is a passive
data format.
2026-05-23 11:50:55 +02:00
nicolargo
0de3b8f875 XML-RPC Multi-Origin CORS Configuration Silently Falls Back to Wildcard - CVE-2026-46608 2026-05-23 11:40:20 +02:00
nicolargo
cad6f985a5 test(xmlrpc): port stripping and missing-Host edge cases
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:53:06 +02:00
nicolargo
8e6c9c955c test(xmlrpc): wildcard Host patterns via fnmatch
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:52:30 +02:00
nicolargo
575dc7e81b test(xmlrpc): allowlisted Host returns 200
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:51:58 +02:00
nicolargo
b88dd7bcfd test(xmlrpc): failing test — spoofed Host should be rejected (CVE-2026-46611)
Adds a second test server bound to a config that enables xmlrpc_allowed_hosts,
plus the failing assertion that a spoofed Host header returns 400. The fix in
glances/server.py follows in the next commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:50:40 +02:00
nicolargo
b2965cca96 test(xmlrpc): lock in current permissive default (regression baseline)
This test passes on the unpatched server and proves the CVE-2026-46611
vulnerability exists today: a spoofed Host header is accepted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:49:43 +02:00
nicolargo
01437d61e2 test(xmlrpc): scaffold for Host header validation tests
Re-creates tests/test_xmlrpc.py (deleted symlink) with a pytest module
modelled on test_restful.py: subprocess-launched server and a helper
to POST XML-RPC calls with a controllable Host header. Restores the
existing 'make test-xmlrpc' Makefile target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 09:48:47 +02:00
20086080
bcc18b4ab3 Fix : Codacy 2026-05-21 03:06:41 +00:00
20086080
389b6d45bb Fix : Codacy 2026-05-21 02:57:23 +00:00
20086080
c3a8fb2f05 Test : Unit tests 2026-05-21 02:22:17 +00:00
nicolargo
a720335f12 fix(v5): categorical default + drop cmd path from default view
processlist UX refinements after observing the v5 layout live:

- ``compute_level_categorical`` now returns ``None`` (was ``"ok"``) when
  the value matches no configured bucket. ``base_v5`` skips emitting
  a ``_levels`` entry in that case → the renderer keeps the DEFAULT
  colour (white/gray) instead of painting "S" / nice=0 in the OK
  green. Mirrors v4 ``get_alert`` returning ``'DEFAULT'`` for
  unmatched categorical values. The alert pipeline still sees no
  event (semantically equivalent to "ok" for alerts).

- Command rendering drops the ``/usr/bin/`` path prefix from the
  default view. Now: bold cmd + plain args only. The full-path mode
  (toggled by ``/`` in v4) is deferred to G5+ along with the rest of
  the hotkey plumbing.

Tests updated accordingly. Suite v5: 1370 green, lint clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 17:53:13 +02:00
nicolargo
80c20863d9 feat(v5): categorical thresholds + processlist VIRT/RES/IO + bold cmd
Three orthogonal improvements bundled under the processlist v5 surface:

1) Categorical thresholds (framework-level)
   - thresholds_v5 grows compute_level_categorical + read_thresholds_categorical:
     value-set membership instead of numeric comparison. Walked
     most-severe-first so misconfigured overlaps escalate to the higher
     bucket. Unmatched values return "ok" (v4 parity).
   - base_v5._compute_levels_for_item dispatches on schema flag
     ``threshold_type: "categorical"``; numeric path unchanged.
   - 8 new threshold unit tests (incl. CSV parsing, whitespace, pk-prefix).

2) processlist status + nice as categorical fields
   - status (R/W/Z/D/...) and nice (-20..19) become watched with
     ``threshold_type: "categorical"``, no defaults — operators opt in:
       [processlist]
       status_ok=R,W,P,I
       status_critical=Z,D
       nice_warning=-20,...,-1,1,...,19
     Without configuration: no _levels entry (no false positives).

3) processlist renderer: VIRT/RES + R/s/W/s + bold cmd
   - Adds VIRT (memory_info.vms) and RES (memory_info.rss) columns.
   - Adds R/s and W/s columns computed from io_counters
     ([r_new, w_new, r_old, w_old, io_tag]) / time_since_update;
     io_tag != 1 renders "?" (access denied or first cycle).
   - Status / nice cells inherit categorical _levels colour.
   - Command rendering ports v4 split_cmdline:
     /usr/bin/python3 myscript.py
       → "/usr/bin/" + **"python3"** + " myscript.py"
     Kthreads (empty cmdline) keep the [name] fallback.

v4 catalogue updated accordingly. Suite v5: 1370+30 green, lint clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 17:30:01 +02:00
nicolargo
645527de06 feat(v5): G4-processlist — port processcount + processlist plugins to v5
Last MCP gap closure. Both plugins reuse the v4 glances_processes
singleton (no engine rewrite — strategy two-phase): processcount calls
engine.update() + get_count() each cycle, processlist consumes the
pre-sorted list via get_list(). KNOWN_V5_MISSING_PLUGINS shrinks to ().

- processcount: scalar with total / running / sleeping / thread /
  pid_max; TUI mirrors v4's "TASKS N (M thr), R run, S slp, O oth"
  header.
- processlist: collection PK=pid; minimal column set CPU% / MEM% / PID /
  USER / THR / NI / S / Command, top-20 rows. cpu_percent and
  memory_percent are watched (50/70/90, prominent=False — parity fs).
- Engine-internal fields (memory_info, cpu_times, io_counters, gids,
  time_since_update, key) flagged internal=True so MCP/export keep
  them but the generic TUI skips them.
- Out of scope (deferred to G5 with args/config plumbing): extended
  view, programs aggregation, filter UI, interactive sort.

41 new tests (14 model + 27 renderer), v4 catalogue updated, MCP gap
log + adapter docstring updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-17 14:01:59 +02:00
nicolargo
08e9976048 Make network fields non prominent 2026-05-17 13:41:16 +02:00
nicolargo
1ddd34658d Merge branch 'develop' into develop-v5 2026-05-17 11:26:07 +02:00
Nicolas Hennion
7e118d5946 Merge pull request #3557 from DeepSpace2/feat-containers-cpu-limits
feat: add cpu limit to docker, podman and lxd containers
2026-05-17 11:24:50 +02:00
Yan
b42defb1d8 Keep auto_unit within limits, so columns stay aligned
Occasionally, columns got misaligned, because auto_unit returned too
many decimals when the number was slightly below 10 or 100.
Actually, when (9.995 <= n < 10) and (99.95 < n < 100).

For example,
10*2**20-1 returned 10.00M instead of 10.0M and
100*2**20-1 returned 100.0M instead of 100M.

Tests added to verify correctness.
2026-05-16 21:45:09 +00:00
nicolargo
779dee82ab feat(v5): G4-diskio — port diskio plugin to v5 (collection)
Last entry of ``KNOWN_V5_MISSING_PLUGINS`` after this commit:
``("processlist",)``.

Model (``glances/plugins/diskio/model_v5.py``):
- Fields: disk_name (PK, string), read_count / write_count
  (rate, number, internal — exportable for IOPS consumers but
  not rendered), read_bytes / write_bytes (rate, bytespers, watched,
  ``prominent=False``, ``strict_thresholds=True``, NO default
  thresholds).
- Sustained disk traffic is host-specific (a DB server may stream
  MB/s by design) — alerts only fire when operators set
  ``read_bytes_warning=...`` per-disk or per-field in
  ``[diskio]``. ``strict_thresholds=True`` blocks the bare-``<level>``
  fallback (same pattern as memswap.sin/sout) so a legacy
  ``[diskio] careful=50`` cannot trigger spurious alerts.
- ``read_time``/``write_time`` and the derived ``read_latency`` /
  ``write_latency`` of v4 are not ported — deferred with the
  ``--diskio-latency`` mode.
- ``psutil.disk_io_counters()`` may raise or return ``None`` on
  platforms without disk I/O support — model returns ``[]`` rather
  than crashing.

Renderer (``glances/plugins/diskio/render_curses_v5.py``):

    DISK I/O              R/s     W/s
    nvme0n1               0B      0B
    sda                   1.4M   732K

- 3-cell rows, 18 + 1 + 7 + 1 + 7 = 34 chars (fits sidebar cap).
- Sorted by disk_name. Cycle-1 disks (no rate baseline) are skipped
  entirely — no ``-`` placeholder wall on startup.
- Rate cells display ``auto_unit(bytes_per_sec)`` WITHOUT a trailing
  ``/s`` — header carries the per-second semantic (v4 parity).
- Long disk names tail-truncated with leading underscore.

Adjacent:
- ``KNOWN_V5_MISSING_PLUGINS`` shrinks to ``("processlist",)``.
- ``test_attach_mcp_logs_known_v5_gaps`` updated.
- v4 catalogue grows a ``## diskio`` section +  footer.

28 new tests (13 model + 15 renderer). Full v5 suite: 762 passed.
2026-05-15 17:47:07 +02:00
nicolargo
a367dce523 feat(v5): fs thresholds 70/80/90 + non-prominent + alert "ongoing" marker
Three small UX adjustments raised on the live TUI:

1. ``fs.percent`` schema: explicit ``prominent: False``. Colours the
   Used cell at alert level but no longer reverse-videos it (the
   framework defaults missing ``prominent`` to True). Consistent with
   sin/sout in memswap.

2. ``fs.percent`` default thresholds raised to 70/80/90. Filesystems
   often sit at 60-70% on healthy hosts and the old 50/70/90 ladder
   produced noisy "careful" warnings. ``mem`` keeps the stricter
   50/70/90.

3. Alert block UI clarifies active-vs-resolved state:
   - Header was ``ALERTS (N)`` — now ``ALERT (X ongoing / Y total)``.
     ``ongoing`` = unique (plugin, key, field) tuples whose
     most-recent event has a non-ok level. ``total`` = full history
     length.
   - Each event row ending in a non-ok level for the **latest** event
     of its tuple gets a visible ``(ongoing)`` suffix on the level
     cell:
       ``careful (ongoing)``
       ``careful → warning (ongoing)``
     Older non-ok events superseded by a later resolution lose the
     marker — they are no longer ongoing. Resolutions
     (``warning → ok``) never carry the marker.

7 new tests:
- ``test_percent_is_watched_but_not_prominent`` (fs schema)
- ``test_percent_default_thresholds_are_70_80_90`` (fs schema)
- 1 fs runtime test updated to the new ladder + non-prominent
- 4 renderer tests covering header counts, ongoing marker, resolution
  exemption, latest-per-tuple semantics.

Full v5 suite: 734 passed, lint clean.
2026-05-15 17:39:22 +02:00
nicolargo
d9c40b6566 fix(v5): sidebar — advance y by block.height, not block width
``_paint_block`` returns the WIDTH it painted (max row width, ≤ the
panel width). ``_paint_sidebar`` was using that return value as a
HEIGHT (``y += painted_h + 1``), so each block in the left/right
sidebar advanced the cursor by ~35 lines instead of ~5 — a huge
empty band between e.g. network and fs in the left sidebar.

``_paint_top_row`` already uses ``block.height`` directly and does
not have the bug; the fix aligns ``_paint_sidebar`` on the same
pattern. Result: one blank line between sidebar blocks, matching v4.

Regression test
``test_paint_sidebar_advances_y_by_block_height_plus_one_blank_line``
locks in the contract — two 2-row blocks painted at y=5,6 and y=8,9,
with y=7 left blank.

Full v5 suite: 729 passed (+1), lint clean.
2026-05-15 17:25:27 +02:00
nicolargo
39c5858dd6 feat(v5): G4-fs — port the fs plugin to v5 (collection)
Collection plugin keyed on ``mnt_point``. Mirrors v4
``glances/plugins/fs/__init__.py``.

Model (``glances/plugins/fs/model_v5.py``):
- Fields: mnt_point (PK), device_name, fs_type (internal), options
  (internal), size, used, free, percent.
- ``percent`` watched/prominent with the standard 50/70/90 ladder.
- ``_grab_stats`` swallows PermissionError from
  ``psutil.disk_partitions`` (locked-down hosts) and per-partition
  OSError from ``disk_usage`` (ejected media, broken NFS mount) —
  the offending partition is dropped from the cycle without aborting
  the whole update.
- SNMP support left out (architecture §10).

Renderer (``glances/plugins/fs/render_curses_v5.py``):

    FILE SYS              Used   Total
    /                   125.0G  500.0G
    /home               512.0G    1.0T

- 3-cell rows (mnt + Used + Total), 18 + 1 + 7 + 1 + 7 = 34 chars —
  fits the left-sidebar cap exactly.
- Filesystems sorted by mountpoint (v4 parity).
- ``Used`` cell inherits the percent-threshold color from
  ``_levels.<mnt>.percent``; title escalates on warning/critical.
- Long mountpoints tail-truncated with a leading underscore.
- ``--fs-free-space`` toggle and the optional ``(device)`` suffix
  deferred to a later phase pending CLI / max_width plumbing.

Adjacent (already committed by the maintainer in 1da70476):
- ``KNOWN_V5_MISSING_PLUGINS`` shrinks to ``(processlist, diskio)``.
- v4 catalogue grows a ``## fs`` section +  footer.
- ``test_attach_mcp_logs_known_v5_gaps`` updated.

24 new tests (11 model + 13 renderer). Full v5 suite: 728 passed.
2026-05-15 17:18:42 +02:00
nicolargo
1da7047688 Change default thresolds minimum duration for memswap 2026-05-15 17:11:47 +02:00
nicolargo
cb25714f8b fix(v5): config — single-file loading, v4-aligned (no cross-file merge)
The previous loader merged keys across every available config layer
(/etc → XDG → \$GLANCES_CONFIG_FILE → -C), producing surprising
cross-file inheritance. The most visible footgun: a v4-era user XDG
glances.conf with bare ``[memswap] careful=50/warning=70/critical=90``
silently bleeding onto the v5 project conf and triggering alerts on
unrelated v5 opt-in fields (``memswap.sin``/``sout``).

Aligns with v4 ``glances/config.py::config_file_paths``: exactly **one**
config file is read.

- **``-C <path>``**: that file, no search, no fallback. A missing path
  logs a WARNING and the loader proceeds with DEFAULTS only.
- **No ``-C``**: walk the v4 search list (user → system), stop at the
  first existing entry:
    1. ``$XDG_CONFIG_HOME/glances/glances.conf`` (or
       ``~/.config/glances/glances.conf``)
    2. ``/etc/glances/glances.conf``

Drops the ``$GLANCES_CONFIG_FILE`` env-var path entirely — v5-only
sugar that did not exist in v4 and that exacerbated the merge
problem. The codebase ``DEFAULTS`` layer (intrinsic baseline) and the
``GLANCES_<SECTION>__<KEY>`` env-overlay (orthogonal to the file
question, useful for containers / CI) both stay.

Tests rewritten end-to-end (3 net new):
- ``test_xdg_does_not_inherit_etc_keys`` — regression guard for the
  bare-keys-from-/etc-leaking bug.
- ``test_cli_path_does_not_merge_with_other_files`` — ``-C`` truly
  picks one file.
- ``test_missing_cli_path_falls_back_to_defaults_only`` — non-existent
  ``-C`` path logs WARNING + uses DEFAULTS, does not silently fall
  back to the search path.
- ``test_glances_config_file_env_is_ignored`` — locked out.
- ``test_loaded_sources_contains_only_the_chosen_file`` — at most one.

Runtime verification with the real user XDG conf:
  $ -C conf/glances.conf →  loaded_sources = [conf/glances.conf]
                            [memswap] has only percent_* keys (clean).
  $ no -C                →  loaded_sources = [~/.config/glances/glances.conf]
                            (single file, no /etc merge).

Full v5 suite: 704 passed (+3), lint clean.
2026-05-15 17:00:20 +02:00
nicolargo
fa7fc0c99d fix(v5): thresholds — strict_thresholds flag isolates opt-in fields
Root cause of the "sin/sout shown in green" report: a v4-era user
XDG ``~/.config/glances/glances.conf`` carrying bare keys
``[memswap] careful=50 / warning=70 / critical=90`` gets merged with
the v5 project conf via the config layering. ``read_thresholds`` then
falls back to those bare keys for ANY watched field in the section —
including the v5-only opt-in sin/sout — so empty ``_levels`` was
never reachable in practice.

Fix: introduce a per-field schema flag ``strict_thresholds: True``
that opts the field out of the bare-``<level>`` fallback inside
``read_thresholds``. Only field-prefixed (``sin_warning``) and pk-
field-level keys still activate it.

- ``read_thresholds`` gains ``strict: bool = False`` parameter.
- ``_compute_levels_for_item`` propagates ``schema.get("strict_thresholds")``.
- ``memswap.sin`` and ``memswap.sout`` ship ``strict_thresholds: True``.
  ``memswap.percent`` keeps the legacy fallback (consistent with v4
  ``[memswap] careful=50`` applying to the swap percent).

5 new tests:
- 4 in ``test_thresholds_v5.py``: strict skips bare for scalar and
  collection cases; strict still honours field-prefixed and
  pk-field-level keys.
- 1 in ``test_plugin_memswap_v5.py``: end-to-end with a fake conf
  containing only bare keys → percent gets a _levels entry, sin/sout
  do not.

Full v5 suite: 701 passed (+5), lint clean.

Runtime check with the real user-XDG layering reproduces the fix:
``memswap _levels: {'percent': {'level': 'ok', 'prominent': True}}``
— sin/sout absent → renderer paints them in DEFAULT colour, not
green.
2026-05-15 16:52:30 +02:00
nicolargo
a715c8b363 fix(v5): memswap — sin/sout never prominent + default colour with no thresholds
Two related tweaks raised on the running TUI:

1. ``sin`` / ``sout`` must never reverse-video the cell, even when a
   user-configured threshold fires. The framework's
   ``_compute_levels_for_item`` defaults a missing ``prominent`` key to
   ``True``, so removing the flag is not enough — we need an explicit
   ``"prominent": False`` in the schema. Added on both fields.

2. When no thresholds are configured (stock install, no
   ``[memswap] sin_*`` keys), ``read_thresholds`` returns ``{}`` and
   ``_compute_levels_for_item`` skips the field via ``if not thresholds:
   continue`` — no ``_levels`` entry, ``_cell_for_field`` returns
   ``ColorRole.DEFAULT``. Behaviour was already correct but was not
   covered by a regression test; locked in via two renderer tests.

4 new tests:
- ``test_sin_sout_are_not_prominent`` — schema assertion.
- ``test_sin_threshold_from_config_carries_non_prominent_level`` —
  runtime: user-configured threshold produces a level entry with
  ``prominent=False``.
- ``test_render_sin_sout_use_default_color_without_thresholds`` —
  renderer lock: no ``_levels`` entry → DEFAULT colour, not prominent.
- ``test_render_sin_carries_level_color_when_thresholds_set`` —
  renderer with thresholds: level colour applied, still non-prominent.

Full v5 suite: 696 passed (+4), lint clean.
2026-05-15 16:40:06 +02:00
nicolargo
bbb93100c3 feat(v5): memswap — surface sin/sout in TUI, drop used/free
Two UX changes on the swap panel:

1. Body rows shifted from (total, used, free) to (total, sin, sout).
   ``used`` and ``free`` are derivable from ``percent`` + ``total`` so
   showing them on dedicated lines is redundant. Replacing them with
   the swap I/O rates surfaces actionable live paging activity:

       SWAP   25.0%
       total  16.0G
       sin    97.7K/s
       sout     0B/s

   On cycle 1 the rates are absent (no baseline yet); the renderer
   shows ``-`` for both fields and stays the same height.

2. ``sin`` / ``sout`` are watched but ship **no default thresholds**.
   Sustained swap traffic is host-specific (database servers may
   page steadily by design), so alerts only fire when the user opts
   in via ``[memswap]`` in glances.conf. Suggested ladder shipped
   commented:

       # sin_careful=40960
       # sin_warning=409600
       # sin_critical=819200
       # (and the same for sout)

   ≈ 40 / 400 / 800 KB/s. ``watched=True`` lets the framework wire
   the levels when the user uncomments them; absent defaults +
   ``read_thresholds → {} → _compute_levels_for_item skips``
   guarantees silence on a stock install.

5 new tests:
- ``test_sin_sout_are_watched_without_default_thresholds`` — schema
  asserts.
- ``test_sin_sout_no_levels_without_user_thresholds`` — runtime
  confirms no _levels entries for sin/sout when config is stock.
- ``test_sin_threshold_from_config_triggers_level`` — config-set
  ``sin_warning=10000`` actually fires a warning at 15_000 bytes/s.
- ``test_render_body_rows_have_total_sin_sout`` /
  ``test_render_does_not_show_used_or_free`` /
  ``test_render_handles_missing_sin_sout_on_first_cycle`` — renderer
  layout contract.

Full v5 suite: 692 passed (+5), lint clean.
2026-05-15 16:36:02 +02:00
Adi
b4b2118933 feat: add cpu limit to docker, podman and lxd containers 2026-05-15 17:32:25 +03:00
nicolargo
94c0c52446 chore(v5): memswap — loosen default thresholds to 60/80/95
Swap usage that would be alarming on RAM is fairly normal on a
healthy host with active paging — a fairly full swap is only really
concerning near saturation. Raise the ``[memswap] percent`` default
ladder accordingly:

- careful:  50 → 60
- warning:  70 → 80
- critical: 90 → 95

Mirrors the maintainer's hands-on threshold judgment. The
``mem`` plugin keeps the stricter 50/70/90 ladder.

Test ``test_percent_level_uses_default_thresholds`` updated to probe
the new boundary (85% → warning).
2026-05-15 16:23:52 +02:00
nicolargo
0a079d8ff5 fix(v5): alert block — close warmup→event gap + local time + date prefix
Two small UX issues raised on real-world startup observation.

1. Spurious "(no events)" between warmup and first commit.
   After warmup, the first non-ok observation enters hysteresis
   (``pending_level`` set, ``pending_since`` started). Until
   ``min_duration`` (default 5 s) elapses, no event is recorded. With
   the previous logic, ``is_initializing()`` already returned False at
   that point → the alert block briefly flashed "(no events)" before
   the first event landed.
   ``GlancesAlerts.is_initializing()`` now also returns True when any
   ``_AlertState.pending_level`` is non-None. The transition from
   "(initializing)" to actual events happens in a single step.

2. Alert timestamps shown in UTC, no date for old events.
   ``_build_event`` stores UTC ISO-8601. The renderer used to crop
   ``ts[11:19]``, displaying the raw UTC clock and losing the date
   entirely. Operators reading a UI in a non-UTC zone saw confusing
   hours; alerts older than today were undated.
   New ``_format_alert_time(ts, now=None)`` in
   ``glances/outputs/curses_renderer_v5.py``: parses the ISO string
   (treats naive timestamps as UTC for back-compat), converts via
   ``astimezone()`` to the local TZ, formats as ``HH:MM:SS`` for
   same-day events and ``MM-DD HH:MM:SS`` otherwise. Unparseable
   input falls back to the first 8 chars — never raises.
   ``render_alert_block`` gains an optional ``now`` parameter so the
   same-day cutoff is testable without TZ flakiness.

6 new tests:
- alerts: hysteresis-pending → still initializing; warmup + pending
  → fires once min_duration elapses.
- renderer: same-day formatting, other-day date prefix, naive UTC
  handling, malformed fallback, end-to-end via render_alert_block.

Full v5 suite: 687 passed (+6), lint clean.
2026-05-15 16:19:22 +02:00
nicolargo
d6b5d60641 fix(v5): alert block — distinguish warmup ("initializing") from empty
Previously the alert block always showed ``(no events)`` on an empty
history — including during the per-plugin warmup window (first 3
refresh cycles by default) where alerts physically cannot have fired
yet. Misleading: the user sees "no events" before any alert has had a
chance to materialise.

Fix:

- ``GlancesAlerts.is_initializing()`` returns ``True`` when the
  ``_plugin_cycles`` map is empty (scheduler not yet ticked) or when
  any tracked plugin still sits inside its warmup window. Switches to
  ``False`` once every ingested plugin has cleared warmup.
- ``render_alert_block(history, limit, is_initializing=False)`` gains
  the flag; on empty history it shows ``(initializing)`` during
  warmup and ``(no events)`` once truly settled. Non-empty history
  always renders the events — the flag is irrelevant then.
- ``build_frame`` accepts ``alerts_initializing`` and threads it to
  ``render_alert_block``.
- ``TuiV5._build_frame`` calls ``self.alerts.is_initializing()`` and
  passes it through.

7 new tests:
- 4 alerts (constructor → True; in-warmup → True; post-warmup → False;
  mixed plugin states → True if any is still warming);
- 3 renderer (placeholder for initializing, placeholder for empty,
  non-empty history ignores the flag).

Full v5 suite: 681 passed (+7), lint clean.
2026-05-15 16:10:51 +02:00
nicolargo
2e16cd18ee fix(v5): alerts — flag initial state events, render as bare level
When Glances starts and the first post-warmup observation is non-ok
(e.g. memory was already at careful), the alert event used to display
``ok → careful``. That is misleading: no transition actually happened
— Glances merely discovered the system was already in that state.

Fix:

- ``_AlertState`` grows ``has_committed: bool`` (default ``False``);
  it flips to ``True`` the first time the state is reconciled — either
  by matching the default ``ok`` or by committing a different observed
  level. The first transition out of the default state is therefore
  identifiable.
- ``_Transition`` grows ``is_initial: bool`` (default ``False``). All
  three commit paths in ``_reconcile`` (immediate, hysteresis-elapsed)
  derive it from ``not state.has_committed`` BEFORE flipping the flag.
- ``_build_event`` records the flag on the emitted history dict; the
  event schema gains an ``is_initial`` boolean.
- ``render_alert_block`` (curses renderer) shows the bare level
  (``"careful"``) instead of ``"ok → careful"`` when ``is_initial`` is
  ``True``. Real transitions keep the arrow.

5 new tests:

- ``test_first_event_after_warmup_is_flagged_initial`` — startup case;
- ``test_subsequent_transitions_are_not_initial`` — real changes keep
  the arrow;
- ``test_initial_flag_set_when_first_observed_is_ok_then_non_ok`` —
  first observation == ok confirms state without an event; later
  rise out of ok is a real change, not initial;
- ``test_render_alert_block_initial_state_omits_arrow`` — renderer;
- ``test_render_alert_block_transition_keeps_arrow`` — renderer.

Full v5 suite: 674 passed (+5), lint clean.
2026-05-15 14:22:38 +02:00
nicolargo
f080a48937 feat(v5): G4-memswap — port the memswap plugin to v5 (scalar)
Sister of the v5 ``mem`` plugin. Same pattern, slimmer layout
(single-column body — v4 ``memswap.msg_curse`` does not 2-col).

Model (``glances/plugins/memswap/model_v5.py``):
- ``total`` / ``used`` / ``free`` — bytes, snapshot.
- ``percent`` — watched + prominent, default thresholds 50/70/90
  (same ladder as ``mem`` for UX consistency).
- ``sin`` / ``sout`` — cumulative in v4; v5 exposes them as
  bytes/sec via ``rate: True``.
- Tolerates platforms without a swap file (Illumos, OpenBSD —
  issues #1767, #2719): psutil raises, model returns ``{}`` so
  the scheduler tick keeps going.

Renderer (``glances/plugins/memswap/render_curses_v5.py``):

    SWAP   25.0%
    total  16.0G
    used    4.0G
    free   12.0G

- Line 1: ``SWAP`` (HEADER) + percent cell coloured by ``_levels.percent``.
  Title escalates to warning/critical when the prominent percent reaches
  those levels.
- Lines 2-4: ``total`` / ``used`` / ``free`` as label/value pairs.
- Value column floored at 6 chars so it does not jiggle between cycles.

Adjacent changes:
- ``KNOWN_V5_MISSING_PLUGINS`` in ``mcp_adapter_v5`` shrinks to
  ``processlist, fs, diskio`` — memswap no longer surfaces in the
  MCP startup gap log.
- v4 catalogue (``docs/architecture/tui-v4-rendering-patterns.md``)
  grows a ``## memswap`` section +  footer pointing to the new
  renderer.

22 new tests (11 model + 11 renderer). Full v5 suite: 669 passed
(+22), lint clean.
2026-05-15 14:17:04 +02:00
nicolargo
c9e88ce1da feat(v5): G3-MCP Task 4 — surface v5↔v4 MCP gaps at startup
Operators enabling MCP via ``--enable-mcp`` must know which v4
resources won't resolve through v5 yet. Two new INFO log lines at
``attach_mcp`` mount time:

- which v4 plugins are not in the v5 registry — MCP returns
  ``ValueError("Plugin '<x>' not found")`` for those. The list is
  driven by a new ``KNOWN_V5_MISSING_PLUGINS`` tuple in
  ``glances/outputs/mcp_adapter_v5.py`` (``processlist``, ``fs``,
  ``diskio``, ``memswap`` at the time of writing).
- the deferred history semantic — ``glances://stats/<plugin>/history``
  returns ``{}`` until v5 ships a real history buffer (out of scope
  per the G3-MCP plan).

The constant is a moving target: as future v5 phases port plugins,
the entries get removed and the log naturally shrinks. The adapter
``get_plugin`` logic itself does not branch on this list — it stays
data-driven via the actual registry.

2 new tests in ``tests/test_webserver_v5.py`` verify both INFO lines
fire on a gate-on mount.

Full v5 suite: 647 passed (+2), lint clean.
2026-05-15 13:54:23 +02:00
nicolargo
c88fdebe09 feat(v5): G3-MCP Task 3 — CLI overlay + attach_mcp() called from assemble()
Wires the ``--enable-mcp`` flag (added in G2 Task 1, already validated
to require ``-s``) to the actual ``/mcp`` mount:

- ``main_v5.assemble`` propagates ``args.enable_mcp`` into
  ``config._merged["outputs"]["enable_mcp"]`` — same overlay
  mechanism used for ``args.api_doc``.
- After ``register_plugin`` populates the plugin registry,
  ``attach_mcp(app, …)`` runs. It is a no-op when the gate is off,
  so existing ``-s`` deployments are unchanged.

Tests in ``tests/test_main_v5.py``:
- ``test_assemble_server_without_enable_mcp_does_not_mount`` — ``-s``
  alone: no /mcp Mount in ``app.routes``;
- ``test_assemble_propagates_enable_mcp_overlay`` — ``-s --enable-mcp``:
  the config gate flips to True and /mcp is mounted.

Manual smoke:
- ``make run-v5-server`` → GET /mcp returns 404 (no mount).
- ``make run-v5-mcp``    → log "MCP endpoint mounted at /mcp" + GET
  /mcp returns 307 (SSE redirect, mount is reachable).

Full v5 suite: 645 passed (+2), lint clean.
2026-05-15 13:47:58 +02:00
nicolargo
488e2bee89 feat(v5): G3-MCP Task 2 — attach_mcp() mounts /mcp on the gate flag
New ``attach_mcp(app, *, config, store, plugins, alerts=None)`` in
``glances/webserver_v5.py``. Behaviour:

- Gate: ``[outputs] enable_mcp`` (default ``False``). When off, the
  function is a silent no-op — no log, no mount. The default REST
  deployment is unchanged.
- Gate on: instantiates ``McpStatsAdapter`` over the v5 store +
  plugins + alerts, hands it to the (unmodified) ``GlancesMcpServer``
  alongside an empty ``args`` namespace (the v4 class stores ``args``
  but never reads it), and mounts the SSE ASGI sub-app at ``/mcp``.
  Records the server on ``app.state.mcp_server`` for diagnostics.
- Optional dependency: when ``mcp`` is not installed
  (``MCP_AVAILABLE`` is False) the function returns ``False`` and
  logs a single WARNING pointing at ``pip install 'glances[mcp]'`` —
  never raises.
- Auth: the global v5 auth middleware passes SSE responses through
  unchanged (HTTP-level middleware, no body buffering). The MCP mount
  inherits Basic + Bearer auth from the existing ``_wire_auth`` path.
- DNS rebinding: read by ``GlancesMcpServer`` from
  ``[outputs] mcp_allowed_hosts`` (loopback only by default) — kept
  independent from ``[outputs] webui_allowed_hosts``.

6 new tests in ``tests/test_webserver_v5.py``:
- default build does not mount /mcp (404);
- ``attach_mcp`` with the gate off returns False + no mount;
- gate-on adds the Mount and stores the server on ``app.state``;
- skip path emits no log noise on the common gate-off case;
- missing ``mcp`` package → False + clear pip install WARN.

Full v5 suite: 643 passed (+6), lint clean.
2026-05-15 13:44:40 +02:00
nicolargo
cf5cfc0fbd feat(v5): G3-MCP Task 1 — McpStatsAdapter (v5→MCP duck-typed facade)
First step of the G3-MCP plan
(``docs/superpowers/plans/2026-05-15-glances-v5-phase2-g3-mcp.md``):
bridge between v5's lockless ``StatsStoreV5`` + plugin registry +
alerts pipeline and the v4-``GlancesStats``-shape surface consumed by
``glances.outputs.glances_mcp.GlancesMcpServer``. The MCP server
class itself stays untouched.

Surface:

- ``McpStatsAdapter``
  - ``getPluginsList()`` — registry names + synthetic ``"alert"``
  - ``getAllAsDict()`` — ``StatsStoreV5.as_dict()``
  - ``getAllLimitsAsDict()`` — aggregates ``default_thresholds`` per
    plugin (synthetic plugins omitted)
  - ``get_plugin(name)`` — view or ``None``; the canonical
    "Plugin not found" error path is left to the MCP server
- ``McpPluginView``
  - ``get_raw()`` — store payload (or synthetic callable for
    ``"alert"``, which returns ``GlancesAlerts.get_history()``)
  - ``get_raw_history()`` — returns ``{}`` and logs a throttled WARN
    (once per plugin) until v5 ships real history (out of scope)
  - ``get_limits()`` — per-field ``default_thresholds`` aggregation

V5 limitations are explicit (not silent):
- No history yet → WARN-once + empty dataset.
- ``processlist`` / ``fs`` / ``diskio`` / ``memswap`` not yet ported
  → ``get_plugin`` returns ``None``; MCP raises its canonical
  ``ValueError("Plugin not found")``.
- Alert schema: v5-native (option (a) from the plan's decision log).

15 new unit tests covering plugin enumeration, raw payload, limits
aggregation, history's deferred semantic + WARN throttling, synthetic
``alert`` view, and unknown plugin → ``None`` for every v4-only name.
2026-05-15 12:10:47 +02:00
nicolargo
38ef93d948 feat(v5): G2 — mode-dispatched lifecycle (default=TUI only, -s=REST only)
Wires the ``-s`` / ``--server`` flag introduced in Task 1 to the actual
runtime — fulfils the G2 design alignment table:

    Mode             | Scheduler | TUI | REST  | MCP
    -----------------|-----------|-----|-------|-----
    Default          | yes       | yes | no    | no
    -s               | yes       | no  | yes   | no (Task 2)
    -s --enable-mcp  | yes       | no  | yes   | yes (Task 2)

(Plan: ``docs/superpowers/plans/2026-05-15-glances-v5-phase2-g2.md``;
Tasks 2 and 3 were swapped — see the previous discussion thread —
because Task 1 standalone left the system in a state contradicting the
design table.)

Refactor:

- ``assemble`` returns ``app=None`` in TUI mode — no FastAPI app built,
  no plugin registration on a server. In ``-s`` mode the TUI is not
  instantiated (``-s`` is headless per alignment #1).
- ``serve`` takes ``args`` as the leading argument; branches on
  ``args.server`` inside a single ``try`` whose ``finally`` cleanly
  stops the TUI + scheduler in both modes. Server mode runs uvicorn as
  before; TUI mode awaits ``scheduler_task`` until SIGINT (raised by
  ``on_quit`` when the user types ``q``/ESC).
- ``main`` logs the mode-specific startup line ("TUI mode (no REST API
  bound)" vs the existing REST line).

Tests:

- Existing ``test_assemble_*`` cases that need a FastAPI app now pass
  ``-s`` instead of ``--no-tui``. Existing semantics preserved.
- New ``test_assemble_default_mode_builds_no_app`` and
  ``test_assemble_server_mode_skips_tui`` lock in the dispatch contract.
- ``test_assemble_server_mode_plus_no_tui_is_idempotent`` covers the
  redundant ``-s --quiet`` combo (TUI still off).
- ``test_assemble_default_mode_no_tui_disables_everything`` documents
  the scheduler-only degenerate mode (useful for test rigs).
- New ``test_serve_tui_mode_does_not_instantiate_uvicorn`` is the
  bind-no-socket regression guard: patches ``uvicorn.Server`` and
  asserts it is never called in default mode.

Manual smoke (regression guard for the bug raised by the user):
- ``python -m glances.main_v5 -C conf/glances.conf --no-tui``: no
  socket on :61208, log line confirms TUI mode.
- ``python -m glances.main_v5 -C conf/glances.conf -s``: :61208
  reachable, log line confirms REST mode.

622 v5 tests green (+5 dispatch cases), lint clean.
2026-05-15 11:20:20 +02:00
nicolargo
33a90f5eb8 feat(v5): G2 — add -s/--server and --enable-mcp CLI flags + validation
Step 1 of the mode-dispatch refactor (plan
``docs/superpowers/plans/2026-05-15-glances-v5-phase2-g2.md``). This
commit only introduces the flags + cross-flag validation; the
dispatch wiring lands in Tasks 2 and 3.

- ``-s`` / ``--server``: opt in to the REST API mode (headless).
- ``--enable-mcp``: mount /mcp; requires ``--server`` — caught by
  ``validate_args`` with a clear stderr message + argparse exit 2.
- ``--quiet`` / ``--no-tui``: kept (open point — see plan G2 §"Open
  points"). When passed together with ``-s`` an info log notes that
  the flag is redundant.
- New ``validate_args`` helper called from ``main()`` after
  ``setup_logging``.

15 new tests in ``tests/test_cli_v5.py`` covering parser shape,
defaults, alias spellings, and the validation rules.
2026-05-15 11:07:15 +02:00
nicolargo
6ade42c142 Increase CPU context switch thresolds 2026-05-15 10:15:19 +02:00