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