| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- # Copyright (c) Microsoft Corporation. All rights reserved.
- # Licensed under the MIT License. See LICENSE in the project root
- # for license information.
- import atexit
- import ctypes
- import os
- import signal
- import struct
- import subprocess
- import sys
- import threading
- from debugpy import launcher
- from debugpy.common import log, messaging
- from debugpy.launcher import output
- if sys.platform == "win32":
- from debugpy.launcher import winapi
- process = None
- """subprocess.Popen instance for the debuggee process."""
- job_handle = None
- """On Windows, the handle for the job object to which the debuggee is assigned."""
- wait_on_exit_predicates = []
- """List of functions that determine whether to pause after debuggee process exits.
- Every function is invoked with exit code as the argument. If any of the functions
- returns True, the launcher pauses and waits for user input before exiting.
- """
- def describe():
- return f"Debuggee[PID={process.pid}]"
- 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" and sys.implementation.name != 'graalpy':
- # GraalPy does not support running code between fork and exec
- 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(
- "Couldn't spawn debuggee: {0}\n\nCommand line:{1!r}".format(
- 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),
- )
- job_info.BasicLimitInformation.LimitFlags |= (
- # Ensure that the job will be terminated by the OS once the
- # launcher exits, even if it doesn't terminate the job explicitly.
- winapi.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
- |
- # Allow the debuggee to create its own jobs unrelated to ours.
- winapi.JOB_OBJECT_LIMIT_BREAKAWAY_OK
- )
- 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("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")
- 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())
- 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
- def _wait_for_user_input():
- if sys.stdout and sys.stdin and sys.stdin.isatty():
- from debugpy.common import log
- try:
- import msvcrt
- except ImportError:
- can_getch = False
- else:
- can_getch = True
- if can_getch:
- log.debug("msvcrt available - waiting for user input via getch()")
- sys.stdout.write("Press any key to continue . . . ")
- sys.stdout.flush()
- msvcrt.getch()
- else:
- log.debug("msvcrt not available - waiting for user input via read()")
- sys.stdout.write("Press Enter to continue . . . ")
- sys.stdout.flush()
- sys.stdin.read(1)
|