Skip to content

Commit 5d5f8f4

Browse files
committed
Re-attach to pid when using --client. Fixes microsoft/ptvsd#1817
1 parent 7d86c23 commit 5d5f8f4

7 files changed

Lines changed: 2758 additions & 2542 deletions

File tree

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_cython.c

Lines changed: 2693 additions & 2514 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_cython.pyx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ from __future__ import print_function
66
# DO NOT edit manually!
77
import sys
88
from _pydevd_bundle.pydevd_constants import (STATE_RUN, PYTHON_SUSPEND, IS_JYTHON,
9-
USE_CUSTOM_SYS_CURRENT_FRAMES, USE_CUSTOM_SYS_CURRENT_FRAMES_MAP, ForkSafeLock)
9+
USE_CUSTOM_SYS_CURRENT_FRAMES, USE_CUSTOM_SYS_CURRENT_FRAMES_MAP, SUPPORT_GEVENT, ForkSafeLock)
1010
from _pydev_bundle import pydev_log
1111
# IFDEF CYTHON -- DONT EDIT THIS FILE (it is automatically generated)
1212
pydev_log.debug("Using Cython speedups")
@@ -134,7 +134,21 @@ cdef class PyDBAdditionalThreadInfo:
134134
'''
135135
# sys._current_frames(): dictionary with thread id -> topmost frame
136136
current_frames = _current_frames()
137-
return current_frames.get(thread.ident)
137+
topmost_frame = current_frames.get(thread.ident)
138+
if topmost_frame is None:
139+
# Note: this is expected for dummy threads (so, getting the topmost frame should be
140+
# treated as optional).
141+
pydev_log.info(
142+
'Unable to get topmost frame for thread: %s, thread.ident: %s, id(thread): %s\nCurrent frames: %s.\n'
143+
'GEVENT_SUPPORT: %s',
144+
thread,
145+
thread.ident,
146+
id(thread),
147+
current_frames,
148+
SUPPORT_GEVENT,
149+
)
150+
151+
return topmost_frame
138152

139153
def __str__(self):
140154
return 'State:%s Stop:%s Cmd: %s Kill:%s' % (
@@ -1206,7 +1220,7 @@ def fix_top_level_trace_and_get_trace_func(py_db, frame):
12061220
return f_trace, False
12071221

12081222
thread_tracer = additional_info.thread_tracer
1209-
if thread_tracer is None:
1223+
if thread_tracer is None or thread_tracer._args[0] is not py_db:
12101224
thread_tracer = ThreadTracer(args)
12111225
additional_info.thread_tracer = thread_tracer
12121226

src/debugpy/_vendored/pydevd/_pydevd_bundle/pydevd_trace_dispatch_regular.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ def fix_top_level_trace_and_get_trace_func(py_db, frame):
181181
return f_trace, False
182182

183183
thread_tracer = additional_info.thread_tracer
184-
if thread_tracer is None:
184+
if thread_tracer is None or thread_tracer._args[0] is not py_db:
185185
thread_tracer = ThreadTracer(args)
186186
additional_info.thread_tracer = thread_tracer
187187

src/debugpy/server/attach_pid_injected.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ def on_critical(msg):
6363
from debugpy.common import log
6464
from debugpy.server import options
6565

66+
import pydevd
67+
68+
py_db = pydevd.get_global_debugger()
69+
if py_db is not None:
70+
py_db.dispose_and_kill_all_pydevd_threads(wait=False)
71+
6672
if log_dir is not None:
6773
log.log_dir = log_dir
6874
options.client = client

src/debugpy/server/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,7 @@ def attach_to_pid():
276276
assert os.path.exists(attach_pid_injected_dirname)
277277

278278
log_dir = (log.log_dir or "").replace("\\", "/")
279-
encode = lambda s: list(bytearray(s.encode("utf-8")))
279+
encode = lambda s: list(bytearray(s.encode("utf-8"))) if s is not None else None
280280
setup = {
281281
"script": encode(attach_pid_injected_dirname),
282282
"host": encode(options.host),
@@ -289,7 +289,7 @@ def attach_to_pid():
289289
python_code = """
290290
import sys;
291291
import codecs;
292-
decode = lambda s: codecs.utf_8_decode(bytearray(s))[0];
292+
decode = lambda s: codecs.utf_8_decode(bytearray(s))[0] if s is not None else None;
293293
script_path = decode({script});
294294
sys.path.insert(0, script_path);
295295
import attach_pid_injected;

tests/debug/session.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -796,14 +796,17 @@ def wait_for_exit(self):
796796
finally:
797797
watchdog.unregister_spawn(self.debuggee.pid, self.debuggee_id)
798798

799-
self.timeline.wait_until_realized(timeline.Event("terminated"))
799+
self.wait_for_terminated()
800800

801801
# FIXME: "exited" event is not properly reported in attach scenarios at the
802802
# moment, so the exit code is only checked if it's present.
803803
if self.debuggee is not None and self.exit_code is not None:
804804
assert self.debuggee.returncode == self.exit_code
805805
return self.exit_code
806806

807+
def wait_for_terminated(self):
808+
self.timeline.wait_until_realized(timeline.Event("terminated"))
809+
807810
def captured_stdout(self, encoding=None):
808811
assert self.debuggee is not None
809812
return self.captured_output.stdout(encoding)
@@ -819,7 +822,7 @@ def disconnect(self, force=False):
819822
try:
820823
if not force:
821824
self.request("disconnect")
822-
self.timeline.wait_until_realized(timeline.Event("terminated"))
825+
self.wait_for_terminated()
823826
except messaging.JsonIOError:
824827
pass
825828
finally:

tests/debugpy/test_attach.py

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def code_to_debug():
139139

140140

141141
@pytest.mark.parametrize("pid_type", ["int", "str"])
142-
def test_attach_by_pid(pyfile, target, pid_type):
142+
def test_attach_by_pid_client(pyfile, target, pid_type):
143143
@pyfile
144144
def code_to_debug():
145145
import debuggee
@@ -148,40 +148,54 @@ def code_to_debug():
148148
debuggee.setup()
149149

150150
def do_something(i):
151-
time.sleep(0.1)
151+
time.sleep(0.2)
152152
proceed = True
153153
print(i) # @bp
154154
return proceed
155155

156-
for i in range(100):
156+
for i in range(500):
157157
if not do_something(i):
158158
break
159159

160-
with debug.Session() as session:
160+
def before_request(command, arguments):
161+
if command == "attach":
162+
assert isinstance(arguments["processId"], int)
163+
if pid_type == "str":
164+
arguments["processId"] = str(arguments["processId"])
165+
166+
session1 = debug.Session()
167+
168+
session1.before_request = before_request
169+
session1.config["redirectOutput"] = True
170+
171+
session1.captured_output = set()
172+
session1.expected_exit_code = None # not expected to exit on disconnect
173+
174+
with session1.attach_by_pid(target(code_to_debug), wait=False):
175+
session1.set_breakpoints(code_to_debug, all)
161176

162-
def before_request(command, arguments):
163-
if command == "attach":
164-
assert isinstance(arguments["processId"], int)
165-
if pid_type == "str":
166-
arguments["processId"] = str(arguments["processId"])
177+
session1.wait_for_stop(expected_frames=[some.dap.frame(code_to_debug, "bp")])
167178

168-
session.before_request = before_request
169-
session.config["redirectOutput"] = True
179+
pid = session1.config["processId"]
170180

171-
with session.attach_by_pid(target(code_to_debug), wait=False):
172-
session.set_breakpoints(code_to_debug, all)
181+
# Note: don't call session1.disconnect because it'd deadlock in channel.close()
182+
# (because the fd is in a read() in a different thread, we can't call close() on it).
183+
session1.request("disconnect")
184+
session1.wait_for_terminated()
173185

174-
stop = session.wait_for_stop(
186+
with debug.Session() as session2:
187+
with session2.attach_by_pid(pid, wait=False):
188+
session2.set_breakpoints(code_to_debug, all)
189+
190+
stop = session2.wait_for_stop(
175191
expected_frames=[some.dap.frame(code_to_debug, "bp")]
176192
)
177193

178194
# Remove breakpoint and continue.
179-
session.request(
195+
session2.set_breakpoints(code_to_debug, [])
196+
session2.request(
180197
"setExpression",
181198
{"frameId": stop.frame_id, "expression": "proceed", "value": "False"},
182199
)
183-
session.set_breakpoints(code_to_debug, [])
184-
session.request_continue()
185-
session.wait_for_next_event(
186-
"output", some.dict.containing({"category": "stdout"})
187-
)
200+
session2.scratchpad["exit"] = True
201+
session2.request_continue()

0 commit comments

Comments
 (0)