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"""
22
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        dict = frame.f_globals
129        did = id(dict)
130        dicttable[did] = dict
131        return did
132
133    def frame_locals(self, fid):
134        frame = frametable[fid]
135        dict = frame.f_locals
136        did = id(dict)
137        dicttable[did] = dict
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##         dict = dicttable[did]
162##         return dict.keys()
163
164    ### Needed until dict_keys is type is finished and pickealable.
165    ### Will probably need to extend rpc.py:SocketIO._proxify at that time.
166    def dict_keys_list(self, did):
167        dict = dicttable[did]
168        return list(dict.keys())
169
170    def dict_item(self, did, key):
171        dict = dicttable[did]
172        value = dict[key]
173        value = repr(value) ### can't pickle module 'builtins'
174        return value
175
176#----------end class IdbAdapter----------
177
178
179def start_debugger(rpchandler, gui_adap_oid):
180    """Start the debugger and its RPC link in the Python subprocess
181
182    Start the subprocess side of the split debugger and set up that side of the
183    RPC link by instantiating the GUIProxy, Idb debugger, and IdbAdapter
184    objects and linking them together.  Register the IdbAdapter with the
185    RPCServer to handle RPC requests from the split debugger GUI via the
186    IdbProxy.
187
188    """
189    gui_proxy = GUIProxy(rpchandler, gui_adap_oid)
190    idb = debugger.Idb(gui_proxy)
191    idb_adap = IdbAdapter(idb)
192    rpchandler.register(idb_adap_oid, idb_adap)
193    return idb_adap_oid
194
195
196#=======================================
197#
198# In the IDLE process:
199
200
201class FrameProxy:
202
203    def __init__(self, conn, fid):
204        self._conn = conn
205        self._fid = fid
206        self._oid = "idb_adapter"
207        self._dictcache = {}
208
209    def __getattr__(self, name):
210        if name[:1] == "_":
211            raise AttributeError(name)
212        if name == "f_code":
213            return self._get_f_code()
214        if name == "f_globals":
215            return self._get_f_globals()
216        if name == "f_locals":
217            return self._get_f_locals()
218        return self._conn.remotecall(self._oid, "frame_attr",
219                                     (self._fid, name), {})
220
221    def _get_f_code(self):
222        cid = self._conn.remotecall(self._oid, "frame_code", (self._fid,), {})
223        return CodeProxy(self._conn, self._oid, cid)
224
225    def _get_f_globals(self):
226        did = self._conn.remotecall(self._oid, "frame_globals",
227                                    (self._fid,), {})
228        return self._get_dict_proxy(did)
229
230    def _get_f_locals(self):
231        did = self._conn.remotecall(self._oid, "frame_locals",
232                                    (self._fid,), {})
233        return self._get_dict_proxy(did)
234
235    def _get_dict_proxy(self, did):
236        if did in self._dictcache:
237            return self._dictcache[did]
238        dp = DictProxy(self._conn, self._oid, did)
239        self._dictcache[did] = dp
240        return dp
241
242
243class CodeProxy:
244
245    def __init__(self, conn, oid, cid):
246        self._conn = conn
247        self._oid = oid
248        self._cid = cid
249
250    def __getattr__(self, name):
251        if name == "co_name":
252            return self._conn.remotecall(self._oid, "code_name",
253                                         (self._cid,), {})
254        if name == "co_filename":
255            return self._conn.remotecall(self._oid, "code_filename",
256                                         (self._cid,), {})
257
258
259class DictProxy:
260
261    def __init__(self, conn, oid, did):
262        self._conn = conn
263        self._oid = oid
264        self._did = did
265
266##    def keys(self):
267##        return self._conn.remotecall(self._oid, "dict_keys", (self._did,), {})
268
269    # 'temporary' until dict_keys is a pickleable built-in type
270    def keys(self):
271        return self._conn.remotecall(self._oid,
272                                     "dict_keys_list", (self._did,), {})
273
274    def __getitem__(self, key):
275        return self._conn.remotecall(self._oid, "dict_item",
276                                     (self._did, key), {})
277
278    def __getattr__(self, name):
279        ##print("*** Failed DictProxy.__getattr__:", name)
280        raise AttributeError(name)
281
282
283class GUIAdapter:
284
285    def __init__(self, conn, gui):
286        self.conn = conn
287        self.gui = gui
288
289    def interaction(self, message, fid, modified_info):
290        ##print("*** Interaction: (%s, %s, %s)" % (message, fid, modified_info))
291        frame = FrameProxy(self.conn, fid)
292        self.gui.interaction(message, frame, modified_info)
293
294
295class IdbProxy:
296
297    def __init__(self, conn, shell, oid):
298        self.oid = oid
299        self.conn = conn
300        self.shell = shell
301
302    def call(self, methodname, /, *args, **kwargs):
303        ##print("*** IdbProxy.call %s %s %s" % (methodname, args, kwargs))
304        value = self.conn.remotecall(self.oid, methodname, args, kwargs)
305        ##print("*** IdbProxy.call %s returns %r" % (methodname, value))
306        return value
307
308    def run(self, cmd, locals):
309        # Ignores locals on purpose!
310        seq = self.conn.asyncqueue(self.oid, "run", (cmd,), {})
311        self.shell.interp.active_seq = seq
312
313    def get_stack(self, frame, tbid):
314        # passing frame and traceback IDs, not the objects themselves
315        stack, i = self.call("get_stack", frame._fid, tbid)
316        stack = [(FrameProxy(self.conn, fid), k) for fid, k in stack]
317        return stack, i
318
319    def set_continue(self):
320        self.call("set_continue")
321
322    def set_step(self):
323        self.call("set_step")
324
325    def set_next(self, frame):
326        self.call("set_next", frame._fid)
327
328    def set_return(self, frame):
329        self.call("set_return", frame._fid)
330
331    def set_quit(self):
332        self.call("set_quit")
333
334    def set_break(self, filename, lineno):
335        msg = self.call("set_break", filename, lineno)
336        return msg
337
338    def clear_break(self, filename, lineno):
339        msg = self.call("clear_break", filename, lineno)
340        return msg
341
342    def clear_all_file_breaks(self, filename):
343        msg = self.call("clear_all_file_breaks", filename)
344        return msg
345
346def start_remote_debugger(rpcclt, pyshell):
347    """Start the subprocess debugger, initialize the debugger GUI and RPC link
348
349    Request the RPCServer start the Python subprocess debugger and link.  Set
350    up the Idle side of the split debugger by instantiating the IdbProxy,
351    debugger GUI, and debugger GUIAdapter objects and linking them together.
352
353    Register the GUIAdapter with the RPCClient to handle debugger GUI
354    interaction requests coming from the subprocess debugger via the GUIProxy.
355
356    The IdbAdapter will pass execution and environment requests coming from the
357    Idle debugger GUI to the subprocess debugger via the IdbProxy.
358
359    """
360    global idb_adap_oid
361
362    idb_adap_oid = rpcclt.remotecall("exec", "start_the_debugger",\
363                                   (gui_adap_oid,), {})
364    idb_proxy = IdbProxy(rpcclt, pyshell, idb_adap_oid)
365    gui = debugger.Debugger(pyshell, idb_proxy)
366    gui_adap = GUIAdapter(rpcclt, gui)
367    rpcclt.register(gui_adap_oid, gui_adap)
368    return gui
369
370def close_remote_debugger(rpcclt):
371    """Shut down subprocess debugger and Idle side of debugger RPC link
372
373    Request that the RPCServer shut down the subprocess debugger and link.
374    Unregister the GUIAdapter, which will cause a GC on the Idle process
375    debugger and RPC link objects.  (The second reference to the debugger GUI
376    is deleted in pyshell.close_remote_debugger().)
377
378    """
379    close_subprocess_debugger(rpcclt)
380    rpcclt.unregister(gui_adap_oid)
381
382def close_subprocess_debugger(rpcclt):
383    rpcclt.remotecall("exec", "stop_the_debugger", (idb_adap_oid,), {})
384
385def restart_subprocess_debugger(rpcclt):
386    idb_adap_oid_ret = rpcclt.remotecall("exec", "start_the_debugger",\
387                                         (gui_adap_oid,), {})
388    assert idb_adap_oid_ret == idb_adap_oid, 'Idb restarted with different oid'
389
390
391if __name__ == "__main__":
392    from unittest import main
393    main('idlelib.idle_test.test_debugger', verbosity=2, exit=False)
394