edu.umd.cs.buildServer.BuildServerDaemon.java Source code

Java tutorial

Introduction

Here is the source code for edu.umd.cs.buildServer.BuildServerDaemon.java

Source

/**
 * Marmoset: a student project snapshot, submission, testing and code review
 * system developed by the Univ. of Maryland, College Park
 * 
 * Developed as part of Jaime Spacco's Ph.D. thesis work, continuing effort led
 * by William Pugh. See http://marmoset.cs.umd.edu/
 * 
 * Copyright 2005 - 2011, Univ. of Maryland
 * 
 * Licensed 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.
 * 
 */

/*
 * Created on Jan 22, 2005
 */
package edu.umd.cs.buildServer;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.net.ConnectException;
import java.security.SecureRandom;
import java.util.concurrent.TimeUnit;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.PosixParser;
import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.HttpException;
import org.apache.commons.httpclient.HttpMethod;
import org.apache.commons.httpclient.HttpStatus;
import org.apache.commons.httpclient.contrib.ssl.EasySSLProtocolSocketFactory;
import org.apache.commons.httpclient.methods.MultipartPostMethod;
import org.apache.commons.httpclient.methods.multipart.ByteArrayPartSource;
import org.apache.commons.httpclient.methods.multipart.FilePart;
import org.apache.commons.httpclient.protocol.Protocol;
import org.apache.commons.io.IOUtils;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import edu.umd.cs.buildServer.util.DevNullOutputStream;
import edu.umd.cs.buildServer.util.IO;
import edu.umd.cs.buildServer.util.ServletAppender;
import edu.umd.cs.marmoset.modelClasses.HttpHeaders;
import edu.umd.cs.marmoset.modelClasses.TestOutcome;
import edu.umd.cs.marmoset.modelClasses.TestOutcomeCollection;
import edu.umd.cs.marmoset.modelClasses.TestProperties;
import edu.umd.cs.marmoset.utilities.SystemInfo;

/**
 * Request projects to build from the SubmitServer, build them, perform quick
 * and release tests, and send the results back to the SubmitServer. This class
 * contains the main() method which runs the build server.
 *
 * @author David Hovemeyer
 */
public class BuildServerDaemon extends BuildServer implements ConfigurationKeys {
    private String configFile;

    /** Our HttpClient instance. */
    private HttpClient client;

    /**
     * Constructor.
     */
    public BuildServerDaemon() {
    }

    /**
     * Set the name of the BuildServer configuration file.
     *
     * @param configFile
     */
    public void setConfigFile(String configFile) {
        this.configFile = configFile;
    }

    /*
     * (non-Javadoc)
     *
     * @see edu.umd.cs.buildServer.BuildServer#prepareToExecute()
     */
    @Override
    protected void prepareToExecute() {
        this.client = new HttpClient();
        client.setConnectionTimeout(getConnectionTimeout());
    }

    /**
     * @return
     */
    protected int getConnectionTimeout() {
        return (int) TimeUnit.MILLISECONDS.convert(3, TimeUnit.MINUTES);
    }

    private HttpClient getClient() {
        if (client == null)
            prepareToExecute();
        return client;
    }

    /*
     * (non-Javadoc)
     *
     * @see edu.umd.cs.buildServer.BuildServer#initConfig()
     */
    @Override
    public void initConfig() throws IOException {
        // TODO: Verify that all the important parameters are set, and FAIL
        // EARLY is necessary
        // TODO: Make getter methods in Config for the required params
        // (build.directory, test.files.directory, etc)
        InputStream defaultConfig = BuildServerDaemon.class.getResourceAsStream("defaultConfig.properties");
        getConfig().load(defaultConfig);
        if (configFile == null) {
            File root = BuildServerConfiguration.getBuildServerRootFromCodeSource();

            if (root == null) {
                throw new IllegalStateException(
                        "No config file specified and could not determine buildserver root");

            }
            File localConfig = new File(root, "config.properties");
            if (!localConfig.exists() || !localConfig.canRead())
                throw new IllegalStateException("No config file specified and not found at " + localConfig);

            configFile = localConfig.getAbsolutePath();
        }

        getConfig().load(new BufferedInputStream(new FileInputStream(configFile)));

        // I'm setting the clover binary database in this method rather than in
        // the config.properties file because it always goes into /tmp and I
        // need
        // a unique name in case there are multiple buildServers on the same
        // host

        // TODO move the location of the Clover DB to the build directory.
        // NOTE: This requires changing the security.policy since Clover needs
        // to be able
        // to read, write and create files in the directory.
        //
        // String cloverDBPath =
        // getConfig().getRequiredProperty(BUILD_DIRECTORY) +"/myclover.db";
        String cloverDBPath = "/tmp/myclover.db." + Long.toHexString(nextRandomLong());
        getConfig().setProperty(CLOVER_DB, cloverDBPath);
    }

    private String getWelcomeURL() {
        return getBuildServerConfiguration().getServletURL(SUBMIT_SERVER_WELCOME_PATH);
    }

    private String getRequestSubmissionURL() {
        return getBuildServerConfiguration().getServletURL(SUBMIT_SERVER_REQUESTSUBMISSION_PATH);
    }

    private String getTestSetupURL() {
        return getBuildServerConfiguration().getServletURL(SUBMIT_SERVER_GETTESTSETUP_PATH);
    }

    private String getReportTestResultsURL() {
        return getBuildServerConfiguration().getServletURL(SUBMIT_SERVER_REPORTTESTRESULTS_PATH);
    }

    private String getReportBuildServerDeathURL() {
        return getBuildServerConfiguration().getServletURL(SUBMIT_SERVER_REPORTBUILDSERVERDEATH_PATH);
    }

    /**
     * Get a required header value. If the header value isn't specified in the
     * server response, returns null.
     *
     * @param method
     *            the HttpMethod representing the request/response
     * @param headerName
     *            the name of the header
     * @return the value of the header, or null if the header isn't present
     * @throws HttpException
     */
    private String getRequiredHeaderValue(HttpMethod method, String headerName) throws HttpException {
        Header header = method.getResponseHeader(headerName);
        if (header == null || header.getValues().length != 1) {
            getLog().error("Internal error: Missing header " + headerName + " in submit server response");
            for (Header h : method.getResponseHeaders()) {
                getLog().error("  have header " + h.getName());
            }
            return null;
        }
        return header.getValue();
    }

    /**
     * Get a required header value. If the header value isn't specified in the
     * server response, returns null.
     *
     * @param method
     *            the HttpMethod representing the request/response
     * @param headerName
     *            the name of the header
     * @return the value of the header, or null if the header isn't present
     * @throws HttpException
     */
    private String getRequiredHeaderValue(HttpMethod method, String headerName, String alternativeHeaderName)
            throws HttpException {
        Header header = method.getResponseHeader(headerName);
        if (header == null || header.getValues().length != 1)
            header = method.getResponseHeader(alternativeHeaderName);
        if (header == null || header.getValues().length != 1) {
            getLog().error("Internal error: Missing header " + headerName + " in submit server response");
            for (Header h : method.getResponseHeaders()) {
                getLog().error("  have header " + h.getName());
            }
            return null;
        }
        return header.getValue();
    }

    @Override
    protected void doWelcome() throws MissingConfigurationPropertyException, IOException {
        if (!isQuiet()) {
            System.out.println(
                    "Connecting to submit server at " + getBuildServerConfiguration().getSubmitServerURL());
            String hostname = getBuildServerConfiguration().getHostname();
            System.out.println("Hostname: " + hostname);
            System.out.println("System load: " + SystemInfo.getSystemLoad());
            System.out.println("Java version: " + System.getProperty("java.version"));
            System.out.println("connection timeout: " + getConnectionTimeout());
            System.out.println();

        }
        String url = getWelcomeURL();
        MultipartPostMethod method = new MultipartPostMethod(url);

        addCommonParameters(method);

        BuildServer.printURI(getLog(), method);
        int responseCode;
        try {
            responseCode = client.executeMethod(method);
        } catch (IOException e) {
            throw new IOException("Buildserver unable to connect to submitserver at " + url, e);
        }
        if (!isQuiet())
            System.out.println(method.getResponseBodyAsString());

        if (responseCode != HttpStatus.SC_OK) {

            getLog().error("HTTP server returned non-OK response: " + responseCode + ": " + method.getStatusText());
            getLog().error(" for URI: " + method.getURI());
            getLog().error("Full error message: " + method.getStatusText());
            throw new IOException("Buildserver unable to connect to submitserver at " + url + ", Status "
                    + responseCode + ": " + method.getStatusText());
        }

    }

    /*
     * (non-Javadoc)
     *
     * @see edu.umd.cs.buildServer.BuildServer#getProjectSubmission()
     */
    @Override
    protected ProjectSubmission<?> getProjectSubmission()
            throws MissingConfigurationPropertyException, IOException {

        try {
            String url = getRequestSubmissionURL();
            MultipartPostMethod method = new MultipartPostMethod(url);

            String supportedCoursePKList = getBuildServerConfiguration().getSupportedCourses();

            String specificProjectNum = getConfig().getOptionalProperty(DEBUG_SPECIFIC_PROJECT);
            String specificCourse = getConfig().getOptionalProperty(DEBUG_SPECIFIC_COURSE);
            if (specificCourse != null)
                supportedCoursePKList = specificCourse;

            String specificSubmission = getConfig().getOptionalProperty(DEBUG_SPECIFIC_SUBMISSION);
            String specificTestSetup = getConfig().getOptionalProperty(DEBUG_SPECIFIC_TESTSETUP);

            if (specificSubmission != null) {
                method.addParameter("submissionPK", specificSubmission);
                if (!isQuiet())
                    System.out.printf("Requesting submissionPK %s%n", specificSubmission);
            }
            if (specificTestSetup != null) {
                method.addParameter("testSetupPK", specificTestSetup);
                if (!isQuiet())
                    System.out.printf("Requesting testSetupPK %s%n", specificTestSetup);
            }

            if (specificProjectNum != null) {
                method.addParameter("projectNumber", specificProjectNum);
            }

            addCommonParameters(method);

            BuildServer.printURI(getLog(), method);

            int responseCode = client.executeMethod(method);
            if (responseCode != HttpStatus.SC_OK) {
                if (responseCode == HttpStatus.SC_SERVICE_UNAVAILABLE) {
                    getLog().trace("Server returned 503 (no work)");
                } else {
                    String msg = "HTTP server returned non-OK response: " + responseCode + ": "
                            + method.getStatusText();
                    getLog().error(msg);
                    getLog().error(" for URI: " + method.getURI());

                    getLog().error("Full error message: " + method.getResponseBodyAsString());
                    if (responseCode == HttpStatus.SC_BAD_REQUEST) {
                        if (!isQuiet()) {
                            System.err.println(msg);
                            System.out.println(msg);
                        }
                        System.exit(1);
                    }
                }
                return null;
            }

            getLog().debug("content-type: " + method.getResponseHeader("Content-type"));
            getLog().debug("content-length: " + method.getResponseHeader("content-length"));
            // Ensure we have a submission PK.
            String submissionPK = getRequiredHeaderValue(method, HttpHeaders.HTTP_SUBMISSION_PK_HEADER);
            if (submissionPK == null) {
                if (specificSubmission != null)
                    getLog().error("Server did not return submission " + specificSubmission);
                return null;
            }

            // Ensure we have a project PK.
            String testSetupPK = specificTestSetup != null ? specificTestSetup : getTestSetupPK(method);
            if (testSetupPK == null)
                return null;

            // This is a boolean value specifying whether the project jar file
            // is NEW, meaning that it needs to be tested against the
            // canonical project solution. The build server doesn't need
            // to do anything with this value except pass it back to
            // the submit server when reporting test outcomes.
            String isNewTestSetup = getIsNewTestSetup(method);
            if (isNewTestSetup == null)
                return null;

            // Opaque boolean value representing whether this was a
            // "background retest".
            // The BuildServer doesn't need to do anything with this except pass it
            // back to the SubmitServer.
            String isBackgroundRetest = getRequiredHeaderValue(method, HttpHeaders.HTTP_BACKGROUND_RETEST);
            if (isBackgroundRetest == null)
                isBackgroundRetest = "no";

            ServletAppender servletAppender = (ServletAppender) getLog().getAppender("servletAppender");
            if (isBackgroundRetest.equals("yes"))
                servletAppender.setThreshold(Level.FATAL);
            else
                servletAppender.setThreshold(Level.INFO);

            String kind = method.getResponseHeader(HttpHeaders.HTTP_KIND_HEADER).getValue();
            String logMsg = "Got submission " + submissionPK + ", testSetup " + testSetupPK + ", kind: " + kind;
            getLog().info(logMsg);

            ProjectSubmission<?> projectSubmission = new ProjectSubmission<TestProperties>(
                    getBuildServerConfiguration(), getLog(), submissionPK, testSetupPK, isNewTestSetup,
                    isBackgroundRetest, kind);

            projectSubmission.setMethod(method);

            getCurrentFile().delete();
            writeToCurrentFile(submissionPK + "\n" + testSetupPK + "\n" + kind + "\n" + SystemInfo.getSystemLoad()
                    + "\n" + logMsg);

            return projectSubmission;
        } catch (ConnectException e) {
            getLog().warn("Unable to connect to " + getBuildServerConfiguration().getSubmitServerURL());
            return null;
        }
    }

    /**
     * @param method
     * @return
     * @throws HttpException
     */
    @SuppressWarnings("deprecation")
    private String getIsNewTestSetup(MultipartPostMethod method) throws HttpException {
        return getRequiredHeaderValue(method, HttpHeaders.HTTP_NEW_TEST_SETUP);
    }

    /**
     * @param method
     * @return
     * @throws HttpException
     */
    @SuppressWarnings("deprecation")
    private String getTestSetupPK(MultipartPostMethod method) throws HttpException {
        return getRequiredHeaderValue(method, HttpHeaders.HTTP_TEST_SETUP_PK_HEADER);

    }

    @Override
    protected void downloadSubmissionZipFile(ProjectSubmission<?> projectSubmission) throws IOException {
        IO.download(projectSubmission.getZipFile(), projectSubmission.getMethod());
    }

    @Override
    protected void downloadProjectJarFile(ProjectSubmission<?> projectSubmission)
            throws MissingConfigurationPropertyException, HttpException, IOException, BuilderException {
        // FIXME: We should cache these

        MultipartPostMethod method = new MultipartPostMethod(getTestSetupURL());
        method.addParameter("testSetupPK", projectSubmission.getTestSetupPK());
        method.addParameter("projectJarfilePK", projectSubmission.getTestSetupPK());
        String supportedCourses = getBuildServerConfiguration().getSupportedCourses();
        method.addParameter("courses", supportedCourses);

        BuildServer.printURI(getLog(), method);

        try {
            int responseCode = client.executeMethod(method);
            if (responseCode != HttpStatus.SC_OK) {
                throw new BuilderException("Could not download project test setup from " + getTestSetupURL() + ": "
                        + responseCode + ": " + method.getStatusText());
            }

            getLog().trace("Downloading test setup file");
            IO.download(projectSubmission.getTestSetup(), method);

            // We're passing the project_jarfile_pk so we don't need to read it
            // from
            // the headers

            // wait for a while in case the files have not "settled"
            // TODO: Verify that this is still necessary; should be OK unless
            // run on NFS
            pause(10);

            getLog().trace("Done.");
        } finally {
            method.releaseConnection();
        }

    }

    @Override
    protected void releaseConnection(ProjectSubmission<?> projectSubmission) {
        projectSubmission.getMethod().releaseConnection();
    }

    private void dumpOutcomes(ProjectSubmission<?> projectSubmission) {
        try {
            // Can't dump outcomes if we don't have a test.properties file.
            if (projectSubmission.getTestProperties() == null)
                return;
            ObjectOutputStream out = null;
            File outputFile = new File(
                    projectSubmission.getBuilderAndTesterFactory().getDirectoryFinder().getBuildDirectory(),
                    "daemonresults.out");
            try {
                out = new ObjectOutputStream(new FileOutputStream(outputFile));
                projectSubmission.getTestOutcomeCollection().write(out);
            } catch (IOException e) {
                System.err.println("Could not save test outcome collection in " + outputFile.getPath());
                e.printStackTrace(); // OK, this is a command line app
            } finally {
                IOUtils.closeQuietly(out);
            }
        } catch (Exception e) {
            // XXX Prevent this from throwing exception
            getLog().warn("Ignoring error in BuildServerDaemon.dumpOutcomes " + e);
        }
    }

    protected void addCommonParameters(MultipartPostMethod method) {
        String hostname = getBuildServerConfiguration().getHostname();

        method.addParameter("testMachine", hostname);
        method.addParameter("hostname", hostname);

        method.addParameter("load", SystemInfo.getSystemLoad());
        String supportedCourses = getBuildServerConfiguration().getSupportedCourses();
        method.addParameter("courses", supportedCourses);
        method.addParameter("javaVersion", System.getProperty("java.version"));
        method.addParameter("serverTimestamp", Long.toString(getBuildServerConfiguration().getServerTimestamp()));
        method.addParameter("connectionTimeout", Integer.toString(getConnectionTimeout()));
    }

    @Override
    protected void reportBuildServerDeath(int submissionPK, int testSetupPK, long lastModified, String kind,
            String load) {

        MultipartPostMethod method = new MultipartPostMethod(getReportBuildServerDeathURL());

        method.addParameter("submissionPK", Integer.toString(submissionPK));
        method.addParameter("testSetupPK", Integer.toString(testSetupPK));

        addCommonParameters(method);
        method.addParameter("kind", kind);
        method.addParameter("lastModified", Long.toString(lastModified));

        try {
            int statusCode = getClient().executeMethod(method);

            if (statusCode != HttpStatus.SC_OK) {
                System.out.println("Error eporting build server death for submissionPK " + submissionPK
                        + ": status " + statusCode + ": " + method.getStatusText());
                System.out.println(method.getResponseBodyAsString());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    @Override
    protected void reportTestResults(ProjectSubmission<?> projectSubmission)
            throws MissingConfigurationPropertyException {

        dumpOutcomes(projectSubmission);

        getLog().info("Test outcome collection for " + projectSubmission.getSubmissionPK() + " for test setup "
                + projectSubmission.getTestSetupPK() + " contains "
                + projectSubmission.getTestOutcomeCollection().size() + " entries");

        // Format the test outcome collection as bytes in memory
        ByteArrayOutputStream sink = new ByteArrayOutputStream();
        ObjectOutputStream out = null;
        try {
            out = new ObjectOutputStream(sink);
        } catch (IOException ignore) {
            getLog().error("IOException creating ObjectOutputStream");
        }

        TestOutcomeCollection c = projectSubmission.getTestOutcomeCollection();

        // Print some info about the size of the collection
        getLog().info("Got TestOutcomeCollection; size: " + c.size());
        for (TestOutcome to : c.getAllOutcomes()) {
            // Truncate to avoid OutOfMemories
            to.truncateLongTestResult();
            // Most important size to print is the longResult len - it
            // can be really long
            int length = to.getLongTestResult().length();
            getLog().info("  Outcome " + to.getTestNumber() + ": " + to.getTestName() + " = " + to.getOutcome()
                    + (length > 0 ? ", longResult len: " + length : ""));

        }

        try {
            c.write(out);
        } catch (IOException ignore) {
            getLog().error("IOException writing to ObjectOutputStream", ignore);
        } catch (Error e) {
            // Can happen if the long test output is really long; we
            // truncate down to 64K (also the limit imposed by the
            // MySQL 'text' type) in order to avoid this, but we should
            // note it.
            getLog().error("While writing, caught Error", e);
            getLog().error("Rethrowing...");
            throw (e);
        }

        try {
            out.close();
        } catch (IOException ignore) {
            getLog().error("IOException closing ObjectOutputStream");
        }

        byte[] testOutcomeData = sink.toByteArray();
        String subPK = projectSubmission.getSubmissionPK();
        String jarfilePK = projectSubmission.getTestSetupPK();
        int outcomes = projectSubmission.getTestOutcomeCollection().size();
        getLog().info("Test data for submission " + subPK + " for test setup " + jarfilePK + " contains "
                + testOutcomeData.length + " bytes from " + outcomes + " test outcomes");

        String hostname = getBuildServerConfiguration().getHostname();

        MultipartPostMethod method = new MultipartPostMethod(getReportTestResultsURL());

        method.addParameter("submissionPK", projectSubmission.getSubmissionPK());
        method.addParameter("testSetupPK", projectSubmission.getTestSetupPK());
        method.addParameter("projectJarfilePK", projectSubmission.getTestSetupPK());
        method.addParameter("newTestSetup", projectSubmission.getIsNewTestSetup());

        method.addParameter("newProjectJarfile", projectSubmission.getIsNewTestSetup());
        method.addParameter("isBackgroundRetest", projectSubmission.getIsBackgroundRetest());
        method.addParameter("testMachine", hostname);
        method.addParameter("testDurationsMillis", Long.toString(projectSubmission.getTestDurationMillis()));

        addCommonParameters(method);

        method.addParameter("kind", projectSubmission.getKind());

        // CodeMetrics
        if (projectSubmission.getCodeMetrics() != null) {
            getLog().debug("Code Metrics: " + projectSubmission.getCodeMetrics());
            projectSubmission.getCodeMetrics().mapIntoHttpHeader(method);
        }
        method.addPart(new FilePart("testResults", new ByteArrayPartSource("testresults.out", testOutcomeData)));
        printURI(method);

        try {
            getLog().debug("Submitting test results for " + projectSubmission.getSubmissionPK() + "...");

            int statusCode = client.executeMethod(method);
            if (statusCode == HttpStatus.SC_OK)
                getLog().debug("Done submitting test results for submissionPK "
                        + projectSubmission.getSubmissionPK() + "; statusCode=" + statusCode);
            else {

                getLog().error("Error submitting test results for submissionPK "
                        + projectSubmission.getSubmissionPK() + ": " + statusCode + ": " + method.getStatusText());
                getLog().error(method.getResponseBodyAsString());
                // TODO: Should we do anything else in case of an error?
            }
        } catch (HttpException e) {
            getLog().error("Internal error: HttpException submitting test results", e);
            return;
        } catch (IOException e) {
            getLog().error("Internal error: IOException submitting test results", e);
            return;
        } finally {
            getLog().trace("Releasing connection...");
            method.releaseConnection();
            getLog().trace("Done releasing connection");
        }
    }

    /**
     * Command-line interface.
     */

    @SuppressWarnings("static-access")
    public static Options getOptions() {
        Options options = new Options();
        Option configFile = OptionBuilder.withArgName("file").hasArg().withDescription("use given config file")
                .withLongOpt("config").create("c");
        Option courseKey = OptionBuilder.withArgName("courseKey").hasArg().withDescription("use given course key")
                .create("course");
        Option downloadOnly = OptionBuilder.withDescription("download only").withLongOpt("downloadOnly")
                .create("d");
        Option submission = OptionBuilder.withArgName("submissionPK").hasArg()
                .withDescription("test the specified submission").withLongOpt("submission").create("s");

        Option quiet = OptionBuilder.withDescription("no output; no warnings if already running")
                .withLongOpt("quiet").create("q");

        Option testSetup = OptionBuilder.withArgName("testSetupPK").hasArg()
                .withDescription("use the specified test setup").withLongOpt("testSetup").create("t");

        Option projectNum = OptionBuilder.withArgName("projectNum").hasArg()
                .withDescription("exhaustively retest the specified project").withLongOpt("projectNum").create("p");
        Option skipDownload = OptionBuilder.withDescription("don't download submission").create("skipDownload");

        Option onceOption = new Option("o", "once", false, "quit after handling one request");
        Option logLevel = OptionBuilder.withArgName("logLevel").hasArg().withDescription("Log4j log level")
                .withLongOpt("logLevel").create("l");
        Option help = new Option("h", "help", false, "print this message");
        Option verifyOnly = OptionBuilder.withDescription("just verify connection to submit server")
                .withLongOpt("verify").create("v");
        options.addOption(help);
        options.addOption(configFile);
        options.addOption(submission);
        options.addOption(skipDownload);

        options.addOption(projectNum);
        options.addOption(courseKey);

        options.addOption(testSetup);
        options.addOption(onceOption);
        options.addOption(logLevel);
        options.addOption(quiet);
        options.addOption(downloadOnly);
        options.addOption(verifyOnly);
        return options;
    }

    private static void printHelp(Options options) {
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp("java -jar buildserver.jar <options> ", options);
    }

    public static void main(String[] args) throws Exception {

        CommandLineParser parser = new PosixParser();
        Options options = getOptions();
        CommandLine line;
        try {
            line = parser.parse(options, args);

        } catch (Exception e) {
            printHelp(options);
            return;
        }
        if (line.hasOption("help")) {
            printHelp(options);
            return;
        }

        String[] remainingArgs = line.getArgs();

        Protocol easyhttps = new Protocol("https", new EasySSLProtocolSocketFactory(), 443);
        Protocol.registerProtocol("easyhttps", easyhttps);

        BuildServerDaemon buildServer = new BuildServerDaemon();
        if (line.hasOption("config")) {
            String c = line.getOptionValue("config");
            buildServer.setConfigFile(c);
        } else if (remainingArgs.length == 1)
            buildServer.setConfigFile(remainingArgs[0]);

        boolean once = line.hasOption("once");

        if (line.hasOption("submission")) {
            once = true;
            buildServer.getConfig().setProperty(DEBUG_SPECIFIC_SUBMISSION, line.getOptionValue("submission"));
            if (line.hasOption("testSetup"))
                buildServer.getConfig().setProperty(DEBUG_SPECIFIC_TESTSETUP, line.getOptionValue("testSetup"));
            if (line.hasOption("skipDownload"))
                buildServer.getConfig().setProperty(DEBUG_SKIP_DOWNLOAD, "true");

        } else if (line.hasOption("testSetup")) {
            throw new IllegalArgumentException(
                    "You can only specify a specific test setup if you also specify a specific submission");
        }

        if (line.hasOption("projectNum")) {
            buildServer.getConfig().setProperty(DEBUG_SPECIFIC_PROJECT, line.getOptionValue("projectNum"));
        }

        if (line.hasOption("quiet"))
            buildServer.setQuiet(true);
        if (line.hasOption("verify"))
            buildServer.setVerifyOnly();

        if (line.hasOption("course")) {
            buildServer.getConfig().setProperty(DEBUG_SPECIFIC_COURSE, line.getOptionValue("course"));
        }

        if (line.hasOption("logLevel"))
            buildServer.getConfig().setProperty(LOG4J_THRESHOLD, line.getOptionValue("logLevel"));

        if (line.hasOption("downloadOnly")) {
            System.out.println("Setting download only");
            buildServer.setDownloadOnly(true);
            once = true;
        }
        if (once) {
            buildServer.getConfig().setProperty(LOG_DIRECTORY, "console");
            buildServer.setDoNotLoop(true);
            buildServer.getConfig().setProperty(DEBUG_PRESERVE_SUBMISSION_ZIPFILES, "true");
        }

        buildServer.initConfig();
        Logger log = buildServer.getLog();

        /** Redirect standard out and err to dev null, since clover
         * writes to standard out and error */

        PrintStream systemOut = System.out;
        PrintStream systemErr = System.err;
        if (buildServer.isQuiet()) {
            System.setOut(new PrintStream(new DevNullOutputStream()));
            System.setErr(new PrintStream(new DevNullOutputStream()));
        }

        try {
            buildServer.executeServerLoop();
            if (log != null)
                log.info("Shutting down");
            System.out.println("Shutting down");
            timedSystemExit0();
        } catch (Throwable e) {
            buildServer.clearMyPidFile();
            if (log != null)
                log.fatal("BuildServerDaemon got fatal exception; waiting for cron to restart me: ", e);
            e.printStackTrace(systemErr);
            System.exit(1);
        }
        if (buildServer.isQuiet()) {
            System.setOut(systemOut);
            System.setErr(systemErr);
        }
    }

    public static void timedSystemExit0() {
        Thread t = new Thread(new Runnable() {

            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    assert true;
                }
                System.exit(0);
            }
        }, "force shutdown thread");
        t.setDaemon(true);
        t.start();
    }

    private static SecureRandom rng = new SecureRandom();

    private static long nextRandomLong() {
        synchronized (rng) {
            return rng.nextLong();
        }
    }

}