Annotation of hatari/python-ui/debugui.py, revision 1.1.1.2

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 by Eero Tamminen <[email protected]>
                      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)
                     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)
                     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:         table, self.dialog = create_table_dialog(parent, "Debugger UI options", 1)
                    123:         self.lines = table_add_entry_row(table, 0, "Memdump/disasm lines:", 2)
                    124:         self.lines.connect("activate", dialog_apply_cb, self.dialog)
                    125:     
                    126:     def run(self, lines):
                    127:         "run(lines) -> lines, as integers"
                    128:         self.lines.set_text(str(lines))
                    129:         self.dialog.show_all()
                    130:         while 1:
                    131:             lines = None
                    132:             response = self.dialog.run()
                    133:             if response == gtk.RESPONSE_APPLY:
                    134:                 text = self.lines.get_text()
                    135:                 if text:
                    136:                     try:
                    137:                         lines = int(text)
                    138:                     except ValueError:
                    139:                         ErrorDialog(self.dialog).run("lines needs an integer number")
                    140:                         continue
                    141:                     break
                    142:                 else:
                    143:                     ErrorDialog(self.dialog).run("please fill the field(s)")
                    144:             else:
                    145:                 break
                    146:         self.dialog.hide()
                    147:         return lines
                    148: 
                    149: 
                    150: # ----------------------------------------------------
                    151: 
                    152: # constants for the other classes
                    153: class Constants:
                    154:     # dump modes
                    155:     DISASM = 1
                    156:     MEMDUMP = 2
                    157:     REGISTERS = 3
                    158:     # move IDs
                    159:     MOVE_MIN = 1
                    160:     MOVE_MED = 2
                    161:     MOVE_MAX = 3
                    162: 
                    163: 
                    164: # class for the memory address entry, view (label) and
                    165: # the logic for memory dump modes and moving in memory
                    166: class MemoryAddress:
                    167:     # class variables
                    168:     debug_output = None
                    169:     hatari = None
                    170: 
                    171:     def __init__(self, hatariobj):
                    172:         # hatari
                    173:         self.debug_output = hatariobj.open_debug_output()
                    174:         self.hatari = hatariobj
                    175:         # widgets
                    176:         self.entry, self.memory = self.create_widgets()
                    177:         # settings
                    178:         self.dumpmode = Constants.REGISTERS
                    179:         self.lines = 12
                    180:         # addresses
                    181:         self.first = None
                    182:         self.second = None
                    183:         self.last = None
                    184:         
                    185:     def clear(self):
                    186:         self.first = None
                    187:         self.second = None
                    188:         self.last  = None
                    189: 
                    190:     def create_widgets(self):
                    191:         entry = gtk.Entry(6)
                    192:         entry.set_width_chars(6)
                    193:         entry.connect("activate", self._entry_cb)
                    194:         memory = gtk.Label()
                    195:         mono = pango.FontDescription("monospace")
                    196:         memory.modify_font(mono)
                    197:         entry.modify_font(mono)
                    198:         return (entry, memory)
                    199: 
                    200:     def _entry_cb(self, widget):
                    201:         try:
                    202:             address = int(widget.get_text(), 16)
                    203:         except ValueError:
                    204:             ErrorDialog(widget.get_toplevel()).run("invalid address")
                    205:             return
                    206:         self.dump(address)
                    207: 
                    208:     def reset_entry(self):
                    209:         self.entry.set_text("%06X" % self.first)
                    210:         
                    211:     def get(self):
                    212:         return self.first
                    213: 
                    214:     def get_memory_label(self):
                    215:         return self.memory
                    216:     
                    217:     def get_address_entry(self):
                    218:         return self.entry
                    219: 
                    220:     def get_lines(self):
                    221:         return self.lines
                    222:     
                    223:     def set_lines(self, lines):
                    224:         self.lines = lines
                    225:     
                    226:     def set_dumpmode(self, mode):
                    227:         self.dumpmode = mode
                    228:         self.dump()
                    229:         
                    230:     def dump(self, address = None, move_idx = 0):
                    231:         if not address:
                    232:             address = self.first
                    233:         
                    234:         if self.dumpmode == Constants.REGISTERS:
                    235:             output = self._get_registers()
                    236:             self.memory.set_label("".join(output))
                    237:             return
                    238:         
                    239:         if not address:
                    240:             print "ERROR: address needed"
                    241:             return
                    242:         
                    243:         if self.dumpmode == Constants.MEMDUMP:
                    244:             output = self._get_memdump(address, move_idx)
                    245:         elif self.dumpmode == Constants.DISASM:
                    246:             output = self._get_disasm(address, move_idx)
                    247:         else:
                    248:             print "ERROR: unknown dumpmode:", self.dumpmode
                    249:             return
                    250:         self.memory.set_label("".join(output))
                    251:         if move_idx:
                    252:             self.reset_entry()
                    253:     
                    254:     def _get_registers(self):
                    255:         self.hatari.debug_command("r")
                    256:         output = self.hatari.get_lines(self.debug_output)
                    257:         if not self.first:
                    258:             # second last line has first PC, last line next PC in next column
                    259:             self.first  = int(output[-2][:output[-2].find(":")], 16)
                    260:             self.second = int(output[-1][output[-1].find(":")+2:], 16)
                    261:             self.reset_entry()
                    262:         return output
                    263: 
                    264:     def _get_memdump(self, address, move_idx):
                    265:         linewidth = 16
                    266:         screenful = self.lines*linewidth
                    267:         # no move, left/right, up/down, page up/down (no overlap)
                    268:         offsets = [0, 2, linewidth, screenful]
                    269:         offset = offsets[abs(move_idx)]
                    270:         if move_idx < 0:
                    271:             address -= offset
                    272:         else:
                    273:             address += offset
                    274:         self._set_clamped(address, address+screenful)
1.1.1.2 ! root      275:         self.hatari.debug_command("m $%06x-$%06x" % (self.first, self.last))
1.1       root      276:         # get & set debugger command results
                    277:         output = self.hatari.get_lines(self.debug_output)
                    278:         self.second = address + linewidth
                    279:         return output
                    280:         
                    281:     def _get_disasm(self, address, move_idx):
                    282:         # TODO: uses brute force i.e. ask for more lines that user has
                    283:         # requested to be sure that the window is filled, assuming
                    284:         # 6 bytes is largest possible instruction+args size
                    285:         # (I don't remember anymore my m68k asm...)
                    286:         screenful = 6*self.lines
                    287:         # no move, left/right, up/down, page up/down
                    288:         offsets = [0, 2, 4, screenful]
                    289:         offset = offsets[abs(move_idx)]
                    290:         # force one line of overlap in page up/down
                    291:         if move_idx < 0:
                    292:             address -= offset
                    293:             if address < 0:
                    294:                 address = 0
                    295:             if move_idx == -Constants.MOVE_MAX and self.second:
                    296:                 screenful = self.second - address
                    297:         else:
                    298:             if move_idx == Constants.MOVE_MED and self.second:
                    299:                 address = self.second
                    300:             elif move_idx == Constants.MOVE_MAX and self.last:
                    301:                 address = self.last
                    302:             else:
                    303:                 address += offset
                    304:         self._set_clamped(address, address+screenful)
1.1.1.2 ! root      305:         self.hatari.debug_command("d $%06x-$%06x" % (self.first, self.last))
1.1       root      306:         # get & set debugger command results
                    307:         output = self.hatari.get_lines(self.debug_output)
                    308:         # cut output to desired length and check new addresses
                    309:         if len(output) > self.lines:
                    310:             if move_idx < 0:
                    311:                 output = output[-self.lines:]
                    312:             else:
                    313:                 output = output[:self.lines]
                    314:         # with disasm need to re-get the addresses from the output
                    315:         self.first  = int(output[0][:output[0].find(":")], 16)
                    316:         self.second = int(output[1][:output[1].find(":")], 16)
                    317:         self.last   = int(output[-1][:output[-1].find(":")], 16)
                    318:         return output
                    319: 
                    320:     def _set_clamped(self, first, last):
                    321:         "set_clamped(first,last), clamp addresses to valid address range and set them"
                    322:         assert(first < last)
                    323:         if first < 0:
                    324:             last = last-first
                    325:             first = 0
                    326:         if last > 0xffffff:
                    327:             first = 0xffffff - (last-first)
                    328:             last = 0xffffff
                    329:         self.first = first
                    330:         self.last = last
                    331: 
                    332: 
                    333: # the Hatari debugger UI class and methods
                    334: class HatariDebugUI:
                    335:     
                    336:     def __init__(self, hatariobj, do_destroy = False):
                    337:         self.address = MemoryAddress(hatariobj)
                    338:         self.hatari = hatariobj
                    339:         # set when needed/created
                    340:         self.dialog_load = None
                    341:         self.dialog_save = None
                    342:         self.dialog_options = None
                    343:         # set when UI created
                    344:         self.keys = None
                    345:         self.stop_button = None
                    346:         # set on option load
                    347:         self.config = None
                    348:         self.load_options()
                    349:         # UI initialization/creation
                    350:         self.window = self.create_ui("Hatari Debug UI", do_destroy)
                    351:         
                    352:     def create_ui(self, title, do_destroy):
                    353:         # buttons at top
                    354:         hbox1 = gtk.HBox()
                    355:         self.create_top_buttons(hbox1)
                    356: 
                    357:         # disasm/memory dump at the middle
                    358:         align = gtk.Alignment()
                    359:         # top, bottom, left, right padding
                    360:         align.set_padding(8, 0, 8, 8)
                    361:         align.add(self.address.get_memory_label())
                    362: 
                    363:         # buttons at bottom
                    364:         hbox2 = gtk.HBox()
                    365:         self.create_bottom_buttons(hbox2)
                    366: 
                    367:         # their container
                    368:         vbox = gtk.VBox()
                    369:         vbox.pack_start(hbox1, False)
                    370:         vbox.pack_start(align, True, True)
                    371:         vbox.pack_start(hbox2, False)
                    372:         
                    373:         # and the window for all of this
                    374:         window = gtk.Window(gtk.WINDOW_TOPLEVEL)
                    375:         window.set_events(gtk.gdk.KEY_RELEASE_MASK)
                    376:         window.connect("key_release_event", self.key_event_cb)
                    377:         if do_destroy:
                    378:             window.connect("delete_event", self.quit)
                    379:         else:
                    380:             window.connect("delete_event", self.hide)
                    381:         window.set_icon_from_file(UInfo.icon)
                    382:         window.set_title(title)
                    383:         window.add(vbox)
                    384:         return window
                    385:     
                    386:     def create_top_buttons(self, box):
                    387:         self.stop_button = create_toggle("Stop", self.stop_cb)
                    388:         box.add(self.stop_button)
                    389: 
                    390:         monitor = create_button("Monitor...", self.monitor_cb)
                    391:         box.add(monitor)
                    392:         
                    393:         buttons = (
                    394:             ("<<<", "Page_Up",  -Constants.MOVE_MAX),
                    395:             ("<<",  "Up",       -Constants.MOVE_MED),
                    396:             ("<",   "Left",     -Constants.MOVE_MIN),
                    397:             (">",   "Right",     Constants.MOVE_MIN),
                    398:             (">>",  "Down",      Constants.MOVE_MED),
                    399:             (">>>", "Page_Down", Constants.MOVE_MAX)
                    400:         )
                    401:         self.keys = {}
                    402:         for label, keyname, offset in buttons:
                    403:             button = create_button(label, self.set_address_offset, offset)
                    404:             keyval = gtk.gdk.keyval_from_name(keyname)
                    405:             self.keys[keyval] =  offset
                    406:             box.add(button)
                    407: 
                    408:         # to middle of <<>> buttons
                    409:         address_entry = self.address.get_address_entry()
                    410:         box.pack_start(address_entry, False)
                    411:         box.reorder_child(address_entry, 5)
                    412: 
                    413:     def create_bottom_buttons(self, box):
                    414:         radios = (
                    415:             ("Registers", Constants.REGISTERS),
                    416:             ("Memdump", Constants.MEMDUMP),
                    417:             ("Disasm", Constants.DISASM)
                    418:         )
                    419:         group = None
                    420:         for label, mode in radios:
                    421:             button = gtk.RadioButton(group, label)
                    422:             if not group:
                    423:                 group = button
                    424:             button.connect("toggled", self.dumpmode_cb, mode)
                    425:             button.unset_flags(gtk.CAN_FOCUS)
                    426:             box.add(button)
                    427:         group.set_active(True)
                    428: 
                    429:         dialogs = (
                    430:             ("Memload...", self.memload_cb),
                    431:             ("Memsave...", self.memsave_cb),
                    432:             ("Options...", self.options_cb)
                    433:         )
                    434:         for label, cb in dialogs:
                    435:             button = create_button(label, cb)
                    436:             box.add(button)
                    437: 
                    438:     def stop_cb(self, widget):
                    439:         if widget.get_active():
                    440:             self.hatari.pause()
                    441:             self.address.clear()
                    442:             self.address.dump()
                    443:         else:
                    444:             self.hatari.unpause()
                    445: 
                    446:     def dumpmode_cb(self, widget, mode):
                    447:         if widget.get_active():
                    448:             self.address.set_dumpmode(mode)
                    449: 
                    450:     def key_event_cb(self, widget, event):
                    451:         if event.keyval in self.keys:
                    452:             self.address.dump(None, self.keys[event.keyval])
                    453: 
                    454:     def set_address_offset(self, widget, move_idx):
                    455:         self.address.dump(None, move_idx)
                    456:     
                    457:     def monitor_cb(self, widget):
                    458:         TodoDialog(self.window).run("add register / memory address range monitor window.")
                    459: 
                    460:     def memload_cb(self, widget):
                    461:         if not self.dialog_load:
                    462:             self.dialog_load = LoadDialog(self.window)
                    463:         (filename, address) = self.dialog_load.run(self.address.get())
                    464:         if filename and address:
1.1.1.2 ! root      465:             self.hatari.debug_command("l %s $%06x" % (filename, address))
1.1       root      466: 
                    467:     def memsave_cb(self, widget):
                    468:         if not self.dialog_save:
                    469:             self.dialog_save = SaveDialog(self.window)
                    470:         (filename, address, length) = self.dialog_save.run(self.address.get())
                    471:         if filename and address and length:
1.1.1.2 ! root      472:             self.hatari.debug_command("s %s $%06x $%06x" % (filename, address, length))
1.1       root      473:         
                    474:     def options_cb(self, widget):
                    475:         if not self.dialog_options:
                    476:             self.dialog_options = OptionsDialog(self.window)
                    477:         lines = self.dialog_options.run(self.config.get("[General]", "nLines"))
                    478:         if lines:
                    479:             self.config.set("[General]", "nLines", lines)
                    480:             self.address.set_lines(lines)
                    481: 
                    482:     def load_options(self):
                    483:         # TODO: move config to MemoryAddress class?
                    484:         # (depends on how monitoring of addresses should work)
                    485:         lines = self.address.get_lines()
                    486:         miss_is_error = False # needed for adding windows
                    487:         defaults = { "[General]": {"nLines": lines} }
                    488:         self.config = ConfigStore("debugui.cfg", defaults, miss_is_error)
                    489:         self.address.set_lines(self.config.get("[General]", "nLines"))
                    490:     
                    491:     def save_options(self):
                    492:         self.config.save()
                    493:     
                    494:     def show(self):
                    495:         self.stop_button.set_active(True)
                    496:         self.window.show_all()
                    497:         self.window.deiconify()
                    498: 
                    499:     def hide(self, widget, arg):
                    500:         self.window.hide()
                    501:         self.stop_button.set_active(False)
                    502:         self.save_options()
                    503:         return True
                    504: 
                    505:     def quit(self, widget, arg):
                    506:         KillDialog(self.window).run(self.hatari)
                    507:         gtk.main_quit()
                    508: 
                    509: 
                    510: def main():
                    511:     import sys
                    512:     from hatari import Hatari
                    513:     hatariobj = Hatari()
                    514:     if len(sys.argv) > 1:
                    515:         if sys.argv[1] in ("-h", "--help"):
                    516:             print "usage: %s [hatari options]" % os.path.basename(sys.argv[0])
                    517:             return
                    518:         args = sys.argv[1:]
                    519:     else:
                    520:         args = None
                    521:     hatariobj.run(args)
                    522: 
                    523:     info = UInfo()
                    524:     debugui = HatariDebugUI(hatariobj, True)
                    525:     debugui.window.show_all()
                    526:     gtk.main()
                    527:     debugui.save_options()
                    528: 
                    529:     
                    530: if __name__ == "__main__":
                    531:     main()

unix.superglobalmegacorp.com

This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.