jpayne@68: """Support for remote Python debugging. jpayne@68: jpayne@68: Some ASCII art to describe the structure: jpayne@68: jpayne@68: IN PYTHON SUBPROCESS # IN IDLE PROCESS jpayne@68: # jpayne@68: # oid='gui_adapter' jpayne@68: +----------+ # +------------+ +-----+ jpayne@68: | GUIProxy |--remote#call-->| GUIAdapter |--calls-->| GUI | jpayne@68: +-----+--calls-->+----------+ # +------------+ +-----+ jpayne@68: | Idb | # / jpayne@68: +-----+<-calls--+------------+ # +----------+<--calls-/ jpayne@68: | IdbAdapter |<--remote#call--| IdbProxy | jpayne@68: +------------+ # +----------+ jpayne@68: oid='idb_adapter' # jpayne@68: jpayne@68: The purpose of the Proxy and Adapter classes is to translate certain jpayne@68: arguments and return values that cannot be transported through the RPC jpayne@68: barrier, in particular frame and traceback objects. jpayne@68: jpayne@68: """ jpayne@68: jpayne@68: import types jpayne@68: from idlelib import debugger jpayne@68: jpayne@68: debugging = 0 jpayne@68: jpayne@68: idb_adap_oid = "idb_adapter" jpayne@68: gui_adap_oid = "gui_adapter" jpayne@68: jpayne@68: #======================================= jpayne@68: # jpayne@68: # In the PYTHON subprocess: jpayne@68: jpayne@68: frametable = {} jpayne@68: dicttable = {} jpayne@68: codetable = {} jpayne@68: tracebacktable = {} jpayne@68: jpayne@68: def wrap_frame(frame): jpayne@68: fid = id(frame) jpayne@68: frametable[fid] = frame jpayne@68: return fid jpayne@68: jpayne@68: def wrap_info(info): jpayne@68: "replace info[2], a traceback instance, by its ID" jpayne@68: if info is None: jpayne@68: return None jpayne@68: else: jpayne@68: traceback = info[2] jpayne@68: assert isinstance(traceback, types.TracebackType) jpayne@68: traceback_id = id(traceback) jpayne@68: tracebacktable[traceback_id] = traceback jpayne@68: modified_info = (info[0], info[1], traceback_id) jpayne@68: return modified_info jpayne@68: jpayne@68: class GUIProxy: jpayne@68: jpayne@68: def __init__(self, conn, gui_adap_oid): jpayne@68: self.conn = conn jpayne@68: self.oid = gui_adap_oid jpayne@68: jpayne@68: def interaction(self, message, frame, info=None): jpayne@68: # calls rpc.SocketIO.remotecall() via run.MyHandler instance jpayne@68: # pass frame and traceback object IDs instead of the objects themselves jpayne@68: self.conn.remotecall(self.oid, "interaction", jpayne@68: (message, wrap_frame(frame), wrap_info(info)), jpayne@68: {}) jpayne@68: jpayne@68: class IdbAdapter: jpayne@68: jpayne@68: def __init__(self, idb): jpayne@68: self.idb = idb jpayne@68: jpayne@68: #----------called by an IdbProxy---------- jpayne@68: jpayne@68: def set_step(self): jpayne@68: self.idb.set_step() jpayne@68: jpayne@68: def set_quit(self): jpayne@68: self.idb.set_quit() jpayne@68: jpayne@68: def set_continue(self): jpayne@68: self.idb.set_continue() jpayne@68: jpayne@68: def set_next(self, fid): jpayne@68: frame = frametable[fid] jpayne@68: self.idb.set_next(frame) jpayne@68: jpayne@68: def set_return(self, fid): jpayne@68: frame = frametable[fid] jpayne@68: self.idb.set_return(frame) jpayne@68: jpayne@68: def get_stack(self, fid, tbid): jpayne@68: frame = frametable[fid] jpayne@68: if tbid is None: jpayne@68: tb = None jpayne@68: else: jpayne@68: tb = tracebacktable[tbid] jpayne@68: stack, i = self.idb.get_stack(frame, tb) jpayne@68: stack = [(wrap_frame(frame2), k) for frame2, k in stack] jpayne@68: return stack, i jpayne@68: jpayne@68: def run(self, cmd): jpayne@68: import __main__ jpayne@68: self.idb.run(cmd, __main__.__dict__) jpayne@68: jpayne@68: def set_break(self, filename, lineno): jpayne@68: msg = self.idb.set_break(filename, lineno) jpayne@68: return msg jpayne@68: jpayne@68: def clear_break(self, filename, lineno): jpayne@68: msg = self.idb.clear_break(filename, lineno) jpayne@68: return msg jpayne@68: jpayne@68: def clear_all_file_breaks(self, filename): jpayne@68: msg = self.idb.clear_all_file_breaks(filename) jpayne@68: return msg jpayne@68: jpayne@68: #----------called by a FrameProxy---------- jpayne@68: jpayne@68: def frame_attr(self, fid, name): jpayne@68: frame = frametable[fid] jpayne@68: return getattr(frame, name) jpayne@68: jpayne@68: def frame_globals(self, fid): jpayne@68: frame = frametable[fid] jpayne@68: dict = frame.f_globals jpayne@68: did = id(dict) jpayne@68: dicttable[did] = dict jpayne@68: return did jpayne@68: jpayne@68: def frame_locals(self, fid): jpayne@68: frame = frametable[fid] jpayne@68: dict = frame.f_locals jpayne@68: did = id(dict) jpayne@68: dicttable[did] = dict jpayne@68: return did jpayne@68: jpayne@68: def frame_code(self, fid): jpayne@68: frame = frametable[fid] jpayne@68: code = frame.f_code jpayne@68: cid = id(code) jpayne@68: codetable[cid] = code jpayne@68: return cid jpayne@68: jpayne@68: #----------called by a CodeProxy---------- jpayne@68: jpayne@68: def code_name(self, cid): jpayne@68: code = codetable[cid] jpayne@68: return code.co_name jpayne@68: jpayne@68: def code_filename(self, cid): jpayne@68: code = codetable[cid] jpayne@68: return code.co_filename jpayne@68: jpayne@68: #----------called by a DictProxy---------- jpayne@68: jpayne@68: def dict_keys(self, did): jpayne@68: raise NotImplementedError("dict_keys not public or pickleable") jpayne@68: ## dict = dicttable[did] jpayne@68: ## return dict.keys() jpayne@68: jpayne@68: ### Needed until dict_keys is type is finished and pickealable. jpayne@68: ### Will probably need to extend rpc.py:SocketIO._proxify at that time. jpayne@68: def dict_keys_list(self, did): jpayne@68: dict = dicttable[did] jpayne@68: return list(dict.keys()) jpayne@68: jpayne@68: def dict_item(self, did, key): jpayne@68: dict = dicttable[did] jpayne@68: value = dict[key] jpayne@68: value = repr(value) ### can't pickle module 'builtins' jpayne@68: return value jpayne@68: jpayne@68: #----------end class IdbAdapter---------- jpayne@68: jpayne@68: jpayne@68: def start_debugger(rpchandler, gui_adap_oid): jpayne@68: """Start the debugger and its RPC link in the Python subprocess jpayne@68: jpayne@68: Start the subprocess side of the split debugger and set up that side of the jpayne@68: RPC link by instantiating the GUIProxy, Idb debugger, and IdbAdapter jpayne@68: objects and linking them together. Register the IdbAdapter with the jpayne@68: RPCServer to handle RPC requests from the split debugger GUI via the jpayne@68: IdbProxy. jpayne@68: jpayne@68: """ jpayne@68: gui_proxy = GUIProxy(rpchandler, gui_adap_oid) jpayne@68: idb = debugger.Idb(gui_proxy) jpayne@68: idb_adap = IdbAdapter(idb) jpayne@68: rpchandler.register(idb_adap_oid, idb_adap) jpayne@68: return idb_adap_oid jpayne@68: jpayne@68: jpayne@68: #======================================= jpayne@68: # jpayne@68: # In the IDLE process: jpayne@68: jpayne@68: jpayne@68: class FrameProxy: jpayne@68: jpayne@68: def __init__(self, conn, fid): jpayne@68: self._conn = conn jpayne@68: self._fid = fid jpayne@68: self._oid = "idb_adapter" jpayne@68: self._dictcache = {} jpayne@68: jpayne@68: def __getattr__(self, name): jpayne@68: if name[:1] == "_": jpayne@68: raise AttributeError(name) jpayne@68: if name == "f_code": jpayne@68: return self._get_f_code() jpayne@68: if name == "f_globals": jpayne@68: return self._get_f_globals() jpayne@68: if name == "f_locals": jpayne@68: return self._get_f_locals() jpayne@68: return self._conn.remotecall(self._oid, "frame_attr", jpayne@68: (self._fid, name), {}) jpayne@68: jpayne@68: def _get_f_code(self): jpayne@68: cid = self._conn.remotecall(self._oid, "frame_code", (self._fid,), {}) jpayne@68: return CodeProxy(self._conn, self._oid, cid) jpayne@68: jpayne@68: def _get_f_globals(self): jpayne@68: did = self._conn.remotecall(self._oid, "frame_globals", jpayne@68: (self._fid,), {}) jpayne@68: return self._get_dict_proxy(did) jpayne@68: jpayne@68: def _get_f_locals(self): jpayne@68: did = self._conn.remotecall(self._oid, "frame_locals", jpayne@68: (self._fid,), {}) jpayne@68: return self._get_dict_proxy(did) jpayne@68: jpayne@68: def _get_dict_proxy(self, did): jpayne@68: if did in self._dictcache: jpayne@68: return self._dictcache[did] jpayne@68: dp = DictProxy(self._conn, self._oid, did) jpayne@68: self._dictcache[did] = dp jpayne@68: return dp jpayne@68: jpayne@68: jpayne@68: class CodeProxy: jpayne@68: jpayne@68: def __init__(self, conn, oid, cid): jpayne@68: self._conn = conn jpayne@68: self._oid = oid jpayne@68: self._cid = cid jpayne@68: jpayne@68: def __getattr__(self, name): jpayne@68: if name == "co_name": jpayne@68: return self._conn.remotecall(self._oid, "code_name", jpayne@68: (self._cid,), {}) jpayne@68: if name == "co_filename": jpayne@68: return self._conn.remotecall(self._oid, "code_filename", jpayne@68: (self._cid,), {}) jpayne@68: jpayne@68: jpayne@68: class DictProxy: jpayne@68: jpayne@68: def __init__(self, conn, oid, did): jpayne@68: self._conn = conn jpayne@68: self._oid = oid jpayne@68: self._did = did jpayne@68: jpayne@68: ## def keys(self): jpayne@68: ## return self._conn.remotecall(self._oid, "dict_keys", (self._did,), {}) jpayne@68: jpayne@68: # 'temporary' until dict_keys is a pickleable built-in type jpayne@68: def keys(self): jpayne@68: return self._conn.remotecall(self._oid, jpayne@68: "dict_keys_list", (self._did,), {}) jpayne@68: jpayne@68: def __getitem__(self, key): jpayne@68: return self._conn.remotecall(self._oid, "dict_item", jpayne@68: (self._did, key), {}) jpayne@68: jpayne@68: def __getattr__(self, name): jpayne@68: ##print("*** Failed DictProxy.__getattr__:", name) jpayne@68: raise AttributeError(name) jpayne@68: jpayne@68: jpayne@68: class GUIAdapter: jpayne@68: jpayne@68: def __init__(self, conn, gui): jpayne@68: self.conn = conn jpayne@68: self.gui = gui jpayne@68: jpayne@68: def interaction(self, message, fid, modified_info): jpayne@68: ##print("*** Interaction: (%s, %s, %s)" % (message, fid, modified_info)) jpayne@68: frame = FrameProxy(self.conn, fid) jpayne@68: self.gui.interaction(message, frame, modified_info) jpayne@68: jpayne@68: jpayne@68: class IdbProxy: jpayne@68: jpayne@68: def __init__(self, conn, shell, oid): jpayne@68: self.oid = oid jpayne@68: self.conn = conn jpayne@68: self.shell = shell jpayne@68: jpayne@68: def call(self, methodname, /, *args, **kwargs): jpayne@68: ##print("*** IdbProxy.call %s %s %s" % (methodname, args, kwargs)) jpayne@68: value = self.conn.remotecall(self.oid, methodname, args, kwargs) jpayne@68: ##print("*** IdbProxy.call %s returns %r" % (methodname, value)) jpayne@68: return value jpayne@68: jpayne@68: def run(self, cmd, locals): jpayne@68: # Ignores locals on purpose! jpayne@68: seq = self.conn.asyncqueue(self.oid, "run", (cmd,), {}) jpayne@68: self.shell.interp.active_seq = seq jpayne@68: jpayne@68: def get_stack(self, frame, tbid): jpayne@68: # passing frame and traceback IDs, not the objects themselves jpayne@68: stack, i = self.call("get_stack", frame._fid, tbid) jpayne@68: stack = [(FrameProxy(self.conn, fid), k) for fid, k in stack] jpayne@68: return stack, i jpayne@68: jpayne@68: def set_continue(self): jpayne@68: self.call("set_continue") jpayne@68: jpayne@68: def set_step(self): jpayne@68: self.call("set_step") jpayne@68: jpayne@68: def set_next(self, frame): jpayne@68: self.call("set_next", frame._fid) jpayne@68: jpayne@68: def set_return(self, frame): jpayne@68: self.call("set_return", frame._fid) jpayne@68: jpayne@68: def set_quit(self): jpayne@68: self.call("set_quit") jpayne@68: jpayne@68: def set_break(self, filename, lineno): jpayne@68: msg = self.call("set_break", filename, lineno) jpayne@68: return msg jpayne@68: jpayne@68: def clear_break(self, filename, lineno): jpayne@68: msg = self.call("clear_break", filename, lineno) jpayne@68: return msg jpayne@68: jpayne@68: def clear_all_file_breaks(self, filename): jpayne@68: msg = self.call("clear_all_file_breaks", filename) jpayne@68: return msg jpayne@68: jpayne@68: def start_remote_debugger(rpcclt, pyshell): jpayne@68: """Start the subprocess debugger, initialize the debugger GUI and RPC link jpayne@68: jpayne@68: Request the RPCServer start the Python subprocess debugger and link. Set jpayne@68: up the Idle side of the split debugger by instantiating the IdbProxy, jpayne@68: debugger GUI, and debugger GUIAdapter objects and linking them together. jpayne@68: jpayne@68: Register the GUIAdapter with the RPCClient to handle debugger GUI jpayne@68: interaction requests coming from the subprocess debugger via the GUIProxy. jpayne@68: jpayne@68: The IdbAdapter will pass execution and environment requests coming from the jpayne@68: Idle debugger GUI to the subprocess debugger via the IdbProxy. jpayne@68: jpayne@68: """ jpayne@68: global idb_adap_oid jpayne@68: jpayne@68: idb_adap_oid = rpcclt.remotecall("exec", "start_the_debugger",\ jpayne@68: (gui_adap_oid,), {}) jpayne@68: idb_proxy = IdbProxy(rpcclt, pyshell, idb_adap_oid) jpayne@68: gui = debugger.Debugger(pyshell, idb_proxy) jpayne@68: gui_adap = GUIAdapter(rpcclt, gui) jpayne@68: rpcclt.register(gui_adap_oid, gui_adap) jpayne@68: return gui jpayne@68: jpayne@68: def close_remote_debugger(rpcclt): jpayne@68: """Shut down subprocess debugger and Idle side of debugger RPC link jpayne@68: jpayne@68: Request that the RPCServer shut down the subprocess debugger and link. jpayne@68: Unregister the GUIAdapter, which will cause a GC on the Idle process jpayne@68: debugger and RPC link objects. (The second reference to the debugger GUI jpayne@68: is deleted in pyshell.close_remote_debugger().) jpayne@68: jpayne@68: """ jpayne@68: close_subprocess_debugger(rpcclt) jpayne@68: rpcclt.unregister(gui_adap_oid) jpayne@68: jpayne@68: def close_subprocess_debugger(rpcclt): jpayne@68: rpcclt.remotecall("exec", "stop_the_debugger", (idb_adap_oid,), {}) jpayne@68: jpayne@68: def restart_subprocess_debugger(rpcclt): jpayne@68: idb_adap_oid_ret = rpcclt.remotecall("exec", "start_the_debugger",\ jpayne@68: (gui_adap_oid,), {}) jpayne@68: assert idb_adap_oid_ret == idb_adap_oid, 'Idb restarted with different oid' jpayne@68: jpayne@68: jpayne@68: if __name__ == "__main__": jpayne@68: from unittest import main jpayne@68: main('idlelib.idle_test.test_debugger', verbosity=2, exit=False)