Skip to content

Add support for dmypy on Windows #5859

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 56 commits into from
Nov 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
63aa4e2
Initial dmypy for Windows work
emmatyping Oct 26, 2018
abc64b7
Fixes related to dmypy server on Windows
emmatyping Oct 30, 2018
cf33bfa
Get dmypy start,stop,run,kill,check,restart working! :tada:
emmatyping Oct 31, 2018
54859da
Only use _winapi on Windows
emmatyping Oct 31, 2018
f760e16
Guard _winapi import dmypy_server
emmatyping Oct 31, 2018
966117c
Get dmypy status working on Windows
emmatyping Oct 31, 2018
68ee937
Add support for timeout
emmatyping Oct 31, 2018
f6a42c3
Fix lint on Linux
emmatyping Oct 31, 2018
168e177
Change a while 1 to a while True
emmatyping Oct 31, 2018
3e0d12e
Minor refactoring
emmatyping Oct 31, 2018
87e61c5
Move conditional imports to end of imports
emmatyping Oct 31, 2018
1883c31
Minor fixes, corrections, simplifications
emmatyping Oct 31, 2018
7932bb7
Merge branch 'master' of github.com:python/mypy into windmypy
emmatyping Oct 31, 2018
8fb7203
Remove outdated docs
emmatyping Nov 5, 2018
69d04dd
Respond to Guido's comments
emmatyping Nov 7, 2018
968e07c
Lower timeout back down to 5s
emmatyping Nov 8, 2018
037267a
Major refactoring into separate IPC module
emmatyping Nov 12, 2018
db202c2
Fix typing import for Python 3.5.1
emmatyping Nov 12, 2018
55250a8
Correct out of date comments and remove unused imports
emmatyping Nov 12, 2018
f066484
Quote types so they don't need to be imported at runtime
emmatyping Nov 12, 2018
54cba38
Minor code cleanup
emmatyping Nov 12, 2018
072518b
Remove unused loop
emmatyping Nov 12, 2018
f033c54
Remove some more unused imports
emmatyping Nov 12, 2018
768a2bf
Make pipe name and options file unique
emmatyping Nov 14, 2018
32ef5c9
Merge branch 'master' into windmypy
msullivan Nov 16, 2018
962072a
Move a conditional def out of IPCBase
msullivan Nov 16, 2018
8d909c5
Remove dead code
emmatyping Nov 16, 2018
7896a73
Merge branch 'windmypy' of github.com:ethanhs/mypy into windmypy
emmatyping Nov 16, 2018
eca5beb
Fix lint failure
emmatyping Nov 16, 2018
941b1f2
Move to passing options via command instead of file
emmatyping Nov 16, 2018
06459a8
Unify interface of daemonize
emmatyping Nov 16, 2018
1feaf0e
Add comment about IPC behavior
emmatyping Nov 16, 2018
34bd86a
Move process status checking and killing into dmypy_os
emmatyping Nov 16, 2018
c4d0bad
Add test for IPC
emmatyping Nov 16, 2018
ed4548d
Handle ERROR_PIPE_CONNECTED
emmatyping Nov 16, 2018
681408f
Give the server more time to get set up in testipc?
emmatyping Nov 16, 2018
f484143
Fix unix daemonize typo (and slightly regeneralize)
msullivan Nov 16, 2018
512c60e
Fix IPC tests
emmatyping Nov 16, 2018
09b4913
Fix typecheck of ipc tests
emmatyping Nov 16, 2018
d2695ca
Reraise if exceptions occure and unify IPC initialization
emmatyping Nov 17, 2018
76e0539
Merge branch master into ethanhs/windmypy
emmatyping Nov 20, 2018
4956188
Get tests passing on Windows
emmatyping Nov 20, 2018
4e0dcc1
Merge branch 'windmypy' of github.com:ethanhs/mypy into windmypy
emmatyping Nov 20, 2018
842712e
Try an empty string/quotes instead of single quotes
emmatyping Nov 20, 2018
5d9e149
Really fix daemon tests
emmatyping Nov 20, 2018
959f89a
Remove busy wait and give better debug info
emmatyping Nov 20, 2018
326758c
Temporary debug info
emmatyping Nov 20, 2018
d806282
Reuse NamedPipe for multiple connections
emmatyping Nov 22, 2018
989ca72
Conditionally import Options in dmypy
emmatyping Nov 22, 2018
da719d6
Remove debug info
emmatyping Nov 22, 2018
c2d8d90
Merge upstream into windmypy
emmatyping Nov 27, 2018
a732a1e
Be more eager in removing status file
emmatyping Nov 27, 2018
dae10eb
Remove redundant parens
emmatyping Nov 27, 2018
9de1b88
Use random bytes for NamedPipe name
emmatyping Nov 27, 2018
f3b8bb6
Document timeouts on Windows
emmatyping Nov 27, 2018
0c43879
Add line in note
emmatyping Nov 27, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions docs/source/mypy_daemon.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,19 @@ you'll find errors sooner.
The mypy daemon is experimental. In particular, the command-line
interface may change in future mypy releases.

.. note::

The mypy daemon currently supports macOS and Linux only.

.. note::

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

.. note::

On Windows, due to platform limitations, the mypy daemon does not currently
support a timeout for the server process. The client will still time out if
a connection to the server cannot be made, but the server will wait forever
for a new client connection.

Basic usage
***********

Expand Down Expand Up @@ -103,5 +106,3 @@ Limitations
limitation. This can be defined
through the command line or through a
:ref:`configuration file <config-file>`.

* Windows is not supported.
81 changes: 45 additions & 36 deletions mypy/dmypy.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@
"""

import argparse
import base64
import json
import os
import pickle
import signal
import socket
import subprocess
import sys
import time

from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple
from typing import Any, Callable, Dict, Mapping, Optional, Tuple

from mypy.dmypy_util import STATUS_FILE, receive
from mypy.ipc import IPCClient, IPCException
from mypy.dmypy_os import alive, kill

from mypy.version import __version__

# Argument parser. Subparsers are tied to action functions by the
Expand Down Expand Up @@ -92,7 +97,7 @@ def __init__(self, prog: str) -> None:
help="Server shutdown timeout (in seconds)")
p.add_argument('flags', metavar='FLAG', nargs='*', type=str,
help="Regular mypy flags (precede with --)")

p.add_argument('--options-data', help=argparse.SUPPRESS)
help_parser = p = subparsers.add_parser('help')

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

Expand All @@ -201,7 +205,7 @@ def wait_for_server(timeout: float = 5.0) -> None:
time.sleep(0.1)
continue
# If the file's content is bogus or the process is dead, fail.
pid, sockname = check_status(data)
check_status(data)
print("Daemon started")
return
sys.exit("Timed out waiting for daemon to start")
Expand All @@ -224,7 +228,6 @@ def do_run(args: argparse.Namespace) -> None:
if not is_running():
# Bad or missing status file or dead process; good to start.
start_server(args, allow_sources=True)

t0 = time.time()
response = request('run', version=__version__, args=args.flags)
# If the daemon signals that a restart is necessary, do it
Expand Down Expand Up @@ -273,9 +276,9 @@ def do_stop(args: argparse.Namespace) -> None:
@action(kill_parser)
def do_kill(args: argparse.Namespace) -> None:
"""Kill daemon process with SIGKILL."""
pid, sockname = get_status()
pid, _ = get_status()
try:
os.kill(pid, signal.SIGKILL)
kill(pid)
except OSError as err:
sys.exit(str(err))
else:
Expand Down Expand Up @@ -363,7 +366,20 @@ def do_daemon(args: argparse.Namespace) -> None:
"""Serve requests in the foreground."""
# Lazy import so this import doesn't slow down other commands.
from mypy.dmypy_server import Server, process_start_options
Server(process_start_options(args.flags, allow_sources=False), timeout=args.timeout).serve()
if args.options_data:
from mypy.options import Options
options_dict, timeout, log_file = pickle.loads(base64.b64decode(args.options_data))
options_obj = Options()
options = options_obj.apply_changes(options_dict)
if log_file:
sys.stdout = sys.stderr = open(log_file, 'a', buffering=1)
fd = sys.stdout.fileno()
os.dup2(fd, 2)
os.dup2(fd, 1)
else:
options = process_start_options(args.flags, allow_sources=False)
timeout = args.timeout
Server(options, timeout=timeout).serve()


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


def request(command: str, *, timeout: Optional[float] = None,
def request(command: str, *, timeout: Optional[int] = None,
**kwds: object) -> Dict[str, Any]:
"""Send a request to the daemon.

Expand All @@ -384,35 +400,30 @@ def request(command: str, *, timeout: Optional[float] = None,
Raise BadStatus if there is something wrong with the status file
or if the process whose pid is in the status file has died.

Return {'error': <message>} if a socket operation or receive()
Return {'error': <message>} if an IPC operation or receive()
raised OSError. This covers cases such as connection refused or
closed prematurely as well as invalid JSON received.
"""
response = {} # type: Dict[str, str]
args = dict(kwds)
args.update(command=command)
bdata = json.dumps(args).encode('utf8')
pid, sockname = get_status()
sock = socket.socket(socket.AF_UNIX)
if timeout is not None:
sock.settimeout(timeout)
_, name = get_status()
try:
sock.connect(sockname)
sock.sendall(bdata)
sock.shutdown(socket.SHUT_WR)
response = receive(sock)
except OSError as err:
with IPCClient(name, timeout) as client:
client.write(bdata)
response = receive(client)
except (OSError, IPCException) as err:
return {'error': str(err)}
# TODO: Other errors, e.g. ValueError, UnicodeError
else:
return response
finally:
sock.close()


def get_status() -> Tuple[int, str]:
"""Read status file and check if the process is alive.

Return (pid, sockname) on success.
Return (pid, connection_name) on success.

Raise BadStatus if something's wrong.
"""
Expand All @@ -423,7 +434,7 @@ def get_status() -> Tuple[int, str]:
def check_status(data: Dict[str, Any]) -> Tuple[int, str]:
"""Check if the process is alive.

Return (pid, sockname) on success.
Return (pid, connection_name) on success.

Raise BadStatus if something's wrong.
"""
Expand All @@ -432,16 +443,14 @@ def check_status(data: Dict[str, Any]) -> Tuple[int, str]:
pid = data['pid']
if not isinstance(pid, int):
raise BadStatus("pid field is not an int")
try:
os.kill(pid, 0)
except OSError:
if not alive(pid):
raise BadStatus("Daemon has died")
if 'sockname' not in data:
raise BadStatus("Invalid status file (no sockname field)")
sockname = data['sockname']
if not isinstance(sockname, str):
raise BadStatus("sockname field is not a string")
return pid, sockname
if 'connection_name' not in data:
raise BadStatus("Invalid status file (no connection_name field)")
connection_name = data['connection_name']
if not isinstance(connection_name, str):
raise BadStatus("connection_name field is not a string")
return pid, connection_name


def read_status() -> Dict[str, object]:
Expand Down
43 changes: 43 additions & 0 deletions mypy/dmypy_os.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import sys

from typing import Any, Callable

if sys.platform == 'win32':
import ctypes
from ctypes.wintypes import DWORD, HANDLE
import subprocess

PROCESS_QUERY_LIMITED_INFORMATION = ctypes.c_ulong(0x1000)

kernel32 = ctypes.windll.kernel32
OpenProcess = kernel32.OpenProcess # type: Callable[[DWORD, int, int], HANDLE]
GetExitCodeProcess = kernel32.GetExitCodeProcess # type: Callable[[HANDLE, Any], int]
else:
import os
import signal


def alive(pid: int) -> bool:
"""Is the process alive?"""
if sys.platform == 'win32':
# why can't anything be easy...
status = DWORD()
handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION,
0,
pid)
GetExitCodeProcess(handle, ctypes.byref(status))
return status.value == 259 # STILL_ACTIVE
else:
try:
os.kill(pid, 0)
except OSError:
return False
return True


def kill(pid: int) -> None:
"""Kill the process."""
if sys.platform == 'win32':
subprocess.check_output("taskkill /pid {pid} /f /t".format(pid=pid))
else:
os.kill(pid, signal.SIGKILL)
Loading