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
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.
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>
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>
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>
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>
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>
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.
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.
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.
``_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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.