#!/bin/bash

# Compile server for systemtap
#
# Copyright (C) 2008, 2009 Red Hat Inc.
#
# This file is part of systemtap, and is free software.  You can
# redistribute it and/or modify it under the terms of the GNU General
# Public License (GPL); either version 2, or (at your option) any
# later version.

# This script unpacks the tar file provided on stdin and uses the information
# contained in the unpacked tree to build the requested systemtap kernel module.
# This module is then written to stdout.

# Catch ctrl-c and other termination signals
trap 'terminate' SIGTERM SIGINT

# Initialize the environment
. `dirname $0`/stap-env

#-----------------------------------------------------------------------------
# Helper functions.
#-----------------------------------------------------------------------------
# function: initialization
function initialization {
    # Initialization
    stap_rc=0
    wd=`pwd`

    # Default options settings
    p_phase=5
    keep_temps=0

    # Request file name.
    zip_client=$1
    test "X$zip_client" != "X" || \
	fatal "Client request file not specified"
    test -f $zip_client || \
	fatal "Unable to find request file $zip_client"

    # Temp directory we will be working in
    tmpdir_server=$2
    test "X$tmpdir_server" != "X" || \
	fatal "Server temporary directory not specified"
    test -d $tmpdir_server || \
	fatal "Unable to find temporary directory $tmpdir_server"
    tmpdir_env=`dirname $tmpdir_server`

    # Signed reponse file name.
    zip_server=$3
    test "X$zip_server" != "X" || \
	fatal ".zip archive file not specified"
    # Make sure the specified .zip file exists.
    test -f $zip_server || \
	fatal "Unable to find .zip archive file $zip_server"

    # Where is the ssl certificate/key database?
    ssl_db=$4
    test "X$ssl_db" != "X" || \
	fatal "SSL certificate database not specified"
    test -d $ssl_db || \
	fatal "Unable to find SSL certificate database $ssl_db"
    nss_pw=$ssl_db/pw
    test -f $nss_pw || \
	fatal "Unable to find SSL certificate database password file $nss_pw"
    nss_cert=stap-server

    touch $tmpdir_server/stdout
    touch $tmpdir_server/stderr
}

# function: unpack_request
#
# Unpack the zip file received from the client and make the contents
# available for use when running 'stap'
function unpack_request {
    cd $tmpdir_server

    # Unpack the zip file.
    unzip $zip_client > /dev/null || \
	fatal "Cannot unpack zip archive $zip_client"

    # Identify the client's request tree. The zip file should have expanded
    # into a single directory named to match $stap_tmpdir_prefix_client.??????
    # which should now be the only item in the current directory.
    test "`ls | wc -l`" = 3 || \
	fatal "Wrong number of files after expansion of client's zip file"

    tmpdir_client=`ls | grep $stap_tmpdir_prefix_client.......\$`

    test "X$tmpdir_client" != "X" || \
	fatal "Client zip file did not expand as expected"

    # Move the client's temp directory to a local temp location
    local local_tmpdir_client=`mktemp -dt $stap_tmpdir_prefix_server.client.XXXXXX` || \
	fatal "Cannot create temporary client request directory " $local_tmpdir_client
    mv $tmpdir_client/* $local_tmpdir_client
    rm -fr $tmpdir_client
    tmpdir_client=$local_tmpdir_client
}

# function: check_request
#
# Examine the contents of the request to make sure that they are valid.
function check_request {
    # Work in the temporary directory provided by the client
    cd $tmpdir_client

    # Add the necessary info from files in our temporary directory.
    cmdline=`read_data_file cmdline`
    test "X$cmdline" != "X" || exit 1

    eval parse_options "$cmdline"

    client_sysinfo=`read_data_file sysinfo`
    test "X$client_sysinfo" != "X" || exit 1

    check_compatibility "$client_sysinfo" "`server_sysinfo`"
}

# function server_sysinfo
#
# Generate the server's sysinfo and echo it to stdout
function server_sysinfo {
    if test "X$sysinfo_server" = "X"; then
	# Add some info from uname
	sysinfo_server="`uname -rvm`"
    fi
    echo "$sysinfo_server"
}

# function check_compaibility SYSINFO1 SYSINFO2
#
# Make sure that systemtap as described by SYSINFO1 and SYSINFO2 are compaible
function check_compatibility {
    # Compatibility is irrelevant if the request is not for phase 5 activity.
    test $p_phase -lt 5 && return

    # TODO: This needs work
    # - Make sure the linux kernel matches exactly
    local sysinfo1=$1
    local sysinfo2=$2

    if test "$sysinfo1" != "$sysinfo2"; then
	error "System configuration mismatch"
	error "  client: $sysinfo1"
	fatal "  server: $sysinfo2"
    fi
}

# function: read_data_file PREFIX
#
# Find a file whose name is '$1' and whose first line
# contents are '$1: .*'. Read and echo the data.
function read_data_file {
    test -f $1 || \
	fatal "Data file $1 not found"

    # Open the file
    exec 3< $1

    # Verify the first line of the file.
    read <&3
    line="$REPLY"
    data=`expr "$line" : "$1: \\\(.*\\\)"`
    if test "X$data" = "X"; then
	fatal "Data in file $1 is incorrect"
	return
    fi

    # Close the file
    exec 3<&-

    # Now read the entire file.
    cat $1 | sed "s/$1: //"
}

# function: parse_options [ STAP-OPTIONS ]
#
# Examine the command line. We need not do much checking, but we do need to
# parse all options in order to discover the ones we're interested in.
function parse_options {
    while test $# != 0
    do
	advance_p=0
	dash_seen=0

        # Start of a new token.
	first_token=$1
	until test $advance_p != 0
	do
            # Identify the next option
	    first_char=`expr "$first_token" : '\(.\).*'`
	    if test $dash_seen = 0; then
		if test "$first_char" = "-"; then
		    if test "$first_token" != "-"; then
	                # It's not a lone dash, so it's an option. Remove the dash.
			first_token=`expr "$first_token" : '-\(.*\)'`
			dash_seen=1
			first_char=`expr "$first_token" : '\(.\).*'`
		    fi
		fi
		if test $dash_seen = 0; then
	            # The dash has not been seen. This is either the script file
	            # name or an arument to be passed to the probe module.
	            # If this is the first time, and -e has not been specified,
	            # then it could be the name of the script file.
		    if test "X$e_script" = "X" -a "X$script_file" = "X"; then
			script_file=$first_token
		    fi
		    advance_p=$(($advance_p + 1))
		    break
		fi
	    fi
	    
            # We are at the start of an option. Look at the first character.
	    case $first_char in
		c)
		    get_arg $first_token "$2"
		    ;;
		D)
		    get_arg $first_token $2
		    ;;
		e)
		    get_arg $first_token "$2"
		    process_e "$stap_arg"
		    ;;
		I)
		    get_arg $first_token $2
		    ;;	
		k)
		    keep_temps=1
		    ;;
		l)
		    get_arg $first_token $2
		    ;;
		m)
		    get_arg $first_token $2
		    ;;
		o)
		    get_arg $first_token $2
		    ;;
		p)
		    get_arg $first_token $2
		    process_p $stap_arg
		    ;;
		r)
		    get_arg $first_token $2
		    ;;	
		R)
		    get_arg $first_token $2
		    ;;	
		s)
		    get_arg $first_token $2
		    ;;	
		x)
		    get_arg $first_token $2
		    ;;
		*)
		    # An unknown flag. Ignore it.
		    ;;
	    esac

	    if test $advance_p = 0; then
	        # Just another flag character. Consume it.
		first_token=`expr "$first_token" : '.\(.*\)'`
		if test "X$first_token" = "X"; then
		    advance_p=$(($advance_p + 1))
		fi
	    fi
	done

        # Consume the arguments we just processed.
	while test $advance_p != 0
	do
	    shift
	    advance_p=$(($advance_p - 1))
	done
    done
}

# function: get_arg FIRSTWORD SECONDWORD
#
# Collect an argument to the given option
function get_arg {
    # Remove first character. Advance to the next token, if the first one
    # is exhausted.
    local first=`expr "$1" : '.\(.*\)'`
    if test "X$first" = "X"; then
	shift
	advance_p=$(($advance_p + 1))
	first=$1
    fi

    stap_arg="$first"
    advance_p=$(($advance_p + 1))
}

# function: process_e ARGUMENT
#
# Process the -e flag.
function process_e {
    if test "X$e_script" = "X"; then
	e_script="$1"
	script_file=
    fi
}

# function: process_p ARGUMENT
#
# Process the -p flag.
function process_p {
    if test $1 -ge 1 -a $1 -le 5; then
	p_phase=$1
    fi
}

# function: call_stap
#
# Call 'stap' with the options provided. Don't run past phase 4.
function call_stap {
    # Invoke systemtap.
    # Use -k so we can return results to the client
    # Limit to -p4. i.e. don't run the module
    cd $tmpdir_client
    if test $p_phase -gt 4; then
	server_p_phase=4
    else
	server_p_phase=$p_phase
    fi

    eval ${stap_exec_prefix}stap "$cmdline" -k -p $server_p_phase \
	>>  $tmpdir_server/stdout \
	2>> $tmpdir_server/stderr

    stap_rc=$?
}

# function: create_response
#
# Add information to the server's temp directory representing the response
# to the client.
function create_response {
    cd $tmpdir_server

    # Get the name of the stap temp directory, which was kept, from stderr.
    tmpdir_line=`cat stderr | grep "Keeping temp"`
    tmpdir_stap=`expr "$tmpdir_line" : '.*"\(.*\)".*'`

    # Remove the message about keeping the stap temp directory from stderr, unless
    # the user did request to keep it.
    if test "X$tmpdir_stap" != "X"; then
	if test $keep_temps != 1; then
	    sed -i "/^Keeping temp/d" stderr
	fi

        # Add the contents of the stap temp directory to the server output directory
	ln -s $tmpdir_stap `basename $tmpdir_stap`
    fi

    # If the user specified -p5, remove the name of the kernel module from stdout.
    if test $p_phase = 5; then
	sed -i '/\.ko$/d' stdout
    fi

    # The return status of the stap command.
    echo -n $stap_rc > rc
}

# function: package_response
#
# Package the server's temp directory into a form suitable for sending to the
# client.
function package_response {
    cd $tmpdir_env

    # Compress the server's temporary directory into a .zip archive.
    (rm $zip_server && zip -r $zip_server `basename $tmpdir_server` > /dev/null) || \
	fatal "zip of request tree, $tmpdir_server, failed"
}

# function: fatal [ MESSAGE ]
#
# Fatal error
# Prints its arguments to stderr and exits
function fatal {
    echo "$0: ERROR:" "$@" >> $tmpdir_server/stderr
    echo -n 1 > $tmpdir_server/rc
    package_response
    cleanup
    exit 1
}

# Non fatal error
# Prints its arguments to stderr but does not exit
function error {
    echo "$0: ERROR:" "$@" >> $tmpdir_server/stderr
}

# function cleanup
#
# Cleanup work files unless asked to keep them.
function cleanup {
    # Clean up.
    cd $tmpdir_env
    if test $keep_temps != 1; then
	rm -fr $tmpdir_server
	rm -fr $tmpdir_client
	rm -fr $tmpdir_stap
    fi
}

# function: terminate
#
# Terminate gracefully.
function terminate {
    # Clean up
    cleanup
    exit 1
}

#-----------------------------------------------------------------------------
# Beginning of main line execution.
#-----------------------------------------------------------------------------
initialization "$@"
unpack_request
check_request
call_stap
create_response
package_response
cleanup

exit 0
