|
|
1.1 ! root 1: #!/usr/bin/env python ! 2: # ! 3: # Hatari console: ! 4: # Allows using Hatari shortcuts & debugger, changing paths, toggling ! 5: # devices and changing Hatari command line options (even for things you ! 6: # cannot change from the UI) from the console while Hatari is running. ! 7: # ! 8: # Copyright (C) 2008-2010 by Eero Tamminen <eerot at berlios> ! 9: # ! 10: # This program is free software; you can redistribute it and/or modify ! 11: # it under the terms of the GNU General Public License as published by ! 12: # the Free Software Foundation; either version 2 of the License, or ! 13: # (at your option) any later version. ! 14: # ! 15: # This program is distributed in the hope that it will be useful, ! 16: # but WITHOUT ANY WARRANTY; without even the implied warranty of ! 17: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ! 18: # GNU General Public License for more details. ! 19: ! 20: import os ! 21: import sys ! 22: import time ! 23: import signal ! 24: import socket ! 25: import readline ! 26: ! 27: # running Hatari instance ! 28: class Hatari: ! 29: controlpath = "/tmp/hatari-console-" + str(os.getpid()) + ".socket" ! 30: hataribin = "hatari" ! 31: ! 32: def __init__(self, args = None): ! 33: # collect hatari process zombies without waitpid() ! 34: signal.signal(signal.SIGCHLD, signal.SIG_IGN) ! 35: self._assert_hatari_compatibility() ! 36: self.server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) ! 37: if os.path.exists(self.controlpath): ! 38: os.unlink(self.controlpath) ! 39: self.server.bind(self.controlpath) ! 40: self.server.listen(1) ! 41: self.control = None ! 42: self.paused = False ! 43: self.pid = 0 ! 44: if not self.run(args): ! 45: print "ERROR: failed to run Hatari" ! 46: sys.exit(1) ! 47: ! 48: def _assert_hatari_compatibility(self): ! 49: for line in os.popen(self.hataribin + " -h").readlines(): ! 50: if line.find("--control-socket") >= 0: ! 51: return ! 52: print "ERROR: Hatari not found or it doesn't support the required --control-socket option!" ! 53: sys.exit(-1) ! 54: ! 55: def is_running(self): ! 56: if not self.pid: ! 57: return False ! 58: try: ! 59: os.waitpid(self.pid, os.WNOHANG) ! 60: except OSError, value: ! 61: print "Hatari PID %d had exited in the meanwhile:\n\t%s" % (self.pid, value) ! 62: self.pid = 0 ! 63: return False ! 64: return True ! 65: ! 66: def run(self, args): ! 67: if self.control: ! 68: print "ERROR: Hatari is already running, stop it first" ! 69: return ! 70: pid = os.fork() ! 71: if pid < 0: ! 72: print "ERROR: fork()ing Hatari failed!" ! 73: return ! 74: if pid: ! 75: # in parent ! 76: self.pid = pid ! 77: print "WAIT hatari to connect to control socket...", ! 78: (self.control, addr) = self.server.accept() ! 79: print "connected!" ! 80: return self.control ! 81: else: ! 82: # child runs Hatari ! 83: allargs = [self.hataribin, "--control-socket", self.controlpath] + args ! 84: print "RUN:", allargs ! 85: os.execvp(self.hataribin, allargs) ! 86: ! 87: def send_message(self, msg): ! 88: if self.control: ! 89: print "->", msg ! 90: self.control.sendall(msg + "\n") ! 91: # KLUDGE: wait so that Hatari output comes before next prompt ! 92: time.sleep(0.2) ! 93: return True ! 94: else: ! 95: print "ERROR: no Hatari (control socket)" ! 96: return False ! 97: ! 98: def change_option(self, option): ! 99: return self.send_message("hatari-option %s" % option) ! 100: ! 101: def trigger_shortcut(self, shortcut): ! 102: return self.send_message("hatari-shortcut %s" % shortcut) ! 103: ! 104: def insert_event(self, event): ! 105: return self.send_message("hatari-event %s" % event) ! 106: ! 107: def debug_command(self, cmd): ! 108: return self.send_message("hatari-debug %s" % cmd) ! 109: ! 110: def change_path(self, path): ! 111: return self.send_message("hatari-path %s" % path) ! 112: ! 113: def toggle_device(self, device): ! 114: return self.send_message("hatari-toggle %s" % device) ! 115: ! 116: def pause(self): ! 117: return self.send_message("hatari-stop") ! 118: ! 119: def unpause(self): ! 120: return self.send_message("hatari-cont") ! 121: ! 122: def stop(self): ! 123: if self.pid: ! 124: os.kill(self.pid, signal.SIGKILL) ! 125: print "killed hatari with PID %d" % self.pid ! 126: self.control = None ! 127: self.pid = 0 ! 128: ! 129: ! 130: # command line parsing with readline ! 131: class CommandInput: ! 132: prompt = "hatari-command: " ! 133: historysize = 99 ! 134: ! 135: def __init__(self, commands): ! 136: readline.set_history_length(self.historysize) ! 137: readline.parse_and_bind("tab: complete") ! 138: readline.set_completer_delims(" \t\r\n") ! 139: readline.set_completer(self.complete) ! 140: self.commands = commands ! 141: ! 142: def complete(self, text, state): ! 143: idx = 0 ! 144: #print "text: '%s', state '%d'" % (text, state) ! 145: for cmd in self.commands: ! 146: if cmd.startswith(text): ! 147: idx += 1 ! 148: if idx > state: ! 149: return cmd ! 150: ! 151: def loop(self): ! 152: try: ! 153: rawline = raw_input(self.prompt) ! 154: return rawline ! 155: except EOFError: ! 156: return "" ! 157: ! 158: ! 159: class Tokens: ! 160: # update with: hatari -h|grep -- --|sed 's/^ *\(--[^ ]*\).*$/ "\1",/' ! 161: option_tokens = [ ! 162: "--help", ! 163: "--version", ! 164: "--confirm-quit", ! 165: "--configfile", ! 166: "--fast-forward", ! 167: "--mono", ! 168: "--monitor", ! 169: "--fullscreen", ! 170: "--window", ! 171: "--grab", ! 172: "--zoom", ! 173: "--max-width", ! 174: "--max-height", ! 175: "--aspect", ! 176: "--borders", ! 177: "--frameskips", ! 178: "--statusbar", ! 179: "--drive-led", ! 180: "--spec512", ! 181: "--bpp", ! 182: "--vdi", ! 183: "--vdi-planes", ! 184: "--vdi-width", ! 185: "--vdi-height", ! 186: "--avirecord", ! 187: "--avi-vcodec", ! 188: "--avi-fps", ! 189: "--avi-crop", ! 190: "--avi-file", ! 191: "--joy0", ! 192: "--joy1", ! 193: "--joy2", ! 194: "--joy3", ! 195: "--joy4", ! 196: "--joy5", ! 197: "--joystick", ! 198: "--printer", ! 199: "--midi-in", ! 200: "--midi-out", ! 201: "--rs232-in", ! 202: "--rs232-out", ! 203: "--disk-a", ! 204: "--disk-b", ! 205: "--slowfdc", ! 206: "--protect-floppy", ! 207: "--protect-hd", ! 208: "--harddrive", ! 209: "--acsi", ! 210: "--ide-master", ! 211: "--ide-slave", ! 212: "--memsize", ! 213: "--tos", ! 214: "--cartridge", ! 215: "--memstate", ! 216: "--cpulevel", ! 217: "--cpuclock", ! 218: "--compatible", ! 219: "--machine", ! 220: "--blitter", ! 221: "--timer-d", ! 222: "--dsp", ! 223: "--sound", ! 224: "--keymap", ! 225: "--debug", ! 226: "--bios-intercept", ! 227: "--trace", ! 228: "--trace-file", ! 229: "--parse", ! 230: "--saveconfig", ! 231: "--log-file", ! 232: "--log-level", ! 233: "--alert-level", ! 234: "--run-vbls" ! 235: ] ! 236: shortcut_tokens = [ ! 237: "mousegrab", ! 238: "coldreset", ! 239: "warmreset", ! 240: "screenshot", ! 241: "bosskey", ! 242: "recanim", ! 243: "recsound", ! 244: "savemem" ! 245: ] ! 246: event_tokens = [ ! 247: "doubleclick", ! 248: "rightpress", ! 249: "rightrelease", ! 250: "keypress", ! 251: "keyrelease" ! 252: ] ! 253: device_tokens = [ ! 254: "printer", ! 255: "rs232", ! 256: "midi", ! 257: ] ! 258: path_tokens = [ ! 259: "memauto", ! 260: "memsave", ! 261: "midiout", ! 262: "printout", ! 263: "soundout", ! 264: "rs232in", ! 265: "rs232out" ! 266: ] ! 267: # use the long variants of the commands for clarity ! 268: debugger_tokens = [ ! 269: "address", ! 270: "breakpoint", ! 271: "cd", ! 272: "cont", ! 273: "cpureg", ! 274: "disasm", ! 275: "dspaddress", ! 276: "dspbreak", ! 277: "dspcont", ! 278: "dspdisasm", ! 279: "dspmemdump", ! 280: "dspreg", ! 281: "dspsymbols", ! 282: "evaluate", ! 283: "exec", ! 284: "help", ! 285: "info", ! 286: "loadbin", ! 287: "lock", ! 288: "logfile", ! 289: "memdump", ! 290: "memwrite", ! 291: "parse", ! 292: "savebin", ! 293: "setopt", ! 294: "stateload", ! 295: "statesave", ! 296: "symbols", ! 297: "trace" ! 298: ] ! 299: ! 300: def __init__(self, hatari): ! 301: self.process_tokens = { ! 302: "console-help": self.show_help, ! 303: "pause": hatari.pause, ! 304: "unpause": hatari.unpause, ! 305: "quit": hatari.stop ! 306: } ! 307: self.hatari = hatari ! 308: ! 309: def get_tokens(self): ! 310: tokens = [] ! 311: for items in [self.option_tokens, self.shortcut_tokens, ! 312: self.event_tokens, self.debugger_tokens, self.device_tokens, ! 313: self.path_tokens, self.process_tokens.keys()]: ! 314: for token in items: ! 315: if token in tokens: ! 316: print "ERROR: token '%s' already in tokens" % token ! 317: sys.exit(1) ! 318: tokens += items ! 319: return tokens ! 320: ! 321: def show_help(self): ! 322: print """ ! 323: Hatari-console help ! 324: ------------------- ! 325: ! 326: Hatari-console allows you to control Hatari through its control socket ! 327: from the provided console prompt, while Hatari is running. All control ! 328: commands support TAB completion on their names and options. ! 329: ! 330: The supported control facilities are:""" ! 331: self.list_items("Command line options", self.option_tokens) ! 332: self.list_items("Keyboard shortcuts", self.shortcut_tokens) ! 333: self.list_items("Event invocation", self.event_tokens) ! 334: self.list_items("Device toggling", self.device_tokens) ! 335: self.list_items("Path setting", self.path_tokens) ! 336: self.list_items("Debugger commands", self.debugger_tokens) ! 337: print """ ! 338: and commands to "pause", "unpause" and "quit" Hatari. ! 339: ! 340: Scripts can use also "sleep" command. ! 341: ! 342: For command line options you can get further help with "--help" ! 343: and for debugger with "help". Some other facilities may give ! 344: help if you give them invalid input. ! 345: """ ! 346: ! 347: def list_items(self, title, items): ! 348: print "\n%s:" % title ! 349: for item in items: ! 350: print "*", item ! 351: ! 352: def sleep(self, line): ! 353: items = line.split()[1:] ! 354: try: ! 355: secs = int(items[0]) ! 356: except: ! 357: secs = 0 ! 358: if secs > 0: ! 359: print "Sleeping for %d secs..." % secs ! 360: time.sleep(secs) ! 361: else: ! 362: print "usage: sleep <seconds>" ! 363: ! 364: def process_command(self, line): ! 365: if not self.hatari.is_running(): ! 366: print "Exiting as there's no Hatari (anymore)..." ! 367: sys.exit(0) ! 368: if not line: ! 369: return ! 370: ! 371: first = line.split()[0] ! 372: # multiple items ! 373: if first in self.event_tokens: ! 374: self.hatari.insert_event(line) ! 375: elif first in self.debugger_tokens: ! 376: self.hatari.debug_command(line) ! 377: elif first in self.option_tokens: ! 378: self.hatari.change_option(line) ! 379: elif first in self.path_tokens: ! 380: self.hatari.change_path(line) ! 381: elif first == "sleep": ! 382: self.sleep(line) ! 383: # single item ! 384: elif line in self.device_tokens: ! 385: self.hatari.toggle_device(line) ! 386: elif line in self.shortcut_tokens: ! 387: self.hatari.trigger_shortcut(line) ! 388: elif line in self.process_tokens: ! 389: self.process_tokens[line]() ! 390: else: ! 391: print "ERROR: unknown hatari-console command:", line ! 392: ! 393: ! 394: class Main: ! 395: def __init__(self): ! 396: args, self.file, self.exit = self.parse_args(sys.argv) ! 397: hatari = Hatari(args) ! 398: self.tokens = Tokens(hatari) ! 399: self.command = CommandInput(self.tokens.get_tokens()) ! 400: ! 401: def parse_args(self, args): ! 402: if "-h" in args or "--help" in args: ! 403: self.usage() ! 404: ! 405: file = [] ! 406: exit = False ! 407: if "--" not in args: ! 408: return (args, file, exit) ! 409: ! 410: for arg in args: ! 411: if arg == "--": ! 412: return (args[args.index("--")+1:], file, exit) ! 413: if arg == "--exit": ! 414: exit = True ! 415: continue ! 416: if os.path.exists(arg): ! 417: file = arg ! 418: else: ! 419: self.usage("file '%s' not found" % arg) ! 420: ! 421: def usage(self, msg=None): ! 422: name = os.path.basename(sys.argv[0]) ! 423: print "\n%s" % name ! 424: print "=" * len(name) ! 425: print """ ! 426: Usage: %s [<console options/args> --] [<hatari options>] ! 427: ! 428: Hatari console options/args: ! 429: \t<file>\t\tread commands from given file ! 430: \t--exit\t\texit after executing the commands in the file ! 431: \t-h, --help\t\tthis help ! 432: ! 433: Except for help, console options/args will be interpreted ! 434: only if '--' is given as one of the arguments. Otherwise ! 435: all arguments are given to Hatari. ! 436: ! 437: For example: ! 438: %s --monitor mono ! 439: %s commands.txt -- --monitor mono ! 440: %s commands.txt --exit -- ! 441: """ % (name, name, name, name) ! 442: if msg: ! 443: print "ERROR: %s!\n" % msg ! 444: sys.exit(1) ! 445: ! 446: def run(self): ! 447: print """ ! 448: **************************************************************** ! 449: * To see available commands, use the TAB key or 'console-help' * ! 450: **************************************************************** ! 451: """ ! 452: if self.file: ! 453: for line in open(self.file).readlines(): ! 454: line = line.strip() ! 455: if not line or line[0] == '#': ! 456: continue ! 457: print ">", line ! 458: self.tokens.process_command(line) ! 459: if self.exit: ! 460: sys.exit(0) ! 461: ! 462: while 1: ! 463: line = self.command.loop().strip() ! 464: self.tokens.process_command(line) ! 465: ! 466: ! 467: if __name__ == "__main__": ! 468: Main().run()
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.