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