Attach the runtime threads to the JVM, or nothing scans

Every scan on the phone failed with "bluetooth error: JNI call failed",
which is the entire symptom: jni's Display for JniCall drops the source
that says what actually went wrong. It was ThreadDetached.

droidplug reaches the JVM through JavaVM::get_env(), which does not
attach — it fails outright on any thread the JVM has never seen. Every
BLE call in this app is made from a Tauri task, and Tauri's default
runtime spawns plain Rust worker threads, so on Android no BLE call could
ever have worked. This was invisible until it ran on hardware: the
desktop build shares the code and does not care.

Attaching inside the tasks would not have fixed it. A Tokio task can move
to another worker at any .await, so the thread that starts a scan is not
necessarily the one that polls it next — the attachment has to belong to
the threads, not the work. on_thread_start is the hook that gets that
right, and it covers the blocking pool too. Permanent rather than scoped,
because a scoped attachment detaches at the end of the guard, which for a
worker thread means after the first task it runs.

Ordering is load-bearing at both ends. The JVM is stashed in
initBtleplug, which runs before the super chain that starts us, so it is
there when the runtime is built; and the runtime is installed before
tauri::Builder, because async_runtime::set only affects later spawns.

The scan log now carries the Debug form as well as Display. The chain
read `Bluetooth(Other(JniCall(ThreadDetached)))` all along and would have
named this in the first minute rather than the last.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-05 20:26:59 +02:00
co-authored by Claude Opus 5
parent aa99b83c40
commit a4670f4992
3 changed files with 81 additions and 1 deletions
+6
View File
@@ -52,6 +52,12 @@ fn init_tracing() {
pub fn run() {
init_tracing();
// Before anything spawns: every BLE call is made from a Tauri task, and on
// Android a task running on a thread the JVM has never seen cannot reach
// droidplug at all. See `android::install_async_runtime`.
#[cfg(target_os = "android")]
android::install_async_runtime();
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.manage(AppState::new())