|
|
1.1 root 1: #!/usr/bin/env python
2: #
3: # A Debug UI for the Hatari, part of PyGtk Hatari UI
4: #
1.1.1.5 root 5: # Copyright (C) 2008-2011 by Eero Tamminen
1.1 root 6: #
7: # This program is free software; you can redistribute it and/or modify
8: # it under the terms of the GNU General Public License as published by
9: # the Free Software Foundation; either version 2 of the License, or
10: # (at your option) any later version.
11: #
12: # This program is distributed in the hope that it will be useful,
13: # but WITHOUT ANY WARRANTY; without even the implied warranty of
14: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15: # GNU General Public License for more details.
16:
17: import os
18: # use correct version of pygtk/gtk
19: import pygtk
20: pygtk.require('2.0')
21: import gtk
22: import pango
23:
24: from config import ConfigStore
25: from uihelpers import UInfo, create_button, create_toggle, \
26: create_table_dialog, table_add_entry_row, table_add_widget_row, \
27: get_save_filename, FselEntry
28: from dialogs import TodoDialog, ErrorDialog, AskDialog, KillDialog
29:
30:
31: def dialog_apply_cb(widget, dialog):
32: dialog.response(gtk.RESPONSE_APPLY)
33:
34:
35: # -------------
36: # Table dialogs
37:
38: class SaveDialog:
39: def __init__(self, parent):
1.1.1.3 root 40: table, self.dialog = create_table_dialog(parent, "Save from memory", 3, 2)
1.1 root 41: self.file = FselEntry(self.dialog)
42: table_add_widget_row(table, 0, "File name:", self.file.get_container())
43: self.address = table_add_entry_row(table, 1, "Save address:", 6)
44: self.address.connect("activate", dialog_apply_cb, self.dialog)
45: self.length = table_add_entry_row(table, 2, "Number of bytes:", 6)
46: self.length.connect("activate", dialog_apply_cb, self.dialog)
47:
48: def run(self, address):
49: "run(address) -> (filename,address,length), all as strings"
50: if address:
51: self.address.set_text("%06X" % address)
52: self.dialog.show_all()
53: filename = length = None
54: while 1:
55: response = self.dialog.run()
56: if response == gtk.RESPONSE_APPLY:
57: filename = self.file.get_filename()
58: address_txt = self.address.get_text()
59: length_txt = self.length.get_text()
60: if filename and address_txt and length_txt:
61: try:
62: address = int(address_txt, 16)
63: except ValueError:
64: ErrorDialog(self.dialog).run("address needs to be in hex")
65: continue
66: try:
67: length = int(length_txt)
68: except ValueError:
69: ErrorDialog(self.dialog).run("length needs to be a number")
70: continue
71: if os.path.exists(filename):
72: question = "File:\n%s\nexists, replace?" % filename
73: if not AskDialog(self.dialog).run(question):
74: continue
75: break
76: else:
77: ErrorDialog(self.dialog).run("please fill the field(s)")
78: else:
79: break
80: self.dialog.hide()
81: return (filename, address, length)
82:
83:
84: class LoadDialog:
85: def __init__(self, parent):
86: chooser = gtk.FileChooserButton('Select a File')
87: chooser.set_local_only(True) # Hatari cannot access URIs
88: chooser.set_width_chars(12)
1.1.1.3 root 89: table, self.dialog = create_table_dialog(parent, "Load to memory", 2, 2)
1.1 root 90: self.file = table_add_widget_row(table, 0, "File name:", chooser)
91: self.address = table_add_entry_row(table, 1, "Load address:", 6)
92: self.address.connect("activate", dialog_apply_cb, self.dialog)
93:
94: def run(self, address):
95: "run(address) -> (filename,address), all as strings"
96: if address:
97: self.address.set_text("%06X" % address)
98: self.dialog.show_all()
99: filename = None
100: while 1:
101: response = self.dialog.run()
102: if response == gtk.RESPONSE_APPLY:
103: filename = self.file.get_filename()
104: address_txt = self.address.get_text()
105: if filename and address_txt:
106: try:
107: address = int(address_txt, 16)
108: except ValueError:
109: ErrorDialog(self.dialog).run("address needs to be in hex")
110: continue
111: break
112: else:
113: ErrorDialog(self.dialog).run("please fill the field(s)")
114: else:
115: break
116: self.dialog.hide()
117: return (filename, address)
118:
119:
120: class OptionsDialog:
121: def __init__(self, parent):
1.1.1.3 root 122: self.dialog = gtk.Dialog("Debugger UI options", parent,
123: gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
124: (gtk.STOCK_APPLY, gtk.RESPONSE_APPLY,
125: gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE))
126:
127: self.lines = gtk.Adjustment(0, 5, 50)
128: scale = gtk.HScale(self.lines)
129: scale.set_digits(0)
130:
131: self.follow_pc = gtk.CheckButton("On stop, set address to PC")
132:
133: vbox = self.dialog.vbox
134: vbox.add(gtk.Label("Memdump/disasm lines:"))
135: vbox.add(scale)
136: vbox.add(self.follow_pc)
137: vbox.show_all()
138:
139: def run(self, lines, follow_pc):
140: "run(lines,follow_pc) -> (lines,follow_pc)"
141: self.follow_pc.set_active(follow_pc)
142: self.lines.set_value(lines)
1.1 root 143: self.dialog.show_all()
1.1.1.3 root 144: response = self.dialog.run()
145: if response == gtk.RESPONSE_APPLY:
146: lines = int(self.lines.get_value())
147: follow_pc = self.follow_pc.get_active()
1.1 root 148: self.dialog.hide()
1.1.1.3 root 149: return (lines, follow_pc)
1.1 root 150:
151:
152: # ----------------------------------------------------
153:
154: # constants for the other classes
155: class Constants:
156: # dump modes
157: DISASM = 1
158: MEMDUMP = 2
159: REGISTERS = 3
160: # move IDs
161: MOVE_MIN = 1
162: MOVE_MED = 2
163: MOVE_MAX = 3
164:
165:
166: # class for the memory address entry, view (label) and
167: # the logic for memory dump modes and moving in memory
168: class MemoryAddress:
169: # class variables
170: debug_output = None
171: hatari = None
172:
173: def __init__(self, hatariobj):
174: # hatari
175: self.debug_output = hatariobj.open_debug_output()
176: self.hatari = hatariobj
177: # widgets
178: self.entry, self.memory = self.create_widgets()
179: # settings
180: self.dumpmode = Constants.REGISTERS
1.1.1.3 root 181: self.follow_pc = True
1.1 root 182: self.lines = 12
183: # addresses
184: self.first = None
185: self.second = None
186: self.last = None
187:
188: def clear(self):
1.1.1.3 root 189: if self.follow_pc:
190: # get first address from PC when next stopped
191: self.first = None
1.1 root 192: self.second = None
193: self.last = None
194:
195: def create_widgets(self):
196: entry = gtk.Entry(6)
197: entry.set_width_chars(6)
198: entry.connect("activate", self._entry_cb)
199: memory = gtk.Label()
200: mono = pango.FontDescription("monospace")
201: memory.modify_font(mono)
202: entry.modify_font(mono)
203: return (entry, memory)
204:
205: def _entry_cb(self, widget):
206: try:
207: address = int(widget.get_text(), 16)
208: except ValueError:
209: ErrorDialog(widget.get_toplevel()).run("invalid address")
210: return
211: self.dump(address)
212:
213: def reset_entry(self):
214: self.entry.set_text("%06X" % self.first)
215:
216: def get(self):
217: return self.first
218:
219: def get_memory_label(self):
220: return self.memory
221:
222: def get_address_entry(self):
223: return self.entry
224:
1.1.1.3 root 225: def get_follow_pc(self):
226: return self.follow_pc
227:
228: def set_follow_pc(self, follow_pc):
229: self.follow_pc = follow_pc
230:
1.1 root 231: def get_lines(self):
232: return self.lines
233:
234: def set_lines(self, lines):
235: self.lines = lines
236:
237: def set_dumpmode(self, mode):
238: self.dumpmode = mode
239: self.dump()
240:
241: def dump(self, address = None, move_idx = 0):
242: if self.dumpmode == Constants.REGISTERS:
243: output = self._get_registers()
244: self.memory.set_label("".join(output))
245: return
1.1.1.3 root 246:
247: if not address:
248: if not self.first:
249: self._get_registers()
250: address = self.first
251:
1.1 root 252: if not address:
1.1.1.4 root 253: print("ERROR: address needed")
1.1 root 254: return
255:
256: if self.dumpmode == Constants.MEMDUMP:
257: output = self._get_memdump(address, move_idx)
258: elif self.dumpmode == Constants.DISASM:
259: output = self._get_disasm(address, move_idx)
260: else:
1.1.1.4 root 261: print("ERROR: unknown dumpmode:", self.dumpmode)
1.1 root 262: return
263: self.memory.set_label("".join(output))
264: if move_idx:
265: self.reset_entry()
266:
267: def _get_registers(self):
268: self.hatari.debug_command("r")
269: output = self.hatari.get_lines(self.debug_output)
270: if not self.first:
1.1.1.6 ! root 271: # 2nd last line has first PC in 1st column, last line next PC in 2nd column
1.1 root 272: self.second = int(output[-1][output[-1].find(":")+2:], 16)
1.1.1.6 ! root 273: # OldUAE CPU core has ':' in both
! 274: offset = output[-2].find(":")
! 275: if offset < 0:
! 276: # WinUAE CPU core only in one
! 277: offset = output[-2].find(" ")
! 278: if offset < 0:
! 279: print("ERROR: unable to parse register dump line:\n\t'%s'", output[-2])
! 280: return output
! 281: self.first = int(output[-2][:offset], 16)
1.1 root 282: self.reset_entry()
283: return output
284:
285: def _get_memdump(self, address, move_idx):
286: linewidth = 16
287: screenful = self.lines*linewidth
288: # no move, left/right, up/down, page up/down (no overlap)
289: offsets = [0, 2, linewidth, screenful]
290: offset = offsets[abs(move_idx)]
291: if move_idx < 0:
292: address -= offset
293: else:
294: address += offset
295: self._set_clamped(address, address+screenful)
1.1.1.2 root 296: self.hatari.debug_command("m $%06x-$%06x" % (self.first, self.last))
1.1 root 297: # get & set debugger command results
298: output = self.hatari.get_lines(self.debug_output)
299: self.second = address + linewidth
300: return output
301:
302: def _get_disasm(self, address, move_idx):
303: # TODO: uses brute force i.e. ask for more lines that user has
304: # requested to be sure that the window is filled, assuming
305: # 6 bytes is largest possible instruction+args size
306: # (I don't remember anymore my m68k asm...)
307: screenful = 6*self.lines
308: # no move, left/right, up/down, page up/down
309: offsets = [0, 2, 4, screenful]
310: offset = offsets[abs(move_idx)]
311: # force one line of overlap in page up/down
312: if move_idx < 0:
313: address -= offset
314: if address < 0:
315: address = 0
316: if move_idx == -Constants.MOVE_MAX and self.second:
317: screenful = self.second - address
318: else:
319: if move_idx == Constants.MOVE_MED and self.second:
320: address = self.second
321: elif move_idx == Constants.MOVE_MAX and self.last:
322: address = self.last
323: else:
324: address += offset
325: self._set_clamped(address, address+screenful)
1.1.1.2 root 326: self.hatari.debug_command("d $%06x-$%06x" % (self.first, self.last))
1.1 root 327: # get & set debugger command results
328: output = self.hatari.get_lines(self.debug_output)
329: # cut output to desired length and check new addresses
330: if len(output) > self.lines:
331: if move_idx < 0:
332: output = output[-self.lines:]
333: else:
334: output = output[:self.lines]
335: # with disasm need to re-get the addresses from the output
1.1.1.4 root 336: self.first = int(output[0][1:output[0].find(":")], 16)
337: self.second = int(output[1][1:output[1].find(":")], 16)
338: self.last = int(output[-1][1:output[-1].find(":")], 16)
1.1 root 339: return output
340:
341: def _set_clamped(self, first, last):
342: "set_clamped(first,last), clamp addresses to valid address range and set them"
343: assert(first < last)
344: if first < 0:
345: last = last-first
346: first = 0
347: if last > 0xffffff:
348: first = 0xffffff - (last-first)
349: last = 0xffffff
350: self.first = first
351: self.last = last
352:
353:
354: # the Hatari debugger UI class and methods
355: class HatariDebugUI:
356:
357: def __init__(self, hatariobj, do_destroy = False):
358: self.address = MemoryAddress(hatariobj)
359: self.hatari = hatariobj
360: # set when needed/created
361: self.dialog_load = None
362: self.dialog_save = None
363: self.dialog_options = None
364: # set when UI created
365: self.keys = None
366: self.stop_button = None
367: # set on option load
368: self.config = None
369: self.load_options()
370: # UI initialization/creation
371: self.window = self.create_ui("Hatari Debug UI", do_destroy)
372:
373: def create_ui(self, title, do_destroy):
374: # buttons at top
375: hbox1 = gtk.HBox()
376: self.create_top_buttons(hbox1)
377:
378: # disasm/memory dump at the middle
379: align = gtk.Alignment()
380: # top, bottom, left, right padding
381: align.set_padding(8, 0, 8, 8)
382: align.add(self.address.get_memory_label())
383:
384: # buttons at bottom
385: hbox2 = gtk.HBox()
386: self.create_bottom_buttons(hbox2)
387:
388: # their container
389: vbox = gtk.VBox()
390: vbox.pack_start(hbox1, False)
391: vbox.pack_start(align, True, True)
392: vbox.pack_start(hbox2, False)
393:
394: # and the window for all of this
395: window = gtk.Window(gtk.WINDOW_TOPLEVEL)
396: window.set_events(gtk.gdk.KEY_RELEASE_MASK)
397: window.connect("key_release_event", self.key_event_cb)
398: if do_destroy:
399: window.connect("delete_event", self.quit)
400: else:
401: window.connect("delete_event", self.hide)
402: window.set_icon_from_file(UInfo.icon)
403: window.set_title(title)
404: window.add(vbox)
405: return window
406:
407: def create_top_buttons(self, box):
408: self.stop_button = create_toggle("Stop", self.stop_cb)
409: box.add(self.stop_button)
410:
411: monitor = create_button("Monitor...", self.monitor_cb)
412: box.add(monitor)
413:
414: buttons = (
415: ("<<<", "Page_Up", -Constants.MOVE_MAX),
416: ("<<", "Up", -Constants.MOVE_MED),
417: ("<", "Left", -Constants.MOVE_MIN),
418: (">", "Right", Constants.MOVE_MIN),
419: (">>", "Down", Constants.MOVE_MED),
420: (">>>", "Page_Down", Constants.MOVE_MAX)
421: )
422: self.keys = {}
423: for label, keyname, offset in buttons:
424: button = create_button(label, self.set_address_offset, offset)
425: keyval = gtk.gdk.keyval_from_name(keyname)
426: self.keys[keyval] = offset
427: box.add(button)
428:
429: # to middle of <<>> buttons
430: address_entry = self.address.get_address_entry()
431: box.pack_start(address_entry, False)
432: box.reorder_child(address_entry, 5)
433:
434: def create_bottom_buttons(self, box):
435: radios = (
436: ("Registers", Constants.REGISTERS),
437: ("Memdump", Constants.MEMDUMP),
438: ("Disasm", Constants.DISASM)
439: )
440: group = None
441: for label, mode in radios:
442: button = gtk.RadioButton(group, label)
443: if not group:
444: group = button
445: button.connect("toggled", self.dumpmode_cb, mode)
446: button.unset_flags(gtk.CAN_FOCUS)
447: box.add(button)
448: group.set_active(True)
449:
450: dialogs = (
451: ("Memload...", self.memload_cb),
452: ("Memsave...", self.memsave_cb),
453: ("Options...", self.options_cb)
454: )
455: for label, cb in dialogs:
456: button = create_button(label, cb)
457: box.add(button)
458:
459: def stop_cb(self, widget):
460: if widget.get_active():
461: self.hatari.pause()
462: self.address.clear()
463: self.address.dump()
464: else:
465: self.hatari.unpause()
466:
467: def dumpmode_cb(self, widget, mode):
468: if widget.get_active():
469: self.address.set_dumpmode(mode)
470:
471: def key_event_cb(self, widget, event):
472: if event.keyval in self.keys:
473: self.address.dump(None, self.keys[event.keyval])
474:
475: def set_address_offset(self, widget, move_idx):
476: self.address.dump(None, move_idx)
477:
478: def monitor_cb(self, widget):
479: TodoDialog(self.window).run("add register / memory address range monitor window.")
480:
481: def memload_cb(self, widget):
482: if not self.dialog_load:
483: self.dialog_load = LoadDialog(self.window)
484: (filename, address) = self.dialog_load.run(self.address.get())
485: if filename and address:
1.1.1.2 root 486: self.hatari.debug_command("l %s $%06x" % (filename, address))
1.1 root 487:
488: def memsave_cb(self, widget):
489: if not self.dialog_save:
490: self.dialog_save = SaveDialog(self.window)
491: (filename, address, length) = self.dialog_save.run(self.address.get())
492: if filename and address and length:
1.1.1.2 root 493: self.hatari.debug_command("s %s $%06x $%06x" % (filename, address, length))
1.1 root 494:
495: def options_cb(self, widget):
496: if not self.dialog_options:
497: self.dialog_options = OptionsDialog(self.window)
1.1.1.3 root 498: old_lines = self.config.get("[General]", "nLines")
499: old_follow_pc = self.config.get("[General]", "bFollowPC")
500: lines, follow_pc = self.dialog_options.run(old_lines, old_follow_pc)
501: if lines != old_lines:
1.1 root 502: self.config.set("[General]", "nLines", lines)
503: self.address.set_lines(lines)
1.1.1.3 root 504: if follow_pc != old_follow_pc:
505: self.config.set("[General]", "bFollowPC", follow_pc)
506: self.address.set_follow_pc(follow_pc)
1.1 root 507:
508: def load_options(self):
509: # TODO: move config to MemoryAddress class?
510: # (depends on how monitoring of addresses should work)
511: lines = self.address.get_lines()
1.1.1.3 root 512: follow_pc = self.address.get_follow_pc()
1.1 root 513: miss_is_error = False # needed for adding windows
1.1.1.3 root 514: defaults = {
515: "[General]": {
516: "nLines": lines,
517: "bFollowPC": follow_pc
518: }
519: }
520: userconfdir = ".hatari"
521: config = ConfigStore(userconfdir, defaults, miss_is_error)
522: configpath = config.get_filepath("debugui.cfg")
523: config.load(configpath) # set defaults
524: try:
525: self.address.set_lines(config.get("[General]", "nLines"))
526: self.address.set_follow_pc(config.get("[General]", "bFollowPC"))
527: except (KeyError, AttributeError):
528: ErrorDialog(None).run("Debug UI configuration mismatch!\nTry again after removing: '%s'." % configpath)
529: self.config = config
1.1 root 530:
531: def save_options(self):
532: self.config.save()
533:
534: def show(self):
535: self.stop_button.set_active(True)
536: self.window.show_all()
537: self.window.deiconify()
538:
539: def hide(self, widget, arg):
540: self.window.hide()
541: self.stop_button.set_active(False)
542: self.save_options()
543: return True
544:
545: def quit(self, widget, arg):
546: KillDialog(self.window).run(self.hatari)
547: gtk.main_quit()
548:
549:
550: def main():
551: import sys
552: from hatari import Hatari
553: hatariobj = Hatari()
554: if len(sys.argv) > 1:
555: if sys.argv[1] in ("-h", "--help"):
1.1.1.4 root 556: print("usage: %s [hatari options]" % os.path.basename(sys.argv[0]))
1.1 root 557: return
558: args = sys.argv[1:]
559: else:
560: args = None
561: hatariobj.run(args)
562:
563: info = UInfo()
564: debugui = HatariDebugUI(hatariobj, True)
565: debugui.window.show_all()
566: gtk.main()
567: debugui.save_options()
568:
569:
570: if __name__ == "__main__":
571: main()
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.