Spaces:
Sleeping
Sleeping
# 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": | |
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) | |