Subversion Repositories DevTools

Rev

Rev 7033 | Blame | Compare with Previous | Last modification | View Log | RSS feed

package com.erggroup.buildtool.daemon;

import com.erggroup.buildtool.abt.BuildToolException;
import com.erggroup.buildtool.abt.RemoteExecution;
import com.erggroup.buildtool.abt.RemoteExecution.LogOutput;
import com.erggroup.buildtool.daemon.BuildDaemon.NagiosInfo;
import com.erggroup.buildtool.daemon.ResumeTimerTask;
import com.erggroup.buildtool.ripple.Package;
import com.erggroup.buildtool.ripple.Phase;
import com.erggroup.buildtool.ripple.ReleaseManager;
import com.erggroup.buildtool.ripple.DaemonInstruction;
import com.erggroup.buildtool.ripple.ReportingData;
import com.erggroup.buildtool.ripple.RunLevel;
import com.erggroup.buildtool.smtp.CreateUrls;
import com.erggroup.buildtool.smtp.Smtpsend;
import com.erggroup.buildtool.utilities.MutableDate;
import com.erggroup.buildtool.utilities.utilities;
import com.erggroup.buildtool.ripple.RippleEngine;
import com.erggroup.buildtool.ripple.RunLevel.BuildState;
import com.erggroup.buildtool.daemon.BuildDaemon;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.PrintStream;
import java.io.StringReader;
import java.sql.SQLException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Timer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DefaultLogger;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.ProjectHelper;

/**Build Thread sub component
 */
public abstract class BuildThread
  extends Thread
{
  /**baseline identifier (which release manager release this BuildThread is dealing with)
   * @attribute
   */
  protected int mRtagId = 0;

  /**unique identifier of this BuildThread
   * @attribute
   */
  protected int mRconId = 0;
  
  /**
   * Used as a monitor for:
   * Slave: Indication that a command has been issued by the master
   * Master: Not yet used
   */
  protected Object mActiveBuildMonitor = new Object();


  /**
   * @aggregation composite
   */
  protected RunLevel mRunLevel = new RunLevel(BuildState.DB_IDLE);

  /**
   * @aggregation composite
   */
  protected ReleaseManager mReleaseManager;
  
  /**
   * 
   */
  protected RippleEngine mRippleEngine;
  
  /** Parsed ANT project
   * 
   */
  Project mProject = null;
  
  /**
   * Indicate the start time of the thread
   * Not nanosecond accurate, just an indication of when it was started
   */
  protected final long mStartTime = System.currentTimeMillis();

  /**unit test support
   * @attribute
   */
  protected String mUnitTest = "";

  /**
   * @aggregation composite
   */
  protected ResumeTimerTask mResumeTimerTask;

  /**
   * @aggregation composite
   */
  private Timer mTimer;

  /**Synchroniser object
   * Use to Synchronize on by multiple threads
   * @attribute
   */
  static final Object mSynchroniser = new Object();

  /**BuildThread group
   * @attribute
   */
  public static final ThreadGroup mThreadGroup = new ThreadGroup("BuildThread");

  /**the advertised build file content when either
   * a) no package in the baseline has a build requirement
   * b) the next package in the baseline with a build requirement is generic
   * @attribute
   */
  protected static final String mDummyBuildFileContent = "<dummy/>";

  /**Set true when last build cycle was benign.
   * @attribute
   */
  protected boolean mLastBuildWasBenign;
  
  /** Last retrieved release sequence number
   *  Used to determine if the Release Content has been changed by a user
   *  The sequence number does not take into account changes by the build system
   */
  protected int mReleaseSeqNum = -1;
  
  /** Holds the current buildfile 
   * 
   */
  protected File mBuildFile = new File("");
  
  /**Set true when last build cycle caught SQLException or Exception.
   * @attribute
   */
  protected boolean mException;

  /**Set true when ant error reported on any target.
   * @attribute
   */
  protected boolean mErrorReported;

  /**Logger
   * @attribute
   */
  private static final Logger mLogger = LoggerFactory.getLogger(BuildThread.class);

  /** Data about the package that is being built
   * @attribute
   */
  protected ReportingData mReporting = new ReportingData();

  /**Indicate the type of indefinite pause being processed
   * If true, then it is one that the daemon threads can recover from
   * If false, then wait for RM Database to indicate that its OK to proceed
   * @attribute
   */
  protected boolean mRecoverable;

  /**Logger for the entire build process
  * @attribute
  */
  DefaultLogger mBuildLogger ;
  
  /**Nagios Monitoring
   * @attribute
   */
  Phase mPhase = new Phase();
  

  /**constructor
   * @param rtagId          - Identifies the Release
   * @param rconId          - Identifies the Controlling entry
   * @param releaseManager  - A ReleaseManager entry to use
   * @param unitTest        - Unit Test data 
   */
  BuildThread(int rtagId, int rconId, ReleaseManager releaseManager, String unitTest)
  {
    super(mThreadGroup, "");
    mLogger.debug("BuildThread");
    mLastBuildWasBenign = true;
    mException = false;
    mErrorReported = false;
    mRecoverable = false;
    
    mRtagId = rtagId;
    mRconId = rconId;
    mReleaseManager = releaseManager;
    mBuildFile = new File(mRtagId + "build.xml");
    mUnitTest = ( unitTest == null ) ? "" : unitTest;
    
    // no need to be synchronized - BuildThreads are instantiated in a single thread
    if ( mResumeTimerTask == null )
    {
      mResumeTimerTask = new ResumeTimerTask();
      mTimer = new Timer();
      mResumeTimerTask.setTimer(mTimer);
    }
  }

  /**Flags that a new build cycle is about to start
  *  Create a new logger to capture all the complete build log
  *  even though its done in stages we want a complete log.
  */
  void flagStartBuildCycle()
  {
    try
    {
      mBuildLogger = new DefaultLogger();
      PrintStream ps = new PrintStream(mRtagId + ".log");
      mBuildLogger.setOutputPrintStream(ps);
      mBuildLogger.setMessageOutputLevel(Project.MSG_INFO);
    }
    catch( FileNotFoundException e )
    {
      mLogger.error("BuildThread caught FileNotFoundException");
    }
  }

  /**
   *    Sleeps when mException is set
   *    Called at the top of the run() loop, just after exception processing
   */
  protected void sleepCheck()
  {
    if (mException && ! BuildDaemon.mShutDown)
    {
        Integer sleepTime = 5 * 60;
        mLogger.warn("sleepCheck sleep {} secs", sleepTime);
        BuildDaemon.daemonSleepSecs(sleepTime);
        mLogger.info("sleepCheck sleep returned");
        
        // Force planning cycle when recovering from an exception
        mReleaseSeqNum = -1;
    }
    mException = false;
  }
  
  /**initially changes the run level to IDLE
   * <br>Determines if the BuildThread is still configured
   * <br>Detects shutdown requests
   * <br>a) Determines if the BuildThread is running in scheduled down time
   * <br>b) Determines if the BuildThread is directed to pause
   * <br>Changes the run level to PAUSED if a) or b) are true
   * <br>throws ExitException when not configured or shutdown request
   * 
   * @param     master  True: Master Daemon
   * @exception ExitException
   * @exception SQLException
   * @exception Exception
   */
  protected void allowedToProceed(boolean master) throws ExitException, SQLException, Exception
  {
    mLogger.debug("allowedToProceed");
    
    //  Release Manager Database operations
    //      Change the run level to IDLE
    //      Discard any reserved version numbers
    //      Reset the current package being built
    try
    {
      mLogger.error("allowedToProceed changing run level to IDLE for rcon_id {}", mRconId);
      mRunLevel.persist(mReleaseManager, mRconId, BuildState.DB_IDLE);
    }
    catch(SQLException e)
    {
      mLogger.warn("allowedToProceed caught SQLException");
    }
  
    //  Sleep for the required time - if the last build cycle was benign
    //      Slaves sleep for 3 seconds
    //      Masters will sleep for up to 60 minutes
    if (mLastBuildWasBenign && !BuildDaemon.mShutDown)
    {
        if (master)
        {
            long startMasterWait = System.currentTimeMillis();
            while(!BuildDaemon.mShutDown)
            {
                int masterCount = mReleaseManager.queryMasterCount( mRtagId );
                if ( masterCount != 1)
                {
                    mLogger.warn("allowedToProceed Invalid MasterCount: {}", masterCount);
                    throw new ExitException();   
                }
                
                int seqNum = mReleaseManager.queryReleaseSeqNum(mRtagId, mRconId, BuildDaemon.mHostname);
                if ( seqNum < 0 )
                {
                    mLogger.warn("allowedToProceed Not part of buildset");
                    throw new ExitException();
                }
                if ( seqNum != mReleaseSeqNum)
                {
                    //  Release Content has been modified
                    mReleaseSeqNum = seqNum;
                    mLogger.error("allowedToProceed. Release Sequence changed");
                    break;
                }
                
                //  Check daemon instructions that need to be processed
                //
                DaemonInstruction di = new DaemonInstruction(mRtagId, -1, true);
                if ( mReleaseManager.getDaemonInst( di ) )
                {
                    // At least one daemon instruction has expired
                    mLogger.error("allowedToProceed. Deamon instruction to be scheduled");
                    break;
                }
                
                //
                //    Backup plan
                //    Force a planning session every 60 minutes
                //
                if ( System.currentTimeMillis() - startMasterWait > 60 * 60 * 1000)
                {
                    mLogger.error("allowedToProceed. Force periodic plan");
                    break;
                }
                
                //  Have no reason to suspect that we need to plan a release
                //  Persist idle state to indicate the master is polling
                //  Wait 60 seconds and test again
                //
                mLogger.warn("allowedToProceed sleep 60 secs no build requirement");
                mRunLevel.persist(mReleaseManager, mRconId, BuildState.DB_IDLE);
                BuildDaemon.daemonSleepSecs(60);
                mLogger.info("allowedToProceed sleep returned");
            }
        }
        else
        {
            //  Slave - Wait 3 seconds
            mLogger.warn("allowedToProceed sleep 3 secs no build requirement");
            BuildDaemon.daemonSleepSecs(3);
            mLogger.info("allowedToProceed sleep returned");
        }
    }
    
    if (BuildDaemon.mShutDown)
    {
        mLogger.warn("allowedToProceed ShutDown requested");
        throw new ExitException();
    }

    //  Loop until this daemon can proceed - or no longer configured as a part of the build set
    //  Things that will hold it up:
    //      Indefinite pause
    //      Scheduled down time
    //      Waiting for recoverable error (dpkg_archive access)
    
    boolean proceed = false;
    try
    {
      while ( !proceed &&  !BuildDaemon.mShutDown )
      {
        mLogger.error("allowedToProceed calling mReleaseManager.connect");                      
        mReleaseManager.connect();
        
        //  Ensure that the machine is a part of the current build set
        mLogger.error("allowedToProceed calling mReleaseManager.queryReleaseConfig");                      
        if ( !mReleaseManager.queryReleaseConfig(mRtagId, mRconId, BuildDaemon.mHostname, getMode(), mStartTime) )
        {
            mLogger.warn("allowedToProceed queryReleaseConfig failed");
            mReleaseManager.disconnect();
            throw new ExitException();
        }
        
        //  If dpkg_archive has been flagged as unavailable, then check for its recovery
        //
        if ( mRecoverable )
        {
            if ( Package.recover() )
            {
                // Package archive(s) exist
                // clear the indefinite pause condition
                mReleaseManager.resumeIndefinitePause();
                notifyIndefiniteRecovery();
                mRecoverable = false;
            }
        }
        
        if ( mUnitTest.compareTo("unit test not allowed to proceed") == 0 )
        {
          mRunLevel.persist(mReleaseManager, mRconId, BuildState.DB_PAUSED);
          throw new ExitException();
        }
        
        //  Check for scheduled downtime or indefinite pause
        MutableDate resumeTime = new MutableDate();
        mLogger.error("allowedToProceed calling mReleaseManager.queryRunLevelSchedule");                      
        if ( !mReleaseManager.queryRunLevelSchedule(resumeTime) )
        {
          mLogger.info("allowedToProceed scheduled downtime");
          mReleaseManager.disconnect();

          mLogger.error("allowedToProceed changing run level to PAUSED for rcon_id {}", mRconId);
          mRunLevel.persist(mReleaseManager, mRconId, BuildState.DB_PAUSED);
          
          //
          //    Limit the pause to 10 minutes at a time
          //    Allows early get out for the daemons
          long  tenMinutesFromNow = System.currentTimeMillis() + 10 * 60 * 1000;
          if (resumeTime.value.getTime() > tenMinutesFromNow)
          {
              resumeTime.value.setTime(tenMinutesFromNow);
              mLogger.warn("allowedToProceed delay limited to 10 minutes");
          }
          
          synchronized(mSynchroniser)
          {
            // contain the schedule and wait in the same synchronized block to prevent a deadlock
            // eg this thread calls schedule, timer thread calls notifyall, this thread calls wait (forever)
            try
            {
              if (mResumeTimerTask.isCancelled())
              {
                mResumeTimerTask = new ResumeTimerTask();
                mTimer = new Timer();
                mResumeTimerTask.setTimer(mTimer);
              }
              mLogger.warn("allowedToProceed schedule passed {}", resumeTime.value.getTime());
              mTimer.schedule(mResumeTimerTask, resumeTime.value);
            }
            catch( IllegalStateException e )
            {
              // this may be thrown by schedule if already scheduled
              // it signifies another BuildThread has already scheduled the ResumeTimerTask
               mLogger.warn("allowedToProceed already scheduled");
            }
  
            try
            {
              mLogger.warn("allowedToProceed wait");
              mSynchroniser.wait();
              mLogger.warn("allowedToProceed wait returned");
            }
            catch( InterruptedException e )
            {
              mLogger.warn("allowedToProceed caught InterruptedException");
              Thread.currentThread().interrupt();
            }
          }
          
        }
        else
        {
            //
            //  If commanded to pause, then wait around until the command has been removed.
            //
            mLogger.error("allowedToProceed calling mReleaseManager.queryDirectedRunLevel");                      
            if ( !mReleaseManager.queryDirectedRunLevel(mRconId) && !BuildDaemon.mShutDown )
            {
                mLogger.error("allowedToProceed changing run level to PAUSED for rcon_id {}", mRconId);
                mReleaseManager.disconnect();
                mRunLevel.persist(mReleaseManager, mRconId, BuildState.DB_PAUSED);

                //  Sleep for periodicMs
                mLogger.warn("allowedToProceed sleep 1 mins directed downtime");
                BuildDaemon.daemonSleepSecs(60);
                mLogger.info("allowedToProceed sleep returned");
            }
            else
            {
                mReleaseManager.disconnect();
                proceed = true;
            }
        }
      }
    }
    finally
    {
      // this block is executed regardless of what happens in the try block
      // even if an exception is thrown
      // ensure disconnect
      mLogger.error("allowedToProceed calling mReleaseManager.disconnect");                      
      mReleaseManager.disconnect();
    }
    
    if (BuildDaemon.mShutDown)
    {
        mLogger.warn("allowedToProceed ShutDown requested");
        throw new ExitException();
    }

  }

/**periodically 
   * <br>a) performs disk housekeeping
   * <br>b) determines if a minimum threshold of disk space is available
   * <br>c) determines if a file can be touched
   * <br>d) Master: Checks that a ssh connection can be established to the package server
   * <br>changes the run level to CANNOT_CONTINUE if insufficient disk space
   * <br>otherwise changes the run level to ACTIVE and returns
   * <p>This function does not return until all check have passed
   * 
   * @return    true - The task was delayed until all checks passed. The caller may 
   *                               wish to reevaluate the ability to continue the current task.
   * 
   */
  protected boolean checkEnvironment() throws Exception
  {
    mLogger.debug("checkEnvironment");
    boolean exit = false;
    boolean waitPerformed = false;
    String  reason;
    
    while( !exit )
    {
      mPhase.setPhase("checkEnvironment");
      exit = false;
      reason = "No Reason";
      
      //  Ugly Unit Testing Hook
      if ( mUnitTest.startsWith("unit test") )
      {
          if ( mUnitTest.startsWith("unit test check environment") )
          {
              if (mUnitTest.endsWith("Pass2"))
              {
                  exit = true;
              }
              else
              {
                  mUnitTest += ".Pass2";
                  reason = "Unit Test Check";
              }
          }
          else
          {
              exit= true;
          }
      }
      else
      {
          housekeep();

          //    Perform checks in order
          //    Failure of the first one terminates the check
          //

          if ( ! hasSufficientDiskSpace() )
          {
              mLogger.warn("checkEnvironment below disk free threshold");
              mPhase.setPhase("checkEnvironment:Wait for diskspace");
              reason = "Insufficient disk space";
          }
          else if (!touch() ) 
          {
              mLogger.warn("checkEnvironment read only file system detected");
              mPhase.setPhase("checkEnvironment:Wait for writable files system");
              reason = "Readonly file system";
          }
          else if ( !checkRemoteExecution() ) 
          {
              mLogger.warn("checkEnvironment Remote connection not established");
              mPhase.setPhase("checkEnvironment:Wait for remote execution");
              reason = "No remote connection";
          }
          else
          {
              exit = true;
              mPhase.setPhase("checkEnvironment");
              reason = "No Reason";
          }
      }
      if ( !exit )
      {
        waitPerformed = true;
        mLogger.error("checkEnvironment changing run level to CANNOT_CONTINUE for rcon_id {}", mRconId);
        mRunLevel.persistFault(mReleaseManager, mRconId, reason);
        
        if ( !mUnitTest.startsWith("unit test check environment") )
        {
          mLogger.warn("checkEnvironment sleep 5 mins");
          exit = BuildDaemon.daemonSleepSecs(5 * 60);
          mLogger.info("checkEnvironment sleep returned");
        }
      }
    }

    mLogger.error("checkEnvironment changing run level to ACTIVE for rcon_id {}", mRconId);
    mRunLevel.persist(mReleaseManager, mRconId, BuildState.DB_ACTIVE);
    
    return waitPerformed;
  }

  /**   Perform check of the remote execution target machine
   *    Used so that we know at the start of a build that the target machine can be contacted
   *    Check is only done on the master
   * 
   * @return    true - the check has passed
   * 
   */
  private boolean checkRemoteExecution()
  {
          if ( getMode() != 'M' || mUnitTest.startsWith("unit test") )
          {
                  // Don't check if
                  //    This thread is not a master
                  //    This is a unit test
                  return true;
          }
          
          RemoteExecution mRemoteExecution = new RemoteExecution(new LogOutput(){

                        @Override
                        public void data(String message) {
                                mLogger.info(message);
                        }
                        
                        @Override
                        public void fatal(String message) {
                                mLogger.error(message);
                        }

                        @Override
                        public void info(String message) {
                                mLogger.info(message);
                        }});
          
          return mRemoteExecution.testConnection();
          
  }
  
  
  /**performs disk housekeeping which involves deleting build directories > 5 days old
   * refer to the sequence diagram check environment
   */
  private void housekeep()
  {
    mLogger.debug("housekeep");
    FilenameFilter filter = new FilenameFilter()
    {
        
      // Filter function to process the files processed within this routine
      public boolean accept(File file, String name)
      {
        mLogger.debug("accept {}", name);
        boolean retVal = false;
        
        if ( file.isDirectory() && !name.startsWith( "." ) )
        {
          retVal = true;
        }
        
        mLogger.info("accept returned {}", retVal);
        return retVal;
      }
    };
    
    try
    {
      // DEVI 46729, 46730, solaris 10 core dumps implicate deleteDirectory
      // let each BuildThread look after its own housekeeping
      String cwdPath = utilities.catDir(BuildDaemon.mGbeLog ,BuildDaemon.mHostname ,String.valueOf( mRtagId )); 
      File cwd = utilities.freshFile(cwdPath);

      File[] children = cwd.listFiles( filter );
      
      if ( children != null )
      {
        for ( int child=0; child < children.length; child++ )
        {
          // child is named uniquely to encapsulate a build
          // 5 days = 432,000,000 milliseconds
          if ( ( System.currentTimeMillis() - children[ child ].lastModified() ) > 432000000 )
          {
            // the directory is over 5 days old
            mLogger.warn("housekeep deleting directory {}", children[ child ].getName());
            deleteDirectory( children[ child ] );
          }
        }
      }
    }
    catch( SecurityException e )
    {
      // this can be thrown by lastModified
      mLogger.warn("housekeep caught SecurityException");
    }
     
  }

  /**returns true if a file exists and can be deleted,
   * created and exists in the file system
   * this is to guard against read-only file systems
   */
  private boolean touch()
  {
    mLogger.debug("touch");
    boolean retVal = true;
    
    try
    {
      File touch = new File( String.valueOf( mRtagId ) + "touch" );
      
      if ( touch.exists() )
      {
        // delete it
        retVal = touch.delete();
      }
      
      if ( retVal )
      {
        // file does not exist
        retVal = touch.createNewFile();
      }
    }
    catch( SecurityException e )
    {
      // this can be thrown by exists, delete, createNewFile
      retVal = false;
      mLogger.warn("touch caught SecurityException");
    }
    catch( IOException e )
    {
      // this can be thrown by createNewFile
      retVal = false;
      mLogger.warn("touch caught IOException");
    }
    
    mLogger.info("touch returned {}", retVal);
    return retVal;
  }
  
  /**returns true if free disk space > 5G
   * this may become configurable if the need arises
   * refer to the sequence diagram check environment
   */
  private boolean hasSufficientDiskSpace()
  {
      mLogger.debug("hasSufficientDiskSpace");
      boolean retVal = true;
      long freeSpace = 0;

      try
      {
          File cwd = new File( "." );

          // 5G = 5368709120 bytes
          // 1G = 1073741824 bytes - useful for testing
          freeSpace = cwd.getUsableSpace();

          if ( freeSpace < 5368709120L )
          {
              mLogger.warn("hasSufficientDiskSpace failed: {} freespace {}", cwd.getAbsolutePath() , freeSpace);
              retVal = false;
          }
      }
      catch( SecurityException e )
      {
          // this can be thrown by getFreeSpace
          mLogger.warn("hasSufficientDiskSpace caught SecurityException");
      }

      mLogger.info("hasSufficientDiskSpace returned {} {}", retVal, freeSpace);
      return retVal;
  }

  /**abstract method
   */
  @Override
  public abstract void run();

  /**   deletes directory and all its files
   *    Will set files and directories to be writable in case the user has messed with
   *    file permissions
   */
  FilenameFilter deleteDirectoryFilter = new FilenameFilter()
  {
    public boolean accept(File file, String name)
    {
      mLogger.debug("accept {}", name);
      boolean retVal = false;
      
      if ( name.compareTo(".") != 0 && ( name.compareTo("..") != 0 ) )
      {
        retVal = true;
      }
      
      mLogger.info("accept returned {}", retVal);
      return retVal;
    }
  };

  
  @SuppressWarnings("squid:S899")  
  protected void deleteDirectory(File directory)
  {
    mLogger.debug("deleteDirectory {}", directory.getName());
    try
    {
      if ( directory.exists() )
      {
      
        //
        directory.setWritable(true);
        File[] children = directory.listFiles( deleteDirectoryFilter );
        
        if ( children != null )
        {
          for ( int child=0; child < children.length; child++ )
          {
            if ( children[ child ].isDirectory() )
            {
              deleteDirectory( children[ child ] );
            }
            else
            {
                children[ child ].setWritable(true);
                children[ child ].delete();
            }
          }
        }
        directory.delete();
      }
    }
    catch( SecurityException e )
    {
      // this can be thrown by exists and delete
       mLogger.warn("deleteDirectory caught SecurityException");
    }
  }

  /**
   * abstract method. Re-implemented by each class
   * Returns the Mode in which the thread is operating.
   * 
   * @return M if Master, S if Slave
   */
  protected abstract char getMode();

  /** Save the buildfile to disk
   *  @param buildFileContent The generated build file that will control this run
 * @throws Exception 
   * 
   */
  protected void saveBuildFile(String buildFileContent) throws BuildSystemException
  {
      mReporting.buildFailureLogFile = null;
      mErrorReported = false;

      mBuildFile.delete();
      FileWriter buildFileWriter = null;
      BufferedReader buildFileBufferedReader = null;
      boolean errorSeen = false;
      
      if ( buildFileContent != null ) {
         
          try {

              StringReader buildFileContentStringReader = new StringReader(buildFileContent);
              buildFileBufferedReader = new BufferedReader(buildFileContentStringReader);

              // Sanitize the buildFileContent
              //      it may contain line.separators of "\n", "\r", or "\r\n" variety, 
              //      depending on the location of the ripple engine
              StringBuilder  sanitisedBFC = new StringBuilder ();
              String lf = System.getProperty("line.separator") ;
              String line;

              while( ( line = buildFileBufferedReader.readLine() ) != null)
              {
                  sanitisedBFC.append (line).append(lf);
              }
              buildFileBufferedReader.close();
              buildFileBufferedReader = null;
              
              buildFileWriter = new FileWriter(mBuildFile);
              buildFileWriter.write(sanitisedBFC.toString());
              buildFileWriter.close();
              buildFileWriter = null;

          }
          catch( IOException e )
          {
              mLogger.error("SaveBuildFile caught IOException");
              errorSeen = true;
              
          }
          finally
          {
              if ( buildFileWriter!= null ) {
                  try {
                    buildFileWriter.close();
                } catch (IOException e) {
                    errorSeen = true;
                    
                }
              }
              if ( buildFileBufferedReader != null) {
                  try {
                    buildFileBufferedReader.close();
                } catch (IOException e) {
                    errorSeen = true;
                }
              }
          }
      }
      
      if (errorSeen) {
          mBuildFile.delete();
          throw new BuildSystemException("SaveBuildFile caught FileNotFoundException");    
      }
  }
  
  /** Delete the build file
   * 
   */
  protected void deleteBuildFile()
  {
      if ( mBuildFile.exists() )
      {
          mBuildFile.delete(); //NOSONAR
      }    
  }
  
  /** Parse the ANT project
   *  Done once per build - useful information is extracted for use within the build
 * @param updateReporting 
   */
  protected void parseBuildFile(boolean updateReporting)
  {
      mLogger.debug("parseBuildFile");

      //try
      {
          mProject = null;
          if (updateReporting) {
              mReporting.resetData();
          }

          if ( mBuildFile.exists() )
          {
              mProject = new Project();
              mProject.setProperty("ant.file", mBuildFile.getAbsolutePath());

              // Add listener for logging the complete build process
              // If the daemon has been restarted, then the listener will not have been
              // set up - so don't add it. Perhaps we need a way to open an existing
              // log file to append.
              if ( mBuildLogger != null )               {
                  mProject.addBuildListener(mBuildLogger);
              }

              // parse can throw BuildException, this is serious
              //  Generate a general exception to force an indefinite pause
              //  Delete build file to prevent attempts to use it 
              try {
                  mProject.init();
                  ProjectHelper pH = ProjectHelper.getProjectHelper();
                  mProject.addReference("ant.projectHelper", pH);
                  pH.parse(mProject, mBuildFile);
              }
              catch (BuildException be)
              {
                  mLogger.error("Parse Error: {}", be );

                  mBuildFile.delete(); //NOSONAR
                  throw new BuildToolException("Error parsing build file:"+ be.getCause());
              }

              if (updateReporting) {
                  // set up project properties for reporting purposes
                  // this first group are hard coded in the build file

                  mReporting.rtagId = mRtagId;
                  mReporting.buildRef = mProject.getProperty("abt_daemon");
                  mReporting.packageName = mProject.getProperty("abt_package_name");
                  mReporting.packageVersion = mProject.getProperty("abt_package_version");
                  mReporting.packageExtension = mProject.getProperty("abt_package_extension");
                  mReporting.packageLocation = mProject.getProperty("basedir") + mProject.getProperty("abt_package_location");
                  mReporting.packageDepends = mProject.getProperty("abt_package_depends");
                  mReporting.packageOwners = mProject.getProperty("abt_package_ownerlist");
                  mReporting.packageBuildInfo = mProject.getProperty("abt_package_build_info");
                  mReporting.isRipple = ReportingData.toBool(mProject.getProperty("abt_is_ripple"));
                  mReporting.buildReason = ReportingData.toBuildReason(mProject.getProperty("abt_build_reason"));
                  mReporting.packageVersionId = ReportingData.toInt(mProject.getProperty("abt_package_version_id"), -1);
                  mReporting.doesNotRequireSourceControlInteraction = ReportingData.toBool(mProject.getProperty("abt_does_not_require_source_control_interaction"));
                  mReporting.isaTestBuild = ReportingData.toBool (mProject.getProperty("abt_test_build_instruction"));
              }
              //
              //  Log the abt_daemon value so that we can trace the build log
              //  Build logs from all machines are stored with this number
              //  Only do it at the start of the build

              mLogger.error("BuildRef: {}", mReporting.buildRef );    

          }
      }
  }
  
  
  /**
   * builds a buildFile from the buildFileContent
   * triggers ant to operate on the buildFile
   * 
   * @param target           The command to be run. This is one of the commands known to the ABT class
   * @param updateReporting  True to update the mReportingData from the buildfiles properties
   * @param forceOpr         True - force operation on errors
   * @throws Exception
   * @throws BuildException 
   * @return Success - False on error             
   */
  protected boolean deliverChange(String target, boolean updateReporting, boolean forceOpr) throws Exception
  {
      mLogger.debug("deliverChange");

      //  Bypass the operation if an error is being propagated
      //      Always perform a AbtSetUp and AbtTearDown
      //
      if (!forceOpr) {
          if ( mErrorReported ) {
              // SaveBuildFile, AbtTestPath, AbtSetUp or the build failed
              // the default target will inevitably fail and will generate further email if allowed to proceed
              // do not mask the root cause
              mLogger.debug("deliverChange - Error already detected");
              return false;    
          }
      }

      //  Parse the build file
      parseBuildFile(updateReporting);
      if (mProject != null)
      {

          if ( target == null )     {
              target = mProject.getDefaultTarget();
          }
          mLogger.warn("deliverChange ant launched: {} Target: {}", mBuildFile.getAbsolutePath() , target);

          try {
              mProject.executeTarget(target);
              mLogger.warn("deliverChange ant returned");
          }

          //   
          //  Do not catch ALL exceptions. The MasterThread::run and SlaveThread::run
          //  relies on exceptions propagating upwards for the indefinite pause feature
          //
          catch( BuildException e )
          {

              if ( mReporting.buildFailureLogFile == null )
              {
                  mReporting.buildFailureLogFile = e.getMessage();
              }
              mLogger.debug("deliverChange caught BuildException, big deal, the build failed {}", mReporting.buildFailureLogFile);
              mErrorReported = true;
          }


          if (updateReporting )
          {
              // this group is set at run time (by the AbtPublish target only)
              // they will be null for every other target,
              // and null if an error occurs in the AbtPublish target
              mReporting.isFullyPublished = ReportingData.toBool(mProject.getProperty("abt_fully_published"));
              mReporting.newVcsTag = mProject.getProperty("abt_new_vcstag");
          }
      }

      return !mErrorReported;
  }
 
   /**
    * eMail an indefinite pause notification
    * 
    * @param cause  Reason for the pause
    */
   protected void notifyIndefinitePause(String cause)
   {
     mLogger.debug("indefinitePause");
       
     String body =
     "Hostname: " + BuildDaemon.mHostname + "<p>" +
     "Release: " + mRippleEngine.mBaselineName + "<p>" +
     "Cause: " + cause + "<p>" +
     "RmRef: "   + CreateUrls.generateReleaseUrl(mRippleEngine.getRtagId()) +"<p>";
     
     try
     {
       Smtpsend.send(
       mRippleEngine.getMailServer(),       // mailServer
       mRippleEngine.getMailSender(),       // source
       mRippleEngine.getMailGlobalTarget(), // target (list)
       null,                                // cc
       null,                                // bcc
       "BUILD DAEMON Indefinite Pause",     // subject
       body,                                // body
       null                                 // attachment
       );
     }
     catch( Exception e )
     {
     }
   }
   

   /**
    * eMail an indefinite pause recovery notification
    */
   private void notifyIndefiniteRecovery()
   {
       mLogger.debug("indefiniteRecovery");
       
       String body =
       "Hostname: " + BuildDaemon.mHostname + "<p>" +
       "Release: "  + mRippleEngine.mBaselineName + "<p>" +
       "Cause: "    + "Recovery of previous condition" + "<p>" +
       "RmRef: "    + CreateUrls.generateReleaseUrl(mRippleEngine.getRtagId()) +"<p>";
       
       try
       {
         Smtpsend.send(
         mRippleEngine.getMailServer(),                 // mailServer
         mRippleEngine.getMailSender(),                 // source
         mRippleEngine.getMailGlobalTarget(),           // target (list)
         null,                                          // cc
         null,                                          // bcc
         "BUILD DAEMON Indefinite Pause Recovery",      // subject
         body,                                          // body
         null                                           // attachment
         );
       }
       catch( Exception e )
       {
       }
       
   }


    /**
     *  Nagios interface
     *      Returns true if the thread looks OK
     * @param nagInfo 
    */
    boolean checkThread(NagiosInfo nagInfo)
    {
      boolean retVal = true;
      if ( mRunLevel.mBuildState == BuildState.DB_CANNOT_CONTINUE ) {
        retVal = false;
        nagInfo.extendedReason.add("[" + mRtagId + "] " + mRunLevel.toString());
      } else  {
        retVal = checkThreadExtended(nagInfo);
      }
      
      if (this.getMode() == 'M') {
          nagInfo.masterCount++;
      } else {
          nagInfo.slaveCount++;
      }

      mLogger.warn("checkThread[{}] returned {}",mRtagId, retVal);
      return retVal;
    }

    /**
     * Nagios interface extension
     * This method should be overridden by classes that extend this class
     * If not overridden then the test indicates OK
     *
     *      Returns true if the thread looks OK
    */
    boolean checkThreadExtended(NagiosInfo nagInfo)
    {
        boolean retVal = mPhase.isHappy();
        if ( ! retVal) {
            nagInfo.extendedReason.add("[" + mRtagId + "] " + mPhase.happyText());
        }
        mLogger.info("checkThreadExtended: {}:{}",retVal, mPhase.toStringSecs() );
        return retVal;
    }


    /**
     * Nagios interface extension
     * This method should be overridden by classes that extend this class
     * If not overridden then the test indicates OK
     *
     *      Returns true if the thread looks OK
     * @param wr Output buffer
     * @param nagInfo 
     * @param estatus 
     * @throws IOException 
    */
    public void extendedStatus(NagiosInfo nagInfo, Map<String, Object> estatus)
    {
        LinkedHashMap<String, Object> data = new LinkedHashMap<String, Object>();
        estatus.put(String.valueOf(mRtagId), data);
        
        data.put("rtagId", Integer.valueOf(mRtagId));
        data.put("rconId", Integer.valueOf(mRconId));
        data.put("Thread Start Text", new SimpleDateFormat("dd-MM-yyyy HH:mm:ss").format(new Date(mStartTime)));
        data.put("Thread Start", Long.valueOf(mStartTime));
        data.put("Thread Alive:", Boolean.valueOf(this.isAlive() ));
        data.put("Thread Check:", Boolean.valueOf(checkThread(nagInfo) ));
        data.put("Mode" , String.valueOf(this.getMode() ));
        data.put("mRunLevel" , mRunLevel.toString() );
        data.put("mLastBuildWasBenign" , Boolean.valueOf(mLastBuildWasBenign));
        data.put("mException" , Boolean.valueOf(mException));
        data.put("mErrorReported" , Boolean.valueOf(mErrorReported));
        data.put("mRecoverable", Boolean.valueOf(mRecoverable));
        data.put("mPhase" ,  mPhase.sText);
        data.put("mPhaseDelta" ,  mPhase.getDeltaSecs());
        if ( mReleaseManager != null)
            data.put("Rm Mutex" , mReleaseManager.mMutexState );
        else
            data.put("Rm Mutex" , null );
    }

}