Skip to content

Commit

Permalink
Provide feedback/information in the debug console if attach to PID is…
Browse files Browse the repository at this point in the history
… slow. Fixes #1003
  • Loading branch information
fabioz committed Oct 17, 2022
1 parent 46efd10 commit 1d03820
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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):

Expand All @@ -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)

Expand All @@ -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

Expand Down Expand Up @@ -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.

Expand All @@ -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):
Expand Down Expand Up @@ -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:
Expand Down
25 changes: 22 additions & 3 deletions src/debugpy/adapter/clients.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,6 @@ def initialize_request(self, request):
# 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")
Expand Down Expand Up @@ -465,7 +464,9 @@ def attach_request(self, request):

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
Expand Down Expand Up @@ -507,7 +508,25 @@ def attach_request(self, request):
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:
Expand Down
127 changes: 111 additions & 16 deletions src/debugpy/adapter/servers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
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."""
Expand Down Expand Up @@ -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 = [
Expand Down Expand Up @@ -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).

output_collected = []
output_collected.append("--- Starting attach to pid: {0} ---\n".format(pid))

def capture_output():
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()
21 changes: 18 additions & 3 deletions tests/debugpy/test_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,15 @@ def code_to_debug():
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()
Expand Down Expand Up @@ -120,10 +128,17 @@ def code_to_debug():
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):
Expand Down

0 comments on commit 1d03820

Please sign in to comment.