Adobe ColdFusion for Linux (Or how not to write shell scripts)

Working in DevOps, I've come to hate ColdFusion. What with it's security issues, the filesystem odities (suid for image files? really?), and other idiosyncracies. At least the init.d script for coldfusion was solid.

Or so I thought.

Let's take a dive in, and see what it does, shall we?

Let's start with the top.

...
SPACE=" "  
...

Hm...this is odd. Why do we need to define the space?

for word in $JVM_ARGS  
do  
 if [ "$word" != "-Xdebug" ]; then
        if [ ${word:0:9} != "-Xrunjdwp" ]; then
            JVM_ARGS_NODEBUG="$JVM_ARGS_NODEBUG$SPACE$word"
        fi
fi  
done  

Wait wait wait...
$JVM_ARGS_NODEBUG$SPACE$word ?

Why not just do this?

for word in $JVM_ARGS  
do  
 if [ "$word" != "-Xdebug" ]; then
        if [ ${word:0:9} != "-Xrunjdwp" ]; then
            JVM_ARGS_NODEBUG="$JVM_ARGS_NODEBUG $word"
        fi
fi  
done  

Ok. Maybe they have some odd reason. Moving on...

arr=$(echo $CF_DIR | tr "/" "\n")  
for x in $arr  
do  
    CF_INSTANCE_NAME="$x"
done  

Erm...Hmmm. Ok. So, this, what? Iterates through a value, replacing "/" with a new line, then keeps overwriting CF_INSTANCE_NAME until it gets to the last one?

Let's test this out.

The real value for CF_DIR in my environment is /opt/coldfusion10/cfusion

user@host:~# CF_DIR='/opt/coldfusion10/cfusion'  
user@host:~# echo $CF_DIR | tr "/" "\n"

opt  
coldfusion10  
cfusion  
user@host:~#  

Hm... Why not use rev and cut?

user@host:~# CF_DIR='/opt/coldfusion10/cfusion'  
user@host:~# echo $CF_DIR|rev|cut -d'/' -f1|rev  
cfusion  
user@host:~#  

So, we would get the same results, but a little less...stupid in how we got there.

Now, less stupid isn't "smart". What is smart is basename

user@host:~# CF_DIR='/opt/coldfusion10/cfusion'  
user@host:~# basename $CF_DIR  
cfusion  

So, we could use this instead:

CF_INSTANCE_NAME=$(basename $CF_DIR)  

This is the smart way.

Moving on. What do we have next (We just got past line 42 of the init script)?

This isn't wrong, but will be needed later for you to understand what it is doing:

cfrunning() {  
    IS_RUNNING="false"
    if [ $OS = "Solaris" ]; then
        # The comm output on Solaris includes the full path
        $PSCMD | fgrep $CF_DIR |fgrep java > /dev/null 2>&1
    else
        # other platforms have only the executable name
        $PSCMD | fgrep java | grep -v grep | grep com.adobe.coldfusion.bootstrap.Bootstrap | grep "start" | grep -w $CF_DIR > /dev/null 2>&1
    fi
    if [ $? -eq 0 ]; then
        IS_RUNNING="true"
    fi
}

This function sets a variable (IS_RUNNING) to true if it is running, or false if it isn't.
Let's move on to the stop service function cfstop():

cfstop() {

    cfrunning

    if [ "$IS_RUNNING" = "false" ]; then
        echo "$VERSION server instance named $CF_INSTANCE_NAME does not seem to be currently running"
        return
    fi

    echo "Stopping $VERSION server instance named $CF_INSTANCE_NAME, please wait"

    eval $CFSTOP

    sleep 10

    cfrunning

    if [ "$IS_RUNNING" = "true" ]; then
        if [ $OS = "Solaris" -a ! -f "/usr/ucb/ps" ]; then
            $PSCMD | fgrep java | fgrep $CF_DIR | awk '{print $2}' | xargs kill -9 > /dev/null 2>&1
        else
            # other platforms have only the executable name
            $PSCMD | fgrep java | grep -v grep | grep com.adobe.coldfusion.bootstrap.Bootstrap | grep "start" | grep -w $CF_DIR | awk '{print $2}' | sudo xargs kill -9 > /dev/null 2>&1
        fi
        sleep 2
    fi

    cfrunning

    if [ "$IS_RUNNING" = "true" ]; then
        echo "$VERSION server is not responding. You have to forcefully stop the following ColdFusion PIDs manually: "
        if [ $OS = "Solaris" ]; then
            $PSCMD | fgrep java | fgrep $CF_DIR | awk '{print $2}'
        else
            # other platforms have only the executable name
            $PSCMD | fgrep java | grep -v grep | grep com.adobe.coldfusion.bootstrap.Bootstrap | grep "start" | grep -w $CF_DIR | awk '{print $2}'
        fi
        echo exiting
        exit 1
    fi

    echo "$VERSION server instance named $CF_INSTANCE_NAME has been stopped"
}

Without going into too much detail, this is just wrong. Using an ion cannon to kill an ant is a bit overkill. Why not see if it can die gracefully first?

Let's see the right(er) way of doing things:

noisySleep(){  
  notice=$1
  timer=$2
  sleep_count=0
  echo $notice
  while [ $sleep_count -lt $timer ]; do
    echo -n '.'
    sleep 1
    sleep_count=$(expr $sleep_count + 1)
  done
  echo ""
}
cfstop() {  
  cfrunning
  stop_attempt=0
  while [ "$IS_RUNNING" == "true" ]; do
    pid_nums=$($PSCMD | fgrep java | grep -v grep | grep com.adobe.coldfusion.bootstrap.Bootstrap | grep "start" | grep -w $CF_DIR | awk '{print $2}')
    case $stop_attempt in
      0)
        su $RUNTIME_USER -c "cd $CF_DIR/bin; $JAVA_EXECUTABLE -classpath $CLASSPATH $JVM_ARGS_NODEBUG com.adobe.coldfusion.bootstrap.Bootstrap -stop"
        stop_attempt=1
        noisySleep "Waiting 5 seconds to see if killing it via the -stop command to the Bootstrap works" 5
        cfrunning
      ;;
      1)
        for i in "$pid_nums"; do kill -15 $i;done
        stop_attempt=2
        noisySleep "Waiting 5 seconds to see if killing it with kill -15 works." 5
        cfrunning
      ;;
      2)
        for i in "$pid_nums"; do kill -2 $i;done
        stop_attempt=3
        noisySleep "Waiting 5 seconds to see if killing it with kill -2 works." 5
        cfrunning
      ;;
      3)
        for i in "$pid_nums"; do kill -1 $i;done
        stop_attempt=4
        noisySleep "Waiting 5 seconds to see if killing it with kill -1 works." 5
        cfrunning
      ;;
      4)
        for i in "$pid_nums"; do kill -9 $i;done
        stop_attempt=5
        noisySleep "Waiting 5 seconds to see if killing it with kill -9 works." 5
        cfrunning
      ;;
      5)
        cfrunning
        if [ "$IS_RUNNING" == "true" ]; then
          echo "Failed to stop ColdFusion.  I'm giving up."
          exit 1
        fi
      ;;
    esac
  done
    echo "$VERSION server instance named $CF_INSTANCE_NAME has been stopped"
}

This will try to kill it the "right way" first -- via a command designed to stop it. If that fails, it goes to a kill -15, then to kill -2, then kill -1 before murdering it with a kill -9.

While poking through this, I noticed a file being source'd. /opt/coldfusion10/cfusion/bin/parseargs.

The first thing that hit me:

jvm_config="jvm.config"  
ARG=$1  
if [ -z "$ARG" ]; then  
    jvm_config="jvm.config"
else  
    jvm_config=$1
fi

if [ -f $jvm_config ]; then  
     jvm_config=$jvm_config
else  
     jvm_config=$JVMCONFIG
fi  

Let's walk through this, shall we?

It first sets the variable jvm_config to point to the file jvm.config (in /opt/coldfusion/cfusion/bin, I assume). It then next sets ARG variable to equal to $1 (the first value passed to the script after the script name itself). Next, it states "If ARG is empty, set jvm_config to equal jvm.config again (I say "again" because they set that variable 2 lines before, to the exact same thing).
It then says "if it's not empty, set it to $1, and ignore the variable we set because stuff.

Now, in the init script, before it sources this file in, it has this line:

JVMCONFIG=$CF_DIR/bin/jvm.config  

so, it next states "if the file that is defined by the variable $jvm_config exists, use it. If not, use what is defined in the init script.

I don't know about you, but I would prefer to have it use my init settings over the included settings. Following that logic, this would make more sense:

if [ -z "$1" ]; then  
    if [ -z "$JVMCONFIG" ]; then
        jvm_config="jvm.config"
    else
        jvm_config=$JVMCONFIG
    fi
else  
    jvm_config=$1
fi  
if [ ! -f $jvm_config ]; then  
    echo "File for JVM Config not found"
    exit 1
fi  

What this code does:
If you enter a variable as the JVM Config, it takes that (because you manually supplied it, it assumes you want to override the defaults). If that isn't set, it tries the JVMCONFIG variable, which should be set in the init script. If both of those fail, it falls back to the default which is internally set to "jvm.config".

Basic rules, dudes. CLI variables, then init variables, then local variables. Give the user the chance to set it.

Conclusions

I've not gone over all their scripts, but to say that these are messy would be an understatement. It was obviously cobbled together by people who don't know BASH.

A bit of advice: next time, spend the money, and contract out to someone who knows what they are doing.