This repository has been archived on 2023-01-27. You can view files and clone it, but cannot push or open issues or pull requests.
cad/fusion360/python/Lib/site-packages/ptvsd/multiproc.py
2022-10-15 19:16:08 +02:00

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))