Skip to content

  • Projects
  • Groups
  • Snippets
  • Help
  • This project
    • Loading...
  • Sign in / Register
T
Tidal
  • Overview
    • Overview
    • Details
    • Activity
    • Cycle Analytics
  • Repository
    • Repository
    • Files
    • Commits
    • Branches
    • Tags
    • Contributors
    • Graph
    • Compare
    • Charts
  • Issues 0
    • Issues 0
    • List
    • Board
    • Labels
    • Milestones
  • Merge Requests 0
    • Merge Requests 0
  • CI / CD
    • CI / CD
    • Pipelines
    • Jobs
    • Schedules
    • Charts
  • Wiki
    • Wiki
  • Snippets
    • Snippets
  • Members
    • Members
  • Collapse sidebar
  • Activity
  • Graph
  • Charts
  • Create a new issue
  • Jobs
  • Commits
  • Issue Boards
  • PLN
  • Tidal
  • Repository

Switch branch/tag
  • Tidal
  • tools
  • midiviz.service
Find file
BlameHistoryPermalink
  • PLN (Algolia)'s avatar
    fix(midiviz): survive an LCXL unplug/replug in-process, no more clean exit · e9d16314
    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.
    PLN (Algolia) authored Sep 05, 2026
    e9d16314
midiviz.service 1.93 KB
EditWeb IDE
×

Replace midiviz.service

Attach a file by drag & drop or click to upload


Cancel
A new branch will be created in your fork and a new merge request will be started.