|
|
1.1 ! root 1: #!/usr/bin/env python ! 2: # ! 3: # A Debug UI for the Hatari, part of PyGtk Hatari UI ! 4: # ! 5: # Copyright (C) 2008-2010 by Eero Tamminen <eerot at berlios> ! 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): ! 40: table, self.dialog = create_table_dialog(parent, "Save from memory", 3, 2) ! 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) ! 89: table, self.dialog = create_table_dialog(parent, "Load to memory", 2, 2) ! 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): ! 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) ! 143: self.dialog.show_all() ! 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() ! 148: self.dialog.hide() ! 149: return (lines, follow_pc) ! 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 ! 181: self.follow_pc = True ! 182: self.lines = 12 ! 183: # addresses ! 184: self.first = None ! 185: self.second = None ! 186: self.last = None ! 187: ! 188: def clear(self): ! 189: if self.follow_pc: ! 190: # get first address from PC when next stopped ! 191: self.first = None ! 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: ! 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: ! 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 ! 246: ! 247: if not address: ! 248: if not self.first: ! 249: self._get_registers() ! 250: address = self.first ! 251: ! 252: if not address: ! 253: print "ERROR: address needed" ! 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: ! 261: print "ERROR: unknown dumpmode:", self.dumpmode ! 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: ! 271: # second last line has first PC, last line next PC in next column ! 272: self.first = int(output[-2][:output[-2].find(":")], 16) ! 273: self.second = int(output[-1][output[-1].find(":")+2:], 16) ! 274: self.reset_entry() ! 275: return output ! 276: ! 277: def _get_memdump(self, address, move_idx): ! 278: linewidth = 16 ! 279: screenful = self.lines*linewidth ! 280: # no move, left/right, up/down, page up/down (no overlap) ! 281: offsets = [0, 2, linewidth, screenful] ! 282: offset = offsets[abs(move_idx)] ! 283: if move_idx < 0: ! 284: address -= offset ! 285: else: ! 286: address += offset ! 287: self._set_clamped(address, address+screenful) ! 288: self.hatari.debug_command("m $%06x-$%06x" % (self.first, self.last)) ! 289: # get & set debugger command results ! 290: output = self.hatari.get_lines(self.debug_output) ! 291: self.second = address + linewidth ! 292: return output ! 293: ! 294: def _get_disasm(self, address, move_idx): ! 295: # TODO: uses brute force i.e. ask for more lines that user has ! 296: # requested to be sure that the window is filled, assuming ! 297: # 6 bytes is largest possible instruction+args size ! 298: # (I don't remember anymore my m68k asm...) ! 299: screenful = 6*self.lines ! 300: # no move, left/right, up/down, page up/down ! 301: offsets = [0, 2, 4, screenful] ! 302: offset = offsets[abs(move_idx)] ! 303: # force one line of overlap in page up/down ! 304: if move_idx < 0: ! 305: address -= offset ! 306: if address < 0: ! 307: address = 0 ! 308: if move_idx == -Constants.MOVE_MAX and self.second: ! 309: screenful = self.second - address ! 310: else: ! 311: if move_idx == Constants.MOVE_MED and self.second: ! 312: address = self.second ! 313: elif move_idx == Constants.MOVE_MAX and self.last: ! 314: address = self.last ! 315: else: ! 316: address += offset ! 317: self._set_clamped(address, address+screenful) ! 318: self.hatari.debug_command("d $%06x-$%06x" % (self.first, self.last)) ! 319: # get & set debugger command results ! 320: output = self.hatari.get_lines(self.debug_output) ! 321: # cut output to desired length and check new addresses ! 322: if len(output) > self.lines: ! 323: if move_idx < 0: ! 324: output = output[-self.lines:] ! 325: else: ! 326: output = output[:self.lines] ! 327: # with disasm need to re-get the addresses from the output ! 328: self.first = int(output[0][:output[0].find(":")], 16) ! 329: self.second = int(output[1][:output[1].find(":")], 16) ! 330: self.last = int(output[-1][:output[-1].find(":")], 16) ! 331: return output ! 332: ! 333: def _set_clamped(self, first, last): ! 334: "set_clamped(first,last), clamp addresses to valid address range and set them" ! 335: assert(first < last) ! 336: if first < 0: ! 337: last = last-first ! 338: first = 0 ! 339: if last > 0xffffff: ! 340: first = 0xffffff - (last-first) ! 341: last = 0xffffff ! 342: self.first = first ! 343: self.last = last ! 344: ! 345: ! 346: # the Hatari debugger UI class and methods ! 347: class HatariDebugUI: ! 348: ! 349: def __init__(self, hatariobj, do_destroy = False): ! 350: self.address = MemoryAddress(hatariobj) ! 351: self.hatari = hatariobj ! 352: # set when needed/created ! 353: self.dialog_load = None ! 354: self.dialog_save = None ! 355: self.dialog_options = None ! 356: # set when UI created ! 357: self.keys = None ! 358: self.stop_button = None ! 359: # set on option load ! 360: self.config = None ! 361: self.load_options() ! 362: # UI initialization/creation ! 363: self.window = self.create_ui("Hatari Debug UI", do_destroy) ! 364: ! 365: def create_ui(self, title, do_destroy): ! 366: # buttons at top ! 367: hbox1 = gtk.HBox() ! 368: self.create_top_buttons(hbox1) ! 369: ! 370: # disasm/memory dump at the middle ! 371: align = gtk.Alignment() ! 372: # top, bottom, left, right padding ! 373: align.set_padding(8, 0, 8, 8) ! 374: align.add(self.address.get_memory_label()) ! 375: ! 376: # buttons at bottom ! 377: hbox2 = gtk.HBox() ! 378: self.create_bottom_buttons(hbox2) ! 379: ! 380: # their container ! 381: vbox = gtk.VBox() ! 382: vbox.pack_start(hbox1, False) ! 383: vbox.pack_start(align, True, True) ! 384: vbox.pack_start(hbox2, False) ! 385: ! 386: # and the window for all of this ! 387: window = gtk.Window(gtk.WINDOW_TOPLEVEL) ! 388: window.set_events(gtk.gdk.KEY_RELEASE_MASK) ! 389: window.connect("key_release_event", self.key_event_cb) ! 390: if do_destroy: ! 391: window.connect("delete_event", self.quit) ! 392: else: ! 393: window.connect("delete_event", self.hide) ! 394: window.set_icon_from_file(UInfo.icon) ! 395: window.set_title(title) ! 396: window.add(vbox) ! 397: return window ! 398: ! 399: def create_top_buttons(self, box): ! 400: self.stop_button = create_toggle("Stop", self.stop_cb) ! 401: box.add(self.stop_button) ! 402: ! 403: monitor = create_button("Monitor...", self.monitor_cb) ! 404: box.add(monitor) ! 405: ! 406: buttons = ( ! 407: ("<<<", "Page_Up", -Constants.MOVE_MAX), ! 408: ("<<", "Up", -Constants.MOVE_MED), ! 409: ("<", "Left", -Constants.MOVE_MIN), ! 410: (">", "Right", Constants.MOVE_MIN), ! 411: (">>", "Down", Constants.MOVE_MED), ! 412: (">>>", "Page_Down", Constants.MOVE_MAX) ! 413: ) ! 414: self.keys = {} ! 415: for label, keyname, offset in buttons: ! 416: button = create_button(label, self.set_address_offset, offset) ! 417: keyval = gtk.gdk.keyval_from_name(keyname) ! 418: self.keys[keyval] = offset ! 419: box.add(button) ! 420: ! 421: # to middle of <<>> buttons ! 422: address_entry = self.address.get_address_entry() ! 423: box.pack_start(address_entry, False) ! 424: box.reorder_child(address_entry, 5) ! 425: ! 426: def create_bottom_buttons(self, box): ! 427: radios = ( ! 428: ("Registers", Constants.REGISTERS), ! 429: ("Memdump", Constants.MEMDUMP), ! 430: ("Disasm", Constants.DISASM) ! 431: ) ! 432: group = None ! 433: for label, mode in radios: ! 434: button = gtk.RadioButton(group, label) ! 435: if not group: ! 436: group = button ! 437: button.connect("toggled", self.dumpmode_cb, mode) ! 438: button.unset_flags(gtk.CAN_FOCUS) ! 439: box.add(button) ! 440: group.set_active(True) ! 441: ! 442: dialogs = ( ! 443: ("Memload...", self.memload_cb), ! 444: ("Memsave...", self.memsave_cb), ! 445: ("Options...", self.options_cb) ! 446: ) ! 447: for label, cb in dialogs: ! 448: button = create_button(label, cb) ! 449: box.add(button) ! 450: ! 451: def stop_cb(self, widget): ! 452: if widget.get_active(): ! 453: self.hatari.pause() ! 454: self.address.clear() ! 455: self.address.dump() ! 456: else: ! 457: self.hatari.unpause() ! 458: ! 459: def dumpmode_cb(self, widget, mode): ! 460: if widget.get_active(): ! 461: self.address.set_dumpmode(mode) ! 462: ! 463: def key_event_cb(self, widget, event): ! 464: if event.keyval in self.keys: ! 465: self.address.dump(None, self.keys[event.keyval]) ! 466: ! 467: def set_address_offset(self, widget, move_idx): ! 468: self.address.dump(None, move_idx) ! 469: ! 470: def monitor_cb(self, widget): ! 471: TodoDialog(self.window).run("add register / memory address range monitor window.") ! 472: ! 473: def memload_cb(self, widget): ! 474: if not self.dialog_load: ! 475: self.dialog_load = LoadDialog(self.window) ! 476: (filename, address) = self.dialog_load.run(self.address.get()) ! 477: if filename and address: ! 478: self.hatari.debug_command("l %s $%06x" % (filename, address)) ! 479: ! 480: def memsave_cb(self, widget): ! 481: if not self.dialog_save: ! 482: self.dialog_save = SaveDialog(self.window) ! 483: (filename, address, length) = self.dialog_save.run(self.address.get()) ! 484: if filename and address and length: ! 485: self.hatari.debug_command("s %s $%06x $%06x" % (filename, address, length)) ! 486: ! 487: def options_cb(self, widget): ! 488: if not self.dialog_options: ! 489: self.dialog_options = OptionsDialog(self.window) ! 490: old_lines = self.config.get("[General]", "nLines") ! 491: old_follow_pc = self.config.get("[General]", "bFollowPC") ! 492: lines, follow_pc = self.dialog_options.run(old_lines, old_follow_pc) ! 493: if lines != old_lines: ! 494: self.config.set("[General]", "nLines", lines) ! 495: self.address.set_lines(lines) ! 496: if follow_pc != old_follow_pc: ! 497: self.config.set("[General]", "bFollowPC", follow_pc) ! 498: self.address.set_follow_pc(follow_pc) ! 499: ! 500: def load_options(self): ! 501: # TODO: move config to MemoryAddress class? ! 502: # (depends on how monitoring of addresses should work) ! 503: lines = self.address.get_lines() ! 504: follow_pc = self.address.get_follow_pc() ! 505: miss_is_error = False # needed for adding windows ! 506: defaults = { ! 507: "[General]": { ! 508: "nLines": lines, ! 509: "bFollowPC": follow_pc ! 510: } ! 511: } ! 512: userconfdir = ".hatari" ! 513: config = ConfigStore(userconfdir, defaults, miss_is_error) ! 514: configpath = config.get_filepath("debugui.cfg") ! 515: config.load(configpath) # set defaults ! 516: try: ! 517: self.address.set_lines(config.get("[General]", "nLines")) ! 518: self.address.set_follow_pc(config.get("[General]", "bFollowPC")) ! 519: except (KeyError, AttributeError): ! 520: ErrorDialog(None).run("Debug UI configuration mismatch!\nTry again after removing: '%s'." % configpath) ! 521: self.config = config ! 522: ! 523: def save_options(self): ! 524: self.config.save() ! 525: ! 526: def show(self): ! 527: self.stop_button.set_active(True) ! 528: self.window.show_all() ! 529: self.window.deiconify() ! 530: ! 531: def hide(self, widget, arg): ! 532: self.window.hide() ! 533: self.stop_button.set_active(False) ! 534: self.save_options() ! 535: return True ! 536: ! 537: def quit(self, widget, arg): ! 538: KillDialog(self.window).run(self.hatari) ! 539: gtk.main_quit() ! 540: ! 541: ! 542: def main(): ! 543: import sys ! 544: from hatari import Hatari ! 545: hatariobj = Hatari() ! 546: if len(sys.argv) > 1: ! 547: if sys.argv[1] in ("-h", "--help"): ! 548: print "usage: %s [hatari options]" % os.path.basename(sys.argv[0]) ! 549: return ! 550: args = sys.argv[1:] ! 551: else: ! 552: args = None ! 553: hatariobj.run(args) ! 554: ! 555: info = UInfo() ! 556: debugui = HatariDebugUI(hatariobj, True) ! 557: debugui.window.show_all() ! 558: gtk.main() ! 559: debugui.save_options() ! 560: ! 561: ! 562: if __name__ == "__main__": ! 563: main()
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.