Move plugins, commands and listeners into their own "manager" objects

Works just like before, only the logic is stored in the objects themselves instead of being scattered around houdini.py
This commit is contained in:
Ben 2019-05-29 22:58:10 +01:00
parent 3c09434012
commit 8d6e726f97
9 changed files with 298 additions and 243 deletions

View File

@ -1,8 +1,12 @@
from collections import OrderedDict
from aiocache import cached
from types import FunctionType
from abc import abstractmethod
import asyncio
import enum
import logging
import copy
class ConflictResolution(enum.Enum):
@ -18,6 +22,33 @@ class Language(enum.Enum):
Es = 8
De = 32
Ru = 64
class _AbstractManager(dict):
def __init__(self, server):
self.server = server
self.logger = logging.getLogger('houdini')
self.__backup = None
super().__init__()
@abstractmethod
def load(self, module):
"""Loads entries from module"""
@abstractmethod
def remove(self, module):
"""Removes all entries by module"""
def backup(self):
self.__backup = copy.copy(self)
def restore(self):
if self.__backup is not None:
self.update(self.__backup)
self.__backup = None
class PenguinStringCompiler(OrderedDict):
def __init__(self, *args, **kwargs):

View File

@ -1,10 +1,11 @@
import inspect
import config
import logging
import copy
from houdini import handlers
from houdini import plugins
from houdini import ConflictResolution
from houdini import ConflictResolution, _AbstractManager
from houdini.converters import _ArgumentDeserializer, _listener
@ -63,42 +64,49 @@ player_data_attribute = handlers.player_data_attribute
player_in_room = handlers.player_in_room
class CommandManager(_AbstractManager):
def load(self, module):
command_objects = inspect.getmembers(module, is_command)
if not isinstance(module, plugins.IPlugin):
raise TypeError('Commands can only be loaded from plugins')
for command_name, command_object in command_objects:
command_object.instance = module
if type(command_object.alias) == str:
command_object.alias = [command_object.alias]
command_object.alias.append(command_object.name)
parent_commands = self if command_object.parent is None else command_object.parent.commands
for name in command_object.alias:
if name in parent_commands and len(parent_commands[name]):
conflict_command = parent_commands[name][0]
if config.commands['ConflictMode'] == ConflictResolution.Exception:
raise NameError('Command name conflict: \'{}\' from plugin \'{}\' '
'conflicts with \'{}\' from module \'{}\''
.format(name, module.__class__.__name__, conflict_command.name,
conflict_command.instance.__class__.__name__))
elif config.commands['ConflictMode'] == ConflictResolution.Append:
parent_commands[name].append(command_object)
elif config.commands['ConflictMode'] == ConflictResolution.Silent:
module.server.logger.warning(
'Command \'{}\' from module \'{}\' disabled due to conflict with \'{}\''.format(
name, module.__class__.__name__, conflict_command.instance.__class__.__name__))
else:
parent_commands[name] = [command_object]
def remove(self, module):
for command_name, command_handlers in self.items():
for command_handler in command_handlers:
if module.__name__ == command_handler.callback.__module__:
command_handlers.remove(command_handler)
def is_command(command_object):
return issubclass(type(command_object), _Command)
def commands_from_plugin(commands, plugin):
command_objects = inspect.getmembers(plugin, is_command)
if not isinstance(plugin, plugins.IPlugin):
raise TypeError('Commands can only be loaded from plugins')
for command_name, command_object in command_objects:
command_object.instance = plugin
if type(command_object.alias) == str:
command_object.alias = [command_object.alias]
command_object.alias.append(command_object.name)
parent_commands = commands if command_object.parent is None else command_object.parent.commands
for name in command_object.alias:
if name in parent_commands:
conflict_command = parent_commands[name][0]
if config.commands['ConflictMode'] == ConflictResolution.Exception:
raise NameError('Command name conflict: \'{}\' from plugin \'{}\' '
'conflicts with \'{}\' from module \'{}\''
.format(name, plugin.__class__.__name__, conflict_command.name,
conflict_command.plugin.__class__.__name__))
elif config.commands['ConflictMode'] == ConflictResolution.Append:
parent_commands[name].append(command_object)
elif config.commands['ConflictMode'] == ConflictResolution.Silent:
plugin.server.logger.warn('Command \'{}\' from plugin \'{}\' disabled due to conflict with \'{}\''
.format(name, plugin.__class__.__name__,
conflict_command.plugin.__class__.__name__))
else:
parent_commands[name] = [command_object]
if type(config.commands['Prefix']) == str:
config.commands['Prefix'] = [config.commands['Prefix']]

View File

@ -1,81 +0,0 @@
import sys
import importlib
import copy
from watchdog.events import FileSystemEventHandler
from houdini.handlers import listeners_from_module, remove_handlers_by_module
from houdini.events import evaluate_handler_file_event
class HandlerFileEventHandler(FileSystemEventHandler):
def __init__(self, server):
self.logger = server.logger
self.server = server
def on_created(self, event):
handler_module_details = evaluate_handler_file_event(event)
if not handler_module_details:
return
handler_module_path, handler_module = handler_module_details
if '__init__.py' in handler_module_path:
return
self.logger.debug('New handler module detected %s', handler_module)
try:
module = importlib.import_module(handler_module)
listeners_from_module(self.server.xt_listeners, self.server.xml_listeners, module)
except Exception as import_error:
self.logger.error('%s detected in %s, not importing.', import_error.__class__.__name__, handler_module)
def on_deleted(self, event):
handler_module_details = evaluate_handler_file_event(event)
if not handler_module_details:
return
handler_module_path, handler_module = handler_module_details
if handler_module not in sys.modules:
return
self.logger.debug('Deleting listeners registered by %s...', handler_module)
remove_handlers_by_module(self.server.xt_listeners, self.server.xml_listeners, handler_module_path)
def on_modified(self, event):
handler_module_details = evaluate_handler_file_event(event)
if not handler_module_details:
return
handler_module_path, handler_module = handler_module_details
if handler_module not in sys.modules:
return False
self.logger.info('Reloading %s', handler_module)
xt_listeners, xml_listeners = copy.copy(self.server.xt_listeners), copy.copy(self.server.xml_listeners)
remove_handlers_by_module(self.server.xt_listeners, self.server.xml_listeners, handler_module_path)
handler_module_object = sys.modules[handler_module]
try:
module = importlib.reload(handler_module_object)
listeners_from_module(self.server.xt_listeners, self.server.xml_listeners, module)
self.logger.info('Successfully reloaded %s!', handler_module)
except Exception as rebuild_error:
self.logger.error('%s detected in %s, not reloading.', rebuild_error.__class__.__name__, handler_module)
self.logger.info('Restoring handler references...')
self.server.xt_listeners = xt_listeners
self.server.xml_listeners = xml_listeners
self.logger.info('Handler references restored. Phew!')

View File

@ -0,0 +1,89 @@
import sys
import importlib
import logging
from watchdog.events import FileSystemEventHandler
from houdini.events import evaluate_listener_file_event
class ListenerFileEventHandler(FileSystemEventHandler):
def __init__(self, server):
self.server = server
self.logger = logging.getLogger('houdini')
def on_created(self, event):
listener_module_details = evaluate_listener_file_event(event)
if not listener_module_details:
return
listener_module_path, listener_module = listener_module_details
if '__init__.py' in listener_module_path:
return
self.logger.debug('New handler module detected %s', listener_module)
try:
module = importlib.import_module(listener_module)
self.server.xt_listeners.load(module)
self.server.xml_listeners.load(module)
except Exception as import_error:
self.logger.error('%s detected in %s, not importing.', import_error.__class__.__name__, listener_module)
def on_deleted(self, event):
listener_module_details = evaluate_listener_file_event(event)
if not listener_module_details:
return
listener_module_path, listener_module = listener_module_details
if listener_module not in sys.modules:
return
listener_module_object = sys.modules[listener_module]
self.logger.debug('Deleting listeners registered by %s...', listener_module)
self.server.xt_listeners.remove(listener_module_object)
self.server.xml_listeners.remove(listener_module_object)
def on_modified(self, event):
listener_module_details = evaluate_listener_file_event(event)
if not listener_module_details:
return
listener_module_path, listener_module = listener_module_details
if listener_module not in sys.modules:
return False
self.logger.info('Reloading %s', listener_module)
self.server.xt_listeners.backup()
self.server.xml_listeners.backup()
self.server.commands.backup()
listener_module_object = sys.modules[listener_module]
self.server.xt_listeners.remove(listener_module_object)
self.server.xml_listeners.remove(listener_module_object)
try:
module = importlib.reload(listener_module_object)
self.server.xt_listeners.load(module)
self.server.xml_listeners.load(module)
self.logger.info('Successfully reloaded %s!', listener_module)
except Exception as rebuild_error:
self.logger.error('%s detected in %s, not reloading.', rebuild_error.__class__.__name__, listener_module)
self.logger.info('Restoring listeners...')
self.server.xt_listeners.restore()
self.server.xml_listeners.restore()
self.server.commands.restore()
self.logger.info('Listeners restored. Phew!')

View File

@ -1,36 +1,35 @@
import sys
import importlib
import os.path
import copy
import logging
import asyncio
from watchdog.events import FileSystemEventHandler
from houdini.events import evaluate_handler_file_event
from houdini.events import evaluate_plugin_file_event
class PluginFileEventHandler(FileSystemEventHandler):
def __init__(self, server):
self.logger = server.logger
self.server = server
self.logger = logging.getLogger('houdini')
def on_created(self, event):
plugin_module_details = evaluate_handler_file_event(event)
plugin_module_details = evaluate_plugin_file_event(event)
if not plugin_module_details:
return
plugin_module_path, plugin_module = plugin_module_details
self.logger.debug('New handler module detected %s', plugin_module)
self.logger.debug('New plugin detected %s', plugin_module)
try:
plugin_module_object = importlib.import_module(plugin_module)
plugin_class = plugin_module_object.__name__.split(".")[2]
asyncio.run(self.server.load_plugin((plugin_module_object, plugin_class)))
self.server.plugin.load(plugin_module_object)
self.logger.info('New plugin \'%s\' has been loaded.' % plugin_class)
self.logger.info('New plugin \'%s\' has been loaded.' % plugin_module)
except Exception as import_error:
self.logger.error('%s detected in %s, not importing.', import_error.__class__.__name__, plugin_module)
@ -45,13 +44,11 @@ class PluginFileEventHandler(FileSystemEventHandler):
self.logger.debug('Deleting listeners registered by %s.', plugin_module)
plugin_module_object = sys.modules[plugin_module]
plugin_class = plugin_module_object.__name__.split(".")[2]
self.server.unload_plugin((plugin_module_object, plugin_class))
self.server.plugins.remove(plugin_module_object)
def on_modified(self, event):
plugin_module_details = evaluate_handler_file_event(event)
plugin_module_details = evaluate_plugin_file_event(event)
if not plugin_module_details:
return
@ -63,24 +60,27 @@ class PluginFileEventHandler(FileSystemEventHandler):
self.logger.info('Reloading %s', plugin_module)
plugin_module_object = sys.modules[plugin_module]
plugin_class = plugin_module_object.__name__.split(".")[2]
xt_listeners, xml_listeners = copy.copy(self.server.xt_listeners), copy.copy(self.server.xml_listeners)
self.server.xt_listeners.backup()
self.server.xml_listeners.backup()
self.server.commands.backup()
self.server.unload_plugin((plugin_module_object, plugin_class))
self.server.plugins.remove(plugin_module_object)
try:
new_plugin_module = importlib.reload(plugin_module_object)
asyncio.run(self.server.load_plugin((new_plugin_module, plugin_class)))
self.server.plugins.load(new_plugin_module)
self.logger.info('Successfully reloaded %s!', plugin_module)
except LookupError as lookup_error:
self.logger.warn('Did not reload plugin \'%s\': %s.', plugin_class, lookup_error)
self.logger.warning('Did not reload plugin \'%s\': %s.', plugin_module, lookup_error)
except Exception as rebuild_error:
self.logger.error('%s detected in %s, not reloading.', rebuild_error.__class__.__name__, plugin_module)
self.logger.info('Restoring handler references...')
self.server.xt_handlers = xt_listeners
self.server.xml_handlers = xml_listeners
self.server.xt_listeners.restore()
self.server.xml_listeners.restore()
self.server.commands.restore()
self.logger.info('Restored handler references. Phew!')

View File

@ -2,12 +2,16 @@ import inspect
import enum
import os
import itertools
import importlib
import sys
import logging
import copy
from types import FunctionType
from houdini.converters import _listener, _ArgumentDeserializer, get_converter, do_conversion, _ConverterContext
from houdini.cooldown import _Cooldown, _CooldownMapping, BucketType
from houdini import plugins
from houdini import plugins, _AbstractManager
class AuthorityError(Exception):
@ -105,11 +109,59 @@ class _XMLListener(_Listener):
return await self.callback(*handler_call_arguments)
def get_relative_function_path(function_obj):
abs_function_file = inspect.getfile(function_obj)
rel_function_file = os.path.relpath(abs_function_file)
class _ListenerManager(_AbstractManager):
def setup(self, module, strict_load=None, exclude_load=None):
for handler_module in self.server.get_package_modules(module):
if not (strict_load and handler_module not in strict_load or exclude_load
and handler_module in exclude_load):
module = sys.modules[handler_module] if handler_module in sys.modules.keys() \
else importlib.import_module(handler_module)
self.load(module)
return rel_function_file
self.logger.info('Handler modules loaded')
def load(self, module):
listener_objects = inspect.getmembers(module, self.is_listener)
for listener_name, listener_object in listener_objects:
if isinstance(module, plugins.IPlugin):
listener_object.instance = module
if listener_object.packet not in self:
self[listener_object.packet] = []
if listener_object not in self[listener_object.packet]:
if listener_object.priority == Priority.High:
self[listener_object.packet].insert(0, listener_object)
elif listener_object.priority == Priority.Override:
self[listener_object.packet] = [listener_object]
else:
self[listener_object.packet].append(listener_object)
for listener_name, listener_object in listener_objects:
for override in listener_object.overrides:
self[override.packet].remove(override)
def remove(self, module):
for handler_id, handler_listeners in self.items():
for handler_listener in handler_listeners:
if module.__name__ == handler_listener.callback.__module__:
handler_listeners.remove(handler_listener)
@classmethod
def is_listener(cls, listener):
return issubclass(type(listener), _Listener)
class XTListenerManager(_ListenerManager):
@classmethod
def is_listener(cls, listener):
return issubclass(type(listener), _XTListener)
class XMLListenerManager(_ListenerManager):
@classmethod
def is_listener(cls, listener):
return issubclass(type(listener), _XMLListener)
def handler(packet, **kwargs):
@ -120,50 +172,6 @@ def handler(packet, **kwargs):
return _listener(listener_class, packet, **kwargs)
def listener_exists(xt_listeners, xml_listeners, packet):
listener_collection = xt_listeners if isinstance(packet, XTPacket) else xml_listeners
return packet in listener_collection
def is_listener(listener):
return issubclass(type(listener), _Listener)
def listeners_from_module(xt_listeners, xml_listeners, module):
listener_objects = inspect.getmembers(module, is_listener)
for listener_name, listener_object in listener_objects:
if isinstance(module, plugins.IPlugin):
listener_object.instance = module
listener_collection = xt_listeners if type(listener_object) == _XTListener else xml_listeners
if listener_object.packet not in listener_collection:
listener_collection[listener_object.packet] = []
if listener_object not in listener_collection[listener_object.packet]:
if listener_object.priority == Priority.High:
listener_collection[listener_object.packet].insert(0, listener_object)
elif listener_object.priority == Priority.Override:
listener_collection[listener_object.packet] = [listener_object]
else:
listener_collection[listener_object.packet].append(listener_object)
for listener_name, listener_object in listener_objects:
listener_collection = xt_listeners if type(listener_object) == _XTListener else xml_listeners
for override in listener_object.overrides:
listener_collection[override.packet].remove(override)
def remove_handlers_by_module(xt_listeners, xml_listeners, handler_module_path):
def remove_handlers(remove_handler_items):
for handler_id, handler_listeners in remove_handler_items:
for handler_listener in handler_listeners:
handler_file = get_relative_function_path(handler_listener.callback)
if handler_file == handler_module_path:
handler_listeners.remove(handler_listener)
remove_handlers(xt_listeners.items())
remove_handlers(xml_listeners.items())
def cooldown(per=1.0, rate=1, bucket_type=BucketType.Default, callback=None):
def decorator(handler_function):
handler_function.__cooldown = _CooldownMapping(callback, _Cooldown(per, rate, bucket_type))

View File

@ -39,13 +39,13 @@ except ImportError:
uvloop = None
import houdini.handlers
from houdini.handlers import listeners_from_module, remove_handlers_by_module
from houdini.events.handler_file_event import HandlerFileEventHandler
import houdini.plugins
from houdini.events.listener_file_event import ListenerFileEventHandler
from houdini.events.plugin_file_event import PluginFileEventHandler
from houdini.commands import commands_from_plugin
import houdini.plugins as plugins
from houdini.handlers import XTListenerManager, XMLListenerManager
from houdini.plugins import PluginManager
from houdini.commands import CommandManager
class HoudiniFactory:
@ -72,9 +72,11 @@ class HoudiniFactory:
self.login_attempts = {}
self.xt_listeners, self.xml_listeners = {}, {}
self.commands = {}
self.plugins = {}
self.xt_listeners = XTListenerManager(self)
self.xml_listeners = XMLListenerManager(self)
self.commands = CommandManager(self)
self.plugins = PluginManager(self)
self.items = None
self.igloos = None
@ -144,7 +146,7 @@ class HoudiniFactory:
self.config.database['Address'],
self.config.database['Name']))
self.logger.info('houdini module instantiated')
self.logger.info('Houdini module instantiated')
self.redis = await aioredis.create_redis_pool('redis://{}:{}'.format(
self.config.redis['Address'], self.config.redis['Port']),
@ -163,10 +165,11 @@ class HoudiniFactory:
PenguinStringCompiler.setup_default_builder(self.penguin_string_compiler)
PenguinStringCompiler.setup_anonymous_default_builder(self.anonymous_penguin_string_compiler)
self.load_handler_modules(exclude_load='houdini.Handlers.Login.Login')
self.xml_listeners.setup(houdini.handlers, exclude_load='houdini.handlers.login.login')
self.xt_listeners.setup(houdini.handlers)
self.logger.info('World server started')
else:
self.load_handler_modules('houdini.Handlers.Login.Login')
self.xml_listeners.setup(houdini.handlers, 'houdini.handlers.login.login')
self.logger.info('Login server started')
self.items = await ItemCrumbsCollection.get_collection()
@ -207,60 +210,20 @@ class HoudiniFactory:
handlers_path = './houdini{}handlers'.format(os.path.sep)
plugins_path = './houdini{}plugins'.format(os.path.sep)
self.configure_observers([handlers_path, HandlerFileEventHandler],
self.configure_observers([handlers_path, ListenerFileEventHandler],
[plugins_path, PluginFileEventHandler])
self.logger.info('Listening on {}:{}'.format(self.server_config['Address'], self.server_config['Port']))
await self.load_plugins()
self.plugins.setup(houdini.plugins)
async with self.server:
await self.server.serve_forever()
async def load_plugins(self):
for plugin_package in self.get_package_modules(plugins):
await self.load_plugin(plugin_package)
async def load_plugin(self, plugin):
plugin_module, plugin_class = plugin
if plugin_class not in self.server_config['Plugins']:
return
plugin_object = getattr(plugin_module, plugin_class)(self)
if isinstance(plugin_object, plugins.IPlugin):
self.plugins[plugin_class] = plugin_object
listeners_from_module(self.xt_listeners, self.xml_listeners, plugin_object)
commands_from_plugin(self.commands, plugin_object)
await plugin_object.ready()
else:
self.logger.warn('{0} plugin object doesn\'t provide the plugin interface'.format(plugin_class))
def unload_plugin(self, plugin):
plugin_module, plugin_class = plugin
if plugin_class in self.plugins:
plugin_module_path = plugin_module.__file__
del self.plugins[plugin_class]
remove_handlers_by_module(self.xt_listeners, self.xml_listeners, plugin_module_path)
async def client_connected(self, reader, writer):
client_object = self.client_class(self, reader, writer)
await client_object.run()
def load_handler_modules(self, strict_load=None, exclude_load=None):
for handler_module in self.get_package_modules(houdini.handlers):
if not (strict_load and handler_module not in strict_load or exclude_load and handler_module in exclude_load):
if handler_module not in sys.modules.keys():
module = importlib.import_module(handler_module)
listeners_from_module(self.xt_listeners, self.xml_listeners, module)
self.logger.info('Handler modules loaded')
def get_package_modules(self, package):
package_modules = []
@ -271,8 +234,8 @@ class HoudiniFactory:
subpackage_object = importlib.import_module(full_module_name, package=package.__path__)
subpackage_object_directory = dir(subpackage_object)
if plugins.IPlugin.__name__ in subpackage_object_directory:
package_modules.append((subpackage_object, module_name))
if houdini.plugins.IPlugin.__name__ in subpackage_object_directory:
package_modules.append(subpackage_object)
continue
sub_package_modules = self.get_package_modules(subpackage_object)

View File

@ -1,6 +1,11 @@
from abc import ABC
from abc import abstractmethod
import inspect
import asyncio
from houdini import _AbstractManager
class IPlugin(ABC):
"""
@ -24,10 +29,42 @@ class IPlugin(ABC):
@abstractmethod
async def ready(self):
"""
Called when the plugin is ready to function.
"""
"""Called when the plugin is ready to function."""
@abstractmethod
def __init__(self, server):
self.server = server
class PluginManager(_AbstractManager):
def setup(self, module):
for plugin_package in self.server.get_package_modules(module):
self.load(plugin_package)
def load(self, module):
plugin_class, plugin_type = inspect.getmembers(module, is_plugin).pop()
if self.server.server_config['Plugins'] is not True and \
plugin_class not in self.server.server_config['Plugins']:
return
plugin_object = plugin_type(self.server)
self[module.__name__] = plugin_object
self.server.commands.load(plugin_object)
self.server.xt_listeners.load(plugin_object)
self.server.xml_listeners.load(plugin_object)
asyncio.run_coroutine_threadsafe(plugin_object.ready(), self.server.server.get_loop())
def remove(self, module):
if module.__name__ in self:
del self[module.__name__]
self.server.commands.remove(module)
self.server.xt_listeners.remove(module)
self.server.xml_listeners.remove(module)
def is_plugin(plugin_class):
return inspect.isclass(plugin_class) and issubclass(plugin_class, IPlugin) and plugin_class != IPlugin

View File

@ -85,7 +85,7 @@ class Spheniscidae:
packet_id = parsed_data[2]
packet = XTPacket(packet_id)
if handlers.listener_exists(self.server.xt_listeners, self.server.xml_listeners, packet):
if packet in self.server.xt_listeners:
xt_listeners = self.server.xt_listeners[packet]
packet_data = parsed_data[4:]
@ -111,7 +111,7 @@ class Spheniscidae:
action = body_tag.get('action')
packet = XMLPacket(action)
if handlers.listener_exists(self.server.xt_listeners, self.server.xml_listeners, packet):
if packet in self.server.xml_listeners:
xml_listeners = self.server.xml_listeners[packet]
for listener in xml_listeners: