| // |
| // ======================================================================== |
| // Copyright (c) 1995-2017 Mort Bay Consulting Pty. Ltd. |
| // ------------------------------------------------------------------------ |
| // All rights reserved. This program and the accompanying materials |
| // are made available under the terms of the Eclipse Public License v1.0 |
| // and Apache License v2.0 which accompanies this distribution. |
| // |
| // The Eclipse Public License is available at |
| // http://www.eclipse.org/legal/epl-v10.html |
| // |
| // The Apache License v2.0 is available at |
| // http://www.opensource.org/licenses/apache2.0.php |
| // |
| // You may elect to redistribute this code under either of these licenses. |
| // ======================================================================== |
| // |
| |
| package org.eclipse.jetty.server; |
| |
| import java.io.IOException; |
| import java.io.InputStreamReader; |
| import java.io.LineNumberReader; |
| import java.io.OutputStream; |
| import java.net.InetAddress; |
| import java.net.InetSocketAddress; |
| import java.net.ServerSocket; |
| import java.net.Socket; |
| import java.nio.charset.StandardCharsets; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Set; |
| import java.util.function.Predicate; |
| |
| import org.eclipse.jetty.util.IO; |
| import org.eclipse.jetty.util.component.Destroyable; |
| import org.eclipse.jetty.util.component.LifeCycle; |
| import org.eclipse.jetty.util.thread.ShutdownThread; |
| |
| /** |
| * Shutdown/Stop Monitor thread. |
| * <p> |
| * 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 request |
| * authenticated with the key given by the STOP.KEY system parameter |
| * (defaults to "eclipse") for admin requests. |
| * <p> |
| * If the stop port is set to zero, then a random port is assigned and the |
| * port number is printed to stdout. |
| * <p> |
| * Commands "stop" and "status" are currently supported. |
| */ |
| public class ShutdownMonitor |
| { |
| // Implementation of safe lazy init, using Initialization on Demand Holder technique. |
| private static class Holder |
| { |
| static ShutdownMonitor instance = new ShutdownMonitor(); |
| } |
| |
| public static ShutdownMonitor getInstance() |
| { |
| return Holder.instance; |
| } |
| |
| protected static void reset() |
| { |
| Holder.instance = new ShutdownMonitor(); |
| } |
| |
| public static void register(LifeCycle... lifeCycles) |
| { |
| getInstance().addLifeCycles(lifeCycles); |
| } |
| |
| public static void deregister(LifeCycle lifeCycle) |
| { |
| getInstance().removeLifeCycle(lifeCycle); |
| } |
| |
| public static boolean isRegistered(LifeCycle lifeCycle) |
| { |
| return getInstance().containsLifeCycle(lifeCycle); |
| } |
| |
| private final Set<LifeCycle> _lifeCycles = new LinkedHashSet<>(); |
| private boolean debug; |
| private final String host; |
| private int port; |
| private String key; |
| private boolean exitVm; |
| private boolean alive; |
| |
| /** |
| * Creates a ShutdownMonitor using configuration from the System properties. |
| * <p> |
| * <code>STOP.PORT</code> = the port to listen on (empty, null, or values less than 0 disable the stop ability)<br> |
| * <code>STOP.KEY</code> = the magic key/passphrase to allow the stop (defaults to "eclipse")<br> |
| * <p> |
| * Note: server socket will only listen on localhost, and a successful stop will issue a System.exit() call. |
| */ |
| private ShutdownMonitor() |
| { |
| this.debug = System.getProperty("DEBUG") != null; |
| this.host = System.getProperty("STOP.HOST", "127.0.0.1"); |
| this.port = Integer.parseInt(System.getProperty("STOP.PORT", "-1")); |
| this.key = System.getProperty("STOP.KEY", null); |
| this.exitVm = true; |
| } |
| |
| private void addLifeCycles(LifeCycle... lifeCycles) |
| { |
| synchronized (this) |
| { |
| _lifeCycles.addAll(Arrays.asList(lifeCycles)); |
| } |
| } |
| |
| private void removeLifeCycle(LifeCycle lifeCycle) |
| { |
| synchronized (this) |
| { |
| _lifeCycles.remove(lifeCycle); |
| } |
| } |
| |
| private boolean containsLifeCycle(LifeCycle lifeCycle) |
| { |
| synchronized (this) |
| { |
| return _lifeCycles.contains(lifeCycle); |
| } |
| } |
| |
| private void debug(String format, Object... args) |
| { |
| if (debug) |
| System.err.printf("[ShutdownMonitor] " + format + "%n", args); |
| } |
| |
| private void debug(Throwable t) |
| { |
| if (debug) |
| t.printStackTrace(System.err); |
| } |
| |
| public String getKey() |
| { |
| synchronized (this) |
| { |
| return key; |
| } |
| } |
| |
| public int getPort() |
| { |
| synchronized (this) |
| { |
| return port; |
| } |
| } |
| |
| public boolean isExitVm() |
| { |
| synchronized (this) |
| { |
| return exitVm; |
| } |
| } |
| |
| public void setDebug(boolean flag) |
| { |
| this.debug = flag; |
| } |
| |
| /** |
| * @param exitVm true to exit the VM on shutdown |
| */ |
| public void setExitVm(boolean exitVm) |
| { |
| synchronized (this) |
| { |
| if (alive) |
| throw new IllegalStateException("ShutdownMonitor already started"); |
| this.exitVm = exitVm; |
| } |
| } |
| |
| public void setKey(String key) |
| { |
| synchronized (this) |
| { |
| if (alive) |
| throw new IllegalStateException("ShutdownMonitor already started"); |
| this.key = key; |
| } |
| } |
| |
| public void setPort(int port) |
| { |
| synchronized (this) |
| { |
| if (alive) |
| throw new IllegalStateException("ShutdownMonitor already started"); |
| this.port = port; |
| } |
| } |
| |
| protected void start() throws Exception |
| { |
| synchronized (this) |
| { |
| if (alive) |
| { |
| debug("Already started"); |
| return; // cannot start it again |
| } |
| ServerSocket serverSocket = listen(); |
| if (serverSocket != null) |
| { |
| alive = true; |
| Thread thread = new Thread(new ShutdownMonitorRunnable(serverSocket)); |
| thread.setDaemon(true); |
| thread.setName("ShutdownMonitor"); |
| thread.start(); |
| } |
| } |
| } |
| |
| private void stop() |
| { |
| synchronized (this) |
| { |
| alive = false; |
| notifyAll(); |
| } |
| } |
| |
| // For test purposes only. |
| void await() throws InterruptedException |
| { |
| synchronized (this) |
| { |
| while (alive) |
| { |
| wait(); |
| } |
| } |
| } |
| |
| protected boolean isAlive() |
| { |
| synchronized (this) |
| { |
| return alive; |
| } |
| } |
| |
| private ServerSocket listen() |
| { |
| int port = getPort(); |
| if (port < 0) |
| { |
| debug("Not enabled (port < 0): %d", port); |
| return null; |
| } |
| |
| String key = getKey(); |
| try |
| { |
| ServerSocket serverSocket = new ServerSocket(); |
| serverSocket.setReuseAddress(true); |
| serverSocket.bind(new InetSocketAddress(InetAddress.getByName(host), port)); |
| if (port == 0) |
| { |
| port = serverSocket.getLocalPort(); |
| System.out.printf("STOP.PORT=%d%n", port); |
| setPort(port); |
| } |
| |
| if (key == null) |
| { |
| key = Long.toString((long)(Long.MAX_VALUE * Math.random() + this.hashCode() + System.currentTimeMillis()), 36); |
| System.out.printf("STOP.KEY=%s%n", key); |
| setKey(key); |
| } |
| |
| return serverSocket; |
| } |
| catch (Throwable x) |
| { |
| debug(x); |
| System.err.println("Error binding ShutdownMonitor to port " + port + ": " + x.toString()); |
| return null; |
| } |
| finally |
| { |
| // establish the port and key that are in use |
| debug("STOP.PORT=%d", port); |
| debug("STOP.KEY=%s", key); |
| } |
| } |
| |
| @Override |
| public String toString() |
| { |
| return String.format("%s[port=%d,alive=%b]", this.getClass().getName(), getPort(), isAlive()); |
| } |
| |
| /** |
| * Thread for listening to STOP.PORT for command to stop Jetty. |
| * If ShutdownMonitor.exitVm is true, then System.exit will also be |
| * called after the stop. |
| */ |
| private class ShutdownMonitorRunnable implements Runnable |
| { |
| private final ServerSocket serverSocket; |
| |
| private ShutdownMonitorRunnable(ServerSocket serverSocket) |
| { |
| this.serverSocket = serverSocket; |
| } |
| |
| @Override |
| public void run() |
| { |
| debug("Started"); |
| try |
| { |
| String key = getKey(); |
| while (true) |
| { |
| try (Socket socket = serverSocket.accept()) |
| { |
| LineNumberReader reader = new LineNumberReader(new InputStreamReader(socket.getInputStream())); |
| String receivedKey = reader.readLine(); |
| if (!key.equals(receivedKey)) |
| { |
| debug("Ignoring command with incorrect key: %s", receivedKey); |
| continue; |
| } |
| |
| String cmd = reader.readLine(); |
| debug("command=%s", cmd); |
| OutputStream out = socket.getOutputStream(); |
| boolean exitVm = isExitVm(); |
| |
| if ("stop".equalsIgnoreCase(cmd)) //historic, for backward compatibility |
| { |
| //Stop the lifecycles, only if they are registered with the ShutdownThread, only destroying if vm is exiting |
| debug("Performing stop command"); |
| stopLifeCycles(ShutdownThread::isRegistered, exitVm); |
| |
| // Reply to client |
| debug("Informing client that we are stopped"); |
| informClient(out, "Stopped\r\n"); |
| |
| if (!exitVm) |
| break; |
| |
| // Kill JVM |
| debug("Killing JVM"); |
| System.exit(0); |
| } |
| else if ("forcestop".equalsIgnoreCase(cmd)) |
| { |
| debug("Performing forced stop command"); |
| stopLifeCycles(l -> true, exitVm); |
| |
| // Reply to client |
| debug("Informing client that we are stopped"); |
| informClient(out, "Stopped\r\n"); |
| |
| if (!exitVm) |
| break; |
| |
| // Kill JVM |
| debug("Killing JVM"); |
| System.exit(0); |
| } |
| else if ("stopexit".equalsIgnoreCase(cmd)) |
| { |
| debug("Performing stop and exit commands"); |
| stopLifeCycles(ShutdownThread::isRegistered, true); |
| |
| // Reply to client |
| debug("Informing client that we are stopped"); |
| informClient(out, "Stopped\r\n"); |
| |
| debug("Killing JVM"); |
| System.exit(0); |
| } |
| else if ("exit".equalsIgnoreCase(cmd)) |
| { |
| debug("Killing JVM"); |
| System.exit(0); |
| } |
| else if ("status".equalsIgnoreCase(cmd)) |
| { |
| // Reply to client |
| informClient(out, "OK\r\n"); |
| } |
| } |
| catch (Throwable x) |
| { |
| debug(x); |
| } |
| } |
| } |
| catch (Throwable x) |
| { |
| debug(x); |
| } |
| finally |
| { |
| IO.close(serverSocket); |
| stop(); |
| debug("Stopped"); |
| } |
| } |
| |
| private void informClient(OutputStream out, String message) throws IOException |
| { |
| out.write(message.getBytes(StandardCharsets.UTF_8)); |
| out.flush(); |
| } |
| |
| private void stopLifeCycles(Predicate<LifeCycle> predicate, boolean destroy) |
| { |
| List<LifeCycle> lifeCycles = new ArrayList<>(); |
| synchronized (this) |
| { |
| lifeCycles.addAll(_lifeCycles); |
| } |
| |
| for (LifeCycle l : lifeCycles) |
| { |
| try |
| { |
| if (l.isStarted() && predicate.test(l)) |
| l.stop(); |
| |
| if ((l instanceof Destroyable) && destroy) |
| ((Destroyable)l).destroy(); |
| } |
| catch (Throwable x) |
| { |
| debug(x); |
| } |
| } |
| } |
| } |
| } |