Repository: microsoft/ptvsd

Issue:
Sub-Process: monkey patch pydevd to start process using ptvsd
Monkey patch `pydev_monkey.py` to start new processes using `ptvsd`. Ensure that the new process uses `--wait` feature.


Discussion:

Resolving patch:
diff --git a/ptvsd/__main__.py b/ptvsd/__main__.py
--- a/ptvsd/__main__.py
+++ b/ptvsd/__main__.py
@@ -6,6 +6,7 @@
 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 @@
     '--DEBUG_RECORD_SOCKET_READS',
     '--cmd-line',
     '--module',
-    '--multiproc',
     '--multiprocess',
     '--print-in-debugger-startup',
     '--save-signatures',
@@ -44,12 +44,6 @@
     '--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
--- 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
--- 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


Test patch:
diff --git a/tests/ptvsd/test___main__.py b/tests/ptvsd/test___main__.py
--- a/tests/ptvsd/test___main__.py
+++ b/tests/ptvsd/test___main__.py
@@ -23,6 +23,7 @@ def test_module(self):
             'nodebug': False,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -41,6 +42,7 @@ def test_module_server(self):
             'nodebug': False,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -59,6 +61,7 @@ def test_module_nodebug(self):
             'nodebug': True,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -76,6 +79,7 @@ def test_script(self):
             'nodebug': False,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -94,6 +98,7 @@ def test_script_server(self):
             'nodebug': False,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -112,6 +117,7 @@ def test_script_nodebug(self):
             'nodebug': True,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -130,6 +136,7 @@ def test_remote(self):
             'nodebug': False,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -148,6 +155,7 @@ def test_remote_localhost(self):
             'nodebug': False,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -167,6 +175,7 @@ def test_remote_nodebug(self):
             'nodebug': True,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -185,6 +194,7 @@ def test_remote_single_session(self):
             'nodebug': False,
             'single_session': True,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -204,6 +214,7 @@ def test_local_single_session(self):
             'nodebug': False,
             'single_session': True,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -223,6 +234,7 @@ def test_remote_wait(self):
             'nodebug': False,
             'single_session': False,
             'wait': True,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -249,6 +261,7 @@ def test_extra(self):
             'nodebug': False,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, [
             '--DEBUG',
@@ -286,6 +299,7 @@ def test_extra_nodebug(self):
             'nodebug': True,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, [
             '--DEBUG',
@@ -314,6 +328,7 @@ def test_empty_host(self):
             'nodebug': False,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -342,6 +357,7 @@ def test_backward_compatibility_host(self):
             'nodebug': False,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -361,6 +377,7 @@ def test_backward_compatibility_host_nodebug(self):
             'nodebug': True,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -379,6 +396,7 @@ def test_backward_compatibility_module(self):
             'nodebug': False,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -398,6 +416,7 @@ def test_backward_compatibility_module_nodebug(self):
             'nodebug': True,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -415,6 +434,7 @@ def test_backward_compatibility_script(self):
             'nodebug': False,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -433,6 +453,7 @@ def test_backward_compatibility_script_nodebug(self):
             'nodebug': True,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, self.EXPECTED_EXTRA)
 
@@ -451,6 +472,7 @@ def test_pseudo_backward_compatibility(self):
             'nodebug': False,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, ['--module'] + self.EXPECTED_EXTRA)
 
@@ -470,5 +492,6 @@ def test_pseudo_backward_compatibility_nodebug(self):
             'nodebug': True,
             'single_session': False,
             'wait': False,
+            'multiprocess': False,
         })
         self.assertEqual(extra, ['--module'] + self.EXPECTED_EXTRA)