diff options
Diffstat (limited to 'lib/modules.sh')
-rw-r--r-- | lib/modules.sh | 447 |
1 files changed, 447 insertions, 0 deletions
diff --git a/lib/modules.sh b/lib/modules.sh new file mode 100644 index 0000000..10428e6 --- /dev/null +++ b/lib/modules.sh @@ -0,0 +1,447 @@ +#!/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 +} |