From a10cc9464fdf6d1f742f324b1c978c096df53d4e Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Sun, 29 Jul 2018 10:41:11 +0100 Subject: [PATCH 01/15] implement cancelable WaitForSingleObject for Windows --- trio/_core/_io_windows.py | 19 +++++++ trio/_core/_windows_cffi.py | 34 +++++++++++++ trio/_core/tests/test_multierror.py | 6 +-- trio/_core/tests/test_windows.py | 78 +++++++++++++++++++++++++++++ trio/tests/test_socket.py | 5 +- trio/tests/test_ssl.py | 12 ++--- 6 files changed, 139 insertions(+), 15 deletions(-) diff --git a/trio/_core/_io_windows.py b/trio/_core/_io_windows.py index 90a79c032..049fe4850 100644 --- a/trio/_core/_io_windows.py +++ b/trio/_core/_io_windows.py @@ -12,6 +12,7 @@ import attr from .. import _core +from .. import _timeouts from . import _public from ._wakeup_socketpair import WakeupSocketpair from .._util import is_main_thread @@ -109,6 +110,24 @@ def _handle(obj): return obj +async def WaitForSingleObject(handle): + """Async and cancelable variant of kernel32.WaitForSingleObject(). + + Args: + handle: A win32 handle. + + """ + # Wait here while the handle is not signaled. The 0 in WaitForSingleObject() + # means zero timeout; the function simply tells us the status of the handle. + # Possible values: WAIT_TIMEOUT, WAIT_OBJECT_0, WAIT_ABANDONED, WAIT_FAILED + # We exit in all cases except when the hadle is still unset. + while True: + await _timeouts.sleep_until(_core.current_time() + 0.01) + retcode = kernel32.WaitForSingleObject(handle, 0) + if retcode != ErrorCodes.WAIT_TIMEOUT: + break + + @attr.s(frozen=True) class _WindowsStatistics: tasks_waiting_overlapped = attr.ib() diff --git a/trio/_core/_windows_cffi.py b/trio/_core/_windows_cffi.py index 3e124e6b6..c8ff6af3d 100644 --- a/trio/_core/_windows_cffi.py +++ b/trio/_core/_windows_cffi.py @@ -35,6 +35,8 @@ typedef OVERLAPPED WSAOVERLAPPED; typedef LPOVERLAPPED LPWSAOVERLAPPED; +typedef PVOID LPSECURITY_ATTRIBUTES; +typedef PVOID LPCSTR; typedef struct _OVERLAPPED_ENTRY { ULONG_PTR lpCompletionKey; @@ -80,6 +82,34 @@ _In_opt_ void* HandlerRoutine, _In_ BOOL Add ); + +HANDLE CreateEventA( + LPSECURITY_ATTRIBUTES lpEventAttributes, + BOOL bManualReset, + BOOL bInitialState, + LPCSTR lpName +); + +BOOL SetEvent( + HANDLE hEvent +); + +BOOL ResetEvent( + HANDLE hEvent +); + +DWORD WaitForSingleObject( + HANDLE hHandle, + DWORD dwMilliseconds +); + +DWORD WaitForMultipleObjects( + DWORD nCount, + HANDLE *lpHandles, + BOOL bWaitAll, + DWORD dwMilliseconds +); + """ # cribbed from pywincffi @@ -116,6 +146,10 @@ def raise_winerror(winerror=None, *, filename=None, filename2=None): class ErrorCodes(enum.IntEnum): STATUS_TIMEOUT = 0x102 + WAIT_TIMEOUT = 0x102 + WAIT_ABANDONED = 0x80 + WAIT_OBJECT_0 = 0x00 # object is signaled + WAIT_FAILED = 0xFFFFFFFF ERROR_IO_PENDING = 997 ERROR_OPERATION_ABORTED = 995 ERROR_ABANDONED_WAIT_0 = 735 diff --git a/trio/_core/tests/test_multierror.py b/trio/_core/tests/test_multierror.py index 3692abfda..04c347f81 100644 --- a/trio/_core/tests/test_multierror.py +++ b/trio/_core/tests/test_multierror.py @@ -169,9 +169,9 @@ def simple_filter(exc): assert isinstance(orig.exceptions[0].exceptions[1], KeyError) # get original traceback summary orig_extracted = ( - extract_tb(orig.__traceback__) + - extract_tb(orig.exceptions[0].__traceback__) + - extract_tb(orig.exceptions[0].exceptions[1].__traceback__) + extract_tb(orig.__traceback__) + extract_tb( + orig.exceptions[0].__traceback__ + ) + extract_tb(orig.exceptions[0].exceptions[1].__traceback__) ) def p(exc): diff --git a/trio/_core/tests/test_windows.py b/trio/_core/tests/test_windows.py index 11fbed45f..090ba1666 100644 --- a/trio/_core/tests/test_windows.py +++ b/trio/_core/tests/test_windows.py @@ -6,8 +6,10 @@ pytestmark = pytest.mark.skipif(not on_windows, reason="windows only") from ... import _core +from ... import _timeouts if on_windows: from .._windows_cffi import ffi, kernel32 + from .._io_windows import WaitForSingleObject async def test_completion_key_listen(): @@ -38,5 +40,81 @@ async def post(key): print("end loop") +async def test_WaitForSingleObject(): + + # Set the timeout used in the tests. The resolution of WaitForSingleObject + # is 0.01 so anything more than a magnitude larger should probably do. + # If too large, the test become slow and we might need to mark it as @slow. + TIMEOUT = 0.1 + + # Test 1, handle is SET after 1 sec in separate coroutine + async def handle_setter(handle): + await _timeouts.sleep(TIMEOUT) + kernel32.SetEvent(handle) + + handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + t0 = _core.current_time() + + async with _core.open_nursery() as nursery: + nursery.start_soon(WaitForSingleObject, handle) + nursery.start_soon(handle_setter, handle) + + kernel32.CloseHandle(handle) + t1 = _core.current_time() + assert TIMEOUT <= (t1 - t0) < 1.1 * TIMEOUT + + # Test 2, handle is CLOSED after 1 sec + async def handle_closer(handle): + await _timeouts.sleep(TIMEOUT) + kernel32.CloseHandle(handle) + + handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + t0 = _core.current_time() + + async with _core.open_nursery() as nursery: + nursery.start_soon(WaitForSingleObject, handle) + nursery.start_soon(handle_closer, handle) + + t1 = _core.current_time() + assert TIMEOUT <= (t1 - t0) < 1.1 * TIMEOUT + + # Test 3, cancelation + + handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + t0 = _core.current_time() + + with _timeouts.move_on_after(TIMEOUT): + await WaitForSingleObject(handle) + + kernel32.CloseHandle(handle) + t1 = _core.current_time() + assert TIMEOUT <= (t1 - t0) < 1.1 * TIMEOUT + + # Test 4, already canceled + + handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + kernel32.SetEvent(handle) + t0 = _core.current_time() + + with _timeouts.move_on_after(TIMEOUT): + await WaitForSingleObject(handle) + + kernel32.CloseHandle(handle) + t1 = _core.current_time() + assert (t1 - t0) < 0.5 * TIMEOUT + + # Test 5, already closed + + handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + kernel32.CloseHandle(handle) + t0 = _core.current_time() + + with _timeouts.move_on_after(TIMEOUT): + await WaitForSingleObject(handle) + + t1 = _core.current_time() + assert (t1 - t0) < 0.5 * TIMEOUT + + # XX test setting the iomanager._iocp to something weird to make sure that the # IOCP thread can send exceptions back to the main thread diff --git a/trio/tests/test_socket.py b/trio/tests/test_socket.py index 1a3559aa4..1e699c363 100644 --- a/trio/tests/test_socket.py +++ b/trio/tests/test_socket.py @@ -773,9 +773,8 @@ async def getnameinfo(self, sockaddr, flags): (0, 0, 0, tsocket.AI_CANONNAME), ]: assert ( - await tsocket.getaddrinfo( - "localhost", "foo", *vals - ) == ("custom_gai", b"localhost", "foo", *vals) + await tsocket.getaddrinfo("localhost", "foo", *vals) == + ("custom_gai", b"localhost", "foo", *vals) ) # IDNA encoding is handled before calling the special object diff --git a/trio/tests/test_ssl.py b/trio/tests/test_ssl.py index 2b5a08314..6d40e2151 100644 --- a/trio/tests/test_ssl.py +++ b/trio/tests/test_ssl.py @@ -972,9 +972,7 @@ async def test_ssl_bad_shutdown(): async def test_ssl_bad_shutdown_but_its_ok(): client, server = ssl_memory_stream_pair( server_kwargs={"https_compatible": True}, - client_kwargs={ - "https_compatible": True - } + client_kwargs={"https_compatible": True} ) async with _core.open_nursery() as nursery: @@ -1039,9 +1037,7 @@ def close_hook(): async def test_ssl_https_compatibility_disagreement(): client, server = ssl_memory_stream_pair( server_kwargs={"https_compatible": False}, - client_kwargs={ - "https_compatible": True - } + client_kwargs={"https_compatible": True} ) async with _core.open_nursery() as nursery: @@ -1063,9 +1059,7 @@ async def receive_and_expect_error(): async def test_https_mode_eof_before_handshake(): client, server = ssl_memory_stream_pair( server_kwargs={"https_compatible": True}, - client_kwargs={ - "https_compatible": True - } + client_kwargs={"https_compatible": True} ) async def server_expect_clean_eof(): From 93313f626cb48dc345ef6fa3395a2fcdf0aae133 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Sun, 29 Jul 2018 11:08:33 +0100 Subject: [PATCH 02/15] using same yapf version as trio instead of latest --- trio/_core/tests/test_multierror.py | 6 +++--- trio/tests/test_socket.py | 5 +++-- trio/tests/test_ssl.py | 12 +++++++++--- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/trio/_core/tests/test_multierror.py b/trio/_core/tests/test_multierror.py index 04c347f81..3692abfda 100644 --- a/trio/_core/tests/test_multierror.py +++ b/trio/_core/tests/test_multierror.py @@ -169,9 +169,9 @@ def simple_filter(exc): assert isinstance(orig.exceptions[0].exceptions[1], KeyError) # get original traceback summary orig_extracted = ( - extract_tb(orig.__traceback__) + extract_tb( - orig.exceptions[0].__traceback__ - ) + extract_tb(orig.exceptions[0].exceptions[1].__traceback__) + extract_tb(orig.__traceback__) + + extract_tb(orig.exceptions[0].__traceback__) + + extract_tb(orig.exceptions[0].exceptions[1].__traceback__) ) def p(exc): diff --git a/trio/tests/test_socket.py b/trio/tests/test_socket.py index 1e699c363..1a3559aa4 100644 --- a/trio/tests/test_socket.py +++ b/trio/tests/test_socket.py @@ -773,8 +773,9 @@ async def getnameinfo(self, sockaddr, flags): (0, 0, 0, tsocket.AI_CANONNAME), ]: assert ( - await tsocket.getaddrinfo("localhost", "foo", *vals) == - ("custom_gai", b"localhost", "foo", *vals) + await tsocket.getaddrinfo( + "localhost", "foo", *vals + ) == ("custom_gai", b"localhost", "foo", *vals) ) # IDNA encoding is handled before calling the special object diff --git a/trio/tests/test_ssl.py b/trio/tests/test_ssl.py index 6d40e2151..2b5a08314 100644 --- a/trio/tests/test_ssl.py +++ b/trio/tests/test_ssl.py @@ -972,7 +972,9 @@ async def test_ssl_bad_shutdown(): async def test_ssl_bad_shutdown_but_its_ok(): client, server = ssl_memory_stream_pair( server_kwargs={"https_compatible": True}, - client_kwargs={"https_compatible": True} + client_kwargs={ + "https_compatible": True + } ) async with _core.open_nursery() as nursery: @@ -1037,7 +1039,9 @@ def close_hook(): async def test_ssl_https_compatibility_disagreement(): client, server = ssl_memory_stream_pair( server_kwargs={"https_compatible": False}, - client_kwargs={"https_compatible": True} + client_kwargs={ + "https_compatible": True + } ) async with _core.open_nursery() as nursery: @@ -1059,7 +1063,9 @@ async def receive_and_expect_error(): async def test_https_mode_eof_before_handshake(): client, server = ssl_memory_stream_pair( server_kwargs={"https_compatible": True}, - client_kwargs={"https_compatible": True} + client_kwargs={ + "https_compatible": True + } ) async def server_expect_clean_eof(): From c6a35cd95ece36b57d474bc1cf0393c9e6d86406 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Sun, 29 Jul 2018 15:45:26 +0100 Subject: [PATCH 03/15] implement WaitForSingleObject using a thread --- trio/_core/_io_windows.py | 55 ++++++++++++++++++++++++---- trio/_core/tests/test_windows.py | 63 +++++++++++++++++++++++--------- 2 files changed, 94 insertions(+), 24 deletions(-) diff --git a/trio/_core/_io_windows.py b/trio/_core/_io_windows.py index 049fe4850..f0ff2517b 100644 --- a/trio/_core/_io_windows.py +++ b/trio/_core/_io_windows.py @@ -111,19 +111,60 @@ def _handle(obj): async def WaitForSingleObject(handle): - """Async and cancelable variant of kernel32.WaitForSingleObject(). + """Async and cancellable variant of kernel32.WaitForSingleObject(). Args: handle: A win32 handle. """ - # Wait here while the handle is not signaled. The 0 in WaitForSingleObject() - # means zero timeout; the function simply tells us the status of the handle. - # Possible values: WAIT_TIMEOUT, WAIT_OBJECT_0, WAIT_ABANDONED, WAIT_FAILED - # We exit in all cases except when the hadle is still unset. + # Quick check; we might not even need to spawn a thread. The zero + # means a zero timeout; this call never blocks. We also exit here + # if the handle is already closed for some reason. + retcode = kernel32.WaitForSingleObject(handle, 0) + if retcode != ErrorCodes.WAIT_TIMEOUT: + return + + # :'( avoid circular imports + from .._threads import run_sync_in_worker_thread + + class StubLimiter: + def release_on_behalf_of(self, x): + pass + + async def acquire_on_behalf_of(self, x): + pass + + # Wait for a thread that waits for two handles: the handle plus a handle + # that we can use to cancel the thread. + cancel_handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + try: + await run_sync_in_worker_thread( + WaitForMultipleObjects_sync, + handle, + cancel_handle, + cancellable=True, + limiter=StubLimiter(), + ) + finally: + # Clean up our cancel handle. In case we get here because this task was + # cancelled, we also want to set the cancel_handle to stop the thread. + kernel32.SetEvent(cancel_handle) + kernel32.CloseHandle(cancel_handle) + + +def WaitForMultipleObjects_sync(*handles): + """Wait for any of the given Windows handles to be signaled. + + """ + n = len(handles) + handle_arr = ffi.new("HANDLE[{}]".format(n)) + for i in range(n): + handle_arr[i] = handles[i] + timeout = 1000 * 60 * 60 * 24 # todo: use INF here, whatever that is, and ditch the while while True: - await _timeouts.sleep_until(_core.current_time() + 0.01) - retcode = kernel32.WaitForSingleObject(handle, 0) + retcode = kernel32.WaitForMultipleObjects( + n, handle_arr, False, timeout + ) if retcode != ErrorCodes.WAIT_TIMEOUT: break diff --git a/trio/_core/tests/test_windows.py b/trio/_core/tests/test_windows.py index 090ba1666..78fdcaf63 100644 --- a/trio/_core/tests/test_windows.py +++ b/trio/_core/tests/test_windows.py @@ -1,4 +1,5 @@ import os +from threading import Thread import pytest on_windows = (os.name == "nt") @@ -9,7 +10,7 @@ from ... import _timeouts if on_windows: from .._windows_cffi import ffi, kernel32 - from .._io_windows import WaitForSingleObject + from .._io_windows import WaitForSingleObject, WaitForMultipleObjects_sync async def test_completion_key_listen(): @@ -40,18 +41,53 @@ async def post(key): print("end loop") +async def test_WaitForMultipleObjects_sync(): + # One handle + handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + t = Thread(target=WaitForMultipleObjects_sync, args=(handle1,)) + t.start() + kernel32.SetEvent(handle1) + t.join() # the test succeeds if we do not block here :) + kernel32.CloseHandle(handle1) + + # Two handles, signal first + handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + t = Thread(target=WaitForMultipleObjects_sync, args=(handle1, handle2)) + t.start() + kernel32.SetEvent(handle1) + t.join() # the test succeeds if we do not block here :) + kernel32.CloseHandle(handle1) + kernel32.CloseHandle(handle2) + + # Two handles, signal seconds + handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + t = Thread(target=WaitForMultipleObjects_sync, args=(handle1, handle2)) + t.start() + kernel32.SetEvent(handle2) + t.join() # the test succeeds if we do not block here :) + kernel32.CloseHandle(handle1) + kernel32.CloseHandle(handle2) + + # Closing the handle will not stop the thread. Initiating a wait on a + # closed handle will fail/return, but closing a handle that is already + # being waited on will not stop whatever is waiting for it. + + async def test_WaitForSingleObject(): # Set the timeout used in the tests. The resolution of WaitForSingleObject # is 0.01 so anything more than a magnitude larger should probably do. # If too large, the test become slow and we might need to mark it as @slow. - TIMEOUT = 0.1 + TIMEOUT = 0.5 - # Test 1, handle is SET after 1 sec in separate coroutine async def handle_setter(handle): await _timeouts.sleep(TIMEOUT) kernel32.SetEvent(handle) + # Test 1, handle is SET after 1 sec in separate coroutine + handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) t0 = _core.current_time() @@ -62,21 +98,11 @@ async def handle_setter(handle): kernel32.CloseHandle(handle) t1 = _core.current_time() assert TIMEOUT <= (t1 - t0) < 1.1 * TIMEOUT + print('test_WaitForSingleObject test 1 OK') - # Test 2, handle is CLOSED after 1 sec - async def handle_closer(handle): - await _timeouts.sleep(TIMEOUT) - kernel32.CloseHandle(handle) - - handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - t0 = _core.current_time() + # Test 2, handle is CLOSED after 1 sec - NOPE, wont work unless we use zero timeout - async with _core.open_nursery() as nursery: - nursery.start_soon(WaitForSingleObject, handle) - nursery.start_soon(handle_closer, handle) - - t1 = _core.current_time() - assert TIMEOUT <= (t1 - t0) < 1.1 * TIMEOUT + pass # Test 3, cancelation @@ -89,8 +115,9 @@ async def handle_closer(handle): kernel32.CloseHandle(handle) t1 = _core.current_time() assert TIMEOUT <= (t1 - t0) < 1.1 * TIMEOUT + print('test_WaitForSingleObject test 3 OK') - # Test 4, already canceled + # Test 4, already cancelled handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) kernel32.SetEvent(handle) @@ -102,6 +129,7 @@ async def handle_closer(handle): kernel32.CloseHandle(handle) t1 = _core.current_time() assert (t1 - t0) < 0.5 * TIMEOUT + print('test_WaitForSingleObject test 4 OK') # Test 5, already closed @@ -114,6 +142,7 @@ async def handle_closer(handle): t1 = _core.current_time() assert (t1 - t0) < 0.5 * TIMEOUT + print('test_WaitForSingleObject test 5 OK') # XX test setting the iomanager._iocp to something weird to make sure that the From b047a459cc86d4a61a729567173dbb91b61081dd Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Mon, 30 Jul 2018 11:43:37 +0200 Subject: [PATCH 04/15] replace loop wih info timeout in WaitForMultipleObjects_sync --- trio/_core/_io_windows.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/trio/_core/_io_windows.py b/trio/_core/_io_windows.py index f0ff2517b..d005a9288 100644 --- a/trio/_core/_io_windows.py +++ b/trio/_core/_io_windows.py @@ -160,13 +160,8 @@ def WaitForMultipleObjects_sync(*handles): handle_arr = ffi.new("HANDLE[{}]".format(n)) for i in range(n): handle_arr[i] = handles[i] - timeout = 1000 * 60 * 60 * 24 # todo: use INF here, whatever that is, and ditch the while - while True: - retcode = kernel32.WaitForMultipleObjects( - n, handle_arr, False, timeout - ) - if retcode != ErrorCodes.WAIT_TIMEOUT: - break + timeout = 0xffffffff # INFINITE + kernel32.WaitForMultipleObjects(n, handle_arr, False, timeout) # blocking @attr.s(frozen=True) From 58352ad58d4b9c086ae104826492b38471759ed4 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Mon, 30 Jul 2018 23:44:33 +0200 Subject: [PATCH 05/15] factor WaitForSingleObject into own file --- trio/_core/_io_windows.py | 55 ---------- trio/_core/tests/test_windows.py | 108 +------------------- trio/_wait_for_single_object.py | 55 ++++++++++ trio/tests/test_wait_for_single_object.py | 118 ++++++++++++++++++++++ 4 files changed, 174 insertions(+), 162 deletions(-) create mode 100644 trio/_wait_for_single_object.py create mode 100644 trio/tests/test_wait_for_single_object.py diff --git a/trio/_core/_io_windows.py b/trio/_core/_io_windows.py index d005a9288..90a79c032 100644 --- a/trio/_core/_io_windows.py +++ b/trio/_core/_io_windows.py @@ -12,7 +12,6 @@ import attr from .. import _core -from .. import _timeouts from . import _public from ._wakeup_socketpair import WakeupSocketpair from .._util import is_main_thread @@ -110,60 +109,6 @@ def _handle(obj): return obj -async def WaitForSingleObject(handle): - """Async and cancellable variant of kernel32.WaitForSingleObject(). - - Args: - handle: A win32 handle. - - """ - # Quick check; we might not even need to spawn a thread. The zero - # means a zero timeout; this call never blocks. We also exit here - # if the handle is already closed for some reason. - retcode = kernel32.WaitForSingleObject(handle, 0) - if retcode != ErrorCodes.WAIT_TIMEOUT: - return - - # :'( avoid circular imports - from .._threads import run_sync_in_worker_thread - - class StubLimiter: - def release_on_behalf_of(self, x): - pass - - async def acquire_on_behalf_of(self, x): - pass - - # Wait for a thread that waits for two handles: the handle plus a handle - # that we can use to cancel the thread. - cancel_handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - try: - await run_sync_in_worker_thread( - WaitForMultipleObjects_sync, - handle, - cancel_handle, - cancellable=True, - limiter=StubLimiter(), - ) - finally: - # Clean up our cancel handle. In case we get here because this task was - # cancelled, we also want to set the cancel_handle to stop the thread. - kernel32.SetEvent(cancel_handle) - kernel32.CloseHandle(cancel_handle) - - -def WaitForMultipleObjects_sync(*handles): - """Wait for any of the given Windows handles to be signaled. - - """ - n = len(handles) - handle_arr = ffi.new("HANDLE[{}]".format(n)) - for i in range(n): - handle_arr[i] = handles[i] - timeout = 0xffffffff # INFINITE - kernel32.WaitForMultipleObjects(n, handle_arr, False, timeout) # blocking - - @attr.s(frozen=True) class _WindowsStatistics: tasks_waiting_overlapped = attr.ib() diff --git a/trio/_core/tests/test_windows.py b/trio/_core/tests/test_windows.py index 78fdcaf63..c0ec5cce6 100644 --- a/trio/_core/tests/test_windows.py +++ b/trio/_core/tests/test_windows.py @@ -1,5 +1,5 @@ import os -from threading import Thread + import pytest on_windows = (os.name == "nt") @@ -7,10 +7,8 @@ pytestmark = pytest.mark.skipif(not on_windows, reason="windows only") from ... import _core -from ... import _timeouts if on_windows: from .._windows_cffi import ffi, kernel32 - from .._io_windows import WaitForSingleObject, WaitForMultipleObjects_sync async def test_completion_key_listen(): @@ -41,109 +39,5 @@ async def post(key): print("end loop") -async def test_WaitForMultipleObjects_sync(): - # One handle - handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - t = Thread(target=WaitForMultipleObjects_sync, args=(handle1,)) - t.start() - kernel32.SetEvent(handle1) - t.join() # the test succeeds if we do not block here :) - kernel32.CloseHandle(handle1) - - # Two handles, signal first - handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - t = Thread(target=WaitForMultipleObjects_sync, args=(handle1, handle2)) - t.start() - kernel32.SetEvent(handle1) - t.join() # the test succeeds if we do not block here :) - kernel32.CloseHandle(handle1) - kernel32.CloseHandle(handle2) - - # Two handles, signal seconds - handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - t = Thread(target=WaitForMultipleObjects_sync, args=(handle1, handle2)) - t.start() - kernel32.SetEvent(handle2) - t.join() # the test succeeds if we do not block here :) - kernel32.CloseHandle(handle1) - kernel32.CloseHandle(handle2) - - # Closing the handle will not stop the thread. Initiating a wait on a - # closed handle will fail/return, but closing a handle that is already - # being waited on will not stop whatever is waiting for it. - - -async def test_WaitForSingleObject(): - - # Set the timeout used in the tests. The resolution of WaitForSingleObject - # is 0.01 so anything more than a magnitude larger should probably do. - # If too large, the test become slow and we might need to mark it as @slow. - TIMEOUT = 0.5 - - async def handle_setter(handle): - await _timeouts.sleep(TIMEOUT) - kernel32.SetEvent(handle) - - # Test 1, handle is SET after 1 sec in separate coroutine - - handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - t0 = _core.current_time() - - async with _core.open_nursery() as nursery: - nursery.start_soon(WaitForSingleObject, handle) - nursery.start_soon(handle_setter, handle) - - kernel32.CloseHandle(handle) - t1 = _core.current_time() - assert TIMEOUT <= (t1 - t0) < 1.1 * TIMEOUT - print('test_WaitForSingleObject test 1 OK') - - # Test 2, handle is CLOSED after 1 sec - NOPE, wont work unless we use zero timeout - - pass - - # Test 3, cancelation - - handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - t0 = _core.current_time() - - with _timeouts.move_on_after(TIMEOUT): - await WaitForSingleObject(handle) - - kernel32.CloseHandle(handle) - t1 = _core.current_time() - assert TIMEOUT <= (t1 - t0) < 1.1 * TIMEOUT - print('test_WaitForSingleObject test 3 OK') - - # Test 4, already cancelled - - handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - kernel32.SetEvent(handle) - t0 = _core.current_time() - - with _timeouts.move_on_after(TIMEOUT): - await WaitForSingleObject(handle) - - kernel32.CloseHandle(handle) - t1 = _core.current_time() - assert (t1 - t0) < 0.5 * TIMEOUT - print('test_WaitForSingleObject test 4 OK') - - # Test 5, already closed - - handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - kernel32.CloseHandle(handle) - t0 = _core.current_time() - - with _timeouts.move_on_after(TIMEOUT): - await WaitForSingleObject(handle) - - t1 = _core.current_time() - assert (t1 - t0) < 0.5 * TIMEOUT - print('test_WaitForSingleObject test 5 OK') - - # XX test setting the iomanager._iocp to something weird to make sure that the # IOCP thread can send exceptions back to the main thread diff --git a/trio/_wait_for_single_object.py b/trio/_wait_for_single_object.py new file mode 100644 index 000000000..e4d908459 --- /dev/null +++ b/trio/_wait_for_single_object.py @@ -0,0 +1,55 @@ +from . import _timeouts +from . import _core +from ._threads import run_sync_in_worker_thread +from ._core._windows_cffi import ffi, kernel32, ErrorCodes + + +async def WaitForSingleObject(handle): + """Async and cancellable variant of kernel32.WaitForSingleObject(). + + Args: + handle: A win32 handle. + + """ + # Quick check; we might not even need to spawn a thread. The zero + # means a zero timeout; this call never blocks. We also exit here + # if the handle is already closed for some reason. + retcode = kernel32.WaitForSingleObject(handle, 0) + if retcode != ErrorCodes.WAIT_TIMEOUT: + return + + class StubLimiter: + def release_on_behalf_of(self, x): + pass + + async def acquire_on_behalf_of(self, x): + pass + + # Wait for a thread that waits for two handles: the handle plus a handle + # that we can use to cancel the thread. + cancel_handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + try: + await run_sync_in_worker_thread( + WaitForMultipleObjects_sync, + handle, + cancel_handle, + cancellable=True, + limiter=StubLimiter(), + ) + finally: + # Clean up our cancel handle. In case we get here because this task was + # cancelled, we also want to set the cancel_handle to stop the thread. + kernel32.SetEvent(cancel_handle) + kernel32.CloseHandle(cancel_handle) + + +def WaitForMultipleObjects_sync(*handles): + """Wait for any of the given Windows handles to be signaled. + + """ + n = len(handles) + handle_arr = ffi.new("HANDLE[{}]".format(n)) + for i in range(n): + handle_arr[i] = handles[i] + timeout = 0xffffffff # INFINITE + kernel32.WaitForMultipleObjects(n, handle_arr, False, timeout) # blocking diff --git a/trio/tests/test_wait_for_single_object.py b/trio/tests/test_wait_for_single_object.py new file mode 100644 index 000000000..6c5211dcd --- /dev/null +++ b/trio/tests/test_wait_for_single_object.py @@ -0,0 +1,118 @@ +import os +from threading import Thread + +import pytest + +on_windows = (os.name == "nt") +# Mark all the tests in this file as being windows-only +pytestmark = pytest.mark.skipif(not on_windows, reason="windows only") + +from .. import _core +from .. import _timeouts +if on_windows: + from .._core._windows_cffi import ffi, kernel32 + from .._wait_for_single_object import WaitForSingleObject, WaitForMultipleObjects_sync + + +async def test_WaitForMultipleObjects_sync(): + # One handle + handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + t = Thread(target=WaitForMultipleObjects_sync, args=(handle1,)) + t.start() + kernel32.SetEvent(handle1) + t.join() # the test succeeds if we do not block here :) + kernel32.CloseHandle(handle1) + + # Two handles, signal first + handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + t = Thread(target=WaitForMultipleObjects_sync, args=(handle1, handle2)) + t.start() + kernel32.SetEvent(handle1) + t.join() # the test succeeds if we do not block here :) + kernel32.CloseHandle(handle1) + kernel32.CloseHandle(handle2) + + # Two handles, signal seconds + handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + t = Thread(target=WaitForMultipleObjects_sync, args=(handle1, handle2)) + t.start() + kernel32.SetEvent(handle2) + t.join() # the test succeeds if we do not block here :) + kernel32.CloseHandle(handle1) + kernel32.CloseHandle(handle2) + + # Closing the handle will not stop the thread. Initiating a wait on a + # closed handle will fail/return, but closing a handle that is already + # being waited on will not stop whatever is waiting for it. + + +async def test_WaitForSingleObject(): + + # Set the timeout used in the tests. The resolution of WaitForSingleObject + # is 0.01 so anything more than a magnitude larger should probably do. + # If too large, the test become slow and we might need to mark it as @slow. + TIMEOUT = 0.5 + + async def handle_setter(handle): + await _timeouts.sleep(TIMEOUT) + kernel32.SetEvent(handle) + + # Test 1, handle is SET after 1 sec in separate coroutine + + handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + t0 = _core.current_time() + + async with _core.open_nursery() as nursery: + nursery.start_soon(WaitForSingleObject, handle) + nursery.start_soon(handle_setter, handle) + + kernel32.CloseHandle(handle) + t1 = _core.current_time() + assert TIMEOUT <= (t1 - t0) < 1.1 * TIMEOUT + print('test_WaitForSingleObject test 1 OK') + + # Test 2, handle is CLOSED after 1 sec - NOPE, wont work unless we use zero timeout + + pass + + # Test 3, cancellation + + handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + t0 = _core.current_time() + + with _timeouts.move_on_after(TIMEOUT): + await WaitForSingleObject(handle) + + kernel32.CloseHandle(handle) + t1 = _core.current_time() + assert TIMEOUT <= (t1 - t0) < 1.1 * TIMEOUT + print('test_WaitForSingleObject test 3 OK') + + # Test 4, already cancelled + + handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + kernel32.SetEvent(handle) + t0 = _core.current_time() + + with _timeouts.move_on_after(TIMEOUT): + await WaitForSingleObject(handle) + + kernel32.CloseHandle(handle) + t1 = _core.current_time() + assert (t1 - t0) < 0.5 * TIMEOUT + print('test_WaitForSingleObject test 4 OK') + + # Test 5, already closed + + handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + kernel32.CloseHandle(handle) + t0 = _core.current_time() + + with _timeouts.move_on_after(TIMEOUT): + await WaitForSingleObject(handle) + + t1 = _core.current_time() + assert (t1 - t0) < 0.5 * TIMEOUT + print('test_WaitForSingleObject test 5 OK') From f1243020b7d76102667cb4fbb878d9baa8cd4b81 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Tue, 31 Jul 2018 00:35:16 +0200 Subject: [PATCH 06/15] fix and refactor tests into fast and slow --- trio/tests/test_wait_for_single_object.py | 148 +++++++++++++++------- 1 file changed, 99 insertions(+), 49 deletions(-) diff --git a/trio/tests/test_wait_for_single_object.py b/trio/tests/test_wait_for_single_object.py index 6c5211dcd..17688b3a5 100644 --- a/trio/tests/test_wait_for_single_object.py +++ b/trio/tests/test_wait_for_single_object.py @@ -1,4 +1,5 @@ import os +import time from threading import Thread import pytest @@ -7,6 +8,7 @@ # Mark all the tests in this file as being windows-only pytestmark = pytest.mark.skipif(not on_windows, reason="windows only") +from .._core.tests.tutil import slow from .. import _core from .. import _timeouts if on_windows: @@ -15,104 +17,152 @@ async def test_WaitForMultipleObjects_sync(): + # This does a series of tests where we set/close the handle before + # initiating the waiting for it. + # + # Note that closing the handle (not signaling) will cause the + # *initiation* of a wait to return immediately. But closing a handle + # that is already being waited on will not stop whatever is waiting + # for it. + # One handle handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - t = Thread(target=WaitForMultipleObjects_sync, args=(handle1,)) - t.start() kernel32.SetEvent(handle1) - t.join() # the test succeeds if we do not block here :) + WaitForMultipleObjects_sync(handle1) kernel32.CloseHandle(handle1) # Two handles, signal first handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - t = Thread(target=WaitForMultipleObjects_sync, args=(handle1, handle2)) - t.start() kernel32.SetEvent(handle1) - t.join() # the test succeeds if we do not block here :) + WaitForMultipleObjects_sync(handle1, handle2) kernel32.CloseHandle(handle1) kernel32.CloseHandle(handle2) - # Two handles, signal seconds + # Two handles, signal second handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - t = Thread(target=WaitForMultipleObjects_sync, args=(handle1, handle2)) - t.start() kernel32.SetEvent(handle2) - t.join() # the test succeeds if we do not block here :) + WaitForMultipleObjects_sync(handle1, handle2) kernel32.CloseHandle(handle1) kernel32.CloseHandle(handle2) - # Closing the handle will not stop the thread. Initiating a wait on a - # closed handle will fail/return, but closing a handle that is already - # being waited on will not stop whatever is waiting for it. + # Two handles, close first + handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + kernel32.CloseHandle(handle1) + WaitForMultipleObjects_sync(handle1, handle2) + kernel32.CloseHandle(handle2) + # Two handles, close second + handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + kernel32.CloseHandle(handle2) + WaitForMultipleObjects_sync(handle1, handle2) + kernel32.CloseHandle(handle1) -async def test_WaitForSingleObject(): - # Set the timeout used in the tests. The resolution of WaitForSingleObject - # is 0.01 so anything more than a magnitude larger should probably do. - # If too large, the test become slow and we might need to mark it as @slow. - TIMEOUT = 0.5 +@slow +async def test_WaitForMultipleObjects_sync_slow(): + # This does a series of test in which the main thread sync-waits for + # handles, while we spawn a thread to set the handles after a short while. - async def handle_setter(handle): - await _timeouts.sleep(TIMEOUT) - kernel32.SetEvent(handle) + TIMEOUT = 0.3 - # Test 1, handle is SET after 1 sec in separate coroutine + def signal_soon_sync(handle): + time.sleep(TIMEOUT) + kernel32.SetEvent(handle) - handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + # One handle + handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) t0 = _core.current_time() + Thread(target=signal_soon_sync, args=(handle1,)).start() + WaitForMultipleObjects_sync(handle1) + t1 = _core.current_time() + assert TIMEOUT <= (t1 - t0) < 1.2 * TIMEOUT + kernel32.CloseHandle(handle1) - async with _core.open_nursery() as nursery: - nursery.start_soon(WaitForSingleObject, handle) - nursery.start_soon(handle_setter, handle) + # Two handles, signal first + handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + t0 = _core.current_time() + Thread(target=signal_soon_sync, args=(handle1,)).start() + WaitForMultipleObjects_sync(handle1, handle2) + t1 = _core.current_time() + assert TIMEOUT <= (t1 - t0) < 1.2 * TIMEOUT + kernel32.CloseHandle(handle1) + kernel32.CloseHandle(handle2) - kernel32.CloseHandle(handle) + # Two handles, signal second + handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + t0 = _core.current_time() + Thread(target=signal_soon_sync, args=(handle2,)).start() + WaitForMultipleObjects_sync(handle1, handle2) t1 = _core.current_time() - assert TIMEOUT <= (t1 - t0) < 1.1 * TIMEOUT - print('test_WaitForSingleObject test 1 OK') + assert TIMEOUT <= (t1 - t0) < 1.2 * TIMEOUT + kernel32.CloseHandle(handle1) + kernel32.CloseHandle(handle2) - # Test 2, handle is CLOSED after 1 sec - NOPE, wont work unless we use zero timeout - pass +async def test_WaitForSingleObject(): + # This does a series of test for setting/closing the handle before + # initiating the wait. - # Test 3, cancellation + # Test already set + handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + kernel32.SetEvent(handle) + await WaitForSingleObject(handle) # should return at once + kernel32.CloseHandle(handle) + print('test_WaitForSingleObject already set OK') + # Test already closed handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - t0 = _core.current_time() + kernel32.CloseHandle(handle) + await WaitForSingleObject(handle) # should return at once + print('test_WaitForSingleObject already closed OK') - with _timeouts.move_on_after(TIMEOUT): - await WaitForSingleObject(handle) - kernel32.CloseHandle(handle) - t1 = _core.current_time() - assert TIMEOUT <= (t1 - t0) < 1.1 * TIMEOUT - print('test_WaitForSingleObject test 3 OK') +@slow +async def test_WaitForSingleObject_slow(): + # This does a series of test for setting the handle in another task, + # and cancelling the wait task. - # Test 4, already cancelled + # Set the timeout used in the tests. We test the waiting time against + # the timeout with a certain margin. + TIMEOUT = 0.3 + + async def signal_soon_async(handle): + await _timeouts.sleep(TIMEOUT) + kernel32.SetEvent(handle) + + # Test handle is SET after TIMEOUT in separate coroutine handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - kernel32.SetEvent(handle) t0 = _core.current_time() - with _timeouts.move_on_after(TIMEOUT): - await WaitForSingleObject(handle) + async with _core.open_nursery() as nursery: + nursery.start_soon(WaitForSingleObject, handle) + nursery.start_soon(signal_soon_async, handle) kernel32.CloseHandle(handle) t1 = _core.current_time() - assert (t1 - t0) < 0.5 * TIMEOUT - print('test_WaitForSingleObject test 4 OK') + assert TIMEOUT <= (t1 - t0) < 1.2 * TIMEOUT + print('test_WaitForSingleObject_slow set from task OK') - # Test 5, already closed + # Test handle is CLOSED after 1 sec - NOPE see comment above + + pass + + # Test cancellation handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) - kernel32.CloseHandle(handle) t0 = _core.current_time() with _timeouts.move_on_after(TIMEOUT): await WaitForSingleObject(handle) + kernel32.CloseHandle(handle) t1 = _core.current_time() - assert (t1 - t0) < 0.5 * TIMEOUT - print('test_WaitForSingleObject test 5 OK') + assert TIMEOUT <= (t1 - t0) < 1.2 * TIMEOUT + print('test_WaitForSingleObject_slow cancellation OK') From 1c0cbec46de45b86292be7c5454edec36ccd3a78 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Tue, 31 Jul 2018 00:39:52 +0200 Subject: [PATCH 07/15] unnest a stub class --- trio/_wait_for_single_object.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/trio/_wait_for_single_object.py b/trio/_wait_for_single_object.py index e4d908459..02f01e2ea 100644 --- a/trio/_wait_for_single_object.py +++ b/trio/_wait_for_single_object.py @@ -4,6 +4,14 @@ from ._core._windows_cffi import ffi, kernel32, ErrorCodes +class StubLimiter: + def release_on_behalf_of(self, x): + pass + + async def acquire_on_behalf_of(self, x): + pass + + async def WaitForSingleObject(handle): """Async and cancellable variant of kernel32.WaitForSingleObject(). @@ -18,13 +26,6 @@ async def WaitForSingleObject(handle): if retcode != ErrorCodes.WAIT_TIMEOUT: return - class StubLimiter: - def release_on_behalf_of(self, x): - pass - - async def acquire_on_behalf_of(self, x): - pass - # Wait for a thread that waits for two handles: the handle plus a handle # that we can use to cancel the thread. cancel_handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) From 7e72d4234c1b16b2d0ee0ab4f1330d68cc063f7e Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Wed, 1 Aug 2018 10:20:22 +0200 Subject: [PATCH 08/15] enhancements and tweaks to WaitForSingleObject --- trio/_core/_io_windows.py | 14 +------ trio/_core/_windows_cffi.py | 13 ++++++ trio/_wait_for_single_object.py | 19 ++++++--- trio/tests/test_wait_for_single_object.py | 49 ++++++++++++++++------- 4 files changed, 63 insertions(+), 32 deletions(-) diff --git a/trio/_core/_io_windows.py b/trio/_core/_io_windows.py index 90a79c032..885ddccf6 100644 --- a/trio/_core/_io_windows.py +++ b/trio/_core/_io_windows.py @@ -22,6 +22,7 @@ INVALID_HANDLE_VALUE, raise_winerror, ErrorCodes, + _handle, ) # There's a lot to be said about the overall design of a Windows event @@ -96,19 +97,6 @@ def _check(success): return success -def _handle(obj): - # For now, represent handles as either cffi HANDLEs or as ints. If you - # try to pass in a file descriptor instead, it's not going to work - # out. (For that msvcrt.get_osfhandle does the trick, but I don't know if - # we'll actually need that for anything...) For sockets this doesn't - # matter, Python never allocates an fd. So let's wait until we actually - # encounter the problem before worrying about it. - if type(obj) is int: - return ffi.cast("HANDLE", obj) - else: - return obj - - @attr.s(frozen=True) class _WindowsStatistics: tasks_waiting_overlapped = attr.ib() diff --git a/trio/_core/_windows_cffi.py b/trio/_core/_windows_cffi.py index c8ff6af3d..16dd9a232 100644 --- a/trio/_core/_windows_cffi.py +++ b/trio/_core/_windows_cffi.py @@ -134,6 +134,19 @@ INVALID_HANDLE_VALUE = ffi.cast("HANDLE", -1) +def _handle(obj): + # For now, represent handles as either cffi HANDLEs or as ints. If you + # try to pass in a file descriptor instead, it's not going to work + # out. (For that msvcrt.get_osfhandle does the trick, but I don't know if + # we'll actually need that for anything...) For sockets this doesn't + # matter, Python never allocates an fd. So let's wait until we actually + # encounter the problem before worrying about it. + if type(obj) is int: + return ffi.cast("HANDLE", obj) + else: + return obj + + def raise_winerror(winerror=None, *, filename=None, filename2=None): if winerror is None: winerror, msg = ffi.getwinerror() diff --git a/trio/_wait_for_single_object.py b/trio/_wait_for_single_object.py index 02f01e2ea..2b7c69c67 100644 --- a/trio/_wait_for_single_object.py +++ b/trio/_wait_for_single_object.py @@ -1,7 +1,7 @@ from . import _timeouts from . import _core from ._threads import run_sync_in_worker_thread -from ._core._windows_cffi import ffi, kernel32, ErrorCodes +from ._core._windows_cffi import ffi, kernel32, ErrorCodes, raise_winerror, _handle class StubLimiter: @@ -12,18 +12,23 @@ async def acquire_on_behalf_of(self, x): pass -async def WaitForSingleObject(handle): +async def WaitForSingleObject(obj): """Async and cancellable variant of kernel32.WaitForSingleObject(). Args: - handle: A win32 handle. + handle: A win32 handle in the form of an int or cffi HANDLE object. """ + # Allow ints or whatever we can convert to a win handle + handle = _handle(obj) + # Quick check; we might not even need to spawn a thread. The zero # means a zero timeout; this call never blocks. We also exit here # if the handle is already closed for some reason. retcode = kernel32.WaitForSingleObject(handle, 0) - if retcode != ErrorCodes.WAIT_TIMEOUT: + if retcode == ErrorCodes.WAIT_FAILED: + raise_winerror() + elif retcode != ErrorCodes.WAIT_TIMEOUT: return # Wait for a thread that waits for two handles: the handle plus a handle @@ -53,4 +58,8 @@ def WaitForMultipleObjects_sync(*handles): for i in range(n): handle_arr[i] = handles[i] timeout = 0xffffffff # INFINITE - kernel32.WaitForMultipleObjects(n, handle_arr, False, timeout) # blocking + retcode = kernel32.WaitForMultipleObjects( + n, handle_arr, False, timeout + ) # blocking + if retcode == ErrorCodes.WAIT_FAILED: + raise_winerror() diff --git a/trio/tests/test_wait_for_single_object.py b/trio/tests/test_wait_for_single_object.py index 17688b3a5..ff1adf6fb 100644 --- a/trio/tests/test_wait_for_single_object.py +++ b/trio/tests/test_wait_for_single_object.py @@ -1,6 +1,5 @@ import os import time -from threading import Thread import pytest @@ -13,7 +12,7 @@ from .. import _timeouts if on_windows: from .._core._windows_cffi import ffi, kernel32 - from .._wait_for_single_object import WaitForSingleObject, WaitForMultipleObjects_sync + from .._wait_for_single_object import WaitForSingleObject, WaitForMultipleObjects_sync, run_sync_in_worker_thread async def test_WaitForMultipleObjects_sync(): @@ -30,6 +29,7 @@ async def test_WaitForMultipleObjects_sync(): kernel32.SetEvent(handle1) WaitForMultipleObjects_sync(handle1) kernel32.CloseHandle(handle1) + print('test_WaitForMultipleObjects_sync one OK') # Two handles, signal first handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) @@ -38,6 +38,7 @@ async def test_WaitForMultipleObjects_sync(): WaitForMultipleObjects_sync(handle1, handle2) kernel32.CloseHandle(handle1) kernel32.CloseHandle(handle2) + print('test_WaitForMultipleObjects_sync set first OK') # Two handles, signal second handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) @@ -46,20 +47,25 @@ async def test_WaitForMultipleObjects_sync(): WaitForMultipleObjects_sync(handle1, handle2) kernel32.CloseHandle(handle1) kernel32.CloseHandle(handle2) + print('test_WaitForMultipleObjects_sync set second OK') # Two handles, close first handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) kernel32.CloseHandle(handle1) - WaitForMultipleObjects_sync(handle1, handle2) + with pytest.raises(OSError): + WaitForMultipleObjects_sync(handle1, handle2) kernel32.CloseHandle(handle2) + print('test_WaitForMultipleObjects_sync close first OK') # Two handles, close second handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) kernel32.CloseHandle(handle2) - WaitForMultipleObjects_sync(handle1, handle2) + with pytest.raises(OSError): + WaitForMultipleObjects_sync(handle1, handle2) kernel32.CloseHandle(handle1) + print('test_WaitForMultipleObjects_sync close second OK') @slow @@ -70,39 +76,46 @@ async def test_WaitForMultipleObjects_sync_slow(): TIMEOUT = 0.3 def signal_soon_sync(handle): - time.sleep(TIMEOUT) + # Using time.sleep(TIMEOUT) can somehow sleep too little when compared + # to a monolytic clock, thus the while loop. + end = time.monotonic() + TIMEOUT + while time.monotonic() < end: + time.sleep(TIMEOUT / 20) kernel32.SetEvent(handle) # One handle handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) t0 = _core.current_time() - Thread(target=signal_soon_sync, args=(handle1,)).start() + await run_sync_in_worker_thread(signal_soon_sync, handle1) WaitForMultipleObjects_sync(handle1) t1 = _core.current_time() - assert TIMEOUT <= (t1 - t0) < 1.2 * TIMEOUT + assert TIMEOUT <= (t1 - t0) < 2.0 * TIMEOUT kernel32.CloseHandle(handle1) + print('test_WaitForMultipleObjects_sync_slow one OK') # Two handles, signal first handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) t0 = _core.current_time() - Thread(target=signal_soon_sync, args=(handle1,)).start() + await run_sync_in_worker_thread(signal_soon_sync, handle1) WaitForMultipleObjects_sync(handle1, handle2) t1 = _core.current_time() - assert TIMEOUT <= (t1 - t0) < 1.2 * TIMEOUT + assert TIMEOUT <= (t1 - t0) < 2.0 * TIMEOUT kernel32.CloseHandle(handle1) kernel32.CloseHandle(handle2) + print('test_WaitForMultipleObjects_sync_slow thread-set first OK') # Two handles, signal second handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) t0 = _core.current_time() - Thread(target=signal_soon_sync, args=(handle2,)).start() + await run_sync_in_worker_thread(signal_soon_sync, handle2) WaitForMultipleObjects_sync(handle1, handle2) t1 = _core.current_time() - assert TIMEOUT <= (t1 - t0) < 1.2 * TIMEOUT + assert TIMEOUT <= (t1 - t0) < 2.0 * TIMEOUT kernel32.CloseHandle(handle1) kernel32.CloseHandle(handle2) + print('test_WaitForMultipleObjects_sync_slow thread-set second OK') async def test_WaitForSingleObject(): @@ -119,9 +132,17 @@ async def test_WaitForSingleObject(): # Test already closed handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) kernel32.CloseHandle(handle) - await WaitForSingleObject(handle) # should return at once + with pytest.raises(OSError): + await WaitForSingleObject(handle) # should return at once print('test_WaitForSingleObject already closed OK') + # Not a handle + with pytest.raises(TypeError): + await WaitForSingleObject("not a handle") # Wrong type + with pytest.raises(OSError): + await WaitForSingleObject(99) # Could be handle, but is invalid + print('test_WaitForSingleObject not a handle OK') + @slow async def test_WaitForSingleObject_slow(): @@ -147,7 +168,7 @@ async def signal_soon_async(handle): kernel32.CloseHandle(handle) t1 = _core.current_time() - assert TIMEOUT <= (t1 - t0) < 1.2 * TIMEOUT + assert TIMEOUT <= (t1 - t0) < 2.0 * TIMEOUT print('test_WaitForSingleObject_slow set from task OK') # Test handle is CLOSED after 1 sec - NOPE see comment above @@ -164,5 +185,5 @@ async def signal_soon_async(handle): kernel32.CloseHandle(handle) t1 = _core.current_time() - assert TIMEOUT <= (t1 - t0) < 1.2 * TIMEOUT + assert TIMEOUT <= (t1 - t0) < 2.0 * TIMEOUT print('test_WaitForSingleObject_slow cancellation OK') From c750f38b94161cf4f9f293f2a0abcd58aeb615ce Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Thu, 2 Aug 2018 16:40:51 +0200 Subject: [PATCH 09/15] made WaitForSingleObject tests more trioish --- trio/tests/test_wait_for_single_object.py | 36 ++++++++++++++--------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/trio/tests/test_wait_for_single_object.py b/trio/tests/test_wait_for_single_object.py index ff1adf6fb..46a07fe52 100644 --- a/trio/tests/test_wait_for_single_object.py +++ b/trio/tests/test_wait_for_single_object.py @@ -75,19 +75,17 @@ async def test_WaitForMultipleObjects_sync_slow(): TIMEOUT = 0.3 - def signal_soon_sync(handle): - # Using time.sleep(TIMEOUT) can somehow sleep too little when compared - # to a monolytic clock, thus the while loop. - end = time.monotonic() + TIMEOUT - while time.monotonic() < end: - time.sleep(TIMEOUT / 20) - kernel32.SetEvent(handle) - # One handle handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) t0 = _core.current_time() - await run_sync_in_worker_thread(signal_soon_sync, handle1) - WaitForMultipleObjects_sync(handle1) + async with _core.open_nursery() as nursery: + nursery.start_soon( + run_sync_in_worker_thread, WaitForMultipleObjects_sync, handle1 + ) + await _timeouts.sleep(TIMEOUT) + # If we would comment the line below, the above thread will be stuck, + # and trio wont exit this scope + kernel32.SetEvent(handle1) t1 = _core.current_time() assert TIMEOUT <= (t1 - t0) < 2.0 * TIMEOUT kernel32.CloseHandle(handle1) @@ -97,8 +95,13 @@ def signal_soon_sync(handle): handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) t0 = _core.current_time() - await run_sync_in_worker_thread(signal_soon_sync, handle1) - WaitForMultipleObjects_sync(handle1, handle2) + async with _core.open_nursery() as nursery: + nursery.start_soon( + run_sync_in_worker_thread, WaitForMultipleObjects_sync, handle1, + handle2 + ) + await _timeouts.sleep(TIMEOUT) + kernel32.SetEvent(handle1) t1 = _core.current_time() assert TIMEOUT <= (t1 - t0) < 2.0 * TIMEOUT kernel32.CloseHandle(handle1) @@ -109,8 +112,13 @@ def signal_soon_sync(handle): handle1 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) handle2 = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) t0 = _core.current_time() - await run_sync_in_worker_thread(signal_soon_sync, handle2) - WaitForMultipleObjects_sync(handle1, handle2) + async with _core.open_nursery() as nursery: + nursery.start_soon( + run_sync_in_worker_thread, WaitForMultipleObjects_sync, handle1, + handle2 + ) + await _timeouts.sleep(TIMEOUT) + kernel32.SetEvent(handle2) t1 = _core.current_time() assert TIMEOUT <= (t1 - t0) < 2.0 * TIMEOUT kernel32.CloseHandle(handle1) From 5795d1841f26d36a134712a2a20fb769195d4b38 Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 3 Aug 2018 09:56:32 +0200 Subject: [PATCH 10/15] exposing/documenting WaitForSingleObject --- docs/source/reference-hazmat.rst | 2 ++ newsfragments/233.feature.rst | 1 + trio/_core/__init__.py | 7 +++++++ ...ait_for_single_object.py => _wait_for_object.py} | 6 ++++-- trio/hazmat.py | 13 +++++++++++++ ...for_single_object.py => test_wait_for_object.py} | 2 +- 6 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 newsfragments/233.feature.rst rename trio/{_wait_for_single_object.py => _wait_for_object.py} (97%) rename trio/tests/{test_wait_for_single_object.py => test_wait_for_object.py} (98%) diff --git a/docs/source/reference-hazmat.rst b/docs/source/reference-hazmat.rst index 094f5901c..c029fdfa3 100644 --- a/docs/source/reference-hazmat.rst +++ b/docs/source/reference-hazmat.rst @@ -268,6 +268,8 @@ anything real. See `#26 .. function:: monitor_completion_key() :with: queue +.. function:: WaitForSingleObject() + :async: Unbounded queues ================ diff --git a/newsfragments/233.feature.rst b/newsfragments/233.feature.rst new file mode 100644 index 000000000..f038194bf --- /dev/null +++ b/newsfragments/233.feature.rst @@ -0,0 +1 @@ +Add ``trio.hazmat.WaitForSingleObject()`` async function to await Windows handles. diff --git a/trio/_core/__init__.py b/trio/_core/__init__.py index 25779eef0..976975ae4 100644 --- a/trio/_core/__init__.py +++ b/trio/_core/__init__.py @@ -1,3 +1,10 @@ +""" +This namespace represents the core functionality that has to be built-in +and deal with private internal data structures. Things in this namespace +are publicly available in either trio, trio.hazmat, or trio.testing. +""" + + # Needs to be defined early so it can be imported: def _public(fn): # Used to mark methods on _Runner and on IOManager implementations that diff --git a/trio/_wait_for_single_object.py b/trio/_wait_for_object.py similarity index 97% rename from trio/_wait_for_single_object.py rename to trio/_wait_for_object.py index 2b7c69c67..4ba70105a 100644 --- a/trio/_wait_for_single_object.py +++ b/trio/_wait_for_object.py @@ -3,6 +3,8 @@ from ._threads import run_sync_in_worker_thread from ._core._windows_cffi import ffi, kernel32, ErrorCodes, raise_winerror, _handle +__all__ = ["WaitForSingleObject"] + class StubLimiter: def release_on_behalf_of(self, x): @@ -13,8 +15,8 @@ async def acquire_on_behalf_of(self, x): async def WaitForSingleObject(obj): - """Async and cancellable variant of kernel32.WaitForSingleObject(). - + """Async and cancellable variant of kernel32.WaitForSingleObject(). Windows only. + Args: handle: A win32 handle in the form of an int or cffi HANDLE object. diff --git a/trio/hazmat.py b/trio/hazmat.py index e5a417ceb..75c403f6f 100644 --- a/trio/hazmat.py +++ b/trio/hazmat.py @@ -1,3 +1,11 @@ +""" +This namespace represents low-level functionality not intended for daily use, +but useful for extending Trio's functionality. It is the union of +a subset of trio/_core/ and some things from trio/*.py. +""" + +import sys + # These are all re-exported from trio._core. See comments in trio/__init__.py # for details. To make static analysis easier, this lists all possible # symbols, and then we prune some below if they aren't available on this @@ -56,3 +64,8 @@ # who knows. remove_from_all = __all__.remove remove_from_all(_sym) + +# Import bits from trio/*.py +if sys.platform.startswith("win"): + from ._wait_for_object import WaitForSingleObject + __all__ += ["WaitForSingleObject"] diff --git a/trio/tests/test_wait_for_single_object.py b/trio/tests/test_wait_for_object.py similarity index 98% rename from trio/tests/test_wait_for_single_object.py rename to trio/tests/test_wait_for_object.py index 46a07fe52..c36227904 100644 --- a/trio/tests/test_wait_for_single_object.py +++ b/trio/tests/test_wait_for_object.py @@ -12,7 +12,7 @@ from .. import _timeouts if on_windows: from .._core._windows_cffi import ffi, kernel32 - from .._wait_for_single_object import WaitForSingleObject, WaitForMultipleObjects_sync, run_sync_in_worker_thread + from .._wait_for_object import WaitForSingleObject, WaitForMultipleObjects_sync, run_sync_in_worker_thread async def test_WaitForMultipleObjects_sync(): From 197d61d15ee0ca866401e1833ae4f620dbe3c89f Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Fri, 3 Aug 2018 14:16:53 +0200 Subject: [PATCH 11/15] comment flaky test --- trio/tests/test_wait_for_object.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/trio/tests/test_wait_for_object.py b/trio/tests/test_wait_for_object.py index c36227904..d0b8f16cf 100644 --- a/trio/tests/test_wait_for_object.py +++ b/trio/tests/test_wait_for_object.py @@ -147,8 +147,8 @@ async def test_WaitForSingleObject(): # Not a handle with pytest.raises(TypeError): await WaitForSingleObject("not a handle") # Wrong type - with pytest.raises(OSError): - await WaitForSingleObject(99) # Could be handle, but is invalid + # with pytest.raises(OSError): + # await WaitForSingleObject(99) # If you're unlucky, it actually IS a handle :( print('test_WaitForSingleObject not a handle OK') From c77e24c928cb035e0dc83cbbfe03745a787bb5ea Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Sun, 5 Aug 2018 15:54:04 +0200 Subject: [PATCH 12/15] added simple module docstring --- trio/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/trio/__init__.py b/trio/__init__.py index 7ea2df21e..5f8422657 100644 --- a/trio/__init__.py +++ b/trio/__init__.py @@ -1,3 +1,6 @@ +"""Trio - Pythonic async I/O for humans and snake people. +""" + # General layout: # # trio/_core/... is the self-contained core library. It does various From 502f964c331867cb7683538b78345fb56ad2864b Mon Sep 17 00:00:00 2001 From: Almar Klein Date: Mon, 6 Aug 2018 10:36:16 +0200 Subject: [PATCH 13/15] WaitForSingleObject polishing --- docs/source/reference-hazmat.rst | 13 +++++++++++-- newsfragments/233.feature.rst | 2 +- trio/_wait_for_object.py | 3 +++ trio/hazmat.py | 12 ++++++------ trio/tests/test_wait_for_object.py | 23 +++++++++++++++++++++++ 5 files changed, 44 insertions(+), 9 deletions(-) diff --git a/docs/source/reference-hazmat.rst b/docs/source/reference-hazmat.rst index c029fdfa3..364006002 100644 --- a/docs/source/reference-hazmat.rst +++ b/docs/source/reference-hazmat.rst @@ -253,6 +253,17 @@ anything real. See `#26 Windows-specific API -------------------- +.. function:: WaitForSingleObject() + :async: + + Async and cancellable variant of kernel32.WaitForSingleObject(). Windows only. + + :arg handle: + A win32 handle in the form of an int or cffi HANDLE object. + :raises OSError: + If the handle is invalid, e.g. when it is already closed. + + TODO: these are implemented, but are currently more of a sketch than anything real. See `#26 `__ and `#52 @@ -268,8 +279,6 @@ anything real. See `#26 .. function:: monitor_completion_key() :with: queue -.. function:: WaitForSingleObject() - :async: Unbounded queues ================ diff --git a/newsfragments/233.feature.rst b/newsfragments/233.feature.rst index f038194bf..cc3c9d0dd 100644 --- a/newsfragments/233.feature.rst +++ b/newsfragments/233.feature.rst @@ -1 +1 @@ -Add ``trio.hazmat.WaitForSingleObject()`` async function to await Windows handles. +Add :func:`trio.hazmat.WaitForSingleObject` async function to await Windows handles. diff --git a/trio/_wait_for_object.py b/trio/_wait_for_object.py index 4ba70105a..01838bd14 100644 --- a/trio/_wait_for_object.py +++ b/trio/_wait_for_object.py @@ -19,6 +19,9 @@ async def WaitForSingleObject(obj): Args: handle: A win32 handle in the form of an int or cffi HANDLE object. + + Raises: + OSError: If the handle is invalid, e.g. when it is already closed. """ # Allow ints or whatever we can convert to a win handle diff --git a/trio/hazmat.py b/trio/hazmat.py index 75c403f6f..4aebcc1ba 100644 --- a/trio/hazmat.py +++ b/trio/hazmat.py @@ -1,15 +1,15 @@ """ This namespace represents low-level functionality not intended for daily use, -but useful for extending Trio's functionality. It is the union of -a subset of trio/_core/ and some things from trio/*.py. +but useful for extending Trio's functionality. """ import sys -# These are all re-exported from trio._core. See comments in trio/__init__.py -# for details. To make static analysis easier, this lists all possible -# symbols, and then we prune some below if they aren't available on this -# system. +# This is the union of a subset of trio/_core/ and some things from trio/*.py. +# See comments in trio/__init__.py for details. To make static analysis easier, +# this lists all possible symbols from trio._core, and then we prune those that +# aren't available on this system. After that we add some symbols from trio/*.py. + __all__ = [ "cancel_shielded_checkpoint", "Abort", diff --git a/trio/tests/test_wait_for_object.py b/trio/tests/test_wait_for_object.py index d0b8f16cf..3a83e454e 100644 --- a/trio/tests/test_wait_for_object.py +++ b/trio/tests/test_wait_for_object.py @@ -137,6 +137,14 @@ async def test_WaitForSingleObject(): kernel32.CloseHandle(handle) print('test_WaitForSingleObject already set OK') + # Test already set, as int + handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + handle_int = int(ffi.cast("intptr_t", handle)) + kernel32.SetEvent(handle) + await WaitForSingleObject(handle_int) # should return at once + kernel32.CloseHandle(handle) + print('test_WaitForSingleObject already set OK') + # Test already closed handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) kernel32.CloseHandle(handle) @@ -179,6 +187,21 @@ async def signal_soon_async(handle): assert TIMEOUT <= (t1 - t0) < 2.0 * TIMEOUT print('test_WaitForSingleObject_slow set from task OK') + # Test handle is SET after TIMEOUT in separate coroutine, as int + + handle = kernel32.CreateEventA(ffi.NULL, True, False, ffi.NULL) + handle_int = int(ffi.cast("intptr_t", handle)) + t0 = _core.current_time() + + async with _core.open_nursery() as nursery: + nursery.start_soon(WaitForSingleObject, handle_int) + nursery.start_soon(signal_soon_async, handle) + + kernel32.CloseHandle(handle) + t1 = _core.current_time() + assert TIMEOUT <= (t1 - t0) < 2.0 * TIMEOUT + print('test_WaitForSingleObject_slow set from task as int OK') + # Test handle is CLOSED after 1 sec - NOPE see comment above pass From 1135421806e20e2a147670a42ac4fe64e6faa506 Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Mon, 6 Aug 2018 01:50:26 -0700 Subject: [PATCH 14/15] Don't mention cffi-specific internals in public docs --- docs/source/reference-hazmat.rst | 6 ++++-- trio/_wait_for_object.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/source/reference-hazmat.rst b/docs/source/reference-hazmat.rst index 364006002..54def35ff 100644 --- a/docs/source/reference-hazmat.rst +++ b/docs/source/reference-hazmat.rst @@ -256,10 +256,12 @@ Windows-specific API .. function:: WaitForSingleObject() :async: - Async and cancellable variant of kernel32.WaitForSingleObject(). Windows only. + Async and cancellable variant of `WaitForSingleObject + `__. + Windows only. :arg handle: - A win32 handle in the form of an int or cffi HANDLE object. + A Win32 object handle, as a Python integer. :raises OSError: If the handle is invalid, e.g. when it is already closed. diff --git a/trio/_wait_for_object.py b/trio/_wait_for_object.py index 01838bd14..8fd2e28ab 100644 --- a/trio/_wait_for_object.py +++ b/trio/_wait_for_object.py @@ -15,10 +15,10 @@ async def acquire_on_behalf_of(self, x): async def WaitForSingleObject(obj): - """Async and cancellable variant of kernel32.WaitForSingleObject(). Windows only. + """Async and cancellable variant of WaitForSingleObject. Windows only. Args: - handle: A win32 handle in the form of an int or cffi HANDLE object. + handle: A Win32 handle, as a Python integer. Raises: OSError: If the handle is invalid, e.g. when it is already closed. From 3c7bb5a0eb6abb2d27bbaad44b4405cac676adbf Mon Sep 17 00:00:00 2001 From: "Nathaniel J. Smith" Date: Mon, 6 Aug 2018 01:54:26 -0700 Subject: [PATCH 15/15] Fix function signature in docs --- docs/source/reference-hazmat.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/reference-hazmat.rst b/docs/source/reference-hazmat.rst index 54def35ff..efee0ba52 100644 --- a/docs/source/reference-hazmat.rst +++ b/docs/source/reference-hazmat.rst @@ -253,7 +253,7 @@ anything real. See `#26 Windows-specific API -------------------- -.. function:: WaitForSingleObject() +.. function:: WaitForSingleObject(handle) :async: Async and cancellable variant of `WaitForSingleObject