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