blob: 6e6284b57145f5b94f1b56cf4e8ec9c3daadcc8e [file] [log] [blame]
/*
* 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) {
}
}
}
}
}