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 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 = reprlib.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_r', verbosity=2, exit=False) 394