mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
Provide feedback/information in the debug console if attach to PID is slow. Fixes #1003
This commit is contained in:
parent
46efd10d4a
commit
1d038201b3
4 changed files with 162 additions and 47 deletions
|
|
@ -278,6 +278,7 @@ def run_python_code_windows(pid, python_code, connect_debugger_tracing=False, sh
|
|||
|
||||
with _acquire_mutex('_pydevd_pid_attach_mutex_%s' % (pid,), 10):
|
||||
print('--- Connecting to %s bits target (current process is: %s) ---' % (bits, 64 if is_python_64bit() else 32))
|
||||
sys.stdout.flush()
|
||||
|
||||
with _win_write_to_shared_named_memory(python_code, pid):
|
||||
|
||||
|
|
@ -290,6 +291,7 @@ def run_python_code_windows(pid, python_code, connect_debugger_tracing=False, sh
|
|||
raise RuntimeError('Could not find expected .dll file in attach to process.')
|
||||
|
||||
print('\n--- Injecting attach dll: %s into pid: %s ---' % (os.path.basename(target_dll), pid))
|
||||
sys.stdout.flush()
|
||||
args = [target_executable, str(pid), target_dll]
|
||||
subprocess.check_call(args)
|
||||
|
||||
|
|
@ -301,12 +303,15 @@ def run_python_code_windows(pid, python_code, connect_debugger_tracing=False, sh
|
|||
|
||||
with _create_win_event('_pydevd_pid_event_%s' % (pid,)) as event:
|
||||
print('\n--- Injecting run code dll: %s into pid: %s ---' % (os.path.basename(target_dll_run_on_dllmain), pid))
|
||||
sys.stdout.flush()
|
||||
args = [target_executable, str(pid), target_dll_run_on_dllmain]
|
||||
subprocess.check_call(args)
|
||||
|
||||
if not event.wait_for_event_set(10):
|
||||
if not event.wait_for_event_set(15):
|
||||
print('Timeout error: the attach may not have completed.')
|
||||
sys.stdout.flush()
|
||||
print('--- Finished dll injection ---\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
return 0
|
||||
|
||||
|
|
@ -433,11 +438,14 @@ def run_python_code_linux(pid, python_code, connect_debugger_tracing=False, show
|
|||
# reason why this is no longer done by default -- see: https://github.com/microsoft/debugpy/issues/882).
|
||||
gdb_load_shared_libraries = os.environ.get('PYDEVD_GDB_SCAN_SHARED_LIBRARIES', '').strip()
|
||||
if gdb_load_shared_libraries:
|
||||
print('PYDEVD_GDB_SCAN_SHARED_LIBRARIES set: %s.' % (gdb_load_shared_libraries,))
|
||||
cmd.extend(["--init-eval-command='set auto-solib-add off'"]) # Don't scan all libraries.
|
||||
|
||||
for lib in gdb_load_shared_libraries.split(','):
|
||||
lib = lib.strip()
|
||||
cmd.extend(["--eval-command='sharedlibrary %s'" % (lib,)]) # Scan the specified library
|
||||
else:
|
||||
print('PYDEVD_GDB_SCAN_SHARED_LIBRARIES not set (scanning all libraries for needed symbols).')
|
||||
|
||||
cmd.extend(["--eval-command='set scheduler-locking off'"]) # If on we'll deadlock.
|
||||
|
||||
|
|
@ -460,18 +468,7 @@ def run_python_code_linux(pid, python_code, connect_debugger_tracing=False, show
|
|||
env.pop('PYTHONIOENCODING', None)
|
||||
env.pop('PYTHONPATH', None)
|
||||
print('Running: %s' % (' '.join(cmd)))
|
||||
p = subprocess.Popen(
|
||||
' '.join(cmd),
|
||||
shell=True,
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
print('Running gdb in target process.')
|
||||
out, err = p.communicate()
|
||||
print('stdout: %s' % (out,))
|
||||
print('stderr: %s' % (err,))
|
||||
return out, err
|
||||
subprocess.check_call(' '.join(cmd), shell=True, env=env)
|
||||
|
||||
|
||||
def find_helper_script(filedir, script_name):
|
||||
|
|
@ -523,23 +520,12 @@ def run_python_code_mac(pid, python_code, connect_debugger_tracing=False, show_d
|
|||
# print ' '.join(cmd)
|
||||
|
||||
env = os.environ.copy()
|
||||
# Remove the PYTHONPATH (if gdb has a builtin Python it could fail if we
|
||||
# Remove the PYTHONPATH (if lldb has a builtin Python it could fail if we
|
||||
# have the PYTHONPATH for a different python version or some forced encoding).
|
||||
env.pop('PYTHONIOENCODING', None)
|
||||
env.pop('PYTHONPATH', None)
|
||||
print('Running: %s' % (' '.join(cmd)))
|
||||
p = subprocess.Popen(
|
||||
' '.join(cmd),
|
||||
shell=True,
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
)
|
||||
print('Running lldb in target process.')
|
||||
out, err = p.communicate()
|
||||
print('stdout: %s' % (out,))
|
||||
print('stderr: %s' % (err,))
|
||||
return out, err
|
||||
subprocess.check_call(' '.join(cmd), shell=True, env=env)
|
||||
|
||||
|
||||
if IS_WINDOWS:
|
||||
|
|
|
|||
|
|
@ -194,7 +194,6 @@ class Client(components.Component):
|
|||
# See https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522
|
||||
# for the sequence of request and events necessary to orchestrate the start.
|
||||
def _start_message_handler(f):
|
||||
|
||||
@components.Component.message_handler
|
||||
def handle(self, request):
|
||||
assert request.is_request("launch", "attach")
|
||||
|
|
@ -465,7 +464,9 @@ class Client(components.Component):
|
|||
|
||||
if listen != ():
|
||||
if servers.is_serving():
|
||||
raise request.isnt_valid('Multiple concurrent "listen" sessions are not supported')
|
||||
raise request.isnt_valid(
|
||||
'Multiple concurrent "listen" sessions are not supported'
|
||||
)
|
||||
host = listen("host", "127.0.0.1")
|
||||
port = listen("port", int)
|
||||
adapter.access_token = None
|
||||
|
|
@ -507,7 +508,25 @@ class Client(components.Component):
|
|||
except Exception:
|
||||
raise request.isnt_valid('"processId" must be parseable as int')
|
||||
debugpy_args = request("debugpyArgs", json.array(str))
|
||||
servers.inject(pid, debugpy_args)
|
||||
|
||||
def on_output(category, output):
|
||||
self.channel.send_event(
|
||||
"output",
|
||||
{
|
||||
"category": category,
|
||||
"output": output,
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
servers.inject(pid, debugpy_args, on_output)
|
||||
except Exception as e:
|
||||
log.swallow_exception()
|
||||
self.session.finalize(
|
||||
"Error when trying to attach to PID:\n%s" % (str(e),)
|
||||
)
|
||||
return
|
||||
|
||||
timeout = common.PROCESS_SPAWN_TIMEOUT
|
||||
pred = lambda conn: conn.pid == pid
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ import debugpy
|
|||
from debugpy import adapter
|
||||
from debugpy.common import json, log, messaging, sockets
|
||||
from debugpy.adapter import components
|
||||
|
||||
import traceback
|
||||
import io
|
||||
|
||||
access_token = None
|
||||
"""Access token used to authenticate with the servers."""
|
||||
|
|
@ -471,7 +472,7 @@ def dont_wait_for_first_connection():
|
|||
_connections_changed.set()
|
||||
|
||||
|
||||
def inject(pid, debugpy_args):
|
||||
def inject(pid, debugpy_args, on_output):
|
||||
host, port = listener.getsockname()
|
||||
|
||||
cmdline = [
|
||||
|
|
@ -504,20 +505,114 @@ def inject(pid, debugpy_args):
|
|||
)
|
||||
)
|
||||
|
||||
# We need to capture the output of the injector - otherwise it can get blocked
|
||||
# on a write() syscall when it tries to print something.
|
||||
# We need to capture the output of the injector - needed so that it doesn't
|
||||
# get blocked on a write() syscall (besides showing it to the user if it
|
||||
# is taking longer than expected).
|
||||
|
||||
def capture_output():
|
||||
output_collected = []
|
||||
output_collected.append("--- Starting attach to pid: {0} ---\n".format(pid))
|
||||
|
||||
def capture(stream):
|
||||
nonlocal output_collected
|
||||
try:
|
||||
while True:
|
||||
line = stream.readline()
|
||||
if not line:
|
||||
break
|
||||
line = line.decode("utf-8", "replace")
|
||||
output_collected.append(line)
|
||||
log.info("Injector[PID={0}] output: {1}", pid, line.rstrip())
|
||||
log.info("Injector[PID={0}] exited.", pid)
|
||||
except Exception:
|
||||
s = io.StringIO()
|
||||
traceback.print_exc(file=s)
|
||||
on_output("stderr", s.getvalue())
|
||||
|
||||
threading.Thread(
|
||||
target=capture,
|
||||
name=f"Injector[PID={pid}] stdout",
|
||||
args=(injector.stdout,),
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
def info_on_timeout():
|
||||
nonlocal output_collected
|
||||
taking_longer_than_expected = False
|
||||
initial_time = time.time()
|
||||
while True:
|
||||
line = injector.stdout.readline()
|
||||
if not line:
|
||||
time.sleep(1)
|
||||
returncode = injector.poll()
|
||||
if returncode is not None:
|
||||
if returncode != 0:
|
||||
# Something didn't work out. Let's print more info to the user.
|
||||
on_output(
|
||||
"stderr",
|
||||
"Attach to PID failed.\n\n",
|
||||
)
|
||||
|
||||
old = output_collected
|
||||
output_collected = []
|
||||
contents = "".join(old)
|
||||
on_output("stderr", "".join(contents))
|
||||
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=f"Injector[PID={pid}] output",
|
||||
)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
elapsed = time.time() - initial_time
|
||||
on_output(
|
||||
"stdout", "Attaching to PID: %s (elapsed: %.2fs).\n" % (pid, elapsed)
|
||||
)
|
||||
|
||||
if not taking_longer_than_expected:
|
||||
if elapsed > 10:
|
||||
taking_longer_than_expected = True
|
||||
if sys.platform in ("linux", "linux2"):
|
||||
on_output(
|
||||
"stdout",
|
||||
"\nThe attach to PID is taking longer than expected.\n",
|
||||
)
|
||||
on_output(
|
||||
"stdout",
|
||||
"On Linux it's possible to customize the value of\n",
|
||||
)
|
||||
on_output(
|
||||
"stdout",
|
||||
"`PYDEVD_GDB_SCAN_SHARED_LIBRARIES` so that fewer libraries.\n",
|
||||
)
|
||||
on_output(
|
||||
"stdout",
|
||||
"are scanned when searching for the needed symbols.\n\n",
|
||||
)
|
||||
on_output(
|
||||
"stdout",
|
||||
"i.e.: set in your environment variables (and restart your editor/client\n",
|
||||
)
|
||||
on_output(
|
||||
"stdout",
|
||||
"so that it picks up the updated environment variable value):\n\n",
|
||||
)
|
||||
on_output(
|
||||
"stdout",
|
||||
"PYDEVD_GDB_SCAN_SHARED_LIBRARIES=libdl, libltdl, libc, libfreebl3\n\n",
|
||||
)
|
||||
on_output(
|
||||
"stdout",
|
||||
"-- the actual library may be different (the gdb output typically\n",
|
||||
)
|
||||
on_output(
|
||||
"stdout",
|
||||
"-- writes the libraries that will be used, so, it should be possible\n",
|
||||
)
|
||||
on_output(
|
||||
"stdout",
|
||||
"-- to test other libraries if the above doesn't work).\n\n",
|
||||
)
|
||||
if taking_longer_than_expected:
|
||||
# If taking longer than expected, start showing the actual output to the user.
|
||||
old = output_collected
|
||||
output_collected = []
|
||||
contents = "".join(old)
|
||||
if contents:
|
||||
on_output("stderr", contents)
|
||||
|
||||
threading.Thread(
|
||||
target=info_on_timeout, name=f"Injector[PID={pid}] info on timeout", daemon=True
|
||||
).start()
|
||||
|
|
|
|||
|
|
@ -32,7 +32,15 @@ def test_with_no_output(pyfile, target, run):
|
|||
session.wait_for_stop("breakpoint")
|
||||
session.request_continue()
|
||||
|
||||
assert not session.output("stdout")
|
||||
output = session.output("stdout")
|
||||
lines = []
|
||||
for line in output.splitlines(keepends=True):
|
||||
if not line.startswith("Attaching to PID:"):
|
||||
lines.append(line)
|
||||
|
||||
output = "".join(lines)
|
||||
|
||||
assert not output
|
||||
assert not session.output("stderr")
|
||||
if session.debuggee is not None:
|
||||
assert not session.captured_stdout()
|
||||
|
|
@ -120,10 +128,17 @@ def test_redirect_output(pyfile, target, run, redirect):
|
|||
session.wait_for_stop()
|
||||
session.request_continue()
|
||||
|
||||
output = session.output("stdout")
|
||||
lines = []
|
||||
for line in output.splitlines(keepends=True):
|
||||
if not line.startswith("Attaching to PID:"):
|
||||
lines.append(line)
|
||||
|
||||
output = "".join(lines)
|
||||
if redirect == "enabled":
|
||||
assert session.output("stdout") == "111\n222\n333\n444\n"
|
||||
assert output == "111\n222\n333\n444\n"
|
||||
else:
|
||||
assert not session.output("stdout")
|
||||
assert not output
|
||||
|
||||
|
||||
def test_non_ascii_output(pyfile, target, run):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue