Implementing a generic start/stop mechanism for services

In Service.java, the main() method accepts two arguments - start and -stop. The -start argument activates a loop which continually logs the current date/time to a log file called service.log. On the other hand, the -stop argument simply sets a stop flag to true at line 35. When the main program loop detects the stop flag, it will terminate (line 23-27).

File: Service.java
1:  package examples;
2:
3:  import java.io.*;
4:  import java.util.*;
5:
6:  public class Service
7:  {
8:      static boolean stop = false;
9:      public static void main(String[] args) throws Exception
10:     {
11:         // Start the service
12:         if (args[0].equals("-start"))
13:         {
14:             while(true)
15:             {
16:                 // Append current date/time to log file
17:                 log("Current date/time is " + new Date());
18:
19:                 // Sleep for 5 secs
20:                 Thread.currentThread().sleep(5000);
21:
22:                 // Check for termination
23:                 if (stop)
24:                 {
25:                     log("Service stopped.");
26:                     break;
27:                 }
28:             }
29:         }
30:         else
31:         // Stop the service
32:         if (args[0].equals("-stop"))
33:         {
34:             // Set the termination flag
35:             stop = true;
36:         }
37:     }
38:
39:     /**
40:      * Log given string to file "service.log".
41:      */
42:     static void log(String msg) throws IOException
43:     {
44:         PrintWriter pw = new PrintWriter(
45:             new FileWriter("service.log", true));
46:         pw.println(msg);
47:         pw.close();
48:     }
49: }

This approach works with NativeJ-generated executables, but it will not work when moved to another platform such as Solaris, where shell scripts are the norm. This is because the -start and -stop arguments will be passed to different JVM instances, which means they will be executing in different address space.

A more platform-independent way of terminating a Java program that is meant to run continuously as a service (Win32) or daemon (Unix) is to implement some form of IPC (inter-process communication). An example is given in Service2.java, which uses IP datagrams to communicate the intent for program termination.

File: Service2.java
1:  package examples;
2:
3:  import java.io.*;
4:  import java.net.*;
5:  import java.util.*;
6:
7:  public class Service2
8:  {
9:      /**
10:      * This is the port over which the termination signal is sent.
11:      */
12:     private final static int port = 5678;
13:
14:     /**
15:      * This is the message to be sent to signal termination.
16:      */
17:     private final static String terminator = "QUIT";
19:
20:     /**
21:      * The main() function accepts one single parameter:
22:      * "start" or "stop".
23:      * The first parameter starts the date/time logging service,
24:      * while the second parameter stops the service.
25:      */
26:     public static void main(String[] args) throws Exception
27:     {
28:         // Start the service
29:         if (args[0].equals("start"))
30:         {
31:             try
32:             {
33:                 // Run the termination listener thread
34:                 TerminationListener t = new TerminationListener(
35:                     port, terminator, Thread.currentThread());
36:                 t.start();
37:
38:                 // Start the logging service
39:                 while(true)
40:                 {
41:                     // Append current date/time to log file
42:                     log("Current date/time is " + new Date());
43:
44:                     // Sleep for 5 secs
45:                     Thread.currentThread().sleep(5000);
46:                 }
47:             }
48:             catch(InterruptedException e)
49:             {
50:                 // Exit when thread is interrupted.
51:                 log("Service stopped.");
52:             }
53:         }
54:         else
55:         // Stop the service
56:         if (args[0].equals("stop"))
57:         {
58:             try
59:             {
60:                 // Send the termination message
61:                 DatagramSocket socket = new DatagramSocket();
62:                 DatagramPacket packet = new DatagramPacket(
63:                     terminator.getBytes(), terminator.length(),
64:                     InetAddress.getLocalHost(), port);
65:                 socket.send(packet);
66:             }
67:             catch(Exception e)
68:             {
69:                 e.printStackTrace();
70:             }
71:         }
72:     }
73:
74:     /**
75:      * Log given string to file "service.log".
76:      */
77:     static void log(String msg) throws IOException
78:     {
79:         PrintWriter pw = new PrintWriter(
80:             new FileWriter("service.log", true));
81:         pw.println(msg);
82:         pw.close();
83:     }
84: }
85:
86: /**
87:  * This is a thread that will listen for the termination message
88:  * over the designated port, then interrupt the parent thread.
89:  */
90: class TerminationListener extends Thread
91: {
92:     Thread parent;
93:     int port;
94:     String terminator;
95:
96:     /**
97:      * Set this thread to daemon mode so that if for some reason
98:      * the main thread exists, this thread will not prevent the
99:      * JVM from terminating.
100:     */
101:    public TerminationListener(
102:        int port, String terminator, Thread parent)
103:    {
104:        setDaemon(true);
105:        this.port = port;
106:        this.terminator = terminator;
107:        this.parent = parent;
108:    }
109:
110:    /**
111:     * Listen for the termination signal over the designated port.
112:     */
113:    public void run()
114:    {
115:        try
116:        {
117:            // Setup datagram socket
118:            DatagramSocket socket = new DatagramSocket(port);
119:            DatagramPacket packet = new DatagramPacket(
120:                new byte[terminator.length()], terminator.length());
121:
122:            // Stop only when we have received the termination message
123:            while(true)
124:            {
125:                // Wait for a message
126:                socket.receive(packet);
127:                String msg = new String(packet.getData());
128:
129:                // Make sure the message is coming from the same
130:                // machine. This is included for additional security
131:                // so that the program cannot be terminated from an
132:                // external machine).
133:                if (!packet.getAddress().equals(
134:                    InetAddress.getLocalHost()))
135:                        continue;
136:
137:                // Make sure the message is the termination message
138:                if (!msg.equals(terminator)) continue;
139:
140:                // Interrupt parent thread
141:                parent.interrupt();
142:
143:                // Terminate this thread
144:                break;
145:            }
146:        }
147:        catch(Exception e)
148:        {
149:        }
150:    }
151:}

A TerminationListener class is defined in line 90, that runs in the background and waits for an IP datagram from a predefined port (default: 5678) on the local machine. This is instantiated and started in lines 34-36 when the -start argument is used. Let's say this runs in JVM instance #1.

When the -stop argument is issued, this runs in JVM instance #2. The instructions in lines 61-65 will send out an IP datagram containing the message "QUIT". This will be routed over to the TerminationListener in JVM instance #1, logging activities will stop, and JVM instance #1 will terminate. Similarly, once JVM instance #2 has sent out the "QUIT" message, it will also terminate.

This approach to graceful termination of a server app will work under NativeJ, as well as all Java-enabled platforms with TCP/IP capability. Hence, you will have one set of source codes that can work with both NativeJ-generated Win32 executables, as well as on other platforms using more traditional batch files, shell scripts, or just plain "java <class>".