Skip to content

fix(tailscale): project status JSON fields before Luau decode - #439

Closed
nicobonada wants to merge 2 commits into
noctalia-dev:mainfrom
nicobonada:fix/tailscale-project-status-json
Closed

nicobonada wants to merge 2 commits into
noctalia-dev:mainfrom
nicobonada:fix/tailscale-project-status-json

Conversation

@nicobonada

Copy link
Copy Markdown
Contributor

Plugin

  • Id: davemhammer/tailscale
  • New plugin
  • Update to an existing plugin (version bumped in plugin.toml)

What it does

Follow-up to #362. That PR filters unused Mullvad exits out of tailscale status --json before Luau decodes it. The leftover JSON is still fat: each remaining peer carries sshHostKeys, Self carries CapMap, and so on — fields parseStatus never reads.

On a loaded machine that leftover decode plus state.set of the snapshot can still exceed Noctalia's 25ms plugin CPU budget, which auto-disables the service (script callback 'async command callback' exceeded its CPU budget).

This keeps the existing Mullvad Peer filter as-is, then jqs the same pipe down to the fields the service actually uses (same idea as the existing debug prefs projection). state.set is skipped when the snapshot signature did not change. Version 1.0.61.0.7.

External dependencies

Unchanged: tailscale, jq, xdg-open. jq already filtered status --json after #362; it now also projects fields, still in that one pipe.

Testing

Reproduced the CPU-budget apply failed log on Niri / NixOS with Mullvad exits in the netmap (raw status --json ~870 KB / 560 peers; after #362 ~15 KB; after this projection ~3.2 KB / 7 real machines). After the change, noctalia msg plugin davemhammer/tailscale:service all refresh succeeds and the bar/panel still show real machines plus the exit-node list.

  • Tested on Niri
  • Tested on Hyprland
  • Tested on Sway
  • Tested on another compositor:
  • Noctalia version tested against: 5.0.0
  • Plugin API level: 10

Screenshots / Videos

None — no UI change. Same bar/panel; smaller JSON on the existing jq pipe.

Checklist

  • The directory name matches the part of id after the / in plugin.toml exactly.
  • It ships plugin.toml, README.md, thumbnail.webp, and translations/en.json.
  • README.md follows the
    README template, documents
    every entry id and dependency, and includes exact panel IPC commands and launcher prefixes where applicable.
  • I created thumbnail.webp with the thumbnail generator. (unchanged; not a new plugin)
  • version follows semver and is bumped in this PR; plugin_api is the oldest API level this plugin requires.
  • Every non-English translation in this PR uses a locale supported by Noctalia core, and I can read, write, and
    understand that language well enough to review and maintain it (no unreviewed machine/LLM translations). (no locale files touched)
  • I did not edit catalog.toml; CI generates it.
  • This PR touches exactly one plugin directory.

Code review attestation

  • The code is readable and not obfuscated, minified, or generated.
  • It does not download and execute remote code.
  • Every network call, filesystem write, and spawned process is something the description above accounts for.
  • I have the right to publish this code under the license declared in plugin.toml.

After the Mullvad Peer filter, leftover objects still carry unused
fields (sshHostKeys, CapMap, …). Keep the same filter, then pick only
the fields parseStatus reads. Skip state.set when nothing changed.
@github-actions

Copy link
Copy Markdown
Contributor

CC @davemhammer

Rx/Tx counters in the field projection changed every poll under
traffic and forced a full decode even though the panel never shows
them. Drop those fields and reuse the last parse when the raw
command strings match.
@nicobonada

Copy link
Copy Markdown
Contributor Author

Follow-up commit on this same branch — the first one still blew Noctalia's 25ms plugin CPU budget on a loaded machine.

status --json still included RxBytes / TxBytes / LastSeen. Those change every poll under traffic, so the snapshot looked new even though the panel never shows them. The new commit drops those fields and skips decode / state.set when the raw jq output is unchanged.

Still the 1.0.61.0.7 bump. Overlay-tested here after the first patch still logged script callback 'update' exceeded its CPU budget.

@ItsLemmy

Copy link
Copy Markdown
Contributor

waiting on original author's approval

@ItsLemmy
ItsLemmy marked this pull request as draft August 24, 2026 22:08
@ItsLemmy

ItsLemmy commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

@davemhammer last call before I review and merge if all is good.

@ItsLemmy

Copy link
Copy Markdown
Contributor
  1. blocking - tailscale/service.luau:452-462

The new raw-equality early return in applyBag() runs before the error bookkeeping, and it also clears
snapshot.error. When all three commands fail with empty stdout (the documented "installed but tailscaled
not running" state, which README.md lists as a requirement), the raw strings equal the module's initial
empty values, so the branch returns without ever evaluating errors. The published snapshot therefore
keeps the loading state published by refreshAll (tailscale/service.luau:614-617) and an empty error, and
the branch additionally clears any error set earlier.

Observed result, compared against main with a stubbed host driving the real service module: main reports
available=false, error="tailscale: failed to connect to local tailscaled"; the PR reports
available=false, error="". In the panel that leaves the Status tab on tr("panel.loading") forever
(panel.luau:564-565) because error stays empty and loading was last published as true, the Peers tab
blank (panel.luau:293-296 suppresses the empty-state label while loading), and the widget tooltip reads
"Tailscale unavailable: " with no reason (widget.luau:74-76). This regresses a supported, documented state
and costs the user the only indication of why Tailscale is unreachable.

  1. non-blocking - tailscale/service.luau:567-580

Gating publishSnapshot() on the revision signature means any field the UI reads that is not part of that
signature never reaches the panel or widget, because noctalia.state.set serializes through JSON and each
entry re-renders only from its own value copy on a watch callback (panel.luau:658-671, widget.luau:88-93).

Same A/B run: with the selected exit node flipping to offline and the peer set unchanged, main publishes
and the UI shows exitNodeOnline=false, while the PR performs no state.set at all and the UI keeps
exitNodeOnline=true until an unrelated signature change happens to occur; the panel's "Updated {time}"
label (panel.luau:649-650) and its Exit list (panel.luau:318) and Health list (panel.luau:260) have the
same blind spot, as does the widget's exit glyph color (widget.luau:57). On an idle tailnet those fields
can stay stale indefinitely. Dropping the RxBytes/TxBytes/LastSeen traffic fields is itself fine: they are
assigned in parseStatus but never read by any entry.

@nicobonada

Copy link
Copy Markdown
Contributor Author

@ItsLemmy Even with the changes in this PR, the plugin still dies under heavy load (last time was during compiling shaders in a game). I'm inclined to close the PR and use the tailnet plugin going forward because further optimization is above my pay grade.

@ItsLemmy ItsLemmy closed this Sep 13, 2026
@nicobonada
nicobonada deleted the fix/tailscale-project-status-json branch September 13, 2026 00:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants