Skip to content

fix: recover from stale libusb context in list_printers (v1.8.1)#46

Merged
szrudi merged 2 commits into
mainfrom
fix/list-printers-stale-libusb-context
Jun 13, 2026
Merged

fix: recover from stale libusb context in list_printers (v1.8.1)#46
szrudi merged 2 commits into
mainfrom
fix/list-printers-stale-libusb-context

Conversation

@szrudi

@szrudi szrudi commented Jun 13, 2026

Copy link
Copy Markdown
Collaborator

Problem

On a long-running deployment (observed on label.hakhorst.eu / hector), /api/printers returned [] even though the Dymo was plugged in and powered. With no printer detected, the client has no printer id to key against, so effectivePrinterId() returns undefined, usePrinterSettings.persist() no-ops, and per-printer settings silently stop being saved.

Root cause

Not lost USB access — a stale process-wide libusb context. pyusb caches its libusb context at module level. When the Dymo re-enumerates to a new bus address (replug, or our own USB power-cycle), the cached context keeps returning an empty scan even though the device is physically attached. The codebase already knew about this (usb_power.invalidate_libusb_cache, called after power_on()), but list_printers() never invalidated — that asymmetry was the bug.

Confirmed on the box: a fresh libusb context inside the stuck container enumerated the printer instantly, while the 8h-old app process saw nothing. uhubctl (libusb-independent) showed Port 3 … connect [0922:1002 …] throughout.

Fix

list_printers() now recovers from this state. On an empty scan, if uhubctl still sees the DYMO on a hub port, drop the cached libusb context and rescan once — so the printer reappears without a process/container restart.

The uhubctl gate is deliberate and addresses the existing comment's concern: re-creating the libusb context can trigger a kernel hub auto-resume that re-energizes a port. By only refreshing when uhubctl confirms a device is physically present, we never resume a deliberately powered-off port (no device on the bus → gate is False).

  • promote usb_power._invalidate_libusb_cache → public invalidate_libusb_cache
  • add usb_power.printer_attached() (uhubctl-based, best-effort presence check)
  • refactor the USB scan into _scan_real_printers() + recovery-aware _list_real_printers()

Testing

  • 4 new regression tests (TestStaleLibusbContextRecovery): recovers when attached; no refresh when uhubctl sees nothing (respects power-off); no uhubctl probe on the happy path; graceful empty when recovery still finds nothing.
  • Full backend suite: 283 passing.
  • Validated the gate would fire in the current stuck production state.

Version

PATCH bump 1.8.01.8.1 (bug fix, no behavior change).

🤖 Generated with Claude Code

In a long-lived process pyusb caches its libusb context. When the Dymo
re-enumerates to a new bus address (replug, or our own USB power-cycle),
the cached context keeps returning an empty scan even though the printer
is physically attached — so /api/printers goes empty, the client has no
printer id to key against, and per-printer settings silently stop saving.

list_printers() now recovers: on an empty scan, if uhubctl (which is
libusb-independent) still sees the DYMO on a hub port, drop the cached
context and rescan once. The uhubctl gate is deliberate — it targets the
stale-context case and guarantees we never resume a deliberately
powered-off port (no device on the bus -> gate is False).

- promote usb_power._invalidate_libusb_cache -> public invalidate_libusb_cache
- add usb_power.printer_attached() (uhubctl-based presence check)
- 4 new regression tests; full suite 283 passing

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a long-running deployment issue where /api/printers could incorrectly return an empty list due to a stale, process-wide pyusb/libusb context after the DYMO re-enumerates. It adds a recovery path that conditionally invalidates libusb’s cached context and rescans, gated by an independent uhubctl-based presence check to avoid resuming deliberately powered-off ports.

Changes:

  • Promote libusb cache invalidation to usb_power.invalidate_libusb_cache() and add usb_power.printer_attached() (uhubctl-based presence gate).
  • Refactor printer enumeration into _scan_real_printers() and recovery-aware _list_real_printers() used by list_printers().
  • Add regression tests covering stale-context recovery behavior and bump package version to 1.8.1.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
server/usb_power.py Exposes libusb cache invalidation publicly and adds an uhubctl-based “printer attached” probe used as a recovery gate.
server/printer_service.py Refactors USB scanning and adds gated stale-libusb-context recovery for /api/printers.
server/tests/test_printer_service.py Adds regression tests ensuring recovery runs only when appropriate and remains quiet on the happy path.
package.json PATCH version bump to 1.8.1.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread server/usb_power.py
printer_attached() runs on the /api/printers read path. Calling through to
_run() when uhubctl isn't installed emits the latched "uhubctl missing"
WARNING that's meant for the deliberate power-control paths — surfacing a
power-feature warning during plain printer listing on setups that never use
power features (e.g. virtual-printer-only dev). Short-circuit with
shutil.which() so the best-effort probe stays silent and returns False.

Addresses Copilot review on PR #46.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@szrudi szrudi merged commit 3954863 into main Jun 13, 2026
4 of 5 checks passed
@szrudi szrudi deleted the fix/list-printers-stale-libusb-context branch June 13, 2026 17: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