Package org.apache.sling.launchpad.app

Source Code of org.apache.sling.launchpad.app.ControlListener

/*
* 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.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Random;
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.
     */
    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 unsuccessfull 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") {
                                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()) {
                FileReader fr = null;
                try {
                    fr = new FileReader(configFile);
                    final LineNumberReader lnr = new LineNumberReader(fr);
                    this.socketAddress = getSocketAddress(lnr.readLine());
                    this.secretKey = lnr.readLine();
                    result = true;
                } catch (final IOException ignore) {
                    // ignore
                } finally {
                    if (fr != null) {
                        try {
                            fr.close();
                        } catch (final IOException ignore) {
                        }
                    }
                }
            }
        } else {
            this.socketAddress = getSocketAddress(this.listenSpec);
            this.secretKey = generateKey();
            result = true;
        }

        return result;
    }

    private static String generateKey() {
        String keys = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz0123456789";
        int len = keys.length();
        Random r = new Random(System.currentTimeMillis() + 33 * System.nanoTime());
        char[] c = new char[32];
        for (int i = 0; i < c.length; i++) {
            c[i] = keys.charAt(r.nextInt(len));
        }
        return new String(c);
    }

    /**
     * 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) {
                }
            }
        }
    }
}
TOP

Related Classes of org.apache.sling.launchpad.app.ControlListener

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.