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