Skip to content

Commit

Permalink
Use a win32 Event to notify the loop in _Win32Handles to stop waiti…
Browse files Browse the repository at this point in the history
…ng for stdin.

This fixes a bug where the prompt_toolkit application is stopped by something
other than user input. (E.g., a progress bar that terminates.) In that case,
the thread which handles the user input will keep running and prevent Python
from exiting.

In the case of a progress bar, it was even worse, because there we close the
asyncio event loop when we finish (because it's a new loop, running in a
different thread). And closing the loop causes the `call_soon_threadsafe` in
the Win32 handles to raise a `RuntimeError` ("RuntimeError: Event loop is
closed").
  • Loading branch information
jonathanslenders committed Jan 16, 2020
1 parent afc3e00 commit 9a0d66d
Showing 1 changed file with 52 additions and 16 deletions.
68 changes: 52 additions & 16 deletions prompt_toolkit/input/win32.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from typing import Callable, ContextManager, Dict, Iterable, Optional, TextIO

from prompt_toolkit.eventloop import run_in_executor_with_context
from prompt_toolkit.eventloop.win32 import wait_for_handles
from prompt_toolkit.eventloop.win32 import create_win32_event, wait_for_handles
from prompt_toolkit.key_binding.key_processor import KeyPress
from prompt_toolkit.keys import Keys
from prompt_toolkit.mouse_events import MouseEventType
Expand Down Expand Up @@ -486,22 +486,45 @@ def _handle_mouse(self, ev):
class _Win32Handles:
"""
Utility to keep track of which handles are connectod to which callbacks.
`add_win32_handle` starts a tiny event loop in another thread which waits
for the Win32 handle to become ready. When this happens, the callback will
be called in the current asyncio event loop using `call_soon_threadsafe`.
`remove_win32_handle` will stop this tiny event loop.
NOTE: We use this technique, so that we don't have to use the
`ProactorEventLoop` on Windows and we can wait for things like stdin
in a `SelectorEventLoop`. This is important, because our inputhook
mechanism (used by IPython), only works with the `SelectorEventLoop`.
"""

def __init__(self) -> None:
self._handle_callbacks: Dict[int, Callable[[], None]] = {}

# Windows Events that are triggered when we have to stop watching this
# handle.
self._remove_events: Dict[int, HANDLE] = {}

def add_win32_handle(self, handle: HANDLE, callback: Callable[[], None]) -> None:
"""
Add a Win32 handle to the event loop.
"""
if handle.value is None:
raise ValueError("Invalid handle.")
handle_value = handle.value

if handle_value is None:
raise ValueError("Invalid handle.")

# Make sure to remove a previous registered handler first.
self.remove_win32_handle(handle)

loop = get_event_loop()
self._handle_callbacks[handle_value] = callback

# Create remove event.
remove_event = create_win32_event()
self._remove_events[handle_value] = remove_event

# Add reader.
def ready() -> None:
# Tell the callback that input's ready.
Expand All @@ -514,20 +537,35 @@ def ready() -> None:
# (Use an executor for this, the Windows asyncio event loop doesn't
# allow us to wait for handles like stdin.)
def wait() -> None:
if self._handle_callbacks.get(handle_value) != callback:
return
# Wait until either the handle becomes ready, or the remove event
# has been set.
result = wait_for_handles([remove_event, handle])

wait_for_handles([handle])
loop.call_soon_threadsafe(ready)
if result is remove_event:
windll.kernel32.CloseHandle(remove_event)
return
else:
loop.call_soon_threadsafe(ready)

run_in_executor_with_context(wait, loop=loop)

def remove_win32_handle(self, handle: HANDLE) -> None:
def remove_win32_handle(self, handle: HANDLE) -> Optional[Callable[[], None]]:
"""
Remove a Win32 handle from the event loop.
Return either the registered handler or `None`.
"""
if handle.value in self._handle_callbacks:
del self._handle_callbacks[handle.value]
# Trigger remove events, so that the reader knows to stop.
try:
event = self._remove_events.pop(handle.value)
except KeyError:
pass
else:
windll.kernel32.SetEvent(event)

try:
return self._handle_callbacks.pop(handle.value)
except KeyError:
return None


@contextmanager
Expand All @@ -545,7 +583,7 @@ def attach_win32_input(input: _Win32InputBase, callback: Callable[[], None]):
raise ValueError("Invalid handle.")

# Add reader.
previous_callback = win32_handles._handle_callbacks.get(handle.value)
previous_callback = win32_handles.remove_win32_handle(handle)
win32_handles.add_win32_handle(handle, callback)

try:
Expand All @@ -565,15 +603,13 @@ def detach_win32_input(input: _Win32InputBase):
if handle.value is None:
raise ValueError("Invalid handle.")

previous = win32_handles._handle_callbacks.get(handle.value)
if previous:
win32_handles.remove_win32_handle(handle)
previous_callback = win32_handles.remove_win32_handle(handle)

try:
yield
finally:
if previous:
win32_handles.add_win32_handle(handle, previous)
if previous_callback:
win32_handles.add_win32_handle(handle, previous_callback)


class raw_mode:
Expand Down

0 comments on commit 9a0d66d

Please sign in to comment.