Skip to content

Commit 1d3c47a

Browse files
committed
Debugger implementation
1 parent 7046b84 commit 1d3c47a

File tree

1 file changed

+287
-0
lines changed

1 file changed

+287
-0
lines changed

ipykernel/debugger.py

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
import logging
2+
import os
3+
4+
from zmq.utils import jsonapi
5+
from traitlets import Instance
6+
7+
from asyncio import Queue
8+
9+
class DebugpyMessageQueue:
10+
11+
HEADER = 'Content Length: '
12+
HEADER_LENGTH = 16
13+
SEPARATOR = '\r\n\r\n'
14+
SEPARATOR_LENGTH = 4
15+
16+
def __init__(self, event_callback):
17+
self.tcp_buffer = ''
18+
self._reset_tcp_pos()
19+
self.event_callback = event_callback
20+
self.message_queue = Queue
21+
22+
def _reset_tcp_pos(self):
23+
self.header_pos = -1
24+
self.separator_pos = -1
25+
self.message_size = 0
26+
self.message_pos = -1
27+
28+
def _put_message(self, raw_msg):
29+
# TODO: forward to iopub if this is an event message
30+
msg = jsonapi.loads(raw_msg)
31+
if mes['type'] == 'event':
32+
self.event_callback(msg)
33+
else:
34+
self.message_queue.put_nowait(msg)
35+
36+
def put_tcp_frame(self, frame):
37+
self.tcp_buffer += frame
38+
# TODO: not sure this is required
39+
#self.tcp_buffer += frame.decode("utf-8")
40+
41+
# Finds header
42+
if self.header_pos == -1:
43+
self.header_pos = self.tcp_buffer.find(DebugpyMessageQueue.HEADER, hint)
44+
if self.header_pos == -1:
45+
return
46+
47+
#Finds separator
48+
if self.separator_pos == -1:
49+
hint = self.header_pos + DebugpyMessageQueue.HEADER_lenth
50+
self.separator_pos = self.tcp_buffer.find(DebugpyMessageQueue.SEPARATOR, hint)
51+
if self.separator_pos == -1:
52+
return
53+
54+
if self.message_pos == -1:
55+
size_pos = self.header_pos + DebugpyMessageQueue.HEADER_lenth
56+
self.message_pos = self.separator_pos + DebugpyMessageQueue.SEPARATOR_LENGTH
57+
self.message_size = int(self.tcp_buf[size_pos:self.separator_pos])
58+
59+
if len(self.tcp_buffer - self.message_pos) < self.message_size:
60+
return
61+
62+
self._put_message(self.tcp_buf[self.message_pos:self.message_size])
63+
if len(self.tcp_buffer - self_message_pos) == self.message_size:
64+
self.reset_buffer()
65+
else:
66+
self.tcp_buffer = self.tcp_buffer[self.message_pos + self.message_size:]
67+
self._reset_tcp_pos()
68+
69+
async def get_message(self):
70+
return await self.message_queue.get()
71+
72+
73+
class DebugpyClient:
74+
75+
def __init__(self, debugpy_socket, debugpy_stream):
76+
self.debugpy_socket = debugpy_socket
77+
self.debugpy_stream = debugpy_stream
78+
self.message_queue = DebugpyMessageQueue(self._forward_event)
79+
self.wait_for_attach = True
80+
self.init_event = asyncio.Event()
81+
82+
def _forward_event(self, msg):
83+
if msg['event'] == 'initialized':
84+
self.init_event.set()
85+
#TODO: send event to iopub
86+
87+
def _send_request(self, msg):
88+
content = jsonapi.dumps(msg)
89+
content_length = len(content)
90+
buf = DebugpyMessageQueue.HEADER + content_length + DebugpyMessageQueue.SEPARATOR + content_msg
91+
self.debugpy_socket.send(buf) # TODO: pass routing_id
92+
93+
async def _wait_for_reponse(self):
94+
# Since events are never pushed to the message_queue
95+
# we can safely assume the next message in queue
96+
# will be an answer to the previous request
97+
return await self.message_queue.get()
98+
99+
async def _handle_init_sequence(self):
100+
# 1] Waits for initialized event
101+
await self.init_event.wait()
102+
103+
# 2] Sends configurationDone request
104+
configurationDone = {
105+
'type': 'request',
106+
'seq': int(self.init_event_message['seq']) + 1,
107+
'command': 'configurationDone'
108+
}
109+
self._send_request(configurationDone)
110+
111+
# 3] Waits for configurationDone response
112+
await self._wait_for_response()
113+
114+
# 4] Waits for attachResponse and returns it
115+
attach_rep = await self._wait_for_response()
116+
return attach_rep
117+
118+
async def send_dap_request(self, msg):
119+
self._send_request(msg)
120+
if self.wait_for_attach and msg['command'] == 'attach':
121+
rep = await self._handle_init_sequence()
122+
self.wait_for_attach = False
123+
return rep
124+
else:
125+
rep = await self._wait_for_reponse()
126+
return rep
127+
128+
class Debugger:
129+
130+
# Requests that requires that the debugger has started
131+
started_debug_msg_types = [
132+
'dumpCell', 'setBreakpoints',
133+
'source', 'stackTrace',
134+
'variables', 'attach',
135+
'configurationDone'
136+
]
137+
138+
# Requests that can be handled even if the debugger is not running
139+
static_debug_msg_types = [
140+
'debugInfo', 'inspectVariables'
141+
]
142+
143+
log = Instance(logging.Logger, allow_none=True)
144+
145+
def __init__(self):
146+
self.is_started = False
147+
148+
self.header = ''
149+
150+
self.started_debug_handlers = {}
151+
for msg_type in started_debug_msg_types:
152+
self.started_debug_handlers[msg_type] = getattr(self, msg_type)
153+
154+
self.static_debug_handlers = {}
155+
for msg_type in static_debug_msg_types:
156+
self.static_debug_handlers[msg_type] = getattr(self, msg_type)
157+
158+
self.breakpoint_list = {}
159+
self.stopped_threads = []
160+
161+
async def _forward_message(self, msg):
162+
return await self.debugpy_client.send_dap_request(msg)
163+
164+
def start(self):
165+
return False
166+
167+
def stop(self):
168+
pass
169+
170+
def dumpCell(self, message):
171+
return {}
172+
173+
async def setBreakpoints(self, message):
174+
source = message['arguments']['source']['path'];
175+
self.breakpoint_list[source] = message['arguments']['breakpoints']
176+
return await self._forward_message(message);
177+
178+
def source(self, message):
179+
reply = {
180+
'type': 'response',
181+
'request_seq': message['seq'],
182+
'command': message['command']
183+
}
184+
source_path = message['arguments']['source']['path'];
185+
if os.path.isfile(source_path):
186+
with open(source_path) as f:
187+
reply['success'] = True
188+
reply['body'] = {
189+
'content': f.read()
190+
}
191+
192+
else:
193+
reply['success'] = False
194+
reply['message'] = 'source unavailable'
195+
reply['body'] = {}
196+
197+
return reply
198+
199+
async def stackTrace(self, message):
200+
reply = await self._forward_message(message)
201+
reply['body']['stackFrames'] =
202+
[frame for frame in reply['body']['stackFrames'] if frame['source']['path'] != '<string>']
203+
return reply
204+
205+
async def variables(self, message):
206+
reply = await self._forward_message(message)
207+
# TODO : check start and count arguments work as expected in debugpy
208+
return reply
209+
210+
async def attach(self, message):
211+
message['arguments']['connect'] = {
212+
'host': self.debugpy_host,
213+
'port': self.debugpy_port
214+
}
215+
message['arguments']['logToFile'] = True
216+
return await self._forward_message(message)
217+
218+
def configurationDone(self, message):
219+
reply = {
220+
'seq': message['seq'],
221+
'type': 'response',
222+
'request_seq': message['seq'],
223+
'success': True,
224+
'command': message['command']
225+
}
226+
return reply;
227+
228+
def debugInfo(self, message):
229+
reply = {
230+
'type': 'response',
231+
'request_seq': message['seq'],
232+
'success': True,
233+
'command': message['command'],
234+
'body': {
235+
'isStarted': self.is_started,
236+
'hashMethod': 'Murmur2',
237+
'hashSeed': 0,
238+
'tmpFilePrefix': 'coincoin',
239+
'tmpFileSuffix': '.py',
240+
'breakpoints': self.breakpoint_list,
241+
'stoppedThreads': self.stopped_threads
242+
}
243+
}
244+
return reply
245+
246+
def inspectVariables(self, message):
247+
return {}
248+
249+
async def process_request(self, header, message):
250+
reply = {}
251+
252+
if message['command'] == 'initialize':
253+
if self.is_started:
254+
self.log.info('The debugger has already started')
255+
else:
256+
self.is_started = self.start()
257+
if self.is_started:
258+
self.log.info('The debugger has started')
259+
else:
260+
reply = {
261+
'command', 'initialize',
262+
'request_seq', message['seq'],
263+
'seq', 3,
264+
'success', False,
265+
'type', 'response'
266+
}
267+
268+
handler = self.static_debug_handlers.get(message['command'], None)
269+
if handler is not None:
270+
reply = await handler(message)
271+
elif self.is_started:
272+
self.header = header
273+
handler = self.started_debug_handlers.get(message['command'], None)
274+
if handler is not None:
275+
reply = await handler(message)
276+
else
277+
reply = await self._forward_message(message)
278+
279+
if message['command'] == 'disconnect':
280+
self.stop()
281+
self.breakpoint_list = {}
282+
self.stopped_threads = []
283+
self.is_started = False
284+
self.log.info('The debugger has stopped')
285+
286+
return reply
287+

0 commit comments

Comments
 (0)