/*==========================================================================*\ | $Id: PluginRunner.java,v 1.3 2011/05/30 13:42:50 aallowat Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2009 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU Affero General Public License as published | by the Free Software Foundation; either version 3 of the License, or | (at your option) any later version. | | Web-CAT is distributed in the hope that it will be useful, | but WITHOUT ANY WARRANTY; without even the implied warranty of | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | GNU General Public License for more details. | | You should have received a copy of the GNU Affero General Public License | along with Web-CAT; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package org.webcat.plugintester; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.Arrays; import java.util.Date; import java.util.Properties; import java.util.Set; import org.apache.commons.configuration.ConfigurationException; import org.webcat.plugintester.util.PluginConfiguration; import org.webcat.plugintester.util.WebCATConfiguration; //------------------------------------------------------------------------- /** * Performs the task of running the plugins based on the settings specified by * the user. * * @author Tony Allevato * @version $Id: PluginRunner.java,v 1.3 2011/05/30 13:42:50 aallowat Exp $ */ public class PluginRunner { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Initializes a new PluginRunner with the specified application * configuration and current settings. * * @param wcConfig the Web-CAT application configuration * @param settings the current settings specified by the user */ public PluginRunner(WebCATConfiguration wcConfig, Properties settings) { webcatConfig = wcConfig; currentSettings = settings; } //~ Methods ............................................................... // ---------------------------------------------------------- /** * Prepares the result area and runs the plugins. */ public void run() { createResultsDirectory(); String[] envp = webcatConfig.envp(); System.out.println("Running plug-ins with the following environment:"); for (String env : envp) { System.out.println(" " + env); } String pluginsString = currentSettings.getProperty( AppConstants.PROP_LAST_PLUGIN_PATHS); String[] plugins = pluginsString.split(File.pathSeparator); int index = 1; for (String plugin : plugins) { // Write properties that are plugin-specific before each one runs. File gradingPropsFile = new File(resultsDir, "grading.properties"); Properties gradingProps = loadProperties(gradingPropsFile); gradingProps.setProperty("scriptHome", plugin); if (!gradingProps.containsKey("timeout")) { gradingProps.setProperty("timeout", "30"); } saveProperties(gradingProps, gradingPropsFile); // Run the plugin. runPlugin(index, plugin); index++; } } // ---------------------------------------------------------- /** * Runs the plugin in the specified folder. * * @param plugin the path to the plugin to execute */ private void runPlugin(int index, String plugin) { String command = getCommandLine(index, plugin); String[] envp = webcatConfig.envp(); String[] cmdArray = null; Process process = null; // Tack on the command shell prefix to the beginning, quoting the // whole argument sequence if necessary. String shell = cmdShell(); if (shell != null && shell.length() > 0) { if (shell.charAt(shell.length() - 1) == '"') { cmdArray = shell.split("\\s+"); cmdArray[cmdArray.length - 1] = command; } else { command = shell + command; } } try { System.out.println("Executing the following argument list:"); System.out.println(Arrays.toString(cmdArray)); if (cmdArray != null) { process = Runtime.getRuntime().exec(cmdArray, envp, resultsDir); } else { process = Runtime.getRuntime().exec(command, envp, resultsDir); } process.waitFor(); } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } // ---------------------------------------------------------- /** * Gets the command shell that should be used to execute the grading * plugins. * * @return the command shell */ private String cmdShell() { if (cmdShellCache == null) { String os = System.getProperty("os.name"); if (os != null && os.indexOf("Windows") >= 0) { cmdShellCache = "cmd /c"; } else { cmdShellCache = "sh -c \""; } int len = cmdShellCache.length(); if (len > 0 && cmdShellCache.charAt(len - 1) != ' ' && cmdShellCache.charAt(len - 1) != '"') { cmdShellCache += " "; } } return cmdShellCache; } // ---------------------------------------------------------- /** * Constructs the command line that should be used to execute the specified * plugin, based on the values in the plugin's config.plist file. * * @param pluginDir the directory that contains the plugin * @return the command line to use to execute the plugin */ private String getCommandLine(int index, String pluginDir) { PluginConfiguration config = null; try { config = new PluginConfiguration(new File(pluginDir)); } catch (ConfigurationException e) { // Do nothing. } String executable = "\"" + pluginDir + "/" + config.getRootProperty("executable") + "\""; String interpreterPrefix = config.getRootProperty("interpreter..prefix"); if (interpreterPrefix != null) { executable = interpreterPrefix + " " + executable; } String cmdLine; cmdLine = substituteApplicationProperties(executable); cmdLine += " "; cmdLine += "\"" + new File(resultsDir, "grading.properties").getAbsolutePath() + "\""; File stdoutFile = new File(resultsDir, "" + index + "-stdout.txt"); File stderrFile = new File(resultsDir, "" + index + "-stderr.txt"); cmdLine += " 1> \"" + stdoutFile.getAbsolutePath() + "\""; cmdLine += " 2> \"" + stderrFile.getAbsolutePath() + "\""; return cmdLine; } // ---------------------------------------------------------- /** * Creates the "Results" directory that will contain the plugins' output * (or clears it if it already exists). */ private void createResultsDirectory() { String subPath = currentSettings.getProperty( AppConstants.PROP_LAST_SUBMISSION_PATH); File subDir = new File(subPath); File parentDir = subDir.getParentFile(); resultsDir = new File(parentDir, "Results"); if (!resultsDir.exists()) { resultsDir.mkdirs(); } else { deleteContents(resultsDir); } // Create the grading.properties for the submission by first using // hard-coded default values, then overriding those with values // specified by the user's test.properties (located in the parent // directory of the submission files), and then finally by those values // entered directly into the user interface. Properties gradingProps = new Properties(); initializeDefaultGradingProperties(gradingProps); Properties testProps = loadProperties( new File(parentDir, "test.properties")); gradingProps.putAll(testProps); Properties userProps = getPropertiesFromString( currentSettings.getProperty( AppConstants.PROP_USER_GRADING_PROPERTIES)); gradingProps.putAll(userProps); saveProperties(gradingProps, new File(resultsDir, "grading.properties")); } // ---------------------------------------------------------- /** * Initializes the specified properties objects with hard-coded default * values for the grading.properties file. * * @param props the Properties object to initialize */ private void initializeDefaultGradingProperties(Properties props) { props.setProperty("numReports", "0"); props.setProperty("max.score.correctness", "50"); props.setProperty("max.score.tools", "50"); props.setProperty("userName", "dummyUser"); String subPath = currentSettings.getProperty( AppConstants.PROP_LAST_SUBMISSION_PATH); File dataDir = new File(new File(subPath).getParentFile(), "ScriptData"); props.setProperty("workingDir", subPath); props.setProperty("resultDir", resultsDir.getAbsolutePath()); props.setProperty("scriptData", dataDir.getAbsolutePath()); props.setProperty("course", "DummyCourse"); props.setProperty("CRN", "DummyCRN"); props.setProperty("assignment", "DummyAssignment"); Date now = new Date(); props.setProperty("dueDateTimestamp", Long.toString(now.getTime() + 86400000)); props.setProperty("submissionTimestamp", Long.toString(now.getTime())); props.setProperty("submissionNo", "1"); String webcatHome = currentSettings.getProperty( AppConstants.PROP_WEBCAT_HOME); props.setProperty("frameworksBaseURL", new File(webcatHome, "WEB-INF/Web-CAT.woa/Contents/Library/Frameworks"). getAbsolutePath()); Properties appProps = webcatConfig.applicationProperties(); for (Object key : appProps.keySet()) { props.setProperty((String) key, appProps.getProperty((String) key)); } } // ---------------------------------------------------------- /** * Deletes the contents of the specified directory (recursively deleting * any child directories as well). The directory itself will <b>not</b> be * deleted; it will be empty after this operation. * * @param dir the directory that will be emptied */ private void deleteContents(File dir) { File[] entries = dir.listFiles(); for(File entry : entries) { if (entry.isDirectory()) { deleteContents(entry); } entry.delete(); } } // ---------------------------------------------------------- /** * Substitutes values from the Web-CAT application configuration into * plugin properties that use the ${var} syntax for variable substitution. * * @param value the value that will have values substituted into it * @return the new value with substitutions made */ private String substituteApplicationProperties(String value) { final String REFERENCE_START = "${"; final String REFERENCE_END = "}"; // Get the index of the first constant, if any. StringBuffer buffer = new StringBuffer(value.length()); int beginIndex = 0; int startName = value.indexOf(REFERENCE_START, beginIndex); while (startName >= 0) { int endName = value.indexOf(REFERENCE_END, startName); if (endName == -1) { // Terminating symbol not found; Return the value as is. break; } if (startName > beginIndex) { buffer.append(value.substring(beginIndex, startName)); beginIndex = startName; } String constName = value.substring(startName + REFERENCE_START.length(), endName); String constValue = webcatConfig.applicationProperties().getProperty(constName); if (constValue == null) { // Property name not found. buffer.append(value.substring(beginIndex, endName + REFERENCE_END.length())); } else { // Insert the constant value into the original property value. buffer.append(constValue); } beginIndex = endName + REFERENCE_END.length(); // Look for the next constant startName = value.indexOf(REFERENCE_START, beginIndex); } buffer.append(value.substring(beginIndex, value.length())); return buffer.toString(); } // ---------------------------------------------------------- /** * Creates a Properties object that contains the key-value pairs in the * specified string. * * @param propString a String containing the textual form of an ASCII * properties file * @return the Properties object containing the properties */ private Properties getPropertiesFromString(String propString) { Properties props = new Properties(); if (propString != null) { ByteArrayInputStream stream = new ByteArrayInputStream( propString.getBytes()); try { props.load(stream); } catch (IOException e) { // Do nothing. } } return props; } // ---------------------------------------------------------- /** * A helper method to load a Properties object from a file, ignoring * exceptions. * * @param file the file from which to load the properties * @return a Properties object containing the properties from the file, or * empty if there was an error */ private Properties loadProperties(File file) { Properties props = new Properties(); FileInputStream stream = null; try { stream = new FileInputStream(file); props.load(stream); } catch (IOException e) { // Do nothing. } finally { try { if (stream != null) { stream.close(); } } catch (IOException e2) { // Do nothing. } } return props; } // ---------------------------------------------------------- /** * A helper method to save a Properties object to a file, ignoring * exceptions. * * @param props the Properties object to save * @param file the file to which to save the properties */ private void saveProperties(Properties props, File file) { FileOutputStream stream = null; try { stream = new FileOutputStream(file); props.store(stream, null); } catch (IOException e) { // Do nothing. } finally { try { if (stream != null) { stream.close(); } } catch (IOException e2) { // Do nothing. } } } //~ Instance/static variables ............................................. private WebCATConfiguration webcatConfig; private Properties currentSettings; private String cmdShellCache; private File resultsDir; }