From 1b728c4ee6f9bffa935c59c566f1972a31e5dbe3 Mon Sep 17 00:00:00 2001 From: Muad'Dib Date: Sat, 16 May 2026 17:20:43 +0200 Subject: [PATCH] fix(android): clean up stale disconnected session in session_add MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On mobile, sessionId is a constant per-app-lifetime UUID (_constSessionId at flutter/lib/models/model.dart:57). When a prior connection wedges silently after Android Doze pauses the io_loop long enough that no error path fires and session_close is never called, the entry stays in the SESSIONS map. The next user tap calls sessionAddSync which bails with "same session id is found", but the Flutter side ignores that error (the return is bound to a `// ignore: unused_local_variable` at model.dart:3723) and calls sessionStart anyway. session_start_ then finds the stale handler with its old event_stream still attached, the is_connected short-circuit prevents a new io_loop from spawning, and the user sees "Connecting..." forever with no error surfaced. Detect the stale case in session_add via the existing ConnectionRoundState::Disconnected state (set by io_loop on exit in src/client/io_loop.rs:341) and clean up the prior session before bailing. This is safe — when state is Disconnected, the prior io_loop has provably exited. Fixes #15060 Co-Authored-By: Claude Opus 4.7 (1M context) --- src/flutter.rs | 17 ++++++++++++++--- src/ui_session_interface.rs | 4 ++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/flutter.rs b/src/flutter.rs index f8b04bf6c..a10f11899 100644 --- a/src/flutter.rs +++ b/src/flutter.rs @@ -1324,11 +1324,22 @@ pub fn session_add( // to-do: check the same id session. if let Some(session) = sessions::get_session_by_session_id(&session_id) { - if session.lc.read().unwrap().conn_type != conn_type { + // Mobile reuses one constant session_id for the entire app lifetime + // (_constSessionId in flutter/lib/models/model.dart). When a prior + // connection wedges silently — typically after Android Doze pauses the + // io_loop long enough that no error path fires and session_close is + // never called — the entry stays in SESSIONS. Detect that here and + // clean it up so the new session_add can succeed instead of bailing, + // which would leave the user stuck on a "Connecting..." spinner with + // no error surfaced (the Flutter side ignores sessionAddSync's return). + if session.connection_round_state.lock().unwrap().is_disconnected() { + sessions::remove_session_by_session_id(&session_id); + } else if session.lc.read().unwrap().conn_type != conn_type { bail!("same session id is found with different conn type?"); + } else { + // The same session is added before? + bail!("same session id is found"); } - // The same session is added before? - bail!("same session id is found"); } LocalConfig::set_remote_id(&id); diff --git a/src/ui_session_interface.rs b/src/ui_session_interface.rs index e6c8ac6a2..5793a61c5 100644 --- a/src/ui_session_interface.rs +++ b/src/ui_session_interface.rs @@ -128,6 +128,10 @@ impl ConnectionRoundState { true } } + + pub fn is_disconnected(&self) -> bool { + matches!(self.state, ConnectionState::Disconnected) + } } impl Default for ConnectionRoundState {