Skip to content

Commit 8383787

Browse files
authored
Merge pull request #849 from JohanMabille/multistopped
2 parents 67fe7df + ef8003d commit 8383787

File tree

6 files changed

+60
-19
lines changed

6 files changed

+60
-19
lines changed

ipykernel/control.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,13 @@
1010
class ControlThread(Thread):
1111

1212
def __init__(self, **kwargs):
13-
Thread.__init__(self, **kwargs)
13+
Thread.__init__(self, name="Control", **kwargs)
1414
self.io_loop = IOLoop(make_current=False)
1515
self.pydev_do_not_trace = True
1616
self.is_pydev_daemon_thread = True
1717

1818
def run(self):
19+
self.name = "Control"
1920
self.io_loop.make_current()
2021
try:
2122
self.io_loop.start()

ipykernel/debugger.py

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ def __init__(self, log, debugpy_stream, event_callback, shell_socket, session):
277277
self.session = session
278278
self.is_started = False
279279
self.event_callback = event_callback
280+
self.stopped_queue = Queue()
280281

281282
self.started_debug_handlers = {}
282283
for msg_type in Debugger.started_debug_msg_types:
@@ -300,22 +301,19 @@ def __init__(self, log, debugpy_stream, event_callback, shell_socket, session):
300301

301302
def _handle_event(self, msg):
302303
if msg['event'] == 'stopped':
303-
self.stopped_threads.add(msg['body']['threadId'])
304+
if msg['body']['allThreadsStopped']:
305+
self.stopped_queue.put_nowait(msg)
306+
# Do not forward the event now, will be done in the handle_stopped_event
307+
return
308+
else:
309+
self.stopped_threads.add(msg['body']['threadId'])
310+
self.event_callback(msg)
304311
elif msg['event'] == 'continued':
305-
try:
306-
if msg['allThreadsContinued']:
307-
self.stopped_threads = set()
308-
else:
309-
self.stopped_threads.remove(msg['body']['threadId'])
310-
except Exception:
311-
# Workaround for debugpy/pydev not setting the correct threadId
312-
# after a next request. Does not work if a the code executed on
313-
# the shell spawns additional threads
314-
if len(self.stopped_threads) == 1:
315-
self.stopped_threads = set()
316-
else:
317-
raise Exception('threadId from continued event not in stopped threads set')
318-
self.event_callback(msg)
312+
if msg['body']['allThreadsContinued']:
313+
self.stopped_threads = set()
314+
else:
315+
self.stopped_threads.remove(msg['body']['threadId'])
316+
self.event_callback(msg)
319317

320318
async def _forward_message(self, msg):
321319
return await self.debugpy_client.send_dap_request(msg)
@@ -334,6 +332,32 @@ def _build_variables_response(self, request, variables):
334332
}
335333
return reply
336334

335+
def _accept_stopped_thread(self, thread_name):
336+
# TODO: identify Thread-2, Thread-3 and Thread-4. These are NOT
337+
# Control, IOPub or Heartbeat threads
338+
forbid_list = [
339+
'IPythonHistorySavingThread',
340+
'Thread-2',
341+
'Thread-3',
342+
'Thread-4'
343+
]
344+
return thread_name not in forbid_list
345+
346+
async def handle_stopped_event(self):
347+
# Wait for a stopped event message in the stopped queue
348+
# This message is used for triggering the 'threads' request
349+
event = await self.stopped_queue.get()
350+
req = {
351+
'seq': event['seq'] + 1,
352+
'type': 'request',
353+
'command': 'threads'
354+
}
355+
rep = await self._forward_message(req)
356+
for t in rep['body']['threads']:
357+
if self._accept_stopped_thread(t['name']):
358+
self.stopped_threads.add(t['id'])
359+
self.event_callback(event)
360+
337361
@property
338362
def tcp_client(self):
339363
return self.debugpy_client

ipykernel/heartbeat.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ class Heartbeat(Thread):
3232
def __init__(self, context, addr=None):
3333
if addr is None:
3434
addr = ('tcp', localhost(), 0)
35-
Thread.__init__(self)
35+
Thread.__init__(self, name="Heartbeat")
3636
self.context = context
3737
self.transport, self.ip, self.port = addr
3838
self.original_port = self.port
@@ -42,6 +42,7 @@ def __init__(self, context, addr=None):
4242
self.daemon = True
4343
self.pydev_do_not_trace = True
4444
self.is_pydev_daemon_thread = True
45+
self.name = "Heartbeat"
4546

4647
def pick_port(self):
4748
if self.transport == 'tcp':
@@ -89,6 +90,7 @@ def _bind_socket(self):
8990
return
9091

9192
def run(self):
93+
self.name = "Heartbeat"
9294
self.socket = self.context.socket(zmq.ROUTER)
9395
self.socket.linger = 1000
9496
try:

ipykernel/iostream.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,11 @@ def __init__(self, socket, pipe=False):
7070
self._events = deque()
7171
self._event_pipes = WeakSet()
7272
self._setup_event_pipe()
73-
self.thread = threading.Thread(target=self._thread_main)
73+
self.thread = threading.Thread(target=self._thread_main, name="IOPub")
7474
self.thread.daemon = True
7575
self.thread.pydev_do_not_trace = True
7676
self.thread.is_pydev_daemon_thread = True
77+
self.thread.name = "IOPub"
7778

7879
def _thread_main(self):
7980
"""The inner loop that's actually run in a thread"""
@@ -176,6 +177,7 @@ def _check_mp_mode(self):
176177

177178
def start(self):
178179
"""Start the IOPub thread"""
180+
self.thread.name = "IOPub"
179181
self.thread.start()
180182
# make sure we don't prevent process exit
181183
# I'm not sure why setting daemon=True above isn't enough, but it doesn't appear to be.

ipykernel/ipkernel.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,13 +169,19 @@ def dispatch_debugpy(self, msg):
169169
def banner(self):
170170
return self.shell.banner
171171

172+
async def poll_stopped_queue(self):
173+
while True:
174+
await self.debugger.handle_stopped_event()
175+
172176
def start(self):
173177
self.shell.exit_now = False
174178
if self.debugpy_stream is None:
175179
self.log.warning("debugpy_stream undefined, debugging will not be enabled")
176180
else:
177181
self.debugpy_stream.on_recv(self.dispatch_debugpy, copy=False)
178182
super().start()
183+
if self.debugpy_stream:
184+
asyncio.run_coroutine_threadsafe(self.poll_stopped_queue(), self.control_thread.io_loop.asyncio_loop)
179185

180186
def set_parent(self, ident, parent, channel='shell'):
181187
"""Overridden from parent to tell the display hook and output streams

ipykernel/tests/test_debugger.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,7 @@ def test_rich_inspect_at_breakpoint(kernel_with_debug):
227227
228228
f(2, 3)"""
229229

230+
230231
r = wait_for_debug_request(kernel_with_debug, "dumpCell", {"code": code})
231232
source = r["body"]["sourcePath"]
232233

@@ -246,6 +247,11 @@ def test_rich_inspect_at_breakpoint(kernel_with_debug):
246247

247248
kernel_with_debug.execute(code)
248249

250+
# Wait for stop on breakpoint
251+
msg = {"msg_type": "", "content": {}}
252+
while msg.get('msg_type') != 'debug_event' or msg["content"].get("event") != "stopped":
253+
msg = kernel_with_debug.get_iopub_msg(timeout=TIMEOUT)
254+
249255
stacks = wait_for_debug_request(kernel_with_debug, "stackTrace", {"threadId": 1})[
250256
"body"
251257
]["stackFrames"]
@@ -276,4 +282,4 @@ def test_rich_inspect_at_breakpoint(kernel_with_debug):
276282
def test_convert_to_long_pathname():
277283
if sys.platform == 'win32':
278284
from ipykernel.compiler import _convert_to_long_pathname
279-
_convert_to_long_pathname(__file__)
285+
_convert_to_long_pathname(__file__)

0 commit comments

Comments
 (0)