/* * Sun Public License * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is available at http://www.sun.com/ * * The Original Code is the SLAMD Distributed Load Generation Engine. * The Initial Developer of the Original Code is Neil A. Wilson. * Portions created by Neil A. Wilson are Copyright (C) 2004-2010. * Some preexisting portions Copyright (C) 2002-2006 Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): Neil A. Wilson */ package com.slamd.tools; import java.io.BufferedInputStream; import java.io.BufferedWriter; import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.util.Date; import java.util.Properties; import com.slamd.job.UnableToRunException; import com.slamd.client.ClientMessageWriter; import com.slamd.client.ClientSideJob; import com.slamd.common.Constants; import com.slamd.common.JobClassLoader; import com.slamd.common.SLAMDException; import com.slamd.job.JobClass; import com.slamd.parameter.BooleanParameter; import com.slamd.parameter.FloatParameter; import com.slamd.parameter.IntegerParameter; import com.slamd.parameter.InvalidValueException; import com.slamd.parameter.LabelParameter; import com.slamd.parameter.MultiChoiceParameter; import com.slamd.parameter.Parameter; import com.slamd.parameter.ParameterList; import com.slamd.parameter.PlaceholderParameter; import com.slamd.stat.StatTracker; /** * This class defines a SLAMD client that may execute SLAMD jobs without the * need for a SLAMD server to be running. It allows a single client to execute * a job from the command line with output sent to standard output and standard * error. The configuration for the job should be specified in a configuration * file. * * * @author Neil A. Wilson */ public class StandaloneClient implements ClientMessageWriter { // Indicates whether the data collected by each thread should be aggregated // before reporting to the end user. private boolean aggregateThreadData; // Indicates whether the program is operating in a mode in which it will // generate a sample configuration file private boolean generateConfigMode; // Indicates whether verbose job information should be written out. private boolean verboseMode; // Indicates whether to use the custom job class loader. private boolean useCustomClassLoader; // The length of time (in seconds) to use as the statistics collection // interval. private int collectionInterval; // The maximum length of time (in seconds) that the job should be allowed to // run. private int duration; // The number of threads to use when running the job. private int numThreads; // The job class used to get parameter information. private JobClass jobInstance; // The writer to which output will be sent. private PrintWriter outputWriter; // The information read from the configuration file. private Properties configProperties; // The location of the job class files. private String classPath; // The path to the configuration file. private String configFile; // The end-of-line character for this platform. private String eol = Constants.EOL; // The name of the file to which all output should be written. private String outputFile; // The name of the job class to run. private String jobClassName; /** * Create a new standalone client instance and pass all the arguments to it. * * @param args The set of arguments provided on the command line. */ public static void main(String[] args) { new StandaloneClient(args); } /** * Create a new standalone client instance, process the configuration, and * run the specified job. * * @param args The set of arguments provided on the command line. */ public StandaloneClient(String[] args) { // Set default values for all the parameters. aggregateThreadData = false; generateConfigMode = false; verboseMode = false; useCustomClassLoader = true; classPath = null; configFile = null; outputFile = null; duration = 0; collectionInterval = Constants.DEFAULT_COLLECTION_INTERVAL; numThreads = 1; // Parse the command-line arguments for (int i=0; i < args.length; i++) { if (args[i].equals("-f")) { configFile = args[++i]; } else if (args[i].equals("-o")) { outputFile = args[++i]; } else if (args[i].equals("-c")) { classPath = args[++i]; } else if (args[i].equals("-g")) { generateConfigMode = true; jobClassName = args[++i]; } else if (args[i].equals("-d")) { try { duration = Integer.parseInt(args[++i]); } catch (NumberFormatException nfe) { System.err.println("ERROR: Duration must be an integer"); displayUsage(); System.exit(1); } } else if (args[i].equals("-i")) { try { collectionInterval = Integer.parseInt(args[++i]); } catch (NumberFormatException nfe) { System.err.println("ERROR: Statistics collection interval must be " + "an integer"); displayUsage(); System.exit(1); } } else if (args[i].equals("-t")) { try { numThreads = Integer.parseInt(args[++i]); } catch (NumberFormatException nfe) { System.err.println("ERROR: Number of threads must be an integer"); displayUsage(); System.exit(1); } } else if (args[i].equals("-a")) { aggregateThreadData = true; } else if (args[i].equals("-L")) { useCustomClassLoader = false; } else if (args[i].equals("-v")) { verboseMode = true; } else if (args[i].equals("-h")) { displayUsage(); System.exit(0); } else { System.err.println("ERROR: Unrecognized parameter " + args[i]); displayUsage(); System.exit(1); } } // Make sure that the required parameters were given values if (configFile == null) { System.err.println("ERROR: No job configuration file specified"); displayUsage(); System.exit(1); } // If there is an output file specified, then open it for writing. // Otherwise, send it to standard out. if (outputFile == null) { outputWriter = new PrintWriter(System.out); } else { try { outputWriter = new PrintWriter(new FileWriter(outputFile, true)); } catch (IOException ioe) { System.err.println("ERROR: Unable to open output file " + outputFile + " for writing: " + ioe); System.exit(1); } } // If the program is to run in "generate config" mode, then do that now if (generateConfigMode) { if (jobClassName == null) { System.err.println("ERROR: No job class name provided"); displayUsage(); System.exit(1); } generateConfigFile(); System.exit(0); } // Parse the configuration file. configProperties = new Properties(); try { BufferedInputStream inputStream = new BufferedInputStream(new FileInputStream(configFile)); configProperties.load(inputStream); inputStream.close(); } catch (IOException ioe) { System.err.println("ERROR: Could not read configuration file: " + ioe); System.exit(1); } // Load and verify the job class. jobClassName = configProperties.getProperty(Constants.SERVLET_PARAM_JOB_CLASS); if (jobClassName == null) { System.err.println("ERROR: Configuration file " + configFile + "does not specify a job class using the parameter " + Constants.SERVLET_PARAM_JOB_CLASS); System.exit(1); } if (useCustomClassLoader) { JobClassLoader jobClassLoader = new JobClassLoader(getClass().getClassLoader(), classPath); try { jobInstance = jobClassLoader.getJobClass(jobClassName); } catch (SLAMDException se) { System.err.println(se.getMessage()); System.exit(1); } } else { try { Class jobClass = Constants.classForName(jobClassName); jobInstance = (JobClass) jobClass.newInstance(); } catch (Exception e) { System.err.println("Unable to load job class " + jobClassName + ": " + e); System.exit(1); } } // Get the parameter stubs from the job class and provide them with values // from the config file. Parameter[] jobParams = jobInstance.getParameterStubs().getParameters(); for (int i=0; i < jobParams.length; i++) { String valueString = configProperties.getProperty(jobParams[i].getName()); try { jobParams[i].setValueFromString(valueString); } catch (InvalidValueException ive) { System.err.println("Invalid value specified for job parameter " + jobParams[i].getName() + " -- " + ive.getMessage()); System.exit(1); } } ParameterList parameters = new ParameterList(jobParams); // Make sure that the job as a whole is acceptable. try { jobInstance.validateJobInfo(1, numThreads, 0, new Date(), null, duration, collectionInterval, parameters); } catch (InvalidValueException ive) { System.err.println("Job parameter validation failed -- " + ive.getMessage()); System.exit(1); } // Perform the job-level initialization that is normally done by the // server-side job. try { jobInstance.initializeJob(parameters); } catch (UnableToRunException utre) { System.err.println("Job initialization failed -- " + utre.getMessage()); System.exit(1); } // Create a new client side job to actually do the processing. ClientSideJob clientJob = new ClientSideJob(this, classPath, jobClassName, numThreads, duration, collectionInterval, parameters, useCustomClassLoader, false, null); // Start the job and wait for it to complete. writeMessage("Starting the " + jobInstance.getJobName() + " job...."); clientJob.startAndWait(); // Run the per-job finalization. jobInstance.finalizeJob(); // Print out information about the job. writeMessage("Job Processing Complete"); int jobDuration = clientJob.getActualDuration(); writeMessage("Job Processing Time: " + jobDuration + " seconds"); StatTracker[] statTrackers = clientJob.getStatTrackers(aggregateThreadData); for (int i=0; i < statTrackers.length; i++) { outputWriter.println(); outputWriter.println(statTrackers[i].getDisplayName() + " -- Thread " + statTrackers[i].getThreadID()); if (verboseMode) { outputWriter.println(statTrackers[i].getDetailString()); } else { outputWriter.println(statTrackers[i].getSummaryString()); } } outputWriter.flush(); outputWriter.close(); } /** * Generates a configuration file that can be used to run the specified * job. */ public void generateConfigFile() { if (useCustomClassLoader) { try { JobClassLoader classLoader = new JobClassLoader(getClass().getClassLoader(), classPath); jobInstance = classLoader.getJobClass(jobClassName); } catch (SLAMDException se) { System.err.println(se.getMessage()); System.exit(1); } } else { try { Class jobClass = Constants.classForName(jobClassName); jobInstance = (JobClass) jobClass.newInstance(); } catch (Exception e) { System.err.println("Unable to load job class " + jobClassName + ": " + e); System.exit(1); } } try { // Create the file writer BufferedWriter writer = new BufferedWriter(new FileWriter(configFile)); // Write the header writer.write("#################################################" + "##############################" + eol); writeComment(writer, "SLAMD Standalone Job Configuration File"); writeComment(writer, ""); writeComment(writer, "Job Name: " + jobInstance.getJobName()); writeComment(writer, ""); writeComment(writer, "Job Class: " + jobClassName); writeComment(writer, ""); writeComment(writer, "Job Description: " + jobInstance.getShortDescription()); writeComment(writer, ""); writer.write("#################################################" + "##############################" + eol); writer.write(eol+eol); // Write the class name parameter writeComment(writer, "Job Class"); writeComment(writer, "The Java class used to execute this job."); writer.write(Constants.SERVLET_PARAM_JOB_CLASS + '=' + jobClassName + eol); writer.write(eol+eol); // Iterate through each of the job-specific parameters and write out the // comments and configuration info for each Parameter[] jobParams = jobInstance.getParameterStubs().getParameters(); for (int i=0; i < jobParams.length; i++) { // If the parameter is a placeholder, then don't do anything with it. if ((jobParams[i] instanceof PlaceholderParameter) || (jobParams[i] instanceof LabelParameter)) { continue; } // Write the display name and description. writeComment(writer, jobParams[i].getDisplayName()); writeComment(writer, jobParams[i].getDescription()); // Indicate whether the parameter is required or optional. if (jobParams[i].isRequired()) { writeComment(writer, "This parameter is required."); } else { writeComment(writer, "This parameter is optional."); } // If there are any constraints on the value, then state what they are. if (jobParams[i] instanceof BooleanParameter) { writeComment(writer, "The value must be either \"true\" or \"false\"."); } else if (jobParams[i] instanceof FloatParameter) { FloatParameter fp = (FloatParameter) jobParams[i]; if (fp.hasLowerBound()) { if (fp.hasUpperBound()) { writeComment(writer, "The value must be between " + fp.getLowerBound() + " and " + fp.getUpperBound() + '.'); } else { writeComment(writer, "The value must be greater than or equal to " + fp.getLowerBound() + '.'); } } else if (fp.hasUpperBound()) { writeComment(writer, "The value must be less than or equal to " + fp.getUpperBound() + '.'); } } else if (jobParams[i] instanceof IntegerParameter) { IntegerParameter ip = (IntegerParameter) jobParams[i]; if (ip.hasLowerBound()) { if (ip.hasUpperBound()) { writeComment(writer, "The value must be between " + ip.getLowerBound() + " and " + ip.getUpperBound() + '.'); } else { writeComment(writer, "The value must be greater than or equal to" + ip.getLowerBound() + '.'); } } else if (ip.hasUpperBound()) { writeComment(writer, "The value must be less than or equal to " + ip.getUpperBound() + '.'); } } else if (jobParams[i] instanceof MultiChoiceParameter) { writeComment(writer, "The value must be one of the following:"); String[] choices = ((MultiChoiceParameter) jobParams[i]).getChoices(); for (int j=0; j < choices.length; j++) { writeComment(writer, " - \"" + choices[j] + '"'); } } // Finally, write the parameter name and possibly a value writer.write(jobParams[i].getName() + '=' + jobParams[i].getValueString() + eol); writer.write(eol+eol); } // Close the file and be done. writer.flush(); writer.close(); writeMessage("A sample configuration file was written to " + configFile + '.'); writeMessage("You may need to edit the file before it can be used to " + "run the job."); } catch (IOException ioe) { System.err.println("ERROR writing sample configuration file " + configFile + ": " + ioe); } } /** * Writes the provided comment to the generated configuration file, wrapping * long lines if necessary. * * @param writer The buffered writer used to write information to the * configuration file. * @param comment The comment to be written to the file. * * @throws IOException If a problem occurs while writing the comment to the * configuration file. */ public void writeComment(BufferedWriter writer, String comment) throws IOException { if (comment.length() > 75) { String indentStr = ""; while (comment.length() > (75-indentStr.length())) { int spacePos = comment.lastIndexOf(' ', 75); if (spacePos < 0) { spacePos = comment.indexOf(' '); if (spacePos < 0) { writer.write("# " + indentStr + comment + eol); return; } else { writer.write("# " + indentStr + comment.substring(0, spacePos) + eol); comment = comment.substring(spacePos+1); } } else { writer.write("# " + indentStr + comment.substring(0, spacePos) + eol); comment = comment.substring(spacePos+1); } indentStr = " "; } writer.write("# " + indentStr + comment + eol); } else { writer.write("# " + comment + eol); } } /** * Writes usage information for this program to standard error. */ public void displayUsage() { System.err.println( "Usage: java StandaloneClient [options]" + eol + " where [options] include:" + eol + "-f {file} -- Specifies the configuration file for the job to run" + eol + "-o {file} -- Specifies the output file to use" + eol + "-g {class} -- Specifies that a configuration file should be created" + eol + " for the specified job class rather than running a job" + eol + "-d {value} -- Specifies the maximum length of time the job should run" + eol + "-i {value} -- Specifies the length of time in seconds that should be " + eol + " used as the statistics collection interval" + eol + "-t {value} -- Specifies the number of threads that should be used" + eol + "-c {path} -- Specifies the location of the job class files" + eol + "-a -- Specifies that data from each of the threads should be " + eol + " aggregated before displaying the results" + eol + "-L -- Disables the custom job class loader" + eol + "-v -- Specifies the verbose job information should be logged" + eol + "-h -- Displays usage information for this program" ); } /** * Writes information logged during job processing to standard output. * * @param message The message to be written to standard output. */ public void writeMessage(String message) { outputWriter.println(message); outputWriter.flush(); } /** * Writes verbose information logged during job processing to standard * output (if verbose logging is enabled). * * @param message The message to be written to standard output. */ public void writeVerbose(String message) { if (verboseMode) { outputWriter.println(message); } } /** * Indicates whether the message writer is using verbose mode and therefore * will display messages written with the <CODE>writeVerbose</CODE> method. * * @return <CODE>true</CODE> if the message writer is using verbose mode, or * <CODE>false</CODE> if not. */ public boolean usingVerboseMode() { return verboseMode; } }