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

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

unix.superglobalmegacorp.com

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