/**************************************************************************** * CruiseControl, a Continuous Integration Toolkit * Copyright (c) 2001, 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.distributed; import java.io.IOException; import java.io.Serializable; import java.net.URL; import java.net.MalformedURLException; import java.rmi.Remote; import java.rmi.RemoteException; import java.rmi.server.ExportException; import java.util.prefs.Preferences; import java.util.prefs.BackingStoreException; import java.util.Iterator; import java.util.Properties; import java.util.Arrays; import java.util.List; import java.util.ArrayList; import java.util.Enumeration; import java.awt.GraphicsEnvironment; import net.jini.core.lookup.ServiceID; import net.jini.core.lookup.ServiceRegistrar; import net.jini.core.discovery.LookupLocator; import net.jini.discovery.DiscoveryEvent; import net.jini.discovery.DiscoveryListener; import net.jini.discovery.LookupLocatorDiscovery; import net.jini.lookup.ServiceIDListener; import net.jini.lookup.JoinManager; import net.jini.export.Exporter; import net.jini.jeri.BasicILFactory; import net.jini.jeri.BasicJeriExporter; import net.jini.jeri.tcp.TcpServerEndpoint; import net.sourceforge.cruisecontrol.distributed.core.PropertiesHelper; import net.sourceforge.cruisecontrol.distributed.core.ReggieUtil; import net.sourceforge.cruisecontrol.distributed.core.CCDistVersion; import net.sourceforge.cruisecontrol.util.MainArgs; import org.apache.log4j.Logger; public class BuildAgent implements DiscoveryListener, ServiceIDListener { static final String MAIN_ARG_AGENT_PROPS = "agentprops"; static final String MAIN_ARG_USER_PROPS = "userprops"; static final String MAIN_ARG_SKIP_UI = "skipUI"; // package visible to allow BuildAgentUI console logger access to this Logger static final Logger LOG = Logger.getLogger(BuildAgent.class); public static final String JAVA_SECURITY_POLICY = "java.security.policy"; private static final String JINI_POLICY_FILE = "jini.policy.file"; /** Optional unicast Lookup Registry URL. * A Unicast Lookup Locater is useful if multicast isn't working. */ private static final String REGISTRY_URL = "registry.url"; private final BuildAgentServiceImpl serviceImpl; private final PropertyEntry[] origEntries; private final Exporter exporter; private final JoinManager joinManager; private ServiceID serviceID; private final Remote proxy; private Properties entryProperties; private Properties configProperties; private final BuildAgentUI ui; /** Preferences node name, under which entry overrides are stored. */ static final String PREFS_NODE_ENTRY_OVERRIDES = "entryOverrides"; static interface LUSCountListener { public void lusCountChanged(final int newLUSCount); } private final List<LUSCountListener> lusCountListeners = new ArrayList<LUSCountListener>(); void addLUSCountListener(final LUSCountListener listener) { lusCountListeners.add(listener); } void removeLUSCountListener(final LUSCountListener listener) { lusCountListeners.remove(listener); } private int registrarCount = 0; private void fireLUSCountChanged() { for (final LUSCountListener lusCountListener : lusCountListeners) { lusCountListener.lusCountChanged(registrarCount); } } private void setRegCount(final int regCount) { registrarCount = regCount; LOG.info("Lookup Services found: " + registrarCount); fireLUSCountChanged(); } /** Only used for unit testing. */ private final int testAgentID; /** Only used for unit testing. */ private final ServiceIDListener testListener; /** * @param propsFile the agent properties file * @param userDefinedPropertiesFilename the user defined properties file * @param isSkipUI if true, do not show the build agent UI. */ private BuildAgent(final String propsFile, final String userDefinedPropertiesFilename, final boolean isSkipUI) { this (propsFile, userDefinedPropertiesFilename, isSkipUI, null, 0); } /** * This constructor only intended for unit tests. * @param propsFile the agent properties file * @param userDefinedPropertiesFilename the user defined properties file * @param isSkipUI if true, do not show the build agent UI. * @param testListener only used for unit testing. * @param testAgentID only used for unit testing. */ BuildAgent(final String propsFile, final String userDefinedPropertiesFilename, final boolean isSkipUI, final ServiceIDListener testListener, final int testAgentID) { // for unit testing only this.testAgentID = testAgentID; // for unit testing only this.testListener = testListener; loadProperties(propsFile, userDefinedPropertiesFilename); serviceImpl = new BuildAgentServiceImpl(this); serviceImpl.setAgentPropertiesFilename(propsFile); origEntries = SearchablePropertyEntries.getPropertiesAsEntryArray(entryProperties); if (!isSkipUI && !GraphicsEnvironment.isHeadless()) { LOG.info("Loading Build Agent UI (use param -" + MAIN_ARG_SKIP_UI + " to bypass)."); ui = new BuildAgentUI(this); //ui.updateAgentInfoUI(getService()); } else { LOG.info("Bypassing Build Agent UI (headless)."); ui = null; } exporter = new BasicJeriExporter(TcpServerEndpoint.getInstance(0), new BasicILFactory(), false, true); try { proxy = exporter.export(getService()); } catch (ExportException e) { final String message = "Error exporting service"; LOG.error(message, e); throw new RuntimeException(message, e); } // Use a comma separated list of Unicast Lookup Locaters (URL's) if defined in agent.properties. // Useful if multicast isn't working. final String registryURLList = configProperties.getProperty(REGISTRY_URL); final LookupLocatorDiscovery lld; if (registryURLList == null) { lld = null; } else { lld = new LookupLocatorDiscovery(parseUnicastLocators(registryURLList)); } try { if (serviceID == null) { joinManager = new JoinManager(getProxy(), getEntries(), this, lld, null); } else { LOG.warn("Didn't expect to have a serviceID: " + serviceID + " (agentID: " + testAgentID + "). Are we storing and re-using the serviceID now?"); joinManager = new JoinManager(getProxy(), getEntries(), serviceID, lld, null); } } catch (IOException e) { final String message = "Error starting discovery"; LOG.error(message, e); throw new RuntimeException(message, e); } getJoinManager().getDiscoveryManager().addDiscoveryListener(this); } /** * Parses a comma separated list of Unicast Lookup Locaters (URL's). * Useful if multicast isn't working. * @param registryURLList a comma separated list of Unicast Lookup Locaters (URL's). * @return null if the given registryURLList is null, or a LookupLocator array populated with the given URL's. */ private static LookupLocator[] parseUnicastLocators(String registryURLList) { final LookupLocator[] lookups; if (registryURLList == null) { lookups = null; } else { final String[] registryURLs = registryURLList.split(","); lookups = new LookupLocator[registryURLs.length]; for (int i = 0; i < registryURLs.length; i++) { try { lookups[i] = new LookupLocator(registryURLs[i]); } catch (MalformedURLException e) { final String message = "Error creating unicast lookup locator: " + registryURLs[i] + "; " + e.getMessage(); LOG.error(message, e); throw new RuntimeException(message, e); } LOG.info("Using Unicast LookupLocator URL: " + registryURLs[i]); } } return lookups; } private final Preferences prefsBase = Preferences.userNodeForPackage(this.getClass()); Preferences getPrefsRoot() { return prefsBase; } /** * Gets the EntryOverrides preferences node this this user, shared among all BuildAgents running * under this userID on the current machine. * //@todo Should this node be more granular, like per Agent ServiceID? if so we must store/resuse serviceID */ private final Preferences prefsEntryOverrides = prefsBase.node(PREFS_NODE_ENTRY_OVERRIDES); void setEntryOverrides(final PropertyEntry[] entryOverrides) { // clear stored override preferences settings clearOverridePrefs(); // store override props using Preferences api putEntryOverrides(prefsEntryOverrides, entryOverrides); // publish using entries reloaded via getEntries, which adds entry overrides from prefs joinManager.setAttributes(getEntries()); } /** * Store override props using Preferences api. * Exposed as package static for unit test cleanup to agent prefs. * @param prefsEntryOverrides preferences node under which to store agent preferences * @param entryOverrides entry overrides to store */ static void putEntryOverrides(final Preferences prefsEntryOverrides, final PropertyEntry[] entryOverrides) { for (final PropertyEntry entryOverride : entryOverrides) { prefsEntryOverrides.put(entryOverride.name, entryOverride.value); } } void clearEntryOverrides() { // clear stored override preferences settings clearOverridePrefs(); // publish using entries reloaded via getEntries, which adds entry overrides from prefs joinManager.setAttributes(getEntries()); } private void clearOverridePrefs() { // clear stored override preferences settings try { prefsEntryOverrides.clear(); } catch (BackingStoreException e) { LOG.error("Error clearing entry override prefs.", e); throw new RuntimeException(e); } } private Properties getEntryOverrideProps() { // check for entry overrides in preferences final String[] overrideKeys; try { overrideKeys = prefsEntryOverrides.keys(); } catch (BackingStoreException e) { LOG.error("Error reading entry override prefs keys.", e); throw new RuntimeException(e); } final Properties overrideEntryProps = new Properties(); if (overrideKeys.length > 0) { for (final String overrideKey : overrideKeys) { overrideEntryProps.put(overrideKey, prefsEntryOverrides.get(overrideKey, "unknown value")); } } return overrideEntryProps; } PropertyEntry[] getEntryOverrides() { return SearchablePropertyEntries.getPropertiesAsEntryArray(getEntryOverrideProps()); } /** * @param propsFile path to config properties file * @param userDefinedPropertiesFilename path to user properties file */ private void loadProperties(final String propsFile, final String userDefinedPropertiesFilename) { configProperties = (Properties) PropertiesHelper.loadRequiredProperties(propsFile); entryProperties = new SearchablePropertyEntries(userDefinedPropertiesFilename).getProperties(); final String policyFileValue = configProperties.getProperty(JINI_POLICY_FILE); LOG.info("policyFileValue: " + policyFileValue); // resource loading technique below dies in webstart //URL policyFile = ClassLoader.getSystemClassLoader().getResource(policyFileValue); final URL policyFile = BuildAgent.class.getClassLoader().getResource(policyFileValue); LOG.info("policyFile: " + policyFile); System.setProperty(JAVA_SECURITY_POLICY, policyFile.toExternalForm()); ReggieUtil.setupRMISecurityManager(); } private Exporter getExporter() { return exporter; } private JoinManager getJoinManager() { return joinManager; } PropertyEntry[] getEntries() { final PropertyEntry[] currentEntries; final Properties entryOverrideProps = getEntryOverrideProps(); if (entryOverrideProps.size() > 0) { // add system entries first (preserves order) final Properties systemEntryProps = SearchablePropertyEntries.getSystemEntryProps(); // use a props object to enforce precendence of overrides over original settings final Properties allEntries = new Properties(); allEntries.putAll(systemEntryProps); // add props loaded from user-defined.properties file allEntries.putAll(entryProperties); // now add override entries that do NOT step on system entries String key; String value; final Enumeration enm = entryOverrideProps.keys(); while (enm.hasMoreElements()) { key = (String) enm.nextElement(); value = (String) entryOverrideProps.get(key); // don't allow override of system entry props if (!systemEntryProps.containsKey(key)) { allEntries.put(key, value); } else { LOG.warn("WARNING: Can't override system entry: " + key + "=" + systemEntryProps.get(key) + " with new value: " + value); } } // add to original entries currentEntries = SearchablePropertyEntries.getPropertiesAsEntryArray(allEntries); } else { // use original props file entries currentEntries = origEntries; } return currentEntries; } void addAgentStatusListener(final BuildAgent.AgentStatusListener listener) { serviceImpl.addAgentStatusListener(listener); } void removeAgentStatusListener(final BuildAgent.AgentStatusListener listener) { serviceImpl.removeAgentStatusListener(listener); } /** Only for unit testing. */ private static boolean isTerminateFast; /** Only for unit testing. */ static void setTerminateFast() { isTerminateFast = true; } /** Only for unit testing. * @param agent the unit test agent to terminate. */ void terminateTestAgent(final BuildAgent agent) { LOG.info("Terminating test agent (agentID: " + agent.testAgentID + ")"); agent.terminate(); if (agent.testAgentID == 0) { throw new IllegalStateException("This does not appear to be a unit test Agent, agentID: " + agent.testAgentID); } } private void terminate() { LOG.info("Terminating build agent."); int unexportAttempts = 0; while (!getExporter().unexport(false) && unexportAttempts < 10) { // wait a bit try { Thread.sleep(500); } catch (InterruptedException e) { LOG.warn("Sleep interrupted during terminate.unexport", e); } unexportAttempts++; } if (!getExporter().unexport(false)) { LOG.warn("Unexport of Agent service failed. Forcing export."); getExporter().unexport(true); } getJoinManager().terminate(); if (!isTerminateFast) { // allow some time for cleanup try { Thread.sleep(2000); } catch (InterruptedException e) { LOG.warn("Sleep interrupted during terminate", e); } } if (ui != null) { ui.dispose(); LOG.info("UI disposed"); } } private Remote getProxy() { return proxy; } public BuildAgentService getService() { return serviceImpl; } /** * Called when the JoinManager gets a valid ServiceID from a lookup * service. * *@param serviceID the service ID assigned by the lookup service. */ public void serviceIDNotify(final ServiceID serviceID) { // @todo technically, should serviceID be stored permanently and reused?.... this.serviceID = serviceID; LOG.info("ServiceID assigned: " + this.serviceID + (testAgentID == 0 ? "" : " (agentID: " + testAgentID + ")") ); if (ui != null) { ui.updateAgentInfoUI(getService()); } // for unit testing only if (testListener != null) { testListener.serviceIDNotify(serviceID); } } ServiceID getServiceID() { return serviceID; } private void logRegistration(final ServiceRegistrar registrar) { String host = null; try { host = registrar.getLocator().getHost(); } catch (RemoteException e) { LOG.warn("Failed to get registrar's hostname.", e); } LOG.info("Registering BuildAgentService with Registrar: " + host); final String machineName = (String) entryProperties.get(SearchablePropertyEntries.HOSTNAME); LOG.debug("Registered machineName: " + machineName); LOG.debug("Entries: "); for (Iterator iter = entryProperties.keySet().iterator(); iter.hasNext();) { final String key = (String) iter.next(); LOG.debug(" " + key + " = " + entryProperties.get(key)); } } private boolean isNotFirstDiscovery; public void discovered(final DiscoveryEvent evt) { final ServiceRegistrar[] registrarsArray = evt.getRegistrars(); for (final ServiceRegistrar registrar : registrarsArray) { logRegistration(registrar); LOG.debug("Registered with registrar: " + registrar.getServiceID()); } if (!isNotFirstDiscovery) { LOG.info("BuildAgentService open for business..."); isNotFirstDiscovery = true; } setRegCount(getJoinManager().getDiscoveryManager().getRegistrars().length); } public void discarded(final DiscoveryEvent evt) { final ServiceRegistrar[] registrarsArray = evt.getRegistrars(); for (final ServiceRegistrar registrar : registrarsArray) { LOG.debug("Discarded registrar: " + registrar.getServiceID()); } setRegCount(getJoinManager().getDiscoveryManager().getRegistrars().length); } private static final Object KEEP_ALIVE = new Object(); private static Thread mainThread; private static void setMainThread(final Thread newMainThread) { mainThread = newMainThread; } static Thread getMainThread() { return mainThread; } /** Intended only for unit tests to avoid killing the unit test VM. */ private static boolean isSkipMainSystemExit; static void setSkipMainSystemExit() { isSkipMainSystemExit = true; } public static void main(final String[] args) { setMainThread(Thread.currentThread()); LOG.info("Starting agent...args: " + Arrays.asList(args).toString()); CCDistVersion.printCCDistVersion(); if (shouldPrintUsage(args)) { printUsage(); } final BuildAgent buildAgent = new BuildAgent( MainArgs.parseArgument(args, MAIN_ARG_AGENT_PROPS, BuildAgentServiceImpl.DEFAULT_AGENT_PROPERTIES_FILE, BuildAgentServiceImpl.DEFAULT_AGENT_PROPERTIES_FILE), MainArgs.parseArgument(args, MAIN_ARG_USER_PROPS, BuildAgentServiceImpl.DEFAULT_USER_DEFINED_PROPERTIES_FILE, BuildAgentServiceImpl.DEFAULT_USER_DEFINED_PROPERTIES_FILE), MainArgs.argumentPresent(args, MAIN_ARG_SKIP_UI) ); // stay around forever synchronized (KEEP_ALIVE) { try { KEEP_ALIVE.wait(); } catch (InterruptedException e) { LOG.info("Keep Alive wait interrupted", e); } finally { buildAgent.terminate(); } } final String mainThreadName = Thread.currentThread().getName(); LOG.info("Agent main thread (" + mainThreadName + ") exiting."); // don't call sys exit during unit tests if (!isSkipMainSystemExit) { // on some JVM's (webstart - restart) the BuildAgent.kill() call doesn't return, // so sys exit is also done here. LOG.info("Agent main thread (" + mainThreadName + ") calling System.exit()."); System.exit(0); } else { LOG.debug("Agent main thread (" + mainThreadName + ") skipping System.exit(), only valid in unit tests."); } } private static boolean shouldPrintUsage(String[] args) { return MainArgs.findIndex(args, "?") != MainArgs.NOT_FOUND || MainArgs.findIndex(args, "help") != MainArgs.NOT_FOUND; } private static void printUsage() { System.out.println(""); System.out.println("Usage:"); System.out.println(""); System.out.println("Starts a distributed Build Agent"); System.out.println(""); System.out.println(BuildAgent.class.getName() + " [options]"); System.out.println(""); System.out.println("Build Agent options are:"); System.out.println(""); System.out.println(" -" + MAIN_ARG_AGENT_PROPS + " file agent properties file; default " + BuildAgentServiceImpl.DEFAULT_AGENT_PROPERTIES_FILE); System.out.println(" -" + MAIN_ARG_USER_PROPS + " file user defined properties file; default " + BuildAgentServiceImpl.DEFAULT_USER_DEFINED_PROPERTIES_FILE); System.out.println(" -" + MAIN_ARG_SKIP_UI + " run in headless mode"); System.out.println(" -? or -help print this usage message"); System.out.println(""); } public static void kill() { final Thread main = getMainThread(); if (main != null) { final String mainThreadName = main.getName(); main.interrupt(); LOG.info("Waiting for main thread (" + mainThreadName + ") to finish."); try { main.join(30 * 1000); //main.join(); } catch (InterruptedException e) { LOG.error("Error while waiting for Agent thread (" + mainThreadName + ") to die.", e); } if (main.isAlive()) { main.interrupt(); // how can this happen? LOG.error("Main thread (" + mainThreadName + ") should have died."); } setMainThread(null); } else { LOG.info("WARNING: Kill called, MainThread is null. Doing nothing. Acceptable only in Unit Tests."); } } static interface AgentStatusListener extends Serializable { public void statusChanged(BuildAgentService buildAgentServiceImpl); } }