#!/bin/bash

# start or stop laptop_mode, best run by a power management daemon when
# ac gets connected/disconnected from a laptop
#
# install as /usr/sbin/laptop_mode
#
# Contributors to this script:   Kiko Piris
#				 Bart Samwel
#				 Micha Feigin
#				 Andrew Morton
#				 Herve Eychenne
#				 Dax Kelson
#
# Original Linux 2.4 version by: Jens Axboe

#############################################################################

if [ "$1" == "defaults" ] ; then
	# This special option is used from multiple scripts to set the config defaults.
	MAX_AGE=600
	MINIMUM_BATTERY_MINUTES=7
	LM_SECONDS_BEFORE_SYNC=2
	LAPTOP_MODE_ALWAYS_ON=0
	LM_WHEN_LID_CLOSED=0
	READAHEAD=3072
	DO_REMOUNTS=1
	DO_REMOUNT_NOATIME=1
	DIRTY_RATIO=60
	DIRTY_BACKGROUND_RATIO=1
	DEF_AGE=30
	DEF_UPDATE=5
	DEF_DIRTY_BACKGROUND_RATIO=10
	DEF_DIRTY_RATIO=40
	DEF_XFS_AGE_BUFFER=15
	DEF_XFS_SYNC_INTERVAL=30
	DEF_XFS_BUFD_INTERVAL=1
	XFS_HZ=100
		DO_CPU=0
	CPU_MAXFREQ=slowest
	AC_HD_WITH_LM=1
	AC_HD_WITHOUT_LM=244
	BATT_HD=1
	DO_HD_POWERMGMT=1
	AC_HDPARM_POWERMGMT_WITH_LM=1
	AC_HDPARM_POWERMGMT_WITHOUT_LM=255
	BATT_HDPARM_POWERMGMT=1
	HD="/dev/hda /dev/hdb /dev/hdc /dev/hdd"
	DO_HD=1
	ACPI_WITHOUT_AC_EVENTS=0
	DO_SYSLOG=0
	AC_SYSLOG_WITH_LM=/etc/syslog-on-ac-with-lm.conf
	AC_SYSLOG_WITHOUT_LM=/etc/syslog-on-ac-without-lm.conf
	BATT_SYSLOG=/etc/syslog-on-battery.conf
	SYSLOG_CONF=/etc/syslog.conf
	SYSLOG_SIGNAL_PROGRAM=syslogd
	REMOUNT_PARTITIONS=
else

# Read the defaults
. $0 defaults

# Source config.
if [ -r /etc/laptop-mode/laptop-mode.conf ] ; then
	. /etc/laptop-mode/laptop-mode.conf
else
	echo $0: Configuration file /etc/laptop-mode/laptop-mode.conf not present or not readable.
	exit 1
fi

# Support for old config settings
if [ "$AC_HD" != "" ] ; then
  AC_HD_WITHOUT_LM="$AC_HD"
  AC_HD_WITH_LM="$AC_HD"
fi

#############################################################################

KLEVEL="$(uname -r |
             {
	       IFS='.' read a b c
	       echo $a.$b
	     }
)"
case "$KLEVEL" in
	"2.4"|"2.6")
		;;
	*)
		echo "Unhandled kernel version: $KLEVEL ('uname -r' = '$(uname -r)')" >&2
		exit 1
		;;
esac

if [ ! -e /proc/sys/vm/laptop_mode ] ; then
	echo "Kernel is not patched with laptop_mode patch." >&2
	exit 1
fi

if [ ! -w /proc/sys/vm/laptop_mode ] ; then
	echo "You do not have enough privileges to enable laptop_mode." >&2
	exit 1
fi

# Remove an option (the first parameter) of the form option=<number> from
# a mount options string (the rest of the parameters).
parse_mount_opts () {
	OPT="$1"
	shift
	echo ",$*," | sed		\
	 -e 's|,'"$OPT"'=[0-9]*,|,|g'	\
	 -e 's/,,*/,/g'			\
	 -e 's/^,//'			\
	 -e 's/,$//'
}

# Remove an option (the first parameter) without any arguments from
# a mount option string (the rest of the parameters).
parse_nonumber_mount_opts () {
	OPT="$1"
	shift
	echo ",$*," | sed		\
	 -e 's|,'"$OPT"',|,|g'		\
	 -e 's/,,*/,/g'			\
	 -e 's/^,//'			\
	 -e 's/,$//'
}

# Find out the state of a yes/no option (e.g. "atime"/"noatime") in
# fstab for a given filesystem, and use this state to replace the
# value of the option in another mount options string. The device
# is the first argument, the option name the second, and the default
# value the third. The remainder is the mount options string.
#
# Example:
# parse_yesno_opts_wfstab /dev/hda1 atime atime defaults,noatime
#
# If fstab contains, say, "rw" for this filesystem, then the result
# will be "defaults,atime".
parse_yesno_opts_wfstab () {
	L_DEV="$1"
	OPT="$2"
	DEF_OPT="$3"
	shift 3
	L_OPTS="$*"
	PARSEDOPTS1="$(parse_nonumber_mount_opts $OPT $L_OPTS)"
	PARSEDOPTS1="$(parse_nonumber_mount_opts no$OPT $PARSEDOPTS1)"
	# Watch for a default atime in fstab
	FSTAB_OPTS="$(awk '$1 == "'$L_DEV'" { print $4 }' /etc/fstab)"
	if echo "$FSTAB_OPTS" | grep "$OPT" > /dev/null ; then
		# option specified in fstab: extract the value and use it
		if echo "$FSTAB_OPTS" | grep "no$OPT" > /dev/null ; then
			echo "$PARSEDOPTS1,no$OPT"
		else
			# no$OPT not found -- so we must have $OPT.
			echo "$PARSEDOPTS1,$OPT"
		fi
	else
		# option not specified in fstab -- choose the default.
		echo "$PARSEDOPTS1,$DEF_OPT"
	fi
}

# Find out the state of a numbered option (e.g. "commit=NNN") in
# fstab for a given filesystem, and use this state to replace the
# value of the option in another mount options string. The device
# is the first argument, and the option name the second. The
# remainder is the mount options string in which the replacement
# must be done.
#
# Example:
# parse_mount_opts_wfstab /dev/hda1 commit defaults,commit=7
#
# If fstab contains, say, "commit=3,rw" for this filesystem, then the
# result will be "rw,commit=3".
parse_mount_opts_wfstab () {
	L_DEV="$1"
	OPT="$2"
	shift 2
	L_OPTS="$*"
	PARSEDOPTS1="$(parse_mount_opts $OPT $L_OPTS)"
	# Watch for a default commit in fstab
	FSTAB_OPTS="$(awk '$1 == "'$L_DEV'" { print $4 }' /etc/fstab)"
	if echo "$FSTAB_OPTS" | grep "$OPT=" > /dev/null ; then
		# option specified in fstab: extract the value, and use it
		echo -n "$PARSEDOPTS1,$OPT="
		echo ",$FSTAB_OPTS," | sed \
		 -e 's/.*,'"$OPT"'=//'	\
		 -e 's/,.*//'
	else
		# option not specified in fstab: set it to 0
		echo "$PARSEDOPTS1,$OPT=0"
	fi
}

deduce_fstype () {
	MP="$1"
	# My root filesystem unfortunately has
	# type "unknown" in /etc/mtab. If we encounter
	# "unknown", we try to get the type from fstab.
	cat /etc/fstab |
	grep -v '^#' |
	while read FSTAB_DEV FSTAB_MP FSTAB_FST FSTAB_OPTS FSTAB_DUMP FSTAB_DUMP ; do
		if [ "$FSTAB_MP" = "$MP" ]; then
			echo $FSTAB_FST
			exit 0
		fi
	done
}

if [ $DO_REMOUNT_NOATIME -eq 1 ] ; then
	NOATIME_OPT=",noatime"
fi

ACTION="$1"

# Determine the power state.
ON_AC=1

if [ -d /proc/acpi/ac_adapter ] ; then
	ADAPTERS_FOUND=0
	ON_AC=0
	for ADAPTER in /proc/acpi/ac_adapter/* ; do
		if [ -f $ADAPTER/state ] ; then
			ADAPTERS_FOUND=1
			STATUS=`awk '/^state: / { print $2 }' $ADAPTER/state`
			if [ "$STATUS" = "on-line" ] ; then
				ON_AC=1
			fi
		fi
	done
elif [ -f /proc/apm ] ; then
	read D1 D2 D3 APM_AC_STATE D0 </proc/apm
	if [ "$APM_AC_STATE" = "0x00" ] ; then
		ON_AC=0
	else
		ON_AC=1
	fi
elif [ -f /proc/pmu/info ] ; then
	if ( grep "^AC Power.*0$" /proc/pmu/info ) ; then
		ON_AC=0
	else
		# It is possible that there is no AC Power = 1 in the file,
		# but we always assume AC power when we're not sure.
		ON_AC=1
	fi
fi


if [ "$ACTION" == "auto" ] ; then  
	if [ $LAPTOP_MODE_ALWAYS_ON -ne 0 ] ; then
		ACTION=start
	elif [ $ON_AC -eq 1 ] ; then
		ACTION=stop
	else
		ACTION=start
	fi

	if [ "$LM_WHEN_LID_CLOSED" -ne 0 ] ; then	
		if [ -f /proc/acpi/button/lid/*/state ] ; then
			if ( cat /proc/acpi/button/lid/*/state | grep "closed" ) ; then
				ACTION=start
			fi
		fi
	fi
fi

# If the init script has not been run or has been run with the "stop"
# argument, then we should never start laptop mode.
if [ "$ACTION" = start -a ! -f /var/run/laptop-mode-enabled ] ; then
	echo "Not starting laptop mode because /var/run/laptop-mode-enabled is missing."
	ACTION=stop
fi

if [ "$2" == "force" ] ; then
  DO_FORCE=1
else
  DO_FORCE=0
fi

case "$ACTION" in
	start)
		if [ "`cat /proc/sys/vm/laptop_mode`" != "0" -a $DO_FORCE -eq 0 ] ; then
			echo Laptop mode already active, not restarting.
			exit 0
		fi
		AGE=$((100*$MAX_AGE))
		XFS_AGE=$(($XFS_HZ*$MAX_AGE))
		echo -n "Starting laptop_mode"

		if [ -d /proc/sys/vm/pagebuf ] ; then
			# (For 2.4 and early 2.6.)
			# This only needs to be set, not reset -- it is only used when
			# laptop mode is enabled.
			echo $XFS_AGE > /proc/sys/vm/pagebuf/lm_flush_age
			echo $XFS_AGE > /proc/sys/fs/xfs/lm_sync_interval
		elif [ -f /proc/sys/fs/xfs/lm_age_buffer ] ; then
			# (A couple of early 2.6 laptop mode patches had these.)
			# The same goes for these.
			echo $XFS_AGE > /proc/sys/fs/xfs/lm_age_buffer
			echo $XFS_AGE > /proc/sys/fs/xfs/lm_sync_interval
		elif [ -f /proc/sys/fs/xfs/age_buffer ] ; then
			# (2.6.6)
			# But not for these -- they are also used in normal
			# operation.
			echo $XFS_AGE > /proc/sys/fs/xfs/age_buffer
			echo $XFS_AGE > /proc/sys/fs/xfs/sync_interval
		elif [ -f /proc/sys/fs/xfs/age_buffer_centisecs ] ; then
			# (2.6.7 upwards)
			# And not for these either. These are in centisecs,
			# not USER_HZ, so we have to use $AGE, not $XFS_AGE.
			echo $AGE > /proc/sys/fs/xfs/age_buffer_centisecs
			echo $AGE > /proc/sys/fs/xfs/xfssyncd_centisecs
			echo 3000 > /proc/sys/fs/xfs/xfsbufd_centisecs
		fi

		case "$KLEVEL" in
			"2.4")
				echo 1					> /proc/sys/vm/laptop_mode
				echo "30 500 0 0 $AGE $AGE 60 20 0"	> /proc/sys/vm/bdflush
				;;
			"2.6")
				echo "$LM_SECONDS_BEFORE_SYNC"		> /proc/sys/vm/laptop_mode
				echo "$AGE"				> /proc/sys/vm/dirty_writeback_centisecs
				echo "$AGE"				> /proc/sys/vm/dirty_expire_centisecs
				echo "$DIRTY_RATIO"			> /proc/sys/vm/dirty_ratio
				echo "$DIRTY_BACKGROUND_RATIO"		> /proc/sys/vm/dirty_background_ratio
				;;
		esac
		if [ $DO_REMOUNTS -eq 1 ]; then
			cat /etc/mtab | while read DEV MP FST OPTS DUMP PASS ; do
				if [ "$REMOUNT_PARTITIONS" != "" ] ; then
					if ( echo " $REMOUNT_PARTITIONS " | grep $DEV > /dev/null ) ; then
						DO=1
					else
						DO=0
					fi
				else
					DO=0
					for THISHD in $HD ; do
						if ( echo " $DEV" | grep "$THISHD" > /dev/null ) ; then
							DO=1
						fi
					done
				fi
				if [ "$DO" -ne 0 ] ; then
					PARSEDOPTS="$(parse_mount_opts "$OPTS")"
					if [ "$FST" = 'unknown' ]; then
						FST=$(deduce_fstype $MP)
					fi
					case "$FST" in
						"ext3"|"reiserfs")
							PARSEDOPTS="$(parse_mount_opts commit "$OPTS")"
							mount $DEV -t $FST $MP -o remount,$PARSEDOPTS,commit=$MAX_AGE$NOATIME_OPT
							;;
						"xfs")
							mount $DEV -t $FST $MP -o remount,$OPTS$NOATIME_OPT
							;;
					esac
					if [ -b $DEV ] ; then
						blockdev --setra $(($READAHEAD * 2)) $DEV > /dev/null 2>&1
					fi
				fi
			done
		fi
		if [ $DO_CPU -eq 1 -a -e /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq ]; then
			if [ $CPU_MAXFREQ = 'slowest' ]; then
				CPU_MAXFREQ=`cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq`
			fi
			echo $CPU_MAXFREQ > /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq
		fi
		echo "."
		;;
	stop)
		if [ "`cat /proc/sys/vm/laptop_mode`" = "0" -a $DO_FORCE -eq 0 ] ; then
			echo Laptop mode was already disabled, not disabling.
			exit 0
		fi
		U_AGE=$((100*$DEF_UPDATE))
		B_AGE=$((100*$DEF_AGE))
		echo -n "Stopping laptop_mode"
		echo 0 > /proc/sys/vm/laptop_mode
		if [ -f /proc/sys/fs/xfs/age_buffer -a ! -f /proc/sys/fs/xfs/lm_age_buffer ] ; then
			# These need to be restored, if there are no lm_*.
			echo $(($XFS_HZ*$DEF_XFS_AGE_BUFFER))	 	> /proc/sys/fs/xfs/age_buffer
			echo $(($XFS_HZ*$DEF_XFS_SYNC_INTERVAL)) 	> /proc/sys/fs/xfs/sync_interval
		elif [ -f /proc/sys/fs/xfs/age_buffer_centisecs ] ; then
			# These need to be restored as well.
			echo $((100*$DEF_XFS_AGE_BUFFER))	> /proc/sys/fs/xfs/age_buffer_centisecs
			echo $((100*$DEF_XFS_SYNC_INTERVAL))	> /proc/sys/fs/xfs/xfssyncd_centisecs
			echo $((100*$DEF_XFS_BUFD_INTERVAL))	> /proc/sys/fs/xfs/xfsbufd_centisecs
		fi
		case "$KLEVEL" in
			"2.4")
				echo "30 500 0 0 $U_AGE $B_AGE 60 20 0"	> /proc/sys/vm/bdflush
				;;
			"2.6")
				echo "$U_AGE"				> /proc/sys/vm/dirty_writeback_centisecs
				echo "$B_AGE"				> /proc/sys/vm/dirty_expire_centisecs
				echo "$DEF_DIRTY_RATIO"			> /proc/sys/vm/dirty_ratio
				echo "$DEF_DIRTY_BACKGROUND_RATIO"	> /proc/sys/vm/dirty_background_ratio
				;;
		esac
		if [ $DO_REMOUNTS -eq 1 ] ; then
			cat /etc/mtab | while read DEV MP FST OPTS DUMP PASS ; do
				if [ "$REMOUNT_PARTITIONS" != "" ] ; then
					if ( echo " $REMOUNT_PARTITIONS " | grep $DEV > /dev/null ) ; then
						DO=1
					else
						DO=0
					fi
				else
					DO=0
					for THISHD in $HD ; do
						if ( echo " $DEV" | grep "$THISHD" > /dev/null ) ; then
							DO=1
						fi
					done
				fi
				if [ "$DO" -ne 0 ] ; then
					# Reset commit and atime options to defaults.
					if [ "$FST" = 'unknown' ]; then
						FST=$(deduce_fstype $MP)
					fi
					case "$FST" in
						"ext3"|"reiserfs")
							PARSEDOPTS="$(parse_mount_opts_wfstab $DEV commit $OPTS)"
							PARSEDOPTS="$(parse_yesno_opts_wfstab $DEV atime atime $PARSEDOPTS)"
							mount $DEV -t $FST $MP -o remount,$PARSEDOPTS
							;;
						"xfs")
							PARSEDOPTS="$(parse_yesno_opts_wfstab $DEV atime atime $OPTS)"
							mount $DEV -t $FST $MP -o remount,$PARSEDOPTS
							;;
					esac
					if [ -b $DEV ] ; then
						blockdev --setra 256 $DEV > /dev/null 2>&1
					fi
				fi
			done
		fi
		if [ $DO_CPU -eq 1 -a -e /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_min_freq ]; then
			echo `cat /sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq` > /sys/devices/system/cpu/cpu0/cpufreq/scaling_max_freq
		fi
		echo "."
		;;
	*)
		echo "Usage: $0 {start|stop|auto}" 2>&1
		exit 1
		;;

esac

# Adjust hard drive parameters

HDPARM_POWERMGMT=$BATT_HDPARM_POWERMGMT
HDPARM_SPINDOWN=$BATT_HD
if [ $ON_AC -eq 1 ] ; then
	if [ "$ACTION" = "start" ] ; then
		HDPARM_POWERMGMT=$AC_HDPARM_POWERMGMT_WITH_LM
		HDPARM_SPINDOWN=$AC_HD_WITH_LM
	else
		HDPARM_POWERMGMT=$AC_HDPARM_POWERMGMT_WITHOUT_LM
		HDPARM_SPINDOWN=$AC_HD_WITHOUT_LM
	fi
fi

if [ $DO_HD_POWERMGMT -eq 1 ] ; then
	for THISHD in $HD ; do
		hdparm -B $HDPARM_POWERMGMT $THISHD > /dev/null 2>&1
	done
fi
if [ $DO_HD -eq 1 ] ; then
	for THISHD in $HD ; do
		hdparm -S $HDPARM_SPINDOWN $THISHD > /dev/null 2>&1
	done
fi

if [ $DO_SYSLOG -eq 1 ] ; then
	if [ $ON_AC -eq 1 ] ; then
		if [ "$ACTION" = "start" ] ; then
			ln -fs $AC_SYSLOG_WITH_LM $SYSLOG_CONF
		elif [ "$ACTION" = "stop" ] ; then
			ln -fs $AC_SYSLOG_WITHOUT_LM $SYSLOG_CONF
		fi
	else
		ln -fs $BATT_SYSLOG $SYSLOG_CONF
	fi
	# Notify syslogd of configuration change.
	if [ "$SYSLOG_SIGNAL_PROGRAM" != "" ] ; then
		killall -q -HUP $SYSLOG_SIGNAL_PROGRAM
	fi
fi
exit 0

# This fi closes the initial if [ "$1" == "defaults" ]. It is not indented
# because that would indent almost the whole file. :)
fi
