Skip to content
This repository was archived by the owner on May 31, 2026. It is now read-only.

fix: sentryusb-ble.py waits for BlueZ adapter to appear before bailing - #47

Closed
chufleco wants to merge 1 commit into
Scottmg1:main-devfrom
chufleco:fix-ble-adapter-boot-race
Closed

chufleco wants to merge 1 commit into
Scottmg1:main-devfrom
chufleco:fix-ble-adapter-boot-race

Conversation

@chufleco

Copy link
Copy Markdown

Replaces the one-shot find_adapter() call at startup with a bounded poll (15s timeout, 200ms interval). The unit's existing busctl wait loop only confirms BlueZ is running — BlueZ exposes /org/bluez/hci0 asynchronously after claiming its bus name. A single GetManagedObjects() can race ahead of adapter enumeration on slow boots, busy SD cards, or service restarts, causing the script to exit 1.

Reproduced live on Rock Pi 4C+ running SentryUSB v3.6.2 (2026-04-26):

14:30      Pi boot
14:30–14:44 (14 min)  service inactive — StartLimitBurst exhausted at boot
14:44:53   manual `systemctl start sentryusb-ble` resets counter → fails
           again with same race
14:45:00   systemd auto-retry (RestartSec=5)            → succeeds

The "Restart=always handles it" assumption only holds when the first 5-attempt burst (default StartLimitBurst=5 in 10s) succeeds. If the race wins all 5 attempts, systemd gives up and the service stays inactive until next reboot. Was 14 minutes of dead BLE in this reproduction.

Same risk applies to every restart, not just boot:

First-principle fix: a service that depends on a resource should wait for the resource, not exit when it's missing. wait_for_adapter() polls every 200ms for ~5 iterations until BlueZ exposes the adapter. Negligible CPU during the typical sub-second wait window.

Defense in depth in the unit file: bump StartLimitBurst to 20 within 120s so unrelated future failures (BlueZ crash, OOM, etc.) don't get locked out either. Not load-bearing for the race — Python handles that.

Why polling instead of D-Bus InterfacesAdded signal: Signal subscription has its own race (if adapter exists at subscribe time you miss the event — still need a fallback poll). Polling once every 200ms for ~5 iterations is effectively free, no concurrency complexity.

Files touched:

  • server/ble/sentryusb-ble.py — new wait_for_adapter() helper, replaces the existing one-shot check at startup
  • server/ble/sentryusb-ble.service — adds StartLimitBurst=20 and StartLimitIntervalSec=120

Verified v3.8.9-beta.4 does not contain this fix; PR is not duplicating existing work.

Replaces the one-shot find_adapter() call at startup with a bounded poll
(15s timeout, 200ms interval). The unit's existing busctl wait loop only
confirms BlueZ is running — BlueZ exposes /org/bluez/hci0 asynchronously
after claiming its bus name. A single GetManagedObjects() can race ahead
of adapter enumeration on slow boots, busy SD cards, or service restarts,
causing the script to exit 1.

Reproduced live on Rock Pi 4C+ running SentryUSB v3.6.2 (2026-04-26):

  14:30      Pi boot
  14:30-14:44 (14 min)  service inactive — StartLimitBurst exhausted at boot
  14:44:53   manual `systemctl start sentryusb-ble` resets counter -> fails
             again with same race
  14:45:00   systemd auto-retry (RestartSec=5)            -> succeeds

The "Restart=always handles it" assumption only holds when the first
5-attempt burst (default StartLimitBurst=5 in 10s) succeeds. If the race
wins all 5 attempts, systemd gives up and the service stays inactive
until next reboot. Was 14 minutes of dead BLE in this reproduction.

Same risk applies to every restart, not just boot:
- After archiveloop's awake_stop (PR Scottmg1#43 fixed the loop, not the race)
- After OOM kill or process crash
- After daily RuntimeMaxSec=86400 forced restart
- During Pi OTA updates that bounce the service

First-principle fix: a service that depends on a resource should wait for
the resource, not exit when it's missing. wait_for_adapter() polls every
200ms for ~5 iterations until BlueZ exposes the adapter. Negligible CPU
during the typical sub-second wait window.

Defense in depth in the unit file: bump StartLimitBurst to 20 within 120s
so unrelated future failures (BlueZ crash, OOM, etc.) don't get locked out
either. Not load-bearing for the race — Python handles that.

Why polling instead of D-Bus InterfacesAdded signal:
Signal subscription has its own race (if adapter exists at subscribe time
you miss the event — still need a fallback poll). Polling once every
200ms for ~5 iterations is effectively free, no concurrency complexity.

Files touched:
- server/ble/sentryusb-ble.py  — new wait_for_adapter() helper, replaces
                                  the existing one-shot check at startup
- server/ble/sentryusb-ble.service — adds StartLimitBurst=20 and
                                      StartLimitIntervalSec=120
@chufleco
chufleco marked this pull request as ready for review April 28, 2026 04:23
Scottmg1 pushed a commit to Sentry-Six/Sentry-USB-Rusty that referenced this pull request May 18, 2026
…ailing

Mirrors merged upstream Sentry-USB PR #47. The unit's `busctl status org.bluez`
ExecStartPre only confirms BlueZ has claimed its bus name — BlueZ exposes
`/org/bluez/hci0` asynchronously *after* that claim. A single
`GetManagedObjects()` can race ahead of adapter enumeration on slow boots,
busy SD/eMMC cards, or service restarts (after archiveloop's awake_stop,
OOM kill, RuntimeMaxSec, etc.), causing the script to exit 1.

First-principles fix: wait for the dependency we actually need (the LE
adapter object), not just the bus name. Bounded poll: 15s timeout, 200ms
interval. Simpler than `InterfacesAdded` signal subscription (which has its
own race when the adapter already exists at subscribe time).

**Validated live on Rock Pi 4C+ / DietPi + Rusty v2.6.6 (2026-05-18):**
After applying this patch + a clean reboot, sentryusb-ble.py finds the
adapter on the first poll (sub-200ms), GATT app + advertisement register
within 1s of service start, and an incoming central connects + authenticates
6 seconds after service start. Previously the service required 1-2
StartLimitBurst retries to land successfully after each boot, leaving the
BLE peripheral unavailable for the first 30+ seconds.

Defense in depth: bump `StartLimitBurst` to 20 within `StartLimitIntervalSec=120`.
The Python `wait_for_adapter()` handles 99% of cases on the first attempt; the
unit-level bump is insurance for unrelated failure modes (BlueZ crash, OOM,
etc.) where systemd's default 5-attempts-in-10s budget would be exhausted.

Refs Scottmg1/Sentry-USB#47 (the merged Go-version PR with the same fix).
@Scottmg1 Scottmg1 closed this May 24, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants