Skip to content

fix(desktop): declare Bluetooth usage strings so the sidecar stops crashing on Finder launch#88

Merged
cnjack merged 1 commit into
mainfrom
fix/desktop-bluetooth-info-plist
Jun 21, 2026
Merged

fix(desktop): declare Bluetooth usage strings so the sidecar stops crashing on Finder launch#88
cnjack merged 1 commit into
mainfrom
fix/desktop-bluetooth-info-plist

Conversation

@cnjack

@cnjack cnjack commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Problem

The notarized desktop bundle hangs forever on the splash (正在启动本地服务) when launched from Finder, even though make desktop-dev works fine. The local server never comes up.

Root cause

The sidecar binary links CoreBluetooth (tinygo.org/x/bluetooth, the optional BLE status channel). The moment any CoreBluetooth API is touched, macOS requires NSBluetoothAlwaysUsageDescription in the bundle Info.plist. Without it, the process is killed with SIGABRT (a TCC privacy abort) before the web server ever binds — and the abort goes to tccd's os_log, not the process stderr, so there's zero output. /api/health therefore never responds and the window never navigates off the splash.

Why it was so confusing:

  • Launch-context dependent. From a terminal the responsible app already holds the Bluetooth grant, so the sidecar survives — that's why make desktop-dev and running the binary by hand worked. Only a Finder/LaunchServices launch (responsible = jcode-desktop, no grant) aborts.
  • jcode --version survives (it exits before CoreBluetooth's async state callback fires); only jcode web dies. This misleads toward Go-version and hardened-runtime theories — both red herrings (ad-hoc-hardened + Go 1.25.8 builds run fine from a terminal).
  • Granting jcode Bluetooth permission in System Settings is the user-side confirm/workaround.

Changes

  • desktop/src-tauri/Info.plist (new) — declares NSBluetoothAlwaysUsageDescription + NSBluetoothPeripheralUsageDescription. Tauri auto-merges src-tauri/Info.plist into the generated bundle Info.plist. With the string present, macOS shows a permission prompt / denies gracefully instead of aborting.
  • desktop/src-tauri/src/sidecar.rs — defense-in-depth so a dying sidecar is never a silent infinite splash: tee the sidecar's stdout/stderr to app_log_dir()/jcode-sidecar.log + a recent-lines ring buffer, and on exit-before-ready show a failure dialog (with the output tail + log path) and quit.

Verification

  • make desktop-buildplutil -p .../jcode.app/Contents/Info.plist | grep Bluetooth shows both keys present (Tauri merge confirmed).
  • cargo check on the desktop crate passes with the sidecar.rs changes.

Reviewer notes

  • make desktop-dev does not catch this class of bug (it spawns the sidecar from a terminal context). Finder/open launch is required to reproduce native launch-context issues.
  • No code change to the BLE channel itself — it remains opt-in via cfg.Channel.BLEEnabled; this PR just makes touching CoreBluetooth non-fatal at the OS level.

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Startup failures now display error dialogs with detailed messages and log file paths instead of leaving the splash screen spinning.
  • Chores

    • Added macOS Bluetooth permission declarations.

…ashing on Finder launch

The notarized desktop bundle hung forever on the splash ("正在启动本地服务")
when launched from Finder, while `make desktop-dev` worked fine.

Root cause: the sidecar links CoreBluetooth (tinygo.org/x/bluetooth, the BLE
status channel). When any CoreBluetooth API is touched, macOS requires
NSBluetoothAlwaysUsageDescription in the bundle Info.plist — without it the
process is killed with SIGABRT (a TCC abort) before the web server ever binds,
with no output on its stderr pipe. So /api/health never responds and the
window never navigates off the splash.

It only reproduces on a Finder/LaunchServices launch: from a terminal the
responsible app already holds the Bluetooth grant, which is why dev (and running
the binary by hand) worked. `jcode --version` also survives because it exits
before CoreBluetooth's async state callback fires.

Changes:
- Add desktop/src-tauri/Info.plist with NSBluetoothAlwaysUsageDescription and
  NSBluetoothPeripheralUsageDescription. Tauri auto-merges src-tauri/Info.plist
  into the bundle Info.plist (verified: `make desktop-build` then
  `plutil -p .../Contents/Info.plist | grep Bluetooth`). macOS now prompts /
  denies gracefully instead of aborting.
- Harden sidecar.rs: tee the sidecar's stdout/stderr to
  app_log_dir()/jcode-sidecar.log plus a recent-lines ring buffer, and surface
  a failure dialog (with the tail + log path) when the sidecar exits before it
  becomes healthy, instead of leaving the splash spinning forever.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds macOS Bluetooth usage description entries to Info.plist for bundle permission compliance. Extends sidecar.rs with a persistent log file, an in-memory ring-buffer of recent output lines, an AtomicBool readiness flag, and a surface_startup_failure function that shows a blocking error dialog (with log path and output tail) and exits the process when the sidecar terminates early or fails its health poll.

Changes

Sidecar startup failure surface and macOS Bluetooth permission

Layer / File(s) Summary
macOS Bluetooth usage description plist
desktop/src-tauri/Info.plist
Adds NSBluetoothAlwaysUsageDescription and NSBluetoothPeripheralUsageDescription keys with usage purpose strings to the macOS app bundle plist.
Sidecar log infrastructure: imports, constants, log path
desktop/src-tauri/src/sidecar.rs
Adds VecDeque, Write, AtomicBool, Arc, Mutex imports; defines HEALTH_POLL_ATTEMPTS, POLL_INTERVAL, MAX_RECENT_LINES constants; implements sidecar_log_path() with app-log-dir creation and temp-dir fallback; wires log_path into start.
stdout/stderr pump with readiness flag and ring buffer
desktop/src-tauri/src/sidecar.rs
Replaces the previous output pump with one that writes each line to the desktop log and on-disk file, maintains a capped VecDeque ring buffer, tracks readiness via AtomicBool, and calls surface_startup_failure when the sidecar exits before becoming ready.
Health-poll early-exit and surface_startup_failure dialog
desktop/src-tauri/src/sidecar.rs
Updates the health-polling thread to clone the readiness flag and abort early on failure; after exhausting the poll budget shows the main window and calls surface_startup_failure, which builds a dialog with the log path and ring-buffer tail, shows it on a blocking thread, and exits with status 1.

Sequence Diagram(s)

sequenceDiagram
  participant Pump as stdout/stderr pump
  participant Health as health poll thread
  participant Surface as surface_startup_failure
  participant Dialog as blocking dialog thread

  rect rgba(100, 149, 237, 0.5)
    note over Pump: Normal output
    Pump->>Pump: write line → log file + ring buffer
  end

  rect rgba(220, 80, 80, 0.5)
    note over Pump,Health: Failure paths
    Pump->>Surface: sidecar exits before ready
    Health->>Health: poll health_ok() × HEALTH_POLL_ATTEMPTS
    Health->>Surface: attempts exhausted
  end

  Surface->>Dialog: spawn thread → blocking dialog(log_path, recent tail)
  Dialog-->>Surface: closed
  Surface->>Surface: process::exit(1)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 A sidecar that crashes? No silent despair!
We log every line with the utmost of care,
A ring buffer spins, a plist declares,
Bluetooth may ask, and the dialog blares —
Exit code one, but at least we know where! 🪵

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main change: declaring Bluetooth usage strings in the plist to fix the sidecar crash on Finder launch, which is the primary objective of the PR.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/desktop-bluetooth-info-plist

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@desktop/src-tauri/src/sidecar.rs`:
- Around line 131-139: The CommandEvent::Terminated handler calls
surface_startup_failure when the sidecar exits before the app is ready, but it
never sets pump_ready to true. This creates a race condition where the poll
thread (at line 174) can also detect that ready is false and call
surface_startup_failure a second time, resulting in duplicate error dialogs.
After calling surface_startup_failure in the CommandEvent::Terminated branch,
add pump_ready.store(true, Ordering::SeqCst) to prevent the poll thread from
also triggering the failure handler. Additionally, the log message at line 133
unconditionally reports "exited before ready" even for normal shutdowns after
the app became healthy, so consider making this log conditional to only appear
when pump_ready is actually false.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b8a50301-2372-47ab-88c4-367cf24d27c8

📥 Commits

Reviewing files that changed from the base of the PR and between a3fbf9e and 0d2f456.

📒 Files selected for processing (2)
  • desktop/src-tauri/Info.plist
  • desktop/src-tauri/src/sidecar.rs

Comment thread desktop/src-tauri/src/sidecar.rs
@cnjack cnjack merged commit 450ba06 into main Jun 21, 2026
3 checks passed
@cnjack cnjack deleted the fix/desktop-bluetooth-info-plist branch June 21, 2026 03:12
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.

1 participant