How To Use Apache Commons Daemon

How To Use Apache Commons Daemon

Apache Commons Daemon is a tool provided by Apache community which can help you to manage your Java application as a standard system daemon. In this article I’d like to show you the basic usage of it.

The Apache Commons Daemon consists of two parts: One part is written in Java called commons-daemon, the other part is called jsvc which is written in C.

The commons-daemon part gives you some Java interfaces that you should follow to wrap your server program. The most important interface you need to implement is Daemon.java:

public interface Daemon
{

    /**
     * Initializes this <code>Daemon</code> instance.
     * <p>
     *   This method gets called once the JVM process is created and the
     *   <code>Daemon</code> instance is created thru its empty public
     *   constructor.
     * </p>
     * <p>
     *   Under certain operating systems (typically Unix based operating
     *   systems) and if the native invocation framework is configured to do
     *   so, this method might be called with <i>super-user</i> privileges.
     * </p>
     * <p>
     *   For example, it might be wise to create <code>ServerSocket</code>
     *   instances within the scope of this method, and perform all operations
     *   requiring <i>super-user</i> privileges in the underlying operating
     *   system.
     * </p>
     * <p>
     *   Apart from set up and allocation of native resources, this method
     *   must not start the actual operation of the <code>Daemon</code> (such
     *   as starting threads calling the <code>ServerSocket.accept()</code>
     *   method) as this would impose some serious security hazards. The
     *   start of operation must be performed in the <code>start()</code>
     *   method.
     * </p>
     *
     * @param context A <code>DaemonContext</code> object used to
     * communicate with the container.
     * @exception DaemonInitException An exception that prevented
     * initialization where you want to display a nice message to the user,
     * rather than a stack trace.
     * @exception Exception Any exception preventing a successful
     *                      initialization.
     */
    public void init(DaemonContext context)
        throws DaemonInitException, Exception;

    /**
     * Starts the operation of this <code>Daemon</code> instance. This
     * method is to be invoked by the environment after the init()
     * method has been successfully invoked and possibly the security
     * level of the JVM has been dropped. Implementors of this
     * method are free to start any number of threads, but need to
     * return control after having done that to enable invocation of
     * the stop()-method.
     */
    public void start()
        throws Exception;

    /**
     * Stops the operation of this <code>Daemon</code> instance. Note
     * that the proper place to free any allocated resources such as
     * sockets or file descriptors is in the destroy method, as the
     * container may restart the Daemon by calling start() after
     * stop().
     */
    public void stop()
        throws Exception;

    /**
     * Frees any resources allocated by this daemon such as file
     * descriptors or sockets. This method gets called by the container
     * after stop() has been called, before the JVM exits. The Daemon
     * can not be restarted after this method has been called without a
     * new call to the init() method.
     */
    public void destroy();
}

From the above interface we can see there are several methods related with a server cycle you should implement. How does commons-daemon uses the above interface to manage your Java application? The answers lies in jsvc part. jsvc is written in C, and it provides you three processes1, which are called the Launcher Process, the Controller Process, and the Controlled Process.

The purpose of the launcher process is very straight-forward, which will launch a child process. The child process will be a Java instance and it’s called the controller process.

The controller process will start JVM and start your Java application by properly calling the init and start methods defined in above interface. Then it will wait for standard Linux/UNIX process signals. So afterthen you can send standard Linux/UNIX signals to stop your Java application, and this process will call stop and destroy methods according to the signal you send to this process.

The started Java application process is called the controlled process, it’s your Java application that is running. This process is downgraded to normal user privileges by using system capabilities like setuid and setgid or so.

How can jsvc start JVM? It uses JNI to interact with Java Virtual Machine. The fullname of JNI is called Java Native Interface2, and it is a stanard framework to enables Java code running in a Java Virtual Machine (JVM) to call and be called by native applications. You don’t have to understand the details of jsvc, but if you are interested in the implementation, you can see the usage of JNI_CreateJavaVM() method provided by Java in jsvc source code as a start point to learn about jsvc. In general, the purpose of the jsvc is to manage the lifecycle of your Java application by interacting with Daemon interface on Java side, and you get the ability to start/stop your Java application by using standard system signals scheme.

Now let’s start to learn how to use commons-daemon and jsvc to manage the lifecycle of our Java application. There is an article that roughly describes the steps to integrate your Java application with commons-daemon3 you can check. In this article I’ll provide a more detailed explaination.

Integrate your Java application with commons-daemon

Firstly, write a Java class that implements the Daemon interface:

import org.apache.commons.daemon.Daemon;
import org.apache.commons.daemon.DaemonContext;

public class MyDaemon implements Daemon {

    @Override
    public void init(DaemonContext context) throws Exception {
        System.out.println("MyDaemon init...");
    }

    @Override
    public void start() throws Exception {
        System.out.println("MyDaemon start...");
    }

    @Override
    public void stop() throws Exception {
        System.out.println("MyDaemon stop...");
    }

    @Override
    public void destroy() {
        System.out.println("MyDaemon destroy...");
    }
}

From the above code, we can see the Daemon interfaces defines four methods that controls you application cycle, which are init, start, stop and destroy. And jsvc will call these methods to start/stop your application properly. So it’s your responsibility to implement the above methods properly.

Then we need to compile our Java project properly. I have put above MyDaemon into a Gradle project4, so you can directly check it out and build a jar from it. You can go to the directory of the project, and then run ./gradlew fatJar. It will download the gradle and build the project for you.

After building it successfully, you can get the jar build/libs/DaemonDemo-all-1.0.jar. This jar contains the above MyDaemon class, and commons-daemon jar as dependency.

For the next step, we need to install jsvc into our system. I use Fedora Linux, so I use dnf command to install the package provided by default:

apache-commons-daemon-jsvc.x86_64 : Java daemon launcher

After installing it, we can see the files provided by above package:

$ rpm -ql apache-commons-daemon-jsvc-1.0.15-10.fc23.x86_64
/usr/bin/jsvc
/usr/share/doc/apache-commons-daemon-jsvc
/usr/share/doc/apache-commons-daemon-jsvc/LICENSE.txt
/usr/share/doc/apache-commons-daemon-jsvc/NOTICE.txt
/usr/share/man/man1/jsvc.1.gz

As the file list shown above, the core file provided by the package is the binary file /usr/bin/jsvc. We will use this jsvc to start our MyDaemon.

Before starting jsvc, we need to make sure the Java side, apache-commons-daemon, is installed. You can download the jar from the Apache commons website directly5. I will use the one provided by Fedora directly:

apache-commons-daemon.noarch : Defines API to support an alternative invocation mechanism

The above package will provide the commons-daemon jar:

[weli@localhost projs]$ rpm -ql apache-commons-daemon-1.0.15-1.redhat_1.ep6.el6.noarch
/usr/share/java/apache-commons-daemon-1.0.15-redhat-1.jar
/usr/share/java/apache-commons-daemon.jar
/usr/share/java/commons-daemon-1.0.15-redhat-1.jar
/usr/share/java/commons-daemon.jar
/usr/share/java/jakarta-commons-daemon-1.0.15-redhat-1.jar
/usr/share/java/jakarta-commons-daemon.jar

Above jars are acutally the same, most of them are symbolic links to the same actual jar file, so referring to anyone is the same. Now I can use jsvc and commons-daemon.jar to start our MyDaemon. The command is like the following:

$ sudo /usr/bin/jsvc \
-debug \
-nodetach \
-cp /home/weli/projs/jboss-webserver-and-eap-high-availability/DaemonDemo/build/libs:/usr/share/java/commons-daemon-1.0.15-redhat-1.jar MyDaemon

As the command shown above, we have used nodetach option to make the program a daemon, and we use cp option to tell jsvc to find our jar and commons-daemon jar. Finally we have told jsvc our class that implements the Daemon interface. Because we have used debug option, so the output of above command is very long. Here is the whole output:

+-- DUMPING PARSED COMMAND LINE ARGUMENTS --------------
| Detach:          False
| Show Version:    No
| Show Help:       No
| Check Only:      Disabled
| Stop:            False
| Wait:            0
| Run as service:  No
| Install service: No
| Remove service:  No
| JVM Name:        "null"
| Java Home:       "null"
| PID File:        "/var/run/jsvc.pid"
| User Name:       "null"
| Extra Options:   1
|   "-Djava.class.path=/home/weli/projs/jboss-webserver-and-eap-high-availability/DaemonDemo/build/libs:/usr/share/java/commons-daemon-1.0.15-redhat-1.jar"
| Class Invoked:   "MyDaemon"
| Class Arguments: 0
+-------------------------------------------------------
Home not specified on command line, using environment
Home not on command line or in environment, searching
Attempting to locate Java Home in /usr/java/default
Path /usr/java/default is not a directory
Attempting to locate Java Home in /usr/java
Path /usr/java is not a directory
Attempting to locate Java Home in /usr/local/java
Path /usr/local/java is not a directory
Attempting to locate Java Home in /usr/lib/jvm/default-java
Path /usr/lib/jvm/default-java is not a directory
Attempting to locate Java Home in /usr/lib/jvm/java
Attempting to locate VM configuration file /usr/lib/jvm/java/jre/lib/jvm.cfg
Attempting to locate VM configuration file /usr/lib/jvm/java/lib/jvm.cfg
Attempting to locate VM configuration file /usr/lib/jvm/java/jre/lib/amd64/jvm.cfg
Found VM configuration file at /usr/lib/jvm/java/jre/lib/amd64/jvm.cfg
Found VM server definition in configuration
Checking library /usr/lib/jvm/java/jre/lib/amd64/server/libjvm.so
Found VM client definition in configuration
Checking library /usr/lib/jvm/java/jre/lib/amd64/client/libjvm.so
Checking library /usr/lib/jvm/java/lib/amd64/client/libjvm.so
Cannot locate library for VM client (skipping)
Java Home located in /usr/lib/jvm/java
+-- DUMPING JAVA HOME STRUCTURE ------------------------
| Java Home:       "/usr/lib/jvm/java"
| Java VM Config.: "/usr/lib/jvm/java/jre/lib/amd64/jvm.cfg"
| Found JVMs:      1
| JVM Name:        "server"
|                  "/usr/lib/jvm/java/jre/lib/amd64/server/libjvm.so"
+-------------------------------------------------------
Using default JVM in /usr/lib/jvm/java/jre/lib/amd64/server/libjvm.so
Invoking w/ LD_LIBRARY_PATH=/usr/lib/jvm/java/jre/lib/amd64/server:/usr/lib/jvm/java/jre/lib/amd64
+-- DUMPING PARSED COMMAND LINE ARGUMENTS --------------
| Detach:          False
| Show Version:    No
| Show Help:       No
| Check Only:      Disabled
| Stop:            False
| Wait:            0
| Run as service:  No
| Install service: No
| Remove service:  No
| JVM Name:        "null"
| Java Home:       "null"
| PID File:        "/var/run/jsvc.pid"
| User Name:       "null"
| Extra Options:   1
|   "-Djava.class.path=/home/weli/projs/jboss-webserver-and-eap-high-availability/DaemonDemo/build/libs:/usr/share/java/commons-daemon-1.0.15-redhat-1.jar"
| Class Invoked:   "MyDaemon"
| Class Arguments: 0
+-------------------------------------------------------
Home not specified on command line, using environment
Home not on command line or in environment, searching
Attempting to locate Java Home in /usr/java/default
Path /usr/java/default is not a directory
Attempting to locate Java Home in /usr/java
Path /usr/java is not a directory
Attempting to locate Java Home in /usr/local/java
Path /usr/local/java is not a directory
Attempting to locate Java Home in /usr/lib/jvm/default-java
Path /usr/lib/jvm/default-java is not a directory
Attempting to locate Java Home in /usr/lib/jvm/java
Attempting to locate VM configuration file /usr/lib/jvm/java/jre/lib/jvm.cfg
Attempting to locate VM configuration file /usr/lib/jvm/java/lib/jvm.cfg
Attempting to locate VM configuration file /usr/lib/jvm/java/jre/lib/amd64/jvm.cfg
Found VM configuration file at /usr/lib/jvm/java/jre/lib/amd64/jvm.cfg
Found VM server definition in configuration
Checking library /usr/lib/jvm/java/jre/lib/amd64/server/libjvm.so
Found VM client definition in configuration
Checking library /usr/lib/jvm/java/jre/lib/amd64/client/libjvm.so
Checking library /usr/lib/jvm/java/lib/amd64/client/libjvm.so
Cannot locate library for VM client (skipping)
Java Home located in /usr/lib/jvm/java
+-- DUMPING JAVA HOME STRUCTURE ------------------------
| Java Home:       "/usr/lib/jvm/java"
| Java VM Config.: "/usr/lib/jvm/java/jre/lib/amd64/jvm.cfg"
| Found JVMs:      1
| JVM Name:        "server"
|                  "/usr/lib/jvm/java/jre/lib/amd64/server/libjvm.so"
+-------------------------------------------------------
Running w/ LD_LIBRARY_PATH=/usr/lib/jvm/java/jre/lib/amd64/server:/usr/lib/jvm/java/jre/lib/amd64
redirecting stdout to /dev/null and stderr to /dev/null
Switching umask back to 022 from 077
Using default JVM in /usr/lib/jvm/java/jre/lib/amd64/server/libjvm.so
Attemtping to load library /usr/lib/jvm/java/jre/lib/amd64/server/libjvm.so
JVM library /usr/lib/jvm/java/jre/lib/amd64/server/libjvm.so loaded
JVM library entry point found (0x924610A0)
+-- DUMPING JAVA VM CREATION ARGUMENTS -----------------
| Version:                       0x010004
| Ignore Unrecognized Arguments: False
| Extra options:                 1
|   "-Djava.class.path=/home/weli/projs/jboss-webserver-and-eap-high-availability/DaemonDemo/build/libs:/usr/share/java/commons-daemon-1.0.15-redhat-1.jar" (0x00000000)
+-------------------------------------------------------
| Internal options:              4
|   "-Dcommons.daemon.process.id=31380" (0x00000000)
|   "-Dcommons.daemon.process.parent=31379" (0x00000000)
|   "-Dcommons.daemon.version=1.0.15-dev" (0x00000000)
|   "abort" (0xf9b3e0a0)
+-------------------------------------------------------
Java VM created successfully
Class org/apache/commons/daemon/support/DaemonLoader found
Native methods registered
java_init done
Daemon loading...
MyDaemon init...
Daemon loaded successfully
java_load done
MyDaemon start...
Daemon started successfully
java_start done
Waiting for a signal to be delivered
create_tmp_file: /tmp/31380.jsvc_up

From above log we can see all the details of the process. We can see jsvc tried to find java from several predefined locations, and finally it found the java provided by Fedora, and it shows how it uses DaemonLoader and start our MyDaemon.

Now let’s analyze the processes created by jsvc. As we have learned, jsvc itself is a Launcher Process, it will start a JVM instance called Controller Process, which will interact with launcher to listen to standard system signals. The controller will start our Daemon program as a standalone process as the Controlled Process, the controlled process will accept the management of controller, because controller process can control the child(controlled) process by using the implemented Daemon interface.

We can use ps command to verify this:

[weli@localhost projs]$ ps -ef | grep jsvc
root      2376   316  0 18:12 pts/5    00:00:00 sudo /usr/bin/jsvc -debug -nodetach -cp /home/weli/projs/jboss-webserver-and-eap-high-availability/DaemonDemo/build/libs:/usr/share/java/commons-daemon-1.0.15-redhat-1.jar MyDaemon
root      2385  2376  0 18:12 pts/5    00:00:00 jsvc.exec -debug -nodetach -cp /home/weli/projs/jboss-webserver-and-eap-high-availability/DaemonDemo/build/libs:/usr/share/java/commons-daemon-1.0.15-redhat-1.jar MyDaemon
root      2386  2385  0 18:12 pts/5    00:00:00 jsvc.exec -debug -nodetach -cp /home/weli/projs/jboss-webserver-and-eap-high-availability/DaemonDemo/build/libs:/usr/share/java/commons-daemon-1.0.15-redhat-1.jar MyDaemon

From above we can see three processes. The jsvc itself is obviously the launcher. For the other to processes, we can refer to jsvc debug output to understand it:

+-------------------------------------------------------
| Internal options:              4
|   "-Dcommons.daemon.process.id=2386" (0x00000000)
|   "-Dcommons.daemon.process.parent=2385" (0x00000000)
|   "-Dcommons.daemon.version=1.0.15-dev" (0x00000000)
|   "abort" (0xfc2fc0a0)
+-------------------------------------------------------
Java VM created successfully

So the parent is the controller, and the other one is the worker. After the jsvc is started as a daemon, we can now pressing CTRL-C to send a SIGINT signal to the process, and we can see the following output:

^CCaught SIGINT: Scheduling a shutdown
remove_tmp_file: /tmp/2386.jsvc_up
Shutdown or reload requested: exiting
MyDaemon stop...
Forwarding signal 2 to process 2386
Caught SIGINT: Scheduling a shutdown
Shutdown or reload already scheduled
Daemon stopped successfully
MyDaemon destroy...
Daemon destroyed successfully
Calling System.exit(0)
Service shut down

As the log shown above, we can see how jsvc handles the signal properly and gracefully shutdown our MyDaemon. So it’s our responsibility to implement Daemon interface correctly, so jsvc can use our implementation properly.

What’s the difference between systemd and jsvc

Currently the systemd can achieve most parts of the process control function provided by jsvc, but jsvc can let the server to bind to privileged port and then drop the root access properly. To see more differences between systemd and jsvc, you can check this page6.

References

My Github Page: https://github.com/liweinan

Powered by Jekyll and Theme by solid

If you have any question want to ask or find bugs regarding with my blog posts, please report it here:
https://github.com/liweinan/liweinan.github.io/issues