/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.sling.launchpad.app; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.LineNumberReader; import java.io.OutputStreamWriter; import java.lang.management.LockInfo; import java.lang.management.ManagementFactory; import java.lang.management.MonitorInfo; import java.lang.management.ThreadInfo; import java.lang.management.ThreadMXBean; import java.math.BigInteger; import java.net.ConnectException; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.Set; /** * The <code>ControlListener</code> class is a helper class for the {@link Main} * class to support in Sling standalone application process communication. This * class implements the client and server sides of a TCP/IP based communication * channel to control a running Sling application. * <p> * The server side listens for commands on a configurable host and port &endash; * <code>localhost:63000</code> by default &endash; supporting the following * commands: * <table> * <tr> * <th>Command</th> * <th>Description</th> * </tr> * <tr> * <td><code>status</code></td> * <td>Request status information. Currently only <i>OK</i> is sent back. If no * connection can be created to the server the client assumes Sling is not * running.</td> * </tr> * <tr> * <td><code>stop</code></td> * <td>Requests Sling to shutdown.</td> * </tr> * </table> */ class ControlListener implements Runnable { // command sent by the client to cause Sling to shutdown static final String COMMAND_STOP = "stop"; // command sent by the client to check for the status of the server static final String COMMAND_STATUS = "status"; // command sent by the client to request a thread dump static final String COMMAND_THREADS = "threads"; // the response sent by the server if the command executed successfully private static final String RESPONSE_OK = "OK"; // the status response sent by the server when shutting down private static final String RESPONSE_STOPPING = "STOPPING"; // The default interface to listen on private static final String DEFAULT_LISTEN_INTERFACE = "127.0.0.1"; // The default port to listen on and to connect to - we select it randomly private static final int DEFAULT_LISTEN_PORT = 0; // The reference to the Main class to shutdown on request private final Main slingMain; private final String listenSpec; private String secretKey; private InetSocketAddress socketAddress; private volatile Thread shutdownThread = null; /** * Creates an instance of this control support class. * <p> * The host (name or address) and port number of the socket is defined by * the <code>listenSpec</code> parameter. This parameter is defined as * <code>[ host ":" ] port</code>. If the parameter is empty or * <code>null</code> it defaults to <i>localhost:0</i>. If the host name * is missing it defaults to <i>localhost</i>. * * @param slingMain The Main class reference. This is only required if this * instance is used for the server side to listen for remote stop * commands. Otherwise this argument may be <code>null</code>. * @param listenSpec The specification for the host and port for the socket * connection. See above for the format of this parameter. */ ControlListener(final Main slingMain, final String listenSpec) { this.slingMain = slingMain; this.listenSpec = listenSpec; // socketAddress = this.getSocketAddress(listenSpec, selectNewPort); } /** * Implements the server side of the control connection starting a thread * listening on the host and port configured on setup of this instance. */ boolean listen() { final File configFile = getConfigFile(); if (configFile.canRead() && statusServer() == 0) { // server already running, fail Main.error("Sling already active in " + this.slingMain.getSlingHome(), null); return false; } configFile.delete(); final Thread listener = new Thread(this); listener.setDaemon(true); listener.setName("Apache Sling Control Listener (inactive)"); listener.start(); return true; } /** * Implements the client side of the control connection sending the command * to shutdown Sling. */ int shutdownServer() { return sendCommand(COMMAND_STOP); } /** * Implements the client side of the control connection sending the command * to check whether Sling is active. */ int statusServer() { return sendCommand(COMMAND_STATUS); } /** * Implements the client side of the control connection sending the command * to retrieve a thread dump. */ int dumpThreads() { return sendCommand(COMMAND_THREADS); } // ---------- Runnable interface /** * Implements the server thread receiving commands from clients and acting * upon them. */ @Override public void run() { this.configure(false); final ServerSocket server; try { server = new ServerSocket(); server.bind(this.socketAddress); writePortToConfigFile(getConfigFile(), new InetSocketAddress(server.getInetAddress(), server.getLocalPort()), this.secretKey); Thread.currentThread().setName( "Apache Sling Control Listener@" + server.getInetAddress() + ":" + server.getLocalPort()); Main.info("Apache Sling Control Listener started", null); } catch (final IOException ioe) { Main.error("Failed to start Apache Sling Control Listener", ioe); return; } long delay = 0; try { while (true) { final Socket s; try { s = server.accept(); } catch (IOException ioe) { // accept terminated, most probably due to Socket.close() // just end the loop and exit break; } // delay processing after unsuccessful attempts if (delay > 0) { Main.info(s.getRemoteSocketAddress() + ": Delay: " + (delay / 1000), null); try { Thread.sleep(delay); } catch (InterruptedException e) { } } try { final String commandLine = readLine(s); if (commandLine == null) { final String msg = "ERR: missing command"; writeLine(s, msg); continue; } final int blank = commandLine.indexOf(' '); if (blank < 0) { final String msg = "ERR: missing key"; writeLine(s, msg); continue; } if (!secretKey.equals(commandLine.substring(0, blank))) { final String msg = "ERR: wrong key"; writeLine(s, msg); delay = (delay > 0) ? delay * 2 : 1000L; continue; } final String command = commandLine.substring(blank + 1); Main.info(s.getRemoteSocketAddress() + ">" + command, null); if (COMMAND_STOP.equals(command)) { if (this.shutdownThread != null) { writeLine(s, RESPONSE_STOPPING); } else { this.shutdownThread = new Thread("Apache Sling Control Listener: Shutdown") { @Override public void run() { slingMain.doStop(); try { server.close(); } catch (final IOException ignore) { } } }; this.shutdownThread.start(); writeLine(s, RESPONSE_OK); } } else if (COMMAND_STATUS.equals(command)) { writeLine(s, (this.shutdownThread == null) ? RESPONSE_OK : RESPONSE_STOPPING); } else if (COMMAND_THREADS.equals(command)) { dumpThreads(s); } else { final String msg = "ERR:" + command; writeLine(s, msg); } } finally { try { s.close(); } catch (IOException ignore) { } } } } catch (final IOException ioe) { Main.error("Failure reading from client", ioe); } finally { try { server.close(); } catch (final IOException ignore) { } } getConfigFile().delete(); // everything has stopped and when this thread terminates, // the VM should stop. If there are still some non-daemon threads // active, this will not happen, so we force this here ... Main.info("Apache Sling terminated, exiting Java VM", null); this.slingMain.terminateVM(0); } // ---------- socket support private void dumpThreads(final Socket socket) throws IOException { final ThreadMXBean threadBean = ManagementFactory.getThreadMXBean(); final ThreadInfo[] threadInfos = threadBean.dumpAllThreads(true, true); for (ThreadInfo thread : threadInfos) { printThread(socket, thread); // add locked synchronizers final LockInfo[] locks = thread.getLockedSynchronizers(); writeLine(socket, "-"); writeLine(socket, "- Locked ownable synchronizers:"); if (locks.length > 0) { for (LockInfo li : locks) { writeLine(socket, String.format("- - locked %s", formatLockInfo( li.getClassName(), li.getIdentityHashCode() ) )); } } else { writeLine(socket, "- - None"); } // empty separator line writeLine(socket, "-"); } final long[] deadLocked; if (threadBean.isSynchronizerUsageSupported()) { deadLocked = threadBean.findDeadlockedThreads(); } else { deadLocked = threadBean.findMonitorDeadlockedThreads(); } if (deadLocked != null) { final ThreadInfo[] dl = threadBean.getThreadInfo(deadLocked, true, true); final Set<ThreadInfo> dlSet = new HashSet<ThreadInfo>(Arrays.asList(dl)); int deadlockCount = 0; for (ThreadInfo current : dl) { if (dlSet.remove(current)) { // find and record a single deadlock ArrayList<ThreadInfo> loop = new ArrayList<ThreadInfo>(); do { loop.add(current); for (ThreadInfo cand : dl) { if (cand.getThreadId() == current.getLockOwnerId()) { current = (dlSet.remove(cand)) ? cand : null; break; } } } while (current != null); deadlockCount++; // print the deadlock writeLine(socket, "-Found one Java-level deadlock:"); writeLine(socket, "-============================="); for (ThreadInfo thread : loop) { writeLine(socket, String.format("-\"%s\" #%d", thread.getThreadName(), thread.getThreadId() )); writeLine(socket, String.format("- waiting on %s", formatLockInfo( thread.getLockInfo().getClassName(), thread.getLockInfo().getIdentityHashCode() ) )); writeLine(socket, String.format("- which is held by \"%s\" #%d", thread.getLockOwnerName(), thread.getLockOwnerId() )); } writeLine(socket, "-"); writeLine(socket, "-Java stack information for the threads listed above:"); writeLine(socket, "-==================================================="); for (ThreadInfo thread : loop) { printThread(socket, thread); } writeLine(socket, "-"); } } // "Thread-8": // waiting to lock monitor 7f89fb80da08 (object 7f37a0968, a java.lang.Object), // which is held by "Thread-7" // "Thread-7": // waiting to lock monitor 7f89fb80b0b0 (object 7f37a0958, a java.lang.Object), // which is held by "Thread-8" writeLine(socket, String.format("-Found %d deadlocks.", deadlockCount )); } writeLine(socket, RESPONSE_OK); } private String formatLockInfo(final String className, final int objectId) { return String.format("<%08x> (a %s)", objectId, className); } private void printThread(final Socket socket, final ThreadInfo thread) throws IOException { writeLine(socket, String.format("-\"%s\" #%d", thread.getThreadName(), thread.getThreadId() )); writeLine(socket, String.format("- java.lang.Thread.State: %s", thread.getThreadState() )); final MonitorInfo[] monitors = thread.getLockedMonitors(); final StackTraceElement[] trace = thread.getStackTrace(); for (int i=0; i < trace.length; i++) { StackTraceElement ste = trace[i]; if (ste.isNativeMethod()) { writeLine(socket, String.format("- at %s.%s(Native Method)", ste.getClassName(), ste.getMethodName() )); } else { writeLine(socket, String.format("- at %s.%s(%s:%d)", ste.getClassName(), ste.getMethodName(), ste.getFileName(), ste.getLineNumber() )); } if (i == 0 && thread.getLockInfo() != null) { writeLine(socket, String.format("- - waiting on %s%s", formatLockInfo( thread.getLockInfo().getClassName(), thread.getLockInfo().getIdentityHashCode() ), (thread.getLockOwnerId() >= 0) ? String.format(" owned by \"%s\" #%d", thread.getLockOwnerName(), thread.getLockOwnerId() ):"" )); } for (MonitorInfo mi : monitors) { if (i == mi.getLockedStackDepth()) { writeLine(socket, String.format("- - locked %s", formatLockInfo( mi.getClassName(), mi.getIdentityHashCode() ) )); } } } } /** * Sends the given command to the server indicated by the configured * socket address and logs the reply. * * @param command The command to send * * @return A code indicating success of sending the command. */ private int sendCommand(final String command) { if (configure(true)) { if (this.secretKey == null) { Main.info("Missing secret key to protect sending '" + command + "' to " + this.socketAddress, null); return 4; // LSB code for unknown status } Socket socket = null; try { socket = new Socket(); socket.connect(this.socketAddress); writeLine0(socket, this.secretKey + " " + command); final String result = readLine(socket); Main.info("Sent '" + command + "' to " + this.socketAddress + ": " + result, null); return 0; // LSB code for everything's fine } catch (final ConnectException ce) { Main.info("No Apache Sling running at " + this.socketAddress, null); return 3; // LSB code for programm not running } catch (final IOException ioe) { Main.error("Failed sending '" + command + "' to " + this.socketAddress, ioe); return 1; // LSB code for programm dead } finally { if (socket != null) { try { socket.close(); } catch (IOException ignore) { } } } } Main.info("No socket address to send '" + command + "' to", null); return 4; // LSB code for unknown status } private String readLine(final Socket socket) throws IOException { final BufferedReader br = new BufferedReader(new InputStreamReader( socket.getInputStream(), "UTF-8")); StringBuilder b = new StringBuilder(); boolean more = true; while (more) { String s = br.readLine(); if (s != null && s.startsWith("-")) { s = s.substring(1); } else { more = false; } if (b.length() > 0) { b.append("\r\n"); } b.append(s); } return b.toString(); } private void writeLine(final Socket socket, final String line) throws IOException { Main.info(socket.getRemoteSocketAddress() + "<" + line, null); this.writeLine0(socket, line); } private void writeLine0(final Socket socket, final String line) throws IOException { final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "UTF-8")); bw.write(line); bw.write("\r\n"); bw.flush(); } /** * Read the port from the config file * @return The port or null */ private boolean configure(final boolean fromConfigFile) { boolean result = false; if (fromConfigFile) { final File configFile = this.getConfigFile(); if (configFile.canRead()) { try ( final LineNumberReader lnr = new LineNumberReader(new FileReader(configFile))) { this.socketAddress = getSocketAddress(lnr.readLine()); this.secretKey = lnr.readLine(); result = true; } catch (final IOException ignore) { // ignore } } } else { this.socketAddress = getSocketAddress(this.listenSpec); this.secretKey = generateKey(); result = true; } return result; } private static String generateKey() { return new BigInteger(165, new SecureRandom()).toString(32); } /** * Return the control port file */ private File getConfigFile() { final File configDir = new File(this.slingMain.getSlingHome(), "conf"); return new File(configDir, "controlport"); } private static InetSocketAddress getSocketAddress(String listenSpec) { try { final String address; final int port; if (listenSpec == null) { address = DEFAULT_LISTEN_INTERFACE; port = DEFAULT_LISTEN_PORT; } else { final int colon = listenSpec.indexOf(':'); if (colon < 0) { address = DEFAULT_LISTEN_INTERFACE; port = Integer.parseInt(listenSpec); } else { address = listenSpec.substring(0, colon); port = Integer.parseInt(listenSpec.substring(colon + 1)); } } final InetSocketAddress addr = new InetSocketAddress(address, port); if (!addr.isUnresolved()) { return addr; } Main.error("Unknown host in '" + listenSpec, null); } catch (final NumberFormatException nfe) { Main.error("Cannot parse port number from '" + listenSpec + "'", null); } return null; } private static void writePortToConfigFile(final File configFile, final InetSocketAddress socketAddress, final String secretKey) { configFile.getParentFile().mkdirs(); FileWriter fw = null; try { fw = new FileWriter(configFile); fw.write(socketAddress.getAddress().getHostAddress()); fw.write(':'); fw.write(String.valueOf(socketAddress.getPort())); fw.write('\n'); fw.write(secretKey); fw.write('\n'); } catch (final IOException ignore) { // ignore } finally { if (fw != null) { try { fw.close(); } catch (final IOException ignore) { } } } } }