|
|
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)
275: self.hatari.debug_command("m %06x-%06x" % (self.first, self.last))
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)
305: self.hatari.debug_command("d %06x-%06x" % (self.first, self.last))
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:
465: self.hatari.debug_command("l %s %06x" % (filename, address))
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:
472: self.hatari.debug_command("s %s %06x %06x" % (filename, address, length))
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()
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.