Exemple #1
0
    def __init__(self, whose, category, fd, stream):
        assert category not in self.instances
        self.instances[category] = self
        log.info("Capturing {0} of {1}.", category, whose)

        self.category = category
        self._whose = whose
        self._fd = fd
        self._decoder = codecs.getincrementaldecoder("utf-8")(
            errors="surrogateescape")

        if stream is None:
            # Can happen if running under pythonw.exe.
            self._stream = None
        else:
            self._stream = stream if sys.version_info < (
                3, ) else stream.buffer
            encoding = stream.encoding
            if encoding is None or encoding == "cp65001":
                encoding = "utf-8"
            try:
                self._encode = codecs.getencoder(encoding)
            except Exception:
                log.swallow_exception(
                    "Unsupported {0} encoding {1!r}; falling back to UTF-8.",
                    category,
                    encoding,
                    level="warning",
                )
                self._encode = codecs.getencoder("utf-8")

        self._worker_thread = threading.Thread(target=self._worker,
                                               name=category)
        self._worker_thread.start()
def wait_for_exit():
    try:
        code = process.wait()
        if sys.platform != "win32" and code < 0:
            # On POSIX, if the process was terminated by a signal, Popen will use
            # a negative returncode to indicate that - but the actual exit code of
            # the process is always an unsigned number, and can be determined by
            # taking the lowest 8 bits of that negative returncode.
            code &= 0xFF
    except Exception:
        log.swallow_exception("Couldn't determine process exit code")
        code = -1

    log.info("{0} exited with code {1}", describe(), code)
    output.wait_for_remaining_output()

    # Determine whether we should wait or not before sending "exited", so that any
    # follow-up "terminate" requests don't affect the predicates.
    should_wait = any(pred(code) for pred in wait_on_exit_predicates)

    try:
        launcher.channel.send_event("exited", {"exitCode": code})
    except Exception:
        pass

    if should_wait:
        _wait_for_user_input()

    try:
        launcher.channel.send_event("terminated")
    except Exception:
        pass
Exemple #3
0
def spawn(process_name, cmdline, env, redirect_output):
    log.info(
        "Spawning debuggee process:\n\n"
        "Command line: {0!r}\n\n"
        "Environment variables: {1!r}\n\n",
        cmdline,
        env,
    )

    close_fds = set()
    try:
        if redirect_output:
            # subprocess.PIPE behavior can vary substantially depending on Python version
            # and platform; using our own pipes keeps it simple, predictable, and fast.
            stdout_r, stdout_w = os.pipe()
            stderr_r, stderr_w = os.pipe()
            close_fds |= {stdout_r, stdout_w, stderr_r, stderr_w}
            kwargs = dict(stdout=stdout_w, stderr=stderr_w)
        else:
            kwargs = {}

        try:
            global process
            process = subprocess.Popen(cmdline, env=env, bufsize=0, **kwargs)
        except Exception as exc:
            raise messaging.MessageHandlingError(
                fmt("Couldn't spawn debuggee: {0}\n\nCommand line:{1!r}", exc, cmdline)
            )

        log.info("Spawned {0}.", describe())
        atexit.register(kill)
        launcher.channel.send_event(
            "process",
            {
                "startMethod": "launch",
                "isLocalProcess": True,
                "systemProcessId": process.pid,
                "name": process_name,
                "pointerSize": struct.calcsize(compat.force_str("P")) * 8,
            },
        )

        if redirect_output:
            for category, fd, tee in [
                ("stdout", stdout_r, sys.stdout),
                ("stderr", stderr_r, sys.stderr),
            ]:
                output.CaptureOutput(describe(), category, fd, tee)
                close_fds.remove(fd)

        wait_thread = threading.Thread(target=wait_for_exit, name="wait_for_exit()")
        wait_thread.daemon = True
        wait_thread.start()

    finally:
        for fd in close_fds:
            try:
                os.close(fd)
            except Exception:
                log.swallow_exception()
Exemple #4
0
    def finalize(self, why, terminate_debuggee=None):
        """Finalizes the debug session.

        If the server is present, sends "disconnect" request with "terminateDebuggee"
        set as specified request to it; waits for it to disconnect, allowing any
        remaining messages from it to be handled; and closes the server channel.

        If the launcher is present, sends "terminate" request to it, regardless of the
        value of terminate; waits for it to disconnect, allowing any remaining messages
        from it to be handled; and closes the launcher channel.

        If the client is present, sends "terminated" event to it.

        If terminate_debuggee=None, it is treated as True if the session has a Launcher
        component, and False otherwise.
        """

        if self.is_finalizing:
            return
        self.is_finalizing = True
        log.info("{0}; finalizing {1}.", why, self)

        if terminate_debuggee is None:
            terminate_debuggee = bool(self.launcher)

        try:
            self._finalize(why, terminate_debuggee)
        except Exception:
            # Finalization should never fail, and if it does, the session is in an
            # indeterminate and likely unrecoverable state, so just fail fast.
            log.swallow_exception("Fatal error while finalizing {0}", self)
            os._exit(1)

        log.info("{0} finalized.", self)
Exemple #5
0
 def delete_listener_file():
     log.info("Listener ports closed; deleting {0!r}", listener_file)
     try:
         os.remove(listener_file)
     except Exception:
         log.swallow_exception("Failed to delete {0!r}",
                               listener_file,
                               level="warning")
Exemple #6
0
def stop():
    if _stream is None:
        return

    try:
        _invoke("stop")
        _stream.close()
    except Exception:
        log.swallow_exception()
Exemple #7
0
def kill():
    if process is None:
        return
    try:
        if process.poll() is None:
            log.info("Killing {0}", describe())
            process.kill()
    except Exception:
        log.swallow_exception("Failed to kill {0}", describe())
Exemple #8
0
 def _process_request(self, request):
     self.timeline.record_request(request, block=False)
     if request.command == "runInTerminal":
         args = request("args", json.array(unicode))
         cwd = request("cwd", ".")
         env = request("env", json.object(unicode))
         try:
             return self.run_in_terminal(args, cwd, env)
         except Exception as exc:
             log.swallow_exception('"runInTerminal" failed:')
             raise request.cant_handle(str(exc))
     else:
         raise request.isnt_valid("not supported")
Exemple #9
0
def inject(pid, debugpy_args):
    host, port = listener.getsockname()

    cmdline = [
        sys.executable,
        compat.filename(os.path.dirname(debugpy.__file__)),
        "--connect",
        host + ":" + str(port),
    ]
    if adapter.access_token is not None:
        cmdline += ["--adapter-access-token", adapter.access_token]
    cmdline += debugpy_args
    cmdline += ["--pid", str(pid)]

    log.info("Spawning attach-to-PID debugger injector: {0!r}", cmdline)
    try:
        injector = subprocess.Popen(
            cmdline,
            bufsize=0,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
        )
    except Exception as exc:
        log.swallow_exception(
            "Failed to inject debug server into process with PID={0}", pid
        )
        raise messaging.MessageHandlingError(
            fmt(
                "Failed to inject debug server into process with PID={0}: {1}", pid, exc
            )
        )

    # We need to capture the output of the injector - otherwise it can get blocked
    # on a write() syscall when it tries to print something.

    def capture_output():
        while True:
            line = injector.stdout.readline()
            if not line:
                break
            log.info("Injector[PID={0}] output:\n{1}", pid, line.rstrip())
        log.info("Injector[PID={0}] exited.", pid)

    thread = threading.Thread(
        target=capture_output, name=fmt("Injector[PID={0}] output", pid)
    )
    thread.daemon = True
    thread.start()
Exemple #10
0
def run_module():
    # Add current directory to path, like Python itself does for -m. This must
    # be in place before trying to use find_spec below to resolve submodules.
    sys.path.insert(0, "")

    # We want to do the same thing that run_module() would do here, without
    # actually invoking it. On Python 3, it's exposed as a public API, but
    # on Python 2, we have to invoke a private function in runpy for this.
    # Either way, if it fails to resolve for any reason, just leave argv as is.
    argv_0 = sys.argv[0]
    try:
        if sys.version_info >= (3,):
            from importlib.util import find_spec

            spec = find_spec(options.target)
            if spec is not None:
                argv_0 = spec.origin
        else:
            _, _, _, argv_0 = runpy._get_module_details(options.target)
    except Exception:
        log.swallow_exception("Error determining module path for sys.argv")

    start_debugging(argv_0)

    # On Python 2, module name must be a non-Unicode string, because it ends up
    # a part of module's __package__, and Python will refuse to run the module
    # if __package__ is Unicode.
    target = (
        compat.filename_bytes(options.target)
        if sys.version_info < (3,)
        else options.target
    )

    log.describe_environment("Pre-launch environment:")
    log.info("Running module {0!r}", target)

    # Docs say that runpy.run_module is equivalent to -m, but it's not actually
    # the case for packages - -m sets __name__ to "__main__", but run_module sets
    # it to "pkg.__main__". This breaks everything that uses the standard pattern
    # __name__ == "__main__" to detect being run as a CLI app. On the other hand,
    # runpy._run_module_as_main is a private function that actually implements -m.
    try:
        run_module_as_main = runpy._run_module_as_main
    except AttributeError:
        log.warning("runpy._run_module_as_main is missing, falling back to run_module.")
        runpy.run_module(target, alter_sys=True)
    else:
        run_module_as_main(target, alter_argv=True)
Exemple #11
0
def kill():
    if process is None:
        return

    try:
        if process.poll() is None:
            log.info("Killing {0}", describe())
            # Clean up the process tree
            if sys.platform == "win32":
                # On Windows, kill the job object.
                winapi.kernel32.TerminateJobObject(job_handle, 0)
            else:
                # On POSIX, kill the debuggee's process group.
                os.killpg(process.pid, signal.SIGKILL)
    except Exception:
        log.swallow_exception("Failed to kill {0}", describe())
Exemple #12
0
 def _process_request(self, request):
     self.timeline.record_request(request, block=False)
     if request.command == "runInTerminal":
         args = request("args", json.array(unicode))
         cwd = request("cwd", ".")
         env = request("env", json.object(unicode))
         try:
             exe = args.pop(0)
             assert not len(self.spawn_debuggee.env)
             self.spawn_debuggee.env = env
             self.spawn_debuggee(args, cwd, exe=exe)
             return {}
         except OSError as exc:
             log.swallow_exception('"runInTerminal" failed:')
             raise request.cant_handle(str(exc))
     else:
         raise request.isnt_valid("not supported")
Exemple #13
0
def _dump_worker_log(command, problem, exc_info=None):
    reason = fmt("{0}.{1}() {2}", _name, command, problem)
    if _worker_log_filename is None:
        reason += ", but there is no log."
    else:
        try:
            with open(_worker_log_filename) as f:
                worker_log = f.read()
        except Exception:
            reason += fmt(", but log {0} could not be retrieved.",
                          _worker_log_filename)
        else:
            reason += fmt("; watchdog worker process log:\n\n{0}", worker_log)

    if exc_info is None:
        log.error("{0}", reason)
    else:
        log.swallow_exception("{0}", reason, exc_info=exc_info)
    return reason
Exemple #14
0
            def preexec_fn():
                try:
                    # Start the debuggee in a new process group, so that the launcher can
                    # kill the entire process tree later.
                    os.setpgrp()

                    # Make the new process group the foreground group in its session, so
                    # that it can interact with the terminal. The debuggee will receive
                    # SIGTTOU when tcsetpgrp() is called, and must ignore it.
                    old_handler = signal.signal(signal.SIGTTOU, signal.SIG_IGN)
                    try:
                        tty = os.open("/dev/tty", os.O_RDWR)
                        try:
                            os.tcsetpgrp(tty, os.getpgrp())
                        finally:
                            os.close(tty)
                    finally:
                        signal.signal(signal.SIGTTOU, old_handler)
                except Exception:
                    # Not an error - /dev/tty doesn't work when there's no terminal.
                    log.swallow_exception("Failed to set up process group",
                                          level="info")
Exemple #15
0
def stop_serving():
    try:
        listener.close()
    except Exception:
        log.swallow_exception(level="warning")
Exemple #16
0
    def __init__(self, sock):
        from debugpy.adapter import sessions

        self.disconnected = False

        self.server = None
        """The Server component, if this debug server belongs to Session.
        """

        self.pid = None

        stream = messaging.JsonIOStream.from_socket(sock, str(self))
        self.channel = messaging.JsonMessageChannel(stream, self)
        self.channel.start()

        try:
            self.authenticate()
            info = self.channel.request("pydevdSystemInfo")
            process_info = info("process", json.object())
            self.pid = process_info("pid", int)
            self.ppid = process_info("ppid", int, optional=True)
            if self.ppid == ():
                self.ppid = None
            self.channel.name = stream.name = str(self)

            debugpy_dir = os.path.dirname(os.path.dirname(debugpy.__file__))
            # Note: we must check if 'debugpy' is not already in sys.modules because the
            # evaluation of an import at the wrong time could deadlock Python due to
            # its import lock.
            #
            # So, in general this evaluation shouldn't do anything. It's only
            # important when pydevd attaches automatically to a subprocess. In this
            # case, we have to make sure that debugpy is properly put back in the game
            # for users to be able to use it.v
            #
            # In this case (when the import is needed), this evaluation *must* be done
            # before the configurationDone request is sent -- if this is not respected
            # it's possible that pydevd already started secondary threads to handle
            # commands, in which case it's very likely that this command would be
            # evaluated at the wrong thread and the import could potentially deadlock
            # the program.
            #
            # Note 2: the sys module is guaranteed to be in the frame globals and
            # doesn't need to be imported.
            inject_debugpy = """
if 'debugpy' not in sys.modules:
    sys.path.insert(0, {debugpy_dir!r})
    try:
        import debugpy
    finally:
        del sys.path[0]
"""
            inject_debugpy = fmt(inject_debugpy, debugpy_dir=debugpy_dir)

            try:
                self.channel.request("evaluate", {"expression": inject_debugpy})
            except messaging.MessageHandlingError:
                # Failure to inject is not a fatal error - such a subprocess can
                # still be debugged, it just won't support "import debugpy" in user
                # code - so don't terminate the session.
                log.swallow_exception(
                    "Failed to inject debugpy into {0}:", self, level="warning"
                )

            with _lock:
                # The server can disconnect concurrently before we get here, e.g. if
                # it was force-killed. If the disconnect() handler has already run,
                # don't register this server or report it, since there's nothing to
                # deregister it.
                if self.disconnected:
                    return

                if any(conn.pid == self.pid for conn in _connections):
                    raise KeyError(
                        fmt("{0} is already connected to this adapter", self)
                    )
                _connections.append(self)
                _connections_changed.set()

        except Exception:
            log.swallow_exception("Failed to accept incoming server connection:")
            self.channel.close()

            # If this was the first server to connect, and the main thread is inside
            # wait_until_disconnected(), we want to unblock it and allow it to exit.
            dont_wait_for_first_connection()

            # If we couldn't retrieve all the necessary info from the debug server,
            # or there's a PID clash, we don't want to track this debuggee anymore,
            # but we want to continue accepting connections.
            return

        parent_session = sessions.get(self.ppid)
        if parent_session is None:
            log.info("No active debug session for parent process of {0}.", self)
        else:
            try:
                parent_session.client.notify_of_subprocess(self)
            except Exception:
                # This might fail if the client concurrently disconnects from the parent
                # session. We still want to keep the connection around, in case the
                # client reconnects later. If the parent session was "launch", it'll take
                # care of closing the remaining server connections.
                log.swallow_exception("Failed to notify parent session about {0}:", self)
Exemple #17
0
 def dumper():
     time.sleep(secs)
     try:
         dump()
     except:
         log.swallow_exception()
Exemple #18
0
def main(tests_pid):
    # To import debugpy, the "" entry in sys.path - which is added automatically on
    # Python 2 - must be removed first; otherwise, we end up importing tests/debugpy.
    if "" in sys.path:
        sys.path.remove("")

    from debugpy.common import fmt, log, messaging

    # log.stderr_levels |= {"info"}
    log.timestamp_format = "06.3f"
    log_file = log.to_file(prefix="tests.watchdog")

    stream = messaging.JsonIOStream.from_stdio(fmt("tests-{0}", tests_pid))
    log.info("Spawned WatchDog-{0} for tests-{0}", tests_pid)
    tests_process = psutil.Process(tests_pid)
    stream.write_json(["watchdog", log_file.filename])

    spawned_processes = {}  # pid -> ProcessInfo
    try:
        stop = False
        while not stop:
            try:
                message = stream.read_json()
            except Exception:
                break

            command = message[0]
            args = message[1:]

            if command == "stop":
                assert not args
                stop = True

            elif command == "register_spawn":
                pid, name = args
                pid = int(pid)

                log.info(
                    "WatchDog-{0} registering spawned process {1} (pid={2})",
                    tests_pid,
                    name,
                    pid,
                )
                try:
                    _, old_name = spawned_processes[pid]
                except KeyError:
                    pass
                else:
                    log.warning(
                        "WatchDog-{0} already tracks a process with pid={1}: {2}",
                        tests_pid,
                        pid,
                        old_name,
                    )
                spawned_processes[pid] = ProcessInfo(psutil.Process(pid), name)

            elif command == "unregister_spawn":
                pid, name = args
                pid = int(pid)

                log.info(
                    "WatchDog-{0} unregistering spawned process {1} (pid={2})",
                    tests_pid,
                    name,
                    pid,
                )
                spawned_processes.pop(pid, None)

            else:
                raise AssertionError(
                    fmt("Unknown watchdog command: {0!r}", command))

            stream.write_json(["ok"])

    except Exception as exc:
        stream.write_json(["error", str(exc)])
        log.reraise_exception()

    finally:
        try:
            stream.close()
        except Exception:
            log.swallow_exception()

        # If the test runner becomes a zombie process, it is still considered alive,
        # and wait() will block indefinitely. Poll status instead.
        while True:
            try:
                status = tests_process.status()
            except Exception:
                # If we can't even get its status, assume that it's dead.
                break

            # If it's dead or a zombie, time to clean it up.
            if status in (psutil.STATUS_DEAD, psutil.STATUS_ZOMBIE):
                break

            # Otherwise, let's wait a bit to see if anything changes.
            try:
                tests_process.wait(0.1)
            except Exception:
                pass

        leftover_processes = {proc for proc, _ in spawned_processes.values()}
        for proc, _ in spawned_processes.values():
            try:
                leftover_processes |= proc.children(recursive=True)
            except Exception:
                pass

        leftover_processes = {
            proc
            for proc in leftover_processes if proc.is_running()
        }
        if not leftover_processes:
            return

        # Wait a bit to allow the terminal to catch up on the test runner output.
        time.sleep(0.3)

        log.newline(level="warning")
        log.warning(
            "tests-{0} process terminated unexpectedly, and left some orphan child "
            "processes behind: {1!r}",
            tests_pid,
            sorted({proc.pid
                    for proc in leftover_processes}),
        )

        for proc in leftover_processes:
            log.warning(
                "WatchDog-{0} killing orphaned test child process (pid={1})",
                tests_pid,
                proc.pid,
            )

            try:
                proc.kill()
            except psutil.NoSuchProcess:
                pass
            except Exception:
                log.swallow_exception()

        log.info("WatchDog-{0} exiting", tests_pid)
Exemple #19
0
    def __exit__(self, exc_type, exc_val, exc_tb):
        log.info("Ending {0}.", self)

        if self.timeline.is_frozen:
            self.timeline.unfreeze()

        # Only wait for exit if there was no exception in the test - if there was one,
        # the debuggee might still be waiting for further requests.
        if exc_type is None:
            # If expected_exit_code is set to None, the debuggee is not expected to
            # exit after this Session is closed (e.g. because another Session will
            # attach to it later on).
            if self.expected_exit_code is not None:
                self.wait_for_exit()
        else:
            # Log the error, in case another one happens during shutdown.
            log.swallow_exception(exc_info=(exc_type, exc_val, exc_tb))

        if exc_type is None:
            self.disconnect()
            self.timeline.close()
        else:
            # If there was an exception, don't try to send any more messages to avoid
            # spamming log with irrelevant entries - just close the channel and kill
            # all the processes immediately. Don't close or finalize the timeline,
            # either, since it'll likely have unobserved events in it.
            if self.adapter is not None:
                log.info("Killing {0}.", self.adapter_id)
                try:
                    self.adapter.kill()
                except Exception:
                    pass
            if self.debuggee is not None:
                log.info("Killing {0}.", self.debuggee_id)
                try:
                    self.debuggee.kill()
                except Exception:
                    pass
            self.disconnect(force=True)

        if self.adapter_endpoints is not None and self.expected_exit_code is not None:
            log.info("Waiting for {0} to close listener ports ...",
                     self.adapter_id)
            while self.adapter_endpoints.check():
                time.sleep(0.1)

        if self.adapter is not None:
            log.info(
                "Waiting for {0} with PID={1} to exit.",
                self.adapter_id,
                self.adapter.pid,
            )
            self.adapter.wait()
            watchdog.unregister_spawn(self.adapter.pid, self.adapter_id)
            self.adapter = None

        if self.backchannel is not None:
            self.backchannel.close()
            self.backchannel = None

        # Work around https://bugs.python.org/issue37380
        for popen in self.debuggee, self.adapter:
            if popen is not None and popen.returncode is None:
                popen.returncode = -1
Exemple #20
0
    def _finalize(self, why, terminate_debuggee):
        # If the client started a session, and then disconnected before issuing "launch"
        # or "attach", the main thread will be blocked waiting for the first server
        # connection to come in - unblock it, so that we can exit.
        servers.dont_wait_for_first_connection()

        if self.server:
            if self.server.is_connected:
                if terminate_debuggee and self.launcher and self.launcher.is_connected:
                    # If we were specifically asked to terminate the debuggee, and we
                    # can ask the launcher to kill it, do so instead of disconnecting
                    # from the server to prevent debuggee from running any more code.
                    self.launcher.terminate_debuggee()
                else:
                    # Otherwise, let the server handle it the best it can.
                    try:
                        self.server.channel.request(
                            "disconnect",
                            {"terminateDebuggee": terminate_debuggee})
                    except Exception:
                        pass
            self.server.detach_from_session()

        if self.launcher and self.launcher.is_connected:
            # If there was a server, we just disconnected from it above, which should
            # cause the debuggee process to exit - so let's wait for that first.
            if self.server:
                log.info('{0} waiting for "exited" event...', self)
                if not self.wait_for(
                        lambda: self.launcher.exit_code is not None,
                        timeout=5):
                    log.warning('{0} timed out waiting for "exited" event.',
                                self)

            # Terminate the debuggee process if it's still alive for any reason -
            # whether it's because there was no server to handle graceful shutdown,
            # or because the server couldn't handle it for some reason.
            self.launcher.terminate_debuggee()

            # Wait until the launcher message queue fully drains. There is no timeout
            # here, because the final "terminated" event will only come after reading
            # user input in wait-on-exit scenarios.
            log.info("{0} waiting for {1} to disconnect...", self,
                     self.launcher)
            self.wait_for(lambda: not self.launcher.is_connected)

            try:
                self.launcher.channel.close()
            except Exception:
                log.swallow_exception()

        if self.client:
            if self.client.is_connected:
                # Tell the client that debugging is over, but don't close the channel until it
                # tells us to, via the "disconnect" request.
                try:
                    self.client.channel.send_event("terminated")
                except Exception:
                    pass

            if (self.client.start_request is not None
                    and self.client.start_request.command == "launch"):
                servers.stop_serving()
                log.info(
                    '"launch" session ended - killing remaining debuggee processes.'
                )

                pids_killed = set()
                if self.launcher and self.launcher.pid is not None:
                    # Already killed above.
                    pids_killed.add(self.launcher.pid)

                while True:
                    conns = [
                        conn for conn in servers.connections()
                        if conn.pid not in pids_killed
                    ]
                    if not len(conns):
                        break
                    for conn in conns:
                        log.info("Killing {0}", conn)
                        try:
                            os.kill(conn.pid, signal.SIGTERM)
                        except Exception:
                            log.swallow_exception("Failed to kill {0}", conn)
                        pids_killed.add(conn.pid)
Exemple #21
0
def spawn(process_name, cmdline, env, redirect_output):
    log.info(
        "Spawning debuggee process:\n\n"
        "Command line: {0!r}\n\n"
        "Environment variables: {1!r}\n\n",
        cmdline,
        env,
    )

    close_fds = set()
    try:
        if redirect_output:
            # subprocess.PIPE behavior can vary substantially depending on Python version
            # and platform; using our own pipes keeps it simple, predictable, and fast.
            stdout_r, stdout_w = os.pipe()
            stderr_r, stderr_w = os.pipe()
            close_fds |= {stdout_r, stdout_w, stderr_r, stderr_w}
            kwargs = dict(stdout=stdout_w, stderr=stderr_w)
        else:
            kwargs = {}

        if sys.platform != "win32":

            def preexec_fn():
                try:
                    # Start the debuggee in a new process group, so that the launcher can
                    # kill the entire process tree later.
                    os.setpgrp()

                    # Make the new process group the foreground group in its session, so
                    # that it can interact with the terminal. The debuggee will receive
                    # SIGTTOU when tcsetpgrp() is called, and must ignore it.
                    old_handler = signal.signal(signal.SIGTTOU, signal.SIG_IGN)
                    try:
                        tty = os.open("/dev/tty", os.O_RDWR)
                        try:
                            os.tcsetpgrp(tty, os.getpgrp())
                        finally:
                            os.close(tty)
                    finally:
                        signal.signal(signal.SIGTTOU, old_handler)
                except Exception:
                    # Not an error - /dev/tty doesn't work when there's no terminal.
                    log.swallow_exception(
                        "Failed to set up process group", level="info"
                    )

            kwargs.update(preexec_fn=preexec_fn)

        try:
            global process
            process = subprocess.Popen(cmdline, env=env, bufsize=0, **kwargs)
        except Exception as exc:
            raise messaging.MessageHandlingError(
                fmt("Couldn't spawn debuggee: {0}\n\nCommand line:{1!r}", exc, cmdline)
            )

        log.info("Spawned {0}.", describe())

        if sys.platform == "win32":
            # Assign the debuggee to a new job object, so that the launcher can kill
            # the entire process tree later.
            try:
                global job_handle
                job_handle = winapi.kernel32.CreateJobObjectA(None, None)

                job_info = winapi.JOBOBJECT_EXTENDED_LIMIT_INFORMATION()
                job_info_size = winapi.DWORD(ctypes.sizeof(job_info))
                winapi.kernel32.QueryInformationJobObject(
                    job_handle,
                    winapi.JobObjectExtendedLimitInformation,
                    ctypes.pointer(job_info),
                    job_info_size,
                    ctypes.pointer(job_info_size),
                )

                # Setting this flag ensures that the job will be terminated by the OS once the
                # launcher exits, even if it doesn't terminate the job explicitly.
                job_info.BasicLimitInformation.LimitFlags |= (
                    winapi.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
                )
                winapi.kernel32.SetInformationJobObject(
                    job_handle,
                    winapi.JobObjectExtendedLimitInformation,
                    ctypes.pointer(job_info),
                    job_info_size,
                )

                process_handle = winapi.kernel32.OpenProcess(
                    winapi.PROCESS_TERMINATE | winapi.PROCESS_SET_QUOTA,
                    False,
                    process.pid,
                )

                winapi.kernel32.AssignProcessToJobObject(job_handle, process_handle)

            except Exception:
                log.swallow_exception("Failed to set up job object", level="warning")

        atexit.register(kill)

        launcher.channel.send_event(
            "process",
            {
                "startMethod": "launch",
                "isLocalProcess": True,
                "systemProcessId": process.pid,
                "name": process_name,
                "pointerSize": struct.calcsize(compat.force_str("P")) * 8,
            },
        )

        if redirect_output:
            for category, fd, tee in [
                ("stdout", stdout_r, sys.stdout),
                ("stderr", stderr_r, sys.stderr),
            ]:
                output.CaptureOutput(describe(), category, fd, tee)
                close_fds.remove(fd)

        wait_thread = threading.Thread(target=wait_for_exit, name="wait_for_exit()")
        wait_thread.daemon = True
        wait_thread.start()

    finally:
        for fd in close_fds:
            try:
                os.close(fd)
            except Exception:
                log.swallow_exception(level="warning")
Exemple #22
0
def listen(address, settrace_kwargs):
    # Errors below are logged with level="info", because the caller might be catching
    # and handling exceptions, and we don't want to spam their stderr unnecessarily.

    import subprocess

    server_access_token = compat.force_str(codecs.encode(os.urandom(32), "hex"))

    try:
        endpoints_listener = sockets.create_server("127.0.0.1", 0, timeout=10)
    except Exception as exc:
        log.swallow_exception("Can't listen for adapter endpoints:")
        raise RuntimeError("can't listen for adapter endpoints: " + str(exc))
    endpoints_host, endpoints_port = endpoints_listener.getsockname()
    log.info(
        "Waiting for adapter endpoints on {0}:{1}...", endpoints_host, endpoints_port
    )

    host, port = address
    adapter_args = [
        sys.executable,
        os.path.dirname(adapter.__file__),
        "--for-server",
        str(endpoints_port),
        "--host",
        host,
        "--port",
        str(port),
        "--server-access-token",
        server_access_token,
    ]
    if log.log_dir is not None:
        adapter_args += ["--log-dir", log.log_dir]
    log.info("debugpy.listen() spawning adapter: {0!j}", adapter_args)

    # On Windows, detach the adapter from our console, if any, so that it doesn't
    # receive Ctrl+C from it, and doesn't keep it open once we exit.
    creationflags = 0
    if sys.platform == "win32":
        creationflags |= 0x08000000  # CREATE_NO_WINDOW
        creationflags |= 0x00000200  # CREATE_NEW_PROCESS_GROUP

    # Adapter will outlive this process, so we shouldn't wait for it. However, we
    # need to ensure that the Popen instance for it doesn't get garbage-collected
    # by holding a reference to it in a non-local variable, to avoid triggering
    # https://bugs.python.org/issue37380.
    try:
        global _adapter_process
        _adapter_process = subprocess.Popen(
            adapter_args, close_fds=True, creationflags=creationflags
        )
        if os.name == "posix":
            # It's going to fork again to daemonize, so we need to wait on it to
            # clean it up properly.
            _adapter_process.wait()
        else:
            # Suppress misleading warning about child process still being alive when
            # this process exits (https://bugs.python.org/issue38890).
            _adapter_process.returncode = 0
            pydevd.add_dont_terminate_child_pid(_adapter_process.pid)
    except Exception as exc:
        log.swallow_exception("Error spawning debug adapter:", level="info")
        raise RuntimeError("error spawning debug adapter: " + str(exc))

    try:
        sock, _ = endpoints_listener.accept()
        try:
            sock.settimeout(None)
            sock_io = sock.makefile("rb", 0)
            try:
                endpoints = json.loads(sock_io.read().decode("utf-8"))
            finally:
                sock_io.close()
        finally:
            sockets.close_socket(sock)
    except socket.timeout:
        log.swallow_exception("Timed out waiting for adapter to connect:", level="info")
        raise RuntimeError("timed out waiting for adapter to connect")
    except Exception as exc:
        log.swallow_exception("Error retrieving adapter endpoints:", level="info")
        raise RuntimeError("error retrieving adapter endpoints: " + str(exc))

    log.info("Endpoints received from adapter: {0!j}", endpoints)

    if "error" in endpoints:
        raise RuntimeError(str(endpoints["error"]))

    try:
        server_host = str(endpoints["server"]["host"])
        server_port = int(endpoints["server"]["port"])
        client_host = str(endpoints["client"]["host"])
        client_port = int(endpoints["client"]["port"])
    except Exception as exc:
        log.swallow_exception(
            "Error parsing adapter endpoints:\n{0!j}\n", endpoints, level="info"
        )
        raise RuntimeError("error parsing adapter endpoints: " + str(exc))
    log.info(
        "Adapter is accepting incoming client connections on {0}:{1}",
        client_host,
        client_port,
    )

    _settrace(
        host=server_host,
        port=server_port,
        wait_for_ready_to_run=False,
        block_until_connected=True,
        access_token=server_access_token,
        **settrace_kwargs
    )
    log.info("pydevd is connected to adapter at {0}:{1}", server_host, server_port)
    return client_host, client_port