Annotation of hatari/tools/hconsole/hconsole.py, revision 1.1.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.