0001: /****************************************************************************
0002: * CruiseControl, a Continuous Integration Toolkit
0003: * Copyright (c) 2001, ThoughtWorks, Inc.
0004: * 200 E. Randolph, 25th Floor
0005: * Chicago, IL 60601 USA
0006: * All rights reserved.
0007: *
0008: * Redistribution and use in source and binary forms, with or without
0009: * modification, are permitted provided that the following conditions
0010: * are met:
0011: *
0012: * + Redistributions of source code must retain the above copyright
0013: * notice, this list of conditions and the following disclaimer.
0014: *
0015: * + Redistributions in binary form must reproduce the above
0016: * copyright notice, this list of conditions and the following
0017: * disclaimer in the documentation and/or other materials provided
0018: * with the distribution.
0019: *
0020: * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
0021: * names of its contributors may be used to endorse or promote
0022: * products derived from this software without specific prior
0023: * written permission.
0024: *
0025: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
0026: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
0027: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
0028: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
0029: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
0030: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
0031: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
0032: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
0033: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
0034: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
0035: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
0036: ****************************************************************************/package net.sourceforge.cruisecontrol.distributed;
0037:
0038: import java.io.File;
0039: import java.io.IOException;
0040: import java.net.InetAddress;
0041: import java.net.UnknownHostException;
0042: import java.net.URL;
0043: import java.net.MalformedURLException;
0044: import java.rmi.RemoteException;
0045: import java.util.Map;
0046: import java.util.Properties;
0047: import java.util.Date;
0048: import java.util.List;
0049: import java.util.ArrayList;
0050: import java.util.HashMap;
0051:
0052: import net.sourceforge.cruisecontrol.Builder;
0053: import net.sourceforge.cruisecontrol.CruiseControlException;
0054: import net.sourceforge.cruisecontrol.Progress;
0055: import net.sourceforge.cruisecontrol.builders.AntBuilder;
0056: import net.sourceforge.cruisecontrol.builders.AntScript;
0057: import net.sourceforge.cruisecontrol.builders.CompositeBuilder;
0058: import net.sourceforge.cruisecontrol.distributed.core.PropertiesHelper;
0059: import net.sourceforge.cruisecontrol.distributed.core.ZipUtil;
0060: import net.sourceforge.cruisecontrol.distributed.core.FileUtil;
0061: import net.sourceforge.cruisecontrol.distributed.core.CCDistVersion;
0062: import net.sourceforge.cruisecontrol.distributed.core.ProgressRemote;
0063: import net.sourceforge.cruisecontrol.distributed.core.RemoteResult;
0064: import net.sourceforge.cruisecontrol.distributed.core.jnlputil.AntProgressLoggerInstaller;
0065: import net.sourceforge.cruisecontrol.util.IO;
0066: import net.sourceforge.cruisecontrol.util.Util;
0067:
0068: import org.apache.log4j.Logger;
0069: import org.apache.log4j.Level;
0070: import org.jdom.Element;
0071:
0072: import javax.jnlp.ServiceManager;
0073: import javax.jnlp.BasicService;
0074: import javax.jnlp.UnavailableServiceException;
0075:
0076: /**
0077: * Build Agent implementation.
0078: */
0079: public class BuildAgentServiceImpl implements BuildAgentService {
0080:
0081: private static final Logger LOG = Logger
0082: .getLogger(BuildAgentServiceImpl.class);
0083:
0084: private static final String CRUISE_BUILD_DIR = "cruise.build.dir";
0085:
0086: static final String DEFAULT_AGENT_PROPERTIES_FILE = "agent.properties";
0087: private String agentPropertiesFilename = DEFAULT_AGENT_PROPERTIES_FILE;
0088:
0089: static final String DEFAULT_USER_DEFINED_PROPERTIES_FILE = "user-defined.properties";
0090:
0091: /**
0092: * The default number of milliseconds a restart() or kill() should delay before executing
0093: * in order to allow remote calls to complete, and thereby allow successful builds
0094: * to complete on the Distributed Master.
0095: */
0096: private static final int DEFAULT_DELAY_MS_KILLRESTART = 5 * 1000;
0097: /** Name of system property who's value, if defined, will override the default delay. */
0098: static final String SYSPROP_CCDIST_DELAY_MS_KILLRESTART = "cc.dist.delayMSKillRestart";
0099:
0100: /** Cache host name. */
0101: private final String machineName;
0102:
0103: private final Date dateStarted;
0104: private boolean isBusy;
0105: private Date dateClaimed;
0106: private boolean isPendingKill;
0107: private Date pendingKillSince;
0108: private boolean isPendingRestart;
0109: private Date pendingRestartSince;
0110:
0111: private Properties configProperties;
0112:
0113: private String projectName;
0114: private ProgressRemote buildProgressRemote;
0115: private final Map distributedAgentProps = new HashMap();
0116:
0117: private File logDir;
0118: private File outputDir;
0119: private RemoteResult[] remoteResults;
0120: private File buildRootDir;
0121: private File zippedLogs;
0122: private File zippedOutput;
0123:
0124: private final List agentStatusListeners = new ArrayList();
0125:
0126: private final String logMsgPrefix;
0127:
0128: /**
0129: * Prepends Agent machine name to error message. This is especially
0130: * useful when combined with an "email logger" config for Log4j using a modified
0131: * log4j.properties on build agents. For example:
0132: * <pre>
0133: * log4j.rootCategory=INFO,A1,FILE,Mail
0134: *
0135: * ...
0136: *
0137: * # Mail is set to be a SMTPAppender
0138: * log4j.appender.Mail=org.apache.log4j.net.SMTPAppender
0139: * log4j.appender.Mail.BufferSize=100
0140: * log4j.appender.Mail.From=ccbuild@yourdomain.com
0141: * log4j.appender.Mail.SMTPHost=yoursmtp.mailhost.com
0142: * log4j.appender.Mail.Subject=CC has had an error!!!
0143: * log4j.appender.Mail.To=youremail@yourdomain.com
0144: * log4j.appender.Mail.layout=org.apache.log4j.PatternLayout
0145: * log4j.appender.Mail.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} %-5p [%x] [%c{3}] %m%n
0146: *
0147: * </pre>
0148: *
0149: * @param message the message to log (will be prefixed with machineName).
0150: */
0151: private void logPrefixDebug(final Object message) {
0152: LOG.debug(logMsgPrefix + message);
0153: }
0154:
0155: private void logPrefixInfo(final Object message) {
0156: LOG.info(logMsgPrefix + message);
0157: }
0158:
0159: private void logPrefixError(final Object message) {
0160: LOG.error(logMsgPrefix + message);
0161: }
0162:
0163: private void logPrefixError(final Object message,
0164: final Throwable throwable) {
0165: LOG.error(logMsgPrefix + message, throwable);
0166: }
0167:
0168: private final BuildAgent serviceContainer;
0169:
0170: /**
0171: * Constructor.
0172: * @param serviceContainer the BuildAgent instance in charge of publishing this service.
0173: */
0174: BuildAgentServiceImpl(final BuildAgent serviceContainer) {
0175: dateStarted = new Date();
0176:
0177: this .serviceContainer = serviceContainer;
0178:
0179: try {
0180: machineName = InetAddress.getLocalHost().getHostName();
0181: } catch (UnknownHostException e) {
0182: final String message = "Failed to get hostname";
0183: // Don't call Log helper method here since hostname is not yet set
0184: LOG.error(message, e);
0185: System.err.println(message + " - " + e.getMessage());
0186: throw new RuntimeException(message, e);
0187: }
0188: logMsgPrefix = "Agent Host: " + machineName + "; ";
0189: }
0190:
0191: /** @return the date this Build Agent started running (not when a specific build started). */
0192: public Date getDateStarted() {
0193: return dateStarted;
0194: }
0195:
0196: /**
0197: * @return the project being built now, or null if no project is being built.
0198: */
0199: public String getProjectName() {
0200: return projectName;
0201: }
0202:
0203: void setAgentPropertiesFilename(final String filename) {
0204: agentPropertiesFilename = filename;
0205: }
0206:
0207: private String getAgentPropertiesFilename() {
0208: return agentPropertiesFilename;
0209: }
0210:
0211: private DelayedAction lastDelayedAction;
0212:
0213: DelayedAction getLastDelayedAction() {
0214: return lastDelayedAction;
0215: }
0216:
0217: private void setLastDelayedAction(DelayedAction lastDelayedAction) {
0218: this .lastDelayedAction = lastDelayedAction;
0219: }
0220:
0221: /**
0222: * Executes the {@link #execAction()} method after a fixed delay has expired.
0223: */
0224: abstract static class DelayedAction extends Thread {
0225: /** Allow unit test to be notified when delayed action completes. */
0226: static interface FinishedListener {
0227: public void finished(final DelayedAction delayedAction);
0228: }
0229:
0230: static final class Type {
0231: public static final Type RESTART = new Type("restart");
0232: public static final Type KILL = new Type("kill");
0233:
0234: private final String name;
0235:
0236: private Type(final String name) {
0237: this .name = name;
0238: }
0239:
0240: public String toString() {
0241: return name;
0242: }
0243: }
0244:
0245: private Throwable thrown;
0246: private final int delay;
0247: private final Type type;
0248: private boolean isFinished;
0249:
0250: /** Allow unit test to be notified when delayed action completes. */
0251: private FinishedListener finishedListener;
0252:
0253: DelayedAction(final Type type) {
0254: delay = Integer.getInteger(
0255: SYSPROP_CCDIST_DELAY_MS_KILLRESTART,
0256: DEFAULT_DELAY_MS_KILLRESTART).intValue();
0257: this .type = type;
0258: start();
0259: }
0260:
0261: public final void run() {
0262: try {
0263: LOG.info("Executing Agent " + type + " in " + delay
0264: + " milliseconds...");
0265: Thread.sleep(delay);
0266: } catch (InterruptedException e) {
0267: // ignore
0268: }
0269:
0270: try {
0271: execAction();
0272: } catch (Throwable t) {
0273: thrown = t;
0274: LOG.error("Error executing delayed action.", t);
0275: } finally {
0276: isFinished = true;
0277: if (finishedListener != null) {
0278: finishedListener.finished(this );
0279: }
0280: }
0281: }
0282:
0283: void setFinishedListener(final FinishedListener finishedListener) {
0284: this .finishedListener = finishedListener;
0285: }
0286:
0287: public Throwable getThrown() {
0288: return thrown;
0289: }
0290:
0291: public Type getType() {
0292: return type;
0293: }
0294:
0295: public boolean isFinished() {
0296: return isFinished;
0297: }
0298:
0299: /**
0300: * Implement in order to run the desired Action
0301: */
0302: public abstract void execAction();
0303: }
0304:
0305: private final String busyLock = "busyLock";
0306:
0307: void setBusy(final boolean newIsBusy) {
0308: if (!newIsBusy) { // means the claim is being released
0309:
0310: if (isPendingRestart()) {
0311: // restart after delay to allow in progress remote calls to finish
0312: setLastDelayedAction(new DelayedAction(
0313: DelayedAction.Type.RESTART) {
0314: public void execAction() {
0315: doRestart();
0316: }
0317: });
0318: // do NOT reset busy state, since action is pending
0319: return;
0320: } else if (isPendingKill()) {
0321: // kill now after delay to allow in progress remote calls to finish
0322: setLastDelayedAction(new DelayedAction(
0323: DelayedAction.Type.KILL) {
0324: public void execAction() {
0325: doKill();
0326: }
0327: });
0328: // do NOT reset busy state, since action is pending
0329: return;
0330: }
0331:
0332: // clear out distributed build agent props
0333: distributedAgentProps.clear();
0334:
0335: // clear project name
0336: projectName = null;
0337:
0338: dateClaimed = null;
0339: } else {
0340: dateClaimed = new Date();
0341: }
0342:
0343: synchronized (busyLock) {
0344: isBusy = newIsBusy;
0345: }
0346:
0347: fireAgentStatusChanged();
0348:
0349: logPrefixInfo("agent busy status changed to: " + newIsBusy);
0350: }
0351:
0352: public Element doBuild(final Builder nestedBuilder,
0353: final Map projectPropertiesMap,
0354: final Map distributedAgentProperties)
0355: throws RemoteException {
0356:
0357: return doBuild(nestedBuilder, projectPropertiesMap,
0358: distributedAgentProperties, null);
0359: }
0360:
0361: public Element doBuild(final Builder nestedBuilder,
0362: final Map projectPropertiesMap,
0363: final Map distributedAgentProperties,
0364: final ProgressRemote progressRemote) throws RemoteException {
0365:
0366: return doBuild(nestedBuilder, projectPropertiesMap,
0367: distributedAgentProperties, progressRemote, null);
0368: }
0369:
0370: public Element doBuild(final Builder nestedBuilder,
0371: final Map projectPropertiesMap,
0372: final Map distributedAgentProperties,
0373: final ProgressRemote progressRemote,
0374: final RemoteResult[] remoteResults) throws RemoteException {
0375:
0376: synchronized (busyLock) {
0377: if (!isBusy()) { // only reclaim if needed, since it resets the dateClaimed.
0378: setBusy(true); // we could remove this, since claim() is called during lookup...
0379: }
0380: }
0381:
0382: projectName = (String) projectPropertiesMap
0383: .get(PropertiesHelper.PROJECT_NAME);
0384: if (null == projectName) {
0385: throw new RemoteException("Missing required property: "
0386: + PropertiesHelper.PROJECT_NAME
0387: + " in projectProperties");
0388: }
0389:
0390: final Level origLogLevel = Logger.getRootLogger().getLevel();
0391: final boolean isDebugBuild = Boolean.valueOf(
0392: (String) distributedAgentProperties
0393: .get(PropertiesHelper.DISTRIBUTED_AGENT_DEBUG))
0394: .booleanValue();
0395: boolean isDebugOverriden = false;
0396: try {
0397: // Override log level if needed
0398: if (isDebugBuild && !LOG.isDebugEnabled()) {
0399: LOG
0400: .info("Switching Agent log level to Debug for build.");
0401: Logger.getRootLogger().setLevel(Level.DEBUG);
0402: isDebugOverriden = true;
0403: }
0404:
0405: buildProgressRemote = progressRemote;
0406:
0407: logPrefixDebug("Build Agent Props: "
0408: + distributedAgentProperties.toString());
0409: distributedAgentProps.putAll(distributedAgentProperties);
0410:
0411: this .remoteResults = remoteResults;
0412:
0413: String remoreResultsMsg = "";
0414: if (remoteResults != null) {
0415: for (int i = 0; i < remoteResults.length; i++) {
0416: remoreResultsMsg += "\n\tRemoteResult: "
0417: + remoteResults[i].getAgentDir()
0418: .getAbsolutePath();
0419: }
0420: }
0421: final String infoMessage = "Building project: "
0422: + projectName
0423: + "\n\tAgentLogDir: "
0424: + distributedAgentProps
0425: .get(PropertiesHelper.DISTRIBUTED_AGENT_LOGDIR)
0426: + "\n\tAgentOutputDir: "
0427: + distributedAgentProps
0428: .get(PropertiesHelper.DISTRIBUTED_AGENT_OUTPUTDIR)
0429: + remoreResultsMsg;
0430:
0431: logPrefixInfo(infoMessage);
0432:
0433: logPrefixDebug("Build Agent Project Props: "
0434: + projectPropertiesMap.toString());
0435:
0436: // this is done only to update agent UI info regarding ProjectName - which isn't available
0437: // until projectPropertiesMap has been set.
0438: fireAgentStatusChanged();
0439:
0440: configProperties = (Properties) PropertiesHelper
0441: .loadRequiredProperties(getAgentPropertiesFilename());
0442:
0443: if (progressRemote != null) {
0444: progressRemote
0445: .setValueRemote("validating remote builder");
0446: fireAgentStatusChanged(); // update UI
0447: }
0448:
0449: // @todo Test under webstart 4, 5 and 6.0
0450: // must do Ant Logger Lib injection before validation
0451: injectAntProgressLoggerLibIfNeeded(nestedBuilder);
0452:
0453: try {
0454: nestedBuilder.validate();
0455: } catch (CruiseControlException e) {
0456: final String message = "Failed to validate nested Builder on agent";
0457: logPrefixError(message, e);
0458: System.err.println(message + " - " + e.getMessage());
0459: throw new RemoteException(message, e);
0460: }
0461:
0462: final String overrideTarget = (String) distributedAgentProps
0463: .get(PropertiesHelper.DISTRIBUTED_OVERRIDE_TARGET);
0464:
0465: // wrap progressRemote with a local progress
0466: final Progress progressLocal;
0467: if (progressRemote != null) {
0468: progressLocal = new Progress() {
0469: public void setValue(String value) {
0470: try {
0471: progressRemote.setValueRemote(value);
0472: fireAgentStatusChanged(); // update UI
0473: } catch (RemoteException e) {
0474: throw new RuntimeException(
0475: "Error setting progress", e);
0476: }
0477: }
0478:
0479: public String getValue() {
0480: try {
0481: return progressRemote.getValueRemote();
0482: } catch (RemoteException e) {
0483: throw new RuntimeException(
0484: "Error getting progress", e);
0485: }
0486: }
0487: };
0488: } else {
0489: progressLocal = null;
0490: }
0491:
0492: if (progressRemote != null) {
0493: progressRemote.setValueRemote("running remote builder");
0494: fireAgentStatusChanged(); // update UI
0495: }
0496: final Element buildResults;
0497: try {
0498: if (overrideTarget == null) {
0499: buildResults = nestedBuilder.build(
0500: projectPropertiesMap, progressLocal);
0501: } else {
0502: buildResults = nestedBuilder.buildWithTarget(
0503: projectPropertiesMap, overrideTarget,
0504: progressLocal);
0505: }
0506: } catch (CruiseControlException e) {
0507: final String message = "Failed to complete build on agent";
0508: logPrefixError(message, e);
0509: System.err.println(message + " - " + e.getMessage());
0510: throw new RemoteException(message, e);
0511: }
0512:
0513: if (progressRemote != null) {
0514: progressRemote.setValueRemote("preparing results");
0515: fireAgentStatusChanged(); // update UI
0516: }
0517: prepareLogsAndArtifacts();
0518: return buildResults;
0519: } catch (RemoteException e) {
0520: logPrefixError("doBuild threw exception, setting busy to false.");
0521: setBusy(false);
0522: throw e; // rethrow original exception
0523: } finally {
0524: buildProgressRemote = null;
0525:
0526: // restore original log level if overriden
0527: if (isDebugOverriden) {
0528: Logger.getRootLogger().setLevel(origLogLevel);
0529: LOG
0530: .info("Restored Agent log level to: "
0531: + origLogLevel);
0532: }
0533: }
0534: }
0535:
0536: static void injectAntProgressLoggerLibIfNeeded(final Builder builder) {
0537: if (builder instanceof AntBuilder) {
0538: doInjectAntProgressLoggerLibIfNeeded((AntBuilder) builder);
0539: } else if (builder instanceof CompositeBuilder) {
0540: final Builder[] builders = ((CompositeBuilder) builder)
0541: .getBuilders();
0542: for (int i = 0; i < builders.length; i++) {
0543: injectAntProgressLoggerLibIfNeeded(builders[i]);
0544: }
0545: }
0546: }
0547:
0548: private static void doInjectAntProgressLoggerLibIfNeeded(
0549: final AntBuilder antBuilder) {
0550:
0551: if (antBuilder.getProgressLoggerLib() != null) {
0552: // path already set, so don't interfere
0553: LOG
0554: .debug("Agent skipping AntProgressLogger injection, already set to: "
0555: + antBuilder.getProgressLoggerLib());
0556: return; // no kludges required
0557: }
0558:
0559: final String defaultAntProgressLoggerLib;
0560: try {
0561: defaultAntProgressLoggerLib = AntScript
0562: .findDefaultProgressLoggerLib();
0563:
0564: if (defaultAntProgressLoggerLib != null) {
0565: // AntScript will be able to find the jar on it's own, so again, don't interfere
0566: LOG
0567: .debug("Agent skipping AntProgressLogger injection, AntScript will set to: "
0568: + defaultAntProgressLoggerLib);
0569: return; // no kludges required
0570: }
0571: } catch (AntScript.ProgressLibLocatorException e) {
0572: // This exception is expected under Webstart, so ignore and continue
0573: LOG
0574: .debug("Couldn't find default AntProgressLogger, will attempt injection.");
0575: }
0576:
0577: // This assumes JNLP Extension for Ant Progress Logger has been installed.
0578: final String jnlpMuffinAntProgressLoggerPath = AntProgressLoggerInstaller
0579: .getJNLPMuffinAntProgressLoggerPath();
0580: LOG.debug("jnlpMuffinAntProgressLoggerPath: "
0581: + jnlpMuffinAntProgressLoggerPath);
0582:
0583: final File progressLoggerJar = new File(
0584: jnlpMuffinAntProgressLoggerPath);
0585: if (!progressLoggerJar.exists()) {
0586: throw new IllegalStateException(
0587: "JNLP Build Agent couldn't find progress logger lib jar in expected location: "
0588: + progressLoggerJar.getAbsolutePath());
0589: }
0590:
0591: antBuilder.setProgressLoggerLib(progressLoggerJar
0592: .getAbsolutePath());
0593: LOG.debug("Injected AntProgressLogger lib: "
0594: + progressLoggerJar.getAbsolutePath()
0595: + " into AntBuilder.");
0596: }
0597:
0598: /**
0599: * Zip any build artifacts found in the logDir and/or outputDir.
0600: */
0601: void prepareLogsAndArtifacts() {
0602: final String buildDirProperty = configProperties
0603: .getProperty(CRUISE_BUILD_DIR);
0604: try {
0605: buildRootDir = new File(buildDirProperty)
0606: .getCanonicalFile();
0607: } catch (IOException e) {
0608: final String message = "Couldn't create "
0609: + buildDirProperty;
0610: logPrefixError(message, e);
0611: System.err.println(message + " - " + e.getMessage());
0612: throw new RuntimeException(message);
0613: }
0614:
0615: logDir = getAgentResultDir(PropertiesHelper.RESULT_TYPE_LOGS,
0616: PropertiesHelper.DISTRIBUTED_AGENT_LOGDIR);
0617:
0618: outputDir = getAgentResultDir(
0619: PropertiesHelper.RESULT_TYPE_OUTPUT,
0620: PropertiesHelper.DISTRIBUTED_AGENT_OUTPUTDIR);
0621:
0622: zippedLogs = ZipUtil.getTempResultsZipFile(buildRootDir,
0623: projectName, PropertiesHelper.RESULT_TYPE_LOGS);
0624: ZipUtil.zipFolderContents(zippedLogs.getAbsolutePath(), logDir
0625: .getAbsolutePath());
0626:
0627: zippedOutput = ZipUtil.getTempResultsZipFile(buildRootDir,
0628: projectName, PropertiesHelper.RESULT_TYPE_OUTPUT);
0629: ZipUtil.zipFolderContents(zippedOutput.getAbsolutePath(),
0630: outputDir.getAbsolutePath());
0631:
0632: if (remoteResults != null) {
0633: for (int i = 0; i < remoteResults.length; i++) {
0634:
0635: final File agentResultDir = remoteResults[i]
0636: .getAgentDir();
0637: ensureDirExists(agentResultDir);
0638:
0639: remoteResults[i].storeTempZippedFile(ZipUtil
0640: .getTempResultsZipFile(buildRootDir,
0641: projectName, "remoteResult" + i));
0642:
0643: ZipUtil.zipFolderContents(remoteResults[i]
0644: .fetchTempZippedFile().getAbsolutePath(),
0645: agentResultDir.getAbsolutePath());
0646: }
0647: }
0648:
0649: }
0650:
0651: private File getAgentResultDir(final String resultType,
0652: final String resultProperty) {
0653: String resultDir;
0654: resultDir = (String) distributedAgentProps.get(resultProperty);
0655: logPrefixDebug("Result: " + resultType + "Prop value: "
0656: + resultDir);
0657:
0658: if (resultDir == null || "".equals(resultDir)) {
0659: // use canonical behavior if attribute is not set
0660: resultDir = buildRootDir + File.separator + resultType
0661: + File.separator + projectName;
0662: }
0663:
0664: // ensure dir exists
0665: final File fileResultDir = new File(resultDir);
0666: ensureDirExists(fileResultDir);
0667:
0668: return fileResultDir;
0669: }
0670:
0671: private static void ensureDirExists(final File fileResultDir) {
0672: if (!fileResultDir.exists()) {
0673: if (!Util.doMkDirs(fileResultDir)) {
0674: final String msg = "Error creating Agent result dir: "
0675: + fileResultDir.getAbsolutePath();
0676: LOG.error(msg);
0677: throw new RuntimeException(msg);
0678: }
0679: }
0680: }
0681:
0682: public String getMachineName() {
0683: return machineName;
0684: }
0685:
0686: public void claim() {
0687: // flag this agent as busy for now. Intended to prevent mulitple builds on same agent,
0688: // when multiple master threads find the same agent, before any build thread has started.
0689: synchronized (busyLock) {
0690: if (isBusy()) {
0691: throw new IllegalStateException(
0692: "Cannot claim agent on " + getMachineName()
0693: + " that is busy building project: "
0694: + projectName);
0695: }
0696: setBusy(true);
0697: }
0698: }
0699:
0700: public boolean isBusy() {
0701: synchronized (busyLock) {
0702: logPrefixDebug("Is busy called. value: " + isBusy);
0703: return isBusy;
0704: }
0705: }
0706:
0707: public Date getDateClaimed() {
0708: return dateClaimed;
0709: }
0710:
0711: private void setPendingKill() {
0712: synchronized (busyLock) {
0713: this .isPendingKill = true;
0714: pendingKillSince = new Date();
0715: }
0716: }
0717:
0718: public boolean isPendingKill() {
0719: synchronized (busyLock) {
0720: return isPendingKill;
0721: }
0722: }
0723:
0724: public Date getPendingKillSince() {
0725: return pendingKillSince;
0726: }
0727:
0728: private void setPendingRestart() {
0729: synchronized (busyLock) {
0730: this .isPendingRestart = true;
0731: pendingRestartSince = new Date();
0732: }
0733: }
0734:
0735: public boolean isPendingRestart() {
0736: synchronized (busyLock) {
0737: return isPendingRestart;
0738: }
0739: }
0740:
0741: public Date getPendingRestartSince() {
0742: return pendingRestartSince;
0743: }
0744:
0745: public boolean resultsExist(final String resultsType)
0746: throws RemoteException {
0747: if (resultsType.equals(PropertiesHelper.RESULT_TYPE_LOGS)) {
0748: return recursiveFilesExist(logDir);
0749: } else if (resultsType
0750: .equals(PropertiesHelper.RESULT_TYPE_OUTPUT)) {
0751: return recursiveFilesExist(outputDir);
0752: } else {
0753: throw new RemoteException("Unrecognized result type: "
0754: + resultsType);
0755: }
0756: }
0757:
0758: public boolean remoteResultExists(final int idx)
0759: throws RemoteException {
0760: if (remoteResults != null) {
0761: final boolean resultsExist = recursiveFilesExist(remoteResults[idx]
0762: .getAgentDir());
0763: if (resultsExist) {
0764: return true;
0765: }
0766: }
0767: return false;
0768: }
0769:
0770: static boolean recursiveFilesExist(final File fileToCheck) {
0771:
0772: if (!fileToCheck.exists()) { // save time, if it's doesn't exist at all
0773: return false;
0774: } else if (fileToCheck.isFile()) { // save time, if it's a file
0775: return true;
0776: }
0777:
0778: final File[] dirs = fileToCheck.listFiles();
0779: for (int i = 0; i < dirs.length; i++) {
0780: if (recursiveFilesExist(dirs[i])) {
0781: return true; // we found a file so return now, no need to keep looking.
0782: }
0783: }
0784: return false;
0785: }
0786:
0787: /**
0788: * Return the file containing the given type of results. Package visible for unit testing.
0789: * @param resultsType the type of results file
0790: * @return the file containing the given type of results. Package visible for unit testing.
0791: * @throws RemoteException if an invalid result type is given.
0792: */
0793: File getResultsZip(final String resultsType) throws RemoteException {
0794: final File zipFile;
0795: if (PropertiesHelper.RESULT_TYPE_LOGS.equals(resultsType)) {
0796: zipFile = zippedLogs;
0797: } else if (PropertiesHelper.RESULT_TYPE_OUTPUT
0798: .equals(resultsType)) {
0799: zipFile = zippedOutput;
0800: } else {
0801: throw new RemoteException("Unrecognized result type: "
0802: + resultsType);
0803: }
0804: return zipFile;
0805: }
0806:
0807: public byte[] retrieveResultsAsZip(final String resultsType)
0808: throws RemoteException {
0809:
0810: final File zipFile = getResultsZip(resultsType);
0811:
0812: final byte[] response;
0813: try {
0814: response = FileUtil.getFileAsBytes(zipFile);
0815: } catch (IOException e) {
0816: final String message = "Unable to get file "
0817: + zipFile.getAbsolutePath();
0818: logPrefixError(message, e);
0819: System.err.println(message + " - " + e.getMessage());
0820: throw new RuntimeException(message, e);
0821: }
0822:
0823: return response;
0824: }
0825:
0826: public byte[] retrieveRemoteResult(final int resultIdx)
0827: throws RemoteException {
0828:
0829: RemoteResult remoteResult = null;
0830: if (remoteResults != null) {
0831: for (int i = 0; i < remoteResults.length; i++) {
0832: if (resultIdx == remoteResults[i].getIdx()) {
0833: remoteResult = remoteResults[i];
0834: break;
0835: }
0836: }
0837: }
0838:
0839: if (remoteResult == null) {
0840: final String message = "Invalid remote result index: "
0841: + resultIdx;
0842: logPrefixError(message);
0843: System.err.println(message);
0844: throw new RuntimeException(message);
0845: }
0846:
0847: final byte[] response;
0848: try {
0849: response = FileUtil.getFileAsBytes(remoteResult
0850: .fetchTempZippedFile());
0851: } catch (IOException e) {
0852: final String message = "Unable to get remote result file: "
0853: + remoteResult.getAgentDir().getAbsolutePath();
0854: logPrefixError(message, e);
0855: System.err.println(message + " - " + e.getMessage());
0856: throw new RuntimeException(message, e);
0857: }
0858:
0859: return response;
0860: }
0861:
0862: public void clearOutputFiles() {
0863: try {
0864: if (logDir != null) {
0865: logPrefixDebug("Deleting contents of " + logDir);
0866: IO.delete(logDir);
0867: } else {
0868: logPrefixDebug("Skip delete agent logDir: " + logDir);
0869: }
0870: if (zippedLogs != null) {
0871: logPrefixDebug("Deleting log zip " + zippedLogs);
0872: zippedLogs.deleteOnExit();
0873: IO.delete(zippedLogs);
0874: } else {
0875: logPrefixError("Skipping delete of log zip, file path is null.");
0876: }
0877:
0878: if (outputDir != null) {
0879: logPrefixDebug("Deleting contents of " + outputDir);
0880: IO.delete(outputDir);
0881: } else {
0882: logPrefixDebug("Skip delete agent outputDir: "
0883: + outputDir);
0884: }
0885: if (zippedOutput != null) {
0886: logPrefixDebug("Deleting output zip " + zippedOutput);
0887: zippedOutput.deleteOnExit();
0888: IO.delete(zippedOutput);
0889: } else {
0890: logPrefixError("Skipping delete of output zip, file path is null.");
0891: }
0892:
0893: if (remoteResults != null) {
0894: for (int i = 0; i < remoteResults.length; i++) {
0895: logPrefixDebug("Deleting contents of "
0896: + remoteResults[i].getAgentDir()
0897: .getAbsolutePath());
0898: IO.delete(remoteResults[i].getAgentDir());
0899:
0900: final File tempZippedFile = remoteResults[i]
0901: .fetchTempZippedFile();
0902: if (tempZippedFile != null) {
0903: logPrefixDebug("Deleting remote result zip "
0904: + tempZippedFile.getAbsolutePath());
0905: tempZippedFile.deleteOnExit();
0906: IO.delete(tempZippedFile);
0907: }
0908: }
0909: }
0910:
0911: setBusy(false);
0912:
0913: } catch (RuntimeException e) {
0914: logPrefixError("Error cleaning agent build files.", e);
0915: throw e;
0916: }
0917: }
0918:
0919: private void doRestart() {
0920: logPrefixInfo("Attempting agent restart.");
0921:
0922: final BasicService basicService;
0923: try {
0924: basicService = (BasicService) ServiceManager
0925: .lookup(BasicService.class.getName());
0926: } catch (UnavailableServiceException e) {
0927: final String errMsg = "Couldn't find webstart Basic Service. Is Agent running outside of webstart?";
0928: logPrefixError(errMsg, e);
0929: throw new RuntimeException(errMsg, e);
0930: }
0931:
0932: // Normally, we would set the busy lock first, but here we should only set the lock after we are certain
0933: // the required Webstart service is available.
0934: synchronized (busyLock) {
0935: if (!isBusy()) {
0936: // claim agent so no new build can start
0937: claim();
0938: }
0939: // set project name to informative message in case agent is found while restarting
0940: projectName = "executingAgentRestart";
0941: }
0942:
0943: final URL codeBaseURL = basicService.getCodeBase();
0944: logPrefixInfo("basicService.getCodeBase()="
0945: + codeBaseURL.toString());
0946:
0947: // relaunch via new browser session
0948: // @todo How to close the browser after jnlp is relaunched?
0949: final URL relaunchURL;
0950: try {
0951: relaunchURL = new URL(codeBaseURL, "agent.jnlp");
0952: } catch (MalformedURLException e) {
0953: final String errMsg = "Error building webstart relaunch URL from "
0954: + codeBaseURL.toString();
0955: logPrefixError(errMsg, e);
0956: throw new RuntimeException(errMsg, e);
0957: }
0958: if (basicService.showDocument(relaunchURL)) {
0959: logPrefixInfo("Relaunched agent via URL: "
0960: + relaunchURL.toString()
0961: + ". Will kill current agent now.");
0962: doKill(); // don't wait for build finish, since we've already relaunched at this point.
0963: } else {
0964: final String errMsg = "Failed to relaunch agent via URL: "
0965: + relaunchURL.toString();
0966: logPrefixError(errMsg);
0967: throw new RuntimeException(errMsg);
0968: }
0969: }
0970:
0971: private void doKill() {
0972: logPrefixInfo("Attempting agent kill.");
0973: synchronized (busyLock) {
0974: if (!isBusy()) {
0975: // claim agent so no new build can start
0976: claim();
0977: }
0978: // set project name to informative message in case agent is found while shutting down
0979: projectName = "executingAgentKill";
0980: }
0981: BuildAgent.kill();
0982: doKillExecuted = true;
0983: }
0984:
0985: /** Intended only for unit tests, indicating if doKill() call has completed. */
0986: private boolean doKillExecuted;
0987:
0988: boolean isDoKillExecuted() {
0989: return doKillExecuted;
0990: }
0991:
0992: public void kill(final boolean afterBuildFinished)
0993: throws RemoteException {
0994: setPendingKill();
0995:
0996: if (!afterBuildFinished // Kill now, don't waiting for build to finish.
0997: || !isBusy()) { // Not busy, so kill now.
0998:
0999: doKill(); // calls back to this agent to terminate lookup stuff
1000: } else if (isBusy()) {
1001: // do nothing. When claim is released, setBusy(false) will perform the kill
1002: }
1003: fireAgentStatusChanged();
1004: }
1005:
1006: public void restart(final boolean afterBuildFinished)
1007: throws RemoteException {
1008: setPendingRestart();
1009:
1010: if (!afterBuildFinished // Restart now, don't waiting for build to finish.
1011: || !isBusy()) { // Not busy, so Restart now.
1012:
1013: doRestart();
1014: } else if (isBusy()) {
1015: // do nothing. When claim is released, setBusy(false) will perform the Restart
1016: }
1017: fireAgentStatusChanged();
1018: }
1019:
1020: public String asString() {
1021: final StringBuffer sb = new StringBuffer();
1022: sb.append("Machine Name: ");
1023: sb.append(machineName);
1024: sb.append(";\t");
1025: sb.append("Started: ");
1026: sb.append(dateStarted);
1027:
1028: sb.append("\n\tBusy: ");
1029: sb.append(isBusy);
1030: sb.append(";\tSince: ");
1031: sb.append(dateClaimed);
1032: sb.append(";\tProject: ");
1033: sb.append(projectName);
1034: // include Progress if available
1035: if (buildProgressRemote != null) {
1036: sb.append("\n\tProgress: ");
1037: try {
1038: sb.append(buildProgressRemote.getValueRemote());
1039: } catch (RemoteException e) {
1040: LOG.info("Error reading remote progress", e);
1041: }
1042: }
1043:
1044: sb.append("\n\tPending Restart: ");
1045: sb.append(isPendingRestart);
1046: sb.append(";\tPending Restart Since: ");
1047: sb.append(pendingRestartSince);
1048:
1049: sb.append("\n\tPending Kill: ");
1050: sb.append(isPendingKill);
1051: sb.append(";\tPending Kill Since: ");
1052: sb.append(pendingKillSince);
1053:
1054: sb.append("\n\tVersion: ");
1055: sb.append(CCDistVersion.getVersion());
1056: sb.append(" (Compiled: ");
1057: sb.append(CCDistVersion.getVersionBuildDate());
1058: sb.append(")");
1059:
1060: return sb.toString();
1061: }
1062:
1063: public void setEntryOverrides(PropertyEntry[] entryOverrides) {
1064: serviceContainer.setEntryOverrides(entryOverrides);
1065: // this is done only to update agent UI info with new entries info
1066: fireAgentStatusChanged();
1067: }
1068:
1069: public PropertyEntry[] getEntryOverrides() {
1070: return serviceContainer.getEntryOverrides();
1071: }
1072:
1073: public void addAgentStatusListener(
1074: final BuildAgent.AgentStatusListener listener) {
1075: agentStatusListeners.add(listener);
1076: }
1077:
1078: public void removeAgentStatusListener(
1079: final BuildAgent.AgentStatusListener listener) {
1080: agentStatusListeners.remove(listener);
1081: }
1082:
1083: private void fireAgentStatusChanged() {
1084: for (int i = 0; i < agentStatusListeners.size(); i++) {
1085: ((BuildAgent.AgentStatusListener) agentStatusListeners
1086: .get(i)).statusChanged(this);
1087: }
1088: }
1089: }
|