1

I'm trying to write a bash script that uses a function to spawn several applications in the background, and then store their PIDs in an associative array with the application name as the key. Running into a problem as I'm using a for loop to loop through another array that has the paths of the applications. After all the applications are spawned, I'll echo all the values of the array, but only the PID of the last application spawned is present. This causes several issues down the line in the rest of the script.

I've found this thread about why this might be happening, but I'm having trouble implementing the fix for my situation. Could someone please point me in the right direction?

proc_name=( application1 application2 application3 )
declare -A proc_pid

spawn_application () {
if [ $# -ne 1 ] || [ ! -x $1 ]
then
    echo "$FUNCNAME: usage $FUNCNAME <proc_name>"
    if [ $# -ne 1]
    then
        echo "$FUNCNAME: invalid amount of arguments, expected 1 received $#"
    elif [ ! -x $1 ]
    then
        echo "$FUNCNAME: $1 is not an executable file"
    fi
    exit 1
else
    ./$1 $host_ipaddr &
    proc_pid[$1]=$!
    echo "$0: starting $1 with PID: ${proc_pid[$1]}"
fi
}

for proc in ${proc_name[@]}
do
    spawn_application $proc
done
echo ${proc_pid[@]}

Here's the full script:

#!/bin/bash
#####################
#     Functions     #
#####################

# init - deletes all existing _metrics.csv files, populates array of executables, initializes map of proc_name:pid
init () {
  rm ./*_metrics.csv >/dev/null 2>&1
  proc_name=( application1 application2 application3 )
  declare -A proc_pid
}

# spawn_application - takes a proc_name, checks that it is executable, and then runs the executable in the background
spawn_application () {
  if [ $# -ne 1 ] || [ ! -x "$1" ]
  then
    echo "$FUNCNAME: usage $FUNCNAME <proc_name>"
    if [ $# -ne 1 ]
    then
      echo "$FUNCNAME: invalid amount of arguments, expected 1 received $#"
    elif [ ! -x "$1" ]
    then
      echo "$FUNCNAME: $1 is not an executable file"
    fi
    exit 1
  else
    ./"$1" "$host_ipaddr" &
    #proc_pid[$1]=$!
    #echo "$0: starting $1 with PID: ${proc_pid[$1]}"
  fi
}

# collect_proc_metrics - takes a proc_name and returns the elapsed time in seconds, cpu percentage, and memory percentage, passes these values to write_proc_metrics
collect_proc_metrics () {
  if [ $# -ne 1 ]
  then
    echo "$FUNCNAME: usage $FUNCNAME <proc_name>"
    if [ $# -ne 1 ]
    then
      echo "$FUNCNAME: invalid amount of arguments, expected 1 received $#"
    fi
    exit 1
  else
    read -r seconds cpu memory <<<"$(ps -q ${proc_pid["$1"]} -eo etimes=,%cpu=,%mem=)"
    write_proc_metrics "$1" "$seconds" "$cpu" "$memory" &
fi
}

# collect_sys_metrics - takes no arguments, obtains the rx and tx rates, disk writes, and available disk space. Uses the last set elapsed time in seconds from the collect_proc_metrics. Automatically passes these values to write_sys_metrics
collect_sys_metrics () {
  read -r rx tx <<<"$(ifstat en* | sed -n 4p | tr -s ' ' | awk '{print $7, $9}')"
  disk_writes=$(iostat sda -k | tail -n 2 | tr -s ' ' | cut -d' ' -f4)
  available_disk_space=$(df /dev/mapper/centos-root --output=avail --block-size=1M | tail -n 1)
  write_sys_metrics "$seconds" "$rx" "$tx" "$disk_writes" "$available_disk_space" &
}

# write_proc_metrics - takes the elapsed time in seconds, cpu percentage, and memory percentage and appends them to a .csv file.
write_proc_metrics () {
if [ $# -ne 4 ]
then
  echo "$FUNCNAME: usage $FUNCNAME <proc_name> <seconds> <cpu> <memory>"
  echo "$FUNCNAME: invalid amount of arguments, expected 4 received $#"
else
  local metricsout="$1_metrics.csv"
  if [ ! -f "$metricsout" ]
  then
    echo "SECS,CPU%,MEM%" > "$metricsout"
  fi
  echo "$2,$3,$4" >> "$metricsout"
fi
}

# write_sys_metrics - takes the elapsed time in seconds, RX and TX rates, disk writes, and disk usage and appends them to a .csv file. 
write_sys_metrics () {
if [ $# -ne 5 ]
then
  echo "$FUNCNAME: usage $FUNCNAME <seconds> <rx> <tx> <disk_writes> <available_disk_space>"
  echo "$FUNCNAME: invalid amount of arguments, expected 5 received $#"
else
  local metricsout="system_metrics.csv"
  if [ ! -f $metricsout ]
  then
    echo "SECS,RX,TX,kB_wrtn/s,MB_avail" > $metricsout
  fi
  echo "$1,$2,$3,$4,$5" >> system_metrics.csv
fi
}

# cleanup - loops through the proc_names in the proc_name array and kills their associated PID with a -9 flag.
cleanup () {
  pkill -f application >/dev/null 2>&1
  pkill -f ifstat >/dev/null 2>&1
  #for proc in ${proc_name[@]}
  #do
  #  kill -9 ${proc_pid[$proc]}
  #done
}
trap cleanup EXIT

################
#     Main     #
################
if [ $# -ne 1 ]
then
  echo "$0: usage $0 <host_ipaddr>"
  echo "$0: invalid amount of arguments, expected 1 received $#"
else
  init
  host_ipaddr=$1
  for proc in "${proc_name[@]}"
  do
    spawn_application "$proc"
    proc_pid["$proc"]=$!
    echo "$0: starting $proc with PID: ${proc_pid[$proc]}"
  done
  echo "${proc_pid[@]}"

  # Starts the ifstat command as a background process.
  ifstat -d1 -n
  ifstat_pid=$( ps aux | grep "ifstat -d1" | head -1 | tr -s ' ' | cut -f2 -d' ' )
  echo "$0: starting ifstat with PID: $ifstat_pid"
  ifstat -nr

  # Duration is how long to sample metrics in seconds. 900 seconds is 15 minutes
  duration=$(( SECONDS+900 ))
  # Interval is how long to wait between sampling
  interval=5
  while [ $SECONDS -lt $duration ]
  do
    for proc in "${proc_name[@]}"
    do
      collect_proc_metrics "$proc"
    done
    collect_sys_metrics
    sleep $interval
  done
  cleanup
fi

And the output:

[rje6459@localhost Desktop]$ ./apm.sh 129.21.229.64
./apm.sh: starting APM1 with PID: 48163
./apm.sh: starting APM2 with PID: 48164
./apm.sh: starting APM3 with PID: 48165
./apm.sh: starting APM4 with PID: 48166
./apm.sh: starting APM5 with PID: 48167
./apm.sh: starting APM6 with PID: 48168
48168
./apm.sh: starting ifstat with PID: 48170
  • 1
    I'm unable to reproduce this. I copied the code from your file into a script `testscript` in a new directory, ran `touch application{1..3}; chmod +x application{1..3}; bash testscript`, and it ran without errors. The last line was "16041 16042 16040". Can you please provide steps to reproduce and complete, unabbreviated output? – that other guy Oct 19 '18 at 00:33
  • I've added the complete script. – Robert Ellegate Oct 19 '18 at 00:50
  • 1
    Do you have `#!/bin/bash` as the first line of each of your scripts? Are you running under `bash` or regular `sh` (or something else)? If you don't specify `bash`, you might not be getting it :) . **Edit** Also, `for proc in ${proc_name[@]}` should have double-quotes: `for proc in "${proc_name[@]}"`. Try running the script through shellcheck.net and see if it flags anything for you. Same with the `echo` - use `echo "${proc_pid[@]}"` – cxw Oct 19 '18 at 00:59
  • Hi cxw, yes I do have #!/bin/bash in the actual script I just forgot to copy that part when I pasted it here. spellcheck.net is giving me a lot of `Double quote to prevent globbing and word splitting` messages. – Robert Ellegate Oct 19 '18 at 01:09
  • Hey @cxw, I fixed those double quote errors and have updated the script. Still getting similar output. – Robert Ellegate Oct 19 '18 at 02:10
  • 1
    Your output does not appear to correspond to your script. Can you please make a MCVE that you're comfortable sharing in its entirety without doctoring? Debugging is a precision art, and you're making things more difficult by posting code you don't run or output from a different script. See [this question](https://meta.stackoverflow.com/questions/359146/why-should-i-post-complete-errors-why-isnt-the-message-itself-enough) for how tiny and seemingly insignificant details make all the difference. – that other guy Oct 19 '18 at 03:05

0 Answers0