Skip to content

[ryoku] shell: reopen the backlight node after it goes away - #766

Open
Matsha wants to merge 3 commits into
Ryoku-dev:unstable-devfrom
Matsha:fix/backlight-reopen-after-resume
Open

Matsha wants to merge 3 commits into
Ryoku-dev:unstable-devfrom
Matsha:fix/backlight-reopen-after-resume

Conversation

@Matsha

@Matsha Matsha commented Nov 8, 2026

Copy link
Copy Markdown

What changed

On a laptop whose backlight is driven by nvidia (nvidia_0), ryoku-shell daemon sits at 100% CPU after waking from suspend until it is restarted.

Cause. nvidia unregisters and re-registers its backlight across suspend, so /sys/class/backlight/nvidia_0 is a new kernfs node after resume. The OSD watcher in ipc/osd.go keeps its fd on the old actual_brightness node. On that dead node poll(2) returns POLLERR at once, pread fails with ENODEV, and watchBacklight does continue straight back into poll. Nothing ever re-arms the notify, so the loop never blocks.

Seen on the live daemon (1.0.5, unchanged in osd.go on unstable-dev):

$ ls -l /proc/<pid>/fd | grep backlight
19 -> /sys/devices/.../backlight/nvidia_0/actual_brightness
$ stat -L -c %i /proc/<pid>/fd/19; stat -c %i /sys/class/backlight/nvidia_0/actual_brightness
34320
73544
$ cat /proc/<pid>/fd/19
cat: /proc/<pid>/fd/19: No such device

/proc/<pid>/io showed about 1.2M read syscalls per second with rchar not moving, all on one thread, and the node's timestamp matched the PM: suspend exit line.

Fix.

  • The poll loop moves into watchBacklightNode, which returns once the node is dead: a read that fails or comes back empty.
  • watchBacklight then re-resolves the device with backlightDevice() and reopens it, backing off from 1s to 30s while it is missing and resetting after a node that worked.
  • readActual now returns an error, so a read that succeeds but does not parse keeps the fd: that read already re-armed the notify.
  • The saved level is still restored only at daemon start, and a reopen publishes without bumping seq, so resume does not flash the OSD.

Area

ryoku

How it was tested

Arch, Hyprland, laptop with an nvidia_0 backlight.

  • Diagnosed on the running daemon as above. Restarting ryoku-shell opens the new node and the daemon drops to 0% CPU, which confirms the stale fd is the cause.
  • go build ./... && go vet ./... && go test ./... in ryoku/shell/ipc passes.
  • New tests: TestReadActualClassifiesDeadNode (closed fd and empty read are dead-node errors, an unparsable value is errBacklightValue) and TestWatchBacklightNodeReturnsOnMissingNode. The sysfs POLLPRI path itself cannot be driven from a regular file in a test.
  • Not yet run: the patched daemon through a real suspend and resume.

Checklist

  • One logical change, with a clear [area] scope: summary commit subject.
  • Matching CHANGELOG.md updated in the area I touched.
  • The git hooks pass locally; I did not use --no-verify.
  • Lua parses (luac -p), shell scripts pass bash -n, and QML passes
    qmllint where applicable.
  • No duplicated config, no dead code, no commented-out code, no em-dash.
  • Docs updated if behavior or layout changed.

nvidia unregisters and re-registers its backlight across suspend. The OSD
watcher kept its fd on the removed actual_brightness node: poll(2) returned
POLLERR at once, pread failed with ENODEV, and the loop went straight back
into poll. On resume the daemon sat at 100% CPU (about 1.2M reads/s) until
restarted.

watchBacklight now runs the poll loop per open node and, when a read fails
or returns nothing, re-resolves the device with backlightDevice() and
reopens it, backing off 1s to 30s while it is missing. A read that succeeds
but does not parse still re-arms the notify and keeps the fd. The saved level
is restored only at daemon start, and a reopen does not bump the sequence,
so resume never flashes the OSD.

Note: Fixed: the shell no longer pins a CPU core after waking from suspend on NVIDIA laptops
@neur0map

neur0map commented Nov 8, 2026 •

Copy link
Copy Markdown
Member

🦊 mika's review

Incremental review: new commits since the last checkpoint.

Summary

  • Reopen vanished backlight nodes with bounded retry backoff.
  • Retry devices unreadable at startup without restoring brightness on reopen.
  • Preserve the watcher on unparsable readings and avoid resume OSD flashes.
  • Add tests for read error classification and missing nodes.

Change diagram

flowchart LR
  Watcher["Backlight watcher"] --> Node["actual_brightness node"]
  Node -->|read failure| Retry["Retry with backoff"]
  Retry -->|re-resolve and reopen| Node
  Node -->|brightness change| OSD["OSD topic"]
Loading

Estimated review effort: 2/5 · risk: low

📁 Walkthrough details

ryoku/shell/ipc/osd.go

  • Reopens missing or failed backlight nodes with exponential backoff, preventing a dead-node polling loop.
  • Distinguishes unparsable values from failed or empty reads, retaining the descriptor when polling can continue.
  • Restores saved brightness only at startup and publishes reopened levels without incrementing the OSD sequence.
    • Extracted watchBacklightNode and added device rediscovery.
    • Added retry delays from one to 30 seconds.
    • Changed readActual to return classified errors.

ryoku/shell/ipc/osd_test.go

  • Tests valid, closed, empty, and unparsable backlight reads.
  • Checks that missing and unspecified devices return without being marked as read.
    • Added TestReadActualClassifiesDeadNode.
    • Added TestWatchBacklightNodeReturnsOnMissingNode.

ryoku/shell/CHANGELOG.md

  • Documents the resume-related CPU usage fix and backlight reopen behavior.
    • Added an unreleased brightness watcher fix entry.

No actionable findings. Mika looked for bugs, security issues, and risks in the diff and came up empty on clear defects.

🛠 Deterministic checks run
  • linters: skipped (no checkout)

🚦 Merge recommendation: no_concerns_found


🔍 mika-agent · self-hosted review for the Ryoku org · @mika-agent help for commands

An unreadable max_brightness at startup returned from watchBacklight and left
the OSD without a watcher for the session. It now enters the reopen loop like
a node that went away, and the saved level is restored only when the device
was readable at startup.

@neur0map neur0map left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 mika reviewed this change; details in the walkthrough comment.

Comment thread ryoku/shell/ipc/osd.go Resolved
The first read in watchBacklightNode treated errBacklightValue as a dead node,
unlike the poll loop. A successful but unparsable read has already re-armed the
notify, so the watcher now keeps the fd and waits for the next change instead
of closing it and backing off.

This branch has not been deployed

No deployments
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