2019-02-05 13:34:13 +00:00
|
|
|
import enum, gc, glob, importlib, io, inspect, os, sys, typing, uuid
|
2018-10-30 14:58:48 +00:00
|
|
|
from src import Config, EventManager, Exports, IRCBot, Logging, Timers, utils
|
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
|
|
|
|
|
2019-05-25 17:24:50 +00:00
|
|
|
class ModuleDependencyNotFulfilled(ModuleException):
|
|
|
|
def __init__(self, message, dependency):
|
|
|
|
ModuleException.__init__(self, message)
|
|
|
|
self.dependency = dependency
|
|
|
|
|
2019-01-18 12:49:11 +00:00
|
|
|
class ModuleType(enum.Enum):
|
|
|
|
FILE = 0
|
|
|
|
DIRECTORY = 1
|
|
|
|
|
2018-09-19 12:28:18 +00:00
|
|
|
class BaseModule(object):
|
2018-10-30 14:58:48 +00:00
|
|
|
def __init__(self,
|
|
|
|
bot: "IRCBot.Bot",
|
|
|
|
events: EventManager.EventHook,
|
|
|
|
exports: Exports.Exports,
|
2018-11-05 12:38:40 +00:00
|
|
|
timers: Timers.Timers,
|
|
|
|
log: Logging.Log):
|
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-11-05 12:38:40 +00:00
|
|
|
self.log = log
|
2018-10-12 17:07:23 +00:00
|
|
|
self.on_load()
|
|
|
|
def on_load(self):
|
|
|
|
pass
|
2018-12-02 16:00:55 +00:00
|
|
|
def unload(self):
|
|
|
|
pass
|
2019-02-24 10:43:46 +00:00
|
|
|
|
|
|
|
def command_line(self, args: str):
|
|
|
|
pass
|
|
|
|
|
2019-05-25 17:24:50 +00:00
|
|
|
class ModuleDefinition(object):
|
|
|
|
def __init__(self,
|
|
|
|
name: str,
|
|
|
|
filename: str,
|
|
|
|
type: ModuleType,
|
2019-05-25 20:42:42 +00:00
|
|
|
hashflags: typing.List[typing.Tuple[str, typing.Optional[str]]]):
|
2019-05-25 17:24:50 +00:00
|
|
|
self.name = name
|
|
|
|
self.filename = filename
|
|
|
|
self.type = type
|
|
|
|
self.hashflags = hashflags
|
|
|
|
def get_dependencies(self):
|
|
|
|
dependencies = []
|
|
|
|
for key, value in self.hashflags:
|
|
|
|
if key == "depends-on":
|
|
|
|
dependencies.append(value)
|
|
|
|
return sorted(dependencies)
|
|
|
|
|
2018-12-09 11:18:55 +00:00
|
|
|
class LoadedModule(object):
|
|
|
|
def __init__(self,
|
|
|
|
name: str,
|
|
|
|
module: BaseModule,
|
|
|
|
context: str,
|
2018-12-09 11:15:04 +00:00
|
|
|
import_name: str):
|
|
|
|
self.name = name
|
|
|
|
self.module = module
|
|
|
|
self.context = context
|
|
|
|
self.import_name = import_name
|
2018-09-19 12:28:18 +00:00
|
|
|
|
2016-03-29 11:56:58 +00:00
|
|
|
class ModuleManager(object):
|
2018-10-30 14:58:48 +00:00
|
|
|
def __init__(self,
|
|
|
|
events: EventManager.EventHook,
|
|
|
|
exports: Exports.Exports,
|
|
|
|
timers: Timers.Timers,
|
|
|
|
config: Config.Config,
|
|
|
|
log: Logging.Log,
|
|
|
|
directory: str):
|
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
|
|
|
|
2018-12-09 11:22:30 +00:00
|
|
|
self.modules = {} # type: typing.Dict[str, LoadedModule]
|
2018-09-28 15:51:36 +00:00
|
|
|
|
2019-05-25 17:24:50 +00:00
|
|
|
def list_modules(self) -> typing.List[ModuleDefinition]:
|
2019-01-18 12:49:11 +00:00
|
|
|
modules = []
|
|
|
|
|
|
|
|
for file_module in glob.glob(os.path.join(self.directory, "*.py")):
|
2019-05-25 17:24:50 +00:00
|
|
|
modules.append(self.define_module(ModuleType.FILE, file_module))
|
2019-01-18 12:49:11 +00:00
|
|
|
|
|
|
|
for directory_module in glob.glob(os.path.join(
|
2019-02-05 15:53:11 +00:00
|
|
|
self.directory, "*", "__init__.py")):
|
2019-05-25 17:24:50 +00:00
|
|
|
modules.append(self.define_module(ModuleType.DIRECTORY,
|
|
|
|
directory_module))
|
|
|
|
return sorted(modules, key=lambda module: module.name)
|
|
|
|
|
|
|
|
def define_module(self, type: ModuleType, filename: str
|
|
|
|
) -> ModuleDefinition:
|
|
|
|
if type == ModuleType.DIRECTORY:
|
|
|
|
name = os.path.dirname(filename)
|
|
|
|
else:
|
|
|
|
name = filename
|
|
|
|
name = self._module_name(name)
|
|
|
|
|
|
|
|
return ModuleDefinition(name, filename, type,
|
|
|
|
utils.parse.hashflags(filename))
|
|
|
|
|
|
|
|
def find_module(self, name: str) -> ModuleDefinition:
|
|
|
|
type = ModuleType.FILE
|
|
|
|
path = self._module_path(name)
|
|
|
|
|
|
|
|
if os.path.isdir(path):
|
|
|
|
type = ModuleType.DIRECTORY
|
|
|
|
path = os.path.join(path, "__init__.py")
|
|
|
|
|
|
|
|
return self.define_module(type, path)
|
2016-04-04 11:36:59 +00:00
|
|
|
|
2018-10-30 14:58:48 +00:00
|
|
|
def _module_name(self, path: str) -> str:
|
2018-09-01 10:29:26 +00:00
|
|
|
return os.path.basename(path).rsplit(".py", 1)[0].lower()
|
2018-10-30 14:58:48 +00:00
|
|
|
def _module_path(self, name: str) -> str:
|
2019-01-18 12:49:11 +00:00
|
|
|
return os.path.join(self.directory, name)
|
2018-10-30 14:58:48 +00:00
|
|
|
def _import_name(self, name: str) -> str:
|
2018-09-24 15:20:58 +00:00
|
|
|
return "bitbot_%s" % name
|
2016-04-04 11:36:59 +00:00
|
|
|
|
2019-05-10 15:11:22 +00:00
|
|
|
def from_context(self, context: str) -> typing.Optional[LoadedModule]:
|
|
|
|
for module in self.modules.values():
|
|
|
|
if module.context == context:
|
2019-05-25 17:24:50 +00:00
|
|
|
return module
|
2019-05-10 15:11:22 +00:00
|
|
|
return None
|
|
|
|
def from_name(self, name: str) -> typing.Optional[LoadedModule]:
|
|
|
|
name_lower = name.lower()
|
|
|
|
for module in self.modules.values():
|
|
|
|
if module.name.lower() == name_lower:
|
|
|
|
return module
|
|
|
|
return None
|
|
|
|
|
2018-10-30 14:58:48 +00:00
|
|
|
def _get_magic(self, obj: typing.Any, magic: str, default: typing.Any
|
|
|
|
) -> typing.Any:
|
2018-09-27 10:45:23 +00:00
|
|
|
return getattr(obj, magic) if hasattr(obj, magic) else default
|
|
|
|
|
2019-05-25 17:24:50 +00:00
|
|
|
def _load_module(self, bot: "IRCBot.Bot", definition: ModuleDefinition
|
|
|
|
) -> LoadedModule:
|
|
|
|
dependencies = definition.get_dependencies()
|
|
|
|
for dependency in dependencies:
|
|
|
|
if not dependency in self.modules:
|
|
|
|
raise ModuleDependencyNotFulfilled(dependency)
|
2017-07-12 09:00:27 +00:00
|
|
|
|
2019-05-25 17:24:50 +00:00
|
|
|
for hashflag, value in definition.hashflags:
|
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")
|
|
|
|
|
2019-05-25 17:24:50 +00:00
|
|
|
import_name = self._import_name(definition.name)
|
|
|
|
import_spec = importlib.util.spec_from_file_location(import_name,
|
|
|
|
definition.filename)
|
2019-02-05 13:34:13 +00:00
|
|
|
module = importlib.util.module_from_spec(import_spec)
|
2019-02-05 15:53:11 +00:00
|
|
|
sys.modules[import_name] = module
|
2019-02-06 22:41:37 +00:00
|
|
|
loader = typing.cast(importlib.abc.Loader, import_spec.loader)
|
|
|
|
loader.exec_module(module)
|
2018-08-31 11:55:52 +00:00
|
|
|
|
2018-12-02 10:14:18 +00:00
|
|
|
module_object_pointer = getattr(module, "Module", None)
|
|
|
|
if not module_object_pointer:
|
2018-09-24 12:10:39 +00:00
|
|
|
raise ModuleLoadException("module '%s' doesn't have a "
|
2019-05-25 17:24:50 +00:00
|
|
|
"'Module' class." % definition.name)
|
2018-12-02 10:14:18 +00:00
|
|
|
if not inspect.isclass(module_object_pointer):
|
2018-09-24 12:10:39 +00:00
|
|
|
raise ModuleLoadException("module '%s' has a 'Module' attribute "
|
2019-05-25 17:24:50 +00:00
|
|
|
"but it is not a class." % definition.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)
|
2018-12-02 10:14:18 +00:00
|
|
|
module_object = module_object_pointer(bot, context_events,
|
|
|
|
context_exports, context_timers, self.log)
|
2018-09-19 11:35:34 +00:00
|
|
|
|
2016-03-29 11:56:58 +00:00
|
|
|
if not hasattr(module_object, "_name"):
|
2019-05-25 17:24:50 +00:00
|
|
|
module_object._name = definition.name.title()
|
2018-09-19 11:35:34 +00:00
|
|
|
for attribute_name in dir(module_object):
|
|
|
|
attribute = getattr(module_object, attribute_name)
|
2018-10-30 14:58:48 +00:00
|
|
|
for hook in self._get_magic(attribute,
|
|
|
|
utils.consts.BITBOT_HOOKS_MAGIC, []):
|
2018-09-27 10:45:23 +00:00
|
|
|
context_events.on(hook["event"]).hook(attribute,
|
2018-10-03 12:22:37 +00:00
|
|
|
**hook["kwargs"])
|
2018-10-30 14:58:48 +00:00
|
|
|
for export in self._get_magic(module_object,
|
|
|
|
utils.consts.BITBOT_EXPORTS_MAGIC, []):
|
2018-09-27 10:45:23 +00:00
|
|
|
context_exports.add(export["setting"], export["value"])
|
2018-09-19 11:35:34 +00:00
|
|
|
|
2019-05-25 17:24:50 +00:00
|
|
|
if definition.name in self.modules:
|
2018-09-26 10:19:48 +00:00
|
|
|
raise ModuleNameCollisionException("Module name '%s' "
|
2019-05-25 17:24:50 +00:00
|
|
|
"attempted to be used twice" % definition.name)
|
2018-12-09 11:15:04 +00:00
|
|
|
|
2019-05-25 17:24:50 +00:00
|
|
|
return LoadedModule(definition.name, module_object, context,
|
|
|
|
import_name)
|
2016-04-04 11:36:59 +00:00
|
|
|
|
2019-05-25 17:24:50 +00:00
|
|
|
def load_module(self, bot: "IRCBot.Bot", definition: ModuleDefinition
|
|
|
|
) -> LoadedModule:
|
2017-09-05 09:03:38 +00:00
|
|
|
try:
|
2019-05-25 17:24:50 +00:00
|
|
|
loaded_module = self._load_module(bot, definition)
|
2018-09-24 12:10:39 +00:00
|
|
|
except ModuleWarning as warning:
|
2019-05-25 17:24:50 +00:00
|
|
|
self.log.warn("Module '%s' not loaded", [definition.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",
|
2019-05-25 17:24:50 +00:00
|
|
|
[definition.name, str(e)])
|
2018-09-24 12:10:39 +00:00
|
|
|
raise
|
|
|
|
|
2018-12-09 11:22:30 +00:00
|
|
|
self.modules[loaded_module.name] = loaded_module
|
2018-12-09 11:15:04 +00:00
|
|
|
self.log.debug("Module '%s' loaded", [loaded_module.name])
|
2019-02-24 10:43:46 +00:00
|
|
|
return loaded_module
|
2017-09-05 09:03:38 +00:00
|
|
|
|
2018-10-30 14:58:48 +00:00
|
|
|
def load_modules(self, bot: "IRCBot.Bot", whitelist: typing.List[str]=[],
|
2019-05-12 13:10:59 +00:00
|
|
|
blacklist: typing.List[str]=[], safe: bool=False
|
|
|
|
) -> typing.Tuple[typing.List[str], typing.List[str]]:
|
|
|
|
fail = []
|
|
|
|
success = []
|
2019-05-25 17:24:50 +00:00
|
|
|
|
|
|
|
module_definitions = self.list_modules()
|
|
|
|
|
|
|
|
#TODO figure out dependency tree
|
|
|
|
|
|
|
|
for definition in module_definitions:
|
|
|
|
if definition.name in whitelist or (
|
|
|
|
not whitelist and not definition.name in blacklist):
|
2018-09-24 12:10:39 +00:00
|
|
|
try:
|
2019-05-25 17:24:50 +00:00
|
|
|
self.load_module(bot, definition)
|
2018-09-24 12:10:39 +00:00
|
|
|
except ModuleWarning:
|
2019-05-25 17:24:50 +00:00
|
|
|
fail.append(definition.name)
|
2019-05-12 13:10:59 +00:00
|
|
|
continue
|
|
|
|
except Exception as e:
|
|
|
|
if safe:
|
2019-05-25 17:24:50 +00:00
|
|
|
fail.append(definition.name)
|
2019-05-12 13:10:59 +00:00
|
|
|
continue
|
|
|
|
else:
|
|
|
|
raise
|
2019-05-25 17:24:50 +00:00
|
|
|
success.append(definition.name)
|
2019-05-12 13:10:59 +00:00
|
|
|
return success, fail
|
2018-09-01 10:29:26 +00:00
|
|
|
|
2018-10-30 14:58:48 +00:00
|
|
|
def unload_module(self, name: str):
|
2018-09-24 12:10:39 +00:00
|
|
|
if not name in self.modules:
|
|
|
|
raise ModuleNotFoundException()
|
2018-12-09 11:15:04 +00:00
|
|
|
loaded_module = self.modules[name]
|
2018-12-09 11:20:55 +00:00
|
|
|
if hasattr(loaded_module.module, "unload"):
|
2018-10-04 13:45:32 +00:00
|
|
|
try:
|
2018-12-09 11:15:04 +00:00
|
|
|
loaded_module.module.unload()
|
2018-10-04 13:45:32 +00:00
|
|
|
except:
|
|
|
|
pass
|
2018-12-09 11:15:04 +00:00
|
|
|
del self.modules[loaded_module.name]
|
2018-09-01 10:29:26 +00:00
|
|
|
|
2018-12-09 11:15:04 +00:00
|
|
|
context = loaded_module.context
|
2018-09-02 18:54:45 +00:00
|
|
|
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-12-09 11:15:04 +00:00
|
|
|
module = loaded_module.module
|
|
|
|
del loaded_module.module
|
2019-02-22 17:58:53 +00:00
|
|
|
|
2018-12-09 11:15:04 +00:00
|
|
|
del sys.modules[loaded_module.import_name]
|
2019-02-22 17:58:53 +00:00
|
|
|
namespace = "%s." % loaded_module.import_name
|
|
|
|
for import_name in list(sys.modules.keys()):
|
|
|
|
if import_name.startswith(namespace):
|
|
|
|
del sys.modules[import_name]
|
|
|
|
|
2018-12-09 11:24:05 +00:00
|
|
|
references = sys.getrefcount(module)
|
|
|
|
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-11-13 16:02:26 +00:00
|
|
|
self.log.debug("Module '%s' unloaded (%d reference%s)",
|
2018-12-09 11:15:04 +00:00
|
|
|
[loaded_module.name, references,
|
|
|
|
"" if references == 1 else "s"])
|
2018-09-30 19:12:28 +00:00
|
|
|
if references > 0:
|
2018-11-13 16:02:26 +00:00
|
|
|
self.log.debug("References left for '%s': %s",
|
2018-12-09 11:15:04 +00:00
|
|
|
[loaded_module.name,
|
|
|
|
", ".join([str(referrer) for referrer in referrers])])
|