/*==========================================================================*\
| $Id: GraderQueueProcessor.java,v 1.19 2012/05/09 16:33:08 stedwar2 Exp $
|*-------------------------------------------------------------------------*|
| Copyright (C) 2006-2012 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.grader;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
import org.webcat.archives.ArchiveManager;
import org.webcat.archives.IWritableContainer;
import org.webcat.core.Application;
import org.webcat.core.FileUtilities;
import org.webcat.core.MutableDictionary;
import org.webcat.core.RepositoryEntryRef;
import org.webcat.core.Status;
import org.webcat.core.User;
import org.webcat.core.WCProperties;
import org.webcat.grader.messaging.AdminReportsForSubmissionMessage;
import org.webcat.grader.messaging.GraderKilledMessage;
import org.webcat.grader.messaging.SubmissionSuspendedMessage;
import org.webcat.woextensions.WCEC;
import org.webcat.woextensions.WCFetchSpecification;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSDictionary;
import com.webobjects.foundation.NSKeyValueCoding;
import com.webobjects.foundation.NSMutableArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation.NSMutableSet;
import com.webobjects.foundation.NSTimestamp;
import er.extensions.eof.ERXConstant;
// -------------------------------------------------------------------------
/**
* This is the main grader processor class that performs the
* compile/reference execution/execute/grade cycle on a student submission
* job.
*
* @author Amit Kulkarni
* @author Last changed by $Author: stedwar2 $
* @version $Revision: 1.19 $, $Date: 2012/05/09 16:33:08 $
*/
public class GraderQueueProcessor
extends Thread
{
//~ Constructors ..........................................................
// ----------------------------------------------------------
/**
* Default constructor
*
* @param queue the queue to operate on
*/
public GraderQueueProcessor(GraderQueue queue)
{
super("GraderQueueProcessor");
this.queue = queue;
}
//~ Methods ...............................................................
// ----------------------------------------------------------
/**
* The actual thread of execution
*/
public void run()
{
while (true)
{
try
{
editingContext = WCEC.newEditingContext();
editingContext.setSharedEditingContext(null);
editingContext.lock();
// Clear discarded jobs
NSArray<EnqueuedJob> jobList = null;
try
{
jobList = EnqueuedJob.objectsMatchingQualifier(
editingContext,
EnqueuedJob.submission.isNull());
}
catch (Exception e)
{
log.info("error fetching jobs: ", e);
jobList = EnqueuedJob.objectsMatchingQualifier(
editingContext,
EnqueuedJob.submission.isNull());
}
if (jobList != null && jobList.size() > 0)
{
// delete all the jobs without submissions
for (EnqueuedJob job : jobList)
{
job.delete();
}
editingContext.saveChanges();
log.debug(
jobList.count() + " submissionless jobs retrieved");
}
try
{
jobList = EnqueuedJob.objectsMatchingQualifier(
editingContext,
EnqueuedJob.discarded.isTrue());
}
catch (Exception e)
{
log.info("error fetching jobs: ", e);
jobList = EnqueuedJob.objectsMatchingQualifier(
editingContext,
EnqueuedJob.discarded.isTrue());
}
if (jobList != null && jobList.size() > 0)
{
// delete all the discarded jobs
for (EnqueuedJob job : jobList)
{
job.delete();
}
editingContext.saveChanges();
log.debug(
jobList.count() + " discarded jobs retrieved");
}
// Look for real jobs
jobList = null;
try
{
jobList = EnqueuedJob.objectsMatchingQualifier(
editingContext,
EnqueuedJob.paused.isFalse().and(
EnqueuedJob.regrading.isFalse()),
EnqueuedJob.submission.dot(Submission.submitTime)
.ascs());
}
catch (Exception e)
{
log.info("error fetching jobs: ", e);
jobList = EnqueuedJob.objectsMatchingQualifier(
editingContext,
EnqueuedJob.paused.isFalse().and(
EnqueuedJob.regrading.isFalse()),
EnqueuedJob.submission.dot(Submission.submitTime)
.ascs());
}
if (log.isDebugEnabled())
{
log.debug((jobList == null ? 0 : jobList.count())
+ " fresh jobs retrieved");
}
// If no real jobs, look for regrading jobs
if (jobList == null || jobList.size() == 0)
{
// Just look for one regrading job, so we can then
// try our hand at other jobs again.
WCFetchSpecification<EnqueuedJob> regrading =
new WCFetchSpecification<EnqueuedJob>(
EnqueuedJob.ENTITY_NAME,
EnqueuedJob.paused.isFalse().and(
EnqueuedJob.regrading.isTrue()),
EnqueuedJob.queueTime.ascs());
regrading.setFetchLimit(1);
try
{
jobList = EnqueuedJob.objectsWithFetchSpecification(
editingContext, regrading);
}
catch (Exception e)
{
log.info("error fetching jobs: ", e);
jobList = EnqueuedJob.objectsWithFetchSpecification(
editingContext, regrading);
}
if (log.isDebugEnabled())
{
log.debug((jobList == null ? 0 : jobList.count())
+ " regrading jobs retrieved");
}
}
// This test is just to make sure the compiler knows it
// isn't null, even though the try/catch above ensures it
if (jobList != null && jobList.size() > 0)
{
for (EnqueuedJob job : jobList)
{
NSTimestamp startProcessing = new NSTimestamp();
Submission submission = job.submission();
if (submission == null)
{
log.error("null submission in enqueued job: "
+ "deleting");
editingContext.deleteObject(job);
}
else if (job.discarded())
{
log.debug("discarded job: deleting");
editingContext.deleteObject(job);
}
else if (submission.assignmentOffering() == null)
{
log.error("submission with null assignment "
+ "offering in enqueued job: deleting");
editingContext.deleteObject(job);
}
else
{
if (submission.assignmentOffering()
.gradingSuspended())
{
log.warn(
"Suspending job " + submission.dirName());
job.setPaused(true);
}
else
{
log.info("processing submission "
+ submission.dirName());
processJobWithProtection(job);
NSTimestamp now = new NSTimestamp();
if (job.queueTime() != null)
{
mostRecentJobWait = now.getTime()
- job.queueTime().getTime();
}
else
{
mostRecentJobWait = now.getTime()
- submission.submitTime().getTime();
}
{
long processingTime = now.getTime() -
startProcessing.getTime();
totalWaitForJobs += processingTime;
jobsCountedWithWaits++;
}
}
}
// Now save all the changes
{
// assignment offering could have changed because
// of a fault, so save any changes before
// forcing it out of editing context cache
try
{
editingContext.saveChanges();
}
catch (IllegalStateException e)
{
log.error("Exception trying to save "
+ "grading results", e);
// Database inconsistency problem
try
{
editingContext.unlock();
}
catch (Throwable ee)
{
log.error("Exception trying to unlock "
+ "context for disposal", ee);
}
try
{
editingContext.dispose();
}
catch (Throwable ee)
{
log.error("Exception trying to dispose "
+ "context", ee);
}
editingContext = null;
queue.enqueue(null);
break;
}
}
// Only process one regrading job before looking for
// more regular submissions.
if (job.regrading())
{
queue.enqueue(null);
break;
}
}
}
else
{
// Wait for more jobs to show up
log.debug("waiting for a token");
// We don't need the return value, since it is just null:
editingContext.unlock();
queue.getJobToken();
editingContext.lock();
log.debug("token received.");
}
}
catch (Throwable e)
{
log.fatal("Job queue: Error processing student submission",
e);
try
{
new GraderKilledMessage(e).send();
}
catch (Throwable t)
{
log.fatal("Error sending GraderKilledMessage", t);
}
log.fatal("Attempting to restart job queue processing.");
// ERXApplication.erxApplication().killInstance();
}
finally
{
if (editingContext != null)
{
try
{
editingContext.unlock();
}
catch (Throwable t)
{
log.fatal("Error unlocking editing context", t);
}
try
{
editingContext.dispose();
}
catch (Throwable t)
{
log.fatal("Error disposing editing context", t);
}
editingContext = null;
}
}
}
}
// ----------------------------------------------------------
/**
* This function processes the job and performs the stages that
* are necessary. It guards against any exceptions while
* processing the job.
*
* @param job the job to process
*/
void processJobWithProtection(EnqueuedJob job)
{
try
{
processJob(job);
}
catch (Exception e)
{
technicalFault(job, "while processing job", e, null);
}
}
// ----------------------------------------------------------
/**
* This function processes the job and performs the stages that
* are necessary.
*
* @param job the job to process
*/
void processJob(EnqueuedJob job)
{
// boolean status = false;
// String extendedErrorInfo = null;
double scoreAdjustments = 0.0;
double correctnessScore = 0.0;
double toolScore = 0.0;
jobCount++;
log.info("Processing job " + jobCount + " for: "
+ job.submission().user().userName());
// Set up the working directory first
try
{
prepareWorkingDirectory(job);
}
catch (Exception e)
{
technicalFault(
job, "while preparing the working directory", e, null);
return;
}
// Get the steps in grading this assignment
NSArray<Step> steps = Step.order.asc().sorted(
job.submission().assignmentOffering().assignment().steps());
// Set up the properties to pass to execution scripts
WCProperties gradingProperties;
File gradingPropertiesFile = job.submission().gradingPropertiesFile();
try
{
gradingProperties =
job.submission().createInitialGradingPropertiesFile();
}
catch (IOException e)
{
technicalFault(job,
"could not create the initial grading.properties file: "
+ gradingPropertiesFile.getAbsolutePath() + ": "
+ e.getMessage(),
null,
gradingPropertiesFile.getParentFile());
return;
}
for (Step thisStep : steps)
{
@SuppressWarnings("unchecked")
NSDictionary<String, Object> exports =
(NSDictionary<String, Object>)thisStep.gradingPlugin()
.configDescription().valueForKey("pipelineExports");
if (exports != null)
{
gradingProperties.addPropertiesFromDictionary(exports);
}
}
writeOutSavedGradingProperties(job, gradingProperties);
for (int stepNo = 0; stepNo < steps.count(); stepNo++)
{
Step thisStep = steps.objectAtIndex(stepNo);
executeStep(
job, thisStep, gradingProperties, gradingPropertiesFile);
if (faultOccurredInStep)
{
// technicalFault was already called by executeStep()
// to pause the assignment and send e-mail to admins,
// so just bail
return;
}
// check the properties to update score and halt, if necessary
if (gradingProperties.getProperty("score.adjustment") != null)
{
scoreAdjustments +=
gradingProperties.doubleForKey("score.adjustment");
gradingProperties.remove("score.adjustment");
}
if (gradingProperties.getProperty("score.correctness") != null)
{
correctnessScore =
gradingProperties.doubleForKey("score.correctness");
}
if (gradingProperties.getProperty("score.tools") != null)
{
toolScore = gradingProperties.doubleForKey("score.tools");
}
if (gradingProperties.getProperty("halt") != null)
{
if (gradingProperties.booleanForKey("halt"))
{
gradingProperties.remove("halt");
log.error("halt requested in step "
+ thisStep + "\n\tfor job " + job);
job.setPaused(true);
return;
}
}
if (gradingProperties.getProperty("canProceed") != null)
{
if (!gradingProperties.booleanForKey("canProceed"))
{
break;
}
}
if (gradingProperties.getProperty("halt.all") != null)
{
if (gradingProperties.booleanForKey("halt.all"))
{
gradingProperties.remove("halt.all");
log.error("halt requested for all jobs in step "
+ thisStep + "\n\tfor job " + job);
job.setPaused(true);
AssignmentOffering assignment =
job.submission().assignmentOffering();
assignment.setGradingSuspended(true);
return;
}
}
if (timeoutOccurredInStep)
{
technicalFault(job,
"script time limit exceeded in stage " + (stepNo + 1),
null,
gradingPropertiesFile.getParentFile());
return;
}
}
// Clean up the working directory.
FileUtilities.deleteDirectory(job.workingDirName());
generateFinalReport(job,
gradingProperties,
correctnessScore,
toolScore);
log.info("Finished job " + jobCount);
}
// ----------------------------------------------------------
/**
* Gets the location where checked out files should be stored, creating it
* if desired.
*
* @param job the grader job to associate the checkout with
* @param clean true to clean out the checkout location and create it fresh,
* or false to just return the path (which may or may not exist)
* @return the path to the checkout location
*/
private File repositoryCheckoutLocation(EnqueuedJob job, boolean clean)
{
File root = new File(org.webcat.core.Application
.configurationProperties().getProperty("grader.workarea"),
"_GraderCheckout");
File location = new File(root, job.id().toString());
if (clean)
{
if (location.exists())
{
FileUtilities.deleteDirectory(location);
}
location.mkdirs();
}
return location;
}
// ----------------------------------------------------------
/**
* Creates and cleans the working directory, if necessary, fills
* it with the student's submission, and creates the reporting
* directory.
*
* @param job the job to operate on
* @throws Exception if it occurs during this stage
*/
private void prepareWorkingDirectory(EnqueuedJob job)
throws java.io.IOException
{
// Create the working compilation directory for the user
File workingDir = new File(job.workingDirName());
if (workingDir.exists())
{
FileUtilities.deleteDirectory(workingDir);
}
workingDir.mkdirs();
// Copy the user's submission to the working dir
Submission submission = job.submission();
org.webcat.archives.ArchiveManager.getInstance()
.unpack(workingDir, submission.file());
// Create the grading output directory
File graderLD = new File(submission.resultDirName());
if (graderLD.exists())
{
FileUtilities.deleteDirectory(graderLD);
}
graderLD.mkdirs();
}
// ----------------------------------------------------------
/**
* Checks out the specified file into the temporary space used for grading.
*
* @param fileInfo a String representing an old-style absolute path to the
* old script-data area, or a dictionary containing the repository info
* for a file
* @return a File object that points to the location of the checked out
* file
* @throws IOException if an I/O error occurred
*/
private File checkOutRepositoryFiles(EnqueuedJob job, Object fileInfo)
throws IOException
{
RepositoryEntryRef entryRef = null;
if (fileInfo instanceof String)
{
entryRef = RepositoryEntryRef.fromOldStylePath((String) fileInfo);
}
else if (fileInfo instanceof NSDictionary<?, ?>)
{
@SuppressWarnings("unchecked")
NSDictionary<String, Object> fileInfoDict =
(NSDictionary<String, Object>)fileInfo;
entryRef = RepositoryEntryRef.fromDictionary(fileInfoDict);
}
if (entryRef != null)
{
entryRef.resolve(editingContext);
File checkoutLocation = repositoryCheckoutLocation(job, false);
File repoDir = new File(entryRef.repositoryName());
File filePath = new File(repoDir, entryRef.path());
File containerPath = new File(checkoutLocation, filePath.getPath());
if (!entryRef.isDirectory())
{
containerPath = containerPath.getParentFile();
}
containerPath.mkdirs();
IWritableContainer container =
ArchiveManager.getInstance().createWritableContainer(
containerPath, false);
entryRef.repository().copyItemToContainer(
entryRef.objectId(), entryRef.name(), container);
return filePath;
}
else
{
return null;
}
}
// ----------------------------------------------------------
/**
* Adds settings from a configuration settings dictionary to the properties
* file for grading, checking out any required files into temporary storage
* if necessary.
*
* @param job the job
* @param config the configuration settings to add
* @param properties the properties file to add the settings to
* @param fileSettings a dictionary whose keys represent settings that are
* intended to be file paths
* @param onlyIfNotDefined true to only add the property if it doesn't
* already exist
*/
private void addConfigSettingsToProperties(
EnqueuedJob job,
MutableDictionary config,
WCProperties properties,
MutableDictionary fileSettings,
boolean onlyIfNotDefined)
throws IOException
{
@SuppressWarnings("unchecked")
NSArray<String> keys = config.allKeys();
for (String property : keys)
{
if (!onlyIfNotDefined || !properties.containsKey(property))
{
Object value = config.objectForKey(property);
if (fileSettings.containsKey(property))
{
// Check out the file or directory, write it to a temporary
// location, and then write the value of the property to
// point to that location.
File location = checkOutRepositoryFiles(job, value);
if (location != null)
{
properties.setProperty(property, location.getPath());
}
}
else
{
properties.setProperty(property, value.toString());
}
}
}
}
// ----------------------------------------------------------
/**
* Execute a single step in the grading process. Communication
* between the Grader subsystem and the script being executed for
* this step is accomplished by using a properties file. The set
* of properties to communicate and the file to pass to store them
* in (and place as the script's command line arg) are passed in
* to this method. This method will set the faultOccurredInStep
* data member if an internal failure occurred in processing this
* step. Alternatively, it will set the timeoutOccurredInStep data
* member if the time limit was exceeded for this step.
*
* @param job the job being processed
* @param step the grading step to execute
* @param properties the cumulative properties settings to use
* @param propertiesFile the file to record the properties in
*/
// * @throws IOException if one occurs
private void executeStep(
EnqueuedJob job,
Step step,
WCProperties properties,
File propertiesFile)
{
faultOccurredInStep = false;
timeoutOccurredInStep = false;
log.debug("step " + step.order() + ": "
+ step.gradingPlugin().mainFilePath());
try
{
step.gradingPlugin().reinitializeConfigAttributesIfNecessary();
log.debug("creating properties file");
MutableDictionary fileProps =
step.gradingPlugin().fileConfigSettings();
// Create a clean checkout location at the beginning of each step.
File checkoutLocation = repositoryCheckoutLocation(job, true);
properties.setProperty("pluginName", step.gradingPlugin().name());
properties.setProperty("pluginData",
checkoutLocation.getAbsolutePath());
properties.setProperty("scriptData", // legacy
checkoutLocation.getAbsolutePath());
// Re-write the properties file
properties.addPropertiesFromDictionaryIfNotDefined(Application
.wcApplication().subsystemManager().pluginProperties());
addConfigSettingsToProperties(job,
step.gradingPlugin().globalConfigSettings(),
properties, fileProps, true);
addConfigSettingsToProperties(job,
step.gradingPlugin().defaultConfigSettings(),
properties, fileProps, true);
if (step.config() != null)
{
addConfigSettingsToProperties(job,
step.config().configSettings(),
properties, fileProps, false);
}
addConfigSettingsToProperties(
job, step.configSettings(), properties, fileProps, false);
/*properties.addPropertiesFromDictionaryIfNotDefined(
step.gradingPlugin().globalConfigSettings() );
properties.addPropertiesFromDictionaryIfNotDefined(
step.gradingPlugin().defaultConfigSettings() );
if ( step.config() != null )
{
properties.addPropertiesFromDictionary(
step.config().configSettings() );
}
properties.addPropertiesFromDictionary(
step.configSettings() );*/
properties.setProperty(
"userName", job.submission().user().userName());
properties.setProperty(
"workingDir", job.workingDirName());
properties.setProperty(
"resultDir", job.submission().resultDirName());
properties.setProperty(
"pluginHome", step.gradingPlugin().dirName());
properties.setProperty(
"scriptHome", // legacy
step.gradingPlugin().dirName());
properties.setProperty("pluginResourcePrefix",
"${pluginResource:" + step.gradingPlugin().name() + "}");
properties.setProperty(
"timeout", Integer.toString(step.effectiveEndToEndTimeout()));
properties.setProperty("timeoutForOneRun",
Integer.toString(step.effectiveTimeoutForOneRun()));
properties.setProperty("course",
job.submission().assignmentOffering().courseOffering()
.course().deptNumber());
{
String crn = job.submission().assignmentOffering()
.courseOffering().crn();
properties.setProperty("CRN",
(crn == null) ? "null" : crn);
}
properties.setProperty("semester",
job.submission().assignmentOffering().courseOffering()
.semester().toString());
properties.setProperty("assignment",
job.submission().assignmentOffering().assignment()
.name() );
properties.setProperty("dueDateTimestamp", Long.toString(
job.submission().assignmentOffering().dueDate().getTime()));
properties.setProperty("lateDeadlineTimestamp",
Long.toString(job.submission().assignmentOffering()
.lateDeadline().getTime()));
properties.setProperty("submissionTimestamp",
Long.toString(job.submission().submitTime().getTime()));
properties.setProperty("jobQueueTimestamp",
Long.toString(job.queueTime().getTime()));
properties.setProperty("jobQueuedAfterLateDeadline",
Boolean.toString(job.queueTime().after(
job.submission().assignmentOffering().lateDeadline())));
properties.setProperty("jobIsRegrading",
Boolean.toString(job.regrading()));
properties.setProperty("submissionNo",
Integer.toString(job.submission().submitNumber()));
properties.setProperty("frameworksBaseURL",
Application.application().frameworksBaseURL());
BufferedOutputStream out = new BufferedOutputStream(
new FileOutputStream(propertiesFile));
properties.store(
out, "Web-CAT grader script configuration properties");
out.close();
File stdout = new File(job.submission().resultDirName(),
"" + step.order() + "-stdout.txt");
File stderr = new File(job.submission().resultDirName(),
"" + step.order() + "-stderr.txt");
// execute the script
log.debug("executing script");
timeoutOccurredInStep = step.execute(
propertiesFile.getPath(),
new File(job.workingDirName()),
stdout,
stderr);
if (stderr.length() != 0)
{
technicalFault(job, "stderr output was produced by " + step,
null, propertiesFile.getParentFile());
return;
}
else
{
stderr.delete();
}
if (stdout.length() == 0)
{
stdout.delete();
}
else
{
log.warn(
"Script produced stdout output in " + stdout.getPath());
}
// Now reload the properties file
log.debug("re-loading properties from file");
BufferedInputStream in = new BufferedInputStream(
new FileInputStream(propertiesFile));
properties.clear();
properties.load(in);
in.close();
// log.debug( "properties:\n" + properties );
}
catch (Exception e)
{
technicalFault(
job, "in stage " + step, e, propertiesFile.getParentFile());
}
finally
{
// Clean up the checked out files.
File checkoutLocation = repositoryCheckoutLocation(job, false);
if (checkoutLocation.exists())
{
FileUtilities.deleteDirectory(checkoutLocation);
}
}
}
// ----------------------------------------------------------
/**
* Collects all the reports in the given properties file, splitting
* them into those that are inline, those that are downloadable,
* and those to send to the administrator.
*
* @param job the job
* @param properties the properties describing the reports
* @param submissionResult the result to link downloadable reports to
* @param inlineStudentReports the array where inline report files are
* added (as InlineFile objects)
* @param inlineStudentReports the array where inline report files
* intended for course staff are added (as
* InlineFile objects)
* @param adminReports the Vector where admin-targeted report files
* are added (as string file names)
*/
void collectReports(
EnqueuedJob job,
WCProperties properties,
SubmissionResult submissionResult,
NSMutableArray<InlineFile> inlineStudentReports,
NSMutableArray<InlineFile> inlineStaffReports,
List<File> adminReports)
{
File parentDir = new File(job.submission().resultDirName());
// First, collect all the report fragments
int numReports = properties.intForKey("numReports");
for (int i = 1; i <= numReports; i++)
{
// First, extract the attributes
String attributeBase = "report" + i + ".";
String fileName = properties.getProperty(attributeBase + "file");
if (fileName == null)
{
continue;
}
String mimeType = properties.getProperty(
attributeBase + "mimeType", "text/plain");
boolean inline = properties.booleanForKeyWithDefault(
attributeBase + "inline", true);
boolean border =
properties.booleanForKey(attributeBase + "border");
int styleVersion = properties.intForKeyWithDefault(
attributeBase + "styleVersion", 0);
String to =
properties.getProperty(attributeBase + "to", "student");
boolean toStudent =
to.equalsIgnoreCase("student")
|| to.equalsIgnoreCase("both")
|| to.equalsIgnoreCase("all");
boolean toStaff =
to.equalsIgnoreCase("staff")
|| to.equalsIgnoreCase("instructor")
|| to.equalsIgnoreCase("both")
|| to.equalsIgnoreCase("all");
boolean toAdmin =
to.equalsIgnoreCase("admin")
|| to.equalsIgnoreCase("administrator")
|| to.equalsIgnoreCase("all");
// Now, populate the lists
if (toStudent)
{
if (inline)
{
int pos = properties.intForKeyWithDefault(
attributeBase + "position",
inlineStudentReports.size());
inlineStudentReports.add(pos, new InlineFile(
parentDir, fileName, mimeType, border));
int currentVersion =
submissionResult.studentReportStyleVersion();
if (submissionResult.studentReportStyleVersionRaw() == null
|| styleVersion < currentVersion)
{
submissionResult.setStudentReportStyleVersion(
styleVersion);
}
}
else
{
ResultFile thisFile = new ResultFile();
editingContext.insertObject(thisFile);
thisFile.setFileName(fileName);
thisFile.setLabel(
properties.getProperty(attributeBase + "label"));
thisFile.setMimeType(mimeType);
thisFile.setSubmissionResultRelationship(submissionResult);
}
}
if (toStaff)
{
if (inline)
{
inlineStaffReports.addObject(new InlineFile(
parentDir, fileName, mimeType, border));
int currentVersion =
submissionResult.staffReportStyleVersion();
if (submissionResult.staffReportStyleVersionRaw() == null
|| styleVersion < currentVersion)
{
submissionResult.setStaffReportStyleVersion(
styleVersion);
}
}
else
{
// FIXME!
// ResultFile thisFile = new ResultFile();
// editingContext.insertObject( thisFile );
// thisFile.setFileName( fileName );
// thisFile.setLabel(
// properties.getProperty( attributeBase + "label" ) );
// thisFile.setMimeType( mimeType );
// thisFile.setSubmissionStaffResultRelationship(
// submissionResult );
toAdmin = true;
}
}
if (toAdmin)
{
adminReports.add(new File(
job.submission().resultDirName() + "/" + fileName));
}
}
// Second, collect all the stats markup files
String statElementsLabel = properties.getProperty("statElementsLabel");
if (statElementsLabel != null)
{
submissionResult.setStatElementsLabel(statElementsLabel);
}
numReports = properties.intForKey("numCodeMarkups");
for (int i = 1; i <= numReports; i++)
{
String attributeBase = "codeMarkup" + i + ".";
SubmissionFileStats stats = new SubmissionFileStats();
editingContext.insertObject(stats);
stats.setClassName(
properties.getProperty(attributeBase + "className"));
stats.setPkgName(
properties.getProperty(attributeBase + "pkgName"));
stats.setSourceFileNameRaw(
properties.getProperty(attributeBase + "sourceFileName"));
stats.setMarkupFileNameRaw(
properties.getProperty(attributeBase + "markupFileName"));
// The tags are zero or more space-delimited strings that describe
// what this file's role is (such as if it is a test case). Note
// that we pad the tag string with a space on each end if necessary
// so that tags can always be searched for in the database using a
// LIKE qualifier such as "% tag %". This way tags that are infixes
// of other tags will not be erroneously detected.
String tags = properties.getProperty(attributeBase + "tags");
if (tags != null)
{
if (tags.length() == 0)
{
tags = null;
}
else
{
if (!tags.startsWith(" "))
{
tags = " " + tags;
}
if (!tags.endsWith(" "))
{
tags = tags + " ";
}
}
}
stats.setTags(tags);
String attr = properties.getProperty(attributeBase + "loc");
if (attr != null)
{
stats.setLocRaw(ERXConstant.integerForString(attr));
}
attr = properties.getProperty(attributeBase + "ncloc");
if (attr != null)
{
stats.setNclocRaw(ERXConstant.integerForString(attr));
}
attr = properties.getProperty(attributeBase + "deductions");
if (attr != null)
{
stats.setDeductionsRaw(new Double(attr));
}
attr = properties.getProperty(attributeBase + "remarks");
if (attr != null)
{
stats.setRemarksRaw(ERXConstant.integerForString(attr));
}
attr = properties.getProperty(attributeBase + "conditionals");
if (attr != null)
{
stats.setConditionalsRaw(ERXConstant.integerForString(attr));
}
attr = properties.getProperty(
attributeBase + "conditionalsCovered");
if (attr != null)
{
stats.setConditionalsCoveredRaw(
ERXConstant.integerForString(attr));
}
attr = properties.getProperty(attributeBase + "statements");
if (attr != null)
{
stats.setStatementsRaw(ERXConstant.integerForString(attr));
}
attr = properties.getProperty(
attributeBase + "statementsCovered");
if (attr != null)
{
stats.setStatementsCoveredRaw(
ERXConstant.integerForString(attr));
}
attr = properties.getProperty(attributeBase + "methods");
if (attr != null)
{
stats.setMethodsRaw(ERXConstant.integerForString(attr));
}
attr = properties.getProperty(
attributeBase + "methodsCovered");
if (attr != null)
{
stats.setMethodsCoveredRaw(
ERXConstant.integerForString(attr));
}
attr = properties.getProperty(attributeBase + "elements");
if (attr != null)
{
stats.setElementsRaw(ERXConstant.integerForString(attr));
}
attr = properties.getProperty(
attributeBase + "elementsCovered");
if (attr != null)
{
stats.setElementsCoveredRaw(ERXConstant.integerForString(attr));
}
stats.setSubmissionResultRelationship(submissionResult);
}
}
// ----------------------------------------------------------
protected class InlineFile
extends File
{
public InlineFile(
File parent, String child, String type, boolean useBorder)
{
super(parent, child);
mimeType = type;
border = useBorder;
}
public String mimeType = null;
public boolean border = false;
}
// ----------------------------------------------------------
/**
* Generates the final report, records the submission results,
* and deletes the job.
*
* @param job the finished job
*/
void generateFinalReport(
EnqueuedJob job,
WCProperties properties,
double correctnessScore,
double toolScore)
{
SubmissionResult submissionResult = new SubmissionResult();
submissionResult.setCorrectnessScore(correctnessScore);
submissionResult.setToolScore(toolScore);
editingContext.insertObject(submissionResult);
NSMutableArray<InlineFile> inlineStudentReports =
new NSMutableArray<InlineFile>();
NSMutableArray<InlineFile> inlineStaffReports =
new NSMutableArray<InlineFile>();
List<File> adminReports = new ArrayList<File>();
collectReports(
job,
properties,
submissionResult,
inlineStudentReports,
inlineStaffReports,
adminReports);
generateCompositeResultFile(
new File(job.submission().resultDirName(),
SubmissionResult.resultFileName()),
inlineStudentReports);
generateCompositeResultFile(
new File(job.submission().resultDirName(),
SubmissionResult.staffResultFileName()),
inlineStaffReports);
// 2009-02-04 (AJA): create result blobs
processSavedProperties(job, submissionResult, properties);
editingContext.saveChanges();
boolean wasRegraded = job.regrading();
submissionResult.addToSubmissionsRelationship(job.submission());
if (!job.submission().assignmentOffering().assignment().usesTAScore())
{
submissionResult.setStatus(Status.CHECK);
}
if (job.submission().assignmentOffering().assignment()
.submissionProfile().allowPartners())
{
connectPartnersFromProperty(job, properties.getProperty(
"grader.potentialpartners"));
}
job.submission().setIsSubmissionForGradingIfNecessary();
try
{
if (job.submission() != null)
{
for (Submission partneredSubmission :
job.submission().partneredSubmissions())
{
partneredSubmission.setResultRelationship(submissionResult);
// Force it to be marked as a partner submission as
// a stop-gap until we find the real problem.
partneredSubmission.setPartnerLink(true);
partneredSubmission.setIsSubmissionForGradingIfNecessary();
}
}
}
catch (Exception e)
{
log.error("Unable to link partner submisisons", e);
}
job.setSubmissionRelationship(null);
editingContext.deleteObject(job);
editingContext.saveChanges();
// The following line self-commits any changes it makes
submissionResult.setAsMostRecentIfNecessary();
// Send out e-mail messages to student
//
NSTimestamp limitTime = submissionResult.submission().submitTime()
.timestampByAddingGregorianUnits(
0, // years
0, // months
0, // days
0, // hours
emailWaitMinutes,
0 // seconds
);
if (limitTime.before(new NSTimestamp())) // compare against now
{
String msg = "is now available";
if (wasRegraded)
{
msg += ".\nA course staff member requested that it be "
+ "regraded";
}
submissionResult.submission().emailNotificationToStudent(msg);
}
// Send out admin reports, if any
//
if (adminReports.size() > 0)
{
Submission submission = submissionResult.submission();
new AdminReportsForSubmissionMessage(submission, adminReports)
.send();
}
}
// ----------------------------------------------------------
/**
* Parses the specified space-separated list of partner candidates and
* pairs the submission with any of those usernames who are in the same
* course.
*
* @param job the job with the primary submission
* @param candidateString the space-separated list of partner candidates
*/
private void connectPartnersFromProperty(EnqueuedJob job,
String candidateString)
{
NSMutableSet<User> potentialPartners = new NSMutableSet<User>();
if (candidateString != null)
{
String[] candidates = candidateString.split("\\s+");
for (String candidate : candidates)
{
candidate = candidate.trim();
if (candidate.length() > 0)
{
User user = User.userWithDomainAndName(
job.editingContext(),
job.submission().user().authenticationDomain(),
candidate);
if (user != null)
{
potentialPartners.addObject(user);
}
}
}
}
// Now that we've found the set of people who can be partners, filter
// out the ones who already have submissions (say, through an external
// submitter) and create submissions for the ones who don't.
for (Submission partneredSub : job.submission().partneredSubmissions())
{
potentialPartners.removeObject(partneredSub.user());
}
job.submission().partnerWith(potentialPartners.allObjects());
}
// ----------------------------------------------------------
private void writeOutSavedGradingProperties(EnqueuedJob job,
WCProperties gradingProperties)
{
MutableDictionary accumulatedValues = mostRecentAccumulatedValues(job);
NSDictionary<String, Object> previousValues =
previousSubmissionSavedProperties(job);
@SuppressWarnings("unchecked")
NSArray<String> keys = accumulatedValues.allKeys();
for (String key : keys)
{
if (!key.matches("^(previous|mostRecent)\\..*\\.results$"))
{
Object value = accumulatedValues.objectForKey(key);
gradingProperties.setObjectForKey(value, "mostRecent." + key);
}
}
for (String key : previousValues.allKeys())
{
if (!key.matches("^(previous|mostRecent)\\..*\\.results$"))
{
Object value = previousValues.objectForKey(key);
gradingProperties.setObjectForKey(value, "previous." + key);
}
}
}
// ----------------------------------------------------------
/**
* Build a saved property dictionary from the result outcomes for the
* specified submission result.
*
* @param submissionResult the submission result
*/
private NSDictionary<String, Object> previousSubmissionSavedProperties(
EnqueuedJob job)
{
NSMutableDictionary<String, Object> props =
new NSMutableDictionary<String, Object>();
Submission submission = job.submission().previousSubmission();
SubmissionResult submissionResult = null;
if (submission != null)
{
submissionResult = submission.result();
}
if (submissionResult != null)
{
for (ResultOutcome outcome : submissionResult.resultOutcomes())
{
String key = outcome.tag();
MutableDictionary contents = outcome.contents();
Integer index = outcome.index();
Object value;
if (contents != null && contents.count() == 1
&& contents.objectForKey("value") != null)
{
value = contents.objectForKey("value");
}
else
{
value = contents;
}
if (index == null)
{
props.setObjectForKey(value, key);
}
else
{
@SuppressWarnings("unchecked")
NSMutableArray<Object> array =
(NSMutableArray<Object>)props.objectForKey(key);
if (array == null)
{
array = new NSMutableArray<Object>();
props.setObjectForKey(array, key);
}
growArrayUpToIndex(array, index);
array.replaceObjectAtIndex(value, index);
}
}
}
return props;
}
// ----------------------------------------------------------
/**
* Grows the specified array until an item at the specified index can be
* set, inserting null values in the gaps.
*
* @param array the array
* @param index the index to grow up to
*/
private void growArrayUpToIndex(NSMutableArray<?> array, int index)
{
while (array.count() <= index)
{
array.addObject(NSKeyValueCoding.NullValue);
}
}
// ----------------------------------------------------------
/**
* Gets the most recent accumulated values for the submission chain
* associated with this job.
*
* @param job the grading job
* @return the most recent accumulated values
*/
private MutableDictionary mostRecentAccumulatedValues(EnqueuedJob job)
{
MutableDictionary accumulatedValues = null;
// Get the previous submission that has a result object so that we can
// get the accumulated values dictionary from it.
Submission prevSub = job.submission().previousSubmission();
while (prevSub != null && prevSub.result() == null)
{
prevSub = prevSub.previousSubmission();
}
if (prevSub != null)
{
SubmissionResult prevResult = prevSub.result();
if (prevResult != null)
{
accumulatedValues = new MutableDictionary(
prevResult.accumulatedSavedProperties());
}
}
if (accumulatedValues == null)
{
accumulatedValues = new MutableDictionary();
}
return accumulatedValues;
}
// ----------------------------------------------------------
/**
* Create result outcome objects from properties in the grading properties
* file.
*
* @param job
* @param submissionResult
* @param properties
*/
private void processSavedProperties( EnqueuedJob job,
SubmissionResult submissionResult,
WCProperties properties )
{
// Get the previous result with any data so that we can merge in these
// values with the accumulated values.
MutableDictionary accumulatedValues = mostRecentAccumulatedValues(job);
// Pull any properties that are prefixed with "saved." into
// ResultOutcome objects
final String SAVED_PROPERTY_PREFIX = "saved.";
final String RESULT_PROPERTY_SUFFIX = ".results";
for (Object propertyAsObj : properties.keySet())
{
String property = (String) propertyAsObj;
String actualName = null;
if (property.startsWith(SAVED_PROPERTY_PREFIX))
{
actualName = property.substring(SAVED_PROPERTY_PREFIX.length());
}
else if (property.endsWith(RESULT_PROPERTY_SUFFIX)
&& !property.startsWith("mostRecent.")
&& !property.startsWith("previous."))
{
actualName = property;
// actualName = property.substring(
// 0, property.length() - RESULT_PROPERTY_SUFFIX.length());
}
if (actualName != null)
{
Object value = properties.valueForKey(property);
if (value != null)
{
// Update the accumulated value dictionary.
accumulatedValues.setObjectForKey(value, actualName);
if (value instanceof NSArray)
{
NSArray<?> array = (NSArray<?>) value;
int index = 0;
for (Object elem : array)
{
createResultOutcome( job, submissionResult, index,
actualName, elem );
index++;
}
}
else
{
createResultOutcome( job, submissionResult, null,
actualName, value );
}
}
}
}
// Save the new accumulated saved properties into this submission
// result.
submissionResult.setAccumulatedSavedProperties(accumulatedValues);
}
// ----------------------------------------------------------
/**
* Creates a single result outcome from the value of a property in the
* grading properties file. If the value is a dictionary, then it is
* stored in the outcome directly; if it is a scalar value, then it is
* stored in the outcome contents as a one-element dictionary with the key
* named "value".
*
* @param job
* @param submissionResult
* @param index
* @param tag
* @param value
*/
private void createResultOutcome( EnqueuedJob job,
SubmissionResult submissionResult,
Integer index,
String tag,
Object value )
{
NSDictionary<String, Object> contents;
if (!(value instanceof NSDictionary))
{
contents = new NSDictionary<String, Object>(value, "value");
}
else
{
@SuppressWarnings("unchecked")
NSDictionary<String, Object> castContents =
(NSDictionary<String, Object>) value;
contents = castContents;
}
ResultOutcome outcome = new ResultOutcome();
outcome.setTag(tag);
outcome.setContents(new MutableDictionary(contents));
if (index != null)
{
outcome.setIndex(index);
}
editingContext.insertObject(outcome);
// TODO remove this when we fix the SubmissionResult.submission
// relationship "problem"
outcome.setSubmissionRelationship(job.submission());
outcome.setSubmissionResultRelationship(submissionResult);
}
// ----------------------------------------------------------
/**
* Generates a single composite file from multiple inlined report
* fragments.
*
* @param destination The output file to create and fill
* @param inlineFragments The array of InlineFiles to fill it with
*/
void generateCompositeResultFile( File destination,
NSArray<InlineFile> inlineFragments )
{
if ( inlineFragments.count() > 0 )
{
try
{
BufferedOutputStream out = new BufferedOutputStream(
new FileOutputStream( destination ) );
final byte[] borderString =
"<hr size=\"1\" noshade />\n".getBytes();
boolean lastNeedsBorder = false;
for (InlineFile thisFile: inlineFragments)
{
boolean isHTML = thisFile.mimeType != null &&
( thisFile.mimeType.equalsIgnoreCase( "text/html" ) ||
thisFile.mimeType.equalsIgnoreCase( "html" ) );
try
{
BufferedInputStream in = new BufferedInputStream(
new FileInputStream( thisFile ) );
if ( lastNeedsBorder || thisFile.border )
{
out.write( borderString );
}
lastNeedsBorder = thisFile.border;
if ( !isHTML )
{
out.write( "<pre>".getBytes() );
}
FileUtilities.copyStream( in, out );
in.close();
if ( !isHTML )
{
out.write( "</pre>".getBytes() );
}
}
catch ( Exception ex1 )
{
log.error( "exception copying inline report "
+ "fragment '"
+ thisFile.getPath()
+ "'",
ex1 );
continue;
}
}
if ( lastNeedsBorder )
{
out.write( borderString );
}
out.flush();
out.close();
}
catch ( Exception ex2 )
{
log.error( "exception generating final report '"
+ destination
+ "'",
ex2 );
}
}
}
// ----------------------------------------------------------
/**
* Handles a technical fault Suspends grading of other submissions for the
* same assignment
*
* @param job the job which faulted
*/
void technicalFault( EnqueuedJob job,
String stage,
Exception e,
File attachmentsDir )
{
job.setPaused( true );
faultOccurredInStep = true;
try
{
SubmissionSuspendedMessage msg = new SubmissionSuspendedMessage(
job.submission(), e, stage, attachmentsDir);
log.info("technicalFault(): " + msg.title());
log.info(msg.shortBody(), e);
msg.send();
}
catch (Exception ee)
{
log.error("Exception sending message to student", ee);
log.error("Cause:", e);
}
/* Vector<String> attachments = null;
if ( attachmentsDir != null && attachmentsDir.exists() )
{
attachments = new Vector<String>();
File[] fileList = attachmentsDir.listFiles();
for ( int i = 0; i < fileList.length; i++ )
{
if ( !fileList[i].isDirectory() )
{
attachments.addElement( fileList[i].getPath() );
}
}
}
String errorMsg = "An " + ( ( e == null ) ? "error": "exception" )
+ " occurred " + stage;
if ( e != null )
{
errorMsg += ":\n" + e;
}
// errorMsg += "\n\nGrading for the submissions has been halted.\n";
errorMsg += "\n\nGrading of this submission has been suspended.\n";
String subject =
"[Grader] Grading error: "
+ job.submission().user().userName() + " #"
+ job.submission().submitNumber();
log.info( "technicalFault(): " + subject );
log.info( errorMsg, e );
Application.sendAdminEmail( null,
job.submission().assignmentOffering()
.courseOffering().instructors(),
true,
subject,
errorMsg,
attachments );*/
}
// ----------------------------------------------------------
/**
* Find out how many grading jobs have been processed so far.
*
* @return the number of jobs process so far
*/
public int processedJobCount()
{
return jobCount;
}
// ----------------------------------------------------------
/**
* Find out the processing delay for the most recently completed job.
*
* @return the time in milliseconds
*/
public long mostRecentJobWait()
{
return mostRecentJobWait;
}
// ----------------------------------------------------------
/**
* Find out the estimated processing delay for any job.
*
* @return the time in milliseconds
*/
public long estimatedJobTime()
{
if ( jobsCountedWithWaits > 0 )
{
return totalWaitForJobs / jobsCountedWithWaits;
}
else
{
return DEFAULT_JOB_WAIT;
}
}
//~ Instance/static variables .............................................
/**
* The grace period is added to the timeout limits for the various
* scripts. The value comes from the application property file.
*/
static final int gracePeriod =
Application.configurationProperties().intForKey( "grader.gracePeriod" );
/**
* The grace period is added to the timeout limits for the various
* scripts. The value comes from the application property file.
*/
static final int emailWaitMinutes =
Application.configurationProperties().intForKeyWithDefault(
"grader.mailResultNotificationAfterMinutes", 15 );
/** The queue to receive processing tokens. */
private GraderQueue queue;
/** Number of jobs processed so far, to report administrative status. */
private int jobCount = 0;
private int jobsCountedWithWaits = 0;
/** Time between submission and grading completion for more recent job. */
private long mostRecentJobWait = 0;
private long totalWaitForJobs = 0;
private static final long DEFAULT_JOB_WAIT = 30000;
// State for the current step being executed
private boolean faultOccurredInStep;
private boolean timeoutOccurredInStep;
private WCEC editingContext;
static Logger log = Logger.getLogger( GraderQueueProcessor.class );
}