fix(midiviz): survive an LCXL unplug/replug in-process, no more clean exit
The bug: midiviz resolved its ALSA source port ONCE at startup and shelled out to `aseqdump -p <port>`. Unplugging the LCXL removes the ALSA client it was subscribed to, `aseqdump`'s stdout hits EOF, the reader thread's for-loop just returns, and nothing else was keeping `app.exec()` alive — the window closed with exit 0. Because that is a CLEAN exit, `Restart=on-failure` never fired, so PLN's "always open" glyph-rain lens was gone for the rest of the session the moment he unplugged the board. Reproduced from tonight's own journal: `midiviz.service` ran 6m57s and exited status=0/SUCCESS the instant the surface came off USB — textbook rig failure mode #1, "a binding resolved once, killed by a replug, never re-resolved", except this time the binding was the whole window's reason to exist rather than just a port variable. The fix, mirroring the resolve/rebind pattern already proven in `tools/lcxl-leds.py` (`find_seq_port` + `invalidate_ports`, cached and dropped on failure): - `Reader` now tracks `.alive()` (child process poll) and marks itself `error = "closed"` when its `aseqdump` child exits on its own (vs. an intentional `.close()`), so the difference between "I quit" and "I died" is visible to the widget. - `MidiViz` gained a second QTimer (`RECONNECT_MS = 2000`, matching `midi-autoconnect.sh`'s own reconcile cadence) that re-resolves the source every tick and swaps the `Reader` in place if it moved, died, or vanished. Losing the source is never fatal any more: the window drops to its existing idle/dim-pulse paint state (already built for "no events recently") and keeps ticking, painting, and listening for the surface's return — no `sys.exit`, no fatal path added anywhere. - Liveness is gated by `_hardware_present()`, a `type=kernel` + name-match scan of `aconnect -l` reused from `midi-autoconnect.sh`'s `DIRECT_LEG_AWK` (`hw = ($0 ~ /type=kernel/ && $0 ~ /Launch Control XL|LCXL/)`). This matters because `lcxl3-driver.service` publishes a VIRTUAL port literally named 'ParVagues LCXL3' — the translated, corpus-numbered stream `resolve_watch_port()` deliberately prefers, since that is the CC numbering the grid and every `.tidal` file actually speak. That virtual client can outlive a physical unplug for a beat if the driver hasn't noticed yet, so a bare name match would report "still connected" against a ghost carrying nothing. The rebind tick distrusts a match ONLY when the matched label is itself LCXL-named and no real hardware backs it; a "Midi Through" catch-all match needs no hardware and is trusted as before. Content still comes from the preferred (possibly virtual) port — only the "is it actually there" judgement moved to hardware. - A user-pinned `-p` port keeps working, checked instead against `aseqdump -l`'s live listing (a pin surviving a client renumbering across replug is not guaranteed, same as any other resolved-by-address binding). - `tools/midiviz.service`: `Restart=on-failure` → `Restart=always` + `StartLimitIntervalSec=0` (moved to `[Unit]`, where it belongs — the first install attempt logged "Unknown key 'StartLimitIntervalSec' in section [Service], ignoring", caught before it shipped) as belt-and-braces under the in-process fix, since the unit holds no audio ports and a restart loop costs nothing real. Verified: - `--selftest`: `parsed=68 ingested=250 frames=159 paints=170 grabs=83 distinct_sampled_colours=165 platform=offscreen -> PASS`. - Reinstalled the unit (`install -m644` + `daemon-reload`); no more "Unknown key" warning in the journal on the next start. - Live restart: `ActiveState=active`, `MainPID=2728202`, `NRestarts=0`, bound to the "Midi Through" fallback (no LCXL physically plugged in tonight, confirming the hardware-gated fallback still works with zero surface present). - Port-loss simulation (couldn't unplug hardware; killed the reader's `aseqdump` child directly — the same failure shape as the source disappearing under it): child pid 2728206 -> `<defunct>`; within the next 2s rebind tick a fresh `aseqdump -p 14:0` (pid 2730846) appeared as midiviz's child. Main PID stayed 2728202 throughout, `NRestarts` stayed 0, `ActiveState` stayed `active` — recovered entirely IN-PROCESS, no systemd restart needed. CPU time kept accumulating (1.237s over 38s wall) proving the paint timer never stopped ticking.
Showing
This diff is collapsed.
Click to expand it.