jpayne@68
|
1 """Support for remote Python debugging.
|
jpayne@68
|
2
|
jpayne@68
|
3 Some ASCII art to describe the structure:
|
jpayne@68
|
4
|
jpayne@68
|
5 IN PYTHON SUBPROCESS # IN IDLE PROCESS
|
jpayne@68
|
6 #
|
jpayne@68
|
7 # oid='gui_adapter'
|
jpayne@68
|
8 +----------+ # +------------+ +-----+
|
jpayne@68
|
9 | GUIProxy |--remote#call-->| GUIAdapter |--calls-->| GUI |
|
jpayne@68
|
10 +-----+--calls-->+----------+ # +------------+ +-----+
|
jpayne@68
|
11 | Idb | # /
|
jpayne@68
|
12 +-----+<-calls--+------------+ # +----------+<--calls-/
|
jpayne@68
|
13 | IdbAdapter |<--remote#call--| IdbProxy |
|
jpayne@68
|
14 +------------+ # +----------+
|
jpayne@68
|
15 oid='idb_adapter' #
|
jpayne@68
|
16
|
jpayne@68
|
17 The purpose of the Proxy and Adapter classes is to translate certain
|
jpayne@68
|
18 arguments and return values that cannot be transported through the RPC
|
jpayne@68
|
19 barrier, in particular frame and traceback objects.
|
jpayne@68
|
20
|
jpayne@68
|
21 """
|
jpayne@68
|
22
|
jpayne@68
|
23 import types
|
jpayne@68
|
24 from idlelib import debugger
|
jpayne@68
|
25
|
jpayne@68
|
26 debugging = 0
|
jpayne@68
|
27
|
jpayne@68
|
28 idb_adap_oid = "idb_adapter"
|
jpayne@68
|
29 gui_adap_oid = "gui_adapter"
|
jpayne@68
|
30
|
jpayne@68
|
31 #=======================================
|
jpayne@68
|
32 #
|
jpayne@68
|
33 # In the PYTHON subprocess:
|
jpayne@68
|
34
|
jpayne@68
|
35 frametable = {}
|
jpayne@68
|
36 dicttable = {}
|
jpayne@68
|
37 codetable = {}
|
jpayne@68
|
38 tracebacktable = {}
|
jpayne@68
|
39
|
jpayne@68
|
40 def wrap_frame(frame):
|
jpayne@68
|
41 fid = id(frame)
|
jpayne@68
|
42 frametable[fid] = frame
|
jpayne@68
|
43 return fid
|
jpayne@68
|
44
|
jpayne@68
|
45 def wrap_info(info):
|
jpayne@68
|
46 "replace info[2], a traceback instance, by its ID"
|
jpayne@68
|
47 if info is None:
|
jpayne@68
|
48 return None
|
jpayne@68
|
49 else:
|
jpayne@68
|
50 traceback = info[2]
|
jpayne@68
|
51 assert isinstance(traceback, types.TracebackType)
|
jpayne@68
|
52 traceback_id = id(traceback)
|
jpayne@68
|
53 tracebacktable[traceback_id] = traceback
|
jpayne@68
|
54 modified_info = (info[0], info[1], traceback_id)
|
jpayne@68
|
55 return modified_info
|
jpayne@68
|
56
|
jpayne@68
|
57 class GUIProxy:
|
jpayne@68
|
58
|
jpayne@68
|
59 def __init__(self, conn, gui_adap_oid):
|
jpayne@68
|
60 self.conn = conn
|
jpayne@68
|
61 self.oid = gui_adap_oid
|
jpayne@68
|
62
|
jpayne@68
|
63 def interaction(self, message, frame, info=None):
|
jpayne@68
|
64 # calls rpc.SocketIO.remotecall() via run.MyHandler instance
|
jpayne@68
|
65 # pass frame and traceback object IDs instead of the objects themselves
|
jpayne@68
|
66 self.conn.remotecall(self.oid, "interaction",
|
jpayne@68
|
67 (message, wrap_frame(frame), wrap_info(info)),
|
jpayne@68
|
68 {})
|
jpayne@68
|
69
|
jpayne@68
|
70 class IdbAdapter:
|
jpayne@68
|
71
|
jpayne@68
|
72 def __init__(self, idb):
|
jpayne@68
|
73 self.idb = idb
|
jpayne@68
|
74
|
jpayne@68
|
75 #----------called by an IdbProxy----------
|
jpayne@68
|
76
|
jpayne@68
|
77 def set_step(self):
|
jpayne@68
|
78 self.idb.set_step()
|
jpayne@68
|
79
|
jpayne@68
|
80 def set_quit(self):
|
jpayne@68
|
81 self.idb.set_quit()
|
jpayne@68
|
82
|
jpayne@68
|
83 def set_continue(self):
|
jpayne@68
|
84 self.idb.set_continue()
|
jpayne@68
|
85
|
jpayne@68
|
86 def set_next(self, fid):
|
jpayne@68
|
87 frame = frametable[fid]
|
jpayne@68
|
88 self.idb.set_next(frame)
|
jpayne@68
|
89
|
jpayne@68
|
90 def set_return(self, fid):
|
jpayne@68
|
91 frame = frametable[fid]
|
jpayne@68
|
92 self.idb.set_return(frame)
|
jpayne@68
|
93
|
jpayne@68
|
94 def get_stack(self, fid, tbid):
|
jpayne@68
|
95 frame = frametable[fid]
|
jpayne@68
|
96 if tbid is None:
|
jpayne@68
|
97 tb = None
|
jpayne@68
|
98 else:
|
jpayne@68
|
99 tb = tracebacktable[tbid]
|
jpayne@68
|
100 stack, i = self.idb.get_stack(frame, tb)
|
jpayne@68
|
101 stack = [(wrap_frame(frame2), k) for frame2, k in stack]
|
jpayne@68
|
102 return stack, i
|
jpayne@68
|
103
|
jpayne@68
|
104 def run(self, cmd):
|
jpayne@68
|
105 import __main__
|
jpayne@68
|
106 self.idb.run(cmd, __main__.__dict__)
|
jpayne@68
|
107
|
jpayne@68
|
108 def set_break(self, filename, lineno):
|
jpayne@68
|
109 msg = self.idb.set_break(filename, lineno)
|
jpayne@68
|
110 return msg
|
jpayne@68
|
111
|
jpayne@68
|
112 def clear_break(self, filename, lineno):
|
jpayne@68
|
113 msg = self.idb.clear_break(filename, lineno)
|
jpayne@68
|
114 return msg
|
jpayne@68
|
115
|
jpayne@68
|
116 def clear_all_file_breaks(self, filename):
|
jpayne@68
|
117 msg = self.idb.clear_all_file_breaks(filename)
|
jpayne@68
|
118 return msg
|
jpayne@68
|
119
|
jpayne@68
|
120 #----------called by a FrameProxy----------
|
jpayne@68
|
121
|
jpayne@68
|
122 def frame_attr(self, fid, name):
|
jpayne@68
|
123 frame = frametable[fid]
|
jpayne@68
|
124 return getattr(frame, name)
|
jpayne@68
|
125
|
jpayne@68
|
126 def frame_globals(self, fid):
|
jpayne@68
|
127 frame = frametable[fid]
|
jpayne@68
|
128 dict = frame.f_globals
|
jpayne@68
|
129 did = id(dict)
|
jpayne@68
|
130 dicttable[did] = dict
|
jpayne@68
|
131 return did
|
jpayne@68
|
132
|
jpayne@68
|
133 def frame_locals(self, fid):
|
jpayne@68
|
134 frame = frametable[fid]
|
jpayne@68
|
135 dict = frame.f_locals
|
jpayne@68
|
136 did = id(dict)
|
jpayne@68
|
137 dicttable[did] = dict
|
jpayne@68
|
138 return did
|
jpayne@68
|
139
|
jpayne@68
|
140 def frame_code(self, fid):
|
jpayne@68
|
141 frame = frametable[fid]
|
jpayne@68
|
142 code = frame.f_code
|
jpayne@68
|
143 cid = id(code)
|
jpayne@68
|
144 codetable[cid] = code
|
jpayne@68
|
145 return cid
|
jpayne@68
|
146
|
jpayne@68
|
147 #----------called by a CodeProxy----------
|
jpayne@68
|
148
|
jpayne@68
|
149 def code_name(self, cid):
|
jpayne@68
|
150 code = codetable[cid]
|
jpayne@68
|
151 return code.co_name
|
jpayne@68
|
152
|
jpayne@68
|
153 def code_filename(self, cid):
|
jpayne@68
|
154 code = codetable[cid]
|
jpayne@68
|
155 return code.co_filename
|
jpayne@68
|
156
|
jpayne@68
|
157 #----------called by a DictProxy----------
|
jpayne@68
|
158
|
jpayne@68
|
159 def dict_keys(self, did):
|
jpayne@68
|
160 raise NotImplementedError("dict_keys not public or pickleable")
|
jpayne@68
|
161 ## dict = dicttable[did]
|
jpayne@68
|
162 ## return dict.keys()
|
jpayne@68
|
163
|
jpayne@68
|
164 ### Needed until dict_keys is type is finished and pickealable.
|
jpayne@68
|
165 ### Will probably need to extend rpc.py:SocketIO._proxify at that time.
|
jpayne@68
|
166 def dict_keys_list(self, did):
|
jpayne@68
|
167 dict = dicttable[did]
|
jpayne@68
|
168 return list(dict.keys())
|
jpayne@68
|
169
|
jpayne@68
|
170 def dict_item(self, did, key):
|
jpayne@68
|
171 dict = dicttable[did]
|
jpayne@68
|
172 value = dict[key]
|
jpayne@68
|
173 value = repr(value) ### can't pickle module 'builtins'
|
jpayne@68
|
174 return value
|
jpayne@68
|
175
|
jpayne@68
|
176 #----------end class IdbAdapter----------
|
jpayne@68
|
177
|
jpayne@68
|
178
|
jpayne@68
|
179 def start_debugger(rpchandler, gui_adap_oid):
|
jpayne@68
|
180 """Start the debugger and its RPC link in the Python subprocess
|
jpayne@68
|
181
|
jpayne@68
|
182 Start the subprocess side of the split debugger and set up that side of the
|
jpayne@68
|
183 RPC link by instantiating the GUIProxy, Idb debugger, and IdbAdapter
|
jpayne@68
|
184 objects and linking them together. Register the IdbAdapter with the
|
jpayne@68
|
185 RPCServer to handle RPC requests from the split debugger GUI via the
|
jpayne@68
|
186 IdbProxy.
|
jpayne@68
|
187
|
jpayne@68
|
188 """
|
jpayne@68
|
189 gui_proxy = GUIProxy(rpchandler, gui_adap_oid)
|
jpayne@68
|
190 idb = debugger.Idb(gui_proxy)
|
jpayne@68
|
191 idb_adap = IdbAdapter(idb)
|
jpayne@68
|
192 rpchandler.register(idb_adap_oid, idb_adap)
|
jpayne@68
|
193 return idb_adap_oid
|
jpayne@68
|
194
|
jpayne@68
|
195
|
jpayne@68
|
196 #=======================================
|
jpayne@68
|
197 #
|
jpayne@68
|
198 # In the IDLE process:
|
jpayne@68
|
199
|
jpayne@68
|
200
|
jpayne@68
|
201 class FrameProxy:
|
jpayne@68
|
202
|
jpayne@68
|
203 def __init__(self, conn, fid):
|
jpayne@68
|
204 self._conn = conn
|
jpayne@68
|
205 self._fid = fid
|
jpayne@68
|
206 self._oid = "idb_adapter"
|
jpayne@68
|
207 self._dictcache = {}
|
jpayne@68
|
208
|
jpayne@68
|
209 def __getattr__(self, name):
|
jpayne@68
|
210 if name[:1] == "_":
|
jpayne@68
|
211 raise AttributeError(name)
|
jpayne@68
|
212 if name == "f_code":
|
jpayne@68
|
213 return self._get_f_code()
|
jpayne@68
|
214 if name == "f_globals":
|
jpayne@68
|
215 return self._get_f_globals()
|
jpayne@68
|
216 if name == "f_locals":
|
jpayne@68
|
217 return self._get_f_locals()
|
jpayne@68
|
218 return self._conn.remotecall(self._oid, "frame_attr",
|
jpayne@68
|
219 (self._fid, name), {})
|
jpayne@68
|
220
|
jpayne@68
|
221 def _get_f_code(self):
|
jpayne@68
|
222 cid = self._conn.remotecall(self._oid, "frame_code", (self._fid,), {})
|
jpayne@68
|
223 return CodeProxy(self._conn, self._oid, cid)
|
jpayne@68
|
224
|
jpayne@68
|
225 def _get_f_globals(self):
|
jpayne@68
|
226 did = self._conn.remotecall(self._oid, "frame_globals",
|
jpayne@68
|
227 (self._fid,), {})
|
jpayne@68
|
228 return self._get_dict_proxy(did)
|
jpayne@68
|
229
|
jpayne@68
|
230 def _get_f_locals(self):
|
jpayne@68
|
231 did = self._conn.remotecall(self._oid, "frame_locals",
|
jpayne@68
|
232 (self._fid,), {})
|
jpayne@68
|
233 return self._get_dict_proxy(did)
|
jpayne@68
|
234
|
jpayne@68
|
235 def _get_dict_proxy(self, did):
|
jpayne@68
|
236 if did in self._dictcache:
|
jpayne@68
|
237 return self._dictcache[did]
|
jpayne@68
|
238 dp = DictProxy(self._conn, self._oid, did)
|
jpayne@68
|
239 self._dictcache[did] = dp
|
jpayne@68
|
240 return dp
|
jpayne@68
|
241
|
jpayne@68
|
242
|
jpayne@68
|
243 class CodeProxy:
|
jpayne@68
|
244
|
jpayne@68
|
245 def __init__(self, conn, oid, cid):
|
jpayne@68
|
246 self._conn = conn
|
jpayne@68
|
247 self._oid = oid
|
jpayne@68
|
248 self._cid = cid
|
jpayne@68
|
249
|
jpayne@68
|
250 def __getattr__(self, name):
|
jpayne@68
|
251 if name == "co_name":
|
jpayne@68
|
252 return self._conn.remotecall(self._oid, "code_name",
|
jpayne@68
|
253 (self._cid,), {})
|
jpayne@68
|
254 if name == "co_filename":
|
jpayne@68
|
255 return self._conn.remotecall(self._oid, "code_filename",
|
jpayne@68
|
256 (self._cid,), {})
|
jpayne@68
|
257
|
jpayne@68
|
258
|
jpayne@68
|
259 class DictProxy:
|
jpayne@68
|
260
|
jpayne@68
|
261 def __init__(self, conn, oid, did):
|
jpayne@68
|
262 self._conn = conn
|
jpayne@68
|
263 self._oid = oid
|
jpayne@68
|
264 self._did = did
|
jpayne@68
|
265
|
jpayne@68
|
266 ## def keys(self):
|
jpayne@68
|
267 ## return self._conn.remotecall(self._oid, "dict_keys", (self._did,), {})
|
jpayne@68
|
268
|
jpayne@68
|
269 # 'temporary' until dict_keys is a pickleable built-in type
|
jpayne@68
|
270 def keys(self):
|
jpayne@68
|
271 return self._conn.remotecall(self._oid,
|
jpayne@68
|
272 "dict_keys_list", (self._did,), {})
|
jpayne@68
|
273
|
jpayne@68
|
274 def __getitem__(self, key):
|
jpayne@68
|
275 return self._conn.remotecall(self._oid, "dict_item",
|
jpayne@68
|
276 (self._did, key), {})
|
jpayne@68
|
277
|
jpayne@68
|
278 def __getattr__(self, name):
|
jpayne@68
|
279 ##print("*** Failed DictProxy.__getattr__:", name)
|
jpayne@68
|
280 raise AttributeError(name)
|
jpayne@68
|
281
|
jpayne@68
|
282
|
jpayne@68
|
283 class GUIAdapter:
|
jpayne@68
|
284
|
jpayne@68
|
285 def __init__(self, conn, gui):
|
jpayne@68
|
286 self.conn = conn
|
jpayne@68
|
287 self.gui = gui
|
jpayne@68
|
288
|
jpayne@68
|
289 def interaction(self, message, fid, modified_info):
|
jpayne@68
|
290 ##print("*** Interaction: (%s, %s, %s)" % (message, fid, modified_info))
|
jpayne@68
|
291 frame = FrameProxy(self.conn, fid)
|
jpayne@68
|
292 self.gui.interaction(message, frame, modified_info)
|
jpayne@68
|
293
|
jpayne@68
|
294
|
jpayne@68
|
295 class IdbProxy:
|
jpayne@68
|
296
|
jpayne@68
|
297 def __init__(self, conn, shell, oid):
|
jpayne@68
|
298 self.oid = oid
|
jpayne@68
|
299 self.conn = conn
|
jpayne@68
|
300 self.shell = shell
|
jpayne@68
|
301
|
jpayne@68
|
302 def call(self, methodname, /, *args, **kwargs):
|
jpayne@68
|
303 ##print("*** IdbProxy.call %s %s %s" % (methodname, args, kwargs))
|
jpayne@68
|
304 value = self.conn.remotecall(self.oid, methodname, args, kwargs)
|
jpayne@68
|
305 ##print("*** IdbProxy.call %s returns %r" % (methodname, value))
|
jpayne@68
|
306 return value
|
jpayne@68
|
307
|
jpayne@68
|
308 def run(self, cmd, locals):
|
jpayne@68
|
309 # Ignores locals on purpose!
|
jpayne@68
|
310 seq = self.conn.asyncqueue(self.oid, "run", (cmd,), {})
|
jpayne@68
|
311 self.shell.interp.active_seq = seq
|
jpayne@68
|
312
|
jpayne@68
|
313 def get_stack(self, frame, tbid):
|
jpayne@68
|
314 # passing frame and traceback IDs, not the objects themselves
|
jpayne@68
|
315 stack, i = self.call("get_stack", frame._fid, tbid)
|
jpayne@68
|
316 stack = [(FrameProxy(self.conn, fid), k) for fid, k in stack]
|
jpayne@68
|
317 return stack, i
|
jpayne@68
|
318
|
jpayne@68
|
319 def set_continue(self):
|
jpayne@68
|
320 self.call("set_continue")
|
jpayne@68
|
321
|
jpayne@68
|
322 def set_step(self):
|
jpayne@68
|
323 self.call("set_step")
|
jpayne@68
|
324
|
jpayne@68
|
325 def set_next(self, frame):
|
jpayne@68
|
326 self.call("set_next", frame._fid)
|
jpayne@68
|
327
|
jpayne@68
|
328 def set_return(self, frame):
|
jpayne@68
|
329 self.call("set_return", frame._fid)
|
jpayne@68
|
330
|
jpayne@68
|
331 def set_quit(self):
|
jpayne@68
|
332 self.call("set_quit")
|
jpayne@68
|
333
|
jpayne@68
|
334 def set_break(self, filename, lineno):
|
jpayne@68
|
335 msg = self.call("set_break", filename, lineno)
|
jpayne@68
|
336 return msg
|
jpayne@68
|
337
|
jpayne@68
|
338 def clear_break(self, filename, lineno):
|
jpayne@68
|
339 msg = self.call("clear_break", filename, lineno)
|
jpayne@68
|
340 return msg
|
jpayne@68
|
341
|
jpayne@68
|
342 def clear_all_file_breaks(self, filename):
|
jpayne@68
|
343 msg = self.call("clear_all_file_breaks", filename)
|
jpayne@68
|
344 return msg
|
jpayne@68
|
345
|
jpayne@68
|
346 def start_remote_debugger(rpcclt, pyshell):
|
jpayne@68
|
347 """Start the subprocess debugger, initialize the debugger GUI and RPC link
|
jpayne@68
|
348
|
jpayne@68
|
349 Request the RPCServer start the Python subprocess debugger and link. Set
|
jpayne@68
|
350 up the Idle side of the split debugger by instantiating the IdbProxy,
|
jpayne@68
|
351 debugger GUI, and debugger GUIAdapter objects and linking them together.
|
jpayne@68
|
352
|
jpayne@68
|
353 Register the GUIAdapter with the RPCClient to handle debugger GUI
|
jpayne@68
|
354 interaction requests coming from the subprocess debugger via the GUIProxy.
|
jpayne@68
|
355
|
jpayne@68
|
356 The IdbAdapter will pass execution and environment requests coming from the
|
jpayne@68
|
357 Idle debugger GUI to the subprocess debugger via the IdbProxy.
|
jpayne@68
|
358
|
jpayne@68
|
359 """
|
jpayne@68
|
360 global idb_adap_oid
|
jpayne@68
|
361
|
jpayne@68
|
362 idb_adap_oid = rpcclt.remotecall("exec", "start_the_debugger",\
|
jpayne@68
|
363 (gui_adap_oid,), {})
|
jpayne@68
|
364 idb_proxy = IdbProxy(rpcclt, pyshell, idb_adap_oid)
|
jpayne@68
|
365 gui = debugger.Debugger(pyshell, idb_proxy)
|
jpayne@68
|
366 gui_adap = GUIAdapter(rpcclt, gui)
|
jpayne@68
|
367 rpcclt.register(gui_adap_oid, gui_adap)
|
jpayne@68
|
368 return gui
|
jpayne@68
|
369
|
jpayne@68
|
370 def close_remote_debugger(rpcclt):
|
jpayne@68
|
371 """Shut down subprocess debugger and Idle side of debugger RPC link
|
jpayne@68
|
372
|
jpayne@68
|
373 Request that the RPCServer shut down the subprocess debugger and link.
|
jpayne@68
|
374 Unregister the GUIAdapter, which will cause a GC on the Idle process
|
jpayne@68
|
375 debugger and RPC link objects. (The second reference to the debugger GUI
|
jpayne@68
|
376 is deleted in pyshell.close_remote_debugger().)
|
jpayne@68
|
377
|
jpayne@68
|
378 """
|
jpayne@68
|
379 close_subprocess_debugger(rpcclt)
|
jpayne@68
|
380 rpcclt.unregister(gui_adap_oid)
|
jpayne@68
|
381
|
jpayne@68
|
382 def close_subprocess_debugger(rpcclt):
|
jpayne@68
|
383 rpcclt.remotecall("exec", "stop_the_debugger", (idb_adap_oid,), {})
|
jpayne@68
|
384
|
jpayne@68
|
385 def restart_subprocess_debugger(rpcclt):
|
jpayne@68
|
386 idb_adap_oid_ret = rpcclt.remotecall("exec", "start_the_debugger",\
|
jpayne@68
|
387 (gui_adap_oid,), {})
|
jpayne@68
|
388 assert idb_adap_oid_ret == idb_adap_oid, 'Idb restarted with different oid'
|
jpayne@68
|
389
|
jpayne@68
|
390
|
jpayne@68
|
391 if __name__ == "__main__":
|
jpayne@68
|
392 from unittest import main
|
jpayne@68
|
393 main('idlelib.idle_test.test_debugger', verbosity=2, exit=False)
|