/**
* Licensed to Cloudera, Inc. under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Cloudera, Inc. 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 com.cloudera.util;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.BindException;
import java.net.InetSocketAddress;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.util.ReflectionUtils;
import org.mortbay.jetty.Server;
import org.mortbay.jetty.nio.SelectChannelConnector;
import org.mortbay.jetty.security.SslSocketConnector;
import org.mortbay.jetty.webapp.WebAppContext;
import org.mortbay.util.MultiException;
import com.google.common.base.Preconditions;
// jon: This is a shamelessly hacked version of the Http status server from the jobtracker,
// simplified for my needs. Originally from apache licensed hadoop 0.18.3, o.a.h.mapred.StatusHttpServer
/**
* Create a Jetty embedded server to answer http requests. The primary goal is
* to serve up status information for the server. There are three contexts:
* "/logs/" -> points to the log directory "/static/" -> points to common static
* files (src/webapps/static) "/" -> the jsp server code from
* (src/webapps/<name>)
*/
public class StatusHttpServer {
private Server webServer;
private SelectChannelConnector channelConnector;
private SslSocketConnector sslConnector;
private boolean findPort;
private WebAppContext webAppContext;
private static final Log LOG = LogFactory.getLog(StatusHttpServer.class
.getName());
/**
* Create a status server on the given port. The jsp scripts are taken from
* src/webapps/<name>.
*
* @param name
* The name of the server
* @param port
* The port to use on the server
* @param findPort
* whether the server should start at the given port and increment by
* 1 until it finds a free port.
*/
public StatusHttpServer(String name, String webAppsPath, String bindAddress,
int port, boolean findPort) throws IOException {
webServer = new org.mortbay.jetty.Server();
this.findPort = findPort;
channelConnector = new SelectChannelConnector();
channelConnector.setPort(port);
channelConnector.setHost(bindAddress);
webServer.addConnector(channelConnector);
String appDir = webAppsPath;
// set up the context for "/" jsp files
String webapp = new File(appDir, name).getAbsolutePath();
LOG.info("starting web app in directory: " + webapp);
webAppContext = new WebAppContext(webapp, "/");
webServer.setHandler(webAppContext);
addServlet("stacks", "/stacks", StackServlet.class);
}
/**
* Sets a value in the webapp context. These values are available to the jsp
* pages as "application.getAttribute(name)".
*
* @param name
* The name of the attribute
* @param value
* The value of the attribute
*/
public void setAttribute(String name, Object value) {
webAppContext.setAttribute(name, value);
}
/**
* Add a servlet in the server.
*
* @param name
* The name of the servlet (can be passed as null)
* @param pathSpec
* The path spec for the servlet
* @param servletClass
* The servlet class
*/
public <T extends HttpServlet> void addServlet(String name, String pathSpec,
Class<T> servletClass) {
WebAppContext context = webAppContext;
if (name == null) {
context.addServlet(pathSpec, servletClass.getName());
} else {
context.addServlet(servletClass, pathSpec);
}
}
/**
* Get the value in the webapp context.
*
* @param name
* The name of the attribute
* @return The value of the attribute
*/
public Object getAttribute(String name) {
return webAppContext.getAttribute(name);
}
/**
* Get the port that the server is on
*
* @return the port
*/
public int getPort() {
return channelConnector.getPort();
}
/**
* Configure an ssl listener on the server.
*
* @param addr
* address to listen on
* @param keystore
* location of the keystore
* @param storPass
* password for the keystore
* @param keyPass
* password for the key
*/
public void addSslListener(InetSocketAddress addr, String keystore,
String storPass, String keyPass) throws IOException {
if (sslConnector != null || webServer.isStarted()) {
throw new IOException("Failed to add ssl listener");
}
sslConnector = new SslSocketConnector();
sslConnector.setHost(addr.getHostName());
sslConnector.setPort(addr.getPort());
sslConnector.setKeystore(keystore);
sslConnector.setPassword(storPass);
sslConnector.setKeyPassword(keyPass);
webServer.addConnector(sslConnector);
}
/**
* Start the server. Does not wait for the server to start.
*/
public void start() throws IOException {
try {
while (true) {
try {
webServer.start();
break;
} catch (MultiException ex) {
// if the multi exception contains ONLY a bind exception,
// then try the next port number.
boolean needNewPort = false;
if (ex.size() == 1) {
Throwable sub = ex.getThrowable(0);
if (sub instanceof BindException) {
if (!findPort)
throw (BindException) sub; // java.net.BindException
needNewPort = true;
}
}
if (!needNewPort)
throw ex;
channelConnector.setPort(channelConnector.getPort() + 1);
}
}
} catch (IOException ie) {
throw ie;
} catch (Exception e) {
IOException ie = new IOException("Problem starting http server");
ie.initCause(e);
throw ie;
}
}
/**
* stop the server
*/
public void stop() throws Exception {
webServer.stop();
}
/**
* A very simple servlet to serve up a text representation of the current
* stack traces. It both returns the stacks to the caller and logs them.
* Currently the stack traces are done sequentially rather than exactly the
* same data.
*/
public static class StackServlet extends HttpServlet {
private static final long serialVersionUID = -6284183679759467039L;
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
OutputStream outStream = response.getOutputStream();
ReflectionUtils.printThreadInfo(new PrintWriter(outStream), "");
outStream.close();
ReflectionUtils.logThreadInfo(LOG, "jsp requested", 1);
}
}
/**
* Test harness to get precompiled jsps working.
*
* @param argv
*/
public static void main(String[] argv) {
Preconditions.checkArgument(argv.length == 3);
String name = argv[0];
String path = argv[1];
int port = Integer.parseInt(argv[2]);
try {
StatusHttpServer http = new StatusHttpServer(name, path, "0.0.0.0", port,
false);
http.start();
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}