|
|
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-2009 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: # update with: hatari -h|grep -- --|sed 's/^ *\(--[^ ]*\).*$/ "\1",/'
160: option_tokens = [
161: "--help",
162: "--version",
163: "--confirm-quit",
164: "--configfile",
165: "--fast-forward",
166: "--mono",
167: "--monitor",
168: "--fullscreen",
169: "--window",
170: "--grab",
171: "--zoom",
172: "--frameskips",
173: "--borders",
174: "--statusbar",
175: "--drive-led",
176: "--spec512",
177: "--bpp",
178: "--vdi",
179: "--vdi-planes",
180: "--vdi-width",
181: "--vdi-height",
182: "--joystick",
183: "--printer",
184: "--midi-in",
185: "--midi-out",
186: "--rs232-in",
187: "--rs232-out",
188: "--disk-a",
189: "--disk-b",
190: "--slowfdc",
191: "--harddrive",
192: "--acsi",
193: "--ide",
194: "--memsize",
195: "--tos",
196: "--cartridge",
197: "--memstate",
198: "--cpulevel",
199: "--cpuclock",
200: "--compatible",
201: "--machine",
202: "--blitter",
203: "--dsp",
204: "--sound",
205: "--keymap",
206: "--debug",
207: "--bios-intercept",
208: "--trace",
209: "--trace-file",
210: "--log-file",
211: "--log-level",
212: "--alert-level",
213: "--run-vbls"
214: ]
215: shortcut_tokens = [
216: "mousemode",
217: "coldreset",
218: "warmreset",
219: "screenshot",
220: "bosskey",
221: "recanim",
222: "recsound",
223: "savemem"
224: ]
225: event_tokens = [
226: "doubleclick",
227: "rightpress",
228: "rightrelease",
229: "keypress",
230: "keyrelease"
231: ]
232: device_tokens = [
233: "printer",
234: "rs232",
235: "midi",
236: ]
237: path_tokens = [
238: "memauto",
239: "memsave",
240: "midiout",
241: "printout",
242: "soundout",
243: "rs232in",
244: "rs232out"
245: ]
246: # use the long variants of the commands for clarity
247: debugger_tokens = [
248: "address",
249: "breakpoint",
250: "cont",
251: "cpureg",
252: "disasm",
253: "dspaddress",
254: "dspbreak",
255: "dspcont",
256: "dspdisasm",
257: "dspmemdump",
258: "dspreg",
259: "help",
260: "loadbin",
261: "logfile",
262: "memdump",
263: "memwrite",
264: "savebin",
265: "setopt"
266: ]
267:
268:
269: def list_items(title, items):
270: print "\n%s:" % title
271: for item in items:
272: print "*", item
273:
274: def show_help():
275: print """
276: Hatari-console help
277: -------------------
278:
279: Hatari-console allows you to invoke Hatari remote configuration facilities
280: from console while Hatari is running and use TAB completion on their names.
281: The supported facilities are:"""
282: list_items("Command line options", option_tokens)
283: list_items("Keyboard shortcuts", shortcut_tokens)
284: list_items("Event invocation", event_tokens)
285: list_items("Device toggling", device_tokens)
286: list_items("Path setting", path_tokens)
287: list_items("Debugger commands", debugger_tokens)
288: print """
289: and commands to "pause", "unpause" and "quit" Hatari.
290:
291: For command line options you can see further help with "--help"
292: and for debugger with "h". Some other facilities may give help
293: if you give them invalid input.
294: """
295:
296:
297: def main():
298: hatari = Hatari(sys.argv[1:])
299: process_tokens = {
300: "pause": hatari.pause,
301: "unpause": hatari.unpause,
302: "quit": hatari.stop
303: }
304:
305: print "************************************************************"
306: print "* Use the TAB key to see all the available Hatari commands *"
307: print "************************************************************"
308: tokens = ["console-help"]
309: for items in [option_tokens, shortcut_tokens, event_tokens,
310: debugger_tokens, device_tokens, path_tokens,
311: process_tokens.keys()]:
312: for token in items:
313: if token in tokens:
314: print "ERROR: token '%s' already in tokens" % token
315: sys.exit(1)
316: tokens += items
317: command = CommandInput(tokens)
318:
319: while 1:
320: line = command.loop().strip()
321: if not hatari.is_running():
322: print "Exiting as there's no Hatari (anymore)..."
323: sys.exit(0)
324: if not line:
325: continue
326: first = line.split(" ")[0]
327: # multiple items
328: if first in event_tokens:
329: hatari.insert_event(line)
330: elif first in debugger_tokens:
331: hatari.debug_command(line)
332: elif first in option_tokens:
333: hatari.change_option(line)
334: elif first in path_tokens:
335: hatari.change_path(line)
336: # single item
337: elif line in device_tokens:
338: hatari.toggle_device(line)
339: elif line in shortcut_tokens:
340: hatari.trigger_shortcut(line)
341: elif line in process_tokens:
342: process_tokens[line]()
343: elif line == "console-help":
344: show_help()
345: else:
346: print "ERROR: unknown hatari-console command:", line
347:
348:
349: if __name__ == "__main__":
350: main()
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.