From 8364e1c4d53dccadbf0b613efe352136c62333b9 Mon Sep 17 00:00:00 2001 From: Alexis Date: Thu, 13 Nov 2025 14:29:42 +0100 Subject: [PATCH 1/3] Fix asyncio event loop cleanup with SSO auth Previously, when applications using SSO authentication exited, the asyncio event loop was closed while background tasks were still running, resulting in: - ERROR: Task was destroyed but it is pending - RuntimeError: Event loop is closed This issue occurred specifically with SSO authentication flows that use push notifications, but not with password-only authentication. This fix ensures proper cleanup by: 1. Cancelling all pending tasks before stopping the event loop 2. Giving tasks time (0.3s) to handle CancelledError gracefully 3. Waiting for the event loop thread to finish before closing This prevents "Task was destroyed but it is pending" errors when shutting down applications that use SSO with push notifications. --- keepersdk-package/src/keepersdk/background.py | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/keepersdk-package/src/keepersdk/background.py b/keepersdk-package/src/keepersdk/background.py index cdd0bb81..4540467b 100644 --- a/keepersdk-package/src/keepersdk/background.py +++ b/keepersdk-package/src/keepersdk/background.py @@ -24,11 +24,6 @@ def init() -> None: time.sleep(0.1) -async def _stop_loop(): - if _loop and _loop.is_running(): - _loop.stop() - - def get_loop() -> asyncio.AbstractEventLoop: global _loop assert _loop @@ -39,9 +34,28 @@ def stop() -> None: global _thread, _loop if isinstance(_thread, threading.Thread) and _thread.is_alive(): assert _loop is not None - asyncio.run_coroutine_threadsafe(_stop_loop(), _loop) - _thread.join(2) - _loop.close() + + # Cancel all pending tasks before stopping the loop + def cancel_all_tasks(): + tasks = [task for task in asyncio.all_tasks(_loop) if not task.done()] + for task in tasks: + task.cancel() + + _loop.call_soon_threadsafe(cancel_all_tasks) + + # Give tasks time to handle cancellation + time.sleep(0.3) + + # Stop the loop + _loop.call_soon_threadsafe(_loop.stop) + + # Wait for thread to finish + _thread.join(4) + + # Close the loop + if _loop and not _loop.is_closed(): + _loop.close() + _loop = None _thread = None From 1988b20ab3f734e35a066687bdeeb96c21082948 Mon Sep 17 00:00:00 2001 From: Alexis Date: Thu, 13 Nov 2025 18:13:44 +0100 Subject: [PATCH 2/3] Revert "Fix asyncio event loop cleanup with SSO auth" This reverts commit 8364e1c4d53dccadbf0b613efe352136c62333b9. --- keepersdk-package/src/keepersdk/background.py | 30 +++++-------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/keepersdk-package/src/keepersdk/background.py b/keepersdk-package/src/keepersdk/background.py index 4540467b..cdd0bb81 100644 --- a/keepersdk-package/src/keepersdk/background.py +++ b/keepersdk-package/src/keepersdk/background.py @@ -24,6 +24,11 @@ def init() -> None: time.sleep(0.1) +async def _stop_loop(): + if _loop and _loop.is_running(): + _loop.stop() + + def get_loop() -> asyncio.AbstractEventLoop: global _loop assert _loop @@ -34,28 +39,9 @@ def stop() -> None: global _thread, _loop if isinstance(_thread, threading.Thread) and _thread.is_alive(): assert _loop is not None - - # Cancel all pending tasks before stopping the loop - def cancel_all_tasks(): - tasks = [task for task in asyncio.all_tasks(_loop) if not task.done()] - for task in tasks: - task.cancel() - - _loop.call_soon_threadsafe(cancel_all_tasks) - - # Give tasks time to handle cancellation - time.sleep(0.3) - - # Stop the loop - _loop.call_soon_threadsafe(_loop.stop) - - # Wait for thread to finish - _thread.join(4) - - # Close the loop - if _loop and not _loop.is_closed(): - _loop.close() - + asyncio.run_coroutine_threadsafe(_stop_loop(), _loop) + _thread.join(2) + _loop.close() _loop = None _thread = None From 1e0985c73a933b8257123dd13c8900e936bb8a6d Mon Sep 17 00:00:00 2001 From: Alexis Date: Thu, 13 Nov 2025 21:23:40 +0100 Subject: [PATCH 3/3] Close login websocket after authentication During SSO authentication flows, a push notification websocket (LoginPushNotifications) is created to handle 2FA, device approval, and SSO data key requests. This websocket was never closed after successful login, causing it to remain active until application shutdown. This resulted in asyncio errors about pending tasks being destroyed. Fix: Close login.push_notifications in _on_logged_in() immediately after authentication completes and before any post-login setup. --- keepersdk-package/src/keepersdk/authentication/login_auth.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/keepersdk-package/src/keepersdk/authentication/login_auth.py b/keepersdk-package/src/keepersdk/authentication/login_auth.py index 8578cbc8..c31c3ba4 100644 --- a/keepersdk-package/src/keepersdk/authentication/login_auth.py +++ b/keepersdk-package/src/keepersdk/authentication/login_auth.py @@ -701,6 +701,11 @@ def _on_logged_in(login: LoginAuth, response: APIRequest_pb2.LoginResponse, auth_context.device_private_key = login.context.device_private_key auth_context.message_session_uid = login.context.message_session_uid + # Close login-time push notifications + if login.push_notifications: + login.push_notifications.shutdown() + login.push_notifications = None + keeper_endpoint = login.keeper_endpoint logged_auth = keeper_auth.KeeperAuth(keeper_endpoint, auth_context) logged_auth.post_login()