Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2#@+leo-ver=5-thin 

3#@+node:ekr.20130302121602.10208: * @file leoDebugger.py 

4#@@first 

5 

6# Disable all mypy errors. 

7# type:ignore 

8 

9#@+<< leoDebugger.py docstring >> 

10#@+node:ekr.20181006100710.1: ** << leoDebugger.py docstring >> 

11""" 

12Leo's integrated debugger supports the xdb and various db-* commands, 

13corresponding to the pdb commands: 

14 

15**Commands** 

16 

17xdb: Start a Leo's integrated debugger. 

18The presently-selected node should be within an @<file> tree. 

19 

20Now you are ready to execute all the db-* commands. You can execute these 

21commands from the minibuffer, or from the the Debug pane. The following 

22correspond to the pdb commands:: 

23 

24 db-b: Set a breakpoint at the line shown in Leo. It should be an executable line. 

25 db-c: Continue, that is, run until the next breakpoint. 

26 db-h: Print the help message (in the console, for now) 

27 db-l: List a few lines around present location. 

28 db-n: Execute the next line. 

29 db-q: End the debugger. 

30 db-r: Return from the present function/method. 

31 db-s: Step into the next line. 

32 db-w: Print the stack. 

33 

34There are two additional commands:: 

35 

36 db-again: Run the previous db-command. 

37 db-input: Prompt for any pdb command, then execute it. 

38 

39The db-input command allows you can enter any pdb command at all. For 

40example: "print c". But you don't have to run these commands from the 

41minibuffer, as discussed next. 

42 

43**Setting breakpoints in the gutter** 

44 

45When @bool use_gutter = True, Leo shows a border in the body pane. By 

46default, the line-numbering.py plugin is enabled, and if so, the gutter 

47shows correct line number in the external file. 

48 

49After executing the xdb command, you can set a breakpoint on any executable 

50line by clicking in the gutter to the right of the line number. You can 

51also set a breakpoint any time the debugger is stopped. 

52 

53Using the gutter is optional. You can also set breakpoints with the db-b 

54command or by typing "d line-number" or "d file-name:line-number" using the 

55db-input command, or by using the Debug pane (see below) 

56 

57**The Debug pane** 

58 

59The xdb_pane.py plugin creates the Debug pane in the Log pane. The pane 

60contains buttons for all the commands listed above. In addition, there is 

61an input area in which you can enter pdb commands. This is a bit easier to 

62use than the db-input command. 

63 

64**Summary** 

65 

66The xdb and db-* commands are always available. They correspond to pdb 

67commands. 

68 

69When xdb is active you may set breakpoints by clicking in the gutter next 

70to any executable line. The line_numbering plugin must be enabled and @bool 

71use_gutter must be True. 

72 

73The xdb_pane plugin creates the Debug pane in the Log window. 

74""" 

75#@-<< leoDebugger.py docstring >> 

76#@+<< leoDebugger.py imports >> 

77#@+node:ekr.20181006100604.1: ** << leoDebugger.py imports >> 

78import bdb 

79import queue 

80import os 

81import pdb 

82import re 

83import sys 

84import threading 

85from leo.core import leoGlobals as g 

86#@-<< leoDebugger.py imports >> 

87#@+others 

88#@+node:ekr.20180701050839.5: ** class Xdb (pdb.Pdb, threading.Thread) 

89class Xdb(pdb.Pdb, threading.Thread): 

90 """ 

91 An debugger, a subclass of Pdb, that runs in a separate thread without 

92 hanging Leo. Only one debugger, g.app.xdb, can be active at any time. 

93 

94 A singleton listener method, g.app,xdb_timer, runs in the main Leo 

95 thread. The listener runs until Leo exists. 

96 

97 Two Queues communicate between the threads: 

98 

99 - xdb.qc contains commands from the main thread to this thread. 

100 All xdb/pdb input comes from this queue. 

101 

102 - xdb.qr contains requests from the xdb thread to the main thread. 

103 All xdb/pdb output goes to this queue. 

104 

105 Settings 

106 -------- 

107 

108 - @bool use_xdb_pane_output_area: when True, all debugger output is sent 

109 to an output area in the Debug pane. 

110 

111 @bool use_gutter: when True, line numbers appear to the left of 

112 the body pane. Clicking to the left of the gutter toggles breakpoints 

113 when xdb is active. 

114 """ 

115 #@+others 

116 #@+node:ekr.20180701050839.4: *3* class QueueStdin (obj) 

117 class QueueStdin: 

118 """ 

119 A replacement for Python's stdin class containing only readline(). 

120 """ 

121 

122 def __init__(self, qc): 

123 self.qc = qc 

124 

125 def readline(self): 

126 """Return the next line from the qc channel.""" 

127 s = self.qc.get() # blocks 

128 if 1: 

129 # Just echo. 

130 print(s.rstrip()) 

131 else: 

132 # Use the output area. 

133 xdb = getattr(g.app, 'xdb') 

134 if xdb: 

135 xdb.write(s) 

136 else: 

137 print(s) 

138 return s 

139 #@+node:ekr.20181003020344.1: *3* class QueueStdout (obj) 

140 class QueueStdout: 

141 """ 

142 A replacement for Python's stdout class containing only write(). 

143 """ 

144 

145 def __init__(self, qr): 

146 self.qr = qr 

147 

148 def flush(self): 

149 pass 

150 

151 def write(self, s): 

152 """Write s to the qr channel""" 

153 self.qr.put(['put-stdout', s]) 

154 #@+node:ekr.20181006160108.1: *3* xdb.__init__ 

155 def __init__(self, path=None): 

156 

157 self.qc = queue.Queue() # The command queue. 

158 self.qr = queue.Queue() # The request queue. 

159 stdin_q = self.QueueStdin(qc=self.qc) 

160 stdout_q = self.QueueStdout(qr=self.qr) 

161 # Start the singleton listener, in the main Leo thread. 

162 timer = getattr(g.app, 'xdb_timer', None) 

163 if timer: 

164 self.timer = timer 

165 else: 

166 self.timer = g.IdleTime(listener, delay=0) 

167 self.timer.start() 

168 # Init the base classes. 

169 threading.Thread.__init__(self) 

170 super().__init__( 

171 stdin=stdin_q, 

172 stdout=stdout_q, 

173 readrc=False, 

174 # Don't read a .rc file. 

175 ) 

176 sys.stdout = stdout_q 

177 self.daemon = True 

178 self.path = path 

179 self.prompt = '(xdb) ' 

180 self.saved_frame = None 

181 self.saved_traceback = None 

182 #@+node:ekr.20181002053718.1: *3* Overrides 

183 #@+node:ekr.20190108040329.1: *4* xdb.checkline (overrides Pdb) 

184 def checkline(self, path, n): 

185 # pylint: disable=arguments-differ 

186 # filename, lineno 

187 try: 

188 return pdb.Pdb.checkline(self, path, n) 

189 except AttributeError: 

190 return False 

191 except Exception: 

192 g.es_exception() 

193 return False 

194 #@+node:ekr.20181002061627.1: *4* xdb.cmdloop (overrides Cmd) 

195 def cmdloop(self, intro=None): 

196 """Override Cmd.cmdloop.""" 

197 assert not intro, repr(intro) 

198 stop = None 

199 while not stop: 

200 if self.cmdqueue: 

201 # Pdb.precmd sets cmdqueue. 

202 line = self.cmdqueue.pop(0) 

203 else: 

204 self.stdout.write(self.prompt) 

205 self.stdout.flush() 

206 line = self.stdin.readline() 

207 # QueueStdin.readline. 

208 # Get the input from Leo's main thread. 

209 line = line.rstrip('\r\n') if line else 'EOF' 

210 line = self.precmd(line) 

211 # Pdb.precmd. 

212 stop = self.onecmd(line) 

213 # Pdb.onecmd. 

214 # Show the line in Leo. 

215 if stop: 

216 self.select_line(self.saved_frame, self.saved_traceback) 

217 #@+node:ekr.20180701050839.6: *4* xdb.do_clear (overides Pdb) 

218 def do_clear(self, arg=None): 

219 """cl(ear) filename:lineno\ncl(ear) [bpnumber [bpnumber...]] 

220 With a space separated list of breakpoint numbers, clear 

221 those breakpoints. Without argument, clear all breaks (but 

222 first ask confirmation). With a filename:lineno argument, 

223 clear all breaks at that line in that file. 

224 """ 

225 # Same as pdb.do_clear except uses self.stdin.readline (as it should). 

226 if not arg: 

227 bplist = [bp for bp in bdb.Breakpoint.bpbynumber if bp] 

228 if bplist: 

229 print('Clear all breakpoints?') 

230 reply = self.stdin.readline().strip().lower() 

231 if reply in ('y', 'yes'): 

232 self.clear_all_breaks() 

233 for bp in bplist: 

234 self.message(f"Deleted {bp}") 

235 return 

236 if ':' in arg: 

237 # Make sure it works for "clear C:\foo\bar.py:12" 

238 i = arg.rfind(':') 

239 filename = arg[:i] 

240 arg = arg[i + 1 :] 

241 try: 

242 lineno = int(arg) 

243 except ValueError: 

244 err = f"Invalid line number ({arg})" 

245 else: 

246 bplist = self.get_breaks(filename, lineno) 

247 err = self.clear_break(filename, lineno) 

248 if err: 

249 self.error(err) 

250 else: 

251 for bp in bplist: 

252 self.message(f"Deleted {bp}") 

253 return 

254 numberlist = arg.split() 

255 for i in numberlist: 

256 try: 

257 bp = self.get_bpbynumber(i) 

258 except ValueError as err: 

259 self.error(err) 

260 else: 

261 self.clear_bpbynumber(i) 

262 self.message(f"Deleted {bp}") 

263 

264 do_cl = do_clear # 'c' is already an abbreviation for 'continue' 

265 

266 # complete_clear = self._complete_location 

267 # complete_cl = self._complete_location 

268 #@+node:ekr.20180701050839.7: *4* xdb.do_quit (overrides Pdb) 

269 def do_quit(self, arg=None): 

270 """q(uit)\nexit 

271 Quit from the debugger. The program being executed is aborted. 

272 """ 

273 self.write('End xdb\n') 

274 self._user_requested_quit = True 

275 self.set_quit() 

276 self.qr.put(['stop-xdb']) 

277 # Kill xdb *after* all other messages have been sent. 

278 return 1 

279 

280 do_q = do_quit 

281 do_exit = do_quit 

282 #@+node:ekr.20180701050839.8: *4* xdb.interaction (overrides Pdb) 

283 def interaction(self, frame, traceback): 

284 """Override.""" 

285 self.saved_frame = frame 

286 self.saved_traceback = traceback 

287 self.select_line(frame, traceback) 

288 pdb.Pdb.interaction(self, frame, traceback) 

289 # Call the base class method. 

290 #@+node:ekr.20180701050839.10: *4* xdb.set_continue (overrides Bdb) 

291 def set_continue(self): 

292 """ override Bdb.set_continue""" 

293 # Don't stop except at breakpoints or when finished 

294 self._set_stopinfo(self.botframe, None, -1) 

295 if not self.breaks: 

296 # no breakpoints; run without debugger overhead. 

297 # Do *not call kill(): only db-kill and db-q do that. 

298 # self.write('clearing sys.settrace\n') 

299 sys.settrace(None) 

300 frame = sys._getframe().f_back 

301 while frame and frame is not self.botframe: 

302 del frame.f_trace 

303 frame = frame.f_back 

304 #@+node:ekr.20181006052604.1: *3* xdb.has_breakpoint & has_breakpoints 

305 def has_breakpoint(self, filename, lineno): 

306 """Return True if there is a breakpoint at the given file and line.""" 

307 filename = self.canonic(filename) 

308 aList = self.breaks.get(filename) or [] 

309 return lineno in aList 

310 

311 def has_breakpoints(self): 

312 """Return True if there are any breakpoints.""" 

313 return self.breaks 

314 #@+node:ekr.20181002094126.1: *3* xdb.run 

315 def run(self): 

316 """The thread's run method: called via start.""" 

317 # pylint: disable=arguments-differ 

318 from leo.core.leoQt import QtCore 

319 QtCore.pyqtRemoveInputHook() # From g.pdb 

320 if self.path: 

321 self.run_path(self.path) 

322 else: 

323 self.set_trace() 

324 #@+node:ekr.20180701090439.1: *3* xdb.run_path 

325 def run_path(self, path): 

326 """Begin execution of the python file.""" 

327 source = g.readFileIntoUnicodeString(path) 

328 fn = g.shortFileName(path) 

329 try: 

330 code = compile(source, fn, 'exec') 

331 except Exception: 

332 g.es_exception() 

333 g.trace('can not compile', path) 

334 return 

335 self.reset() 

336 sys.settrace(self.trace_dispatch) 

337 try: 

338 self.quitting = False 

339 exec(code, {}, {}) 

340 except bdb.BdbQuit: 

341 if not self.quitting: 

342 self.do_quit() 

343 finally: 

344 self.quitting = True 

345 sys.settrace(None) 

346 #@+node:ekr.20180701151233.1: *3* xdb.select_line 

347 def select_line(self, frame, traceback): 

348 """Select the given line in Leo.""" 

349 stack, curindex = self.get_stack(frame, traceback) 

350 frame, lineno = stack[curindex] 

351 filename = frame.f_code.co_filename 

352 self.qr.put(['select-line', lineno, filename]) 

353 # Select the line in the main thread. 

354 # xdb.show_line finalizes the file name. 

355 #@+node:ekr.20181007044254.1: *3* xdb.write 

356 def write(self, s): 

357 """Write s to the output stream.""" 

358 self.qr.put(['put-stdout', s]) 

359 #@-others 

360#@+node:ekr.20181007063214.1: ** top-level functions 

361# These functions run in Leo's main thread. 

362#@+node:ekr.20181004120344.1: *3* function: get_gnx_from_file 

363def get_gnx_from_file(file_s, p, path): 

364 """Set p's gnx from the @file node in the derived file.""" 

365 pat = re.compile(r'^#@\+node:(.*): \*+ @file (.+)$') 

366 for line in g.splitLines(file_s): 

367 m = pat.match(line) 

368 if m: 

369 gnx, path2 = m.group(1), m.group(2) 

370 path2 = path2.replace('\\', '/') 

371 p.v.fileIndex = gnx 

372 if path == path2: 

373 return True 

374 g.trace(f"Not found: @+node for {path}") 

375 g.trace('Reverting to @auto') 

376 return False 

377#@+node:ekr.20180701050839.3: *3* function: listener 

378def listener(timer): 

379 """ 

380 Listen, at idle-time, in Leo's main thread, for data on the qr channel. 

381 

382 This is a singleton timer, created by the xdb command. 

383 """ 

384 if g.app.killed: 

385 return 

386 xdb = getattr(g.app, 'xdb', None) 

387 if not xdb: 

388 return 

389 c = g.app.log.c 

390 xpd_pane = getattr(c, 'xpd_pane', None) 

391 kill = False 

392 while not xdb.qr.empty(): 

393 aList = xdb.qr.get() # blocks 

394 kind = aList[0] 

395 if kind == 'clear-stdout': 

396 if xpd_pane: 

397 xpd_pane.clear() 

398 elif kind == 'put-stdout': 

399 message = aList[1] 

400 if xpd_pane: 

401 xpd_pane.write(message) 

402 else: 

403 sys.stdout.write(message) 

404 sys.stdout.flush() 

405 elif kind == 'stop-xdb': 

406 kill = True 

407 elif kind == 'select-line': 

408 line, fn = aList[1], aList[2] 

409 show_line(line, fn) 

410 else: 

411 g.es('unknown qr message:', aList) 

412 if kill: 

413 g.app.xdb = None 

414 sys.stdout = sys.__stdout__ 

415 # Never stop the singleton timer. 

416 # self.timer.stop() 

417#@+node:ekr.20181004060517.1: *3* function: make_at_file_node 

418def make_at_file_node(line, path): 

419 """ 

420 Make and populate an @auto node for the given path. 

421 """ 

422 c = g.app.log.c 

423 if not c: 

424 return None 

425 path = g.os_path_finalize(path).replace('\\', '/') 

426 if not g.os_path_exists(path): 

427 g.trace('Not found:', repr(path)) 

428 return None 

429 # Create the new node. 

430 p = c.lastTopLevel().insertAfter() 

431 # Like c.looksLikeDerivedFile, but retaining the contents. 

432 with open(path, 'r') as f: 

433 file_s = f.read() 

434 is_derived = file_s.find('@+leo-ver=') > -1 

435 if is_derived: 

436 # Set p.v.gnx from the derived file. 

437 is_derived = get_gnx_from_file(file_s, p, path) 

438 kind = '@file' if is_derived else '@auto' 

439 p.h = f"{kind} {path}" 

440 c.selectPosition(p) 

441 c.refreshFromDisk() 

442 return p 

443#@+node:ekr.20180701061957.1: *3* function: show_line 

444def show_line(line, fn): 

445 """ 

446 Put the cursor on the requested line of the given file. 

447 fn should be a full path to a file. 

448 """ 

449 c = g.app.log.c 

450 target = g.os_path_finalize(fn).replace('\\', '/') 

451 if not g.os_path_exists(fn): 

452 g.trace('===== Does not exist', fn) 

453 return 

454 for p in c.all_positions(): 

455 if p.isAnyAtFileNode(): 

456 path = g.fullPath(c, p).replace('\\', '/') 

457 if target == path: 

458 # Select the line. 

459 junk_p, junk_offset, ok = c.gotoCommands.find_file_line(n=line, p=p) 

460 if not ok: 

461 g.trace('FAIL:', target) 

462 c.bodyWantsFocusNow() 

463 return 

464 p = make_at_file_node(line, target) 

465 junk_p, junk_offset, ok = c.gotoCommands.find_file_line(n=line, p=p) 

466 if not ok: 

467 g.trace('FAIL:', target) 

468#@+node:ekr.20181001054314.1: ** top-level xdb commands 

469#@+node:ekr.20181003015017.1: *3* db-again 

470@g.command('db-again') 

471def xdb_again(event): 

472 """Repeat the previous xdb command.""" 

473 xdb = getattr(g.app, 'xdb', None) 

474 if xdb: 

475 xdb.qc.put(xdb.lastcmd) 

476 else: 

477 print('xdb not active') 

478#@+node:ekr.20181003054157.1: *3* db-b 

479@g.command('db-b') 

480def xdb_breakpoint(event): 

481 """Set the breakpoint at the presently select line in Leo.""" 

482 c = event.get('c') 

483 if not c: 

484 return 

485 p = c.p 

486 xdb = getattr(g.app, 'xdb', None) 

487 if not xdb: 

488 print('xdb not active') 

489 return 

490 w = c.frame.body.wrapper 

491 if not w: 

492 return 

493 x = c.gotoCommands 

494 root, fileName = x.find_root(p) 

495 if not root: 

496 g.trace('no root', p.h) 

497 return 

498 path = g.fullPath(c, root) 

499 n0 = x.find_node_start(p=p) 

500 if n0 is None: 

501 g.trace('no n0') 

502 return 

503 c.bodyWantsFocusNow() 

504 i = w.getInsertPoint() 

505 s = w.getAllText() 

506 row, col = g.convertPythonIndexToRowCol(s, i) 

507 n = x.node_offset_to_file_line(row, p, root) 

508 if n is not None: 

509 xdb.qc.put(f"b {path}:{n + 1}") 

510#@+node:ekr.20180702074705.1: *3* db-c/h/l/n/q/r/s/w 

511@g.command('db-c') 

512def xdb_c(event): 

513 """execute the pdb 'continue' command.""" 

514 db_command(event, 'c') 

515 

516@g.command('db-h') 

517def xdb_h(event): 

518 """execute the pdb 'continue' command.""" 

519 db_command(event, 'h') 

520 

521@g.command('db-l') 

522def xdb_l(event): 

523 """execute the pdb 'list' command.""" 

524 db_command(event, 'l') 

525 

526@g.command('db-n') 

527def xdb_n(event): 

528 """execute the pdb 'next' command.""" 

529 db_command(event, 'n') 

530 

531@g.command('db-q') 

532def xdb_q(event): 

533 """execute the pdb 'quit' command.""" 

534 db_command(event, 'q') 

535 

536@g.command('db-r') 

537def xdb_r(event): 

538 """execute the pdb 'return' command.""" 

539 db_command(event, 'r') 

540 

541@g.command('db-s') 

542def xdb_s(event): 

543 """execute the pdb 'step' command.""" 

544 db_command(event, 's') 

545 

546@g.command('db-w') 

547def xdb_w(event): 

548 """execute the pdb 'where' command.""" 

549 db_command(event, 'w') 

550#@+node:ekr.20180701050839.2: *3* db-input 

551@g.command('db-input') 

552def xdb_input(event): 

553 """Prompt the user for a pdb command and execute it.""" 

554 c = event.get('c') 

555 if not c: 

556 g.trace('no c') 

557 return 

558 xdb = getattr(g.app, 'xdb', None) 

559 if not xdb: 

560 print('xdb not active') 

561 return 

562 

563 def callback(args, c, event): 

564 xdb = getattr(g.app, 'xdb', None) 

565 if xdb: 

566 command = args[0].strip() 

567 if not command: 

568 command = xdb.lastcmd 

569 xdb.qc.put(command) 

570 else: 

571 g.trace('xdb not active') 

572 

573 c.interactive(callback, event, prompts=['Debugger command: ']) 

574#@+node:ekr.20181003015636.1: *3* db-status 

575@g.command('db-status') 

576def xdb_status(event): 

577 """Print whether xdb is active.""" 

578 xdb = getattr(g.app, 'xdb', None) 

579 print('active' if xdb else 'inactive') 

580#@+node:ekr.20181006163454.1: *3* do_command 

581def db_command(event, command): 

582 

583 xdb = getattr(g.app, 'xdb', None) 

584 if xdb: 

585 xdb.qc.put(command) 

586 else: 

587 print('xdb not active') 

588#@+node:ekr.20180701050839.1: *3* xdb 

589@g.command('xdb') 

590def xdb_command(event): 

591 """Start the external debugger on a toy test program.""" 

592 c = event.get('c') 

593 if not c: 

594 return 

595 path = g.fullPath(c, c.p) 

596 if not path: 

597 g.trace('Not in an @<file> tree') 

598 return 

599 if not g.os_path_exists(path): 

600 g.trace('not found', path) 

601 return 

602 os.chdir(g.os_path_dirname(path)) 

603 xdb = getattr(g.app, 'xdb', None) 

604 if xdb: 

605 # Just issue a message. 

606 xdb.write('xdb active: use Quit button or db-q to terminate') 

607 # Killing the previous debugger works, 

608 # *provided* we don't try to restart xdb! 

609 # That would create a race condition on g.app.xdb. 

610 # xdb.do_quit() 

611 else: 

612 # Start the debugger in a separate thread. 

613 g.app.xdb = xdb = Xdb(path) 

614 xdb.start() 

615 xdb.qr.put(['clear-stdout']) 

616 # This is Threading.start(). 

617 # It runs the debugger in a separate thread. 

618 # It also selects the start of the file. 

619#@-others 

620#@@language python 

621#@@tabwidth -4 

622#@-leo