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        ##print >>sys.__stderr__, "get_stack(%r, %r)" % (fid, tbid)
96        frame = frametable[fid]
97        if tbid is None:
98            tb = None
99        else:
100            tb = tracebacktable[tbid]
101        stack, i = self.idb.get_stack(frame, tb)
102        ##print >>sys.__stderr__, "get_stack() ->", stack
103        stack = [(wrap_frame(frame2), k) for frame2, k in stack]
104        ##print >>sys.__stderr__, "get_stack() ->", stack
105        return stack, i
106
107    def run(self, cmd):
108        import __main__
109        self.idb.run(cmd, __main__.__dict__)
110
111    def set_break(self, filename, lineno):
112        msg = self.idb.set_break(filename, lineno)
113        return msg
114
115    def clear_break(self, filename, lineno):
116        msg = self.idb.clear_break(filename, lineno)
117        return msg
118
119    def clear_all_file_breaks(self, filename):
120        msg = self.idb.clear_all_file_breaks(filename)
121        return msg
122
123    #----------called by a FrameProxy----------
124
125    def frame_attr(self, fid, name):
126        frame = frametable[fid]
127        return getattr(frame, name)
128
129    def frame_globals(self, fid):
130        frame = frametable[fid]
131        dict = frame.f_globals
132        did = id(dict)
133        dicttable[did] = dict
134        return did
135
136    def frame_locals(self, fid):
137        frame = frametable[fid]
138        dict = frame.f_locals
139        did = id(dict)
140        dicttable[did] = dict
141        return did
142
143    def frame_code(self, fid):
144        frame = frametable[fid]
145        code = frame.f_code
146        cid = id(code)
147        codetable[cid] = code
148        return cid
149
150    #----------called by a CodeProxy----------
151
152    def code_name(self, cid):
153        code = codetable[cid]
154        return code.co_name
155
156    def code_filename(self, cid):
157        code = codetable[cid]
158        return code.co_filename
159
160    #----------called by a DictProxy----------
161
162    def dict_keys(self, did):
163        dict = dicttable[did]
164        return dict.keys()
165
166    def dict_item(self, did, key):
167        dict = dicttable[did]
168        value = dict[key]
169        value = repr(value)
170        return value
171
172#----------end class IdbAdapter----------
173
174
175def start_debugger(rpchandler, gui_adap_oid):
176    """Start the debugger and its RPC link in the Python subprocess
177
178    Start the subprocess side of the split debugger and set up that side of the
179    RPC link by instantiating the GUIProxy, Idb debugger, and IdbAdapter
180    objects and linking them together.  Register the IdbAdapter with the
181    RPCServer to handle RPC requests from the split debugger GUI via the
182    IdbProxy.
183
184    """
185    gui_proxy = GUIProxy(rpchandler, gui_adap_oid)
186    idb = Debugger.Idb(gui_proxy)
187    idb_adap = IdbAdapter(idb)
188    rpchandler.register(idb_adap_oid, idb_adap)
189    return idb_adap_oid
190
191
192#=======================================
193#
194# In the IDLE process:
195
196
197class FrameProxy:
198
199    def __init__(self, conn, fid):
200        self._conn = conn
201        self._fid = fid
202        self._oid = "idb_adapter"
203        self._dictcache = {}
204
205    def __getattr__(self, name):
206        if name[:1] == "_":
207            raise AttributeError, name
208        if name == "f_code":
209            return self._get_f_code()
210        if name == "f_globals":
211            return self._get_f_globals()
212        if name == "f_locals":
213            return self._get_f_locals()
214        return self._conn.remotecall(self._oid, "frame_attr",
215                                     (self._fid, name), {})
216
217    def _get_f_code(self):
218        cid = self._conn.remotecall(self._oid, "frame_code", (self._fid,), {})
219        return CodeProxy(self._conn, self._oid, cid)
220
221    def _get_f_globals(self):
222        did = self._conn.remotecall(self._oid, "frame_globals",
223                                    (self._fid,), {})
224        return self._get_dict_proxy(did)
225
226    def _get_f_locals(self):
227        did = self._conn.remotecall(self._oid, "frame_locals",
228                                    (self._fid,), {})
229        return self._get_dict_proxy(did)
230
231    def _get_dict_proxy(self, did):
232        if did in self._dictcache:
233            return self._dictcache[did]
234        dp = DictProxy(self._conn, self._oid, did)
235        self._dictcache[did] = dp
236        return dp
237
238
239class CodeProxy:
240
241    def __init__(self, conn, oid, cid):
242        self._conn = conn
243        self._oid = oid
244        self._cid = cid
245
246    def __getattr__(self, name):
247        if name == "co_name":
248            return self._conn.remotecall(self._oid, "code_name",
249                                         (self._cid,), {})
250        if name == "co_filename":
251            return self._conn.remotecall(self._oid, "code_filename",
252                                         (self._cid,), {})
253
254
255class DictProxy:
256
257    def __init__(self, conn, oid, did):
258        self._conn = conn
259        self._oid = oid
260        self._did = did
261
262    def keys(self):
263        return self._conn.remotecall(self._oid, "dict_keys", (self._did,), {})
264
265    def __getitem__(self, key):
266        return self._conn.remotecall(self._oid, "dict_item",
267                                     (self._did, key), {})
268
269    def __getattr__(self, name):
270        ##print >>sys.__stderr__, "failed DictProxy.__getattr__:", name
271        raise AttributeError, name
272
273
274class GUIAdapter:
275
276    def __init__(self, conn, gui):
277        self.conn = conn
278        self.gui = gui
279
280    def interaction(self, message, fid, modified_info):
281        ##print "interaction: (%s, %s, %s)" % (message, fid, modified_info)
282        frame = FrameProxy(self.conn, fid)
283        self.gui.interaction(message, frame, modified_info)
284
285
286class IdbProxy:
287
288    def __init__(self, conn, shell, oid):
289        self.oid = oid
290        self.conn = conn
291        self.shell = shell
292
293    def call(self, methodname, *args, **kwargs):
294        ##print "**IdbProxy.call %s %s %s" % (methodname, args, kwargs)
295        value = self.conn.remotecall(self.oid, methodname, args, kwargs)
296        ##print "**IdbProxy.call %s returns %r" % (methodname, value)
297        return value
298
299    def run(self, cmd, locals):
300        # Ignores locals on purpose!
301        seq = self.conn.asyncqueue(self.oid, "run", (cmd,), {})
302        self.shell.interp.active_seq = seq
303
304    def get_stack(self, frame, tbid):
305        # passing frame and traceback IDs, not the objects themselves
306        stack, i = self.call("get_stack", frame._fid, tbid)
307        stack = [(FrameProxy(self.conn, fid), k) for fid, k in stack]
308        return stack, i
309
310    def set_continue(self):
311        self.call("set_continue")
312
313    def set_step(self):
314        self.call("set_step")
315
316    def set_next(self, frame):
317        self.call("set_next", frame._fid)
318
319    def set_return(self, frame):
320        self.call("set_return", frame._fid)
321
322    def set_quit(self):
323        self.call("set_quit")
324
325    def set_break(self, filename, lineno):
326        msg = self.call("set_break", filename, lineno)
327        return msg
328
329    def clear_break(self, filename, lineno):
330        msg = self.call("clear_break", filename, lineno)
331        return msg
332
333    def clear_all_file_breaks(self, filename):
334        msg = self.call("clear_all_file_breaks", filename)
335        return msg
336
337def start_remote_debugger(rpcclt, pyshell):
338    """Start the subprocess debugger, initialize the debugger GUI and RPC link
339
340    Request the RPCServer start the Python subprocess debugger and link.  Set
341    up the Idle side of the split debugger by instantiating the IdbProxy,
342    debugger GUI, and debugger GUIAdapter objects and linking them together.
343
344    Register the GUIAdapter with the RPCClient to handle debugger GUI
345    interaction requests coming from the subprocess debugger via the GUIProxy.
346
347    The IdbAdapter will pass execution and environment requests coming from the
348    Idle debugger GUI to the subprocess debugger via the IdbProxy.
349
350    """
351    global idb_adap_oid
352
353    idb_adap_oid = rpcclt.remotecall("exec", "start_the_debugger",\
354                                   (gui_adap_oid,), {})
355    idb_proxy = IdbProxy(rpcclt, pyshell, idb_adap_oid)
356    gui = Debugger.Debugger(pyshell, idb_proxy)
357    gui_adap = GUIAdapter(rpcclt, gui)
358    rpcclt.register(gui_adap_oid, gui_adap)
359    return gui
360
361def close_remote_debugger(rpcclt):
362    """Shut down subprocess debugger and Idle side of debugger RPC link
363
364    Request that the RPCServer shut down the subprocess debugger and link.
365    Unregister the GUIAdapter, which will cause a GC on the Idle process
366    debugger and RPC link objects.  (The second reference to the debugger GUI
367    is deleted in PyShell.close_remote_debugger().)
368
369    """
370    close_subprocess_debugger(rpcclt)
371    rpcclt.unregister(gui_adap_oid)
372
373def close_subprocess_debugger(rpcclt):
374    rpcclt.remotecall("exec", "stop_the_debugger", (idb_adap_oid,), {})
375
376def restart_subprocess_debugger(rpcclt):
377    idb_adap_oid_ret = rpcclt.remotecall("exec", "start_the_debugger",\
378                                         (gui_adap_oid,), {})
379    assert idb_adap_oid_ret == idb_adap_oid, 'Idb restarted with different oid'
380