#!/bin/sh
#
# ------------------------------------------------------------------
#  show_proc_lib.sh
#
#  October 2010, Michael C. Scott
#
#  Copyright (c) 2010-2014, 2016 by Cisco Systems, Inc.
#  All rights reserved.
# ------------------------------------------------------------------


# The sh_proc_lib library provides a variety of common functionality that is
# used by the show proc and pidin commands.


# sh_proc_time_conversion
#
# Used for converting time values from the /proc filesytem into seconds. This
# is retrieved by a wrapper program that gets the value using a system call.
#
#sh_proc_time_conversion=`sh_proc_get_constants`
#if [ $? -ne 0 ]; then
#    printf "Error retrieving time conversion constants\n"
#    exit 1
#fi

# sh_proc_time_offset
#
# Used when calculating the start time for a thread. The start time of a thread
# is calculated by calculating
# (current time) - (system uptime) + (thread start time)
# and then converting this to a standard date. To improve performance we only
# calculate the first part of this expression once and store it as
# sh_proc_time_offset.
#
sh_proc_time_offset=`date +%s`-`cat /proc/uptime | cut -d' ' -f 1 | cut -d'.' -f 1`

# sh_proc_node_proc_path
#
# Store the path of the /proc directory of the node currently under
# consideration.
sh_proc_node_proc_path=""


# sh_proc_cache_stat_*
#
# Used to store a copy of the most recently opened /proc/<pid>/stat file
# along with the PID of the process/thread that the file corresponds to.
# This improves performance when retrieving multiple items from the same file.
sh_proc_cache_stat_pid=-2
sh_proc_cache_stat_file=""


# sh_proc_cache_status_*
#
# Used to store a copy of the most recently opened /proc/<pid>/status file
# along with the PID of the process/thread that the file corresponds to.
# This improves performance when retrieving multiple items from the same file.
sh_proc_cache_status_pid=-2
sh_proc_cache_status_file=""


# sh_proc_location
#
# Used to store the value of command line arguments.
# sh_proc_location stores either "local" (the default), "all" or the name of a
# specific node.
sh_proc_location="local"


# sh_proc_verbose_flag
#
# sh_proc_verbose_flag is a boolean value that is 1 when either the "details"
# argument has been passed to "show proc files" or the "extended" argument has
# been passed to "show proc blocked".
sh_proc_verbose_flag=0


# sh_proc_no_headers
#
# sh_proc_no_headers is a boolean value that is 1 if no headers are to be
# printed for the "show proc blocked" function (where it is used for following
# blocking functions).
sh_proc_no_headers=0


# sh_proc_specific_jid
#
# sh_proc_specific_jid is -1 if no JID has been specified and has the value
# of the JID if one has been specified.
sh_proc_specific_jid=-1


# sh_proc_specific_pid
#
# sh_proc_specific_pid is used by the pidin command and is -1 if no PID has
# been specified and has teh value of the PID if one has been specified
sh_proc_specific_pid=-1


# sh_proc_*_out
#
# Used to store the intput/output/error values from the relevant functions.
sh_proc_node_iterator_out=""
sh_proc_process_iterator_out=""
sh_proc_thread_iterator_out=""
sh_proc_get_file_descriptors_out=""
sh_proc_format_time_out=""
sh_proc_format_memory_out=""
sh_proc_get_item_out=""
sh_proc_get_item_error=0
sh_proc_pid_list_to_jid_list_out=""
sh_proc_pid_list_to_jid_list_in=""


#sh_proc_usage
#
# Reports a usage error. This should only occur if one of the show proc
# commands is called manually with unrecognised options.
#
# Return: Null
#
function sh_proc_usage
{
    printf "Error parsing the input to a show processes command.\n"
}


#sh_proc_is_thinxr
#
# Check whether we're running on a ThinXR platform or not.
#
# Return: true if ThinXR, false if not.
#
function sh_proc_is_thinxr()
{
    [[ -e "/opt/cisco/thinxr/am_i_thinxr" ]]
}


# sh_proc_command_parser
#
# Parses the command line options/arguments and set the appropriate global
# variables.
#
# Return: None
#    
function sh_proc_command_parser
{
    while getopts "p:l:d:e:" Option
    do
        case $Option in
            p)
                if [[ $OPTARG =~ ^[0-9]*$ ]]; then
                    sh_proc_specific_pid=$OPTARG
                fi
                ;;
            l)
                if sh_proc_is_thinxr; then
                    # ThinXR platforms are single-node, and uname returns the
                    # hostname rather than the nodeid. So always ignore
                    # location options.
                    :
                elif [[ $OPTARG != "" ]];then 
                    given_location=$OPTARG
                    my_node=$(uname -n | cut -d "_"  -f2,3,4,5)
                    conv_given_location=`node_conversion -N $given_location` 
                    if [[ "$my_node" != "$conv_given_location" ]]; then
                        node_ip=`nodename_to_ipaddress -n $given_location 2>&1` 
                        ssh -o UserKnownHostsFile=/dev/null -o LogLevel=quiet -o StrictHostKeychecking=no  root@$node_ip '/pkg/bin/sh_proc_signal'
                        rc=$?
                        if [[ $rc != 0 ]]; then
                            echo "Failed to ssh onto node : $given_location"
                        fi
                        exit
                    fi
                else
                     echo "Location cannot be NULL"
                     exit
                fi
                ;;
            d)
                if [ $OPTARG -ne 0 ]; then
                sh_proc_verbose_flag=1
                fi 
                ;;
            x)
                sh_proc_no_headers=1
                ;;
            *)
                sh_proc_usage
                exit 1
                ;;
        esac
    done
    shift $(($OPTIND -1))
}


# sh_proc_node_iterator
#
# Generates a list of nodes based on the value of the sh_proc_location
# variable.
#
# Return: 0 on success, 1 on error
#
function sh_proc_node_iterator
{
#TODO - The whole function is dependent on external requirements.
    local ret_code=0

        sh_proc_node_iterator_out=("local")
    return $ret_code
}


# sh_proc_print_node_header
# 
# Prints a header that is used to prefix the output for each node when the
# "all" argument has been passed to the "location" option.
#
# Return: Null
#
# Argument: node
#   IN - The name of the node.
#
function sh_proc_print_node_header
{
    local node=$1
    printf "node:      %s\n" "$node"
    printf "%66s\n" "" | tr ' ' '-'
}

# sh_proc_clear_caches
#
# Clears the caches for the "stat" and "status" files. Used when switching
# nodes to avoid potential erroneous cache hits.
#
# Return: None
#
function sh_proc_clear_caches
{
    sh_proc_cache_stat_pid=-2
    sh_proc_cache_status_pid=-2
}

# sh_proc_set_proc_path
#
# Set the path to the /proc filesystem of a specified node.
#
# Return: 0 on success, 1 on error
#
# Argument: node
#    IN - The specfied node.
#
function sh_proc_set_proc_path
{
    local node=$1

    local ret_code=0

    #TODO - The path should be set dependent on the node. This has external
    #requirement aspects.
    sh_proc_node_proc_path="/proc"

    #We must clear the caches to avoid any risk of erroneous cache hits.
    sh_proc_clear_caches
}


# sh_proc_process_iterator
#
# Generates a list of processes for the current node. If a specific JID or PID
# has been specified then it is used (after conversion for the JID case). If
# not a list of all processes is generated from the /proc filesystem.
#
# Return: 0 on success, 1 on error
#
# Arguments: node
#    IN - The current node. Used if a JID to PID conversion is required.
#
function sh_proc_process_iterator
{
    local node=$1

    local ret_code=0

    if [ $sh_proc_specific_pid -ne "-1" ]; then
        sh_proc_process_iterator_out="$sh_proc_specific_pid"
#    else if [ $sh_proc_specific_jid -ne "-1" ]; then
#        sh_proc_process_iterator_out=`sh_proc_pid_jid_conversion j2p $sh_proc_specific_jid`
#        if [ $? -ne 0 ]; then
#            printf "Error converting JID to PID\n"
#            exit 1
#        fi

        #sh_proc_check_and_refresh_stat_cache $sh_proc_process_iterator_out
        #if [ $? -ne 0 ]; then
        #    printf "Invalid Job id or Job not running\n"
        #fi
    else
        local node_proc_path_ls=`ls $sh_proc_node_proc_path`

        if [ $? -eq 0 ]; then
            sh_proc_process_iterator_out=`echo $node_proc_path_ls | grep -v ^[a-z] | sort -u | sort -n`
        else
            ret_code=1
            sh_proc_process_iterator_out=""
        fi
    fi
    #fi
    return $ret_code
}


# sh_proc_pid_list_to_jid_list
#
# Converts a list of PIDs into a list of JIDs. The list of PIDs is passed in
# using a global array and the list of JIDs is passed out using a global array.
#
# Return: 0 on success, 1 on error
#
function sh_proc_pid_list_to_jid_list
{
    local pid_list=("${sh_proc_pid_list_to_jid_list_in[@]}")
    local jid_list=("`/pkg/bin/sh_proc_pid_jid_conversion p2j ${pid_list[@]}`")
    
    if [ $? -eq 0 ]; then
        sh_proc_pid_list_to_jid_list_out="$jid_list"
    else
        printf "Error converting PIDs to JIDs\n"
        exit 1
    fi
}


# sh_proc_thread_iterator
#
# Generates a list of threads for the specified process.
#
# Return: 0 on success, 1 on error
#
# Arguments: pid
#    IN - The PID of the specified process.
#
function sh_proc_thread_iterator
{
    local pid=$1

    local ret_code=0

    local proc_task_ls=`ls $sh_proc_node_proc_path/$pid/task 2>/dev/null`

    if [ $? -eq 0 ]; then
        sh_proc_thread_iterator_out=`echo $proc_task_ls | sort -n`
    else
        sh_proc_thread_iterator_out=""
        ret_code=1
    fi

    return $ret_code
}


# sh_proc_get_file_descriptors
#
# Retrieves a list of file descriptors for a specified process/thread.
#
# Return: 0 on success, 1 on error
#
# Argument: pid
#    IN - The pid/tid of the process/thread.
#
function sh_proc_get_file_descriptors
{
    local pid=$1

    local ret_code=0

    sh_proc_get_file_descriptors_out=(`ls $sh_proc_node_proc_path/$pid/fd 2>/dev/null | sort -n`)

    if [ $? -ne 0 ]; then
        sh_proc_get_file_descriptors_out=""
        ret_code=1
    fi

    return $ret_code
}




# sh_proc_check_and_refresh_stat_cache
#
# Checks whether the cached "stat" file corresponds to the specified process/
# thread. If not the relevant file is copied into the cache.
#
# Return: 0 on success, 1 on error
#
# Argument: pid
#    IN - The process/thread ID
#
function sh_proc_check_and_refresh_stat_cache
{
    local pid=$1
    local ret_code=0

    if [ $pid != $sh_proc_cache_stat_pid ]; then
        sh_proc_cache_stat_file=`cat $sh_proc_node_proc_path/$1/stat 2>/dev/null`
        if [ "$?" -ne "0" ]; then
            ret_code=1
        else
            sh_proc_cache_stat_file=`echo "$sh_proc_cache_stat_file" | sed s#\(.\*\)#name#`
            sh_proc_cache_stat_pid=$pid
        fi
    fi

    return $ret_code
}


# sh_proc_check_and_refresh_status_cache
#
# Checks whether the cached "status" file corresponds to the specified process/
# thread. If not the relevant file is copied into the cache.
#
# Return: 0 on success, 1 on error
#
# Argument: pid
#    IN - The process/thread ID
#
function sh_proc_check_and_refresh_status_cache
{
    local pid=$1
    local ret_code=0

    if [ $pid != $sh_proc_cache_status_pid ]; then
        sh_proc_cache_status_file=`cat $sh_proc_node_proc_path/$pid/status 2>/dev/null`
        if [ "$?" -ne "0" ]; then
            ret_code=1
        else
            sh_proc_cache_status_pid=$pid
        fi
    fi

    return $ret_code
}


# sh_proc_format_memory
#
# Takes a number of kilobytes as the input and creates a string describing the
# amount of memory. Up to 8192 kB this is just <X>K where X is the number of
# kilobytes. For values between 8192kB and 8192*1024kB (i.e 8192MB) the string
# is <X>M where X is the number of megabytes. For values greater than 8192MB the
# string is <X>G where X is the number of gigabytes.
#
# Return: None
#
# Argument: kB
#    IN - The number of kilobytes
#
function sh_proc_format_memory
{
    local kB=$1
    local result=""

    if [ $kB -lt 8192 ]; then
        #format as kB
        result="$kB""K"
    else if [ $kB -lt 8388608 ]; then
        #format as MB
        local MB=0
        let MB=kB/1024
        result="$MB""M"
    else
        #format as GB
        local GB=0
        let GB=kB/1048576
        result="$GB""G"
    fi
    fi

    sh_proc_format_memory_out="$result"
}


# sh_proc_format_time
#
# Takes a time in jiffies (a unit of time in the Linux kernel) and creates a
# string describing the length of the time value. There are 5 cases to consider:
# 1. t < 60s      - The time is output as <x> where x is the number of seconds
#                   to millisecond precision
# 2. t < 1 hour   - The time is output as <x>m<y>s where x and y are number of 
#                   minutes and seconds.
# 3. t < 1 day    - The time is output as <x>h<y>m where x and y are the number 
#                   of hours and minutes
# 4. t < 99 days  - The time is output as <x>d<y>h where x and y are the number
#                   of days and hours
# 5. t >= 99 days - The time is output as <x>d where x is the number of days
#
# Return: None
#
# Argument: time
#    IN - The time in jiffies
#
function sh_proc_format_time
{
    local time=$1
    local result=""
    local rough_seconds=0

    # This variable is the rough number of seconds as bash doesn't support
    # floating point calculations.
    let rough_seconds=time/sh_proc_time_conversion

    local first_block=""
    local second_block=""

    if [ $rough_seconds -lt 60 ]; then
        # Case 1: seconds with millisecond precision
        local milliseconds=0
        let milliseconds=time*1000/sh_proc_time_conversion
        let first_block=milliseconds/1000
        let second_block=milliseconds%1000
        result=`printf "%2d.%3.3d\n" $first_block $second_block`
    else if [ $rough_seconds -lt 3600 ]; then
        # Case 2: minutes/seconds
        let first_block=rough_seconds/60
        let second_block=rough_seconds%60
        result=`printf "%2dm%2.2ds\n" $first_block $second_block`
    else if [ $rough_seconds -lt 86400 ]; then
        # Case 3: hours/minutes
        let first_block=rough_seconds/3600
        let second_block=(rough_seconds%3600)/60
        result=`printf "%2dh%2.2dm\n" $first_block $second_block`
    else if [ $rough_seconds -lt 8553600 ]; then
        # Case 4: days/hours
        let first_block=rough_seconds/86400
        let second_block=(rough_seconds%86400)/3600
        result=`printf "%2dd%2.2dh\n" $first_block $second_block`
    else
        # Case 5: more than 99 days
        let first_block=rough_seconds/86400
        result=`printf "%s\n" $first_block`
    fi
    fi
    fi
    fi

    sh_proc_format_time_out=$result
}


# sh_proc_get_item
#
# Retrieves a specified item of data for a specified process/thread.
#
# Return: 0 on success, 1 on error
#
# Argument: pid
#    IN - The process/thread ID.
#
# Argument: item
#    IN - The item to retrieve.
#
function sh_proc_get_item
{
    local pid=$1
    local item=$2

    local ret_code=0

    local result=""

    if [ $# -ne 2 ]; then
        echo "Incorrect usage of the library function sh_proc_get_item"
        exit 2
    fi

    case "$item" in
        #status file items
        "ppid" | "sig_pend" | "sig_ign" | "sig_queue" | "sig_blk" \
        | "threadname" | "code_size" | "data_size" | "stack_size" \
        | "dynamic_size" | "n_threads" | "uid" | "gid" | "euid" \
		| "egid" | "suid" | "sgid" | "runmask")
            sh_proc_check_and_refresh_status_cache $pid
            if [ $? -eq 0 ]; then
                local result_column=2
                local search_string=""
                case "$item" in
                    "threadname")
                        search_string="^Name:"
                        ;;
                    "ppid")
                        search_string="^PPid:"
                        ;;
                    "sig_pend")
                        search_string="^SigPnd:"
                        ;;
                    "sig_ign")
                        search_string="^SigIgn:"
                        ;;
                    "sig_queue")
                        search_string="^ShdPnd:"
                        ;;
                    "sig_blk")
                        search_string="^SigBlk:"
                        ;;
                    "code_size")
                        search_string="^VmExe:"
                        ;;
                    "data_size")
                        search_string="^VmData:"
                        ;;
                    "stack_size")
                        search_string="^VmStk:"
                        ;;
                    "dynamic_size")
                        search_string="^VmSize:"
                        ;;
                    "n_threads")
                        search_string="^Threads:"
                        ;;
                    "uid")
                        search_string="^Uid:"
                        ;;
                    "gid")
                        search_string="^Gid:"
                        ;;
                    "euid")
                        search_string="^Uid:"
                        result_column=3
                        ;;
                    "egid")
                        search_string="^Gid:"
                        result_column=3
                        ;;
                    "suid")
                        search_string="^Uid:"
                        result_column=4
                        ;;
                    "sgid")
                        search_string="^Gid:"
                        result_column=4
                        ;;
                    "runmask")
                        search_string="^Cpus_allowed:"
                        ;;
                esac
                result=`echo "$sh_proc_cache_status_file" | grep $search_string | cut -f $result_column`

                #Check for a memory field and format appropriately
                case "$item" in
                    "code_size" | "data_size" | "stack_size" | "dynamic_size")
                        local kB=`echo $result | tr -d [:alpha:][:blank:]`
                        if [ ${#result} -gt 0 ]; then
                            sh_proc_format_memory "$kB"
                            result=$sh_proc_format_memory_out
                        fi
                        ;;
                esac

            else
                ret_code=1 
            fi
            ;;
        
        #stat file items
        "pgroup" | "session" | "utime" | "stime" | "cutime" | "cstime" \
        | "priority" | "starttime" | "last_cpu" | "sutime")
            sh_proc_check_and_refresh_stat_cache $pid
            if [ $? -eq 0 ]; then
                local column=1
                case "$item" in
                    "pgroup")
                        column=5
                        ;;
                    "session")
                        column=6
                        ;;
                    "utime")
                        column=14
                        ;;
                    "stime")
                        column=15
                        ;;
                    "cutime")
                        column=16
                        ;;
                    "cstime")
                        column=17
                        ;;
                    "priority")
                        column=18
                        ;;
                    "starttime")
                        column=22
                        ;;
                    "last_cpu")
                        column=39
                        ;;
                esac
                result=`echo "$sh_proc_cache_stat_file" | cut -d' ' -f $column`
                
                if [ "$item" == "sutime" ]; then
                    local utime=`echo "$sh_proc_cache_stat_file" | cut -d' ' -f 14`
                    local stime=`echo "$sh_proc_cache_stat_file" | cut -d' ' -f 15`
                    let result=utime+stime
                fi

                #Check for a time field and format appropriately
                case "$item" in
                    "utime" | "stime" | "cutime"  | "cstime" | "sutime")
                        sh_proc_format_time $result
                        result=$sh_proc_format_time_out
                        ;;
                    "starttime")
                        let result=result/sh_proc_time_conversion+sh_proc_time_offset
                        result=`date -d "1970-01-01 UTC $result sec" +"%b %d %H:%M"`
                        ;;
                esac
            else
                ret_code=1
            fi
            ;;

        "args")
            result=`cat $sh_proc_node_proc_path/$pid/cmdline 2>/dev/null | tr '\0' ' '`
            if [ $? -ne 0 ]; then
                ret_code=1
            fi
            ;;

        "env")
            result=`cat $sh_proc_node_proc_path/$pid/environ 2>/dev/null | tr '\0' ' '`
            if [ $? -ne 0 ]; then
                ret_code=1
            fi
            ;;  

        "long_name" | "name"[0-9]* | "basename"[0-9]*)
            result=`readlink $sh_proc_node_proc_path/$pid/exe 2>/dev/null`
            if [ $? -ne 0 ]; then
                sh_proc_check_and_refresh_status_cache $pid
                if [ $? -eq 0 ]; then
                    result=`echo "$sh_proc_cache_status_file" | grep ^Name: | cut -f 2`
                else
                    ret_code=1
                fi
            else
                if [[ $item =~ ^basename[0-9]* ]]; then
                    result=`basename $result`
                fi
            fi

            if [ $ret_code -eq 0 ]; then
                #truncate appropriately
                if [[ $item =~ name[0-9]+$ ]]; then
                    local length=`echo $item | tr -d [:alpha:]`
                    if [ ${#result} -gt $length ]; then
                        result=${result:${#result}-$length:${#result}}
                    fi
                fi
            fi
            ;; 

        "thread_state")
            sh_proc_check_and_refresh_stat_cache $pid
            if [ $? -eq 0 ]; then
                result=`echo "$sh_proc_cache_stat_file" | cut -d' ' -f 3`
            else
                ret_code=1
            fi
            #TODO - Reporting other states depends on external requirements
            ;;

        "time_in_state")
            #TODO - Depends on external requirements
            result="nnnn:nn:nn:nnnn"
            ;;

        "blocked_on")
            #TODO - Depends on external requirements
            result="unknown"
            ;;
    esac
    
    if [ $ret_code -eq "0" ]; then
        sh_proc_get_item_out="$result"
    else
        sh_proc_get_item_out=""
    fi

    if [ $ret_code -ne 0 ]; then
        sh_proc_get_item_error=1
    fi

    return $ret_code
}
