View Javadoc

1   //
2   //  ========================================================================
3   //  Copyright (c) 1995-2016 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         this.DEBUG = System.getProperty("DEBUG") != null;
349 
350         // Use values passed thru via /jetty-start/
351         this.host = System.getProperty("STOP.HOST","127.0.0.1");
352         this.port = Integer.parseInt(System.getProperty("STOP.PORT","-1"));
353         this.key = System.getProperty("STOP.KEY",null);
354         this.exitVm = true;
355     }
356 
357     private void close(ServerSocket server)
358     {
359         if (server == null)
360         {
361             return;
362         }
363 
364         try
365         {
366             server.close();
367         }
368         catch (IOException ignore)
369         {
370             debug(ignore);
371         }
372     }
373 
374     private void close(Socket socket)
375     {
376         if (socket == null)
377         {
378             return;
379         }
380 
381         try
382         {
383             socket.close();
384         }
385         catch (IOException ignore)
386         {
387             debug(ignore);
388         }
389     }
390 
391     
392     private void shutdownInput(Socket socket)
393     {
394         if (socket == null)
395             return;
396         
397         try
398         {
399             socket.shutdownInput();
400         }   
401         catch (IOException ignore)
402         {
403             debug(ignore);
404         }
405     }
406     
407     
408     private void debug(String format, Object... args)
409     {
410         if (DEBUG)
411         {
412             System.err.printf("[ShutdownMonitor] " + format + "%n",args);
413         }
414     }
415 
416     private void debug(Throwable t)
417     {
418         if (DEBUG)
419         {
420             t.printStackTrace(System.err);
421         }
422     }
423 
424     public String getKey()
425     {
426         return key;
427     }
428 
429     public int getPort()
430     {
431         return port;
432     }
433 
434     public ServerSocket getServerSocket()
435     {
436         return serverSocket;
437     }
438 
439     public boolean isExitVm()
440     {
441         return exitVm;
442     }
443 
444 
445     public void setDebug(boolean flag)
446     {
447         this.DEBUG = flag;
448     }
449 
450     /**
451      * @param exitVm true to exit the VM on shutdown
452      */
453     public void setExitVm(boolean exitVm)
454     {
455         synchronized (this)
456         {
457             if (thread != null && thread.isAlive())
458             {
459                 throw new IllegalStateException("ShutdownMonitorThread already started");
460             }
461             this.exitVm = exitVm;
462         }
463     }
464 
465     public void setKey(String key)
466     {
467         synchronized (this)
468         {
469             if (thread != null && thread.isAlive())
470             {
471                 throw new IllegalStateException("ShutdownMonitorThread already started");
472             }
473             this.key = key;
474         }
475     }
476 
477     public void setPort(int port)
478     {
479         synchronized (this)
480         {
481             if (thread != null && thread.isAlive())
482             {
483                 throw new IllegalStateException("ShutdownMonitorThread already started");
484             }
485             this.port = port;
486         }
487     }
488 
489     protected void start() throws Exception
490     {
491         Thread t = null;
492         
493         synchronized (this)
494         {
495             if (thread != null && thread.isAlive())
496             {
497                 if (DEBUG)
498                     System.err.printf("ShutdownMonitorThread already started");
499                 return; // cannot start it again
500             }
501          
502             thread = new Thread(new ShutdownMonitorRunnable());
503             thread.setDaemon(true);
504             thread.setName("ShutdownMonitor");
505             t = thread;
506         }
507          
508         if (t != null)
509             t.start();
510     }
511 
512 
513     protected boolean isAlive ()
514     {
515         boolean result = false;
516         synchronized (this)
517         {
518             result = (thread != null && thread.isAlive());
519         }
520         return result;
521     }
522     
523  
524     @Override
525     public String toString()
526     {
527         return String.format("%s[port=%d]",this.getClass().getName(),port);
528     }
529 }