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.Properties;
32  import java.util.Set;
33  import java.util.concurrent.CopyOnWriteArraySet;
34  
35  import org.eclipse.jetty.util.component.Destroyable;
36  import org.eclipse.jetty.util.component.LifeCycle;
37  import org.eclipse.jetty.util.thread.ShutdownThread;
38  
39  /**
40   * Shutdown/Stop Monitor thread.
41   * <p>
42   * This thread listens on the host/port specified by the STOP.HOST/STOP.PORT system parameter (defaults to 127.0.0.1/-1 for not listening) for 
43   * request authenticated with the key given by the STOP.KEY system parameter (defaults to "eclipse") for admin requests.
44   * <p>
45   * If the stop port is set to zero, then a random port is assigned and the port number is printed to stdout.
46   * <p>
47   * Commands "stop" and "status" are currently supported.
48   */
49  public class ShutdownMonitor 
50  {
51      private final Set<LifeCycle> _lifeCycles = new CopyOnWriteArraySet<LifeCycle>();
52      
53      // Implementation of safe lazy init, using Initialization on Demand Holder technique.
54      static class Holder
55      {
56          static ShutdownMonitor instance = new ShutdownMonitor();
57      }
58  
59      public static ShutdownMonitor getInstance()
60      {
61          return Holder.instance;
62      }
63      
64      /* ------------------------------------------------------------ */
65      public static synchronized void register(LifeCycle... lifeCycles)
66      {
67          getInstance()._lifeCycles.addAll(Arrays.asList(lifeCycles));
68      }
69  
70     
71      /* ------------------------------------------------------------ */
72      public static synchronized void deregister(LifeCycle lifeCycle)
73      {
74          getInstance()._lifeCycles.remove(lifeCycle);
75      }
76      
77      /* ------------------------------------------------------------ */
78      public static synchronized boolean isRegistered(LifeCycle lifeCycle)
79      {
80          return getInstance()._lifeCycles.contains(lifeCycle);
81      }
82      
83      /* ------------------------------------------------------------ */
84      /**
85       * ShutdownMonitorRunnable
86       *
87       * Thread for listening to STOP.PORT for command to stop Jetty.
88       * If ShowndownMonitor.exitVm is true, then Sytem.exit will also be
89       * called after the stop.
90       *
91       */
92      private class ShutdownMonitorRunnable implements Runnable
93      {
94          public ShutdownMonitorRunnable()
95          {
96              startListenSocket();
97          }
98          
99          @Override
100         public void run()
101         {
102             if (serverSocket == null)
103             {
104                 return;
105             }
106 
107             while (serverSocket != null)
108             {
109                 Socket socket = null;
110                 try
111                 {
112                     socket = serverSocket.accept();
113 
114                     LineNumberReader lin = new LineNumberReader(new InputStreamReader(socket.getInputStream()));
115                     String receivedKey = lin.readLine();
116                     if (!key.equals(receivedKey))
117                     {
118                         System.err.println("Ignoring command with incorrect key");
119                         continue;
120                     }
121 
122                     OutputStream out = socket.getOutputStream();
123 
124                     String cmd = lin.readLine();
125                     debug("command=%s",cmd);
126                     if ("stop".equalsIgnoreCase(cmd)) //historic, for backward compatibility
127                     {
128                         //Stop the lifecycles, only if they are registered with the ShutdownThread, only destroying if vm is exiting
129                         debug("Issuing stop...");
130                         
131                         for (LifeCycle l:_lifeCycles)
132                         {
133                             try
134                             {
135                                 if (l.isStarted() && ShutdownThread.isRegistered(l))
136                                 {
137                                     l.stop();
138                                 }
139                                 
140                                 if ((l instanceof Destroyable) && exitVm)
141                                     ((Destroyable)l).destroy();
142                             }
143                             catch (Exception e)
144                             {
145                                 debug(e);
146                             }
147                         }
148 
149                         //Stop accepting any more commands
150                         stopInput(socket);
151 
152                         // Reply to client
153                         debug("Informing client that we are stopped.");
154                         informClient(out, "Stopped\r\n");
155 
156                         //Stop the output and close the monitor socket
157                         stopOutput(socket);
158 
159                         if (exitVm)
160                         {
161                             // Kill JVM
162                             debug("Killing JVM");
163                             System.exit(0);
164                         }
165                     }
166                     else if ("forcestop".equalsIgnoreCase(cmd))
167                     {
168                         debug("Issuing force stop...");
169                         
170                         //Ensure that objects are stopped, destroyed only if vm is forcibly exiting
171                         stopLifeCycles(exitVm);
172 
173                         //Stop accepting any more commands
174                         stopInput(socket);
175 
176                         // Reply to client
177                         debug("Informing client that we are stopped.");
178                         informClient(out, "Stopped\r\n");
179 
180                         //Stop the output and close the monitor socket
181                         stopOutput(socket);
182                         
183                         //Honour any pre-setup config to stop the jvm when this command is given
184                         if (exitVm)
185                         {
186                             // Kill JVM
187                             debug("Killing JVM");
188                             System.exit(0);
189                         }
190                     }
191                     else if ("stopexit".equalsIgnoreCase(cmd))
192                     {
193                         debug("Issuing stop and exit...");
194                         //Make sure that objects registered with the shutdown thread will be stopped
195                         stopLifeCycles(true);
196                         
197                         //Stop accepting any more input
198                         stopInput(socket);
199 
200                         // Reply to client
201                         debug("Informing client that we are stopped.");                       
202                         informClient(out, "Stopped\r\n");              
203 
204                         //Stop the output and close the monitor socket
205                         stopOutput(socket);
206                         
207                         debug("Killing JVM");
208                         System.exit(0);
209                     }
210                     else if ("exit".equalsIgnoreCase(cmd))
211                     {
212                         debug("Killing JVM");
213                         System.exit(0);
214                     }
215                     else if ("status".equalsIgnoreCase(cmd))
216                     {
217                         // Reply to client
218                         informClient(out, "OK\r\n");
219                     }
220                 }
221                 catch (Exception e)
222                 {
223                     debug(e);
224                     System.err.println(e.toString());
225                 }
226                 finally
227                 {
228                     close(socket);
229                     socket = null;
230                 }
231             }
232         }
233         
234         public void stopInput (Socket socket)
235         {
236             //Stop accepting any more input
237             close(serverSocket);
238             serverSocket = null;
239             //Shutdown input from client
240             shutdownInput(socket);  
241         }
242         
243         public void stopOutput (Socket socket) throws IOException
244         {
245             socket.shutdownOutput();
246             close(socket);
247             socket = null;                        
248             debug("Shutting down monitor");
249             serverSocket = null;
250         }
251         
252         public void informClient (OutputStream out, String message) throws IOException
253         {
254             out.write(message.getBytes(StandardCharsets.UTF_8));
255             out.flush();
256         }
257 
258         /**
259          * Stop the registered lifecycles, optionally
260          * calling destroy on them.
261          * 
262          * @param destroy true if {@link Destroyable}'s should also be destroyed.
263          */
264         public void stopLifeCycles (boolean destroy)
265         {
266             for (LifeCycle l:_lifeCycles)
267             {
268                 try
269                 {
270                     if (l.isStarted())
271                     {
272                         l.stop();
273                     }
274                     
275                     if ((l instanceof Destroyable) && destroy)
276                         ((Destroyable)l).destroy();
277                 }
278                 catch (Exception e)
279                 {
280                     debug(e);
281                 }
282             }
283         }
284 
285         public void startListenSocket()
286         {
287             if (port < 0)
288             {            
289                 if (DEBUG)
290                     System.err.println("ShutdownMonitor not in use (port < 0): " + port);
291                 return;
292             }
293 
294             try
295             {
296                 serverSocket = new ServerSocket();
297                 serverSocket.setReuseAddress(true);
298                 serverSocket.bind(new InetSocketAddress(InetAddress.getByName(host), port), 1);
299                 if (port == 0)
300                 {
301                     // server assigned port in use
302                     port = serverSocket.getLocalPort();
303                     System.out.printf("STOP.PORT=%d%n",port);
304                 }
305 
306                 if (key == null)
307                 {
308                     // create random key
309                     key = Long.toString((long)(Long.MAX_VALUE * Math.random() + this.hashCode() + System.currentTimeMillis()),36);
310                     System.out.printf("STOP.KEY=%s%n",key);
311                 }
312             }
313             catch (Exception e)
314             {
315                 debug(e);
316                 System.err.println("Error binding monitor port " + port + ": " + e.toString());
317                 serverSocket = null;
318             }
319             finally
320             {
321                 // establish the port and key that are in use
322                 debug("STOP.PORT=%d",port);
323                 debug("STOP.KEY=%s",key);
324                 debug("%s",serverSocket);
325             }
326         }
327 
328     }
329     
330     private boolean DEBUG;
331     private String host;
332     private int port;
333     private String key;
334     private boolean exitVm;
335     private ServerSocket serverSocket;
336     private Thread thread;
337 
338     /**
339      * Create a ShutdownMonitor using configuration from the System properties.
340      * <p>
341      * <code>STOP.PORT</code> = the port to listen on (empty, null, or values less than 0 disable the stop ability)<br>
342      * <code>STOP.KEY</code> = the magic key/passphrase to allow the stop (defaults to "eclipse")<br>
343      * <p>
344      * Note: server socket will only listen on localhost, and a successful stop will issue a System.exit() call.
345      */
346     private ShutdownMonitor()
347     {
348         Properties props = System.getProperties();
349 
350         this.DEBUG = props.containsKey("DEBUG");
351 
352         // Use values passed thru via /jetty-start/
353         this.host = props.getProperty("STOP.HOST","127.0.0.1");
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 true to exit the VM on shutdown
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 }