Repository: microsoft/ptvsd

Issue:
Refactor branch: Restore attach to pid tests



Discussion:

Resolving patch:
diff --git a/src/ptvsd/adapter/debuggee.py b/src/ptvsd/adapter/debuggee.py
--- a/src/ptvsd/adapter/debuggee.py
+++ b/src/ptvsd/adapter/debuggee.py
@@ -82,39 +82,39 @@ def attach_by_pid(request):
     """Start server to receive connection from the debug server injected into the
     debuggee process.
     """
-
     def _parse_request_and_inject(request, address):
-        cmdline = [sys.executable]
-
         host, port = address
         ptvsd_args = request("ptvsdArgs", json.array(unicode))
-        cmdline += [
+        cmdline = [
+            sys.executable,
             compat.filename(ptvsd.__main__.__file__),
             "--client",
             "--host",
             host,
             "--port",
             str(port),
+        ] + ptvsd_args + [
             "--pid",
-            str(request("processId", int)),
-        ] + ptvsd_args
+            str(request("processId", int))
+        ]
 
         log.debug("Launching debugger injector: {0!r}", cmdline)
 
         try:
             # This process will immediately exit after injecting debug server
-            proc = subprocess.Popen(cmdline, bufsize=0)
+            subprocess.Popen(
+                cmdline,
+                bufsize=0,
+                stdin=subprocess.PIPE,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE
+            )
         except Exception as exc:
             raise request.cant_handle("Error launching debug process: {0}", exc)
-        proc.wait()
-        if proc.returncode != 0:
-            raise request.cant_handle(
-                "Failed to inject debugger with error code: {0}", proc.returncode
-            )
 
     channels.Channels().accept_connection_from_server(
         ("127.0.0.1", 0),
-        before_accept=lambda address: _parse_request_and_inject(address),
+        before_accept=lambda address: _parse_request_and_inject(request, address),
     )
 
 
diff --git a/src/ptvsd/server/attach_pid_injected.py b/src/ptvsd/server/attach_pid_injected.py
new file mode 100644
--- /dev/null
+++ b/src/ptvsd/server/attach_pid_injected.py
@@ -0,0 +1,78 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License. See LICENSE in the project root
+# for license information.
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+import os
+
+
+__file__ = os.path.abspath(__file__)
+_ptvsd_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
+
+
+def attach(host, port, client, log_dir=None):
+    try:
+        import sys
+        if 'threading' not in sys.modules:
+            try:
+
+                def on_warn(msg):
+                    print(msg, file=sys.stderr)
+
+                def on_exception(msg):
+                    print(msg, file=sys.stderr)
+
+                def on_critical(msg):
+                    print(msg, file=sys.stderr)
+
+                pydevd_attach_to_process_path = os.path.join(
+                    _ptvsd_dir,
+                    'ptvsd',
+                    '_vendored',
+                    'pydevd',
+                    'pydevd_attach_to_process')
+                assert os.path.exists(pydevd_attach_to_process_path)
+                sys.path.insert(0, pydevd_attach_to_process_path)
+
+                # NOTE: that it's not a part of the pydevd PYTHONPATH
+                import attach_script
+                attach_script.fix_main_thread_id(
+                    on_warn=on_warn, on_exception=on_exception, on_critical=on_critical)
+
+                # NOTE: At this point it should be safe to remove this.
+                sys.path.remove(pydevd_attach_to_process_path)
+            except:
+                import traceback
+                traceback.print_exc()
+                raise
+
+        sys.path.insert(0, _ptvsd_dir)
+        import ptvsd
+
+        # NOTE: Don't do sys.path.remove here it will remove all instances of that path
+        # and the user may have set that to ptvsd path via PYTHONPATH
+        assert sys.path[0] == _ptvsd_dir
+        del sys.path[0]
+
+        from ptvsd.common import options as common_opts
+        from ptvsd.server import options
+        common_opts.log_dir = log_dir
+        options.client = client
+        options.host = host
+        options.port = port
+
+        from ptvsd.common import log
+        log.to_file()
+        log.info("Debugger injection begin")
+
+        if options.client:
+            ptvsd.attach((options.host, options.port))
+        else:
+            ptvsd.enable_attach((options.host, options.port))
+
+        log.info("Debugger successfully injected")
+    except:
+        import traceback
+        traceback.print_exc()
+        raise log.exception()
diff --git a/src/ptvsd/server/main.py b/src/ptvsd/server/main.py
--- a/src/ptvsd/server/main.py
+++ b/src/ptvsd/server/main.py
@@ -266,62 +266,69 @@ def run_code():
 
 
 def attach_to_pid():
-    def quoted_str(s):
-        if s is None:
-            return s
-        assert not isinstance(s, bytes)
-        unescaped = set(chr(ch) for ch in range(32, 127)) - {'"', "'", "\\"}
-
-        def escape(ch):
-            return ch if ch in unescaped else "\\u%04X" % ord(ch)
-
-        return 'u"' + "".join(map(escape, s)) + '"'
-
-    log.info("Attaching to process with ID {0}", options.target)
+    log.info('Attaching to process with ID {0}', options.target)
 
     pid = options.target
-    host = quoted_str(options.host)
+    host = options.host
     port = options.port
     client = options.client
-    multiprocess = options.multiprocess
-    log_dir = quoted_str(ptvsd.common.options.log_dir)
-
-    ptvsd_path = os.path.abspath(os.path.join(ptvsd.server.__file__, "../.."))
-    if isinstance(ptvsd_path, bytes):
-        ptvsd_path = ptvsd_path.decode(sys.getfilesystemencoding())
-    ptvsd_path = quoted_str(ptvsd_path)
-
-    # pydevd requires injected code to not contain any single quotes.
-    code = """
-import os
-assert os.getpid() == {pid}
-
-import sys
-sys.path.insert(0, {ptvsd_path})
-import ptvsd
-from ptvsd.common import log, options
-del sys.path[0]
+    log_dir = common_opts.log_dir
+    if log_dir is None:
+        log_dir = ""
 
-options.log_dir = {log_dir}
-log.to_file()
-log.info("Bootstrapping injected debugger.")
-
-debug = ptvsd.attach if {client} else ptvsd.enable_attach
-debug(({host}, {port}), multiprocess={multiprocess})
-""".format(
-        **locals()
-    )
-
-    log.debug('Injecting debugger into target process: \n"""{0}\n"""'.format(code))
-    assert "'" not in code, "Injected code should not contain any single quotes"
-
-    pydevd_attach_to_process_path = os.path.join(
-        os.path.dirname(pydevd.__file__), "pydevd_attach_to_process"
-    )
-    sys.path.insert(0, pydevd_attach_to_process_path)
-    from add_code_to_python_process import run_python_code
-
-    run_python_code(pid, code, connect_debugger_tracing=True)
+    try:
+        attach_pid_injected_dirname = os.path.join(os.path.dirname(ptvsd.__file__), 'server')
+        assert os.path.exists(attach_pid_injected_dirname)
+
+        log_dir = log_dir.replace('\\', '/')
+
+        encode = lambda s: list(bytearray(s.encode("utf-8")))
+        setup = {
+            'script': encode(attach_pid_injected_dirname),
+            'host': encode(host),
+            'port': port,
+            'client': client,
+            'log_dir': encode(log_dir),
+        }
+
+        python_code = '''import sys;
+import codecs;
+decode = lambda s: codecs.utf_8_decode(bytearray(s))[0];
+script_path = decode({script});
+sys.path.insert(0, script_path);
+import attach_pid_injected;
+sys.path.remove(script_path);
+host = decode({host});
+log_dir = decode({log_dir}) or None;
+attach_pid_injected.attach(port={port}, host=host, client={client}, log_dir=log_dir)
+'''.replace('\r', '').replace('\n', '')
+
+        python_code = python_code.format(**setup)
+        log.info("Code to be injected: \n{0}", python_code.replace(';', '\r\n'))
+
+        # pydevd requires injected code to not contain any single quotes, double quotes or
+        # new lines.
+        assert "'" not in python_code, "Injected code should not contain any single quotes"
+        assert "\"" not in python_code, "Injected code should not contain any double quotes"
+        assert "\n" not in python_code, "Injected code should not contain any new lines"
+        assert "\r" not in python_code, "Injected code should not contain any "
+
+        pydevd_attach_to_process_path = os.path.join(
+            os.path.dirname(pydevd.__file__),
+            'pydevd_attach_to_process')
+
+        assert os.path.exists(pydevd_attach_to_process_path)
+        sys.path.append(pydevd_attach_to_process_path)
+
+    
+        import add_code_to_python_process  # noqa
+        show_debug_info_on_target_process = 0  # hard-coded (1 to debug)
+        log.info('Code injector begin')
+        add_code_to_python_process.run_python_code(
+            pid, python_code, connect_debugger_tracing=True, show_debug_info=show_debug_info_on_target_process)
+    except:
+        raise log.exception()
+    log.info('Code injector exiting')
 
 
 def main():


Test patch:
diff --git a/tests/ptvsd/server/test_attach.py b/tests/ptvsd/server/test_attach.py
--- a/tests/ptvsd/server/test_attach.py
+++ b/tests/ptvsd/server/test_attach.py
@@ -99,11 +99,9 @@ def code_to_debug():
 
 @pytest.mark.parametrize("start_method", [start_methods.AttachProcessId])
 @pytest.mark.parametrize("run_as", ["program", "module", "code"])
-@pytest.mark.skip(reason="Enable after #846, #863 and #1144 are fixed")
 def test_attaching_by_pid(pyfile, run_as, start_method):
     @pyfile
     def code_to_debug():
-        import debug_me  # noqa
         import time
 
         def do_something(i):
diff --git a/tests/start_methods.py b/tests/start_methods.py
--- a/tests/start_methods.py
+++ b/tests/start_methods.py
@@ -308,8 +308,8 @@ def run_in_terminal(self, request):
 class AttachBase(DebugStartBase):
     ignore_unobserved = DebugStartBase.ignore_unobserved + []
 
-    def __init__(self, session, name):
-        super(AttachBase, self).__init__(session, name)
+    def __init__(self, session, method):
+        super(AttachBase, self).__init__(session, method)
         self._attach_args = {}
 
     def _build_attach_args(
@@ -403,15 +403,18 @@ def configure(self, run_as, target, **kwargs):
         )
         watchdog.register_spawn(self.debuggee_process.pid, self.session.debuggee_id)
         self.captured_output.capture(self.debuggee_process)
-
-        connected = False
         pid = self.debuggee_process.pid
-        while connected is False:
-            time.sleep(0.1)
-            connections = psutil.net_connections()
-            connected = (
-                len(list(p for (_, _, _, _, _, _, p) in connections if p == pid)) > 0
-            )
+
+        if self.method == "attach_pid":
+            self._attach_args["processId"] = pid
+        else: 
+            connected = False
+            while connected is False:
+                time.sleep(0.1)
+                connections = psutil.net_connections()
+                connected = (
+                    any(p for (_, _, _, _, _, _, p) in connections if p == pid)
+                )
 
         self._attach_request = self.session.send_request("attach", self._attach_args)
         self.session.wait_for_next_event("initialized")
@@ -497,7 +500,7 @@ def configure(
         run_as,
         target,
         pythonPath=sys.executable,
-        args=[],
+        args=(),
         cwd=None,
         env=None,
         **kwargs
@@ -529,10 +532,34 @@ def configure(
         )
 
 
-class AttachProcessId(DebugStartBase):
+class AttachProcessId(AttachBase):
     def __init__(self, session):
-        super().__init__(session, "attach_pid")
+        super(AttachProcessId, self).__init__(session, "attach_pid")
+
+
+    def configure(
+        self,
+        run_as,
+        target,
+        pythonPath=sys.executable,
+        args=(),
+        cwd=None,
+        env=None,
+        **kwargs
+    ):
+        env = {} if env is None else dict(env)
+        self._attach_args = self._build_attach_args({}, run_as, target, **kwargs)
 
+        log_dir = (
+            self.session.log_dir if self._attach_args.get("logToFile", False) else None
+        )
+        if log_dir:
+            self._attach_args["ptvsdArgs"] = ["--log-dir", log_dir]
+
+        cli_args = [pythonPath]
+        super(AttachProcessId, self).configure(
+            run_as, target, cwd=cwd, env=env, args=args, cli_args=cli_args, **kwargs
+        )
 
 class CustomServer(DebugStartBase):
     def __init__(self, session):