From 9e07e0b53c73771972be87743e3b09d33c5df267 Mon Sep 17 00:00:00 2001 From: gryf Date: Thu, 15 Feb 2018 17:49:24 +0100 Subject: [PATCH] Initial import --- LICENSE | 24 ++++ README.rst | 19 +++ e_uae_wrapper/__init__.py | 1 + e_uae_wrapper/base.py | 244 ++++++++++++++++++++++++++++++++++++++ e_uae_wrapper/plain.py | 26 ++++ e_uae_wrapper/utils.py | 123 +++++++++++++++++++ e_uae_wrapper/wrapper.py | 87 ++++++++++++++ script/e-uae-wrapper | 16 +++ 8 files changed, 540 insertions(+) create mode 100644 LICENSE create mode 100644 README.rst create mode 100644 e_uae_wrapper/__init__.py create mode 100644 e_uae_wrapper/base.py create mode 100644 e_uae_wrapper/plain.py create mode 100644 e_uae_wrapper/utils.py create mode 100644 e_uae_wrapper/wrapper.py create mode 100755 script/e-uae-wrapper diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f56ea99 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +Copyright (c) 2018, Roman Dobosz +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the organization nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL ROMAN DOBOSZ BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, +EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..e3edd7e --- /dev/null +++ b/README.rst @@ -0,0 +1,19 @@ +============= +E-UAE Wrapper +============= + +This little utility is a wrapper on old E-UAE_ emulator, to help divide +configuration to common and specific per ``.uaerc`` configuration, and combine +them together during runtime. + +Global (a common) config file will reside on ``XDG_CONFIG_HOME/e-uaerc``, and +supports following options: + +(TODO) + +License +======= + +This work is licensed on 3-clause BSD license. See LICENSE file for details. + +.. _e-uae: http://www.rcdrummond.net/uae/ diff --git a/e_uae_wrapper/__init__.py b/e_uae_wrapper/__init__.py new file mode 100644 index 0000000..63f930e --- /dev/null +++ b/e_uae_wrapper/__init__.py @@ -0,0 +1 @@ +WRAPPER_KEY = 'wrapper' diff --git a/e_uae_wrapper/base.py b/e_uae_wrapper/base.py new file mode 100644 index 0000000..64a8a12 --- /dev/null +++ b/e_uae_wrapper/base.py @@ -0,0 +1,244 @@ +""" +Base class for all wrapper modules +""" +import logging +import os +import shutil +import sys +import tempfile + +from e_uae_wrapper import utils +from e_uae_wrapper import path + + +class Base(object): + """ + Base class for wrapper modules + """ + def __init__(self, conf_file, config): + """ + Params: + config: parsed lines combined from global and local config + """ + self.config = config + self.dir = None + self.save_filename = None + self.conf_file = conf_file + + def run(self): + """ + Main function which accepts config file for e-uae + It will do as follows: + - set needed full path for asset files + - extract archive file + - copy configuration + - [copy save if exists] + - run the emulation + - archive save state + """ + if not self._validate_options(): + return False + + self.dir = tempfile.mkdtemp() + self._interpolate_options() + self._set_assets_paths() + + return True + + def clean(self): + """Remove temporary file""" + if self.dir: + shutil.rmtree(self.dir) + return + + def _set_assets_paths(self): + """ + Set full paths for archive file (without extension) and for save state + archive file + """ + conf_abs_dir = os.path.dirname(os.path.abspath(self.conf_file)) + conf_base = os.path.basename(self.conf_file) + conf_base = os.path.splitext(conf_base)[0] + + # set optional save_state + arch_ext = utils.get_arch_ext(self.config.get('wrapper_archiver')) + if arch_ext: + self.save_filename = os.path.join(conf_abs_dir, conf_base + + '_save' + arch_ext) + + def _copy_conf(self): + """copy provided configuration as .uaerc""" + shutil.copy(self.conf_file, self.dir) + os.rename(os.path.join(self.dir, os.path.basename(self.conf_file)), + os.path.join(self.dir, '.uaerc')) + return True + + def _run_emulator(self): + """execute e-uae""" + curdir = os.path.abspath('.') + os.chdir(self.dir) + utils.run_command(['e-uae']) + os.chdir(curdir) + return True + + def _get_title(self): + """ + Return the title if found in config. As a fallback archive file + name will be used as title. + """ + title = '' + gui_msg = self.config.get('wrapper_gui_msg', '0') + if gui_msg == '1': + title = self.config.get('title') + if not title: + title = self.config['wrapper_archive'] + return title + + def _save_save(self): + """ + Get the saves from emulator and store it where configuration is placed + """ + if self.config.get('wrapper_save_state', '0') != '1': + return True + + os.chdir(self.dir) + save_path = self._get_saves_dir() + if not save_path: + return True + + if os.path.exists(self.save_filename): + os.unlink(self.save_filename) + + curdir = os.path.abspath('.') + + if not utils.create_archive(self.save_filename, '', [save_path]): + logging.error('Error: archiving save state failed.') + os.chdir(curdir) + return False + + os.chdir(curdir) + return True + + def _load_save(self): + """ + Put the saves (if exists) to the temp directory. + """ + if self.config.get('wrapper_save_state', '0') != '1': + return True + + if not os.path.exists(self.save_filename): + return True + + curdir = os.path.abspath('.') + os.chdir(self.dir) + utils.extract_archive(self.save_filename) + os.chdir(curdir) + return True + + def _get_saves_dir(self): + """ + Return path to save state directory or None in cases: + - there is no save state dir set relative to copied config file + - save state dir is set globally + - save state dir is set relative to the config file + - save state dir doesn't exists + Note, that returned path is relative not absolute + """ + if not self.config.get('save_states_dir'): + return None + + if self.config['save_states_dir'].startswith('$WRAPPER') and \ + '..' not in self.config['save_states_dir']: + save = self.config['save_states_dir'].replace('$WRAPPER/', '') + else: + return None + + save_path = os.path.join(self.dir, save) + if not os.path.exists(save_path) or not os.path.isdir(save_path): + return None + + if save.endswith('/'): + save = save[:-1] + + return save + + def _interpolate_options(self): + """ + Search and replace values for options which contains {{ and }} + markers for replacing them with correpsonding calculated values + """ + for key, val in self.config.items(): + print key, val + + def _validate_options(self): + """Validate mandatory options""" + if 'wrapper' not in self.config: + logging.error("Configuration lacks of required `wrapper' option.") + return False + + if self.config.get('wrapper_save_state', '0') == '0': + return True + + if 'wrapper_archiver' not in self.config: + logging.error("Configuration lacks of required " + "`wrapper_archiver' option.") + return False + + if not path.which(self.config['wrapper_archiver']): + logging.error("Cannot find archiver `%s'.", + self.config['wrapper_archiver']) + return False + + return True + + +class ArchiveBase(Base): + """ + Base class for archive based wrapper modules + """ + def __init__(self, conf_path, config): + """ + Params: + conf_file: a relative path to provided configuration file + fsuae_options: is an CmdOption object created out of command line + parameters + config: is config dictionary created out of config file + """ + super(ArchiveBase, self).__init__(conf_path, config) + self.arch_filepath = None + + def _set_assets_paths(self): + """ + Set full paths for archive file (without extension) and for save state + archive file + """ + super(ArchiveBase, self)._set_assets_paths() + + conf_abs_dir = os.path.dirname(os.path.abspath(self.conf_file)) + arch = self.config.get('wrapper_archive') + if arch: + if os.path.isabs(arch): + self.arch_filepath = arch + else: + self.arch_filepath = os.path.join(conf_abs_dir, arch) + + def _extract(self): + """Extract archive to temp dir""" + + title = self._get_title() + curdir = os.path.abspath('.') + os.chdir(self.dir) + result = utils.extract_archive(self.arch_filepath, title) + os.chdir(curdir) + return result + + def _validate_options(self): + + validation_result = super(ArchiveBase, self)._validate_options() + + if 'wrapper_archive' not in self.config: + sys.stderr.write("Configuration lacks of required " + "`wrapper_archive' option.\n") + validation_result = False + + return validation_result diff --git a/e_uae_wrapper/plain.py b/e_uae_wrapper/plain.py new file mode 100644 index 0000000..2bf8d8e --- /dev/null +++ b/e_uae_wrapper/plain.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Simple class for executing e-uae with specified parameters. This is a +failsafe class for running e-uae. +""" +from e_uae_wrapper import base +from e_uae_wrapper import utils + + +class Wrapper(base.Base): + """Simple class for running e-uae""" + + def run(self): + """ + Main function which run e-uae + """ + self._run_emulator() + + def _run_emulator(self): + """execute e-uae""" + utils.run_command(['e-uae', self.conf_file]) + + def clean(self): + """Do the cleanup. Here - just do nothing""" + return diff --git a/e_uae_wrapper/utils.py b/e_uae_wrapper/utils.py new file mode 100644 index 0000000..792ca67 --- /dev/null +++ b/e_uae_wrapper/utils.py @@ -0,0 +1,123 @@ +""" +Misc utilities +""" +import collections +import logging +import os +import subprocess +try: + import configparser +except ImportError: + import ConfigParser as configparser + +from e_uae_wrapper import file_archive + + +def load_conf(conf_file): + """ + Read global config and provided config file and return dict with combined + options.""" + conf = _get_common_config() + local_conf = collections.OrderedDict() + + with open(conf_file) as fobj: + for line in fobj: + key, val = line.strip().split('=') + if key in local_conf: + raise Exception('%s already in conf' % key) + local_conf[key] = val + + conf.update(local_conf) + return conf + + +def operate_archive(arch_name, operation, text, params): + """ + Create archive from contents of current directory + """ + + archiver = file_archive.get_archiver(arch_name) + + if archiver is None: + return False + + res = False + + if operation == 'extract': + res = archiver.extract(arch_name) + + if operation == 'create': + res = archiver.create(arch_name, params) + + return res + + +def create_archive(arch_name, title='', params=None): + """ + Create archive from contents of current directory + """ + msg = '' + if title: + msg = "Creating archive for `%s'. Please be patient" % title + return operate_archive(arch_name, 'create', msg, params) + + +def extract_archive(arch_name, title='', params=None): + """ + Extract provided archive to current directory + """ + msg = '' + if title: + msg = "Extracting files for `%s'. Please be patient" % title + return operate_archive(arch_name, 'extract', msg, params) + + +def run_command(cmd): + """ + Run provided command. Return true if command execution returns zero exit + code, false otherwise. If cmd is not a list, there would be an attempt to + split it up for subprocess call method. May throw exception if cmd is not + a list neither a string. + """ + + if not isinstance(cmd, list): + cmd = cmd.split() + + logging.debug("Executing `%s'.", " ".join(cmd)) + code = subprocess.call(cmd) + if code != 0: + logging.error('Command `%s` returned non 0 exit code.', cmd[0]) + return False + return True + + +def _get_common_config(): + """ + Try to find common configuration file and return data as a dict. + File will be gather from $XDG_CONFIG_HOME/e-uaerc, which ususally is + ~/.config/e-uaerc + """ + + parser = configparser.SafeConfigParser() + + xdg_conf = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) + conf_path = os.path.join(xdg_conf, 'e-uae.ini') + + try: + parser.read(conf_path) + except configparser.ParsingError: + # Configuration syntax is wrong + return {} + + section = parser.sections()[0] + conf = collections.OrderedDict() + for option in parser.options(section): + if option in ['wrapper_rom_path']: + conf[option] = parser.get(section, option) + + return conf + + +def get_arch_ext(archiver_name): + """Return extension for the archiver""" + return file_archive.Archivers.get_extension_by_name(archiver_name) diff --git a/e_uae_wrapper/wrapper.py b/e_uae_wrapper/wrapper.py new file mode 100644 index 0000000..aa6aecb --- /dev/null +++ b/e_uae_wrapper/wrapper.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +""" +Wrapper for e-uae to perform some actions before and or after running the +emulator, if appropriate option is enabled. +""" +import argparse +import importlib +import logging +import os +import sys + +from e_uae_wrapper import utils +from e_uae_wrapper import WRAPPER_KEY + + +def setup_logger(args): + """Setup logger format and level""" + + level = logging.WARNING + + if args.quiet: + level = logging.ERROR + if args.quiet > 1: + level = logging.CRITICAL + + if args.verbose: + level = logging.INFO + if args.verbose > 1: + level = logging.DEBUG + + logging.basicConfig(level=level, + format="%(asctime)s %(levelname)s: %(message)s") + + +def parse_args(): + """ + Look out for config file and for config options which would be blindly + passed to e-uae. + """ + parser = argparse.ArgumentParser() + parser.add_argument('config', help='Configuration file for e-uae.') + parser.add_argument('-v', '--verbose', help='Be verbose. Adding more "v" ' + 'will increase verbosity', action="count", + default=None) + parser.add_argument('-q', '--quiet', help='Be quiet. Adding more "q" will' + ' decrease verbosity', action="count", default=None) + + args = parser.parse_args() + setup_logger(args) + logging.debug("args: %s", args) + + return args + + +def run(): + """run wrapper module""" + + args = parse_args() + configuration = utils.load_conf(args.config) + + if not configuration: + logging.error('Error: Configuration file have syntax issues') + sys.exit(2) + + wrapper_module = configuration.get(WRAPPER_KEY, 'plain') + + try: + wrapper = importlib.import_module('e_uae_wrapper.' + + wrapper_module) + except ImportError: + logging.error("Error: provided wrapper module: `%s' doesn't " + "exists.", wrapper_module) + sys.exit(3) + + runner = wrapper.Wrapper(os.path.abspath(args.config), configuration) + + try: + exit_code = runner.run() + finally: + runner.clean() + + if not exit_code: + sys.exit(4) + + +if __name__ == "__main__": + run() diff --git a/script/e-uae-wrapper b/script/e-uae-wrapper new file mode 100755 index 0000000..d9b92be --- /dev/null +++ b/script/e-uae-wrapper @@ -0,0 +1,16 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Run e-uae emulator with some configuration goodies :) +""" + +from e_uae_wrapper import wrapper + + +def main(): + """run wrapper""" + wrapper.run() + + +if __name__ == "__main__": + main()