001: /****************************************************************************
002: * CruiseControl, a Continuous Integration Toolkit
003: * Copyright (c) 2001, ThoughtWorks, Inc.
004: * 200 E. Randolph, 25th Floor
005: * Chicago, IL 60601 USA
006: * All rights reserved.
007: *
008: * Redistribution and use in source and binary forms, with or without
009: * modification, are permitted provided that the following conditions
010: * are met:
011: *
012: * + Redistributions of source code must retain the above copyright
013: * notice, this list of conditions and the following disclaimer.
014: *
015: * + Redistributions in binary form must reproduce the above
016: * copyright notice, this list of conditions and the following
017: * disclaimer in the documentation and/or other materials provided
018: * with the distribution.
019: *
020: * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
021: * names of its contributors may be used to endorse or promote
022: * products derived from this software without specific prior
023: * written permission.
024: *
025: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
026: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
027: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
028: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
029: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
030: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
031: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
032: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
033: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
034: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
035: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
036: ****************************************************************************/package net.sourceforge.cruisecontrol.distributed;
037:
038: import java.io.IOException;
039: import java.net.URL;
040: import java.net.MalformedURLException;
041: import java.rmi.Remote;
042: import java.rmi.RemoteException;
043: import java.rmi.server.ExportException;
044: import java.util.prefs.Preferences;
045: import java.util.prefs.BackingStoreException;
046: import java.util.Iterator;
047: import java.util.Properties;
048: import java.util.Arrays;
049: import java.util.List;
050: import java.util.ArrayList;
051: import java.util.Enumeration;
052: import java.awt.GraphicsEnvironment;
053:
054: import net.jini.core.lookup.ServiceID;
055: import net.jini.core.lookup.ServiceRegistrar;
056: import net.jini.core.discovery.LookupLocator;
057: import net.jini.discovery.DiscoveryEvent;
058: import net.jini.discovery.DiscoveryListener;
059: import net.jini.discovery.LookupLocatorDiscovery;
060: import net.jini.lookup.ServiceIDListener;
061: import net.jini.lookup.JoinManager;
062: import net.jini.export.Exporter;
063: import net.jini.jeri.BasicILFactory;
064: import net.jini.jeri.BasicJeriExporter;
065: import net.jini.jeri.tcp.TcpServerEndpoint;
066: import net.sourceforge.cruisecontrol.distributed.core.PropertiesHelper;
067: import net.sourceforge.cruisecontrol.distributed.core.ReggieUtil;
068: import net.sourceforge.cruisecontrol.distributed.core.CCDistVersion;
069: import net.sourceforge.cruisecontrol.util.MainArgs;
070:
071: import org.apache.log4j.Logger;
072:
073: public class BuildAgent implements DiscoveryListener, ServiceIDListener {
074:
075: static final String MAIN_ARG_AGENT_PROPS = "agentprops";
076: static final String MAIN_ARG_USER_PROPS = "userprops";
077: static final String MAIN_ARG_SKIP_UI = "skipUI";
078:
079: // package visible to allow BuildAgentUI console logger access to this Logger
080: static final Logger LOG = Logger.getLogger(BuildAgent.class);
081:
082: public static final String JAVA_SECURITY_POLICY = "java.security.policy";
083: private static final String JINI_POLICY_FILE = "jini.policy.file";
084:
085: /** Optional unicast Lookup Registry URL.
086: * A Unicast Lookup Locater is useful if multicast isn't working. */
087: private static final String REGISTRY_URL = "registry.url";
088:
089: private final BuildAgentServiceImpl serviceImpl;
090: private final PropertyEntry[] origEntries;
091: private final Exporter exporter;
092: private final JoinManager joinManager;
093: private ServiceID serviceID;
094: private final Remote proxy;
095:
096: private Properties entryProperties;
097: private Properties configProperties;
098:
099: private final BuildAgentUI ui;
100:
101: static interface LUSCountListener {
102: public void lusCountChanged(final int newLUSCount);
103: }
104:
105: private final List lusCountListeners = new ArrayList();
106:
107: void addLUSCountListener(final LUSCountListener listener) {
108: lusCountListeners.add(listener);
109: }
110:
111: void removeLUSCountListener(final LUSCountListener listener) {
112: lusCountListeners.remove(listener);
113: }
114:
115: private int registrarCount = 0;
116:
117: private void fireLUSCountChanged() {
118: for (int i = 0; i < lusCountListeners.size(); i++) {
119: ((LUSCountListener) lusCountListeners.get(i))
120: .lusCountChanged(registrarCount);
121: }
122: }
123:
124: private void setRegCount(final int regCount) {
125: registrarCount = regCount;
126: LOG.info("Lookup Services found: " + registrarCount);
127: fireLUSCountChanged();
128: }
129:
130: /** Only used for unit testing. */
131: private final int testAgentID;
132: /** Only used for unit testing. */
133: private final ServiceIDListener testListener;
134:
135: /**
136: * @param propsFile the agent properties file
137: * @param userDefinedPropertiesFilename the user defined properties file
138: * @param isSkipUI if true, do not show the build agent UI.
139: */
140: private BuildAgent(final String propsFile,
141: final String userDefinedPropertiesFilename,
142: final boolean isSkipUI) {
143:
144: this (propsFile, userDefinedPropertiesFilename, isSkipUI, null,
145: 0);
146: }
147:
148: /**
149: * This constructor only intended for unit tests.
150: * @param propsFile the agent properties file
151: * @param userDefinedPropertiesFilename the user defined properties file
152: * @param isSkipUI if true, do not show the build agent UI.
153: * @param testListener only used for unit testing.
154: * @param testAgentID only used for unit testing.
155: */
156: BuildAgent(final String propsFile,
157: final String userDefinedPropertiesFilename,
158: final boolean isSkipUI,
159: final ServiceIDListener testListener, final int testAgentID) {
160:
161: // for unit testing only
162: this .testAgentID = testAgentID;
163: // for unit testing only
164: this .testListener = testListener;
165:
166: loadProperties(propsFile, userDefinedPropertiesFilename);
167:
168: serviceImpl = new BuildAgentServiceImpl(this );
169: serviceImpl.setAgentPropertiesFilename(propsFile);
170:
171: origEntries = SearchablePropertyEntries
172: .getPropertiesAsEntryArray(entryProperties);
173: if (!isSkipUI && !GraphicsEnvironment.isHeadless()) {
174: LOG.info("Loading Build Agent UI (use param -"
175: + MAIN_ARG_SKIP_UI + " to bypass).");
176: ui = new BuildAgentUI(this );
177: //ui.updateAgentInfoUI(getService());
178: } else {
179: LOG.info("Bypassing Build Agent UI (headless).");
180: ui = null;
181: }
182:
183: exporter = new BasicJeriExporter(TcpServerEndpoint
184: .getInstance(0), new BasicILFactory(), false, true);
185:
186: try {
187: proxy = exporter.export(getService());
188: } catch (ExportException e) {
189: final String message = "Error exporting service";
190: LOG.error(message, e);
191: throw new RuntimeException(message, e);
192: }
193:
194: // Use a comma separated list of Unicast Lookup Locaters (URL's) if defined in agent.properties.
195: // Useful if multicast isn't working.
196: final String registryURLList = configProperties
197: .getProperty(REGISTRY_URL);
198: final LookupLocatorDiscovery lld;
199: if (registryURLList == null) {
200: lld = null;
201: } else {
202: lld = new LookupLocatorDiscovery(
203: parseUnicastLocators(registryURLList));
204: }
205:
206: try {
207: if (serviceID == null) {
208: joinManager = new JoinManager(getProxy(), getEntries(),
209: this , lld, null);
210: } else {
211: LOG
212: .warn("Didn't expect to have a serviceID: "
213: + serviceID
214: + " (agentID: "
215: + testAgentID
216: + "). Are we storing and re-using the serviceID now?");
217: joinManager = new JoinManager(getProxy(), getEntries(),
218: serviceID, lld, null);
219: }
220: } catch (IOException e) {
221: final String message = "Error starting discovery";
222: LOG.error(message, e);
223: throw new RuntimeException(message, e);
224: }
225:
226: getJoinManager().getDiscoveryManager().addDiscoveryListener(
227: this );
228: }
229:
230: /**
231: * Parses a comma separated list of Unicast Lookup Locaters (URL's).
232: * Useful if multicast isn't working.
233: * @param registryURLList a comma separated list of Unicast Lookup Locaters (URL's).
234: * @return null if the given registryURLList is null, or a LookupLocator array populated with the given URL's.
235: */
236: private static LookupLocator[] parseUnicastLocators(
237: String registryURLList) {
238: final LookupLocator[] lookups;
239: if (registryURLList == null) {
240: lookups = null;
241: } else {
242: final String[] registryURLs = registryURLList.split(",");
243: lookups = new LookupLocator[registryURLs.length];
244: for (int i = 0; i < registryURLs.length; i++) {
245: try {
246: lookups[i] = new LookupLocator(registryURLs[i]);
247: } catch (MalformedURLException e) {
248: final String message = "Error creating unicast lookup locator: "
249: + registryURLs[i] + "; " + e.getMessage();
250: LOG.error(message, e);
251: throw new RuntimeException(message, e);
252: }
253: LOG.info("Using Unicast LookupLocator URL: "
254: + registryURLs[i]);
255: }
256: }
257: return lookups;
258: }
259:
260: private final Preferences prefsBase = Preferences
261: .userNodeForPackage(this .getClass());
262:
263: Preferences getPrefsRoot() {
264: return prefsBase;
265: }
266:
267: /**
268: * Gets the EntryOverrides preferences node this this user, shared among all BuildAgents running
269: * under this userID on the current machine.
270: * @todo Should this node be more granular, like per Agent ServiceID? if so we must store/resuse serviceID
271: */
272: private final Preferences prefsEntryOverrides = prefsBase
273: .node("entryOverrides");
274:
275: void setEntryOverrides(final PropertyEntry[] entryOverrides) {
276: // clear stored override preferences settings
277: clearOverridePrefs();
278:
279: // store override props using Preferences api
280: for (int i = 0; i < entryOverrides.length; i++) {
281: prefsEntryOverrides.put(entryOverrides[i].name,
282: entryOverrides[i].value);
283: }
284:
285: // publish using entries reloaded via getEntries, which adds entry overrides from prefs
286: joinManager.setAttributes(getEntries());
287: }
288:
289: void clearEntryOverrides() {
290:
291: // clear stored override preferences settings
292: clearOverridePrefs();
293:
294: // publish using entries reloaded via getEntries, which adds entry overrides from prefs
295: joinManager.setAttributes(getEntries());
296: }
297:
298: private void clearOverridePrefs() {
299: // clear stored override preferences settings
300: try {
301: prefsEntryOverrides.clear();
302: } catch (BackingStoreException e) {
303: LOG.error("Error clearing entry override prefs.", e);
304: throw new RuntimeException(e);
305: }
306: }
307:
308: private Properties getEntryOverrideProps() {
309: // check for entry overrides in preferences
310: final String[] overrideKeys;
311: try {
312: overrideKeys = prefsEntryOverrides.keys();
313: } catch (BackingStoreException e) {
314: LOG.error("Error reading entry override prefs keys.", e);
315: throw new RuntimeException(e);
316: }
317: final Properties overrideEntryProps = new Properties();
318: if (overrideKeys.length > 0) {
319: String key;
320: for (int i = 0; i < overrideKeys.length; i++) {
321: key = overrideKeys[i];
322: overrideEntryProps.put(key, prefsEntryOverrides.get(
323: key, "unknown value"));
324: }
325: }
326: return overrideEntryProps;
327: }
328:
329: PropertyEntry[] getEntryOverrides() {
330: return SearchablePropertyEntries
331: .getPropertiesAsEntryArray(getEntryOverrideProps());
332: }
333:
334: /**
335: * @param propsFile path to config properties file
336: * @param userDefinedPropertiesFilename path to user properties file
337: */
338: private void loadProperties(final String propsFile,
339: final String userDefinedPropertiesFilename) {
340: configProperties = (Properties) PropertiesHelper
341: .loadRequiredProperties(propsFile);
342: entryProperties = new SearchablePropertyEntries(
343: userDefinedPropertiesFilename).getProperties();
344:
345: final String policyFileValue = configProperties
346: .getProperty(JINI_POLICY_FILE);
347: LOG.info("policyFileValue: " + policyFileValue);
348:
349: // resource loading technique below dies in webstart
350: //URL policyFile = ClassLoader.getSystemClassLoader().getResource(policyFileValue);
351: final URL policyFile = BuildAgent.class.getClassLoader()
352: .getResource(policyFileValue);
353: LOG.info("policyFile: " + policyFile);
354: System.setProperty(JAVA_SECURITY_POLICY, policyFile
355: .toExternalForm());
356: ReggieUtil.setupRMISecurityManager();
357: }
358:
359: private Exporter getExporter() {
360: return exporter;
361: }
362:
363: private JoinManager getJoinManager() {
364: return joinManager;
365: }
366:
367: PropertyEntry[] getEntries() {
368:
369: final PropertyEntry[] currentEntries;
370: final Properties entryOverrideProps = getEntryOverrideProps();
371: if (entryOverrideProps.size() > 0) {
372: // add system entries first (preserves order)
373: final Properties systemEntryProps = SearchablePropertyEntries
374: .getSystemEntryProps();
375: // use a props object to enforce precendence of overrides over original settings
376: final Properties allEntries = new Properties();
377: allEntries.putAll(systemEntryProps);
378: // add props loaded from user-defined.properties file
379: allEntries.putAll(entryProperties);
380: // now add override entries that do NOT step on system entries
381: String key;
382: String value;
383: final Enumeration enm = entryOverrideProps.keys();
384: while (enm.hasMoreElements()) {
385: key = (String) enm.nextElement();
386: value = (String) entryOverrideProps.get(key);
387: // don't allow override of system entry props
388: if (!systemEntryProps.containsKey(key)) {
389: allEntries.put(key, value);
390: } else {
391: LOG.warn("WARNING: Can't override system entry: "
392: + key + "=" + systemEntryProps.get(key)
393: + " with new value: " + value);
394: }
395: }
396:
397: // add to original entries
398: currentEntries = SearchablePropertyEntries
399: .getPropertiesAsEntryArray(allEntries);
400: } else {
401: // use original props file entries
402: currentEntries = origEntries;
403: }
404: return currentEntries;
405: }
406:
407: void addAgentStatusListener(
408: final BuildAgent.AgentStatusListener listener) {
409: serviceImpl.addAgentStatusListener(listener);
410: }
411:
412: void removeAgentStatusListener(
413: final BuildAgent.AgentStatusListener listener) {
414: serviceImpl.removeAgentStatusListener(listener);
415: }
416:
417: /** Only for unit testing. */
418: private static boolean isTerminateFast;
419:
420: /** Only for unit testing. */
421: static void setTerminateFast() {
422: isTerminateFast = true;
423: }
424:
425: /** Only for unit testing.
426: * @param agent the unit test agent to terminate.
427: */
428: void terminateTestAgent(final BuildAgent agent) {
429: LOG.info("Terminating test agent (agentID: "
430: + agent.testAgentID + ")");
431: agent.terminate();
432: if (agent.testAgentID == 0) {
433: throw new IllegalStateException(
434: "This does not appear to be a unit test Agent, agentID: "
435: + agent.testAgentID);
436: }
437: }
438:
439: private void terminate() {
440: LOG.info("Terminating build agent.");
441: int unexportAttempts = 0;
442: while (!getExporter().unexport(false) && unexportAttempts < 10) {
443: // wait a bit
444: try {
445: Thread.sleep(500);
446: } catch (InterruptedException e) {
447: LOG.warn("Sleep interrupted during terminate.unexport",
448: e);
449: }
450: unexportAttempts++;
451: }
452: if (!getExporter().unexport(false)) {
453: LOG
454: .warn("Unexport of Agent service failed. Forcing export.");
455: getExporter().unexport(true);
456: }
457: getJoinManager().terminate();
458:
459: if (!isTerminateFast) {
460: // allow some time for cleanup
461: try {
462: Thread.sleep(2000);
463: } catch (InterruptedException e) {
464: LOG.warn("Sleep interrupted during terminate", e);
465: }
466: }
467:
468: if (ui != null) {
469: ui.dispose();
470: LOG.info("UI disposed");
471: }
472: }
473:
474: private Remote getProxy() {
475: return proxy;
476: }
477:
478: public BuildAgentService getService() {
479: return serviceImpl;
480: }
481:
482: /**
483: * Called when the JoinManager gets a valid ServiceID from a lookup
484: * service.
485: *
486: *@param serviceID the service ID assigned by the lookup service.
487: */
488: public void serviceIDNotify(final ServiceID serviceID) {
489: // @todo technically, should serviceID be stored permanently and reused?....
490: this .serviceID = serviceID;
491: LOG.info("ServiceID assigned: " + this .serviceID
492: + (testAgentID == 0 ? "" : " (agentID: " + testAgentID)
493: + ")");
494: if (ui != null) {
495: ui.updateAgentInfoUI(getService());
496: }
497:
498: // for unit testing only
499: if (testListener != null) {
500: testListener.serviceIDNotify(serviceID);
501: }
502: }
503:
504: ServiceID getServiceID() {
505: return serviceID;
506: }
507:
508: private void logRegistration(final ServiceRegistrar registrar) {
509: String host = null;
510: try {
511: host = registrar.getLocator().getHost();
512: } catch (RemoteException e) {
513: LOG.warn("Failed to get registrar's hostname");
514: }
515: LOG.info("Registering BuildAgentService with Registrar: "
516: + host);
517:
518: final String machineName = (String) entryProperties
519: .get(SearchablePropertyEntries.HOSTNAME);
520: LOG.debug("Registered machineName: " + machineName);
521:
522: LOG.debug("Entries: ");
523: for (Iterator iter = entryProperties.keySet().iterator(); iter
524: .hasNext();) {
525: final String key = (String) iter.next();
526: LOG.debug(" " + key + " = " + entryProperties.get(key));
527: }
528: }
529:
530: private boolean isNotFirstDiscovery;
531:
532: public void discovered(final DiscoveryEvent evt) {
533: final ServiceRegistrar[] registrarsArray = evt.getRegistrars();
534: ServiceRegistrar registrar;
535: for (int n = 0; n < registrarsArray.length; n++) {
536: registrar = registrarsArray[n];
537: logRegistration(registrar);
538: LOG.debug("Registered with registrar: "
539: + registrar.getServiceID());
540: }
541: if (!isNotFirstDiscovery) {
542: LOG.info("BuildAgentService open for business...");
543: isNotFirstDiscovery = true;
544: }
545:
546: setRegCount(getJoinManager().getDiscoveryManager()
547: .getRegistrars().length);
548: }
549:
550: public void discarded(final DiscoveryEvent evt) {
551: final ServiceRegistrar[] registrarsArray = evt.getRegistrars();
552: ServiceRegistrar registrar;
553: for (int n = 0; n < registrarsArray.length; n++) {
554: registrar = registrarsArray[n];
555: LOG.debug("Discarded registrar: "
556: + registrar.getServiceID());
557: }
558:
559: setRegCount(getJoinManager().getDiscoveryManager()
560: .getRegistrars().length);
561: }
562:
563: private static final Object KEEP_ALIVE = new Object();
564: private static Thread mainThread;
565:
566: private static void setMainThread(final Thread newMainThread) {
567: mainThread = newMainThread;
568: }
569:
570: static Thread getMainThread() {
571: return mainThread;
572: }
573:
574: /** Intended only for unit tests to avoid killing the unit test VM. */
575: private static boolean isSkipMainSystemExit;
576:
577: static void setSkipMainSystemExit() {
578: isSkipMainSystemExit = true;
579: }
580:
581: public static void main(final String[] args) {
582:
583: setMainThread(Thread.currentThread());
584:
585: LOG.info("Starting agent...args: "
586: + Arrays.asList(args).toString());
587:
588: CCDistVersion.printCCDistVersion();
589:
590: if (shouldPrintUsage(args)) {
591: printUsage();
592: }
593:
594: final BuildAgent buildAgent = new BuildAgent(
595: MainArgs
596: .parseArgument(
597: args,
598: MAIN_ARG_AGENT_PROPS,
599: BuildAgentServiceImpl.DEFAULT_AGENT_PROPERTIES_FILE,
600: BuildAgentServiceImpl.DEFAULT_AGENT_PROPERTIES_FILE),
601:
602: MainArgs
603: .parseArgument(
604: args,
605: MAIN_ARG_USER_PROPS,
606: BuildAgentServiceImpl.DEFAULT_USER_DEFINED_PROPERTIES_FILE,
607: BuildAgentServiceImpl.DEFAULT_USER_DEFINED_PROPERTIES_FILE),
608:
609: MainArgs.argumentPresent(args, MAIN_ARG_SKIP_UI));
610:
611: // stay around forever
612: synchronized (KEEP_ALIVE) {
613: try {
614: KEEP_ALIVE.wait();
615: } catch (InterruptedException e) {
616: LOG.info("Keep Alive wait interrupted", e);
617: } finally {
618: buildAgent.terminate();
619: }
620: }
621:
622: final String mainThreadName = Thread.currentThread().getName();
623: LOG.info("Agent main thread (" + mainThreadName + ") exiting.");
624: // don't call sys exit during unit tests
625: if (!isSkipMainSystemExit) {
626: // on some JVM's (webstart - restart) the BuildAgent.kill() call doesn't return,
627: // so sys exit is also done here.
628: LOG.info("Agent main thread (" + mainThreadName
629: + ") calling System.exit().");
630: System.exit(0);
631: } else {
632: LOG
633: .debug("Agent main thread ("
634: + mainThreadName
635: + ") skipping System.exit(), only valid in unit tests.");
636: }
637: }
638:
639: private static boolean shouldPrintUsage(String[] args) {
640: return MainArgs.findIndex(args, "?") != MainArgs.NOT_FOUND
641: || MainArgs.findIndex(args, "help") != MainArgs.NOT_FOUND;
642: }
643:
644: private static void printUsage() {
645: System.out.println("");
646: System.out.println("Usage:");
647: System.out.println("");
648: System.out.println("Starts a distributed Build Agent");
649: System.out.println("");
650: System.out.println(BuildAgent.class.getName() + " [options]");
651: System.out.println("");
652: System.out.println("Build Agent options are:");
653: System.out.println("");
654: System.out.println(" -" + MAIN_ARG_AGENT_PROPS
655: + " file agent properties file; default "
656: + BuildAgentServiceImpl.DEFAULT_AGENT_PROPERTIES_FILE);
657: System.out
658: .println(" -"
659: + MAIN_ARG_USER_PROPS
660: + " file user defined properties file; default "
661: + BuildAgentServiceImpl.DEFAULT_USER_DEFINED_PROPERTIES_FILE);
662: System.out.println(" -" + MAIN_ARG_SKIP_UI
663: + " run in headless mode");
664: System.out
665: .println(" -? or -help print this usage message");
666: System.out.println("");
667: }
668:
669: public static void kill() {
670: final Thread main = getMainThread();
671: if (main != null) {
672: final String mainThreadName = main.getName();
673: main.interrupt();
674: LOG.info("Waiting for main thread (" + mainThreadName
675: + ") to finish.");
676: try {
677: main.join(30 * 1000);
678: //main.join();
679: } catch (InterruptedException e) {
680: LOG.error("Error while waiting for Agent thread ("
681: + mainThreadName + ") to die.", e);
682: }
683: if (main.isAlive()) {
684: main.interrupt(); // how can this happen?
685: LOG.error("Main thread (" + mainThreadName
686: + ") should have died.");
687: }
688: setMainThread(null);
689: } else {
690: LOG
691: .info("WARNING: Kill called, MainThread is null. Doing nothing. Acceptable only in Unit Tests.");
692: }
693: }
694:
695: static interface AgentStatusListener {
696: public void statusChanged(
697: BuildAgentService buildAgentServiceImpl);
698: }
699:
700: }
|