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