Skip to content

Commit f010360

Browse files
authored
Add support for dmypy on Windows (#5859)
This also adds an IPC module to abstract communication between the client and server for Unix and Windows. Closes #5019
1 parent b790539 commit f010360

File tree

9 files changed

+491
-160
lines changed

9 files changed

+491
-160
lines changed

docs/source/mypy_daemon.rst

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,19 @@ you'll find errors sooner.
2222
The mypy daemon is experimental. In particular, the command-line
2323
interface may change in future mypy releases.
2424

25-
.. note::
26-
27-
The mypy daemon currently supports macOS and Linux only.
28-
2925
.. note::
3026

3127
Each mypy daemon process supports one user and one set of source files,
3228
and it can only process one type checking request at a time. You can
3329
run multiple mypy daemon processes to type check multiple repositories.
3430

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.
37+
3538
Basic usage
3639
***********
3740

@@ -103,5 +106,3 @@ Limitations
103106
limitation. This can be defined
104107
through the command line or through a
105108
:ref:`configuration file <config-file>`.
106-
107-
* Windows is not supported.

mypy/dmypy.py

Lines changed: 45 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,21 @@
77
"""
88

99
import argparse
10+
import base64
1011
import json
1112
import os
13+
import pickle
1214
import signal
13-
import socket
15+
import subprocess
1416
import sys
1517
import time
1618

17-
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple
19+
from typing import Any, Callable, Dict, Mapping, Optional, Tuple
1820

1921
from mypy.dmypy_util import STATUS_FILE, receive
22+
from mypy.ipc import IPCClient, IPCException
23+
from mypy.dmypy_os import alive, kill
24+
2025
from mypy.version import __version__
2126

2227
# Argument parser. Subparsers are tied to action functions by the
@@ -92,7 +97,7 @@ def __init__(self, prog: str) -> None:
9297
help="Server shutdown timeout (in seconds)")
9398
p.add_argument('flags', metavar='FLAG', nargs='*', type=str,
9499
help="Regular mypy flags (precede with --)")
95-
100+
p.add_argument('--options-data', help=argparse.SUPPRESS)
96101
help_parser = p = subparsers.add_parser('help')
97102

98103
del p
@@ -179,10 +184,9 @@ def restart_server(args: argparse.Namespace, allow_sources: bool = False) -> Non
179184
def start_server(args: argparse.Namespace, allow_sources: bool = False) -> None:
180185
"""Start the server from command arguments and wait for it."""
181186
# Lazy import so this import doesn't slow down other commands.
182-
from mypy.dmypy_server import daemonize, Server, process_start_options
183-
if daemonize(Server(process_start_options(args.flags, allow_sources),
184-
timeout=args.timeout).serve,
185-
args.log_file) != 0:
187+
from mypy.dmypy_server import daemonize, process_start_options
188+
start_options = process_start_options(args.flags, allow_sources)
189+
if daemonize(start_options, timeout=args.timeout, log_file=args.log_file):
186190
sys.exit(1)
187191
wait_for_server()
188192

@@ -201,7 +205,7 @@ def wait_for_server(timeout: float = 5.0) -> None:
201205
time.sleep(0.1)
202206
continue
203207
# If the file's content is bogus or the process is dead, fail.
204-
pid, sockname = check_status(data)
208+
check_status(data)
205209
print("Daemon started")
206210
return
207211
sys.exit("Timed out waiting for daemon to start")
@@ -224,7 +228,6 @@ def do_run(args: argparse.Namespace) -> None:
224228
if not is_running():
225229
# Bad or missing status file or dead process; good to start.
226230
start_server(args, allow_sources=True)
227-
228231
t0 = time.time()
229232
response = request('run', version=__version__, args=args.flags)
230233
# If the daemon signals that a restart is necessary, do it
@@ -273,9 +276,9 @@ def do_stop(args: argparse.Namespace) -> None:
273276
@action(kill_parser)
274277
def do_kill(args: argparse.Namespace) -> None:
275278
"""Kill daemon process with SIGKILL."""
276-
pid, sockname = get_status()
279+
pid, _ = get_status()
277280
try:
278-
os.kill(pid, signal.SIGKILL)
281+
kill(pid)
279282
except OSError as err:
280283
sys.exit(str(err))
281284
else:
@@ -363,7 +366,20 @@ def do_daemon(args: argparse.Namespace) -> None:
363366
"""Serve requests in the foreground."""
364367
# Lazy import so this import doesn't slow down other commands.
365368
from mypy.dmypy_server import Server, process_start_options
366-
Server(process_start_options(args.flags, allow_sources=False), timeout=args.timeout).serve()
369+
if args.options_data:
370+
from mypy.options import Options
371+
options_dict, timeout, log_file = pickle.loads(base64.b64decode(args.options_data))
372+
options_obj = Options()
373+
options = options_obj.apply_changes(options_dict)
374+
if log_file:
375+
sys.stdout = sys.stderr = open(log_file, 'a', buffering=1)
376+
fd = sys.stdout.fileno()
377+
os.dup2(fd, 2)
378+
os.dup2(fd, 1)
379+
else:
380+
options = process_start_options(args.flags, allow_sources=False)
381+
timeout = args.timeout
382+
Server(options, timeout=timeout).serve()
367383

368384

369385
@action(help_parser)
@@ -375,7 +391,7 @@ def do_help(args: argparse.Namespace) -> None:
375391
# Client-side infrastructure.
376392

377393

378-
def request(command: str, *, timeout: Optional[float] = None,
394+
def request(command: str, *, timeout: Optional[int] = None,
379395
**kwds: object) -> Dict[str, Any]:
380396
"""Send a request to the daemon.
381397
@@ -384,35 +400,30 @@ def request(command: str, *, timeout: Optional[float] = None,
384400
Raise BadStatus if there is something wrong with the status file
385401
or if the process whose pid is in the status file has died.
386402
387-
Return {'error': <message>} if a socket operation or receive()
403+
Return {'error': <message>} if an IPC operation or receive()
388404
raised OSError. This covers cases such as connection refused or
389405
closed prematurely as well as invalid JSON received.
390406
"""
407+
response = {} # type: Dict[str, str]
391408
args = dict(kwds)
392409
args.update(command=command)
393410
bdata = json.dumps(args).encode('utf8')
394-
pid, sockname = get_status()
395-
sock = socket.socket(socket.AF_UNIX)
396-
if timeout is not None:
397-
sock.settimeout(timeout)
411+
_, name = get_status()
398412
try:
399-
sock.connect(sockname)
400-
sock.sendall(bdata)
401-
sock.shutdown(socket.SHUT_WR)
402-
response = receive(sock)
403-
except OSError as err:
413+
with IPCClient(name, timeout) as client:
414+
client.write(bdata)
415+
response = receive(client)
416+
except (OSError, IPCException) as err:
404417
return {'error': str(err)}
405418
# TODO: Other errors, e.g. ValueError, UnicodeError
406419
else:
407420
return response
408-
finally:
409-
sock.close()
410421

411422

412423
def get_status() -> Tuple[int, str]:
413424
"""Read status file and check if the process is alive.
414425
415-
Return (pid, sockname) on success.
426+
Return (pid, connection_name) on success.
416427
417428
Raise BadStatus if something's wrong.
418429
"""
@@ -423,7 +434,7 @@ def get_status() -> Tuple[int, str]:
423434
def check_status(data: Dict[str, Any]) -> Tuple[int, str]:
424435
"""Check if the process is alive.
425436
426-
Return (pid, sockname) on success.
437+
Return (pid, connection_name) on success.
427438
428439
Raise BadStatus if something's wrong.
429440
"""
@@ -432,16 +443,14 @@ def check_status(data: Dict[str, Any]) -> Tuple[int, str]:
432443
pid = data['pid']
433444
if not isinstance(pid, int):
434445
raise BadStatus("pid field is not an int")
435-
try:
436-
os.kill(pid, 0)
437-
except OSError:
446+
if not alive(pid):
438447
raise BadStatus("Daemon has died")
439-
if 'sockname' not in data:
440-
raise BadStatus("Invalid status file (no sockname field)")
441-
sockname = data['sockname']
442-
if not isinstance(sockname, str):
443-
raise BadStatus("sockname field is not a string")
444-
return pid, sockname
448+
if 'connection_name' not in data:
449+
raise BadStatus("Invalid status file (no connection_name field)")
450+
connection_name = data['connection_name']
451+
if not isinstance(connection_name, str):
452+
raise BadStatus("connection_name field is not a string")
453+
return pid, connection_name
445454

446455

447456
def read_status() -> Dict[str, object]:

mypy/dmypy_os.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import sys
2+
3+
from typing import Any, Callable
4+
5+
if sys.platform == 'win32':
6+
import ctypes
7+
from ctypes.wintypes import DWORD, HANDLE
8+
import subprocess
9+
10+
PROCESS_QUERY_LIMITED_INFORMATION = ctypes.c_ulong(0x1000)
11+
12+
kernel32 = ctypes.windll.kernel32
13+
OpenProcess = kernel32.OpenProcess # type: Callable[[DWORD, int, int], HANDLE]
14+
GetExitCodeProcess = kernel32.GetExitCodeProcess # type: Callable[[HANDLE, Any], int]
15+
else:
16+
import os
17+
import signal
18+
19+
20+
def alive(pid: int) -> bool:
21+
"""Is the process alive?"""
22+
if sys.platform == 'win32':
23+
# why can't anything be easy...
24+
status = DWORD()
25+
handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
26+
0,
27+
pid)
28+
GetExitCodeProcess(handle, ctypes.byref(status))
29+
return status.value == 259 # STILL_ACTIVE
30+
else:
31+
try:
32+
os.kill(pid, 0)
33+
except OSError:
34+
return False
35+
return True
36+
37+
38+
def kill(pid: int) -> None:
39+
"""Kill the process."""
40+
if sys.platform == 'win32':
41+
subprocess.check_output("taskkill /pid {pid} /f /t".format(pid=pid))
42+
else:
43+
os.kill(pid, signal.SIGKILL)

0 commit comments

Comments
 (0)