View Javadoc
1   /*
2    * The MIT License
3    *
4    * Copyright (c) 2009-, Sun Microsystems, Inc.
5    *
6    * Permission is hereby granted, free of charge, to any person obtaining a copy
7    * of this software and associated documentation files (the "Software"), to deal
8    * in the Software without restriction, including without limitation the rights
9    * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10   * copies of the Software, and to permit persons to whom the Software is
11   * furnished to do so, subject to the following conditions:
12   *
13   * The above copyright notice and this permission notice shall be included in
14   * all copies or substantial portions of the Software.
15   *
16   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22   * THE SOFTWARE.
23   */
24  package com.sun.akuma;
25  
26  import com.sun.jna.StringArray;
27  
28  import java.io.FileDescriptor;
29  import java.lang.reflect.Constructor;
30  import java.lang.reflect.Field;
31  import java.lang.reflect.InvocationTargetException;
32  import java.lang.reflect.Method;
33  import java.net.InetSocketAddress;
34  import java.net.ServerSocket;
35  import java.net.SocketImpl;
36  import java.util.logging.Logger;
37  import java.util.List;
38  import java.util.Collections;
39  import java.util.Arrays;
40  
41  import static com.sun.akuma.CLibrary.LIBC;
42  import sun.misc.Signal;
43  import sun.misc.SignalHandler;
44  
45  /**
46   * Multi-process network server that accepts connections on the same TCP port.
47   *
48   * <p>
49   * This class lets you write a Unix-like multi-process network daemon. The first process acts
50   * as the frontend. This creates a new socket, then fork several worker processes, which inherits
51   * this socket.
52   *
53   * <p>
54   * Worker threads will all accept connections on this port, so even when one of the worker processes
55   * die off, your clients won't notice that there's a problem.
56   *
57   * <p>
58   * The user of this class needs to override this class and implement abstract methods.
59   * Several protected methods can be also overridden to customize the behaviors.
60   * See {@link EchoServer} source code as an example.
61   *
62   * <p>
63   * This class also inherits from {@link Daemon} to support the daemonization.
64   *
65   * <p>
66   * From your main method, call into {@link #run()} method. Depending on whether the current process
67   * is started as a front end or a worker process, the run method behave accordingly.
68   *
69   * @author Kohsuke Kawaguchi
70   */
71  public abstract class NetworkServer extends Daemon {
72      /**
73       * Java arguments.
74       */
75      protected final List<String> arguments;
76  
77      protected NetworkServer(String[] args) {
78          this.arguments = Collections.unmodifiableList(Arrays.asList(args));
79      }
80  
81      /**
82       * Entry point. Should be called from your main method.
83       */
84      public void run() throws Exception {
85          String mode = System.getProperty(MODE_PROPERTY);
86          if("worker".equals(mode)) {
87              // worker process
88              worker();
89          } else {
90              // to run the frontend in the foreground
91              if(isDaemonized()) {
92                  // running as a daemon
93                  init();
94              } else {
95                  // running in the foreground
96                  if(shouldBeDaemonized()) {
97                      // to launch the whole thing into a daemon
98                      daemonize();
99                      System.exit(0);
100                 }
101             }
102 
103             frontend();
104         }
105     }
106 
107     /**
108      * Determine if we should daemonize ourselves.
109      */
110     protected boolean shouldBeDaemonized() {
111         return !arguments.isEmpty() && arguments.get(0).equals("daemonize");
112     }
113 
114     /**
115      * Front-end.
116      */
117     protected void frontend() throws Exception {
118         ServerSocket ss = createServerSocket();
119         int fdn = getUnixFileDescriptor(ss);
120 
121         LOGGER.fine("Listening to port "+ss.getLocalPort()+" (fd="+fdn+")");
122 
123         // prepare the parameters for the exec.
124         JavaVMArguments forkArgs = JavaVMArguments.current();
125         forkArgs.setSystemProperty(NetworkServer.class.getName()+".port",String.valueOf(fdn));
126 
127         forkWorkers(forkArgs);
128     }
129 
130     /**
131      * Forks the worker thread with the given JVM args.
132      *
133      * The implementation is expected to modify the arguments to suit their need,
134      * then call into {@link #forkWorkerThreads(JavaVMArguments, int)}.
135      */
136     protected abstract void forkWorkers(JavaVMArguments args) throws Exception;
137 
138     /**
139      * Called by the front-end code to fork a number of worker processes into the background.
140      *
141      * This method never returns.
142      */
143     protected void forkWorkerThreads(JavaVMArguments arguments, int n) throws Exception {
144         String exe = Daemon.getCurrentExecutable();
145         arguments.setSystemProperty(MODE_PROPERTY,"worker"); // the forked process should run as workers
146         LOGGER.fine("Forking worker: "+arguments);
147         StringArray sa = arguments.toStringArray();
148 
149         // fork several worker processes
150         for( int i=0; i< n; i++ ) {
151             int r = LIBC.fork();
152             if(r<0) {
153                 LIBC.perror("forking a worker process failed");
154                 System.exit(-1);
155             }
156             if(r==0) {
157                 // newly created child will exec to itself to get the proper Java environment back
158                 LIBC.execv(exe,sa);
159                 System.err.println("exec failed");
160                 LIBC.perror("initial exec failed");
161                 System.exit(-1);
162             }
163         }
164 
165         // when we are killed, kill all the worker processes, too.
166         Signal.handle(new Signal("TERM"),
167             new SignalHandler() {
168                 public void handle(Signal sig) {
169                     LIBC.kill(0,SIGTERM);
170                     System.exit(-1);
171                 }
172             });
173 
174         // hang forever
175         Object o = new Object();
176         synchronized (o) {
177             o.wait();
178         }
179     }
180 
181     /**
182      * Creates a bound {@link ServerSocket} that will be shared by all worker processes.
183      * This method is called in the frontend process.
184      */
185     protected abstract ServerSocket createServerSocket() throws Exception;
186 
187     /**
188      * Determines the Unix file descriptor number of the given {@link ServerSocket}.
189      */
190     private int getUnixFileDescriptor(ServerSocket ss) throws NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
191         Field $impl = ss.getClass().getDeclaredField("impl");
192         $impl.setAccessible(true);
193         SocketImpl socketImpl = (SocketImpl)$impl.get(ss);
194         Method $getFileDescriptor = SocketImpl.class.getDeclaredMethod("getFileDescriptor");
195         $getFileDescriptor.setAccessible(true);
196         FileDescriptor fd = (FileDescriptor) $getFileDescriptor.invoke(socketImpl);
197         Field $fd = fd.getClass().getDeclaredField("fd");
198         $fd.setAccessible(true);
199         return (Integer)$fd.get(fd);
200     }
201 
202     protected void worker() throws Exception {
203         String port = System.getProperty(NetworkServer.class.getName() + ".port");
204         worker(recreateServerSocket(Integer.parseInt(port)));
205     }
206 
207     /**
208      * Worker thread main code.
209      *
210      * @param ss
211      *      The server socket that the frontend process created.
212      */
213     protected abstract void worker(ServerSocket ss) throws Exception;
214 
215     /**
216      * Recreates a bound {@link ServerSocket} on the given file descriptor.
217      */
218     private ServerSocket recreateServerSocket(int fdn) throws Exception {
219         // create a properly populated FileDescriptor
220         FileDescriptor fd = new FileDescriptor();
221         Field $fd = FileDescriptor.class.getDeclaredField("fd");
222         $fd.setAccessible(true);
223         $fd.set(fd,fdn);
224 
225         // now create a PlainSocketImpl
226         Class $PlainSocketImpl = Class.forName("java.net.PlainSocketImpl");
227         Constructor $init = $PlainSocketImpl.getDeclaredConstructor(FileDescriptor.class);
228         $init.setAccessible(true);
229         SocketImpl socketImpl = (SocketImpl)$init.newInstance(fd);
230 
231         // then wrap that into ServerSocket
232         ServerSocket ss = new ServerSocket();
233         ss.bind(new InetSocketAddress(0));
234         Field $impl = ServerSocket.class.getDeclaredField("impl");
235         $impl.setAccessible(true);
236         $impl.set(ss,socketImpl);
237         return ss;
238     }
239 
240     private static final Logger LOGGER = Logger.getLogger(NetworkServer.class.getName());
241     private static final int SIGTERM = 15;
242     private static final String MODE_PROPERTY = NetworkServer.class.getName() + ".mode";
243 }