Fix #810: Sub-Process: monkey patch pydevd to start process using ptvsd (#862)

* Fix #810: Sub-Process: monkey patch pydevd to start process using ptvsd

* Add --multiprocess to ParseArgsTests tests.

* Pass port range down to child processes, and let them pick a port themselves.

* Remove ptvsd.port and replace with debugging output.

Pick ports from the range randomly rather than always scanning start to end, to reduce collisions.
This commit is contained in:
Pavel Minaev 2018-10-02 17:20:40 -07:00 committed by Karthik Nadig
parent fc921c0aef
commit 55059ca379
4 changed files with 103 additions and 16 deletions

View file

@ -6,6 +6,7 @@ import argparse
import os.path
import sys
from ptvsd import pydevd_hooks
from ptvsd._attach import attach_main
from ptvsd._local import debug_main, run_main
from ptvsd.socket import Address
@ -34,7 +35,6 @@ PYDEVD_FLAGS = {
'--DEBUG_RECORD_SOCKET_READS',
'--cmd-line',
'--module',
'--multiproc',
'--multiprocess',
'--print-in-debugger-startup',
'--save-signatures',
@ -44,12 +44,6 @@ PYDEVD_FLAGS = {
'--qt-support=auto',
}
USAGE = """
{0} [-h] [-V] [--nodebug] [--host HOST | --server-host HOST] --port PORT -m MODULE [arg ...]
{0} [-h] [-V] [--nodebug] [--host HOST | --server-host HOST] --port PORT FILENAME [arg ...]
{0} [-h] [-V] --host HOST --port PORT --pid PROCESS_ID
""" # noqa
def parse_args(argv=None):
"""Return the parsed args to use in main()."""
@ -131,7 +125,7 @@ def _group_args(argv):
supported.append(arg)
# ptvsd support
elif arg in ('--host', '--server-host', '--port', '--pid', '-m'):
elif arg in ('--host', '--server-host', '--port', '--pid', '-m', '--multiprocess-port-range'):
if arg == '-m' or arg == '--pid':
gottarget = True
supported.append(arg)
@ -153,10 +147,7 @@ def _group_args(argv):
def _parse_args(prog, argv):
parser = argparse.ArgumentParser(
prog=prog,
usage=USAGE.format(prog),
)
parser = argparse.ArgumentParser(prog=prog)
parser.add_argument('--nodebug', action='store_true')
@ -165,6 +156,15 @@ def _parse_args(prog, argv):
host.add_argument('--server-host')
parser.add_argument('--port', type=int, required=True)
def port_range(arg):
arg = tuple(int(s) for s in arg.split('-'))
if len(arg) != 2:
raise ValueError
return arg
parser.add_argument('--multiprocess', action='store_true')
parser.add_argument('--multiprocess-port-range', type=port_range)
target = parser.add_mutually_exclusive_group(required=True)
target.add_argument('-m', dest='module')
target.add_argument('--pid', type=int)
@ -191,6 +191,10 @@ def _parse_args(prog, argv):
else:
args.address = Address.as_client(clienthost, ns.pop('port'))
multiprocess_port_range = ns.pop('multiprocess_port_range')
if multiprocess_port_range is not None:
pydevd_hooks.multiprocess_port_range = multiprocess_port_range
pid = ns.pop('pid')
module = ns.pop('module')
filename = ns.pop('filename')

View file

@ -35,7 +35,7 @@ def _pydevd_settrace(redirect_output=None, _pydevd=pydevd, **kwargs):
# Then move at least some parts to the appropriate modules. This module
# is focused on running the debugger.
global_next_session = None
global_next_session = lambda: None
def enable_attach(address, redirect_output=True,

View file

@ -2,15 +2,23 @@
# Licensed under the MIT License. See LICENSE in the project root
# for license information.
import os
import sys
import pydevd
from _pydev_bundle import pydev_monkey
from _pydevd_bundle import pydevd_comm
import ptvsd
from ptvsd.socket import Address
from ptvsd.daemon import Daemon, DaemonStoppedError, DaemonClosedError
from ptvsd._util import debug, new_hidden_thread
# The intersection of default ephemeral port ranges for various common systems.
multiprocess_port_range = (49152, 61000)
def start_server(daemon, host, port, **kwargs):
"""Return a socket to a (new) local pydevd-handling daemon.
@ -67,7 +75,55 @@ def start_client(daemon, host, port, **kwargs):
return sock
def install(pydevd, address,
# See pydevd/_vendored/pydevd/_pydev_bundle/pydev_monkey.py
def get_python_c_args(host, port, indC, args, setup):
runner = '''
import os
import random
import sys
sys.path.append(r'{ptvsd_syspath}')
import ptvsd
from ptvsd._util import DEBUG
from ptvsd import pydevd_hooks
pydevd_hooks.multiprocess_port_range = ({first_port}, {last_port})
from _pydev_bundle import pydev_monkey
pydev_monkey.patch_new_process_functions()
ports = list(range({first_port}, {last_port}))
random.shuffle(ports)
for port in ports:
try:
ptvsd.enable_attach(('localhost', port))
except IOError:
pass
else:
if DEBUG:
print('Child process %d listening on port %d' % (os.getpid(), port))
break
else:
raise Exception('Could not find a free port in range {first_port}-{last_port}')
ptvsd.wait_for_attach()
{rest}
'''
first_port, last_port = multiprocess_port_range
# __file__ will be .../ptvsd/__init__.py, and we want the ...
ptvsd_syspath = os.path.join(ptvsd.__file__, '../..')
return runner.format(
first_port=first_port,
last_port=last_port,
ptvsd_syspath=ptvsd_syspath,
rest=args[indC + 1])
def install(pydevd_module, address,
start_server=start_server, start_client=start_client,
**kwargs):
"""Configure pydevd to use our wrapper.
@ -89,12 +145,16 @@ def install(pydevd, address,
pydevd_comm.start_server = _start_server
pydevd_comm.start_client = _start_client
# This is invoked when a child process is spawned with multiproc debugging enabled.
pydev_monkey._get_python_c_args = get_python_c_args
# Ensure that pydevd is using our functions.
pydevd.start_server = _start_server
pydevd.start_client = _start_client
pydevd_module.start_server = _start_server
pydevd_module.start_client = _start_client
__main__ = sys.modules['__main__']
if __main__ is not pydevd:
if getattr(__main__, '__file__', None) == pydevd.__file__:
__main__.start_server = _start_server
__main__.start_client = _start_client
return daemon

View file

@ -23,6 +23,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': False,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -41,6 +42,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': False,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -59,6 +61,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': True,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -76,6 +79,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': False,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -94,6 +98,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': False,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -112,6 +117,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': True,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -130,6 +136,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': False,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -148,6 +155,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': False,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -167,6 +175,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': True,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -185,6 +194,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': False,
'single_session': True,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -204,6 +214,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': False,
'single_session': True,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -223,6 +234,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': False,
'single_session': False,
'wait': True,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -249,6 +261,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': False,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, [
'--DEBUG',
@ -286,6 +299,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': True,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, [
'--DEBUG',
@ -314,6 +328,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': False,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -342,6 +357,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': False,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -361,6 +377,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': True,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -379,6 +396,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': False,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -398,6 +416,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': True,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -415,6 +434,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': False,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -433,6 +453,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': True,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, self.EXPECTED_EXTRA)
@ -451,6 +472,7 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': False,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, ['--module'] + self.EXPECTED_EXTRA)
@ -470,5 +492,6 @@ class ParseArgsTests(unittest.TestCase):
'nodebug': True,
'single_session': False,
'wait': False,
'multiprocess': False,
})
self.assertEqual(extra, ['--module'] + self.EXPECTED_EXTRA)