|
|
1.1 root 1: #!/usr/bin/env python
2: #
3: # Classes for Hatari emulator instance and mapping its congfiguration
4: # variables with its command line option.
5: #
6: # Copyright (C) 2008 by Eero Tamminen <[email protected]>
7: #
8: # This program is free software; you can redistribute it and/or modify
9: # it under the terms of the GNU General Public License as published by
10: # the Free Software Foundation; either version 2 of the License, or
11: # (at your option) any later version.
12: #
13: # This program is distributed in the hope that it will be useful,
14: # but WITHOUT ANY WARRANTY; without even the implied warranty of
15: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16: # GNU General Public License for more details.
17:
18: import os
19: import sys
20: import time
21: import signal
22: import socket
23: import select
24: from config import ConfigStore
25:
26:
27: # Running Hatari instance
28: class Hatari:
29: "running hatari instance and methods for communicating with it"
30: basepath = "/tmp/hatari-ui-" + str(os.getpid())
31: logpath = basepath + ".log"
32: tracepath = basepath + ".trace"
33: debugpath = basepath + ".debug"
34: controlpath = basepath + ".socket"
35: server = None # singleton due to path being currently one per user
36:
37: def __init__(self, hataribin = None):
38: # collect hatari process zombies without waitpid()
39: signal.signal(signal.SIGCHLD, signal.SIG_IGN)
40: if hataribin:
41: self.hataribin = hataribin
42: else:
43: self.hataribin = "hatari"
44: self._assert_hatari_compatibility()
45: self._create_server()
46: self.control = None
47: self.paused = False
48: self.pid = 0
49:
50: def _assert_hatari_compatibility(self):
51: for line in os.popen(self.hataribin + " -h").readlines():
52: if line.find("--control-socket") >= 0:
53: return
54: print "ERROR: Hatari not found or it doesn't support the required --control-socket option!"
55: sys.exit(-1)
56:
57: def _create_server(self):
58: if self.server:
59: return
60: self.server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
61: if os.path.exists(self.controlpath):
62: os.unlink(self.controlpath)
63: self.server.bind(self.controlpath)
64: self.server.listen(1)
65:
66: def _send_message(self, msg):
67: if self.control:
68: self.control.send(msg)
69: return True
70: else:
71: print "ERROR: no Hatari (control socket)"
72: return False
73:
74: def change_option(self, option):
75: "change_option(option), changes given Hatari cli option"
76: return self._send_message("hatari-option %s\n" % option)
77:
78: def set_path(self, key, path):
79: "set_path(key, path), sets path with given key"
80: return self._send_message("hatari-path %s %s\n" % (key, path))
81:
82: def set_device(self, device, enabled):
83: # needed because CLI options cannot disable devices, only enable
84: "set_path(device, enabled), sets whether given device is enabled or not"
85: if enabled:
86: return self._send_message("hatari-enable %s\n" % device)
87: else:
88: return self._send_message("hatari-disable %s\n" % device)
89:
90: def trigger_shortcut(self, shortcut):
91: "trigger_shortcut(shortcut), triggers given Hatari (keyboard) shortcut"
92: return self._send_message("hatari-shortcut %s\n" % shortcut)
93:
94: def insert_event(self, event):
95: "insert_event(event), synthetizes given key/mouse Atari event"
96: return self._send_message("hatari-event %s\n" % event)
97:
98: def debug_command(self, cmd):
99: "debug_command(command), runs given Hatari debugger command"
100: return self._send_message("hatari-debug %s\n" % cmd)
101:
102: def pause(self):
103: "pause(), pauses Hatari emulation"
104: return self._send_message("hatari-stop\n")
105:
106: def unpause(self):
107: "unpause(), continues Hatari emulation"
108: return self._send_message("hatari-cont\n")
109:
110: def _open_output_file(self, hataricommand, option, path):
111: if os.path.exists(path):
112: os.unlink(path)
113: # TODO: why fifo doesn't work properly (blocks forever on read or
114: # reads only byte at the time and stops after first newline)?
115: #os.mkfifo(path)
116: #raw_input("attach strace now, then press Enter\n")
117:
118: # ask Hatari to open/create the requested output file...
119: hataricommand("%s %s" % (option, path))
120: wait = 0.025
121: # ...and wait for it to appear before returning it
122: for i in range(0, 8):
123: time.sleep(wait)
124: if os.path.exists(path):
125: return open(path, "r")
126: wait += wait
127: return None
128:
129: def open_debug_output(self):
130: "open_debug_output() -> file, opens Hatari debugger output file"
131: return self._open_output_file(self.debug_command, "f", self.debugpath)
132:
133: def open_trace_output(self):
134: "open_trace_output() -> file, opens Hatari tracing output file"
135: return self._open_output_file(self.change_option, "--trace-file", self.tracepath)
136:
137: def open_log_output(self):
138: "open_trace_output() -> file, opens Hatari debug log file"
139: return self._open_output_file(self.change_option, "--log-file", self.logpath)
140:
141: def get_lines(self, fileobj):
142: "get_lines(file) -> list of lines readable from given Hatari output file"
143: # wait until data is available, then wait for some more
144: # and only then the data can be read, otherwise its old
145: print "Request&wait data from Hatari..."
146: select.select([fileobj], [], [])
147: time.sleep(0.1)
148: print "...read the data lines"
149: lines = fileobj.readlines()
150: print "".join(lines)
151: return lines
152:
153: def enable_embed_info(self):
154: "enable_embed_info(), request embedded Hatari window ID change information"
155: self._send_message("hatari-embed-info\n")
156:
157: def get_embed_info(self):
158: "get_embed_info() -> (width, height), get embedded Hatari window size"
159: width, height = self.control.recv(12).split("x")
160: return (int(width), int(height))
161:
162: def get_control_socket(self):
163: "get_control_socket() -> socket which can be checked for embed ID changes"
164: return self.control
165:
166: def is_running(self):
167: "is_running() -> bool, True if Hatari is running, False otherwise"
168: if not self.pid:
169: return False
170: try:
171: os.waitpid(self.pid, os.WNOHANG)
172: except OSError, value:
173: print "Hatari PID %d had exited in the meanwhile:\n\t%s" % (self.pid, value)
174: self.pid = 0
175: return False
176: return True
177:
178: def run(self, extra_args = None, parent_win = None):
179: "run([parent window][,embedding args]), runs Hatari"
180: # if parent_win given, embed Hatari to it
181: pid = os.fork()
182: if pid < 0:
183: print "ERROR: fork()ing Hatari failed!"
184: return
185: if pid:
186: # in parent
187: self.pid = pid
188: if self.server:
189: print "WAIT hatari to connect to control socket...",
190: (self.control, addr) = self.server.accept()
191: print "connected!"
192: else:
193: # child runs Hatari
194: env = os.environ
195: if parent_win:
196: self._set_embed_env(env, parent_win)
197: # callers need to take care of confirming quitting
198: args = [self.hataribin, "--confirm-quit", "off"]
199: if self.server:
200: args += ["--control-socket", self.controlpath]
201: if extra_args:
202: args += extra_args
203: print "RUN:", args
204: os.execvpe(self.hataribin, args, env)
205:
206: def _set_embed_env(self, env, parent_win):
207: if sys.platform == 'win32':
208: win_id = parent_win.handle
209: else:
210: win_id = parent_win.xid
211: # tell SDL to use given widget's window
212: #env["SDL_WINDOWID"] = str(win_id)
213:
214: # above is broken: when SDL uses a window it hasn't created itself,
215: # it for some reason doesn't listen to any events delivered to that
216: # window nor implements XEMBED protocol to get them in a way most
217: # friendly to embedder:
218: # http://standards.freedesktop.org/xembed-spec/latest/
219: #
220: # Instead we tell hatari to reparent itself after creating
221: # its own window into this program widget window
222: env["PARENT_WIN_ID"] = str(win_id)
223:
224: def kill(self):
225: "kill(), kill Hatari if it's running"
226: if self.pid:
227: os.kill(self.pid, signal.SIGKILL)
228: print "killed hatari with PID %d" % self.pid
229: self.pid = 0
230: if self.control:
231: self.control.close()
232: self.control = None
233:
234:
235: # Mapping of requested values both to Hatari configuration
236: # and command line options.
237: #
238: # By default this doesn't allow setting any other configuration
239: # variables than the ones that were read from the configuration
240: # file i.e. you get an exception if configuration variables
241: # don't match to current Hatari. So before using this you should
242: # have saved Hatari configuration at least once.
243: #
244: # Because of some inconsistencies in the values (see e.g. sound),
245: # this cannot just do these according to some mapping table, but
246: # it needs actual method for (each) setting.
247: class HatariConfigMapping(ConfigStore):
248: _paths = {
249: "memauto": ("[Memory]", "szAutoSaveFileName", "Automatic memory snapshot"),
250: "memsave": ("[Memory]", "szMemoryCaptureFileName", "Manual memory snapshot"),
251: "midiin": ("[Midi]", "sMidiInFileName", "Midi input"),
252: "midiout": ("[Midi]", "sMidiOutFileName", "Midi output"),
253: "rs232in": ("[RS232]", "szInFileName", "RS232 I/O input"),
254: "rs232out": ("[RS232]", "szOutFileName", "RS232 I/O output"),
255: "printout": ("[Printer]", "szPrintToFileName", "Printer output"),
256: "soundout": ("[Sound]", "szYMCaptureFileName", "Sound output")
257: }
258: "access methods to Hatari configuration file variables and command line options"
259: def __init__(self, hatari):
260: ConfigStore.__init__(self, "hatari.cfg")
261: self._hatari = hatari
262: self._lock_updates = False
263: self._options = []
264:
265: def _change_option(self, option):
266: if self._lock_updates:
267: self._options.append(option)
268: else:
269: self._hatari.change_option(option)
270:
271: def lock_updates(self):
272: "lock_updates(), collect Hatari configuration changes"
273: self._lock_updates = True
274:
275: def flush_updates(self):
276: "flush_updates(), apply collected Hatari configuration changes"
277: self._lock_updates = False
278: if not self._options:
279: return
280: self._hatari.change_option(" ".join(self._options))
281: self._options = []
282:
283: # ------------ paths ---------------
284: def get_paths(self):
285: paths = []
286: for key, item in self._paths.items():
287: paths.append((key, self.get(item[0], item[1]), item[2]))
288: return paths
289:
290: def set_paths(self, paths):
291: for key, path in paths:
292: self.set(self._paths[key][0], self._paths[key][1], path)
293: self._hatari.set_path(key, path)
294:
295: # ------------ midi ---------------
296: def get_midi(self):
297: return self.get("[Midi]", "bEnableMidi")
298:
299: def set_midi(self, value):
300: self.set("[Midi]", "bEnableMidi", value)
301: self._hatari.set_device("midi", value)
302:
303: # ------------ printer ---------------
304: def get_printer(self):
305: return self.get("[Printer]", "bEnablePrinting")
306:
307: def set_printer(self, value):
308: self.set("[Printer]", "bEnablePrinting", value)
309: self._hatari.set_device("printer", value)
310:
311: # ------------ RS232 ---------------
312: def get_rs232(self):
313: return self.get("[RS232]", "bEnableRS232")
314:
315: def set_rs232(self, value):
316: self.set("[RS232]", "bEnableRS232", value)
317: self._hatari.set_device("rs232", value)
318:
319: # ------------ machine ---------------
320: def get_machine_types(self):
321: return ("ST", "STE", "TT", "Falcon")
322:
323: def get_machine(self):
324: return self.get("[System]", "nMachineType")
325:
326: def set_machine(self, value):
327: self.set("[System]", "nMachineType", value)
328: self._change_option("--machine %s" % ("st", "ste", "tt", "falcon")[value])
329:
330: # ------------ compatible ---------------
331: def get_compatible(self):
332: return self.get("[System]", "bCompatibleCpu")
333:
334: def set_compatible(self, value):
335: self.set("[System]", "bCompatibleCpu", value)
336: self._change_option("--compatible %s" % str(value))
337:
338: # ------------ Timer-D ---------------
339: def get_timerd(self):
340: return self.get("[System]", "bPatchTimerD")
341:
342: def set_timerd(self, value):
343: self.set("[System]", "bPatchTimerD", value)
344: self._change_option("--timer-d %s" % str(value))
345:
346: # ------------ fastforward ---------------
347: def get_fastforward(self):
348: return self.get("[System]", "bFastForward")
349:
350: def set_fastforward(self, value):
351: self.set("[System]", "bFastForward", value)
352: self._change_option("--fast-forward %s" % str(value))
353:
354: # ------------ sound ---------------
355: def get_sound_values(self):
356: # 48kHz, 44.1kHz and STE/TT/Falcon DMA 50066Hz divisable values
357: return ("6000", "6258", "8000", "11025", "12000", "12517",
358: "16000", "22050", "24000", "25033", "32000",
359: "48000", "44100", "50066")
360:
361: def get_sound(self):
362: enabled = self.get("[Sound]", "bEnableSound")
363: hz = str(self.get("[Sound]", "nPlaybackFreq"))
364: return (enabled, hz)
365:
366: def set_sound(self, enabled, hz):
367: # map get_sound_values() index to Hatari config
368: try:
369: hz = int(hz)
370: except ValueError:
371: hz = 0
372: if hz < 6000 or hz > 50066:
373: hz = 44100
374: self.set("[Sound]", "nPlaybackFreq", hz)
375: self.set("[Sound]", "bEnableSound", enabled)
376: value = str(hz)
377: # and to cli option
378: if enabled:
379: self._change_option("--sound %s" % value)
380: else:
381: self._change_option("--sound off")
382: return value
383:
384:
385: # ----------- joystick --------------
386: def get_joystick_types(self):
387: return ("Disabled", "Real joystick", "Keyboard")
388:
389: def get_joystick_names(self):
390: return (
391: "ST Joystick 0",
392: "ST Joystick 1",
393: "STE Joypad A",
394: "STE Joypad B",
395: "Parport stick 1",
396: "Parport stick 2"
397: )
398:
399: def get_joystick(self, port):
400: # return index to get_joystick_values() array
401: return self.get("[Joystick%d]" % port, "nJoystickMode")
402:
403: def set_joystick(self, port, value):
404: # map get_sound_values() index to Hatari config
405: self.set("[Joystick%d]" % port, "nJoystickMode", value)
406: joytype = ("none", "real", "keys")[value]
407: self._change_option("--joy%d %s" % (port, joytype))
408:
409: # ------------ floppy image dir ---------------
410: def get_floppydir(self):
411: return self.get("[Floppy]", "szDiskImageDirectory")
412:
413: def set_floppydir(self, path):
414: return self.set("[Floppy]", "szDiskImageDirectory", path)
415:
416: # ------------ floppy disk images ---------------
417: def get_floppy(self, drive):
418: return self.get("[Floppy]", "szDisk%cFileName" % ("A", "B")[drive])
419:
420: def set_floppy(self, drive, filename):
421: self.set("[Floppy]", "szDisk%cFileName" % ("A", "B")[drive], filename)
422: self._change_option("--disk-%c %s" % (("a", "b")[drive], filename))
423:
424: # ------------ use harddisk ---------------
425: def get_use_harddisk(self):
426: return self.get("[HardDisk]", "bUseHardDiskDirectory")
427:
428: def set_use_harddisk(self, value):
429: self.set("[HardDisk]", "bUseHardDiskDirectory", value)
430: # TODO: add to hatari option for this
431:
432: # ------------ harddisk (dir) ---------------
433: def get_harddisk(self):
434: return self.get("[HardDisk]", "szHardDiskDirectory")
435:
436: def set_harddisk(self, dirname):
437: self.set("[HardDisk]", "szHardDiskDirectory", dirname)
438: if self.get_use_harddisk():
439: self._change_option("--harddrive %s" % dirname)
440:
441: # ------------ TOS ROM ---------------
442: def get_tos(self):
443: return self.get("[ROM]", "szTosImageFileName")
444:
445: def set_tos(self, filename):
446: self.set("[ROM]", "szTosImageFileName", filename)
447: self._change_option("--tos %s" % filename)
448:
449: # ------------ memory ---------------
450: def get_memory_names(self):
451: # empty item in list shouldn't be shown, filter them out
452: return ("512kB", "1MB", "2MB", "4MB", "8MB", "14MB")
453:
454: def get_memory(self):
455: "return index to what get_memory_names() returns"
456: sizemap = (0, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5)
457: memsize = self.get("[Memory]", "nMemorySize")
458: if memsize >= 0 and memsize < len(sizemap):
459: return sizemap[memsize]
460: return 1 # default = 1BM
461:
462: def set_memory(self, idx):
463: # map memory item index to memory size
464: sizemap = (0, 1, 2, 4, 8, 14)
465: if idx >= 0 and idx < len(sizemap):
466: memsize = sizemap[idx]
467: else:
468: memsize = 1
469: self.set("[Memory]", "nMemorySize", memsize)
470: self._change_option("--memsize %d" % memsize)
471:
472: # ------------ monitor ---------------
473: def get_monitor_types(self):
474: return ("Mono", "RGB", "VGA", "TV")
475:
476: def get_monitor(self):
477: return self.get("[Screen]", "nMonitorType")
478:
479: def set_monitor(self, value):
480: self.set("[Screen]", "nMonitorType", value)
481: self._change_option("--monitor %s" % ("mono", "rgb", "vga", "tv")[value])
482:
483: # ------------ frameskip ---------------
484: def get_frameskip_names(self):
485: return (
486: "Disabled",
487: "1 frame",
488: "2 frames",
489: "3 frames",
490: "4 frames",
491: "Automatic"
492: )
493:
494: def get_frameskip(self):
495: fs = self.get("[Screen]", "nFrameSkips")
496: print "Frameskip", fs
497: if fs < 0 or fs > 5:
498: return 5
499: return fs
500:
501: def set_frameskip(self, value):
502: value = int(value) # guarantee correct type
503: self.set("[Screen]", "nFrameSkips", value)
504: self._change_option("--frameskips %d" % value)
505:
506: # ------------ spec512 ---------------
507: def get_spec512threshold(self):
508: return self.get("[Screen]", "nSpec512Threshold")
509:
510: def set_spec512threshold(self, value):
511: value = int(value) # guarantee correct type
512: self.set("[Screen]", "nSpec512Threshold", value)
513: self._change_option("--spec512 %d" % value)
514:
515: # ------------ show borders ---------------
516: def get_borders(self):
517: return self.get("[Screen]", "bAllowOverscan")
518:
519: def set_borders(self, value):
520: self.set("[Screen]", "bAllowOverscan", value)
521: self._change_option("--borders %s" % str(value))
522:
523: # ------------ show statusbar ---------------
524: def get_statusbar(self):
525: return self.get("[Screen]", "bShowStatusbar")
526:
527: def set_statusbar(self, value):
528: self.set("[Screen]", "bShowStatusbar", value)
529: self._change_option("--statusbar %s" % str(value))
530:
531: # ------------ show led ---------------
532: def get_led(self):
533: return self.get("[Screen]", "bShowDriveLed")
534:
535: def set_led(self, value):
536: self.set("[Screen]", "bShowDriveLed", value)
537: self._change_option("--drive-led %s" % str(value))
538:
539: # ------------ use zoom ---------------
540: def get_zoom(self):
541: return self.get("[Screen]", "bZoomLowRes")
542:
543: def set_zoom(self, value):
544: self.set("[Screen]", "bZoomLowRes", value)
545: if value:
546: zoom = 2
547: else:
548: zoom = 1
549: self._change_option("--zoom %d" % zoom)
550:
551: # ------------ configured Hatari window size ---------------
552: def get_window_size(self):
553: if self.get("[Screen]", "bFullScreen"):
554: print "WARNING: don't start Hatari UI with fullscreened Hatari!"
555:
556: # VDI resolution?
557: if self.get("[Screen]", "bUseExtVdiResolutions"):
558: width = self.get("[Screen]", "nVdiWidth")
559: height = self.get("[Screen]", "nVdiHeight")
560: return (width, height)
561:
562: # window sizes for other than ST & STE can differ
563: if self.get("[System]", "nMachineType") not in (0, 1):
564: print "WARNING: neither ST nor STE machine, window size inaccurate!"
565:
566: # mono monitor?
567: if self.get("[Screen]", "nMonitorType") == 0:
568: return (640, 400)
569:
570: # no, color
571: width = 320
572: height = 200
573: # add overscan borders?
574: if self.get("[Screen]", "bAllowOverscan"):
575: # max size with overscan borders
576: width += self.get("[Screen]", "nWindowBorderPixelsLeft")
577: width += self.get("[Screen]", "nWindowBorderPixelsRight")
578: height = 29 + 200 + self.get("[Screen]", "nWindowBorderPixelsBottom")
579: # statusbar?
580: if self.get("[Screen]", "bShowStatusbar"):
581: height += 10
582: # zoomed?
583: if self.get("[Screen]", "bZoomLowRes"):
584: width *= 2
585: height *= 2
586:
587: return (width, height)
This archive runs on limited infrastructure. Preserving old code on modern bandwidth. Automated agents are requested to crawl responsibly.