Coverage for debugger_r.py: 33%

218 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 13:22 -0700

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 1b

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] 1b

172 value = dict[key] 1b

173 value = reprlib.repr(value) ### can't pickle module 'builtins' 1b

174 return value 1b

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__": 391 ↛ 392line 391 didn't jump to line 392, because the condition on line 391 was never true

392 from unittest import main 

393 main('idlelib.idle_test.test_debugger_r', verbosity=2, exit=False)