#!/bin/bash # Compile server client for systemtap # # Copyright (C) 2008 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 examines the systemtap command line and packages the files and # information needed to execute the command. This is then sent to a trusted # systemtap server which will process the request and return the resulting # kernel module (if requested) and any other information generated by the # request. If a kernel module is generated, this script will load the module # and execute it using 'staprun', if requested. # Catch ctrl-c and other termination signals trap 'terminate' SIGTERM trap 'interrupt' SIGINT #----------------------------------------------------------------------------- # Helper functions. #----------------------------------------------------------------------------- # function: configuration function configuration { tmpdir_prefix_client=stap.client tmpdir_prefix_server=stap.server avahi_service_tag=_stap._tcp } # function: initialization function initialization { rc=0 wd=`pwd` umask 0 staprun_running=0 # Default options settings p_phase=5 v_level=0 keep_temps=0 # Create a temporary directory to package things in # Do this before parsing the command line so that there is a place # to put -I and -R directories. tmpdir_client=`mktemp -dt $tmpdir_prefix_client.XXXXXX` || \ fatal "ERROR: cannot create temporary directory " $tmpdir_client tmpdir_env=`dirname $tmpdir_client` } # 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. # The server will take care of most situations and return the appropriate # output. # function parse_options { cmdline= cmdline1= cmdline2= 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" : '\(.\).*'` second_char= 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. # Is it a long option (i.e. --option)? second_char=`expr "$first_token" : '.\(.\).*'` if test "$second_char" != "-"; then # It's not a lone dash, or a long option, so it's a short option string. # Remove the dash. first_token=`expr "$first_token" : '-\(.*\)'` dash_seen=1 first_char=`expr "$first_token" : '\(.\).*'` cmdline2="$cmdline2 -" fi fi fi if test $dash_seen = 0; then # The dash has not been seen. This is either the script file # name, a long argument or an argument 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$second_char" = "X-"; then cmdline2="$cmdline2 $first_token" elif test "X$e_script" = "X" -a "X$script_file" = "X"; then script_file=$first_token cmdline1="$cmdline2" cmdline2= else cmdline2="$cmdline2 '$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" process_c "$stap_arg" ;; D) get_arg $first_token $2 cmdline2="${cmdline2}D '$stap_arg'" ;; e) get_arg $first_token "$2" process_e "$stap_arg" ;; I) get_arg $first_token $2 process_I $stap_arg ;; k) keep_temps=1 ;; l) get_arg $first_token $2 cmdline2="${cmdline2}l '$stap_arg'" ;; m) get_arg $first_token $2 cmdline2="${cmdline2}m $stap_arg" ;; o) get_arg $first_token $2 process_o $stap_arg ;; p) get_arg $first_token $2 process_p $stap_arg ;; r) get_arg $first_token $2 cmdline2="${cmdline2}r $stap_arg" ;; R) get_arg $first_token $2 process_R $stap_arg ;; s) get_arg $first_token $2 cmdline2="${cmdline2}s $stap_arg" ;; v) v_level=$(($v_level + 1)) ;; x) get_arg $first_token $2 cmdline2="${cmdline2}x $stap_arg" ;; *) # An unknown or unimportant flag. Ignore it, but pass it on to the server. ;; esac if test $advance_p = 0; then # Just another flag character. Consume it. cmdline2="$cmdline2$first_char" 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 # If the script file was given and it's not '-', then replace it with its # client-temp-name in the command string. if test "X$script_file" != "X"; then local local_name if test "$script_file" != "-"; then local_name=`generate_client_temp_name $script_file` else local_name=$script_file fi cmdline="$cmdline1 script/$local_name $cmdline2" else cmdline="$cmdline1 $cmdline2" fi } # function: get_arg FIRSTWORD SECONDWORD # # Collect an argument to the given option function get_arg { # Remove first character. local opt=`expr "$1" : '\(.\).*'` local first=`expr "$1" : '.\(.*\)'` # Advance to the next token, if the first one is exhausted. if test "X$first" = "X"; then shift advance_p=$(($advance_p + 1)) first=$1 fi test "X$first" != "X" || \ fatal "Missing argument to -$opt" stap_arg="$first" advance_p=$(($advance_p + 1)) } # function: process_c ARGUMENT # # Process the -c flag. function process_c { c_cmd="$1" cmdline2="${cmdline2}c '$1'" } # function: process_e ARGUMENT # # Process the -e flag. function process_e { # Only the first -e option is recognized and it overrides any script file name # which may have already been identified. if test "X$e_script" = "X"; then e_script="$1" if test "X$script_file" != "X"; then cmdline1="$cmdline1 $script_file $cmdline2" cmdline2= script_file= fi fi cmdline2="${cmdline2}e '$1'" } # function: process_I ARGUMENT # # Process the -I flag. function process_I { local local_name=`include_file_or_directory tapsets $1` test "X$local_name" != "X" || return cmdline2="${cmdline2}I 'tapsets/$local_name'" } # function: process_o ARGUMENT # # Process the -o flag. function process_o { stdout_redirection="> $1" cmdline2="${cmdline2}o '$1'" } # function: process_p ARGUMENT # # Process the -p flag. function process_p { p_phase=$1 cmdline2="${cmdline2}p '$1'" } # function: process_R ARGUMENT # # Process the -R flag. function process_R { local local_name=`include_file_or_directory runtime $1` test "X$local_name" != "X" || return cmdline2="${cmdline2}R 'runtime/$local_name'" } # function: include_file_or_directory PREFIX NAME # # Include the given file or directory in the client's temporary # tree to be sent to the server. function include_file_or_directory { # Add a symbolic link of the named file or directory to our temporary directory local local_name=`generate_client_temp_name $2` mkdir -p $tmpdir_client/$1/`dirname $local_name` || \ fatal "ERROR: could not create $tmpdir_client/$1/`dirname $local_name`" ln -s /$local_name $tmpdir_client/$1/$local_name || \ fatal "ERROR: could not link $tmpdir_client/$1/$local_name to /$local_name" echo "$local_name" } # function: generate_client_temp_name NAME # # Generate the name to be used for the given file/directory relative to the # client's temporary directory. function generate_client_temp_name { # Transform the name into a fully qualified path name local full_name=`echo "$1" | sed "s,^\\\([^/]\\\),$wd/\\\\1,"` # The same name without the initial / or trailing / local local_name=`echo "$full_name" | sed 's,^/\(.*\),\1,'` local_name=`echo "$local_name" | sed 's,\(.*\)/$,\1,'` echo "$local_name" } # function: create_request # # Add information to the client's temp directory representing the request # to the server. function create_request { # Work in our temporary directory cd $tmpdir_client if test "X$script_file" != "X"; then if test "$script_file" = "-"; then mkdir -p $tmpdir_client/script || \ fatal "ERROR: cannot create temporary directory " $tmpdir_client/script cat > $tmpdir_client/script/$script_file else include_file_or_directory script $script_file > /dev/null fi fi # Add the necessary info to special files in our temporary directory. echo "cmdline: $cmdline" > cmdline echo "sysinfo: `client_sysinfo`" > sysinfo } # function client_sysinfo # # Generate the client's sysinfo and echo it to stdout function client_sysinfo { if test "X$sysinfo_client" = "X"; then # Add some info from uname sysinfo_client="`uname -rvm`" fi echo "$sysinfo_client" } # function: package_request # # Package the client's temp directory into a form suitable for sending to the # server. function package_request { # Package up the temporary directory into a tar file cd $tmpdir_env local tmpdir_client_base=`basename $tmpdir_client` tar_client=$tmpdir_env/`mktemp $tmpdir_client_base.tgz.XXXXXX` || \ fatal "ERROR: cannot create temporary file " $tar_client tar -czhf $tar_client $tmpdir_client_base || \ fatal "ERROR: tar of request tree, $tmpdir_client, failed" } # function: send_request # # Notify the server and then send $tar_client to the server # The protocol is: # client -> "request:" # client -> $tar_client function send_request { # Send the request file. We need to redirect to /dev/null # in order to workaround a nc bug. It closes the connection # early if stdin from the other side is not provided. until nc $server $(($port + 1)) < $tar_client > /dev/null do sleep 1 done } # function: receive_response # # Wait for a response from the server indicating the results of our request. function receive_response { # Make a place to receive the response file. tar_server=`mktemp -t $tmpdir_prefix_client.server.tgz.XXXXXX` || \ fatal "ERROR: cannot create temporary file " $tar_server # Retrieve the file. We need to redirect stdin from /dev/zero to work # around a bug in nc. It closes the connection early is stdin is not # provided. until nc $server $(($port + 1)) < /dev/zero > $tar_server do sleep 1 done } # function: unpack_response # # Unpack the tar file received from the server and make the contents available # for printing the results and/or running 'staprun'. function unpack_response { tmpdir_server=`mktemp -dt $tmpdir_prefix_client.server.XXXXXX` || \ fatal "ERROR: cannot create temporary file " $tmpdir_server # Unpack the server output directory cd $tmpdir_server tar -xzf $tar_server || \ fatal "ERROR: Unpacking of server response, $tar_server, failed" # Identify the server's response tree. The tar file should have expanded # into a single directory named to match $tmpdir_prefix_server.?????? # which should now be the only item in the current directory. test "`ls | wc -l`" = 1 || \ fatal "ERROR: Wrong number of files after expansion of server's tar file" tmpdir_server=`ls` tmpdir_server=`expr "$tmpdir_server" : "\\\($tmpdir_prefix_server\\\\.......\\\)"` test "X$tmpdir_server" != "X" || \ fatal "ERROR: server tar file did not expand as expected" # Check the contents of the expanded directory. It should contain: # 1) a file called stdout # 2) a file called stderr # 3) a file called rc # 4) optionally a directory named to match stap?????? local num_files=`ls $tmpdir_server | wc -l` test $num_files = 4 -o $num_files = 3 || \ fatal "ERROR: Wrong number of files in server's temp directory" test -f $tmpdir_server/stdout || \ fatal "ERROR: `pwd`/$tmpdir_server/stdout does not exist or is not a regular file" test -f $tmpdir_server/stderr || \ fatal "ERROR: `pwd`/$tmpdir_server/stderr does not exist or is not a regular file" test -f $tmpdir_server/rc || \ fatal "ERROR: `pwd`/$tmpdir_server/rc does not exist or is not a regular file" # See if there is a systemtap temp directory tmpdir_stap=`ls $tmpdir_server | grep stap` tmpdir_stap=`expr "$tmpdir_stap" : "\\\(stap......\\\)"` if test "X$tmpdir_stap" != "X"; then test -d $tmpdir_server/$tmpdir_stap || \ fatal "ERROR: `pwd`/$tmpdir_server/$tmpdir_stap is not a directory" # Move the systemtap temp directory to a local temp location, if -k # was specified. if test $keep_temps = 1; then local local_tmpdir_stap=`mktemp -dt stapXXXXXX` || \ fatal "ERROR: cannot create temporary directory " $local_tmpdir_stap mv $tmpdir_server/$tmpdir_stap/* $local_tmpdir_stap 2>/dev/null rm -fr $tmpdir_server/$tmpdir_stap # Correct the name of the temp directory in the server's stderr output sed -i "s,^Keeping temporary directory.*,Keeping temporary directory \"$local_tmpdir_stap\"," $tmpdir_server/stderr tmpdir_stap=$local_tmpdir_stap else # Make sure we own the systemtap temp directory if we are root. test $EUID = 0 && chown $EUID:$EUID $tmpdir_server/$tmpdir_stap # The temp directory will be moved to here below. tmpdir_stap=`pwd`/$tmpdir_stap fi fi # Move the contents of the server's tmpdir down one level to the # current directory (our local server tmpdir) mv $tmpdir_server/* . 2>/dev/null rm -fr $tmpdir_server tmpdir_server=`pwd` } # function: find_and_connect_to_server # # Find and establish connection with a compatible stap server. function find_and_connect_to_server { # Use a temp file here instead of a pipeline so that the side effects # of choose_server are seen by the rest of this script. cd $tmpdir_client stap-find-servers > servers choose_server < servers rm -fr servers } # function: choose_server # # Examine each line from stdin and attempt to connect to each server # specified until successful. function choose_server { local num_servers=0 local name while read name server port remain do num_servers=$(($num_servers + 1)) if test "X$server" = "X"; then fatal "ERROR: server ip address not provided" fi if test "X$port" = "X"; then fatal "ERROR: server port not provided" fi if connect_to_server $server $port; then return 0 fi done if test num_servers = 0; then fatal "ERROR: cannot find a server" fi fatal "ERROR: unable to connect to a server" } # function: connect_to_server IP PORT # # Establish connection with the given server function connect_to_server { until echo "request:" | nc $1 $2 > /dev/null do sleep 1 done } # function: disconnect_from_server # # Disconnect from the server. function disconnect_from_server { : } # function: process_response # # Write the stdout and stderr from the server to stdout and stderr respectively. function process_response { # Output stdout and stderr as directed cd $tmpdir_server cat stderr >&2 eval cat stdout $stdout_redirection # Pick up the results of running stap on the server. rc=`cat rc` } # function: maybe_call_staprun # # Call staprun using the module returned from the server, if requested. function maybe_call_staprun { if test $rc != 0; then # stap run on the server failed, so don't bother return fi if test $p_phase -ge 4; then # There should be a systemtap temporary directory. if test "X$tmpdir_stap" = "X"; then # OK if no script specified if test "X$e_script" != "X" -o "X$script_file" != "X"; then fatal "ERROR: systemtap temporary directory is missing in server response" fi return fi # There should be a module. local mod_name=`ls $tmpdir_stap | grep '.ko$'` if test "X$mod_name" = "X"; then fatal "ERROR: no module was found in $tmpdir_stap" fi if test $p_phase = 5; then # We have a module. Try to run it # If a -c command was specified, pass it along. if test "X$c_cmd" != "X"; then staprun_opts="-c '$c_cmd'" fi # The -v level will be one less than what was specified # for us. for ((--v_level; $v_level > 0; --v_level)) do staprun_opts="$staprun_opts -v" done # Run it in the background and wait for it. This # way any signals send to us can be caught. PATH=`staprun_PATH` eval staprun "$staprun_opts" \ $tmpdir_stap/`ls $tmpdir_stap | grep '.ko$'` & staprun_running=1 wait %?staprun rc=$? staprun_running=0 # 127 from wait means that the job was already finished. test $rc=127 && rc=0 fi fi } # function: staprun_PATH # # Compute a PATH suitable for running staprun. function staprun_PATH { # staprun may invoke 'stap'. So we can use the current PATH if we were # not invoked as 'stap' or we are not the first 'stap' on the PATH. local first_stap=`which stap 2>/dev/null` if test `which $0 2>/dev/null` != $first_stap; then echo "$PATH" return fi # Otherwise, remove the PATH component where we live from the PATH local PATH_component=`dirname $first_stap` echo "$PATH" | sed "s,$PATH_component,,g" } # function: fatal [ MESSAGE ] # # Fatal error # Prints its arguments to stderr and exits function fatal { echo "$0:" "$@" >&2 disconnect_from_server cleanup exit 1 } # 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_client rm -f $tar_client rm -f $tar_server rm -fr $tmpdir_server fi } # function: terminate # # Terminate gracefully. function terminate { # Clean up echo "$0: terminated by signal" cleanup # Kill any running staprun job kill -s SIGTERM %?staprun 2>/dev/null exit 1 } # function: interrupt # # Pass an interrupt (ctrl-C) to staprun function interrupt { # Pass the signal on to any running staprun job kill -s SIGINT %?staprun 2>/dev/null # If staprun was running, then just interrupt it. Otherwise # we exit. test $staprun_running = 0 && exit 1 } #----------------------------------------------------------------------------- # Beginning of main line execution. #----------------------------------------------------------------------------- configuration initialization parse_options "$@" create_request package_request find_and_connect_to_server send_request receive_response disconnect_from_server unpack_response process_response maybe_call_staprun cleanup exit $rc