8000 Implement overlapped I/O and timeouts on server side Windows IPC (#6148) · python/mypy@41562a0 · GitHub
[go: up one dir, main page]

Skip to content

Commit 41562a0

Browse files
authored
Implement overlapped I/O and timeouts on server side Windows IPC (#6148)
This PR does a few things: - Changes reads/writes/connects to use overlapped I/O on Windows - Removes the need for special casing b'' as an EOF signal - Adds support for timing out on connects for the server process (which means dmypy's --timeout flag works on Windows). - Make the default timeout infinite (though this doesn't change anything functionally since we always specify alternatives in our usage) Fixes #5962
1 parent 5d53566 commit 41562a0

File tree

3 files changed

+79
-34
lines changed

3 files changed

+79
-34
lines changed

docs/source/mypy_daemon.rst

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,6 @@ you'll find errors sooner.
2828
and it can only process one type checking request at a time. You can
2929
run multiple mypy daemon processes to type check multiple repositories.
3030

31-
.. note::
32-
33-
On Windows, due to platform limitations, the mypy daemon does not currently
34-
support a timeout for the server process. The client will still time out if
35-
a connection to the server cannot be made, but the server will wait forever
36-
for a new client connection.
3731

3832
Basic usage
3933
***********

mypy/ipc.py

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -47,36 +47,66 @@ class IPCBase:
4747

4848
connection = None # type: _IPCHandle
4949

50-
def __init__(self, name: str) -> None:
50+
def __init__(self, name: str, timeout: Optional[float]) -> None:
5151
self.READ_SIZE = 100000
5252
self.name = name
53+
self.timeout = timeout
5354

5455
def read(self) -> bytes:
5556
"""Read bytes from an IPC connection until its empty."""
5657
bdata = bytearray()
57-
while True:
58-
if sys.platform == 'win32':
59-
more, _ = _winapi.ReadFile(self.connection, self.READ_SIZE)
60-
else:
58+
if sys.platform == 'win32':
59+
while True:
60+
ov, err = _winapi.ReadFile(self.connection, self.READ_SIZE, overlapped=True)
61+
# TODO: remove once typeshed supports Literal types
62+
assert isinstance(ov, _winapi.Overlapped)
63+
assert isinstance(err, int)
64+
try:
65+
if err != 0:
66+
assert err == _winapi.ERROR_IO_PENDING
67+
timeout = int(self.timeout * 1000) if self.timeout else _winapi.INFINITE
68+
res = _winapi.WaitForSingleObject(ov.event, timeout)
69+
assert res == _winapi.WAIT_OBJECT_0
70+
except BaseException:
71+
ov.cancel()
72+
raise
73+
_, err = ov.GetOverlappedResult(True)
74+
more = ov.getbuffer()
75+
if more:
76+
bdata.extend(more)
77+
if err == 0:
78+
# we are done!
79+
break
80+
elif err == _winapi.ERROR_OPERATION_ABORTED:
81+
raise IPCException("ReadFile operation aborted.")
82+
else:
83+
while True:
6184
more = self.connection.recv(self.READ_SIZE)
62-
if not more:
63-
break
64-
bdata.extend(more)
85+
if not more:
86+
break
87+
bdata.extend(more)
6588
return bytes(bdata)
6689

6790
def write(self, data: bytes) -> None:
6891
"""Write bytes to an IPC connection."""
6992
if sys.platform == 'win32':
7093
try:
71-
# Only send data if there is data to send, to avoid it
72-
# being confused with the empty message sent to terminate
73-
# the connection. (We will still send the end-of-message
74-
# empty message below, which will cause read to return.)
75-
if data:
67E6
76-
_winapi.WriteFile(self.connection, data)
77-
# this empty write is to copy the behavior of socket.sendall,
78-
# which also sends an empty message to signify it is done writing
79-
_winapi.WriteFile(self.connection, b'')
94+
ov, err = _winapi.WriteFile(self.connection, data, overlapped=True)
95+
# TODO: remove once typeshed supports Literal types
96+
assert isinstance(ov, _winapi.Overlapped)
97+
assert isinstance(err, int)
98+
try:
99+
if err != 0:
100+
assert err == _winapi.ERROR_IO_PENDING
101+
timeout = int(self.timeout * 1000) if self.timeout else _winapi.INFINITE
102+
res = _winapi.WaitForSingleObject(ov.event, timeout)
103+
assert res == _winapi.WAIT_OBJECT_0
104+
except BaseException:
105+
ov.cancel()
106+
raise
107+
bytes_written, err = ov.GetOverlappedResult(True)
108+
assert err == 0
109+
assert bytes_written == len(data)
80110
except WindowsError as e:
81111
raise IPCException("Failed to write with error: {}".format(e.winerror))
82112
else:
@@ -95,9 +125,9 @@ class IPCClient(IPCBase):
95125
"""The client side of an IPC connection."""
96126

97127
def __init__(self, name: str, timeout: Optional[float]) -> None:
98-
super().__init__(name)
128+
super().__init__(name, timeout)
99129
if sys.platform == 'win32':
100-
timeout = int(timeout * 1000) if timeout else 0xFFFFFFFF # NMPWAIT_WAIT_FOREVER
130+
timeout = int(self.timeout * 1000) if self.timeout else _winapi.NMPWAIT_WAIT_FOREVER
101131
try:
102132
_winapi.WaitNamedPipe(self.name, timeout)
103133
except FileNotFoundError:
@@ -114,7 +144,7 @@ def __init__(self, name: str, timeout: Optional[float]) -> None:
114144
0,
115145
_winapi.NULL,
116146
_winapi.OPEN_EXISTING,
117-
0,
147+
_winapi.FILE_FLAG_OVERLAPPED,
118148
_winapi.NULL,
119149
)
120150
except WindowsError as e:
@@ -147,25 +177,26 @@ class IPCServer(IPCBase):
147177

148178
BUFFER_SIZE = 2**16
149179

150-
def __init__(self, name: str, timeout: Optional[int] = None) -> None:
180+
def __init__(self, name: str, timeout: Optional[float] = None) -> None:
151181
if sys.platform == 'win32':
152182
name = r'\\.\pipe\{}-{}.pipe'.format(
153183
name, base64.urlsafe_b64encode(os.urandom(6)).decode())
154184
else:
155185
name = '{}.sock'.format(name)
156-
super().__init__(name)
186+
super().__init__(name, timeout)
157187
if sys.platform == 'win32':
158188
self.connection = _winapi.CreateNamedPipe(self.name,
159189
_winapi.PIPE_ACCESS_DUPLEX
160-
| _winapi.FILE_FLAG_FIRST_PIPE_INSTANCE,
190+
| _winapi.FILE_FLAG_FIRST_PIPE_INSTANCE
191+
| _winapi.FILE_FLAG_OVERLAPPED,
161192
_winapi.PIPE_READMODE_MESSAGE
162193
| _winapi.PIPE_TYPE_MESSAGE
163194
| _winapi.PIPE_WAIT
164195
| 0x8, # PIPE_REJECT_REMOTE_CLIENTS
165196
1, # one instance
166197
self.BUFFER_SIZE,
167198
self.BUFFER_SIZE,
168-
1000, # Default timeout in milis
199+
_winapi.NMPWAIT_WAIT_FOREVER,
169200
0, # Use default security descriptor
170201
)
171202
if self.connection == -1: # INVALID_HANDLE_VALUE
@@ -185,12 +216,24 @@ def __enter__(self) -> 'IPCServer':
185216
# NOTE: It is theoretically possible that this will hang forever if the
186217
# client never connects, though this can be "solved" by killing the server
187218
try:
188-
_winapi.ConnectNamedPipe(self.connection, _winapi.NULL)
219+
ov = _winapi.ConnectNamedPipe(self.connection, overlapped=True)
220+
# TODO: remove once typeshed supports Literal types
221+
assert isinstance(ov, _winapi.Overlapped)
189222
except WindowsError as e:
190-
if e.winerror == _winapi.ERROR_PIPE_CONNECTED:
191-
pass # The client already exists, which is fine.
192-
else:
223+
# Don't raise if the client already exists, or the client already connected
224+
if e.winerror not in (_winapi.ERROR_PIPE_CONNECTED, _winapi.ERROR_NO_DATA):
225+
raise
226+
else:
227+
try:
228+
timeout = int(self.timeout * 1000) if self.timeout else _winapi.INFINITE
229+
res = _winapi.WaitForSingleObject(ov.event, timeout)
230+
assert res == _winapi.WAIT_OBJECT_0
231+
except BaseException:
232+
ov.cancel()
233+
_winapi.CloseHandle(self.connection)
193234
raise
235+
_, err = ov.GetOverlappedResult(True)
236+
assert err == 0
194237
else:
195238
try:
196239
self.connection, _ = self.sock.accept()

test-data/unit/daemon.test

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,14 @@ import bar
123123
[file bar.py]
124124
pass
125125

126+
[case testDaemonTimeout]
127+
$ dmypy start --timeout 1 -- --follow-imports=error
128+
Daemon started
129+
$ {python} -c "import time;time.sleep(1)"
130+
$ dmypy status
131+
No status file found
132+
== Return code: 2
133+
126134
[case testDaemonRunNoTarget]
127135
$ dmypy run -- --follow-imports=error
128136
Daemon started

0 commit comments

Comments
 (0)
0