/*
* NOTE: This copyright does *not* cover user programs that use HQ
* program services by normal system calls through the application
* program interfaces provided as part of the Hyperic Plug-in Development
* Kit or the Hyperic Client Development Kit - this is merely considered
* normal use of the program, and does *not* fall under the heading of
* "derived work".
*
* Copyright (C) [2004, 2005, 2006], Hyperic, Inc.
* This file is part of HQ.
*
* HQ is free software; you can redistribute it and/or modify
* it under the terms version 2 of the GNU General Public License as
* published by the Free Software Foundation. This program is distributed
* in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
* USA.
*/
package org.hyperic.lather.server;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.ServletConfig;
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.hyperic.hq.appdef.shared.AIPlatformValue;
import org.hyperic.hq.appdef.shared.AgentUnauthorizedException;
import org.hyperic.hq.bizapp.server.session.LatherDispatcher;
import org.hyperic.hq.bizapp.shared.lather.AiPlatformLatherValue;
import org.hyperic.hq.bizapp.shared.lather.CommandInfo;
import org.hyperic.hq.context.Bootstrap;
import org.hyperic.hq.measurement.server.session.DataInserterException;
import org.hyperic.lather.LatherContext;
import org.hyperic.lather.LatherRemoteException;
import org.hyperic.lather.LatherValue;
import org.hyperic.lather.NullLatherValue;
import org.hyperic.lather.client.LatherHTTPClient;
import org.hyperic.lather.xcode.LatherXCoder;
import org.hyperic.util.encoding.Base64;
/**
* The purpose of this class is to take servlet requests for
* remote execution, and translate them into service calls.
*
* The servlet understands POST requests which must have the
* following data:
*
* method = method name
* args = an encoded LatherValue object
* argsClass = the class which the 'args' is encoded for
*
* The response is an encoded LatherValue object.
*/
@SuppressWarnings("serial")
public class LatherServlet extends HttpServlet {
private static final AtomicLong ids = new AtomicLong();
private static final String PROP_PREFIX = ConnManager.PROP_PREFIX;
private static final String PROP_MAXCONNS = ConnManager.PROP_MAXCONNS;
private static final String PROP_EXECTIMEOUT = PROP_PREFIX + "execTimeout";
private static final String PROP_CONNID = PROP_PREFIX + "connID";
private static final Log log = LogFactory.getLog(LatherServlet.class);
private Random rand;
private int execTimeout;
private static final AtomicReference<ConnManager> connManager = new AtomicReference<ConnManager>();
private String getReqCfg(ServletConfig cfg, String prop) throws ServletException {
String res;
if ((res = cfg.getInitParameter(prop)) == null) {
throw new ServletException("init-param '" + prop + "' not set");
}
return res;
}
public void init(ServletConfig cfg) throws ServletException {
// Call super per the javadoc
super.init(cfg);
rand = new Random();
if (connManager.get() == null) {
connManager.compareAndSet(null, getConnManager(cfg));
}
execTimeout = Integer.parseInt(getReqCfg(cfg, PROP_EXECTIMEOUT));
}
private ConnManager getConnManager(ServletConfig cfg) throws ServletException {
@SuppressWarnings("unchecked")
final Enumeration<String> paramNames = cfg.getInitParameterNames();
final Map<String, Semaphore> maxConnMap = new HashMap<String, Semaphore>();
while (paramNames.hasMoreElements()) {
final String name = paramNames.nextElement();
if (!name.startsWith(PROP_PREFIX) || name.contains(PROP_EXECTIMEOUT)) {
continue;
}
final String param = cfg.getInitParameter(name);
try {
final int value = Integer.parseInt(param);
maxConnMap.put(name.replace(PROP_PREFIX, ""), new Semaphore(value));
} catch (NumberFormatException e) {
log.error("could not initialize max conn setting for " + name + " value=" + param);
}
}
if (!maxConnMap.containsKey(PROP_MAXCONNS)) {
throw new ServletException("init-params do not contain key=" + PROP_MAXCONNS + ")");
}
return new ConnManager(maxConnMap);
}
private static void issueErrorResponse(HttpServletResponse resp, String errMsg)
throws IOException {
resp.setContentType("text/raw");
resp.setIntHeader(LatherHTTPClient.HDR_ERROR, 1);
resp.getOutputStream().print(errMsg);
}
private static void issueSuccessResponse(HttpServletResponse resp, LatherXCoder xCoder,
LatherValue res)
throws IOException {
ByteArrayOutputStream bOs;
DataOutputStream dOs;
byte[] rawData;
resp.setContentType("text/latherValue");
resp.setHeader(LatherHTTPClient.HDR_VALUECLASS,
res.getClass().getName());
bOs = new ByteArrayOutputStream();
dOs = new DataOutputStream(bOs);
xCoder.encode(res, dOs);
rawData = bOs.toByteArray();
resp.getOutputStream().print(Base64.encode(rawData));
}
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
final boolean debug = log.isDebugEnabled();
boolean gotConn = false;
int connRnd;
connRnd = this.rand.nextInt();
String method = req.getParameter("method");
try {
gotConn = connManager.get().grabConn(method);
if (!gotConn) {
final String msg = new StringBuilder(128)
.append("Denied request from ").append(req.getRemoteAddr())
.append(" availablePermits=").append(connManager.get().getAvailablePermits(method))
.append(", method=").append(method)
.toString();
if (debug) log.debug(msg);
resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, msg);
return;
}
req.setAttribute(PROP_CONNID, new Integer(connRnd));
if (debug) {
log.debug("Accepting request from " + req.getRemoteAddr() +
" availablePermits=" + connManager.get().getAvailablePermits(method) +
" conID=" + connRnd);
}
super.service(req, resp);
} finally {
if (gotConn) {
connManager.get().releaseConn(method);
if (debug) log.debug("Releasing request from " + req.getRemoteAddr() +
" availablePermits=" + connManager.get().getAvailablePermits(method) +
" connID=" + connRnd);
}
}
}
public void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
ByteArrayInputStream bIs;
DataInputStream dIs;
LatherXCoder xCoder;
LatherValue val;
String[] method, args, argsClass;
LatherContext ctx;
byte[] decodedArgs;
Class<?> valClass;
ctx = new LatherContext();
ctx.setCallerIP(req.getRemoteAddr());
ctx.setRequestTime(System.currentTimeMillis());
xCoder = new LatherXCoder();
method = req.getParameterValues("method");
args = req.getParameterValues("args");
argsClass = req.getParameterValues("argsClass");
if (method == null || args == null || argsClass == null ||
method.length != 1 || args.length != 1 || argsClass.length != 1) {
String msg = "Invalid Lather request made from " + req.getRemoteAddr();
log.error(msg);
resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, msg);
return;
}
if (log.isDebugEnabled()) {
log.debug("Invoking method '" + method[0] +
"' for connID=" + req.getAttribute(PROP_CONNID));
}
try {
valClass = Class.forName(argsClass[0], true,
xCoder.getClass().getClassLoader());
} catch(ClassNotFoundException exc){
String msg = "Lather request from " + req.getRemoteAddr() +
" required an argument object of class '" + argsClass[0] +
"' which could not be found";
log.error(msg);
resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, msg);
return;
}
decodedArgs = Base64.decode(args[0]);
bIs = new ByteArrayInputStream(decodedArgs);
dIs = new DataInputStream(bIs);
try {
val = xCoder.decode(dIs, valClass);
} catch(LatherRemoteException exc){
LatherServlet.issueErrorResponse(resp, exc.toString());
return;
}
this.doServiceCall(req, resp, method[0], val, xCoder, ctx);
}
private class ServiceCaller implements Runnable {
private HttpServletResponse resp;
private LatherXCoder xcoder;
private LatherValue arg;
private LatherContext ctx;
private String method;
private LatherDispatcher latherDispatcher;
private Thread thread;
private final AtomicBoolean finished = new AtomicBoolean(false);
private AtomicLong startTime;
private ServiceCaller(HttpServletResponse resp, LatherXCoder xcoder, LatherContext ctx, String method,
LatherValue arg, LatherDispatcher latherDispatcher) {
this.resp = resp;
this.xcoder = xcoder;
this.ctx = ctx;
this.method = method;
this.arg = arg;
this.latherDispatcher = latherDispatcher;
this.thread = Thread.currentThread();
}
private boolean hasStarted() {
return startTime != null;
}
private boolean isFinished() {
return finished.get();
}
private void markFinished() {
this.thread = null;
finished.set(true);
}
public void run() {
try {
startTime = new AtomicLong(System.currentTimeMillis());
LatherValue res = latherDispatcher.dispatch(ctx, method, arg);
if (thread.isInterrupted()) {
return;
}
if (CommandInfo.CMD_AI_SEND_REPORT.equals(method)) {
res = handleAutoApprovals(res);
}
issueSuccessResponse(this.resp, this.xcoder, res);
} catch(Exception e) {
Throwable cause = e.getCause();
// no need to log a full stack trace for known exceptions
if (e instanceof AgentUnauthorizedException || (cause != null && cause instanceof AgentUnauthorizedException)) {
log.warn("Unauthorized agent from [" + ctx.getCallerIP() + "] denied", e);
log.debug(e,e);
} else if (e instanceof DataInserterException || (cause != null && cause instanceof DataInserterException)) {
log.warn(e);
log.debug(e,e);
} else if (e.getMessage().equals("Server still initializing")) {
log.warn(e);
log.debug(e,e);
} else {
log.error("error while invoking LatherDispatcher from ip=" + ctx.getCallerIP() +
", method=" + method + ": " + e, e);
}
try {
issueErrorResponse(resp, e.toString());
} catch(IOException ioe){
log.warn("IO error sending lather response method=" + method + ", ip=" + ctx.getCallerIP() + ": " + ioe, ioe);
}
}
}
private LatherValue handleAutoApprovals(LatherValue arg) throws LatherRemoteException {
if (arg == null || arg instanceof NullLatherValue) {
return NullLatherValue.INSTANCE;
}
AiPlatformLatherValue aiPlatformLatherValue = (AiPlatformLatherValue) arg;
AIPlatformValue aiPlatformValue = aiPlatformLatherValue.getAIPlatformValue();
if (aiPlatformValue.isAutoApprove()) {
this.latherDispatcher.invokeAutoApprove(aiPlatformValue);
}
return NullLatherValue.INSTANCE;
}
public void interrupt() {
if (thread != null)
thread.interrupt();
}
public boolean isExpired() {
if (startTime == null) {
return false;
}
final long now = System.currentTimeMillis();
if ((startTime.get() + execTimeout) <= now) {
return true;
}
return false;
}
public String toString() {
return "method=" + method + ", ip=" + ctx.getCallerIP();
}
}
private void doServiceCall(HttpServletRequest req, HttpServletResponse resp, String methName, LatherValue args,
LatherXCoder xCoder, LatherContext ctx)
throws IOException {
final LatherDispatcher latherDispatcher = Bootstrap.getBean(LatherDispatcher.class);
final ServiceCaller caller = new ServiceCaller(resp, xCoder, ctx, methName, args, latherDispatcher);
final Thread currentThread = Thread.currentThread();
final String threadName = currentThread.getName();
try {
currentThread.setName(methName + "-" + ids.getAndIncrement());
LatherThreadMonitor.get().register(caller);
caller.run();
if (currentThread.isInterrupted()) {
throw new InterruptedException();
}
} catch(InterruptedException exc){
log.warn("Interrupted while trying to execute lather method=" + methName + " from ip=" + ctx.getCallerIP());
} finally {
caller.markFinished();
currentThread.setName(threadName);
}
}
private static class LatherThreadMonitor extends Thread {
private static final LatherThreadMonitor instance = new LatherThreadMonitor();
static {
log.info("Starting Lather Thread Monitor");
instance.start();
}
private static final ConcurrentLinkedQueue<ServiceCaller> queue = new ConcurrentLinkedQueue<LatherServlet.ServiceCaller>();
private LatherThreadMonitor() {
super("LatherThreadMonitor");
setDaemon(true);
}
private static LatherThreadMonitor get() {
return instance;
}
public void register(ServiceCaller caller) {
queue.add(caller);
}
public void run() {
final boolean debug = log.isDebugEnabled();
final long inspectionInterval = 1000;
int callerIndex = 0;
while (true) {
try {
for (ServiceCaller caller : queue) {
if (caller.isFinished()) {
queue.remove(caller);
} else if (caller.isExpired()) {
log.warn("Expiring Lather thread " + caller);
caller.interrupt();
queue.remove(caller);
}
callerIndex++;
}
sleep(inspectionInterval);
if (debug)
log.debug("LatherThreadMonitor current queue size = " + queue.size() + ", " + callerIndex + " threads were inspected");
callerIndex = 0;
} catch (Throwable t) {
log.error(t,t);
}
}
}
}
}