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