View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd.
4   //  ------------------------------------------------------------------------
5   //  All rights reserved. This program and the accompanying materials
6   //  are made available under the terms of the Eclipse Public License v1.0
7   //  and Apache License v2.0 which accompanies this distribution.
8   //
9   //      The Eclipse Public License is available at
10  //      http://www.eclipse.org/legal/epl-v10.html
11  //
12  //      The Apache License v2.0 is available at
13  //      http://www.opensource.org/licenses/apache2.0.php
14  //
15  //  You may elect to redistribute this code under either of these licenses.
16  //  ========================================================================
17  //
18  
19  package org.eclipse.jetty.server;
20  
21  import java.io.IOException;
22  import java.io.InputStreamReader;
23  import java.io.LineNumberReader;
24  import java.io.OutputStream;
25  import java.net.InetAddress;
26  import java.net.InetSocketAddress;
27  import java.net.ServerSocket;
28  import java.net.Socket;
29  import java.nio.charset.StandardCharsets;
30  import java.util.Arrays;
31  import java.util.List;
32  import java.util.Properties;
33  import java.util.Set;
34  import java.util.concurrent.CopyOnWriteArrayList;
35  import java.util.concurrent.CopyOnWriteArraySet;
36  
37  import org.eclipse.jetty.util.component.Destroyable;
38  import org.eclipse.jetty.util.component.LifeCycle;
39  import org.eclipse.jetty.util.thread.ShutdownThread;
40  
41  /**
42   * Shutdown/Stop Monitor thread.
43   * <p>
44   * This thread listens on the port specified by the STOP.PORT system parameter (defaults to -1 for not listening) for request authenticated with the key given
45   * by the STOP.KEY system parameter (defaults to "eclipse") for admin requests.
46   * <p>
47   * If the stop port is set to zero, then a random port is assigned and the port number is printed to stdout.
48   * <p>
49   * Commands "stop" and "status" are currently supported.
50   */
51  public class ShutdownMonitor 
52  {
53      private final Set<LifeCycle> _lifeCycles = new CopyOnWriteArraySet<LifeCycle>();
54      
55      // Implementation of safe lazy init, using Initialization on Demand Holder technique.
56      static class Holder
57      {
58          static ShutdownMonitor instance = new ShutdownMonitor();
59      }
60  
61      public static ShutdownMonitor getInstance()
62      {
63          return Holder.instance;
64      }
65      
66      /* ------------------------------------------------------------ */
67      public static synchronized void register(LifeCycle... lifeCycles)
68      {
69          getInstance()._lifeCycles.addAll(Arrays.asList(lifeCycles));
70      }
71  
72     
73      /* ------------------------------------------------------------ */
74      public static synchronized void deregister(LifeCycle lifeCycle)
75      {
76          getInstance()._lifeCycles.remove(lifeCycle);
77      }
78      
79      /* ------------------------------------------------------------ */
80      public static synchronized boolean isRegistered(LifeCycle lifeCycle)
81      {
82          return getInstance()._lifeCycles.contains(lifeCycle);
83      }
84      
85  
86      /**
87       * ShutdownMonitorRunnable
88       *
89       * Thread for listening to STOP.PORT for command to stop Jetty.
90       * If ShowndownMonitor.exitVm is true, then Sytem.exit will also be
91       * called after the stop.
92       *
93       */
94      private class ShutdownMonitorRunnable implements Runnable
95      {
96          public ShutdownMonitorRunnable()
97          {
98              startListenSocket();
99          }
100         
101         @Override
102         public void run()
103         {
104             if (serverSocket == null)
105             {
106                 return;
107             }
108 
109             while (serverSocket != null)
110             {
111                 Socket socket = null;
112                 try
113                 {
114                     socket = serverSocket.accept();
115 
116                     LineNumberReader lin = new LineNumberReader(new InputStreamReader(socket.getInputStream()));
117                     String receivedKey = lin.readLine();
118                     if (!key.equals(receivedKey))
119                     {
120                         System.err.println("Ignoring command with incorrect key");
121                         continue;
122                     }
123 
124                     OutputStream out = socket.getOutputStream();
125 
126                     String cmd = lin.readLine();
127                     debug("command=%s",cmd);
128                     if ("stop".equalsIgnoreCase(cmd)) //historic, for backward compatibility
129                     {
130                         //Stop the lifecycles, only if they are registered with the ShutdownThread, only destroying if vm is exiting
131                         debug("Issuing stop...");
132                         
133                         for (LifeCycle l:_lifeCycles)
134                         {
135                             try
136                             {
137                                 if (l.isStarted() && ShutdownThread.isRegistered(l))
138                                 {
139                                     l.stop();
140                                 }
141                                 
142                                 if ((l instanceof Destroyable) && exitVm)
143                                     ((Destroyable)l).destroy();
144                             }
145                             catch (Exception e)
146                             {
147                                 debug(e);
148                             }
149                         }
150 
151                         //Stop accepting any more commands
152                         stopInput(socket);
153 
154                         // Reply to client
155                         debug("Informing client that we are stopped.");
156                         informClient(out, "Stopped\r\n");
157 
158                         //Stop the output and close the monitor socket
159                         stopOutput(socket);
160 
161                         if (exitVm)
162                         {
163                             // Kill JVM
164                             debug("Killing JVM");
165                             System.exit(0);
166                         }
167                     }
168                     else if ("forcestop".equalsIgnoreCase(cmd))
169                     {
170                         debug("Issuing force stop...");
171                         
172                         //Ensure that objects are stopped, destroyed only if vm is forcibly exiting
173                         stopLifeCycles(exitVm);
174 
175                         //Stop accepting any more commands
176                         stopInput(socket);
177 
178                         // Reply to client
179                         debug("Informing client that we are stopped.");
180                         informClient(out, "Stopped\r\n");
181 
182                         //Stop the output and close the monitor socket
183                         stopOutput(socket);
184                         
185                         //Honour any pre-setup config to stop the jvm when this command is given
186                         if (exitVm)
187                         {
188                             // Kill JVM
189                             debug("Killing JVM");
190                             System.exit(0);
191                         }
192                     }
193                     else if ("stopexit".equalsIgnoreCase(cmd))
194                     {
195                         debug("Issuing stop and exit...");
196                         //Make sure that objects registered with the shutdown thread will be stopped
197                         stopLifeCycles(true);
198                         
199                         //Stop accepting any more input
200                         stopInput(socket);
201 
202                         // Reply to client
203                         debug("Informing client that we are stopped.");                       
204                         informClient(out, "Stopped\r\n");              
205 
206                         //Stop the output and close the monitor socket
207                         stopOutput(socket);
208                         
209                         debug("Killing JVM");
210                         System.exit(0);
211                     }
212                     else if ("exit".equalsIgnoreCase(cmd))
213                     {
214                         debug("Killing JVM");
215                         System.exit(0);
216                     }
217                     else if ("status".equalsIgnoreCase(cmd))
218                     {
219                         // Reply to client
220                         informClient(out, "OK\r\n");
221                     }
222                 }
223                 catch (Exception e)
224                 {
225                     debug(e);
226                     System.err.println(e.toString());
227                 }
228                 finally
229                 {
230                     close(socket);
231                     socket = null;
232                 }
233             }
234         }
235         
236         public void stopInput (Socket socket)
237         {
238             //Stop accepting any more input
239             close(serverSocket);
240             serverSocket = null;
241             //Shutdown input from client
242             shutdownInput(socket);  
243         }
244         
245         public void stopOutput (Socket socket) throws IOException
246         {
247             socket.shutdownOutput();
248             close(socket);
249             socket = null;                        
250             debug("Shutting down monitor");
251             serverSocket = null;
252         }
253         
254         public void informClient (OutputStream out, String message) throws IOException
255         {
256             out.write(message.getBytes(StandardCharsets.UTF_8));
257             out.flush();
258         }
259 
260         /**
261          * Stop the registered lifecycles, optionally
262          * calling destroy on them.
263          * 
264          * @param destroy
265          */
266         public void stopLifeCycles (boolean destroy)
267         {
268             for (LifeCycle l:_lifeCycles)
269             {
270                 try
271                 {
272                     if (l.isStarted())
273                     {
274                         l.stop();
275                     }
276                     
277                     if ((l instanceof Destroyable) && destroy)
278                         ((Destroyable)l).destroy();
279                 }
280                 catch (Exception e)
281                 {
282                     debug(e);
283                 }
284             }
285         }
286 
287         public void startListenSocket()
288         {
289             if (port < 0)
290             {            
291                 if (DEBUG)
292                     System.err.println("ShutdownMonitor not in use (port < 0): " + port);
293                 return;
294             }
295 
296             try
297             {
298                 serverSocket = new ServerSocket();
299                 serverSocket.setReuseAddress(true);
300                 serverSocket.bind(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), port), 1);
301                 if (port == 0)
302                 {
303                     // server assigned port in use
304                     port = serverSocket.getLocalPort();
305                     System.out.printf("STOP.PORT=%d%n",port);
306                 }
307 
308                 if (key == null)
309                 {
310                     // create random key
311                     key = Long.toString((long)(Long.MAX_VALUE * Math.random() + this.hashCode() + System.currentTimeMillis()),36);
312                     System.out.printf("STOP.KEY=%s%n",key);
313                 }
314             }
315             catch (Exception e)
316             {
317                 debug(e);
318                 System.err.println("Error binding monitor port " + port + ": " + e.toString());
319                 serverSocket = null;
320             }
321             finally
322             {
323                 // establish the port and key that are in use
324                 debug("STOP.PORT=%d",port);
325                 debug("STOP.KEY=%s",key);
326                 debug("%s",serverSocket);
327             }
328         }
329 
330     }
331     
332     private boolean DEBUG;
333     private int port;
334     private String key;
335     private boolean exitVm;
336     private ServerSocket serverSocket;
337     private Thread thread;
338 
339     /**
340      * Create a ShutdownMonitor using configuration from the System properties.
341      * <p>
342      * <code>STOP.PORT</code> = the port to listen on (empty, null, or values less than 0 disable the stop ability)<br>
343      * <code>STOP.KEY</code> = the magic key/passphrase to allow the stop (defaults to "eclipse")<br>
344      * <p>
345      * Note: server socket will only listen on localhost, and a successful stop will issue a System.exit() call.
346      */
347     private ShutdownMonitor()
348     {
349         Properties props = System.getProperties();
350 
351         this.DEBUG = props.containsKey("DEBUG");
352 
353         // Use values passed thru via /jetty-start/
354         this.port = Integer.parseInt(props.getProperty("STOP.PORT","-1"));
355         this.key = props.getProperty("STOP.KEY",null);
356         this.exitVm = true;
357     }
358 
359     private void close(ServerSocket server)
360     {
361         if (server == null)
362         {
363             return;
364         }
365 
366         try
367         {
368             server.close();
369         }
370         catch (IOException ignore)
371         {
372             debug(ignore);
373         }
374     }
375 
376     private void close(Socket socket)
377     {
378         if (socket == null)
379         {
380             return;
381         }
382 
383         try
384         {
385             socket.close();
386         }
387         catch (IOException ignore)
388         {
389             debug(ignore);
390         }
391     }
392 
393     
394     private void shutdownInput(Socket socket)
395     {
396         if (socket == null)
397             return;
398         
399         try
400         {
401             socket.shutdownInput();
402         }   
403         catch (IOException ignore)
404         {
405             debug(ignore);
406         }
407     }
408     
409     
410     private void debug(String format, Object... args)
411     {
412         if (DEBUG)
413         {
414             System.err.printf("[ShutdownMonitor] " + format + "%n",args);
415         }
416     }
417 
418     private void debug(Throwable t)
419     {
420         if (DEBUG)
421         {
422             t.printStackTrace(System.err);
423         }
424     }
425 
426     public String getKey()
427     {
428         return key;
429     }
430 
431     public int getPort()
432     {
433         return port;
434     }
435 
436     public ServerSocket getServerSocket()
437     {
438         return serverSocket;
439     }
440 
441     public boolean isExitVm()
442     {
443         return exitVm;
444     }
445 
446 
447     public void setDebug(boolean flag)
448     {
449         this.DEBUG = flag;
450     }
451 
452     /**
453      * @param exitVm
454      */
455     public void setExitVm(boolean exitVm)
456     {
457         synchronized (this)
458         {
459             if (thread != null && thread.isAlive())
460             {
461                 throw new IllegalStateException("ShutdownMonitorThread already started");
462             }
463             this.exitVm = exitVm;
464         }
465     }
466 
467     public void setKey(String key)
468     {
469         synchronized (this)
470         {
471             if (thread != null && thread.isAlive())
472             {
473                 throw new IllegalStateException("ShutdownMonitorThread already started");
474             }
475             this.key = key;
476         }
477     }
478 
479     public void setPort(int port)
480     {
481         synchronized (this)
482         {
483             if (thread != null && thread.isAlive())
484             {
485                 throw new IllegalStateException("ShutdownMonitorThread already started");
486             }
487             this.port = port;
488         }
489     }
490 
491     protected void start() throws Exception
492     {
493         Thread t = null;
494         
495         synchronized (this)
496         {
497             if (thread != null && thread.isAlive())
498             {
499                 if (DEBUG)
500                     System.err.printf("ShutdownMonitorThread already started");
501                 return; // cannot start it again
502             }
503          
504             thread = new Thread(new ShutdownMonitorRunnable());
505             thread.setDaemon(true);
506             thread.setName("ShutdownMonitor");
507             t = thread;
508         }
509          
510         if (t != null)
511             t.start();
512     }
513 
514 
515     protected boolean isAlive ()
516     {
517         boolean result = false;
518         synchronized (this)
519         {
520             result = (thread != null && thread.isAlive());
521         }
522         return result;
523     }
524     
525  
526     @Override
527     public String toString()
528     {
529         return String.format("%s[port=%d]",this.getClass().getName(),port);
530     }
531 }