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.BuildThread;import com.erggroup.buildtool.ripple.*;import com.erggroup.buildtool.ripple.BuildFile.BuildFileState;import com.erggroup.buildtool.ripple.Package;import com.erggroup.buildtool.ripple.ReleaseManager.BuildResult;import com.erggroup.buildtool.ripple.RunLevel.BuildState;import com.erggroup.buildtool.smtp.CreateUrls;import com.erggroup.buildtool.smtp.Smtpsend;import com.erggroup.buildtool.utilities.*;import java.io.BufferedReader;import java.io.DataInputStream;import java.io.File;import java.io.FileInputStream;import java.io.InputStreamReader;import java.sql.SQLException;import java.util.Iterator;import org.slf4j.Logger;import org.slf4j.LoggerFactory;/**Master Thread sub component*/public class MasterThread extends BuildThread{/**Logger* @attribute*/private static final Logger mLogger = LoggerFactory.getLogger(MasterThread.class);/** Current BuildFile**/private BuildFile mMasterBuildFile = new BuildFile();/** Measure the duration of the current build**/private ElapseTime mBuildDuration = new ElapseTime();/**constructor* @param rtagId - Identifies the Release* @param rconId - Identifies the Controlling entry* @param releaseManager - A ReleaseManager entry to use* @param unitTest - Unit Test data*/public MasterThread(int rtagId, int rconId, ReleaseManager releaseManager, String unitTest){super(rtagId, rconId, releaseManager, unitTest);mLogger.warn("MasterThread rtag_id " + rtagId + " rcon_id " + rconId);}/**implements the sequence diagrams coordinate slave threads, generate build files, allowed to proceed, check environment*/public void run(){Integer id = Integer.valueOf(mRtagId);setName(id.toString());mLogger.warn("Master Thread run");boolean exit = false;mRippleEngine = new RippleEngine(mReleaseManager, mRtagId, true);while(!exit){try{mLogger.error("run calling sleepCheck");mPhase.setPhase("sleepCheck");sleepCheck();mLogger.error("run calling rippleEngine.collectMetaData");mPhase.setPhase("collectMetaData", 600);mRippleEngine.collectMetaData();if ( Thread.currentThread().isInterrupted() ){mLogger.warn("run is interrupted");// unit test techniquethrow new ExitException();}if ( mUnitTest.compareTo("unit test spawn thread") == 0){throw new Exception();}if ( !mUnitTest.startsWith("unit test check environment")){if ( mUnitTest.compareTo("unit test generate build files") != 0){if ((mUnitTest.compareTo("unit test allowed to proceed") != 0) &&(mUnitTest.compareTo("unit test not allowed to proceed") != 0) &&(mUnitTest.compareTo("unit test exit") != 0)){//---------------------------------------------------------------// Wait for all Slaves to enter waiting or paused state// They may be building// They may be starting up//mLogger.error("run changing run level to WAITING (New) for rcon_id " + mRconId);mPhase.setPhase("Wait for Slave Threads");mRunLevel.persist(mReleaseManager, mRconId, BuildState.DB_WAITING);while (true){StringBuilder waitList = new StringBuilder();mLogger.info("run calling mReleaseManager.queryRunLevel");mReleaseManager.queryRunLevel(mRtagId);// Process all run entriesfor (Iterator<RunLevelData> it = mReleaseManager.mRunLevelCollection.iterator(); it.hasNext(); ){RunLevelData rl = it.next();//// Only examine Slaves.// This is being run on a Master and at this point the Master is not waiting// If there are multiple (misconfigured) Masters, then we can get confused//// If I am no longer a Master we will process till the end of the build cycle// at which point it will be detected.//// If there are other Masters, then we assume that they are not waiting//// Ignore myself. I am not waiting. I am asking the questionsif ( rl.get_rcon_id() == mRconId) {continue;}// Ignore other Master. They should not be waiting.if ( rl.isMaster()) {continue;}//// A slave that is 'Paused' is not processing a package// It will have been included in the build-set, so we must wait for// the pause to be removed.//// A slave that is 'Waiting' must also have no BuildFile present// This signals that the buildfile has been taken by the slave//if (! (rl.isAt(BuildState.DB_WAITING) && !rl.hasBuildFile() ) ){// A non-waiting slave has been foundwaitList.append(" ");waitList.append(rl.get_rcon_id());}}if ( waitList.length() > 0 ){// One or more slaves still processing ...if ( mUnitTest.compareTo("unit test coordinate slave threads") == 0 ){Thread.currentThread().interrupt();}else{mLogger.warn("Waiting 3 seconds for slaves:" + waitList.toString() );Thread.sleep(3000);mLogger.info("run sleep returned");}continue;}else{// All slaves have completed there tasksbreak;}}if ( mUnitTest.compareTo("unit test coordinate slave threads") == 0 ){throw new ExitException();}//---------------------------------------------------------------// Publish Package Build Results// Indicate to RM that we are publishing// Ensure that all build machines have placed a marker in dpkg_archive// Save updated build files into VCS//mLogger.error("run changing run level to PUBLISHING for rcon_id " + mRconId);mPhase.setPhase("Publishing");mRunLevel.persist(mReleaseManager, mRconId, BuildState.DB_PUBLISHING);mLogger.info("run calling deliverChange on AbtPublish");deliverChange("AbtPublish", true, false);//---------------------------------------------------------------// TearDown the build workspace// Remove the build workspace// Save build log files// Don't update the Reporting Information as it will null out// some essential information that has been collected.//mLogger.info("run calling deliverChange on AbtTearDown");mPhase.setPhase("AbtTearDown");deliverChange("AbtTearDown", false, true);// Delete the build file now// This will prevent any chance of multiply publishing a package versiondeleteBuildFile();//---------------------------------------------------------------// Update Release Manager (publish to Release Manager)//mPhase.setPhase("Update Release Manager");if ( mReporting.packageName != null ){// A dummy build file did not apply// Publishing is done outside ant by design// Do ALL Release Manager work outside ant (and JATS)boolean buildErrorOccurred = true;boolean publishError = true;try{// Create a build instance entry in the Release Manager database// Create one per build// mReporting.packageVersionId will have been updated if this is a new package-version// Save the build idmPhase.setPhase("Create Build Instance");mReporting.buildId = mReleaseManager.createBuildInstance(mRtagId,mReporting.packageVersionId,mReporting.buildReason );//// Locate and collect the unit test results//mPhase.setPhase("Parse Unit Test Results");BuildTestResults btr = new BuildTestResults(System.getenv("GBE_DPKG"), mReporting, mLogger);mReleaseManager.insertTestResults(mReporting.buildId, btr);if ( mReporting.isaTestBuild ){// Test Build// Notify the user requesting the build// Build Failure emails have already been sent//mLogger.info("run completing test build on " + mReporting.packageName + mReporting.packageExtension);mPhase.setPhase("Report Test Build Complete");Package p = mRippleEngine.findPackage(mReporting.packageName + mReporting.packageExtension);if (p != ReleaseManager.NULL_PACKAGE){mLogger.error("run calling p.completeTestBuild");p.completeTestBuild(mRippleEngine, mReporting.isFullyPublished);}//// Determine the total time taken by the build// Done as late a possible in the process// Does not include planning time// Does not handle case where build cycle is broken// Only record for successful tests. Don't want to record short build times on failure// in particular if the package has been released. Would be OK for a WIP, but how to identify theseif ( mReporting.isFullyPublished){int buildDuration = mBuildDuration.toIntSecs();mLogger.error("Build Duration:" + buildDuration);mReleaseManager.updateBuildDuration(mReporting.packageVersionId, buildDuration);}mPhase.setPhase("Update Build Instance for Test");mReleaseManager.updateBuildInstance(mReporting.buildId, 0, mReporting.isFullyPublished ? BuildResult.Complete :BuildResult.BuildError );// ... and clean up the mess in the archivepublishError = false;throw new Exception();}// Package not fully published// An error has occurred in the building of the packageif ( !mReporting.isFullyPublished ){mLogger.info("run build error occurred on " + mReporting.packageName + mReporting.packageVersion);// ... and clean up the mess in the archivepublishError = false;throw new Exception();}//---------------------------------------------------------------// Package was successfully built and published into dpkg_archive by all daemons// Update Release Manager - formally release the package// Insert Package Metrics// Insert Unit Test ResultsbuildErrorOccurred = false;//// Detect Badly formed VCS label returned by the builder// Have had zero length values returned// Treat as a publishing error//if ( mReporting.newVcsTag == null || mReporting.newVcsTag.isEmpty() ){mLogger.error("run package not labelled in Version Control on " + mReporting.packageName + mReporting.packageVersion);mReporting.errMsg = "Error publishing to Version Control System"; // Max 50 Charactersthrow new Exception();}// Publish to release manager// On error mReporting.errMsg will contain error string// Publishing errors only affect the current package//mLogger.error("run calling mReleaseManager.autoMakeRelease");mPhase.setPhase("autoMakeRelease");publishError = mReleaseManager.autoMakeRelease(mReporting);if ( publishError ){mLogger.error("autoMakeRelease publishError: " + mReporting.errMsg);throw new Exception();}//// Determine the total time taken by the build// Done as late a possible in the process// Does not include planning time// Does not handle case where build cycle is broken//int buildDuration = mBuildDuration.toIntSecs();mLogger.error("Build Duration:" + buildDuration);mReleaseManager.updateBuildDuration(mReporting.packageVersionId, buildDuration);//// Update the Build Instance information// If this has been a ripple then the pv_id of the build needs to be associated with// the newly created versionmPhase.setPhase("Update Build Instance");mReleaseManager.updateBuildInstance(mReporting.buildId, mReporting.packageVersionId, BuildResult.Complete );//// Insert package metrics into Release Manager// Metrics have been placed in a file in a format suitable for RM//mReporting.errMsg = "Error publishing Package Metrics"; // Max 50 CharactersmPhase.setPhase("Insert Metrics");FileInputStream abtmetrics = new FileInputStream( String.valueOf(mRtagId) + "abtmetrics.txt" );DataInputStream din = new DataInputStream( abtmetrics );InputStreamReader isr = new InputStreamReader( din );BufferedReader br = new BufferedReader( isr );String metrics = br.readLine();mLogger.warn( "execute read metrics string " + metrics );br.close();isr.close();din.close();mLogger.error("run calling mReleaseManager.insertPackageMetrics");mReleaseManager.insertPackageMetrics(mRtagId, mReporting.packageName, mReporting.packageExtension, metrics );//// Info reporting// Send an email to track build system usage - may be noise//mReporting.errMsg = null;mLogger.error("run calling emailBuildComplete");mPhase.setPhase("Post Build Email");emailPostBuild(mRippleEngine, false);//---------------------------------------------------------------// All done// The package has been built and releasedmPhase.setPhase("Build Complete");}catch( Exception e){// Some form of error has occurred. Types include// Test build (Treated as an error so that dpkg_archive gets removed)// buildErrorOccurred - True// publishError - False// Build Error - Package was not fully published to dpkg_archive// buildErrorOccurred - True// publishError - False//// Badly formed VCS tag (Error Publishing to Version Control)// buildErrorOccurred - False// publishError - True// Error Publishing to Release Manager// buildErrorOccurred - False// publishError - True// Error inserting package metrics// buildErrorOccurred - False// publishError - True// Error inserting unit test results// buildErrorOccurred - False// publishError - True//// Uses// buildErrorOccurred - Package not in dpkg_archive as expected (or was a test build)// Email should have been sent to the user// publishError - Could not publish to Release Manager// Post build error. Email has not been sent to user// Takes precedence over buildErrorOccurred// Insert generic message if we don't have one// Should only occur under unexpected conditionsif (mReporting.errMsg == null || mReporting.errMsg.isEmpty()){mReporting.errMsg = "Error publishing to Release Manager";mReporting.errMsgDetail = mPhase.sText;}// Indicate some form of build errorif ( ! mReporting.isaTestBuild ){mPhase.setPhase("Update Build Instance");mReleaseManager.updateBuildInstance(mReporting.buildId, 0, publishError ? BuildResult.SystemError :BuildResult.BuildError );}//// Delete the dpkg_archive entry - the package is a dud//mPhase.setPhase("Delete package from archive");mLogger.error("Delete package from archive");deletePackageVersion();// A publishing error. The error occurred, after the build, in the RM publishing// We won't have a log file, but we will have a rootCause// Exclude package from the build// Unless its a test build (handled within mReleaseManager.excludeFromBuild)//if ( publishError ){mLogger.error("an error occurred publishing to Version Control or Release Manager");// Post Build Error// eMails have not been sent by the ABT class as the error has occurred outside of that processmPhase.setPhase("Post Build Failure Email");emailPostBuild(mRippleEngine, true);mLogger.error("run calling excludeFromBuild");mPhase.setPhase("Excluded from build");mReleaseManager.excludeFromBuild(false,mReporting.packageVersionId,null,mReporting.rtagId,null,mReporting.errMsg,null,false, mReporting.isaTestBuild);}else if ( buildErrorOccurred ){// Build Error Occurred// Package has not been built on all configured platforms// Under normal circumstances, this root cause will be masked by a previously reported error// Under abnormal circumstances amidst build activity such as:// - slave build machine reboots// - slave build machines with a read only file system// The rootCause is required to keep the end user informed//// Use excludeFromBuild such that this call will not supersede existing entries// In this manner the log file/cause provided here will only be used if we haven't// already excluded this package from the build// If we have a log file, then provide it to the user, else insert a generic messagemReporting.errMsg = (mReporting.buildFailureLogFile == null) ? "Buildtool env error occurred. Attempt build again" : null;mLogger.error("run calling excludeFromBuild");mPhase.setPhase("Excluded from build");mReleaseManager.excludeFromBuild(false,mReporting.packageVersionId,mReporting.packageVersion,mReporting.rtagId,null,mReporting.errMsg,mReporting.buildFailureLogFile,false, mReporting.isaTestBuild);}else{// Not a buildError and not a publishing Error// Don't know how we got here - I don't think we can// Will cause an indefinite pause !throw new Exception("an error occurred publishing to Version Control or Release Manager");}} // End Catch}if ( mUnitTest.compareTo("unit test coordinate slave threads") == 0 ){throw new ExitException();}mLogger.info("run coordinate slave threads returned");}//---------------------------------------------------------------// Start of a new build cycle// Determine if the daemon has been paused.// Set RunLevel.IDLE (within allowedToProceed)//mLogger.error("run calling allowedToProceed");mPhase.setPhase("allowedToProceed");mReleaseManager.discardVersion();mReleaseManager.clearCurrentPackageBeingBuilt(mRconId);allowedToProceed(true);mLogger.info("run allowedToProceed returned");if ( mUnitTest.compareTo("unit test allowed to proceed") == 0 ){throw new ExitException();}// Set up mReleaseConfigCollection// This will be used when creating a build filemLogger.error("Collect fresh MetaData");mPhase.setPhase("collectMetaData");mRippleEngine.collectMetaData();//---------------------------------------------------------------// Plan the next build// Snap current Release Sequence Number// Determine what is to be built// Generate build filesmLogger.error("run calling planRelease");mPhase.setPhase("Plan Release", 600);mReleaseSeqNum = mReleaseManager.queryReleaseSeqNum(mRtagId, mRconId, BuildDaemon.mHostname);mRippleEngine.planRelease(!mLastBuildWasBenign);// get the build file from the ripple enginemPhase.setPhase("Get Build File", 10);mMasterBuildFile = mRippleEngine.getFirstBuildFileContent();}}//---------------------------------------------------------------// Check that we are good to proceed with the build// Check environment// Ensure we have enough disk space//mLogger.error("run calling checkEnvironment");mPhase.setPhase("checkEnvironment");checkEnvironment();mLogger.info("run checkEnvironment returned");if ( mUnitTest.startsWith("unit test check environment") ){throw new ExitException();}mLogger.info("run generated build files");// Start of a build cycle. Set new log file to capture entire build logmPhase.setPhase("Start Build Cycle");flagStartBuildCycle();mBuildDuration.reset();mReporting.resetData();mRunLevel.persist(mReleaseManager, mRconId, BuildState.DB_ACTIVE);mReleaseManager.setCurrentPackageBeingBuilt(mRconId, mMasterBuildFile.mPkgId, mMasterBuildFile.mPvId);// Save the buildfilemLogger.error("Save buildfile");mPhase.setPhase("Save buildfile");saveBuildFile(mMasterBuildFile.content);// Quick Test to validate the provided Version Control tag/labelmPhase.setPhase("Validate Version Tag");if ( deliverChange("AbtTestPath", true, false) ){//---------------------------------------------------------------// Notify all slaves that there is work to be done// This is done by publishing a buildfile for them to consume// A NonGeneric build will be done with a real build file on all machines// Otherwise do a Dummy build on all Machines//mPhase.setPhase("Notify slaves of work", 10);if ( mMasterBuildFile.state != BuildFileState.NonGeneric){// publish a dummy build file for either generic or dummy cases// this results in the slave not running antmLogger.error("run calling publishBuildFile on dummy");mMasterBuildFile.content = mDummyBuildFileContent;}else{// publish the build filemLogger.error("run calling publishBuildFile");}mReleaseManager.publishBuildFile(mRtagId, mMasterBuildFile);if ( mUnitTest.compareTo("unit test generate build files") == 0 ){throw new ExitException();}mLogger.info("run changed run levels");//---------------------------------------------------------------// Build the package// Setup the workspace// Build the package - deliver change to product baseline//mLogger.error("Build Package " + mReporting.packageName + "_" + mReporting.packageVersion);mLogger.info("run calling setViewUp");mPhase.setPhase("Setup View for build");deliverChange("AbtSetUp", true, false);mLogger.info("run calling deliverChange - the actual build");mPhase.setPhase("Build Package");deliverChange(null, true, false);mLogger.info("run deliverChange returned");}mLastBuildWasBenign = false;if ( mMasterBuildFile.state == BuildFileState.Dummy){// no build requirement, so let things settlemLogger.warn("run no build requirement, so let things settle");mLastBuildWasBenign = true;}}//---------------------------------------------------------------// Build loop exception handling (only)//catch( SQLException e ){// Oracle connection issues// Request a prolonged sleep at the start of the build loopmLogger.error("run oracle connection issues");mException = true;}catch( ExitException e ){mLogger.warn("run ExitException");exit = true;}catch( InterruptedException e ){mLogger.warn("run InterruptedException");Thread.currentThread().interrupt();}catch (BuildSystemException e) {// BuildSystem exception - a (possibly) correctable error// Flag as recoverable// - Can't create XML build file// -//mLogger.error("run BuildSystemException");mPhase.setPhase("BuildSystemException indefinitePause");notifyIndefinitePause(e.getMessage());mReleaseManager.indefinitePause(false);mException = true;mRecoverable = true;}catch( BuildToolException e){// Buildtool exception - an uncorrectable error// - Can't parse XML build file//mLogger.error("run BuildToolException");mPhase.setPhase("BuildToolException indefinitePause");notifyIndefinitePause(e.getMessage());mReleaseManager.indefinitePause(false);mException = true;}catch( Exception e ){// Uncaptured exception// These are not good. Current mechanism to handle this condition is:// 1) Email build system administrator// 2) Pause the build system//mLogger.error("run indefinitePause " + e.toString());// DEVI 51366 force sleep at beginning of while loopmException = true;String cause = e.getMessage();if ( cause == null || cause.compareTo("null") == 0 ){cause = "Internal Error: " + e.toString();}try{// notify first// many reasons for indefinite pause, including database related, so do database lastmRecoverable = false;if ( cause.compareTo(Package.mRecoverable) == 0 ){mRecoverable = true;}mPhase.setPhase("Notify indefinitePause");notifyIndefinitePause(cause);mReleaseManager.indefinitePause(mRecoverable);mReleaseManager.updateBuildInstance(mReporting.buildId, 0, BuildResult.SystemError );}catch( Exception f ){mLogger.error("run indefinitePause failed");}}}try {mReleaseManager.clearCurrentPackageBeingBuilt(mRconId);} catch (Exception e) {mLogger.error("Exception in clearCurrentPackageBeingBuilt:" + e.getMessage());}mPhase.setPhase("Exit Thread");}/** Delete a specified version of a package from dpkg_archive* Used during error processing to cleanup packages that did not build correctly** The package-version is in dpkg_archive. This may be on a remote server* Execute the deletion command on the remote server as it will be a lot faster* for large packages.** Ensure that the file system cache is cleared before we attempt to look* for the directory*/private void deletePackageVersion(){// Setup a Remote Execution environment// The logging interface is is hard partRemoteExecution mRemoteExecution = new RemoteExecution(new LogOutput(){@Overridepublic void data(String message) {mLogger.info(message);}@Overridepublic void fatal(String message) {mLogger.error(message);}@Overridepublic void info(String message) {mLogger.info(message);}});// Execute the remote command// This is a best effort deletion - can't really handle conditions// where the command fails.CommandBuilder cmd = new CommandBuilder();cmd.add("sudo","-n", "-u", "buildadm", "/home/releasem/sbin/jatsTool", "assemble_dpkg");cmd.add("-DeleteVersion");cmd.add(!ReleaseManager.getUseMutex(), "-testArchive");cmd.add("'-pname=" + mReporting.packageName + "'");cmd.add("'-pversion=" + mReporting.packageVersion + "'");mLogger.error("run remote command to delete " + mReporting.packageName +", "+ mReporting.packageVersion);int rv = mRemoteExecution.execute(cmd.toString());mLogger.error("run remote command to delete. Returned " + rv);// Belt and Braces// Ensure that the package-version has been deleted,// and if not then try to delete it the slow way.//// Note: Need to flush NFS cache before we check existenceString dpkgArchiveEntry = utilities.catDir(Package.mGbeDpkg, mReporting.packageName, mReporting.packageVersion);File dpkgArchiveEntryFile = utilities.freshFile(dpkgArchiveEntry);mLogger.info("run checking existence of " + dpkgArchiveEntry);if ( dpkgArchiveEntryFile.exists() ){if ( dpkgArchiveEntryFile.isDirectory() ){mLogger.error("run calling deleteDirectory on " + dpkgArchiveEntryFile);deleteDirectory(dpkgArchiveEntryFile);}}}/** Sends email notification and mark the successful completion of a build* Used simply to keep an email trail of build operations*/public void emailPostBuild( RippleEngine rippleEngine, boolean isaError ){mLogger.debug("emailPostBuild");String subject;String emailTo;String mailBody = "";if (isaError){subject = "BUILD FAILURE on package " + mReporting.packageName + " " + mReporting.packageVersion;emailTo = mReporting.packageOwners;mailBody="An error occured within the build system after the package had been built<p><hr>";}else{subject = "BUILD SUCCESS on package " + mReporting.packageName + " " + mReporting.packageVersion;emailTo = rippleEngine.getMailSender();}// Cleanup the dependency string// Expecting: 'junit','3.7.0.cots';'ant-ejbdoclet','1.0000.cr'String indent = "<br> ";String depends = mReporting.packageDepends.replace("','", " ");depends = depends.replace("';'",indent);depends = depends.replace("'","");// Cleanup the build standard string// Expecting win32: jats all;linux_i386: jats prodString buildStandards = mReporting.packageBuildInfo;buildStandards = buildStandards.replace(";",indent);mailBody += "Release : " + rippleEngine.mBaselineName + "<br>" +"Package : " + mReporting.packageName + "<br>" +"Version : " + mReporting.packageVersion + "<br>";if (isaError){mailBody += "Cause : " + mReporting.errMsg + "<br>";if (mReporting.errMsgDetail != null && mReporting.errMsgDetail.length() > 0){mailBody += "Detail : " + mReporting.errMsgDetail + "<br>";}}mailBody += "RM Link : " + CreateUrls.generateRmUrl(mRtagId,mReporting.packageVersionId) + "<br>" +"PVID : " + mReporting.packageVersionId + "<br>" +"VcsTag : " + mReporting.newVcsTag + "<br>" +"Ripple : " + (mReporting.isRipple ? "Yes" : "No") + "<br>" +"Dependencies : " + indent + depends + "<br>" +"Build Standards : " + indent + buildStandards;mailBody += "<p><hr>";try{Smtpsend.send( rippleEngine.getMailServer(), // mailServerrippleEngine.getMailSender(), // sourceemailTo, // target - send to package owners, or myselfrippleEngine.getMailSender(), // ccnull, // bccsubject, // subjectmailBody, // bodynull // attachment);}catch( Exception e ){mLogger.warn("Email Failure: emailPostBuild:" + e.getMessage());}mLogger.debug("emailPostBuild. Returning");}/**returns 'M'*/protected char getMode(){mLogger.debug("getMode");return 'M';}}