Contents |
If you have an application that needs to be started or stopped in a specific way, one that needs to run without a controlling terminal, and especially one that needs to start when the system starts and shut itself down when the system shuts itself down, you need to learn about init scripts. Daemons are a particular type of application that usually can benefit from an init script, but there are other uses for init scripts as well.
In this article we'll learn how to write an init script for a simple application that I use at home called DVArchive. DVArchive is an application that runs on a PC which allows a PC to interface with a DVR like ReplayTV, to store and stream programs. DVArchive usually runs as a rich client application, but on Linux you can also run it in "headless" mode. With an init script for DVArchive, I can turn my home server into a media storage and streaming device. Sweet!
To start out, we need to understand that most init scripts are bash scripts. In fact, pretty much all of the scripts I have seen are bash scripts. Technically, however, they are just programs that invoke other programs in a certain way and conform to certain conventions. I assume they can be written in another scripting language, like Perl, or even in another programming language like C++ if you desire.
However, since most init scripts are bash scripts, your best examples will be in bash. And SUSE Linux comes with a lot of built in commands and facilities to make it easy to create init scripts in bash. That's probably the way you will want to go.
Because most init scripts are written in bash script, they are all available to you in source code form. In fact, SUSE Linux comes with a skeleton script at the location /etc/init.d/skeleton that is thoroughly documented and can be copied to use as a starting point for your init script. And of course you can reference the other scripts for help if you need to.
For instructive purposes, I'll start from scratch. To start, we'll create a script called "dvarchive", which gets created in the /etc/init.d directory with permissions 744.
The first line of the script looks like this:
#!/bin/sh
or
#!/bin/bash
It really doesn't matter since on SUSE Linux /bin/sh is just a symbolic link that points to /bin/bash anyway.
The next line you will want to include is the following:
test -s /etc/rc.status && . /etc/rc.status && rc_reset
What does this line do?
At the head of your script you can provide an "INIT INFO" section, which is essentially a description in a standardized format of what your service is about. This is used by the YaST runlevel editor.
# Provides: dvarchive # Required-Start: network # Required-Stop: # Default-Start: 3 5 # Default-Stop: 0 1 2 6 # Short-Description: Starts the dvarchive daemon # Description: Starts the dvarchive daemon, attached to a virtual # frame buffer.
Many init scripts at this point will perform a few other more customized initialization steps, such as defining variables for paths and values to be used later, finding, reading, and validating configuration information and files, verifying the existence and executability of the application that pertains to this init script, etc.
In my case, I'm going to set up a few variables for convenience sake.
XVFB_PATH=/usr/X11R6/bin
XVFB_APP=Xvfb
XVFB=${XVFB_PATH}/${XVFB_APP}
DSP=7
XFONTS=/usr/X11R6/lib/X11/fonts/misc
XVFB_CMD="${XVFB} :${DSP} -ac -fp ${XFONTS}"
JAVA=/usr/bin/java
DVARCHIVE_PATH=/opt/dvarchive
DVARCHIVE_APP=DVArchive
DVARCHIVE_JAR=${DVARCHIVE_APP}.jar
DVARCHIVE_CMD="${JAVA} -jar ${DVARCHIVE_PATH}/${DVARCHIVE_JAR} --daemon"
DVARCHIVE_PIDFILE=/var/run/dvarchive.pid
DVARCHIVE_XVFB_PIDFILE=/var/run/dvarchive_xvfb.pid
DVARCHIVE_LOG=/var/log/dvarchive
One quirk with DVArchive is that, in order to run it without a terminal, you need to set up Xvfb - the X virtual frame buffer. This acts like a terminal for the application to display to without actually requiring a terminal. To run DVArchive we'll need to start Xvfb along with the DVArchive app itself.
Everything else should be pretty much self-evident.
Before we get any farther, we can do a few checks to see whether we can have success. I wrote a function called test_for_app that can test for the existence of a specific executable or file, and then display an error message if it isn't found. Then I use this to check for Xvfb, java, and DVArchive. For example:
test_for_app ()
{
app_found=0
if [ "f" = "$2" ] && [ -f $1 ]; then
app_found=1
elif [ "x" = "$2" ] && [ -x $1 ]; then
app_found=1
fi
if [ 0 = $app_found ]; then
echo -n "Warning: Couldn't find $1"
if [ "$1" = "stop" ] || [ "$1" = "xvfb_stop" ]; then
rc_failed 0
else
rc_failed 5
fi
rc_status -v
rc_exit
fi
}
test_for_app ${XVFB} x
test_for_app ${JAVA} x
test_for_app ${DVARCHIVE_PATH}/${DVARCHIVE_JAR} f
Here we are just checking to see if the Xvfb application, the java applicatoin, and the DVArchive application are installed. If they are not installed we can abort. Other checks are also possible here - for example, if there is a configuration file, we could check for that.
Notice the sequence of steps if the tests fail. We first print out a somewhat descriptive error message. rc_failed 5 sets the status to a failed state with a return code of 5. rc_status -v prints out the currently reported state. rc_exit causes the script to exit with the return code.
We'll use this sequence frequently.
Here's an example of the output displayed if the DVArchive app isn't found:
Couldn't find /opt/dvarchive/DVArchive.jar failed
In addition, on my computer the word "failed" is displayed in red. This allows my error messages to give the same appearance as other startup applications when it succeeds or fails.
Now we get into the meat of the script. Usually the script will be invokved with a single argument, which tells the script what we want to do - start, stop, etc. You can put what you want in here, but some basics that you'll want to consider are the following:
Many applications include other options, like restart. restart in some cases is just stop followed by start, but in other cases, the script might just send a signal (like SIGHUP) to the application telling it to reload the configuration. Alternatively, you may wish to implement restart as stop followed by start, and implement a reload option in addition that will send SIGHUP.
Regardless of what options you provide, you should make it easy for your users to learn how to invoke the script. This is best done with a usage or help option, or by default if no other option is selected. Let's do that first.
First, my usage function:
usage ()
{
echo ""
echo "Usage: $0 <command>"
echo ""
echo "where <command> is one of the following:"
echo " start - start DVArchive if it is not running"
echo " stop - stop DVArchive if it is running"
echo " status - report whether DVArchive is running"
echo " restart - stop and restart DVArchive"
echo " usage, help - print this message"
}
Option processing is usually implemented as a simple case statement. The first cases I'll handle are usage, help, and * - the default case.
case "$1" in
usage|help)
usage
rc_exit
;;
*)
usage
rc_failed 1
rc_status -v
rc_exit
esac
If I run /etc/init.d/dvarchive usage (or help), I will see the usage information printed out:
Usage: /etc/init.d/dvarchive <command>
where <command> is one of the following:
start - start DVArchive if not running
stop - stop DVArchive if running
status - report whether DVArchive is running
restart - stop and restart DVArchive
usage, help - print this message
I can assume that if the user specifies the usage or help option, they wanted the usage message. If another option is provided that I don't know about, or no option is provided, I don't know what the user wanted. So I mark this as a failed execution - we exit with a failure status and show the failure message. Everything else is the same.
Usage: /etc/init.d/dvarchive <command>
where <command> is one of the following:
start - start DVArchive if not running
stop - stop DVArchive if running
status - report whether DVArchive is running
restart - stop and restart DVArchive
usage, help - print this message
failed
As we said earlier, the primary purposes of this script are to start, stop, and get the status of an application. There are three applications provided on SUSE Linux to faciliate this:
You'll definitely want to check the man pages for details on these commands. We'll be using them in our script.
Next lets implement the start option. Starting the application uses the following logic sequence:
This is a bit more complicated in our scenario, since we have to also start a virtual frame buffer session for our application. Here's how I did it:
case "$1" in
start|xvfb_start)
checkproc -p ${DVARCHIVE_XVFB_PIDFILE} ${XVFB}
case $? in
0)
echo "${XVFB_APP} already running"
;;
1)
echo "Found stale pidfile for ${XVFB_APP} - unclean shutdown?"
rm ${DVARCHIVE_XVFB_PIDFILE}
;;
3)
# not running - ok
;;
*)
echo "Check for ${XVFB_APP} failed"
rc_failed
rc_status -v1
rc_exit
esac
export DISPLAY=":${DSP}.0"
echo -n "Starting ${XVFB_APP} for ${DVARCHIVE_APP} "
startproc -f -p ${DVARCHIVE_XVFB_PIDFILE} ${XVFB_CMD} >> ${DVARCHIVE_LOG} 2>&1
if ! [ 0 = $? ]; then
echo -n "(Error - "
case $? in
2)
echo -n "invalid arguments"
;;
4)
echo -n "insufficient permission"
;;
5)
echo -n "no such program"
;;
7)
echo -n "launch failure"
;;
*)
echo -n "unspecified error"
;;
esac
echo -n ")"
rc_failed
rc_status -v1
rc_exit
fi
get_pid_for_cmd `eval echo ${XVFB_CMD} | sed -e 's/ //g'`
if [ 0 = $pid ]; then
echo "Warning - Couldn't obtain PID for ${XVFB_APP} "
else
echo $pid > ${DVARCHIVE_XVFB_PIDFILE}
fi
rc_status -v
# Xvfb started
if [ $1 = "start" ]; then
checkproc -p ${DVARCHIVE_PIDFILE} ${JAVA}
case $? in
0)
echo "${DVARCHIVE_APP} already running"
;;
1)
echo "Found stale pidfile for ${DVARCHIVE_APP} - unclean shutdown?"
rm ${DVARCHIVE_PIDFILE}
;;
3)
# not running - ok
;;
*)
echo "Check for ${DVARCHIVE_APP} failed"
rc_failed
rc_status -v1
rc_exit
esac
echo -n "Starting ${DVARCHIVE_APP}"
echo Y | ${DVARCHIVE_CMD} >> ${DVARCHIVE_LOG} 2>&1 &
if ! [ 0 = $? ]; then
echo "Failed to start ${DVARCHIVE_APP}"
rc_failed
rc_status -v1
rc_exit
fi
get_pid_for_cmd `eval echo ${DVARCHIVE_CMD} | sed -e 's/ //g'`
if [ 0 = $pid ]; then
echo "Warning - Couldn't obtain PID for ${DVARCHIVE_APP} "
else
echo $pid > ${DVARCHIVE_PIDFILE}
fi
rc_status -v
fi
;;
This is pretty lengthy, but really not that hard to understand. The first thing we are going to do is check to see whether the Xvfb application is running, and based on the return value from checkproc, take appropriate steps.
If there were no errors, we continue on to run the application itself. We need to export a value for the DISPLAY variable which will be the same as the display we assign to Xvfb. We use startproc to start Xvfb, and if there are any errors in trying to start it, we report those errors to the user.
Once the process has started, we need to obtain the process ID to write it to the pid file. A function I wrote, called get_pid_for_cmd, does this for us (we'll talk about this function later).
After we are done starting Xvfb, we check to see if the user wanted to start Xvfb only, or if they wanted to start everything. Usually we will start everything; this capability was added primarily to ease debugging. Essentially the same sequence of steps is followed to start DVArchive.
There is one exception - I didn't use startproc to start DVArchive. When you run DVArchive with the --daemon option, it displays a text dialog of the EULA that I've read and agreed to many times, but you have to manually agree to it each time. Unfortunately, DVArchive doesn't remember that I accepted the EULA, nor does it give me an option to bypass the agreement. In order to make this work, I echo Y and pipe that into the command. This isn't as good as what startproc does for me but it will have to do.
If you looked, you may have noticed that the command that I pass into checkproc isn't the same as the one executed (either directly or via startproc). checkproc only looks for the full path to the command run, without arguments. This is why it is important that we specify the pid file to checkproc, especially when checking for the DVArchive process. The command is /usr/bin/java; it is not unlikely that there could be other running processes with the same command path. Specifying the pid file tells it to only consider any applications that are associated with that pid file.
Whew, that was a bit of work. Fortunately, starting the process is usually the part that requires the most work. Our next step is to be able to stop the process if it is running.
Stopping a process is fairly straightforward. The basic line of logic is the following:
Here's the code:
stop|xvfb_stop)
if [ $1 = "stop" ]; then
checkproc -p ${DVARCHIVE_PIDFILE} ${JAVA} || echo -n "(Warning: not running)"
killproc -p ${DVARCHIVE_PIDFILE} -t 10 ${JAVA}
rc_status -v
fi
echo -n "Shutting down ${XVFB_APP} "
checkproc -p ${DVARCHIVE_XVFB_PIDFILE} ${XVFB} || echo -n "(Warning: not running)"
killproc -p ${DVARCHIVE_XVFB_PIDFILE} -t 10 ${XVFB}
rc_status -v
;;
This is pretty straightforward. First, we stop DVArchive, then we stop Xvfb. Checking for '"1 = "stop"' allows us to stop only Xvfb if we want (again, primarily useful for debugging).
By the way, killproc isn't quite as brutal as it sounds. It first sends a SIGTERM to the process, waits for it to die, then after a few seconds it will send a SIGKILL if necessary. By specifying the pid file to killproc, we make sure we only terminate the process that is associated with the pid file. This should be the one we started earlier.
Now that I can stop and start, implementing restart is a piece of cake:
restart)
$0 stop &>/dev/null
$0 start &>/dev/null
rc_status -v
;;
I simply invoke the same script to stop and start in succession. This works for my case. Some applications can be more gracefully restarted; in some cases, what is really required is to re-initialize the process. Whatever works for you.
The last thing to do is to allow the user to query the status of the applicaitons. This is pretty easy to do, since all we have to do is run checkproc and report the results of this command.
status|xvfb_status)
app_chk ${XVFB_APP} ${DVARCHIVE_XVFB_PIDFILE} ${XVFB}
# Xvfb is running
if [ $1 = "status" ]; then
app_chk ${DVARCHIVE_APP} ${DVARCHIVE_PIDFILE} ${JAVA}
fi
;;
I implemented the bulk of the logic for this in another function. I guess it is time to take a look at those functions now.
I wrote two helper functions, get_pid_for_cmd and app_chk. Here's get_pid_for_cmd:
get_pid_for_cmd ()
{
for pid in `ls -t /proc`; do
if [ -d /proc/$pid ] && [ -f /proc/$pid/cmdline ]; then
if [ "$1" = "$(</proc/$pid/cmdline)" ]; then
return
fi
fi
done
pid=0
}
This function simply looks through /proc to find a process that matches the supplied command. By listing the entries in /proc with the -t option, we get them sorted by order of last modification, most recent first, which should make this search pretty fast. If the entry in /proc is a directory (not all are, but all the entries with the name of a process id are) and if the entry has a file named cmdline (which ours should have), then we simply compare the contents of cmdline to the command that is passed into our function as $1. If there is a match, we return and $pid is equal to the process id of the matching process. If no match is found, $pid is 0, which indicates an error.
If this is unfamiliar to you, take a look at the /proc filesystem and familiarize yourself with how it works. It is pretty cool.
Here's app_chk:
app_chk ()
{
app=$1
pidfile=$2
cmd=$3
echo -n "Checking for $app: "
checkproc -p $pidfile $cmd
case $? in
0)
echo -n "(running)"
rc_failed 0
;;
1)
echo "(not running)"
echo -n "Warning - PID file found"
rc_failed 3
;;
3)
echo -n "(not running)"
rc_failed 1
;;
*)
echo "(unknown)"
echo "Warning - Couldn't get status"
rc_failed 1
rc_status -v1
rc_exit
esac
rc_status -v
}
This function is mostly just a time-saver. It prints a "Checking for" message, then runs checkproc. An appropriate status message is printed based upon the return value from checkproc.
Here's what we ended up with, top to bottom:
#!/bin/sh
# Provides: dvarchive
# Required-Start: network
# Required-Stop:
# Default-Start: 3 5
# Default-Stop: 0 1 2 6
# Short-Description: Starts the dvarchive daemon
# Description: Starts the dvarchive daemon, attached to a virtual
# frame buffer.
XVFB_PATH=/usr/X11R6/bin
XVFB_APP=Xvfb
XVFB=${XVFB_PATH}/${XVFB_APP}
DSP=7
XFONTS=/usr/X11R6/lib/X11/fonts/misc
XVFB_CMD="${XVFB} :${DSP} -ac -fp ${XFONTS}"
JAVA=/usr/bin/java
DVARCHIVE_PATH=/opt/dvarchive
DVARCHIVE_APP=DVArchive
DVARCHIVE_JAR=${DVARCHIVE_APP}.jar
DVARCHIVE_CMD="${JAVA} -jar ${DVARCHIVE_PATH}/${DVARCHIVE_JAR} --daemon"
DVARCHIVE_PIDFILE=/var/run/dvarchive.pid
DVARCHIVE_XVFB_PIDFILE=/var/run/dvarchive_xvfb.pid
DVARCHIVE_LOG=/var/log/dvarchive
test -s /etc/rc.status && . /etc/rc.status && rc_reset
usage ()
{
echo ""
echo "Usage: $0 <command> "
echo ""
echo "where <command> is one of the following:"
echo " start - start $DVARCHIVE_APP if not running"
echo " stop - stop $DVARCHIVE_APP if running"
echo " status - report whether $DVARCHIVE_APP is running"
echo " restart - stop and restart $DVARCHIVE_APP"
echo " usage, help - print this message"
}
get_pid_for_cmd ()
{
for pid in `ls -t /proc`; do
if [ -d /proc/$pid ] && [ -f /proc/$pid/cmdline ]; then
if [ "$1" = "$(</proc/$pid/cmdline)" ]; then
return
fi
fi
done
pid=0
}
app_chk ()
{
app=$1
pidfile=$2
cmd=$3
echo -n "Checking for $app: "
checkproc -p $pidfile $cmd
case $? in
0)
echo -n "(running)"
rc_failed 0
;;
1)
echo "(not running)"
echo -n "Warning - PID file found"
rc_failed 3
;;
3)
echo -n "(not running)"
rc_failed 1
;;
*)
echo "(unknown)"
echo "Warning - Couldn't get status"
rc_failed 1
rc_status -v1
rc_exit
esac
rc_status -v
}
test_for_app ()
{
app_found=0
if [ "f" = "$2" ] && [ -f $1 ]; then
app_found=1
elif [ "x" = "$2" ] && [ -x $1 ]; then
app_found=1
fi
if [ 0 = $app_found ]; then
echo -n "Warning: Couldn't find $1"
if [ "$1" = "stop" ] || [ "$1" = "xvfb_stop" ]; then
rc_failed 0
else
rc_failed 5
fi
rc_status -v
rc_exit
fi
}
test_for_app ${XVFB} x
test_for_app ${JAVA} x
test_for_app ${DVARCHIVE_PATH}/${DVARCHIVE_JAR} f
case "$1" in
start|xvfb_start)
checkproc -p ${DVARCHIVE_XVFB_PIDFILE} ${XVFB}
case $? in
0)
echo "${XVFB_APP} already running"
;;
1)
echo "Found stale pidfile for ${XVFB_APP} - unclean shutdown?"
rm ${DVARCHIVE_XVFB_PIDFILE}
;;
3)
# not running - ok
;;
*)
echo "Check for ${XVFB_APP} failed"
rc_failed
rc_status -v1
rc_exit
esac
export DISPLAY=":${DSP}.0"
echo -n "Starting ${XVFB_APP} for ${DVARCHIVE_APP} "
startproc -f -p ${DVARCHIVE_XVFB_PIDFILE} ${XVFB_CMD} >> ${DVARCHIVE_LOG} 2>&1
if ! [ 0 = $? ]; then
echo -n "(Error - "
case $? in
2)
echo -n "invalid arguments"
;;
4)
echo -n "insufficient permission"
;;
5)
echo -n "no such program"
;;
7)
echo -n "launch failure"
;;
*)
echo -n "unspecified error"
;;
esac
echo -n ")"
rc_failed
rc_status -v1
rc_exit
fi
get_pid_for_cmd `eval echo ${XVFB_CMD} | sed -e 's/ //g'`
if [ 0 = $pid ]; then
echo "Warning - Couldn't obtain PID for ${XVFB_APP} "
else
echo $pid > ${DVARCHIVE_XVFB_PIDFILE}
fi
rc_status -v
# Xvfb started
if [ $1 = "start" ]; then
checkproc -p ${DVARCHIVE_PIDFILE} ${JAVA}
case $? in
0)
echo "${DVARCHIVE_APP} already running"
;;
1)
echo "Found stale pidfile for ${DVARCHIVE_APP} - unclean shutdown?"
rm ${DVARCHIVE_PIDFILE}
;;
3)
# not running - ok
;;
*)
echo "Check for ${DVARCHIVE_APP} failed"
rc_failed
rc_status -v1
rc_exit
esac
echo -n "Starting ${DVARCHIVE_APP}"
echo Y | ${DVARCHIVE_CMD} >> ${DVARCHIVE_LOG} 2>&1 &
if ! [ 0 = $? ]; then
echo "Failed to start ${DVARCHIVE_APP}"
rc_failed
rc_status -v1
rc_exit
fi
get_pid_for_cmd `eval echo ${DVARCHIVE_CMD} | sed -e 's/ //g'`
if [ 0 = $pid ]; then
echo "Warning - Couldn't obtain PID for ${DVARCHIVE_APP} "
else
echo $pid > ${DVARCHIVE_PIDFILE}
fi
rc_status -v
fi
;;
stop|xvfb_stop)
if [ $1 = "stop" ]; then
checkproc -p ${DVARCHIVE_PIDFILE} ${JAVA} || echo -n "(Warning: not running)"
killproc -p ${DVARCHIVE_PIDFILE} -t 10 ${JAVA}
rc_status -v
fi
echo -n "Shutting down ${XVFB_APP} "
checkproc -p ${DVARCHIVE_XVFB_PIDFILE} ${XVFB} || echo -n "(Warning: not running)"
killproc -p ${DVARCHIVE_XVFB_PIDFILE} -t 10 ${XVFB}
rc_status -v
;;
status|xvfb_status)
app_chk ${XVFB_APP} ${DVARCHIVE_XVFB_PIDFILE} ${XVFB}
# Xvfb is running
if [ $1 = "status" ]; then
app_chk ${DVARCHIVE_APP} ${DVARCHIVE_PIDFILE} ${JAVA}
fi
;;
restart)
$0 stop &>/dev/null
$0 start &>/dev/null
rc_status -v
;;
usage|help)
usage
rc_exit
;;
*)
usage
rc_failed 1
rc_status -v
rc_exit
esac
Although this script is specific to my purpose, it is pretty similar to other init scripts for other purposes.
To summarize: If you need to write an init script:
Good luck!
See Also:
Creating Custom init Scripts (Novell Cool Solutions)
© 2009 Novell, Inc. All Rights Reserved.