/* * * Copyright (c) void.fm * All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, this list * of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, this * list of conditions and the following disclaimer in the documentation and/or * other materials provided with the distribution. * * Neither the name void.fm nor the names of its contributors may be * used to endorse or promote products derived from this software without specific * prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * */ package etm.contrib.console; import etm.contrib.console.actions.ActionRegistry; import etm.contrib.console.actions.StatusCodeAction; import etm.contrib.console.standalone.StandaloneConsoleRequest; import etm.contrib.console.standalone.StandaloneConsoleResponse; import etm.contrib.console.util.ConsoleUtil; import etm.contrib.console.util.ResourceAccessor; import etm.core.monitor.EtmMonitor; import etm.core.util.Log; import etm.core.util.LogAdapter; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InterruptedIOException; import java.io.OutputStream; import java.net.ServerSocket; import java.net.Socket; import java.util.Map; import java.util.Stack; /** * HttpConsoleServer is a drop-in http Server that renders EtmMonitor * results. By default it uses 2 worker threads for processing and listens to * port 40000. Use <a href="http://localhost:40000">http://localhost:40000</a> * to access the console. * <p/> * By default this console uses a collapsed view that renders top level measurement * points in an overview page and allows direct access to nested results on * a per-point level basis. * <p/> * By setting {@link #setExpanded(boolean)} to true all measurement points * including all nested ones will be rendered in a single page. * <p/> * This console is not intended for high traffic usage. * <p/> * * @author void.fm * @version $Revision$ */ public class HttpConsoleServer { public static final String DEFAULT_ENCODING = "UTF-8"; private static final LogAdapter LOG = Log.getLog(HttpConsoleServer.class); public static final int DEFAULT_LISTEN_PORT = 40000; private static final int DEFAULT_WORKER_SIZE = 2; protected EtmMonitor etmMonitor; private int listenPort = DEFAULT_LISTEN_PORT; private int workerSize = DEFAULT_WORKER_SIZE; private boolean expanded = false; private ActionRegistry actionRegistry; private Stack workers; private ListenerThread listenerThread; // default actions private ConsoleAction error400 = new StatusCodeAction(400, "Bad request"); private ConsoleAction error404 = new StatusCodeAction(404, "File not found"); private ConsoleAction error500 = new StatusCodeAction(500, "Internal server error"); public HttpConsoleServer(EtmMonitor aEtmMonitor) { etmMonitor = aEtmMonitor; } /** * Overrides default listen port. * * @param aListenPort The new listen port. */ public void setListenPort(int aListenPort) { listenPort = aListenPort; } /** * Enables expanded result rendering. Be aware that large or deep * performance measurement results may be hard to read in expanded * view. * * @param aExpanded True to enable expanded views. */ public void setExpanded(boolean aExpanded) { expanded = aExpanded; } /** * Overrides default worker size. * * @param aWorkerSize The worker size, has to be 2 or more. * @throws IllegalArgumentException Thrown if size is lower than two. */ public void setWorkerSize(int aWorkerSize) { if (workerSize < 2) { throw new IllegalArgumentException("Worker size has to be higher than two."); } workerSize = aWorkerSize; } public void start() { if (etmMonitor == null) { throw new IllegalStateException("Missing EtmMonitor reference."); } actionRegistry = new ActionRegistry(new ResourceAccessor(), expanded); // create our worker pool synchronized (this) { workers = new Stack(); for (int i = 0; i < workerSize; i++) { ConsoleWorker item = new ConsoleWorker("JETM HTTP Console Worker - " + (i + 1)); item.setDaemon(true); item.start(); workers.push(item); } } try { ServerSocket socket = new ServerSocket(listenPort); listenerThread = new ListenerThread(socket); listenerThread.start(); LOG.info("Started JETM console server listening at " + socket.toString()); } catch (IOException e) { throw new ConsoleException(e); } } public void stop() { listenerThread.shutdown(); synchronized (this) { for (int i = 0; i < workers.size(); i++) { ConsoleWorker worker = (ConsoleWorker) workers.get(i); worker.shouldStop(); } workers.clear(); } } protected ConsoleWorker getWorker() { synchronized (this) { if (!workers.isEmpty()) { return (ConsoleWorker) workers.pop(); } else { return null; } } } protected void returnWorker(ConsoleWorker aConsoleWorker) { synchronized (this) { workers.push(aConsoleWorker); } } class ListenerThread extends Thread { private boolean shouldRun = true; private ServerSocket socket; public ListenerThread(ServerSocket aSocket) { super("JETM HTTP Console Listener - Port " + listenPort); socket = aSocket; } public void run() { while (shouldRun) { try { Socket clientSocket = socket.accept(); ConsoleWorker worker = getWorker(); if (worker != null) { worker.setClientSocket(clientSocket); } else { // process in current thread new ConsoleWorker().process(clientSocket); } } catch (Exception e) { if (shouldRun) { LOG.warn("Error processing HTTP request", e); } else { // don't do anything. we are shutting down probably // so there is no need to LOG the exception LOG.debug("Error during shutdown" + e.toString()); } } } } public void shutdown() { shouldRun = false; if (socket != null) { try { socket.close(); } catch (IOException e) { // ignored } } socket = null; } } /** * A Worker that processes incoming HTTP reqests. */ class ConsoleWorker extends Thread { private Socket clientSocket; private boolean shouldRun = true; public ConsoleWorker() { super(); } public ConsoleWorker(String workerName) { super(workerName); } public void setClientSocket(Socket aClientSocket) { clientSocket = aClientSocket; synchronized (this) { notifyAll(); } } public void shouldStop() { shouldRun = false; synchronized (this) { notifyAll(); } } public void run() { while (shouldRun) { try { synchronized (this) { try { wait(); } catch (InterruptedException e) { // ignored } } if (shouldRun) { process(clientSocket); } } catch (InterruptedIOException e) { // ignored, just close socket } catch (Exception e) { LOG.warn("Error processing HTTP request", e); } finally { returnWorker(this); } } } protected void process(Socket aClientSocket) throws IOException { BufferedInputStream inputStream = null; try { aClientSocket.setSoTimeout(15 * 1000); inputStream = new BufferedInputStream(aClientSocket.getInputStream()); byte[] temp = new byte[3192]; int i = 0; while (i < temp.length) { int r = inputStream.read(temp, i, temp.length - i); if (r == -1) { return; } else { // extract first line only and delegate // to process for (int j = i; j < i + r; j++) { if (temp[j] == '\r' || temp[j] == '\n') { int endOfLine = i + j; BufferedOutputStream out = new BufferedOutputStream(aClientSocket.getOutputStream()); process(out, temp, endOfLine); out.flush(); out.close(); return; } } i += r; } } } finally { if (inputStream != null) { try { inputStream.close(); } catch (IOException e) { // ignored } } try { aClientSocket.close(); } catch (IOException e) { // ignored } } } protected void process(OutputStream out, byte[] aTemp, int endOfLine) throws IOException { StandaloneConsoleRequest consoleRequest = new StandaloneConsoleRequest(etmMonitor); // if we don't find an action it is a bad request ConsoleAction action = error400; // do we have an GET request try { if (endOfLine >= 5 && (aTemp[0] == 'G') && (aTemp[1] == 'E') && (aTemp[2] == 'T')) { // extract request name and parameters int endOfRequestString = 0; int parameterStart = 0; for (int i = 4; i < endOfLine; i++) { if (aTemp[i] == ' ') { endOfRequestString = i; break; } else if (aTemp[i] == '?' && parameterStart == 0) { parameterStart = i; } } if (endOfRequestString > 0) { String requestName; // do we have get parameters in our request if (parameterStart > 0) { requestName = new String(aTemp, 4, parameterStart - 4, DEFAULT_ENCODING); Map parameters = ConsoleUtil.extractRequestParameters(aTemp, parameterStart, endOfRequestString); consoleRequest.setRequestParameters(parameters); } else { requestName = new String(aTemp, 4, endOfRequestString - 4, DEFAULT_ENCODING); } action = actionRegistry.getAction(requestName); if (action == null) { // unsupported request action = error404; } } } } catch (Exception e) { LOG.warn("Error processing HTTP request", e); action = error500; } StandaloneConsoleResponse consoleResponse = new StandaloneConsoleResponse(out); LOG.debug("Processing " + action.getClass()); action.execute(consoleRequest, consoleResponse); consoleResponse.flush(); } } protected int getListenPort() { return listenPort; } protected int getWorkerSize() { return workerSize; } protected boolean isExpanded() { return expanded; } }