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