/* The contents of this file are subject to the terms
* of the Common Development and Distribution License
* (the License). You may not use this file except in
* compliance with the License.
*
* You can obtain a copy of the License at
* https://faban.dev.java.net/public/CDDLv1.0.html or
* install_dir/license.txt
* See the License for the specific language governing
* permission and limitations under the License.
*
* When distributing Covered Code, include this CDDL
* Header Notice in each file and include the License file
* at faban/src/legal/CDDLv1.0.txt.
* If applicable, add the following below the CDDL Header,
* with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* $Id$
*
* Copyright 2005-2009 Sun Microsystems Inc. All Rights Reserved
*/
package com.sun.faban.harness.webclient;
import com.sun.faban.common.NameValuePair;
import com.sun.faban.harness.common.Config;
import com.sun.faban.harness.common.Run;
import com.sun.faban.harness.engine.RunEntryException;
import com.sun.faban.harness.engine.RunQ;
import com.sun.faban.harness.util.FileHelper;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.*;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* This class represents both the servlet that allows fetching runs from
* the run queue and the client side utility pollRun used to retrieve
* remote runs.
*
* @author Akara Sucharitakul
*/
public class RunRetriever extends HttpServlet {
static final String SERVLET_PATH = "pollrun";
private static Logger logger = Logger.getLogger(
RunRetriever.class.getName());
/**
* Post method to retrieve a run for a remote queue. Used only by pollees.
* @param request The servlet request
* @param response The servlet response
* @throws ServletException If there is an error in the servlet
* @throws IOException If the servlet has an I/O error
*/
public void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// Check that we are a pollee
if (Config.daemonMode != Config.DaemonModes.POLLEE) {
logger.warning("Being polled for runs, not pollee!");
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
// Check access rights
String hostName = request.getParameter("host");
String key = request.getParameter("key");
if (hostName == null || key == null) {
logger.warning("Being polled for runs, no hostname or key!");
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
if (!authenticate(hostName, key)) {
logger.warning("Polling authentication from host " + hostName +
" denied!");
response.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
// Is this an age probe?
String v = request.getParameter("minage");
if (v != null) {
nextRunAge(Long.parseLong(v), response);
return;
}
v = request.getParameter("runid");
if (v != null) {
try {
fetchNextRun(v, response);
} catch (ClassNotFoundException ex) {
Logger.getLogger(RunRetriever.class.getName()).log(Level.SEVERE, null, ex);
}
return;
}
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
}
/**
* Authenticates the host and key against the stored host/key pair.
* @param host The name of the communicating host
* @param key The host's key
* @return True if the authentication succeeds, false otherwise
*/
static boolean authenticate(String host, String key) {
boolean authenticated = false;
// We do not expect too many hosts polling, so we use sequential
// search. If this turns out wrong, we can always go for alternatives.
for (int i = 0; i < Config.pollHosts.length; i++)
if (host.equals(Config.pollHosts[i].name) &&
key.equals(Config.pollHosts[i].key)) {
authenticated = true;
break;
}
return authenticated;
}
private void nextRunAge(long minAge, HttpServletResponse response)
throws IOException {
NameValuePair<Long> runAge = RunQ.getHandle().nextRunAge(minAge);
if (runAge == null) {
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
return;
}
StringBuilder output = new StringBuilder(128);
output.append(runAge.name).append('\t').append(runAge.value);
response.setContentType("txt/plain");
response.setStatus(HttpServletResponse.SC_OK);
OutputStream out = response.getOutputStream();
out.write(output.toString().getBytes());
out.flush();
out.close();
}
private void fetchNextRun(String runId, HttpServletResponse response)
throws IOException, ClassNotFoundException {
Run nextRun = null;
for (;;)
try {
nextRun = RunQ.getHandle().fetchNextRun(runId);
break;
} catch (RunEntryException e) {
}
if (nextRun == null) { // Queue empty
logger.warning("Fetching run " + runId + ": No longer available!");
response.setStatus(HttpServletResponse.SC_NO_CONTENT);
return;
}
// Jar up the run.
File jarFile = null;
jarFile = jar(nextRun);
// Send the run jar to the output stream
long length = jarFile.length();
int bufferSize = 1024 * 1024 * 128; // 128MB buffer limit
if (length < bufferSize)
bufferSize = (int) length;
byte[] buffer = new byte[bufferSize];
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/java-archive");
OutputStream out = response.getOutputStream();
FileInputStream jarIn = new FileInputStream(jarFile);
int readSize = 0;
while ((readSize = jarIn.read(buffer)) != -1)
out.write(buffer, 0, readSize);
out.flush();
out.close();
// Update status locally
nextRun.updateStatus(Run.RECEIVED);
// Clear tmp file
jarFile.delete();
}
/**
* Jars up the run directory.
* @param run The run to jar up
* @return The resulting jar file
* @throws IOException Error creating the jar
*/
public static File jar(Run run) throws IOException {
logger.info("Preparing run " + run.getRunId() + " for download.");
String runId = run.getRunId();
String jarName = runId + ".jar";
File jar = new File(Config.TMP_DIR, jarName);
String[] files = new File(Config.OUT_DIR, runId).list();
if (jar.exists())
jar.delete();
FileHelper.jar(Config.OUT_DIR + runId, files, jar.getAbsolutePath());
return jar;
}
/**
* Client side method to poll for the oldest run which must be older than
* localAge. If found, the run will be downloaded into the temp space.
* and the run name will be returned.
* @param localAge The age of the oldest local run in the queue
* @return The file reference to the local run in the directory
*/
public static File pollRun(long localAge) {
Config.HostInfo selectedHost = null;
NameValuePair<Long> selectedRun = null;
for (int i = 0; i < Config.pollHosts.length; i++) {
Config.HostInfo pollHost = Config.pollHosts[i];
NameValuePair<Long> run = null;
try {
run = poll(pollHost, localAge);
} catch (IOException e) {
logger.log(Level.WARNING, "Error polling " + pollHost.url +
'.', e);
}
if (run != null &&
(selectedRun == null || run.value > selectedRun.value)) {
selectedRun = run;
selectedHost = pollHost;
}
}
File tmpDir = null;
if (selectedRun != null) {
try {
// Download and unjar the run.
File tmpJar = download(selectedHost, selectedRun);
if (tmpJar == null) {
logger.warning("Download null jar file.");
return null;
}
tmpDir = FileHelper.unjarTmp(tmpJar);
File metaInf = new File(tmpDir, "META-INF");
if (!metaInf.isDirectory())
metaInf.mkdir();
// Create origin file to know where this run came from.
FileHelper.writeStringToFile(selectedHost.name + '.' +
selectedRun.name, new File(metaInf, "origin"));
tmpJar.delete();
} catch (IOException e) {
logger.log(Level.WARNING, "Error downloading run " +
selectedRun.name + " from " + selectedHost.url + '.',
e);
}
}
return tmpDir;
}
private static NameValuePair<Long> poll(Config.HostInfo host, long minAge)
throws IOException {
NameValuePair<Long> run = null;
URL target = new URL(host.url, SERVLET_PATH);
HttpURLConnection c;
if (host.proxyHost != null)
c = (HttpURLConnection) target.openConnection(
new Proxy(Proxy.Type.HTTP,
new InetSocketAddress(host.proxyHost, host.proxyPort)));
else
c = (HttpURLConnection) target.openConnection();
try {
c.setRequestMethod("POST");
c.setConnectTimeout(2000);
c.setDoOutput(true);
c.setDoInput(true);
PrintWriter out = new PrintWriter(c.getOutputStream());
out.write("host=" + Config.FABAN_HOST + "&key=" + host.key +
"&minage=" + minAge);
out.flush();
out.close();
} catch (SocketTimeoutException e) {
logger.log(Level.WARNING, "Timeout trying to connect to " +
target + '.', e);
throw new IOException("Socket connect timeout");
}
int responseCode = c.getResponseCode();
if (responseCode == HttpServletResponse.SC_OK) {
InputStream is = c.getInputStream();
// The input is a one liner in the form runId\tAge
byte[] buffer = new byte[256]; // Very little cost for this new/GC.
int size = is.read(buffer);
// We have to close the input stream in order to return it to
// the cache, so we get it for all content, even if we don't
// use it. It's (I believe) a bug that the content handlers used
// by getContent() don't close the input stream, but the JDK team
// has marked those bugs as "will not fix."
is.close();
StringTokenizer t = new StringTokenizer(
new String(buffer, 0, size), "\t\n");
run = new NameValuePair<Long>();
run.name = t.nextToken();
run.value = Long.parseLong(t.nextToken());
} else if (responseCode != HttpServletResponse.SC_NO_CONTENT) {
logger.warning("Polling " + target + " got response code " +
responseCode);
}
return run;
}
private static File download(Config.HostInfo host, NameValuePair<Long> run)
throws IOException {
File jarFile = null;
FileOutputStream jarOut = null;
URL target = new URL(host.url, SERVLET_PATH);
HttpURLConnection c = null;
try {
c = (HttpURLConnection) target.openConnection();
c.setRequestMethod("POST");
c.setConnectTimeout(2000);
c.setDoOutput(true);
c.setDoInput(true);
PrintWriter out = new PrintWriter(c.getOutputStream());
out.write("host=" + Config.FABAN_HOST + "&key=" + host.key +
"&runid=" + run.name);
out.flush();
out.close();
} catch (SocketTimeoutException e) {
logger.log(Level.WARNING, "Timeout trying to connect to " +
target + '.', e);
throw new IOException("Socket connect timeout");
}
int responseCode = c.getResponseCode();
if (responseCode == HttpServletResponse.SC_OK) {
InputStream is = c.getInputStream();
// We allocate in every method instead of a central buffer
// to allow concurrent downloads. This can be expanded to use
// buffer pools to avoid GC, if necessary.
byte[] buffer = new byte[8192];
int size;
while ((size = is.read(buffer)) != -1) {
if (size > 0 && jarFile == null) {
jarFile = new File(Config.TMP_DIR, host.name + '.' +
run.name + ".jar");
jarOut = new FileOutputStream(jarFile);
}
jarOut.write(buffer, 0, size);
}
is.close();
jarOut.flush();
jarOut.close();
} else {
logger.warning("Downloading run " + run.name + " from " + target +
" got response code " + responseCode);
}
return jarFile;
}
}