|
|
1.1.1.8 ! root 1: #!/usr/bin/env python 1.1 root 2: # 1.1.1.8 ! root 3: # A Debug UI for the Hatari, part of Python Gtk Hatari UI 1.1 root 4: # 1.1.1.8 ! root 5: # Copyright (C) 2008-2019 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 1.1.1.8 ! root 18: import gi ! 19: # use correct version of gtk ! 20: gi.require_version('Gtk', '3.0') ! 21: from gi.repository import Gtk ! 22: from gi.repository import Gdk ! 23: from gi.repository import Pango 1.1 root 24: 25: from config import ConfigStore 26: from uihelpers import UInfo, create_button, create_toggle, \ 27: create_table_dialog, table_add_entry_row, table_add_widget_row, \ 28: get_save_filename, FselEntry 29: from dialogs import TodoDialog, ErrorDialog, AskDialog, KillDialog 30: 31: 32: def dialog_apply_cb(widget, dialog): 1.1.1.8 ! root 33: dialog.response(Gtk.ResponseType.APPLY) 1.1 root 34: 35: 36: # ------------- 37: # Table dialogs 38: 39: class SaveDialog: 40: def __init__(self, parent): 1.1.1.3 root 41: table, self.dialog = create_table_dialog(parent, "Save from memory", 3, 2) 1.1 root 42: self.file = FselEntry(self.dialog) 1.1.1.8 ! root 43: table_add_widget_row(table, 0, 0, "File name:", self.file.get_container()) ! 44: self.address = table_add_entry_row(table, 1, 0, "Save address:", 6) 1.1 root 45: self.address.connect("activate", dialog_apply_cb, self.dialog) 1.1.1.8 ! root 46: self.length = table_add_entry_row(table, 2, 0, "Number of bytes:", 6) 1.1 root 47: self.length.connect("activate", dialog_apply_cb, self.dialog) 1.1.1.8 ! root 48: 1.1 root 49: def run(self, address): 50: "run(address) -> (filename,address,length), all as strings" 51: if address: 52: self.address.set_text("%06X" % address) 53: self.dialog.show_all() 54: filename = length = None 55: while 1: 56: response = self.dialog.run() 1.1.1.8 ! root 57: if response == Gtk.ResponseType.APPLY: 1.1 root 58: filename = self.file.get_filename() 59: address_txt = self.address.get_text() 60: length_txt = self.length.get_text() 61: if filename and address_txt and length_txt: 62: try: 63: address = int(address_txt, 16) 64: except ValueError: 65: ErrorDialog(self.dialog).run("address needs to be in hex") 66: continue 67: try: 68: length = int(length_txt) 69: except ValueError: 70: ErrorDialog(self.dialog).run("length needs to be a number") 71: continue 72: if os.path.exists(filename): 73: question = "File:\n%s\nexists, replace?" % filename 74: if not AskDialog(self.dialog).run(question): 75: continue 76: break 77: else: 78: ErrorDialog(self.dialog).run("please fill the field(s)") 79: else: 80: break 81: self.dialog.hide() 82: return (filename, address, length) 83: 84: 85: class LoadDialog: 86: def __init__(self, parent): 1.1.1.8 ! root 87: chooser = Gtk.FileChooserButton('Select a File') 1.1 root 88: chooser.set_local_only(True) # Hatari cannot access URIs 89: chooser.set_width_chars(12) 1.1.1.3 root 90: table, self.dialog = create_table_dialog(parent, "Load to memory", 2, 2) 1.1.1.8 ! root 91: self.file = table_add_widget_row(table, 0, 0, "File name:", chooser) ! 92: self.address = table_add_entry_row(table, 1, 0, "Load address:", 6) 1.1 root 93: self.address.connect("activate", dialog_apply_cb, self.dialog) 94: 95: def run(self, address): 96: "run(address) -> (filename,address), all as strings" 97: if address: 98: self.address.set_text("%06X" % address) 99: self.dialog.show_all() 100: filename = None 101: while 1: 102: response = self.dialog.run() 1.1.1.8 ! root 103: if response == Gtk.ResponseType.APPLY: 1.1 root 104: filename = self.file.get_filename() 105: address_txt = self.address.get_text() 106: if filename and address_txt: 107: try: 108: address = int(address_txt, 16) 109: except ValueError: 110: ErrorDialog(self.dialog).run("address needs to be in hex") 111: continue 112: break 113: else: 114: ErrorDialog(self.dialog).run("please fill the field(s)") 115: else: 116: break 117: self.dialog.hide() 118: return (filename, address) 119: 120: 121: class OptionsDialog: 122: def __init__(self, parent): 1.1.1.8 ! root 123: self.dialog = Gtk.Dialog("Debugger UI options", parent, ! 124: Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, ! 125: (Gtk.STOCK_APPLY, Gtk.ResponseType.APPLY, ! 126: Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE)) 1.1.1.3 root 127: 1.1.1.8 ! root 128: self.lines = Gtk.Adjustment(0, 5, 50) ! 129: scale = Gtk.HScale(adjustment=self.lines) 1.1.1.3 root 130: scale.set_digits(0) 1.1.1.8 ! root 131: ! 132: self.follow_pc = Gtk.CheckButton("On stop, set address to PC") 1.1.1.3 root 133: 134: vbox = self.dialog.vbox 1.1.1.8 ! root 135: vbox.add(Gtk.Label(label="Memdump/disasm lines:")) 1.1.1.3 root 136: vbox.add(scale) 137: vbox.add(self.follow_pc) 138: vbox.show_all() 139: 140: def run(self, lines, follow_pc): 141: "run(lines,follow_pc) -> (lines,follow_pc)" 142: self.follow_pc.set_active(follow_pc) 143: self.lines.set_value(lines) 1.1 root 144: self.dialog.show_all() 1.1.1.3 root 145: response = self.dialog.run() 1.1.1.8 ! root 146: if response == Gtk.ResponseType.APPLY: 1.1.1.3 root 147: lines = int(self.lines.get_value()) 148: follow_pc = self.follow_pc.get_active() 1.1 root 149: self.dialog.hide() 1.1.1.3 root 150: return (lines, follow_pc) 1.1 root 151: 152: 153: # ---------------------------------------------------- 154: 155: # constants for the other classes 156: class Constants: 157: # dump modes 158: DISASM = 1 159: MEMDUMP = 2 160: REGISTERS = 3 161: # move IDs 162: MOVE_MIN = 1 163: MOVE_MED = 2 164: MOVE_MAX = 3 165: 166: 167: # class for the memory address entry, view (label) and 168: # the logic for memory dump modes and moving in memory 169: class MemoryAddress: 170: # class variables 171: debug_output = None 172: hatari = None 173: 174: def __init__(self, hatariobj): 175: # hatari 176: self.debug_output = hatariobj.open_debug_output() 177: self.hatari = hatariobj 178: # widgets 179: self.entry, self.memory = self.create_widgets() 180: # settings 181: self.dumpmode = Constants.REGISTERS 1.1.1.3 root 182: self.follow_pc = True 1.1 root 183: self.lines = 12 184: # addresses 185: self.first = None 186: self.second = None 187: self.last = None 1.1.1.8 ! root 188: 1.1 root 189: def clear(self): 1.1.1.3 root 190: if self.follow_pc: 191: # get first address from PC when next stopped 192: self.first = None 1.1 root 193: self.second = None 194: self.last = None 195: 196: def create_widgets(self): 1.1.1.8 ! root 197: entry = Gtk.Entry(max_length=6, width_chars=6) 1.1 root 198: entry.connect("activate", self._entry_cb) 1.1.1.8 ! root 199: memory = Gtk.Label(halign=Gtk.Align.START, margin_start=8, margin_end=8, margin_top=8) ! 200: mono = Pango.FontDescription("monospace") 1.1 root 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) 1.1.1.8 ! root 215: 1.1 root 216: def get(self): 217: return self.first 218: 219: def get_memory_label(self): 220: return self.memory 1.1.1.8 ! root 221: 1.1 root 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 1.1.1.8 ! root 227: 1.1.1.3 root 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 1.1.1.8 ! root 233: 1.1 root 234: def set_lines(self, lines): 235: self.lines = lines 1.1.1.8 ! root 236: 1.1 root 237: def set_dumpmode(self, mode): 238: self.dumpmode = mode 239: self.dump() 1.1.1.8 ! root 240: 1.1 root 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 1.1.1.8 ! root 255: 1.1 root 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() 1.1.1.8 ! root 266: 1.1 root 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 1.1.1.8 ! root 301: 1.1 root 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: 1.1.1.8 ! root 356: 1.1 root 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) 1.1.1.8 ! root 372: 1.1 root 373: def create_ui(self, title, do_destroy): 374: # buttons at top 1.1.1.8 ! root 375: hbox1 = Gtk.HBox() 1.1 root 376: self.create_top_buttons(hbox1) 377: 378: # disasm/memory dump at the middle 1.1.1.8 ! root 379: addr = self.address.get_memory_label() 1.1 root 380: 381: # buttons at bottom 1.1.1.8 ! root 382: hbox2 = Gtk.HBox() 1.1 root 383: self.create_bottom_buttons(hbox2) 384: 385: # their container 1.1.1.8 ! root 386: vbox = Gtk.VBox() ! 387: vbox.pack_start(hbox1, False, True, 0) ! 388: vbox.pack_start(addr, True, True, 0) ! 389: vbox.pack_start(hbox2, False, True, 0) ! 390: 1.1 root 391: # and the window for all of this 1.1.1.8 ! root 392: window = Gtk.Window(Gtk.WindowType.TOPLEVEL) ! 393: window.set_events(Gdk.EventMask.KEY_RELEASE_MASK) 1.1 root 394: window.connect("key_release_event", self.key_event_cb) 395: if do_destroy: 396: window.connect("delete_event", self.quit) 397: else: 398: window.connect("delete_event", self.hide) 399: window.set_icon_from_file(UInfo.icon) 400: window.set_title(title) 401: window.add(vbox) 402: return window 1.1.1.8 ! root 403: 1.1 root 404: def create_top_buttons(self, box): 405: self.stop_button = create_toggle("Stop", self.stop_cb) 406: box.add(self.stop_button) 407: 408: monitor = create_button("Monitor...", self.monitor_cb) 409: box.add(monitor) 1.1.1.8 ! root 410: 1.1 root 411: buttons = ( 412: ("<<<", "Page_Up", -Constants.MOVE_MAX), 413: ("<<", "Up", -Constants.MOVE_MED), 414: ("<", "Left", -Constants.MOVE_MIN), 415: (">", "Right", Constants.MOVE_MIN), 416: (">>", "Down", Constants.MOVE_MED), 417: (">>>", "Page_Down", Constants.MOVE_MAX) 418: ) 419: self.keys = {} 420: for label, keyname, offset in buttons: 421: button = create_button(label, self.set_address_offset, offset) 1.1.1.8 ! root 422: keyval = Gdk.keyval_from_name(keyname) 1.1 root 423: self.keys[keyval] = offset 424: box.add(button) 425: 426: # to middle of <<>> buttons 427: address_entry = self.address.get_address_entry() 1.1.1.8 ! root 428: box.pack_start(address_entry, False, True, 0) 1.1 root 429: box.reorder_child(address_entry, 5) 430: 431: def create_bottom_buttons(self, box): 432: radios = ( 433: ("Registers", Constants.REGISTERS), 434: ("Memdump", Constants.MEMDUMP), 435: ("Disasm", Constants.DISASM) 436: ) 437: group = None 438: for label, mode in radios: 1.1.1.8 ! root 439: button = Gtk.RadioButton(label=label, group=group, can_focus=False) 1.1 root 440: if not group: 441: group = button 442: button.connect("toggled", self.dumpmode_cb, mode) 443: box.add(button) 444: group.set_active(True) 445: 446: dialogs = ( 447: ("Memload...", self.memload_cb), 448: ("Memsave...", self.memsave_cb), 449: ("Options...", self.options_cb) 450: ) 451: for label, cb in dialogs: 452: button = create_button(label, cb) 453: box.add(button) 454: 455: def stop_cb(self, widget): 456: if widget.get_active(): 457: self.hatari.pause() 458: self.address.clear() 459: self.address.dump() 460: else: 461: self.hatari.unpause() 462: 463: def dumpmode_cb(self, widget, mode): 464: if widget.get_active(): 465: self.address.set_dumpmode(mode) 466: 467: def key_event_cb(self, widget, event): 468: if event.keyval in self.keys: 469: self.address.dump(None, self.keys[event.keyval]) 470: 471: def set_address_offset(self, widget, move_idx): 472: self.address.dump(None, move_idx) 1.1.1.8 ! root 473: 1.1 root 474: def monitor_cb(self, widget): 475: TodoDialog(self.window).run("add register / memory address range monitor window.") 476: 477: def memload_cb(self, widget): 478: if not self.dialog_load: 479: self.dialog_load = LoadDialog(self.window) 480: (filename, address) = self.dialog_load.run(self.address.get()) 481: if filename and address: 1.1.1.2 root 482: self.hatari.debug_command("l %s $%06x" % (filename, address)) 1.1 root 483: 484: def memsave_cb(self, widget): 485: if not self.dialog_save: 486: self.dialog_save = SaveDialog(self.window) 487: (filename, address, length) = self.dialog_save.run(self.address.get()) 488: if filename and address and length: 1.1.1.2 root 489: self.hatari.debug_command("s %s $%06x $%06x" % (filename, address, length)) 1.1.1.8 ! root 490: 1.1 root 491: def options_cb(self, widget): 492: if not self.dialog_options: 493: self.dialog_options = OptionsDialog(self.window) 1.1.1.3 root 494: old_lines = self.config.get("[General]", "nLines") 495: old_follow_pc = self.config.get("[General]", "bFollowPC") 496: lines, follow_pc = self.dialog_options.run(old_lines, old_follow_pc) 497: if lines != old_lines: 1.1 root 498: self.config.set("[General]", "nLines", lines) 499: self.address.set_lines(lines) 1.1.1.3 root 500: if follow_pc != old_follow_pc: 501: self.config.set("[General]", "bFollowPC", follow_pc) 502: self.address.set_follow_pc(follow_pc) 1.1 root 503: 504: def load_options(self): 505: # TODO: move config to MemoryAddress class? 506: # (depends on how monitoring of addresses should work) 507: lines = self.address.get_lines() 1.1.1.3 root 508: follow_pc = self.address.get_follow_pc() 1.1 root 509: miss_is_error = False # needed for adding windows 1.1.1.3 root 510: defaults = { 511: "[General]": { 512: "nLines": lines, 513: "bFollowPC": follow_pc 514: } 515: } 516: userconfdir = ".hatari" 517: config = ConfigStore(userconfdir, defaults, miss_is_error) 518: configpath = config.get_filepath("debugui.cfg") 519: config.load(configpath) # set defaults 520: try: 521: self.address.set_lines(config.get("[General]", "nLines")) 522: self.address.set_follow_pc(config.get("[General]", "bFollowPC")) 523: except (KeyError, AttributeError): 524: ErrorDialog(None).run("Debug UI configuration mismatch!\nTry again after removing: '%s'." % configpath) 525: self.config = config 1.1.1.8 ! root 526: 1.1 root 527: def save_options(self): 528: self.config.save() 1.1.1.8 ! root 529: 1.1 root 530: def show(self): 531: self.stop_button.set_active(True) 532: self.window.show_all() 533: self.window.deiconify() 534: 535: def hide(self, widget, arg): 536: self.window.hide() 537: self.stop_button.set_active(False) 538: self.save_options() 539: return True 540: 541: def quit(self, widget, arg): 542: KillDialog(self.window).run(self.hatari) 1.1.1.8 ! root 543: Gtk.main_quit() 1.1 root 544: 545: 546: def main(): 547: import sys 548: from hatari import Hatari 549: hatariobj = Hatari() 550: if len(sys.argv) > 1: 551: if sys.argv[1] in ("-h", "--help"): 1.1.1.4 root 552: print("usage: %s [hatari options]" % os.path.basename(sys.argv[0])) 1.1 root 553: return 554: args = sys.argv[1:] 555: else: 556: args = None 557: hatariobj.run(args) 558: 559: info = UInfo() 560: debugui = HatariDebugUI(hatariobj, True) 561: debugui.window.show_all() 1.1.1.8 ! root 562: Gtk.main() 1.1 root 563: debugui.save_options() 564: 1.1.1.8 ! root 565: 1.1 root 566: if __name__ == "__main__": 567: main()
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.