Home | History | Annotate | Download | only in idlelib
      1 """Support for remote Python debugging.
      2 
      3 Some 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 
     17 The purpose of the Proxy and Adapter classes is to translate certain
     18 arguments and return values that cannot be transported through the RPC
     19 barrier, in particular frame and traceback objects.
     20 
     21 """
     22 
     23 import types
     24 from idlelib import debugger
     25 
     26 debugging = 0
     27 
     28 idb_adap_oid = "idb_adapter"
     29 gui_adap_oid = "gui_adapter"
     30 
     31 #=======================================
     32 #
     33 # In the PYTHON subprocess:
     34 
     35 frametable = {}
     36 dicttable = {}
     37 codetable = {}
     38 tracebacktable = {}
     39 
     40 def wrap_frame(frame):
     41     fid = id(frame)
     42     frametable[fid] = frame
     43     return fid
     44 
     45 def 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 
     57 class 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 
     70 class 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 
    179 def 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 
    201 class 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 
    243 class 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 
    259 class 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 
    283 class 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 
    295 class 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 
    346 def 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 
    370 def 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 
    382 def close_subprocess_debugger(rpcclt):
    383     rpcclt.remotecall("exec", "stop_the_debugger", (idb_adap_oid,), {})
    384 
    385 def 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 
    391 if __name__ == "__main__":
    392     from unittest import main
    393     main('idlelib.idle_test.test_debugger', verbosity=2, exit=False)
    394