mirror of
https://github.com/Wiimpathy/HatariWii.git
synced 2024-11-26 03:24:18 +01:00
572 lines
19 KiB
Python
572 lines
19 KiB
Python
|
#!/usr/bin/env python
|
||
|
#
|
||
|
# A Debug UI for the Hatari, part of PyGtk Hatari UI
|
||
|
#
|
||
|
# Copyright (C) 2008-2011 by Eero Tamminen
|
||
|
#
|
||
|
# This program is free software; you can redistribute it and/or modify
|
||
|
# it under the terms of the GNU General Public License as published by
|
||
|
# the Free Software Foundation; either version 2 of the License, or
|
||
|
# (at your option) any later version.
|
||
|
#
|
||
|
# This program is distributed in the hope that it will be useful,
|
||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
|
# GNU General Public License for more details.
|
||
|
|
||
|
import os
|
||
|
# use correct version of pygtk/gtk
|
||
|
import pygtk
|
||
|
pygtk.require('2.0')
|
||
|
import gtk
|
||
|
import pango
|
||
|
|
||
|
from config import ConfigStore
|
||
|
from uihelpers import UInfo, create_button, create_toggle, \
|
||
|
create_table_dialog, table_add_entry_row, table_add_widget_row, \
|
||
|
get_save_filename, FselEntry
|
||
|
from dialogs import TodoDialog, ErrorDialog, AskDialog, KillDialog
|
||
|
|
||
|
|
||
|
def dialog_apply_cb(widget, dialog):
|
||
|
dialog.response(gtk.RESPONSE_APPLY)
|
||
|
|
||
|
|
||
|
# -------------
|
||
|
# Table dialogs
|
||
|
|
||
|
class SaveDialog:
|
||
|
def __init__(self, parent):
|
||
|
table, self.dialog = create_table_dialog(parent, "Save from memory", 3, 2)
|
||
|
self.file = FselEntry(self.dialog)
|
||
|
table_add_widget_row(table, 0, "File name:", self.file.get_container())
|
||
|
self.address = table_add_entry_row(table, 1, "Save address:", 6)
|
||
|
self.address.connect("activate", dialog_apply_cb, self.dialog)
|
||
|
self.length = table_add_entry_row(table, 2, "Number of bytes:", 6)
|
||
|
self.length.connect("activate", dialog_apply_cb, self.dialog)
|
||
|
|
||
|
def run(self, address):
|
||
|
"run(address) -> (filename,address,length), all as strings"
|
||
|
if address:
|
||
|
self.address.set_text("%06X" % address)
|
||
|
self.dialog.show_all()
|
||
|
filename = length = None
|
||
|
while 1:
|
||
|
response = self.dialog.run()
|
||
|
if response == gtk.RESPONSE_APPLY:
|
||
|
filename = self.file.get_filename()
|
||
|
address_txt = self.address.get_text()
|
||
|
length_txt = self.length.get_text()
|
||
|
if filename and address_txt and length_txt:
|
||
|
try:
|
||
|
address = int(address_txt, 16)
|
||
|
except ValueError:
|
||
|
ErrorDialog(self.dialog).run("address needs to be in hex")
|
||
|
continue
|
||
|
try:
|
||
|
length = int(length_txt)
|
||
|
except ValueError:
|
||
|
ErrorDialog(self.dialog).run("length needs to be a number")
|
||
|
continue
|
||
|
if os.path.exists(filename):
|
||
|
question = "File:\n%s\nexists, replace?" % filename
|
||
|
if not AskDialog(self.dialog).run(question):
|
||
|
continue
|
||
|
break
|
||
|
else:
|
||
|
ErrorDialog(self.dialog).run("please fill the field(s)")
|
||
|
else:
|
||
|
break
|
||
|
self.dialog.hide()
|
||
|
return (filename, address, length)
|
||
|
|
||
|
|
||
|
class LoadDialog:
|
||
|
def __init__(self, parent):
|
||
|
chooser = gtk.FileChooserButton('Select a File')
|
||
|
chooser.set_local_only(True) # Hatari cannot access URIs
|
||
|
chooser.set_width_chars(12)
|
||
|
table, self.dialog = create_table_dialog(parent, "Load to memory", 2, 2)
|
||
|
self.file = table_add_widget_row(table, 0, "File name:", chooser)
|
||
|
self.address = table_add_entry_row(table, 1, "Load address:", 6)
|
||
|
self.address.connect("activate", dialog_apply_cb, self.dialog)
|
||
|
|
||
|
def run(self, address):
|
||
|
"run(address) -> (filename,address), all as strings"
|
||
|
if address:
|
||
|
self.address.set_text("%06X" % address)
|
||
|
self.dialog.show_all()
|
||
|
filename = None
|
||
|
while 1:
|
||
|
response = self.dialog.run()
|
||
|
if response == gtk.RESPONSE_APPLY:
|
||
|
filename = self.file.get_filename()
|
||
|
address_txt = self.address.get_text()
|
||
|
if filename and address_txt:
|
||
|
try:
|
||
|
address = int(address_txt, 16)
|
||
|
except ValueError:
|
||
|
ErrorDialog(self.dialog).run("address needs to be in hex")
|
||
|
continue
|
||
|
break
|
||
|
else:
|
||
|
ErrorDialog(self.dialog).run("please fill the field(s)")
|
||
|
else:
|
||
|
break
|
||
|
self.dialog.hide()
|
||
|
return (filename, address)
|
||
|
|
||
|
|
||
|
class OptionsDialog:
|
||
|
def __init__(self, parent):
|
||
|
self.dialog = gtk.Dialog("Debugger UI options", parent,
|
||
|
gtk.DIALOG_MODAL | gtk.DIALOG_DESTROY_WITH_PARENT,
|
||
|
(gtk.STOCK_APPLY, gtk.RESPONSE_APPLY,
|
||
|
gtk.STOCK_CLOSE, gtk.RESPONSE_CLOSE))
|
||
|
|
||
|
self.lines = gtk.Adjustment(0, 5, 50)
|
||
|
scale = gtk.HScale(self.lines)
|
||
|
scale.set_digits(0)
|
||
|
|
||
|
self.follow_pc = gtk.CheckButton("On stop, set address to PC")
|
||
|
|
||
|
vbox = self.dialog.vbox
|
||
|
vbox.add(gtk.Label("Memdump/disasm lines:"))
|
||
|
vbox.add(scale)
|
||
|
vbox.add(self.follow_pc)
|
||
|
vbox.show_all()
|
||
|
|
||
|
def run(self, lines, follow_pc):
|
||
|
"run(lines,follow_pc) -> (lines,follow_pc)"
|
||
|
self.follow_pc.set_active(follow_pc)
|
||
|
self.lines.set_value(lines)
|
||
|
self.dialog.show_all()
|
||
|
response = self.dialog.run()
|
||
|
if response == gtk.RESPONSE_APPLY:
|
||
|
lines = int(self.lines.get_value())
|
||
|
follow_pc = self.follow_pc.get_active()
|
||
|
self.dialog.hide()
|
||
|
return (lines, follow_pc)
|
||
|
|
||
|
|
||
|
# ----------------------------------------------------
|
||
|
|
||
|
# constants for the other classes
|
||
|
class Constants:
|
||
|
# dump modes
|
||
|
DISASM = 1
|
||
|
MEMDUMP = 2
|
||
|
REGISTERS = 3
|
||
|
# move IDs
|
||
|
MOVE_MIN = 1
|
||
|
MOVE_MED = 2
|
||
|
MOVE_MAX = 3
|
||
|
|
||
|
|
||
|
# class for the memory address entry, view (label) and
|
||
|
# the logic for memory dump modes and moving in memory
|
||
|
class MemoryAddress:
|
||
|
# class variables
|
||
|
debug_output = None
|
||
|
hatari = None
|
||
|
|
||
|
def __init__(self, hatariobj):
|
||
|
# hatari
|
||
|
self.debug_output = hatariobj.open_debug_output()
|
||
|
self.hatari = hatariobj
|
||
|
# widgets
|
||
|
self.entry, self.memory = self.create_widgets()
|
||
|
# settings
|
||
|
self.dumpmode = Constants.REGISTERS
|
||
|
self.follow_pc = True
|
||
|
self.lines = 12
|
||
|
# addresses
|
||
|
self.first = None
|
||
|
self.second = None
|
||
|
self.last = None
|
||
|
|
||
|
def clear(self):
|
||
|
if self.follow_pc:
|
||
|
# get first address from PC when next stopped
|
||
|
self.first = None
|
||
|
self.second = None
|
||
|
self.last = None
|
||
|
|
||
|
def create_widgets(self):
|
||
|
entry = gtk.Entry(6)
|
||
|
entry.set_width_chars(6)
|
||
|
entry.connect("activate", self._entry_cb)
|
||
|
memory = gtk.Label()
|
||
|
mono = pango.FontDescription("monospace")
|
||
|
memory.modify_font(mono)
|
||
|
entry.modify_font(mono)
|
||
|
return (entry, memory)
|
||
|
|
||
|
def _entry_cb(self, widget):
|
||
|
try:
|
||
|
address = int(widget.get_text(), 16)
|
||
|
except ValueError:
|
||
|
ErrorDialog(widget.get_toplevel()).run("invalid address")
|
||
|
return
|
||
|
self.dump(address)
|
||
|
|
||
|
def reset_entry(self):
|
||
|
self.entry.set_text("%06X" % self.first)
|
||
|
|
||
|
def get(self):
|
||
|
return self.first
|
||
|
|
||
|
def get_memory_label(self):
|
||
|
return self.memory
|
||
|
|
||
|
def get_address_entry(self):
|
||
|
return self.entry
|
||
|
|
||
|
def get_follow_pc(self):
|
||
|
return self.follow_pc
|
||
|
|
||
|
def set_follow_pc(self, follow_pc):
|
||
|
self.follow_pc = follow_pc
|
||
|
|
||
|
def get_lines(self):
|
||
|
return self.lines
|
||
|
|
||
|
def set_lines(self, lines):
|
||
|
self.lines = lines
|
||
|
|
||
|
def set_dumpmode(self, mode):
|
||
|
self.dumpmode = mode
|
||
|
self.dump()
|
||
|
|
||
|
def dump(self, address = None, move_idx = 0):
|
||
|
if self.dumpmode == Constants.REGISTERS:
|
||
|
output = self._get_registers()
|
||
|
self.memory.set_label("".join(output))
|
||
|
return
|
||
|
|
||
|
if not address:
|
||
|
if not self.first:
|
||
|
self._get_registers()
|
||
|
address = self.first
|
||
|
|
||
|
if not address:
|
||
|
print("ERROR: address needed")
|
||
|
return
|
||
|
|
||
|
if self.dumpmode == Constants.MEMDUMP:
|
||
|
output = self._get_memdump(address, move_idx)
|
||
|
elif self.dumpmode == Constants.DISASM:
|
||
|
output = self._get_disasm(address, move_idx)
|
||
|
else:
|
||
|
print("ERROR: unknown dumpmode:", self.dumpmode)
|
||
|
return
|
||
|
self.memory.set_label("".join(output))
|
||
|
if move_idx:
|
||
|
self.reset_entry()
|
||
|
|
||
|
def _get_registers(self):
|
||
|
self.hatari.debug_command("r")
|
||
|
output = self.hatari.get_lines(self.debug_output)
|
||
|
if not self.first:
|
||
|
# 2nd last line has first PC in 1st column, last line next PC in 2nd column
|
||
|
self.second = int(output[-1][output[-1].find(":")+2:], 16)
|
||
|
# OldUAE CPU core has ':' in both
|
||
|
offset = output[-2].find(":")
|
||
|
if offset < 0:
|
||
|
# WinUAE CPU core only in one
|
||
|
offset = output[-2].find(" ")
|
||
|
if offset < 0:
|
||
|
print("ERROR: unable to parse register dump line:\n\t'%s'", output[-2])
|
||
|
return output
|
||
|
self.first = int(output[-2][:offset], 16)
|
||
|
self.reset_entry()
|
||
|
return output
|
||
|
|
||
|
def _get_memdump(self, address, move_idx):
|
||
|
linewidth = 16
|
||
|
screenful = self.lines*linewidth
|
||
|
# no move, left/right, up/down, page up/down (no overlap)
|
||
|
offsets = [0, 2, linewidth, screenful]
|
||
|
offset = offsets[abs(move_idx)]
|
||
|
if move_idx < 0:
|
||
|
address -= offset
|
||
|
else:
|
||
|
address += offset
|
||
|
self._set_clamped(address, address+screenful)
|
||
|
self.hatari.debug_command("m $%06x-$%06x" % (self.first, self.last))
|
||
|
# get & set debugger command results
|
||
|
output = self.hatari.get_lines(self.debug_output)
|
||
|
self.second = address + linewidth
|
||
|
return output
|
||
|
|
||
|
def _get_disasm(self, address, move_idx):
|
||
|
# TODO: uses brute force i.e. ask for more lines that user has
|
||
|
# requested to be sure that the window is filled, assuming
|
||
|
# 6 bytes is largest possible instruction+args size
|
||
|
# (I don't remember anymore my m68k asm...)
|
||
|
screenful = 6*self.lines
|
||
|
# no move, left/right, up/down, page up/down
|
||
|
offsets = [0, 2, 4, screenful]
|
||
|
offset = offsets[abs(move_idx)]
|
||
|
# force one line of overlap in page up/down
|
||
|
if move_idx < 0:
|
||
|
address -= offset
|
||
|
if address < 0:
|
||
|
address = 0
|
||
|
if move_idx == -Constants.MOVE_MAX and self.second:
|
||
|
screenful = self.second - address
|
||
|
else:
|
||
|
if move_idx == Constants.MOVE_MED and self.second:
|
||
|
address = self.second
|
||
|
elif move_idx == Constants.MOVE_MAX and self.last:
|
||
|
address = self.last
|
||
|
else:
|
||
|
address += offset
|
||
|
self._set_clamped(address, address+screenful)
|
||
|
self.hatari.debug_command("d $%06x-$%06x" % (self.first, self.last))
|
||
|
# get & set debugger command results
|
||
|
output = self.hatari.get_lines(self.debug_output)
|
||
|
# cut output to desired length and check new addresses
|
||
|
if len(output) > self.lines:
|
||
|
if move_idx < 0:
|
||
|
output = output[-self.lines:]
|
||
|
else:
|
||
|
output = output[:self.lines]
|
||
|
# with disasm need to re-get the addresses from the output
|
||
|
self.first = int(output[0][1:output[0].find(":")], 16)
|
||
|
self.second = int(output[1][1:output[1].find(":")], 16)
|
||
|
self.last = int(output[-1][1:output[-1].find(":")], 16)
|
||
|
return output
|
||
|
|
||
|
def _set_clamped(self, first, last):
|
||
|
"set_clamped(first,last), clamp addresses to valid address range and set them"
|
||
|
assert(first < last)
|
||
|
if first < 0:
|
||
|
last = last-first
|
||
|
first = 0
|
||
|
if last > 0xffffff:
|
||
|
first = 0xffffff - (last-first)
|
||
|
last = 0xffffff
|
||
|
self.first = first
|
||
|
self.last = last
|
||
|
|
||
|
|
||
|
# the Hatari debugger UI class and methods
|
||
|
class HatariDebugUI:
|
||
|
|
||
|
def __init__(self, hatariobj, do_destroy = False):
|
||
|
self.address = MemoryAddress(hatariobj)
|
||
|
self.hatari = hatariobj
|
||
|
# set when needed/created
|
||
|
self.dialog_load = None
|
||
|
self.dialog_save = None
|
||
|
self.dialog_options = None
|
||
|
# set when UI created
|
||
|
self.keys = None
|
||
|
self.stop_button = None
|
||
|
# set on option load
|
||
|
self.config = None
|
||
|
self.load_options()
|
||
|
# UI initialization/creation
|
||
|
self.window = self.create_ui("Hatari Debug UI", do_destroy)
|
||
|
|
||
|
def create_ui(self, title, do_destroy):
|
||
|
# buttons at top
|
||
|
hbox1 = gtk.HBox()
|
||
|
self.create_top_buttons(hbox1)
|
||
|
|
||
|
# disasm/memory dump at the middle
|
||
|
align = gtk.Alignment()
|
||
|
# top, bottom, left, right padding
|
||
|
align.set_padding(8, 0, 8, 8)
|
||
|
align.add(self.address.get_memory_label())
|
||
|
|
||
|
# buttons at bottom
|
||
|
hbox2 = gtk.HBox()
|
||
|
self.create_bottom_buttons(hbox2)
|
||
|
|
||
|
# their container
|
||
|
vbox = gtk.VBox()
|
||
|
vbox.pack_start(hbox1, False)
|
||
|
vbox.pack_start(align, True, True)
|
||
|
vbox.pack_start(hbox2, False)
|
||
|
|
||
|
# and the window for all of this
|
||
|
window = gtk.Window(gtk.WINDOW_TOPLEVEL)
|
||
|
window.set_events(gtk.gdk.KEY_RELEASE_MASK)
|
||
|
window.connect("key_release_event", self.key_event_cb)
|
||
|
if do_destroy:
|
||
|
window.connect("delete_event", self.quit)
|
||
|
else:
|
||
|
window.connect("delete_event", self.hide)
|
||
|
window.set_icon_from_file(UInfo.icon)
|
||
|
window.set_title(title)
|
||
|
window.add(vbox)
|
||
|
return window
|
||
|
|
||
|
def create_top_buttons(self, box):
|
||
|
self.stop_button = create_toggle("Stop", self.stop_cb)
|
||
|
box.add(self.stop_button)
|
||
|
|
||
|
monitor = create_button("Monitor...", self.monitor_cb)
|
||
|
box.add(monitor)
|
||
|
|
||
|
buttons = (
|
||
|
("<<<", "Page_Up", -Constants.MOVE_MAX),
|
||
|
("<<", "Up", -Constants.MOVE_MED),
|
||
|
("<", "Left", -Constants.MOVE_MIN),
|
||
|
(">", "Right", Constants.MOVE_MIN),
|
||
|
(">>", "Down", Constants.MOVE_MED),
|
||
|
(">>>", "Page_Down", Constants.MOVE_MAX)
|
||
|
)
|
||
|
self.keys = {}
|
||
|
for label, keyname, offset in buttons:
|
||
|
button = create_button(label, self.set_address_offset, offset)
|
||
|
keyval = gtk.gdk.keyval_from_name(keyname)
|
||
|
self.keys[keyval] = offset
|
||
|
box.add(button)
|
||
|
|
||
|
# to middle of <<>> buttons
|
||
|
address_entry = self.address.get_address_entry()
|
||
|
box.pack_start(address_entry, False)
|
||
|
box.reorder_child(address_entry, 5)
|
||
|
|
||
|
def create_bottom_buttons(self, box):
|
||
|
radios = (
|
||
|
("Registers", Constants.REGISTERS),
|
||
|
("Memdump", Constants.MEMDUMP),
|
||
|
("Disasm", Constants.DISASM)
|
||
|
)
|
||
|
group = None
|
||
|
for label, mode in radios:
|
||
|
button = gtk.RadioButton(group, label)
|
||
|
if not group:
|
||
|
group = button
|
||
|
button.connect("toggled", self.dumpmode_cb, mode)
|
||
|
button.unset_flags(gtk.CAN_FOCUS)
|
||
|
box.add(button)
|
||
|
group.set_active(True)
|
||
|
|
||
|
dialogs = (
|
||
|
("Memload...", self.memload_cb),
|
||
|
("Memsave...", self.memsave_cb),
|
||
|
("Options...", self.options_cb)
|
||
|
)
|
||
|
for label, cb in dialogs:
|
||
|
button = create_button(label, cb)
|
||
|
box.add(button)
|
||
|
|
||
|
def stop_cb(self, widget):
|
||
|
if widget.get_active():
|
||
|
self.hatari.pause()
|
||
|
self.address.clear()
|
||
|
self.address.dump()
|
||
|
else:
|
||
|
self.hatari.unpause()
|
||
|
|
||
|
def dumpmode_cb(self, widget, mode):
|
||
|
if widget.get_active():
|
||
|
self.address.set_dumpmode(mode)
|
||
|
|
||
|
def key_event_cb(self, widget, event):
|
||
|
if event.keyval in self.keys:
|
||
|
self.address.dump(None, self.keys[event.keyval])
|
||
|
|
||
|
def set_address_offset(self, widget, move_idx):
|
||
|
self.address.dump(None, move_idx)
|
||
|
|
||
|
def monitor_cb(self, widget):
|
||
|
TodoDialog(self.window).run("add register / memory address range monitor window.")
|
||
|
|
||
|
def memload_cb(self, widget):
|
||
|
if not self.dialog_load:
|
||
|
self.dialog_load = LoadDialog(self.window)
|
||
|
(filename, address) = self.dialog_load.run(self.address.get())
|
||
|
if filename and address:
|
||
|
self.hatari.debug_command("l %s $%06x" % (filename, address))
|
||
|
|
||
|
def memsave_cb(self, widget):
|
||
|
if not self.dialog_save:
|
||
|
self.dialog_save = SaveDialog(self.window)
|
||
|
(filename, address, length) = self.dialog_save.run(self.address.get())
|
||
|
if filename and address and length:
|
||
|
self.hatari.debug_command("s %s $%06x $%06x" % (filename, address, length))
|
||
|
|
||
|
def options_cb(self, widget):
|
||
|
if not self.dialog_options:
|
||
|
self.dialog_options = OptionsDialog(self.window)
|
||
|
old_lines = self.config.get("[General]", "nLines")
|
||
|
old_follow_pc = self.config.get("[General]", "bFollowPC")
|
||
|
lines, follow_pc = self.dialog_options.run(old_lines, old_follow_pc)
|
||
|
if lines != old_lines:
|
||
|
self.config.set("[General]", "nLines", lines)
|
||
|
self.address.set_lines(lines)
|
||
|
if follow_pc != old_follow_pc:
|
||
|
self.config.set("[General]", "bFollowPC", follow_pc)
|
||
|
self.address.set_follow_pc(follow_pc)
|
||
|
|
||
|
def load_options(self):
|
||
|
# TODO: move config to MemoryAddress class?
|
||
|
# (depends on how monitoring of addresses should work)
|
||
|
lines = self.address.get_lines()
|
||
|
follow_pc = self.address.get_follow_pc()
|
||
|
miss_is_error = False # needed for adding windows
|
||
|
defaults = {
|
||
|
"[General]": {
|
||
|
"nLines": lines,
|
||
|
"bFollowPC": follow_pc
|
||
|
}
|
||
|
}
|
||
|
userconfdir = ".hatari"
|
||
|
config = ConfigStore(userconfdir, defaults, miss_is_error)
|
||
|
configpath = config.get_filepath("debugui.cfg")
|
||
|
config.load(configpath) # set defaults
|
||
|
try:
|
||
|
self.address.set_lines(config.get("[General]", "nLines"))
|
||
|
self.address.set_follow_pc(config.get("[General]", "bFollowPC"))
|
||
|
except (KeyError, AttributeError):
|
||
|
ErrorDialog(None).run("Debug UI configuration mismatch!\nTry again after removing: '%s'." % configpath)
|
||
|
self.config = config
|
||
|
|
||
|
def save_options(self):
|
||
|
self.config.save()
|
||
|
|
||
|
def show(self):
|
||
|
self.stop_button.set_active(True)
|
||
|
self.window.show_all()
|
||
|
self.window.deiconify()
|
||
|
|
||
|
def hide(self, widget, arg):
|
||
|
self.window.hide()
|
||
|
self.stop_button.set_active(False)
|
||
|
self.save_options()
|
||
|
return True
|
||
|
|
||
|
def quit(self, widget, arg):
|
||
|
KillDialog(self.window).run(self.hatari)
|
||
|
gtk.main_quit()
|
||
|
|
||
|
|
||
|
def main():
|
||
|
import sys
|
||
|
from hatari import Hatari
|
||
|
hatariobj = Hatari()
|
||
|
if len(sys.argv) > 1:
|
||
|
if sys.argv[1] in ("-h", "--help"):
|
||
|
print("usage: %s [hatari options]" % os.path.basename(sys.argv[0]))
|
||
|
return
|
||
|
args = sys.argv[1:]
|
||
|
else:
|
||
|
args = None
|
||
|
hatariobj.run(args)
|
||
|
|
||
|
info = UInfo()
|
||
|
debugui = HatariDebugUI(hatariobj, True)
|
||
|
debugui.window.show_all()
|
||
|
gtk.main()
|
||
|
debugui.save_options()
|
||
|
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
main()
|