From 39e4d75d5c8403445f19d82b06e8293fa29bab19 Mon Sep 17 00:00:00 2001 From: David Lechner Date: Tue, 30 Apr 2024 17:12:33 -0500 Subject: [PATCH] backends/winrt: raise exception when trying to scan with STA The pywin32-related packages will implicitly set the threading model to STA when imported. Since Bleak is using WinRT for async methods and we don't have a Windows event loop running, we need to let users know that Bleak is not going to work in this case. Also add a utility function and troubleshooting docs to provide a workaround for this issue. Fixes: https://github.com/hbldh/bleak/issues/1132 --- CHANGELOG.rst | 2 + bleak/backends/winrt/scanner.py | 6 +++ bleak/backends/winrt/util.py | 96 +++++++++++++++++++++++++++++++++ docs/troubleshooting.rst | 26 +++++++++ 4 files changed, 130 insertions(+) create mode 100644 bleak/backends/winrt/util.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b69a4227..aef37052 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,6 +13,7 @@ and this project adheres to `Semantic Versioning = (3, 12): from winrt.windows.devices.bluetooth.advertisement import ( BluetoothLEAdvertisementReceivedEventArgs, @@ -224,6 +226,10 @@ async def start(self) -> None: if self.watcher: raise BleakError("Scanner already started") + # Callbacks for WinRT async methods will never happen in STA mode if + # there is nothing pumping a Windows message loop. + assert_mta() + # start with fresh list of discovered devices self.seen_devices = {} self._advertisement_pairs.clear() diff --git a/bleak/backends/winrt/util.py b/bleak/backends/winrt/util.py new file mode 100644 index 00000000..8ac6c390 --- /dev/null +++ b/bleak/backends/winrt/util.py @@ -0,0 +1,96 @@ +import ctypes +from enum import IntEnum +from typing import Tuple + +from ...exc import BleakError + + +def _check_hresult(result, func, args): + if result: + raise ctypes.WinError(result) + + return args + + +# https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cogetapartmenttype +_CoGetApartmentType = ctypes.windll.ole32.CoGetApartmentType +_CoGetApartmentType.restype = ctypes.c_int +_CoGetApartmentType.argtypes = [ + ctypes.POINTER(ctypes.c_int), + ctypes.POINTER(ctypes.c_int), +] +_CoGetApartmentType.errcheck = _check_hresult + +_CO_E_NOTINITIALIZED = -2147221008 + + +# https://learn.microsoft.com/en-us/windows/win32/api/objidl/ne-objidl-apttype +class _AptType(IntEnum): + CURRENT = -1 + STA = 0 + MTA = 1 + NA = 2 + MAIN_STA = 3 + + +# https://learn.microsoft.com/en-us/windows/win32/api/objidl/ne-objidl-apttypequalifier +class _AptQualifierType(IntEnum): + NONE = 0 + IMPLICIT_MTA = 1 + NA_ON_MTA = 2 + NA_ON_STA = 3 + NA_ON_IMPLICIT_STA = 4 + NA_ON_MAIN_STA = 5 + APPLICATION_STA = 6 + RESERVED_1 = 7 + + +def _get_apartment_type() -> Tuple[_AptType, _AptQualifierType]: + """ + Calls CoGetApartmentType to get the current apartment type and qualifier. + + Returns: + The current apartment type and qualifier. + Raises: + OSError: If the call to CoGetApartmentType fails. + """ + api_type = ctypes.c_int() + api_type_qualifier = ctypes.c_int() + _CoGetApartmentType(ctypes.byref(api_type), ctypes.byref(api_type_qualifier)) + return _AptType(api_type.value), _AptQualifierType(api_type_qualifier.value) + + +def assert_mta() -> None: + """ + Asserts that the current apartment type is MTA. + + Raises: + BleakError: If the current apartment type is not MTA. + """ + try: + apt_type, _ = _get_apartment_type() + if apt_type != _AptType.MTA: + raise BleakError( + f"The current thread apartment type is not MTA: {apt_type.name}. Beware of packages like pywin32 that may change the apartment type implicitly." + ) + except OSError as e: + # All is OK if not initialized yet. WinRT will initialize it. + if e.winerror != _CO_E_NOTINITIALIZED: + raise + + +def uninitialize_sta(): + """ + Uninitialize the COM library on the current thread if it was not initialized + as MTA. + + This is intended to undo the implicit initialization of the COM library as STA + by packages like pywin32. + + It should be called as early as possible in your application after the + offending package has been imported. + """ + try: + assert_mta() + except BleakError: + ctypes.windll.ole32.CoUninitialize() diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst index 441dcade..e299d18f 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -65,6 +65,32 @@ See `#635 `_ and `#720 `_ for more information including some partial workarounds if you need to support these macOS versions. +------------ +Windows Bugs +------------ + +Not working when threading model is STA +======================================= + +Packages like ``pywin32`` and it's subsidiaries have an unfortunate side effect +of initializing the threading model to Single Threaded Apartment (STA) when +imported. This causes async WinRT functions to never complete. because there +isn't a message loop running. Bleak needs to run in a Multi Threaded Apartment +(MTA) instead (this happens automatically on the first WinRT call). + +Bleak should detect this and raise an exception with a message similar to:: + + The current thread apartment type is not MTA: STA. + +To work around this, you can use a utility function provided by Bleak to +uninitialize the threading model after importing an offending package:: + + import win32com # this sets current thread to STA :-( + from bleak.backends.winrt.utils import uninitialize_sta + + uninitialize_sta() # undo the unwanted side effect + + -------------- Enable Logging --------------