/**************************************************************************** * 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.File; import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.net.URL; import java.net.MalformedURLException; import java.rmi.RemoteException; import java.util.Map; import java.util.Properties; import java.util.Date; import java.util.List; import java.util.ArrayList; import java.util.HashMap; import java.text.DateFormat; import java.text.SimpleDateFormat; import net.sourceforge.cruisecontrol.BuildOutputLoggerManager; import net.sourceforge.cruisecontrol.Builder; import net.sourceforge.cruisecontrol.CruiseControlException; import net.sourceforge.cruisecontrol.Progress; import net.sourceforge.cruisecontrol.builders.AntBuilder; import net.sourceforge.cruisecontrol.builders.AntScript; import net.sourceforge.cruisecontrol.builders.CompositeBuilder; import net.sourceforge.cruisecontrol.distributed.core.PropertiesHelper; import net.sourceforge.cruisecontrol.distributed.core.ZipUtil; import net.sourceforge.cruisecontrol.distributed.core.FileUtil; import net.sourceforge.cruisecontrol.distributed.core.CCDistVersion; import net.sourceforge.cruisecontrol.distributed.core.ProgressRemote; import net.sourceforge.cruisecontrol.distributed.core.RemoteResult; import net.sourceforge.cruisecontrol.distributed.core.jnlputil.AntProgressLoggerInstaller; import net.sourceforge.cruisecontrol.util.IO; import net.sourceforge.cruisecontrol.util.Util; import org.apache.log4j.Logger; import org.apache.log4j.Level; import org.jdom.Element; import javax.jnlp.ServiceManager; import javax.jnlp.BasicService; import javax.jnlp.UnavailableServiceException; /** * Build Agent implementation. */ public class BuildAgentServiceImpl implements BuildAgentService { private static final Logger LOG = Logger.getLogger(BuildAgentServiceImpl.class); private static final long serialVersionUID = 2116738757011580074L; private static final String CRUISE_BUILD_DIR = "cruise.build.dir"; static final String DEFAULT_AGENT_PROPERTIES_FILE = "agent.properties"; private String agentPropertiesFilename = DEFAULT_AGENT_PROPERTIES_FILE; static final String DEFAULT_USER_DEFINED_PROPERTIES_FILE = "user-defined.properties"; /** * The default number of milliseconds a restart() or kill() should delay before executing * in order to allow remote calls to complete, and thereby allow successful builds * to complete on the Distributed Master. */ private static final int DEFAULT_DELAY_MS_KILLRESTART = 5 * 1000; /** Name of system property who's value, if defined, will override the default delay. */ static final String SYSPROP_CCDIST_DELAY_MS_KILLRESTART = "cc.dist.delayMSKillRestart"; /** Cache host name. */ private final String machineName; private final Date dateStarted; private boolean isBusy; private Date dateClaimed; private boolean isPendingKill; private Date pendingKillSince; private boolean isPendingRestart; private Date pendingRestartSince; private Properties configProperties; private String projectName; private ProgressRemote buildProgressRemote; private final Map<String, String> distributedAgentProps = new HashMap<String, String>(); private File logDir; private File outputDir; private RemoteResult[] remoteResults; private File buildRootDir; private File zippedLogs; private File zippedOutput; private final List<BuildAgent.AgentStatusListener> agentStatusListeners = new ArrayList<BuildAgent.AgentStatusListener>(); static final String LOGMSGPREFIX_PREFIX = "Agent Host: "; private final String logMsgPrefix; /** * Prepends Agent machine name to error message. This is especially * useful when combined with an "email logger" config for Log4j using a modified * log4j.properties on build agents. For example: * <pre> * log4j.rootCategory=INFO,A1,FILE,Mail * * ... * * # Mail is set to be a SMTPAppender * log4j.appender.Mail=org.apache.log4j.net.SMTPAppender * log4j.appender.Mail.BufferSize=100 * log4j.appender.Mail.From=ccbuild@yourdomain.com * log4j.appender.Mail.SMTPHost=yoursmtp.mailhost.com * log4j.appender.Mail.Subject=CC has had an error!!! * log4j.appender.Mail.To=youremail@yourdomain.com * log4j.appender.Mail.layout=org.apache.log4j.PatternLayout * log4j.appender.Mail.layout.ConversionPattern=%d{dd.MM.yyyy HH:mm:ss} %-5p [%x] [%c{3}] %m%n * * </pre> * * @param message the message to log (will be prefixed with machineName). */ private void logPrefixDebug(final Object message) { LOG.debug(logMsgPrefix + message); } private void logPrefixInfo(final Object message) { LOG.info(logMsgPrefix + message); } private void logPrefixError(final Object message) { LOG.error(logMsgPrefix + message); } private void logPrefixError(final Object message, final Throwable throwable) { LOG.error(logMsgPrefix + message, throwable); } private final transient BuildAgent serviceContainer; /** * Constructor. * @param serviceContainer the BuildAgent instance in charge of publishing this service. */ BuildAgentServiceImpl(final BuildAgent serviceContainer) { dateStarted = new Date(); this.serviceContainer = serviceContainer; try { machineName = InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { final String message = "Failed to get hostname"; // Don't call Log helper method here since hostname is not yet set LOG.error(message, e); System.err.println(message + " - " + e.getMessage()); throw new RuntimeException(message, e); } logMsgPrefix = LOGMSGPREFIX_PREFIX + machineName + "; "; } /** @return the date this Build Agent started running (not when a specific build started). */ public Date getDateStarted() { return dateStarted; } /** * @return the project being built now, or null if no project is being built. */ public String getProjectName() { return projectName; } void setAgentPropertiesFilename(final String filename) { agentPropertiesFilename = filename; } private String getAgentPropertiesFilename() { return agentPropertiesFilename; } private transient DelayedAction lastDelayedAction; DelayedAction getLastDelayedAction() { return lastDelayedAction; } private void setLastDelayedAction(DelayedAction lastDelayedAction) { this.lastDelayedAction = lastDelayedAction; } /** * Executes the {@link #execAction()} method after a fixed delay has expired. */ abstract static class DelayedAction implements Runnable { /** Allow unit test to be notified when delayed action completes. */ static interface FinishedListener { public void finished(final DelayedAction delayedAction); } static final class Type { public static final Type RESTART = new Type("restart"); public static final Type KILL = new Type("kill"); private final String name; private Type(final String name) { this.name = name; } public String toString() { return name; } } private Throwable thrown; private final int delay; private final Type type; private boolean isFinished; private final Thread executingThread; /** Allow unit test to be notified when delayed action completes. */ private FinishedListener finishedListener; DelayedAction(final Type type) { delay = Integer.getInteger( SYSPROP_CCDIST_DELAY_MS_KILLRESTART, DEFAULT_DELAY_MS_KILLRESTART); this.type = type; this.executingThread = new Thread(this, "DelayedActionThread, type: " + type.toString()); this.executingThread.start(); } public final void run() { try { LOG.info("Executing Agent " + type + " in " + delay + " milliseconds..."); Thread.sleep(delay); } catch (InterruptedException e) { // ignore } try { execAction(); } catch (Throwable t) { thrown = t; LOG.error("Error executing delayed action.", t); } finally { isFinished = true; if (finishedListener != null) { finishedListener.finished(this); } } } void setFinishedListener(final FinishedListener finishedListener) { this.finishedListener = finishedListener; } public Throwable getThrown() { return thrown; } public Type getType() { return type; } public boolean isFinished() { return isFinished; } /** * Implement in order to run the desired Action */ public abstract void execAction(); } private final String busyLock = "busyLock"; void setBusy(final boolean newIsBusy) { if (!newIsBusy) { // means the claim is being released if (isPendingRestart()) { // restart after delay to allow in progress remote calls to finish setLastDelayedAction(new DelayedAction(DelayedAction.Type.RESTART) { public void execAction() { doRestart(); } }); // do NOT reset busy state, since action is pending return; } else if (isPendingKill()) { // kill now after delay to allow in progress remote calls to finish setLastDelayedAction(new DelayedAction(DelayedAction.Type.KILL) { public void execAction() { doKill(); } }); // do NOT reset busy state, since action is pending return; } // clear out distributed build agent props distributedAgentProps.clear(); // clear project name projectName = null; dateClaimed = null; } else { dateClaimed = new Date(); } synchronized (busyLock) { isBusy = newIsBusy; } fireAgentStatusChanged(); logPrefixInfo("agent busy status changed to: " + newIsBusy); } public Element doBuild(final Builder nestedBuilder, final Map<String, String> projectPropertiesMap, final Map<String, String> distributedAgentProperties, final ProgressRemote progressRemote, final RemoteResult[] remoteResults) throws RemoteException { synchronized (busyLock) { if (!isBusy()) { // only reclaim if needed, since it resets the dateClaimed. setBusy(true); // we could remove this, since claim() is called during lookup... } } projectName = projectPropertiesMap.get(PropertiesHelper.PROJECT_NAME); if (null == projectName) { throw new RemoteException("Missing required property: " + PropertiesHelper.PROJECT_NAME + " in projectProperties"); } final Level origLogLevel = Logger.getRootLogger().getLevel(); final boolean isDebugBuild = Boolean.valueOf( distributedAgentProperties.get(PropertiesHelper.DISTRIBUTED_AGENT_DEBUG)); boolean isDebugOverriden = false; try { // Override log level if needed if (isDebugBuild && !LOG.isDebugEnabled()) { LOG.info("Switching Agent log level to Debug for build."); Logger.getRootLogger().setLevel(Level.DEBUG); isDebugOverriden = true; } buildProgressRemote = progressRemote; logPrefixDebug("Build Agent Props: " + distributedAgentProperties.toString()); distributedAgentProps.putAll(distributedAgentProperties); this.remoteResults = remoteResults; String remoteResultsMsg = ""; if (remoteResults != null) { for (final RemoteResult remoteResult : remoteResults) { remoteResultsMsg += "\n\tRemoteResult: " + remoteResult.getAgentDir().getAbsolutePath(); } } final String infoMessage = "Building project: " + projectName + "\n\tAgentLogDir: " + distributedAgentProps.get( PropertiesHelper.DISTRIBUTED_AGENT_LOGDIR) + "\n\tAgentOutputDir: " + distributedAgentProps.get( PropertiesHelper.DISTRIBUTED_AGENT_OUTPUTDIR) + remoteResultsMsg; logPrefixInfo(infoMessage); logPrefixDebug("Build Agent Project Props: " + projectPropertiesMap.toString()); // this is done only to update agent UI info regarding ProjectName - which isn't available // until projectPropertiesMap has been set. fireAgentStatusChanged(); configProperties = (Properties) PropertiesHelper.loadRequiredProperties( getAgentPropertiesFilename()); if (progressRemote != null) { progressRemote.setValueRemote("validating remote builder"); fireAgentStatusChanged(); // update UI } // must do Ant Logger Lib injection before validation injectAntProgressLoggerLibIfNeeded(nestedBuilder); try { nestedBuilder.validate(); } catch (CruiseControlException e) { final String message = "Failed to validate nested Builder on agent"; logPrefixError(message, e); System.err.println(message + " - " + e.getMessage()); throw new RemoteException(message, e); } final String overrideTarget = distributedAgentProps.get(PropertiesHelper.DISTRIBUTED_OVERRIDE_TARGET); // wrap progressRemote with a local progress final Progress progressLocal; if (progressRemote != null) { progressLocal = new WrappedRemoteProgress(progressRemote); } else { progressLocal = null; } if (progressRemote != null) { progressRemote.setValueRemote("running remote builder"); fireAgentStatusChanged(); // update UI } final long startTime = System.currentTimeMillis(); final Element buildResults; try { if (overrideTarget == null) { buildResults = nestedBuilder.build(projectPropertiesMap, progressLocal); } else { buildResults = nestedBuilder.buildWithTarget(projectPropertiesMap, overrideTarget, progressLocal); } } catch (CruiseControlException e) { final String message = "Failed to complete build on agent"; logPrefixError(message, e); System.err.println(message + " - " + e.getMessage()); throw new RemoteException(message, e); } // add agent builder info to build log CompositeBuilder.insertBuildLogHeader(buildResults, logMsgPrefix + nestedBuilder.getClass().getName() + "; agent", startTime, "agent", "agent-childbuilder"); if (progressRemote != null) { progressRemote.setValueRemote("preparing results"); fireAgentStatusChanged(); // update UI } prepareLogsAndArtifacts(); return buildResults; } catch (RemoteException e) { logPrefixError("doBuild threw exception, setting busy to false."); setBusy(false); throw e; // rethrow original exception } finally { buildProgressRemote = null; // restore original log level if overriden if (isDebugOverriden) { Logger.getRootLogger().setLevel(origLogLevel); LOG.info("Restored Agent log level to: " + origLogLevel); } } } private final class WrappedRemoteProgress implements Progress { private static final long serialVersionUID = -5980080533166620643L; private final ProgressRemote progressRemote; private WrappedRemoteProgress(final ProgressRemote progressRemote) { this.progressRemote = progressRemote; } public void setValue(String value) { try { progressRemote.setValueRemote(value); fireAgentStatusChanged(); // update UI } catch (RemoteException e) { throw new RuntimeException("Error setting progress", e); } } public String getValue() { try { return progressRemote.getValueRemote(); } catch (RemoteException e) { throw new RuntimeException("Error getting progress", e); } } public Date getLastUpdated() { try { return progressRemote.getLastUpdatedRemote(); } catch (RemoteException e) { throw new RuntimeException("Error getting progress", e); } } public String getText() { try { return progressRemote.getTextRemote(); } catch (RemoteException e) { throw new RuntimeException("Error getting progress", e); } } } public String getIDRemote() { return BuildOutputLoggerManager.INSTANCE.lookup(getProjectName()).getID(); } public String[] retrieveLinesRemote(final int firstLine) { return BuildOutputLoggerManager.INSTANCE.lookup(getProjectName()).retrieveLines(firstLine); } static void injectAntProgressLoggerLibIfNeeded(final Builder builder) { if (builder instanceof AntBuilder) { doInjectAntProgressLoggerLibIfNeeded((AntBuilder) builder); } else if (builder instanceof CompositeBuilder) { final Builder[] builders = ((CompositeBuilder) builder).getBuilders(); for (final Builder childBuilder : builders) { injectAntProgressLoggerLibIfNeeded(childBuilder); } } } private static void doInjectAntProgressLoggerLibIfNeeded(final AntBuilder antBuilder) { if (antBuilder.getProgressLoggerLib() != null) { // path already set, so don't interfere LOG.debug("Agent skipping AntProgressLogger injection, already set to: " + antBuilder.getProgressLoggerLib()); return; // no kludges required } final String defaultAntProgressLoggerLib; try { defaultAntProgressLoggerLib = AntScript.findDefaultProgressLoggerLib(); if (defaultAntProgressLoggerLib != null) { // AntScript will be able to find the jar on it's own, so again, don't interfere LOG.debug("Agent skipping AntProgressLogger injection, AntScript will set to: " + defaultAntProgressLoggerLib); return; // no kludges required } } catch (AntScript.ProgressLibLocatorException e) { // This exception is expected under Webstart, so ignore and continue LOG.debug("Couldn't find default AntProgressLogger, will attempt injection."); } // This assumes JNLP Extension for Ant Progress Logger has been installed. final String jnlpMuffinAntProgressLoggerPath = AntProgressLoggerInstaller.getJNLPMuffinAntProgressLoggerPath(); LOG.debug("jnlpMuffinAntProgressLoggerPath: " + jnlpMuffinAntProgressLoggerPath); final File progressLoggerJar = new File(jnlpMuffinAntProgressLoggerPath); if (!progressLoggerJar.exists()) { throw new IllegalStateException( "JNLP Build Agent couldn't find progress logger lib jar in expected location: " + progressLoggerJar.getAbsolutePath()); } antBuilder.setProgressLoggerLib(progressLoggerJar.getAbsolutePath()); LOG.debug("Injected AntProgressLogger lib: " + progressLoggerJar.getAbsolutePath() + " into AntBuilder."); } /** * Zip any build artifacts found in the logDir and/or outputDir. */ void prepareLogsAndArtifacts() { final String buildDirProperty = configProperties.getProperty(CRUISE_BUILD_DIR); try { buildRootDir = new File(buildDirProperty).getCanonicalFile(); } catch (IOException e) { final String message = "Couldn't create " + buildDirProperty; logPrefixError(message, e); System.err.println(message + " - " + e.getMessage()); throw new RuntimeException(message); } logDir = getAgentResultDir(PropertiesHelper.RESULT_TYPE_LOGS, PropertiesHelper.DISTRIBUTED_AGENT_LOGDIR); outputDir = getAgentResultDir(PropertiesHelper.RESULT_TYPE_OUTPUT, PropertiesHelper.DISTRIBUTED_AGENT_OUTPUTDIR); zippedLogs = ZipUtil.getTempResultsZipFile(buildRootDir, projectName, PropertiesHelper.RESULT_TYPE_LOGS); ZipUtil.zipFolderContents(zippedLogs.getAbsolutePath(), logDir.getAbsolutePath()); zippedOutput = ZipUtil.getTempResultsZipFile(buildRootDir, projectName, PropertiesHelper.RESULT_TYPE_OUTPUT); ZipUtil.zipFolderContents(zippedOutput.getAbsolutePath(), outputDir.getAbsolutePath()); if (remoteResults != null) { for (int i = 0; i < remoteResults.length; i++) { final File agentResultDir = remoteResults[i].getAgentDir(); ensureDirExists(agentResultDir); remoteResults[i].storeTempZippedFile( ZipUtil.getTempResultsZipFile(buildRootDir, projectName, "remoteResult" + i)); ZipUtil.zipFolderContents(remoteResults[i].fetchTempZippedFile().getAbsolutePath(), agentResultDir.getAbsolutePath()); } } } private File getAgentResultDir(final String resultType, final String resultProperty) { String resultDir = distributedAgentProps.get(resultProperty); logPrefixDebug("Result: " + resultType + "Prop value: " + resultDir); if (resultDir == null || "".equals(resultDir)) { // use canonical behavior if attribute is not set resultDir = buildRootDir + File.separator + resultType + File.separator + projectName; } // ensure dir exists final File fileResultDir = new File(resultDir); ensureDirExists(fileResultDir); return fileResultDir; } private static void ensureDirExists(final File fileResultDir) { if (!fileResultDir.exists()) { if (!Util.doMkDirs(fileResultDir)) { final String msg = "Error creating Agent result dir: " + fileResultDir.getAbsolutePath(); LOG.error(msg); throw new RuntimeException(msg); } } } public String getMachineName() { return machineName; } public void claim() { // flag this agent as busy for now. Intended to prevent mulitple builds on same agent, // when multiple master threads find the same agent, before any build thread has started. synchronized (busyLock) { if (isBusy()) { throw new IllegalStateException("Cannot claim agent on " + getMachineName() + " that is busy building project: " + projectName); } setBusy(true); } } public boolean isBusy() { synchronized (busyLock) { logPrefixDebug("Is busy called. value: " + isBusy); return isBusy; } } public Date getDateClaimed() { return dateClaimed; } private void setPendingKill() { synchronized (busyLock) { this.isPendingKill = true; pendingKillSince = new Date(); } } public boolean isPendingKill() { synchronized (busyLock) { return isPendingKill; } } public Date getPendingKillSince() { return pendingKillSince; } private void setPendingRestart() { synchronized (busyLock) { this.isPendingRestart = true; pendingRestartSince = new Date(); } } public boolean isPendingRestart() { synchronized (busyLock) { return isPendingRestart; } } public Date getPendingRestartSince() { return pendingRestartSince; } public boolean resultsExist(final String resultsType) throws RemoteException { if (resultsType.equals(PropertiesHelper.RESULT_TYPE_LOGS)) { return recursiveFilesExist(logDir); } else if (resultsType.equals(PropertiesHelper.RESULT_TYPE_OUTPUT)) { return recursiveFilesExist(outputDir); } else { throw new RemoteException("Unrecognized result type: " + resultsType); } } public boolean remoteResultExists(final int idx) throws RemoteException { if (remoteResults != null) { final boolean resultsExist = recursiveFilesExist(remoteResults[idx].getAgentDir()); if (resultsExist) { return true; } } return false; } static boolean recursiveFilesExist(final File fileToCheck) { if (!fileToCheck.exists()) { // save time, if it's doesn't exist at all return false; } else if (fileToCheck.isFile()) { // save time, if it's a file return true; } final File[] dirs = fileToCheck.listFiles(); for (final File dir : dirs) { if (recursiveFilesExist(dir)) { return true; // we found a file so return now, no need to keep looking. } } return false; } /** * Return the file containing the given type of results. Package visible for unit testing. * @param resultsType the type of results file * @return the file containing the given type of results. Package visible for unit testing. * @throws RemoteException if an invalid result type is given. */ File getResultsZip(final String resultsType) throws RemoteException { final File zipFile; if (PropertiesHelper.RESULT_TYPE_LOGS.equals(resultsType)) { zipFile = zippedLogs; } else if (PropertiesHelper.RESULT_TYPE_OUTPUT.equals(resultsType)) { zipFile = zippedOutput; } else { throw new RemoteException("Unrecognized result type: " + resultsType); } return zipFile; } public byte[] retrieveResultsAsZip(final String resultsType) throws RemoteException { final File zipFile = getResultsZip(resultsType); final byte[] response; try { response = FileUtil.getFileAsBytes(zipFile); } catch (IOException e) { final String message = "Unable to get file " + zipFile.getAbsolutePath(); logPrefixError(message, e); System.err.println(message + " - " + e.getMessage()); throw new RuntimeException(message, e); } return response; } public byte[] retrieveRemoteResult(final int resultIdx) throws RemoteException { RemoteResult remoteResult = null; if (remoteResults != null) { for (final RemoteResult remoteResultTry : remoteResults) { if (resultIdx == remoteResultTry.getIdx()) { remoteResult = remoteResultTry; break; } } } if (remoteResult == null) { final String message = "Invalid remote result index: " + resultIdx; logPrefixError(message); System.err.println(message); throw new RuntimeException(message); } final byte[] response; try { response = FileUtil.getFileAsBytes(remoteResult.fetchTempZippedFile()); } catch (IOException e) { final String message = "Unable to get remote result file: " + remoteResult.getAgentDir().getAbsolutePath(); logPrefixError(message, e); System.err.println(message + " - " + e.getMessage()); throw new RuntimeException(message, e); } return response; } public void clearOutputFiles() { try { if (logDir != null) { logPrefixDebug("Deleting contents of " + logDir); IO.delete(logDir); } else { logPrefixDebug("Skip delete agent logDir: " + logDir); } if (zippedLogs != null) { logPrefixDebug("Deleting log zip " + zippedLogs); zippedLogs.deleteOnExit(); IO.delete(zippedLogs); } else { logPrefixError("Skipping delete of log zip, file path is null."); } if (outputDir != null) { logPrefixDebug("Deleting contents of " + outputDir); IO.delete(outputDir); } else { logPrefixDebug("Skip delete agent outputDir: " + outputDir); } if (zippedOutput != null) { logPrefixDebug("Deleting output zip " + zippedOutput); zippedOutput.deleteOnExit(); IO.delete(zippedOutput); } else { logPrefixError("Skipping delete of output zip, file path is null."); } if (remoteResults != null) { for (final RemoteResult remoteResult : remoteResults) { logPrefixDebug("Deleting contents of " + remoteResult.getAgentDir().getAbsolutePath()); IO.delete(remoteResult.getAgentDir()); final File tempZippedFile = remoteResult.fetchTempZippedFile(); if (tempZippedFile != null) { logPrefixDebug("Deleting remote result zip " + tempZippedFile.getAbsolutePath()); tempZippedFile.deleteOnExit(); IO.delete(tempZippedFile); } } } setBusy(false); } catch (RuntimeException e) { logPrefixError("Error cleaning agent build files.", e); throw e; } } private void doRestart() { logPrefixInfo("Attempting agent restart."); final BasicService basicService; try { basicService = (BasicService) ServiceManager.lookup(BasicService.class.getName()); } catch (UnavailableServiceException e) { final String errMsg = "Couldn't find webstart Basic Service. Is Agent running outside of webstart?"; logPrefixError(errMsg, e); throw new RuntimeException(errMsg, e); } // Normally, we would set the busy lock first, but here we should only set the lock after we are certain // the required Webstart service is available. synchronized (busyLock) { if (!isBusy()) { // claim agent so no new build can start claim(); } // set project name to informative message in case agent is found while restarting projectName = "executingAgentRestart"; } final URL codeBaseURL = basicService.getCodeBase(); logPrefixInfo("basicService.getCodeBase()=" + codeBaseURL.toString()); // relaunch via new browser session // @todo How to close the browser after jnlp is relaunched? final URL relaunchURL; try { relaunchURL = new URL(codeBaseURL, "agent.jnlp"); } catch (MalformedURLException e) { final String errMsg = "Error building webstart relaunch URL from " + codeBaseURL.toString(); logPrefixError(errMsg, e); throw new RuntimeException(errMsg, e); } if (basicService.showDocument(relaunchURL)) { logPrefixInfo("Relaunched agent via URL: " + relaunchURL.toString() + ". Will kill current agent now."); doKill(); // don't wait for build finish, since we've already relaunched at this point. } else { final String errMsg = "Failed to relaunch agent via URL: " + relaunchURL.toString(); logPrefixError(errMsg); throw new RuntimeException(errMsg); } } private void doKill() { logPrefixInfo("Attempting agent kill."); synchronized (busyLock) { if (!isBusy()) { // claim agent so no new build can start claim(); } // set project name to informative message in case agent is found while shutting down projectName = "executingAgentKill"; } BuildAgent.kill(); doKillExecuted = true; } /** Intended only for unit tests, indicating if doKill() call has completed. */ private boolean doKillExecuted; boolean isDoKillExecuted() { return doKillExecuted; } public void kill(final boolean afterBuildFinished) throws RemoteException { setPendingKill(); if (!afterBuildFinished // Kill now, don't waiting for build to finish. || !isBusy()) { // Not busy, so kill now. doKill(); // calls back to this agent to terminate lookup stuff } else if (isBusy()) { // do nothing. When claim is released, setBusy(false) will perform the kill } fireAgentStatusChanged(); } public void restart(final boolean afterBuildFinished) throws RemoteException { setPendingRestart(); if (!afterBuildFinished // Restart now, don't waiting for build to finish. || !isBusy()) { // Not busy, so Restart now. doRestart(); } else if (isBusy()) { // do nothing. When claim is released, setBusy(false) will perform the Restart } fireAgentStatusChanged(); } private static final DateFormat DF_PROGRESS_LASTUPDATED = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss"); public String asString() { final StringBuilder sb = new StringBuilder(); sb.append("Machine Name: "); sb.append(machineName); sb.append(";\t"); sb.append("Started: "); sb.append(dateStarted); sb.append("\n\tBusy: "); sb.append(isBusy); sb.append(";\tSince: "); sb.append(dateClaimed); sb.append(";\tProject: "); sb.append(projectName); // include Progress if available if (buildProgressRemote != null) { sb.append("\n\tProgress: "); try { // preserve former date format //sb.append(buildProgressRemote.getValueRemote()); sb.append(DF_PROGRESS_LASTUPDATED.format(buildProgressRemote.getLastUpdatedRemote())); sb.append(" "); sb.append(buildProgressRemote.getTextRemote()); } catch (RemoteException e) { LOG.info("Error reading remote progress", e); } } sb.append("\n\tPending Restart: "); sb.append(isPendingRestart); sb.append(";\tPending Restart Since: "); sb.append(pendingRestartSince); sb.append("\n\tPending Kill: "); sb.append(isPendingKill); sb.append(";\tPending Kill Since: "); sb.append(pendingKillSince); sb.append("\n\tVersion: "); sb.append(CCDistVersion.getVersion()); sb.append(" (Compiled: "); sb.append(CCDistVersion.getVersionBuildDate()); sb.append(")"); return sb.toString(); } public void setEntryOverrides(PropertyEntry[] entryOverrides) { serviceContainer.setEntryOverrides(entryOverrides); // this is done only to update agent UI info with new entries info fireAgentStatusChanged(); } public PropertyEntry[] getEntryOverrides() { return serviceContainer.getEntryOverrides(); } public void addAgentStatusListener(final BuildAgent.AgentStatusListener listener) { agentStatusListeners.add(listener); } public void removeAgentStatusListener(final BuildAgent.AgentStatusListener listener) { agentStatusListeners.remove(listener); } private void fireAgentStatusChanged() { for (final BuildAgent.AgentStatusListener agentStatusListener : agentStatusListeners) { agentStatusListener.statusChanged(this); } } }