mirror of
https://github.com/solero/houdini.git
synced 2025-01-23 04:37:00 +00:00
Remove all code related to "hot-reloading"
Support is being dropped for now and until further notice.
This commit is contained in:
parent
9098ae50a1
commit
8a9f2e8b99
@ -5,7 +5,8 @@ from abc import abstractmethod
|
|||||||
import asyncio
|
import asyncio
|
||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
import copy
|
import importlib
|
||||||
|
import pkgutil
|
||||||
|
|
||||||
|
|
||||||
class StatusField(enum.IntEnum):
|
class StatusField(enum.IntEnum):
|
||||||
@ -19,7 +20,7 @@ class StatusField(enum.IntEnum):
|
|||||||
HasWalkedPuffleFirstTime = 65536
|
HasWalkedPuffleFirstTime = 65536
|
||||||
HasWalkedPuffleSecondTime = 131072
|
HasWalkedPuffleSecondTime = 131072
|
||||||
|
|
||||||
|
package_modules = package_modules + sub_package_modules
|
||||||
class ConflictResolution(enum.Enum):
|
class ConflictResolution(enum.Enum):
|
||||||
Silent = 0
|
Silent = 0
|
||||||
Append = 1
|
Append = 1
|
||||||
@ -45,24 +46,15 @@ class _AbstractManager(dict):
|
|||||||
self.server = server
|
self.server = server
|
||||||
self.logger = logging.getLogger('houdini')
|
self.logger = logging.getLogger('houdini')
|
||||||
|
|
||||||
self.__backup = None
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def load(self, module):
|
async def setup(self, module):
|
||||||
"""Loads entries from module"""
|
"""Setup manager class"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def remove(self, module):
|
async def load(self, module):
|
||||||
"""Removes all entries by module"""
|
"""Loads entries from 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):
|
class PenguinStringCompiler(OrderedDict):
|
||||||
|
@ -64,6 +64,7 @@ player_in_room = handlers.player_in_room
|
|||||||
|
|
||||||
class CommandManager(_AbstractManager):
|
class CommandManager(_AbstractManager):
|
||||||
def load(self, module):
|
def load(self, module):
|
||||||
|
async def load(self, module):
|
||||||
command_objects = inspect.getmembers(module, is_command)
|
command_objects = inspect.getmembers(module, is_command)
|
||||||
if not isinstance(module, plugins.IPlugin):
|
if not isinstance(module, plugins.IPlugin):
|
||||||
raise TypeError('Commands can only be loaded from plugins')
|
raise TypeError('Commands can only be loaded from plugins')
|
||||||
@ -94,12 +95,6 @@ class CommandManager(_AbstractManager):
|
|||||||
else:
|
else:
|
||||||
parent_commands[name] = [command_object]
|
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):
|
def is_command(command_object):
|
||||||
return issubclass(type(command_object), _Command)
|
return issubclass(type(command_object), _Command)
|
||||||
|
@ -1,41 +0,0 @@
|
|||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate_listener_file_event(listener_file_event):
|
|
||||||
# Ignore all directory events
|
|
||||||
if listener_file_event.is_directory:
|
|
||||||
return False
|
|
||||||
|
|
||||||
listener_module_path = listener_file_event.src_path[2:]
|
|
||||||
|
|
||||||
# Ignore non-Python files
|
|
||||||
if listener_module_path[-3:] != ".py":
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Ignore package index files
|
|
||||||
if '__init__.py' in listener_module_path:
|
|
||||||
return False
|
|
||||||
|
|
||||||
listener_module = listener_module_path.replace(os.path.sep, ".")[:-3]
|
|
||||||
|
|
||||||
return listener_module_path, listener_module
|
|
||||||
|
|
||||||
|
|
||||||
def evaluate_plugin_file_event(plugin_file_event):
|
|
||||||
# Ignore all directory events
|
|
||||||
if plugin_file_event.is_directory:
|
|
||||||
return False
|
|
||||||
|
|
||||||
plugin_module_path = plugin_file_event.src_path[2:]
|
|
||||||
|
|
||||||
# Ignore non-Python files
|
|
||||||
if plugin_module_path[-3:] != ".py":
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Remove file extension and replace path separator with dots. Then make like a banana.. and split.
|
|
||||||
plugin_module_tokens = plugin_module_path.replace(os.path.sep, ".")[:-3].split(".")
|
|
||||||
|
|
||||||
if plugin_module_tokens.pop() == "__init__":
|
|
||||||
return plugin_module_path, ".".join(plugin_module_tokens)
|
|
||||||
|
|
||||||
return False
|
|
@ -1,88 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
self.logger.debug('New listener module detected {}'.format(listener_module))
|
|
||||||
|
|
||||||
try:
|
|
||||||
module = importlib.import_module(listener_module)
|
|
||||||
self.server.xt_listeners.load(module)
|
|
||||||
self.server.xml_listeners.load(module)
|
|
||||||
|
|
||||||
self.logger.info('New listener module loaded {}'.format(listener_module))
|
|
||||||
except Exception as import_error:
|
|
||||||
self.logger.error('{} detected in {}, not importing'.format(
|
|
||||||
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 listener module {}'.format(listener_module))
|
|
||||||
|
|
||||||
self.server.xt_listeners.remove(listener_module_object)
|
|
||||||
self.server.xml_listeners.remove(listener_module_object)
|
|
||||||
del sys.modules[listener_module]
|
|
||||||
|
|
||||||
self.logger.info('Deleted listener module {}'.format(listener_module))
|
|
||||||
|
|
||||||
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 listener module {}'.format(listener_module))
|
|
||||||
|
|
||||||
self.server.xt_listeners.backup()
|
|
||||||
self.server.xml_listeners.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 listener module {}!'.format(listener_module))
|
|
||||||
except Exception as rebuild_error:
|
|
||||||
self.logger.error('{} detected in {}, not reloading.'.format(
|
|
||||||
rebuild_error.__class__.__name__, listener_module))
|
|
||||||
|
|
||||||
self.server.xt_listeners.restore()
|
|
||||||
self.server.xml_listeners.restore()
|
|
@ -1,87 +0,0 @@
|
|||||||
import sys
|
|
||||||
import importlib
|
|
||||||
import os.path
|
|
||||||
import logging
|
|
||||||
from watchdog.events import FileSystemEventHandler
|
|
||||||
|
|
||||||
from houdini.events import evaluate_plugin_file_event
|
|
||||||
|
|
||||||
|
|
||||||
class PluginFileEventHandler(FileSystemEventHandler):
|
|
||||||
|
|
||||||
def __init__(self, server):
|
|
||||||
self.server = server
|
|
||||||
self.logger = logging.getLogger('houdini')
|
|
||||||
|
|
||||||
def on_created(self, 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 plugin detected {}'.format(plugin_module))
|
|
||||||
|
|
||||||
try:
|
|
||||||
plugin_module_object = importlib.import_module(plugin_module)
|
|
||||||
|
|
||||||
self.server.plugins.load(plugin_module_object)
|
|
||||||
|
|
||||||
self.logger.info('New plugin {} has been loaded'.format(plugin_module))
|
|
||||||
except Exception as import_error:
|
|
||||||
self.logger.error('{} detected in {}, not importing'.format(
|
|
||||||
import_error.__class__.__name__, plugin_module))
|
|
||||||
|
|
||||||
def on_deleted(self, event):
|
|
||||||
plugin_module_path = event.src_path[2:]
|
|
||||||
|
|
||||||
plugin_module = plugin_module_path.replace(os.path.sep, ".")
|
|
||||||
|
|
||||||
if plugin_module not in sys.modules:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.logger.debug('Deleting plugin {}'.format(plugin_module))
|
|
||||||
|
|
||||||
plugin_module_object = sys.modules[plugin_module]
|
|
||||||
|
|
||||||
self.server.plugins.remove(plugin_module_object)
|
|
||||||
del sys.modules[plugin_module]
|
|
||||||
|
|
||||||
self.logger.info('Plugin {} has been removed'.format(plugin_module))
|
|
||||||
|
|
||||||
def on_modified(self, event):
|
|
||||||
plugin_module_details = evaluate_plugin_file_event(event)
|
|
||||||
if not plugin_module_details:
|
|
||||||
return
|
|
||||||
|
|
||||||
plugin_module_path, plugin_module = plugin_module_details
|
|
||||||
|
|
||||||
if plugin_module not in sys.modules:
|
|
||||||
return
|
|
||||||
|
|
||||||
self.logger.info('Reloading plugin {}'.format(plugin_module))
|
|
||||||
|
|
||||||
plugin_module_object = sys.modules[plugin_module]
|
|
||||||
|
|
||||||
self.server.xt_listeners.backup()
|
|
||||||
self.server.xml_listeners.backup()
|
|
||||||
self.server.commands.backup()
|
|
||||||
|
|
||||||
self.server.plugins.remove(plugin_module_object)
|
|
||||||
|
|
||||||
try:
|
|
||||||
new_plugin_module = importlib.reload(plugin_module_object)
|
|
||||||
|
|
||||||
self.server.plugins.load(new_plugin_module)
|
|
||||||
|
|
||||||
self.logger.info('Successfully reloaded plugin {}'.format(plugin_module))
|
|
||||||
except LookupError as lookup_error:
|
|
||||||
self.logger.warning('Did not reload plugin \'{}\': {}'.format(plugin_module, lookup_error))
|
|
||||||
except Exception as rebuild_error:
|
|
||||||
self.logger.error('{} detected in {}, not reloading'.format(
|
|
||||||
rebuild_error.__class__.__name__, plugin_module))
|
|
||||||
|
|
||||||
self.server.xt_listeners.restore()
|
|
||||||
self.server.xml_listeners.restore()
|
|
||||||
self.server.commands.restore()
|
|
@ -118,16 +118,12 @@ class _ListenerManager(_AbstractManager):
|
|||||||
|
|
||||||
super().__init__(server)
|
super().__init__(server)
|
||||||
|
|
||||||
def setup(self, module, strict_load=None, exclude_load=None):
|
async def setup(self, module, strict_load=None, exclude_load=None):
|
||||||
self.strict_load, self.exclude_load = strict_load, exclude_load
|
self.strict_load, self.exclude_load = strict_load, exclude_load
|
||||||
for handler_module in self.server.get_package_modules(module):
|
for handler_module in get_package_modules(module):
|
||||||
module = sys.modules[handler_module] if handler_module in sys.modules.keys() \
|
await self.load(handler_module)
|
||||||
else importlib.import_module(handler_module)
|
|
||||||
self.load(module)
|
|
||||||
|
|
||||||
self.logger.debug('Loaded {} listeners'.format(len(self)))
|
async def load(self, module):
|
||||||
|
|
||||||
def load(self, module):
|
|
||||||
module_name = module.__module__ if isinstance(module, plugins.IPlugin) else module.__name__
|
module_name = module.__module__ if isinstance(module, plugins.IPlugin) else module.__name__
|
||||||
if not (self.strict_load and module_name not in self.strict_load or
|
if not (self.strict_load and module_name not in self.strict_load or
|
||||||
self.exclude_load and module_name in self.exclude_load):
|
self.exclude_load and module_name in self.exclude_load):
|
||||||
@ -159,12 +155,6 @@ class _ListenerManager(_AbstractManager):
|
|||||||
for override in listener_object.overrides:
|
for override in listener_object.overrides:
|
||||||
self[override.packet].remove(override)
|
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
|
@classmethod
|
||||||
def is_listener(cls, listener):
|
def is_listener(cls, listener):
|
||||||
return issubclass(type(listener), _Listener)
|
return issubclass(type(listener), _Listener)
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import pkgutil
|
|
||||||
import importlib
|
|
||||||
import copy
|
import copy
|
||||||
|
|
||||||
from houdini.spheniscidae import Spheniscidae
|
from houdini.spheniscidae import Spheniscidae
|
||||||
@ -15,7 +13,6 @@ from logging.handlers import RotatingFileHandler
|
|||||||
|
|
||||||
import aioredis
|
import aioredis
|
||||||
from aiocache import SimpleMemoryCache, caches
|
from aiocache import SimpleMemoryCache, caches
|
||||||
from watchdog.observers import Observer
|
|
||||||
|
|
||||||
from houdini.data import db
|
from houdini.data import db
|
||||||
from houdini.data.item import ItemCrumbsCollection
|
from houdini.data.item import ItemCrumbsCollection
|
||||||
@ -37,8 +34,6 @@ except ImportError:
|
|||||||
|
|
||||||
import houdini.handlers
|
import houdini.handlers
|
||||||
import houdini.plugins
|
import houdini.plugins
|
||||||
from houdini.events.listener_file_event import ListenerFileEventHandler
|
|
||||||
from houdini.events.plugin_file_event import PluginFileEventHandler
|
|
||||||
|
|
||||||
from houdini.handlers import XTListenerManager, XMLListenerManager
|
from houdini.handlers import XTListenerManager, XMLListenerManager
|
||||||
from houdini.plugins import PluginManager
|
from houdini.plugins import PluginManager
|
||||||
@ -170,11 +165,12 @@ class Houdini:
|
|||||||
PenguinStringCompiler.setup_default_builder(self.penguin_string_compiler)
|
PenguinStringCompiler.setup_default_builder(self.penguin_string_compiler)
|
||||||
PenguinStringCompiler.setup_anonymous_default_builder(self.anonymous_penguin_string_compiler)
|
PenguinStringCompiler.setup_anonymous_default_builder(self.anonymous_penguin_string_compiler)
|
||||||
|
|
||||||
self.xml_listeners.setup(houdini.handlers, exclude_load='houdini.handlers.login.login')
|
await self.xml_listeners.setup(houdini.handlers, exclude_load='houdini.handlers.login.login')
|
||||||
self.xt_listeners.setup(houdini.handlers)
|
await self.xt_listeners.setup(houdini.handlers)
|
||||||
|
await self.dummy_event_listeners.setup(houdini.handlers)
|
||||||
self.logger.info('World server started')
|
self.logger.info('World server started')
|
||||||
else:
|
else:
|
||||||
self.xml_listeners.setup(houdini.handlers, 'houdini.handlers.login.login')
|
await self.xml_listeners.setup(houdini.handlers, 'houdini.handlers.login.login')
|
||||||
self.logger.info('Login server started')
|
self.logger.info('Login server started')
|
||||||
|
|
||||||
self.items = await ItemCrumbsCollection.get_collection()
|
self.items = await ItemCrumbsCollection.get_collection()
|
||||||
@ -218,11 +214,6 @@ class Houdini:
|
|||||||
|
|
||||||
self.permissions = await PermissionCrumbsCollection.get_collection()
|
self.permissions = await PermissionCrumbsCollection.get_collection()
|
||||||
|
|
||||||
handlers_path = os.path.join(os.path.dirname(__file__), 'handlers')
|
|
||||||
plugins_path = os.path.join(os.path.dirname(__file__), 'plugins')
|
|
||||||
self.configure_observers([handlers_path, ListenerFileEventHandler],
|
|
||||||
[plugins_path, PluginFileEventHandler])
|
|
||||||
|
|
||||||
self.logger.info('Multi-client support is {}'.format(
|
self.logger.info('Multi-client support is {}'.format(
|
||||||
'enabled' if self.config.client['MultiClientSupport'] else 'disabled'))
|
'enabled' if self.config.client['MultiClientSupport'] else 'disabled'))
|
||||||
self.logger.info('Listening on {}:{}'.format(self.server_config['Address'], self.server_config['Port']))
|
self.logger.info('Listening on {}:{}'.format(self.server_config['Address'], self.server_config['Port']))
|
||||||
@ -231,7 +222,7 @@ class Houdini:
|
|||||||
self.logger.warning('The static key has been changed from the default, '
|
self.logger.warning('The static key has been changed from the default, '
|
||||||
'this may cause authentication issues!')
|
'this may cause authentication issues!')
|
||||||
|
|
||||||
self.plugins.setup(houdini.plugins)
|
await self.plugins.setup(houdini.plugins)
|
||||||
|
|
||||||
async with self.server:
|
async with self.server:
|
||||||
await self.server.serve_forever()
|
await self.server.serve_forever()
|
||||||
@ -239,31 +230,3 @@ class Houdini:
|
|||||||
async def client_connected(self, reader, writer):
|
async def client_connected(self, reader, writer):
|
||||||
client_object = self.client_class(self, reader, writer)
|
client_object = self.client_class(self, reader, writer)
|
||||||
await client_object.run()
|
await client_object.run()
|
||||||
|
|
||||||
def get_package_modules(self, package):
|
|
||||||
package_modules = []
|
|
||||||
|
|
||||||
for importer, module_name, is_package in pkgutil.iter_modules(package.__path__):
|
|
||||||
full_module_name = '{0}.{1}'.format(package.__name__, module_name)
|
|
||||||
|
|
||||||
if is_package:
|
|
||||||
subpackage_object = importlib.import_module(full_module_name, package=package.__path__)
|
|
||||||
subpackage_object_directory = dir(subpackage_object)
|
|
||||||
|
|
||||||
if houdini.plugins.IPlugin.__name__ in subpackage_object_directory:
|
|
||||||
package_modules.append(subpackage_object)
|
|
||||||
continue
|
|
||||||
|
|
||||||
sub_package_modules = self.get_package_modules(subpackage_object)
|
|
||||||
|
|
||||||
package_modules = package_modules + sub_package_modules
|
|
||||||
else:
|
|
||||||
package_modules.append(full_module_name)
|
|
||||||
|
|
||||||
return package_modules
|
|
||||||
|
|
||||||
def configure_observers(self, *observer_settings):
|
|
||||||
for observer_path, observer_class in observer_settings:
|
|
||||||
event_observer = Observer()
|
|
||||||
event_observer.schedule(observer_class(self), observer_path, recursive=True)
|
|
||||||
event_observer.start()
|
|
||||||
|
@ -2,9 +2,8 @@ from abc import ABC
|
|||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from houdini import _AbstractManager
|
from houdini import _AbstractManager, get_package_modules
|
||||||
|
|
||||||
|
|
||||||
class IPlugin(ABC):
|
class IPlugin(ABC):
|
||||||
@ -37,11 +36,11 @@ class IPlugin(ABC):
|
|||||||
|
|
||||||
|
|
||||||
class PluginManager(_AbstractManager):
|
class PluginManager(_AbstractManager):
|
||||||
def setup(self, module):
|
async def setup(self, module):
|
||||||
for plugin_package in self.server.get_package_modules(module):
|
for plugin_package in get_package_modules(module):
|
||||||
self.load(plugin_package)
|
await self.load(plugin_package)
|
||||||
|
|
||||||
def load(self, module):
|
async def load(self, module):
|
||||||
plugin_class, plugin_type = inspect.getmembers(module, is_plugin).pop()
|
plugin_class, plugin_type = inspect.getmembers(module, is_plugin).pop()
|
||||||
|
|
||||||
if self.server.server_config['Plugins'] is not True and \
|
if self.server.server_config['Plugins'] is not True and \
|
||||||
@ -51,19 +50,12 @@ class PluginManager(_AbstractManager):
|
|||||||
plugin_object = plugin_type(self.server)
|
plugin_object = plugin_type(self.server)
|
||||||
self[module.__name__] = plugin_object
|
self[module.__name__] = plugin_object
|
||||||
|
|
||||||
self.server.commands.load(plugin_object)
|
await self.server.commands.load(plugin_object)
|
||||||
self.server.xt_listeners.load(plugin_object)
|
await self.server.xt_listeners.load(plugin_object)
|
||||||
self.server.xml_listeners.load(plugin_object)
|
await self.server.xml_listeners.load(plugin_object)
|
||||||
|
await self.server.dummy_event_listeners.load(plugin_object)
|
||||||
|
|
||||||
asyncio.run_coroutine_threadsafe(plugin_object.ready(), self.server.server.get_loop())
|
await plugin_object.ready()
|
||||||
|
|
||||||
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):
|
def is_plugin(plugin_class):
|
||||||
|
Loading…
Reference in New Issue
Block a user