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