Skip to content

Commit

Permalink
backends/winrt: don't throw exeception for properly configued GUI apps
Browse files Browse the repository at this point in the history
In commit 4a653e6 ("backends/winrt: raise exception when trying to scan
with STA") we added a check to raise an exception when trying to scan
when PyWinRT set the aparatment model to STA. However, properly working
GUI apps will have the apartment model set to STA but Bleak will still
work because there is something pumping the Windows message loop.

We don't want to raise an exception in this case to avoid breaking
working apps. We can improve the test by checking if the current thread
is actually pumping the message loop by scheduling a callback via a
the win32 SetTimeout function. If the callback is called, then we know
that the message loop is being pumped. If not, then we probably are not
going to get async callbacks from the WinRT APIs and we raise an
exception in this case.
  • Loading branch information
dlech committed May 23, 2024
1 parent d45ec90 commit 69b8c69
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 10 deletions.
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

# 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
"""
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):
await event.wait()
except asyncio.TimeoutError:
raise BleakError(
"Thread is configured for Windows GUI but callbacks are not working. Suspect PyWin32 unwanted side effects."
)
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():
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

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

0 comments on commit 69b8c69

Please sign in to comment.