|
|
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-2011 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: # Python v2: ! 28: # - lacks Python v3 encoding arg for bytes() ! 29: # - input() evaluates given string and fails on empty one ! 30: if str is bytes: ! 31: def bytes(s, encoding): ! 32: return s ! 33: def input(prompt): ! 34: return raw_input(prompt) ! 35: ! 36: # Atari scancodes for special keys, needed when wanting to ! 37: # output something else than alphanumberic keys to Hatari ! 38: # (when using keypress/down/up commands) ! 39: class Scancode: ! 40: Tab = "15" ! 41: Return = "28" ! 42: Enter = "114" ! 43: Space = "57" ! 44: Delete = "83" ! 45: Backspace = "14" ! 46: Escape = "0x1" ! 47: Control = "29" ! 48: Alternate = "56" ! 49: LeftShift = "42" ! 50: RightShift = "54" ! 51: CapsLock = "53" ! 52: Insert = "82" ! 53: Home = "71" ! 54: Help = "98" ! 55: Undo = "97" ! 56: CursorUp = "72" ! 57: CursorDown = "80" ! 58: CursorLeft = "75" ! 59: CursorRight = "77" ! 60: ! 61: ! 62: # running Hatari instance ! 63: class Hatari: ! 64: controlpath = "/tmp/hatari-console-" + str(os.getpid()) + ".socket" ! 65: hataribin = "hatari" ! 66: ! 67: def __init__(self, args = None): ! 68: # collect hatari process zombies without waitpid() ! 69: signal.signal(signal.SIGCHLD, signal.SIG_IGN) ! 70: self._assert_hatari_compatibility() ! 71: self.server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) ! 72: if os.path.exists(self.controlpath): ! 73: os.unlink(self.controlpath) ! 74: self.server.bind(self.controlpath) ! 75: self.server.listen(1) ! 76: self.control = None ! 77: self.paused = False ! 78: self.interval = 0.2 ! 79: self.pid = 0 ! 80: if not self.run_hatari(args): ! 81: print("ERROR: failed to run Hatari") ! 82: sys.exit(1) ! 83: self.verbose = False ! 84: ! 85: def _assert_hatari_compatibility(self): ! 86: for line in os.popen(self.hataribin + " -h").readlines(): ! 87: if line.find("--control-socket") >= 0: ! 88: return ! 89: print("ERROR: Hatari not found or it doesn't support the required --control-socket option!") ! 90: sys.exit(-1) ! 91: ! 92: def is_running(self): ! 93: if not self.pid: ! 94: return False ! 95: try: ! 96: os.waitpid(self.pid, os.WNOHANG) ! 97: except OSError as value: ! 98: print("Hatari PID %d had exited in the meanwhile:\n\t%s" % (self.pid, value)) ! 99: self.pid = 0 ! 100: return False ! 101: return True ! 102: ! 103: def run_hatari(self, args): ! 104: if self.control: ! 105: print("ERROR: Hatari is already running, stop it first") ! 106: return ! 107: pid = os.fork() ! 108: if pid < 0: ! 109: print("ERROR: fork()ing Hatari failed!") ! 110: return ! 111: if pid: ! 112: # in parent ! 113: self.pid = pid ! 114: print("WAIT hatari to connect to control socket...") ! 115: (self.control, addr) = self.server.accept() ! 116: print("connected!") ! 117: return self.control ! 118: else: ! 119: # child runs Hatari ! 120: allargs = [self.hataribin, "--control-socket", self.controlpath] + args ! 121: print("RUN:", allargs) ! 122: os.execvp(self.hataribin, allargs) ! 123: ! 124: def send_message(self, msg, fast = False): ! 125: if self.control: ! 126: if self.verbose: ! 127: print("-> '%s'" % msg) ! 128: self.control.sendall(bytes(msg + "\n", "ASCII")) ! 129: # KLUDGE: wait so that Hatari output comes before next prompt ! 130: if fast: ! 131: interval = self.interval/2 ! 132: else: ! 133: interval = self.interval ! 134: time.sleep(interval) ! 135: return True ! 136: else: ! 137: print("ERROR: no Hatari (control socket)") ! 138: return False ! 139: ! 140: def change_option(self, option): ! 141: return self.send_message("hatari-option %s" % option) ! 142: ! 143: def trigger_shortcut(self, shortcut): ! 144: return self.send_message("hatari-shortcut %s" % shortcut) ! 145: ! 146: def send_string(self, text): ! 147: print("string:", text) ! 148: for item in text: ! 149: if item == ' ': ! 150: # white space gets stripped, use scancode instead ! 151: item = "57" ! 152: if not self.send_message("hatari-event keypress %s" % item, True): ! 153: return False ! 154: return True ! 155: ! 156: def insert_event(self, event): ! 157: if event.startswith("text "): ! 158: cmd, value = event.split(None, 1) ! 159: if value: ! 160: return self.send_string(value) ! 161: return self.send_message("hatari-event %s" % event, True) ! 162: ! 163: def debug_command(self, cmd): ! 164: return self.send_message("hatari-debug %s" % cmd) ! 165: ! 166: def change_path(self, path): ! 167: return self.send_message("hatari-path %s" % path) ! 168: ! 169: def toggle_device(self, device): ! 170: return self.send_message("hatari-toggle %s" % device) ! 171: ! 172: def toggle_pause(self): ! 173: self.paused = not self.paused ! 174: if self.paused: ! 175: return self.send_message("hatari-stop") ! 176: else: ! 177: return self.send_message("hatari-cont") ! 178: ! 179: def toggle_verbose(self): ! 180: self.verbose = not self.verbose ! 181: print("debug output", self.verbose) ! 182: ! 183: def kill_hatari(self): ! 184: if self.pid: ! 185: os.kill(self.pid, signal.SIGKILL) ! 186: print("killed hatari with PID %d" % self.pid) ! 187: self.control = None ! 188: self.pid = 0 ! 189: ! 190: ! 191: # command line parsing with readline ! 192: class CommandInput: ! 193: prompt = "hatari-command: " ! 194: historysize = 99 ! 195: ! 196: def __init__(self, commands): ! 197: readline.set_history_length(self.historysize) ! 198: readline.parse_and_bind("tab: complete") ! 199: readline.set_completer_delims(" \t\r\n") ! 200: readline.set_completer(self.complete) ! 201: self.commands = commands ! 202: ! 203: def complete(self, text, state): ! 204: idx = 0 ! 205: #print "text: '%s', state '%d'" % (text, state) ! 206: for cmd in self.commands: ! 207: if cmd.startswith(text): ! 208: idx += 1 ! 209: if idx > state: ! 210: return cmd ! 211: ! 212: def loop(self): ! 213: try: ! 214: rawline = input(self.prompt) ! 215: return rawline ! 216: except EOFError: ! 217: return "" ! 218: ! 219: ! 220: class Tokens: ! 221: # update with: hatari -h|grep -- --|sed 's/^ *\(--[^ ]*\).*$/ "\1",/' ! 222: option_tokens = [ ! 223: "--help", ! 224: "--version", ! 225: "--confirm-quit", ! 226: "--configfile", ! 227: "--fast-forward", ! 228: "--mono", ! 229: "--monitor", ! 230: "--fullscreen", ! 231: "--window", ! 232: "--grab", ! 233: "--zoom", ! 234: "--max-width", ! 235: "--max-height", ! 236: "--aspect", ! 237: "--borders", ! 238: "--frameskips", ! 239: "--statusbar", ! 240: "--drive-led", ! 241: "--spec512", ! 242: "--bpp", ! 243: "--vdi", ! 244: "--vdi-planes", ! 245: "--vdi-width", ! 246: "--vdi-height", ! 247: "--avirecord", ! 248: "--avi-vcodec", ! 249: "--avi-fps", ! 250: "--avi-crop", ! 251: "--avi-file", ! 252: "--joy0", ! 253: "--joy1", ! 254: "--joy2", ! 255: "--joy3", ! 256: "--joy4", ! 257: "--joy5", ! 258: "--joystick", ! 259: "--printer", ! 260: "--midi-in", ! 261: "--midi-out", ! 262: "--rs232-in", ! 263: "--rs232-out", ! 264: "--disk-a", ! 265: "--disk-b", ! 266: "--slowfdc", ! 267: "--protect-floppy", ! 268: "--protect-hd", ! 269: "--harddrive", ! 270: "--acsi", ! 271: "--ide-master", ! 272: "--ide-slave", ! 273: "--memsize", ! 274: "--tos", ! 275: "--cartridge", ! 276: "--memstate", ! 277: "--cpulevel", ! 278: "--cpuclock", ! 279: "--compatible", ! 280: "--machine", ! 281: "--blitter", ! 282: "--timer-d", ! 283: "--dsp", ! 284: "--sound", ! 285: "--keymap", ! 286: "--debug", ! 287: "--bios-intercept", ! 288: "--trace", ! 289: "--trace-file", ! 290: "--parse", ! 291: "--saveconfig", ! 292: "--log-file", ! 293: "--log-level", ! 294: "--alert-level", ! 295: "--run-vbls" ! 296: ] ! 297: shortcut_tokens = [ ! 298: "mousegrab", ! 299: "coldreset", ! 300: "warmreset", ! 301: "screenshot", ! 302: "bosskey", ! 303: "recanim", ! 304: "recsound", ! 305: "savemem" ! 306: ] ! 307: event_tokens = [ ! 308: "doubleclick", ! 309: "rightdown", ! 310: "rightup", ! 311: "keypress", ! 312: "keydown", ! 313: "keyup", ! 314: "text" # simulated with keypresses ! 315: ] ! 316: device_tokens = [ ! 317: "printer", ! 318: "rs232", ! 319: "midi", ! 320: ] ! 321: path_tokens = [ ! 322: "memauto", ! 323: "memsave", ! 324: "midiout", ! 325: "printout", ! 326: "soundout", ! 327: "rs232in", ! 328: "rs232out" ! 329: ] ! 330: # use the long variants of the commands for clarity ! 331: debugger_tokens = [ ! 332: "address", ! 333: "breakpoint", ! 334: "cd", ! 335: "cont", ! 336: "cpureg", ! 337: "disasm", ! 338: "dspaddress", ! 339: "dspbreak", ! 340: "dspcont", ! 341: "dspdisasm", ! 342: "dspmemdump", ! 343: "dspreg", ! 344: "dspsymbols", ! 345: "evaluate", ! 346: "exec", ! 347: "help", ! 348: "info", ! 349: "loadbin", ! 350: "lock", ! 351: "logfile", ! 352: "memdump", ! 353: "memwrite", ! 354: "parse", ! 355: "savebin", ! 356: "setopt", ! 357: "stateload", ! 358: "statesave", ! 359: "symbols", ! 360: "trace" ! 361: ] ! 362: ! 363: def __init__(self, hatari): ! 364: self.process_tokens = { ! 365: "kill": hatari.kill_hatari, ! 366: "pause": hatari.toggle_pause ! 367: } ! 368: self.script_tokens = { ! 369: "script": self.do_script, ! 370: "sleep": self.do_sleep ! 371: } ! 372: self.help_tokens = { ! 373: "usage": self.show_help, ! 374: "verbose": hatari.toggle_verbose ! 375: } ! 376: self.hatari = hatari ! 377: ! 378: def get_tokens(self): ! 379: tokens = [] ! 380: for items in [self.option_tokens, self.shortcut_tokens, ! 381: self.event_tokens, self.debugger_tokens, self.device_tokens, ! 382: self.path_tokens, list(self.process_tokens.keys()), ! 383: list(self.script_tokens.keys()), list(self.help_tokens.keys())]: ! 384: for token in items: ! 385: if token in tokens: ! 386: print("ERROR: token '%s' already in tokens" % token) ! 387: sys.exit(1) ! 388: tokens += items ! 389: return tokens ! 390: ! 391: def show_help(self): ! 392: print(""" ! 393: Hatari-console help ! 394: ------------------- ! 395: ! 396: Hatari-console allows you to control Hatari through its control socket ! 397: from the provided console prompt, while Hatari is running. All control ! 398: commands support TAB completion on their names and options. ! 399: ! 400: The supported control facilities are:""") ! 401: self.list_items("Command line options", self.option_tokens) ! 402: self.list_items("Keyboard shortcuts", self.shortcut_tokens) ! 403: self.list_items("Event invocation", self.event_tokens) ! 404: self.list_items("Device toggling", self.device_tokens) ! 405: self.list_items("Path setting", self.path_tokens) ! 406: self.list_items("Debugger commands", self.debugger_tokens) ! 407: print(""" ! 408: "pause" toggles Hatari paused state on/off. ! 409: "kill" will terminate Hatari. ! 410: ! 411: "script" command reads commands from the given file. ! 412: "sleep" command can be used in script to wait given number of seconds. ! 413: "verbose" command toggles commands debug output on/off. ! 414: ! 415: For command line options you can get further help with "--help" ! 416: and for debugger commands with "help". Some of the other facilities ! 417: give help when you give them invalid input. ! 418: """) ! 419: ! 420: def list_items(self, title, items): ! 421: print("\n%s:" % title) ! 422: for item in items: ! 423: print("*", item) ! 424: ! 425: def do_sleep(self, line): ! 426: items = line.split()[1:] ! 427: try: ! 428: secs = int(items[0]) ! 429: except: ! 430: secs = 0 ! 431: if secs > 0: ! 432: print("Sleeping for %d secs..." % secs) ! 433: time.sleep(secs) ! 434: else: ! 435: print("usage: sleep <seconds>") ! 436: ! 437: def do_script(self, line): ! 438: try: ! 439: filename = line.split()[1] ! 440: f = open(filename) ! 441: except: ! 442: print("usage: script <filename>") ! 443: return ! 444: ! 445: for line in f.readlines(): ! 446: line = line.strip() ! 447: if not line or line[0] == '#': ! 448: continue ! 449: print(">", line) ! 450: self.process_command(line) ! 451: ! 452: def process_command(self, line): ! 453: if not self.hatari.is_running(): ! 454: print("Exiting as there's no Hatari (anymore)...") ! 455: sys.exit(0) ! 456: if not line: ! 457: return ! 458: ! 459: first = line.split()[0] ! 460: # multiple items ! 461: if first in self.event_tokens: ! 462: self.hatari.insert_event(line) ! 463: elif first in self.debugger_tokens: ! 464: self.hatari.debug_command(line) ! 465: elif first in self.option_tokens: ! 466: self.hatari.change_option(line) ! 467: elif first in self.path_tokens: ! 468: self.hatari.change_path(line) ! 469: elif first in self.script_tokens: ! 470: self.script_tokens[first](line) ! 471: # single item ! 472: elif line in self.device_tokens: ! 473: self.hatari.toggle_device(line) ! 474: elif line in self.shortcut_tokens: ! 475: self.hatari.trigger_shortcut(line) ! 476: elif line in self.process_tokens: ! 477: self.process_tokens[line]() ! 478: elif line in self.help_tokens: ! 479: self.help_tokens[line]() ! 480: else: ! 481: print("ERROR: unknown hatari-console command:", line) ! 482: ! 483: ! 484: class Main: ! 485: def __init__(self): ! 486: args, self.file, self.exit = self.parse_args(sys.argv) ! 487: hatari = Hatari(args) ! 488: self.tokens = Tokens(hatari) ! 489: self.command = CommandInput(self.tokens.get_tokens()) ! 490: ! 491: def parse_args(self, args): ! 492: if "-h" in args or "--help" in args: ! 493: self.usage() ! 494: ! 495: file = [] ! 496: exit = False ! 497: if "--" not in args: ! 498: return (args[1:], file, exit) ! 499: ! 500: for arg in args: ! 501: if arg == "--": ! 502: return (args[args.index("--")+1:], file, exit) ! 503: if arg == "--exit": ! 504: exit = True ! 505: continue ! 506: if os.path.exists(arg): ! 507: file = arg ! 508: else: ! 509: self.usage("file '%s' not found" % arg) ! 510: ! 511: def usage(self, msg=None): ! 512: name = os.path.basename(sys.argv[0]) ! 513: print("\n%s" % name) ! 514: print("=" * len(name)) ! 515: print(""" ! 516: Usage: %s [<console options/args> --] [<hatari options>] ! 517: ! 518: Hatari console options/args: ! 519: \t<file>\t\tread commands from given file ! 520: \t--exit\t\texit after executing the commands in the file ! 521: \t-h, --help\t\tthis help ! 522: ! 523: Except for help, console options/args will be interpreted ! 524: only if '--' is given as one of the arguments. Otherwise ! 525: all arguments are given to Hatari. ! 526: ! 527: For example: ! 528: %s --monitor mono test.prg ! 529: %s commands.txt -- --monitor mono ! 530: %s commands.txt --exit -- ! 531: """ % (name, name, name, name)) ! 532: if msg: ! 533: print("ERROR: %s!\n" % msg) ! 534: sys.exit(1) ! 535: ! 536: def loop(self): ! 537: print(""" ! 538: ********************************************************* ! 539: * To see available commands, use the TAB key or 'usage' * ! 540: ********************************************************* ! 541: """) ! 542: if self.file: ! 543: self.script(self.file) ! 544: if self.exit: ! 545: sys.exit(0) ! 546: ! 547: while 1: ! 548: line = self.command.loop().strip() ! 549: self.tokens.process_command(line) ! 550: ! 551: def script(self, filename): ! 552: self.tokens.do_script("script " + filename) ! 553: ! 554: def run(self, line): ! 555: "helper method for scripts using hatari-console" ! 556: self.tokens.process_command(line) ! 557: ! 558: ! 559: if __name__ == "__main__": ! 560: Main().loop()
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.