263 lines
9.4 KiB
Python
263 lines
9.4 KiB
Python
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
# Licensed under the MIT License. See LICENSE in the project root
|
|
# for license information.
|
|
|
|
from __future__ import print_function, with_statement, absolute_import
|
|
|
|
import atexit
|
|
import itertools
|
|
import os
|
|
import re
|
|
import socket
|
|
import sys
|
|
import time
|
|
import traceback
|
|
|
|
try:
|
|
import queue
|
|
except ImportError:
|
|
import Queue as queue
|
|
|
|
from ptvsd import options
|
|
from ptvsd.socket import create_server, create_client
|
|
from ptvsd.messaging import JsonIOStream, JsonMessageChannel
|
|
from ptvsd._util import new_hidden_thread, debug
|
|
|
|
from _pydev_bundle import pydev_monkey
|
|
from _pydevd_bundle.pydevd_comm import get_global_debugger
|
|
|
|
|
|
subprocess_listener_socket = None
|
|
|
|
subprocess_queue = queue.Queue()
|
|
"""A queue of incoming 'ptvsd_subprocess' notifications. Whenenever a new request
|
|
is received, a tuple of (subprocess_request, subprocess_response) is placed in the
|
|
queue.
|
|
|
|
subprocess_request is the body of the 'ptvsd_subprocess' notification request that
|
|
was received, with additional information about the root process added.
|
|
|
|
subprocess_response is the body of the response that will be sent to respond to the
|
|
request. It contains a single item 'incomingConnection', which is initially set to
|
|
False. If, as a result of processing the entry, the subprocess shall receive an
|
|
incoming DAP connection on the port it specified in the request, its value should be
|
|
set to True, indicating that the subprocess should wait for that connection before
|
|
proceeding. If no incoming connection is expected, it is set to False, indicating
|
|
that the subprocess shall proceed executing user code immediately.
|
|
|
|
subprocess_queue.task_done() must be invoked for every subprocess_queue.get(), for
|
|
the corresponding subprocess_response to be delivered back to the subprocess.
|
|
"""
|
|
|
|
root_start_request = None
|
|
"""The 'launch' or 'attach' request that started debugging in this process, in its
|
|
entirety (i.e. dict representation of JSON request). This information is added to
|
|
'ptvsd_subprocess' notifications before they're placed in subprocess_queue.
|
|
"""
|
|
|
|
|
|
def listen_for_subprocesses():
|
|
"""Starts a listener for incoming 'ptvsd_subprocess' notifications that
|
|
enqueues them in subprocess_queue.
|
|
"""
|
|
|
|
global subprocess_listener_socket
|
|
assert subprocess_listener_socket is None
|
|
|
|
subprocess_listener_socket = create_server('localhost', 0)
|
|
atexit.register(stop_listening_for_subprocesses)
|
|
new_hidden_thread('SubprocessListener', _subprocess_listener).start()
|
|
|
|
|
|
def stop_listening_for_subprocesses():
|
|
global subprocess_listener_socket
|
|
if subprocess_listener_socket is None:
|
|
return
|
|
try:
|
|
subprocess_listener_socket.shutdown(socket.SHUT_RDWR)
|
|
except Exception:
|
|
pass
|
|
subprocess_listener_socket = None
|
|
|
|
|
|
def subprocess_listener_port():
|
|
if subprocess_listener_socket is None:
|
|
return None
|
|
_, port = subprocess_listener_socket.getsockname()
|
|
return port
|
|
|
|
|
|
def _subprocess_listener():
|
|
counter = itertools.count(1)
|
|
while subprocess_listener_socket:
|
|
try:
|
|
(sock, _) = subprocess_listener_socket.accept()
|
|
except Exception:
|
|
break
|
|
stream = JsonIOStream.from_socket(sock)
|
|
_handle_subprocess(next(counter), stream)
|
|
|
|
|
|
def _handle_subprocess(n, stream):
|
|
class Handlers(object):
|
|
def ptvsd_subprocess_request(self, request):
|
|
# When child process is spawned, the notification it sends only
|
|
# contains information about itself and its immediate parent.
|
|
# Add information about the root process before passing it on.
|
|
arguments = dict(request.arguments)
|
|
arguments.update({
|
|
'rootProcessId': os.getpid(),
|
|
'rootStartRequest': root_start_request,
|
|
})
|
|
|
|
debug('ptvsd_subprocess: %r' % arguments)
|
|
response = {'incomingConnection': False}
|
|
subprocess_queue.put((arguments, response))
|
|
subprocess_queue.join()
|
|
return response
|
|
|
|
name = 'SubprocessListener-%d' % n
|
|
channel = JsonMessageChannel(stream, Handlers(), name)
|
|
channel.start()
|
|
|
|
|
|
def notify_root(port):
|
|
assert options.subprocess_of
|
|
|
|
debug('Subprocess %d notifying root process at port %d' % (os.getpid(), options.subprocess_notify))
|
|
conn = create_client()
|
|
conn.connect(('localhost', options.subprocess_notify))
|
|
stream = JsonIOStream.from_socket(conn)
|
|
channel = JsonMessageChannel(stream)
|
|
channel.start()
|
|
|
|
# Send the notification about ourselves to root, and wait for it to tell us
|
|
# whether an incoming connection is anticipated. This will be true if root
|
|
# had successfully propagated the notification to the IDE, and false if it
|
|
# couldn't do so (e.g. because the IDE is not attached). There's also the
|
|
# possibility that connection to root will just drop, e.g. if it crashes -
|
|
# in that case, just exit immediately.
|
|
|
|
request = channel.send_request('ptvsd_subprocess', {
|
|
'parentProcessId': options.subprocess_of,
|
|
'processId': os.getpid(),
|
|
'port': port,
|
|
})
|
|
|
|
try:
|
|
response = request.wait_for_response()
|
|
except Exception:
|
|
print('Failed to send subprocess notification; exiting', file=sys.__stderr__)
|
|
traceback.print_exc()
|
|
sys.exit(0)
|
|
|
|
if not response['incomingConnection']:
|
|
debugger = get_global_debugger()
|
|
while debugger is None:
|
|
time.sleep(0.1)
|
|
debugger = get_global_debugger()
|
|
debugger.ready_to_run = True
|
|
|
|
|
|
def patch_args(args):
|
|
"""
|
|
Patches a command line invoking Python such that it has the same meaning, but
|
|
the process runs under ptvsd. In general, this means that given something like:
|
|
|
|
python -R -Q warn -m app
|
|
|
|
the result should be:
|
|
|
|
python -R -Q warn .../ptvsd/__main__.py --host localhost --port 0 ... -m app
|
|
"""
|
|
if not options.multiprocess:
|
|
return args
|
|
|
|
assert options.multiprocess
|
|
|
|
args = list(args)
|
|
|
|
# First, let's find the target of the invocation. This is one of:
|
|
#
|
|
# filename.py
|
|
# -m module_name
|
|
# -c "code"
|
|
# -
|
|
#
|
|
# This needs to take into account other switches that have values:
|
|
#
|
|
# -Q -W -X --check-hash-based-pycs
|
|
#
|
|
# because in something like "-X -c", -c is a value, not a switch.
|
|
expect_value = False
|
|
for i, arg in enumerate(args):
|
|
# Skip Python binary.
|
|
if i == 0:
|
|
continue
|
|
|
|
if arg == '-':
|
|
# We do not support debugging while reading from stdin, so just let this
|
|
# process run without debugging.
|
|
return args
|
|
|
|
if expect_value:
|
|
# Consume the value and move on.
|
|
expect_value = False
|
|
continue
|
|
|
|
if not arg.startswith('-') or arg in ('-c', '-m'):
|
|
# This is the target.
|
|
break
|
|
|
|
if arg.startswith('--'):
|
|
expect_value = (arg == '--check-hash-based-pycs')
|
|
continue
|
|
|
|
# All short switches other than -c and -m can be combined together, including
|
|
# those with values. So, instead of -R -B -v -Q old, we might see -RBvQ old.
|
|
# Furthermore, the value itself can be concatenated with the switch, so rather
|
|
# than -Q old, we might have -Qold. When switches are combined, any switch that
|
|
# has a value "eats" the rest of the argument; for example, -RBQv is treated as
|
|
# -R -B -Qv, and not as -R -B -Q -v. So, we need to check whether one of 'Q',
|
|
# 'W' or 'X' was present somewhere in the arg, and whether there was anything
|
|
# following it in the arg. If it was there but nothing followed after it, then
|
|
# the switch is expecting a value.
|
|
split = re.split(r'[QWX]', arg, maxsplit=1)
|
|
expect_value = (len(split) > 1 and split[-1] != '')
|
|
|
|
else:
|
|
# Didn't find the target, so we don't know how to patch this command line; let
|
|
# it run without debugging.
|
|
return args
|
|
|
|
if not args[i].startswith('-'):
|
|
# If it was a filename, it can be a Python file, a directory, or a zip archive
|
|
# that is treated as if it were a directory. However, ptvsd only supports the
|
|
# first scenario. Distinguishing between these can be tricky, and getting it
|
|
# wrong means that process fails to launch, so be conservative.
|
|
if not args[i].endswith('.py'):
|
|
return args
|
|
|
|
# Now we need to inject the ptvsd invocation right before the target. The target
|
|
# itself can remain as is, because ptvsd is compatible with Python in that respect.
|
|
from ptvsd import __main__
|
|
args[i:i] = [
|
|
__main__.__file__,
|
|
'--host', options.host,
|
|
'--port', '0',
|
|
'--wait',
|
|
'--multiprocess',
|
|
'--subprocess-of', str(os.getpid()),
|
|
'--subprocess-notify', str(options.subprocess_notify or subprocess_listener_port()),
|
|
]
|
|
|
|
return args
|
|
|
|
|
|
def patch_and_quote_args(args):
|
|
# On Windows, pydevd expects arguments to be quoted and escaped as necessary, such
|
|
# that simply concatenating them via ' ' produces a valid command line. This wraps
|
|
# patch_args and applies quoting (quote_args contains platform check), so that the
|
|
# implementation of patch_args can be kept simple.
|
|
return pydev_monkey.quote_args(patch_args(args))
|