diff --git a/ptvsd/__main__.py b/ptvsd/__main__.py index 94b082f7..49015ada 100644 --- a/ptvsd/__main__.py +++ b/ptvsd/__main__.py @@ -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') diff --git a/ptvsd/_remote.py b/ptvsd/_remote.py index 20d44888..49704625 100644 --- a/ptvsd/_remote.py +++ b/ptvsd/_remote.py @@ -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, diff --git a/ptvsd/pydevd_hooks.py b/ptvsd/pydevd_hooks.py index 9a0b480c..b83a29f0 100644 --- a/ptvsd/pydevd_hooks.py +++ b/ptvsd/pydevd_hooks.py @@ -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 diff --git a/tests/ptvsd/test___main__.py b/tests/ptvsd/test___main__.py index f921d8f6..9188dfa8 100644 --- a/tests/ptvsd/test___main__.py +++ b/tests/ptvsd/test___main__.py @@ -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)