Annotation of hatari/python-ui/hatari-console.py, revision 1.1.1.2

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: #
1.1.1.2 ! root        8: # Copyright (C) 2008-2010 by Eero Tamminen <eerot at berlios>
1.1       root        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: 
1.1.1.2 ! root      159: class Tokens:
        !           160:     # update with: hatari -h|grep -- --|sed 's/^ *\(--[^ ]*\).*$/    "\1",/'
        !           161:     option_tokens = [
1.1       root      162:     "--help",
                    163:     "--version",
                    164:     "--confirm-quit",
                    165:     "--configfile",
                    166:     "--fast-forward",
                    167:     "--mono",
                    168:     "--monitor",
                    169:     "--fullscreen",
                    170:     "--window",
                    171:     "--grab",
                    172:     "--zoom",
1.1.1.2 ! root      173:     "--max-width",
        !           174:     "--max-height",
        !           175:     "--aspect",
1.1       root      176:     "--borders",
1.1.1.2 ! root      177:     "--frameskips",
1.1       root      178:     "--statusbar",
                    179:     "--drive-led",
                    180:     "--spec512",
                    181:     "--bpp",
                    182:     "--vdi",
                    183:     "--vdi-planes",
                    184:     "--vdi-width",
                    185:     "--vdi-height",
1.1.1.2 ! root      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",
1.1       root      197:     "--joystick",
                    198:     "--printer",
                    199:     "--midi-in",
                    200:     "--midi-out",
                    201:     "--rs232-in",
                    202:     "--rs232-out",
                    203:     "--disk-a",
                    204:     "--disk-b",
                    205:     "--slowfdc",
1.1.1.2 ! root      206:     "--protect-floppy",
        !           207:     "--protect-hd",
1.1       root      208:     "--harddrive",
                    209:     "--acsi",
1.1.1.2 ! root      210:     "--ide-master",
        !           211:     "--ide-slave",
1.1       root      212:     "--memsize",
                    213:     "--tos",
                    214:     "--cartridge",
                    215:     "--memstate",
                    216:     "--cpulevel",
                    217:     "--cpuclock",
                    218:     "--compatible",
                    219:     "--machine",
                    220:     "--blitter",
1.1.1.2 ! root      221:     "--timer-d",
1.1       root      222:     "--dsp",
                    223:     "--sound",
                    224:     "--keymap",
                    225:     "--debug",
                    226:     "--bios-intercept",
                    227:     "--trace",
                    228:     "--trace-file",
1.1.1.2 ! root      229:     "--parse",
        !           230:     "--saveconfig",
1.1       root      231:     "--log-file",
                    232:     "--log-level",
                    233:     "--alert-level",
                    234:     "--run-vbls"
1.1.1.2 ! root      235:     ]
        !           236:     shortcut_tokens = [
        !           237:     "mousegrab",
1.1       root      238:     "coldreset",
                    239:     "warmreset",
                    240:     "screenshot",
                    241:     "bosskey",
                    242:     "recanim",
                    243:     "recsound",
                    244:     "savemem"
1.1.1.2 ! root      245:     ]
        !           246:     event_tokens = [
1.1       root      247:     "doubleclick",
                    248:     "rightpress",
                    249:     "rightrelease",
                    250:     "keypress",
                    251:     "keyrelease"
1.1.1.2 ! root      252:     ]
        !           253:     device_tokens = [
1.1       root      254:     "printer",
                    255:     "rs232",
                    256:     "midi",
1.1.1.2 ! root      257:     ]
        !           258:     path_tokens = [
1.1       root      259:     "memauto",
                    260:     "memsave",
                    261:     "midiout",
                    262:     "printout",
                    263:     "soundout",
                    264:     "rs232in",
                    265:     "rs232out"
1.1.1.2 ! root      266:     ]
        !           267:     # use the long variants of the commands for clarity
        !           268:     debugger_tokens = [
1.1       root      269:     "address",
                    270:     "breakpoint",
1.1.1.2 ! root      271:     "cd",
1.1       root      272:     "cont",
                    273:     "cpureg",
                    274:     "disasm",
                    275:     "dspaddress",
                    276:     "dspbreak",
                    277:     "dspcont",
                    278:     "dspdisasm",
                    279:     "dspmemdump",
                    280:     "dspreg",
1.1.1.2 ! root      281:     "dspsymbols",
        !           282:     "evaluate",
        !           283:     "exec",
1.1       root      284:     "help",
1.1.1.2 ! root      285:     "info",
1.1       root      286:     "loadbin",
1.1.1.2 ! root      287:     "lock",
1.1       root      288:     "logfile",
                    289:     "memdump",
                    290:     "memwrite",
1.1.1.2 ! root      291:     "parse",
1.1       root      292:     "savebin",
1.1.1.2 ! root      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
1.1       root      320: 
1.1.1.2 ! root      321:     def show_help(self):
        !           322:         print """
1.1       root      323: Hatari-console help
                    324: -------------------
                    325: 
1.1.1.2 ! root      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 """
1.1       root      338: and commands to "pause", "unpause" and "quit" Hatari.
                    339: 
1.1.1.2 ! root      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.
1.1       root      345: """
                    346: 
1.1.1.2 ! root      347:     def list_items(self, title, items):
        !           348:         print "\n%s:" % title
        !           349:         for item in items:
        !           350:             print "*", item
1.1       root      351: 
1.1.1.2 ! root      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():
1.1       root      366:             print "Exiting as there's no Hatari (anymore)..."
                    367:             sys.exit(0)
                    368:         if not line:
1.1.1.2 ! root      369:             return
        !           370: 
        !           371:         first = line.split()[0]
1.1       root      372:         # multiple items
1.1.1.2 ! root      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)
1.1       root      383:         # single item
1.1.1.2 ! root      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]()
1.1       root      390:         else:
                    391:             print "ERROR: unknown hatari-console command:", line
                    392: 
                    393: 
1.1.1.2 ! root      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: 
1.1       root      467: if __name__ == "__main__":
1.1.1.2 ! root      468:     Main().run()

unix.superglobalmegacorp.com

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