Source code for emsm.core.application

#!/usr/bin/env python3

# The MIT License (MIT)
#
# Copyright (c) 2014-2018 <see AUTHORS.txt>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


# Modules
# ------------------------------------------------

# std
import os
import pwd
import grp
import sys
import logging
import traceback

# third party
import filelock
import colorama
import termcolor

# local
from . import argparse_
from . import base_plugin
from . import conf
from . import logging_
from . import paths
from . import plugins
from . import server
from . import worlds
from . import license_
from . import version


# Data
# ------------------------------------------------

__all__ = [
    "ApplicationException",
    "WrongUserError",
    "Application"
    ]

log = logging.getLogger(__file__)


# Exceptions
# ------------------------------------------------

[docs]class ApplicationException(Exception): """ Base class for all exceptions in this module. """
[docs]class WrongUserError(ApplicationException): """ Raised if the EMSM is executed by the wrong user. """ def __init__(self, required_user): self.required_user = required_user return None def __str__(self): temp = "This script requires a user named '{}'."\ .format(self.required_user) return temp
# Classes # ------------------------------------------------
[docs]class Application(object): """ This class manages the initialisation and the complete run process of the EMSM. An EMSM application should be executed in a code structure similar to this one: .. code-block:: python app = Application() try: app.setup() app.run() except Exception as err: app.handle_exception() raise finally: exit(app.finish()) """ def __init__(self, instance_dir): """ """ # The order of the initialisation is not trivial! self._paths = paths.Pathsystem(instance_dir) self._lock = filelock.FileLock( os.path.join(self._paths.instance(), "app.lock") ) self._logger = logging_.Logger(self) self._conf = conf.Configuration(self) self._argparser = argparse_.ArgumentParser(self) self._worlds = worlds.WorldManager(self) self._server = server.ServerManager(self) self._plugins = plugins.PluginManager(self) # The exit code can be changed by plugins. This is useful # since a plugin should not throw a SystemExit exception. self._exit_code = 0 return None
[docs] def paths(self): """ Returns the used :class:`~emsm.core.paths.Pathsystem` instance. """ return self._paths
[docs] def conf(self): """ Returns the used :class:`~emsm.core.conf.Configuration` instance. """ return self._conf
[docs] def argparser(self): """ Returns the EMSM :class:`~emsm.core.argparse_.ArgumentParser`, that is used internally. """ return self._argparser
[docs] def worlds(self): """ Returns the used :class:`~emsm.core.worlds.WorldManager` instance. """ return self._worlds
[docs] def server(self): """ Returns the used :class:`~emsm.core.server.ServerManager` instance. """ return self._server
[docs] def plugins(self): """ Returns the used :class:`~emsm.core.plugins.PluginManager` instance. """ return self._plugins
[docs] def exit_code(self): """ Returns the exit code of the application. .. seealso:: :meth:`set_exit_code` """ return self._exit_code
[docs] def set_exit_code(self, code): """ Sets the exit code to *code*. This is the exit code, that is used for the Python :func:`exit` function. :raises TypeError: if *code* is not an int. :raises ValueError: if *code* < 0. .. seealso:: * :meth:`exit_code` * :func:`exit` """ if not isinstance(code, int): raise TypeError("*code* is not an int.") if code < 0: raise ValueError("*code* is < 0.") self._exit_code = code return None
def _switch_user(self): """ Switches the *uid* (user id) and *gui* (group id) of the current EMSM process to match the expected user defined in the :file:`main.conf` configuration file. :raises WrongUserError: if the rights could not be changed to the target user and group. .. seealso:: * :meth:`emsm.core.conf.Configuration.main` * :func:`os.setuid` * :func:`os.setgid` """ username = self._conf.main()["emsm"]["user"] try: user = pwd.getpwnam(username) except KeyError as err: log.critical(err, exc_info=True) raise WrongUserError(username) group = grp.getgrgid(user.pw_gid) try: # Switch the group first. if os.getegid() != user.pw_gid: os.setgid(user.pw_gid) log.info("switched gid to '{}' ('{}')."\ .format(user.pw_gid, group.gr_name)) # Switch the user. if os.geteuid() != user.pw_uid: os.setuid(user.pw_uid) log.info("switched uid to '{}' ('{}')."\ .format(user.pw_uid, user.pw_name)) # We failed to switch the user and group. except OSError as err: log.critical(err, exc_info=True) raise WrongUserError(username) return None
[docs] def handle_exception(self): """ Checks :func:`sys.exc_info` if there is currently an uncaught exception and logs it. """ exc_info = sys.exc_info() # Break if there is no exception that is currently handled. if None in exc_info: return None # Log all exceptions log.exception("uncaught exception:") # Handle the exception by creating a log entry and printing # a short error message. msg = "EMSM: Uncaught exception:\n"\ " > Exception: {exc_type}\n"\ " > Module {exc_mod}\n"\ " > Message: {exc_msg}\n"\ " > A full traceback can be found in the log file."\ .format(exc_type = exc_info[0].__name__, exc_mod = traceback.extract_tb(exc_info[2])[-1][0], exc_msg = exc_info[1] ) msg = termcolor.colored(msg, "red") # Don't print the SystemExit exception. if not issubclass(exc_info[0], SystemExit): print(msg, file=sys.stderr) return None
[docs] def setup(self): """ Initialises all components of the EMSM. This method will block, until the EMSM filelock could be acquired or the configuration timeout value is reached. """ log.info("----------") log.info("setting the EMSM {} up ...".format(version.VERSION)) # Initialise colorama. colorama.init() # Read the configuration, so that we get to know some startup # parameters like the file lock *timeout* or the EMSM user. # Note, that the configuration wrappers defines default values for # the EMSM. So the configuration files may not exist at this point and # we can call ``self._paths.create()`` later. self._conf.read() # Downgrade the privileges before doing anything else. self._switch_user() os.chdir(self._paths.instance()) # Create the EMSM directories. # Note, that we must execute this after ``switch_user`` to make sure, the # files are owned by the EMSM user. self._paths.create() # Wait for the file lock to avoid running multiple EMSM applications # at the same time. log.info("waiting for the file lock ...") lock_timeout = self._conf.main()["emsm"].getint("timeout", 0) lock_timeout = lock_timeout if lock_timeout > 0 else None self._lock.acquire(lock_timeout, 0.05) # Now we have the file lock, so we can acquire the emsm.log file. self._logger.setup() # Reload the configuration again, since it may have changed while # waiting for the file lock. self._conf.read() self._plugins.setup() self._plugins.init_plugins() self._worlds.load_worlds() self._argparser.setup() return None
[docs] def run(self): """ Runs the plugins. .. seealso:: * :meth:`emsm.core.plugins.PluginManager.run` * :meth:`emsm.core.plugins.PluginManager.finish` """ # Parse the arguments. self._argparser.args(cache=False) # Dispatch the plugins. self._plugins.run() self._plugins.finish() # Save changes to the configuration that have been made during # execution. self._conf.write() return None
[docs] def finish(self): """ Performs some clean up and background stuff. :returns: :meth:`exit_code` .. note:: Do not mix this method up with the :meth:`emsm.core.plugins.PluginManager.finish` method. These are not related. .. seealso:: * :meth:`run` * :meth:`exit_code` """ # Disable colorama. colorama.deinit() log.info("EMSM finished.") self._lock.release() return self._exit_code