# -*- sh -*-
# vim:ft=sh:ts=8:sw=4:noet

AddConfigHandler ModulesOptions
AddConfigHelp "UnloadModules <module name> [...]" "Names of modules to unload prior to suspending."
AddConfigHelp "LoadModules auto|<module name> [...]" "Names of modules to load after resumimg. If auto is specified, the modules that were unloaded previously are loaded."
AddConfigHelp "UnloadBlacklistedModules <boolean>" "Try to remove any modules that are known to be incompatible with hibernation prior to suspending."
AddConfigHelp "UnloadAllModules <boolean>" "Try to remove all modules loaded prior to suspending."
AddConfigHelp "LoadModulesFromFile <filename>" "Load default modules after resuming from a given filename. Each module name must appear on it's own line, and line starting with # are ignored. (eg, Debian's /etc/modules, Gentoo's /etc/modules.autoload)"

MODULES_BLACKLIST_FILE="/etc/hibernate/blacklisted-modules"

CheckModuleSupport() {
    [ -f "/proc/modules" ] && return 0
    vecho 1 "Kernel does not have module support compiled in! Module functions disabled."
    return 1
}

# SaveKernelModprobe disables module loading while suspending, by setting
# the kernel's modprobe to /doesnt/exist, and saving the old value.
SaveKernelModprobe() {
    [ -n "$KERNEL_MODPROBE" ] && return 0
    if [ -r "/proc/sys/kernel/modprobe" ] ; then
	KERNEL_MODPROBE=`cat /proc/sys/kernel/modprobe`
	if [ -n "$KERNEL_MODPROBE" ] ; then
	    vecho 3 "Saved /proc/sys/kernel/modprobe is $KERNEL_MODPROBE"
	    echo "/doesnt/exist" > /proc/sys/kernel/modprobe
	fi
    fi
    if [ -r "/proc/sys/kernel/hotplug" ] ; then
	KERNEL_HOTPLUG=`cat /proc/sys/kernel/hotplug`
	if [ -n "$KERNEL_HOTPLUG" ] ; then
	    vecho 3 "Saved /proc/sys/kernel/hotplug is $KERNEL_HOTPLUG"
	    echo "/doesnt/exist" > /proc/sys/kernel/hotplug
	fi
    fi

    return 0
}

RestoreKernelModprobe() {
    if [ -n "$KERNEL_MODPROBE" ] ; then
	echo $KERNEL_MODPROBE > /proc/sys/kernel/modprobe
	unset KERNEL_MODPROBE
    fi
    if [ -n "$KERNEL_HOTPLUG" ] ; then
	echo $KERNEL_HOTPLUG > /proc/sys/kernel/hotplug
	unset KERNEL_HOTPLUG
    fi
    return 0
}

ModulesUnload() {
    [ -z "$MOD_UNLOAD" ] && return 0
    CheckModuleSupport || return 0
    local mod
    local ret
    ret=0
    for mod in $MOD_UNLOAD ; do
	vecho 2 -n "Unloading module $mod... "

	if ! grep -q "^$mod " /proc/modules ; then
	    vecho 2 "not loaded."
	    continue
	fi

	if rmmod `FindModuleDeps $mod` > /dev/null 2>&1 ; then
	    vecho 2
	else
	    ret=1
	    vecho 2 "failed!"
	    [ "$VERBOSITY" -eq 1 ] && echo "Unloading module $mod failed!"
	fi
	# It's possible that the module was unloaded anyway (eg, if it Ctrl+C'd)
	MOD_UNLOADED="$MOD_UNLOADED $mod"
    done
    return $ret
}

ModulesLoad() {
    [ -z "$MOD_LOAD" ] && return 0
    CheckModuleSupport || return 0
    RestoreKernelModprobe

    local mod
    for mod in $MOD_LOAD ; do
	if [ "$mod" = "auto" ] ; then
	    for mod in $MOD_UNLOADED ; do
		vecho 2 "Loading module $mod (from auto)..."
		modprobe $mod
	    done
	    continue
	fi
	vecho 2 "Loading module $mod..."
	modprobe $mod
    done
    return 0
}

ModulesUnloadAllOnce() {
    local module
    for module in `awk '($3==0){print $1}' /proc/modules` ;do
	case $module in
	    suspend_*)
		vecho 2 "Skipping suspend module $module."
		;;
	    *)
		vecho 2 "Unloading module $module..."
		rmmod $module && MOD_UNLOADED="$MOD_UNLOADED $module"
		;;
	esac
    done
}

Get26ModulesFile() {
    if grep -q '^2\.4\.' /proc/sys/kernel/osrelease ; then
	sed -n -e '/\[\(.*\)\]/ { s/^\([^ ]\+\).*\[\(.*\)\].*$/\1:0:0:\2 /g; y/ :/, /; p; d }; { s/^\([^ ]\+\).*$/\1 0 0 -/g; p }' < /proc/modules
    else
	cat /proc/modules
    fi
}

FindModuleDeps() {
    Get26ModulesFile | awk -v "module=$1" '
function find_related_modules(mod, i, a) {
    modules[mod] = deps[mod]
    split(deps[mod], a, ",")
    for (i in a) {
        if (a[i] == "") continue
        if (!modules[a[i]]) {
            modules[a[i]] = deps[a[i]]
            find_related_modules(a[i])
        }
    }
}

function top_sort(modules, i, j) {
    changed = 1
    while (changed) {
        changed = 0
        for(i in modules) {
            if (modules[i] == "") {
                print i
                delete modules[i]
                for (j in modules) {
                    sub("^"i",", "", modules[j])
                    sub(","i",", ",", modules[j])
                }
                changed = 1
            }
        }
    }
}

BEGIN {
    have_module = 0
}

{
    if ($4 != "-") deps[$1] = $4
    if ($1 == module) have_module = 1
}

END {
    if (have_module == 1) {
        find_related_modules(module) # puts results into "modules" array
        top_sort(modules)
    }
}
    '
}

IsInVersionRange() {
    local kver
    kver=$1
    shift

    local min_ver
    local max_ver
    while [ -n "$*" ] ; do
	min_ver=`echo $1 | awk 'BEGIN{FS="[^0-9]"}{print($1*0x10000)+($2*0x100)+$3}'`
	max_ver=`echo $2 | awk 'BEGIN{FS="[^0-9]"}{print($1*0x10000)+($2*0x100)+$3}'`
	shift 2
	[ "$kver" -ge "$min_ver" ] && [ "$kver" -le "$max_ver" ] && return 0
    done

    return 1
}

ModulesUnloadBlacklist() {
    [ x"$MODULES_UNLOAD_BLACKLIST" = "x1" ] || return 0
    CheckModuleSupport || return 0

    if [ ! -r "$MODULES_BLACKLIST_FILE" ] ; then
	vecho 1 "Blacklisted modules file doesn't exist or isn't readable."
	return 1
    fi


    local kver=`awk 'BEGIN{FS="[^0-9]"}{print($1*0x10000)+($2*0x100)+$3}' /proc/sys/kernel/osrelease`

    # Loop over every line in given file.
    vecho 2 "Unloading blacklisted modules listed $MODULES_BLACKLIST_FILE"
    local failedmods=""
    while : ; do
	local mod vers use_mod_ver
	use_mod_ver=
	read mod vers
	[ $? -ne 0 ] && [ -z "$mod" ] && break
	case $mod in
	    \#*|"") continue ;;
	    @*)
		mod=${mod#@}
		use_mod_ver=1
		;;
	esac
	if [ -n "$vers" ] ; then
	    if [ -z "$use_mod_ver" ] ; then
		IsInVersionRange $kver $vers || continue
	    else
		local modver
		modver=`modinfo $mod 2>/dev/null | sed -e 's/^version:[ \t]\+\(.*\)$/\1/;t;d' | awk 'BEGIN{FS="[^0-9]"}{print($1*0x10000)+($2*0x100)+$3}'`
		vecho 3 "Module version for $mod is $modver"
		if [ -n "$modver" ] ; then
		    IsInVersionRange $modver $vers || continue
		fi
	    fi
	fi
	local deps
	deps=`FindModuleDeps $mod`
	[ -z "$deps" ] && continue # not loaded
	vecho 2 "Unloading blacklisted module $mod (and dependencies)"
	local i
	for i in $deps ; do
	    vecho 3 "Unloading $i ..."
	    if rmmod $i > /dev/null 2>&1 ; then
		MOD_UNLOADED="$MOD_UNLOADED $i"
	    else
		failedmods="$failedmods $i"
	    fi
	done
    done < $MODULES_BLACKLIST_FILE

    [ -z "$failedmods" ] && return 0
    vecho 0 "Some modules failed to unload: $failedmods"
    # Blacklisted modules failing to suspend is noteworthy, and should fail.
    return 1
}

ModulesUnloadAll() {
    [ x"$MODULES_UNLOADALL" = "x1" ] || return 0
    CheckModuleSupport || return 0

    local modbefore
    local modafter
    local mod_retry_count

    # read from /proc/modules which modules are loaded
    modbefore="`awk '($3==0){print $1}' /proc/modules`"
    ModulesUnloadAllOnce
    modafter="`awk '($3==0){print $1}' /proc/modules`"

    # check which are still loaded and retry until nothing changes
    mod_retry_count=0
    while [ "$modafter" != "$modbefore" ] ; do
	modbefore="$modafter"

	# Some sleep implementations might not support decimal sleep values:
	sleep 0.2 > /dev/null 2>&1 ; [ $? -ne 0 ] && [ $? -ne 130 ] && sleep 1

	ModulesUnloadAllOnce
	modafter="`awk '($3==0){print $1}' /proc/modules`"
	[ "$mod_retry_count" -gt 15 ] && break
	[ -z "$modafter" ] && return 0
	mod_retry_count=$(($mod_retry_count+1))
    done

    vecho 0 "Some modules failed to unload: "`awk '{print $1}' /proc/modules`
    # If they're trying to unload everything, some things are bound to fail.
    # So don't complain about it.
    return 0
}

ModulesLoadFromFile() {
    [ -n "$MODULES_FROMFILE" ] || return 0
    [ -r "$MODULES_FROMFILE" ] || return 0
    CheckModuleSupport || return 0
    RestoreKernelModprobe

    local MOD
    local args
    # Loop over every line in given file.
    vecho 2 "Loading modules listed $MODULES_FROMFILE"
    while : ; do
	read MOD args
	[ $? -ne 0 ] && [ -z "$MOD" ] && break
	case "${MOD}" in
	    \#*|"") continue ;;
	esac
	vecho 2 "Loading $MOD"
	modprobe ${MOD} ${args}
    done < $MODULES_FROMFILE

    return 0
}


ModulesOptions() {
    case $1 in
	unloadmodules)
	    [ -z "$MOD_UNLOAD" ] && AddSuspendHook 90 ModulesUnload
	    shift
	    MOD_UNLOAD="$MOD_UNLOAD $@"
	    ;;
	loadmodules)
	    [ -z "$MOD_LOAD" ] && AddResumeHook 90 ModulesLoad
	    shift
	    MOD_LOAD="$MOD_LOAD $@"
	    ;;
	unloadblacklistedmodules)
	    if BoolIsOn "$1" "$2" ; then
		MODULES_UNLOAD_BLACKLIST=1
		AddSuspendHook 91 ModulesUnloadBlacklist
	    fi
	    ;;
	unloadallmodules)
	    if BoolIsOn "$1" "$2" ; then
		MODULES_UNLOADALL=1
		AddSuspendHook 91 ModulesUnloadAll
	    fi
	    ;;
	loadmodulesfromfile)
	    if [ -z "$MODULES_FROMFILE" ] ; then
		MODULES_FROMFILE="$2"
		AddResumeHook 91 ModulesLoadFromFile
	    fi
	    ;;
	*)
	    return 1
    esac
    if [ -z "$MODPROBE_HOOKED" ] ; then
	AddSuspendHook 89 SaveKernelModprobe
	# Restoration is also done by individual loaders. The following is just
	# to ensure that we do restore it at some point.
	AddResumeHook 89 RestoreKernelModprobe
	MODPROBE_HOOKED=1
	MOD_UNLOADED="" # Clear this early on
    fi
    return 0
}

# $Id: modules 734 2005-02-21 00:54:30Z dagobah $
