hyperbot/lib/modules.sh
2017-06-02 15:44:54 -03:00

448 lines
16 KiB
Bash

#!/bin/bash
# -*- coding: utf-8 -*-
###########################################################################
# #
# envbot - an IRC bot in bash #
# Copyright (C) 2007-2008 Arvid Norlander #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
# #
###########################################################################
#---------------------------------------------------------------------
## Modules management
#---------------------------------------------------------------------
#---------------------------------------------------------------------
## List of loaded modules. Don't change from other code.
## @Type Semi-private
#---------------------------------------------------------------------
modules_loaded=""
#---------------------------------------------------------------------
## Current module API version.
#---------------------------------------------------------------------
declare -r modules_current_API=2
#---------------------------------------------------------------------
## Call from after_load with a list of modules that you depend on
## @Type API
## @param What module you are calling from.
## @param Space separated list of modules you depend on
## @return 0 Success
## @return 1 Other error. You should return 1 from after_load.
## @return 2 One or several of the dependencies could found. You should return 1 from after_load.
## @return 3 Not all of the dependencies could be loaded (modules exist but did not load correctly). You should return 1 from after_load.
#---------------------------------------------------------------------
modules_depends_register() {
local callermodule="$1"
local dep
for dep in $2; do
if [[ $dep == $callermodule ]]; then
log_error_file modules.log "To the module author of $callermodule: You can't list yourself as a dependency of yourself!"
log_error_file modules.log "Aborting!"
return 1
fi
if ! list_contains "modules_loaded" "$dep"; then
log_info_file modules.log "Loading dependency of $callermodule: $dep"
modules_load "$dep"
local status="$?"
if [[ $status -eq 4 ]]; then
return 2
elif [[ $status -ne 0 ]]; then
return 3
fi
fi
if list_contains "modules_depends_${dep}" "$callermodule"; then
log_warning_file modules.log "Dependency ${callermodule} already listed as depending on ${dep}!?"
fi
# Use printf not eval here.
local listname="modules_depends_${dep}"
printf -v "modules_depends_${dep}" '%s' "${!listname} $callermodule"
done
}
#---------------------------------------------------------------------
## Call from after_load or INIT with a list of modules that you
## depend on optionally.
## @Type API
## @param What module you are calling from.
## @param The module you want to depend on optionally.
## @return 0 Success, module loaded
## @return 1 User didn't list it as loaded, don't use the features in question
## @return 2 Other error. You should return 1 from after_load.
## @return 3 One or several of the dependencies could found. You should return 1 from after_load.
## @return 4 Not all of the dependencies could be loaded (modules exist but did not load correctly). You should return 1 from after_load.
#---------------------------------------------------------------------
modules_depends_register_optional() {
local callermodule="$1"
local dep="$2"
if ! list_contains "modules_loaded" "$dep"; then
# So not loaded, now we need to find out if we should load it or not
# We use $config_modules for it
if ! list_contains 'config_modules' "$dep"; then
log_info_file modules.log "Optional dependency of $callermodule ($dep) not loaded."
return 1
fi
log_info_file modules.log "Loading optional dependency of $callermodule: ($dep)"
fi
# Ah we should load it then? Call modules_depends_register
modules_depends_register "$@"
}
#---------------------------------------------------------------------
## Semi internal!
## List modules that depend on another module.
## @Type Semi-private
## @param Module to check
## @Stdout List of modules that depend on this.
#---------------------------------------------------------------------
modules_depends_list_deps() {
# This is needed to be able to use indirect refs
local deplistname="modules_depends_${1}"
# Clean out spaces, fastest way
echo ${!deplistname}
}
###########################################################################
# Internal functions to core or this file below this line! #
# Module authors: go away #
# See doc/module_api.txt instead #
###########################################################################
#---------------------------------------------------------------------
## Used by unload to unregister from depends system
## (That is: remove from list of "depended on by" of other modules)
## @Type Private
## @param Module to unregister
#---------------------------------------------------------------------
modules_depends_unregister() {
local module newval
for module in $modules_loaded; do
if list_contains "modules_depends_${module}" "$1"; then
list_remove "modules_depends_${module}" "$1" "modules_depends_${module}"
fi
done
}
#---------------------------------------------------------------------
## Check if a module can be unloaded
## @Type Private
## @param Name of module to check
## @return Can be unloaded
## @return Is needed by some other module.
#---------------------------------------------------------------------
modules_depends_can_unload() {
# This is needed to be able to use indirect refs
local deplistname="modules_depends_${1}"
# Not empty/only whitespaces?
if ! [[ ${!deplistname} =~ ^\ *$ ]]; then
return 1
fi
return 0
}
#---------------------------------------------------------------------
## Add hooks for a module
## @Type Private
## @param Module name
## @param MODULE_BASE_PATH, exported to INIT as a part of the API
## @return 0 Success
## @return 1 module_modulename_INIT returned non-zero
## @return 2 Module wanted to register an unknown hook.
#---------------------------------------------------------------------
modules_add_hooks() {
local module="$1"
local modinit_HOOKS
local modinit_API
local MODULE_BASE_PATH="$2"
module_${module}_INIT "$module"
[[ $? -ne 0 ]] && { log_error_file modules.log "Failed to get initialize module \"$module\""; return 1; }
# Check if it didn't set any modinit_API, in that case it is a API 1 module.
if [[ -z $modinit_API ]]; then
log_error "Please upgrade \"$module\" to new module API $modules_current_API. This old API is obsolete and no longer supported."
return 1
elif [[ $modinit_API -ne $modules_current_API ]]; then
log_error "Current module API version is $modules_current_API, but the API version of \"$module\" is $module_API."
return 1
fi
local hook
for hook in $modinit_HOOKS; do
case $hook in
"FINALISE")
modules_FINALISE+=" $module"
;;
"after_load")
modules_after_load+=" $module"
;;
"before_connect")
modules_before_connect+=" $module"
;;
"on_connect")
modules_on_connect+=" $module"
;;
"after_connect")
modules_after_connect+=" $module"
;;
"before_disconnect")
modules_before_disconnect+=" $module"
;;
"after_disconnect")
modules_after_disconnect+=" $module"
;;
"on_module_UNLOAD")
modules_on_module_UNLOAD+=" $module"
;;
"on_server_ERROR")
modules_on_server_ERROR+=" $module"
;;
"on_NOTICE")
modules_on_NOTICE+=" $module"
;;
"on_PRIVMSG")
modules_on_PRIVMSG+=" $module"
;;
"on_TOPIC")
modules_on_TOPIC+=" $module"
;;
"on_channel_MODE")
modules_on_channel_MODE+=" $module"
;;
"on_user_MODE")
modules_on_user_MODE+=" $module"
;;
"on_INVITE")
modules_on_INVITE+=" $module"
;;
"on_JOIN")
modules_on_JOIN+=" $module"
;;
"on_PART")
modules_on_PART+=" $module"
;;
"on_KICK")
modules_on_KICK+=" $module"
;;
"on_QUIT")
modules_on_QUIT+=" $module"
;;
"on_KILL")
modules_on_KILL+=" $module"
;;
"on_NICK")
modules_on_NICK+=" $module"
;;
"on_numeric")
modules_on_numeric+=" $module"
;;
"on_PONG")
modules_on_PONG+=" $module"
;;
"on_raw")
modules_on_raw+=" $module"
;;
*)
log_error_file modules.log "Unknown hook $hook requested. Module may malfunction. Module will be unloaded"
return 2
;;
esac
done
}
#---------------------------------------------------------------------
## List of all the optional hooks.
## @Type Private
#---------------------------------------------------------------------
modules_hooks="FINALISE after_load before_connect on_connect after_connect before_disconnect after_disconnect on_module_UNLOAD on_server_ERROR on_NOTICE on_PRIVMSG on_TOPIC on_channel_MODE on_user_MODE on_INVITE on_JOIN on_PART on_KICK on_QUIT on_KILL on_NICK on_numeric on_PONG on_raw"
#---------------------------------------------------------------------
## Unload a module
## @Type Private
## @param Module name
## @return 0 Unloaded
## @return 2 Module not loaded
## @return 3 Can't unload, some other module depends on this.
## @Note If the unload fails for other reasons the bot will quit.
#---------------------------------------------------------------------
modules_unload() {
local module="$1"
local hook newval to_unset
if ! list_contains "modules_loaded" "$module"; then
log_warning_file modules.log "No such module as $1 is loaded."
return 2
fi
if ! modules_depends_can_unload "$module"; then
log_error_file modules.log "Can't unload $module because these module(s) depend(s) on it: $(modules_depends_list_deps "$module")"
return 3
fi
# Remove hooks from list first in case unloading fails so we can do quit hooks if something break.
for hook in $modules_hooks; do
# List so we can unset.
if list_contains "modules_${hook}" "$module"; then
to_unset+=" module_${module}_${hook}"
fi
list_remove "modules_${hook}" "$module" "modules_${hook}"
done
commands_unregister "$module" || {
log_fatal_file modules.log "Could not unregister commands for ${module}"
bot_quit "Fatal error in module unload, please see log"
}
module_${module}_UNLOAD || {
log_fatal_file modules.log "Could not unload ${module}, module_${module}_UNLOAD returned ${?}!"
bot_quit "Fatal error in module unload, please see log"
}
unset module_${module}_UNLOAD
unset module_${module}_INIT
unset module_${module}_REHASH
# Unset from list created above.
for hook in $to_unset; do
unset "$hook" || {
log_fatal_file modules.log "Could not unset the hook $hook of module $module!"
bot_quit "Fatal error in module unload, please see log"
}
done
modules_depends_unregister "$module"
list_remove "modules_loaded" "$module" "modules_loaded"
# Call any hooks for unloading modules.
local othermodule
for othermodule in $modules_on_module_UNLOAD; do
module_${othermodule}_on_module_UNLOAD "$module"
done
# Unset help string
unset helpentry_module_${module}_description
return 0
}
#---------------------------------------------------------------------
## Generate awk script to validate module functions.
## @param Module name
## @Type Private
## @return 0 If the file is OK
## @return 1 If the file lacks one of more of the functions.
#---------------------------------------------------------------------
modules_check_function() {
local module="$1"
# This is a one liner. Well mostly. ;)
# We check that the needed functions exist.
awk "function check_found() { if (init && unload && rehash) exit 0 }
/^declare -f module_${module}_INIT$/ { init=1; check_found() }
/^declare -f module_${module}_UNLOAD$/ { unload=1; check_found() }
/^declare -f module_${module}_REHASH$/ { rehash=1; check_found() }
END { if (! (init && unload && rehash)) exit 1 }"
}
#---------------------------------------------------------------------
## Load a module
## @Type Private
## @param Name of module to load
## @return 0 Loaded Ok
## @return 1 Other errors
## @return 2 Module already loaded
## @return 3 Failed to source it in safe subshell
## @return 4 Failed to source it
## @return 5 No such module
## @return 6 Getting hooks failed
## @return 7 after_load failed
## @Note If the load fails in a fatal way the bot will quit.
#---------------------------------------------------------------------
modules_load() {
local module="$1"
if list_contains "modules_loaded" "$module"; then
log_warning_file modules.log "Module ${module} is already loaded."
return 2
fi
# modulebase is exported as MODULE_BASE_PATH
# with ${config_modules_dir} prepended to the
# INIT function, useful for multi-file
# modules, but available for other modules too.
local modulefilename modulebase
if [[ -f "${config_modules_dir}/m_${module}.sh" ]]; then
modulefilename="m_${module}.sh"
modulebase="${modulefilename}"
elif [[ -d "${config_modules_dir}/m_${module}" && -f "${config_modules_dir}/m_${module}/__main__.sh" ]]; then
modulefilename="m_${module}/__main__.sh"
modulebase="m_${module}"
else
log_error_file modules.log "No such module as ${module} exists."
return 5
fi
( source "${config_modules_dir}/${modulefilename}" )
if [[ $? -ne 0 ]]; then
log_error_file modules.log "Could not load ${module}, failed to source it in safe subshell."
return 3
fi
( source "${config_modules_dir}/${modulefilename}" && declare -F ) | modules_check_function "$module"
if [[ $? -ne 0 ]]; then
log_error_file modules.log "Could not load ${module}, it lacks some important functions it should have."
return 3
fi
source "${config_modules_dir}/${modulefilename}"
if [[ $? -eq 0 ]]; then
modules_loaded+=" $module"
modules_add_hooks "$module" "${config_modules_dir}/${modulebase}" || \
{
log_error_file modules.log "Hooks failed for $module"
# Try to unload.
modules_unload "$module" || {
log_fatal_file modules.log "Failed Unloading of $module (that failed to load)."
bot_quit "Fatal error in module unload of failed module load, please see log"
}
return 6
}
if grep -qw "$module" <<< "$modules_after_load"; then
module_${module}_after_load
if [[ $? -ne 0 ]]; then
modules_unload ${module} || {
log_fatal_file modules.log "Unloading of $module that failed after_load failed."
bot_quit "Fatal error in module unload of failed module load (after_load), please see log"
}
return 7
fi
fi
else
log_error_file modules.log "Could not load ${module}, failed to source it."
return 4
fi
}
#---------------------------------------------------------------------
## Load modules from the config
## @Type Private
#---------------------------------------------------------------------
modules_load_from_config() {
local module
IFS=" "
for module in $modules_loaded; do
if ! list_contains config_modules "$module"; then
modules_unload "$module"
fi
done
unset IFS
for module in $config_modules; do
if [[ -f "${config_modules_dir}/m_${module}.sh" || -d "${config_modules_dir}/m_${module}" ]]; then
if ! list_contains modules_loaded "$module"; then
modules_load "$module"
fi
else
log_warning_file modules.log "$module doesn't exist! Removing it from list"
fi
done
}