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