#!/bin/bash
#
# Bootchart logger script
# Ziga Mahkovec  <ziga.mahkovec@klika.si>
#
# This script is used for data collection for the bootchart
# boot performance visualization tool (http://www.bootchart.org).
#
# To profile the boot process, bootchartd should be called instead of
# /sbin/init.  Modify the kernel command line to include:
# init=/sbin/bootchartd
#
# bootchartd will then start itself in background and exec /sbin/init.
#
# To profile a running system, run:
# $ /sbin/bootchartd start; sleep 30; /sbin/bootchartd stop
#

VERSION="0.8"
TMPFS_MOUNT=/mnt/bootchartd
TMPFS_SIZE=64m
BOOTLOG_LOCK="$TMPFS_MOUNT/.lock"
SAMPLE_PERIOD=0.2
ACCTON=/etc/accton
PROCESS_ACCOUNTING="no"
[ -f ${ACCTON} ] && PROCESS_ACCOUNTING="yes"
BOOTLOG_DEST=/var/log/bootchart.tgz
AUTO_RENDER="no"
AUTO_RENDER_FORMAT="png"

# Start the boot logger.
start()
{
	# Make sure only a single instance is running
	[ -f "$BOOTLOG_LOCK" ] && return
	
	# Mount the temporary file system for log file storage
	[ ! -d "$TMPFS_MOUNT" ] && /bin/mkdir "$TMPFS_MOUNT"
	/bin/mount -n -t tmpfs -o size=$TMPFS_SIZE none "$TMPFS_MOUNT" >/dev/null 2>&1
	/bin/touch "$BOOTLOG_LOCK"

	# Enable process accounting if configured
	if [ "$PROCESS_ACCOUNTING" = "yes" -a -x $ACCTON ]; then
		/bin/touch "$TMPFS_MOUNT/kernel_pacct"
		$ACCTON "$TMPFS_MOUNT/kernel_pacct"
	fi
	
	# Wait for /proc to be mounted
	while [ ! -f /proc/stat ]; do /bin/sleep $SAMPLE_PERIOD; done
	/bin/sleep $SAMPLE_PERIOD

	#
	# Run loggers in background
	#
	log_output "/bin/cat /proc/stat" proc_stat.log &

	# /proc/diskstats is available in 2.6 kernels
	[ -f /proc/diskstats ] && log_output "/bin/cat /proc/diskstats" proc_diskstats.log &

	# "__proc_stat__" should have been "/bin/cat /proc/*/stat", but since we
	# have to perform the globbing in-place, use this hack
	log_output "__proc_stat__" proc_ps.log &

	if [ -n "$IN_INIT" ]; then
		# If we were called during init, wait for the boot process to end
		wait_boot &
	elif [ "$#" -gt 0 ]; then
		# If a command was passed, run it (used for profiling applications)
		$@
		echo "bootchartd.process = $@" >> "$TMPFS_MOUNT"/header
		stop
	fi
}


# Run the command ($1) every $SAMPLE_PERIOD seconds and append output to
# file ($2).  Mostly pure bash, so we avoid creating an excessive number of
# processes (thus polluting the process tree).
log_output()
{
	local cmd="$1"
	local logfile="$2"
	while [ -f "$BOOTLOG_LOCK" ]; do
		# Write the time (in jiffies).  If it fails, it's most likely
		# due to the tmpfs filling up, so stop logging
		read cpustat < /proc/stat
		get_uptime $cpustat >> "$TMPFS_MOUNT/$logfile" || stop

		# Log the command output
		if [ "$cmd" = "__proc_stat__" ]; then
			/bin/cat /proc/*/stat 2>/dev/null >> "$TMPFS_MOUNT/$logfile"
		else
			$cmd 2>/dev/null >> "$TMPFS_MOUNT/$logfile"
		fi
		echo >> "$TMPFS_MOUNT/$logfile"
		/bin/sleep $SAMPLE_PERIOD
	done;
}

# Retrieves the uptime from the first line of /proc/stat:
# cpu  11562 0 1918 96634 7040 202 0
# All CPU times are summed up.
get_uptime()
{
	shift
	local sum=0
	for i in $@; do
		sum=$(( $sum + $i ))
	done
	echo $sum
}

# Wait for the boot process to end.
wait_boot()
{
	local pidof_path=/sbin/pidof
	[ -x /bin/pidof ] && pidof_path=/bin/pidof
	local runlevel=$( /bin/sed -n 's/.*:\(.*\):initdefault:.*/\1/gp' /etc/inittab )

	# The processes we have to wait for
	local exit_proc="gdmgreeter gdm-binary kdm_greet kdm xdm"
	if [ "$runlevel" = "2" -o "$runlevel" = "3" ]; then
		exit_proc="mingetty agetty rungetty getty"
	fi
	while [ -f "$BOOTLOG_LOCK" ]; do
		if [ -n "$( $pidof_path $exit_proc )" ]; then
			# Give the exit process some time to start
			/bin/sleep 5
			# Flush the log files
			stop
			return
		fi
		/bin/sleep 2
	done;
}


# Stop the boot logger.  The lock file is removed to force the loggers in
# background to exit.  Some final log files are created and then all log files
# from the tmpfs are packaged and stored in /var/log. 
stop()
{
	if [ ! -f "$BOOTLOG_LOCK" ]; then
		echo "${0##*/} not running"
		return
	fi
	/bin/rm -f "$BOOTLOG_LOCK"
	/bin/sleep $SAMPLE_PERIOD
	/bin/sleep $SAMPLE_PERIOD

	# Stop process accounting if configured
	local pacct=
	if [ "$PROCESS_ACCOUNTING" = "yes" -a -x $ACCTON ]; then
		$ACCTON
		pacct=kernel_pacct
	fi

	# Write system information
	log_header

	# Package log files
	pushd "$TMPFS_MOUNT" > /dev/null
	/bin/tar -zcf "$BOOTLOG_DEST" header $pacct *.log
	popd > /dev/null
	/bin/umount "$TMPFS_MOUNT"

	# Render the chart if configured (and the renderer is installed)
	[ "$AUTO_RENDER" = "yes" -a -x /usr/bin/bootchart ] && \
		/usr/bin/bootchart -o /var/log/ -f $AUTO_RENDER_FORMAT
}


# Log some basic information about the system.
log_header()
{
	HEADER="$TMPFS_MOUNT/header"
	echo "version = $VERSION" >> "$HEADER"
	echo "title = Boot chart for $( /bin/hostname | /bin/sed q ) ($(LC_ALL=C /bin/date ))" >> "$HEADER"
	echo "system.uname = $( /bin/uname -srvm | /bin/sed q )" >> "$HEADER"
	if [ -f /etc/gentoo-release ]; then
		echo "system.release = $( /bin/sed q /etc/gentoo-release )" >> "$HEADER"
	elif [ -f /etc/SuSE-release ]; then
		echo "system.release = $( /bin/sed q /etc/SuSE-release )" >> "$HEADER"
	else
		echo "system.release = $( /bin/sed 's/\\.//g;q' /etc/issue )" >> "$HEADER"
	fi
	echo "system.cpu = $( /bin/grep '^model name' /proc/cpuinfo | /bin/sed q )"\
	     "($( /bin/grep -c '^model name' /proc/cpuinfo ))" >> "$HEADER"
	echo "system.kernel.options = $( /bin/sed q /proc/cmdline )" >> "$HEADER"
}


if [ $$ = 1 ]; then
	# Started by the kernel.  Start the logger in background and exec
	# init(1).
	IN_INIT="yes"
	echo "Starting bootchart logging"
	start &
	exec /sbin/init
fi

case "$1" in
	"init")
		# Started by the init script
		IN_INIT="yes"
		echo "Starting bootchart logging"
		start &
		;;
	"start")
		# Started by the user
		shift
		start $@
		;;
	"stop")
		stop
		/bin/rm $0
		/bin/rm $ACCTON
		/bin/mv /etc/init.org  /etc/init
		;;
	*)
		echo $"Usage: $0 {init|start|stop}"
		;;
esac
