Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

backends/winrt: raise exception when trying to scan with STA #1556

Merged
merged 1 commit into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ and this project adheres to `Semantic Versioning <https://semver.org/spec/v2.0.0
Added
-----
* Added ``BleakCharacteristicNotFoundError`` which is raised if a device does not support a characteristic.
* Added utility function to work around ``pywin32`` setting threading model to STA on Windows.

Changed
-------
* Updated PyObjC dependency on macOS to v10.x.
* Updated missing Bluetooth SIG characteristics and service UUIDs.
* Updated ``BlueZManager`` to remove empty interfaces from `_properties` during InterfacesRemoved message.
* Updated PyWinRT dependency to v2. Fixes #1529.
* Raise exception when trying to scan while in a single-treaded apartment (STA) on Windows. Fixes #1132.

Fixed
-----
Expand Down
6 changes: 6 additions & 0 deletions bleak/backends/winrt/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from typing import Dict, List, Literal, NamedTuple, Optional
from uuid import UUID

from .util import assert_mta

if sys.version_info >= (3, 12):
from winrt.windows.devices.bluetooth.advertisement import (
BluetoothLEAdvertisementReceivedEventArgs,
Expand Down Expand Up @@ -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()
Expand Down
96 changes: 96 additions & 0 deletions bleak/backends/winrt/util.py
Original file line number Diff line number Diff line change
@@ -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()
26 changes: 26 additions & 0 deletions docs/troubleshooting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,32 @@ See `#635 <https://github.com/hbldh/bleak/issues/635>`_ and
`#720 <https://github.com/hbldh/bleak/issues/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
--------------
Expand Down
Loading