Annotation of hatari/tools/hconsole/hconsole.py, revision 1.1

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()

unix.superglobalmegacorp.com

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