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 rpc
25from idlelib import Debugger
26
27debugging = 0
28
29idb_adap_oid = "idb_adapter"
30gui_adap_oid = "gui_adapter"
31
32#=======================================
33#
34# In the PYTHON subprocess:
35
36frametable = {}
37dicttable = {}
38codetable = {}
39tracebacktable = {}
40
41def wrap_frame(frame):
42    fid = id(frame)
43    frametable[fid] = frame
44    return fid
45
46def wrap_info(info):
47    "replace info[2], a traceback instance, by its ID"
48    if info is None:
49        return None
50    else:
51        traceback = info[2]
52        assert isinstance(traceback, types.TracebackType)
53        traceback_id = id(traceback)
54        tracebacktable[traceback_id] = traceback
55        modified_info = (info[0], info[1], traceback_id)
56        return modified_info
57
58class GUIProxy:
59
60    def __init__(self, conn, gui_adap_oid):
61        self.conn = conn
62        self.oid = gui_adap_oid
63
64    def interaction(self, message, frame, info=None):
65        # calls rpc.SocketIO.remotecall() via run.MyHandler instance
66        # pass frame and traceback object IDs instead of the objects themselves
67        self.conn.remotecall(self.oid, "interaction",
68                             (message, wrap_frame(frame), wrap_info(info)),
69                             {})
70
71class IdbAdapter:
72
73    def __init__(self, idb):
74        self.idb = idb
75
76    #----------called by an IdbProxy----------
77
78    def set_step(self):
79        self.idb.set_step()
80
81    def set_quit(self):
82        self.idb.set_quit()
83
84    def set_continue(self):
85        self.idb.set_continue()
86
87    def set_next(self, fid):
88        frame = frametable[fid]
89        self.idb.set_next(frame)
90
91    def set_return(self, fid):
92        frame = frametable[fid]
93        self.idb.set_return(frame)
94
95    def get_stack(self, fid, tbid):
96        ##print >>sys.__stderr__, "get_stack(%r, %r)" % (fid, tbid)
97        frame = frametable[fid]
98        if tbid is None:
99            tb = None
100        else:
101            tb = tracebacktable[tbid]
102        stack, i = self.idb.get_stack(frame, tb)
103        ##print >>sys.__stderr__, "get_stack() ->", stack
104        stack = [(wrap_frame(frame), k) for frame, k in stack]
105        ##print >>sys.__stderr__, "get_stack() ->", stack
106        return stack, i
107
108    def run(self, cmd):
109        import __main__
110        self.idb.run(cmd, __main__.__dict__)
111
112    def set_break(self, filename, lineno):
113        msg = self.idb.set_break(filename, lineno)
114        return msg
115
116    def clear_break(self, filename, lineno):
117        msg = self.idb.clear_break(filename, lineno)
118        return msg
119
120    def clear_all_file_breaks(self, filename):
121        msg = self.idb.clear_all_file_breaks(filename)
122        return msg
123
124    #----------called by a FrameProxy----------
125
126    def frame_attr(self, fid, name):
127        frame = frametable[fid]
128        return getattr(frame, name)
129
130    def frame_globals(self, fid):
131        frame = frametable[fid]
132        dict = frame.f_globals
133        did = id(dict)
134        dicttable[did] = dict
135        return did
136
137    def frame_locals(self, fid):
138        frame = frametable[fid]
139        dict = frame.f_locals
140        did = id(dict)
141        dicttable[did] = dict
142        return did
143
144    def frame_code(self, fid):
145        frame = frametable[fid]
146        code = frame.f_code
147        cid = id(code)
148        codetable[cid] = code
149        return cid
150
151    #----------called by a CodeProxy----------
152
153    def code_name(self, cid):
154        code = codetable[cid]
155        return code.co_name
156
157    def code_filename(self, cid):
158        code = codetable[cid]
159        return code.co_filename
160
161    #----------called by a DictProxy----------
162
163    def dict_keys(self, did):
164        dict = dicttable[did]
165        return dict.keys()
166
167    def dict_item(self, did, key):
168        dict = dicttable[did]
169        value = dict[key]
170        value = repr(value)
171        return value
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    def __getitem__(self, key):
267        return self._conn.remotecall(self._oid, "dict_item",
268                                     (self._did, key), {})
269
270    def __getattr__(self, name):
271        ##print >>sys.__stderr__, "failed DictProxy.__getattr__:", name
272        raise AttributeError, name
273
274
275class GUIAdapter:
276
277    def __init__(self, conn, gui):
278        self.conn = conn
279        self.gui = gui
280
281    def interaction(self, message, fid, modified_info):
282        ##print "interaction: (%s, %s, %s)" % (message, fid, modified_info)
283        frame = FrameProxy(self.conn, fid)
284        self.gui.interaction(message, frame, modified_info)
285
286
287class IdbProxy:
288
289    def __init__(self, conn, shell, oid):
290        self.oid = oid
291        self.conn = conn
292        self.shell = shell
293
294    def call(self, methodname, *args, **kwargs):
295        ##print "**IdbProxy.call %s %s %s" % (methodname, args, kwargs)
296        value = self.conn.remotecall(self.oid, methodname, args, kwargs)
297        ##print "**IdbProxy.call %s returns %r" % (methodname, value)
298        return value
299
300    def run(self, cmd, locals):
301        # Ignores locals on purpose!
302        seq = self.conn.asyncqueue(self.oid, "run", (cmd,), {})
303        self.shell.interp.active_seq = seq
304
305    def get_stack(self, frame, tbid):
306        # passing frame and traceback IDs, not the objects themselves
307        stack, i = self.call("get_stack", frame._fid, tbid)
308        stack = [(FrameProxy(self.conn, fid), k) for fid, k in stack]
309        return stack, i
310
311    def set_continue(self):
312        self.call("set_continue")
313
314    def set_step(self):
315        self.call("set_step")
316
317    def set_next(self, frame):
318        self.call("set_next", frame._fid)
319
320    def set_return(self, frame):
321        self.call("set_return", frame._fid)
322
323    def set_quit(self):
324        self.call("set_quit")
325
326    def set_break(self, filename, lineno):
327        msg = self.call("set_break", filename, lineno)
328        return msg
329
330    def clear_break(self, filename, lineno):
331        msg = self.call("clear_break", filename, lineno)
332        return msg
333
334    def clear_all_file_breaks(self, filename):
335        msg = self.call("clear_all_file_breaks", filename)
336        return msg
337
338def start_remote_debugger(rpcclt, pyshell):
339    """Start the subprocess debugger, initialize the debugger GUI and RPC link
340
341    Request the RPCServer start the Python subprocess debugger and link.  Set
342    up the Idle side of the split debugger by instantiating the IdbProxy,
343    debugger GUI, and debugger GUIAdapter objects and linking them together.
344
345    Register the GUIAdapter with the RPCClient to handle debugger GUI
346    interaction requests coming from the subprocess debugger via the GUIProxy.
347
348    The IdbAdapter will pass execution and environment requests coming from the
349    Idle debugger GUI to the subprocess debugger via the IdbProxy.
350
351    """
352    global idb_adap_oid
353
354    idb_adap_oid = rpcclt.remotecall("exec", "start_the_debugger",\
355                                   (gui_adap_oid,), {})
356    idb_proxy = IdbProxy(rpcclt, pyshell, idb_adap_oid)
357    gui = Debugger.Debugger(pyshell, idb_proxy)
358    gui_adap = GUIAdapter(rpcclt, gui)
359    rpcclt.register(gui_adap_oid, gui_adap)
360    return gui
361
362def close_remote_debugger(rpcclt):
363    """Shut down subprocess debugger and Idle side of debugger RPC link
364
365    Request that the RPCServer shut down the subprocess debugger and link.
366    Unregister the GUIAdapter, which will cause a GC on the Idle process
367    debugger and RPC link objects.  (The second reference to the debugger GUI
368    is deleted in PyShell.close_remote_debugger().)
369
370    """
371    close_subprocess_debugger(rpcclt)
372    rpcclt.unregister(gui_adap_oid)
373
374def close_subprocess_debugger(rpcclt):
375    rpcclt.remotecall("exec", "stop_the_debugger", (idb_adap_oid,), {})
376
377def restart_subprocess_debugger(rpcclt):
378    idb_adap_oid_ret = rpcclt.remotecall("exec", "start_the_debugger",\
379                                         (gui_adap_oid,), {})
380    assert idb_adap_oid_ret == idb_adap_oid, 'Idb restarted with different oid'
381