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: don't throw exception for properly configured GUI apps #1581

Merged
merged 11 commits into from
Jun 1, 2024
Merged
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Fixed
* Fixed ``discovered_devices_and_advertisement_data`` returning devices that should
be filtered out by service UUIDs. Fixes #1576.
* Fixed a ``Descriptor None was not found!`` exception occurring in ``start_notify()`` on Android. Fixes #823.
* Fixed exception raised when starting ``BleakScanner`` while running in a Windows GUI app.

`0.22.1`_ (2024-05-07)
======================
Expand Down
2 changes: 1 addition & 1 deletion bleak/backends/winrt/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ async def start(self) -> None:

# Callbacks for WinRT async methods will never happen in STA mode if
# there is nothing pumping a Windows message loop.
assert_mta()
await assert_mta()

# start with fresh list of discovered devices
self.seen_devices = {}
Expand Down
79 changes: 72 additions & 7 deletions bleak/backends/winrt/util.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,49 @@
import asyncio
import ctypes
from ctypes import wintypes
from enum import IntEnum
from typing import Tuple

from ...exc import BleakError


def _check_result(result, func, args):
if not result:
raise ctypes.WinError()

return args


def _check_hresult(result, func, args):
if result:
raise ctypes.WinError(result)

return args


# not defined in wintypes
if ctypes.sizeof(ctypes.c_long) == ctypes.sizeof(ctypes.c_void_p):
_UINT_PTR = ctypes.c_ulong
elif ctypes.sizeof(ctypes.c_longlong) == ctypes.sizeof(ctypes.c_void_p):
_UINT_PTR = ctypes.c_ulonglong

dlech marked this conversation as resolved.
Show resolved Hide resolved
# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-timerproc
_TIMERPROC = ctypes.WINFUNCTYPE(
None, wintypes.HWND, _UINT_PTR, wintypes.UINT, wintypes.DWORD
)

# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-settimer
_SetTimer = ctypes.windll.user32.SetTimer
_SetTimer.restype = _UINT_PTR
_SetTimer.argtypes = [wintypes.HWND, _UINT_PTR, wintypes.UINT, _TIMERPROC]
_SetTimer.errcheck = _check_result

# https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-killtimer
_KillTimer = ctypes.windll.user32.KillTimer
_KillTimer.restype = wintypes.BOOL
_KillTimer.argtypes = [wintypes.HWND, wintypes.UINT]


# https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-cogetapartmenttype
_CoGetApartmentType = ctypes.windll.ole32.CoGetApartmentType
_CoGetApartmentType.restype = ctypes.c_int
Expand Down Expand Up @@ -60,28 +92,61 @@ def _get_apartment_type() -> Tuple[_AptType, _AptQualifierType]:
return _AptType(api_type.value), _AptQualifierType(api_type_qualifier.value)


def assert_mta() -> None:
async def assert_mta() -> None:
"""
Asserts that the current apartment type is MTA.

Raises:
BleakError: If the current apartment type is not MTA.

.. versionadded:: 0.22

.. versionchanged:: unreleased
dlech marked this conversation as resolved.
Show resolved Hide resolved
"""
if hasattr(allow_sta, "_allowed"):
return

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
if e.winerror == _CO_E_NOTINITIALIZED:
return

raise

if apt_type == _AptType.MTA:
# if we get here, WinRT probably set the apartment type to MTA and all
# is well, we don't need to check again
setattr(assert_mta, "_allowed", True)
return

event = asyncio.Event()

def wait_event(*_):
event.set()

# have to keep a reference to the callback or it will be garbage collected
# before it is called
callback = _TIMERPROC(wait_event)

# set a timer to see if we get a callback to ensure the windows event loop
# is running
timer = _SetTimer(None, 1, 0, callback)

try:
async with asyncio.timeout(1):
dlech marked this conversation as resolved.
Show resolved Hide resolved
await event.wait()
except asyncio.TimeoutError:
raise BleakError(
"Thread is configured for Windows GUI but callbacks are not working. Suspect PyWin32 unwanted side effects."
dlech marked this conversation as resolved.
Show resolved Hide resolved
)
else:
# if the windows event loop is running, we assume it is going to keep
# running and we don't need to check again
setattr(assert_mta, "_allowed", True)
finally:
_KillTimer(None, timer)


def allow_sta():
dlech marked this conversation as resolved.
Show resolved Hide resolved
Expand Down
4 changes: 2 additions & 2 deletions docs/troubleshooting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ isn't a message loop running. Bleak needs to run in a Multi Threaded Apartment

Bleak should detect this and raise an exception with a message similar to::

The current thread apartment type is not MTA: STA.
Thread is configured for Windows GUI but callbacks are not working.

To work around this, you can use one of the utility functions provided by Bleak.

Expand All @@ -202,7 +202,7 @@ thread then call ``allow_sta()`` before calling any other Bleak APis::
pass

dlech marked this conversation as resolved.
Show resolved Hide resolved
The more typical case, though, is that some library has imported something like
``pywin32`` which breaks Bleak. In this case, you can uninitialize the threading
``win32com`` which breaks Bleak. In this case, you can uninitialize the threading
model like this::

import win32com # this sets current thread to STA :-(
Expand Down
Loading