|
|
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()
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.