# Copyright 1999-2013 Gentoo Foundation # Distributed under the terms of the GNU General Public License v2 # $Header: $ # @ECLASS: ehooker.eclass # @MAINTAINER: # Greg Turner # @BLURB: Simple hooking framework consumable by other ebuilds/eclasses # @DESCRIPTION: # ehooker.eclass Provides a very simple api for exposing and consuming hookable # events in ebuilds and eclasses. This can be a handy way for eclasses to # effect polymorphic behavior or to offer callbacks to ebuilds. # # Hooks are identified by arbitrary names. The names should have no # spaces in them and should not contain characters likely to cause # interactions with shell globbing. There's no need to push your luck :) # # Hook names share a global namespace, so it's a good practice to prefix groups # hooks with something consistent and memorable. The name of an eclass, for # example, seems like a sensible prefix. # # No action is required to expose a hook to consumers; anyone can register # to listen to any hook by name, and any hook may be fired, without any # declarative prerequisite. This unfortunately makes it pretty easy to # miss your callbacks due to typos. Fixing that is kinda hard without # placing limitations on the framework, and probably not worth it. Instead, # it is easy to debug hook processing by setting ECLASS_DEBUG_OUTPUT=on. # # Hook listener's effects on the bash environment can be very aggressively # debugged using the EHOOKER_DEBUG_HOOKS variable -- just remember to turn # this off once you're done debugging, as it's non-portable (to non-portage # package managers) and damned expensive. # # To consume a hook, all that's necessary is to call the ehook function. # Hook consumers are tracked in a global bash variable (exactly which depends # on the version of bash used -- it is the associative array _EHOOKER_ADATA for # bash v4 and up, and the indexed array _EHOOKER_IDATA for earlier bash versions). # # Hooks should probably be avoided in error-handling code-paths, as this # could result in situations where you never get around to failing due to bugs # in other people's code. # # Must-execute framework-level cleanup code-paths also probably should not be # in hook-listeners. It's simply too easy for someone to install hooks on top # of yours and cause some kind of problem where you never get your hook notification. # # The ehooks framework enforces no particular policy with respect # to the flow of information between the invoking function and the # hook-listeners. Only the name of the hook is provided as an argument to # the hook-listeners, and ehook_fire returns zero if and only if all invoked # listeners returned zero. Aside from that, you're on your own. # # However, in non-parallel modes, the global and local bash lexical environment, # including exported environment variables, is preserved, and no sub-shells are # employed in the hook-listener invocation codepath. Therefore, bash variables # and environment variables may be used as a means of coordinating the flow of # information both into and out of hook-listener functions from hook-firing functions, # according to any design pattern you can dream up. # # Here is an example illustrating how the author mostly tends to use the framework: # # @EXAMPLE: # ebuild hook consumer code: # # @CODE # src_configure() { # # sign up for hook # ehook whizbang-pre_src_compile my_perabi_pre_config # # # call potentially hook-firing code # whizbang-multi-abi_src_configure # } # # my_perabi_pre_config() { # export LIBX11_PATH="/usr/$(get_libdir)" # } # @CODE # # @EXAMPLE: # eclass hook publisher code: # # @CODE # whizbang-multi-abi_src_configure() { # multilib_foreach_abi _whizbang_perabi_econf "$@" # } # # _whizbang_perabi_econf() { # declare -a econf_args=("$@") # ehook_fire whizbang-pre_src_compile && # econf "${econf_args[@]}"a # ehook_fire whizbang-post_src_compile --uninterruptible # } # @CODE # @ECLASS-VARIABLE: EHOOKER_DEBUG_HOOKS # @DEFAULT_UNSET # @DESCRIPTION: # If set to an array of hook names, detailed diagnostic # informational messages are displayed during listener invocation # describing changes to the envoironment effected by the # hook listener. # # Does not work for parallel hooks. This will not work for # non-portage package managers and contains nonportable, non-future-proof # code. For diagnostic purposes only. if [[ ${___ECLASS_ONCE_EHOOKER} != "recur -_+^+_- spank" ]] ; then ___ECLASS_ONCE_EHOOKER="recur -_+^+_- spank" inherit multiprocessing declare -g -a _EHOOKER_HOOKS_FIRING=( ) declare -g -i _EHOOKER_BVMAJOR=${BASH_VERSION%%.*} # do we have bash 4.x or newer (and hence the ability to # use associative arrays)? _EHOOKER_BASH4() { if (( _EHOOKER_BVMAJOR >= 4 )) ; then return 0 else return 1 fi } # This is the registration datastore. Just to be sure we don't get # mixed up somehow, use different names for the bash4/+ associative # and bash<4 indexed versions. The associative version is expected # to perform considerably better in the face of massive numbers of # registrations and listeners (although it's far from clear that # scenario will ever occur, and it's not a ton of work anyhow). if _EHOOKER_BASH4 ; then declare -g -A _EHOOKER_ADATA=( ) else declare -g -a _EHOOKER_IDATA=( ) fi # some internal functions to manipulate the ehook registration data # -> _EHOOKER_GET_HOOK_LISTENERS_RESULT="" _EHOOKER_GET_HOOK_LISTENERS() { declare -g _EHOOKER_GET_HOOK_LISTENERS_RESULT="" local item if _EHOOKER_BASH4 ; then _EHOOKER_GET_HOOK_LISTENERS_RESULT="${_EHOOKER_ADATA["${1}"]}" return 0 else for item in "${_EHOOKER_IDATA[@]}" ; do if [[ ${item%% *} == ${1} ]] ; then _EHOOKER_GET_HOOK_LISTENERS_RESULT="${item#* }" return 0 fi done fi return 0 } # [] (no data returned) _EHOOKER_SET_HOOK_LISTENERS() { local hook="$1" local listeners="${*:2}" if _EHOOKER_BASH4 ; then [[ -n ${listeners} ]] || { unset -v _EHOOKER_ADATA["${hook}"] ; return 0 ; } _EHOOKER_ADATA["${hook}"]="${listeners}" else declare -i idx for idx in "${!_EHOOKER_IDATA[@]}" ; do [[ ${_EHOOKER_IDATA[${idx}]%% *} == ${hook} ]] || continue # we found an entry. if the caller requested an empty list then # we ca simply delete it. Otherwise, replace the old list with # the new one [[ -n ${listeners} ]] || { unset -v "_EHOOKER_IDATA[${idx}]" ; return 0 ; } _EHOOKER_IDATA[${idx}]="${hook} ${listeners}" return 0 done # no entry exists yet, so we only need to make one if the caller # requested a non-empty list of listener functions [[ -n ${listeners} ]] && _EHOOKER_IDATA+=("${hook} ${listeners}") fi return 0 } # @FUNCTION: ehook_is_firing # @USAGE: # @DESCRIPTION: # There is no provision by ehooker.eclass for "hook recursion." # # In liberally configured userspace environments, infinite recursion # could sometimes lead to system-wide resource-starvation-induced # paralysis, so ehooker.eclass implements a simple and effective # countermeasure: recursion is "impossible" within a given bash process # environment. No hook may fire while ehook_fire is already servicing # that same hook's listeners somewhere lower down in the function call # chain. # # However, in multi-process environments, no guarantee exists that the same # hook will not fire simultaneously in different processes. Furthermore, # the mechanisms prohibiting recursion are not designed to be resilient # against deliberate subversion -- only to prevent mistakes by well-meaning users. # # The policy is enforced by calling die any time a recursive firing request # is made to ehook_fire(). In order to avoid such death dynamically, ehook_is_firing # may be used to test whether the immediate execution context is already servicing # a hook firing request for the hook in question. # # ehook_is_firing returns a nonzero- ("true") result if the hook indicated # is currently being processed (fired) by ehook_fire and/or hook-listener # functions, and a zero- ("false") result when the indicated hook is # currently idle. ehook_is_firing() { local hot_hook for hot_hook in "${_EHOOKER_HOOKS_FIRING[@]}" ; do [[ ${hot_hook} == $1 ]] && return 0 done return 1 } # @FUNCTION: ehook_fire # @USAGE: [-u | --uninterruptible] | [-p | --parallel] # @DESCRIPTION: # Causes hook-listener functions that have registered for the # named hook events to be invoked, either one-by-one, or when the # --parallel option is used, in parallel. # # When not parallelizing, hooks are fired in reverse-order of # registration. No such guarantee can exist in the parallelized case, # for reasons that should be obvious. # # By default, the first hook consumer that returns a nonzero value from # its hook function will immediately interrupt processing and cause # a nonzero value to be returned from ehook_fire. # # If the --uninterruptible option is provided, then all registered hook # consumers will be invoked regardless of their return values, and # ehook_fire will return the logical AND of all their return values. # # Regardless, ehook_fire returns zero if and only if all listeners # invoked return zero (or if there are no listeners). # If any hook returns a nonzero value, the function returns 1. # # The --parallel option implies the --uninterruptible option. # # If an attempt is made to fire a hook while that hook is already firing, # ehook_fire will die. # # Years of bitter experince have led most experienced ebuild and eclass # developers to the counter-intuitive conclusion that ameliorative # error handling is undesirable in ebuild code, as this leads to # non-reproducibility. # # That stated, if you don't want your ebuild to die due to recursive # hook invocation, you can check ehook_is_firing before invocation # and take some other action if the hook event is in-progress. ehook_fire() { debug-print-function ${FUNCNAME} "$@" [[ "$#" -eq 0 ]] && die "ehook_fire invoked with no arguments" # N.B....! # # Although a given hook may not recursively fire itself, # recursion through this /code path/ is completely # legitimate and supported, as hook-listener functions # responding to hook "a" are free to fire hook "b." # # Therefore we must: # # o meticulously ensure all scratch-variables # are declared as local variables.... but really, # better yet, don't use scratch-variables at all -- # use the positional parameters and relegate # any scratchwork to sub-functions which can't # muck up the lexical environment we bequeath to # the listener functions. # # o architect any interactions with global variables # in a manner resilient to recursive invocation # # Note: in bash, "local" variables bleed right through function # invocations as if they were global -- basically almost nothing # distinguishes local from global variables until they go out # of scope. This is why we go to such pains to avoid creating # local variables below: such variables would flow into the # hook-listener functions we invoked, scribbling over any like-named variable # the hook-firing function intended to provide to listeners. Although # we could make this a non-issue by using high-bandwith variable names, # this would only clutter up the environment even more and we'd still # have to worry about recursion-safety at every turn. [[ -n "$1" ]] || die "empty hook name not allowed" _EHOOKER_GET_HOOK_LISTENERS "$1" # no point screwing around any further if nobody's listening [[ -n "${_EHOOKER_GET_HOOK_LISTENERS_RESULT}" ]] || { debug-print "${FUNCNAME}: nobody listening for \"$1\"" return 0 } ehook_is_firing "$1" && \ die "Recursive invocation of hook \"$1\"" _EHOOKER_HOOKS_FIRING=( "$1" "${_EHOOKER_HOOKS_FIRING[@]}" ) # boil down the other arguments to something more managable _ehook_fire_normalize_args() { local result="d" arg for arg in "${@}" ; do case "${arg}" in -u|--uninterruptible) case "${result}" in p|u) continue ;; # already uninterruptible d) result="u" ;; *) result="e:wtf???" ;; esac ;; -p|--parallel) case "${result}" in p|d|u) result="p" ;; *) result="e:wtf???" ;; esac ;; *) result="e:${arg}" ; break ;; esac done echo "${result}" return 0 } # new positional parameters: # $1 - hook name # $2 - one of: # o "d" - default: interruptible invocation # o "u" - uninterruptible: nonparallel, ordered, uninterruptible invocation # o "p" - parallel: parallel, and implicitly uninterruptible, invocation # o "e:" - some bad argument was provided, here it is # $3 - one or more listener functions we want to invoke # # These are the arguments we pass to _ehooker_fire_away, which does all the # heavy lifting. # # NB: it's critically important that we stop using _EHOOKER_GET_HOOK_LISTENERS_RESULT # before any recursion occurs, as it's global and likely to be changed by listener code set -- "$1" "$(_ehook_fire_normalize_args "${@:2}" )" ${_EHOOKER_GET_HOOK_LISTENERS_RESULT} [[ $2 == e* ]] && \ die "ehook_fire called with illegal argument \"${2#e:}\"" debug-print "${FUNCNAME}: firing notifications for \"$1\" event to listeners: ${_EHOOKER_GET_HOOK_LISTENERS_RESULT}" # can't hurt just to tidy this up... unset _EHOOKER_GET_HOOK_LISTENERS_RESULT # ready. set.... _ehooker_fire_away "$@" # the hook processing is complete so now it's fine to use some variables. local _EHOOKER_HOOK_FIRE_RESULT=$? # filter $1 back out of the _EHOOKER_HOOKS_FIRING array # pretty sure it will always be at the beginning and we don't # need to go to the trouble, but if the semantics changed later # this should be more robust... plus I'm really not sure. _EHOOKER_HOOKS_FIRING=( $( for hot_hook in "${_EHOOKER_HOOKS_FIRING[@]}" ; do [[ ${hot_hook} != $1 ]] && echo "${hot_hook}" done ) ) debug-print "${FUNCNAME}: notifications for hook \"$1\" complete (result-code: ${_EHOOKER_HOOK_FIRE_RESULT})" return ${_EHOOKER_HOOK_FIRE_RESULT} } # -> true(0) - debug | false(1) - dont debug _ehooker_hook_is_debuggee() { [[ -z ${EHOOKER_DEBUG_HOOKS+1} ]] && return 1 local ahook if [[ $(declare -p EHOOKER_DEBUG_HOOKS) == 'declare -'[aA]* ]] ; then for ahook in ${EHOOKER_DEBUG_HOOKS[@]} ; do [[ ${ahook} == $1 ]] && return 0 done else ewarn "EHOOKER_DEBUG_HOOKS is not an array. Treating as string." for ahook in ${EHOOKER_DEBUG_HOOKS} ; do [[ ${ahook} == $1 ]] && return 0 done fi return 1 } # args: [] # invoke the listener and handle debug mode processing if indicated. # The fourth parameter is "fake" -- that is, one cant really pass it in; # it is conditionally set in code, if we decide to activate debugging. _ehooker_invoke_listener() { debug-print-function ${FUNCNAME} "$@" if _ehooker_hook_is_debuggee $1 ; then if [[ $2 == p ]] ; then ewarn "Unable to debug ${1} -> ${3} hook invocation (PID=$$): parallelized" elif ! type md5sum &> /dev/null ; then ewarn "Unable to debug ${1} -> ${3} hook invocation (PID=$$): missing md5sum executable" else set -- "$@" "${T}/ehook_debug_env_$(mktemp XXXX)_$$" fi fi # the number of parameters now tells us whether we are debugged or not if [[ $# -eq 4 ]] ; then ( source "${PORTAGE_BIN_PATH}"/save-ebuild-env.sh && __save_ebuild_env | \ grep -v '^declare\s-a\sBASH_LINENO' > "${4}.old" ) fi if ! declare -F "$3" >/dev/null 2>&1 ; then die "Listener \"$3\" for ehook \"$1\" does not appear to exist." fi # and... invoke! $3 "$1" # OK, invocation is complete. No lexical scope pollution threat remains, but we # still don't want to make local variables, as they would clutter up debugging output. # Yet we need at least to check for debug mode before returning. So we need to # store $? somehow. The best solution is unchanged -- positional parameter abuse. # # The new positional parameters will be: # [] set -- "$?" "$@" # after the above we must check for five parameters to test for debug mode :) if [[ $# -eq 5 ]] ; then # yes, this is completely evil and wrong. consider whether or not this # is of sufficient utility to leave in... ( source "${PORTAGE_BIN_PATH}"/save-ebuild-env.sh && __save_ebuild_env | \ grep -v '^declare\s-a\sBASH_LINENO' > "${5}.new" ) md5sum "${5}.old" | sed 's:\.old$:.new:' | { if md5sum --quiet --status -c ; then einfo einfo "environment not modified during execution of function" einfo "${4} while processing ehook: ${2}" einfo else einfo einfo "environment modified during execution of function" einfo "${4} while processing ehook: ${2}" einfo einfo "$(diff -du "${5}.old" "${5}.new")" einfo fi } case ${EAPI:-0} in 4|5) nonfatal rm -f "${T}"/${5}.{old,new} ;; *) rm -f "${T}"/${5}.{old,new} ;; esac fi return $1 } # ehook_fire helper function: # args: _ehooker_fire_away() { [[ $2 == p ]] && multijob_init while [[ $# -gt 2 ]] ; do case $2 in d) _ehooker_invoke_listener "${@:1:3}" || return 1 ;; u) if ! _ehooker_invoke_listener "${@:1:3}" ; then # kind of a hack -- we know we want to return # 1 no matter what, now, so just recurse and # then return it. Argument #3 should disappear, # same as if we were iterating. _ehooker_fire_away "$1" u "${@:4}" return 1 fi ;; p) multijob_child_init _ehooker_invoke_listener "${@:1:3}" ;; esac # make argument #3 disappear set -- "$1" "$2" "${@:4}" done if [[ $2 == p ]] ; then # multijob_finish will return with the correct result-code multijob_finish else # we get here via "d" and "u" mode when nobody failed, so... return 0 fi } # @FUNCTION: ehook # @USAGE: # @DESCRIPTION: # Registers a new listener for the named hook. New regsitrations # go to the top of the list of listeners so if the hook is fired # unparallelized, the most recently registered listener gets the # chance to abort processing first. # # The provided hook-listener function need not exist until the hook # is invoked. However, at that time, if it isn't defined, # ehook_fire dies. # # Hook listener functions may be registered to more than one hook. # This could be useful, i.e., for logging. # # Hook listener functions recieve a single argument, containing the # name of the hook that triggered their execution. # # If your hook listener function returns a nonzero value, this will # be communicated to the hook-firing function, and, at the option of # the hook-firing function, may also prevent additional hook-listening # functions from recieving notification. # # Note that manipulation of hook registration using the ehook and # eunhook functions is a fatal error if the hook is already in the process # of being fired. If this is unacceptable then check ehook_is_firing before # invoking these functions. ehook() { [[ "$#" -ne 2 ]] && die "ehook called with invalid number of arguments." local hookname="$1" local hookfunc="$2" [[ -n "${hookname}" ]] || die "ehook called with empty hook name" [[ -n "${hookfunc}" ]] || die "ehook called with empty listener argument" # [[ -n "$( declare -F "${hookfunc}" )" ]] || \ # die "ehook called with non-function listener argument \"${hookfunc}\"" case "${hookname}${hookfunc}" in *\ *) die "ehook called with illegal space-containing argument" ;; esac ehook_is_firing "${hookname}" && \ die "ehook \"${hookname}\" already firing during ehook invocation." _EHOOKER_GET_HOOK_LISTENERS "${hookname}" local new_listeners=$( echo ${hookfunc} $( for listener in $_EHOOKER_GET_HOOK_LISTENERS_RESULT ; do [[ ${listener} != ${hookfunc} ]] && echo "${listener}" done ) ) # figure out if the request is a noop. If so, do nothing. Otherwise # enforce the rule about hook-listener registration manipulation during # hook firing. if [[ ${new_listeners} != ${_EHOOKER_GET_HOOK_LISTENERS_RESULT} ]] ; then ehook_is_firing "${hookname}" && \ die "ehook \"${hookname}\" already firing during ehook invocation." _EHOOKER_SET_HOOK_LISTENERS "${hookname}" ${new_listeners} fi return 0 } # @FUNCTION: eunhook # @USAGE: # @DESCRIPTION: # reverses any active registration of to # A noop if no such registration exists. # # Note that manipulation of hook registration using the ehook and # eunhook functions is a fatal error if the hook is already in the process # of being fired. If this is unacceptable then check ehook_is_firing before # invoking these functions. eunhook() { [[ "$#" -ne 2 ]] && die "ehook called with invalid number of arguments." local hookname="$1" local hookfunc="$2" [[ -n ${hookname} ]] || die "ehook called with empty hook name" [[ -n ${hookfunc} ]] || die "ehook called with empty listener argument" case "${hookname}${hookfunc}" in *\ *) die "ehook called with illegal space-containing argument" ;; esac _EHOOKER_GET_HOOK_LISTENERS "${hookname}" local new_listeners=$( echo $( for listener in $_EHOOKER_GET_HOOK_LISTENERS_RESULT ; do [[ ${listener} != ${hookfunc} ]] && echo "${listener}" done ) ) # figure out if the request is a noop. If so, do nothing. Otherwise # enforce the rule about hook-listener registration manipulation during # hook firing. if [[ ${new_listeners} != ${_EHOOKER_GET_HOOK_LISTENERS_RESULT} ]] ; then ehook_is_firing "${hookname}" && \ die "ehook \"${hookname}\" already firing during eunhook invocation." _EHOOKER_SET_HOOK_LISTENERS "${hookname}" ${new_listeners} fi return 0 } fi # ___ECLASS_ONCE_EHOOKER