Skip to content

Commit

Permalink
Deal with more Windows quirks.
Browse files Browse the repository at this point in the history
Signed-off-by: Michel Hidalgo <michel@ekumenlabs.com>
  • Loading branch information
hidmic committed Dec 23, 2020
1 parent 3f9ce9c commit 71ab3f2
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 10 deletions.
43 changes: 36 additions & 7 deletions launch/launch/utilities/signal_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

import asyncio
import os
import platform
import signal
import socket
import threading
Expand All @@ -26,6 +27,20 @@
from typing import Union


def is_winsock_handle(fd):
"""Check if the given file descriptor is WinSock handle."""
if platform.system() != 'Windows':
return False
try:
# On Windows, WinSock handles and regular file handles
# have disjoint APIs. This test leverages the fact that
# attempting to os.dup a WinSock handle will fail.
os.close(os.dup(fd))
return False
except OSError:
return True


class AsyncSafeSignalManager:
"""
A context manager class for asynchronous handling of signals.
Expand Down Expand Up @@ -63,7 +78,7 @@ def __init__(
self.__loop = loop # type: asyncio.AbstractEventLoop
self.__background_loop = None # type: Optional[asyncio.AbstractEventLoop]
self.__handlers = {} # type: dict
self.__prev_wsock_fd = -1 # type: int
self.__prev_wakeup_handle = -1 # type: Union[int, socket.socket]
self.__wsock, self.__rsock = socket.socketpair() # type: Tuple[socket.socket, socket.socket] # noqa
self.__wsock.setblocking(False)
self.__rsock.setblocking(False)
Expand All @@ -72,8 +87,9 @@ def __enter__(self):
try:
self.__loop.add_reader(self.__rsock.fileno(), self.__handle_signal)
except NotImplementedError:
# NOTE(hidmic): some event loops, like the asyncio.ProactorEventLoop
# on Windows, do not support asynchronous socket reads. Emulate it.
# Some event loops, like the asyncio.ProactorEventLoop
# on Windows, do not support asynchronous socket reads.
# So we emulate it.
self.__background_loop = asyncio.SelectorEventLoop()
self.__background_loop.add_reader(
self.__rsock.fileno(),
Expand All @@ -86,11 +102,20 @@ def run_background_loop():

self.__background_thread = threading.Thread(target=run_background_loop)
self.__background_thread.start()
self.__prev_wsock_fd = signal.set_wakeup_fd(self.__wsock.fileno())
self.__prev_wakeup_handle = signal.set_wakeup_fd(self.__wsock.fileno())
if self.__prev_wakeup_handle != -1 and is_winsock_handle(self.__prev_wakeup_handle):
# On Windows, os.write will fail on a WinSock handle. There is no WinSock API
# in the standard library either. Thus we wrap it in a socket.socket instance.
self.__prev_wakeup_handle = socket.socket(fileno=self.__prev_wakeup_handle)
return self

def __exit__(self, type_, value, traceback):
assert self.__wsock.fileno() == signal.set_wakeup_fd(self.__prev_wsock_fd)
if isinstance(self.__prev_wakeup_handle, socket.socket):
# Detach (Windows) socket and retrieve the raw OS handle.
prev_wakeup_handle = self.__prev_wakeup_handle.fileno()
self.__prev_wakeup_handle.detach()
self.__prev_wakeup_handle = prev_wakeup_handle
assert self.__wsock.fileno() == signal.set_wakeup_fd(self.__prev_wakeup_handle)
if self.__background_loop:
self.__background_loop.call_soon_threadsafe(self.__background_loop.stop)
self.__background_thread.join()
Expand All @@ -108,8 +133,12 @@ def __handle_signal(self):
if signum not in self.__handlers:
continue
self.__handlers[signum](signum)
if self.__prev_wsock_fd != -1:
os.write(self.__prev_wsock_fd, data)
if self.__prev_wakeup_handle != -1:
# Send over (Windows) socket or write file.
if isinstance(self.__prev_wakeup_handle, socket.socket):
self.__prev_wakeup_handle.send(data)
else:
os.write(self.__prev_wakeup_handle, data)
except InterruptedError:
continue
except BlockingIOError:
Expand Down
9 changes: 6 additions & 3 deletions launch/test/launch/utilities/test_signal_management.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ def _noop(*args):
def _decorator(func):
@functools.wraps(func)
def _wrapper(*args, **kwargs):
handlers = {}
try:
handlers = {s: signal.signal(s, _noop) for s in signals}
for s in signals:
handlers[s] = signal.signal(s, _noop)
return func(*args, **kwargs)
finally:
assert all(signal.signal(s, h) is _noop for s, h in handlers.items())
Expand All @@ -42,8 +44,9 @@ def _wrapper(*args, **kwargs):


if platform.system() == 'Windows':
SIGNAL = signal.CTRL_C_EVENT
ANOTHER_SIGNAL = signal.CTRL_BREAK_EVENT
# NOTE(hidmic): this is risky, but we have few options.
SIGNAL = signal.SIGINT
ANOTHER_SIGNAL = signal.SIGBREAK
else:
SIGNAL = signal.SIGUSR1
ANOTHER_SIGNAL = signal.SIGUSR2
Expand Down

0 comments on commit 71ab3f2

Please sign in to comment.