2018-09-30 19:12:28 +00:00
|
|
|
import gc, glob, imp, io, inspect, os, sys, uuid
|
2018-10-03 12:22:37 +00:00
|
|
|
from . import utils
|
2016-03-29 11:56:58 +00:00
|
|
|
|
2018-09-19 11:35:34 +00:00
|
|
|
BITBOT_HOOKS_MAGIC = "__bitbot_hooks"
|
2018-09-27 10:45:23 +00:00
|
|
|
BITBOT_EXPORTS_MAGIC = "__bitbot_exports"
|
2018-09-19 11:35:34 +00:00
|
|
|
|
2018-09-24 12:10:39 +00:00
|
|
|
class ModuleException(Exception):
|
|
|
|
pass
|
|
|
|
class ModuleWarning(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class ModuleNotFoundException(ModuleException):
|
|
|
|
pass
|
|
|
|
class ModuleNameCollisionException(ModuleException):
|
|
|
|
pass
|
|
|
|
class ModuleLoadException(ModuleException):
|
|
|
|
pass
|
|
|
|
class ModuleUnloadException(ModuleException):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class ModuleNotLoadedWarning(ModuleWarning):
|
|
|
|
pass
|
|
|
|
|
2018-09-19 12:28:18 +00:00
|
|
|
class BaseModule(object):
|
2018-10-12 16:54:15 +00:00
|
|
|
def __init__(self, bot, events, exports, timers):
|
2018-09-27 10:46:10 +00:00
|
|
|
self.bot = bot
|
|
|
|
self.events = events
|
|
|
|
self.exports = exports
|
2018-10-12 16:54:15 +00:00
|
|
|
self.timers = timers
|
2018-10-12 17:07:23 +00:00
|
|
|
self.on_load()
|
|
|
|
def on_load(self):
|
|
|
|
pass
|
2018-09-19 12:28:18 +00:00
|
|
|
|
2016-03-29 11:56:58 +00:00
|
|
|
class ModuleManager(object):
|
2018-10-12 16:54:15 +00:00
|
|
|
def __init__(self, events, exports, timers, config, log, directory):
|
2018-08-31 11:55:52 +00:00
|
|
|
self.events = events
|
2018-09-02 18:54:45 +00:00
|
|
|
self.exports = exports
|
2018-09-28 15:51:36 +00:00
|
|
|
self.config = config
|
2018-10-12 16:54:15 +00:00
|
|
|
self.timers = timers
|
2018-09-28 15:51:36 +00:00
|
|
|
self.log = log
|
2016-03-29 11:56:58 +00:00
|
|
|
self.directory = directory
|
2018-09-28 15:51:36 +00:00
|
|
|
|
2016-03-29 11:56:58 +00:00
|
|
|
self.modules = {}
|
2016-04-04 11:36:59 +00:00
|
|
|
self.waiting_requirement = {}
|
2018-09-28 15:51:36 +00:00
|
|
|
|
2016-03-29 11:56:58 +00:00
|
|
|
def list_modules(self):
|
|
|
|
return sorted(glob.glob(os.path.join(self.directory, "*.py")))
|
2016-04-04 11:36:59 +00:00
|
|
|
|
2018-09-01 10:29:26 +00:00
|
|
|
def _module_name(self, path):
|
|
|
|
return os.path.basename(path).rsplit(".py", 1)[0].lower()
|
|
|
|
def _module_path(self, name):
|
|
|
|
return os.path.join(self.directory, "%s.py" % name)
|
2018-09-24 15:20:58 +00:00
|
|
|
def _import_name(self, name):
|
|
|
|
return "bitbot_%s" % name
|
2016-04-04 11:36:59 +00:00
|
|
|
|
2018-09-27 10:45:23 +00:00
|
|
|
def _get_magic(self, obj, magic, default):
|
|
|
|
return getattr(obj, magic) if hasattr(obj, magic) else default
|
|
|
|
|
2018-09-28 15:51:36 +00:00
|
|
|
def _load_module(self, bot, name):
|
2018-09-01 10:29:26 +00:00
|
|
|
path = self._module_path(name)
|
2017-07-12 09:00:27 +00:00
|
|
|
|
2018-10-03 12:22:37 +00:00
|
|
|
for hashflag, value in utils.get_hashflags(path):
|
2018-09-29 08:23:40 +00:00
|
|
|
if hashflag == "ignore":
|
|
|
|
# nope, ignore this module.
|
|
|
|
raise ModuleNotLoadedWarning("module ignored")
|
|
|
|
|
|
|
|
elif hashflag == "require-config" and value:
|
|
|
|
if not self.config.get(value.lower(), None):
|
|
|
|
# nope, required config option not present.
|
|
|
|
raise ModuleNotLoadedWarning("required config not present")
|
|
|
|
|
|
|
|
elif hashflag == "require-module" and value:
|
|
|
|
requirement = value.lower()
|
|
|
|
if not requirement in self.modules:
|
|
|
|
if not requirement in self.waiting_requirement:
|
|
|
|
self.waiting_requirement[requirement] = set([])
|
|
|
|
self.waiting_requirement[requirement].add(path)
|
|
|
|
raise ModuleNotLoadedWarning("waiting for requirement")
|
|
|
|
|
2018-09-24 15:20:58 +00:00
|
|
|
module = imp.load_source(self._import_name(name), path)
|
2018-08-31 11:55:52 +00:00
|
|
|
|
2018-01-09 15:35:33 +00:00
|
|
|
if not hasattr(module, "Module"):
|
2018-09-24 12:10:39 +00:00
|
|
|
raise ModuleLoadException("module '%s' doesn't have a "
|
2018-09-26 10:20:18 +00:00
|
|
|
"'Module' class." % name)
|
2018-01-09 15:35:33 +00:00
|
|
|
if not inspect.isclass(module.Module):
|
2018-09-24 12:10:39 +00:00
|
|
|
raise ModuleLoadException("module '%s' has a 'Module' attribute "
|
2018-09-26 10:20:18 +00:00
|
|
|
"but it is not a class." % name)
|
2018-08-31 11:55:52 +00:00
|
|
|
|
2018-09-02 18:54:45 +00:00
|
|
|
context = str(uuid.uuid4())
|
2018-09-19 11:35:34 +00:00
|
|
|
context_events = self.events.new_context(context)
|
|
|
|
context_exports = self.exports.new_context(context)
|
2018-10-12 16:54:15 +00:00
|
|
|
context_timers = self.timers.new_context(context)
|
|
|
|
module_object = module.Module(bot, context_events, context_exports,
|
|
|
|
context_timers)
|
2018-09-19 11:35:34 +00:00
|
|
|
|
2016-03-29 11:56:58 +00:00
|
|
|
if not hasattr(module_object, "_name"):
|
|
|
|
module_object._name = name.title()
|
2018-09-19 11:35:34 +00:00
|
|
|
for attribute_name in dir(module_object):
|
|
|
|
attribute = getattr(module_object, attribute_name)
|
2018-09-27 10:45:23 +00:00
|
|
|
for hook in self._get_magic(attribute, BITBOT_HOOKS_MAGIC, []):
|
|
|
|
context_events.on(hook["event"]).hook(attribute,
|
2018-10-03 12:22:37 +00:00
|
|
|
**hook["kwargs"])
|
2018-09-27 10:45:23 +00:00
|
|
|
for export in self._get_magic(module_object, BITBOT_EXPORTS_MAGIC, []):
|
|
|
|
context_exports.add(export["setting"], export["value"])
|
2018-09-19 11:35:34 +00:00
|
|
|
|
2018-09-02 18:54:45 +00:00
|
|
|
module_object._context = context
|
2018-08-31 11:55:52 +00:00
|
|
|
module_object._import_name = name
|
|
|
|
|
2018-09-26 10:19:48 +00:00
|
|
|
if name in self.modules:
|
|
|
|
raise ModuleNameCollisionException("Module name '%s' "
|
|
|
|
"attempted to be used twice")
|
2016-03-29 11:56:58 +00:00
|
|
|
return module_object
|
2016-04-04 11:36:59 +00:00
|
|
|
|
2018-09-28 15:51:36 +00:00
|
|
|
def load_module(self, bot, name):
|
2017-09-05 09:03:38 +00:00
|
|
|
try:
|
2018-09-28 15:51:36 +00:00
|
|
|
module = self._load_module(bot, name)
|
2018-09-24 12:10:39 +00:00
|
|
|
except ModuleWarning as warning:
|
2018-09-28 15:51:36 +00:00
|
|
|
self.log.error("Module '%s' not loaded", [name])
|
2018-09-24 12:10:39 +00:00
|
|
|
raise
|
|
|
|
except Exception as e:
|
2018-09-28 15:51:36 +00:00
|
|
|
self.log.error("Failed to load module \"%s\": %s",
|
2018-09-24 14:13:27 +00:00
|
|
|
[name, str(e)])
|
2018-09-24 12:10:39 +00:00
|
|
|
raise
|
|
|
|
|
|
|
|
self.modules[module._import_name] = module
|
|
|
|
if name in self.waiting_requirement:
|
|
|
|
for requirement_name in self.waiting_requirement:
|
2018-09-28 15:51:36 +00:00
|
|
|
self.load_module(bot, requirement_name)
|
|
|
|
self.log.info("Module '%s' loaded", [name])
|
2017-09-05 09:03:38 +00:00
|
|
|
|
2018-09-28 15:51:36 +00:00
|
|
|
def load_modules(self, bot, whitelist=[], blacklist=[]):
|
2018-09-01 10:29:26 +00:00
|
|
|
for path in self.list_modules():
|
|
|
|
name = self._module_name(path)
|
2018-09-13 12:35:05 +00:00
|
|
|
if name in whitelist or (not whitelist and not name in blacklist):
|
2018-09-24 12:10:39 +00:00
|
|
|
try:
|
2018-09-28 15:51:36 +00:00
|
|
|
self.load_module(bot, name)
|
2018-09-24 12:10:39 +00:00
|
|
|
except ModuleWarning:
|
|
|
|
pass
|
2018-09-01 10:29:26 +00:00
|
|
|
|
|
|
|
def unload_module(self, name):
|
2018-09-24 12:10:39 +00:00
|
|
|
if not name in self.modules:
|
|
|
|
raise ModuleNotFoundException()
|
2018-09-01 10:29:26 +00:00
|
|
|
module = self.modules[name]
|
2018-10-04 13:45:32 +00:00
|
|
|
if hasattr(module, "unload"):
|
|
|
|
try:
|
|
|
|
module.unload()
|
|
|
|
except:
|
|
|
|
pass
|
2018-09-01 10:29:26 +00:00
|
|
|
del self.modules[name]
|
|
|
|
|
2018-09-02 18:54:45 +00:00
|
|
|
context = module._context
|
|
|
|
self.events.purge_context(context)
|
|
|
|
self.exports.purge_context(context)
|
2018-10-12 16:54:15 +00:00
|
|
|
self.timers.purge_context(context)
|
2018-09-01 10:29:26 +00:00
|
|
|
|
2018-09-24 15:20:58 +00:00
|
|
|
del sys.modules[self._import_name(name)]
|
2018-09-01 17:49:50 +00:00
|
|
|
references = sys.getrefcount(module)
|
2018-09-30 19:12:28 +00:00
|
|
|
referrers = gc.get_referrers(module)
|
2018-09-01 10:29:26 +00:00
|
|
|
del module
|
2018-09-01 17:49:50 +00:00
|
|
|
references -= 1 # 'del module' removes one reference
|
|
|
|
references -= 1 # one of the refs is from getrefcount
|
|
|
|
|
2018-09-28 15:51:36 +00:00
|
|
|
self.log.info("Module '%s' unloaded (%d reference%s)",
|
2018-09-01 17:49:50 +00:00
|
|
|
[name, references, "" if references == 1 else "s"])
|
2018-09-30 19:12:28 +00:00
|
|
|
if references > 0:
|
|
|
|
self.log.info("References left for '%s': %s",
|
|
|
|
[name, ", ".join([str(referrer) for referrer in referrers])])
|