/******************************************************************************** * CruiseControl, a Continuous Integration Toolkit * Copyright (c) 2001-2003, ThoughtWorks, Inc. * 200 E. Randolph, 25th Floor * Chicago, IL 60601 USA * 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 of ThoughtWorks, Inc., CruiseControl, 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 REGENTS 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 net.sourceforge.cruisecontrol; import java.io.File; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.util.Properties; import net.sourceforge.cruisecontrol.jmx.CruiseControlControllerAgent; import net.sourceforge.cruisecontrol.launch.Configuration; import net.sourceforge.cruisecontrol.launch.CruiseControlMain; import net.sourceforge.cruisecontrol.launch.LaunchException; import net.sourceforge.cruisecontrol.launch.LogInterface; import net.sourceforge.cruisecontrol.report.BuildLoopMonitorRepository; import net.sourceforge.cruisecontrol.report.BuildLoopPostingConfiguration; import net.sourceforge.cruisecontrol.util.threadpool.ThreadQueueProperties; import net.sourceforge.cruisecontrol.web.EmbeddedJettyServer; import org.apache.log4j.Level; import org.apache.log4j.Logger; /** * Command line entry point. * * @author alden almagro, ThoughtWorks, Inc. 2002 * @author <a href="mailto:jcyip@thoughtworks.com">Jason Yip</a> */ public final class Main implements CruiseControlMain { private static final Logger LOG = Logger.getLogger(Main.class); private CruiseControlController controller; private CruiseControlControllerAgent agent; /** * Print the version, configure the project with serialized build info and/or arguments and start the project build * process. * * @return true indicates normal return/exit. */ public boolean start(Configuration config) { Properties versionProperties = getBuildVersionProperties(); printVersion(versionProperties); try { if (shouldPrintUsage(config)) { printUsage(); return false; } if (config.getOptionBool(Configuration.KEY_DEBUG)) { Logger.getRootLogger().setLevel(Level.DEBUG); } // Set the logger. Now it is fully configured try { Configuration.setRealLog(new Log4jLog()); } catch (LaunchException e) { LOG.error("Unable to set real logger to config class; all previous messages are probably lost", e); } checkDeprecatedArguments(config, LOG); controller = createController(config, versionProperties); if (shouldStartJmxAgent(config)) { startJmxAgent(config); } if (shouldStartEmbeddedServer(config)) { startEmbeddedServer(config); } if (shouldPostDataToDashboard(config)) { startPostingToDashboard(config); } parseCCName(config); controller.resume(); } catch (Exception e) { LOG.fatal(e.getMessage()); printUsage(); return false; } return true; } private void startJmxAgent(Configuration conf) { agent = new CruiseControlControllerAgent(controller, parseJMXHttpPort(conf), parseRmiPort(conf), parseUser(conf), parsePassword(conf), parseXslPath(conf), parseEnableJMXAgentUtility(conf)); agent.start(); } private CruiseControlController createController(Configuration config, Properties versionProperties) throws CruiseControlException { CruiseControlController ccController = new CruiseControlController(); ccController.setVersionProperties(versionProperties); File configFile = new File(parseConfigFileName(config, CruiseControlController.DEFAULT_CONFIG_FILE_NAME)); try { ccController.setConfigFile(configFile); } catch (CruiseControlException e) { LOG.error("error setting config file on controller", e); throw e; } int maxNbThreads = ccController.getConfigManager().getCruiseControlConfig().getMaxNbThreads(); ThreadQueueProperties.setMaxThreadCount(maxNbThreads); return ccController; } /** * Starts the embedded Jetty server on the port given by the command line argument -webport and loads the * application from the path specified by the command line argument -webapppath. Uses default values if either * argument are not specified. * * @param conf configuration holder * @throws CruiseControlException if final configfile value is null */ void startEmbeddedServer(Configuration conf) throws CruiseControlException { String configFileName = parseConfigFileName(conf, CruiseControlController.DEFAULT_CONFIG_FILE_NAME); int jmxPort = parseJMXHttpPort(conf); int rmiPort = parseRmiPort(conf); int webPort = parseWebPort(conf); setUpSystemPropertiesForDashboard(configFileName, jmxPort, rmiPort, webPort); File ccHome; try { ccHome = conf.getOptionFile(Configuration.KEY_HOME_DIR); } catch (IllegalArgumentException e) { throw new CruiseControlException(e); } System.setProperty("jetty.home", ccHome.getAbsolutePath()); File jettyXml = new File(parseJettyXml(conf, ccHome.getAbsolutePath())); EmbeddedJettyServer embeddedJettyServer = new EmbeddedJettyServer(jettyXml, webPort); embeddedJettyServer.start(); } public static void setUpSystemPropertiesForDashboard(String configFileName, int jmxPort, int rmiPort, int webPort) { if (configFileName != null) { File configFile = new File(configFileName); if (!configFile.exists()) { throw new RuntimeException("Cannot find config file at " + configFile.getAbsolutePath()); } System.setProperty("cc.config.file", configFile.getAbsolutePath()); } System.setProperty("cc.rmiport", String.valueOf(rmiPort)); System.setProperty("cc.jmxport", String.valueOf(jmxPort)); System.setProperty("cc.webport", String.valueOf(webPort)); } static void checkDeprecatedArguments(Configuration conf, Logger logger) { if (conf.wasOptionSet(Configuration.KEY_PORT)) { logger.warn("WARNING: The port argument is deprecated. Use jmxport instead."); } } /** * System property name, when if true, bypasses the system.exit call when printing * the usage message. Intended for unit tests only. */ static final String SYSPROP_CCMAIN_SKIP_USAGE = "cc.main.skip.usage"; public static void printUsage() { if (Boolean.getBoolean(SYSPROP_CCMAIN_SKIP_USAGE)) { return; } System.out.println(""); System.out.println("Usage:"); System.out.println(""); System.out.println("Starts a continuous integration loop"); System.out.println(""); System.out.println("cruisecontrol [options]"); System.out.println(""); System.out.println("Build loop options are:"); System.out.println(""); System.out.println(" -configfile file configuration file; default config.xml"); System.out.println(" -" + Configuration.KEY_DEBUG + " set logging level to DEBUG"); System.out.println(" -" + Configuration.KEY_LOG4J_CONFIG + " url URL to a log4j config (example: " + "\"file:/c:/mylog4j.xml\")"); System.out.println(" -" + Configuration.KEY_PRINT_HELP1 + " or -" + Configuration.KEY_PRINT_HELP2 + " print this usage message"); System.out.println(""); System.out.println("Options when using JMX"); System.out.println(" Note: JMX server only started if -jmxport and/or -rmiport specified"); System.out.println(" -jmxport [number] port of the JMX HttpAdapter; default 8000"); System.out.println(" -rmiport [number] RMI port of the Controller; default 1099"); System.out.println(" -user username username for HttpAdapter; default no login required"); System.out.println(" -password pwd password for HttpAdapter; default no login required"); System.out.println(" -xslpath directory location of jmx xsl files; default files in package"); System.out.println(" -" + Configuration.KEY_JMX_AGENT_UTIL + " [true/false] load JMX Build Agent utility; default is true"); System.out.println(""); System.out.println("Options when using embedded Jetty"); System.out.println(" -webport [number] port for the Reporting website; default 8080, removing"); System.out.println(" this propery will make cruisecontrol start without Jetty"); System.out.println(" -jettyxml file Jetty configuration xml. Defaults to jetty.xml"); System.out.println(" -postenabled enabled switch of posting current build information to dashboard"); System.out.println(" default is true"); System.out.println(" -dashboardurl url the url for dashboard (used for posting build information)"); System.out.println(" default is http://localhost:8080/dashboard"); System.out.println(" -postinterval interval how frequently build information will be posted to dashboard"); System.out.println(" default is 5 (in seconds)."); System.out.println(" -ccname name A logical name which will be displayed in the"); System.out.println(" Reporting Application's status page."); System.out.println(""); } /** * Parse cc Name from arguments. * * @param args command line arguments. * @return the name of this instance if specified on the command line, otherwise DEFAULT_NAME. */ static String parseCCName(Configuration conf) { final String theCCName = conf.getOptionStr(Configuration.KEY_CC_NAME); System.setProperty("ccname", theCCName); return theCCName; } static boolean shouldPostDataToDashboard(Configuration conf) { return parseHttpPostingEnabled(conf) && BuildLoopMonitorRepository.getBuildLoopMonitor() == null; } public void startPostingToDashboard(Configuration conf) { String url = parseDashboardUrl(conf); long interval = parseHttpPostingInterval(conf); BuildLoopMonitorRepository.cancelExistingAndStartNewPosting(controller, new BuildLoopPostingConfiguration(url, interval)); } /** * Parse webport from arguments. * * @param args command line arguments. * @return the webport if specified, otherwise its default value. */ static int parseWebPort(Configuration conf) { return conf.getOptionInt(Configuration.KEY_WEB_PORT); //return MainArgs.parseInt(args, "webport", DEFAULT_WEB_PORT, DEFAULT_WEB_PORT); } /** * Parse webapppath from arguments. * * @param args command line arguments. * @return the webappdir if specified in the command line arguments, otherwise returns DEFAULT_WEBAPP_DIR. */ String parseWebappPath(Configuration conf) { final File webappPath = conf.getOptionDir(Configuration.KEY_WEBAPP_PATH); // TODO: validate is useless, since conf.getOptionDir() already validates the dir validateWebAppPath(webappPath, Configuration.KEY_WEBAPP_PATH); return webappPath.getAbsolutePath(); } /** * Parse dashboardpath (new webapp) from arguments. * * @param args command line arguments. * @return the directory if specified in the command line arguments, otherwise returns DEFAULT_DASHBOARD_PATH. */ static String parseDashboardPath(Configuration conf) { File dashboardPath = conf.getOptionFile(Configuration.KEY_DASHBOARD); validateWebAppPath(dashboardPath, Configuration.KEY_DASHBOARD); return dashboardPath.getAbsolutePath(); } private static void validateWebAppPath(File webappPath, String path) { if (!webappPath.isDirectory()) { throw new IllegalArgumentException( "'" + path + "' argument must specify an existing directory but was " + webappPath); } webappPath = new File(webappPath, "WEB-INF"); if (!webappPath.isDirectory()) { throw new IllegalArgumentException("'" + path + "' argument must point to an exploded web app. " + "directory " + webappPath + " does not exist"); } } /** * Parse configfile from arguments and override any existing configfile value from reading serialized Project info. * * @param conf configuration holder * @param configFileName existing configfile value read from serialized Project info (DEPRECATED!) * @return final value of configFileName; never null * @throws CruiseControlException if final configfile value is invalid */ static String parseConfigFileName(Configuration conf, String configFileName) throws CruiseControlException { try { configFileName = conf.getOptionFile(Configuration.KEY_CONFIG_FILE).getAbsolutePath(); } catch (IllegalArgumentException e) { // This is deprecated behaviour where the given config file is used instad of relaying on // Configuration's ability to check that the (default) config file does not exist LOG.warn("DEPRECATED behaviour. The default config file was not found (as reported by the" + "embedded exception), so relying on " + configFileName + " path instead", e); } catch (Exception e) { throw new CruiseControlException(e); } if (configFileName == null) { throw new CruiseControlException("'configfile' is a required argument to CruiseControl."); } return configFileName; } static String parseJettyXml(Configuration conf, String ccHome) { if (conf.wasOptionSet(Configuration.KEY_JETTY_XML)) { return conf.getOptionFile(Configuration.KEY_JETTY_XML).getAbsolutePath(); } final boolean nullOrEmpty = ccHome == null || ccHome.length() == 0; final String defaultJettyXml = conf.getOptionFile(Configuration.KEY_JETTY_XML).getPath(); return nullOrEmpty ? defaultJettyXml : ccHome + File.separatorChar + defaultJettyXml; } static boolean shouldStartJmxAgent(Configuration conf) { return conf.wasOptionSet(Configuration.KEY_JMX_PORT) || conf.wasOptionSet(Configuration.KEY_RMI_PORT) || conf.wasOptionSet(Configuration.KEY_PORT); } /** * If either -webport or -webapppath are specified on the command line, then the embedded Jetty server should be * started, otherwise it should not. * * @param conf configuration holder * @return true if the embedded Jetty server should be started, false if not. */ static boolean shouldStartEmbeddedServer(Configuration conf) { return conf.wasOptionSet(Configuration.KEY_WEB_PORT) || conf.wasOptionSet(Configuration.KEY_WEBAPP_PATH); } /** * Parse port number from arguments. * * @param conf configuration holder * @return port number * @throws IllegalArgumentException if port argument is invalid */ static int parseJMXHttpPort(Configuration conf) { if (conf.wasOptionSet(Configuration.KEY_JMX_PORT) && conf.wasOptionSet(Configuration.KEY_PORT)) { throw new IllegalArgumentException( "'jmxport' and 'port' arguments are not valid together. Use" + " 'jmxport' instead."); } else if (conf.wasOptionSet(Configuration.KEY_JMX_PORT)) { return conf.getOptionInt(Configuration.KEY_JMX_PORT); } else { return conf.getOptionInt(Configuration.KEY_PORT); } } static int parseRmiPort(Configuration conf) { return conf.getOptionInt(Configuration.KEY_RMI_PORT); } static String parseXslPath(Configuration conf) { return conf.getOptionDir(Configuration.KEY_XLS_PATH).getAbsolutePath(); } static CruiseControlControllerAgent.LOAD_JMX_AGENTUTIL parseEnableJMXAgentUtility(Configuration conf) { final String argval = conf.getOptionStr(Configuration.KEY_JMX_AGENT_UTIL); if (argval.isEmpty()) { /** default, if no command line arg present. Not an error if load fails. */ return CruiseControlControllerAgent.LOAD_JMX_AGENTUTIL.LOAD_IF_AVAILABLE; } if (Boolean.parseBoolean(argval)) { /** -agentutil true. Considered an error if load fails. */ return CruiseControlControllerAgent.LOAD_JMX_AGENTUTIL.FORCE_LOAD; } /** -agentutil false or not set. Do not attempt to load. */ return CruiseControlControllerAgent.LOAD_JMX_AGENTUTIL.FORCE_BYPASS; } /** * Parse password from arguments and override any existing password value from reading serialized Project info. * * @param args command line arguments * @return final value of password. */ static String parsePassword(Configuration conf) { if (conf.wasOptionSet(Configuration.KEY_PASSWORD)) { return conf.getOptionStr(Configuration.KEY_PASSWORD); } return null; } /** * Parse user from arguments and override any existing user value from reading serialized Project info. * * @param args command line arguments * @return final value of user. */ static String parseUser(Configuration conf) { if (conf.wasOptionSet(Configuration.KEY_USER)) { return conf.getOptionStr(Configuration.KEY_USER); } return null; } /** * @return the current version information, as indicated in the version.properties file. */ private static Properties getBuildVersionProperties() { Properties props = new Properties(); try { props.load(Main.class.getResourceAsStream("/version.properties")); } catch (IOException e) { LOG.error("Error reading version properties", e); } return props; } /** * Writes the current version information to the logging information stream. * @param props the current version information, as indicated in the version.properties file. */ private static void printVersion(Properties props) { LOG.info("CruiseControl Version " + props.getProperty("version") + " " + props.getProperty("version.info")); } static boolean shouldPrintUsage(Configuration conf) { return (conf.getOptionBool(Configuration.KEY_PRINT_HELP1) || conf.getOptionBool(Configuration.KEY_PRINT_HELP2)); } public void stop() { controller.pause(); agent.stop(); } public static String parseDashboardUrl(Configuration conf) { URL url = conf.getOptionUrl(Configuration.KEY_DASHBOARD_URL); // Special composition of some arguments if port has been set but dashboard url // has not. Use default dashboard url, but change the default port in it if (conf.wasOptionSet(Configuration.KEY_WEB_PORT) && !conf.wasOptionSet(Configuration.KEY_DASHBOARD_URL)) { try { url = new URL(url.getProtocol(), url.getHost(), conf.getOptionInt(Configuration.KEY_WEB_PORT), url.getPath()); } catch (MalformedURLException e) { // should not happen ... } } return url.toString(); } public static int parseHttpPostingInterval(Configuration conf) { return conf.getOptionInt(Configuration.KEY_POST_INTERVAL); } public static boolean parseHttpPostingEnabled(Configuration conf) { return conf.getOptionBool(Configuration.KEY_POST_ENABLED); } /** Implementation of the {@link LogInterface} passing data to Log4j logger instance */ private static class Log4jLog implements LogInterface { @Override public void error(Object message) { LOG.error(message); // use the context of Main } @Override public void warn(Object message) { LOG.warn(message); } @Override public void info(Object message) { LOG.info(message); } @Override /** Does nothing, throws LaunchException when called */ public void flush(LogInterface log) throws LaunchException { throw new LaunchException("Cannot flush log4j to nother log, probably trying to set " + "log4j when one already set"); } } }