From b37fe5260b499bbf9c4fcf528321e2d2038ffc4c Mon Sep 17 00:00:00 2001 From: John Buck Date: Sun, 30 Jun 2024 17:06:20 -0400 Subject: [PATCH 01/55] i_972 Add support for better human judge reviewing Modify scripts to generate information to help a human judge figure out why a run failed. Better determination of which CPU to run a submission on in a sandbox. Add some basic Web page scripts to make judge results web pages on each judge machine. Add a configurable execute folder that allows substitute variables. This is good so that the execute folders may be retained for each run {:runnumber} in the execute folder, for example. CI: toString method of ExecutionData was not correct. --- scripts/pc2sandbox.sh | 191 +++++-- scripts/pc2sandbox_interactive.sh | 55 +- src/edu/csus/ecs/pc2/core/Constants.java | 2 +- .../csus/ecs/pc2/core/execute/Executable.java | 109 +++- .../ecs/pc2/core/execute/ExecutionData.java | 54 +- .../pc2/core/model/ContestInformation.java | 252 +++++----- .../ecs/pc2/ui/ContestInformationPane.java | 473 +++++++++++------- support/judge_webcgi/judge | 178 +++++++ support/judge_webcgi/showrun.sh | 106 ++++ 9 files changed, 1061 insertions(+), 359 deletions(-) create mode 100644 support/judge_webcgi/judge create mode 100644 support/judge_webcgi/showrun.sh diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh index 30afd9aeb..08d536e93 100755 --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -1,15 +1,24 @@ #!/bin/bash -# Copyright (C) 1989-2023 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. +# Copyright (C) 1989-2024 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. # # File: pc2sandbox.sh # Purpose: a sandbox for pc2 using Linux CGroups v2. # Input arguments: # $1: memory limit in MB # $2: time limit in seconds -# $3: command to be executed -# $4... : command arguments +# $3: judges_input_file +# $4: judges_answer_file +# $5: testcase number +# $6: command to be executed +# $7... : command arguments # # Author: John Buck, based on earlier versions by John Clevenger and Doug Lane +DEFAULT_CPU_NUM=3 +CPU_OVERRIDE_FILE=$HOME/pc2_cpu_override + +# Where to the the result of the first failure. If this file is not created, then the +# run was accepted. (correct) +RESULT_FAILURE_FILE=failure.txt # FAIL_RETCODE_BASE is 128 + 64 + xx # 128 = system error, like signal @@ -32,14 +41,43 @@ FAIL_SANDBOX_ERROR=$((FAIL_RETCODE_BASE+54)) # This gets added to the current number of executing processes for this user. MAXPROCS=32 -# taskset cpu mask for running submission on single processor -cpunum=${USER/judge/} -if [[ "$cpunum" =~ ^[1-5]$ ]] +# Compute taskset cpu mask for running submission on single processor + +# Get system's maximum CPU number +MAX_CPU_NUM=`lscpu -p=cpu | tail -1` + +# See if the admin wants to override the CPU by reading the override file +if test -s ${CPU_OVERRIDE_FILE} then - CPUMASK=$((1<<(cpunum-1))) -else - CPUMASK=0x08 + # This will get the first line that consists of numbers only + cpunum=`egrep '^[0-9]+$' ${CPU_OVERRIDE_FILE} | head -1` + if test -z ${cpunum} + then + cpunum="" + elif test ${cpunum} -gt ${MAX_CPU_NUM} + then + cpunum="" + fi +fi + +# If there was no override or the override was bad, let's try to figure out the cpunum +if test -z ${cpunum} +then + # The login id must be "judge#" where # is the desired CPU and judge number. + # If the login is not "judge#", then the system default is used. + # This special case is for when you want to run multiple judges on one computer + # that has lots of CPU's, but want to pin each judge to its own cpu. + cpunum=${USER/judge/} + if [[ "$cpunum" =~ ^[1-9][0-9]*$ ]] + then + # Restrict to number of CPU's. + cpunum=$(((cpunum-1)%(MAX_CPU_NUM+1))) + else + cpunum=$(((DEFAULT_CPU_NUM+1))) + fi fi +cpunum=$((cpunum-1)) +CPUMASK=$((1<> $DEBUG_FILE } +# For per-testcase reporting/logging +function REPORT() +{ + if test -n "$REPORTFILE" + then + echo "$@" >> $REPORTFILE + fi +} +function REPORT_BRIEF() +{ + if test -n "$BRIEFREPORTFILE" + then + echo "$@" >> $BRIEFREPORTFILE + fi +} + +# For per-testcase report and debugging both +function REPORT_DEBUG() +{ + if test -n "$REPORTFILE" + then + echo "$@" >> $REPORTFILE + fi + [ "$_DEBUG" == "on" ] && echo "$@" >> $DEBUG_FILE +} + # ------------------------------------------------------------ usage() @@ -109,9 +176,10 @@ KillChildProcs() # is wall-time exceeded which is execute time limit + 1 second HandleTerminateFromPC2() { - DEBUG echo "Received TERMINATE signal from PC2" + REPORT_DEBUG "Received TERMINATE signal from PC2" + REPORT_DEBUG "Kiling off submission process group $submissionpid and all children" KillChildProcs - DEBUG echo $0: Wall time exceeded - exiting with code $FAIL_WALL_TIME_LIMIT_EXCEEDED + REPORT_DEBUG $0: Wall time exceeded - exiting with code $FAIL_WALL_TIME_LIMIT_EXCEEDED exit $FAIL_WALL_TIME_LIMIT_EXCEEDED } @@ -136,23 +204,49 @@ ShowStats() walltime=$3 memused=$4 memlim=$5 - DEBUG echo Resources used for this run: - DEBUG printf " CPU ms Limit ms Wall ms Memory Used Memory Limit\n" - DEBUG printf "%5d.%03d %5d.%03d %6d.%03d %12s %12d\n" $((cpuused / 1000)) $((cpuused % 1000)) \ + REPORT_BRIEF "$(printf '%d.%03d' $((cpuused / 1000)) $((cpuused % 1000)))" \ + "$(printf '%d.%03d' $((cpulim / 1000)) $((cpulim % 1000)))" \ + "$(printf '%d.%03d' $((walltime / 1000)) $((walltime % 1000)))" \ + ${memused} $((memlim)) + REPORT_DEBUG Resources used for this run: + REPORT_DEBUG "$(printf ' CPU ms Limit ms Wall ms Memory Used Memory Limit\n')" + REPORT_DEBUG "$(printf '%5d.%03d %5d.%03d %6d.%03d %12s %12d\n' $((cpuused / 1000)) $((cpuused % 1000)) \ $((cpulim / 1000)) $((cpulim % 1000)) \ $((walltime / 1000)) $((walltime % 1000)) \ - $((memused)) $((memlim)) + ${memused} $((memlim)))" +} + +# Generate a system failure result file. This will be the FIRST failure as it will not overwrite the file +# if it exists. +SysFailure() +{ + if test ! -s ${RESULT_FAILURE_FILE} + then + (echo system; echo $* ) > ${RESULT_FAILURE_FILE} + fi +} + +# Generate a failure result file. This will be the FIRST failure as it will not overwrite the file +# if it exists. +Failure() +{ + if test ! -s ${RESULT_FAILURE_FILE} + then + echo $* > ${RESULT_FAILURE_FILE} + fi } # ------------------------------------------------------------ if [ "$#" -lt 1 ] ; then echo $0: No command line arguments + SysFailure No command line arguments to $0 exit $FAIL_NO_ARGS_EXIT_CODE fi if [ "$#" -lt 3 ] ; then echo $0: expected 3 or more arguments, found: $* + SysFailure Expected 3 or more arguments to $0, found: $* exit $FAIL_INSUFFICIENT_ARGS_EXIT_CODE fi @@ -163,14 +257,30 @@ fi MEMLIMIT=$1 TIMELIMIT=$2 -COMMAND=$3 +JUDGEIN=$3 +JUDGEANS=$4 +TESTCASE=$5 +COMMAND=$6 +DEBUG echo "+---------------- Test Case ${TESTCASE} ----------------+" +DEBUG echo Command line: $0 $* shift shift shift - -#### Debugging - just set expected first 3 args to: 16MB 5seconds +shift +shift +DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" +DEBUG echo -e "\n$0" ${MEMLIMIT} ${TIMELIMIT} xxx xxx $* "< ${JUDGEIN} > $TESTCASE.ans" +DEBUG echo -e "\nor, without the sandbox by using the following command:" +DEBUG echo -e "\n$* < ${JUDGEIN} > $TESTCASE.ans" +DEBUG echo -e "\nAnd compare with the judge's answer:" +DEBUG echo -e "\ndiff -w ${JUDGEANS} $TESTCASE.ans | more\n" + +#### Debugging - just set expected first args to: 8MB 2seconds ###MEMLIMIT=8 ###TIMELIMIT=2 +###JUDGEIN=none +###JUDGEANS=none +###TESTCASE=1 ###COMMAND=$1 ###shift @@ -181,27 +291,32 @@ shift DEBUG echo checking PC2 CGroup V2 installation... if [ ! -d "$PC2_CGROUP_PATH" ]; then echo $0: expected pc2sandbox CGroups v2 installation in $PC2_CGROUP_PATH + SysFailure CGroups v2 not installed in $PC2_CGROUP_PATH exit $FAIL_INVALID_CGROUP_INSTALLATION fi if [ ! -f "$CGROUP_PATH/cgroup.controllers" ]; then echo $0: missing file cgroup.controllers in $CGROUP_PATH + SysFailure Missing cgroup.controllers in $CGROUP_PATH exit $FAIL_MISSING_CGROUP_CONTROLLERS_FILE fi if [ ! -f "$CGROUP_PATH/cgroup.subtree_control" ]; then echo $0: missing file cgroup.subtree_control in $CGROUP_PATH + SysFailure Missing cgroup.subtree_controll in $CGORUP_PATH exit $FAIL_MISSING_CGROUP_SUBTREE_CONTROL_FILE fi # make sure the cpu and memory controllers are enabled if ! grep -q -F "cpu" "$CGROUP_PATH/cgroup.subtree_control"; then echo $0: cgroup.subtree_control in $CGROUP_PATH does not enable cpu controller + SysFailure CPU controller not enabled in cgroup.subtree_control in $CGROUP_PATH exit $FAIL_CPU_CONTROLLER_NOT_ENABLED fi if ! grep -q -F "memory" "$CGROUP_PATH/cgroup.subtree_control"; then echo $0: cgroup.subtree_control in $CGROUP_PATH does not enable memory controller + SysFailure Memory controller not enabled in cgroup.subtree_control in $CGROUP_PATH exit $FAIL_MEMORY_CONTROLLER_NOT_ENABLED fi @@ -216,6 +331,7 @@ then if ! rmdir $PC2_SANDBOX_CGROUP_PATH then DEBUG echo Cannot remove previous sandbox: $PC2_SANDBOX_CGROUP_PATH + SysFailure Can not remove previous sandbox $PC2_SANDBOX_CGROUP_PATH exit $FAIL_SANDBOX_ERROR fi fi @@ -224,9 +340,18 @@ DEBUG echo Creating sandbox $PC2_SANDBOX_CGROUP_PATH if ! mkdir $PC2_SANDBOX_CGROUP_PATH then DEBUG echo Cannot create $PC2_SANDBOX_CGROUP_PATH + SysFailure Can not create $PC2_SANDBOX_CGROUP_PATH exit $FAIL_INVALID_CGROUP_INSTALLATION fi +# Set up report directory for per-case logging +mkdir -p "$REPORTDIR" + +# Set report file to be testcase specific one now +REPORTFILE=`printf "$REPORTDIR/testcase_%03d.log" $TESTCASE` +BRIEFREPORTFILE=`printf "$REPORTDIR/briefcase_%03d.log" $TESTCASE` +DEBUG echo Report file: ${REPORTFILE} Brief Report File: ${BRIEFREORTFILE} + # set the specified memory limit - input is in MB, cgroup v2 requires bytes, so multiply by 1M # but only if > 0. # "max" means unlimited, which is the cgroup v2 default @@ -241,25 +366,32 @@ else echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max fi +REPORT Test case $TESTCASE: # We use ulimit to limit CPU time, not cgroups. Time is supplied in seconds. This may have to # be reworked if ms accuracy is needed. The problem is, cgroups do not kill off a process that # exceeds the time limit, ulimit does. TIMELIMIT_US=$((TIMELIMIT * 1000000)) -DEBUG echo setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" +REPORT Setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" ulimit -t $TIMELIMIT MAXPROCS=$((MAXPROCS+`ps -T -u $USER | wc -l`)) -DEBUG echo setting maximum user processes to $MAXPROCS +REPORT Setting maximum user processes to $MAXPROCS ulimit -u $MAXPROCS +# Keep track of details for reports +REPORT_BRIEF $cpunum +REPORT_BRIEF $$ +REPORT_BRIEF $(date "+%F %T.%6N") + # Remember wall time when we started starttime=`GetTimeInMicros` #put the current process (and implicitly its children) into the pc2sandbox cgroup. -DEBUG echo putting $$ into $PC2_SANDBOX_CGROUP_PATH cgroup +REPORT Putting $$ into $PC2_SANDBOX_CGROUP_PATH cgroup if ! echo $$ > $PC2_SANDBOX_CGROUP_PATH/cgroup.procs then echo $0: Could not add current process to $PC2_SANDBOX_CGROUP_PATH/cgroup.procs - not executing submission. + SysFailure Could not add current process to $PC2_SANDBOX_CGROUP_PATH/cgroup.procs exit $FAIL_SANDBOX_ERROR fi @@ -270,7 +402,7 @@ fi # since we don't know how to use cgroup-tools to execute, just execute it directly (it's a child so it # should still fall under the cgroup limits). -DEBUG echo Executing "setsid taskset $CPUMASK $COMMAND $*" +REPORT_DEBUG Executing "setsid taskset $CPUMASK $COMMAND $*" # Set up trap handler to catch wall-clock time exceeded and getting killed by PC2's execute timer trap HandleTerminateFromPC2 15 @@ -311,23 +443,26 @@ ShowStats ${cputime} ${TIMELIMIT_US} ${walltime} ${peakmem} $((MEMLIMIT*1024*102 if test "$kills" != "0" then - DEBUG echo The command was killed because it exceeded the memory limit + REPORT_DEBUG The command was killed because it exceeded the memory limit + REPORT_BRIEF MLE COMMAND_EXIT_CODE=${FAIL_MEMORY_LIMIT_EXCEEDED} else # See why we terminated. 137 = 128 + 9 = SIGKILL, which is what ulimit -t sends. if test "$COMMAND_EXIT_CODE" -eq 137 -o "$cputime" -gt "$TIMELIMIT_US" then - DEBUG echo The command was killed because it exceeded the CPU Time limit + REPORT_DEBUG The command was killed because it exceeded the CPU Time limit + REPORT_BRIEF TLE COMMAND_EXIT_CODE=${FAIL_TIME_LIMIT_EXCEEDED} elif test "$COMMAND_EXIT_CODE" -ge 128 then - DEBUG echo The command terminated abnormally with exit code $COMMAND_EXIT_CODE + REPORT_DEBUG The command terminated abnormally with exit code $COMMAND_EXIT_CODE + REPORT_BRIEF RTE Exit:$COMMAND_EXIT_CODE else - DEBUG echo The command terminated normally. + REPORT_DEBUG The command terminated normally. fi fi -DEBUG echo Finished executing $COMMAND $* -DEBUG echo $COMMAND exited with exit code $COMMAND_EXIT_CODE +REPORT_DEBUG Finished executing $COMMAND $* +REPORT_DEBUG $COMMAND exited with exit code $COMMAND_EXIT_CODE DEBUG echo diff --git a/scripts/pc2sandbox_interactive.sh b/scripts/pc2sandbox_interactive.sh index 6213b4e1b..d37f66a07 100755 --- a/scripts/pc2sandbox_interactive.sh +++ b/scripts/pc2sandbox_interactive.sh @@ -15,6 +15,10 @@ # # Author: John Buck, based on earlier versions by John Clevenger and Doug Lane +# CPU to run submission on. This is 0 based, so 3 means the 4th CPU +DEFAULT_CPU_NUM=3 +CPU_OVERRIDE_FILE=$HOME/pc2_cpu_override + # FAIL_RETCODE_BASE is 128 + 64 + xx # 128 = system error, like signal # 64 = biggest used signal @@ -37,14 +41,42 @@ FAIL_INTERACTIVE_ERROR=$((FAIL_RETCODE_BASE+55)) # This gets added to the current number of executing processes for this user. MAXPROCS=32 -# taskset cpu mask for running submission on single processor -cpunum=${USER/judge/} -if [[ "$cpunum" =~ ^[1-5]$ ]] +# Compute taskset cpu mask for running submission on single processor + +# Get system's maximum CPU number +MAX_CPU_NUM=`lscpu -p=cpu | tail -1` + +# See if the admin wants to override the CPU by reading the override file +if test -s ${CPU_OVERRIDE_FILE} then - CPUMASK=$((1<<(cpunum-1))) -else - CPUMASK=0x08 + # This will get the first line that consists of numbers only + cpunum=`egrep '^[0-9]+$' ${CPU_OVERRIDE_FILE} | head -1` + if test -z ${cpunum} + then + cpunum="" + elif test ${cpunum} -gt ${MAX_CPU_NUM} + then + cpunum="" + fi +fi + +# If there was no override or the override was bad, let's try to figure out the cpunum +if test -z ${cpunum} +then + # The login id must be "judge#" where # is the desired CPU and judge number. + # If the login is not "judge#", then the system default is used. + # This special case is for when you want to run multiple judges on one computer + # that has lots of CPU's, but want to pin each judge to its own cpu. + cpunum=${USER/judge/} + if [[ "$cpunum" =~ ^[1-9][0-9]*$ ]] + then + # Restrict to number of CPU's. + cpunum=$(((cpunum-1)%(MAX_CPU_NUM+1))) + else + cpunum=$(((DEFAULT_CPU_NUM+1))) + fi fi +CPUMASK=$((1< $INT_RESULTFILE -$msg2 + EOF sent_xml=1 } @@ -240,6 +273,8 @@ JUDGEIN="$4" JUDGEANS="$5" TESTCASE="$6" COMMAND="$7" +DEBUG echo "+---------------- Test Case ${TESTCASE} ----------------+" +DEBUG echo Command line: $0 $* shift 7 # the rest of the commmand line arguments are the command args for the submission @@ -431,7 +466,7 @@ do # If validator created a feedback file, put the last line in the judgement if test -s "$feedbackfile" then - GenXML "No - Wrong answer" `tail -1 $feedbackfile` + GenXML "No - Wrong answer" `head -n 1 $feedbackfile` else GenXML "No - Wrong answer" "No feedback file" fi diff --git a/src/edu/csus/ecs/pc2/core/Constants.java b/src/edu/csus/ecs/pc2/core/Constants.java index 5a71b2131..408518592 100644 --- a/src/edu/csus/ecs/pc2/core/Constants.java +++ b/src/edu/csus/ecs/pc2/core/Constants.java @@ -212,7 +212,7 @@ private Constants() { /** * Command line to run submission in a sandbox */ - public static final String PC2_INTERNAL_SANDBOX_COMMAND_LINE = "./{:sandboxprogramname} {:memlimit} {:timelimit}"; + public static final String PC2_INTERNAL_SANDBOX_COMMAND_LINE = "./{:sandboxprogramname} {:memlimit} {:timelimit} {:infilename} {:ansfilename} {:testcase}"; /** * File name of script to run submission in sandbox ({:sandboxprogramname} if non-interactive) diff --git a/src/edu/csus/ecs/pc2/core/execute/Executable.java b/src/edu/csus/ecs/pc2/core/execute/Executable.java index 9d3ef1827..ad93794c9 100644 --- a/src/edu/csus/ecs/pc2/core/execute/Executable.java +++ b/src/edu/csus/ecs/pc2/core/execute/Executable.java @@ -81,6 +81,8 @@ public class Executable extends Plugin implements IExecutable, IExecutableNotify private static final String NL = System.getProperty("line.separator"); + private static final String DEFAULT_EXECUTE_DIRECTORY_TEMPLATE = "executesite{:clientsite}{:clientname}"; + private Run run = null; private Language language = null; @@ -174,7 +176,7 @@ public class Executable extends Plugin implements IExecutable, IExecutableNotify /** * Sandbox constants */ - public static final long SANDBOX_EXTRA_KILLTIME_MS = 1000; + public static final long SANDBOX_EXTRA_KILLTIME_MS = 5000; /** * Return codes from sandbox @@ -323,6 +325,9 @@ public Executable(IInternalContest inContest, IInternalController inController, */ private void initialize() { + // set log early in case of exceptions + log = controller.getLog(); + this.executorId = contest.getClientId(); if (runFiles != null) { @@ -330,8 +335,6 @@ private void initialize() { } executeDirectoryName = getExecuteDirectoryName(); - log = controller.getLog(); - if (executorId.getClientType() != ClientType.Type.TEAM) { this.problemDataFiles = contest.getProblemDataFile(problem); } @@ -528,6 +531,8 @@ public IFileViewer execute(boolean clearDirFirst) { atLeastOneTestFailed = true; failedResults = executionData.getValidationResults(); } + // Create summary of execution results in execute folder for human judge + saveExecuteData(dataSetNumber); } else { @@ -541,6 +546,9 @@ public IFileViewer execute(boolean clearDirFirst) { // execute against one specific data set passed = executeAndValidateDataSet(dataSetNumber); + // Create summary of execution results in execute folder for human judge + saveExecuteData(dataSetNumber); + dataSetNumber++; if (!passed) { log.info("FAILED test case " + dataSetNumber + " for run " + run.getNumber() + " reason " + getFailureReason()); @@ -609,6 +617,9 @@ public IFileViewer execute(boolean clearDirFirst) { setException(errorMessage); fileViewer.addTextPane("Error during compile", errorMessage); } // else they will get a tab hopefully showing something wrong + + // Create summary of compilation failure for human judge + saveExecuteData(0); } // we've finished the compile/execute/validate steps (for better or worse); do the required final steps to display the results @@ -2988,16 +2999,24 @@ public String substituteAllStrings(Run inRun, String origString) { *
      *             valid fields are:
      *              {:mainfile} - submitted file (hello.java)
+     *              {:package} - if a package was specified
      *              {:basename} - mainfile without extension (hello)
      *              {:validator} - validator program name
-     *              {:language}
-     *              {:problem}
-     *              {:teamid}
-     *              {:siteid}
+     *              {:language} - index into languages (1 based)
+     *              {:languageletter} - index converted to letter, eg 1=A, 2=B
+     *              {:languagename} - Display name of language (spaces converted to _)
+     *              {:languageid} - CLICS language id, eg cpp
+     *              {:problem} - Index into problem table
+     *              {:problemletter} - A,B,C...
+     *              {:problemshort} - problem short name
+     *              {:teamid} - team's id number
+     *              {:siteid} - team's site
+     *              {:clientname} - this client's name, eg judge1
+     *              {:clientid} - this client's id number, eg. 1
+     *              {:clientsite} - this client's site
      *              {:infile}
      *              {:outfile}
      *              {:ansfile}
-     *              {:pc2home}
      *              {:sandboxprogramname} - the sandbox program name as defined in the Problem
      *              {:sandboxcommandline} - the command line used to invoke the sandbox as defined in the Problem
      *              {:ensuresuffix=...} - add supplied suffix if not present already
@@ -3007,6 +3026,10 @@ public String substituteAllStrings(Run inRun, String origString) {
      *              {:ansfilename} - full path to judges answer file
      *              {:timelimit} - CPU time limit in seconds
      *              {:memlimit} - memory limit in MB
+     *              {:exitvalue} - results exit code
+     *              {:executetime} - result execution time in MS
+     *              {:pc2home} - where pc2 is installed
+     *              {:runnumber} - the run number
      * 
* * @param dataSetNumber @@ -3059,9 +3082,11 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb Language[] langs = contest.getLanguages(); int index = 0; String displayName = ""; + String languageid = ""; for (int i = 0; i < langs.length; i++) { if (langs[i] != null && langs[i].getElementId().equals(inRun.getLanguageId())) { displayName = langs[i].getDisplayName().toLowerCase().replaceAll(" ", "_"); + languageid = langs[i].getID(); index = i + 1; break; } @@ -3070,6 +3095,7 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb newString = replaceString(newString, "{:language}", index); newString = replaceString(newString, "{:languageletter}", Utilities.convertNumber(index)); newString = replaceString(newString, "{:languagename}", displayName); + newString = replaceString(newString, "{:languageid}", languageid); } } if (inRun.getProblemId() != null) { @@ -3083,8 +3109,8 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb } if (index > 0) { newString = replaceString(newString, "{:problem}", index); - newString = replaceString(newString, "{:problemletter}", Utilities.convertNumber(index)); if(problem != null) { + newString = replaceString(newString, "{:problemletter}", problem.getLetter()); newString = replaceString(newString, "{:problemshort}", problem.getShortName()); } } @@ -3093,7 +3119,9 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb newString = replaceString(newString, "{:teamid}", inRun.getSubmitter().getClientNumber()); newString = replaceString(newString, "{:siteid}", inRun.getSubmitter().getSiteNumber()); } - + newString = replaceString(newString, "{:clientname}", contest.getClientId().getName()); + newString = replaceString(newString, "{:clientid}", contest.getClientId().getClientNumber()); + newString = replaceString(newString, "{:clientsite}", contest.getClientId().getSiteNumber()); if (problem != null) { if (problem.getDataFileName() != null && !problem.getDataFileName().equals("")) { newString = replaceString(newString, "{:infile}", problem.getDataFileName()); @@ -3135,6 +3163,8 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb } newString = replaceString(newString, "{:timelimit}", Long.toString(problem.getTimeOutInSeconds())); + newString = replaceString(newString, "{:runnumber}", Integer.toString(inRun.getNumber())); + // TODO REFACTOR replace vars with constants for: memlimit, sandboxcommandline,sandboxprogramname newString = replaceString(newString, "{:memlimit}", Integer.toString(problem.getMemoryLimitMB())); @@ -3174,6 +3204,7 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb newString = replaceString(newString, "{:pc2home}", pc2home); } + // Check for conditional suffix (that is, the previous chars match), if not, add them newString = ExecuteUtilities.replaceStringConditional(newString, Constants.CMDSUB_COND_SUFFIX); @@ -3377,14 +3408,19 @@ public void setProblem(Problem problem) { /** * Execute directory name for this client instance. * - * The name is individual for each client. + * The name is individual for each client and run. * * @see #getExecuteDirectoryNameSuffix() * * @return the name of the execute directory for this client. */ public String getExecuteDirectoryName() { - return "executesite" + contest.getClientId().getSiteNumber() + contest.getClientId().getName() + getExecuteDirectoryNameSuffix(); + String dirName = getContestInformation().getJudgesExecuteFolder(); + if(StringUtilities.isEmpty(dirName)) { + dirName = DEFAULT_EXECUTE_DIRECTORY_TEMPLATE; + } + dirName = substituteAllStrings(run, dirName) + getExecuteDirectoryNameSuffix(); + return(dirName); } /** @@ -3700,4 +3736,53 @@ private void createValidatorProgram() } } + + /** + * Shows a 'null' string as "" instead of "null" + * + * @param str String to check + * @return "" if null or the original string if not. + */ + private String showNullAsEmpty(String str) + { + if(str == null) { + return(""); + } + return(str); + } + + /** + * Write execute summary data to the specified testcase executedata. file in the execute folder. + * + * @param dataSetNumber The dataset number, used to create a file name + * @return true if it worked, false otherwise. + */ + private boolean saveExecuteData(int dataSetNumber) + { + // create execution data results summary file - useful for human judges reviewing the problem + String executionDataFilename = prefixExecuteDirname("executedata." + dataSetNumber + ".txt"); + boolean bWritten = false; + try (PrintWriter executeDataWriter = new PrintWriter(executionDataFilename)){ + // The file produced may be "sourced" in bash if desired, eg. ". ./executedata.1.txt" + executeDataWriter.println("executeDateTime='" + Utilities.getIso8601formatterWithMS().format(new Date()) + "'"); + executeDataWriter.println("compileExeFileName='" + showNullAsEmpty(executionData.getCompileExeFileName()) + "'"); + executeDataWriter.println("compileSuccess='" + executionData.isCompileSuccess() + "'"); + executeDataWriter.println("compileResultCode='" + executionData.getCompileResultCode() + "'"); + executeDataWriter.println("executeExitValue='" + executionData.getExecuteExitValue() + "'"); + executeDataWriter.println("executeSuccess='" + executionData.isExecuteSucess() + "'"); + executeDataWriter.println("validationReturnCode ='" + executionData.getValidationReturnCode() + "'"); + executeDataWriter.println("validationSuccess='" + executionData.isValidationSuccess() + "'"); + executeDataWriter.println("validationResults='" + showNullAsEmpty(executionData.getValidationResults()) + "'"); + executeDataWriter.println("compileTimeMS='" + executionData.getCompileTimeMS() + "'"); + executeDataWriter.println("executeTimeMS='" + executionData.getExecuteTimeMS() + "'"); + executeDataWriter.println("validateTimeMS='" + executionData.getvalidateTimeMS() + "'"); + executeDataWriter.println("executionException='" + showNullAsEmpty(executionData.getExecutionException().getMessage()) + "'"); + executeDataWriter.println("runTimeLimitExceeded='" + executionData.isRunTimeLimitExceeded() + "'"); + executeDataWriter.println("additionalInformation='" + showNullAsEmpty(executionData.getAdditionalInformation()) + "'"); + bWritten = true; + } catch(Exception e) { + log.log(Log.WARNING, "Can not save execution data file " + executionDataFilename); + } + return bWritten; + } } diff --git a/src/edu/csus/ecs/pc2/core/execute/ExecutionData.java b/src/edu/csus/ecs/pc2/core/execute/ExecutionData.java index ec802fcd5..782853431 100644 --- a/src/edu/csus/ecs/pc2/core/execute/ExecutionData.java +++ b/src/edu/csus/ecs/pc2/core/execute/ExecutionData.java @@ -1,4 +1,4 @@ -// Copyright (C) 1989-2019 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. +// Copyright (C) 1989-2024 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. package edu.csus.ecs.pc2.core.execute; import java.io.Serializable; @@ -7,9 +7,9 @@ /** * Execution Data. - * + * * Contains data for compilation, execution and validation. - * + * * @author pc2@ecs.csus.edu * @version $Id$ */ @@ -19,7 +19,7 @@ public class ExecutionData implements Serializable { /** - * + * */ private static final long serialVersionUID = 3095803815304273632L; @@ -40,9 +40,9 @@ public class ExecutionData implements Serializable { private SerializedFile executeProgramOutput; // this is STDOUT of the execution private int executeExitValue = 0; - + private boolean executeSucess; - + private SerializedFile validationStdout; private SerializedFile validationStderr; // this is STDOUT of the validation @@ -52,23 +52,23 @@ public class ExecutionData implements Serializable { private boolean validationSuccess; private String validationResults; - + private long compileTimeMS = 0; - + private long executeTimeMS = 0; - + private long validateTimeMS = 0; - + private Exception executionException = null; - + private boolean runTimeLimitExceeded = false; - + private boolean memoryLimitExceeded = false; private boolean failedToCompile = false; private String additionalInformation = ""; - + /** * @return Returns the validationReturnCode. */ @@ -235,10 +235,10 @@ public SerializedFile getValidationStdout() { /** * Did the validator program run without an error ?. - * + * * This does NOT indicate that the team's run was a Yes/accepted. This * only returns whether the validator program ran without any OS or other sort of error. - * + * * @return Returns whether the validator program executed successfully. */ public boolean isValidationSuccess() { @@ -282,17 +282,17 @@ public void setExecutionException(Exception executionException) { } public void setCompileTimeMS(long compileTime) { - compileTimeMS = compileTime; + compileTimeMS = compileTime; } public long getCompileTimeMS() { return compileTimeMS; } - + public void setExecuteTimeMS(long inExecuteTime){ executeTimeMS = inExecuteTime; } - + public long getExecuteTimeMS(){ return executeTimeMS; } @@ -300,15 +300,15 @@ public long getExecuteTimeMS(){ public void setvalidateTimeMS(long validateTime){ validateTimeMS = validateTime; } - + public long getvalidateTimeMS(){ return validateTimeMS; } - + public boolean isExecuteSucess() { return executeSucess; } - + public void setExecuteSucess(boolean executeSucess) { this.executeSucess = executeSucess; } @@ -318,15 +318,17 @@ public void setExecuteSucess(boolean executeSucess) { * @return * @deprecated use {@link #isCompileSuccess()}. */ + @Deprecated public boolean isFailedToCompile() { return failedToCompile; } /** - * + * * @param failedToCompile * @deprecated use {@link #setCompileSuccess(boolean)} */ + @Deprecated public void setFailedToCompile(boolean failedToCompile) { this.failedToCompile = failedToCompile; } @@ -354,7 +356,7 @@ public boolean isMemoryLimitExceeded() { public void setMemoryLimitExceeded(boolean memoryLimitExceeded) { this.memoryLimitExceeded = memoryLimitExceeded; } - + public void setAdditionalInformation(String additionalInformation) { this.additionalInformation = additionalInformation; } @@ -366,7 +368,7 @@ public void setAdditionalInformation(String additionalInformation) { public String getAdditionalInformation() { return additionalInformation; } - + @Override public String toString() { String retStr = "["; @@ -381,7 +383,7 @@ public String toString() { retStr += "executeSucess=" + executeSucess + ","; retStr += "validationStdout=" + validationStdout + ","; retStr += "validationStderr=" + validationStderr + ","; - retStr += "validationReturnCode" + validationReturnCode + ","; + retStr += "validationReturnCode=" + validationReturnCode + ","; retStr += "validationSuccess=" + validationSuccess + ","; retStr += "validationResults=" + validationResults + ","; retStr += "compileTimeMS=" + compileTimeMS + ","; @@ -392,7 +394,7 @@ public String toString() { retStr += "failedToCompile=" + failedToCompile + ","; retStr += "additionalInformation=" + additionalInformation; retStr += "]"; - + return retStr; } } diff --git a/src/edu/csus/ecs/pc2/core/model/ContestInformation.java b/src/edu/csus/ecs/pc2/core/model/ContestInformation.java index 0338325e3..383d74fd2 100644 --- a/src/edu/csus/ecs/pc2/core/model/ContestInformation.java +++ b/src/edu/csus/ecs/pc2/core/model/ContestInformation.java @@ -1,4 +1,4 @@ -// Copyright (C) 1989-2023 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. +// Copyright (C) 1989-2024 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. package edu.csus.ecs.pc2.core.model; import java.io.Serializable; @@ -15,14 +15,14 @@ /** * Contest-wide Information/settings. - * + * * @author pc2@ecs.csus.edu */ public class ContestInformation implements Serializable{ /** - * + * */ private static final long serialVersionUID = -7333255582657988200L; @@ -34,33 +34,33 @@ public class ContestInformation implements Serializable{ private String contestTitle = "Programming Contest"; private String contestURL; - + private TeamDisplayMask teamDisplayMode = TeamDisplayMask.LOGIN_NAME_ONLY; private String judgesDefaultAnswer = "No response, read problem statement"; - + private boolean preliminaryJudgementsUsedByBoard = false; private boolean preliminaryJudgementsTriggerNotifications = false; - + private boolean sendAdditionalRunStatusInformation = false; - + private String judgeCDPBasePath = null; - + private String adminCDPBasePath = null; - + /** * Test mode allow run submission with override elapsed time. - * + * */ private boolean ccsTestMode = false; - + //Shadow Mode settings private boolean shadowMode = false; private String primaryCCS_URL = null; private String primaryCCS_user_login = ""; private String primaryCCS_user_pw = ""; private String lastShadowEventID = ""; - + /** * Global setting for maximum output allowed by a submission. * Note that this value can be overridden on a per-problem basis. @@ -69,35 +69,40 @@ public class ContestInformation implements Serializable{ /** * This is a list of the judgement notification end of contest control settings. - * + * */ private JudgementNotificationsList judgementNotificationsList = new JudgementNotificationsList(); - + private String externalYamlPath = null; - + private String rsiCommand = null; - + private int lastRunNumberSubmitted = 0; - + /** * Memory Limit for team's solution (application). */ private int memoryLimitInMeg = 0; - + /** * Sandbox application or application command line. */ private String sandboxCommandLine = ""; - + /** * Display string for team display on standings. - * + * * @see ScoreboardVariableReplacer#substituteDisplayNameVariables(String, Account, Group) */ private String teamScoreboardDisplayFormat = ScoreboardVariableReplacer.TEAM_NAME; /** - * + * String to append to the execute folder for judged runs. May contain substitution strings. + */ + private String judgesExecuteFolder = ""; + + /** + * * @author pc2@ecs.csus.edu */ public enum TeamDisplayMask { @@ -114,7 +119,7 @@ public enum TeamDisplayMask { */ DISPLAY_NAME_ONLY, /** - * name and number, teamN Johns Hopkins Team 1. + * name and number, teamN Johns Hopkins Team 1. */ NUMBERS_AND_NAME, /** @@ -124,15 +129,15 @@ public enum TeamDisplayMask { */ ALIAS, } - + private Properties scoringProperties = new Properties(); /** * Enable team auto registration. - * + * */ private boolean enableAutoRegistration = false; - + /** * The password type for the new passwords. */ @@ -143,20 +148,20 @@ public enum TeamDisplayMask { // * Contest Start/Date Time. // */ // private Date startDate; - + /** * The date/time when the contest is scheduled (intended) to start. * This value is null (undefined) if no scheduled start time has been set. - * This value ONLY applies BEFORE THE CONTEST STARTS; once + * This value ONLY applies BEFORE THE CONTEST STARTS; once * any "start contest" operation (e.g. pushing the "Start Button") has occurred, * this value no longer has meaning. */ private GregorianCalendar scheduledStartTime = null ; - + private boolean autoStartContest = false; private boolean autoStopContest = false ; - + /** * Scoreboard freeze time. */ @@ -167,44 +172,44 @@ public enum TeamDisplayMask { /** * Whether the contest is thawed(unfrozen). Meaning the final scoreboard * can be revealed (despite the scoreboad freeze). - * + * * Typically a contest is not thawed until the awards ceremony. */ private Date thawed = null; private boolean allowMultipleLoginsPerTeam; - - + + /** - * Load CDP samples/data in front of judges secret data. - * + * Load CDP samples/data in front of judges secret data. + * *
-     * # in yaml, do not load sample judges data 
+     * # in yaml, do not load sample judges data
      * loadSampleJudgesData : false
      * 
- * + * */ private boolean loadSampleJudgesData = true; - - + + /** * stop-on-first-failed-test-case - * + * */ private boolean stopOnFirstFailedtestCase = false; private String overrideLoadAccountsFilename = null; - + /** * Returns the date/time when the contest is scheduled (intended) to start. * This value is null if no scheduled start time has been set, - * or if the contest has already started. + * or if the contest has already started. * @see ContestTime#getContestStartTime() */ public GregorianCalendar getScheduledStartTime() { return scheduledStartTime; } - + /** * Receives a {@link GregorianCalendar} object specifying a (future) instant in time; * sets the specified date/time as the scheduled (intended) start time for the @@ -212,10 +217,10 @@ public GregorianCalendar getScheduledStartTime() { * after the contest has started, since the value of "scheduled start time" is meaningless after * the contest is under way. It is the responsibility of clients to insure * this method is only invoked with a non-null value before the contest has been started. - * + * */ public void setScheduledStartTime(GregorianCalendar newScheduledStartTime) { - scheduledStartTime = newScheduledStartTime; + scheduledStartTime = newScheduledStartTime; } public String getContestTitle() { @@ -248,7 +253,7 @@ public String getJudgesDefaultAnswer() { /** * judgesDefaultAnswer must be a non-zero length trimmed string. - * + * * @param judgesDefaultAnswer The judgesDefaultAnswer to set. */ public void setJudgesDefaultAnswer(String judgesDefaultAnswer) { @@ -256,7 +261,25 @@ public void setJudgesDefaultAnswer(String judgesDefaultAnswer) { this.judgesDefaultAnswer = judgesDefaultAnswer.trim(); } } - + + public String getJudgesExecuteFolder() { + if(judgesExecuteFolder == null) { + judgesExecuteFolder = ""; + } + return judgesExecuteFolder ; + } + + /** + * judgesDefaultAnswer must be a non-zero length trimmed string. + * + * @param judgesDefaultAnswer The judgesDefaultAnswer to set. + */ + public void setJudgesExecuteFolder(String judgesExecuteFolder) { + if (judgesExecuteFolder != null && judgesExecuteFolder.trim().length() > 0) { + this.judgesExecuteFolder = judgesExecuteFolder.trim(); + } + } + public boolean isSameAs(ContestInformation contestInformation) { try { if (contestTitle == null) { @@ -271,6 +294,9 @@ public boolean isSameAs(ContestInformation contestInformation) { if (!judgesDefaultAnswer.equals(contestInformation.getJudgesDefaultAnswer())) { return false; } + if (!judgesExecuteFolder.equals(contestInformation.getJudgesExecuteFolder())) { + return false; + } if (!teamDisplayMode.equals(contestInformation.getTeamDisplayMode())) { return false; } @@ -309,10 +335,10 @@ public boolean isSameAs(ContestInformation contestInformation) { } if (enableAutoRegistration != contestInformation.isEnableAutoRegistration()) { return false; - } + } if (! StringUtilities.stringSame(primaryCCS_URL, contestInformation.primaryCCS_URL)) { return false; - } + } if (! StringUtilities.stringSame(primaryCCS_user_login, contestInformation.primaryCCS_user_login)) { return false; } @@ -331,9 +357,9 @@ public boolean isSameAs(ContestInformation contestInformation) { //DateUtilities.dateSame() expects Date objects but ContestInformation now maintains // scheduledStartTime (formerly "StartDate") as a GregorianCalendar; need to convert. //Also need to first check for null references (to avoid NPEs on fetch of Date from GregorianCalendar) - - //If the references to scheduledStartTime in the two ContestInfos are such that one is null and the other - // is not, the ContestInfos are not the same so return false. + + //If the references to scheduledStartTime in the two ContestInfos are such that one is null and the other + // is not, the ContestInfos are not the same so return false. // Note that "one is null and the other is not null" can be computed with the ^ (XOR) operator: // "A XOR B" = true iff A != B if (scheduledStartTime==null ^ contestInformation.getScheduledStartTime()==null) { @@ -343,12 +369,12 @@ public boolean isSameAs(ContestInformation contestInformation) { //If both are null, this test for equality passes and we fall through to other cases //If both non-null, get Dates from both and compare them if (scheduledStartTime!=null /*and therefore contestInformation.getScheduledStartTime() also != null*/) { - if (!DateUtilities.dateSame(scheduledStartTime.getTime(), + if (!DateUtilities.dateSame(scheduledStartTime.getTime(), contestInformation.getScheduledStartTime().getTime())) { return false; } - } - + } + //both scheduledStartTime and contestInformation.getScheduledStartTime() must be null (hence, "same") //continue; @@ -366,21 +392,21 @@ public boolean isSameAs(ContestInformation contestInformation) { if (!StringUtilities.stringSame(judgeCDPBasePath, contestInformation.getJudgeCDPBasePath())) { return false; } - + if (!StringUtilities.stringSame(adminCDPBasePath, contestInformation.getAdminCDPBasePath())) { return false; } if (autoStopContest != contestInformation.isAutoStopContest()) { return false; } - + if (allowMultipleLoginsPerTeam != contestInformation.isAllowMultipleLoginsPerTeam()) { return false; } - + return true; } catch (Exception e) { - e.printStackTrace(System.err); // TODO log this exception + e.printStackTrace(System.err); // TODO log this exception return false; } } @@ -395,7 +421,7 @@ public void setPreliminaryJudgementsUsedByBoard(boolean preliminaryJudgementsUse /** * The Scoring Algorithm should use this to determine whether to count preliminary judgements * as scoreable. - * + * * @return the preliminaryJudgementsUsedByBoard */ public boolean isPreliminaryJudgementsUsedByBoard() { @@ -423,7 +449,7 @@ public boolean isSendAdditionalRunStatusInformation() { public void setSendAdditionalRunStatusInformation(boolean sendAdditionalRunStatusInformation) { this.sendAdditionalRunStatusInformation = sendAdditionalRunStatusInformation; } - + public JudgementNotificationsList getJudgementNotificationsList() { return judgementNotificationsList; } @@ -431,7 +457,7 @@ public JudgementNotificationsList getJudgementNotificationsList() { public void setJudgementNotificationsList(JudgementNotificationsList judgementNotificationsList) { this.judgementNotificationsList = judgementNotificationsList; } - + public void updateJudgementNotification (NotificationSetting notificationSetting ){ judgementNotificationsList.update(notificationSetting); } @@ -442,7 +468,7 @@ public void updateJudgementNotification (NotificationSetting notificationSetting * via a call to {@link Problem#setMaxOutputFileSizeKB(int)}; note also that * the parameter for {@link Problem#setMaxOutputFileSizeKB(int)} is in KB, whereas * the value returned by THIS method is in BYTES. - * + * * @return maximum output file size in bytes. */ public long getMaxOutputSizeInBytes() { @@ -455,21 +481,21 @@ public long getMaxOutputSizeInBytes() { * Problem does NOT have a specified problem-specific output size limit. * Note also that problem-specific output size limits are specified in KB, * whereas the parameter for this method gives the (global) output size limit in BYTES. - * + * * @param maxOutputSizeInBytes - * - * @see Problem#setMaxOutputSizeKB(long) + * + * @see Problem#setMaxOutputSizeKB(long) * @see Problem#getMaxOutputSizeKB() */ public void setMaxOutputSizeInBytes(long maxOutputSizeInBytes) { this.maxOutputSizeInBytes = maxOutputSizeInBytes; } - + /** * Scoring Properties. - * + * * Minute penalties, etc. - * + * * @return */ public Properties getScoringProperties() { @@ -483,11 +509,11 @@ public void setScoringProperties(Properties scoringProperties) { public boolean isCcsTestMode() { return ccsTestMode; } - + public void setCcsTestMode(boolean ccsTestMode) { this.ccsTestMode = ccsTestMode; } - + public boolean isShadowMode() { return shadowMode; } @@ -499,9 +525,9 @@ public void setShadowMode(boolean shadowMode) { public void setRsiCommand(String rsiCommand) { this.rsiCommand = rsiCommand; } - + /** - * Returns the String representation for a "Primary CCS" URL (that is, the URL of a Remote CCS being shadowed); + * Returns the String representation for a "Primary CCS" URL (that is, the URL of a Remote CCS being shadowed); * only relevant when operating this instance of the PC2 CCS as a "Shadow CCS". * @return a String containing the URL of the Primary CCS which we're shadowing */ @@ -510,7 +536,7 @@ public String getPrimaryCCS_URL() { } /** - * Sets the String representation for a "Primary CCS" URL (that is, the URL of a Remote CCS being shadowed); + * Sets the String representation for a "Primary CCS" URL (that is, the URL of a Remote CCS being shadowed); * only relevant when operating this instance of the PC2 CCS as a "Shadow CCS". * @param primaryCCS_URL a String giving the URL of the Primary (remote) CCS (the CCS being shadowed) */ @@ -519,7 +545,7 @@ public void setPrimaryCCS_URL(String primaryCCS_URL) { } /** - * Returns a String containing the user login account to be used when connecting + * Returns a String containing the user login account to be used when connecting * to a Primary CCS (only useful when operating this instance of PC2 as a "Shadow CCS"). * @return a String containing the Primary CCS user account name */ @@ -537,7 +563,7 @@ public void setPrimaryCCS_user_login(String primaryCCS_user_login) { } /** - * Returns a String containing the password used for logging in to the + * Returns a String containing the password used for logging in to the * Primary CCS (only useful when operating this instance of PC2 as a * "Shadow CCS"). * @return a String containing a password @@ -580,39 +606,39 @@ public void setLastShadowEventID(String lastShadowEventID) { /** * Get the Run Submission Interface (RSI) command. - * + * * @return the command string to run when each run is submitted. */ public String getRsiCommand() { return rsiCommand; } - + public void setExternalYamlPath(String externalYamlPath) { this.externalYamlPath = externalYamlPath; } /** * Get the external YAML path. - * + * * @return the location of contest.yaml and problem data files. */ public String getExternalYamlPath() { return externalYamlPath; } - + public void setLastRunNumberSubmitted(int lastRunNumberSubmitted) { this.lastRunNumberSubmitted = lastRunNumberSubmitted; } - + /** * Get the last run that was sent to the RSI. - * + * * @see #getRsiCommand() * @return */ public int getLastRunNumberSubmitted() { return lastRunNumberSubmitted; } - + public boolean isEnableAutoRegistration() { return enableAutoRegistration; } @@ -632,12 +658,12 @@ public void setAutoRegistrationPasswordType(PasswordType autoRegistrationPasswor /** * Sets the contest scheduled start time from the specified Date. * Note: previously, ContestInformation stored "startDate" as an object of - * class {@link Date}. It nows stores the scheduled start time as a + * class {@link Date}. It nows stores the scheduled start time as a * {@link GregorianCalendar}; however, this method is maintained for compatibility. * The method converts the given {@link Date} into an equivalent {@link GregorianCalendar} * and invokes {@link #scheduledStartTime} with the resulting {@link GregorianCalendar} object. - * - * @param startDate - the date at which the contest is scheduled to start; + * + * @param startDate - the date at which the contest is scheduled to start; * specifying "null" as the start date causes the scheduled start time to become undefined */ public void setScheduledStartDate(Date startDate) { @@ -649,7 +675,7 @@ public void setScheduledStartDate(Date startDate) { setScheduledStartTime(newStartDate); } } - + /** * Returns a {@link Date} object representing the scheduled start time for the contest, * or null if no scheduled start time has been set. @@ -661,19 +687,19 @@ public Date getScheduledStartDate() { if (scheduledStartTime == null) { return null; } else { - return scheduledStartTime.getTime(); + return scheduledStartTime.getTime(); } } - + public boolean isAutoStartContest() { return autoStartContest; } - + public void setAutoStartContest(boolean autoStartContest) { this.autoStartContest = autoStartContest; } - + public void setAutoStopContest(boolean autoStopContest) { this.autoStopContest = autoStopContest; } @@ -681,11 +707,11 @@ public void setAutoStopContest(boolean autoStopContest) { public boolean isAutoStopContest() { return autoStopContest; } - + public String getFreezeTime() { return freezeTime; } - + public void setFreezeTime(String freezeTime) { this.freezeTime = freezeTime; } @@ -693,16 +719,16 @@ public void setFreezeTime(String freezeTime) { public String getContestShortName() { return contestShortName; } - + public void setContestShortName(String contestShortName) { this.contestShortName = contestShortName; } - + /** * Set base path for CDP/external files on judge. - * + * * This is the base path location where the problem data files are located. - * + * * @param judgeCDPBasePath */ public void setJudgeCDPBasePath(String judgeCDPBasePath) { @@ -716,11 +742,11 @@ public void setJudgeCDPBasePath(String judgeCDPBasePath) { public String getJudgeCDPBasePath() { return judgeCDPBasePath; } - + public void setAdminCDPBasePath(String adminCDPBasePath) { this.adminCDPBasePath = adminCDPBasePath; } - + /** * Get base path for CDP on Admin. */ @@ -737,20 +763,20 @@ public boolean isUnfrozen() { /** * Returns the date the was thawed, or null if not thawed. - * + * * @return Date the contest was thawed, else null */ public Date getThawed() { return thawed; } - + /** * @param thawed when/if the contest is thawed */ public void setThawed(Date date) { thawed = date; } - + /** * @param thawed whether the contest is thawed */ @@ -764,38 +790,38 @@ public void setThawed(boolean thawed) { /** * Sets the boolean flag indicating whether or not teams are allowed to have multiple simultaneous logins. - * Note that this is a GLOBAL setting, configured on the Admin's "Configure Contest>Settings" screen (or via YAML); + * Note that this is a GLOBAL setting, configured on the Admin's "Configure Contest>Settings" screen (or via YAML); * either ALL teams are allowed to have multiple simultaneous logins, or NO team is allowed to have multiple simultaneous logins. - * + * * @param allowMultipleLoginsPerTeam whether or not a team is allowed to have multiple simultaneous login sessions. */ public void setAllowMultipleLoginsPerTeam(boolean allowMultipleLoginsPerTeam) { this.allowMultipleLoginsPerTeam = allowMultipleLoginsPerTeam; } - + /** * Returns a boolean indicating whether or not the Contest Settings allow teams to have multiple simultaneous logins. * Note that this is a GLOBAL setting, configured on the Admin's "Configure Contest>Settings" screen; either ALL teams * are allowed to have multiple simultaneous logins, or NO team is allowed to have multiple simultaneous logins. - * + * * @return a boolean indicating the current "allow multiple simultaneous logins" setting for teams. */ public boolean isAllowMultipleLoginsPerTeam() { - return this.allowMultipleLoginsPerTeam; + return this.allowMultipleLoginsPerTeam; } public boolean isStopOnFirstFailedtestCase() { return stopOnFirstFailedtestCase; } - + public void setStopOnFirstFailedtestCase(boolean stopOnFirstFailedtestCase) { this.stopOnFirstFailedtestCase = stopOnFirstFailedtestCase; } - - + + /** * Returns a string which defines which fields will be dispayed on the scoreboard. - * + * * @see ScoreboardVariableReplacer#substituteDisplayNameVariables(String, Account, Group) * @return */ @@ -827,11 +853,11 @@ public String getSandboxCommandLine() { public void setSandboxCommandLine(String sandboxCommandLine) { this.sandboxCommandLine = sandboxCommandLine; } - + public String getOverrideLoadAccountsFilename() { return overrideLoadAccountsFilename; } - + public void setOverrideLoadAccountsFilename(String overrideLoadAccountsFilename) { this.overrideLoadAccountsFilename = overrideLoadAccountsFilename; } @@ -839,7 +865,7 @@ public void setOverrideLoadAccountsFilename(String overrideLoadAccountsFilename) public boolean isLoadSampleJudgesData() { return loadSampleJudgesData; } - + public void setLoadSampleJudgesData(boolean loadSampleJudgesData) { this.loadSampleJudgesData = loadSampleJudgesData; } diff --git a/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java b/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java index e173da27b..ed3742985 100644 --- a/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java +++ b/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java @@ -46,6 +46,7 @@ import javax.swing.border.EmptyBorder; import javax.swing.border.LineBorder; import javax.swing.border.TitledBorder; + import edu.csus.ecs.pc2.core.CommandVariableReplacer; import edu.csus.ecs.pc2.core.IInternalController; import edu.csus.ecs.pc2.core.model.ContestInformation; @@ -57,56 +58,56 @@ /** * Contest Information edit/update Pane. - * + * * This pane displays and allows updating of Contest Information Settings. - * The pane uses a vertical BoxLayout to display a collection of settings sub-panes. Each settings sub-pane is a singleton + * The pane uses a vertical BoxLayout to display a collection of settings sub-panes. Each settings sub-pane is a singleton * which is constructed by a getter method. Each getter returns a self-contained pane (including that each returned pane * has a layout manager controlling how things are laid out within that pane and also has size and alignment * constraints defining how the components within that pane are managed by the layout manager for the pane). * Each sub-pane also has a CompoundBorder consisting of a {@link TitledBorder} compounded with a "margin border" * (an {@link EmptyBorder} with {@link Insets}); this provides an offset for each sub-pane within the outer pane. - * - * Method {@link #initialize()}, which is invoked whenever this ContestInformationPane is instantiated, adds two + * + * Method {@link #initialize()}, which is invoked whenever this ContestInformationPane is instantiated, adds two * components to *this* pane: a {@link JScrollPane} containing a "center pane" * (returned by {@link #getCenterPane()}), plus a button bar. The sub-panes displaying the Contest Information * Settings are added to the center pane (within the scroll pane) in the method {@link #getCenterPane()}. - * - * Method {@link #setContestAndController()} (which is expected to be invoked by any client instantiating - * this ContestInformationPane class) invokes method {@link #populateGUI()}, which in turn invokes + * + * Method {@link #setContestAndController()} (which is expected to be invoked by any client instantiating + * this ContestInformationPane class) invokes method {@link #populateGUI()}, which in turn invokes * {@link IInternalController.getContest().getContestInformation()} to obtain the current contest information settings from * the server; it then uses the returned values to initialize the GUI display settings. - * - * Each active component in the pane (including its sub-panes) has a listener attached to it. Whenever a component + * + * Each active component in the pane (including its sub-panes) has a listener attached to it. Whenever a component * is changed (key typed/release, checkbox checked, button pushed, etc.) it invokes method {@link #enableUpdateButton()}. * This method (despite its name) doesn't actually necessarily *enable* the Update button; rather, it invokes {@link #getFromFields()} - * to obtain the data currently displayed in the GUI fields and compares it with the current contest information settings. - * If they DIFFER then the Update and Cancel buttons are enabled. Subsequently pressing the Update button invokes - * {@link #updateContestInformation()}, which (again) invokes {@link #getFromFields()} to fetch the GUI settings and then + * to obtain the data currently displayed in the GUI fields and compares it with the current contest information settings. + * If they DIFFER then the Update and Cancel buttons are enabled. Subsequently pressing the Update button invokes + * {@link #updateContestInformation()}, which (again) invokes {@link #getFromFields()} to fetch the GUI settings and then * invokes {@link IInternalController.updateContestInformation(contestInformation)} to save the new GUI information in the * local controller (which presumably responds by saving it on the server). - * + * * Developer's Notes: - * + * * To add a new sub-pane to this ContestInformationPane, define a getter method (e.g. getNewPane()) * which returns the new pane as an instance of {@link JPanel}, and add a call centerPane.add(getNewPane()) * in method {@link #getCenterPane()}. - * + * * To add a new {@link JComponent} to an *existing* sub-pane, first create an accessor which creates the new component - * (for example, getNewComponent()), then go to the getter method for the sub-pane to which the new component - * is to be added (for example, {@link #getCCSTestModePane()}) and add to the body of that - * method an "add" statement which calls the new getter (for example, in the body of {@link #getCCSTestModePane()} you might add + * (for example, getNewComponent()), then go to the getter method for the sub-pane to which the new component + * is to be added (for example, {@link #getCCSTestModePane()}) and add to the body of that + * method an "add" statement which calls the new getter (for example, in the body of {@link #getCCSTestModePane()} you might add * ccsTestModePane.add(getNewComponent()). Note that the new component could be either an individual component * (such as a JLabel or JCheckBox) or a {@link JPanel} which itself contains sub-components. - * + * * Note that you may (probably will) have to adjust the maximum, minimum, and preferred sizes of the pane to which the * new component is being added in order to accommodate the new component in the layout. Note also that you must include * the necessary size and alignment attributes in any new component being added. - * + * * Note also that if you add new information to the GUI, you must update {@link #getFromFields()} to fetch the new information from * the GUI fields and save it, and you must update method {@link ContestInformation#isSameAs(ContestInformation)} to include * a check of the new information. - * - * + * + * * @author pc2@ecs.csus.edu */ public class ContestInformationPane extends JPanePlugin { @@ -139,6 +140,8 @@ public class ContestInformationPane extends JPanePlugin { private JTextField judgesDefaultAnswerTextField = null; + private JTextField judgesExecuteFolderTextField = null; + private JCheckBox jCheckBoxShowPreliminaryOnBoard = null; private JCheckBox jCheckBoxShowPreliminaryOnNotifications = null; @@ -160,7 +163,7 @@ public class ContestInformationPane extends JPanePlugin { private JTextField runSubmissionInterfaceCommandTextField = null; private JLabel runSubmissionInterfaceLabel = null; - + private JTextField startTimeTextField; private JLabel startTimeLabel; @@ -179,8 +182,11 @@ public class ContestInformationPane extends JPanePlugin { private JPanel judgesDefaultAnswerPane; + private JPanel judgesExecutePane; + private JLabel judgesExecuteFolderWhatsThisButton; + private JPanel judgingOptionsPane; - + private ScoringPropertiesPane scoringPropertiesPane; private JPanel teamSettingsPane; @@ -233,17 +239,17 @@ public class ContestInformationPane extends JPanePlugin { private JLabel teamScoreboardDisplayFormatLabel; private JLabel teamDisplayFormatWhatsThisButton; - + // private JTextField textfieldPrimaryCCSURL; // // private JTextField textfieldPrimaryCCSLogin; // // private JTextField textfieldPrimaryCCSPasswd; - + /** * This method initializes this Contest Information Pane - * + * */ public ContestInformationPane() { super(); @@ -252,22 +258,22 @@ public ContestInformationPane() { /** * This method initializes this - * + * */ private void initialize() { this.setLayout(new BorderLayout()); this.setSize(new Dimension(900, 700)); - + //put the center pane in a scrollpane so the user can access it without expanding the window JScrollPane sp = new JScrollPane(getCenterPane()); this.add(sp,BorderLayout.CENTER); - + this.add(getButtonPanel(), java.awt.BorderLayout.SOUTH); } /** * This method initializes buttonPanel - * + * * @return javax.swing.JPanel */ private JPanel getButtonPanel() { @@ -285,98 +291,98 @@ private JPanel getButtonPanel() { /** * This method initializes centerPane - the central pane containing the Contest Information Settings * and control components. - * + * * @return javax.swing.JPanel */ private JPanel getCenterPane() { if (centerPane == null) { - + centerPane = new JPanel(); centerPane.setToolTipText(""); centerPane.setLayout(new BoxLayout(centerPane,BoxLayout.Y_AXIS)); - + //contents of the pane: - + centerPane.add(Box.createVerticalStrut(15)); centerPane.add(getContestSettingsPane()) ; centerPane.add(Box.createVerticalStrut(15)); - + centerPane.add(getJudgingSettingsPane(),null); centerPane.add(Box.createVerticalStrut(15)); - + centerPane.add(getTeamSettingsPane()); centerPane.add(Box.createVerticalStrut(15)); - + centerPane.add(getRemoteCCSSettingsPane()); centerPane.add(Box.createVerticalStrut(15)); - + } return centerPane; } private Component getRemoteCCSSettingsPane() { if (remoteCCSSettingsPane == null) { - + remoteCCSSettingsPane = new JPanel(); remoteCCSSettingsPane.setAlignmentX(LEFT_ALIGNMENT); remoteCCSSettingsPane.setMaximumSize(new Dimension(900, 250)); remoteCCSSettingsPane.setMinimumSize(new Dimension(900, 250)); remoteCCSSettingsPane.setPreferredSize(new Dimension(900,250)); - - + + if (showPaneOutlines) { - + TitledBorder titleBorder = new TitledBorder("Remote CCS Settings "); titleBorder.setBorder(lineBorderBlue2px); remoteCCSSettingsPane.setBorder(new CompoundBorder(margin,titleBorder)); - + } else { remoteCCSSettingsPane.setBorder(new EmptyBorder(2,2,2,2)); } - + remoteCCSSettingsPane.setLayout(new BoxLayout(remoteCCSSettingsPane, BoxLayout.Y_AXIS)); //the contents of the pane: - + remoteCCSSettingsPane.add(Box.createVerticalStrut(15)); - + remoteCCSSettingsPane.add(getCCSTestModePane(),JComponent.LEFT_ALIGNMENT); remoteCCSSettingsPane.add(Box.createVerticalStrut(15)); - + remoteCCSSettingsPane.add(getShadowSettingsPane(),JComponent.LEFT_ALIGNMENT); } return remoteCCSSettingsPane; - + } private JPanel getCCSTestModePane() { if (ccsTestModePane == null) { - + ccsTestModePane = new JPanel(); ccsTestModePane.setLayout(new FlowLayout(FlowLayout.LEFT)); ccsTestModePane.setPreferredSize(new Dimension(700, 80)); ccsTestModePane.setMaximumSize(new Dimension(700, 80)); ccsTestModePane.setMinimumSize(new Dimension(700, 80)); - + TitledBorder tb = BorderFactory.createTitledBorder("CCS Test Mode"); ccsTestModePane.setBorder(new CompoundBorder(margin,tb)); - ccsTestModePane.setAlignmentX(LEFT_ALIGNMENT); - + ccsTestModePane.setAlignmentX(LEFT_ALIGNMENT); + //the contents of the pane: - + ccsTestModePane.add(getCcsTestModeCheckbox(), null); ccsTestModePane.add(getHorizontalStrut_2()); - + ccsTestModePane.add(getRunSubmissionCommandPane(),null); - + } return ccsTestModePane; - + } @@ -384,15 +390,15 @@ private JPanel getRunSubmissionCommandPane() { if (runSubmissionCommandPane == null) { runSubmissionCommandPane = new JPanel(); runSubmissionCommandPane.setMaximumSize(new Dimension(500, 20)); - + runSubmissionInterfaceLabel = new JLabel(); runSubmissionInterfaceLabel.setHorizontalTextPosition(SwingConstants.TRAILING); runSubmissionInterfaceLabel.setText("Run Submission Command: "); runSubmissionInterfaceLabel.setToolTipText("The command used to submit to a remote CCS"); runSubmissionInterfaceLabel.setHorizontalAlignment(SwingConstants.RIGHT); - + //the contents of the pane: - + runSubmissionCommandPane.add(runSubmissionInterfaceLabel, null); runSubmissionCommandPane.add(getRunSubmissionInterfaceCommandTextField(), null); @@ -402,9 +408,9 @@ private JPanel getRunSubmissionCommandPane() { private Component getScoreboardFreezePane() { if (scoreboardFreezePane == null) { - + scoreboardFreezePane = new JPanel(); - + scoreboardFreezePane.add(getContestFreezeLengthLabel(),null); scoreboardFreezePane.add(getContestFreezeLengthtextField()); @@ -423,7 +429,7 @@ private Component getScheduledStartTimePane() { private JLabel getContestFreezeLengthLabel() { if (contestFreezeLengthLabel == null) { - + contestFreezeLengthLabel = new JLabel(); contestFreezeLengthLabel.setText("Scoreboard Freeze Length (hh:mm:ss) "); contestFreezeLengthLabel.setHorizontalTextPosition(SwingConstants.TRAILING); @@ -442,12 +448,12 @@ private Component getContestSettingsPane() { contestSettingsPane.setAlignmentX(LEFT_ALIGNMENT); if (showPaneOutlines) { - + TitledBorder titleBorder = new TitledBorder("Contest Settings"); titleBorder.setBorder(lineBorderBlue2px); - + contestSettingsPane.setBorder(new CompoundBorder(margin,titleBorder)); - + } else { contestSettingsPane.setBorder(new EmptyBorder(2, 2, 2, 2)); } @@ -469,9 +475,9 @@ private Component getContestSettingsPane() { private JPanel getContestTitlePane() { if (contestTitlePane == null) { - + contestTitlePane = new JPanel(); - + contestTitlePane.add(getContestTitleLabel()); contestTitlePane.add(getContestTitleTextField(), null); @@ -480,9 +486,9 @@ private JPanel getContestTitlePane() { } private JLabel getContestTitleLabel() { - + if (contestTitleLabel == null) { - + contestTitleLabel = new JLabel("Contest title: "); } return contestTitleLabel; @@ -495,38 +501,40 @@ private JLabel getContestTitleLabel() { */ private JPanel getJudgingSettingsPane() { if (judgeSettingsPane == null) { - + judgeSettingsPane = new JPanel(); - + judgeSettingsPane.setAlignmentX(LEFT_ALIGNMENT); - judgeSettingsPane.setMaximumSize(new Dimension(800, 425)); - judgeSettingsPane.setMinimumSize(new Dimension(800, 425)); - judgeSettingsPane.setPreferredSize(new Dimension(800,375)); + judgeSettingsPane.setMaximumSize(new Dimension(800, 600)); + judgeSettingsPane.setMinimumSize(new Dimension(800, 600)); + judgeSettingsPane.setPreferredSize(new Dimension(800,550)); if (showPaneOutlines) { - + TitledBorder titleBorder = new TitledBorder("Judging Settings"); titleBorder.setBorder(lineBorderBlue2px); - + judgeSettingsPane.setBorder(new CompoundBorder(margin,titleBorder)); } else { judgeSettingsPane.setBorder(new EmptyBorder(2,2,2,2)); } - + judgeSettingsPane.setLayout(new FlowLayout((FlowLayout.LEFT))); - + //the contents of the pane: - + judgeSettingsPane.add(Box.createVerticalStrut(15)); judgeSettingsPane.add(getTeamInformationDisplaySettingsPane(), LEFT_ALIGNMENT); - + judgeSettingsPane.add(getJudgesDefaultAnswerPane(),LEFT_ALIGNMENT); - + judgeSettingsPane.add(getJudgingOptionsPane(),LEFT_ALIGNMENT); - + + judgeSettingsPane.add(getJudgesExecutePane(),LEFT_ALIGNMENT); + judgeSettingsPane.add(getScoringPropertiesPane(),LEFT_ALIGNMENT); - + judgeSettingsPane.add(Box.createHorizontalStrut(20)); } @@ -535,14 +543,14 @@ private JPanel getJudgingSettingsPane() { private Component getTeamSettingsPane() { if (teamSettingsPane == null ) { - + teamSettingsPane = new JPanel(); teamSettingsPane.setMaximumSize(new Dimension(800, 120)); teamSettingsPane.setPreferredSize(new Dimension(800,120)); - teamSettingsPane.setAlignmentX(LEFT_ALIGNMENT); + teamSettingsPane.setAlignmentX(LEFT_ALIGNMENT); if (showPaneOutlines) { - + TitledBorder titleBorder = new TitledBorder("Team Settings"); titleBorder.setBorder(lineBorderBlue2px); @@ -551,11 +559,11 @@ private Component getTeamSettingsPane() { } else { teamSettingsPane.setBorder(new EmptyBorder(2,2,2,2)); } - + teamSettingsPane.setLayout(new FlowLayout(FlowLayout.LEFT)); //contents of the pane: - + teamSettingsPane.add(getMaxOutputSizeLabel(), null); teamSettingsPane.add(getMaxOutputSizeInKTextField(), null); teamSettingsPane.add(getRigidArea1()); @@ -570,9 +578,9 @@ private JPanel getTeamScoreboardDisplayFormatPane() { if (teamScoreboardDisplayFormatPane==null) { teamScoreboardDisplayFormatPane = new JPanel(); - + //contents of the pane: - + teamScoreboardDisplayFormatPane.add(getTeamScoreboardDisplayFormatLabel()); teamScoreboardDisplayFormatPane.add(getTeamScoreboardDisplayFormatTextfield()); teamScoreboardDisplayFormatPane.add(getTeamScoreboardDisplayFormatWhatsThisButton()); @@ -581,7 +589,7 @@ private JPanel getTeamScoreboardDisplayFormatPane() { } private JLabel getTeamScoreboardDisplayFormatWhatsThisButton() { - + if (teamDisplayFormatWhatsThisButton == null) { Icon questionIcon = UIManager.getIcon("OptionPane.questionIcon"); if (questionIcon == null || !(questionIcon instanceof ImageIcon)) { @@ -628,11 +636,11 @@ public void mousePressed(MouseEvent e) { + "\n {:sitenumber} -- the PC^2 site number (in a multi-site contest) to which the team logs in (e.g., \"1\" or \"5\")" // + "\n {:countrycode} -- the ISO Country Code associated with the team (e.g. \"CAN\" or \"USA\")" // + "\n {:externalid} -- the ICPC CMS id number (if any) associated with the team (e.g., \"309407\")" // - + + "\n\nSo for example a display format string like \"{:teamname} ({:shortschoolname}) might display the following on the scoreboard:" // + "\n Hot Coders (CSUS) " // + "\n(Notice the addition of the literal parentheses around the short school name.)" // - + + "\n\nSubstitution values depend on the corresponding data having been loaded into the PC^2 Server; if there is no value defined for a" // + "\nspecified substitution string then the substitution string itself appears in the result." + " If the defined value is null or empty then an empty string appears in the result." @@ -642,8 +650,9 @@ public void mousePressed(MouseEvent e) { private JTextField getTeamScoreboardDisplayFormatTextfield() { if (teamScoreboardDisplayFormatTextfield==null) { teamScoreboardDisplayFormatTextfield = new JTextField("Undefined",30); - + teamScoreboardDisplayFormatTextfield.addKeyListener(new java.awt.event.KeyAdapter() { + @Override public void keyReleased(java.awt.event.KeyEvent e) { enableUpdateButton(); } @@ -661,7 +670,7 @@ private Component getTeamScoreboardDisplayFormatLabel() { private JLabel getMaxOutputSizeLabel() { if (labelMaxOutputSize == null) { - + labelMaxOutputSize = new JLabel(); labelMaxOutputSize.setHorizontalAlignment(SwingConstants.RIGHT); labelMaxOutputSize.setBorder(new EmptyBorder(0,10,5,5)); @@ -672,22 +681,22 @@ private JLabel getMaxOutputSizeLabel() { /** * This method initializes teamDisplaySettingPane - * + * * @return javax.swing.JPanel */ private JPanel getTeamInformationDisplaySettingsPane() { if (teamInformationDisplaySettingsPane == null) { - + teamInformationDisplaySettingsPane = new JPanel(); teamInformationDisplaySettingsPane.setMaximumSize(new Dimension(700, 200)); teamInformationDisplaySettingsPane.setAlignmentX(Component.LEFT_ALIGNMENT); - + teamInformationDisplaySettingsPane.setLayout(new FlowLayout(FlowLayout.LEFT)); - - teamInformationDisplaySettingsPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, + + teamInformationDisplaySettingsPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Team Information Displayed to Judges", javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, javax.swing.border.TitledBorder.DEFAULT_POSITION, null, null)); - + //contents of pane: teamInformationDisplaySettingsPane.add(getDisplayNoneRadioButton(), null); teamInformationDisplaySettingsPane.add(getHorizontalStrut()); @@ -706,23 +715,23 @@ private JPanel getScoringPropertiesPane() { if (scoringPropertiesPane == null) { scoringPropertiesPane = new ScoringPropertiesPane(getUpdateButton(),getCancelButton()); - scoringPropertiesPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Scoring Properties", + scoringPropertiesPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Scoring Properties", javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, javax.swing.border.TitledBorder.DEFAULT_POSITION, null, null)); - + } return scoringPropertiesPane; } - + private JPanel getJudgingOptionsPane() { if (judgingOptionsPane == null) { - + judgingOptionsPane = new JPanel(); - + judgingOptionsPane.setLayout(new BoxLayout(judgingOptionsPane,BoxLayout.Y_AXIS)); - judgingOptionsPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Judging Options", + judgingOptionsPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Judging Options", javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, javax.swing.border.TitledBorder.DEFAULT_POSITION, null, null)); judgingOptionsPane.add(getJCheckBoxShowPreliminaryOnBoard(), LEFT_ALIGNMENT); @@ -735,25 +744,47 @@ private JPanel getJudgingOptionsPane() { private JPanel getJudgesDefaultAnswerPane() { if (judgesDefaultAnswerPane == null) { - + judgesDefaultAnswerPane = new JPanel(); judgesDefaultAnswerPane.setMaximumSize(new Dimension(500, 200)); judgesDefaultAnswerPane.setAlignmentX(Component.LEFT_ALIGNMENT); - + judgesDefaultAnswerPane.setLayout(new FlowLayout(FlowLayout.LEFT)); - - judgesDefaultAnswerPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Judge's Default Answer", + + judgesDefaultAnswerPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Judge's Default Answer", javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, javax.swing.border.TitledBorder.DEFAULT_POSITION, null, null)); //the contents of the pane: - + judgesDefaultAnswerPane.add(getJudgesDefaultAnswerTextField(), null); } return judgesDefaultAnswerPane; } + private JPanel getJudgesExecutePane() { + if (judgesExecutePane == null) { + + judgesExecutePane = new JPanel(); + judgesExecutePane.setMaximumSize(new Dimension(500, 200)); + judgesExecutePane.setAlignmentX(Component.LEFT_ALIGNMENT); + + judgesExecutePane.setLayout(new FlowLayout(FlowLayout.LEFT)); + + judgesExecutePane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Execute Folder", + javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, + javax.swing.border.TitledBorder.DEFAULT_POSITION, null, null)); + + //the contents of the pane: + + judgesExecutePane.add(getJudgesExecuteFolderTextField(), null); + judgesExecutePane.add(getJudgesExecuteFolderWhatsThisButton(), null); + + } + return judgesExecutePane; + } + /** * Returns a ShadowSettingsPane containing the components comprising Shadow Mode configuration options. * Because the ShadowSettingsPane class does not add any listeners to its components (because it can't @@ -761,14 +792,15 @@ private JPanel getJudgesDefaultAnswerPane() { * {@link JTextField} component on the ShadowSettingsPane, and adds an ActionListener to the Shadow Mode * checkbox on the ShadowSettingsPane. All of these listeners do the same (one) thing: invoke * {@link #enableUpdateButton()}. - * + * * @return a ShadowSettingsPane containing Shadow Mode Settings components with listeners attached to them */ private ShadowSettingsPane getShadowSettingsPane() { if (shadowSettingsPane == null) { shadowSettingsPane = new ShadowSettingsPane(); - + KeyListener keyListener = new java.awt.event.KeyAdapter() { + @Override public void keyReleased(java.awt.event.KeyEvent e) { enableUpdateButton(); } @@ -778,12 +810,13 @@ public void keyReleased(java.awt.event.KeyEvent e) { shadowSettingsPane.getRemoteCCSPasswdTextfield().addKeyListener(keyListener); ActionListener actionListener = new ActionListener() { + @Override public void actionPerformed(ActionEvent e) { enableUpdateButton(); } }; shadowSettingsPane.getShadowModeCheckbox().addActionListener(actionListener); - + } return shadowSettingsPane; } @@ -792,10 +825,11 @@ public void actionPerformed(ActionEvent e) { private JButton getUnfreezeScoreboardButton() { if (unfreezeScoreboardButton == null) { - + unfreezeScoreboardButton = new JButton("Unfreeze Scoreboard"); unfreezeScoreboardButton.setToolTipText("Unfreezing means the final results can be released to the public via the Contest API and public html"); unfreezeScoreboardButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { String message = "Unfreezing the scoreboard is permanent (cannot be undone);" + "\nunfreezing means the final results are released for public viewing." @@ -815,6 +849,7 @@ private JTextField getContestFreezeLengthtextField() { if (contestFreezeLengthTextField == null) { contestFreezeLengthTextField = new JTextField(8); contestFreezeLengthTextField.addKeyListener(new java.awt.event.KeyAdapter() { + @Override public void keyReleased(java.awt.event.KeyEvent e) { enableUpdateButton(); } @@ -842,7 +877,7 @@ private JLabel getStartTimeLabel() { } private JCheckBox getShadowModeCheckbox() { - + if (shadowModeCheckbox==null) { shadowModeCheckbox = getShadowSettingsPane().getShadowModeCheckbox(); } @@ -850,11 +885,11 @@ private JCheckBox getShadowModeCheckbox() { } private JTextField getPrimaryCCSURLTextfield() { - + if (primaryCCSURLTextfield==null) { primaryCCSURLTextfield = getShadowSettingsPane().getRemoteCCSURLTextfield() ; } - return primaryCCSURLTextfield; + return primaryCCSURLTextfield; } private JTextField getPrimaryCCSLoginTextfield() { @@ -866,7 +901,7 @@ private JTextField getPrimaryCCSLoginTextfield() { } private JTextField getPrimaryCCSPasswdTextfield() { - + if (primaryCCSPasswdTextfield==null) { primaryCCSPasswdTextfield = getShadowSettingsPane().getRemoteCCSPasswdTextfield() ; } @@ -876,7 +911,7 @@ private JTextField getPrimaryCCSPasswdTextfield() { /** * This method initializes updateButton - * + * * @return javax.swing.JButton */ private JButton getUpdateButton() { @@ -887,6 +922,7 @@ private JButton getUpdateButton() { updateButton.setPreferredSize(new java.awt.Dimension(100, 26)); updateButton.setMnemonic(java.awt.event.KeyEvent.VK_U); updateButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { updateContestInformation(); } @@ -897,7 +933,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes contestTitleTextField - * + * * @return javax.swing.JTextField */ private JTextField getContestTitleTextField() { @@ -905,6 +941,7 @@ private JTextField getContestTitleTextField() { contestTitleTextField = new JTextField(0); //'0' causes textfield to resize based on its data contestTitleTextField.setAlignmentX(LEFT_ALIGNMENT); contestTitleTextField.addKeyListener(new java.awt.event.KeyAdapter() { + @Override public void keyReleased(java.awt.event.KeyEvent e) { enableUpdateButton(); } @@ -918,6 +955,7 @@ public String getPluginTitle() { return "Contest Information Pane"; } + @Override public void setContestAndController(IInternalContest inContest, IInternalController inController) { super.setContestAndController(inContest, inController); @@ -937,15 +975,15 @@ public void setContestAndController(IInternalContest inContest, IInternalControl protected ContestInformation getFromFields() { ContestInformation newContestInformation = new ContestInformation(); ContestInformation currentContestInformation = getContest().getContestInformation(); - + //fill in the Contest URL if (currentContestInformation.getContestURL() != null) { newContestInformation.setContestURL(new String(currentContestInformation.getContestURL())); } - + //fill in the Contest Title newContestInformation.setContestTitle(getContestTitleTextField().getText()); - + //fill in the Team Display mode if (getDisplayNoneRadioButton().isSelected()) { newContestInformation.setTeamDisplayMode(TeamDisplayMask.NONE); @@ -961,17 +999,18 @@ protected ContestInformation getFromFields() { // DEFAULT newContestInformation.setTeamDisplayMode(TeamDisplayMask.LOGIN_NAME_ONLY); } - + //fill in judging information newContestInformation.setJudgesDefaultAnswer(getJudgesDefaultAnswerTextField().getText()); + newContestInformation.setJudgesExecuteFolder(getJudgesExecuteFolderTextField().getText()); newContestInformation.setPreliminaryJudgementsTriggerNotifications(getJCheckBoxShowPreliminaryOnNotifications().isSelected()); newContestInformation.setPreliminaryJudgementsUsedByBoard(getJCheckBoxShowPreliminaryOnBoard().isSelected()); newContestInformation.setSendAdditionalRunStatusInformation(getAdditionalRunStatusCheckBox().isSelected()); - + //fill in older Run Submission Interface (RSI) data newContestInformation.setRsiCommand(getRunSubmissionInterfaceCommandTextField().getText()); newContestInformation.setCcsTestMode(getCcsTestModeCheckbox().isSelected()); - + //fill in Shadow Mode information newContestInformation.setShadowMode(getShadowSettingsPane().getShadowModeCheckbox().isSelected()); newContestInformation.setPrimaryCCS_URL(getShadowSettingsPane().getRemoteCCSURLTextfield().getText()); @@ -979,7 +1018,7 @@ protected ContestInformation getFromFields() { newContestInformation.setPrimaryCCS_user_pw(getShadowSettingsPane().getRemoteCCSPasswdTextfield().getText()); // preserve last shadow event since there is no way to change it on this pane. newContestInformation.setLastShadowEventID(currentContestInformation.getLastShadowEventID()); - + //fill in additional field values String maxFileSizeString = "0" + getMaxOutputSizeInKTextField().getText(); long maximumFileSize = Long.parseLong(maxFileSizeString); @@ -990,27 +1029,27 @@ protected ContestInformation getFromFields() { //fill in values already saved, if any if (savedContestInformation != null) { newContestInformation.setJudgementNotificationsList(savedContestInformation.getJudgementNotificationsList()); - + newContestInformation.setJudgeCDPBasePath(savedContestInformation.getJudgeCDPBasePath()); newContestInformation.setScheduledStartDate(savedContestInformation.getScheduledStartDate()); - + newContestInformation.setAdminCDPBasePath(savedContestInformation.getAdminCDPBasePath()); newContestInformation.setContestShortName(savedContestInformation.getContestShortName()); newContestInformation.setExternalYamlPath(savedContestInformation.getExternalYamlPath()); - + //TODO: why is the following being done here when it is overridden below? newContestInformation.setFreezeTime(savedContestInformation.getFreezeTime()); - + newContestInformation.setLastRunNumberSubmitted(savedContestInformation.getLastRunNumberSubmitted()); newContestInformation.setAutoStartContest(savedContestInformation.isAutoStartContest()); } newContestInformation.setScoringProperties(scoringPropertiesPane.getProperties()); - + newContestInformation.setFreezeTime(contestFreezeLengthTextField.getText()); newContestInformation.setThawed(scoreboardHasBeenUnfrozen); - + return (newContestInformation); } @@ -1023,7 +1062,7 @@ protected void dumpProperties(String comment, Properties properties) { Set set = properties.keySet(); - String[] keys = (String[]) set.toArray(new String[set.size()]); + String[] keys = set.toArray(new String[set.size()]); Arrays.sort(keys); @@ -1050,38 +1089,40 @@ void setEnableButtons(boolean isEnabled) { private void populateGUI() { SwingUtilities.invokeLater(new Runnable() { + @Override public void run() { ContestInformation contestInformation = getContest().getContestInformation(); - + getContestTitleTextField().setText(contestInformation.getContestTitle()); selectDisplayRadioButton(); - + getJudgesDefaultAnswerTextField().setText(contestInformation.getJudgesDefaultAnswer()); + getJudgesExecuteFolderTextField().setText(contestInformation.getJudgesExecuteFolder()); getJCheckBoxShowPreliminaryOnBoard().setSelected(contestInformation.isPreliminaryJudgementsUsedByBoard()); getJCheckBoxShowPreliminaryOnNotifications().setSelected(contestInformation.isPreliminaryJudgementsTriggerNotifications()); getAdditionalRunStatusCheckBox().setSelected(contestInformation.isSendAdditionalRunStatusInformation()); - + getMaxOutputSizeInKTextField().setText((contestInformation.getMaxOutputSizeInBytes() / 1024) + ""); getAllowMultipleTeamLoginsCheckbox().setSelected(contestInformation.isAllowMultipleLoginsPerTeam()); getTeamScoreboardDisplayFormatTextfield().setText(contestInformation.getTeamScoreboardDisplayFormat()); getContestFreezeLengthtextField().setText(contestInformation.getFreezeTime()); - + getCcsTestModeCheckbox().setSelected(contestInformation.isCcsTestMode()); getRunSubmissionInterfaceCommandTextField().setText(contestInformation.getRsiCommand()); if (contestInformation.getRsiCommand() == null || "".equals(contestInformation.getRsiCommand().trim())) { String cmd = "# /usr/local/bin/sccsrs " + CommandVariableReplacer.OPTIONS + " " + CommandVariableReplacer.FILELIST; getRunSubmissionInterfaceCommandTextField().setText(cmd); } - + getShadowModeCheckbox().setSelected(contestInformation.isShadowMode()); getPrimaryCCSURLTextfield().setText(contestInformation.getPrimaryCCS_URL()); getPrimaryCCSLoginTextfield().setText(contestInformation.getPrimaryCCS_user_login()); getPrimaryCCSPasswdTextfield().setText(contestInformation.getPrimaryCCS_user_pw()); - + //add the scheduled start time to the GUI GregorianCalendar cal = contestInformation.getScheduledStartTime(); - getStartTimeTextField().setText(getScheduledStartTimeStr(cal)); + getStartTimeTextField().setText(getScheduledStartTimeStr(cal)); getUnfreezeScoreboardButton().setSelected(contestInformation.isUnfrozen()); setContestInformation(contestInformation); @@ -1092,12 +1133,12 @@ public void run() { }); } - + /** * Convert a GregorianCalendar date/time to a displayable string in yyyy-mm-dd:hh:mm form. */ private String getScheduledStartTimeStr(GregorianCalendar cal) { - + String retString = ""; if (cal != null) { //extract fields from input and build string @@ -1115,35 +1156,40 @@ private String getScheduledStartTimeStr(GregorianCalendar cal) { private void updateContestInformation() { ContestInformation contestInformation = getFromFields(); - + getController().updateContestInformation(contestInformation); } class ContestInformationListenerImplementation implements IContestInformationListener { + @Override public void contestInformationAdded(ContestInformationEvent event) { populateGUI(); savedContestInformation = event.getContestInformation(); } + @Override public void contestInformationChanged(ContestInformationEvent event) { populateGUI(); savedContestInformation = event.getContestInformation(); } + @Override public void contestInformationRemoved(ContestInformationEvent event) { // TODO Auto-generated method stub } + @Override public void contestInformationRefreshAll(ContestInformationEvent contestInformationEvent) { populateGUI(); savedContestInformation = getContest().getContestInformation(); - + } - + + @Override public void finalizeDataChanged(ContestInformationEvent contestInformationEvent) { // Not used } @@ -1184,7 +1230,7 @@ private void selectDisplayRadioButton() { /** * This method initializes displayNoneButton - * + * * @return javax.swing.JRadioButton */ private JRadioButton getDisplayNoneRadioButton() { @@ -1192,6 +1238,7 @@ private JRadioButton getDisplayNoneRadioButton() { displayNoneRadioButton = new JRadioButton(); displayNoneRadioButton.setText("None"); displayNoneRadioButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { // getActionCommand called with text from button // getSource returns the JRadioButton @@ -1204,7 +1251,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes displayNumbersOnlyRadioButton - * + * * @return javax.swing.JRadioButton */ private JRadioButton getDisplayNumbersOnlyRadioButton() { @@ -1212,6 +1259,7 @@ private JRadioButton getDisplayNumbersOnlyRadioButton() { displayNumbersOnlyRadioButton = new JRadioButton(); displayNumbersOnlyRadioButton.setText("Show Numbers Only"); displayNumbersOnlyRadioButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { // getActionCommand called with text from button // getSource returns the JRadioButton @@ -1224,7 +1272,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes displayNameAndNumberRadioButton - * + * * @return javax.swing.JRadioButton */ private JRadioButton getDisplayNameAndNumberRadioButton() { @@ -1232,6 +1280,7 @@ private JRadioButton getDisplayNameAndNumberRadioButton() { displayNameAndNumberRadioButton = new JRadioButton(); displayNameAndNumberRadioButton.setText("Show Number and Name"); displayNameAndNumberRadioButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { // getActionCommand called with text from button // getSource returns the JRadioButton @@ -1244,7 +1293,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes displayAliasNameRadioButton - * + * * @return javax.swing.JRadioButton */ private JRadioButton getDisplayAliasNameRadioButton() { @@ -1252,6 +1301,7 @@ private JRadioButton getDisplayAliasNameRadioButton() { displayAliasNameRadioButton = new JRadioButton(); displayAliasNameRadioButton.setText("Show Alias"); displayAliasNameRadioButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { // getActionCommand called with text from button // getSource returns the JRadioButton @@ -1264,7 +1314,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes showNamesOnlyRadioButton - * + * * @return javax.swing.JRadioButton */ private JRadioButton getDisplayNamesOnlyRadioButton() { @@ -1272,6 +1322,7 @@ private JRadioButton getDisplayNamesOnlyRadioButton() { displayNamesOnlyRadioButton = new JRadioButton(); displayNamesOnlyRadioButton.setText("Show Names only"); displayNamesOnlyRadioButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { // getActionCommand called with text from button // getSource returns the JRadioButton @@ -1284,7 +1335,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes displayNameButtonGroup - * + * * @return javax.swing.ButtonGroup */ private ButtonGroup getDisplayNameButtonGroup() { @@ -1302,7 +1353,7 @@ private ButtonGroup getDisplayNameButtonGroup() { /** * This method initializes cancelButton - * + * * @return javax.swing.JButton */ private JButton getCancelButton() { @@ -1313,6 +1364,7 @@ private JButton getCancelButton() { cancelButton.setMnemonic(KeyEvent.VK_C); cancelButton.setPreferredSize(new java.awt.Dimension(100, 26)); cancelButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { populateGUI(); } @@ -1323,7 +1375,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes JudgesDefaultAnswerTextField - * + * * @return javax.swing.JTextField */ private JTextField getJudgesDefaultAnswerTextField() { @@ -1333,6 +1385,7 @@ private JTextField getJudgesDefaultAnswerTextField() { // judgesDefaultAnswerTextField.setSize(new Dimension(280, 29)); // judgesDefaultAnswerTextField.setLocation(new Point(208, 214)); judgesDefaultAnswerTextField.addKeyListener(new java.awt.event.KeyAdapter() { + @Override public void keyReleased(java.awt.event.KeyEvent e) { enableUpdateButton(); } @@ -1341,9 +1394,84 @@ public void keyReleased(java.awt.event.KeyEvent e) { return judgesDefaultAnswerTextField; } + /** + * This method initializes JudgesExecuteFolderTextField + * + * @return javax.swing.JTextField + */ + private JTextField getJudgesExecuteFolderTextField() { + if (judgesExecuteFolderTextField == null) { + judgesExecuteFolderTextField = new JTextField(50); + judgesExecuteFolderTextField.setText(""); + judgesExecuteFolderTextField.addKeyListener(new java.awt.event.KeyAdapter() { + @Override + public void keyReleased(java.awt.event.KeyEvent e) { + enableUpdateButton(); + } + }); + } + return judgesExecuteFolderTextField; + } + + private JLabel getJudgesExecuteFolderWhatsThisButton() { + + if (judgesExecuteFolderWhatsThisButton == null) { + Icon questionIcon = UIManager.getIcon("OptionPane.questionIcon"); + if (questionIcon == null || !(questionIcon instanceof ImageIcon)) { + // the current PLAF doesn't have an OptionPane.questionIcon that's an ImageIcon + judgesExecuteFolderWhatsThisButton = new JLabel(""); + judgesExecuteFolderWhatsThisButton.setForeground(Color.blue); + } else { + Image image = ((ImageIcon) questionIcon).getImage(); + judgesExecuteFolderWhatsThisButton = new JLabel(new ImageIcon(getScaledImage(image, 20, 20))); + } + + judgesExecuteFolderWhatsThisButton.setToolTipText("What's This? (click for additional information)"); + judgesExecuteFolderWhatsThisButton.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + JOptionPane.showMessageDialog(null, judgesExecuteFolderWhatsThisMessage, "About Judges Execute Folder", JOptionPane.INFORMATION_MESSAGE, null); + } + }); + judgesExecuteFolderWhatsThisButton.setBorder(new EmptyBorder(0, 15, 0, 0)); + } + return judgesExecuteFolderWhatsThisButton; + } + + // the string which will be displayed when the "What's This" icon in the Team Settings panel is clicked + private String judgesExecuteFolderWhatsThisMessage = // + "\nThe Judges Execute Folder field allows you to specify a string which gets used as the judge's execute folder " // + + "\neg. \"executesite1judge1\"" // + + + "\n\nThe string is a pattern which may contain \"substitution variables\", identified by substrings starting with \"{:\"" // + + " and ending with \"}\" (for example, {:runnumber} )." // + + "\nPC^2 automatically replaces substitution variables with the corresponding value for each team" // + + " (for example, the substitution variable {:runnumber} " // + + "\ngets replaced with the current Run's ID Number defined by the PC^2 Server)." // + + + "\n\nLiteral characters (i.e., anything NOT part of a substituion variable) are displayed exactly as written in the format string." // + + + "\n\nSome useful, recognized substitution variables include:" // + + "\n {:runnumber} -- the run Id number, eg. 220 for Run Id 220" // + + "\n {:basename} -- mainfile without extension (hello)" + + "\n {:languageid} -- CLICS language ID (cpp, java, kotlin, etc.)" + + "\n {:problemletter} -- A,B,C..." + + "\n {:problemshort} -- problem short name" + + "\n {:teamid} -- team number (23)" + + + "\n\nAny substitution string available for an executable is allowed here." + + + "\n\nSo for example a judge's execute folder string like \"executesite{:siteid}judge{:clientname}_Run_{:runnumber}\" would change the execute folder to something like:" // + + "\n executesite1judge1_Run_220 " // + + + "\n\nSubstitution values depend on the corresponding data having been loaded into the PC^2 Server; if there is no value defined for a" // + + "\nspecified substitution string then the substitution string itself appears in the result." + + " If the defined value is null or empty then an empty string appears in the result." + + "\n\n"; // + /** * This method initializes jCheckBoxShowPreliminaryOnBoard - * + * * @return javax.swing.JCheckBox */ private JCheckBox getJCheckBoxShowPreliminaryOnBoard() { @@ -1355,6 +1483,7 @@ private JCheckBox getJCheckBoxShowPreliminaryOnBoard() { jCheckBoxShowPreliminaryOnBoard.setMnemonic(KeyEvent.VK_UNDEFINED); jCheckBoxShowPreliminaryOnBoard.setText("Include Preliminary Judgements in Scoring Algorithm"); jCheckBoxShowPreliminaryOnBoard.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { enableUpdateButton(); } @@ -1365,7 +1494,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes jCheckBoxShowPreliminaryOnNotifications - * + * * @return javax.swing.JCheckBox */ private JCheckBox getJCheckBoxShowPreliminaryOnNotifications() { @@ -1378,6 +1507,7 @@ private JCheckBox getJCheckBoxShowPreliminaryOnNotifications() { jCheckBoxShowPreliminaryOnNotifications.setMnemonic(KeyEvent.VK_UNDEFINED); jCheckBoxShowPreliminaryOnNotifications.setText("Send Balloon Notifications for Preliminary Judgements"); jCheckBoxShowPreliminaryOnNotifications.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { enableUpdateButton(); } @@ -1388,7 +1518,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes additionalRunStatusCheckBox - * + * * @return javax.swing.JCheckBox */ private JCheckBox getAdditionalRunStatusCheckBox() { @@ -1399,6 +1529,7 @@ private JCheckBox getAdditionalRunStatusCheckBox() { additionalRunStatusCheckBox.setText("Send Additional Run Status Information"); additionalRunStatusCheckBox.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { enableUpdateButton(); } @@ -1422,17 +1553,18 @@ public void setContestInformation(ContestInformation contestInformation) { /** * This method initializes maxFieldSizeInKTextField - * + * * @return javax.swing.JTextField */ private JTextField getMaxOutputSizeInKTextField() { if (textfieldMaxOutputSizeInK == null) { - + textfieldMaxOutputSizeInK = new JTextField(6); - + textfieldMaxOutputSizeInK.setDocument(new IntegerDocument()); textfieldMaxOutputSizeInK.addKeyListener(new java.awt.event.KeyAdapter() { + @Override public void keyReleased(java.awt.event.KeyEvent e) { enableUpdateButton(); } @@ -1440,31 +1572,33 @@ public void keyReleased(java.awt.event.KeyEvent e) { } return textfieldMaxOutputSizeInK; } - + private JCheckBox getAllowMultipleTeamLoginsCheckbox() { if (allowMultipleTeamLoginsCheckbox==null) { allowMultipleTeamLoginsCheckbox = new JCheckBox("Allow multiple logins per team", true); allowMultipleTeamLoginsCheckbox.addActionListener (new ActionListener() { - + + @Override public void actionPerformed(ActionEvent e) { enableUpdateButton(); } }); - + } return allowMultipleTeamLoginsCheckbox ; } private JCheckBox getCcsTestModeCheckbox() { if (ccsTestModeCheckbox == null) { - + ccsTestModeCheckbox = new JCheckBox(); - + ccsTestModeCheckbox.setText("Enable CCS Test Mode"); ccsTestModeCheckbox.setToolTipText("CCS Test Mode is used to allow PC2 to forward team submissions to a remote" + " Contest Control System via the CLICS 'Run Submission Interface'"); ccsTestModeCheckbox.setMnemonic(KeyEvent.VK_T); ccsTestModeCheckbox.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { enableUpdateButton(); } @@ -1475,7 +1609,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes runSubmissionInterfaceCommandTextField - * + * * @return javax.swing.JTextField */ private JTextField getRunSubmissionInterfaceCommandTextField() { @@ -1484,6 +1618,7 @@ private JTextField getRunSubmissionInterfaceCommandTextField() { runSubmissionInterfaceCommandTextField.setMaximumSize(new Dimension(2147483647, 20)); runSubmissionInterfaceCommandTextField.setText(""); runSubmissionInterfaceCommandTextField.addKeyListener(new java.awt.event.KeyAdapter() { + @Override public void keyReleased(java.awt.event.KeyEvent e) { enableUpdateButton(); } diff --git a/support/judge_webcgi/judge b/support/judge_webcgi/judge new file mode 100644 index 000000000..3d850abff --- /dev/null +++ b/support/judge_webcgi/judge @@ -0,0 +1,178 @@ +#!/bin/bash +PC2_RUN_DIR=/home/icpc/pc2 +PC2_CDP=${PC2_RUN_DIR}/current/config +BANNER_FILE=banner.png +BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} +###PROBLEMSET_YAML=problemset.yaml +###PROBLEMSET_YAML_PATH=${PC2_CDP}/${PROBLEMSET_YAML} + +###declare -A problet_to_name + +# +# Display very curt textual error message +# +Error() +{ + echo "Content-type: text/plain" + echo "" + echo ERROR $* + echo $now Error: $* >> $LOGFILE +} + +###ParseProblemYaml() +###{ +### CURDIR="$PWD" +### tmpdir=/tmp/probset$$ +### mkdir $tmpdir +### # Need a copy since web doesnt have access to full path +### cp ${PROBLEMSET_YAML_PATH} $tmpdir +### cd $tmpdir +### csplit --prefix="x$USER" -q ${PROBLEMSET_YAML} "/^ *- /" "{*}" +### for file in $(echo "x$USER"*) +### do +### letter=`sed -n -e 's/^ *letter: \([A-Z]\).*/\1/p' < $file` +### short=`sed -n -e 's/.* short-name: \(.*\)$/\1/p' < $file` +### if test -n "$letter" -a -n "$short" +### then +### problet_to_name[$letter]="$short" +### fi +### done +### cd $CURDIR +### rm -r $tmpdir +###} + +Preamble() +{ + echo "Content-type: text/html" + echo "" +} + +Header() +{ + if test ! -e ../${BANNER_FILE} + then + ln -s ${BANNER_IMAGE} ../${BANNER_FILE} + fi +cat << EOF + + + +PC² Judge 1 + + + +
+ +

PC2 Judging Results for Judge 1

+
+

+EOF +} + +Trailer() +{ +cat << EOF2 + + +EOF2 +} + +StartTable() +{ + cat << EOF3 +

+ + + +EOF3 +} + +EndTable() +{ + cat << EOF4 +
Run IDTeamProblemLanguageTime Judged
+

+EOF4 +} + +TableRow() +{ + dir="$1" + runid=$2 + problet=$3 + shortname=$4 + langid=$5 + teamnum=$6 + runtime=`stat -c '%y' $dir` + probname="" + probdir="" + if test -n ${shortname} + then + probdir=${PC2_CDP}/${shortname} + probstatement=${probdir}/problem_statement/problem.en.tex + if test ! -s ${probstatement} + then + probstatement=${probdir}/problem_statement/problem.tex + fi + if test -s ${probstatement} + then + probname=`head -1 ${probstatement}` + probname=${probname##*{} + probname=${probname%\}} + fi + fi + problem="$problet - $probname (${shortname})" + echo ''"Run $runidteam$teamnum$problem$langid$runtime" +} + +###ParseProblemYaml +Preamble +Header +StartTable +# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid +for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+$' | sort --field-separator=_ +1rn` +do + # exdir looks like: ex_188_Y_compression_46103_cpp + # RId P ProbShort team# Lang + # RId = Run ID + # P = problem letter + # Lang = CLICS Language id + saveIFS="$IFS" + IFS="_" + set ${exdir} + IFS="$saveIFS" + if test $# -eq 6 + then + runid=$2 + problet=$3 + probshort=$4 + teamnum=$5 + langid=$6 + TableRow "${PC2_RUN_DIR}/$exdir" $runid $problet $probshort $langid $teamnum + fi +done +EndTable +Trailer +exit 0 diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh new file mode 100644 index 000000000..c4bfbe7b0 --- /dev/null +++ b/support/judge_webcgi/showrun.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# +# Display very curt textual error message +# +Error() +{ + echo "Content-type: text/plain" + echo "" + echo ERROR $* + echo $now Error: $* >> $LOGFILE +} + +Preamble() +{ + echo "Content-type: text/html" + echo "" +} + +Header() +{ +cat << EOF + + + +PC² Judge 1 + + +

+

PC2 Judging Results for Judge 1

+
+

+EOF +} + +Trailer() +{ +cat << EOF2 + + +EOF2 +} + +StartTable() +{ + cat << EOF3 +

+ + + +EOF3 +} + +EndTable() +{ + cat << EOF4 +
Run IDTime Judged
+

+EOF4 +} + +TableRow() +{ + dir="$1" + runid=${dir#../Run} + runtime=`stat -c '%y' $dir` + echo ''"Run $runid$runtime" +} + +Preamble +Header + +# Parse query string into dictionary args +sIFS="$IFS" +IFS='=&' +declare -a parm +parm=($QUERY_STRING) +IFS=$sIFS +declare -A args +for ((i=0; i<${#parm[@]}; i+=2)) +do + args[${parm[i]}]=${parm[i+1]} + echo "${parm[i]} = ${args[${parm[i]}]}
" +done +# Done parsing + +echo '

The run is '${args["run"]} in problem directory ${args["probdir"]}. The execute folder is: ${args["dir"]} +Trailer +exit 0 From e4e0fc701790f0af380e139723a46c62d875ed73 Mon Sep 17 00:00:00 2001 From: John Buck Date: Sun, 30 Jun 2024 21:36:18 -0400 Subject: [PATCH 02/55] i_972 Add script to fetch judgement from execute folder Ongoing work in making human judging of results easier. --- support/judge_webcgi/getjudgment.sh | 114 ++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 support/judge_webcgi/getjudgment.sh diff --git a/support/judge_webcgi/getjudgment.sh b/support/judge_webcgi/getjudgment.sh new file mode 100644 index 000000000..b24514dfc --- /dev/null +++ b/support/judge_webcgi/getjudgment.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# Where to the the result of the first failure. If this file is not created, then the +# run was accepted. (correct) +RESULT_FAILURE_FILE=failure.txt + +# Where judgments are +REJECT_INI=$HOME/pc2/reject.ini + +# Where PC2 puts CLICS validator results +EXECUTE_DATA_PREFIX=executedata + +declare -A Judgments + +InitJudgments() +{ + # Defauls, may be overriden by reject.ini + Judgments["accepted"]="AC" + Judgments["Accepted"]="AC" + Judgments["timelimit"]="TLE" + Judgments["run error"]="RTE" + Judgments["compiler error"]="CE" + if test -s ${REJECT_INIT} + then + while read j + do + if [[ $j = \#* ]] + then + continue + fi + savIFS="$IFS" + IFS='|' + set $j + IFS="$savIFS" + key="$1" + shortcode="$2" + case ${shortcode} in + AC|CE|RTE|WA|TLE) ;; + MLE) shortcode="RTE (MLE)" ;; + *) shortcode="WA (${shortcode})" ;; + esac +# echo Mapping $key to $shortcode + Judgments[$key]="$shortcode" + done < $REJECT_INI + fi +} + +# Takes the judgement string, eg. "Wrong answer" as arg 1 and the validation Result (42 or 42) as arg 2 +MapJudgment() +{ + jm="$1" + vr="$2" + jv=${Judgments[$jm]} + if test -z ${jv} + then + if test ${validationReturnCode} -eq 0 + then + if test ${vr} = "43" + then + jv="WA" + elif test ${vr} = "42" + then + jv="AC" + else + jv="WA (Default)" + fi + else + jv="JE (Validator EC=${validationReturnCode})" + fi + + fi + echo $jv +} + +GetJudgment() +{ + dir=$1 + if ! cd ${dir} + then + echo "Not found" + else + # We got a real live run + # Check out the biggest executedata file + exdata=`ls ${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sort -t. +1rn | head -1` + if test -z "${exdata}" + then + echo "No results" + else + # Source the file + . ./${exdata} + if test ${compileSuccess} = "false" + then + echo "CE" + elif test ${executeSuccess} = "true" + then + if test ${validationSuccess} = "true" + then + MapJudgment "${validationResults}" "${validationReturnCode}" + else + echo "JE (Validator error)" + fi + else + echo "RTE (Execute error)" + fi + fi + fi +} + +InitJudgments + +for file in $* +do + j=`GetJudgment $file` + echo $file: $j +done From e3970a4524bec974ef24acc2ba2a4e2a559db431 Mon Sep 17 00:00:00 2001 From: John Buck Date: Mon, 1 Jul 2024 16:39:45 -0400 Subject: [PATCH 03/55] i_972 Update judge web scripts More updates to make judging easier. Add judge number support {:clientid} to execute folder in scripts --- support/judge_webcgi/Correct.png | Bin 0 -> 46323 bytes support/judge_webcgi/Wrong.png | Bin 0 -> 47549 bytes support/judge_webcgi/cgi-bin/cdpcommon.sh | 21 +++ support/judge_webcgi/cgi-bin/judge.sh | 120 ++++++++++++ .../{getjudgment.sh => cgi-bin/pc2common.sh} | 83 +++++--- support/judge_webcgi/cgi-bin/problemyaml.sh | 26 +++ support/judge_webcgi/cgi-bin/showrun.sh | 25 +++ support/judge_webcgi/cgi-bin/webcommon.sh | 154 +++++++++++++++ support/judge_webcgi/judge | 178 ------------------ support/judge_webcgi/showrun.sh | 106 ----------- 10 files changed, 406 insertions(+), 307 deletions(-) create mode 100644 support/judge_webcgi/Correct.png create mode 100644 support/judge_webcgi/Wrong.png create mode 100644 support/judge_webcgi/cgi-bin/cdpcommon.sh create mode 100644 support/judge_webcgi/cgi-bin/judge.sh rename support/judge_webcgi/{getjudgment.sh => cgi-bin/pc2common.sh} (51%) create mode 100644 support/judge_webcgi/cgi-bin/problemyaml.sh create mode 100644 support/judge_webcgi/cgi-bin/showrun.sh create mode 100644 support/judge_webcgi/cgi-bin/webcommon.sh delete mode 100644 support/judge_webcgi/judge delete mode 100644 support/judge_webcgi/showrun.sh diff --git a/support/judge_webcgi/Correct.png b/support/judge_webcgi/Correct.png new file mode 100644 index 0000000000000000000000000000000000000000..6e73ae8871b9ebb40efe1d237d97388b6855c7b6 GIT binary patch literal 46323 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

|;6-?wa-BR;twrttn%Og)F3)$0E7z!E*^cI3 z1=N+VU8?lOzbQ{YWIGBEcxc$3jo%_}U2phTEZ}=ZmmruU$#~3a{~sFOoj4Wc#qyQ~ zS-D64xB%aw<$1oAP|hpVBg$gYzb;!<(eP!D?&(9P209CXrM>CW?~&$Cjt%(Dw>~fT zuskL5RLs!mH+{3CNBrgdMzvc6#MW z@NIE)_%(i51(0t?p**lZ)Jyx8jeJS#hLjaPZR$?aihNXai$&Yd0E)X}TSioT6{`n`8tYed&5SVqy9u zfIG==e^0(2r?Tz0s0?KfRrae`gBbb_^o)?cry0L@Io@7NB$lwFvSrlpO7p;LEBmld z#0o7#USd@nG|JTyZt@%nOFoF(`mXTV=|-S{{&g>#=$`APxm*X<_lankb9t6G!Z&W) zlXn@{f_miqTOCIYq8DTd{E5xgDrBhn8jdeFdBT*KPy#G&*? zz)FpB(%2(^na6qr#v6#pea}Sln}X>C)^q8mQ%zn*-6G)LN{hdUv>(b`BeS;&euqyf zJuw0V(I$1vn$mA3vm?sK4euP7Dg^yU(a7msQ%y35ca8Azd^cBKea^Efi`wqb%rRm} zbsAbhyO?6hXWf<&XZ-GzLZbgvsjhh!dDrqtET-#n0;{M>gynz&O@vg3WiILDbsb)q zURS_AHN}CS=6E}Uq8!CM%;S4wtxLLvFM%Q{b$cofw|#iO;t{S^jj^^qzaxpaLSm`w z;PbWH>=w6-xold4D$sVVOP=59*glihsO@o^DSs~!#+!7H73{xp*y5-r=NdeZqRy|= zTL}N~X_m+!xU!1cWIfZ`+j<2)a|jT?^bFIlVIJzDCa4X$~cMXbjSy>6=|J8;no za(WHFU-nxFya1zV{uz-|T&>{$>FQlMF&sH(#*-fGL%E#3YbO3O#A;Cgqh)nM6{*5M zCHw~K)}H`%ipi%1QOq_RsyLur`Jw1f!r+m0PDhe&47mOio{kk2yX{k=GQC+AeE(iF zf%rDrmU8)8q!N!$98K;d>sF=%xSfn5nGQ+QTF1UEPE#@8x&z-nWp*JiX+=HC`e^%Y zN+US*56jnEEWG!mCfx0KdqSQ6Ag@3UKhk~lc(&-2$JEWl86I@nT;crVCZ))OtuGd+ z#q%OQ5&6|WLm^ST2fli_QKrW-0v{@IA=4&~;Jp{fhzhnrdn{lf3 z`Hfl{0r;Uv*1;fhiAqx|oTS?Ii3Jsv8|oy>vDvR-=xh5IfCB^itE`> z-S5P0p8oTNDZN6*H6pbSxJ*lyu<+Fy$Yc=qs!ht;itib{a zx=nHomyRA*T`6JMLJt39E#uo<_kGJG9b2NMkj8bl9S@x%V5G8NyLdLIx5)SK>knE( zPvz)NhD=+@&7EnAb5UuBg|FL<4MKyIh{_3vOqQMh`oZBd6Di?Rmrf|JQR4s9?zGON zlJ1~tCo~mOmUvXm@?(rkWJM|v_a19?m zF?0yNn*f~kxRwU+C#(g=w|@~J-blA@+UzO~TPu{q`wq7mHP|kRs+2uFXJ4O06i^>% t95lL2a2pBJ@;(LSPX9mth*9m-vvue06_I#VjUWN2C~7K{$Upz*{{ZoLNEZMA literal 0 HcmV?d00001 diff --git a/support/judge_webcgi/Wrong.png b/support/judge_webcgi/Wrong.png new file mode 100644 index 0000000000000000000000000000000000000000..43401f44b4516d7f934179af25ca905ef07fbf9c GIT binary patch literal 47549 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 literal 0 HcmV?d00001 diff --git a/support/judge_webcgi/cgi-bin/cdpcommon.sh b/support/judge_webcgi/cgi-bin/cdpcommon.sh new file mode 100644 index 000000000..e1ced5e2f --- /dev/null +++ b/support/judge_webcgi/cgi-bin/cdpcommon.sh @@ -0,0 +1,21 @@ +# This file is meant to be 'source'd' to give functionality to read the CDP + +CountSamples() +{ + datadir="$1" + if test -d ${datadir} + then + result=`ls $datadir/*.in | wc -l` + else + result=0 + fi +} + +GetNumberOfTestCases() +{ + probdir="$1" + CountSamples $probdir/data/sample + tot=$result + CountSamples $probdir/data/secret + result=$((tot+result)) +} diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh new file mode 100644 index 000000000..5c9d129a0 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -0,0 +1,120 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + +TableHeader() +{ + cat << EOFTH + +Run IDDispJudgmentProblemTeamTest CasesLanguageJudgeTime Judged + +EOFTH +} + +TableRow() +{ + dir="$1" + runid=$2 + problet=$3 + shortname=$4 + langid=$5 + teamnum=$6 + judgment="$7" + runtime="$8" + testinfo="$9" + judge="${10}" + probname="" + probdir="" + if test -n ${shortname} + then + probdir=${PC2_CDP}/${shortname} + probstatement=${probdir}/problem_statement/problem.en.tex + if test ! -s ${probstatement} + then + probstatement=${probdir}/problem_statement/problem.tex + fi + if test -s ${probstatement} + then + probname=`head -1 ${probstatement}` + probname=${probname##*\{} + probname=${probname%\}} + fi + fi + problem="$problet - $probname (${shortname})" + if test "${judgment}" = "AC" + then + jstyle="green" + jicon='' + else + jstyle="red" + jicon='' + fi + echo ' ' + echo ' '"Run $runid" +# echo ' '$judgment"" +# echo '

'$judgment"
" + echo ' '$jicon'' + echo ' '$judgment'' + echo " $problem" + echo " team$teamnum" + echo " $testinfo" + echo " $langid" + echo " $judge" + echo " $runtime" + echo " " +} + +###ParseProblemYaml +Preamble +Header +StartTable +TableHeader + +# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid_judge +for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+' | sort --field-separator=_ +1rn` +do + # exdir looks like: ex_188_Y_compression_46103_cpp + # RId P ProbShort team# Lang + # RId = Run ID + # P = problem letter + # Lang = CLICS Language id + saveIFS="$IFS" + IFS="_" + set ${exdir} + IFS="$saveIFS" + if test $# -ge 6 + then + exedir=${PC2_RUN_DIR}/$exdir + runid=$2 + problet=$3 + probshort=$4 + teamnum=$5 + langid=$6 + judge=$7 + if test -z "${judge}" + then + judge="N/A" + fi + GetJudgment "${exedir}" + judgment="${result}" + runtime="${executeDateTime}" + # Get how many total test cases there are + probdir=${PC2_CDP}/${probshort} + if test -n "${probdir}" + then + GetNumberOfTestCases "${probdir}" + numcases=${result} + else + numcases="??" + fi + # Note that GetJudgment also filled in exdata with the last execute data + GetLastTestCaseNumber "${exdata}" + testcaseinfo=$((result+1))/${numcases} + + TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" + fi +done +EndTable +Trailer +exit 0 diff --git a/support/judge_webcgi/getjudgment.sh b/support/judge_webcgi/cgi-bin/pc2common.sh similarity index 51% rename from support/judge_webcgi/getjudgment.sh rename to support/judge_webcgi/cgi-bin/pc2common.sh index b24514dfc..3db5cd6c4 100644 --- a/support/judge_webcgi/getjudgment.sh +++ b/support/judge_webcgi/cgi-bin/pc2common.sh @@ -1,10 +1,21 @@ -#!/bin/bash -# Where to the the result of the first failure. If this file is not created, then the -# run was accepted. (correct) +# Meant to be "source'd" into bash scripts. + +# The judge's home directory +JUDGE_HOME=/home/icpc + +# Modify constants as necessary for your installation +PC2_RUN_DIR=${JUDGE_HOME}/pc2 +PC2_CDP=${PC2_RUN_DIR}/current/config + +# Where can we find the contest banner png file for webpage headers +BANNER_FILE=banner.png +BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} + +# Where to the the result of script failure before execution RESULT_FAILURE_FILE=failure.txt # Where judgments are -REJECT_INI=$HOME/pc2/reject.ini +REJECT_INI=${JUDGE_HOME}/pc2/reject.ini # Where PC2 puts CLICS validator results EXECUTE_DATA_PREFIX=executedata @@ -19,7 +30,7 @@ InitJudgments() Judgments["timelimit"]="TLE" Judgments["run error"]="RTE" Judgments["compiler error"]="CE" - if test -s ${REJECT_INIT} + if test -s ${REJECT_INI} then while read j do @@ -41,6 +52,7 @@ InitJudgments() # echo Mapping $key to $shortcode Judgments[$key]="$shortcode" done < $REJECT_INI + else echo NO REJECT FILE fi } @@ -49,57 +61,87 @@ MapJudgment() { jm="$1" vr="$2" - jv=${Judgments[$jm]} - if test -z ${jv} + result=${Judgments[$jm]} + if test -z ${result} then if test ${validationReturnCode} -eq 0 then if test ${vr} = "43" then - jv="WA" + resul="WA" elif test ${vr} = "42" then - jv="AC" + result="AC" else - jv="WA (Default)" + result="WA (Default)" fi else - jv="JE (Validator EC=${validationReturnCode})" + result="JE (Validator EC=${validationReturnCode})" fi fi - echo $jv +} + +GetLastJudgmentFile() +{ + lj_exdir="$1" + result=`ls $lj_exdir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1rn | head -1` +} + +GetLastTestCaseNumber() +{ + if test -z "$1" + then + result=0 + else + saveIFS="$IFS" + IFS=. + set ${1} + result="$2" + IFS="$saveIFS" + if test -z "${result}" + then + result=0 + fi + fi } GetJudgment() { dir=$1 + exdata="" if ! cd ${dir} then - echo "Not found" + result="Not found" + elif test -s ${RESULT_FAILURE_FILE} + then + jerr=`cat ${RESULT_FAILURE_FILE}` + result="JE ($jerr)" else # We got a real live run # Check out the biggest executedata file - exdata=`ls ${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sort -t. +1rn | head -1` +# exdata=`ls ${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sort -t. +1rn | head -1` + GetLastJudgmentFile $dir + exdata=${result} if test -z "${exdata}" then - echo "No results" + result="No results" else # Source the file . ./${exdata} if test ${compileSuccess} = "false" then - echo "CE" + result="CE" elif test ${executeSuccess} = "true" then if test ${validationSuccess} = "true" then MapJudgment "${validationResults}" "${validationReturnCode}" else - echo "JE (Validator error)" + result="JE (Validator error)" fi else - echo "RTE (Execute error)" + result="RTE (Execute error)" fi fi fi @@ -107,8 +149,3 @@ GetJudgment() InitJudgments -for file in $* -do - j=`GetJudgment $file` - echo $file: $j -done diff --git a/support/judge_webcgi/cgi-bin/problemyaml.sh b/support/judge_webcgi/cgi-bin/problemyaml.sh new file mode 100644 index 000000000..b1b2154d9 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/problemyaml.sh @@ -0,0 +1,26 @@ +###PROBLEMSET_YAML=problemset.yaml +###PROBLEMSET_YAML_PATH=${PC2_CDP}/${PROBLEMSET_YAML} + +###declare -A problet_to_name + +###ParseProblemYaml() +###{ +### CURDIR="$PWD" +### tmpdir=/tmp/probset$$ +### mkdir $tmpdir +### # Need a copy since web doesnt have access to full path +### cp ${PROBLEMSET_YAML_PATH} $tmpdir +### cd $tmpdir +### csplit --prefix="x$USER" -q ${PROBLEMSET_YAML} "/^ *- /" "{*}" +### for file in $(echo "x$USER"*) +### do +### letter=`sed -n -e 's/^ *letter: \([A-Z]\).*/\1/p' < $file` +### short=`sed -n -e 's/.* short-name: \(.*\)$/\1/p' < $file` +### if test -n "$letter" -a -n "$short" +### then +### problet_to_name[$letter]="$short" +### fi +### done +### cd $CURDIR +### rm -r $tmpdir +###} diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh new file mode 100644 index 000000000..03e31042a --- /dev/null +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -0,0 +1,25 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + + +# Parse query string into dictionary args +# This is not terribly secure. +declare -a parm +sIFS="$IFS" +IFS='=&' +parm=($QUERY_STRING) +IFS=$sIFS +for ((i=0; i<${#parm[@]}; i+=2)) +do + a=${parm[i]} + eval $a=${parm[i+1]} +done +# Done parsing + +Preamble - 'Run '$run +HeaderNoBanner Details for RunId $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" + +Trailer +exit 0 diff --git a/support/judge_webcgi/cgi-bin/webcommon.sh b/support/judge_webcgi/cgi-bin/webcommon.sh new file mode 100644 index 000000000..fef94f341 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/webcommon.sh @@ -0,0 +1,154 @@ +# File meant to be "source'd" to support generating web pages + +# +# Display very curt textual error message +# +Error() +{ + echo "Content-type: text/plain" + echo "" + echo ERROR $* +} + + +Preamble() +{ + echo "Content-type: text/html" + echo "" + if test $# -eq 0 + then + headmsg="Judge" + else + headmsg="$*" + fi + cat << PREEOF + + + +PC² $headmsg + + + +PREEOF +} + +Header() +{ + # Make sure link to banner page is in a place apache can read + if test ! -e ../${BANNER_FILE} + then + ln -s ${BANNER_IMAGE} ../${BANNER_FILE} + fi +cat << HDREOF +
+ +

PC2 Judging Results

+
+

+HDREOF +} + +HeaderNoBanner() +{ + if test $# -eq 0 + then + hdrmsg="Judging Results" + else + hdrmsg="$*" + fi +cat << EOF +

+

PC2 $hdrmsg

+
+

+EOF +} + +Trailer() +{ +cat << EOF2 + + +EOF2 +} + +StartTable() +{ + cat << EOF3 +

+ +EOF3 +} + +EndTable() +{ + cat << EOF4 +
+

+EOF4 +} + diff --git a/support/judge_webcgi/judge b/support/judge_webcgi/judge deleted file mode 100644 index 3d850abff..000000000 --- a/support/judge_webcgi/judge +++ /dev/null @@ -1,178 +0,0 @@ -#!/bin/bash -PC2_RUN_DIR=/home/icpc/pc2 -PC2_CDP=${PC2_RUN_DIR}/current/config -BANNER_FILE=banner.png -BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} -###PROBLEMSET_YAML=problemset.yaml -###PROBLEMSET_YAML_PATH=${PC2_CDP}/${PROBLEMSET_YAML} - -###declare -A problet_to_name - -# -# Display very curt textual error message -# -Error() -{ - echo "Content-type: text/plain" - echo "" - echo ERROR $* - echo $now Error: $* >> $LOGFILE -} - -###ParseProblemYaml() -###{ -### CURDIR="$PWD" -### tmpdir=/tmp/probset$$ -### mkdir $tmpdir -### # Need a copy since web doesnt have access to full path -### cp ${PROBLEMSET_YAML_PATH} $tmpdir -### cd $tmpdir -### csplit --prefix="x$USER" -q ${PROBLEMSET_YAML} "/^ *- /" "{*}" -### for file in $(echo "x$USER"*) -### do -### letter=`sed -n -e 's/^ *letter: \([A-Z]\).*/\1/p' < $file` -### short=`sed -n -e 's/.* short-name: \(.*\)$/\1/p' < $file` -### if test -n "$letter" -a -n "$short" -### then -### problet_to_name[$letter]="$short" -### fi -### done -### cd $CURDIR -### rm -r $tmpdir -###} - -Preamble() -{ - echo "Content-type: text/html" - echo "" -} - -Header() -{ - if test ! -e ../${BANNER_FILE} - then - ln -s ${BANNER_IMAGE} ../${BANNER_FILE} - fi -cat << EOF - - - -PC² Judge 1 - - - -

- -

PC2 Judging Results for Judge 1

-
-

-EOF -} - -Trailer() -{ -cat << EOF2 - - -EOF2 -} - -StartTable() -{ - cat << EOF3 -

- - - -EOF3 -} - -EndTable() -{ - cat << EOF4 -
Run IDTeamProblemLanguageTime Judged
-

-EOF4 -} - -TableRow() -{ - dir="$1" - runid=$2 - problet=$3 - shortname=$4 - langid=$5 - teamnum=$6 - runtime=`stat -c '%y' $dir` - probname="" - probdir="" - if test -n ${shortname} - then - probdir=${PC2_CDP}/${shortname} - probstatement=${probdir}/problem_statement/problem.en.tex - if test ! -s ${probstatement} - then - probstatement=${probdir}/problem_statement/problem.tex - fi - if test -s ${probstatement} - then - probname=`head -1 ${probstatement}` - probname=${probname##*{} - probname=${probname%\}} - fi - fi - problem="$problet - $probname (${shortname})" - echo ''"Run $runidteam$teamnum$problem$langid$runtime" -} - -###ParseProblemYaml -Preamble -Header -StartTable -# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid -for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+$' | sort --field-separator=_ +1rn` -do - # exdir looks like: ex_188_Y_compression_46103_cpp - # RId P ProbShort team# Lang - # RId = Run ID - # P = problem letter - # Lang = CLICS Language id - saveIFS="$IFS" - IFS="_" - set ${exdir} - IFS="$saveIFS" - if test $# -eq 6 - then - runid=$2 - problet=$3 - probshort=$4 - teamnum=$5 - langid=$6 - TableRow "${PC2_RUN_DIR}/$exdir" $runid $problet $probshort $langid $teamnum - fi -done -EndTable -Trailer -exit 0 diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh deleted file mode 100644 index c4bfbe7b0..000000000 --- a/support/judge_webcgi/showrun.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/bin/bash -# -# Display very curt textual error message -# -Error() -{ - echo "Content-type: text/plain" - echo "" - echo ERROR $* - echo $now Error: $* >> $LOGFILE -} - -Preamble() -{ - echo "Content-type: text/html" - echo "" -} - -Header() -{ -cat << EOF - - - -PC² Judge 1 - - -

-

PC2 Judging Results for Judge 1

-
-

-EOF -} - -Trailer() -{ -cat << EOF2 - - -EOF2 -} - -StartTable() -{ - cat << EOF3 -

- - - -EOF3 -} - -EndTable() -{ - cat << EOF4 -
Run IDTime Judged
-

-EOF4 -} - -TableRow() -{ - dir="$1" - runid=${dir#../Run} - runtime=`stat -c '%y' $dir` - echo ''"Run $runid$runtime" -} - -Preamble -Header - -# Parse query string into dictionary args -sIFS="$IFS" -IFS='=&' -declare -a parm -parm=($QUERY_STRING) -IFS=$sIFS -declare -A args -for ((i=0; i<${#parm[@]}; i+=2)) -do - args[${parm[i]}]=${parm[i+1]} - echo "${parm[i]} = ${args[${parm[i]}]}
" -done -# Done parsing - -echo '

The run is '${args["run"]} in problem directory ${args["probdir"]}. The execute folder is: ${args["dir"]} -Trailer -exit 0 From 5e4ad79a0d874739f36c90d306f2c7738ee8c4bb Mon Sep 17 00:00:00 2001 From: John Buck Date: Tue, 2 Jul 2024 07:11:02 -0400 Subject: [PATCH 04/55] i_972 Updated judge help web scripts Also added Warning icon --- support/judge_webcgi/Warning.png | Bin 0 -> 11345 bytes support/judge_webcgi/cgi-bin/judge.sh | 3 + support/judge_webcgi/cgi-bin/pc2common.sh | 57 +++++----- support/judge_webcgi/cgi-bin/showrun.sh | 94 +++++++++++++++++ support/judge_webcgi/judge.sh | 123 ++++++++++++++++++++++ 5 files changed, 253 insertions(+), 24 deletions(-) create mode 100644 support/judge_webcgi/Warning.png create mode 100644 support/judge_webcgi/judge.sh diff --git a/support/judge_webcgi/Warning.png b/support/judge_webcgi/Warning.png new file mode 100644 index 0000000000000000000000000000000000000000..e33a821d9fdac2891f6d08ee75b1c2bb3d61001f GIT binary patch literal 11345 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 VRFJlT1#k=)q$sN@QzvZ}`hRe3YU}_2 literal 0 HcmV?d00001 diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh index 5c9d129a0..c4bbd86ca 100644 --- a/support/judge_webcgi/cgi-bin/judge.sh +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -46,6 +46,9 @@ TableRow() then jstyle="green" jicon='' + elif test "${judgment}" = "CE" + then + jicon='' else jstyle="red" jicon='' diff --git a/support/judge_webcgi/cgi-bin/pc2common.sh b/support/judge_webcgi/cgi-bin/pc2common.sh index 3db5cd6c4..082b1770b 100644 --- a/support/judge_webcgi/cgi-bin/pc2common.sh +++ b/support/judge_webcgi/cgi-bin/pc2common.sh @@ -19,6 +19,11 @@ REJECT_INI=${JUDGE_HOME}/pc2/reject.ini # Where PC2 puts CLICS validator results EXECUTE_DATA_PREFIX=executedata +# Where PC2 puts run output/error +TEST_OUT_PREFIX="teamoutput" +TEST_ERR_PREFIX="teamstderr" +TEST_VALOUT_PREFIX="valout" +TEST_VALERR_PREFIX="valerr" declare -A Judgments @@ -88,7 +93,7 @@ GetLastJudgmentFile() result=`ls $lj_exdir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1rn | head -1` } -GetLastTestCaseNumber() +GetTestCaseNumber() { if test -z "$1" then @@ -106,6 +111,32 @@ GetLastTestCaseNumber() fi } +GetJudgmentFromFile() +{ + exdata="$1" + if test -z "${exdata}" + then + result="No results" + else + # Source the file + . ${exdata} + if test ${compileSuccess} = "false" + then + result="CE" + elif test ${executeSuccess} = "true" + then + if test ${validationSuccess} = "true" + then + MapJudgment "${validationResults}" "${validationReturnCode}" + else + result="JE (Validator error)" + fi + else + result="RTE (Execute error)" + fi + fi +} + GetJudgment() { dir=$1 @@ -120,30 +151,8 @@ GetJudgment() else # We got a real live run # Check out the biggest executedata file -# exdata=`ls ${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sort -t. +1rn | head -1` GetLastJudgmentFile $dir - exdata=${result} - if test -z "${exdata}" - then - result="No results" - else - # Source the file - . ./${exdata} - if test ${compileSuccess} = "false" - then - result="CE" - elif test ${executeSuccess} = "true" - then - if test ${validationSuccess} = "true" - then - MapJudgment "${validationResults}" "${validationReturnCode}" - else - result="JE (Validator error)" - fi - else - result="RTE (Execute error)" - fi - fi + GetJudgmentFromFile ./${result} fi } diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh index 03e31042a..38b3cf7f7 100644 --- a/support/judge_webcgi/cgi-bin/showrun.sh +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -1,8 +1,82 @@ #!/bin/bash +EXE_DIR_LINK=../exedir$$ +PROB_DIR_LINK=../probdir$$ + . ./pc2common.sh . ./webcommon.sh . ./cdpcommon.sh +TableHeader() +{ + cat << EOFTH + +TestDispJudgmentExitCompile TimeExecute TimeVal TimeVal SuccessRun OutRun ErrJudge InJudge AnsVal OutVal Err + +EOFTH +} + +GenFileLink() +{ + tstcase="$2" + tstcase=$((tstcase-1)) + tstfile=$1.$tstcase.txt + tstpath=$dir/$1.$tstcase.txt + if test -s ${tstpath} + then + bytes=`stat -c %s ${tstpath}` + echo ' View ('$bytes' bytes)' + elif test -e ${tstpath} + then + echo ' (Empty)' + else + echo ' Not found' + fi +} + +TableRow() +{ + tc=$1 + judgment="$2" + ec=$3 + comptm="$4" + exetm="$5" + valtm="$6" + if test "$7" = "true" + then + valsucc=Yes + elif test "$7" = "false" + then + valsucc=No + else + valsucc="N/A" + fi + if test "${judgment}" = "AC" + then + jicon='' + elif test "${judgment}" = "CE" + then + jicon='' + else + jicon='' + fi + + echo ' ' + + echo ' '$tc'' + echo ' '$jicon'' + echo ' '$judgment'' + echo ' '$ec'' + echo ' '$comptm'ms' + echo ' '$exetm'ms' + echo ' '$valtm'ms' + echo ' '$valsucc'' + GenFileLink $TEST_OUT_PREFIX $tc + GenFileLink $TEST_ERR_PREFIX $tc + GenFileLink $TEST_VALOUT_PREFIX $tc + GenFileLink $TEST_VALERR_PREFIX $tc + + echo ' ' +} # Parse query string into dictionary args # This is not terribly secure. @@ -21,5 +95,25 @@ done Preamble - 'Run '$run HeaderNoBanner Details for RunId $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" +StartTable +TableHeader +rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} +ln -s ${dir} ${EXE_DIR_LINK} +ln -s ${probdir} ${PROB_DIR_LINK} +for testcase in `ls $dir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1n` +do + GetTestCaseNumber $testcase + tc=$((result+1)) + # This will also source the execute data + GetJudgmentFromFile $dir/$testcase + judgment=$result + ec=$executeExitValue + comptm=$compileTimeMS + exetm=$executeTimeMS + valtm=$validateTimeMS + valsuc=$validationSuccess + TableRow "$tc" "$judgment" "$ec" "$comptm" "$exetm" "$valtm" "$valsuc" +done + Trailer exit 0 diff --git a/support/judge_webcgi/judge.sh b/support/judge_webcgi/judge.sh new file mode 100644 index 000000000..c4bbd86ca --- /dev/null +++ b/support/judge_webcgi/judge.sh @@ -0,0 +1,123 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + +TableHeader() +{ + cat << EOFTH + +Run IDDispJudgmentProblemTeamTest CasesLanguageJudgeTime Judged + +EOFTH +} + +TableRow() +{ + dir="$1" + runid=$2 + problet=$3 + shortname=$4 + langid=$5 + teamnum=$6 + judgment="$7" + runtime="$8" + testinfo="$9" + judge="${10}" + probname="" + probdir="" + if test -n ${shortname} + then + probdir=${PC2_CDP}/${shortname} + probstatement=${probdir}/problem_statement/problem.en.tex + if test ! -s ${probstatement} + then + probstatement=${probdir}/problem_statement/problem.tex + fi + if test -s ${probstatement} + then + probname=`head -1 ${probstatement}` + probname=${probname##*\{} + probname=${probname%\}} + fi + fi + problem="$problet - $probname (${shortname})" + if test "${judgment}" = "AC" + then + jstyle="green" + jicon='' + elif test "${judgment}" = "CE" + then + jicon='' + else + jstyle="red" + jicon='' + fi + echo ' ' + echo ' '"Run $runid" +# echo ' '$judgment"" +# echo '

'$judgment"
" + echo ' '$jicon'' + echo ' '$judgment'' + echo " $problem" + echo " team$teamnum" + echo " $testinfo" + echo " $langid" + echo " $judge" + echo " $runtime" + echo " " +} + +###ParseProblemYaml +Preamble +Header +StartTable +TableHeader + +# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid_judge +for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+' | sort --field-separator=_ +1rn` +do + # exdir looks like: ex_188_Y_compression_46103_cpp + # RId P ProbShort team# Lang + # RId = Run ID + # P = problem letter + # Lang = CLICS Language id + saveIFS="$IFS" + IFS="_" + set ${exdir} + IFS="$saveIFS" + if test $# -ge 6 + then + exedir=${PC2_RUN_DIR}/$exdir + runid=$2 + problet=$3 + probshort=$4 + teamnum=$5 + langid=$6 + judge=$7 + if test -z "${judge}" + then + judge="N/A" + fi + GetJudgment "${exedir}" + judgment="${result}" + runtime="${executeDateTime}" + # Get how many total test cases there are + probdir=${PC2_CDP}/${probshort} + if test -n "${probdir}" + then + GetNumberOfTestCases "${probdir}" + numcases=${result} + else + numcases="??" + fi + # Note that GetJudgment also filled in exdata with the last execute data + GetLastTestCaseNumber "${exdata}" + testcaseinfo=$((result+1))/${numcases} + + TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" + fi +done +EndTable +Trailer +exit 0 From 6b16d1c77be4248ac74f2cc9a60a801c404572ad Mon Sep 17 00:00:00 2001 From: John Buck Date: Tue, 2 Jul 2024 16:43:06 -0400 Subject: [PATCH 05/55] i_972 More web script updates --- scripts/pc2sandbox.sh | 2 + support/judge_webcgi/judge.sh | 1 + support/judge_webcgi/pc2common.sh | 211 ++++++++++++++++++++++++++++++ support/judge_webcgi/showrun.sh | 209 +++++++++++++++++++++++++++++ support/judge_webcgi/webcommon.sh | 157 ++++++++++++++++++++++ 5 files changed, 580 insertions(+) create mode 100644 support/judge_webcgi/pc2common.sh create mode 100644 support/judge_webcgi/showrun.sh create mode 100644 support/judge_webcgi/webcommon.sh diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh index 08d536e93..f45237d41 100755 --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -379,6 +379,8 @@ REPORT Setting maximum user processes to $MAXPROCS ulimit -u $MAXPROCS # Keep track of details for reports +REPORT_BRIEF ${JUDGEIN} +REPORT_BRIEF ${JUDGEANS} REPORT_BRIEF $cpunum REPORT_BRIEF $$ REPORT_BRIEF $(date "+%F %T.%6N") diff --git a/support/judge_webcgi/judge.sh b/support/judge_webcgi/judge.sh index c4bbd86ca..e1b6bb740 100644 --- a/support/judge_webcgi/judge.sh +++ b/support/judge_webcgi/judge.sh @@ -71,6 +71,7 @@ TableRow() ###ParseProblemYaml Preamble Header +LogButton StartTable TableHeader diff --git a/support/judge_webcgi/pc2common.sh b/support/judge_webcgi/pc2common.sh new file mode 100644 index 000000000..54723de21 --- /dev/null +++ b/support/judge_webcgi/pc2common.sh @@ -0,0 +1,211 @@ +# Meant to be "source'd" into bash scripts. + +# The judge's home directory +JUDGE_HOME=/home/icpc + +# Modify constants as necessary for your installation +PC2_RUN_DIR=${JUDGE_HOME}/pc2 +PC2_CDP=${PC2_RUN_DIR}/current/config + +EXE_LINK_PREFIX=../exedir +PROB_LINK_PREFIX=../probdir + +# How many exedir and probdir links to keep around +NUM_LINK_KEEP=4 + +# Where can we find the contest banner png file for webpage headers +BANNER_FILE=banner.png +BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} + +# Where to the the result of script failure before execution +RESULT_FAILURE_FILE=failure.txt + +# Where judgments are +REJECT_INI=${JUDGE_HOME}/pc2/reject.ini + +# Where PC2 puts CLICS validator results +EXECUTE_DATA_PREFIX=executedata +# Where PC2 puts run output/error +TEST_OUT_PREFIX="teamoutput" +TEST_ERR_PREFIX="teamstderr" +TEST_VALOUT_PREFIX="valout" +TEST_VALERR_PREFIX="valerr" + +# Detailed log of entire judging process +SANDBOX_LOG=sandbox.log + +# Briefcase file prefix +BRIEFCASE_FILE_PREFIX="briefcase" +REPORTS_DIR=reports + +declare -A Judgments + +InitJudgments() +{ + # Defauls, may be overriden by reject.ini + Judgments["accepted"]="AC" + Judgments["Accepted"]="AC" + Judgments["timelimit"]="TLE" + Judgments["run error"]="RTE" + Judgments["compiler error"]="CE" + if test -s ${REJECT_INI} + then + while read j + do + if [[ $j = \#* ]] + then + continue + fi + savIFS="$IFS" + IFS='|' + set $j + IFS="$savIFS" + key="$1" + shortcode="$2" + case ${shortcode} in + AC|CE|RTE|WA|TLE) ;; + MLE) shortcode="RTE (MLE)" ;; + *) shortcode="WA (${shortcode})" ;; + esac +# echo Mapping $key to $shortcode + Judgments[$key]="$shortcode" + done < $REJECT_INI + else echo NO REJECT FILE + fi +} + +# Takes the judgement string, eg. "Wrong answer" as arg 1 and the validation Result (42 or 42) as arg 2 +MapJudgment() +{ + jm="$1" + vr="$2" + result=${Judgments[$jm]} + if test -z ${result} + then + if test ${validationReturnCode} -eq 0 + then + if test ${vr} = "43" + then + resul="WA" + elif test ${vr} = "42" + then + result="AC" + else + result="WA (Default)" + fi + else + result="JE (Validator EC=${validationReturnCode})" + fi + + fi +} + +GetLastJudgmentFile() +{ + lj_exdir="$1" + result=`ls $lj_exdir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1rn | head -1` +} + +GetTestCaseNumber() +{ + if test -z "$1" + then + result=0 + else + saveIFS="$IFS" + IFS=. + set ${1} + result="$2" + IFS="$saveIFS" + if test -z "${result}" + then + result=0 + fi + fi +} + +GetJudgmentFromFile() +{ + exdata="$1" + if test -z "${exdata}" + then + result="No results" + else + # Source the file + . ${exdata} + if test ${compileSuccess} = "false" + then + result="CE" + elif test ${executeSuccess} = "true" + then + if test ${validationSuccess} = "true" + then + MapJudgment "${validationResults}" "${validationReturnCode}" + else + result="JE (Validator error)" + fi + else + result="RTE (Execute error)" + fi + fi +} + +GetJudgment() +{ + dir=$1 + exdata="" + if ! cd ${dir} + then + result="Not found" + elif test -s ${RESULT_FAILURE_FILE} + then + jerr=`cat ${RESULT_FAILURE_FILE}` + result="JE ($jerr)" + else + # We got a real live run + # Check out the biggest executedata file + GetLastJudgmentFile $dir + GetJudgmentFromFile ./${result} + fi +} + +MakeBriefcaseFile() +{ + d="$1" + t="$2" + result=`printf '%s/%s/%s_%03d.log' "$d" "$REPORTS_DIR" "$BRIEFCASE_FILE_PREFIX" "$t"` +} + +# Must redirect from briefcase file +ReadBriefcase() +{ + read judgein + read judgeans + read cpunum + read exepid + read exetime + read execpums cpulimms exewallms mempeak memlim + if [[ $mempeak = [0-9]* ]] + then + mempeak=$((mempeak/(1024*1024))) + fi + if [[ $memlim = [0-9]* ]] + then + memlim=$((memlim/(1024*1024))) + fi +} + +DeleteOldestLinks() +{ + for linkpref in ${EXE_LINK_PREFIX} ${PROB_LINK_PREFIX} + do + dellist=`ls -1td ${linkpref}* | sed 1,${NUM_LINK_KEEP}d` + if test -n "${dellist}" + then + rm -f ${dellist} + fi + done +} + +InitJudgments + diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh new file mode 100644 index 000000000..e88937dea --- /dev/null +++ b/support/judge_webcgi/showrun.sh @@ -0,0 +1,209 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + +EXE_DIR_LINK=${EXE_LINK_PREFIX}$$ +PROB_DIR_LINK=${PROB_LINK_PREFIX}$$ + +# Provide a way to look at the sandbox log +LogButton() +{ + # Read first judgment file to get compile time - it's compiled once for all + GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" + if test -n ${result} -a -n ${compileTimeMS} + then + cat << LBEOF0 +
+

The program took ${compileTimeMS}ms to compile. +

+LBEOF0 + fi + + # Read the first briefcase file (if any) for limits + MakeBriefcaseFile ${EXE_DIR_LINK} 1 + ReadBriefcase < $result + if test -n ${cpulimms} + then + cpulimms=${cpulimms%%.*} + cpusecs="$((cpulimms/1000))" + if test ${cpusecs} != "1" + then + cpusecs="${cpusecs} seconds" + else + cpusecs="1 second" + fi + cat << LBEOF1 +
+

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms). +

+LBEOF1 + fi + if test -n ${memlim} + then + if test ${memlim} = "0" + then + memlim="Unlimited" + else + memlim=${memlim}MB + fi + cat << LBEOF1A +
+

The Memory limit for this problem is ${memlim}. +

+LBEOF1A + fi + sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} + if test -s ${sandlog} + then + cat << LBEOF2 +Click here for the full sandbox log for this run +

+

+LBEOF2 + fi +} + +TableHeader() +{ + cat << EOFTH + +TestDispJudgmentExitExecute TimeVal TimeVal SuccessRun OutRun ErrJudge InJudge AnsVal OutVal Err + +EOFTH +} + +GenFileLink() +{ + tstcase="$2" + tstcase=$((tstcase-1)) + tstfile=$1.$tstcase.txt + tstpath=$dir/$1.$tstcase.txt + if test -s ${tstpath} + then + bytes=`stat -c %s ${tstpath}` + echo ' View ('$bytes' bytes)' + elif test -e ${tstpath} + then + echo ' (Empty)' + else + echo ' Not found' + fi +} + +GenFileLinkWithText() +{ + linkaddr="$1" + linktext="$2" + linkcolor="$3" + bytes=`stat -c %s ${linkaddr}` + echo ' '$linktext' ('$bytes' bytes)' +} + +TableRow() +{ + tc=$1 + judgment="$2" + ec=$3 + exetm="$4" + valtm="$5" + jin="$6" + jans="$7" + if test "$8" = "true" + then + valsucc=Yes + elif test "$8" = "false" + then + valsucc=No + else + valsucc="N/A" + fi + # Strip the stuff of before sample (or secret) + jin=${jin##*/data/} + jans=${jans##*/data/} + # Just the basenames for link text + jinbase=${jin##*/} + jansbase=${jans##*/} + if [[ $jin = sample/* ]] + then + lcolor=#00a0a0 + else + lcolor=#00a000 + fi + if test "${judgment}" = "AC" + then + jicon='' + elif test "${judgment}" = "CE" + then + jicon='' + else + jicon='' + fi + + echo ' ' + + echo ' '$tc'' + echo ' '$jicon'' + echo ' '$judgment'' + echo ' '$ec'' + echo ' '$exetm'ms' + echo ' '$valtm'ms' + echo ' '$valsucc'' + GenFileLink $TEST_OUT_PREFIX $tc + GenFileLink $TEST_ERR_PREFIX $tc + GenFileLinkWithText $PROB_DIR_LINK/data/"$jin" "$jinbase" $lcolor + GenFileLinkWithText $PROB_DIR_LINK/data/"$jans" "$jansbase" $lcolor + GenFileLink $TEST_VALOUT_PREFIX $tc + GenFileLink $TEST_VALERR_PREFIX $tc + + echo ' ' +} + +# Parse query string into dictionary args +# This is not terribly secure. +declare -a parm +sIFS="$IFS" +IFS='=&' +parm=($QUERY_STRING) +IFS=$sIFS +for ((i=0; i<${#parm[@]}; i+=2)) +do + a=${parm[i]} + eval $a=${parm[i+1]} +done +# Done parsing + +DeleteOldestLinks +Preamble - 'Run '$run +HeaderNoBanner Details for Run Id $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" + +# Create links apache can access in our html folder +#rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} +ln -s ${dir} ${EXE_DIR_LINK} +ln -s ${probdir} ${PROB_DIR_LINK} + +LogButton + +StartTable +TableHeader +for testcase in `ls $dir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1n` +do + GetTestCaseNumber $testcase + tc=$((result+1)) + # This will also source the execute data + GetJudgmentFromFile $dir/$testcase + judgment=$result + ec=$executeExitValue +# comptm=$compileTimeMS + exetm=$executeTimeMS + valtm=$validateTimeMS + valsuc=$validationSuccess + MakeBriefcaseFile "$dir" "$tc" + ReadBriefcase < ${result} + TableRow "$tc" "$judgment" "$ec" "$exetm" "$valtm" "$judgein" "$judgeans" "$valsuc" +done + +Trailer + +#rm -f ${EXE_DIR_LINK} ${PROB_DIR_LINK} +exit 0 diff --git a/support/judge_webcgi/webcommon.sh b/support/judge_webcgi/webcommon.sh new file mode 100644 index 000000000..26c65cd62 --- /dev/null +++ b/support/judge_webcgi/webcommon.sh @@ -0,0 +1,157 @@ +# File meant to be "source'd" to support generating web pages + +# +# Display very curt textual error message +# +Error() +{ + echo "Content-type: text/plain" + echo "" + echo ERROR $* +} + + +Preamble() +{ + echo "Content-type: text/html" + echo "" + if test $# -eq 0 + then + headmsg="Judge" + else + headmsg="$*" + fi + cat << PREEOF + + + +PC² $headmsg + + + +PREEOF +} + +Header() +{ + # Make sure link to banner page is in a place apache can read + if test ! -e ../${BANNER_FILE} + then + ln -s ${BANNER_IMAGE} ../${BANNER_FILE} + fi +cat << HDREOF +

+ +

PC2 Judging Results

+
+

+HDREOF +} + +HeaderNoBanner() +{ + if test $# -eq 0 + then + hdrmsg="Judging Results" + else + hdrmsg="$*" + fi +cat << EOF +

+

PC2 $hdrmsg

+
+

+EOF +} + +Trailer() +{ +cat << EOF2 + + +EOF2 +} + +StartTable() +{ + cat << EOF3 +

+ +EOF3 +} + +EndTable() +{ + cat << EOF4 +
+

+EOF4 +} + From c09e95f8415fbb2a10eeed94225e4b345a089b45 Mon Sep 17 00:00:00 2001 From: John Buck Date: Sat, 6 Jul 2024 17:57:47 -0400 Subject: [PATCH 06/55] i_972 Update scripts for more detail Allow display of sandbox cpu time. Allow display of reports/testcase_xxx.log by link Make all links open a new tab as opposed to overwriting the current one. Add memory used column since it is now supported in latest kernel. --- scripts/pc2sandbox.sh | 30 +-- support/judge_webcgi/cgi-bin/judge.sh | 1 + support/judge_webcgi/cgi-bin/pc2common.sh | 63 +++++++ support/judge_webcgi/cgi-bin/showrun.sh | 168 +++++++++++++++-- support/judge_webcgi/cgi-bin/webcommon.sh | 30 +++ support/judge_webcgi/judge.sh | 124 ------------- support/judge_webcgi/pc2common.sh | 211 ---------------------- support/judge_webcgi/showrun.sh | 209 --------------------- support/judge_webcgi/webcommon.sh | 157 ---------------- 9 files changed, 259 insertions(+), 734 deletions(-) delete mode 100644 support/judge_webcgi/judge.sh delete mode 100644 support/judge_webcgi/pc2common.sh delete mode 100644 support/judge_webcgi/showrun.sh delete mode 100644 support/judge_webcgi/webcommon.sh diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh index f45237d41..950bcf432 100755 --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -268,12 +268,15 @@ shift shift shift shift +shift DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" -DEBUG echo -e "\n$0" ${MEMLIMIT} ${TIMELIMIT} xxx xxx $* "< ${JUDGEIN} > $TESTCASE.ans" +RUN_LOCAL_CMD="$0 ${MEMLIMIT} ${TIMELIMIT} xxx xxx ${COMMAND} $* < ${JUDGEIN} > $TESTCASE.ans" +DIFF_OUTPUT_CMD="diff -w ${JUDGEANS} $TESTCASE.ans | more" +DEBUG echo -e "\n${RUN_LOCAL_CMD}" DEBUG echo -e "\nor, without the sandbox by using the following command:" -DEBUG echo -e "\n$* < ${JUDGEIN} > $TESTCASE.ans" +DEBUG echo -e "\n${COMMAND} $* < ${JUDGEIN} > $TESTCASE.ans" DEBUG echo -e "\nAnd compare with the judge's answer:" -DEBUG echo -e "\ndiff -w ${JUDGEANS} $TESTCASE.ans | more\n" +DEBUG echo -e "\n${DIFF_OUTPUT_CMD}\n" #### Debugging - just set expected first args to: 8MB 2seconds ###MEMLIMIT=8 @@ -351,31 +354,33 @@ mkdir -p "$REPORTDIR" REPORTFILE=`printf "$REPORTDIR/testcase_%03d.log" $TESTCASE` BRIEFREPORTFILE=`printf "$REPORTDIR/briefcase_%03d.log" $TESTCASE` DEBUG echo Report file: ${REPORTFILE} Brief Report File: ${BRIEFREORTFILE} +REPORT Test case $TESTCASE: +REPORT Command: "${RUN_LOCAL_CMD}" +REPORT Diff: " ${DIFF_OUTPUT_CMD}" # set the specified memory limit - input is in MB, cgroup v2 requires bytes, so multiply by 1M # but only if > 0. # "max" means unlimited, which is the cgroup v2 default DEBUG echo checking memory limit if [ "$MEMLIMIT" -gt "0" ] ; then - DEBUG echo setting memory limit to $MEMLIMIT MB + REPORT_DEBUG echo Setting memory limit to $MEMLIMIT MB echo $(( $MEMLIMIT * 1024 * 1024 )) > $PC2_SANDBOX_CGROUP_PATH/memory.max echo 1 > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max else - DEBUG echo setting memory limit to max, meaning no limit + REPORT_DEBUG echo Setting memory limit to max, meaning no limit echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.max echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max fi -REPORT Test case $TESTCASE: # We use ulimit to limit CPU time, not cgroups. Time is supplied in seconds. This may have to # be reworked if ms accuracy is needed. The problem is, cgroups do not kill off a process that # exceeds the time limit, ulimit does. TIMELIMIT_US=$((TIMELIMIT * 1000000)) -REPORT Setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" +REPORT_DEBUG Setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" ulimit -t $TIMELIMIT MAXPROCS=$((MAXPROCS+`ps -T -u $USER | wc -l`)) -REPORT Setting maximum user processes to $MAXPROCS +REPORT_DEBUG Setting maximum user processes to $MAXPROCS ulimit -u $MAXPROCS # Keep track of details for reports @@ -398,12 +403,7 @@ then fi # run the command -# the following are the cgroup-tools V1 commands; need to find cgroup-tools v2 commands -# echo Using cgexec to run $COMMAND $* -# cgexec -g cpu,memory:/pc2 $COMMAND $* - -# since we don't know how to use cgroup-tools to execute, just execute it directly (it's a child so it -# should still fall under the cgroup limits). +# execute it directly (it's a child so it should still fall under the cgroup limits). REPORT_DEBUG Executing "setsid taskset $CPUMASK $COMMAND $*" # Set up trap handler to catch wall-clock time exceeded and getting killed by PC2's execute timer @@ -443,6 +443,8 @@ else fi ShowStats ${cputime} ${TIMELIMIT_US} ${walltime} ${peakmem} $((MEMLIMIT*1024*1024)) +REPORT_DEBUG The command exited with code: ${COMMAND_EXIT_CODE} + if test "$kills" != "0" then REPORT_DEBUG The command was killed because it exceeded the memory limit diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh index c4bbd86ca..e1b6bb740 100644 --- a/support/judge_webcgi/cgi-bin/judge.sh +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -71,6 +71,7 @@ TableRow() ###ParseProblemYaml Preamble Header +LogButton StartTable TableHeader diff --git a/support/judge_webcgi/cgi-bin/pc2common.sh b/support/judge_webcgi/cgi-bin/pc2common.sh index 082b1770b..eac62c936 100644 --- a/support/judge_webcgi/cgi-bin/pc2common.sh +++ b/support/judge_webcgi/cgi-bin/pc2common.sh @@ -7,6 +7,12 @@ JUDGE_HOME=/home/icpc PC2_RUN_DIR=${JUDGE_HOME}/pc2 PC2_CDP=${PC2_RUN_DIR}/current/config +EXE_LINK_PREFIX=../exedir +PROB_LINK_PREFIX=../probdir + +# How many exedir and probdir links to keep around +NUM_LINK_KEEP=4 + # Where can we find the contest banner png file for webpage headers BANNER_FILE=banner.png BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} @@ -25,6 +31,15 @@ TEST_ERR_PREFIX="teamstderr" TEST_VALOUT_PREFIX="valout" TEST_VALERR_PREFIX="valerr" +# Detailed log of entire judging process +SANDBOX_LOG=sandbox.log + +# Testcase file prefix +TESTCASE_REPORT_FILE_PREFIX="testcase" +# Briefcase file prefix +BRIEFCASE_FILE_PREFIX="briefcase" +REPORTS_DIR=reports + declare -A Judgments InitJudgments() @@ -156,5 +171,53 @@ GetJudgment() fi } +MakeTestcaseFile() +{ + d="$1" + t="$2" + result=`printf '%s/%s/%s_%03d.log' "$d" "$REPORTS_DIR" "$TESTCASE_REPORT_FILE_PREFIX" "$t"` +} + +MakeBriefcaseFile() +{ + d="$1" + t="$2" + result=`printf '%s/%s/%s_%03d.log' "$d" "$REPORTS_DIR" "$BRIEFCASE_FILE_PREFIX" "$t"` +} + +# Must redirect from briefcase file +ReadBriefcase() +{ + read judgein + read judgeans + read cpunum + read exepid + read exetime + read execpums cpulimms exewallms mempeakbytes memlimbytes + mempeak=$mempeakbytes + memlim=$memlimbytes + # Calculate Mib from bytes, round upto next Mib + if [[ $mempeak = [0-9]* ]] + then + mempeak=$(((mempeak+(1024*1024)-1)/(1024*1024))) + fi + if [[ $memlim = [0-9]* ]] + then + memlim=$(((memlim+(1024*1024)-1)/(1024*1024))) + fi +} + +DeleteOldestLinks() +{ + for linkpref in ${EXE_LINK_PREFIX} ${PROB_LINK_PREFIX} + do + dellist=`ls -1td ${linkpref}* | sed 1,${NUM_LINK_KEEP}d` + if test -n "${dellist}" + then + rm -f ${dellist} + fi + done +} + InitJudgments diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh index 38b3cf7f7..87a848d00 100644 --- a/support/judge_webcgi/cgi-bin/showrun.sh +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -1,16 +1,87 @@ #!/bin/bash -EXE_DIR_LINK=../exedir$$ -PROB_DIR_LINK=../probdir$$ - . ./pc2common.sh . ./webcommon.sh . ./cdpcommon.sh +EXE_DIR_LINK=${EXE_LINK_PREFIX}$$ +PROB_DIR_LINK=${PROB_LINK_PREFIX}$$ + +# Provide a way to look at the sandbox log +LogButton() +{ + # Read first judgment file to get compile time - it's compiled once for all + GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" + if test -n ${result} -a -n ${compileTimeMS} + then + cat << LBEOF0 +

+

The program took ${compileTimeMS}ms to compile. +

+LBEOF0 + fi + + # Read the first briefcase file (if any) for limits + MakeBriefcaseFile ${EXE_DIR_LINK} 1 + ReadBriefcase < $result + if test -n ${cpulimms} + then + cpulimms=${cpulimms%%.*} + cpusecs="$((cpulimms/1000))" + if test ${cpusecs} != "1" + then + cpusecs="${cpusecs} seconds" + else + cpusecs="1 second" + fi + cat << LBEOF1 +
+

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms). +

+LBEOF1 + fi + if test -n ${memlim} + then + if test ${memlim} = "0" + then + memlim="Unlimited" + else + memlim=${memlim}MiB + fi + cat << LBEOF1A +
+

The Memory limit for this problem is ${memlim}. +

+LBEOF1A + fi + sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} + if test -s ${sandlog} + then + cat << LBEOF2 +
Click here for the full sandbox log for this run +

+

+LBEOF2 + fi +} + TableHeader() { cat << EOFTH -TestDispJudgmentExitCompile TimeExecute TimeVal TimeVal SuccessRun OutRun ErrJudge InJudge AnsVal OutVal Err + Test + Disp + Judgment + Exit + Execute Time + MiB Used + Val Time + Val Success + Run Out + Run Err + Judge In + Judge Ans + Val Out + Val Err EOFTH } @@ -24,7 +95,7 @@ GenFileLink() if test -s ${tstpath} then bytes=`stat -c %s ${tstpath}` - echo ' View ('$bytes' bytes)' + echo ' View ('$bytes' bytes)' elif test -e ${tstpath} then echo ' (Empty)' @@ -33,23 +104,56 @@ GenFileLink() fi } +GenFileLinkWithText() +{ + linkaddr="$1" + linktext="$2" + linkcolor="$3" + bytes=`stat -c %s ${linkaddr}` + echo ' '$linktext' ('$bytes' bytes)' +} + TableRow() { tc=$1 judgment="$2" ec=$3 - comptm="$4" - exetm="$5" - valtm="$6" - if test "$7" = "true" + exetm="$4" + valtm="$5" + jin="$6" + jans="$7" + if test "$8" = "true" then valsucc=Yes - elif test "$7" = "false" + elif test "$8" = "false" then valsucc=No else valsucc="N/A" fi + memused="$9" + memusedbytes="${10}" + exesandms="${11}" + + # Create link to report/testcase file for testcase number + MakeTestcaseFile ${EXE_DIR_LINK} ${tc} + tcreport="${result}" + if test ! -s "${tcreport}" + then + tcreport="" + fi + # Strip the stuff of before sample (or secret) + jin=${jin##*/data/} + jans=${jans##*/data/} + # Just the basenames for link text + jinbase=${jin##*/} + jansbase=${jans##*/} + if [[ $jin = sample/* ]] + then + lcolor=#00a0a0 + else + lcolor=#00a000 + fi if test "${judgment}" = "AC" then jicon='' @@ -62,16 +166,33 @@ TableRow() echo ' ' - echo ' '$tc'' + if test -n ${tcreport} + then + echo ' '$tc'' + else + echo ' '$tc'' + fi echo ' '$jicon'' echo ' '$judgment'' echo ' '$ec'' - echo ' '$comptm'ms' - echo ' '$exetm'ms' + if [[ ${exesandms} = [0-9]* ]] + then + echo '

'$exetm'ms'$exesandms'ms in the Sandbox
' + else + echo ' '$exetm'ms' + fi + if [[ ${memusedbytes} = [0-9]* ]] + then + echo '
'$memused'MiB'$memusedbytes' bytes
' + else + echo ' '$memused'' + fi echo ' '$valtm'ms' echo ' '$valsucc'' GenFileLink $TEST_OUT_PREFIX $tc GenFileLink $TEST_ERR_PREFIX $tc + GenFileLinkWithText $PROB_DIR_LINK/data/"$jin" "$jinbase" $lcolor + GenFileLinkWithText $PROB_DIR_LINK/data/"$jans" "$jansbase" $lcolor GenFileLink $TEST_VALOUT_PREFIX $tc GenFileLink $TEST_VALERR_PREFIX $tc @@ -92,14 +213,19 @@ do done # Done parsing +DeleteOldestLinks Preamble - 'Run '$run -HeaderNoBanner Details for RunId $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" +HeaderNoBanner Details for Run Id $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" -StartTable -TableHeader -rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} +# Create links apache can access in our html folder +#rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} ln -s ${dir} ${EXE_DIR_LINK} ln -s ${probdir} ${PROB_DIR_LINK} + +LogButton + +StartTable +TableHeader for testcase in `ls $dir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1n` do GetTestCaseNumber $testcase @@ -108,12 +234,16 @@ do GetJudgmentFromFile $dir/$testcase judgment=$result ec=$executeExitValue - comptm=$compileTimeMS +# comptm=$compileTimeMS exetm=$executeTimeMS valtm=$validateTimeMS valsuc=$validationSuccess - TableRow "$tc" "$judgment" "$ec" "$comptm" "$exetm" "$valtm" "$valsuc" + MakeBriefcaseFile "$dir" "$tc" + ReadBriefcase < ${result} + TableRow "$tc" "$judgment" "$ec" "$exetm" "$valtm" "$judgein" "$judgeans" "$valsuc" "$mempeak" "$mempeakbytes" "$execpums" done Trailer + +#rm -f ${EXE_DIR_LINK} ${PROB_DIR_LINK} exit 0 diff --git a/support/judge_webcgi/cgi-bin/webcommon.sh b/support/judge_webcgi/cgi-bin/webcommon.sh index fef94f341..03fddd8a3 100644 --- a/support/judge_webcgi/cgi-bin/webcommon.sh +++ b/support/judge_webcgi/cgi-bin/webcommon.sh @@ -1,4 +1,7 @@ # File meant to be "source'd" to support generating web pages +TOOLTIP_UL_COLOR="blue" +TOOLTIP_TEXT_COLOR="white" +TOOLTIP_BG_COLOR="black" # # Display very curt textual error message @@ -27,6 +30,9 @@ Preamble() PC² $headmsg diff --git a/support/judge_webcgi/judge.sh b/support/judge_webcgi/judge.sh deleted file mode 100644 index e1b6bb740..000000000 --- a/support/judge_webcgi/judge.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/bin/bash -. ./pc2common.sh -. ./webcommon.sh -. ./cdpcommon.sh - -TableHeader() -{ - cat << EOFTH - -Run IDDispJudgmentProblemTeamTest CasesLanguageJudgeTime Judged - -EOFTH -} - -TableRow() -{ - dir="$1" - runid=$2 - problet=$3 - shortname=$4 - langid=$5 - teamnum=$6 - judgment="$7" - runtime="$8" - testinfo="$9" - judge="${10}" - probname="" - probdir="" - if test -n ${shortname} - then - probdir=${PC2_CDP}/${shortname} - probstatement=${probdir}/problem_statement/problem.en.tex - if test ! -s ${probstatement} - then - probstatement=${probdir}/problem_statement/problem.tex - fi - if test -s ${probstatement} - then - probname=`head -1 ${probstatement}` - probname=${probname##*\{} - probname=${probname%\}} - fi - fi - problem="$problet - $probname (${shortname})" - if test "${judgment}" = "AC" - then - jstyle="green" - jicon='' - elif test "${judgment}" = "CE" - then - jicon='' - else - jstyle="red" - jicon='' - fi - echo ' ' - echo ' '"Run $runid" -# echo ' '$judgment"" -# echo '
'$judgment"
" - echo ' '$jicon'' - echo ' '$judgment'' - echo " $problem" - echo " team$teamnum" - echo " $testinfo" - echo " $langid" - echo " $judge" - echo " $runtime" - echo " " -} - -###ParseProblemYaml -Preamble -Header -LogButton -StartTable -TableHeader - -# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid_judge -for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+' | sort --field-separator=_ +1rn` -do - # exdir looks like: ex_188_Y_compression_46103_cpp - # RId P ProbShort team# Lang - # RId = Run ID - # P = problem letter - # Lang = CLICS Language id - saveIFS="$IFS" - IFS="_" - set ${exdir} - IFS="$saveIFS" - if test $# -ge 6 - then - exedir=${PC2_RUN_DIR}/$exdir - runid=$2 - problet=$3 - probshort=$4 - teamnum=$5 - langid=$6 - judge=$7 - if test -z "${judge}" - then - judge="N/A" - fi - GetJudgment "${exedir}" - judgment="${result}" - runtime="${executeDateTime}" - # Get how many total test cases there are - probdir=${PC2_CDP}/${probshort} - if test -n "${probdir}" - then - GetNumberOfTestCases "${probdir}" - numcases=${result} - else - numcases="??" - fi - # Note that GetJudgment also filled in exdata with the last execute data - GetLastTestCaseNumber "${exdata}" - testcaseinfo=$((result+1))/${numcases} - - TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" - fi -done -EndTable -Trailer -exit 0 diff --git a/support/judge_webcgi/pc2common.sh b/support/judge_webcgi/pc2common.sh deleted file mode 100644 index 54723de21..000000000 --- a/support/judge_webcgi/pc2common.sh +++ /dev/null @@ -1,211 +0,0 @@ -# Meant to be "source'd" into bash scripts. - -# The judge's home directory -JUDGE_HOME=/home/icpc - -# Modify constants as necessary for your installation -PC2_RUN_DIR=${JUDGE_HOME}/pc2 -PC2_CDP=${PC2_RUN_DIR}/current/config - -EXE_LINK_PREFIX=../exedir -PROB_LINK_PREFIX=../probdir - -# How many exedir and probdir links to keep around -NUM_LINK_KEEP=4 - -# Where can we find the contest banner png file for webpage headers -BANNER_FILE=banner.png -BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} - -# Where to the the result of script failure before execution -RESULT_FAILURE_FILE=failure.txt - -# Where judgments are -REJECT_INI=${JUDGE_HOME}/pc2/reject.ini - -# Where PC2 puts CLICS validator results -EXECUTE_DATA_PREFIX=executedata -# Where PC2 puts run output/error -TEST_OUT_PREFIX="teamoutput" -TEST_ERR_PREFIX="teamstderr" -TEST_VALOUT_PREFIX="valout" -TEST_VALERR_PREFIX="valerr" - -# Detailed log of entire judging process -SANDBOX_LOG=sandbox.log - -# Briefcase file prefix -BRIEFCASE_FILE_PREFIX="briefcase" -REPORTS_DIR=reports - -declare -A Judgments - -InitJudgments() -{ - # Defauls, may be overriden by reject.ini - Judgments["accepted"]="AC" - Judgments["Accepted"]="AC" - Judgments["timelimit"]="TLE" - Judgments["run error"]="RTE" - Judgments["compiler error"]="CE" - if test -s ${REJECT_INI} - then - while read j - do - if [[ $j = \#* ]] - then - continue - fi - savIFS="$IFS" - IFS='|' - set $j - IFS="$savIFS" - key="$1" - shortcode="$2" - case ${shortcode} in - AC|CE|RTE|WA|TLE) ;; - MLE) shortcode="RTE (MLE)" ;; - *) shortcode="WA (${shortcode})" ;; - esac -# echo Mapping $key to $shortcode - Judgments[$key]="$shortcode" - done < $REJECT_INI - else echo NO REJECT FILE - fi -} - -# Takes the judgement string, eg. "Wrong answer" as arg 1 and the validation Result (42 or 42) as arg 2 -MapJudgment() -{ - jm="$1" - vr="$2" - result=${Judgments[$jm]} - if test -z ${result} - then - if test ${validationReturnCode} -eq 0 - then - if test ${vr} = "43" - then - resul="WA" - elif test ${vr} = "42" - then - result="AC" - else - result="WA (Default)" - fi - else - result="JE (Validator EC=${validationReturnCode})" - fi - - fi -} - -GetLastJudgmentFile() -{ - lj_exdir="$1" - result=`ls $lj_exdir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1rn | head -1` -} - -GetTestCaseNumber() -{ - if test -z "$1" - then - result=0 - else - saveIFS="$IFS" - IFS=. - set ${1} - result="$2" - IFS="$saveIFS" - if test -z "${result}" - then - result=0 - fi - fi -} - -GetJudgmentFromFile() -{ - exdata="$1" - if test -z "${exdata}" - then - result="No results" - else - # Source the file - . ${exdata} - if test ${compileSuccess} = "false" - then - result="CE" - elif test ${executeSuccess} = "true" - then - if test ${validationSuccess} = "true" - then - MapJudgment "${validationResults}" "${validationReturnCode}" - else - result="JE (Validator error)" - fi - else - result="RTE (Execute error)" - fi - fi -} - -GetJudgment() -{ - dir=$1 - exdata="" - if ! cd ${dir} - then - result="Not found" - elif test -s ${RESULT_FAILURE_FILE} - then - jerr=`cat ${RESULT_FAILURE_FILE}` - result="JE ($jerr)" - else - # We got a real live run - # Check out the biggest executedata file - GetLastJudgmentFile $dir - GetJudgmentFromFile ./${result} - fi -} - -MakeBriefcaseFile() -{ - d="$1" - t="$2" - result=`printf '%s/%s/%s_%03d.log' "$d" "$REPORTS_DIR" "$BRIEFCASE_FILE_PREFIX" "$t"` -} - -# Must redirect from briefcase file -ReadBriefcase() -{ - read judgein - read judgeans - read cpunum - read exepid - read exetime - read execpums cpulimms exewallms mempeak memlim - if [[ $mempeak = [0-9]* ]] - then - mempeak=$((mempeak/(1024*1024))) - fi - if [[ $memlim = [0-9]* ]] - then - memlim=$((memlim/(1024*1024))) - fi -} - -DeleteOldestLinks() -{ - for linkpref in ${EXE_LINK_PREFIX} ${PROB_LINK_PREFIX} - do - dellist=`ls -1td ${linkpref}* | sed 1,${NUM_LINK_KEEP}d` - if test -n "${dellist}" - then - rm -f ${dellist} - fi - done -} - -InitJudgments - diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh deleted file mode 100644 index e88937dea..000000000 --- a/support/judge_webcgi/showrun.sh +++ /dev/null @@ -1,209 +0,0 @@ -#!/bin/bash -. ./pc2common.sh -. ./webcommon.sh -. ./cdpcommon.sh - -EXE_DIR_LINK=${EXE_LINK_PREFIX}$$ -PROB_DIR_LINK=${PROB_LINK_PREFIX}$$ - -# Provide a way to look at the sandbox log -LogButton() -{ - # Read first judgment file to get compile time - it's compiled once for all - GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" - if test -n ${result} -a -n ${compileTimeMS} - then - cat << LBEOF0 -
-

The program took ${compileTimeMS}ms to compile. -

-LBEOF0 - fi - - # Read the first briefcase file (if any) for limits - MakeBriefcaseFile ${EXE_DIR_LINK} 1 - ReadBriefcase < $result - if test -n ${cpulimms} - then - cpulimms=${cpulimms%%.*} - cpusecs="$((cpulimms/1000))" - if test ${cpusecs} != "1" - then - cpusecs="${cpusecs} seconds" - else - cpusecs="1 second" - fi - cat << LBEOF1 -
-

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms). -

-LBEOF1 - fi - if test -n ${memlim} - then - if test ${memlim} = "0" - then - memlim="Unlimited" - else - memlim=${memlim}MB - fi - cat << LBEOF1A -
-

The Memory limit for this problem is ${memlim}. -

-LBEOF1A - fi - sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} - if test -s ${sandlog} - then - cat << LBEOF2 -Click here for the full sandbox log for this run -

-

-LBEOF2 - fi -} - -TableHeader() -{ - cat << EOFTH - -TestDispJudgmentExitExecute TimeVal TimeVal SuccessRun OutRun ErrJudge InJudge AnsVal OutVal Err - -EOFTH -} - -GenFileLink() -{ - tstcase="$2" - tstcase=$((tstcase-1)) - tstfile=$1.$tstcase.txt - tstpath=$dir/$1.$tstcase.txt - if test -s ${tstpath} - then - bytes=`stat -c %s ${tstpath}` - echo ' View ('$bytes' bytes)' - elif test -e ${tstpath} - then - echo ' (Empty)' - else - echo ' Not found' - fi -} - -GenFileLinkWithText() -{ - linkaddr="$1" - linktext="$2" - linkcolor="$3" - bytes=`stat -c %s ${linkaddr}` - echo ' '$linktext' ('$bytes' bytes)' -} - -TableRow() -{ - tc=$1 - judgment="$2" - ec=$3 - exetm="$4" - valtm="$5" - jin="$6" - jans="$7" - if test "$8" = "true" - then - valsucc=Yes - elif test "$8" = "false" - then - valsucc=No - else - valsucc="N/A" - fi - # Strip the stuff of before sample (or secret) - jin=${jin##*/data/} - jans=${jans##*/data/} - # Just the basenames for link text - jinbase=${jin##*/} - jansbase=${jans##*/} - if [[ $jin = sample/* ]] - then - lcolor=#00a0a0 - else - lcolor=#00a000 - fi - if test "${judgment}" = "AC" - then - jicon='' - elif test "${judgment}" = "CE" - then - jicon='' - else - jicon='' - fi - - echo ' ' - - echo ' '$tc'' - echo ' '$jicon'' - echo ' '$judgment'' - echo ' '$ec'' - echo ' '$exetm'ms' - echo ' '$valtm'ms' - echo ' '$valsucc'' - GenFileLink $TEST_OUT_PREFIX $tc - GenFileLink $TEST_ERR_PREFIX $tc - GenFileLinkWithText $PROB_DIR_LINK/data/"$jin" "$jinbase" $lcolor - GenFileLinkWithText $PROB_DIR_LINK/data/"$jans" "$jansbase" $lcolor - GenFileLink $TEST_VALOUT_PREFIX $tc - GenFileLink $TEST_VALERR_PREFIX $tc - - echo ' ' -} - -# Parse query string into dictionary args -# This is not terribly secure. -declare -a parm -sIFS="$IFS" -IFS='=&' -parm=($QUERY_STRING) -IFS=$sIFS -for ((i=0; i<${#parm[@]}; i+=2)) -do - a=${parm[i]} - eval $a=${parm[i+1]} -done -# Done parsing - -DeleteOldestLinks -Preamble - 'Run '$run -HeaderNoBanner Details for Run Id $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" - -# Create links apache can access in our html folder -#rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} -ln -s ${dir} ${EXE_DIR_LINK} -ln -s ${probdir} ${PROB_DIR_LINK} - -LogButton - -StartTable -TableHeader -for testcase in `ls $dir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1n` -do - GetTestCaseNumber $testcase - tc=$((result+1)) - # This will also source the execute data - GetJudgmentFromFile $dir/$testcase - judgment=$result - ec=$executeExitValue -# comptm=$compileTimeMS - exetm=$executeTimeMS - valtm=$validateTimeMS - valsuc=$validationSuccess - MakeBriefcaseFile "$dir" "$tc" - ReadBriefcase < ${result} - TableRow "$tc" "$judgment" "$ec" "$exetm" "$valtm" "$judgein" "$judgeans" "$valsuc" -done - -Trailer - -#rm -f ${EXE_DIR_LINK} ${PROB_DIR_LINK} -exit 0 diff --git a/support/judge_webcgi/webcommon.sh b/support/judge_webcgi/webcommon.sh deleted file mode 100644 index 26c65cd62..000000000 --- a/support/judge_webcgi/webcommon.sh +++ /dev/null @@ -1,157 +0,0 @@ -# File meant to be "source'd" to support generating web pages - -# -# Display very curt textual error message -# -Error() -{ - echo "Content-type: text/plain" - echo "" - echo ERROR $* -} - - -Preamble() -{ - echo "Content-type: text/html" - echo "" - if test $# -eq 0 - then - headmsg="Judge" - else - headmsg="$*" - fi - cat << PREEOF - - - -PC² $headmsg - - - -PREEOF -} - -Header() -{ - # Make sure link to banner page is in a place apache can read - if test ! -e ../${BANNER_FILE} - then - ln -s ${BANNER_IMAGE} ../${BANNER_FILE} - fi -cat << HDREOF -

- -

PC2 Judging Results

-
-

-HDREOF -} - -HeaderNoBanner() -{ - if test $# -eq 0 - then - hdrmsg="Judging Results" - else - hdrmsg="$*" - fi -cat << EOF -

-

PC2 $hdrmsg

-
-

-EOF -} - -Trailer() -{ -cat << EOF2 - - -EOF2 -} - -StartTable() -{ - cat << EOF3 -

- -EOF3 -} - -EndTable() -{ - cat << EOF4 -
-

-EOF4 -} - From c9de0c74f40be68b0a34f9c236a511c76e7e9ecf Mon Sep 17 00:00:00 2001 From: John Buck Date: Sun, 7 Jul 2024 14:15:08 -0400 Subject: [PATCH 07/55] i_972 Clean up scripts Interactive sandbox script needed cleaning up and updating. Fix some bash 'if' statements in case arg is empty string. --- scripts/pc2sandbox.sh | 6 +- scripts/pc2sandbox_interactive.sh | 132 +++++++++++++++++----- support/judge_webcgi/cgi-bin/judge.sh | 6 +- support/judge_webcgi/cgi-bin/pc2common.sh | 18 +-- support/judge_webcgi/cgi-bin/showrun.sh | 34 ++++-- 5 files changed, 146 insertions(+), 50 deletions(-) diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh index 950bcf432..9007e4072 100755 --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -244,9 +244,9 @@ if [ "$#" -lt 1 ] ; then exit $FAIL_NO_ARGS_EXIT_CODE fi -if [ "$#" -lt 3 ] ; then - echo $0: expected 3 or more arguments, found: $* - SysFailure Expected 3 or more arguments to $0, found: $* +if [ "$#" -lt 6 ] ; then + echo $0: expected 6 or more arguments, found: $* + SysFailure Expected 6 or more arguments to $0, found: $* exit $FAIL_INSUFFICIENT_ARGS_EXIT_CODE fi diff --git a/scripts/pc2sandbox_interactive.sh b/scripts/pc2sandbox_interactive.sh index d37f66a07..4c45ab5f7 100755 --- a/scripts/pc2sandbox_interactive.sh +++ b/scripts/pc2sandbox_interactive.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (C) 1989-2023 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. +# Copyright (C) 1989-2024 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. # # File: pc2sandbox_interacive.sh # Purpose: a sandbox for pc2 using Linux CGroups v2 and supporting interactive problems @@ -19,6 +19,10 @@ DEFAULT_CPU_NUM=3 CPU_OVERRIDE_FILE=$HOME/pc2_cpu_override +# Where to the the result of the first failure. If this file is not created, then the +# run was accepted. (correct) +RESULT_FAILURE_FILE=failure.txt + # FAIL_RETCODE_BASE is 128 + 64 + xx # 128 = system error, like signal # 64 = biggest used signal @@ -130,6 +134,13 @@ function REPORT() echo "$@" >> $REPORTFILE fi } +function REPORT_BRIEF() +{ + if test -n "$BRIEFREPORTFILE" + then + echo "$@" >> $BRIEFREPORTFILE + fi +} # For per-testcase report and debugging both function REPORT_DEBUG() @@ -163,24 +174,24 @@ SAGE # Function to kill all processes in the cgroupv2 after process has exited KillcgroupProcs() { - if test -n ${PC2_SANDBOX_CGROUP_PATH_KILL} - then + if test -n ${PC2_SANDBOX_CGROUP_PATH_KILL} + then DEBUG echo "Purging cgroup ${PC2_SANDBOX_CGROUP_PATH_KILL} of processes" - echo 1 > ${PC2_SANDBOX_CGROUP_PATH_KILL} - fi + echo 1 > ${PC2_SANDBOX_CGROUP_PATH_KILL} + fi } # Kill active children and stragglers KillChildProcs() { - DEBUG echo "Killing off submission process group $submissionpid and all children" - # Kill off process group - if test -n "$submissionpid" - then - pkill -9 -s $submissionpid - fi - # and... extra stragglers with us as a parent - pkill -9 -P $$ + DEBUG echo "Killing off submission process group $submissionpid and all children" + # Kill off process group + if test -n "$submissionpid" + then + pkill -9 -s $submissionpid + fi + # and... extra stragglers with us as a parent + pkill -9 -P $$ } # Kill off the validator if it is still running @@ -226,6 +237,10 @@ ShowStats() walltime=$3 memused=$4 memlim=$5 + REPORT_BRIEF "$(printf '%d.%03d' $((cpuused / 1000)) $((cpuused % 1000)))" \ + "$(printf '%d.%03d' $((cpulim / 1000)) $((cpulim % 1000)))" \ + "$(printf '%d.%03d' $((walltime / 1000)) $((walltime % 1000)))" \ + ${memused} $((memlim)) REPORT_DEBUG Resources used for this run: REPORT_DEBUG "$(printf ' CPU ms Limit ms Wall ms Memory Used Memory Limit\n')" REPORT_DEBUG "$(printf '%5d.%03d %5d.%03d %6d.%03d %12s %12d\n' $((cpuused / 1000)) $((cpuused % 1000)) \ @@ -234,6 +249,27 @@ ShowStats() ${memused} $((memlim)))" } +# Generate a system failure result file. This will be the FIRST failure as it will not overwrite the file +# if it exists. +SysFailure() +{ + if test ! -s ${RESULT_FAILURE_FILE} + then + (echo system; echo $* ) > ${RESULT_FAILURE_FILE} + fi +} + +# Generate a failure result file. This will be the FIRST failure as it will not overwrite the file +# if it exists. +Failure() +{ + if test ! -s ${RESULT_FAILURE_FILE} + then + echo $* > ${RESULT_FAILURE_FILE} + fi +} + + sent_xml=0 GenXML() @@ -253,6 +289,7 @@ EOF if [ "$#" -lt 1 ] ; then echo $0: Missing command line arguments. Try '"'"$0 --help"'"' for help. + SysFailure No command line arguments to $0 exit $FAIL_NO_ARGS_EXIT_CODE fi @@ -263,6 +300,7 @@ fi if [ "$#" -lt 7 ] ; then echo $0: expected 7 or more arguments, found: $* + SysFailure Expected 7 or more arguments to $0, found: $* exit $FAIL_INSUFFICIENT_ARGS_EXIT_CODE fi @@ -277,6 +315,19 @@ DEBUG echo "+---------------- Test Case ${TESTCASE} ----------------+" DEBUG echo Command line: $0 $* shift 7 +DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" +RUN_LOCAL_CMD="$0 ${MEMLIMIT} ${TIMELIMIT} ${VALIDATOR} ${JUDGEIN} ${JUDGEANS} ${TESTCASE} ${COMMAND} $*" +tcfile=`printf "$REPORTDIR/testcase_%03d.log" $TESTCASE` +REPORT_OUTPUT_CMD="more ${tcfile}" +DEBUG echo -e "\n${RUN_LOCAL_CMD}" +#DEBUG echo -e "\nor, without the sandbox by using the following command:" +#DEBUG echo -e "\n${COMMAND} $* < ${JUDGEIN} > $TESTCASE.ans" +DEBUG echo -e "\nAnd see the run report using the following command:" +DEBUG echo -e "\n${REPORT_OUTPUT_CMD}\n" + +# Skip used arguments +shift 7 + # the rest of the commmand line arguments are the command args for the submission @@ -284,42 +335,49 @@ shift 7 DEBUG echo checking PC2 CGroup V2 installation... if [ ! -d "$PC2_CGROUP_PATH" ]; then - echo $0: expected pc2sandbox CGroups v2 installation in $PC2_CGROUP_PATH + echo $0: expected pc2sandbox CGroups v2 installation in $PC2_CGROUP_PATH + SysFailure CGroups v2 not installed in $PC2_CGROUP_PATH exit $FAIL_INVALID_CGROUP_INSTALLATION fi if [ ! -f "$CGROUP_PATH/cgroup.controllers" ]; then echo $0: missing file cgroup.controllers in $CGROUP_PATH + SysFailure Missing cgroup.controllers in $CGROUP_PATH exit $FAIL_MISSING_CGROUP_CONTROLLERS_FILE fi if [ ! -f "$CGROUP_PATH/cgroup.subtree_control" ]; then echo $0: missing file cgroup.subtree_control in $CGROUP_PATH + SysFailure Missing cgroup.subtree_controll in $CGORUP_PATH exit $FAIL_MISSING_CGROUP_SUBTREE_CONTROL_FILE fi # make sure the cpu and memory controllers are enabled if ! grep -q -F "cpu" "$CGROUP_PATH/cgroup.subtree_control"; then echo $0: cgroup.subtree_control in $CGROUP_PATH does not enable cpu controller + SysFailure CPU controller not enabled in cgroup.subtree_control in $CGROUP_PATH exit $FAIL_CPU_CONTROLLER_NOT_ENABLED fi if ! grep -q -F "memory" "$CGROUP_PATH/cgroup.subtree_control"; then echo $0: cgroup.subtree_control in $CGROUP_PATH does not enable memory controller + SysFailure Memory controller not enabled in cgroup.subtree_control in $CGROUP_PATH exit $FAIL_MEMORY_CONTROLLER_NOT_ENABLED fi if [ ! -e "$VALIDATOR" ]; then echo $0: The interactive validator \'"$VALIDATOR"\' was not found + SysFailure The interactive validator \'"$VALIDATOR"\' was not found exit $FAIL_INTERACTIVE_ERROR fi if [ ! -x "$VALIDATOR" ]; then echo $0: The interactive validator \'"$VALIDATOR"\' is not an executable file + SysFailure The interactive validator \'"$VALIDATOR"\' is not an executable file exit $FAIL_INTERACTIVE_ERROR fi -# we seem to have a valid CGroup installation +# we seem to have a valid CGroup and validator setup DEBUG echo ...done. if test -d $PC2_SANDBOX_CGROUP_PATH @@ -329,6 +387,7 @@ then if ! rmdir $PC2_SANDBOX_CGROUP_PATH then DEBUG echo Cannot purge old sandbox: $PC2_SANDBOX_CGROUP_PATH + SysFailure Cannot purge old sandbox: $PC2_SANDBOX_CGROUP_PATH exit $FAIL_SANDBOX_ERROR fi fi @@ -337,6 +396,7 @@ DEBUG echo Creating sandbox $PC2_SANDBOX_CGROUP_PATH if ! mkdir $PC2_SANDBOX_CGROUP_PATH then DEBUG echo Cannot create $PC2_SANDBOX_CGROUP_PATH + SysFailure Cannot create sandbox: $PC2_SANDBOX_CGROUP_PATH exit $FAIL_INVALID_CGROUP_INSTALLATION fi @@ -344,12 +404,14 @@ fi rm -f "$INFIFO" "$OUTFIFO" if ! mkfifo --mode=$FIFOMODE $INFIFO then - DEBUG Can not create FIFO $INFIFO 1>&2 + DEBUG Can not create input FIFO $INFIFO 1>&2 + SysFailure Cannot create input FIFO: $INFIFO exit $FAIL_INTERACTIVE_ERROR fi if ! mkfifo --mode=$FIFOMODE $OUTFIFO then - DEBUG Can not create FIFO $INFIFO 1>&2 + DEBUG Can not create output FIFO $OUTFIFO 1>&2 + SysFailure Cannot create output FIFO: $OUTFIFO rm $INFIFO exit $FAIL_INTERACTIVE_ERROR fi @@ -359,17 +421,22 @@ mkdir -p "$REPORTDIR" # Set report file to be testcase specific one now REPORTFILE=`printf "$REPORTDIR/testcase_%03d.log" $TESTCASE` +BRIEFREPORTFILE=`printf "$REPORTDIR/briefcase_%03d.log" $TESTCASE` +DEBUG echo Report file: ${REPORTFILE} Brief Report File: ${BRIEFREORTFILE} +REPORT Test case $TESTCASE: +REPORT Command: "${RUN_LOCAL_CMD}" +REPORT Report: " ${REPORT_OUTPUT_CMD}" # set the specified memory limit - input is in MB, cgroup v2 requires bytes, so multiply by 1M # but only if > 0. # "max" means unlimited, which is the cgroup v2 default DEBUG echo checking memory limit if [ "$MEMLIMIT" -gt "0" ] ; then - REPORT Setting memory limit to $MEMLIMIT MB + REPORT_DEBUG Setting memory limit to $MEMLIMIT MiB echo $(( $MEMLIMIT * 1024 * 1024 )) > $PC2_SANDBOX_CGROUP_PATH/memory.max echo 1 > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max else - REPORT Setting memory limit to max, meaning no limit + REPORT_DEBUG Setting memory limit to max, meaning no limit echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.max echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max fi @@ -384,22 +451,29 @@ mkdir -p "$INT_FEEDBACKDIR" # Note that starting of the validator will block until the submission is started since # it will block on the $INFIFO (no one else connected yet) -REPORT Starting: "$VALIDATOR $JUDGEIN $JUDGEANS $INT_FEEDBACKDIR > $INFIFO < $OUTFIFO &" +REPORT_DEBUG Starting: "$VALIDATOR $JUDGEIN $JUDGEANS $INT_FEEDBACKDIR > $INFIFO < $OUTFIFO &" $VALIDATOR $JUDGEIN $JUDGEANS $INT_FEEDBACKDIR > $INFIFO < $OUTFIFO & intv_pid=$! -REPORT Started interactive validator PID $intv_pid +REPORT_DEBUG Started interactive validator PID $intv_pid # We use ulimit to limit CPU time, not cgroups. Time is supplied in seconds. This may have to # be reworked if ms accuracy is needed. The problem is, cgroups do not kill off a process that # exceeds the time limit, ulimit does. TIMELIMIT_US=$((TIMELIMIT * 1000000)) -REPORT Setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" +REPORT_DEBUG Setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" ulimit -t $TIMELIMIT MAXPROCS=$((MAXPROCS+`ps -T -u $USER | wc -l`)) -REPORT Setting maximum user processes to $MAXPROCS +REPORT_DEBUG Setting maximum user processes to $MAXPROCS ulimit -u $MAXPROCS +# Keep track of details for reports +REPORT_BRIEF ${JUDGEIN} +REPORT_BRIEF ${JUDGEANS} +REPORT_BRIEF $cpunum +REPORT_BRIEF $$ +REPORT_BRIEF $(date "+%F %T.%6N") + # Remember wall time when we started starttime=`GetTimeInMicros` @@ -408,6 +482,7 @@ REPORT Putting $$ into $PC2_SANDBOX_CGROUP_PATH cgroup if ! echo $$ > $PC2_SANDBOX_CGROUP_PATH/cgroup.procs then echo $0: Could not add current process to $PC2_SANDBOX_CGROUP_PATH/cgroup.procs - not executing submission. + SysFailure Could not add current process to $PC2_SANDBOX_CGROUP_PATH/cgroup.procs rm -f "$INFIFO" "$OUTFIFO" exit $FAIL_SANDBOX_ERROR fi @@ -435,19 +510,19 @@ do # A return code 127 indicates there are no more children. How did that happen? if test $wstat -eq 127 then - REPORT No more children found while waiting: Submission PID was $submissionpid and Interactive Validator PID was $intv_pid + REPORT_DEBUG No more children found while waiting: Submission PID was $submissionpid and Interactive Validator PID was $intv_pid break fi # If interactive validator finishes if test "$child_pid" -eq "$intv_pid" then - REPORT Validator finishes with status $wstat + REPORT_DEBUG Validator finishes with exit code $wstat if test "$contestant_done" -eq 0 then # Only kill it if it still exists if test -d /proc/$contestantpid then - REPORT Contestant PID $submissionpid has not finished - killing it + REPORT_DEBUG Contestant PID $submissionpid has not finished - killing it # TODO: We should kill and wait for it here and print out the stats fi # This just determines if the program ran, not if it's correct. @@ -493,7 +568,7 @@ do fi - REPORT Contestant PID $submissionpid finished with status $wstat + REPORT_DEBUG Contestant PID $submissionpid finished with exit code $wstat contestant_done=1 COMMAND_EXIT_CODE=$wstat @@ -505,6 +580,7 @@ do if test "$kills" != "0" then REPORT_DEBUG The command was killed because it exceeded the memory limit + REPORT_BRIEF MLE COMMAND_EXIT_CODE=${FAIL_MEMORY_LIMIT_EXCEEDED} GenXML "No - Memory limit exceeded" "" KillValidator @@ -514,6 +590,7 @@ do if test "$COMMAND_EXIT_CODE" -eq 137 -o "$cputime" -gt "$TIMELIMIT_US" then REPORT_DEBUG The command was killed because it exceeded the CPU Time limit + REPORT_BRIEF TLE COMMAND_EXIT_CODE=${FAIL_TIME_LIMIT_EXCEEDED} GenXML "No - Time limit exceeded" "${cputime}us > ${TIMELIMIT_US}us" KillValidator @@ -531,6 +608,7 @@ do if test "$wstat" -ne 0 then REPORT_DEBUG Contestant finished abnormally - killing validator + REPORT_BRIEF RTE KillValidator GenXML "No - Run-time Error" "Exit status $wstat" break diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh index e1b6bb740..c3b121849 100644 --- a/support/judge_webcgi/cgi-bin/judge.sh +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -26,15 +26,15 @@ TableRow() judge="${10}" probname="" probdir="" - if test -n ${shortname} + if test -n "${shortname}" then probdir=${PC2_CDP}/${shortname} probstatement=${probdir}/problem_statement/problem.en.tex - if test ! -s ${probstatement} + if test ! -s "{probstatement}" then probstatement=${probdir}/problem_statement/problem.tex fi - if test -s ${probstatement} + if test -s "${probstatement}" then probname=`head -1 ${probstatement}` probname=${probname##*\{} diff --git a/support/judge_webcgi/cgi-bin/pc2common.sh b/support/judge_webcgi/cgi-bin/pc2common.sh index eac62c936..d9b6e2aed 100644 --- a/support/judge_webcgi/cgi-bin/pc2common.sh +++ b/support/judge_webcgi/cgi-bin/pc2common.sh @@ -50,7 +50,7 @@ InitJudgments() Judgments["timelimit"]="TLE" Judgments["run error"]="RTE" Judgments["compiler error"]="CE" - if test -s ${REJECT_INI} + if test -s "${REJECT_INI}" then while read j do @@ -82,14 +82,14 @@ MapJudgment() jm="$1" vr="$2" result=${Judgments[$jm]} - if test -z ${result} + if test -z "${result}" then - if test ${validationReturnCode} -eq 0 + if test "${validationReturnCode}" -eq 0 then - if test ${vr} = "43" + if test "${vr}" = "43" then resul="WA" - elif test ${vr} = "42" + elif test "${vr}" = "42" then result="AC" else @@ -135,12 +135,12 @@ GetJudgmentFromFile() else # Source the file . ${exdata} - if test ${compileSuccess} = "false" + if test "${compileSuccess}" = "false" then result="CE" - elif test ${executeSuccess} = "true" + elif test "${executeSuccess}" = "true" then - if test ${validationSuccess} = "true" + if test "${validationSuccess}" = "true" then MapJudgment "${validationResults}" "${validationReturnCode}" else @@ -159,7 +159,7 @@ GetJudgment() if ! cd ${dir} then result="Not found" - elif test -s ${RESULT_FAILURE_FILE} + elif test -s "${RESULT_FAILURE_FILE}" then jerr=`cat ${RESULT_FAILURE_FILE}` result="JE ($jerr)" diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh index 87a848d00..4c1e5b2c1 100644 --- a/support/judge_webcgi/cgi-bin/showrun.sh +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -11,7 +11,7 @@ LogButton() { # Read first judgment file to get compile time - it's compiled once for all GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" - if test -n ${result} -a -n ${compileTimeMS} + if test -n "${result}" -a -n "${compileTimeMS}" then cat << LBEOF0

@@ -22,8 +22,14 @@ LBEOF0 # Read the first briefcase file (if any) for limits MakeBriefcaseFile ${EXE_DIR_LINK} 1 - ReadBriefcase < $result - if test -n ${cpulimms} + if test -s "${result}" + then + ReadBriefcase < $result + else + cpulimms="" + memlim="" + fi + if test -n "${cpulimms}" then cpulimms=${cpulimms%%.*} cpusecs="$((cpulimms/1000))" @@ -38,8 +44,14 @@ LBEOF0

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms).

LBEOF1 + else + cat << LBEOF1AA +
+

The CPU Limit for this problem is N/A. +

+LBEOF1AA fi - if test -n ${memlim} + if test -n "${memlim}" then if test ${memlim} = "0" then @@ -52,9 +64,15 @@ LBEOF1

The Memory limit for this problem is ${memlim}. LBEOF1A + else + cat << LBEOF1AAA +

+

The Memory limit for this problem is N/A. +

+LBEOF1AAA fi sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} - if test -s ${sandlog} + if test -s "${sandlog}" then cat << LBEOF2
Click here for the full sandbox log for this run @@ -92,11 +110,11 @@ GenFileLink() tstcase=$((tstcase-1)) tstfile=$1.$tstcase.txt tstpath=$dir/$1.$tstcase.txt - if test -s ${tstpath} + if test -s "${tstpath}" then bytes=`stat -c %s ${tstpath}` echo ' View ('$bytes' bytes)' - elif test -e ${tstpath} + elif test -e "${tstpath}" then echo ' (Empty)' else @@ -166,7 +184,7 @@ TableRow() echo ' ' - if test -n ${tcreport} + if test -n "${tcreport}" then echo ' '$tc'' else From 2f036439b319dccb01a9e698f888dd525a4c2916 Mon Sep 17 00:00:00 2001 From: John Buck Date: Sun, 7 Jul 2024 17:20:45 -0400 Subject: [PATCH 08/55] i_972 More judge web page fixes Remove extra space when generating the execute data file. Correct the "whats this" message for the judge execute folder pattern. Add jquery for sorting tables. --- scripts/pc2sandbox.sh | 12 ++-- .../csus/ecs/pc2/core/execute/Executable.java | 2 +- .../ecs/pc2/ui/ContestInformationPane.java | 2 +- support/judge_webcgi/cgi-bin/judge.sh | 28 +++++++++- support/judge_webcgi/cgi-bin/showrun.sh | 3 + support/judge_webcgi/cgi-bin/webcommon.sh | 56 ++++++++++++++++++- .../scripts/jquery-3.7.1.slim.min.js | 2 + 7 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 support/judge_webcgi/scripts/jquery-3.7.1.slim.min.js diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh index 9007e4072..fe3de06a4 100755 --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -263,12 +263,8 @@ TESTCASE=$5 COMMAND=$6 DEBUG echo "+---------------- Test Case ${TESTCASE} ----------------+" DEBUG echo Command line: $0 $* -shift -shift -shift -shift -shift -shift +shift 6 + DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" RUN_LOCAL_CMD="$0 ${MEMLIMIT} ${TIMELIMIT} xxx xxx ${COMMAND} $* < ${JUDGEIN} > $TESTCASE.ans" DIFF_OUTPUT_CMD="diff -w ${JUDGEANS} $TESTCASE.ans | more" @@ -363,11 +359,11 @@ REPORT Diff: " ${DIFF_OUTPUT_CMD}" # "max" means unlimited, which is the cgroup v2 default DEBUG echo checking memory limit if [ "$MEMLIMIT" -gt "0" ] ; then - REPORT_DEBUG echo Setting memory limit to $MEMLIMIT MB + REPORT_DEBUG Setting memory limit to $MEMLIMIT MiB echo $(( $MEMLIMIT * 1024 * 1024 )) > $PC2_SANDBOX_CGROUP_PATH/memory.max echo 1 > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max else - REPORT_DEBUG echo Setting memory limit to max, meaning no limit + REPORT_DEBUG Setting memory limit to max, meaning no limit echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.max echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max fi diff --git a/src/edu/csus/ecs/pc2/core/execute/Executable.java b/src/edu/csus/ecs/pc2/core/execute/Executable.java index ad93794c9..89a273895 100644 --- a/src/edu/csus/ecs/pc2/core/execute/Executable.java +++ b/src/edu/csus/ecs/pc2/core/execute/Executable.java @@ -3770,7 +3770,7 @@ private boolean saveExecuteData(int dataSetNumber) executeDataWriter.println("compileResultCode='" + executionData.getCompileResultCode() + "'"); executeDataWriter.println("executeExitValue='" + executionData.getExecuteExitValue() + "'"); executeDataWriter.println("executeSuccess='" + executionData.isExecuteSucess() + "'"); - executeDataWriter.println("validationReturnCode ='" + executionData.getValidationReturnCode() + "'"); + executeDataWriter.println("validationReturnCode='" + executionData.getValidationReturnCode() + "'"); executeDataWriter.println("validationSuccess='" + executionData.isValidationSuccess() + "'"); executeDataWriter.println("validationResults='" + showNullAsEmpty(executionData.getValidationResults()) + "'"); executeDataWriter.println("compileTimeMS='" + executionData.getCompileTimeMS() + "'"); diff --git a/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java b/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java index ed3742985..0832e475e 100644 --- a/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java +++ b/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java @@ -1461,7 +1461,7 @@ public void mousePressed(MouseEvent e) { + "\n\nAny substitution string available for an executable is allowed here." - + "\n\nSo for example a judge's execute folder string like \"executesite{:siteid}judge{:clientname}_Run_{:runnumber}\" would change the execute folder to something like:" // + + "\n\nSo for example a judge's execute folder string like \"executesite{:siteid}{:clientname}_Run_{:runnumber}\" would change the execute folder to something like:" // + "\n executesite1judge1_Run_220 " // + "\n\nSubstitution values depend on the corresponding data having been loaded into the PC^2 Server; if there is no value defined for a" // diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh index c3b121849..f33a23a86 100644 --- a/support/judge_webcgi/cgi-bin/judge.sh +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -3,15 +3,33 @@ . ./webcommon.sh . ./cdpcommon.sh + TableHeader() { cat << EOFTH -Run IDDispJudgmentProblemTeamTest CasesLanguageJudgeTime Judged +Run ID +Disp +Judgment +Problem +Team +Test Cases +Language +Judge +Time Judged EOFTH } +MyTableStyles() +{ + cat << EOFMYSTYLES +th { + cursor: pointer; +} +EOFMYSTYLES +} + TableRow() { dir="$1" @@ -70,8 +88,11 @@ TableRow() ###ParseProblemYaml Preamble +Styles +MyTableStyles +EndStyles +StartHTMLDoc Header -LogButton StartTable TableHeader @@ -113,12 +134,13 @@ do numcases="??" fi # Note that GetJudgment also filled in exdata with the last execute data - GetLastTestCaseNumber "${exdata}" + GetTestCaseNumber "${exdata##./}" testcaseinfo=$((result+1))/${numcases} TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" fi done EndTable +TableSortScripts Trailer exit 0 diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh index 4c1e5b2c1..97c0c3391 100644 --- a/support/judge_webcgi/cgi-bin/showrun.sh +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -233,6 +233,9 @@ done DeleteOldestLinks Preamble - 'Run '$run +Styles +EndStyles +StartHTMLDoc HeaderNoBanner Details for Run Id $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" # Create links apache can access in our html folder diff --git a/support/judge_webcgi/cgi-bin/webcommon.sh b/support/judge_webcgi/cgi-bin/webcommon.sh index 03fddd8a3..2e513343f 100644 --- a/support/judge_webcgi/cgi-bin/webcommon.sh +++ b/support/judge_webcgi/cgi-bin/webcommon.sh @@ -29,6 +29,12 @@ Preamble() PC² $headmsg +PREEOF +} + +Styles() +{ + cat << EOFSTYLE +EndStyles() +{ + echo "" +} + +StartHTMLDoc() +{ + cat << EOFSHTML -PREEOF +EOFSHTML +} + +TableSortScripts() +{ + cat << EOFSCR + + +EOFSCR } Header() diff --git a/support/judge_webcgi/scripts/jquery-3.7.1.slim.min.js b/support/judge_webcgi/scripts/jquery-3.7.1.slim.min.js new file mode 100644 index 000000000..35906b929 --- /dev/null +++ b/support/judge_webcgi/scripts/jquery-3.7.1.slim.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},m=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||m).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),b=new RegExp(ge+"|>"),A=new RegExp(g),D=new RegExp("^"+t+"$"),N={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+d),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},L=/^(?:input|select|textarea|button)$/i,j=/^h\d$/i,O=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,P=/[+~]/,H=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),q=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},R=function(){V()},M=K(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{E.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){E={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,d=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==d&&9!==d&&11!==d)return n;if(!r&&(V(e),e=e||C,T)){if(11!==d&&(u=O.exec(t)))if(i=u[1]){if(9===d){if(!(a=e.getElementById(i)))return n;if(a.id===i)return E.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return E.call(n,a),n}else{if(u[2])return E.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return E.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||p&&p.test(t))){if(c=t,f=e,1===d&&(b.test(t)||m.test(t))){(f=P.test(t)&&X(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=k)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+G(l[o]);c=l.join(",")}try{return E.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>x.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function B(e){return e[k]=!0,e}function F(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function $(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&M(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function U(a){return B(function(o){return o=+o,B(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function X(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=C&&9===n.nodeType&&n.documentElement&&(r=(C=n).documentElement,T=!ce.isXMLDoc(C),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=C&&(t=C.defaultView)&&t.top!==t&&t.addEventListener("unload",R),le.getById=F(function(e){return r.appendChild(e).id=ce.expando,!C.getElementsByName||!C.getElementsByName(ce.expando).length}),le.disconnectedMatch=F(function(e){return i.call(e,"*")}),le.scope=F(function(){return C.querySelectorAll(":scope")}),le.cssHas=F(function(){try{return C.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(x.filter.ID=function(e){var t=e.replace(H,q);return function(e){return e.getAttribute("id")===t}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&T){var n=t.getElementById(e);return n?[n]:[]}}):(x.filter.ID=function(e){var n=e.replace(H,q);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&T){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),x.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},x.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&T)return t.getElementsByClassName(e)},p=[],F(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||p.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+k+"-]").length||p.push("~="),e.querySelectorAll("a#"+k+"+*").length||p.push(".#.+[+~]"),e.querySelectorAll(":checked").length||p.push(":checked"),(t=C.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&p.push(":enabled",":disabled"),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||p.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||p.push(":has"),p=p.length&&new RegExp(p.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===C||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),C}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),T&&!h[t+" "]&&(!p||!p.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(H,q),e[3]=(e[3]||e[4]||e[5]||"").replace(H,q),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return N.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&A.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(H,q).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||E,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:k.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:m,!0)),C.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=m.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,E=ce(m);var S=/^(?:parents|prev(?:Until|All))/,A={children:!0,contents:!0,next:!0,prev:!0};function D(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;re=m.createDocumentFragment().appendChild(m.createElement("div")),(be=m.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),re.appendChild(be),le.checkClone=re.cloneNode(!0).cloneNode(!0).lastChild.checked,re.innerHTML="",le.noCloneChecked=!!re.cloneNode(!0).lastChild.defaultValue,re.innerHTML="",le.option=!!re.lastChild;var Te={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Ee(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function ke(e,t){for(var n=0,r=e.length;n",""]);var Se=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),d=[],p=0,h=e.length;p\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Me(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Ie(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function We(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n
",2===yt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=m.implementation.createHTMLDocument("")).createElement("base")).href=m.location.href,t.head.appendChild(r)):t=m),o=!n&&[],(i=C.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||K})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return R(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Qe(le.pixelPosition,function(e,t){if(t)return t=Ve(e,n),$e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return R(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0 Date: Sun, 30 Jun 2024 17:06:20 -0400 Subject: [PATCH 09/55] i_972 Add support for better human judge reviewing Modify scripts to generate information to help a human judge figure out why a run failed. Better determination of which CPU to run a submission on in a sandbox. Add some basic Web page scripts to make judge results web pages on each judge machine. Add a configurable execute folder that allows substitute variables. This is good so that the execute folders may be retained for each run {:runnumber} in the execute folder, for example. CI: toString method of ExecutionData was not correct. --- scripts/pc2sandbox.sh | 191 +++++-- scripts/pc2sandbox_interactive.sh | 55 +- src/edu/csus/ecs/pc2/core/Constants.java | 2 +- .../csus/ecs/pc2/core/execute/Executable.java | 109 +++- .../ecs/pc2/core/execute/ExecutionData.java | 54 +- .../pc2/core/model/ContestInformation.java | 252 +++++----- .../ecs/pc2/ui/ContestInformationPane.java | 473 +++++++++++------- support/judge_webcgi/judge | 178 +++++++ support/judge_webcgi/showrun.sh | 106 ++++ 9 files changed, 1061 insertions(+), 359 deletions(-) create mode 100644 support/judge_webcgi/judge create mode 100644 support/judge_webcgi/showrun.sh diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh index 30afd9aeb..08d536e93 100755 --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -1,15 +1,24 @@ #!/bin/bash -# Copyright (C) 1989-2023 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. +# Copyright (C) 1989-2024 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. # # File: pc2sandbox.sh # Purpose: a sandbox for pc2 using Linux CGroups v2. # Input arguments: # $1: memory limit in MB # $2: time limit in seconds -# $3: command to be executed -# $4... : command arguments +# $3: judges_input_file +# $4: judges_answer_file +# $5: testcase number +# $6: command to be executed +# $7... : command arguments # # Author: John Buck, based on earlier versions by John Clevenger and Doug Lane +DEFAULT_CPU_NUM=3 +CPU_OVERRIDE_FILE=$HOME/pc2_cpu_override + +# Where to the the result of the first failure. If this file is not created, then the +# run was accepted. (correct) +RESULT_FAILURE_FILE=failure.txt # FAIL_RETCODE_BASE is 128 + 64 + xx # 128 = system error, like signal @@ -32,14 +41,43 @@ FAIL_SANDBOX_ERROR=$((FAIL_RETCODE_BASE+54)) # This gets added to the current number of executing processes for this user. MAXPROCS=32 -# taskset cpu mask for running submission on single processor -cpunum=${USER/judge/} -if [[ "$cpunum" =~ ^[1-5]$ ]] +# Compute taskset cpu mask for running submission on single processor + +# Get system's maximum CPU number +MAX_CPU_NUM=`lscpu -p=cpu | tail -1` + +# See if the admin wants to override the CPU by reading the override file +if test -s ${CPU_OVERRIDE_FILE} then - CPUMASK=$((1<<(cpunum-1))) -else - CPUMASK=0x08 + # This will get the first line that consists of numbers only + cpunum=`egrep '^[0-9]+$' ${CPU_OVERRIDE_FILE} | head -1` + if test -z ${cpunum} + then + cpunum="" + elif test ${cpunum} -gt ${MAX_CPU_NUM} + then + cpunum="" + fi +fi + +# If there was no override or the override was bad, let's try to figure out the cpunum +if test -z ${cpunum} +then + # The login id must be "judge#" where # is the desired CPU and judge number. + # If the login is not "judge#", then the system default is used. + # This special case is for when you want to run multiple judges on one computer + # that has lots of CPU's, but want to pin each judge to its own cpu. + cpunum=${USER/judge/} + if [[ "$cpunum" =~ ^[1-9][0-9]*$ ]] + then + # Restrict to number of CPU's. + cpunum=$(((cpunum-1)%(MAX_CPU_NUM+1))) + else + cpunum=$(((DEFAULT_CPU_NUM+1))) + fi fi +cpunum=$((cpunum-1)) +CPUMASK=$((1<> $DEBUG_FILE } +# For per-testcase reporting/logging +function REPORT() +{ + if test -n "$REPORTFILE" + then + echo "$@" >> $REPORTFILE + fi +} +function REPORT_BRIEF() +{ + if test -n "$BRIEFREPORTFILE" + then + echo "$@" >> $BRIEFREPORTFILE + fi +} + +# For per-testcase report and debugging both +function REPORT_DEBUG() +{ + if test -n "$REPORTFILE" + then + echo "$@" >> $REPORTFILE + fi + [ "$_DEBUG" == "on" ] && echo "$@" >> $DEBUG_FILE +} + # ------------------------------------------------------------ usage() @@ -109,9 +176,10 @@ KillChildProcs() # is wall-time exceeded which is execute time limit + 1 second HandleTerminateFromPC2() { - DEBUG echo "Received TERMINATE signal from PC2" + REPORT_DEBUG "Received TERMINATE signal from PC2" + REPORT_DEBUG "Kiling off submission process group $submissionpid and all children" KillChildProcs - DEBUG echo $0: Wall time exceeded - exiting with code $FAIL_WALL_TIME_LIMIT_EXCEEDED + REPORT_DEBUG $0: Wall time exceeded - exiting with code $FAIL_WALL_TIME_LIMIT_EXCEEDED exit $FAIL_WALL_TIME_LIMIT_EXCEEDED } @@ -136,23 +204,49 @@ ShowStats() walltime=$3 memused=$4 memlim=$5 - DEBUG echo Resources used for this run: - DEBUG printf " CPU ms Limit ms Wall ms Memory Used Memory Limit\n" - DEBUG printf "%5d.%03d %5d.%03d %6d.%03d %12s %12d\n" $((cpuused / 1000)) $((cpuused % 1000)) \ + REPORT_BRIEF "$(printf '%d.%03d' $((cpuused / 1000)) $((cpuused % 1000)))" \ + "$(printf '%d.%03d' $((cpulim / 1000)) $((cpulim % 1000)))" \ + "$(printf '%d.%03d' $((walltime / 1000)) $((walltime % 1000)))" \ + ${memused} $((memlim)) + REPORT_DEBUG Resources used for this run: + REPORT_DEBUG "$(printf ' CPU ms Limit ms Wall ms Memory Used Memory Limit\n')" + REPORT_DEBUG "$(printf '%5d.%03d %5d.%03d %6d.%03d %12s %12d\n' $((cpuused / 1000)) $((cpuused % 1000)) \ $((cpulim / 1000)) $((cpulim % 1000)) \ $((walltime / 1000)) $((walltime % 1000)) \ - $((memused)) $((memlim)) + ${memused} $((memlim)))" +} + +# Generate a system failure result file. This will be the FIRST failure as it will not overwrite the file +# if it exists. +SysFailure() +{ + if test ! -s ${RESULT_FAILURE_FILE} + then + (echo system; echo $* ) > ${RESULT_FAILURE_FILE} + fi +} + +# Generate a failure result file. This will be the FIRST failure as it will not overwrite the file +# if it exists. +Failure() +{ + if test ! -s ${RESULT_FAILURE_FILE} + then + echo $* > ${RESULT_FAILURE_FILE} + fi } # ------------------------------------------------------------ if [ "$#" -lt 1 ] ; then echo $0: No command line arguments + SysFailure No command line arguments to $0 exit $FAIL_NO_ARGS_EXIT_CODE fi if [ "$#" -lt 3 ] ; then echo $0: expected 3 or more arguments, found: $* + SysFailure Expected 3 or more arguments to $0, found: $* exit $FAIL_INSUFFICIENT_ARGS_EXIT_CODE fi @@ -163,14 +257,30 @@ fi MEMLIMIT=$1 TIMELIMIT=$2 -COMMAND=$3 +JUDGEIN=$3 +JUDGEANS=$4 +TESTCASE=$5 +COMMAND=$6 +DEBUG echo "+---------------- Test Case ${TESTCASE} ----------------+" +DEBUG echo Command line: $0 $* shift shift shift - -#### Debugging - just set expected first 3 args to: 16MB 5seconds +shift +shift +DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" +DEBUG echo -e "\n$0" ${MEMLIMIT} ${TIMELIMIT} xxx xxx $* "< ${JUDGEIN} > $TESTCASE.ans" +DEBUG echo -e "\nor, without the sandbox by using the following command:" +DEBUG echo -e "\n$* < ${JUDGEIN} > $TESTCASE.ans" +DEBUG echo -e "\nAnd compare with the judge's answer:" +DEBUG echo -e "\ndiff -w ${JUDGEANS} $TESTCASE.ans | more\n" + +#### Debugging - just set expected first args to: 8MB 2seconds ###MEMLIMIT=8 ###TIMELIMIT=2 +###JUDGEIN=none +###JUDGEANS=none +###TESTCASE=1 ###COMMAND=$1 ###shift @@ -181,27 +291,32 @@ shift DEBUG echo checking PC2 CGroup V2 installation... if [ ! -d "$PC2_CGROUP_PATH" ]; then echo $0: expected pc2sandbox CGroups v2 installation in $PC2_CGROUP_PATH + SysFailure CGroups v2 not installed in $PC2_CGROUP_PATH exit $FAIL_INVALID_CGROUP_INSTALLATION fi if [ ! -f "$CGROUP_PATH/cgroup.controllers" ]; then echo $0: missing file cgroup.controllers in $CGROUP_PATH + SysFailure Missing cgroup.controllers in $CGROUP_PATH exit $FAIL_MISSING_CGROUP_CONTROLLERS_FILE fi if [ ! -f "$CGROUP_PATH/cgroup.subtree_control" ]; then echo $0: missing file cgroup.subtree_control in $CGROUP_PATH + SysFailure Missing cgroup.subtree_controll in $CGORUP_PATH exit $FAIL_MISSING_CGROUP_SUBTREE_CONTROL_FILE fi # make sure the cpu and memory controllers are enabled if ! grep -q -F "cpu" "$CGROUP_PATH/cgroup.subtree_control"; then echo $0: cgroup.subtree_control in $CGROUP_PATH does not enable cpu controller + SysFailure CPU controller not enabled in cgroup.subtree_control in $CGROUP_PATH exit $FAIL_CPU_CONTROLLER_NOT_ENABLED fi if ! grep -q -F "memory" "$CGROUP_PATH/cgroup.subtree_control"; then echo $0: cgroup.subtree_control in $CGROUP_PATH does not enable memory controller + SysFailure Memory controller not enabled in cgroup.subtree_control in $CGROUP_PATH exit $FAIL_MEMORY_CONTROLLER_NOT_ENABLED fi @@ -216,6 +331,7 @@ then if ! rmdir $PC2_SANDBOX_CGROUP_PATH then DEBUG echo Cannot remove previous sandbox: $PC2_SANDBOX_CGROUP_PATH + SysFailure Can not remove previous sandbox $PC2_SANDBOX_CGROUP_PATH exit $FAIL_SANDBOX_ERROR fi fi @@ -224,9 +340,18 @@ DEBUG echo Creating sandbox $PC2_SANDBOX_CGROUP_PATH if ! mkdir $PC2_SANDBOX_CGROUP_PATH then DEBUG echo Cannot create $PC2_SANDBOX_CGROUP_PATH + SysFailure Can not create $PC2_SANDBOX_CGROUP_PATH exit $FAIL_INVALID_CGROUP_INSTALLATION fi +# Set up report directory for per-case logging +mkdir -p "$REPORTDIR" + +# Set report file to be testcase specific one now +REPORTFILE=`printf "$REPORTDIR/testcase_%03d.log" $TESTCASE` +BRIEFREPORTFILE=`printf "$REPORTDIR/briefcase_%03d.log" $TESTCASE` +DEBUG echo Report file: ${REPORTFILE} Brief Report File: ${BRIEFREORTFILE} + # set the specified memory limit - input is in MB, cgroup v2 requires bytes, so multiply by 1M # but only if > 0. # "max" means unlimited, which is the cgroup v2 default @@ -241,25 +366,32 @@ else echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max fi +REPORT Test case $TESTCASE: # We use ulimit to limit CPU time, not cgroups. Time is supplied in seconds. This may have to # be reworked if ms accuracy is needed. The problem is, cgroups do not kill off a process that # exceeds the time limit, ulimit does. TIMELIMIT_US=$((TIMELIMIT * 1000000)) -DEBUG echo setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" +REPORT Setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" ulimit -t $TIMELIMIT MAXPROCS=$((MAXPROCS+`ps -T -u $USER | wc -l`)) -DEBUG echo setting maximum user processes to $MAXPROCS +REPORT Setting maximum user processes to $MAXPROCS ulimit -u $MAXPROCS +# Keep track of details for reports +REPORT_BRIEF $cpunum +REPORT_BRIEF $$ +REPORT_BRIEF $(date "+%F %T.%6N") + # Remember wall time when we started starttime=`GetTimeInMicros` #put the current process (and implicitly its children) into the pc2sandbox cgroup. -DEBUG echo putting $$ into $PC2_SANDBOX_CGROUP_PATH cgroup +REPORT Putting $$ into $PC2_SANDBOX_CGROUP_PATH cgroup if ! echo $$ > $PC2_SANDBOX_CGROUP_PATH/cgroup.procs then echo $0: Could not add current process to $PC2_SANDBOX_CGROUP_PATH/cgroup.procs - not executing submission. + SysFailure Could not add current process to $PC2_SANDBOX_CGROUP_PATH/cgroup.procs exit $FAIL_SANDBOX_ERROR fi @@ -270,7 +402,7 @@ fi # since we don't know how to use cgroup-tools to execute, just execute it directly (it's a child so it # should still fall under the cgroup limits). -DEBUG echo Executing "setsid taskset $CPUMASK $COMMAND $*" +REPORT_DEBUG Executing "setsid taskset $CPUMASK $COMMAND $*" # Set up trap handler to catch wall-clock time exceeded and getting killed by PC2's execute timer trap HandleTerminateFromPC2 15 @@ -311,23 +443,26 @@ ShowStats ${cputime} ${TIMELIMIT_US} ${walltime} ${peakmem} $((MEMLIMIT*1024*102 if test "$kills" != "0" then - DEBUG echo The command was killed because it exceeded the memory limit + REPORT_DEBUG The command was killed because it exceeded the memory limit + REPORT_BRIEF MLE COMMAND_EXIT_CODE=${FAIL_MEMORY_LIMIT_EXCEEDED} else # See why we terminated. 137 = 128 + 9 = SIGKILL, which is what ulimit -t sends. if test "$COMMAND_EXIT_CODE" -eq 137 -o "$cputime" -gt "$TIMELIMIT_US" then - DEBUG echo The command was killed because it exceeded the CPU Time limit + REPORT_DEBUG The command was killed because it exceeded the CPU Time limit + REPORT_BRIEF TLE COMMAND_EXIT_CODE=${FAIL_TIME_LIMIT_EXCEEDED} elif test "$COMMAND_EXIT_CODE" -ge 128 then - DEBUG echo The command terminated abnormally with exit code $COMMAND_EXIT_CODE + REPORT_DEBUG The command terminated abnormally with exit code $COMMAND_EXIT_CODE + REPORT_BRIEF RTE Exit:$COMMAND_EXIT_CODE else - DEBUG echo The command terminated normally. + REPORT_DEBUG The command terminated normally. fi fi -DEBUG echo Finished executing $COMMAND $* -DEBUG echo $COMMAND exited with exit code $COMMAND_EXIT_CODE +REPORT_DEBUG Finished executing $COMMAND $* +REPORT_DEBUG $COMMAND exited with exit code $COMMAND_EXIT_CODE DEBUG echo diff --git a/scripts/pc2sandbox_interactive.sh b/scripts/pc2sandbox_interactive.sh index 6213b4e1b..d37f66a07 100755 --- a/scripts/pc2sandbox_interactive.sh +++ b/scripts/pc2sandbox_interactive.sh @@ -15,6 +15,10 @@ # # Author: John Buck, based on earlier versions by John Clevenger and Doug Lane +# CPU to run submission on. This is 0 based, so 3 means the 4th CPU +DEFAULT_CPU_NUM=3 +CPU_OVERRIDE_FILE=$HOME/pc2_cpu_override + # FAIL_RETCODE_BASE is 128 + 64 + xx # 128 = system error, like signal # 64 = biggest used signal @@ -37,14 +41,42 @@ FAIL_INTERACTIVE_ERROR=$((FAIL_RETCODE_BASE+55)) # This gets added to the current number of executing processes for this user. MAXPROCS=32 -# taskset cpu mask for running submission on single processor -cpunum=${USER/judge/} -if [[ "$cpunum" =~ ^[1-5]$ ]] +# Compute taskset cpu mask for running submission on single processor + +# Get system's maximum CPU number +MAX_CPU_NUM=`lscpu -p=cpu | tail -1` + +# See if the admin wants to override the CPU by reading the override file +if test -s ${CPU_OVERRIDE_FILE} then - CPUMASK=$((1<<(cpunum-1))) -else - CPUMASK=0x08 + # This will get the first line that consists of numbers only + cpunum=`egrep '^[0-9]+$' ${CPU_OVERRIDE_FILE} | head -1` + if test -z ${cpunum} + then + cpunum="" + elif test ${cpunum} -gt ${MAX_CPU_NUM} + then + cpunum="" + fi +fi + +# If there was no override or the override was bad, let's try to figure out the cpunum +if test -z ${cpunum} +then + # The login id must be "judge#" where # is the desired CPU and judge number. + # If the login is not "judge#", then the system default is used. + # This special case is for when you want to run multiple judges on one computer + # that has lots of CPU's, but want to pin each judge to its own cpu. + cpunum=${USER/judge/} + if [[ "$cpunum" =~ ^[1-9][0-9]*$ ]] + then + # Restrict to number of CPU's. + cpunum=$(((cpunum-1)%(MAX_CPU_NUM+1))) + else + cpunum=$(((DEFAULT_CPU_NUM+1))) + fi fi +CPUMASK=$((1< $INT_RESULTFILE -$msg2 + EOF sent_xml=1 } @@ -240,6 +273,8 @@ JUDGEIN="$4" JUDGEANS="$5" TESTCASE="$6" COMMAND="$7" +DEBUG echo "+---------------- Test Case ${TESTCASE} ----------------+" +DEBUG echo Command line: $0 $* shift 7 # the rest of the commmand line arguments are the command args for the submission @@ -431,7 +466,7 @@ do # If validator created a feedback file, put the last line in the judgement if test -s "$feedbackfile" then - GenXML "No - Wrong answer" `tail -1 $feedbackfile` + GenXML "No - Wrong answer" `head -n 1 $feedbackfile` else GenXML "No - Wrong answer" "No feedback file" fi diff --git a/src/edu/csus/ecs/pc2/core/Constants.java b/src/edu/csus/ecs/pc2/core/Constants.java index 5a71b2131..408518592 100644 --- a/src/edu/csus/ecs/pc2/core/Constants.java +++ b/src/edu/csus/ecs/pc2/core/Constants.java @@ -212,7 +212,7 @@ private Constants() { /** * Command line to run submission in a sandbox */ - public static final String PC2_INTERNAL_SANDBOX_COMMAND_LINE = "./{:sandboxprogramname} {:memlimit} {:timelimit}"; + public static final String PC2_INTERNAL_SANDBOX_COMMAND_LINE = "./{:sandboxprogramname} {:memlimit} {:timelimit} {:infilename} {:ansfilename} {:testcase}"; /** * File name of script to run submission in sandbox ({:sandboxprogramname} if non-interactive) diff --git a/src/edu/csus/ecs/pc2/core/execute/Executable.java b/src/edu/csus/ecs/pc2/core/execute/Executable.java index 9d3ef1827..ad93794c9 100644 --- a/src/edu/csus/ecs/pc2/core/execute/Executable.java +++ b/src/edu/csus/ecs/pc2/core/execute/Executable.java @@ -81,6 +81,8 @@ public class Executable extends Plugin implements IExecutable, IExecutableNotify private static final String NL = System.getProperty("line.separator"); + private static final String DEFAULT_EXECUTE_DIRECTORY_TEMPLATE = "executesite{:clientsite}{:clientname}"; + private Run run = null; private Language language = null; @@ -174,7 +176,7 @@ public class Executable extends Plugin implements IExecutable, IExecutableNotify /** * Sandbox constants */ - public static final long SANDBOX_EXTRA_KILLTIME_MS = 1000; + public static final long SANDBOX_EXTRA_KILLTIME_MS = 5000; /** * Return codes from sandbox @@ -323,6 +325,9 @@ public Executable(IInternalContest inContest, IInternalController inController, */ private void initialize() { + // set log early in case of exceptions + log = controller.getLog(); + this.executorId = contest.getClientId(); if (runFiles != null) { @@ -330,8 +335,6 @@ private void initialize() { } executeDirectoryName = getExecuteDirectoryName(); - log = controller.getLog(); - if (executorId.getClientType() != ClientType.Type.TEAM) { this.problemDataFiles = contest.getProblemDataFile(problem); } @@ -528,6 +531,8 @@ public IFileViewer execute(boolean clearDirFirst) { atLeastOneTestFailed = true; failedResults = executionData.getValidationResults(); } + // Create summary of execution results in execute folder for human judge + saveExecuteData(dataSetNumber); } else { @@ -541,6 +546,9 @@ public IFileViewer execute(boolean clearDirFirst) { // execute against one specific data set passed = executeAndValidateDataSet(dataSetNumber); + // Create summary of execution results in execute folder for human judge + saveExecuteData(dataSetNumber); + dataSetNumber++; if (!passed) { log.info("FAILED test case " + dataSetNumber + " for run " + run.getNumber() + " reason " + getFailureReason()); @@ -609,6 +617,9 @@ public IFileViewer execute(boolean clearDirFirst) { setException(errorMessage); fileViewer.addTextPane("Error during compile", errorMessage); } // else they will get a tab hopefully showing something wrong + + // Create summary of compilation failure for human judge + saveExecuteData(0); } // we've finished the compile/execute/validate steps (for better or worse); do the required final steps to display the results @@ -2988,16 +2999,24 @@ public String substituteAllStrings(Run inRun, String origString) { *
      *             valid fields are:
      *              {:mainfile} - submitted file (hello.java)
+     *              {:package} - if a package was specified
      *              {:basename} - mainfile without extension (hello)
      *              {:validator} - validator program name
-     *              {:language}
-     *              {:problem}
-     *              {:teamid}
-     *              {:siteid}
+     *              {:language} - index into languages (1 based)
+     *              {:languageletter} - index converted to letter, eg 1=A, 2=B
+     *              {:languagename} - Display name of language (spaces converted to _)
+     *              {:languageid} - CLICS language id, eg cpp
+     *              {:problem} - Index into problem table
+     *              {:problemletter} - A,B,C...
+     *              {:problemshort} - problem short name
+     *              {:teamid} - team's id number
+     *              {:siteid} - team's site
+     *              {:clientname} - this client's name, eg judge1
+     *              {:clientid} - this client's id number, eg. 1
+     *              {:clientsite} - this client's site
      *              {:infile}
      *              {:outfile}
      *              {:ansfile}
-     *              {:pc2home}
      *              {:sandboxprogramname} - the sandbox program name as defined in the Problem
      *              {:sandboxcommandline} - the command line used to invoke the sandbox as defined in the Problem
      *              {:ensuresuffix=...} - add supplied suffix if not present already
@@ -3007,6 +3026,10 @@ public String substituteAllStrings(Run inRun, String origString) {
      *              {:ansfilename} - full path to judges answer file
      *              {:timelimit} - CPU time limit in seconds
      *              {:memlimit} - memory limit in MB
+     *              {:exitvalue} - results exit code
+     *              {:executetime} - result execution time in MS
+     *              {:pc2home} - where pc2 is installed
+     *              {:runnumber} - the run number
      * 
* * @param dataSetNumber @@ -3059,9 +3082,11 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb Language[] langs = contest.getLanguages(); int index = 0; String displayName = ""; + String languageid = ""; for (int i = 0; i < langs.length; i++) { if (langs[i] != null && langs[i].getElementId().equals(inRun.getLanguageId())) { displayName = langs[i].getDisplayName().toLowerCase().replaceAll(" ", "_"); + languageid = langs[i].getID(); index = i + 1; break; } @@ -3070,6 +3095,7 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb newString = replaceString(newString, "{:language}", index); newString = replaceString(newString, "{:languageletter}", Utilities.convertNumber(index)); newString = replaceString(newString, "{:languagename}", displayName); + newString = replaceString(newString, "{:languageid}", languageid); } } if (inRun.getProblemId() != null) { @@ -3083,8 +3109,8 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb } if (index > 0) { newString = replaceString(newString, "{:problem}", index); - newString = replaceString(newString, "{:problemletter}", Utilities.convertNumber(index)); if(problem != null) { + newString = replaceString(newString, "{:problemletter}", problem.getLetter()); newString = replaceString(newString, "{:problemshort}", problem.getShortName()); } } @@ -3093,7 +3119,9 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb newString = replaceString(newString, "{:teamid}", inRun.getSubmitter().getClientNumber()); newString = replaceString(newString, "{:siteid}", inRun.getSubmitter().getSiteNumber()); } - + newString = replaceString(newString, "{:clientname}", contest.getClientId().getName()); + newString = replaceString(newString, "{:clientid}", contest.getClientId().getClientNumber()); + newString = replaceString(newString, "{:clientsite}", contest.getClientId().getSiteNumber()); if (problem != null) { if (problem.getDataFileName() != null && !problem.getDataFileName().equals("")) { newString = replaceString(newString, "{:infile}", problem.getDataFileName()); @@ -3135,6 +3163,8 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb } newString = replaceString(newString, "{:timelimit}", Long.toString(problem.getTimeOutInSeconds())); + newString = replaceString(newString, "{:runnumber}", Integer.toString(inRun.getNumber())); + // TODO REFACTOR replace vars with constants for: memlimit, sandboxcommandline,sandboxprogramname newString = replaceString(newString, "{:memlimit}", Integer.toString(problem.getMemoryLimitMB())); @@ -3174,6 +3204,7 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb newString = replaceString(newString, "{:pc2home}", pc2home); } + // Check for conditional suffix (that is, the previous chars match), if not, add them newString = ExecuteUtilities.replaceStringConditional(newString, Constants.CMDSUB_COND_SUFFIX); @@ -3377,14 +3408,19 @@ public void setProblem(Problem problem) { /** * Execute directory name for this client instance. * - * The name is individual for each client. + * The name is individual for each client and run. * * @see #getExecuteDirectoryNameSuffix() * * @return the name of the execute directory for this client. */ public String getExecuteDirectoryName() { - return "executesite" + contest.getClientId().getSiteNumber() + contest.getClientId().getName() + getExecuteDirectoryNameSuffix(); + String dirName = getContestInformation().getJudgesExecuteFolder(); + if(StringUtilities.isEmpty(dirName)) { + dirName = DEFAULT_EXECUTE_DIRECTORY_TEMPLATE; + } + dirName = substituteAllStrings(run, dirName) + getExecuteDirectoryNameSuffix(); + return(dirName); } /** @@ -3700,4 +3736,53 @@ private void createValidatorProgram() } } + + /** + * Shows a 'null' string as "" instead of "null" + * + * @param str String to check + * @return "" if null or the original string if not. + */ + private String showNullAsEmpty(String str) + { + if(str == null) { + return(""); + } + return(str); + } + + /** + * Write execute summary data to the specified testcase executedata. file in the execute folder. + * + * @param dataSetNumber The dataset number, used to create a file name + * @return true if it worked, false otherwise. + */ + private boolean saveExecuteData(int dataSetNumber) + { + // create execution data results summary file - useful for human judges reviewing the problem + String executionDataFilename = prefixExecuteDirname("executedata." + dataSetNumber + ".txt"); + boolean bWritten = false; + try (PrintWriter executeDataWriter = new PrintWriter(executionDataFilename)){ + // The file produced may be "sourced" in bash if desired, eg. ". ./executedata.1.txt" + executeDataWriter.println("executeDateTime='" + Utilities.getIso8601formatterWithMS().format(new Date()) + "'"); + executeDataWriter.println("compileExeFileName='" + showNullAsEmpty(executionData.getCompileExeFileName()) + "'"); + executeDataWriter.println("compileSuccess='" + executionData.isCompileSuccess() + "'"); + executeDataWriter.println("compileResultCode='" + executionData.getCompileResultCode() + "'"); + executeDataWriter.println("executeExitValue='" + executionData.getExecuteExitValue() + "'"); + executeDataWriter.println("executeSuccess='" + executionData.isExecuteSucess() + "'"); + executeDataWriter.println("validationReturnCode ='" + executionData.getValidationReturnCode() + "'"); + executeDataWriter.println("validationSuccess='" + executionData.isValidationSuccess() + "'"); + executeDataWriter.println("validationResults='" + showNullAsEmpty(executionData.getValidationResults()) + "'"); + executeDataWriter.println("compileTimeMS='" + executionData.getCompileTimeMS() + "'"); + executeDataWriter.println("executeTimeMS='" + executionData.getExecuteTimeMS() + "'"); + executeDataWriter.println("validateTimeMS='" + executionData.getvalidateTimeMS() + "'"); + executeDataWriter.println("executionException='" + showNullAsEmpty(executionData.getExecutionException().getMessage()) + "'"); + executeDataWriter.println("runTimeLimitExceeded='" + executionData.isRunTimeLimitExceeded() + "'"); + executeDataWriter.println("additionalInformation='" + showNullAsEmpty(executionData.getAdditionalInformation()) + "'"); + bWritten = true; + } catch(Exception e) { + log.log(Log.WARNING, "Can not save execution data file " + executionDataFilename); + } + return bWritten; + } } diff --git a/src/edu/csus/ecs/pc2/core/execute/ExecutionData.java b/src/edu/csus/ecs/pc2/core/execute/ExecutionData.java index ec802fcd5..782853431 100644 --- a/src/edu/csus/ecs/pc2/core/execute/ExecutionData.java +++ b/src/edu/csus/ecs/pc2/core/execute/ExecutionData.java @@ -1,4 +1,4 @@ -// Copyright (C) 1989-2019 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. +// Copyright (C) 1989-2024 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. package edu.csus.ecs.pc2.core.execute; import java.io.Serializable; @@ -7,9 +7,9 @@ /** * Execution Data. - * + * * Contains data for compilation, execution and validation. - * + * * @author pc2@ecs.csus.edu * @version $Id$ */ @@ -19,7 +19,7 @@ public class ExecutionData implements Serializable { /** - * + * */ private static final long serialVersionUID = 3095803815304273632L; @@ -40,9 +40,9 @@ public class ExecutionData implements Serializable { private SerializedFile executeProgramOutput; // this is STDOUT of the execution private int executeExitValue = 0; - + private boolean executeSucess; - + private SerializedFile validationStdout; private SerializedFile validationStderr; // this is STDOUT of the validation @@ -52,23 +52,23 @@ public class ExecutionData implements Serializable { private boolean validationSuccess; private String validationResults; - + private long compileTimeMS = 0; - + private long executeTimeMS = 0; - + private long validateTimeMS = 0; - + private Exception executionException = null; - + private boolean runTimeLimitExceeded = false; - + private boolean memoryLimitExceeded = false; private boolean failedToCompile = false; private String additionalInformation = ""; - + /** * @return Returns the validationReturnCode. */ @@ -235,10 +235,10 @@ public SerializedFile getValidationStdout() { /** * Did the validator program run without an error ?. - * + * * This does NOT indicate that the team's run was a Yes/accepted. This * only returns whether the validator program ran without any OS or other sort of error. - * + * * @return Returns whether the validator program executed successfully. */ public boolean isValidationSuccess() { @@ -282,17 +282,17 @@ public void setExecutionException(Exception executionException) { } public void setCompileTimeMS(long compileTime) { - compileTimeMS = compileTime; + compileTimeMS = compileTime; } public long getCompileTimeMS() { return compileTimeMS; } - + public void setExecuteTimeMS(long inExecuteTime){ executeTimeMS = inExecuteTime; } - + public long getExecuteTimeMS(){ return executeTimeMS; } @@ -300,15 +300,15 @@ public long getExecuteTimeMS(){ public void setvalidateTimeMS(long validateTime){ validateTimeMS = validateTime; } - + public long getvalidateTimeMS(){ return validateTimeMS; } - + public boolean isExecuteSucess() { return executeSucess; } - + public void setExecuteSucess(boolean executeSucess) { this.executeSucess = executeSucess; } @@ -318,15 +318,17 @@ public void setExecuteSucess(boolean executeSucess) { * @return * @deprecated use {@link #isCompileSuccess()}. */ + @Deprecated public boolean isFailedToCompile() { return failedToCompile; } /** - * + * * @param failedToCompile * @deprecated use {@link #setCompileSuccess(boolean)} */ + @Deprecated public void setFailedToCompile(boolean failedToCompile) { this.failedToCompile = failedToCompile; } @@ -354,7 +356,7 @@ public boolean isMemoryLimitExceeded() { public void setMemoryLimitExceeded(boolean memoryLimitExceeded) { this.memoryLimitExceeded = memoryLimitExceeded; } - + public void setAdditionalInformation(String additionalInformation) { this.additionalInformation = additionalInformation; } @@ -366,7 +368,7 @@ public void setAdditionalInformation(String additionalInformation) { public String getAdditionalInformation() { return additionalInformation; } - + @Override public String toString() { String retStr = "["; @@ -381,7 +383,7 @@ public String toString() { retStr += "executeSucess=" + executeSucess + ","; retStr += "validationStdout=" + validationStdout + ","; retStr += "validationStderr=" + validationStderr + ","; - retStr += "validationReturnCode" + validationReturnCode + ","; + retStr += "validationReturnCode=" + validationReturnCode + ","; retStr += "validationSuccess=" + validationSuccess + ","; retStr += "validationResults=" + validationResults + ","; retStr += "compileTimeMS=" + compileTimeMS + ","; @@ -392,7 +394,7 @@ public String toString() { retStr += "failedToCompile=" + failedToCompile + ","; retStr += "additionalInformation=" + additionalInformation; retStr += "]"; - + return retStr; } } diff --git a/src/edu/csus/ecs/pc2/core/model/ContestInformation.java b/src/edu/csus/ecs/pc2/core/model/ContestInformation.java index 0338325e3..383d74fd2 100644 --- a/src/edu/csus/ecs/pc2/core/model/ContestInformation.java +++ b/src/edu/csus/ecs/pc2/core/model/ContestInformation.java @@ -1,4 +1,4 @@ -// Copyright (C) 1989-2023 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. +// Copyright (C) 1989-2024 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. package edu.csus.ecs.pc2.core.model; import java.io.Serializable; @@ -15,14 +15,14 @@ /** * Contest-wide Information/settings. - * + * * @author pc2@ecs.csus.edu */ public class ContestInformation implements Serializable{ /** - * + * */ private static final long serialVersionUID = -7333255582657988200L; @@ -34,33 +34,33 @@ public class ContestInformation implements Serializable{ private String contestTitle = "Programming Contest"; private String contestURL; - + private TeamDisplayMask teamDisplayMode = TeamDisplayMask.LOGIN_NAME_ONLY; private String judgesDefaultAnswer = "No response, read problem statement"; - + private boolean preliminaryJudgementsUsedByBoard = false; private boolean preliminaryJudgementsTriggerNotifications = false; - + private boolean sendAdditionalRunStatusInformation = false; - + private String judgeCDPBasePath = null; - + private String adminCDPBasePath = null; - + /** * Test mode allow run submission with override elapsed time. - * + * */ private boolean ccsTestMode = false; - + //Shadow Mode settings private boolean shadowMode = false; private String primaryCCS_URL = null; private String primaryCCS_user_login = ""; private String primaryCCS_user_pw = ""; private String lastShadowEventID = ""; - + /** * Global setting for maximum output allowed by a submission. * Note that this value can be overridden on a per-problem basis. @@ -69,35 +69,40 @@ public class ContestInformation implements Serializable{ /** * This is a list of the judgement notification end of contest control settings. - * + * */ private JudgementNotificationsList judgementNotificationsList = new JudgementNotificationsList(); - + private String externalYamlPath = null; - + private String rsiCommand = null; - + private int lastRunNumberSubmitted = 0; - + /** * Memory Limit for team's solution (application). */ private int memoryLimitInMeg = 0; - + /** * Sandbox application or application command line. */ private String sandboxCommandLine = ""; - + /** * Display string for team display on standings. - * + * * @see ScoreboardVariableReplacer#substituteDisplayNameVariables(String, Account, Group) */ private String teamScoreboardDisplayFormat = ScoreboardVariableReplacer.TEAM_NAME; /** - * + * String to append to the execute folder for judged runs. May contain substitution strings. + */ + private String judgesExecuteFolder = ""; + + /** + * * @author pc2@ecs.csus.edu */ public enum TeamDisplayMask { @@ -114,7 +119,7 @@ public enum TeamDisplayMask { */ DISPLAY_NAME_ONLY, /** - * name and number, teamN Johns Hopkins Team 1. + * name and number, teamN Johns Hopkins Team 1. */ NUMBERS_AND_NAME, /** @@ -124,15 +129,15 @@ public enum TeamDisplayMask { */ ALIAS, } - + private Properties scoringProperties = new Properties(); /** * Enable team auto registration. - * + * */ private boolean enableAutoRegistration = false; - + /** * The password type for the new passwords. */ @@ -143,20 +148,20 @@ public enum TeamDisplayMask { // * Contest Start/Date Time. // */ // private Date startDate; - + /** * The date/time when the contest is scheduled (intended) to start. * This value is null (undefined) if no scheduled start time has been set. - * This value ONLY applies BEFORE THE CONTEST STARTS; once + * This value ONLY applies BEFORE THE CONTEST STARTS; once * any "start contest" operation (e.g. pushing the "Start Button") has occurred, * this value no longer has meaning. */ private GregorianCalendar scheduledStartTime = null ; - + private boolean autoStartContest = false; private boolean autoStopContest = false ; - + /** * Scoreboard freeze time. */ @@ -167,44 +172,44 @@ public enum TeamDisplayMask { /** * Whether the contest is thawed(unfrozen). Meaning the final scoreboard * can be revealed (despite the scoreboad freeze). - * + * * Typically a contest is not thawed until the awards ceremony. */ private Date thawed = null; private boolean allowMultipleLoginsPerTeam; - - + + /** - * Load CDP samples/data in front of judges secret data. - * + * Load CDP samples/data in front of judges secret data. + * *
-     * # in yaml, do not load sample judges data 
+     * # in yaml, do not load sample judges data
      * loadSampleJudgesData : false
      * 
- * + * */ private boolean loadSampleJudgesData = true; - - + + /** * stop-on-first-failed-test-case - * + * */ private boolean stopOnFirstFailedtestCase = false; private String overrideLoadAccountsFilename = null; - + /** * Returns the date/time when the contest is scheduled (intended) to start. * This value is null if no scheduled start time has been set, - * or if the contest has already started. + * or if the contest has already started. * @see ContestTime#getContestStartTime() */ public GregorianCalendar getScheduledStartTime() { return scheduledStartTime; } - + /** * Receives a {@link GregorianCalendar} object specifying a (future) instant in time; * sets the specified date/time as the scheduled (intended) start time for the @@ -212,10 +217,10 @@ public GregorianCalendar getScheduledStartTime() { * after the contest has started, since the value of "scheduled start time" is meaningless after * the contest is under way. It is the responsibility of clients to insure * this method is only invoked with a non-null value before the contest has been started. - * + * */ public void setScheduledStartTime(GregorianCalendar newScheduledStartTime) { - scheduledStartTime = newScheduledStartTime; + scheduledStartTime = newScheduledStartTime; } public String getContestTitle() { @@ -248,7 +253,7 @@ public String getJudgesDefaultAnswer() { /** * judgesDefaultAnswer must be a non-zero length trimmed string. - * + * * @param judgesDefaultAnswer The judgesDefaultAnswer to set. */ public void setJudgesDefaultAnswer(String judgesDefaultAnswer) { @@ -256,7 +261,25 @@ public void setJudgesDefaultAnswer(String judgesDefaultAnswer) { this.judgesDefaultAnswer = judgesDefaultAnswer.trim(); } } - + + public String getJudgesExecuteFolder() { + if(judgesExecuteFolder == null) { + judgesExecuteFolder = ""; + } + return judgesExecuteFolder ; + } + + /** + * judgesDefaultAnswer must be a non-zero length trimmed string. + * + * @param judgesDefaultAnswer The judgesDefaultAnswer to set. + */ + public void setJudgesExecuteFolder(String judgesExecuteFolder) { + if (judgesExecuteFolder != null && judgesExecuteFolder.trim().length() > 0) { + this.judgesExecuteFolder = judgesExecuteFolder.trim(); + } + } + public boolean isSameAs(ContestInformation contestInformation) { try { if (contestTitle == null) { @@ -271,6 +294,9 @@ public boolean isSameAs(ContestInformation contestInformation) { if (!judgesDefaultAnswer.equals(contestInformation.getJudgesDefaultAnswer())) { return false; } + if (!judgesExecuteFolder.equals(contestInformation.getJudgesExecuteFolder())) { + return false; + } if (!teamDisplayMode.equals(contestInformation.getTeamDisplayMode())) { return false; } @@ -309,10 +335,10 @@ public boolean isSameAs(ContestInformation contestInformation) { } if (enableAutoRegistration != contestInformation.isEnableAutoRegistration()) { return false; - } + } if (! StringUtilities.stringSame(primaryCCS_URL, contestInformation.primaryCCS_URL)) { return false; - } + } if (! StringUtilities.stringSame(primaryCCS_user_login, contestInformation.primaryCCS_user_login)) { return false; } @@ -331,9 +357,9 @@ public boolean isSameAs(ContestInformation contestInformation) { //DateUtilities.dateSame() expects Date objects but ContestInformation now maintains // scheduledStartTime (formerly "StartDate") as a GregorianCalendar; need to convert. //Also need to first check for null references (to avoid NPEs on fetch of Date from GregorianCalendar) - - //If the references to scheduledStartTime in the two ContestInfos are such that one is null and the other - // is not, the ContestInfos are not the same so return false. + + //If the references to scheduledStartTime in the two ContestInfos are such that one is null and the other + // is not, the ContestInfos are not the same so return false. // Note that "one is null and the other is not null" can be computed with the ^ (XOR) operator: // "A XOR B" = true iff A != B if (scheduledStartTime==null ^ contestInformation.getScheduledStartTime()==null) { @@ -343,12 +369,12 @@ public boolean isSameAs(ContestInformation contestInformation) { //If both are null, this test for equality passes and we fall through to other cases //If both non-null, get Dates from both and compare them if (scheduledStartTime!=null /*and therefore contestInformation.getScheduledStartTime() also != null*/) { - if (!DateUtilities.dateSame(scheduledStartTime.getTime(), + if (!DateUtilities.dateSame(scheduledStartTime.getTime(), contestInformation.getScheduledStartTime().getTime())) { return false; } - } - + } + //both scheduledStartTime and contestInformation.getScheduledStartTime() must be null (hence, "same") //continue; @@ -366,21 +392,21 @@ public boolean isSameAs(ContestInformation contestInformation) { if (!StringUtilities.stringSame(judgeCDPBasePath, contestInformation.getJudgeCDPBasePath())) { return false; } - + if (!StringUtilities.stringSame(adminCDPBasePath, contestInformation.getAdminCDPBasePath())) { return false; } if (autoStopContest != contestInformation.isAutoStopContest()) { return false; } - + if (allowMultipleLoginsPerTeam != contestInformation.isAllowMultipleLoginsPerTeam()) { return false; } - + return true; } catch (Exception e) { - e.printStackTrace(System.err); // TODO log this exception + e.printStackTrace(System.err); // TODO log this exception return false; } } @@ -395,7 +421,7 @@ public void setPreliminaryJudgementsUsedByBoard(boolean preliminaryJudgementsUse /** * The Scoring Algorithm should use this to determine whether to count preliminary judgements * as scoreable. - * + * * @return the preliminaryJudgementsUsedByBoard */ public boolean isPreliminaryJudgementsUsedByBoard() { @@ -423,7 +449,7 @@ public boolean isSendAdditionalRunStatusInformation() { public void setSendAdditionalRunStatusInformation(boolean sendAdditionalRunStatusInformation) { this.sendAdditionalRunStatusInformation = sendAdditionalRunStatusInformation; } - + public JudgementNotificationsList getJudgementNotificationsList() { return judgementNotificationsList; } @@ -431,7 +457,7 @@ public JudgementNotificationsList getJudgementNotificationsList() { public void setJudgementNotificationsList(JudgementNotificationsList judgementNotificationsList) { this.judgementNotificationsList = judgementNotificationsList; } - + public void updateJudgementNotification (NotificationSetting notificationSetting ){ judgementNotificationsList.update(notificationSetting); } @@ -442,7 +468,7 @@ public void updateJudgementNotification (NotificationSetting notificationSetting * via a call to {@link Problem#setMaxOutputFileSizeKB(int)}; note also that * the parameter for {@link Problem#setMaxOutputFileSizeKB(int)} is in KB, whereas * the value returned by THIS method is in BYTES. - * + * * @return maximum output file size in bytes. */ public long getMaxOutputSizeInBytes() { @@ -455,21 +481,21 @@ public long getMaxOutputSizeInBytes() { * Problem does NOT have a specified problem-specific output size limit. * Note also that problem-specific output size limits are specified in KB, * whereas the parameter for this method gives the (global) output size limit in BYTES. - * + * * @param maxOutputSizeInBytes - * - * @see Problem#setMaxOutputSizeKB(long) + * + * @see Problem#setMaxOutputSizeKB(long) * @see Problem#getMaxOutputSizeKB() */ public void setMaxOutputSizeInBytes(long maxOutputSizeInBytes) { this.maxOutputSizeInBytes = maxOutputSizeInBytes; } - + /** * Scoring Properties. - * + * * Minute penalties, etc. - * + * * @return */ public Properties getScoringProperties() { @@ -483,11 +509,11 @@ public void setScoringProperties(Properties scoringProperties) { public boolean isCcsTestMode() { return ccsTestMode; } - + public void setCcsTestMode(boolean ccsTestMode) { this.ccsTestMode = ccsTestMode; } - + public boolean isShadowMode() { return shadowMode; } @@ -499,9 +525,9 @@ public void setShadowMode(boolean shadowMode) { public void setRsiCommand(String rsiCommand) { this.rsiCommand = rsiCommand; } - + /** - * Returns the String representation for a "Primary CCS" URL (that is, the URL of a Remote CCS being shadowed); + * Returns the String representation for a "Primary CCS" URL (that is, the URL of a Remote CCS being shadowed); * only relevant when operating this instance of the PC2 CCS as a "Shadow CCS". * @return a String containing the URL of the Primary CCS which we're shadowing */ @@ -510,7 +536,7 @@ public String getPrimaryCCS_URL() { } /** - * Sets the String representation for a "Primary CCS" URL (that is, the URL of a Remote CCS being shadowed); + * Sets the String representation for a "Primary CCS" URL (that is, the URL of a Remote CCS being shadowed); * only relevant when operating this instance of the PC2 CCS as a "Shadow CCS". * @param primaryCCS_URL a String giving the URL of the Primary (remote) CCS (the CCS being shadowed) */ @@ -519,7 +545,7 @@ public void setPrimaryCCS_URL(String primaryCCS_URL) { } /** - * Returns a String containing the user login account to be used when connecting + * Returns a String containing the user login account to be used when connecting * to a Primary CCS (only useful when operating this instance of PC2 as a "Shadow CCS"). * @return a String containing the Primary CCS user account name */ @@ -537,7 +563,7 @@ public void setPrimaryCCS_user_login(String primaryCCS_user_login) { } /** - * Returns a String containing the password used for logging in to the + * Returns a String containing the password used for logging in to the * Primary CCS (only useful when operating this instance of PC2 as a * "Shadow CCS"). * @return a String containing a password @@ -580,39 +606,39 @@ public void setLastShadowEventID(String lastShadowEventID) { /** * Get the Run Submission Interface (RSI) command. - * + * * @return the command string to run when each run is submitted. */ public String getRsiCommand() { return rsiCommand; } - + public void setExternalYamlPath(String externalYamlPath) { this.externalYamlPath = externalYamlPath; } /** * Get the external YAML path. - * + * * @return the location of contest.yaml and problem data files. */ public String getExternalYamlPath() { return externalYamlPath; } - + public void setLastRunNumberSubmitted(int lastRunNumberSubmitted) { this.lastRunNumberSubmitted = lastRunNumberSubmitted; } - + /** * Get the last run that was sent to the RSI. - * + * * @see #getRsiCommand() * @return */ public int getLastRunNumberSubmitted() { return lastRunNumberSubmitted; } - + public boolean isEnableAutoRegistration() { return enableAutoRegistration; } @@ -632,12 +658,12 @@ public void setAutoRegistrationPasswordType(PasswordType autoRegistrationPasswor /** * Sets the contest scheduled start time from the specified Date. * Note: previously, ContestInformation stored "startDate" as an object of - * class {@link Date}. It nows stores the scheduled start time as a + * class {@link Date}. It nows stores the scheduled start time as a * {@link GregorianCalendar}; however, this method is maintained for compatibility. * The method converts the given {@link Date} into an equivalent {@link GregorianCalendar} * and invokes {@link #scheduledStartTime} with the resulting {@link GregorianCalendar} object. - * - * @param startDate - the date at which the contest is scheduled to start; + * + * @param startDate - the date at which the contest is scheduled to start; * specifying "null" as the start date causes the scheduled start time to become undefined */ public void setScheduledStartDate(Date startDate) { @@ -649,7 +675,7 @@ public void setScheduledStartDate(Date startDate) { setScheduledStartTime(newStartDate); } } - + /** * Returns a {@link Date} object representing the scheduled start time for the contest, * or null if no scheduled start time has been set. @@ -661,19 +687,19 @@ public Date getScheduledStartDate() { if (scheduledStartTime == null) { return null; } else { - return scheduledStartTime.getTime(); + return scheduledStartTime.getTime(); } } - + public boolean isAutoStartContest() { return autoStartContest; } - + public void setAutoStartContest(boolean autoStartContest) { this.autoStartContest = autoStartContest; } - + public void setAutoStopContest(boolean autoStopContest) { this.autoStopContest = autoStopContest; } @@ -681,11 +707,11 @@ public void setAutoStopContest(boolean autoStopContest) { public boolean isAutoStopContest() { return autoStopContest; } - + public String getFreezeTime() { return freezeTime; } - + public void setFreezeTime(String freezeTime) { this.freezeTime = freezeTime; } @@ -693,16 +719,16 @@ public void setFreezeTime(String freezeTime) { public String getContestShortName() { return contestShortName; } - + public void setContestShortName(String contestShortName) { this.contestShortName = contestShortName; } - + /** * Set base path for CDP/external files on judge. - * + * * This is the base path location where the problem data files are located. - * + * * @param judgeCDPBasePath */ public void setJudgeCDPBasePath(String judgeCDPBasePath) { @@ -716,11 +742,11 @@ public void setJudgeCDPBasePath(String judgeCDPBasePath) { public String getJudgeCDPBasePath() { return judgeCDPBasePath; } - + public void setAdminCDPBasePath(String adminCDPBasePath) { this.adminCDPBasePath = adminCDPBasePath; } - + /** * Get base path for CDP on Admin. */ @@ -737,20 +763,20 @@ public boolean isUnfrozen() { /** * Returns the date the was thawed, or null if not thawed. - * + * * @return Date the contest was thawed, else null */ public Date getThawed() { return thawed; } - + /** * @param thawed when/if the contest is thawed */ public void setThawed(Date date) { thawed = date; } - + /** * @param thawed whether the contest is thawed */ @@ -764,38 +790,38 @@ public void setThawed(boolean thawed) { /** * Sets the boolean flag indicating whether or not teams are allowed to have multiple simultaneous logins. - * Note that this is a GLOBAL setting, configured on the Admin's "Configure Contest>Settings" screen (or via YAML); + * Note that this is a GLOBAL setting, configured on the Admin's "Configure Contest>Settings" screen (or via YAML); * either ALL teams are allowed to have multiple simultaneous logins, or NO team is allowed to have multiple simultaneous logins. - * + * * @param allowMultipleLoginsPerTeam whether or not a team is allowed to have multiple simultaneous login sessions. */ public void setAllowMultipleLoginsPerTeam(boolean allowMultipleLoginsPerTeam) { this.allowMultipleLoginsPerTeam = allowMultipleLoginsPerTeam; } - + /** * Returns a boolean indicating whether or not the Contest Settings allow teams to have multiple simultaneous logins. * Note that this is a GLOBAL setting, configured on the Admin's "Configure Contest>Settings" screen; either ALL teams * are allowed to have multiple simultaneous logins, or NO team is allowed to have multiple simultaneous logins. - * + * * @return a boolean indicating the current "allow multiple simultaneous logins" setting for teams. */ public boolean isAllowMultipleLoginsPerTeam() { - return this.allowMultipleLoginsPerTeam; + return this.allowMultipleLoginsPerTeam; } public boolean isStopOnFirstFailedtestCase() { return stopOnFirstFailedtestCase; } - + public void setStopOnFirstFailedtestCase(boolean stopOnFirstFailedtestCase) { this.stopOnFirstFailedtestCase = stopOnFirstFailedtestCase; } - - + + /** * Returns a string which defines which fields will be dispayed on the scoreboard. - * + * * @see ScoreboardVariableReplacer#substituteDisplayNameVariables(String, Account, Group) * @return */ @@ -827,11 +853,11 @@ public String getSandboxCommandLine() { public void setSandboxCommandLine(String sandboxCommandLine) { this.sandboxCommandLine = sandboxCommandLine; } - + public String getOverrideLoadAccountsFilename() { return overrideLoadAccountsFilename; } - + public void setOverrideLoadAccountsFilename(String overrideLoadAccountsFilename) { this.overrideLoadAccountsFilename = overrideLoadAccountsFilename; } @@ -839,7 +865,7 @@ public void setOverrideLoadAccountsFilename(String overrideLoadAccountsFilename) public boolean isLoadSampleJudgesData() { return loadSampleJudgesData; } - + public void setLoadSampleJudgesData(boolean loadSampleJudgesData) { this.loadSampleJudgesData = loadSampleJudgesData; } diff --git a/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java b/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java index e173da27b..ed3742985 100644 --- a/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java +++ b/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java @@ -46,6 +46,7 @@ import javax.swing.border.EmptyBorder; import javax.swing.border.LineBorder; import javax.swing.border.TitledBorder; + import edu.csus.ecs.pc2.core.CommandVariableReplacer; import edu.csus.ecs.pc2.core.IInternalController; import edu.csus.ecs.pc2.core.model.ContestInformation; @@ -57,56 +58,56 @@ /** * Contest Information edit/update Pane. - * + * * This pane displays and allows updating of Contest Information Settings. - * The pane uses a vertical BoxLayout to display a collection of settings sub-panes. Each settings sub-pane is a singleton + * The pane uses a vertical BoxLayout to display a collection of settings sub-panes. Each settings sub-pane is a singleton * which is constructed by a getter method. Each getter returns a self-contained pane (including that each returned pane * has a layout manager controlling how things are laid out within that pane and also has size and alignment * constraints defining how the components within that pane are managed by the layout manager for the pane). * Each sub-pane also has a CompoundBorder consisting of a {@link TitledBorder} compounded with a "margin border" * (an {@link EmptyBorder} with {@link Insets}); this provides an offset for each sub-pane within the outer pane. - * - * Method {@link #initialize()}, which is invoked whenever this ContestInformationPane is instantiated, adds two + * + * Method {@link #initialize()}, which is invoked whenever this ContestInformationPane is instantiated, adds two * components to *this* pane: a {@link JScrollPane} containing a "center pane" * (returned by {@link #getCenterPane()}), plus a button bar. The sub-panes displaying the Contest Information * Settings are added to the center pane (within the scroll pane) in the method {@link #getCenterPane()}. - * - * Method {@link #setContestAndController()} (which is expected to be invoked by any client instantiating - * this ContestInformationPane class) invokes method {@link #populateGUI()}, which in turn invokes + * + * Method {@link #setContestAndController()} (which is expected to be invoked by any client instantiating + * this ContestInformationPane class) invokes method {@link #populateGUI()}, which in turn invokes * {@link IInternalController.getContest().getContestInformation()} to obtain the current contest information settings from * the server; it then uses the returned values to initialize the GUI display settings. - * - * Each active component in the pane (including its sub-panes) has a listener attached to it. Whenever a component + * + * Each active component in the pane (including its sub-panes) has a listener attached to it. Whenever a component * is changed (key typed/release, checkbox checked, button pushed, etc.) it invokes method {@link #enableUpdateButton()}. * This method (despite its name) doesn't actually necessarily *enable* the Update button; rather, it invokes {@link #getFromFields()} - * to obtain the data currently displayed in the GUI fields and compares it with the current contest information settings. - * If they DIFFER then the Update and Cancel buttons are enabled. Subsequently pressing the Update button invokes - * {@link #updateContestInformation()}, which (again) invokes {@link #getFromFields()} to fetch the GUI settings and then + * to obtain the data currently displayed in the GUI fields and compares it with the current contest information settings. + * If they DIFFER then the Update and Cancel buttons are enabled. Subsequently pressing the Update button invokes + * {@link #updateContestInformation()}, which (again) invokes {@link #getFromFields()} to fetch the GUI settings and then * invokes {@link IInternalController.updateContestInformation(contestInformation)} to save the new GUI information in the * local controller (which presumably responds by saving it on the server). - * + * * Developer's Notes: - * + * * To add a new sub-pane to this ContestInformationPane, define a getter method (e.g. getNewPane()) * which returns the new pane as an instance of {@link JPanel}, and add a call centerPane.add(getNewPane()) * in method {@link #getCenterPane()}. - * + * * To add a new {@link JComponent} to an *existing* sub-pane, first create an accessor which creates the new component - * (for example, getNewComponent()), then go to the getter method for the sub-pane to which the new component - * is to be added (for example, {@link #getCCSTestModePane()}) and add to the body of that - * method an "add" statement which calls the new getter (for example, in the body of {@link #getCCSTestModePane()} you might add + * (for example, getNewComponent()), then go to the getter method for the sub-pane to which the new component + * is to be added (for example, {@link #getCCSTestModePane()}) and add to the body of that + * method an "add" statement which calls the new getter (for example, in the body of {@link #getCCSTestModePane()} you might add * ccsTestModePane.add(getNewComponent()). Note that the new component could be either an individual component * (such as a JLabel or JCheckBox) or a {@link JPanel} which itself contains sub-components. - * + * * Note that you may (probably will) have to adjust the maximum, minimum, and preferred sizes of the pane to which the * new component is being added in order to accommodate the new component in the layout. Note also that you must include * the necessary size and alignment attributes in any new component being added. - * + * * Note also that if you add new information to the GUI, you must update {@link #getFromFields()} to fetch the new information from * the GUI fields and save it, and you must update method {@link ContestInformation#isSameAs(ContestInformation)} to include * a check of the new information. - * - * + * + * * @author pc2@ecs.csus.edu */ public class ContestInformationPane extends JPanePlugin { @@ -139,6 +140,8 @@ public class ContestInformationPane extends JPanePlugin { private JTextField judgesDefaultAnswerTextField = null; + private JTextField judgesExecuteFolderTextField = null; + private JCheckBox jCheckBoxShowPreliminaryOnBoard = null; private JCheckBox jCheckBoxShowPreliminaryOnNotifications = null; @@ -160,7 +163,7 @@ public class ContestInformationPane extends JPanePlugin { private JTextField runSubmissionInterfaceCommandTextField = null; private JLabel runSubmissionInterfaceLabel = null; - + private JTextField startTimeTextField; private JLabel startTimeLabel; @@ -179,8 +182,11 @@ public class ContestInformationPane extends JPanePlugin { private JPanel judgesDefaultAnswerPane; + private JPanel judgesExecutePane; + private JLabel judgesExecuteFolderWhatsThisButton; + private JPanel judgingOptionsPane; - + private ScoringPropertiesPane scoringPropertiesPane; private JPanel teamSettingsPane; @@ -233,17 +239,17 @@ public class ContestInformationPane extends JPanePlugin { private JLabel teamScoreboardDisplayFormatLabel; private JLabel teamDisplayFormatWhatsThisButton; - + // private JTextField textfieldPrimaryCCSURL; // // private JTextField textfieldPrimaryCCSLogin; // // private JTextField textfieldPrimaryCCSPasswd; - + /** * This method initializes this Contest Information Pane - * + * */ public ContestInformationPane() { super(); @@ -252,22 +258,22 @@ public ContestInformationPane() { /** * This method initializes this - * + * */ private void initialize() { this.setLayout(new BorderLayout()); this.setSize(new Dimension(900, 700)); - + //put the center pane in a scrollpane so the user can access it without expanding the window JScrollPane sp = new JScrollPane(getCenterPane()); this.add(sp,BorderLayout.CENTER); - + this.add(getButtonPanel(), java.awt.BorderLayout.SOUTH); } /** * This method initializes buttonPanel - * + * * @return javax.swing.JPanel */ private JPanel getButtonPanel() { @@ -285,98 +291,98 @@ private JPanel getButtonPanel() { /** * This method initializes centerPane - the central pane containing the Contest Information Settings * and control components. - * + * * @return javax.swing.JPanel */ private JPanel getCenterPane() { if (centerPane == null) { - + centerPane = new JPanel(); centerPane.setToolTipText(""); centerPane.setLayout(new BoxLayout(centerPane,BoxLayout.Y_AXIS)); - + //contents of the pane: - + centerPane.add(Box.createVerticalStrut(15)); centerPane.add(getContestSettingsPane()) ; centerPane.add(Box.createVerticalStrut(15)); - + centerPane.add(getJudgingSettingsPane(),null); centerPane.add(Box.createVerticalStrut(15)); - + centerPane.add(getTeamSettingsPane()); centerPane.add(Box.createVerticalStrut(15)); - + centerPane.add(getRemoteCCSSettingsPane()); centerPane.add(Box.createVerticalStrut(15)); - + } return centerPane; } private Component getRemoteCCSSettingsPane() { if (remoteCCSSettingsPane == null) { - + remoteCCSSettingsPane = new JPanel(); remoteCCSSettingsPane.setAlignmentX(LEFT_ALIGNMENT); remoteCCSSettingsPane.setMaximumSize(new Dimension(900, 250)); remoteCCSSettingsPane.setMinimumSize(new Dimension(900, 250)); remoteCCSSettingsPane.setPreferredSize(new Dimension(900,250)); - - + + if (showPaneOutlines) { - + TitledBorder titleBorder = new TitledBorder("Remote CCS Settings "); titleBorder.setBorder(lineBorderBlue2px); remoteCCSSettingsPane.setBorder(new CompoundBorder(margin,titleBorder)); - + } else { remoteCCSSettingsPane.setBorder(new EmptyBorder(2,2,2,2)); } - + remoteCCSSettingsPane.setLayout(new BoxLayout(remoteCCSSettingsPane, BoxLayout.Y_AXIS)); //the contents of the pane: - + remoteCCSSettingsPane.add(Box.createVerticalStrut(15)); - + remoteCCSSettingsPane.add(getCCSTestModePane(),JComponent.LEFT_ALIGNMENT); remoteCCSSettingsPane.add(Box.createVerticalStrut(15)); - + remoteCCSSettingsPane.add(getShadowSettingsPane(),JComponent.LEFT_ALIGNMENT); } return remoteCCSSettingsPane; - + } private JPanel getCCSTestModePane() { if (ccsTestModePane == null) { - + ccsTestModePane = new JPanel(); ccsTestModePane.setLayout(new FlowLayout(FlowLayout.LEFT)); ccsTestModePane.setPreferredSize(new Dimension(700, 80)); ccsTestModePane.setMaximumSize(new Dimension(700, 80)); ccsTestModePane.setMinimumSize(new Dimension(700, 80)); - + TitledBorder tb = BorderFactory.createTitledBorder("CCS Test Mode"); ccsTestModePane.setBorder(new CompoundBorder(margin,tb)); - ccsTestModePane.setAlignmentX(LEFT_ALIGNMENT); - + ccsTestModePane.setAlignmentX(LEFT_ALIGNMENT); + //the contents of the pane: - + ccsTestModePane.add(getCcsTestModeCheckbox(), null); ccsTestModePane.add(getHorizontalStrut_2()); - + ccsTestModePane.add(getRunSubmissionCommandPane(),null); - + } return ccsTestModePane; - + } @@ -384,15 +390,15 @@ private JPanel getRunSubmissionCommandPane() { if (runSubmissionCommandPane == null) { runSubmissionCommandPane = new JPanel(); runSubmissionCommandPane.setMaximumSize(new Dimension(500, 20)); - + runSubmissionInterfaceLabel = new JLabel(); runSubmissionInterfaceLabel.setHorizontalTextPosition(SwingConstants.TRAILING); runSubmissionInterfaceLabel.setText("Run Submission Command: "); runSubmissionInterfaceLabel.setToolTipText("The command used to submit to a remote CCS"); runSubmissionInterfaceLabel.setHorizontalAlignment(SwingConstants.RIGHT); - + //the contents of the pane: - + runSubmissionCommandPane.add(runSubmissionInterfaceLabel, null); runSubmissionCommandPane.add(getRunSubmissionInterfaceCommandTextField(), null); @@ -402,9 +408,9 @@ private JPanel getRunSubmissionCommandPane() { private Component getScoreboardFreezePane() { if (scoreboardFreezePane == null) { - + scoreboardFreezePane = new JPanel(); - + scoreboardFreezePane.add(getContestFreezeLengthLabel(),null); scoreboardFreezePane.add(getContestFreezeLengthtextField()); @@ -423,7 +429,7 @@ private Component getScheduledStartTimePane() { private JLabel getContestFreezeLengthLabel() { if (contestFreezeLengthLabel == null) { - + contestFreezeLengthLabel = new JLabel(); contestFreezeLengthLabel.setText("Scoreboard Freeze Length (hh:mm:ss) "); contestFreezeLengthLabel.setHorizontalTextPosition(SwingConstants.TRAILING); @@ -442,12 +448,12 @@ private Component getContestSettingsPane() { contestSettingsPane.setAlignmentX(LEFT_ALIGNMENT); if (showPaneOutlines) { - + TitledBorder titleBorder = new TitledBorder("Contest Settings"); titleBorder.setBorder(lineBorderBlue2px); - + contestSettingsPane.setBorder(new CompoundBorder(margin,titleBorder)); - + } else { contestSettingsPane.setBorder(new EmptyBorder(2, 2, 2, 2)); } @@ -469,9 +475,9 @@ private Component getContestSettingsPane() { private JPanel getContestTitlePane() { if (contestTitlePane == null) { - + contestTitlePane = new JPanel(); - + contestTitlePane.add(getContestTitleLabel()); contestTitlePane.add(getContestTitleTextField(), null); @@ -480,9 +486,9 @@ private JPanel getContestTitlePane() { } private JLabel getContestTitleLabel() { - + if (contestTitleLabel == null) { - + contestTitleLabel = new JLabel("Contest title: "); } return contestTitleLabel; @@ -495,38 +501,40 @@ private JLabel getContestTitleLabel() { */ private JPanel getJudgingSettingsPane() { if (judgeSettingsPane == null) { - + judgeSettingsPane = new JPanel(); - + judgeSettingsPane.setAlignmentX(LEFT_ALIGNMENT); - judgeSettingsPane.setMaximumSize(new Dimension(800, 425)); - judgeSettingsPane.setMinimumSize(new Dimension(800, 425)); - judgeSettingsPane.setPreferredSize(new Dimension(800,375)); + judgeSettingsPane.setMaximumSize(new Dimension(800, 600)); + judgeSettingsPane.setMinimumSize(new Dimension(800, 600)); + judgeSettingsPane.setPreferredSize(new Dimension(800,550)); if (showPaneOutlines) { - + TitledBorder titleBorder = new TitledBorder("Judging Settings"); titleBorder.setBorder(lineBorderBlue2px); - + judgeSettingsPane.setBorder(new CompoundBorder(margin,titleBorder)); } else { judgeSettingsPane.setBorder(new EmptyBorder(2,2,2,2)); } - + judgeSettingsPane.setLayout(new FlowLayout((FlowLayout.LEFT))); - + //the contents of the pane: - + judgeSettingsPane.add(Box.createVerticalStrut(15)); judgeSettingsPane.add(getTeamInformationDisplaySettingsPane(), LEFT_ALIGNMENT); - + judgeSettingsPane.add(getJudgesDefaultAnswerPane(),LEFT_ALIGNMENT); - + judgeSettingsPane.add(getJudgingOptionsPane(),LEFT_ALIGNMENT); - + + judgeSettingsPane.add(getJudgesExecutePane(),LEFT_ALIGNMENT); + judgeSettingsPane.add(getScoringPropertiesPane(),LEFT_ALIGNMENT); - + judgeSettingsPane.add(Box.createHorizontalStrut(20)); } @@ -535,14 +543,14 @@ private JPanel getJudgingSettingsPane() { private Component getTeamSettingsPane() { if (teamSettingsPane == null ) { - + teamSettingsPane = new JPanel(); teamSettingsPane.setMaximumSize(new Dimension(800, 120)); teamSettingsPane.setPreferredSize(new Dimension(800,120)); - teamSettingsPane.setAlignmentX(LEFT_ALIGNMENT); + teamSettingsPane.setAlignmentX(LEFT_ALIGNMENT); if (showPaneOutlines) { - + TitledBorder titleBorder = new TitledBorder("Team Settings"); titleBorder.setBorder(lineBorderBlue2px); @@ -551,11 +559,11 @@ private Component getTeamSettingsPane() { } else { teamSettingsPane.setBorder(new EmptyBorder(2,2,2,2)); } - + teamSettingsPane.setLayout(new FlowLayout(FlowLayout.LEFT)); //contents of the pane: - + teamSettingsPane.add(getMaxOutputSizeLabel(), null); teamSettingsPane.add(getMaxOutputSizeInKTextField(), null); teamSettingsPane.add(getRigidArea1()); @@ -570,9 +578,9 @@ private JPanel getTeamScoreboardDisplayFormatPane() { if (teamScoreboardDisplayFormatPane==null) { teamScoreboardDisplayFormatPane = new JPanel(); - + //contents of the pane: - + teamScoreboardDisplayFormatPane.add(getTeamScoreboardDisplayFormatLabel()); teamScoreboardDisplayFormatPane.add(getTeamScoreboardDisplayFormatTextfield()); teamScoreboardDisplayFormatPane.add(getTeamScoreboardDisplayFormatWhatsThisButton()); @@ -581,7 +589,7 @@ private JPanel getTeamScoreboardDisplayFormatPane() { } private JLabel getTeamScoreboardDisplayFormatWhatsThisButton() { - + if (teamDisplayFormatWhatsThisButton == null) { Icon questionIcon = UIManager.getIcon("OptionPane.questionIcon"); if (questionIcon == null || !(questionIcon instanceof ImageIcon)) { @@ -628,11 +636,11 @@ public void mousePressed(MouseEvent e) { + "\n {:sitenumber} -- the PC^2 site number (in a multi-site contest) to which the team logs in (e.g., \"1\" or \"5\")" // + "\n {:countrycode} -- the ISO Country Code associated with the team (e.g. \"CAN\" or \"USA\")" // + "\n {:externalid} -- the ICPC CMS id number (if any) associated with the team (e.g., \"309407\")" // - + + "\n\nSo for example a display format string like \"{:teamname} ({:shortschoolname}) might display the following on the scoreboard:" // + "\n Hot Coders (CSUS) " // + "\n(Notice the addition of the literal parentheses around the short school name.)" // - + + "\n\nSubstitution values depend on the corresponding data having been loaded into the PC^2 Server; if there is no value defined for a" // + "\nspecified substitution string then the substitution string itself appears in the result." + " If the defined value is null or empty then an empty string appears in the result." @@ -642,8 +650,9 @@ public void mousePressed(MouseEvent e) { private JTextField getTeamScoreboardDisplayFormatTextfield() { if (teamScoreboardDisplayFormatTextfield==null) { teamScoreboardDisplayFormatTextfield = new JTextField("Undefined",30); - + teamScoreboardDisplayFormatTextfield.addKeyListener(new java.awt.event.KeyAdapter() { + @Override public void keyReleased(java.awt.event.KeyEvent e) { enableUpdateButton(); } @@ -661,7 +670,7 @@ private Component getTeamScoreboardDisplayFormatLabel() { private JLabel getMaxOutputSizeLabel() { if (labelMaxOutputSize == null) { - + labelMaxOutputSize = new JLabel(); labelMaxOutputSize.setHorizontalAlignment(SwingConstants.RIGHT); labelMaxOutputSize.setBorder(new EmptyBorder(0,10,5,5)); @@ -672,22 +681,22 @@ private JLabel getMaxOutputSizeLabel() { /** * This method initializes teamDisplaySettingPane - * + * * @return javax.swing.JPanel */ private JPanel getTeamInformationDisplaySettingsPane() { if (teamInformationDisplaySettingsPane == null) { - + teamInformationDisplaySettingsPane = new JPanel(); teamInformationDisplaySettingsPane.setMaximumSize(new Dimension(700, 200)); teamInformationDisplaySettingsPane.setAlignmentX(Component.LEFT_ALIGNMENT); - + teamInformationDisplaySettingsPane.setLayout(new FlowLayout(FlowLayout.LEFT)); - - teamInformationDisplaySettingsPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, + + teamInformationDisplaySettingsPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Team Information Displayed to Judges", javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, javax.swing.border.TitledBorder.DEFAULT_POSITION, null, null)); - + //contents of pane: teamInformationDisplaySettingsPane.add(getDisplayNoneRadioButton(), null); teamInformationDisplaySettingsPane.add(getHorizontalStrut()); @@ -706,23 +715,23 @@ private JPanel getScoringPropertiesPane() { if (scoringPropertiesPane == null) { scoringPropertiesPane = new ScoringPropertiesPane(getUpdateButton(),getCancelButton()); - scoringPropertiesPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Scoring Properties", + scoringPropertiesPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Scoring Properties", javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, javax.swing.border.TitledBorder.DEFAULT_POSITION, null, null)); - + } return scoringPropertiesPane; } - + private JPanel getJudgingOptionsPane() { if (judgingOptionsPane == null) { - + judgingOptionsPane = new JPanel(); - + judgingOptionsPane.setLayout(new BoxLayout(judgingOptionsPane,BoxLayout.Y_AXIS)); - judgingOptionsPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Judging Options", + judgingOptionsPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Judging Options", javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, javax.swing.border.TitledBorder.DEFAULT_POSITION, null, null)); judgingOptionsPane.add(getJCheckBoxShowPreliminaryOnBoard(), LEFT_ALIGNMENT); @@ -735,25 +744,47 @@ private JPanel getJudgingOptionsPane() { private JPanel getJudgesDefaultAnswerPane() { if (judgesDefaultAnswerPane == null) { - + judgesDefaultAnswerPane = new JPanel(); judgesDefaultAnswerPane.setMaximumSize(new Dimension(500, 200)); judgesDefaultAnswerPane.setAlignmentX(Component.LEFT_ALIGNMENT); - + judgesDefaultAnswerPane.setLayout(new FlowLayout(FlowLayout.LEFT)); - - judgesDefaultAnswerPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Judge's Default Answer", + + judgesDefaultAnswerPane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Judge's Default Answer", javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, javax.swing.border.TitledBorder.DEFAULT_POSITION, null, null)); //the contents of the pane: - + judgesDefaultAnswerPane.add(getJudgesDefaultAnswerTextField(), null); } return judgesDefaultAnswerPane; } + private JPanel getJudgesExecutePane() { + if (judgesExecutePane == null) { + + judgesExecutePane = new JPanel(); + judgesExecutePane.setMaximumSize(new Dimension(500, 200)); + judgesExecutePane.setAlignmentX(Component.LEFT_ALIGNMENT); + + judgesExecutePane.setLayout(new FlowLayout(FlowLayout.LEFT)); + + judgesExecutePane.setBorder(javax.swing.BorderFactory.createTitledBorder(null, "Execute Folder", + javax.swing.border.TitledBorder.DEFAULT_JUSTIFICATION, + javax.swing.border.TitledBorder.DEFAULT_POSITION, null, null)); + + //the contents of the pane: + + judgesExecutePane.add(getJudgesExecuteFolderTextField(), null); + judgesExecutePane.add(getJudgesExecuteFolderWhatsThisButton(), null); + + } + return judgesExecutePane; + } + /** * Returns a ShadowSettingsPane containing the components comprising Shadow Mode configuration options. * Because the ShadowSettingsPane class does not add any listeners to its components (because it can't @@ -761,14 +792,15 @@ private JPanel getJudgesDefaultAnswerPane() { * {@link JTextField} component on the ShadowSettingsPane, and adds an ActionListener to the Shadow Mode * checkbox on the ShadowSettingsPane. All of these listeners do the same (one) thing: invoke * {@link #enableUpdateButton()}. - * + * * @return a ShadowSettingsPane containing Shadow Mode Settings components with listeners attached to them */ private ShadowSettingsPane getShadowSettingsPane() { if (shadowSettingsPane == null) { shadowSettingsPane = new ShadowSettingsPane(); - + KeyListener keyListener = new java.awt.event.KeyAdapter() { + @Override public void keyReleased(java.awt.event.KeyEvent e) { enableUpdateButton(); } @@ -778,12 +810,13 @@ public void keyReleased(java.awt.event.KeyEvent e) { shadowSettingsPane.getRemoteCCSPasswdTextfield().addKeyListener(keyListener); ActionListener actionListener = new ActionListener() { + @Override public void actionPerformed(ActionEvent e) { enableUpdateButton(); } }; shadowSettingsPane.getShadowModeCheckbox().addActionListener(actionListener); - + } return shadowSettingsPane; } @@ -792,10 +825,11 @@ public void actionPerformed(ActionEvent e) { private JButton getUnfreezeScoreboardButton() { if (unfreezeScoreboardButton == null) { - + unfreezeScoreboardButton = new JButton("Unfreeze Scoreboard"); unfreezeScoreboardButton.setToolTipText("Unfreezing means the final results can be released to the public via the Contest API and public html"); unfreezeScoreboardButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { String message = "Unfreezing the scoreboard is permanent (cannot be undone);" + "\nunfreezing means the final results are released for public viewing." @@ -815,6 +849,7 @@ private JTextField getContestFreezeLengthtextField() { if (contestFreezeLengthTextField == null) { contestFreezeLengthTextField = new JTextField(8); contestFreezeLengthTextField.addKeyListener(new java.awt.event.KeyAdapter() { + @Override public void keyReleased(java.awt.event.KeyEvent e) { enableUpdateButton(); } @@ -842,7 +877,7 @@ private JLabel getStartTimeLabel() { } private JCheckBox getShadowModeCheckbox() { - + if (shadowModeCheckbox==null) { shadowModeCheckbox = getShadowSettingsPane().getShadowModeCheckbox(); } @@ -850,11 +885,11 @@ private JCheckBox getShadowModeCheckbox() { } private JTextField getPrimaryCCSURLTextfield() { - + if (primaryCCSURLTextfield==null) { primaryCCSURLTextfield = getShadowSettingsPane().getRemoteCCSURLTextfield() ; } - return primaryCCSURLTextfield; + return primaryCCSURLTextfield; } private JTextField getPrimaryCCSLoginTextfield() { @@ -866,7 +901,7 @@ private JTextField getPrimaryCCSLoginTextfield() { } private JTextField getPrimaryCCSPasswdTextfield() { - + if (primaryCCSPasswdTextfield==null) { primaryCCSPasswdTextfield = getShadowSettingsPane().getRemoteCCSPasswdTextfield() ; } @@ -876,7 +911,7 @@ private JTextField getPrimaryCCSPasswdTextfield() { /** * This method initializes updateButton - * + * * @return javax.swing.JButton */ private JButton getUpdateButton() { @@ -887,6 +922,7 @@ private JButton getUpdateButton() { updateButton.setPreferredSize(new java.awt.Dimension(100, 26)); updateButton.setMnemonic(java.awt.event.KeyEvent.VK_U); updateButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { updateContestInformation(); } @@ -897,7 +933,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes contestTitleTextField - * + * * @return javax.swing.JTextField */ private JTextField getContestTitleTextField() { @@ -905,6 +941,7 @@ private JTextField getContestTitleTextField() { contestTitleTextField = new JTextField(0); //'0' causes textfield to resize based on its data contestTitleTextField.setAlignmentX(LEFT_ALIGNMENT); contestTitleTextField.addKeyListener(new java.awt.event.KeyAdapter() { + @Override public void keyReleased(java.awt.event.KeyEvent e) { enableUpdateButton(); } @@ -918,6 +955,7 @@ public String getPluginTitle() { return "Contest Information Pane"; } + @Override public void setContestAndController(IInternalContest inContest, IInternalController inController) { super.setContestAndController(inContest, inController); @@ -937,15 +975,15 @@ public void setContestAndController(IInternalContest inContest, IInternalControl protected ContestInformation getFromFields() { ContestInformation newContestInformation = new ContestInformation(); ContestInformation currentContestInformation = getContest().getContestInformation(); - + //fill in the Contest URL if (currentContestInformation.getContestURL() != null) { newContestInformation.setContestURL(new String(currentContestInformation.getContestURL())); } - + //fill in the Contest Title newContestInformation.setContestTitle(getContestTitleTextField().getText()); - + //fill in the Team Display mode if (getDisplayNoneRadioButton().isSelected()) { newContestInformation.setTeamDisplayMode(TeamDisplayMask.NONE); @@ -961,17 +999,18 @@ protected ContestInformation getFromFields() { // DEFAULT newContestInformation.setTeamDisplayMode(TeamDisplayMask.LOGIN_NAME_ONLY); } - + //fill in judging information newContestInformation.setJudgesDefaultAnswer(getJudgesDefaultAnswerTextField().getText()); + newContestInformation.setJudgesExecuteFolder(getJudgesExecuteFolderTextField().getText()); newContestInformation.setPreliminaryJudgementsTriggerNotifications(getJCheckBoxShowPreliminaryOnNotifications().isSelected()); newContestInformation.setPreliminaryJudgementsUsedByBoard(getJCheckBoxShowPreliminaryOnBoard().isSelected()); newContestInformation.setSendAdditionalRunStatusInformation(getAdditionalRunStatusCheckBox().isSelected()); - + //fill in older Run Submission Interface (RSI) data newContestInformation.setRsiCommand(getRunSubmissionInterfaceCommandTextField().getText()); newContestInformation.setCcsTestMode(getCcsTestModeCheckbox().isSelected()); - + //fill in Shadow Mode information newContestInformation.setShadowMode(getShadowSettingsPane().getShadowModeCheckbox().isSelected()); newContestInformation.setPrimaryCCS_URL(getShadowSettingsPane().getRemoteCCSURLTextfield().getText()); @@ -979,7 +1018,7 @@ protected ContestInformation getFromFields() { newContestInformation.setPrimaryCCS_user_pw(getShadowSettingsPane().getRemoteCCSPasswdTextfield().getText()); // preserve last shadow event since there is no way to change it on this pane. newContestInformation.setLastShadowEventID(currentContestInformation.getLastShadowEventID()); - + //fill in additional field values String maxFileSizeString = "0" + getMaxOutputSizeInKTextField().getText(); long maximumFileSize = Long.parseLong(maxFileSizeString); @@ -990,27 +1029,27 @@ protected ContestInformation getFromFields() { //fill in values already saved, if any if (savedContestInformation != null) { newContestInformation.setJudgementNotificationsList(savedContestInformation.getJudgementNotificationsList()); - + newContestInformation.setJudgeCDPBasePath(savedContestInformation.getJudgeCDPBasePath()); newContestInformation.setScheduledStartDate(savedContestInformation.getScheduledStartDate()); - + newContestInformation.setAdminCDPBasePath(savedContestInformation.getAdminCDPBasePath()); newContestInformation.setContestShortName(savedContestInformation.getContestShortName()); newContestInformation.setExternalYamlPath(savedContestInformation.getExternalYamlPath()); - + //TODO: why is the following being done here when it is overridden below? newContestInformation.setFreezeTime(savedContestInformation.getFreezeTime()); - + newContestInformation.setLastRunNumberSubmitted(savedContestInformation.getLastRunNumberSubmitted()); newContestInformation.setAutoStartContest(savedContestInformation.isAutoStartContest()); } newContestInformation.setScoringProperties(scoringPropertiesPane.getProperties()); - + newContestInformation.setFreezeTime(contestFreezeLengthTextField.getText()); newContestInformation.setThawed(scoreboardHasBeenUnfrozen); - + return (newContestInformation); } @@ -1023,7 +1062,7 @@ protected void dumpProperties(String comment, Properties properties) { Set set = properties.keySet(); - String[] keys = (String[]) set.toArray(new String[set.size()]); + String[] keys = set.toArray(new String[set.size()]); Arrays.sort(keys); @@ -1050,38 +1089,40 @@ void setEnableButtons(boolean isEnabled) { private void populateGUI() { SwingUtilities.invokeLater(new Runnable() { + @Override public void run() { ContestInformation contestInformation = getContest().getContestInformation(); - + getContestTitleTextField().setText(contestInformation.getContestTitle()); selectDisplayRadioButton(); - + getJudgesDefaultAnswerTextField().setText(contestInformation.getJudgesDefaultAnswer()); + getJudgesExecuteFolderTextField().setText(contestInformation.getJudgesExecuteFolder()); getJCheckBoxShowPreliminaryOnBoard().setSelected(contestInformation.isPreliminaryJudgementsUsedByBoard()); getJCheckBoxShowPreliminaryOnNotifications().setSelected(contestInformation.isPreliminaryJudgementsTriggerNotifications()); getAdditionalRunStatusCheckBox().setSelected(contestInformation.isSendAdditionalRunStatusInformation()); - + getMaxOutputSizeInKTextField().setText((contestInformation.getMaxOutputSizeInBytes() / 1024) + ""); getAllowMultipleTeamLoginsCheckbox().setSelected(contestInformation.isAllowMultipleLoginsPerTeam()); getTeamScoreboardDisplayFormatTextfield().setText(contestInformation.getTeamScoreboardDisplayFormat()); getContestFreezeLengthtextField().setText(contestInformation.getFreezeTime()); - + getCcsTestModeCheckbox().setSelected(contestInformation.isCcsTestMode()); getRunSubmissionInterfaceCommandTextField().setText(contestInformation.getRsiCommand()); if (contestInformation.getRsiCommand() == null || "".equals(contestInformation.getRsiCommand().trim())) { String cmd = "# /usr/local/bin/sccsrs " + CommandVariableReplacer.OPTIONS + " " + CommandVariableReplacer.FILELIST; getRunSubmissionInterfaceCommandTextField().setText(cmd); } - + getShadowModeCheckbox().setSelected(contestInformation.isShadowMode()); getPrimaryCCSURLTextfield().setText(contestInformation.getPrimaryCCS_URL()); getPrimaryCCSLoginTextfield().setText(contestInformation.getPrimaryCCS_user_login()); getPrimaryCCSPasswdTextfield().setText(contestInformation.getPrimaryCCS_user_pw()); - + //add the scheduled start time to the GUI GregorianCalendar cal = contestInformation.getScheduledStartTime(); - getStartTimeTextField().setText(getScheduledStartTimeStr(cal)); + getStartTimeTextField().setText(getScheduledStartTimeStr(cal)); getUnfreezeScoreboardButton().setSelected(contestInformation.isUnfrozen()); setContestInformation(contestInformation); @@ -1092,12 +1133,12 @@ public void run() { }); } - + /** * Convert a GregorianCalendar date/time to a displayable string in yyyy-mm-dd:hh:mm form. */ private String getScheduledStartTimeStr(GregorianCalendar cal) { - + String retString = ""; if (cal != null) { //extract fields from input and build string @@ -1115,35 +1156,40 @@ private String getScheduledStartTimeStr(GregorianCalendar cal) { private void updateContestInformation() { ContestInformation contestInformation = getFromFields(); - + getController().updateContestInformation(contestInformation); } class ContestInformationListenerImplementation implements IContestInformationListener { + @Override public void contestInformationAdded(ContestInformationEvent event) { populateGUI(); savedContestInformation = event.getContestInformation(); } + @Override public void contestInformationChanged(ContestInformationEvent event) { populateGUI(); savedContestInformation = event.getContestInformation(); } + @Override public void contestInformationRemoved(ContestInformationEvent event) { // TODO Auto-generated method stub } + @Override public void contestInformationRefreshAll(ContestInformationEvent contestInformationEvent) { populateGUI(); savedContestInformation = getContest().getContestInformation(); - + } - + + @Override public void finalizeDataChanged(ContestInformationEvent contestInformationEvent) { // Not used } @@ -1184,7 +1230,7 @@ private void selectDisplayRadioButton() { /** * This method initializes displayNoneButton - * + * * @return javax.swing.JRadioButton */ private JRadioButton getDisplayNoneRadioButton() { @@ -1192,6 +1238,7 @@ private JRadioButton getDisplayNoneRadioButton() { displayNoneRadioButton = new JRadioButton(); displayNoneRadioButton.setText("None"); displayNoneRadioButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { // getActionCommand called with text from button // getSource returns the JRadioButton @@ -1204,7 +1251,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes displayNumbersOnlyRadioButton - * + * * @return javax.swing.JRadioButton */ private JRadioButton getDisplayNumbersOnlyRadioButton() { @@ -1212,6 +1259,7 @@ private JRadioButton getDisplayNumbersOnlyRadioButton() { displayNumbersOnlyRadioButton = new JRadioButton(); displayNumbersOnlyRadioButton.setText("Show Numbers Only"); displayNumbersOnlyRadioButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { // getActionCommand called with text from button // getSource returns the JRadioButton @@ -1224,7 +1272,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes displayNameAndNumberRadioButton - * + * * @return javax.swing.JRadioButton */ private JRadioButton getDisplayNameAndNumberRadioButton() { @@ -1232,6 +1280,7 @@ private JRadioButton getDisplayNameAndNumberRadioButton() { displayNameAndNumberRadioButton = new JRadioButton(); displayNameAndNumberRadioButton.setText("Show Number and Name"); displayNameAndNumberRadioButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { // getActionCommand called with text from button // getSource returns the JRadioButton @@ -1244,7 +1293,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes displayAliasNameRadioButton - * + * * @return javax.swing.JRadioButton */ private JRadioButton getDisplayAliasNameRadioButton() { @@ -1252,6 +1301,7 @@ private JRadioButton getDisplayAliasNameRadioButton() { displayAliasNameRadioButton = new JRadioButton(); displayAliasNameRadioButton.setText("Show Alias"); displayAliasNameRadioButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { // getActionCommand called with text from button // getSource returns the JRadioButton @@ -1264,7 +1314,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes showNamesOnlyRadioButton - * + * * @return javax.swing.JRadioButton */ private JRadioButton getDisplayNamesOnlyRadioButton() { @@ -1272,6 +1322,7 @@ private JRadioButton getDisplayNamesOnlyRadioButton() { displayNamesOnlyRadioButton = new JRadioButton(); displayNamesOnlyRadioButton.setText("Show Names only"); displayNamesOnlyRadioButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { // getActionCommand called with text from button // getSource returns the JRadioButton @@ -1284,7 +1335,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes displayNameButtonGroup - * + * * @return javax.swing.ButtonGroup */ private ButtonGroup getDisplayNameButtonGroup() { @@ -1302,7 +1353,7 @@ private ButtonGroup getDisplayNameButtonGroup() { /** * This method initializes cancelButton - * + * * @return javax.swing.JButton */ private JButton getCancelButton() { @@ -1313,6 +1364,7 @@ private JButton getCancelButton() { cancelButton.setMnemonic(KeyEvent.VK_C); cancelButton.setPreferredSize(new java.awt.Dimension(100, 26)); cancelButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { populateGUI(); } @@ -1323,7 +1375,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes JudgesDefaultAnswerTextField - * + * * @return javax.swing.JTextField */ private JTextField getJudgesDefaultAnswerTextField() { @@ -1333,6 +1385,7 @@ private JTextField getJudgesDefaultAnswerTextField() { // judgesDefaultAnswerTextField.setSize(new Dimension(280, 29)); // judgesDefaultAnswerTextField.setLocation(new Point(208, 214)); judgesDefaultAnswerTextField.addKeyListener(new java.awt.event.KeyAdapter() { + @Override public void keyReleased(java.awt.event.KeyEvent e) { enableUpdateButton(); } @@ -1341,9 +1394,84 @@ public void keyReleased(java.awt.event.KeyEvent e) { return judgesDefaultAnswerTextField; } + /** + * This method initializes JudgesExecuteFolderTextField + * + * @return javax.swing.JTextField + */ + private JTextField getJudgesExecuteFolderTextField() { + if (judgesExecuteFolderTextField == null) { + judgesExecuteFolderTextField = new JTextField(50); + judgesExecuteFolderTextField.setText(""); + judgesExecuteFolderTextField.addKeyListener(new java.awt.event.KeyAdapter() { + @Override + public void keyReleased(java.awt.event.KeyEvent e) { + enableUpdateButton(); + } + }); + } + return judgesExecuteFolderTextField; + } + + private JLabel getJudgesExecuteFolderWhatsThisButton() { + + if (judgesExecuteFolderWhatsThisButton == null) { + Icon questionIcon = UIManager.getIcon("OptionPane.questionIcon"); + if (questionIcon == null || !(questionIcon instanceof ImageIcon)) { + // the current PLAF doesn't have an OptionPane.questionIcon that's an ImageIcon + judgesExecuteFolderWhatsThisButton = new JLabel(""); + judgesExecuteFolderWhatsThisButton.setForeground(Color.blue); + } else { + Image image = ((ImageIcon) questionIcon).getImage(); + judgesExecuteFolderWhatsThisButton = new JLabel(new ImageIcon(getScaledImage(image, 20, 20))); + } + + judgesExecuteFolderWhatsThisButton.setToolTipText("What's This? (click for additional information)"); + judgesExecuteFolderWhatsThisButton.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + JOptionPane.showMessageDialog(null, judgesExecuteFolderWhatsThisMessage, "About Judges Execute Folder", JOptionPane.INFORMATION_MESSAGE, null); + } + }); + judgesExecuteFolderWhatsThisButton.setBorder(new EmptyBorder(0, 15, 0, 0)); + } + return judgesExecuteFolderWhatsThisButton; + } + + // the string which will be displayed when the "What's This" icon in the Team Settings panel is clicked + private String judgesExecuteFolderWhatsThisMessage = // + "\nThe Judges Execute Folder field allows you to specify a string which gets used as the judge's execute folder " // + + "\neg. \"executesite1judge1\"" // + + + "\n\nThe string is a pattern which may contain \"substitution variables\", identified by substrings starting with \"{:\"" // + + " and ending with \"}\" (for example, {:runnumber} )." // + + "\nPC^2 automatically replaces substitution variables with the corresponding value for each team" // + + " (for example, the substitution variable {:runnumber} " // + + "\ngets replaced with the current Run's ID Number defined by the PC^2 Server)." // + + + "\n\nLiteral characters (i.e., anything NOT part of a substituion variable) are displayed exactly as written in the format string." // + + + "\n\nSome useful, recognized substitution variables include:" // + + "\n {:runnumber} -- the run Id number, eg. 220 for Run Id 220" // + + "\n {:basename} -- mainfile without extension (hello)" + + "\n {:languageid} -- CLICS language ID (cpp, java, kotlin, etc.)" + + "\n {:problemletter} -- A,B,C..." + + "\n {:problemshort} -- problem short name" + + "\n {:teamid} -- team number (23)" + + + "\n\nAny substitution string available for an executable is allowed here." + + + "\n\nSo for example a judge's execute folder string like \"executesite{:siteid}judge{:clientname}_Run_{:runnumber}\" would change the execute folder to something like:" // + + "\n executesite1judge1_Run_220 " // + + + "\n\nSubstitution values depend on the corresponding data having been loaded into the PC^2 Server; if there is no value defined for a" // + + "\nspecified substitution string then the substitution string itself appears in the result." + + " If the defined value is null or empty then an empty string appears in the result." + + "\n\n"; // + /** * This method initializes jCheckBoxShowPreliminaryOnBoard - * + * * @return javax.swing.JCheckBox */ private JCheckBox getJCheckBoxShowPreliminaryOnBoard() { @@ -1355,6 +1483,7 @@ private JCheckBox getJCheckBoxShowPreliminaryOnBoard() { jCheckBoxShowPreliminaryOnBoard.setMnemonic(KeyEvent.VK_UNDEFINED); jCheckBoxShowPreliminaryOnBoard.setText("Include Preliminary Judgements in Scoring Algorithm"); jCheckBoxShowPreliminaryOnBoard.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { enableUpdateButton(); } @@ -1365,7 +1494,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes jCheckBoxShowPreliminaryOnNotifications - * + * * @return javax.swing.JCheckBox */ private JCheckBox getJCheckBoxShowPreliminaryOnNotifications() { @@ -1378,6 +1507,7 @@ private JCheckBox getJCheckBoxShowPreliminaryOnNotifications() { jCheckBoxShowPreliminaryOnNotifications.setMnemonic(KeyEvent.VK_UNDEFINED); jCheckBoxShowPreliminaryOnNotifications.setText("Send Balloon Notifications for Preliminary Judgements"); jCheckBoxShowPreliminaryOnNotifications.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { enableUpdateButton(); } @@ -1388,7 +1518,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes additionalRunStatusCheckBox - * + * * @return javax.swing.JCheckBox */ private JCheckBox getAdditionalRunStatusCheckBox() { @@ -1399,6 +1529,7 @@ private JCheckBox getAdditionalRunStatusCheckBox() { additionalRunStatusCheckBox.setText("Send Additional Run Status Information"); additionalRunStatusCheckBox.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { enableUpdateButton(); } @@ -1422,17 +1553,18 @@ public void setContestInformation(ContestInformation contestInformation) { /** * This method initializes maxFieldSizeInKTextField - * + * * @return javax.swing.JTextField */ private JTextField getMaxOutputSizeInKTextField() { if (textfieldMaxOutputSizeInK == null) { - + textfieldMaxOutputSizeInK = new JTextField(6); - + textfieldMaxOutputSizeInK.setDocument(new IntegerDocument()); textfieldMaxOutputSizeInK.addKeyListener(new java.awt.event.KeyAdapter() { + @Override public void keyReleased(java.awt.event.KeyEvent e) { enableUpdateButton(); } @@ -1440,31 +1572,33 @@ public void keyReleased(java.awt.event.KeyEvent e) { } return textfieldMaxOutputSizeInK; } - + private JCheckBox getAllowMultipleTeamLoginsCheckbox() { if (allowMultipleTeamLoginsCheckbox==null) { allowMultipleTeamLoginsCheckbox = new JCheckBox("Allow multiple logins per team", true); allowMultipleTeamLoginsCheckbox.addActionListener (new ActionListener() { - + + @Override public void actionPerformed(ActionEvent e) { enableUpdateButton(); } }); - + } return allowMultipleTeamLoginsCheckbox ; } private JCheckBox getCcsTestModeCheckbox() { if (ccsTestModeCheckbox == null) { - + ccsTestModeCheckbox = new JCheckBox(); - + ccsTestModeCheckbox.setText("Enable CCS Test Mode"); ccsTestModeCheckbox.setToolTipText("CCS Test Mode is used to allow PC2 to forward team submissions to a remote" + " Contest Control System via the CLICS 'Run Submission Interface'"); ccsTestModeCheckbox.setMnemonic(KeyEvent.VK_T); ccsTestModeCheckbox.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { enableUpdateButton(); } @@ -1475,7 +1609,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes runSubmissionInterfaceCommandTextField - * + * * @return javax.swing.JTextField */ private JTextField getRunSubmissionInterfaceCommandTextField() { @@ -1484,6 +1618,7 @@ private JTextField getRunSubmissionInterfaceCommandTextField() { runSubmissionInterfaceCommandTextField.setMaximumSize(new Dimension(2147483647, 20)); runSubmissionInterfaceCommandTextField.setText(""); runSubmissionInterfaceCommandTextField.addKeyListener(new java.awt.event.KeyAdapter() { + @Override public void keyReleased(java.awt.event.KeyEvent e) { enableUpdateButton(); } diff --git a/support/judge_webcgi/judge b/support/judge_webcgi/judge new file mode 100644 index 000000000..3d850abff --- /dev/null +++ b/support/judge_webcgi/judge @@ -0,0 +1,178 @@ +#!/bin/bash +PC2_RUN_DIR=/home/icpc/pc2 +PC2_CDP=${PC2_RUN_DIR}/current/config +BANNER_FILE=banner.png +BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} +###PROBLEMSET_YAML=problemset.yaml +###PROBLEMSET_YAML_PATH=${PC2_CDP}/${PROBLEMSET_YAML} + +###declare -A problet_to_name + +# +# Display very curt textual error message +# +Error() +{ + echo "Content-type: text/plain" + echo "" + echo ERROR $* + echo $now Error: $* >> $LOGFILE +} + +###ParseProblemYaml() +###{ +### CURDIR="$PWD" +### tmpdir=/tmp/probset$$ +### mkdir $tmpdir +### # Need a copy since web doesnt have access to full path +### cp ${PROBLEMSET_YAML_PATH} $tmpdir +### cd $tmpdir +### csplit --prefix="x$USER" -q ${PROBLEMSET_YAML} "/^ *- /" "{*}" +### for file in $(echo "x$USER"*) +### do +### letter=`sed -n -e 's/^ *letter: \([A-Z]\).*/\1/p' < $file` +### short=`sed -n -e 's/.* short-name: \(.*\)$/\1/p' < $file` +### if test -n "$letter" -a -n "$short" +### then +### problet_to_name[$letter]="$short" +### fi +### done +### cd $CURDIR +### rm -r $tmpdir +###} + +Preamble() +{ + echo "Content-type: text/html" + echo "" +} + +Header() +{ + if test ! -e ../${BANNER_FILE} + then + ln -s ${BANNER_IMAGE} ../${BANNER_FILE} + fi +cat << EOF + + + +PC² Judge 1 + + + +
+ +

PC2 Judging Results for Judge 1

+
+

+EOF +} + +Trailer() +{ +cat << EOF2 + + +EOF2 +} + +StartTable() +{ + cat << EOF3 +

+ + + +EOF3 +} + +EndTable() +{ + cat << EOF4 +
Run IDTeamProblemLanguageTime Judged
+

+EOF4 +} + +TableRow() +{ + dir="$1" + runid=$2 + problet=$3 + shortname=$4 + langid=$5 + teamnum=$6 + runtime=`stat -c '%y' $dir` + probname="" + probdir="" + if test -n ${shortname} + then + probdir=${PC2_CDP}/${shortname} + probstatement=${probdir}/problem_statement/problem.en.tex + if test ! -s ${probstatement} + then + probstatement=${probdir}/problem_statement/problem.tex + fi + if test -s ${probstatement} + then + probname=`head -1 ${probstatement}` + probname=${probname##*{} + probname=${probname%\}} + fi + fi + problem="$problet - $probname (${shortname})" + echo ''"Run $runidteam$teamnum$problem$langid$runtime" +} + +###ParseProblemYaml +Preamble +Header +StartTable +# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid +for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+$' | sort --field-separator=_ +1rn` +do + # exdir looks like: ex_188_Y_compression_46103_cpp + # RId P ProbShort team# Lang + # RId = Run ID + # P = problem letter + # Lang = CLICS Language id + saveIFS="$IFS" + IFS="_" + set ${exdir} + IFS="$saveIFS" + if test $# -eq 6 + then + runid=$2 + problet=$3 + probshort=$4 + teamnum=$5 + langid=$6 + TableRow "${PC2_RUN_DIR}/$exdir" $runid $problet $probshort $langid $teamnum + fi +done +EndTable +Trailer +exit 0 diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh new file mode 100644 index 000000000..c4bfbe7b0 --- /dev/null +++ b/support/judge_webcgi/showrun.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# +# Display very curt textual error message +# +Error() +{ + echo "Content-type: text/plain" + echo "" + echo ERROR $* + echo $now Error: $* >> $LOGFILE +} + +Preamble() +{ + echo "Content-type: text/html" + echo "" +} + +Header() +{ +cat << EOF + + + +PC² Judge 1 + + +

+

PC2 Judging Results for Judge 1

+
+

+EOF +} + +Trailer() +{ +cat << EOF2 + + +EOF2 +} + +StartTable() +{ + cat << EOF3 +

+ + + +EOF3 +} + +EndTable() +{ + cat << EOF4 +
Run IDTime Judged
+

+EOF4 +} + +TableRow() +{ + dir="$1" + runid=${dir#../Run} + runtime=`stat -c '%y' $dir` + echo ''"Run $runid$runtime" +} + +Preamble +Header + +# Parse query string into dictionary args +sIFS="$IFS" +IFS='=&' +declare -a parm +parm=($QUERY_STRING) +IFS=$sIFS +declare -A args +for ((i=0; i<${#parm[@]}; i+=2)) +do + args[${parm[i]}]=${parm[i+1]} + echo "${parm[i]} = ${args[${parm[i]}]}
" +done +# Done parsing + +echo '

The run is '${args["run"]} in problem directory ${args["probdir"]}. The execute folder is: ${args["dir"]} +Trailer +exit 0 From 701288b8f7b2647e4fed7a33f70bfc5b0b447179 Mon Sep 17 00:00:00 2001 From: John Buck Date: Sun, 30 Jun 2024 21:36:18 -0400 Subject: [PATCH 10/55] i_972 Add script to fetch judgement from execute folder Ongoing work in making human judging of results easier. --- support/judge_webcgi/getjudgment.sh | 114 ++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 support/judge_webcgi/getjudgment.sh diff --git a/support/judge_webcgi/getjudgment.sh b/support/judge_webcgi/getjudgment.sh new file mode 100644 index 000000000..b24514dfc --- /dev/null +++ b/support/judge_webcgi/getjudgment.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# Where to the the result of the first failure. If this file is not created, then the +# run was accepted. (correct) +RESULT_FAILURE_FILE=failure.txt + +# Where judgments are +REJECT_INI=$HOME/pc2/reject.ini + +# Where PC2 puts CLICS validator results +EXECUTE_DATA_PREFIX=executedata + +declare -A Judgments + +InitJudgments() +{ + # Defauls, may be overriden by reject.ini + Judgments["accepted"]="AC" + Judgments["Accepted"]="AC" + Judgments["timelimit"]="TLE" + Judgments["run error"]="RTE" + Judgments["compiler error"]="CE" + if test -s ${REJECT_INIT} + then + while read j + do + if [[ $j = \#* ]] + then + continue + fi + savIFS="$IFS" + IFS='|' + set $j + IFS="$savIFS" + key="$1" + shortcode="$2" + case ${shortcode} in + AC|CE|RTE|WA|TLE) ;; + MLE) shortcode="RTE (MLE)" ;; + *) shortcode="WA (${shortcode})" ;; + esac +# echo Mapping $key to $shortcode + Judgments[$key]="$shortcode" + done < $REJECT_INI + fi +} + +# Takes the judgement string, eg. "Wrong answer" as arg 1 and the validation Result (42 or 42) as arg 2 +MapJudgment() +{ + jm="$1" + vr="$2" + jv=${Judgments[$jm]} + if test -z ${jv} + then + if test ${validationReturnCode} -eq 0 + then + if test ${vr} = "43" + then + jv="WA" + elif test ${vr} = "42" + then + jv="AC" + else + jv="WA (Default)" + fi + else + jv="JE (Validator EC=${validationReturnCode})" + fi + + fi + echo $jv +} + +GetJudgment() +{ + dir=$1 + if ! cd ${dir} + then + echo "Not found" + else + # We got a real live run + # Check out the biggest executedata file + exdata=`ls ${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sort -t. +1rn | head -1` + if test -z "${exdata}" + then + echo "No results" + else + # Source the file + . ./${exdata} + if test ${compileSuccess} = "false" + then + echo "CE" + elif test ${executeSuccess} = "true" + then + if test ${validationSuccess} = "true" + then + MapJudgment "${validationResults}" "${validationReturnCode}" + else + echo "JE (Validator error)" + fi + else + echo "RTE (Execute error)" + fi + fi + fi +} + +InitJudgments + +for file in $* +do + j=`GetJudgment $file` + echo $file: $j +done From 7aabe73341398d1b6e7cf6dce57b3cad904eefc9 Mon Sep 17 00:00:00 2001 From: John Buck Date: Mon, 1 Jul 2024 16:39:45 -0400 Subject: [PATCH 11/55] i_972 Update judge web scripts More updates to make judging easier. Add judge number support {:clientid} to execute folder in scripts --- support/judge_webcgi/Correct.png | Bin 0 -> 46323 bytes support/judge_webcgi/Wrong.png | Bin 0 -> 47549 bytes support/judge_webcgi/cgi-bin/cdpcommon.sh | 21 +++ support/judge_webcgi/cgi-bin/judge.sh | 120 ++++++++++++ .../{getjudgment.sh => cgi-bin/pc2common.sh} | 83 +++++--- support/judge_webcgi/cgi-bin/problemyaml.sh | 26 +++ support/judge_webcgi/cgi-bin/showrun.sh | 25 +++ support/judge_webcgi/cgi-bin/webcommon.sh | 154 +++++++++++++++ support/judge_webcgi/judge | 178 ------------------ support/judge_webcgi/showrun.sh | 106 ----------- 10 files changed, 406 insertions(+), 307 deletions(-) create mode 100644 support/judge_webcgi/Correct.png create mode 100644 support/judge_webcgi/Wrong.png create mode 100644 support/judge_webcgi/cgi-bin/cdpcommon.sh create mode 100644 support/judge_webcgi/cgi-bin/judge.sh rename support/judge_webcgi/{getjudgment.sh => cgi-bin/pc2common.sh} (51%) create mode 100644 support/judge_webcgi/cgi-bin/problemyaml.sh create mode 100644 support/judge_webcgi/cgi-bin/showrun.sh create mode 100644 support/judge_webcgi/cgi-bin/webcommon.sh delete mode 100644 support/judge_webcgi/judge delete mode 100644 support/judge_webcgi/showrun.sh diff --git a/support/judge_webcgi/Correct.png b/support/judge_webcgi/Correct.png new file mode 100644 index 0000000000000000000000000000000000000000..6e73ae8871b9ebb40efe1d237d97388b6855c7b6 GIT binary patch literal 46323 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

|;6-?wa-BR;twrttn%Og)F3)$0E7z!E*^cI3 z1=N+VU8?lOzbQ{YWIGBEcxc$3jo%_}U2phTEZ}=ZmmruU$#~3a{~sFOoj4Wc#qyQ~ zS-D64xB%aw<$1oAP|hpVBg$gYzb;!<(eP!D?&(9P209CXrM>CW?~&$Cjt%(Dw>~fT zuskL5RLs!mH+{3CNBrgdMzvc6#MW z@NIE)_%(i51(0t?p**lZ)Jyx8jeJS#hLjaPZR$?aihNXai$&Yd0E)X}TSioT6{`n`8tYed&5SVqy9u zfIG==e^0(2r?Tz0s0?KfRrae`gBbb_^o)?cry0L@Io@7NB$lwFvSrlpO7p;LEBmld z#0o7#USd@nG|JTyZt@%nOFoF(`mXTV=|-S{{&g>#=$`APxm*X<_lankb9t6G!Z&W) zlXn@{f_miqTOCIYq8DTd{E5xgDrBhn8jdeFdBT*KPy#G&*? zz)FpB(%2(^na6qr#v6#pea}Sln}X>C)^q8mQ%zn*-6G)LN{hdUv>(b`BeS;&euqyf zJuw0V(I$1vn$mA3vm?sK4euP7Dg^yU(a7msQ%y35ca8Azd^cBKea^Efi`wqb%rRm} zbsAbhyO?6hXWf<&XZ-GzLZbgvsjhh!dDrqtET-#n0;{M>gynz&O@vg3WiILDbsb)q zURS_AHN}CS=6E}Uq8!CM%;S4wtxLLvFM%Q{b$cofw|#iO;t{S^jj^^qzaxpaLSm`w z;PbWH>=w6-xold4D$sVVOP=59*glihsO@o^DSs~!#+!7H73{xp*y5-r=NdeZqRy|= zTL}N~X_m+!xU!1cWIfZ`+j<2)a|jT?^bFIlVIJzDCa4X$~cMXbjSy>6=|J8;no za(WHFU-nxFya1zV{uz-|T&>{$>FQlMF&sH(#*-fGL%E#3YbO3O#A;Cgqh)nM6{*5M zCHw~K)}H`%ipi%1QOq_RsyLur`Jw1f!r+m0PDhe&47mOio{kk2yX{k=GQC+AeE(iF zf%rDrmU8)8q!N!$98K;d>sF=%xSfn5nGQ+QTF1UEPE#@8x&z-nWp*JiX+=HC`e^%Y zN+US*56jnEEWG!mCfx0KdqSQ6Ag@3UKhk~lc(&-2$JEWl86I@nT;crVCZ))OtuGd+ z#q%OQ5&6|WLm^ST2fli_QKrW-0v{@IA=4&~;Jp{fhzhnrdn{lf3 z`Hfl{0r;Uv*1;fhiAqx|oTS?Ii3Jsv8|oy>vDvR-=xh5IfCB^itE`> z-S5P0p8oTNDZN6*H6pbSxJ*lyu<+Fy$Yc=qs!ht;itib{a zx=nHomyRA*T`6JMLJt39E#uo<_kGJG9b2NMkj8bl9S@x%V5G8NyLdLIx5)SK>knE( zPvz)NhD=+@&7EnAb5UuBg|FL<4MKyIh{_3vOqQMh`oZBd6Di?Rmrf|JQR4s9?zGON zlJ1~tCo~mOmUvXm@?(rkWJM|v_a19?m zF?0yNn*f~kxRwU+C#(g=w|@~J-blA@+UzO~TPu{q`wq7mHP|kRs+2uFXJ4O06i^>% t95lL2a2pBJ@;(LSPX9mth*9m-vvue06_I#VjUWN2C~7K{$Upz*{{ZoLNEZMA literal 0 HcmV?d00001 diff --git a/support/judge_webcgi/Wrong.png b/support/judge_webcgi/Wrong.png new file mode 100644 index 0000000000000000000000000000000000000000..43401f44b4516d7f934179af25ca905ef07fbf9c GIT binary patch literal 47549 zcmd?Q=QrHn_dYx!NC=W>A!;g5WSDy2GJ6tM~Pm7 zj2drfJs4eAdtT|2(%sv z0!giaK=eP~Ng+HzAkt@c3JTiVw$5(O?`)l2SXC7iSY2G5-`F`?gFq_X=}Ddt$Rus( zP>ZKe_JGg`y-&P=P~f2eE7NuGy*I@7`am7GUOI*q$tm8sb4@8WEAU!wwn}N36&qKP z1*OVuZ7ZROqN3L?Ce-}Ay)i2*C*3UzGd7raC)2)}bpoJ=S}X#BL`ej!pE>`K_=u0{ zBb0}$a3Er@z-t8{A;b0JR{5Rpat{T?=zPBR+`NdKzjaOJqSgBEx>jMxrPfF(q!k3} zG5@jZs41cTCxod*Bhdd+P7WATgb zbg$#%lQ1ve53%f4Ka_tE(VJ)%ggHkV2&<<2q}elm>-Z$}GX*H(A&N}A)ay=KSjrDd z3b#9Zt{|7Z20`&vGFq4F>x&AFd3kRR8sF{_d=*fSak7UEWcj*2i5Djb%9f2!>{+f^ z7vSzK)-ZbtinPJ=(Z7YQ=R7b@H8M70NTvTA04}=xdMd`Tv;Clj^z^rI*u+WbcEyFx z2FT)Pgni8>rSZ=IkOYc=)$@`vZIuo5P|4q+ZLibYcF{YW?uHKRW0bJ4a8v_h(x-;3 zwoSOi1-bv8H2a=MTXN`5U$`}C{B^gta|Yb1+|OmCr4xG>hsiIceq4(0RvZ{G^n|bM zK9d3~7TNYSG>e0qydBE})7o?2d0!TXUkYt$8GUj1#C4Pj679R8GM7cLye1QvbNkv| zZ|&bl3)s~VN}|Oyx&nzR@sms<9Jbsg{Pfdc6qa%A?mZ2gn`iWq4vP*qhXX+muhH}n z(qvt83t|%@)cZy&P9RoEa2jNBn__~1HjJbuF=WkxxcS<_?dz6oPZWce*`O+9PXd3k62GQwl$X(FrKV6) zczc`j?(@cB+M*jg2%c=CqC4lb5=5qf<bh4BmV+PAY^%?5(C$yH;R7KF0j9$LrlPA}$h~S*uTVNEI7UV_*LQMfCN0Z9~Rt9euF^}<&aoH0e z-F*3iH=BI;sjc84j~k-|m1&@WvRKyaaPqL{@X}wgd$&G(`1ReuQtB}}<@!$vjmN8? z0-ATs!h!T1Kdq_^yi2c5P;7=;TMsh>cNPmRsK=D_|s*l;RYjRawu=N zD0*;__$^g~cQdsanRCcVN6p^!>0LkRRx&uDY;-243YFUpkM|bO2`q)PgNL6tD0-=Q zDLuS99zO8{^dPw*+(2GRv-vqAM=N`8?4hbao=RM7ZM@riw+y$K!w8K>4)ILAcV1uf z=4|}r_vs?#LNEA%b_oOK}tbmXtm92W!oDs(^i$#a}tL&hd?``9{JPSG=N|>sJ##pJ@&1)Qq!;B7)e_A%Dl=-EY5p4R6XuiQQ*zEu9!#vj zu9vHkYy9ekE_761Sii>bpq17vGMlENPOC)ULDN`SQo}dRBlI{g`b%`OX0&F_pn_?2 zd5BZ0Q~C}}`XjS~AQ%%k|I*)J;km3?o7&}vYmOFn-$cbNges|9#(cP)?dC=-72>kv zEk9V+po{Jd^mKC#6?ObYR295^zvr}Mwxsol_L1tNqeqk))*AXLtDikS9{SH3cRO2Mx{I{C;ZAMce5+h*@dX$Dm|i z%PwZEese2&=e1ab?(uMR`QkG2PR&JP^{{>V$7js%RjDWX5PgOw9(%g%%dgR zMZNXoDbqRW!Io+Nu8#Is>D}I1wT1O-PD7T0)`Cg;xsyLhOK3Lr553}>Lz;P8GEe_v zcm2QmSuW7D`ueeyj~VqEPoqgLnlFw|rS~WIv<@z1KvaKNv}txlhDC(LD#gO?Qqzo4 zA3mCT*6{FDKFldhC{GgdMC3{BV>f}HqNO5^uM_u>M101 z4`V9LY9+gQ`anN^ul%S7_rigTkz<)b@HSyTyRkIcH)f)qW zQvcF5M`aIs--uV;+$$PCW;&C)G`prdx5LvSJi`QIGFQL4@HPrkNm1F~UA>FD8}m@$ zA%dgs(NbJ)yw=`pukhv!>coW7gdj-BSn${>y4_-UiEqPQH|$ot>uO_zZQzR=(=)AeSXh(JAZu&kLvDckw3ow zRm|S;=Yn`^@2k;Y1MdgI)8IYAy}SdyEgX*O`mL_`f{Us$X5%!nhoq-O9kY)0N)oHir%ticzO}f`Ftc5WwJmnD z(_EioUXb3lKCEu9p7hN<^ z&p>~R-K?eWNenBgIjJY14WT;|K4Yj!yU9eBF2+Iei&~l5))R-W$tZGCawT$>nBC}) zjO0=?qqgHyQ+Z>Mky~P9X@${B%uQ!en_}twANcFaYA14*{xs~K);Q<3F-f5PbFdGW zUiW(%EkNtRGo-WG$6Lprqfeth4ekyG>GEp2dro`Q`8yxtK8@NJmJIe}a6eNQPlJ1) zO$HmM4~o|c1%H^{+l<}_aCtl4IV*il8kgKGXxg|HaAsAZi)rNb(APAtYxLP*STgE% z@88_^r}c-)8e9xdI8QGGbZop>d%2lrO4v^_NFi~{tYO7xb01bQJBcZ678as@;^ryt z>a{F@-C1tQ^A*I{{LX||uCtw6qS=d;S7H*GCvin5uQtP0dOm|Ef3dLrG zz>kLRT@)@Cu23PVqT=7g7Z(05bdoW~IbL{g9GtBOXXeAp1G2G;P3Da7^)y3KVSL=B zD|&KKf4?HS+3fhM?*#mKFKt=u;2pwwWr5kh+iMalvxWQNw@R`V8WH2e(j?;z_i2c2 zuH3zU^8Nt3xXRQH5YC$}Du(YsAb|&0pKHj)xi}E0d`DGL9^z%Tg}dp+xR$==ji2w) z07=OQa;ij(STY9woh^d?6~b%z|Cx7|(# z2Zg1jS9DQM;1fH;2}|Q7j?K!Q$OoNR(2Wex6XAK>rSRRPzKLtGpeIibu#ZFT>=CQz zex}D@@%#Sl&jIEBfB5t5^!+)*OQ-bnRi#AV+>wkpk^cD^dZ^t8>cVOCdbpK7jlJTr1ukw@eu@|HiI<{pvp7B9NUzl~vQRp432KSjE#nO-clrC>2jp&B*1}J{af-4#4#pSBh!?QhD9W;h zvuql;1@2d!PW5!(q_LVwy~*r^@w3$5EO3Egxy>;#QwBplZ4-OWg%PA%L;6SMsq79? z(#X=Eg<-TXQ@$zRHTFOhF4TIg5Jm{dfN#YRe(yfI$z7=yeB2EMDVSkMxL3x9?P*Ef$;A z{7QdJ>+Y_CIG5U+2|wE-0z{-ZGBe1udM%6z8tfMrzv&5BaIltywR9cB6M0&-q39NR zHL*eHtKzG8xY2`~3Pb>U8=QXCnV~ zpnn7{$A|v-m2crc6>(&wOzk<1KQ5~5T z$ijx(>5Gp8_`&NRi@btpm|n5$Q@|}ZSzNE?X0~GonD1`-uZyFrREc!9Gara4w`1H+qN65*OVwVEgB{av&PdjB*Fi$Hb zqmRn|aNyvmyFUF=lII9oH1}NmK3>7z%OU>c64*% zF5~@GhPZjgn3W$xvt14!?$y=&uvx6cy@?EM6L-9%pbS!=y@B0ccxHi>+#cGhv~Gfe z6pn2|FCNxpqFYD+?+~oAFjd{8@=&6YC=(W+J&%|}$x2prqSlP2Z*4w0_ZOBZTd;Mw zoF1MAV}gy&Sl;xWJ(<4f5}miJ!*7P+jOQx{E=9rX8A>zFy)mz>?%eMBCUa_;`<1fx z-J+l)wkf>*uB_xEN$T^?xw5?u#ItN=NI}AowM?k(kWN|t(!ExRHYYuYSOyX zG(1jOC;u{_o;bkQ02X_l3gL44Y@*oNXKYtVelK&j|A>oYCH%t+hS5#saZNGqm3U|s$=o|#s%EXIV9X*i`cOPt{@Z{20&X@K_W zCwITLA##UD6zE;uDG~|4qO(wH=$Q@5*Bx2e@Y*{5Q}czwPWG_Y18gN$G+5#IQp{qL zj;eFPU>YMyJvnoEa%_jX7-C?&nC$_N+I! z^GW6h`|rOxtYAN^o&@Kdq^INNLgd#4bvXdT_+(0GV4g%YWZnqPw1`U5RIlCK%397e zkdw^K6Y|YrvA_s6lRUS9dyYT<73)b+H zlO*Kjv-1&b!7*|6@%qh85llny2;6FRGC6N2oRR;K&d2RWGgAC@NwO|S8#h<$m!jRV z=P++E$*c4#(gxkg(A5!_=K5mMV%f&Is2@psfSY=KU9rtvIp?&Y4Bx>6gI_p|%+F9- z{Jasm3S}C9th_lSShPE&9-SuY6WMiaWNd}h7hPMqH(<@ctMBi#XI-wPXpZUF*} z=0a0X5OE`sB@s!}?Wz;9W;?j0YNa3Hq~S8RN9`hy8W)OrF1m9_J^99xqUu8K#iJ-g z2{$rol=DkMM&On#1+OmHf~bzJ@lWg2CVN#_wY3aa7?JulXL;FV5?>VG1!Tw>PnYjx zn2uP>_j1UiPlMgGm7j(^u}pS#pMLU?`~?DDxgyIyK2cx8ekuiHq7bw1YKNp>itykM z;@bT(Ur5~X%Nv$m_NCrdD4yhZniqcgwNanYp*jV077od0-pM1EjVCBCUF9te zt?~n!eudvF+QT;-ZS&HK>#L2o86&^b0q}gV`xIhD*l$J4ZKwfRE$TN~Kg4KcCZ`q9 zu_Mx8G!xchA*smcK!Utny6P!81Are`)9PAnI#;6%5iiWyP3Fk&Hg5CFW;vMl!}b?` zHey+5I*Xa!z?|3OxXEOxL{%#}>a4|o=S_ze@Lb4l#lMAuExYj1_s%1yEc;zNaDNzB zxIgk-h=b8(GO(F#*(+x9M@yQGCfVUv97Uoag<9LI?46#txX;l3w^-kI6fDhqZue+1 zU(#G|--W>g15ejy1Q^T5OFKf;E+2uLAA2!YRE6(5Mvl8Q|R zT$$(Fno(ivIy8zgy~xKpXPJkLhs4laa(aRD#_O0kGW?G~eLg4rfED??GyFu+6g;6b z!^7w_a zrAgiDfZMFOxbsftyk0gr$a`VV<~*O&c`FeQg9>A$R7!AruBR!e8==4Ol9Tq|^~sPI zo0^CNUcGAB@53S1I76P0A3^TXhVAB>m=RGs=1-IRC`E2 z{t!KPD^j*20u!Zjxn%XS?b$D*48-YU*d1VFPk6z53vS$B);tl@3l;{|$*_tMv5@x5 zr#bCqBzqUs0HQ^^QgNlc^jcxWrM0%JW$%7J{wK-Nr!Su;l3BohaK&(3xcOQ{rNxP4+EcZ(EWqcZE%7oC;gVd-)3Ir=iY=r3LE&Lp%De{DZ>TAyaS zg9<-tb<}1id;LnkxH3IYtx(d1hzPi}ZZ94G@9jzVFw==P79GF-_Qgy8@N@l%$XTQQ z4;`YZ{A|Y zkMcz=ki3Qy2dbeGXw1ub@Fyg;SVBWPtbMOb1DK3f4~mD+gsuv1lVg5Qs{X*IkY;{J z<+*)N$1 zdOGfkb}8|(q2$G@W3{jq5_wLSk1WmKJjB-DIJW68K5W39wVCJOySF=MlH0Cw-M8z; zWke)&5Ih|b;(o-)g+J5n{XkoPye$)sgsSrrrA1g_tvsWpj_9a&w_VdXx=cP^#e15L zNL>bg6dO1rTV#nXYG^{bm}{S6WnjKR7FQf`xt}C;QXg_|O6Gp4lv6n-xrH6EtB`GL zwtu!bp|}pOH<;D8QJ#rh%Szg9hNKqY)weo!hYB#tLO*5hz12tVj>g5c8J5C^sBT?V z-iv42#$p=!7v?$bm;LQ?$X|9jODoB7H2kDfnQeWp-(=^q5MjdL8ao7qt&F~o7CCDd6D-jQsH)<8EFE0c1 zs%SZ@cjr2Tv-}8S15k#?lI*tUw|Q_=``_V3DlF%G-vRZr3hpJCfuY?rRh=38yLPEh z8OtYrcOJ)FmSnjg+jN&&26tc8m-q|u;7-VDkJ7iUoFnj!$4AswL@K@mvL~BNQ-6k2 z>gqpGlDB*dIgKPwednV$GXnt8LR)ya%p9EEe#PUwPT{p}(CEGm3`ANHEpk& z;aBu3@X-O>7K_Opf!FJ=r{TjQ_pU3lsP?^xZ+{F%TyBsLq(f(6@&MXo3D(Q^RjcUB zY@6m$RsS2)c#T>Suc5FASuDq1EylW>Em~y7`?vPPRIy;X6L5O;!%q zHlTK8q}up=qqefFLwGTd%nEktrh<&r3ZW)=?|t=13BSqDA|_k5mq1l<@G;kJ7#zVp zUSbk0CFw8)qYF$^J)2sxm#}14i>m#In8CXucQ1K)TbB&8Wjb6mE=Evs!RGJ0Oyv(+ zO?VyI8Yx4QmPv~x26r{7J{*j>~@m*JPA4kLABH=XmYy zq!+<2TUu@|W+U)XpL^=F3bV1ht}GN{NGwSGSU)*$MP|rT%8&Ui!~|B7Mr+%RG`b3n zKvdG-IkaMD(_KWN^llU<^|&%+q2Y_hF?_Uc^~gl*rpxL3l_p51Zf^Tcd_k1w*zkOV zC06)^2xe}F4T!&j01X4=^BLFKTOR+v*|w#l>eBJADZ67Lb4b@5CjpunL<^vJWzh=M zC11|uxP9|dCf__s@*kY2KU8h1F}*_Q`Ki7CuK#|($w;5qj@bsfvS`>Ee%#J9;|m1T zh3kC^|FO!vPx0GCvTvI&RW8qvi#tVLm`Y-^pa1sd4|`3mcRZv)Sk3ut3hniRvc@p# ztv7hx(Aa4C`)MP2sJXUQCT^*`$~t>!lZmqJ?Mp)+uNcx3zWC!e`B(CHV_N3q;f+uz!9VzWZ`R2h%`Gd%n3k9YL8LRA0;JKP3r6JS{Sy3$K=maj8AylUmcEs zc5g4~6NV78X?MAk zqo(yH?ce0}B>PPg`MpqD3eS(1GYTAF^dbGmGj3j(S>vtH;y&}*TLEvseONF!*Y_ktJ2XsrX^!GpbHq zDg@#v=kcD{aD`}XndDXr4hEyT65VCdh;q8A7NDJoim^EyCd@%}2=uda0(?%PW@I&? z$;Ci#rLxE?oO5?ile;dVjUVa?&-B2GhF(n;dPKX!17@O}UM-=$Jz5pb{M9lt?ARt+ zu*5H*rHFflhtX(5Q->y+n}<<0L#zWnG5^svdmnGL?WFK;x{P|Qr7@(TET;ihDsSgz zzplY?;OM?V%>MdQII?Hr@Hh9v@xg3x z+^m?bg@WD{?EqmP@f@u&76+$kaQpCXycVd!<)rVAr_J8kyWZojJfzY25Vtf8Y%|8v$#O!Qt#_HHRh{E2khNB6^Fe5|Ap*E9d7 zuQt|A1MGC@T_z@@>W}{KVFD};LT;T~Fr#GT`R?5^mLi%f!+i_jQI)MXI9>E=&!%*H zsk=BNCFK!1mX`Q5#^im+`V8uu*`XkH_ZenSj2pTypR*eci#e(KS8jI0Glo$wQ9c7n z`vx6oF0x1Jl2ECl)>#n-W(t04VyrSF%vjm?>;281Y%Smd_G`b{Ih{n}q0PS_tGq1# zgao5$)VWH!gnj;MnN?9qTbyc{H;mYI^S!lpn+UnF0+&aEJxU}|}h!msKXqSL7Qxext#?7Ma19(Hr zAB>w?#jKAM{EEYaWG1c1OY6t>tR$j~^O-f&NL3-?;>Zz5Os%b#xJDvhExha6(cI%b z?tI@wirI~*YPDI1e~~Q|#CKaScu^A7=mF#fJD9!Q7l&sy-hh9Lh%nBOeMz9f`E)2< z1y@tTOeT(%%isIc)9teV^nG6~*D$jmp(V&vvt`C(nS}U}t6$Q@$ve>Y;e!JFJ)*@? zxywhKag)rfmV>L=yw=6mnbwn=2+&uE8=ycG5rXH(Q_2($vXY~y=nWm^QK^xfY_v;~ zhtVKe==2j$Db?SXzm5+!k*TdRyjKM(FuY!ShK72+>RWO&C!+@!wbbUvA_>8N0)d?! zrAZLjdv73kZW7Rf7RXls@3kn+h$JNb^NM8^8nNr0C*8tX*4~}ZHNPwr_w=9*w)zBy zzoczPMVUL>s`|ZzqWRSm#(f@N*;FI0owlE~;&j45xvrX(z95E+*>?67^DT$G4+sU{ zpsmX$uRPdbuUc2Z>qy-6`0;9UV&c`jZ_D&x>sLAL{jJiiF83t2wHRCFBU7#H?bwwm zB6ZFYMcPfiTE~n2GMRez6Ax2cd1kM;Wnk~SEMgM=re>LmpNrL*`WD7o!Cbv5{5 z2I3|aGHvUBzstYmV_accP$$*?Z)JHvm&q?)R;CuRME|VzTl`d}u^!mY zq=w^uz9qU+w?oO-GWijyjINu;MbteVMn10T*#9ZYThh?oK8g((jjy_{NEwQMS3H)? zbC*K4o6on=r#rP7x8}#qThNGmos(W6t?dKkBBCjBQw;y9`sAFQB%`7j{l~wn`3lP5wE2yj#X2C zzCBT%D>$=8cHE};w*T(HypG_N83I-74elQsahW)zEzvLRv!H$^^8GFmZ!$|TCchb4 zVJQAl4a>I^W4KE(X>Vs$ zG3;o(F;si$hmX*sIWb82{rNv@j};f0pk7zQOb*nxDGic78)|2JsG4SoK4heY9A%F? z`F^^PKYc|@Ep=~a@c2I=)X-h-e8OUC|6O~&t+imBpC?43oKsZfxk9@oIPpijWlW3a zE7DMW`{d|ZdC_Yv)Xl4Tx`pgyp^}b!HfXrOkPvf&<7X&+H%^ZspAGGzJ^m1!V%Foj zkn)-`_G4274;95#U}-c)L?jl8ueyl$;eURNvnp;)yRw=jwSxA@%H`V9=ZgE(A9Og_ zkN(k_lqb+_P5#8Kx&g;6`;T`y@*#+~raF^1fEgsSaXisyPSUxfYOvxw(j~R^D~*Ki zq*fVW=eQfGBMyG1t;=}>s13{(FaX94MS@~7$4EvN{UPW^JjeczIa{6fu|GQ!SYAh(_1{CqPh8{Lba=rTpyfUB)*s%Dm zp7w(V!JfmLt&MFFS1dqIec#3Qt+NSw_{?vFHS<%dGr4*NR>=Mz1i7$srwH_3m56Y; z%M_ohZ0$b_J{yVqK78k&WkB!$c_s*b`%0q4jepbt)c%hc2D<#i5c)quei{xBoNb|06ZI#wK;` z>%RJ`@ifn}9Yv4z%S3Wc)uMiMtEpX9kcpn^Y=amcsUc#XUR(aPt=DbN4&7|}_Wta? zL+Yiw|9!b7FKM_tioQ%2GiR+)O>QKfi`;Lkoy4|P4Z55dm>)7nn&Oc24T`gH9l<`1 zcDCbQ4`0gA@xAegzIVCLJ7v@$DsqF0`RPogCC9dZ5FvTlK94Jh%n~JhL@+5u8`%@w zq|l9ktRSnqG?Hp+Pl;&)TC|Nl#wjiC0vBr6(3NMfU%w4IBH+$6qbnX(x4cP$p7qJK z8Wd)aBG2lB7Z;A~{@f|qrC;w3Ff7c_g&o!FEwC`tTpawcA+-fz=r|(~kZfCb?8~Nc z>;g-HXJ}1sipKj6RXK4-@(I<3d70;zqay2p)!V+5*=#Iapv;~jWm&& zNSo(dT2eXQ)=3a6YxZmQnU9rWuV|VG{EZ&z$AMWL8^gVboWrPJ>Nh|lN$H{Y`B=+M za$wFAk%2em#eW1;$rMJG7(l%L=^MThCWQ*vX@fX0bk30FFn@ED7a*b)L*B z^MXX8Piijuy~sjkOOWSYWw%r04p*ucRvl)IX>VaPX~gz}2=-nT2IwB)Uneaq}fn9r=zQ;=D&2?GXHui{b*V!4Z zp7Tu{kE6cMB~e}&^P7W~BeibUDvFCCcjsi>S<(^-G9Al&k`)n~5hGyh7vw=74m5os zexn47+hnyi01wyTYiE0;fwUVk=|%E7-BSm9EPMxB{} z*h20vur;1-5dH`85wm^CeNs~O24$IHgZa$ruaadvGP>ZiL5`V9PDug{DG8z!ZS^`F zxiK!Qv>}a>E>>%P8k0a#J3j6piitI1(_V{_tcw0C3PwL%uo=5;%ZJg*k1*agxd^qM zsp8=!dPIWr)F`sR4HkJ7!th9*|5j5WTTA@sLUA22fwp)gj?cQf&uuPvIPm8ejod45 z<+;HQv?6+qeZ%YbIaKD_^%Rl!3K|Xh3B z=+rg^9J!I?PZ}v7-hOc_sB0e|f0DapV)80{mxbb20e>Cgw}%fHt)Bca-0&Jh)|nfT zxV?9|u{^4RI*&-N);H{aWrT|Kux1*5ts*hrCRFLu6py}{Rn>g?8duZ;-uo0sIy57* zFl{k%hvG5!8;1_Pl8#pAeymyJo!dbddxLOQ%NLLauSlQ4N;8r|`}?y+J1fWZr-kyC z+|t?r%#-6Yyq;E@{cJsOb!2*_4B#gthjxF8A-(MQGCFBZ)=Vi@;!gG#)Of#=)nn^? zTQOSVCLKg%#K@VMITZ!4A_^a+8K<=+X%C;)lthB~7?&{BS2QyjMP4VyVPKjg^urRJ}Y6R)Z2?M2fpZo8>GPEK{gBoQEQ{Byb+r@Zj)9p zU%&?oh=KVVLUs*#F6NL61cnMQ#Mu;e`>Rsj{kiRponlOGC-8#)r*^lP;{lH^hN>W* zAWBri-jz}fgf zBH)B>k%ux2wBM~vS#u!#%tlcHo|xo0?(r+`sofwLj2=<6WImV(?BX@#D^+QWRKuj@ zGU1M_EYtv};0UJ9eUVXJmNtyb#2j?JqN(;E(vaI|;9!EX&cZ7Z7BFx^ zFM6G!Uvil}!dJI5lfnV(T&-iV0YF$M*(S^0Tj)Szu43L!FUJmJ#Dq( zTwwueGHt$7Fx#*}?x-rv+XVUBGgbs52@(OSzb+CPL|3|xwS>NkNWv zIt=qsNW5Dks%%ItsEds;=Z=*j`g74~p1WqB-wnVeDk{pBMqo_Ikx!8){f6ABP-OmS zgj#v6#EBv${+1Rf3R<0C+8|lvgV7u~_17a6p~bn1{CILyp`JKzpXP9So8WAjT|FRx zxU*vB80x$^JUDYrB>#v(;sMu%p?r2I;1PRqGY#2hC0wT72@}pGGz8=?+8D{>i}p*Q zA1U?n*j7ufoh17wPwcjd2e3pDut18XjKeEJFR_k{Z=Bt%+@wh=Y zS3nH8Pt8&Ww(tXC?JZ<=*7{{P{=wKMM+h1dc@I@CQe^s4vcmliD-D|1UW&gq%`HRoF$05i^F3J#Bmnb|;Raq>>z zVP&}yYTIBfaSAJWh?y&Ki-)HX1KQLU2jI}37K^Azs#8$aMQ`! ztCmdI7+{^-NuGS2FBfo|;+KoOU_#mGSr$x&)-eWsgEjMF6Sg_!Hgo2)CqSO*6!fVd z@y;=XcCeD+*s4TV3w6Q|&~=d{{S;f;~IlRy$(`99oc5UIu-kF1|04f^tc@qs>YUv7xip0`gQlK$O7-NT7ler;nT zH5l9t)Y!qC*lOQBCm$5x!d{%_R!xX`jcj4gk)`wGDG)h)+GV z$l*q)T)5L782#H$bZY;A9mDd|;!1r)O>p}&B_gNigd!<$2fa_gG4BEgQcA>p z*5i$GpdUNqgZi%tGImEBA=Y*&SMIiXwvO=bpWpv`VR7-f1DTLqL z5HSfIj~JiH<&B%=o!K*Lxz{7z&gNQTELjgt{B1q=MeO3fI6&yMp8DAOv#oF3S^sUs zR`7BBlPP;d^33^RaNXuVvtUe;X+{;%^km^NR|oORffs-xlM)FR6A}`hh8#^DYdeHJ zEG&il4!;X{8Et*V$|t|9)DfoKy)S}IDOx@k;5gv>LTFnV$4}E4g*yLUGIMNJcH8zY zV3e8Xu<1me^4h_*$ax|TW+1g)tFmt^9BGtCY)+_*Rc zD)!+=uJeIl76$N=m&1?c5_z-J`8&c6_Gcq(_=@O)$@Xw*Z! z8w>7xTO8*M8~#XiWTg>IIp(jOP+sG5N>na-a}(#rxIqEMW;GdMlBkQUl-$ie^R z6VrD^FXK=T4<0keiaq-IFyVY^fbGUWYOt<2o+1|qV1IPpD~Ke@!2G-H7f5&`Bu{q% zWyF+KsaFl*>obi!0QI+e;AV%_FJo?QRp`A1Q2#ClCee=(EK?ZIb88_$mG~TZRDl(Z zOWlJxN2VMb+^nn2H@S>)Csubq`MnQUx2I-dxJonOHM#^dW5{XTLKYd25M+Oxb6A&P zn8T4Waft*2k*%VsgRbPwZ&>D z{Xeh=`Z1)R-lHB;qY+f&aMG-yym|>JzitiOZ{qn>L0A0n9(6!Em-p$9nK7NLvpW>O zYVXoyprS_zp(U|rtb}JPOhv)Z_2s{MT0C#yb9Qcz2-40`O?SO zK42HPd5Kb4{Ov``wxTV1Ly8xD@#M_8p>i_b;y;~OX<8j^HX`apG|=xvTMbvTXPmbSyngfQFQ1k6xJ;eOdC@ueP;*W#BFbkSE}KAz-IP_oH4yOzL2 z=$rmLC%R&KBaH*0d6kDU7B;_~GZ}9;U#MoDFAS;$O~@^MEO@Gg*$G3=+b|W-`DIdy z#G2#BeL~xWn&+-uIA=LjF!3|ahSGJIVm<3yP6-V5V}ulgNrug7keeP|U) zowpkBmIgQX+ONs)n2;_f$1wYRX_`^6)QZ`IQF60B9d5mI>cw>yIsK&*hLoM zY`}H#D|@D;(d>HdEnqLqhBvaa4)1KL;o2oKxLNF3+ob+b)4Wke(5Q=?O9e3uVdR9pe-2){~?hnye0Zo3cPP!F?#Qc>jHGUX>073AOzaX{;R2D`c76IKn3m& zj>RWx(gawh#X$1z$V|xvcn?>-tEHwM;x2L1)QbQgpTFPw#L_D|!Iu64po&9+##Isy z@#!(LGIGlPv55p_p)w*yT~e@FCg5>=dU5@R63DfuBYMHTCd)-zzL)&|n7_p~$o{E4 z+OMR$yk399jfIaAm;G&_MSUeuKNEf?U= zx9g|GW9Dys-03%aFv(WrXAc3gDr`Zm8!_R}_I8cY$7(;^|o@}%u))!+nJ9&wptEn@3A)&SH`cD*tEW%SD~&mGY~rU8 zb&FGKcPJH}>AYfSe&xP~ZzNFVPU7pd>Jdq`MBYXg4hqKq0fW{i2S8!-aQD9LIc%HQPz z!mah}u-EZsVgANfFW>>xBt`8f#TB4tN8~*JL>*_Q6*eL(p6U*TNq zBfHVnqQmdV#ruB4L4F5vuhXkEH^clL6xa6*?R*UiGpxZhWFI7AEp9A_XmLl4|#mgbISm?99gw>VFfn~Ntm}CHJRK7y4rng2e$smBf~7!{?vp_ zoqyp)+4%2{?KUsOxNhX8(p5Kg^GMKcrwF<4g}PZw+0JG(|KO5S-|;5w*=6Q{TNcf+|#EqSA(E`(b%fLSE!XL*-|D& z%2+T}D7P%B0zGX%VO$JzsDEUWX>6QuL5?UeTg9&Ho$kj zt@D5f@8%uRF-vqg>yg&7o%h<3>8*FCGvM=KbMZVBk9}?cNhinKqI#tPxk!HX)fSH| zrP7go0lUA?u^6d}*V{rs_px-A%bT;H_G2dY0{ zcDQ~#R^dPptH4ar;z>)*r?Q?JW#rwi_A7GpO*NQ zD$P67(7bspa!n*-=ph66cf1;tsH^aK>OL?=qgd(277TNds8 z>zvTfCIqD{u4rtO#Z*#yr=bqn(088%bhd)7%$+bp97L!M!DTi_MHA~%Jq;ku6Hk*z zu|UpgyLX(AJe(v-#QX(bbpV7Tjh!)v-!LTh5rg+{2|L;PXq!V~0>EEYjKM|;Tmint zw5MEFqm@1d7VbY0@@PlOMiU3X+tMR>c*|>a_oIns&NTH%0|ASS7tW=Xjd~=zj|D9B zF6*#P`jL7~SRe!oktQf;ON~R*ON(@$)(23*I-`QnE03Mzpy4)Z*+0mMptQ~h0a*i9 zrqEfK6W850@xtW^=u8ilubwRlZO$%rvC>O*2J_I^=YBZni75x_QXA=U+Ak>eLhbQo zH1KtZVh@lUInJ_H6ke+YhO$UskOP?qZx}jwznoAvU*E|oIBm34f5?#bwx9-efiTce zW_bGb>x@stP(%Erc!`97gD6zJ9AO)I1n&vY%J);bLGN!oJZ^?d?qI`2VScZbW8-xF zu{o*)6DsQHBY$i?fh{ZM&*}W{BQi3FbMOS{-Ws^(PMm1Z|Sm zz(>-4NB52wM~o{g-Xi+FiSp}bM8#J}A46>*0re2Qkl&5kgl)ZQ*Zdf97T@@W2kUbe z%L3M(Xp*unNHNmp-=-cc;xV!O&g&V2GN>?X%iX9~BUSgSjR*3lp7g!0tW!ngGX5LK zw{M&|EVss@Y?s%<+goUg`+)SfAmw7Xn`+0@T*4pb&(1p#DkkEohdhNjSZEz;t6b?| zLu|-kvXuKly>7d#l=>SBJ0pQRiQP~avORs`lW&dLb=E>DmvQCWF?B8XCmeoV%mfkF z!1Ys#`&3chhRLvVCcSGSNm~4s@2VWH?V%&$IIDF)nZU`a-_Ki6v&x!N zr^-@{6BSf4g1>f2Dc{r)lGg%XCZmBipo&~>07ZnXf?=^qX}#Qf!#p|h+Udjh3V&Yt zh%~fh?OGsDQ$-(Dp`~AfGW(P%{e9L&I3jfs4!{fNFhH3qxRzy%M5|NwQ8NW%P_2bg zZpm#*M~@LW)P#ua1DWt!A#isFe_e8JUPra`)IkH~n@a%j9@d%0ji_nviL)lBf6acu zywQNf6kZyxr)?mkgH66>o3}+aKU0LhA0FqMWIH)wbSF?tN&Gs#=kbVug9dZ$z#5YD zQ~dQFvCP(KwwLwdsQ*XPcgIut{{P>05e=g~)eoQc5<5C?X|f@BMq7KHuM;JnqMJuKOCV`FuX_w2Wli4&5vYyRjKMf+cON zf5(9Ijq$LfQk~L`E0b)vozvO#xAPCxJEV=U1ctrs-6KCA#+X(gH)w zeYgDg*>>Hm_QO*<#+b2w?)IgpdQkJ^vr6%PExN5BTH^r*jKqh`zuXsDFLJ?zbb8A7 zi^0j6`f+bJe1Tp$!b|F>3pS;EMV&JF4d=LRy+{+t$Y0#5 zHA^LFm~A%wab^!0R_P`X3Hu zTr|M13c2{cBTX6J8LE?{m12X#=VAUcmNCvnmos=W*}#BXU?h;fFtyWZV!F9X*3)45 z`~KDH&CH`5g`&D6^X;PlY*L4&;@*}CcFu;>5D%B&r<%&qW8unp%bOytc*>u^79G0r zl#yc}+WR!Q26z3_{e`!G%dp6JChn+M;bJSPIoRvBZC~FVOA)`~Ly@C-#Is{%_b4+r z&=wQ;O(B`{1fFJm=uevyfthQ}bH#L}eRVOa2rbQJ$>*jY--r%Y)PRFB;U7f57fy&( z+>)svc_Z}|;0e!7QQGlQ%)9vU>2y9ra1kb#SeS{J(GMqy4g-EwBUv*`nV*CQQ0aoQ8+DXRfGP{ zM0vRO^;AtzO#az9qY_V1G1^Ym-N%NjLi)bn$+NbZd^_dV$QGD{M@>iY119Jdd*??s z^UgffqJt!lTJKGC^O9>%-_~}G0TgzbQ3VeO4vQM<0{qp2&NFup5PTTDlwfBE%>Gli z`$pcQaG$K(b(4%HBa0ob20d@2hQ_{gW=%y&-X#8fYf}}Si?SMY3OTy=k%`;Yv$vDd zVHvsF1^g0gMZ_)zmgBCa@Jv1`sFyq4)-p@l{Rqv5d~AO0>umq83M={@-XC4!Dklam zhLlU#t`_Z2U8vsC=ZySYy*+t(r4Ewo0sdv@dqMLAM%%Z-lOj1~$<*7sltUDXa3b3r zmhs&5g*oMnb2v+N%*9l0&(T6#?cNGn9k-W9nomgX47kmN{5{%uvgqL1=l8`>+h64P zbIj|Wg~F2Bio6o#y*@g7A3tUNDlWLTFR|(Z3i`hn!~>pihiWak`j7}o632pFqsTSS zU?#S(9{hZ%yX8=*aZTd)7nCiq--npAYi~1CW-U~e0Vp-oaC4(4v&b7IJ_5(#l(lvAY`R~;EXGgEc-YL;f6T*n(5NjW zrn;He>{AW*%PL3V%RJVe(5wVPQkifC)`C{NKeG?L@S8NIWq=ac12@d6YrK97^HGz$ z7n#&E;2kUa+J0gY8!U2d&jRARy)jv>2wNf4cb|k|zIFnlcY3q9p1w#+Od`iE#AsWy zk4&9MhpW`}$3LtTZTBX*y!eyjba$pMetu$g{bR*`))@9(O?^$vIMJG*r*ioa4d}@< zo|IDi?L4hDePtMYtAv5Lz+Rw6XI)G2FELIbvwV{gPcbpJGgjBMJC=U}aP;v6p2VPD z_jO(!og6)Mae4?5rU^2)3p2mYc@?#zg!OH)%s)BjN!udNe_VV(Qqi{X5%&St7fue&)I71+_o4AJm28TW20Cg!OpkYb*@B7W9x}`Hx^cn&qTG~wbi=v{t zUTNdDUxyb?a8naeKkf3o3pX6bzjL;2c$qTWl^kNbx1Vgzq~ehLnF&QnfSb47S|D~H zSUoW}pU&UUC_zn%ezC#uOZNN?^g=^;+2G|{Ny!vj(P4OA>mUtsO!46TCc-o`X}H=5Hw-{{ zJvmr&9WJN&pobA}pF2MWKK?P5xmTt$W@&EIMawtfy-@I8IvDJ)m#JxbxULZQQAjjwc-;O@SFbmY)e_DBZW>l=@2lbD?LnF3@MTG*AYx z9IoYV6`!-(0VwxZSVoZ0WbAt*OCg2K)t#t|JO(IJ<^y?ehQ~@yBHz=wj%Z)l00Iw{ zOBG@Z{gA_hMY3WHo%@yz@0}?#uOep<WJMhikT zlRRqs@^H0}#$6`)EE=S&)Og0SY5XgJc~|epQkD)F!aB0WBU-&dA7Q3iOo^d|843xM4=OaL@;4J00|=-wOBu zY};L&TnAd!7IuR)9$Uz-Q|`-cL@9A?7GN)IfHg_CQZsb2cYK zJsq)(dwUnVb+cjKiRD4`;Gik82yRqnz#fnu3n!gVhg^RwEFy%Y3UP7-Dokc(FSpal z%=_0IQ+?8Tkip$9id=gqS21l&?gZ%TIm#;EzK~|VznlFgv981`k&h-u1lqGq)lc=} zwoh56Up$3<2}jUTDA>%!`6nfg(-oJ=H`o7=W*m@fEm1;)t7O?%Z-*A`pW=Feob#Vb zhmZMWOmkC}V(&&?x!dk`0CEv-uX`g;D}mV%_i*D+%(a)3M4%N-@agYWN~5YzaJbC& zWVXR=bdQ35Xv@99%9Ged6P5?Ai*|vCYUz@s0vrl+rRsNi|1QWP0iM#UW9|SQgjwvj zpC)&KT%@(j>2NXW^5&}@@1gVg-(kZA-ol2r^JeK+r8nlI7vBDFaliTw{gOAkN|b$Z zFr-L>j?5V218*BS++FOdE%f%_RUFu3M26>JWZd7AYlbtX?zZ`dRA<3R@hX@YnpSem z6@OTf$40b7!IE&DI50g!Nm5(Lip&YoCbJ04{qdl^C~LUmoU0pRiLOkXfUQc5@HyKG z`?C4RpH=B~9IYcG{N6_eY^6Lpus7@Xp+hu?pZs9)qi)x5=C`lRTceOf23XkQh{2N9 z!~tC1)18`>yjMj?s8X)2bHydJ)z>V1U8^pQ49jMsYP&@D56Seg8bLzU1$!9i7amhC z=Xfl{#%RHYg+OLWec9Ph@7~Vc{}gkVSWd26)rx<{1!Owq+m6a*nNtN3gNrWzp6Z}a ztT)Do5L3rrV+KF#upliwu1RSU*vW=if=VG=4TWXtd%}lq3*P?(5q|f?A2+Zzsy6En z-bhlEYg}h~Ygo#>)Mt>;Oh73nz6W+p2lw|l^j&<-J|y)1Qu_^}oZB&11I#YolsWY{ zET=HFc>k-R1kXh^NHU-QWZN4B=8)Xxj?4^N!fic6eWnCM$Iq29#NIp@u{$h4hjfa5 z&9?u>!&xVyzX$#NR14}uJ#Q}7J=S$8{@|NHz50)h>)NFTDIF$4K9W6yHD3=SZE#*> zUfR^CBxL*M;%lD|x>B!D=HMUC?(B+_qC196*(uYb_qf1rC3YI%{re3@_)UT5rlY4y zkLU@x`WQP!l_aO<)!^0Rj$`=Afu^+F3eKV#0=5yKhNj+k;h3a;M zY_~e#I_qyNkw3WJH^ur2p(|<(lf3)cfISCRAgZnFhbn1)CwrbOKR~Ym4=z5rjvc%` zUB%_Z1G$DJTuDcFbWbq;&HNkE*jd1v+Lie7u;YtYe;z2cX9Ef_ z6-_TDAlgRsxKa!fb?0!Z)gEsc&A@|N?uUVBf(BO!9Q@fRHY%Q!9S=eCi%5suZRw#k zYXnqm_LU*d8QLV$HwTH8pDRHKBCOZ-g0LKb8k;9JXTFY;xW{ejQmX%Dd(jZ%(6{E0DUf9vBOG=fr zjx4aw3~hbn5$)zVi*)4{9X8~cBt*3M7Na!ZeqJwSNI4w6mU-@R-4I!IvzTl$hHab0 zYQa7JIWDbh%YynYhvNYlRi`l6pN9%Y;m*dBx12}h-O}ig52PDcv4bx7me1c4H?-h5 z@pU@TTVi6m*;rL>W^MfB%xV0@hO8?o_na1epS`Y&o~()dv(xZ*U#Pcg$YDo&Dh^O> z^KRPuSrJ9#$C4-DIXrVY=FC%H&cS*QGVExkHhYm4FOz&n9+Z7_l>+90$fp9sgE?Mo zDLJCb363+vP_7_D-ezeP&G?py)3yBB4g=Km51g_76Z|%J|4ZW3PqmOF3LH!^STI)TW@nPV@o=PwQ$*mfNRfI5 z@0X0_8du+Z!pJn$j0{pwCi|X+;s`6V&m6ikCmgOrHbgZiiB)+fzH^ND5O8&!PhiQC z)^TU}(7wHL{3Sw1;6e`{UUo~+t(!9XI3sckHmnQ^YMojyoQF2UVyhxK&AcG*MDQJo zVNKm+%);=y!DUNWSZlbaaweF-Q>bCvr=<7lh@?ep4g% zxj3ziqucF7xK55LHfsSQMWprG{5kW?T`#!CgV1|%ROj)UUt(xZ5+hXkNhu>`{DoVs z1a&U(uxnYrh zS7Q7WM8DzC-+uZ?G9YD{oBEC@F18S~(d0yA--)~X5Pk@q@Gh`LdFe$ZsG6xQC>9}> z(fu=;xX;~#q_Jo&r=G+?8f2XcU6-D}mNsrMEpuS~F;K$to3&ZBIkb;NY&nQ2-;@p7 zxlcsq55r>;PggaXw=&kl@}nYwCs}Q<)_i+S{?1DJ309XicVr!qAc*XA-<}K5#!JQ& z>cDTMZusG6lWR)KBv3Uo$MIX%GZ22#Hl7SkWY3@L%Qui3FbC3_*77GUf0(94-Z@+K=V%qu;8z)|$J2aOV!W+NRM!5%z~PAosWL zd7ZkJg)$T-L_ZN_L#u(n)1saE!d2EdW$Co1RbWKcJ(qWoZNcF5FpW_WY;*t4toI{+ zTGgfFe6j}!uA0q?@c`+e&W#b~rcw-bVRC3i$thMN9jaJVWfkhN#^jshx+gkxX?~P; z&C$(^ggt+Osr|0N*|DC?k}Zm)`hmyn8>v;|$5yBR&r!@@{If)}uCY|UBr$FG4G;Oc zHaI%oYL9r`9frv#LW7G*c^rr`|LCFmtU=>1Y~em1aN z^%uZQ2a+Tme>9~ot4-g|1GFKOLZ)xCM9^U)0=RPb9>YK%z;6HW&7D_acjfy}(5mkGezz)U6J=G=!TPn+rX4&*TVFw)Qo+U`xiov(W+<>d zfCs~i#PBPp3s&uaYy%n}o;ltwxqL|I-Lz;XJ3aF41sr8(-CmRX26gV~%E?p{&oAEf zq91nWm0Rny=!uDTq{q7q>Cc6~zPKr;Mr!&TCqQ3?~GVhQ4cS~c?B{xp7 z{O2#AG3x=}Bs9pB@+BaB`M-{vY3FG|b4Ev{M0fM@KG#I*G+CTM{;F(xAgf>9gDR9o zzp-Ocl^x3e4IP6nCw-y$MX3;!O8GWH`cyJ37=_#YXn z(ueL$@Rj!l3lXB3IJbY>{sQd+JtbJ9{nGk$&ct;(mIq45_z~eypWd`GfIu!>#_tNi zIsOE`P|9BX&<7{okYrSiAZqqa7)@xan5!U+urn-Kt zW_L^EGzMU#kfJ$A`%L2tNZD>%dNadTuns!v)Tf4sQ_c|N1bcAr{f4MVSrd0ZZuUVp z9r{;%&@C-Eqry;Mum4Zcc*q;d4mf5>lTS$fx|gUq_oKqh?&Q|8^g5dZ$TLDPPpHnh zwERM%uImZ(xABosh#@j>w8nHxM8yu}CN`&wA$rH*Ep_fD$nbK^#y390o^f9TOBR^! zMeIv5)WXhPypkIU00^l`>Y}yn72GSQbiKQzJGUu63N;Ff(}J6Z<;|7fd=F(>sH1^* z4b@qCYs@XX$O6fw84<vAyDwYcI8NNPWs;WwH2Ujb?K%&OL*o29SoG{yWF&Ov5;(ORKZ{4m0v(Cl>xwg{c}Z|@F5fI ziRX=C=%>E>QuBS29o}?Cvf7@>U3V*FWTXye4ui24V-#)5XS?#7jO#+e*^rsECa%IQ zao*jcjP4}3^)irnsH&U)Dur?g&BKTejD(Q!aT-UaV8H_2nJ!L$6t==OO73qC7+u~} z6~xjPTB!K!>XC5v|d58#@QGcqvPHhged42jq&6vEizzJGM zq4eZ@(Mx97QQ_R5&<+uZho{`%Xb**?hz?&$^Hc=q<0!Nn0Y>-P^WnEvII3L}2%$dI zP^V)^#2H68Y@e9J7$BDSI__r=9^rVMkm=?M2FabUdo=W$Ibd2S5tT&o{Dsd^lSCx( zt0Ny5(I?-a3SYpeoOhH)^(;gWh?Q>-AOb4(qgOQ{WsQVK@ z(-H@MP8wYyjoq#>l)SloNL6~VvUN^@ozqratmfl18RDpxb8x%99jE-YHSeFAC5^e% z*)SoA`oKP2v$XaeiXra_gG~Z-Ds4-0IFM1%femsh%dW#vl<3q`=9WD$ca$SV@k`wj zZKyDWiZO~ESBV;}t6;ZHQ!1mOuqiM?U8Y}Dg%V7~H&PY=$~oabiBADUCL$&pD3R@^)4TnWy1mr6 z?Ok`iuM4Ik`?S}Q12i(yg-y)H@%aSWigkL9YECNTCe;2&Mit1?cYe{D zmcsE%M2Ok+=e=7wg|q-$FVw22-6WPYJ(cR4s8Sn-R!8#dr%Tc>(BbJYodiY}vIwOh zfurQx1<_nKeK|MK)+ z!H6S{3^rLCkM`k0yv!%dA>3~{3BP%NC=G9Gz@{ZO9ppxxQWOi_ zFopIjL|1RX=}-MaIte97J^xv#S)z^Ca^NbCGf`)Ehq4_maVoeKl}4w1Oq#xPOgYhp zq<)06wn}H|C`jW_%ZW5H?}rr1q_G>@q{c};<_1MQq;o28GcfCN!&LPW36*mFUh#8q zK_B2`I6ipEy`IL<Ko5~cjSRBVWe7TW5I4eu6>0l zT|TAx6RL{2<}%`Ed_}9>HGWA${N~6@9ZNcOmd>PkTJgEK&@oy^A7zimcsRjfjo2jF z!a{EZ;f6}4ovudEyE5D-PKHme-))kN8IYOw!YZ#%BiQI9RX z0x=b7SMAA$L|Wv=c56vm<0%$`SzezK^hHzcVLyeFfwzFNR<_<;bgMal#2io7K^rMu z%J$#Df33L<>|(gwdX?Fug$$EpVV95g=+OKy^!q-L^M3Ak;MacHt^eROW^%z=Welbs zXAwVUgT-8Mm!v+%?l<$NLs?u`gr2i7K;6UPrZ$kksng<~{>x$FwZZDxUO3`LgUp`+ z%>8LEQ%*@po8@685)|MrJ^QWRrXq#1`TnNpTiszMB#?X*EBem+A+_00V|SZ6 z7Qw4~IgJ#5nqYg7aL;iclG34lFn?GtZS3n!;?cSpi^E9fKI-HiB>Gf%xfGm1eG%(# zg}wqjR`p!G#AUFrv~0c;-a(j}Li~rzpFMG=h9U=-A3?7Fl9sr(TK6pNbNpIv7H}Vs zXAfcZ8#ysb4V7~7T%5NsQ#X;&;-#)8wg?xN2l0R7otnW&6WM%cB|_ ztQ__sTkRj&!W)ZC<89u7+Fjp68oO@l^p8!~|EEEe4(jbh#T%UPJqXV^*gVg0Qmec` z@^y!>`5D!ZHsQS^mxmHvQ*Tb*7h?dcA4vVyQzt02Xjl<+-Cn znJUY}+J~WXle25LeJEo|fup^=GOiXvC3OZmyUQBbI;)rhvGd9B&d;MySVp$-vnYitbMdDQcnWK*fL{IVLc z$7HhGZb(mrAv9`2`aHq7LBYk$LUsJN3?d9=+WO!Pqnd~LL+()wO-Q8@~8W=1+C|NSKc3^0K`6-0jWJR{_B>(2S z7T0pqG*0|_FND0dtACr^Vj`M2dCKPNsoMw0{CZ_S?9>C@!q~A%o2OK^`m>JdL35r< zk4E&cwJocPC4xxN9(C@YtX59Mixynr@ZWbIk@A}AKha*EP;u>XrC%Atzerum1xNgv zGv<>=dvS1We-kPuS6eLvZ zcyi5Qj)Pa8?>67kpQGlBLC{i=5~lKp)1HWhdmcGh%@i6qUs;iA^oZF3)9aTr14@>N zR4`2Lx-oWxe)|45b47ZLM>y+E!{!jK3;u#_s9vy?++RqxFT&29VljsFXq;MSmYVQm- zxjcMe){dQ9fLRAB!<(s$9;`m7TWf7}p`CgjI+e7h{|W>(lMD6AJuRV#uM9oa`jji2 zl7w!*=0SWH4bt{udNwR9vXMp3>Ca!tNCVkmn2;O!{%lv`h6mobg^naY=jr-iVZt@DJ3n8JuC;Fp(P^$wJR_8wJDi&EJ@Ca0| z@RaJ>Y&UfycMZQ~E=DBug_J@KhWVqgZtW7`#a-CH6bNEf1xc?@#J%dzG0N)&TbtiB zy?Qo+4eaBpW5*|Q>#{Kt0=r!Hj;hPKxvw85X>Sn$`|N0;EdBoW#xDkrBwbB0=D{V6Z7kGnQI)i(zYGX(ja!Qkw&rP@rzR#C68 zV)UJ>)SB+sG(X!11T(KQ%L{t25QfVBf3C)`mFKsbInR#4?fO@beTtb9&-M3H8!aoD zkp}BM7~jErruWvfKUO%~De0?V-3B>f_JYoRTSCrTlF6B!ZE&H>8|P6Kb^@p zFMJnCZI0qp{VA0{rXB*Q*1fs(F4x?78Gv|>KjlyD|c}+ z)2Z20tPDa-{~nW3r)Y6-vgrb&E_;@43d-f+(jD!cGn_a%C0TV1R)^x)S!s}z4?_=P zFK44(*`Heo!&(l<@zHdih1G>`9;`j24A?q|Z1SG4hH9;nAlG3<;szII;aR7(rx3>B zsmC!}qB}3!_hs_9_5$WQQg8mKU8;Cxzgip%m1+Zk7amB}C2n8wFf8edGfuE+BY*VT_rAmV72r$f;xU2Y!lZa>37F-=A%rL%kB`tg$9it{NO{$41{=j40{9s zUsP&=4Jv*KKppa-=kJUq+^}6hEd$v&^NM2OD@F+BXgi4+^4Sv6R z@Stv8jqZ03K()yLsJ-)sG2h@jvxOw|XkHqI>R_e%lz4@ittqG=k3_;q(M}AUWsv4p zefBl2L2%~%4hzoT4?J2by5#Wt@lev3Z6SJ14d#Cfx`BxgY}akED`Tm?X(`piQcy16 zH_W*4Y(8IqQwtsOuwSPKayc0Z_*sqn#Ao^EYAaJaA>%_*yOacknR~*QlQmeV$*&Ax zLOV|nV@9e$?@zW_IsZ8rY1Jk_9HwqyejRw}L;w7hqZU{b9Z%;!uTtIY(Qn?vNCJ}E z9~>E4AD|Z!FX>Y4gbdYA&{J|8EHD+PMb9n;QFq7h+@LVSq^fi7SsakKAobOUlJod0 z*~*5?qnczt2!W^-H^k3`|5BOlLU&rZ8O6KnBYol_k+U%#{G^FLsYQ(jl!)Q=^RW0W zNnPYK=s%@3oYS&vMSxEgh8)!W3-{19oyvB(y5C2)sXN&s=9*r8-3i|>e}ou6{NmCi zttvZMri*0s_beiFJa8A>;VP=W-uPoLS0l50Wwg7eH{cqzZx83f{j)2R4R-N+nCAru z6FLtlS1zACVT-NqT7|Y(rv~f}o5gi*?_6KCn09d;A&_kVbOVz{h*WKUyG`zE*Is5C z83up|s_#r$aB~!%L0KpnOVS~!$9*s4F}+u3W|ptcdOZu){vF(Wcj2i=V-Lodhm`Wo z1G1r0+1Bh(ZO%hE%-QiO)7Qe3Szr!+usyG@RU0QzO*VPcfB9Ms2e|E61`R;-uebYq zNkDaK{5XImo>KMSjVk0&ucQ&pY(f`=sD_&C}YI1MrExW1|k(&uvYg48U8^NyU9f{m#M8j~wLWo~MK z1c6G>y;sN37kUyqTwi2dBSKX|wX{|1;LfhhLL)}q7K%aRhhawd^b>=wkd{drZ=$JZ zDmUj=E?szI-*j)O_z**kJRGQo*YUbw-OCu4mcuxvSVa*;qG^cCHtq9aQ2b@%MRbYq zLDgX@&(v^$7;6 z2J_DPPxGJ9uW#ah!3RuoS$Nm+^HGiaDU3GMIsnj`+^N=~p53@Od->FmQkKFdJ&^GCVzouwh_dmq84NA2RA zh7Eib4O0JcrRcl*I)EQ zFn7*~zPg#*w>0uX0qnaT3XVlX+V5A#I3$aoIV*sM0#f8!%{qfgaGK7ol-Z)&zuvfGn0(6OVhGTM{o4Z=XL-IP72N0m}YpR-@9VLD8JD2^bYc-&AWbe|-~XO2GVrK7#8DR~ zDt`JHFe>@lCckxxA2T8QnQ0b}snDgq{I3bW zk-L(3vCamuqlGO9DuOf!MSAVZ+9OX`h2zvB!hcw=m3adCB5**)Pj)Ci`Q6MGF%-z`MBd8UEOYW8xq;BrbjXv?eKJfcAM+(< z%T{EYbn8ImbbueOC9k*R?`no^s+@^d3f4v3mut!Y>aqV%ks=N9^K;~5Tt5{;<9;7@I zisL5-4ei{y)a||XE@>vZW<2CI%p-F1kG(d@EiS+I#s4uh9#MlMBXj%Y4|Fx>uJs;j z$=bxT^bc%C0`ejG&e{@{Wz5AgN3t}^xJ#edaQSEv~*erh+2BOX>Xn=_Q%E=#VszI);${}X-*-a-EP3I6l``(*a3e0u1; zIkcJNJsd18#!w^)`+6+TWbH1R8fXUsc-)rZp(lDlbNck~H{dBwH(w`f3=QvR_Q7!W zcd630H8j1E^qxIiJYtsDV+E($k)=r3Mk2mXuftK>{76==(cO zzLWs=M8hK)W#X4y+R_;`v<}|8HER2fqFA0aFziJ!4TwYRUcn?BiarS4>Fk`O{IMD& z{-%zqjWJiSyo#*@s<05teOlKhPqKO5PNf4#Y`Br=-? z7^$a#A;JcBH=U$8?8tV4?IE*tp3Q~A3~L84)*k?O7f3iLyCot;geb9b96BsRn#C0yoHFEXDa zs);>Y-d1>;vH)$D7Lht(w=0t|n2KD|q@e?mUGPd=4N8fN)65i~#K;cD?v{_}X-aU) zIcp-P;r8f47LeBPeDnK{%qX{;g8KVA;67}OvnEL9G)n#zukm|Hna$@*P>pn462=9M zypVU2y7zk&@1vHOA{sogZ-v)Jl5zf`<8-J>cj4<xZ za%Do$q~*m_w|$pX1yYo~Cpyj8g?yHQ1;4qQalDhf79N{q(!9dQzjqXA$?x+1);=JZ z#%xxbuek(KnljX-*@i zQ1sW|>|BS$-h&AOH>;CO`CxlNUC?%otgJ};;~7}g%JM`nM@;nx1A-PguVqukMYMk_Eq)`&R6(}=If^Gv`KJB^9cS}NZllzTz8A#5778Sr zpxs|bXc*Z3FmZE7ZY2+^beZ!FGik;eSL9H? znADx~)@I1&3G>?nSLuX+C7}Q21Y6L>zV?w^o0W-ix^-xCm&w*i1togE&Nt=YV1}~A z;wONkW>gHd4_4jTN%Fs|{z^3Ns_ra#bBxASBZ_+O8i>j#Zb66(4mqd+`{yITu_TIi@T zFVOLM3miUv&?F$ge!F@S^{!K{W7U;C3heSmZ(GTj9YE_exqVA*zPUv;KI5`-ecS$9 zih6m6hjA1A{AOOkI(!pwd9`2~WUkyb7PW=6vk@DD8tMY^fxVpycXw~@neZ`-;YTD{ z+avxV(a|Y3n_+v3D%_*=F%E)c^P!MX&1NwQ=_BReC@-LfRN36XNqMQ5e_4v&{ZrG6 z=$5Y+r{v#kL^2FkzVQ~&-t_!&6>g0<>x&Y|gayopT=Vzdk!u?%2X^T|5Uwoe^8!W2 zigLzlD~28vJ{d`*ijNAeYtfFs1udLv>1&J(Fg5*Qf19`t<;sv$J&KmFJ)FNaBE%>; z)Y1+zb8N8PvMrJ4xs6D(q~A|OAf)>4;<+H4jVNkofl8@Yj8PBb-maf|o@Cs-7Px+I zk_c7x9b@uHc%xZHqu0*VWcw3j1~~lQ>?G7H6!EC%mE>T11yK5Q`vsugSkZ5?w&BWN z(jC_=Q^XHI$#(tbvuH83il zqiMx$4mU2sq{w8T{VR{0^Xf?u-gxa;)yXC2wEHED6t6QXmM+MSEdIKle!q@IcXlZ! zv7Ao-#y#)6*lt{2>4rTM-62(zdC5l-y!TVx+%1SpHhpM55Ovvslgfoc@$+-KlpWlb zp?aA`o_84pb?+y`FGT&$is8?4G35qegTL%vJ#pMP52#EK3;lVMT(`tR==XA-J`6Jn zhIR3SH2fYzQ|?Stu=u~Xd9*#quV}_EDduPQj{xm>Ir8?}Z_Mb{s_l|)*D6)8NiwP2 z2$zzO9oWnTCS}8&1ayW|&Q;N-C&X4Gt%CYIG zO8{FQq{DMsb}KklVk!pY458-;L9$M1yIGmtdSKe}Dm4i4&hQfN+r+P?_`a1W+*E#i zwv$)uc6#K39!z>FryVprg33%Bb)tR;_mNguSeSeEQ6odT8xs?jTRYkhWhxZJ@R#Vg zeH(Xq%uG3ay8l2x<-qGD`lU!=s-(@*Ef7{xdGJixt2IwIACfOVZTafIm3(a29IV-# z(uVogPV8qi<&K>O9qMarP;oF5Af0uTQc#_ zyj@yr!F=+<$+)87O@MA;d7q)-v}i$*?)LN4dXKDouNLiYO#G3)*<6)>n7dsUHxh`) zjpQ^MvNyj43v_ayz~1)NTe+pol)7~-Y>o=|!&+%eZhp5i`rN!G(BQStbsd_uD*O^e z6C-)zfOdvHSIn$+<~{k+Zt~(~>?pYe^P5K;Zi-%pfCSLk=Q{J$ON>)x!Kxn_HtV>P zTjpBydCPn{kP!l1!>^v7*Hy9pz3pZx#eJq!uZ;nPVpf}Fe*%aA@0ED;bx_>KTsS35 zgKCG=T2Jw&OGTbuo&Z7KD@8`7p{K`H2cON7g@#sketBCr^ zO}o*yRBa|-l{o!@g&Fm#PIn5l87%4XhZ9Y86b-I$K-_RE;3lg;$W(Inn)vp&vMW{c zH;Ko}F}M-(rt?iw1sU`;CbnB&r8VY}x~FetZ?(W>W2Gvu=_jg`3Rv55F5lffDa;Fw zp+_f|5Re|L$c$1>iW@T|9;A;DsX3dDW?byp;yrN)fAL-3_&2J6I%v1il&L%FVzIR@ zaZ5x^Cq_{I-TtI?z!@hwc(JmCN;L9b5P5McdZi*)yk?>>NfKFXn|>QB^PSSK#To)a zUx|Pr(RY9ka}$}(%#Tj}iB?EO7T+uAE8ngV!fy6phHTn&7o1R}?aR7zp9?zZV|>zs zqEqLI+2h^imP&7J=w)O@K%(Qn#6tNd7&$}H1#P=fnVtgH;J!4%Mu(U zhzEZC?Yg}pC_uY05ogr^nv@UOSwX6VArbO<{hQ&H2MOJ>rpG1MTxY1GWHqJflh(xh1UO;}D4e=+PIPdUQ=QV*1XK z^J)Vtba`6%q2pn3w?*OV!6(^^84d`}0%z<<$Kk z8XAz5{k8-3jVG+}4JY^}t3|sC?S=5{Wr_`JUe3|$Bk$>Y_hS3JRrKO!p<=utKd4w- z`}wIU|8NELe5U@1FP`d$_CB9(M_HS+0##KyMm?B4@vx@irJtA7nGfC|4)AmB)4rAM zUx!!5OWx51C994tQLY13e*I)?^~o&k!L*)kOcc&^gAc zg9%hdSNuA}dVs=aSLfyI7S|Uta*&X_?Qei=#9QJWy~Y+($0Zqi-&QH8wxIklxz2Xv zdQie!V*B5HoxG0DF*u&Spb~6BzqwE=5Yu=U8Q-_J^`8IIAuc@ZIw95}^UCfj$XV$u zyvSZxJU9mAi@6UsSAJUM^3=(ic+`P5ciTO;2UeE(E-e2=Jj5?KjyPOC6QjbwU>NQk z)+lkcd}YS}obbS{IGeH`LD38gGrPPx$j0v=t~ooVW=W8D+xl5?n~942)Ho}|Y+HJ8 z;--+0^}p!wBr(r(puKNP>-jcHtog^?DustrQcOn^`j+F-(cEleo7Ot6!|C9;U($a3 zvjP(72QBzEva!z~^l_wB_6H;mSDcNUda?s(|JG7RftJ>M_IPD%BQ1C;m0`#AcTS_@ z7DKhuQY&-WeDxW)YzZC~$jHQk;)eLaAf!jung_l6LWwW@r;n=o1P0+Tz561KWOkJ3 z`p7gJSrn?GNVycvw(ECfrzCai6-Olg>gO9)5NBQb3*K&A&)U7hLuH^LtzTvp4CMCs0Eqjp}-^*o^4 zklVcZwl8I=I=a;0yDeCFcY-&=yCQ?2CF2NZX5HGd_;Zj@u zfE^7L3hOAEEB?@$d891ACn^HyJ%8pE&t!nm`&fQ!+0XBwmHo|y*fA8~u5EGGFv@q| z`0F(f0%eVjd4j8ax-pNV1%hPmDfJcj*ma_q0tXk4Da(dgK(hL2Hi%3ZhScH&*KQG8 z!qsJ@j>Qe*WhXpr0=iS_Hsr4bC7+F;`}?ToRyfxQpq99Ylj&JY)e@hI+Pp*s_Dzh@ zwZwN7i?W<3{OZ1BPc1L~_sTr2q5nt(|3R_u_2r=OwQ=;~igHH|7F#g(Tc(8v<{{ko ziajh=?5eR3^y~yw0u`LFAT}~=()9=!2;)%<>+Tl*@29%8#R!7CuW3NM=}h056-J^N zM_)&}L=1%5GVkH5lI77OE0p4kuOI}0^s>nA*i3&9mM9hvpW6KU|JwWRw2nbjX5SpO$t|FqLw@^YyS_n;g38)B26i|8(Aiai?1ZklNNbiJzf`D|8-b1)= z_&)cqxcB`j4-Y%LJF_!8yF2rlD?5w8!3=k}!haClNN53u@#lCsTlqiiZNvlJ*;NYg zJ+MLmL^J`11e+@8>>dy@WB8Hl9i;$+O)fI6^m8pD@^T1(3>~xHh*V_>HrJ_jF!o>r zFb{SH95jeu*pHZf53#&E~5g> zfbjZ{ARs@n)5gUzBjN5QD{Ob|VEU(Q>BPV?0RBSeHPaO&5(9jas{ueckLL+Z^N&&6 zDz|4yA6~6}oLb=+03H&v2hf0<7zLCGiD%bUXqOX_{*Rvb;y`sdJE!&@h|E)86Ph+C zQB;Lhwy)fB4HP{b{!VjjR2Ib@PGr4)4o?k;DPg0z_PG9yK&8p<$$Fd5SHobn9Sw?% zAD|hLsLYt?LibGoTw#MMwg2(3PH}6W;R<12_bQd_;_lIz*@n)mw`JuPXXkF4T3KEK zYw|JYnE^=_n;Q$$>Lb2*_pcm2?fkfJ5At@AH42IVrrJ_Ej0lDP8GZ z50`$+{MOKth@__emnG)`@-Mb4S$E%8c@mleeP=L9xYd;N^D^AM&?}o$&fbTIG~Okv zML68#$@yBrw4lrHAy#8CQh#%Ly;1Q;p+J>rYiQUe1kn5peH3UCT~zd`M-e?#*ApM~}As z+`s4Q3Te1T%6S=0BinniPOR~W?ElVGG>f-;xIs-A>xhB&uO4L5Ub$rQ)*Aa zRmlZV;}Dw7jR7}_tcqw6U?M;ursv{6E-vcvW=?TzCkl<>k1GAd_L@Hl1_|jO zv!)%Eq@v%Ym)(mH5=8X(;}1_|lPymkZ68D3*NqY8s@pE|HBI*bI&^q`F$aj_5m1=G z1&C<}@iB$RA!DX0-fFuIUgESBF#XbvNNM%Pmcv?0f|B3q}Xyhy74LSo85 z1&JH=H9m&<>um5HTJFDBB>zIT2VQedy!K^osb1okNov5l?kVlH49Ah*Y-NKDw!@3Z z?M%!lx~K92mR6nXfzL2DhST)Diot4DCE11+0M-?y>*SNM8_pG z3TF)l4nVXpOMtsCn{|PjP+MONFX^ zQ8J#O_jop^SnfW?f7Pc~*5_ewv4O2aJj;w_Yybdpq@yrxD{*Hd2J9+hcp99Hv-$dd zl@y%69&R9~afylB-iFrVs^q=l$ufcLdfTQ!gWw$LjjO5 zzn)*!x*;_%e#zlZx34U^#d9k4yKLsaCuZRiqC3%TmF)`Pv}^NT}Du226oz@(kbBD8=LJLja*3wA+`5(Pkh;&<)yPt z7NwlLWDH+rBxKC&s*l$;FoUF+-t(T$bZmZ1t^M9Hx|Mum-8i{2spPO!gJ>w_UreWR zLC8Wy{yvT_K}j|%ozRe-nOVf*Gfdaq1!1~ZgPSss1%8*-!l#-vU!4IgVd^B4LA zt@up$wsnaw*P5O>>j((TLJEoSHQ81K^!G#G5ZgAXHKrKLHqmh-Q!YEaylKB)>17!U zN_`aIWSR+q*0drF<5KuiVGP?18 z!nBRNuKb1mf9BcIJHIdg%xxlP=pWRj%oT$uSKQm(MU8-OfvK06q4PV#2U$l4Px&oB zl!vZbnxJUyffzZF>Nq`6(Mi8S-8ba>hm@flMKr-C949RN!EC>t)8b@SFn6ZMv<>0JBTVi$l5r97zFgxa-<#!MGM;*Ji;;+wRq8<|&VRoP*_DD{$KB+KPaab5x)ZD!O}Vd~(j3Q+k+ z`Xr{+-?V@ZD*|4f9d42^^=BvZsWrpJ8i~n9^@_Kvy{%dU9{ufMPw7Ig=ixQ;5DKS& z%A9*PG6mZu0W0|%O(y(=(JpaCaXSHf9VUg~=u_IX@^12erEL-i&q!-@=#qDMKtDFF_p}&%`9kfq z=OE1A3f*z>KNYfQI(K-Rd-t6Dk~YjyFIYm1f$9!nOP4Bh(Ng*cEi|T$l5Vu7Kw&RW zj@Y_?!*CP7)kAH5X=(r)UPU&`=Bar5+GWX;vuEOBwybp3S69}gyr*()qTXHt%t*vS z!gnN0XXZPsoT;XqXmM|7_bD;9Pn3x!|?bhL2lc7IcCpZT%9_Hfp z#}?u;wEUihUV1eCCs3sFr}*LH%a_A#u33~A<~7B}`X)&%#_-UX&Ycc5OqFLgA;B@k z{wd`l$H4oUR*jv4`XsO_5zjNYe7MRWR^U_O_9ZH~B6sT&KKJ{kPTg(!zh9$%N%JhD z@9n&=QuOg@urGV2?AFQecD184a1hoPyl-whwB-*FjB2K?#;JHD4PL36xjb!u?&-6M6yo0-x zkkfika?Eui_g;)Nt2OI&#wDs*caMnz$)7%YE86kA{6N)*FE^>^ZC7hc1=y$a+tfJv zn$yv7Avblk2a?C6rM?x%+OOTA+J*mUwT{T)%6(x9ITn)^Of!GiCJV$!0az>O3E_SB z<*y9DPbzdTkfTER0i-cdJ|M#e_^g0bAIb;B`vD&X1_b#2pQ=2mY1(}W2tc6BTl1!_ z$W`RnV6r8Q^}k!?@iu&89MziDZcz+uS~W2A7_{4 z>g7GSkjw63?QrZuvQH1M2<=lQrK^e&1FjR7Ph0tSzb-8)evDcR=!vwEMO_s0J-)%L zm}r0)76*%yXke^P%d#W^J!lg4RfmF{^b^ZC8)(kQ%nR}EmgfpbA~&RndeLAOXWi_D z!17)kZCz>2@2^`=h0cnjt`iOQhf}mKD*cjDbkvBiy1;300X1T0`ju`zj_NSX?OFFi za%LMGs0TZ;B76QU?oaF!H3R^+Kt;8vD-bd;3+7A>E&+DDk$Wkz+d);e0M9Z@z^n-$ z!}(AeL&n)s{Sf=^I?ifJ3o*CuZUyFp#MB_)bo}?~kJr=$k+%CC)BpoP8{KeKuF8bb zy|jmjJRqQ(H{pMaa?xDVgjcXMl^)TDCYT&VsyTyKWP_xbh;mgdULcT$6wBAB3Q{ow zuj}MCeNhN?x4_&wj2471t#LYbZgWiQ)>JOD&K7@-;B; z|8?ZK`N9QO@q!axMLg0=ifF6Fsq;qzR>ApkYj#;ya*@JIA*Y-K$u!@t4t+2^&Hf8Y zN6B=Cnn#V=Ql`P%@`+OI)zNm`JxU%SsPGk8lG9so{l-k$KXi*@2J zR)oF_%Cx^!IqDc-5ry_{$X(Y_GJykaVv0?x_Zox@;U|xexEV{pCl4=F|F!>UQEuC- z?@r{8Dg(R&ouUH!1e0Hr&MG4rHVjTJt+QVz9kRug?1YYLD{AeJx;JVzWd)(<@3Gj0 zGRoYHB@)Fp?$&&KR14nk)%RjxXB+{a`5%>eS1uB=Iy4_+*g9CH7%3fT zsO7(Ge%mRl^s|>YuRj#5bCaQ)d%Bc{ZfD#EigsTvPW2mx{F&{5>+`vIP9Ue_LGWsm z|4mVrC80bLxwJUa*A)uhD-c(38VSSe7eNQzC(6oiU?&f1s`5#gpWr&-yRJWr_8T@S z7P2`Vye9qa?+gz;oMbx%;KETaR7}j%_E#Ut>)9=y|f=|yM33xbp_QBhmT~jZ&>#n zD04%5@tR_WUUeQT{RGR@Jo5jM9}J4R!I)-kEmqA!qDd=`XtN(%-IV`WUD9>4~xqZlm6yNh)Z8WBy4?zlv0+Y4=P2nyo zE}D{G`|_d3I+T(6Sxk-jgAIU?nkcOOjZC_@RGXUZxEq3^NHt>(lP&kSWDC5kqtG!zQ<*&t`amr^XAgR;td7P#ve46)wAfAZMk0mfr9zRzO^)Xdl+xI zI||8F8y>xm3i&njvEs=7sBLR1es6NwQd4lFLi)mD38Tf5C)}GB;#@NIQ?EtXsY|f_ z9RU;kS*`lzUeuzhH4t;hsneNRxaB@Mw_h8@hO%$7Pji(rx@hbfVDPg9?v;2=(^7AB z=Qg{Kk;u$%kq>HjTC-~qdSIStWIgnSUzEB!!Q_6>y#DHJhXiHL_Nutzipuouy94}v z;VSsty;>U)kdj3CwyI~+ut332h`7Y0UO_?&sX|Uz^kYl&M14_sjRQtZs2fB0io6PS#fK!n@n# zC`~n{T+3uc;`(vu-~JU&A=cATT{8hf?-!9I5BH44R!|K)!-A{x&xU+ zbjq*NCv6%iH(u=T_Jt=>>g*1FsGPR1#hLIEXq7CaWB!oNG-o~JYG-pF{; zO@gH=OH?@>UuJ09o^DcE^Dm|7!E2|h#TNF*bHYsEzRN00OlUEnKKXIJCT1r)Ot%n_ za^)h^w+ey4!UW7p1%~HaNTRQS*D#O4RZ7ny{(5NvB2Y7PJsIBWV|($)nWNvybB+cF z8=4$k{ZKa4lQ0$OqRB6bb;_GF#?>=FMxO}@mG&{uG?*(QqWCBz{45d$z+7?xl~I;( z>xv(6u|0At>}=+!QKKtqE$V~8Z0qbZE?M~0LcalrlTWC|wwK$qzTV2)po={&bLLas zsfae|iOH(-zSI}V_7|K8htP1IZYXO(E2VEgylSH;v7>9%=#P}O;B%knoWd)3Cu7CN z|Mh-UB&=?Hzqn~K-@!k3gM)ofgX zg~|Zv;6wrxl&2qO2`nC5Yj|6AUgSKu$=aV(AC;dk8NhU!yS}5ezXhPKQU+lCROs}J z*H_vG&l~Ll{QA`jwX z+)rYzgYWe#t|E5b*hmf+);Q$Ac^_N2e#ZJQHo#{q+hp1~S%4cPaJKWBalH@9hHs_I zzb1z^rHddlB5{E^`kQv}H7g$0|`=G%J(p+r~gqv4LD^zrkHxo@9@F84Ni; z?R?X~WX~6bsB4W-fv&`1w@E1xGjIvXz7hP{>&Pfec|OEo#~y$aN=4Q4gL@x;Uq8rA zHM=0eYT1{6|BbJfi$(}%Vk+}3L^wQk=4f1a0a;&N{HBoH*%=lP= zuVwaW6=QCb(zPxmk%M`1=binG1RP~7###@rdGQvXAs0SK6R(>h^0S!|pK+HK?WK%J zDwtZL-wCJo2qPBg?4*tL@D6f~5&cT53n{G?e}GGXX8_zZ)oZ(7k26fThQEy^UY*uY zC@xbN89YAI=F6MaSmBp;Ja6+~8?JI}%?^Cp=1Ui*9U~yWXkJ(5sBkKF6={^jGK0`L z6WZkk2vH^04wn3^sIoc|;sRejtMo0~8hpufV`C)f?EUcC#%|4t0Be{k3!`WvjD?o~ z9Xypk?|_~yD&EEll-87;RHhEA?_8KEbs}S#tYYC_r4R3twsD8^0wO%P&n8oTt}XEn zd9Rmur}@;Oy0A5SSDu>KLGsevzB8kvB+=tb9a3v&XpPSQt<2;!=Dc>{j|;ybCstJr zpId#M9X6=UJ7kw*(vWoe4|KYIIV$L$n|Ug$xs2DEB#9|yZf3GWk+|Bnjd0kyqg&fd zy{L9rvZT6CS#?y2@m!`vOFa@=(_+Q#!ESU8#LswV{d%5T0hruMZ7-zVWbDOi2Z&2g zomn|f;D-&TOj`dblcK>SQ^-ngtmV%v(xx)#Ay^_*S1(OMe3d=gtMcS*z7* zFfMv436M2QeO+S$Ow21&RBt6KN*SYd&$1ghNvLu%nrlY<5fnP^(csvD_F#dU#nxfW zN`u+%eV;CCbtO^eX8bY7;LmWjZz8j@J%-F4JZ2>%o#hd1mtz+uEEqKOEZBD#f(ohx ziLAQUs&X&@z`9Y0-C7CRjMvK{s_2+CP3H45mtALWBf_I5ioe=yGuPsdI}z1pChMog ziaU=F!^G0-(nrJVVcq1-tY;`dLoTy=%Xrbqp>esBsEKFi$N}658Q4<%{!0mo-1ym6}F&|DU`TUl+|)WTM^&Jd+_-Z-!;*d`Z%SL9Ht1@ zM{3;a@#N284W~kh+lnhV*pKk4Cn&^Tg=XQw=SI0O?xz4btADnvYN2PF?*-4;dT5rk zrEi{S3UxBa6bz%{4av=?Y(ZYM}%k7+WUcZc+ULGfFg$oMf&0RDx zGc{c^j{3_7Xu@jrtkAa`o_yB`@#d()y@EBbWwremr-s2(cc(=sp1Gp_xP);KI8f+MN3>Iq$%HJ% zbI<=A)|1 z(Stb&i}ho^)+VzA@s$kml`F5N*(HjO91Tbr7>?~F_WNtmS-hN#Ke_kKJ{6H7T##<) z42NgXfUvUS`EDNvdcx&Al&RK$JY{cXopinClPKZgZ*ZW_1-6j1*Bx>ut-x^B8rEv| zEn84IQc+3m6L+Tw6&%$0ewEmv$J9~9&Jw$pFWruGq0fYv{rS{h)ImzQdBJD-H3kaF z-jqB0@Mr+j8DP6N2%h=pi(q<)SyFpeT=#+Lt&>omm2_>hQhEE~OJ83KAGe1-IPIe^xgzAyz=jm~o<+DRUKmr`qYobIhnL)fA2=J&^*R$ip zkPO611^#5{?5OD`XqN)lr9lFTsxl4c6#}SiItxzb5WId@4<8(t?DS}_#itlZ9T(Lf zb2Y6P+8k%2>;TD?DyvgsDC9FS6ONgcI`?4 z5qx!=xx+%36PCxs&YodavQcQtx>?g4ssC9~&UxN!rbZVgWlq^;aMrx(7C`yY8l&SF zQiCW9DLHpeYb?zb`-M5xn6_NT1dfFlqT7&%=Rg^$ zJbo+&zkIX1SR6Z0I@D4CCwlSJQ}`lGH~UXUU7fmZk|~Jf&+tkVYwd0S**)L8&T?f5 zQ^)dK`BRm0(9?SaOS6#DJDGh|e@D1JM>sePyYbN*5y6hA7t;I8GslzggiM`Vk;8{M zRh<8hxtf*O((M=lk!6*wFX^)I5kRrg<79mk&pk{i@&C4atUL2KJYivq>xf;pQE? zeUFI}?j9KLX5rVuT=zu6ApcpP>9M-~%hfVVsi3d8`f2sZ-2$HPIG&Fk z+({$lX*rE_HTXl0^74ZbyI1Y%Mcz%@$)6}^8u+ZSb4;h;FFXb%?#h4t!vZW(0{$Op zKN}_HdQO#D9O%e!8tur2)SlJOC6CPz5)4mg3LN+3kN;Ib+3_#}L8j$DQ-h3DXYXOf z8@=GHzFL+oWV5H6jPJlj1K zICnbF6s=}H?EZk-ihA51N?%j3aj9MFfTl;Ul@vENEnJtY^t=K>qP!)x~=bHs-r-lp5hdw z7p+OfO~IV;A-0X!%rc#2>yFTD+E!bLeu&ePZBeOJG+|sEVe_--z10=@>>a2J z?o1jx-&AuKb382xm9GR|5tZdWZhX7J8}{WuQUNsWN;i^`GR&_oxWPHPr4+aU|AD6L zg2{#uo`}X((;Eit-wjOI5}GEcJ2-`JHVr=cMPK4kPkJV<&vn1rL_1JqL$VH0ruL8* zB_ecOqVM&~!oZ|#Y$-m09FVgm1=U3~RoPdwc_23@^nDGgvSjS7 z-6ixV+~iLbr4TqS4W|6=1BjtKpEnQBXwy8|gTjU&q+E3!RNm-g$&azT3C@Iu=GX~! z_)yP{b`jQbi0MYdFwZ8W+GxOs?!&GYs{^K5`8!!2^5wisccsVdnVjdlwxvq7pU%Sk zO(T3EvV!ThkMD1D1>Gm#n&rI5glzFSaC|&;PIw?s6`hAX-Ef|EvplXZYCYrP2q6_t zYI&8UXX-e(R7*j_8}4BS9yA+XBA?BxUqvp85hN66~P9at!&} z^c^-jvYuxS2?}7cc4&tgmzb=*albETkWSY|^6YFp7}!5Rn8_EmF+s1bTe6Bjm4UxK zyqj5^36vgF2L|l}JlZxz{zmjJlR_ z&|UsCyDgBekuQ_}ohMOc*1M7^&i_h-xZvr$R+v2S1i0GsKwcp<>%~1&Vu)TI!GWHq zlK4Ro8`JOoJbjK6wwkqtUM79!JV=O{!t)E)Ov+@|@ba$%E4Xc32_8DbzW+SurG)-1 z9KqP}?;8jHdJHMHtYW*$O~Bx+B9V7=;XUyN-N?wWuLp&%Bz4DK_N^;u8(jl+W5yy& z;0>*?cE%DOm-iVEbsx2`exI-^qpV=>G`1N1HnZJH0>0d+@c@IW+FUR_m31|%2qjNH zS)DLhT;mYgviPPydiq5E{j}qobsF9k4QS-~tD+*N-FE5e5jttf1LdMo_)EIccL?q2 z4>I;M8Ip9IIhMhzba97I`2+oVFwi`_kwV2AroUf(Q3iPD&}p}TlZHv=E*{JkjMt^^-3ml~Rm z!+C#YzkCIRZ54nq4xlqSj8`z|4vc?rnzx*Kl8sMT{Qx0On;JGCFpQhsNjBx5?%019 zl~m+wR4OFNK~vbKqJzTE1v-+BwHmL}jcK=aTn1DkK4-MZ;)~W;QPd{jd*lB}nAX+x znd0C=j3%-bm0DDNXQ*yI7)7fnHuCZ&P0}Mgb!rDEu?cL;ukAed4USr!IFxK=-!;Y4K9xzgWm@fpRUEiP zB9nNLZq+PgaqQ&jRBo~8;iu&g)>#1OfvQr6Oda-$?)Zpil(D}L2i!{WcWJwd8Qxz`qcL#rV;MgDU| z(|9y;#joYGTIsCgG>L31fg4sEmD}`z9t9ZhiOa6Im1wD{(PPfg-L)2=5D3uEFUc(6 zg_mygKeVad4JpIwM-Dw&#%D726>?5}o$!@^1lTJ%JSP@i-k(0?4HO){e|lv8IAJ4n z()UNAb>y8}_b+LiQo;2%n{&a=-Q5u4r5OiK?39!nF08pg?oCtv_(E0dY55bYxBmx~cW4&? literal 0 HcmV?d00001 diff --git a/support/judge_webcgi/cgi-bin/cdpcommon.sh b/support/judge_webcgi/cgi-bin/cdpcommon.sh new file mode 100644 index 000000000..e1ced5e2f --- /dev/null +++ b/support/judge_webcgi/cgi-bin/cdpcommon.sh @@ -0,0 +1,21 @@ +# This file is meant to be 'source'd' to give functionality to read the CDP + +CountSamples() +{ + datadir="$1" + if test -d ${datadir} + then + result=`ls $datadir/*.in | wc -l` + else + result=0 + fi +} + +GetNumberOfTestCases() +{ + probdir="$1" + CountSamples $probdir/data/sample + tot=$result + CountSamples $probdir/data/secret + result=$((tot+result)) +} diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh new file mode 100644 index 000000000..5c9d129a0 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -0,0 +1,120 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + +TableHeader() +{ + cat << EOFTH + +Run IDDispJudgmentProblemTeamTest CasesLanguageJudgeTime Judged + +EOFTH +} + +TableRow() +{ + dir="$1" + runid=$2 + problet=$3 + shortname=$4 + langid=$5 + teamnum=$6 + judgment="$7" + runtime="$8" + testinfo="$9" + judge="${10}" + probname="" + probdir="" + if test -n ${shortname} + then + probdir=${PC2_CDP}/${shortname} + probstatement=${probdir}/problem_statement/problem.en.tex + if test ! -s ${probstatement} + then + probstatement=${probdir}/problem_statement/problem.tex + fi + if test -s ${probstatement} + then + probname=`head -1 ${probstatement}` + probname=${probname##*\{} + probname=${probname%\}} + fi + fi + problem="$problet - $probname (${shortname})" + if test "${judgment}" = "AC" + then + jstyle="green" + jicon='' + else + jstyle="red" + jicon='' + fi + echo ' ' + echo ' '"Run $runid" +# echo ' '$judgment"" +# echo '

'$judgment"
" + echo ' '$jicon'' + echo ' '$judgment'' + echo " $problem" + echo " team$teamnum" + echo " $testinfo" + echo " $langid" + echo " $judge" + echo " $runtime" + echo " " +} + +###ParseProblemYaml +Preamble +Header +StartTable +TableHeader + +# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid_judge +for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+' | sort --field-separator=_ +1rn` +do + # exdir looks like: ex_188_Y_compression_46103_cpp + # RId P ProbShort team# Lang + # RId = Run ID + # P = problem letter + # Lang = CLICS Language id + saveIFS="$IFS" + IFS="_" + set ${exdir} + IFS="$saveIFS" + if test $# -ge 6 + then + exedir=${PC2_RUN_DIR}/$exdir + runid=$2 + problet=$3 + probshort=$4 + teamnum=$5 + langid=$6 + judge=$7 + if test -z "${judge}" + then + judge="N/A" + fi + GetJudgment "${exedir}" + judgment="${result}" + runtime="${executeDateTime}" + # Get how many total test cases there are + probdir=${PC2_CDP}/${probshort} + if test -n "${probdir}" + then + GetNumberOfTestCases "${probdir}" + numcases=${result} + else + numcases="??" + fi + # Note that GetJudgment also filled in exdata with the last execute data + GetLastTestCaseNumber "${exdata}" + testcaseinfo=$((result+1))/${numcases} + + TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" + fi +done +EndTable +Trailer +exit 0 diff --git a/support/judge_webcgi/getjudgment.sh b/support/judge_webcgi/cgi-bin/pc2common.sh similarity index 51% rename from support/judge_webcgi/getjudgment.sh rename to support/judge_webcgi/cgi-bin/pc2common.sh index b24514dfc..3db5cd6c4 100644 --- a/support/judge_webcgi/getjudgment.sh +++ b/support/judge_webcgi/cgi-bin/pc2common.sh @@ -1,10 +1,21 @@ -#!/bin/bash -# Where to the the result of the first failure. If this file is not created, then the -# run was accepted. (correct) +# Meant to be "source'd" into bash scripts. + +# The judge's home directory +JUDGE_HOME=/home/icpc + +# Modify constants as necessary for your installation +PC2_RUN_DIR=${JUDGE_HOME}/pc2 +PC2_CDP=${PC2_RUN_DIR}/current/config + +# Where can we find the contest banner png file for webpage headers +BANNER_FILE=banner.png +BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} + +# Where to the the result of script failure before execution RESULT_FAILURE_FILE=failure.txt # Where judgments are -REJECT_INI=$HOME/pc2/reject.ini +REJECT_INI=${JUDGE_HOME}/pc2/reject.ini # Where PC2 puts CLICS validator results EXECUTE_DATA_PREFIX=executedata @@ -19,7 +30,7 @@ InitJudgments() Judgments["timelimit"]="TLE" Judgments["run error"]="RTE" Judgments["compiler error"]="CE" - if test -s ${REJECT_INIT} + if test -s ${REJECT_INI} then while read j do @@ -41,6 +52,7 @@ InitJudgments() # echo Mapping $key to $shortcode Judgments[$key]="$shortcode" done < $REJECT_INI + else echo NO REJECT FILE fi } @@ -49,57 +61,87 @@ MapJudgment() { jm="$1" vr="$2" - jv=${Judgments[$jm]} - if test -z ${jv} + result=${Judgments[$jm]} + if test -z ${result} then if test ${validationReturnCode} -eq 0 then if test ${vr} = "43" then - jv="WA" + resul="WA" elif test ${vr} = "42" then - jv="AC" + result="AC" else - jv="WA (Default)" + result="WA (Default)" fi else - jv="JE (Validator EC=${validationReturnCode})" + result="JE (Validator EC=${validationReturnCode})" fi fi - echo $jv +} + +GetLastJudgmentFile() +{ + lj_exdir="$1" + result=`ls $lj_exdir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1rn | head -1` +} + +GetLastTestCaseNumber() +{ + if test -z "$1" + then + result=0 + else + saveIFS="$IFS" + IFS=. + set ${1} + result="$2" + IFS="$saveIFS" + if test -z "${result}" + then + result=0 + fi + fi } GetJudgment() { dir=$1 + exdata="" if ! cd ${dir} then - echo "Not found" + result="Not found" + elif test -s ${RESULT_FAILURE_FILE} + then + jerr=`cat ${RESULT_FAILURE_FILE}` + result="JE ($jerr)" else # We got a real live run # Check out the biggest executedata file - exdata=`ls ${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sort -t. +1rn | head -1` +# exdata=`ls ${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sort -t. +1rn | head -1` + GetLastJudgmentFile $dir + exdata=${result} if test -z "${exdata}" then - echo "No results" + result="No results" else # Source the file . ./${exdata} if test ${compileSuccess} = "false" then - echo "CE" + result="CE" elif test ${executeSuccess} = "true" then if test ${validationSuccess} = "true" then MapJudgment "${validationResults}" "${validationReturnCode}" else - echo "JE (Validator error)" + result="JE (Validator error)" fi else - echo "RTE (Execute error)" + result="RTE (Execute error)" fi fi fi @@ -107,8 +149,3 @@ GetJudgment() InitJudgments -for file in $* -do - j=`GetJudgment $file` - echo $file: $j -done diff --git a/support/judge_webcgi/cgi-bin/problemyaml.sh b/support/judge_webcgi/cgi-bin/problemyaml.sh new file mode 100644 index 000000000..b1b2154d9 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/problemyaml.sh @@ -0,0 +1,26 @@ +###PROBLEMSET_YAML=problemset.yaml +###PROBLEMSET_YAML_PATH=${PC2_CDP}/${PROBLEMSET_YAML} + +###declare -A problet_to_name + +###ParseProblemYaml() +###{ +### CURDIR="$PWD" +### tmpdir=/tmp/probset$$ +### mkdir $tmpdir +### # Need a copy since web doesnt have access to full path +### cp ${PROBLEMSET_YAML_PATH} $tmpdir +### cd $tmpdir +### csplit --prefix="x$USER" -q ${PROBLEMSET_YAML} "/^ *- /" "{*}" +### for file in $(echo "x$USER"*) +### do +### letter=`sed -n -e 's/^ *letter: \([A-Z]\).*/\1/p' < $file` +### short=`sed -n -e 's/.* short-name: \(.*\)$/\1/p' < $file` +### if test -n "$letter" -a -n "$short" +### then +### problet_to_name[$letter]="$short" +### fi +### done +### cd $CURDIR +### rm -r $tmpdir +###} diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh new file mode 100644 index 000000000..03e31042a --- /dev/null +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -0,0 +1,25 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + + +# Parse query string into dictionary args +# This is not terribly secure. +declare -a parm +sIFS="$IFS" +IFS='=&' +parm=($QUERY_STRING) +IFS=$sIFS +for ((i=0; i<${#parm[@]}; i+=2)) +do + a=${parm[i]} + eval $a=${parm[i+1]} +done +# Done parsing + +Preamble - 'Run '$run +HeaderNoBanner Details for RunId $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" + +Trailer +exit 0 diff --git a/support/judge_webcgi/cgi-bin/webcommon.sh b/support/judge_webcgi/cgi-bin/webcommon.sh new file mode 100644 index 000000000..fef94f341 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/webcommon.sh @@ -0,0 +1,154 @@ +# File meant to be "source'd" to support generating web pages + +# +# Display very curt textual error message +# +Error() +{ + echo "Content-type: text/plain" + echo "" + echo ERROR $* +} + + +Preamble() +{ + echo "Content-type: text/html" + echo "" + if test $# -eq 0 + then + headmsg="Judge" + else + headmsg="$*" + fi + cat << PREEOF + + + +PC² $headmsg + + + +PREEOF +} + +Header() +{ + # Make sure link to banner page is in a place apache can read + if test ! -e ../${BANNER_FILE} + then + ln -s ${BANNER_IMAGE} ../${BANNER_FILE} + fi +cat << HDREOF +
+ +

PC2 Judging Results

+
+

+HDREOF +} + +HeaderNoBanner() +{ + if test $# -eq 0 + then + hdrmsg="Judging Results" + else + hdrmsg="$*" + fi +cat << EOF +

+

PC2 $hdrmsg

+
+

+EOF +} + +Trailer() +{ +cat << EOF2 + + +EOF2 +} + +StartTable() +{ + cat << EOF3 +

+ +EOF3 +} + +EndTable() +{ + cat << EOF4 +
+

+EOF4 +} + diff --git a/support/judge_webcgi/judge b/support/judge_webcgi/judge deleted file mode 100644 index 3d850abff..000000000 --- a/support/judge_webcgi/judge +++ /dev/null @@ -1,178 +0,0 @@ -#!/bin/bash -PC2_RUN_DIR=/home/icpc/pc2 -PC2_CDP=${PC2_RUN_DIR}/current/config -BANNER_FILE=banner.png -BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} -###PROBLEMSET_YAML=problemset.yaml -###PROBLEMSET_YAML_PATH=${PC2_CDP}/${PROBLEMSET_YAML} - -###declare -A problet_to_name - -# -# Display very curt textual error message -# -Error() -{ - echo "Content-type: text/plain" - echo "" - echo ERROR $* - echo $now Error: $* >> $LOGFILE -} - -###ParseProblemYaml() -###{ -### CURDIR="$PWD" -### tmpdir=/tmp/probset$$ -### mkdir $tmpdir -### # Need a copy since web doesnt have access to full path -### cp ${PROBLEMSET_YAML_PATH} $tmpdir -### cd $tmpdir -### csplit --prefix="x$USER" -q ${PROBLEMSET_YAML} "/^ *- /" "{*}" -### for file in $(echo "x$USER"*) -### do -### letter=`sed -n -e 's/^ *letter: \([A-Z]\).*/\1/p' < $file` -### short=`sed -n -e 's/.* short-name: \(.*\)$/\1/p' < $file` -### if test -n "$letter" -a -n "$short" -### then -### problet_to_name[$letter]="$short" -### fi -### done -### cd $CURDIR -### rm -r $tmpdir -###} - -Preamble() -{ - echo "Content-type: text/html" - echo "" -} - -Header() -{ - if test ! -e ../${BANNER_FILE} - then - ln -s ${BANNER_IMAGE} ../${BANNER_FILE} - fi -cat << EOF - - - -PC² Judge 1 - - - -

- -

PC2 Judging Results for Judge 1

-
-

-EOF -} - -Trailer() -{ -cat << EOF2 - - -EOF2 -} - -StartTable() -{ - cat << EOF3 -

- - - -EOF3 -} - -EndTable() -{ - cat << EOF4 -
Run IDTeamProblemLanguageTime Judged
-

-EOF4 -} - -TableRow() -{ - dir="$1" - runid=$2 - problet=$3 - shortname=$4 - langid=$5 - teamnum=$6 - runtime=`stat -c '%y' $dir` - probname="" - probdir="" - if test -n ${shortname} - then - probdir=${PC2_CDP}/${shortname} - probstatement=${probdir}/problem_statement/problem.en.tex - if test ! -s ${probstatement} - then - probstatement=${probdir}/problem_statement/problem.tex - fi - if test -s ${probstatement} - then - probname=`head -1 ${probstatement}` - probname=${probname##*{} - probname=${probname%\}} - fi - fi - problem="$problet - $probname (${shortname})" - echo ''"Run $runidteam$teamnum$problem$langid$runtime" -} - -###ParseProblemYaml -Preamble -Header -StartTable -# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid -for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+$' | sort --field-separator=_ +1rn` -do - # exdir looks like: ex_188_Y_compression_46103_cpp - # RId P ProbShort team# Lang - # RId = Run ID - # P = problem letter - # Lang = CLICS Language id - saveIFS="$IFS" - IFS="_" - set ${exdir} - IFS="$saveIFS" - if test $# -eq 6 - then - runid=$2 - problet=$3 - probshort=$4 - teamnum=$5 - langid=$6 - TableRow "${PC2_RUN_DIR}/$exdir" $runid $problet $probshort $langid $teamnum - fi -done -EndTable -Trailer -exit 0 diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh deleted file mode 100644 index c4bfbe7b0..000000000 --- a/support/judge_webcgi/showrun.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/bin/bash -# -# Display very curt textual error message -# -Error() -{ - echo "Content-type: text/plain" - echo "" - echo ERROR $* - echo $now Error: $* >> $LOGFILE -} - -Preamble() -{ - echo "Content-type: text/html" - echo "" -} - -Header() -{ -cat << EOF - - - -PC² Judge 1 - - -

-

PC2 Judging Results for Judge 1

-
-

-EOF -} - -Trailer() -{ -cat << EOF2 - - -EOF2 -} - -StartTable() -{ - cat << EOF3 -

- - - -EOF3 -} - -EndTable() -{ - cat << EOF4 -
Run IDTime Judged
-

-EOF4 -} - -TableRow() -{ - dir="$1" - runid=${dir#../Run} - runtime=`stat -c '%y' $dir` - echo ''"Run $runid$runtime" -} - -Preamble -Header - -# Parse query string into dictionary args -sIFS="$IFS" -IFS='=&' -declare -a parm -parm=($QUERY_STRING) -IFS=$sIFS -declare -A args -for ((i=0; i<${#parm[@]}; i+=2)) -do - args[${parm[i]}]=${parm[i+1]} - echo "${parm[i]} = ${args[${parm[i]}]}
" -done -# Done parsing - -echo '

The run is '${args["run"]} in problem directory ${args["probdir"]}. The execute folder is: ${args["dir"]} -Trailer -exit 0 From a96d1c9d7358b208d67cdb530ef9c5cb4ff1a859 Mon Sep 17 00:00:00 2001 From: John Buck Date: Tue, 2 Jul 2024 07:11:02 -0400 Subject: [PATCH 12/55] i_972 Updated judge help web scripts Also added Warning icon --- support/judge_webcgi/Warning.png | Bin 0 -> 11345 bytes support/judge_webcgi/cgi-bin/judge.sh | 3 + support/judge_webcgi/cgi-bin/pc2common.sh | 57 +++++----- support/judge_webcgi/cgi-bin/showrun.sh | 94 +++++++++++++++++ support/judge_webcgi/judge.sh | 123 ++++++++++++++++++++++ 5 files changed, 253 insertions(+), 24 deletions(-) create mode 100644 support/judge_webcgi/Warning.png create mode 100644 support/judge_webcgi/judge.sh diff --git a/support/judge_webcgi/Warning.png b/support/judge_webcgi/Warning.png new file mode 100644 index 0000000000000000000000000000000000000000..e33a821d9fdac2891f6d08ee75b1c2bb3d61001f GIT binary patch literal 11345 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 VRFJlT1#k=)q$sN@QzvZ}`hRe3YU}_2 literal 0 HcmV?d00001 diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh index 5c9d129a0..c4bbd86ca 100644 --- a/support/judge_webcgi/cgi-bin/judge.sh +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -46,6 +46,9 @@ TableRow() then jstyle="green" jicon='' + elif test "${judgment}" = "CE" + then + jicon='' else jstyle="red" jicon='' diff --git a/support/judge_webcgi/cgi-bin/pc2common.sh b/support/judge_webcgi/cgi-bin/pc2common.sh index 3db5cd6c4..082b1770b 100644 --- a/support/judge_webcgi/cgi-bin/pc2common.sh +++ b/support/judge_webcgi/cgi-bin/pc2common.sh @@ -19,6 +19,11 @@ REJECT_INI=${JUDGE_HOME}/pc2/reject.ini # Where PC2 puts CLICS validator results EXECUTE_DATA_PREFIX=executedata +# Where PC2 puts run output/error +TEST_OUT_PREFIX="teamoutput" +TEST_ERR_PREFIX="teamstderr" +TEST_VALOUT_PREFIX="valout" +TEST_VALERR_PREFIX="valerr" declare -A Judgments @@ -88,7 +93,7 @@ GetLastJudgmentFile() result=`ls $lj_exdir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1rn | head -1` } -GetLastTestCaseNumber() +GetTestCaseNumber() { if test -z "$1" then @@ -106,6 +111,32 @@ GetLastTestCaseNumber() fi } +GetJudgmentFromFile() +{ + exdata="$1" + if test -z "${exdata}" + then + result="No results" + else + # Source the file + . ${exdata} + if test ${compileSuccess} = "false" + then + result="CE" + elif test ${executeSuccess} = "true" + then + if test ${validationSuccess} = "true" + then + MapJudgment "${validationResults}" "${validationReturnCode}" + else + result="JE (Validator error)" + fi + else + result="RTE (Execute error)" + fi + fi +} + GetJudgment() { dir=$1 @@ -120,30 +151,8 @@ GetJudgment() else # We got a real live run # Check out the biggest executedata file -# exdata=`ls ${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sort -t. +1rn | head -1` GetLastJudgmentFile $dir - exdata=${result} - if test -z "${exdata}" - then - result="No results" - else - # Source the file - . ./${exdata} - if test ${compileSuccess} = "false" - then - result="CE" - elif test ${executeSuccess} = "true" - then - if test ${validationSuccess} = "true" - then - MapJudgment "${validationResults}" "${validationReturnCode}" - else - result="JE (Validator error)" - fi - else - result="RTE (Execute error)" - fi - fi + GetJudgmentFromFile ./${result} fi } diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh index 03e31042a..38b3cf7f7 100644 --- a/support/judge_webcgi/cgi-bin/showrun.sh +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -1,8 +1,82 @@ #!/bin/bash +EXE_DIR_LINK=../exedir$$ +PROB_DIR_LINK=../probdir$$ + . ./pc2common.sh . ./webcommon.sh . ./cdpcommon.sh +TableHeader() +{ + cat << EOFTH + +TestDispJudgmentExitCompile TimeExecute TimeVal TimeVal SuccessRun OutRun ErrJudge InJudge AnsVal OutVal Err + +EOFTH +} + +GenFileLink() +{ + tstcase="$2" + tstcase=$((tstcase-1)) + tstfile=$1.$tstcase.txt + tstpath=$dir/$1.$tstcase.txt + if test -s ${tstpath} + then + bytes=`stat -c %s ${tstpath}` + echo ' View ('$bytes' bytes)' + elif test -e ${tstpath} + then + echo ' (Empty)' + else + echo ' Not found' + fi +} + +TableRow() +{ + tc=$1 + judgment="$2" + ec=$3 + comptm="$4" + exetm="$5" + valtm="$6" + if test "$7" = "true" + then + valsucc=Yes + elif test "$7" = "false" + then + valsucc=No + else + valsucc="N/A" + fi + if test "${judgment}" = "AC" + then + jicon='' + elif test "${judgment}" = "CE" + then + jicon='' + else + jicon='' + fi + + echo ' ' + + echo ' '$tc'' + echo ' '$jicon'' + echo ' '$judgment'' + echo ' '$ec'' + echo ' '$comptm'ms' + echo ' '$exetm'ms' + echo ' '$valtm'ms' + echo ' '$valsucc'' + GenFileLink $TEST_OUT_PREFIX $tc + GenFileLink $TEST_ERR_PREFIX $tc + GenFileLink $TEST_VALOUT_PREFIX $tc + GenFileLink $TEST_VALERR_PREFIX $tc + + echo ' ' +} # Parse query string into dictionary args # This is not terribly secure. @@ -21,5 +95,25 @@ done Preamble - 'Run '$run HeaderNoBanner Details for RunId $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" +StartTable +TableHeader +rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} +ln -s ${dir} ${EXE_DIR_LINK} +ln -s ${probdir} ${PROB_DIR_LINK} +for testcase in `ls $dir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1n` +do + GetTestCaseNumber $testcase + tc=$((result+1)) + # This will also source the execute data + GetJudgmentFromFile $dir/$testcase + judgment=$result + ec=$executeExitValue + comptm=$compileTimeMS + exetm=$executeTimeMS + valtm=$validateTimeMS + valsuc=$validationSuccess + TableRow "$tc" "$judgment" "$ec" "$comptm" "$exetm" "$valtm" "$valsuc" +done + Trailer exit 0 diff --git a/support/judge_webcgi/judge.sh b/support/judge_webcgi/judge.sh new file mode 100644 index 000000000..c4bbd86ca --- /dev/null +++ b/support/judge_webcgi/judge.sh @@ -0,0 +1,123 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + +TableHeader() +{ + cat << EOFTH + +Run IDDispJudgmentProblemTeamTest CasesLanguageJudgeTime Judged + +EOFTH +} + +TableRow() +{ + dir="$1" + runid=$2 + problet=$3 + shortname=$4 + langid=$5 + teamnum=$6 + judgment="$7" + runtime="$8" + testinfo="$9" + judge="${10}" + probname="" + probdir="" + if test -n ${shortname} + then + probdir=${PC2_CDP}/${shortname} + probstatement=${probdir}/problem_statement/problem.en.tex + if test ! -s ${probstatement} + then + probstatement=${probdir}/problem_statement/problem.tex + fi + if test -s ${probstatement} + then + probname=`head -1 ${probstatement}` + probname=${probname##*\{} + probname=${probname%\}} + fi + fi + problem="$problet - $probname (${shortname})" + if test "${judgment}" = "AC" + then + jstyle="green" + jicon='' + elif test "${judgment}" = "CE" + then + jicon='' + else + jstyle="red" + jicon='' + fi + echo ' ' + echo ' '"Run $runid" +# echo ' '$judgment"" +# echo '

'$judgment"
" + echo ' '$jicon'' + echo ' '$judgment'' + echo " $problem" + echo " team$teamnum" + echo " $testinfo" + echo " $langid" + echo " $judge" + echo " $runtime" + echo " " +} + +###ParseProblemYaml +Preamble +Header +StartTable +TableHeader + +# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid_judge +for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+' | sort --field-separator=_ +1rn` +do + # exdir looks like: ex_188_Y_compression_46103_cpp + # RId P ProbShort team# Lang + # RId = Run ID + # P = problem letter + # Lang = CLICS Language id + saveIFS="$IFS" + IFS="_" + set ${exdir} + IFS="$saveIFS" + if test $# -ge 6 + then + exedir=${PC2_RUN_DIR}/$exdir + runid=$2 + problet=$3 + probshort=$4 + teamnum=$5 + langid=$6 + judge=$7 + if test -z "${judge}" + then + judge="N/A" + fi + GetJudgment "${exedir}" + judgment="${result}" + runtime="${executeDateTime}" + # Get how many total test cases there are + probdir=${PC2_CDP}/${probshort} + if test -n "${probdir}" + then + GetNumberOfTestCases "${probdir}" + numcases=${result} + else + numcases="??" + fi + # Note that GetJudgment also filled in exdata with the last execute data + GetLastTestCaseNumber "${exdata}" + testcaseinfo=$((result+1))/${numcases} + + TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" + fi +done +EndTable +Trailer +exit 0 From 5d67e763461ed769d817567b7213a8e76810449e Mon Sep 17 00:00:00 2001 From: John Buck Date: Tue, 2 Jul 2024 16:43:06 -0400 Subject: [PATCH 13/55] i_972 More web script updates --- scripts/pc2sandbox.sh | 2 + support/judge_webcgi/judge.sh | 1 + support/judge_webcgi/pc2common.sh | 211 ++++++++++++++++++++++++++++++ support/judge_webcgi/showrun.sh | 209 +++++++++++++++++++++++++++++ support/judge_webcgi/webcommon.sh | 157 ++++++++++++++++++++++ 5 files changed, 580 insertions(+) create mode 100644 support/judge_webcgi/pc2common.sh create mode 100644 support/judge_webcgi/showrun.sh create mode 100644 support/judge_webcgi/webcommon.sh diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh index 08d536e93..f45237d41 100755 --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -379,6 +379,8 @@ REPORT Setting maximum user processes to $MAXPROCS ulimit -u $MAXPROCS # Keep track of details for reports +REPORT_BRIEF ${JUDGEIN} +REPORT_BRIEF ${JUDGEANS} REPORT_BRIEF $cpunum REPORT_BRIEF $$ REPORT_BRIEF $(date "+%F %T.%6N") diff --git a/support/judge_webcgi/judge.sh b/support/judge_webcgi/judge.sh index c4bbd86ca..e1b6bb740 100644 --- a/support/judge_webcgi/judge.sh +++ b/support/judge_webcgi/judge.sh @@ -71,6 +71,7 @@ TableRow() ###ParseProblemYaml Preamble Header +LogButton StartTable TableHeader diff --git a/support/judge_webcgi/pc2common.sh b/support/judge_webcgi/pc2common.sh new file mode 100644 index 000000000..54723de21 --- /dev/null +++ b/support/judge_webcgi/pc2common.sh @@ -0,0 +1,211 @@ +# Meant to be "source'd" into bash scripts. + +# The judge's home directory +JUDGE_HOME=/home/icpc + +# Modify constants as necessary for your installation +PC2_RUN_DIR=${JUDGE_HOME}/pc2 +PC2_CDP=${PC2_RUN_DIR}/current/config + +EXE_LINK_PREFIX=../exedir +PROB_LINK_PREFIX=../probdir + +# How many exedir and probdir links to keep around +NUM_LINK_KEEP=4 + +# Where can we find the contest banner png file for webpage headers +BANNER_FILE=banner.png +BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} + +# Where to the the result of script failure before execution +RESULT_FAILURE_FILE=failure.txt + +# Where judgments are +REJECT_INI=${JUDGE_HOME}/pc2/reject.ini + +# Where PC2 puts CLICS validator results +EXECUTE_DATA_PREFIX=executedata +# Where PC2 puts run output/error +TEST_OUT_PREFIX="teamoutput" +TEST_ERR_PREFIX="teamstderr" +TEST_VALOUT_PREFIX="valout" +TEST_VALERR_PREFIX="valerr" + +# Detailed log of entire judging process +SANDBOX_LOG=sandbox.log + +# Briefcase file prefix +BRIEFCASE_FILE_PREFIX="briefcase" +REPORTS_DIR=reports + +declare -A Judgments + +InitJudgments() +{ + # Defauls, may be overriden by reject.ini + Judgments["accepted"]="AC" + Judgments["Accepted"]="AC" + Judgments["timelimit"]="TLE" + Judgments["run error"]="RTE" + Judgments["compiler error"]="CE" + if test -s ${REJECT_INI} + then + while read j + do + if [[ $j = \#* ]] + then + continue + fi + savIFS="$IFS" + IFS='|' + set $j + IFS="$savIFS" + key="$1" + shortcode="$2" + case ${shortcode} in + AC|CE|RTE|WA|TLE) ;; + MLE) shortcode="RTE (MLE)" ;; + *) shortcode="WA (${shortcode})" ;; + esac +# echo Mapping $key to $shortcode + Judgments[$key]="$shortcode" + done < $REJECT_INI + else echo NO REJECT FILE + fi +} + +# Takes the judgement string, eg. "Wrong answer" as arg 1 and the validation Result (42 or 42) as arg 2 +MapJudgment() +{ + jm="$1" + vr="$2" + result=${Judgments[$jm]} + if test -z ${result} + then + if test ${validationReturnCode} -eq 0 + then + if test ${vr} = "43" + then + resul="WA" + elif test ${vr} = "42" + then + result="AC" + else + result="WA (Default)" + fi + else + result="JE (Validator EC=${validationReturnCode})" + fi + + fi +} + +GetLastJudgmentFile() +{ + lj_exdir="$1" + result=`ls $lj_exdir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1rn | head -1` +} + +GetTestCaseNumber() +{ + if test -z "$1" + then + result=0 + else + saveIFS="$IFS" + IFS=. + set ${1} + result="$2" + IFS="$saveIFS" + if test -z "${result}" + then + result=0 + fi + fi +} + +GetJudgmentFromFile() +{ + exdata="$1" + if test -z "${exdata}" + then + result="No results" + else + # Source the file + . ${exdata} + if test ${compileSuccess} = "false" + then + result="CE" + elif test ${executeSuccess} = "true" + then + if test ${validationSuccess} = "true" + then + MapJudgment "${validationResults}" "${validationReturnCode}" + else + result="JE (Validator error)" + fi + else + result="RTE (Execute error)" + fi + fi +} + +GetJudgment() +{ + dir=$1 + exdata="" + if ! cd ${dir} + then + result="Not found" + elif test -s ${RESULT_FAILURE_FILE} + then + jerr=`cat ${RESULT_FAILURE_FILE}` + result="JE ($jerr)" + else + # We got a real live run + # Check out the biggest executedata file + GetLastJudgmentFile $dir + GetJudgmentFromFile ./${result} + fi +} + +MakeBriefcaseFile() +{ + d="$1" + t="$2" + result=`printf '%s/%s/%s_%03d.log' "$d" "$REPORTS_DIR" "$BRIEFCASE_FILE_PREFIX" "$t"` +} + +# Must redirect from briefcase file +ReadBriefcase() +{ + read judgein + read judgeans + read cpunum + read exepid + read exetime + read execpums cpulimms exewallms mempeak memlim + if [[ $mempeak = [0-9]* ]] + then + mempeak=$((mempeak/(1024*1024))) + fi + if [[ $memlim = [0-9]* ]] + then + memlim=$((memlim/(1024*1024))) + fi +} + +DeleteOldestLinks() +{ + for linkpref in ${EXE_LINK_PREFIX} ${PROB_LINK_PREFIX} + do + dellist=`ls -1td ${linkpref}* | sed 1,${NUM_LINK_KEEP}d` + if test -n "${dellist}" + then + rm -f ${dellist} + fi + done +} + +InitJudgments + diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh new file mode 100644 index 000000000..e88937dea --- /dev/null +++ b/support/judge_webcgi/showrun.sh @@ -0,0 +1,209 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + +EXE_DIR_LINK=${EXE_LINK_PREFIX}$$ +PROB_DIR_LINK=${PROB_LINK_PREFIX}$$ + +# Provide a way to look at the sandbox log +LogButton() +{ + # Read first judgment file to get compile time - it's compiled once for all + GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" + if test -n ${result} -a -n ${compileTimeMS} + then + cat << LBEOF0 +
+

The program took ${compileTimeMS}ms to compile. +

+LBEOF0 + fi + + # Read the first briefcase file (if any) for limits + MakeBriefcaseFile ${EXE_DIR_LINK} 1 + ReadBriefcase < $result + if test -n ${cpulimms} + then + cpulimms=${cpulimms%%.*} + cpusecs="$((cpulimms/1000))" + if test ${cpusecs} != "1" + then + cpusecs="${cpusecs} seconds" + else + cpusecs="1 second" + fi + cat << LBEOF1 +
+

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms). +

+LBEOF1 + fi + if test -n ${memlim} + then + if test ${memlim} = "0" + then + memlim="Unlimited" + else + memlim=${memlim}MB + fi + cat << LBEOF1A +
+

The Memory limit for this problem is ${memlim}. +

+LBEOF1A + fi + sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} + if test -s ${sandlog} + then + cat << LBEOF2 +Click here for the full sandbox log for this run +

+

+LBEOF2 + fi +} + +TableHeader() +{ + cat << EOFTH + +TestDispJudgmentExitExecute TimeVal TimeVal SuccessRun OutRun ErrJudge InJudge AnsVal OutVal Err + +EOFTH +} + +GenFileLink() +{ + tstcase="$2" + tstcase=$((tstcase-1)) + tstfile=$1.$tstcase.txt + tstpath=$dir/$1.$tstcase.txt + if test -s ${tstpath} + then + bytes=`stat -c %s ${tstpath}` + echo ' View ('$bytes' bytes)' + elif test -e ${tstpath} + then + echo ' (Empty)' + else + echo ' Not found' + fi +} + +GenFileLinkWithText() +{ + linkaddr="$1" + linktext="$2" + linkcolor="$3" + bytes=`stat -c %s ${linkaddr}` + echo ' '$linktext' ('$bytes' bytes)' +} + +TableRow() +{ + tc=$1 + judgment="$2" + ec=$3 + exetm="$4" + valtm="$5" + jin="$6" + jans="$7" + if test "$8" = "true" + then + valsucc=Yes + elif test "$8" = "false" + then + valsucc=No + else + valsucc="N/A" + fi + # Strip the stuff of before sample (or secret) + jin=${jin##*/data/} + jans=${jans##*/data/} + # Just the basenames for link text + jinbase=${jin##*/} + jansbase=${jans##*/} + if [[ $jin = sample/* ]] + then + lcolor=#00a0a0 + else + lcolor=#00a000 + fi + if test "${judgment}" = "AC" + then + jicon='' + elif test "${judgment}" = "CE" + then + jicon='' + else + jicon='' + fi + + echo ' ' + + echo ' '$tc'' + echo ' '$jicon'' + echo ' '$judgment'' + echo ' '$ec'' + echo ' '$exetm'ms' + echo ' '$valtm'ms' + echo ' '$valsucc'' + GenFileLink $TEST_OUT_PREFIX $tc + GenFileLink $TEST_ERR_PREFIX $tc + GenFileLinkWithText $PROB_DIR_LINK/data/"$jin" "$jinbase" $lcolor + GenFileLinkWithText $PROB_DIR_LINK/data/"$jans" "$jansbase" $lcolor + GenFileLink $TEST_VALOUT_PREFIX $tc + GenFileLink $TEST_VALERR_PREFIX $tc + + echo ' ' +} + +# Parse query string into dictionary args +# This is not terribly secure. +declare -a parm +sIFS="$IFS" +IFS='=&' +parm=($QUERY_STRING) +IFS=$sIFS +for ((i=0; i<${#parm[@]}; i+=2)) +do + a=${parm[i]} + eval $a=${parm[i+1]} +done +# Done parsing + +DeleteOldestLinks +Preamble - 'Run '$run +HeaderNoBanner Details for Run Id $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" + +# Create links apache can access in our html folder +#rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} +ln -s ${dir} ${EXE_DIR_LINK} +ln -s ${probdir} ${PROB_DIR_LINK} + +LogButton + +StartTable +TableHeader +for testcase in `ls $dir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1n` +do + GetTestCaseNumber $testcase + tc=$((result+1)) + # This will also source the execute data + GetJudgmentFromFile $dir/$testcase + judgment=$result + ec=$executeExitValue +# comptm=$compileTimeMS + exetm=$executeTimeMS + valtm=$validateTimeMS + valsuc=$validationSuccess + MakeBriefcaseFile "$dir" "$tc" + ReadBriefcase < ${result} + TableRow "$tc" "$judgment" "$ec" "$exetm" "$valtm" "$judgein" "$judgeans" "$valsuc" +done + +Trailer + +#rm -f ${EXE_DIR_LINK} ${PROB_DIR_LINK} +exit 0 diff --git a/support/judge_webcgi/webcommon.sh b/support/judge_webcgi/webcommon.sh new file mode 100644 index 000000000..26c65cd62 --- /dev/null +++ b/support/judge_webcgi/webcommon.sh @@ -0,0 +1,157 @@ +# File meant to be "source'd" to support generating web pages + +# +# Display very curt textual error message +# +Error() +{ + echo "Content-type: text/plain" + echo "" + echo ERROR $* +} + + +Preamble() +{ + echo "Content-type: text/html" + echo "" + if test $# -eq 0 + then + headmsg="Judge" + else + headmsg="$*" + fi + cat << PREEOF + + + +PC² $headmsg + + + +PREEOF +} + +Header() +{ + # Make sure link to banner page is in a place apache can read + if test ! -e ../${BANNER_FILE} + then + ln -s ${BANNER_IMAGE} ../${BANNER_FILE} + fi +cat << HDREOF +

+ +

PC2 Judging Results

+
+

+HDREOF +} + +HeaderNoBanner() +{ + if test $# -eq 0 + then + hdrmsg="Judging Results" + else + hdrmsg="$*" + fi +cat << EOF +

+

PC2 $hdrmsg

+
+

+EOF +} + +Trailer() +{ +cat << EOF2 + + +EOF2 +} + +StartTable() +{ + cat << EOF3 +

+ +EOF3 +} + +EndTable() +{ + cat << EOF4 +
+

+EOF4 +} + From c827535af2f4db420a25e2dec2410d3c59685ac5 Mon Sep 17 00:00:00 2001 From: John Buck Date: Sat, 6 Jul 2024 17:57:47 -0400 Subject: [PATCH 14/55] i_972 Update scripts for more detail Allow display of sandbox cpu time. Allow display of reports/testcase_xxx.log by link Make all links open a new tab as opposed to overwriting the current one. Add memory used column since it is now supported in latest kernel. --- scripts/pc2sandbox.sh | 30 +-- support/judge_webcgi/cgi-bin/judge.sh | 1 + support/judge_webcgi/cgi-bin/pc2common.sh | 63 +++++++ support/judge_webcgi/cgi-bin/showrun.sh | 168 +++++++++++++++-- support/judge_webcgi/cgi-bin/webcommon.sh | 30 +++ support/judge_webcgi/judge.sh | 124 ------------- support/judge_webcgi/pc2common.sh | 211 ---------------------- support/judge_webcgi/showrun.sh | 209 --------------------- support/judge_webcgi/webcommon.sh | 157 ---------------- 9 files changed, 259 insertions(+), 734 deletions(-) delete mode 100644 support/judge_webcgi/judge.sh delete mode 100644 support/judge_webcgi/pc2common.sh delete mode 100644 support/judge_webcgi/showrun.sh delete mode 100644 support/judge_webcgi/webcommon.sh diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh index f45237d41..950bcf432 100755 --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -268,12 +268,15 @@ shift shift shift shift +shift DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" -DEBUG echo -e "\n$0" ${MEMLIMIT} ${TIMELIMIT} xxx xxx $* "< ${JUDGEIN} > $TESTCASE.ans" +RUN_LOCAL_CMD="$0 ${MEMLIMIT} ${TIMELIMIT} xxx xxx ${COMMAND} $* < ${JUDGEIN} > $TESTCASE.ans" +DIFF_OUTPUT_CMD="diff -w ${JUDGEANS} $TESTCASE.ans | more" +DEBUG echo -e "\n${RUN_LOCAL_CMD}" DEBUG echo -e "\nor, without the sandbox by using the following command:" -DEBUG echo -e "\n$* < ${JUDGEIN} > $TESTCASE.ans" +DEBUG echo -e "\n${COMMAND} $* < ${JUDGEIN} > $TESTCASE.ans" DEBUG echo -e "\nAnd compare with the judge's answer:" -DEBUG echo -e "\ndiff -w ${JUDGEANS} $TESTCASE.ans | more\n" +DEBUG echo -e "\n${DIFF_OUTPUT_CMD}\n" #### Debugging - just set expected first args to: 8MB 2seconds ###MEMLIMIT=8 @@ -351,31 +354,33 @@ mkdir -p "$REPORTDIR" REPORTFILE=`printf "$REPORTDIR/testcase_%03d.log" $TESTCASE` BRIEFREPORTFILE=`printf "$REPORTDIR/briefcase_%03d.log" $TESTCASE` DEBUG echo Report file: ${REPORTFILE} Brief Report File: ${BRIEFREORTFILE} +REPORT Test case $TESTCASE: +REPORT Command: "${RUN_LOCAL_CMD}" +REPORT Diff: " ${DIFF_OUTPUT_CMD}" # set the specified memory limit - input is in MB, cgroup v2 requires bytes, so multiply by 1M # but only if > 0. # "max" means unlimited, which is the cgroup v2 default DEBUG echo checking memory limit if [ "$MEMLIMIT" -gt "0" ] ; then - DEBUG echo setting memory limit to $MEMLIMIT MB + REPORT_DEBUG echo Setting memory limit to $MEMLIMIT MB echo $(( $MEMLIMIT * 1024 * 1024 )) > $PC2_SANDBOX_CGROUP_PATH/memory.max echo 1 > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max else - DEBUG echo setting memory limit to max, meaning no limit + REPORT_DEBUG echo Setting memory limit to max, meaning no limit echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.max echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max fi -REPORT Test case $TESTCASE: # We use ulimit to limit CPU time, not cgroups. Time is supplied in seconds. This may have to # be reworked if ms accuracy is needed. The problem is, cgroups do not kill off a process that # exceeds the time limit, ulimit does. TIMELIMIT_US=$((TIMELIMIT * 1000000)) -REPORT Setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" +REPORT_DEBUG Setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" ulimit -t $TIMELIMIT MAXPROCS=$((MAXPROCS+`ps -T -u $USER | wc -l`)) -REPORT Setting maximum user processes to $MAXPROCS +REPORT_DEBUG Setting maximum user processes to $MAXPROCS ulimit -u $MAXPROCS # Keep track of details for reports @@ -398,12 +403,7 @@ then fi # run the command -# the following are the cgroup-tools V1 commands; need to find cgroup-tools v2 commands -# echo Using cgexec to run $COMMAND $* -# cgexec -g cpu,memory:/pc2 $COMMAND $* - -# since we don't know how to use cgroup-tools to execute, just execute it directly (it's a child so it -# should still fall under the cgroup limits). +# execute it directly (it's a child so it should still fall under the cgroup limits). REPORT_DEBUG Executing "setsid taskset $CPUMASK $COMMAND $*" # Set up trap handler to catch wall-clock time exceeded and getting killed by PC2's execute timer @@ -443,6 +443,8 @@ else fi ShowStats ${cputime} ${TIMELIMIT_US} ${walltime} ${peakmem} $((MEMLIMIT*1024*1024)) +REPORT_DEBUG The command exited with code: ${COMMAND_EXIT_CODE} + if test "$kills" != "0" then REPORT_DEBUG The command was killed because it exceeded the memory limit diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh index c4bbd86ca..e1b6bb740 100644 --- a/support/judge_webcgi/cgi-bin/judge.sh +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -71,6 +71,7 @@ TableRow() ###ParseProblemYaml Preamble Header +LogButton StartTable TableHeader diff --git a/support/judge_webcgi/cgi-bin/pc2common.sh b/support/judge_webcgi/cgi-bin/pc2common.sh index 082b1770b..eac62c936 100644 --- a/support/judge_webcgi/cgi-bin/pc2common.sh +++ b/support/judge_webcgi/cgi-bin/pc2common.sh @@ -7,6 +7,12 @@ JUDGE_HOME=/home/icpc PC2_RUN_DIR=${JUDGE_HOME}/pc2 PC2_CDP=${PC2_RUN_DIR}/current/config +EXE_LINK_PREFIX=../exedir +PROB_LINK_PREFIX=../probdir + +# How many exedir and probdir links to keep around +NUM_LINK_KEEP=4 + # Where can we find the contest banner png file for webpage headers BANNER_FILE=banner.png BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} @@ -25,6 +31,15 @@ TEST_ERR_PREFIX="teamstderr" TEST_VALOUT_PREFIX="valout" TEST_VALERR_PREFIX="valerr" +# Detailed log of entire judging process +SANDBOX_LOG=sandbox.log + +# Testcase file prefix +TESTCASE_REPORT_FILE_PREFIX="testcase" +# Briefcase file prefix +BRIEFCASE_FILE_PREFIX="briefcase" +REPORTS_DIR=reports + declare -A Judgments InitJudgments() @@ -156,5 +171,53 @@ GetJudgment() fi } +MakeTestcaseFile() +{ + d="$1" + t="$2" + result=`printf '%s/%s/%s_%03d.log' "$d" "$REPORTS_DIR" "$TESTCASE_REPORT_FILE_PREFIX" "$t"` +} + +MakeBriefcaseFile() +{ + d="$1" + t="$2" + result=`printf '%s/%s/%s_%03d.log' "$d" "$REPORTS_DIR" "$BRIEFCASE_FILE_PREFIX" "$t"` +} + +# Must redirect from briefcase file +ReadBriefcase() +{ + read judgein + read judgeans + read cpunum + read exepid + read exetime + read execpums cpulimms exewallms mempeakbytes memlimbytes + mempeak=$mempeakbytes + memlim=$memlimbytes + # Calculate Mib from bytes, round upto next Mib + if [[ $mempeak = [0-9]* ]] + then + mempeak=$(((mempeak+(1024*1024)-1)/(1024*1024))) + fi + if [[ $memlim = [0-9]* ]] + then + memlim=$(((memlim+(1024*1024)-1)/(1024*1024))) + fi +} + +DeleteOldestLinks() +{ + for linkpref in ${EXE_LINK_PREFIX} ${PROB_LINK_PREFIX} + do + dellist=`ls -1td ${linkpref}* | sed 1,${NUM_LINK_KEEP}d` + if test -n "${dellist}" + then + rm -f ${dellist} + fi + done +} + InitJudgments diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh index 38b3cf7f7..87a848d00 100644 --- a/support/judge_webcgi/cgi-bin/showrun.sh +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -1,16 +1,87 @@ #!/bin/bash -EXE_DIR_LINK=../exedir$$ -PROB_DIR_LINK=../probdir$$ - . ./pc2common.sh . ./webcommon.sh . ./cdpcommon.sh +EXE_DIR_LINK=${EXE_LINK_PREFIX}$$ +PROB_DIR_LINK=${PROB_LINK_PREFIX}$$ + +# Provide a way to look at the sandbox log +LogButton() +{ + # Read first judgment file to get compile time - it's compiled once for all + GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" + if test -n ${result} -a -n ${compileTimeMS} + then + cat << LBEOF0 +

+

The program took ${compileTimeMS}ms to compile. +

+LBEOF0 + fi + + # Read the first briefcase file (if any) for limits + MakeBriefcaseFile ${EXE_DIR_LINK} 1 + ReadBriefcase < $result + if test -n ${cpulimms} + then + cpulimms=${cpulimms%%.*} + cpusecs="$((cpulimms/1000))" + if test ${cpusecs} != "1" + then + cpusecs="${cpusecs} seconds" + else + cpusecs="1 second" + fi + cat << LBEOF1 +
+

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms). +

+LBEOF1 + fi + if test -n ${memlim} + then + if test ${memlim} = "0" + then + memlim="Unlimited" + else + memlim=${memlim}MiB + fi + cat << LBEOF1A +
+

The Memory limit for this problem is ${memlim}. +

+LBEOF1A + fi + sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} + if test -s ${sandlog} + then + cat << LBEOF2 +
Click here for the full sandbox log for this run +

+

+LBEOF2 + fi +} + TableHeader() { cat << EOFTH -TestDispJudgmentExitCompile TimeExecute TimeVal TimeVal SuccessRun OutRun ErrJudge InJudge AnsVal OutVal Err + Test + Disp + Judgment + Exit + Execute Time + MiB Used + Val Time + Val Success + Run Out + Run Err + Judge In + Judge Ans + Val Out + Val Err EOFTH } @@ -24,7 +95,7 @@ GenFileLink() if test -s ${tstpath} then bytes=`stat -c %s ${tstpath}` - echo ' View ('$bytes' bytes)' + echo ' View ('$bytes' bytes)' elif test -e ${tstpath} then echo ' (Empty)' @@ -33,23 +104,56 @@ GenFileLink() fi } +GenFileLinkWithText() +{ + linkaddr="$1" + linktext="$2" + linkcolor="$3" + bytes=`stat -c %s ${linkaddr}` + echo ' '$linktext' ('$bytes' bytes)' +} + TableRow() { tc=$1 judgment="$2" ec=$3 - comptm="$4" - exetm="$5" - valtm="$6" - if test "$7" = "true" + exetm="$4" + valtm="$5" + jin="$6" + jans="$7" + if test "$8" = "true" then valsucc=Yes - elif test "$7" = "false" + elif test "$8" = "false" then valsucc=No else valsucc="N/A" fi + memused="$9" + memusedbytes="${10}" + exesandms="${11}" + + # Create link to report/testcase file for testcase number + MakeTestcaseFile ${EXE_DIR_LINK} ${tc} + tcreport="${result}" + if test ! -s "${tcreport}" + then + tcreport="" + fi + # Strip the stuff of before sample (or secret) + jin=${jin##*/data/} + jans=${jans##*/data/} + # Just the basenames for link text + jinbase=${jin##*/} + jansbase=${jans##*/} + if [[ $jin = sample/* ]] + then + lcolor=#00a0a0 + else + lcolor=#00a000 + fi if test "${judgment}" = "AC" then jicon='' @@ -62,16 +166,33 @@ TableRow() echo ' ' - echo ' '$tc'' + if test -n ${tcreport} + then + echo ' '$tc'' + else + echo ' '$tc'' + fi echo ' '$jicon'' echo ' '$judgment'' echo ' '$ec'' - echo ' '$comptm'ms' - echo ' '$exetm'ms' + if [[ ${exesandms} = [0-9]* ]] + then + echo '

'$exetm'ms'$exesandms'ms in the Sandbox
' + else + echo ' '$exetm'ms' + fi + if [[ ${memusedbytes} = [0-9]* ]] + then + echo '
'$memused'MiB'$memusedbytes' bytes
' + else + echo ' '$memused'' + fi echo ' '$valtm'ms' echo ' '$valsucc'' GenFileLink $TEST_OUT_PREFIX $tc GenFileLink $TEST_ERR_PREFIX $tc + GenFileLinkWithText $PROB_DIR_LINK/data/"$jin" "$jinbase" $lcolor + GenFileLinkWithText $PROB_DIR_LINK/data/"$jans" "$jansbase" $lcolor GenFileLink $TEST_VALOUT_PREFIX $tc GenFileLink $TEST_VALERR_PREFIX $tc @@ -92,14 +213,19 @@ do done # Done parsing +DeleteOldestLinks Preamble - 'Run '$run -HeaderNoBanner Details for RunId $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" +HeaderNoBanner Details for Run Id $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" -StartTable -TableHeader -rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} +# Create links apache can access in our html folder +#rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} ln -s ${dir} ${EXE_DIR_LINK} ln -s ${probdir} ${PROB_DIR_LINK} + +LogButton + +StartTable +TableHeader for testcase in `ls $dir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1n` do GetTestCaseNumber $testcase @@ -108,12 +234,16 @@ do GetJudgmentFromFile $dir/$testcase judgment=$result ec=$executeExitValue - comptm=$compileTimeMS +# comptm=$compileTimeMS exetm=$executeTimeMS valtm=$validateTimeMS valsuc=$validationSuccess - TableRow "$tc" "$judgment" "$ec" "$comptm" "$exetm" "$valtm" "$valsuc" + MakeBriefcaseFile "$dir" "$tc" + ReadBriefcase < ${result} + TableRow "$tc" "$judgment" "$ec" "$exetm" "$valtm" "$judgein" "$judgeans" "$valsuc" "$mempeak" "$mempeakbytes" "$execpums" done Trailer + +#rm -f ${EXE_DIR_LINK} ${PROB_DIR_LINK} exit 0 diff --git a/support/judge_webcgi/cgi-bin/webcommon.sh b/support/judge_webcgi/cgi-bin/webcommon.sh index fef94f341..03fddd8a3 100644 --- a/support/judge_webcgi/cgi-bin/webcommon.sh +++ b/support/judge_webcgi/cgi-bin/webcommon.sh @@ -1,4 +1,7 @@ # File meant to be "source'd" to support generating web pages +TOOLTIP_UL_COLOR="blue" +TOOLTIP_TEXT_COLOR="white" +TOOLTIP_BG_COLOR="black" # # Display very curt textual error message @@ -27,6 +30,9 @@ Preamble() PC² $headmsg diff --git a/support/judge_webcgi/judge.sh b/support/judge_webcgi/judge.sh deleted file mode 100644 index e1b6bb740..000000000 --- a/support/judge_webcgi/judge.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/bin/bash -. ./pc2common.sh -. ./webcommon.sh -. ./cdpcommon.sh - -TableHeader() -{ - cat << EOFTH - -Run IDDispJudgmentProblemTeamTest CasesLanguageJudgeTime Judged - -EOFTH -} - -TableRow() -{ - dir="$1" - runid=$2 - problet=$3 - shortname=$4 - langid=$5 - teamnum=$6 - judgment="$7" - runtime="$8" - testinfo="$9" - judge="${10}" - probname="" - probdir="" - if test -n ${shortname} - then - probdir=${PC2_CDP}/${shortname} - probstatement=${probdir}/problem_statement/problem.en.tex - if test ! -s ${probstatement} - then - probstatement=${probdir}/problem_statement/problem.tex - fi - if test -s ${probstatement} - then - probname=`head -1 ${probstatement}` - probname=${probname##*\{} - probname=${probname%\}} - fi - fi - problem="$problet - $probname (${shortname})" - if test "${judgment}" = "AC" - then - jstyle="green" - jicon='' - elif test "${judgment}" = "CE" - then - jicon='' - else - jstyle="red" - jicon='' - fi - echo ' ' - echo ' '"Run $runid" -# echo ' '$judgment"" -# echo '
'$judgment"
" - echo ' '$jicon'' - echo ' '$judgment'' - echo " $problem" - echo " team$teamnum" - echo " $testinfo" - echo " $langid" - echo " $judge" - echo " $runtime" - echo " " -} - -###ParseProblemYaml -Preamble -Header -LogButton -StartTable -TableHeader - -# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid_judge -for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+' | sort --field-separator=_ +1rn` -do - # exdir looks like: ex_188_Y_compression_46103_cpp - # RId P ProbShort team# Lang - # RId = Run ID - # P = problem letter - # Lang = CLICS Language id - saveIFS="$IFS" - IFS="_" - set ${exdir} - IFS="$saveIFS" - if test $# -ge 6 - then - exedir=${PC2_RUN_DIR}/$exdir - runid=$2 - problet=$3 - probshort=$4 - teamnum=$5 - langid=$6 - judge=$7 - if test -z "${judge}" - then - judge="N/A" - fi - GetJudgment "${exedir}" - judgment="${result}" - runtime="${executeDateTime}" - # Get how many total test cases there are - probdir=${PC2_CDP}/${probshort} - if test -n "${probdir}" - then - GetNumberOfTestCases "${probdir}" - numcases=${result} - else - numcases="??" - fi - # Note that GetJudgment also filled in exdata with the last execute data - GetLastTestCaseNumber "${exdata}" - testcaseinfo=$((result+1))/${numcases} - - TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" - fi -done -EndTable -Trailer -exit 0 diff --git a/support/judge_webcgi/pc2common.sh b/support/judge_webcgi/pc2common.sh deleted file mode 100644 index 54723de21..000000000 --- a/support/judge_webcgi/pc2common.sh +++ /dev/null @@ -1,211 +0,0 @@ -# Meant to be "source'd" into bash scripts. - -# The judge's home directory -JUDGE_HOME=/home/icpc - -# Modify constants as necessary for your installation -PC2_RUN_DIR=${JUDGE_HOME}/pc2 -PC2_CDP=${PC2_RUN_DIR}/current/config - -EXE_LINK_PREFIX=../exedir -PROB_LINK_PREFIX=../probdir - -# How many exedir and probdir links to keep around -NUM_LINK_KEEP=4 - -# Where can we find the contest banner png file for webpage headers -BANNER_FILE=banner.png -BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} - -# Where to the the result of script failure before execution -RESULT_FAILURE_FILE=failure.txt - -# Where judgments are -REJECT_INI=${JUDGE_HOME}/pc2/reject.ini - -# Where PC2 puts CLICS validator results -EXECUTE_DATA_PREFIX=executedata -# Where PC2 puts run output/error -TEST_OUT_PREFIX="teamoutput" -TEST_ERR_PREFIX="teamstderr" -TEST_VALOUT_PREFIX="valout" -TEST_VALERR_PREFIX="valerr" - -# Detailed log of entire judging process -SANDBOX_LOG=sandbox.log - -# Briefcase file prefix -BRIEFCASE_FILE_PREFIX="briefcase" -REPORTS_DIR=reports - -declare -A Judgments - -InitJudgments() -{ - # Defauls, may be overriden by reject.ini - Judgments["accepted"]="AC" - Judgments["Accepted"]="AC" - Judgments["timelimit"]="TLE" - Judgments["run error"]="RTE" - Judgments["compiler error"]="CE" - if test -s ${REJECT_INI} - then - while read j - do - if [[ $j = \#* ]] - then - continue - fi - savIFS="$IFS" - IFS='|' - set $j - IFS="$savIFS" - key="$1" - shortcode="$2" - case ${shortcode} in - AC|CE|RTE|WA|TLE) ;; - MLE) shortcode="RTE (MLE)" ;; - *) shortcode="WA (${shortcode})" ;; - esac -# echo Mapping $key to $shortcode - Judgments[$key]="$shortcode" - done < $REJECT_INI - else echo NO REJECT FILE - fi -} - -# Takes the judgement string, eg. "Wrong answer" as arg 1 and the validation Result (42 or 42) as arg 2 -MapJudgment() -{ - jm="$1" - vr="$2" - result=${Judgments[$jm]} - if test -z ${result} - then - if test ${validationReturnCode} -eq 0 - then - if test ${vr} = "43" - then - resul="WA" - elif test ${vr} = "42" - then - result="AC" - else - result="WA (Default)" - fi - else - result="JE (Validator EC=${validationReturnCode})" - fi - - fi -} - -GetLastJudgmentFile() -{ - lj_exdir="$1" - result=`ls $lj_exdir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1rn | head -1` -} - -GetTestCaseNumber() -{ - if test -z "$1" - then - result=0 - else - saveIFS="$IFS" - IFS=. - set ${1} - result="$2" - IFS="$saveIFS" - if test -z "${result}" - then - result=0 - fi - fi -} - -GetJudgmentFromFile() -{ - exdata="$1" - if test -z "${exdata}" - then - result="No results" - else - # Source the file - . ${exdata} - if test ${compileSuccess} = "false" - then - result="CE" - elif test ${executeSuccess} = "true" - then - if test ${validationSuccess} = "true" - then - MapJudgment "${validationResults}" "${validationReturnCode}" - else - result="JE (Validator error)" - fi - else - result="RTE (Execute error)" - fi - fi -} - -GetJudgment() -{ - dir=$1 - exdata="" - if ! cd ${dir} - then - result="Not found" - elif test -s ${RESULT_FAILURE_FILE} - then - jerr=`cat ${RESULT_FAILURE_FILE}` - result="JE ($jerr)" - else - # We got a real live run - # Check out the biggest executedata file - GetLastJudgmentFile $dir - GetJudgmentFromFile ./${result} - fi -} - -MakeBriefcaseFile() -{ - d="$1" - t="$2" - result=`printf '%s/%s/%s_%03d.log' "$d" "$REPORTS_DIR" "$BRIEFCASE_FILE_PREFIX" "$t"` -} - -# Must redirect from briefcase file -ReadBriefcase() -{ - read judgein - read judgeans - read cpunum - read exepid - read exetime - read execpums cpulimms exewallms mempeak memlim - if [[ $mempeak = [0-9]* ]] - then - mempeak=$((mempeak/(1024*1024))) - fi - if [[ $memlim = [0-9]* ]] - then - memlim=$((memlim/(1024*1024))) - fi -} - -DeleteOldestLinks() -{ - for linkpref in ${EXE_LINK_PREFIX} ${PROB_LINK_PREFIX} - do - dellist=`ls -1td ${linkpref}* | sed 1,${NUM_LINK_KEEP}d` - if test -n "${dellist}" - then - rm -f ${dellist} - fi - done -} - -InitJudgments - diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh deleted file mode 100644 index e88937dea..000000000 --- a/support/judge_webcgi/showrun.sh +++ /dev/null @@ -1,209 +0,0 @@ -#!/bin/bash -. ./pc2common.sh -. ./webcommon.sh -. ./cdpcommon.sh - -EXE_DIR_LINK=${EXE_LINK_PREFIX}$$ -PROB_DIR_LINK=${PROB_LINK_PREFIX}$$ - -# Provide a way to look at the sandbox log -LogButton() -{ - # Read first judgment file to get compile time - it's compiled once for all - GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" - if test -n ${result} -a -n ${compileTimeMS} - then - cat << LBEOF0 -
-

The program took ${compileTimeMS}ms to compile. -

-LBEOF0 - fi - - # Read the first briefcase file (if any) for limits - MakeBriefcaseFile ${EXE_DIR_LINK} 1 - ReadBriefcase < $result - if test -n ${cpulimms} - then - cpulimms=${cpulimms%%.*} - cpusecs="$((cpulimms/1000))" - if test ${cpusecs} != "1" - then - cpusecs="${cpusecs} seconds" - else - cpusecs="1 second" - fi - cat << LBEOF1 -
-

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms). -

-LBEOF1 - fi - if test -n ${memlim} - then - if test ${memlim} = "0" - then - memlim="Unlimited" - else - memlim=${memlim}MB - fi - cat << LBEOF1A -
-

The Memory limit for this problem is ${memlim}. -

-LBEOF1A - fi - sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} - if test -s ${sandlog} - then - cat << LBEOF2 -Click here for the full sandbox log for this run -

-

-LBEOF2 - fi -} - -TableHeader() -{ - cat << EOFTH - -TestDispJudgmentExitExecute TimeVal TimeVal SuccessRun OutRun ErrJudge InJudge AnsVal OutVal Err - -EOFTH -} - -GenFileLink() -{ - tstcase="$2" - tstcase=$((tstcase-1)) - tstfile=$1.$tstcase.txt - tstpath=$dir/$1.$tstcase.txt - if test -s ${tstpath} - then - bytes=`stat -c %s ${tstpath}` - echo ' View ('$bytes' bytes)' - elif test -e ${tstpath} - then - echo ' (Empty)' - else - echo ' Not found' - fi -} - -GenFileLinkWithText() -{ - linkaddr="$1" - linktext="$2" - linkcolor="$3" - bytes=`stat -c %s ${linkaddr}` - echo ' '$linktext' ('$bytes' bytes)' -} - -TableRow() -{ - tc=$1 - judgment="$2" - ec=$3 - exetm="$4" - valtm="$5" - jin="$6" - jans="$7" - if test "$8" = "true" - then - valsucc=Yes - elif test "$8" = "false" - then - valsucc=No - else - valsucc="N/A" - fi - # Strip the stuff of before sample (or secret) - jin=${jin##*/data/} - jans=${jans##*/data/} - # Just the basenames for link text - jinbase=${jin##*/} - jansbase=${jans##*/} - if [[ $jin = sample/* ]] - then - lcolor=#00a0a0 - else - lcolor=#00a000 - fi - if test "${judgment}" = "AC" - then - jicon='' - elif test "${judgment}" = "CE" - then - jicon='' - else - jicon='' - fi - - echo ' ' - - echo ' '$tc'' - echo ' '$jicon'' - echo ' '$judgment'' - echo ' '$ec'' - echo ' '$exetm'ms' - echo ' '$valtm'ms' - echo ' '$valsucc'' - GenFileLink $TEST_OUT_PREFIX $tc - GenFileLink $TEST_ERR_PREFIX $tc - GenFileLinkWithText $PROB_DIR_LINK/data/"$jin" "$jinbase" $lcolor - GenFileLinkWithText $PROB_DIR_LINK/data/"$jans" "$jansbase" $lcolor - GenFileLink $TEST_VALOUT_PREFIX $tc - GenFileLink $TEST_VALERR_PREFIX $tc - - echo ' ' -} - -# Parse query string into dictionary args -# This is not terribly secure. -declare -a parm -sIFS="$IFS" -IFS='=&' -parm=($QUERY_STRING) -IFS=$sIFS -for ((i=0; i<${#parm[@]}; i+=2)) -do - a=${parm[i]} - eval $a=${parm[i+1]} -done -# Done parsing - -DeleteOldestLinks -Preamble - 'Run '$run -HeaderNoBanner Details for Run Id $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" - -# Create links apache can access in our html folder -#rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} -ln -s ${dir} ${EXE_DIR_LINK} -ln -s ${probdir} ${PROB_DIR_LINK} - -LogButton - -StartTable -TableHeader -for testcase in `ls $dir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1n` -do - GetTestCaseNumber $testcase - tc=$((result+1)) - # This will also source the execute data - GetJudgmentFromFile $dir/$testcase - judgment=$result - ec=$executeExitValue -# comptm=$compileTimeMS - exetm=$executeTimeMS - valtm=$validateTimeMS - valsuc=$validationSuccess - MakeBriefcaseFile "$dir" "$tc" - ReadBriefcase < ${result} - TableRow "$tc" "$judgment" "$ec" "$exetm" "$valtm" "$judgein" "$judgeans" "$valsuc" -done - -Trailer - -#rm -f ${EXE_DIR_LINK} ${PROB_DIR_LINK} -exit 0 diff --git a/support/judge_webcgi/webcommon.sh b/support/judge_webcgi/webcommon.sh deleted file mode 100644 index 26c65cd62..000000000 --- a/support/judge_webcgi/webcommon.sh +++ /dev/null @@ -1,157 +0,0 @@ -# File meant to be "source'd" to support generating web pages - -# -# Display very curt textual error message -# -Error() -{ - echo "Content-type: text/plain" - echo "" - echo ERROR $* -} - - -Preamble() -{ - echo "Content-type: text/html" - echo "" - if test $# -eq 0 - then - headmsg="Judge" - else - headmsg="$*" - fi - cat << PREEOF - - - -PC² $headmsg - - - -PREEOF -} - -Header() -{ - # Make sure link to banner page is in a place apache can read - if test ! -e ../${BANNER_FILE} - then - ln -s ${BANNER_IMAGE} ../${BANNER_FILE} - fi -cat << HDREOF -

- -

PC2 Judging Results

-
-

-HDREOF -} - -HeaderNoBanner() -{ - if test $# -eq 0 - then - hdrmsg="Judging Results" - else - hdrmsg="$*" - fi -cat << EOF -

-

PC2 $hdrmsg

-
-

-EOF -} - -Trailer() -{ -cat << EOF2 - - -EOF2 -} - -StartTable() -{ - cat << EOF3 -

- -EOF3 -} - -EndTable() -{ - cat << EOF4 -
-

-EOF4 -} - From 4547ed087943bcba68b101b92054f7a7aa464d20 Mon Sep 17 00:00:00 2001 From: John Buck Date: Sun, 7 Jul 2024 14:15:08 -0400 Subject: [PATCH 15/55] i_972 Clean up scripts Interactive sandbox script needed cleaning up and updating. Fix some bash 'if' statements in case arg is empty string. --- scripts/pc2sandbox.sh | 6 +- scripts/pc2sandbox_interactive.sh | 132 +++++++++++++++++----- support/judge_webcgi/cgi-bin/judge.sh | 6 +- support/judge_webcgi/cgi-bin/pc2common.sh | 18 +-- support/judge_webcgi/cgi-bin/showrun.sh | 34 ++++-- 5 files changed, 146 insertions(+), 50 deletions(-) diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh index 950bcf432..9007e4072 100755 --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -244,9 +244,9 @@ if [ "$#" -lt 1 ] ; then exit $FAIL_NO_ARGS_EXIT_CODE fi -if [ "$#" -lt 3 ] ; then - echo $0: expected 3 or more arguments, found: $* - SysFailure Expected 3 or more arguments to $0, found: $* +if [ "$#" -lt 6 ] ; then + echo $0: expected 6 or more arguments, found: $* + SysFailure Expected 6 or more arguments to $0, found: $* exit $FAIL_INSUFFICIENT_ARGS_EXIT_CODE fi diff --git a/scripts/pc2sandbox_interactive.sh b/scripts/pc2sandbox_interactive.sh index d37f66a07..4c45ab5f7 100755 --- a/scripts/pc2sandbox_interactive.sh +++ b/scripts/pc2sandbox_interactive.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (C) 1989-2023 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. +# Copyright (C) 1989-2024 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. # # File: pc2sandbox_interacive.sh # Purpose: a sandbox for pc2 using Linux CGroups v2 and supporting interactive problems @@ -19,6 +19,10 @@ DEFAULT_CPU_NUM=3 CPU_OVERRIDE_FILE=$HOME/pc2_cpu_override +# Where to the the result of the first failure. If this file is not created, then the +# run was accepted. (correct) +RESULT_FAILURE_FILE=failure.txt + # FAIL_RETCODE_BASE is 128 + 64 + xx # 128 = system error, like signal # 64 = biggest used signal @@ -130,6 +134,13 @@ function REPORT() echo "$@" >> $REPORTFILE fi } +function REPORT_BRIEF() +{ + if test -n "$BRIEFREPORTFILE" + then + echo "$@" >> $BRIEFREPORTFILE + fi +} # For per-testcase report and debugging both function REPORT_DEBUG() @@ -163,24 +174,24 @@ SAGE # Function to kill all processes in the cgroupv2 after process has exited KillcgroupProcs() { - if test -n ${PC2_SANDBOX_CGROUP_PATH_KILL} - then + if test -n ${PC2_SANDBOX_CGROUP_PATH_KILL} + then DEBUG echo "Purging cgroup ${PC2_SANDBOX_CGROUP_PATH_KILL} of processes" - echo 1 > ${PC2_SANDBOX_CGROUP_PATH_KILL} - fi + echo 1 > ${PC2_SANDBOX_CGROUP_PATH_KILL} + fi } # Kill active children and stragglers KillChildProcs() { - DEBUG echo "Killing off submission process group $submissionpid and all children" - # Kill off process group - if test -n "$submissionpid" - then - pkill -9 -s $submissionpid - fi - # and... extra stragglers with us as a parent - pkill -9 -P $$ + DEBUG echo "Killing off submission process group $submissionpid and all children" + # Kill off process group + if test -n "$submissionpid" + then + pkill -9 -s $submissionpid + fi + # and... extra stragglers with us as a parent + pkill -9 -P $$ } # Kill off the validator if it is still running @@ -226,6 +237,10 @@ ShowStats() walltime=$3 memused=$4 memlim=$5 + REPORT_BRIEF "$(printf '%d.%03d' $((cpuused / 1000)) $((cpuused % 1000)))" \ + "$(printf '%d.%03d' $((cpulim / 1000)) $((cpulim % 1000)))" \ + "$(printf '%d.%03d' $((walltime / 1000)) $((walltime % 1000)))" \ + ${memused} $((memlim)) REPORT_DEBUG Resources used for this run: REPORT_DEBUG "$(printf ' CPU ms Limit ms Wall ms Memory Used Memory Limit\n')" REPORT_DEBUG "$(printf '%5d.%03d %5d.%03d %6d.%03d %12s %12d\n' $((cpuused / 1000)) $((cpuused % 1000)) \ @@ -234,6 +249,27 @@ ShowStats() ${memused} $((memlim)))" } +# Generate a system failure result file. This will be the FIRST failure as it will not overwrite the file +# if it exists. +SysFailure() +{ + if test ! -s ${RESULT_FAILURE_FILE} + then + (echo system; echo $* ) > ${RESULT_FAILURE_FILE} + fi +} + +# Generate a failure result file. This will be the FIRST failure as it will not overwrite the file +# if it exists. +Failure() +{ + if test ! -s ${RESULT_FAILURE_FILE} + then + echo $* > ${RESULT_FAILURE_FILE} + fi +} + + sent_xml=0 GenXML() @@ -253,6 +289,7 @@ EOF if [ "$#" -lt 1 ] ; then echo $0: Missing command line arguments. Try '"'"$0 --help"'"' for help. + SysFailure No command line arguments to $0 exit $FAIL_NO_ARGS_EXIT_CODE fi @@ -263,6 +300,7 @@ fi if [ "$#" -lt 7 ] ; then echo $0: expected 7 or more arguments, found: $* + SysFailure Expected 7 or more arguments to $0, found: $* exit $FAIL_INSUFFICIENT_ARGS_EXIT_CODE fi @@ -277,6 +315,19 @@ DEBUG echo "+---------------- Test Case ${TESTCASE} ----------------+" DEBUG echo Command line: $0 $* shift 7 +DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" +RUN_LOCAL_CMD="$0 ${MEMLIMIT} ${TIMELIMIT} ${VALIDATOR} ${JUDGEIN} ${JUDGEANS} ${TESTCASE} ${COMMAND} $*" +tcfile=`printf "$REPORTDIR/testcase_%03d.log" $TESTCASE` +REPORT_OUTPUT_CMD="more ${tcfile}" +DEBUG echo -e "\n${RUN_LOCAL_CMD}" +#DEBUG echo -e "\nor, without the sandbox by using the following command:" +#DEBUG echo -e "\n${COMMAND} $* < ${JUDGEIN} > $TESTCASE.ans" +DEBUG echo -e "\nAnd see the run report using the following command:" +DEBUG echo -e "\n${REPORT_OUTPUT_CMD}\n" + +# Skip used arguments +shift 7 + # the rest of the commmand line arguments are the command args for the submission @@ -284,42 +335,49 @@ shift 7 DEBUG echo checking PC2 CGroup V2 installation... if [ ! -d "$PC2_CGROUP_PATH" ]; then - echo $0: expected pc2sandbox CGroups v2 installation in $PC2_CGROUP_PATH + echo $0: expected pc2sandbox CGroups v2 installation in $PC2_CGROUP_PATH + SysFailure CGroups v2 not installed in $PC2_CGROUP_PATH exit $FAIL_INVALID_CGROUP_INSTALLATION fi if [ ! -f "$CGROUP_PATH/cgroup.controllers" ]; then echo $0: missing file cgroup.controllers in $CGROUP_PATH + SysFailure Missing cgroup.controllers in $CGROUP_PATH exit $FAIL_MISSING_CGROUP_CONTROLLERS_FILE fi if [ ! -f "$CGROUP_PATH/cgroup.subtree_control" ]; then echo $0: missing file cgroup.subtree_control in $CGROUP_PATH + SysFailure Missing cgroup.subtree_controll in $CGORUP_PATH exit $FAIL_MISSING_CGROUP_SUBTREE_CONTROL_FILE fi # make sure the cpu and memory controllers are enabled if ! grep -q -F "cpu" "$CGROUP_PATH/cgroup.subtree_control"; then echo $0: cgroup.subtree_control in $CGROUP_PATH does not enable cpu controller + SysFailure CPU controller not enabled in cgroup.subtree_control in $CGROUP_PATH exit $FAIL_CPU_CONTROLLER_NOT_ENABLED fi if ! grep -q -F "memory" "$CGROUP_PATH/cgroup.subtree_control"; then echo $0: cgroup.subtree_control in $CGROUP_PATH does not enable memory controller + SysFailure Memory controller not enabled in cgroup.subtree_control in $CGROUP_PATH exit $FAIL_MEMORY_CONTROLLER_NOT_ENABLED fi if [ ! -e "$VALIDATOR" ]; then echo $0: The interactive validator \'"$VALIDATOR"\' was not found + SysFailure The interactive validator \'"$VALIDATOR"\' was not found exit $FAIL_INTERACTIVE_ERROR fi if [ ! -x "$VALIDATOR" ]; then echo $0: The interactive validator \'"$VALIDATOR"\' is not an executable file + SysFailure The interactive validator \'"$VALIDATOR"\' is not an executable file exit $FAIL_INTERACTIVE_ERROR fi -# we seem to have a valid CGroup installation +# we seem to have a valid CGroup and validator setup DEBUG echo ...done. if test -d $PC2_SANDBOX_CGROUP_PATH @@ -329,6 +387,7 @@ then if ! rmdir $PC2_SANDBOX_CGROUP_PATH then DEBUG echo Cannot purge old sandbox: $PC2_SANDBOX_CGROUP_PATH + SysFailure Cannot purge old sandbox: $PC2_SANDBOX_CGROUP_PATH exit $FAIL_SANDBOX_ERROR fi fi @@ -337,6 +396,7 @@ DEBUG echo Creating sandbox $PC2_SANDBOX_CGROUP_PATH if ! mkdir $PC2_SANDBOX_CGROUP_PATH then DEBUG echo Cannot create $PC2_SANDBOX_CGROUP_PATH + SysFailure Cannot create sandbox: $PC2_SANDBOX_CGROUP_PATH exit $FAIL_INVALID_CGROUP_INSTALLATION fi @@ -344,12 +404,14 @@ fi rm -f "$INFIFO" "$OUTFIFO" if ! mkfifo --mode=$FIFOMODE $INFIFO then - DEBUG Can not create FIFO $INFIFO 1>&2 + DEBUG Can not create input FIFO $INFIFO 1>&2 + SysFailure Cannot create input FIFO: $INFIFO exit $FAIL_INTERACTIVE_ERROR fi if ! mkfifo --mode=$FIFOMODE $OUTFIFO then - DEBUG Can not create FIFO $INFIFO 1>&2 + DEBUG Can not create output FIFO $OUTFIFO 1>&2 + SysFailure Cannot create output FIFO: $OUTFIFO rm $INFIFO exit $FAIL_INTERACTIVE_ERROR fi @@ -359,17 +421,22 @@ mkdir -p "$REPORTDIR" # Set report file to be testcase specific one now REPORTFILE=`printf "$REPORTDIR/testcase_%03d.log" $TESTCASE` +BRIEFREPORTFILE=`printf "$REPORTDIR/briefcase_%03d.log" $TESTCASE` +DEBUG echo Report file: ${REPORTFILE} Brief Report File: ${BRIEFREORTFILE} +REPORT Test case $TESTCASE: +REPORT Command: "${RUN_LOCAL_CMD}" +REPORT Report: " ${REPORT_OUTPUT_CMD}" # set the specified memory limit - input is in MB, cgroup v2 requires bytes, so multiply by 1M # but only if > 0. # "max" means unlimited, which is the cgroup v2 default DEBUG echo checking memory limit if [ "$MEMLIMIT" -gt "0" ] ; then - REPORT Setting memory limit to $MEMLIMIT MB + REPORT_DEBUG Setting memory limit to $MEMLIMIT MiB echo $(( $MEMLIMIT * 1024 * 1024 )) > $PC2_SANDBOX_CGROUP_PATH/memory.max echo 1 > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max else - REPORT Setting memory limit to max, meaning no limit + REPORT_DEBUG Setting memory limit to max, meaning no limit echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.max echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max fi @@ -384,22 +451,29 @@ mkdir -p "$INT_FEEDBACKDIR" # Note that starting of the validator will block until the submission is started since # it will block on the $INFIFO (no one else connected yet) -REPORT Starting: "$VALIDATOR $JUDGEIN $JUDGEANS $INT_FEEDBACKDIR > $INFIFO < $OUTFIFO &" +REPORT_DEBUG Starting: "$VALIDATOR $JUDGEIN $JUDGEANS $INT_FEEDBACKDIR > $INFIFO < $OUTFIFO &" $VALIDATOR $JUDGEIN $JUDGEANS $INT_FEEDBACKDIR > $INFIFO < $OUTFIFO & intv_pid=$! -REPORT Started interactive validator PID $intv_pid +REPORT_DEBUG Started interactive validator PID $intv_pid # We use ulimit to limit CPU time, not cgroups. Time is supplied in seconds. This may have to # be reworked if ms accuracy is needed. The problem is, cgroups do not kill off a process that # exceeds the time limit, ulimit does. TIMELIMIT_US=$((TIMELIMIT * 1000000)) -REPORT Setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" +REPORT_DEBUG Setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" ulimit -t $TIMELIMIT MAXPROCS=$((MAXPROCS+`ps -T -u $USER | wc -l`)) -REPORT Setting maximum user processes to $MAXPROCS +REPORT_DEBUG Setting maximum user processes to $MAXPROCS ulimit -u $MAXPROCS +# Keep track of details for reports +REPORT_BRIEF ${JUDGEIN} +REPORT_BRIEF ${JUDGEANS} +REPORT_BRIEF $cpunum +REPORT_BRIEF $$ +REPORT_BRIEF $(date "+%F %T.%6N") + # Remember wall time when we started starttime=`GetTimeInMicros` @@ -408,6 +482,7 @@ REPORT Putting $$ into $PC2_SANDBOX_CGROUP_PATH cgroup if ! echo $$ > $PC2_SANDBOX_CGROUP_PATH/cgroup.procs then echo $0: Could not add current process to $PC2_SANDBOX_CGROUP_PATH/cgroup.procs - not executing submission. + SysFailure Could not add current process to $PC2_SANDBOX_CGROUP_PATH/cgroup.procs rm -f "$INFIFO" "$OUTFIFO" exit $FAIL_SANDBOX_ERROR fi @@ -435,19 +510,19 @@ do # A return code 127 indicates there are no more children. How did that happen? if test $wstat -eq 127 then - REPORT No more children found while waiting: Submission PID was $submissionpid and Interactive Validator PID was $intv_pid + REPORT_DEBUG No more children found while waiting: Submission PID was $submissionpid and Interactive Validator PID was $intv_pid break fi # If interactive validator finishes if test "$child_pid" -eq "$intv_pid" then - REPORT Validator finishes with status $wstat + REPORT_DEBUG Validator finishes with exit code $wstat if test "$contestant_done" -eq 0 then # Only kill it if it still exists if test -d /proc/$contestantpid then - REPORT Contestant PID $submissionpid has not finished - killing it + REPORT_DEBUG Contestant PID $submissionpid has not finished - killing it # TODO: We should kill and wait for it here and print out the stats fi # This just determines if the program ran, not if it's correct. @@ -493,7 +568,7 @@ do fi - REPORT Contestant PID $submissionpid finished with status $wstat + REPORT_DEBUG Contestant PID $submissionpid finished with exit code $wstat contestant_done=1 COMMAND_EXIT_CODE=$wstat @@ -505,6 +580,7 @@ do if test "$kills" != "0" then REPORT_DEBUG The command was killed because it exceeded the memory limit + REPORT_BRIEF MLE COMMAND_EXIT_CODE=${FAIL_MEMORY_LIMIT_EXCEEDED} GenXML "No - Memory limit exceeded" "" KillValidator @@ -514,6 +590,7 @@ do if test "$COMMAND_EXIT_CODE" -eq 137 -o "$cputime" -gt "$TIMELIMIT_US" then REPORT_DEBUG The command was killed because it exceeded the CPU Time limit + REPORT_BRIEF TLE COMMAND_EXIT_CODE=${FAIL_TIME_LIMIT_EXCEEDED} GenXML "No - Time limit exceeded" "${cputime}us > ${TIMELIMIT_US}us" KillValidator @@ -531,6 +608,7 @@ do if test "$wstat" -ne 0 then REPORT_DEBUG Contestant finished abnormally - killing validator + REPORT_BRIEF RTE KillValidator GenXML "No - Run-time Error" "Exit status $wstat" break diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh index e1b6bb740..c3b121849 100644 --- a/support/judge_webcgi/cgi-bin/judge.sh +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -26,15 +26,15 @@ TableRow() judge="${10}" probname="" probdir="" - if test -n ${shortname} + if test -n "${shortname}" then probdir=${PC2_CDP}/${shortname} probstatement=${probdir}/problem_statement/problem.en.tex - if test ! -s ${probstatement} + if test ! -s "{probstatement}" then probstatement=${probdir}/problem_statement/problem.tex fi - if test -s ${probstatement} + if test -s "${probstatement}" then probname=`head -1 ${probstatement}` probname=${probname##*\{} diff --git a/support/judge_webcgi/cgi-bin/pc2common.sh b/support/judge_webcgi/cgi-bin/pc2common.sh index eac62c936..d9b6e2aed 100644 --- a/support/judge_webcgi/cgi-bin/pc2common.sh +++ b/support/judge_webcgi/cgi-bin/pc2common.sh @@ -50,7 +50,7 @@ InitJudgments() Judgments["timelimit"]="TLE" Judgments["run error"]="RTE" Judgments["compiler error"]="CE" - if test -s ${REJECT_INI} + if test -s "${REJECT_INI}" then while read j do @@ -82,14 +82,14 @@ MapJudgment() jm="$1" vr="$2" result=${Judgments[$jm]} - if test -z ${result} + if test -z "${result}" then - if test ${validationReturnCode} -eq 0 + if test "${validationReturnCode}" -eq 0 then - if test ${vr} = "43" + if test "${vr}" = "43" then resul="WA" - elif test ${vr} = "42" + elif test "${vr}" = "42" then result="AC" else @@ -135,12 +135,12 @@ GetJudgmentFromFile() else # Source the file . ${exdata} - if test ${compileSuccess} = "false" + if test "${compileSuccess}" = "false" then result="CE" - elif test ${executeSuccess} = "true" + elif test "${executeSuccess}" = "true" then - if test ${validationSuccess} = "true" + if test "${validationSuccess}" = "true" then MapJudgment "${validationResults}" "${validationReturnCode}" else @@ -159,7 +159,7 @@ GetJudgment() if ! cd ${dir} then result="Not found" - elif test -s ${RESULT_FAILURE_FILE} + elif test -s "${RESULT_FAILURE_FILE}" then jerr=`cat ${RESULT_FAILURE_FILE}` result="JE ($jerr)" diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh index 87a848d00..4c1e5b2c1 100644 --- a/support/judge_webcgi/cgi-bin/showrun.sh +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -11,7 +11,7 @@ LogButton() { # Read first judgment file to get compile time - it's compiled once for all GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" - if test -n ${result} -a -n ${compileTimeMS} + if test -n "${result}" -a -n "${compileTimeMS}" then cat << LBEOF0

@@ -22,8 +22,14 @@ LBEOF0 # Read the first briefcase file (if any) for limits MakeBriefcaseFile ${EXE_DIR_LINK} 1 - ReadBriefcase < $result - if test -n ${cpulimms} + if test -s "${result}" + then + ReadBriefcase < $result + else + cpulimms="" + memlim="" + fi + if test -n "${cpulimms}" then cpulimms=${cpulimms%%.*} cpusecs="$((cpulimms/1000))" @@ -38,8 +44,14 @@ LBEOF0

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms).

LBEOF1 + else + cat << LBEOF1AA +
+

The CPU Limit for this problem is N/A. +

+LBEOF1AA fi - if test -n ${memlim} + if test -n "${memlim}" then if test ${memlim} = "0" then @@ -52,9 +64,15 @@ LBEOF1

The Memory limit for this problem is ${memlim}. LBEOF1A + else + cat << LBEOF1AAA +

+

The Memory limit for this problem is N/A. +

+LBEOF1AAA fi sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} - if test -s ${sandlog} + if test -s "${sandlog}" then cat << LBEOF2
Click here for the full sandbox log for this run @@ -92,11 +110,11 @@ GenFileLink() tstcase=$((tstcase-1)) tstfile=$1.$tstcase.txt tstpath=$dir/$1.$tstcase.txt - if test -s ${tstpath} + if test -s "${tstpath}" then bytes=`stat -c %s ${tstpath}` echo ' View ('$bytes' bytes)' - elif test -e ${tstpath} + elif test -e "${tstpath}" then echo ' (Empty)' else @@ -166,7 +184,7 @@ TableRow() echo ' ' - if test -n ${tcreport} + if test -n "${tcreport}" then echo ' '$tc'' else From 3901a43ec452acc67175c6e3245d9805c12a10ce Mon Sep 17 00:00:00 2001 From: John Buck Date: Sun, 7 Jul 2024 17:20:45 -0400 Subject: [PATCH 16/55] i_972 More judge web page fixes Remove extra space when generating the execute data file. Correct the "whats this" message for the judge execute folder pattern. Add jquery for sorting tables. --- scripts/pc2sandbox.sh | 12 ++-- .../csus/ecs/pc2/core/execute/Executable.java | 2 +- .../ecs/pc2/ui/ContestInformationPane.java | 2 +- support/judge_webcgi/cgi-bin/judge.sh | 28 +++++++++- support/judge_webcgi/cgi-bin/showrun.sh | 3 + support/judge_webcgi/cgi-bin/webcommon.sh | 56 ++++++++++++++++++- .../scripts/jquery-3.7.1.slim.min.js | 2 + 7 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 support/judge_webcgi/scripts/jquery-3.7.1.slim.min.js diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh index 9007e4072..fe3de06a4 100755 --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -263,12 +263,8 @@ TESTCASE=$5 COMMAND=$6 DEBUG echo "+---------------- Test Case ${TESTCASE} ----------------+" DEBUG echo Command line: $0 $* -shift -shift -shift -shift -shift -shift +shift 6 + DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" RUN_LOCAL_CMD="$0 ${MEMLIMIT} ${TIMELIMIT} xxx xxx ${COMMAND} $* < ${JUDGEIN} > $TESTCASE.ans" DIFF_OUTPUT_CMD="diff -w ${JUDGEANS} $TESTCASE.ans | more" @@ -363,11 +359,11 @@ REPORT Diff: " ${DIFF_OUTPUT_CMD}" # "max" means unlimited, which is the cgroup v2 default DEBUG echo checking memory limit if [ "$MEMLIMIT" -gt "0" ] ; then - REPORT_DEBUG echo Setting memory limit to $MEMLIMIT MB + REPORT_DEBUG Setting memory limit to $MEMLIMIT MiB echo $(( $MEMLIMIT * 1024 * 1024 )) > $PC2_SANDBOX_CGROUP_PATH/memory.max echo 1 > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max else - REPORT_DEBUG echo Setting memory limit to max, meaning no limit + REPORT_DEBUG Setting memory limit to max, meaning no limit echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.max echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max fi diff --git a/src/edu/csus/ecs/pc2/core/execute/Executable.java b/src/edu/csus/ecs/pc2/core/execute/Executable.java index ad93794c9..89a273895 100644 --- a/src/edu/csus/ecs/pc2/core/execute/Executable.java +++ b/src/edu/csus/ecs/pc2/core/execute/Executable.java @@ -3770,7 +3770,7 @@ private boolean saveExecuteData(int dataSetNumber) executeDataWriter.println("compileResultCode='" + executionData.getCompileResultCode() + "'"); executeDataWriter.println("executeExitValue='" + executionData.getExecuteExitValue() + "'"); executeDataWriter.println("executeSuccess='" + executionData.isExecuteSucess() + "'"); - executeDataWriter.println("validationReturnCode ='" + executionData.getValidationReturnCode() + "'"); + executeDataWriter.println("validationReturnCode='" + executionData.getValidationReturnCode() + "'"); executeDataWriter.println("validationSuccess='" + executionData.isValidationSuccess() + "'"); executeDataWriter.println("validationResults='" + showNullAsEmpty(executionData.getValidationResults()) + "'"); executeDataWriter.println("compileTimeMS='" + executionData.getCompileTimeMS() + "'"); diff --git a/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java b/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java index ed3742985..0832e475e 100644 --- a/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java +++ b/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java @@ -1461,7 +1461,7 @@ public void mousePressed(MouseEvent e) { + "\n\nAny substitution string available for an executable is allowed here." - + "\n\nSo for example a judge's execute folder string like \"executesite{:siteid}judge{:clientname}_Run_{:runnumber}\" would change the execute folder to something like:" // + + "\n\nSo for example a judge's execute folder string like \"executesite{:siteid}{:clientname}_Run_{:runnumber}\" would change the execute folder to something like:" // + "\n executesite1judge1_Run_220 " // + "\n\nSubstitution values depend on the corresponding data having been loaded into the PC^2 Server; if there is no value defined for a" // diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh index c3b121849..f33a23a86 100644 --- a/support/judge_webcgi/cgi-bin/judge.sh +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -3,15 +3,33 @@ . ./webcommon.sh . ./cdpcommon.sh + TableHeader() { cat << EOFTH -Run IDDispJudgmentProblemTeamTest CasesLanguageJudgeTime Judged +Run ID +Disp +Judgment +Problem +Team +Test Cases +Language +Judge +Time Judged EOFTH } +MyTableStyles() +{ + cat << EOFMYSTYLES +th { + cursor: pointer; +} +EOFMYSTYLES +} + TableRow() { dir="$1" @@ -70,8 +88,11 @@ TableRow() ###ParseProblemYaml Preamble +Styles +MyTableStyles +EndStyles +StartHTMLDoc Header -LogButton StartTable TableHeader @@ -113,12 +134,13 @@ do numcases="??" fi # Note that GetJudgment also filled in exdata with the last execute data - GetLastTestCaseNumber "${exdata}" + GetTestCaseNumber "${exdata##./}" testcaseinfo=$((result+1))/${numcases} TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" fi done EndTable +TableSortScripts Trailer exit 0 diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh index 4c1e5b2c1..97c0c3391 100644 --- a/support/judge_webcgi/cgi-bin/showrun.sh +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -233,6 +233,9 @@ done DeleteOldestLinks Preamble - 'Run '$run +Styles +EndStyles +StartHTMLDoc HeaderNoBanner Details for Run Id $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" # Create links apache can access in our html folder diff --git a/support/judge_webcgi/cgi-bin/webcommon.sh b/support/judge_webcgi/cgi-bin/webcommon.sh index 03fddd8a3..2e513343f 100644 --- a/support/judge_webcgi/cgi-bin/webcommon.sh +++ b/support/judge_webcgi/cgi-bin/webcommon.sh @@ -29,6 +29,12 @@ Preamble() PC² $headmsg +PREEOF +} + +Styles() +{ + cat << EOFSTYLE +EndStyles() +{ + echo "" +} + +StartHTMLDoc() +{ + cat << EOFSHTML -PREEOF +EOFSHTML +} + +TableSortScripts() +{ + cat << EOFSCR + + +EOFSCR } Header() diff --git a/support/judge_webcgi/scripts/jquery-3.7.1.slim.min.js b/support/judge_webcgi/scripts/jquery-3.7.1.slim.min.js new file mode 100644 index 000000000..35906b929 --- /dev/null +++ b/support/judge_webcgi/scripts/jquery-3.7.1.slim.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},m=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||m).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),b=new RegExp(ge+"|>"),A=new RegExp(g),D=new RegExp("^"+t+"$"),N={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+d),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},L=/^(?:input|select|textarea|button)$/i,j=/^h\d$/i,O=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,P=/[+~]/,H=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),q=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},R=function(){V()},M=K(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{E.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){E={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,d=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==d&&9!==d&&11!==d)return n;if(!r&&(V(e),e=e||C,T)){if(11!==d&&(u=O.exec(t)))if(i=u[1]){if(9===d){if(!(a=e.getElementById(i)))return n;if(a.id===i)return E.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return E.call(n,a),n}else{if(u[2])return E.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return E.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||p&&p.test(t))){if(c=t,f=e,1===d&&(b.test(t)||m.test(t))){(f=P.test(t)&&X(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=k)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+G(l[o]);c=l.join(",")}try{return E.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>x.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function B(e){return e[k]=!0,e}function F(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function $(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&M(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function U(a){return B(function(o){return o=+o,B(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function X(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=C&&9===n.nodeType&&n.documentElement&&(r=(C=n).documentElement,T=!ce.isXMLDoc(C),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=C&&(t=C.defaultView)&&t.top!==t&&t.addEventListener("unload",R),le.getById=F(function(e){return r.appendChild(e).id=ce.expando,!C.getElementsByName||!C.getElementsByName(ce.expando).length}),le.disconnectedMatch=F(function(e){return i.call(e,"*")}),le.scope=F(function(){return C.querySelectorAll(":scope")}),le.cssHas=F(function(){try{return C.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(x.filter.ID=function(e){var t=e.replace(H,q);return function(e){return e.getAttribute("id")===t}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&T){var n=t.getElementById(e);return n?[n]:[]}}):(x.filter.ID=function(e){var n=e.replace(H,q);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&T){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),x.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},x.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&T)return t.getElementsByClassName(e)},p=[],F(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||p.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+k+"-]").length||p.push("~="),e.querySelectorAll("a#"+k+"+*").length||p.push(".#.+[+~]"),e.querySelectorAll(":checked").length||p.push(":checked"),(t=C.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&p.push(":enabled",":disabled"),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||p.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||p.push(":has"),p=p.length&&new RegExp(p.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===C||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),C}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),T&&!h[t+" "]&&(!p||!p.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(H,q),e[3]=(e[3]||e[4]||e[5]||"").replace(H,q),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return N.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&A.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(H,q).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||E,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:k.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:m,!0)),C.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=m.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,E=ce(m);var S=/^(?:parents|prev(?:Until|All))/,A={children:!0,contents:!0,next:!0,prev:!0};function D(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;re=m.createDocumentFragment().appendChild(m.createElement("div")),(be=m.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),re.appendChild(be),le.checkClone=re.cloneNode(!0).cloneNode(!0).lastChild.checked,re.innerHTML="",le.noCloneChecked=!!re.cloneNode(!0).lastChild.defaultValue,re.innerHTML="",le.option=!!re.lastChild;var Te={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Ee(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function ke(e,t){for(var n=0,r=e.length;n",""]);var Se=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),d=[],p=0,h=e.length;p\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Me(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Ie(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function We(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n
",2===yt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=m.implementation.createHTMLDocument("")).createElement("base")).href=m.location.href,t.head.appendChild(r)):t=m),o=!n&&[],(i=C.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||K})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return R(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Qe(le.pixelPosition,function(e,t){if(t)return t=Ve(e,n),$e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return R(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0 Date: Sun, 14 Jul 2024 15:05:25 -0400 Subject: [PATCH 17/55] i_972 Added scripts to pull from all judges The main web server machine will now pull all submissions from each judge and show them on one page. This is done using a python script that reads a json result from each judge. Added link to display compiler results on compile error Added link for source code of submission. Change Run Out and Run err to Run stdout and Run stderr --- scripts/pc2sandbox_interactive.sh | 202 +++++++++++++++--- support/judge_webcgi/cgi-bin/HtmlFunctions.py | 68 ++++++ support/judge_webcgi/cgi-bin/alljudgments.py | 84 ++++++++ support/judge_webcgi/cgi-bin/getjudgments.sh | 109 ++++++++++ support/judge_webcgi/cgi-bin/pc2common.sh | 25 +++ support/judge_webcgi/cgi-bin/showrun.sh | 133 +++++++++--- support/judge_webcgi/css/judgestyles.css | 94 ++++++++ support/judge_webcgi/scripts/tablesort.js | 28 +++ 8 files changed, 687 insertions(+), 56 deletions(-) create mode 100644 support/judge_webcgi/cgi-bin/HtmlFunctions.py create mode 100644 support/judge_webcgi/cgi-bin/alljudgments.py create mode 100644 support/judge_webcgi/cgi-bin/getjudgments.sh create mode 100644 support/judge_webcgi/css/judgestyles.css create mode 100644 support/judge_webcgi/scripts/tablesort.js diff --git a/scripts/pc2sandbox_interactive.sh b/scripts/pc2sandbox_interactive.sh index 4c45ab5f7..070db2857 100755 --- a/scripts/pc2sandbox_interactive.sh +++ b/scripts/pc2sandbox_interactive.sh @@ -15,6 +15,19 @@ # # Author: John Buck, based on earlier versions by John Clevenger and Doug Lane +# KILL_WA_VALIDATOR may be set to 1 to kill off a submission if the validator finishes first with +# a WA judgment. In this case, we don't care what the submission does. This matches how DOMjudge +# handles this case. Setting this to 0 will always make it wait for the contestant submission to +# finish, and use whatever judgement is appropriate. If contestant finishes with 0, then we use the +# vaildator result, else we use the exit code of the submission. +KILL_WA_VALIDATOR=1 +# If WAIT_FOR_SUB_ON_AC is 1, we wait for the submission if the validator says "AC". We then +# use the exit code of the submission if it is non-zero to determine disposition, otherise, +# we return AC +# If WAIT_FOR_SUB_ON_AC is 0, we will kill off the submission as soon as we get an AC from the +# validator if the submission is not complete yet. +WAIT_FOR_SUB_ON_AC=1 + # CPU to run submission on. This is 0 based, so 3 means the 4th CPU DEFAULT_CPU_NUM=3 CPU_OVERRIDE_FILE=$HOME/pc2_cpu_override @@ -285,6 +298,59 @@ EOF sent_xml=1 } +# Dont complain to me - this is the order they appear in ./x86_64-linux-gnu/bits/signum-generic.h +declare -A signal_names=( + ["2"]="SIGINT" + ["4"]="SIGILL" + ["6"]="SIGABRT" + ["8"]="SIGFPE" + ["11"]="SIGSEGV" + ["15"]="SIGTERM" + ["1"]="SIGHUP" + ["3"]="SIGQUIT" + ["5"]="SIGTRAP" + ["9"]="SIGKILL" + ["13"]="SIGPIPE" + ["14"]="SIGALRM" +) + +declare -A fail_codes=( + ["$FAIL_EXIT_CODE"]="FAIL_EXIT_CODE" + ["$FAIL_NO_ARGS_EXIT_CODE"]="FAIL_NO_ARGS_EXIT_CODE" + ["$FAIL_INSUFFICIENT_ARGS_EXIT_CODE"]="FAIL_INSUFFICIENT_ARGS_EXIT_CODE" + ["$FAIL_INVALID_CGROUP_INSTALLATION"]="FAIL_INVALID_CGROUP_INSTALLATION" + ["$FAIL_MISSING_CGROUP_CONTROLLERS_FILE"]="FAIL_MISSING_CGROUP_CONTROLLERS_FILE" + ["$FAIL_MISSING_CGROUP_SUBTREE_CONTROL_FILE"]="FAIL_MISSING_CGROUP_SUBTREE_CONTROL_FILE" + ["$FAIL_CPU_CONTROLLER_NOT_ENABLED"]="FAIL_CPU_CONTROLLER_NOT_ENABLED" + ["$FAIL_MEMORY_CONTROLLER_NOT_ENABLED"]="FAIL_MEMORY_CONTROLLER_NOT_ENABLED" + ["$FAIL_MEMORY_LIMIT_EXCEEDED"]="FAIL_MEMORY_LIMIT_EXCEEDED" + ["$FAIL_TIME_LIMIT_EXCEEDED"]="FAIL_TIME_LIMIT_EXCEEDED" + ["$FAIL_WALL_TIME_LIMIT_EXCEEDED"]="FAIL_WALL_TIME_LIMIT_EXCEEDED" + ["$FAIL_SANDBOX_ERROR"]="FAIL_SANDBOX_ERROR" + ["$FAIL_INTERACTIVE_ERROR"]="FAIL_INTERACTIVE_ERROR" +) +FormatExitCode() +{ + result="$1" + if [[ "$result" = [0-9]* ]] + then + failerr=${fail_codes[$result]} + if test -n "$failerr" + then + result="$result ($failerr)" + elif test "$result" -gt 128 -a "$result" -lt 192 + then + sig=$((result-128)) + signame=${signal_names[$sig]} + if test -n "$signame" + then + signame="$signame " + fi + result="$result (Signal $signame$((result-128)))" + fi + fi +} + # ------------------------------------------------------------ if [ "$#" -lt 1 ] ; then @@ -496,6 +562,10 @@ submissionpid=$! # Flag to indicate if contestant submission has terminated contestant_done=0 +# Validator exit status, if it finished before submission +val_result="" +# Indicates if we still have to wait for the submission +wait_sub=0 # Now we wait. We wait for either the child or interactive validator to finish. # If the validator finishes, no matter what, we're done. If the submission is still @@ -519,36 +589,80 @@ do REPORT_DEBUG Validator finishes with exit code $wstat if test "$contestant_done" -eq 0 then - # Only kill it if it still exists - if test -d /proc/$contestantpid + # If we always kill off the child when the validator finishes with WA, or the validator finishes with "AC" and we dont wait for submission on AC, + # then kill off the child, otherwise we wait for it. + if test "(" ${KILL_WA_VALIDATOR} -eq 1 -a "$wstat" -ne "${EXITCODE_AC}" ")" -o "(" ${WAIT_FOR_SUB_ON_AC} -eq 0 -a "$wstat" -eq ${EXITCODE_AC} ")" then - REPORT_DEBUG Contestant PID $submissionpid has not finished - killing it - # TODO: We should kill and wait for it here and print out the stats + # Only kill it if it still exists + if test -d /proc/$contestantpid + then + REPORT_DEBUG Contestant PID $submissionpid has not finished - killing it "(KILL_WA_VALIDATOR=1)" + # TODO: We should kill and wait for it here and print out the stats + fi + # This just determines if the program ran, not if it's correct. + # The result file has the correctness in it. + # We only do this if the contestant program has not finished yet. + REPORT_DEBUG Indicating that the submission exited with code 0 because we killed it. + COMMAND_EXIT_CODE=0 + else + REPORT_DEBUG Contestant PID $submissionpid has not finished - waiting for it "(KILL_WA_VALIDATOR=1)" + # Need to wait for child. Remember validator result. + val_result="$wstat" + wait_sub=1 fi - # This just determines if the program ran, not if it's correct. - # The result file has the correctness in it. - # We only do this if the contestant program has not finished yet. - COMMAND_EXIT_CODE=0 fi - KillChildProcs + # Kill everything off if we don't have to wait for submission + if test "$wait_sub" -eq 0 + then + KillChildProcs + fi if test "$wstat" -eq $EXITCODE_AC then - GenXML Accepted "" + # COMMAND_EXIT_CODE will be set to 0 above, or 0 if the submission finished already with EC 0. + # If COMMAND_EXIT_CODE is anything else, we will either be waiting for the submission, or, use + # it's exit code as the result. The GenXML will have been filled in in this case with any errors. + if test -z "${COMMAND_EXIT_CODE}" -o "${COMMAND_EXIT_CODE}" = "0" + then + REPORT_DEBUG Both the validator and the submission indicate AC + # Set the result to AC. If we have to wait for the submission, this will get overwritten with + # the submissions disposition. + GenXML Accepted "" + fi elif test "$wstat" -eq $EXITCODE_WA then - # If validator created a feedback file, put the last line in the judgement - if test -s "$feedbackfile" + # COMMAND_EXIT_CODE may be set to something here, either 0 from above, or + # an exit code from when the submission finished below. In the latter case, the + # XML result file will have already been created with the disposition from below. + # We will only generate an XML here if the validator says it failed (WA). + # COMMAND_EXIT_CODE can be empty if we are waiting for the submission to finish. + if test -z "${COMMAND_EXIT_CODE}" -o "${COMMAND_EXIT_CODE}" = "0" then - GenXML "No - Wrong answer" `head -n 1 $feedbackfile` - else - GenXML "No - Wrong answer" "No feedback file" + REPORT_DEBUG Submission has an exit code of 0 and validator says WA. + # If validator created a feedback file, put the last line in the judgement + if test -s "$feedbackfile" + then + GenXML "No - Wrong answer" `head -n 1 $feedbackfile` + else + GenXML "No - Wrong answer" "No feedback file" + fi fi else - GenXML "Other - contact staff" "bad return code $wstat" + REPORT_DEBUG Validator returned code $wstat which is not 42 or 43 - validator error. + GenXML "Other - contact staff" "bad validator return code $wstat" + if test "$wait_sub" -ne 0 + then + KillChildProcs + fi + COMMAND_EXIT_CODE=${FAIL_INTERACTIVE_ERROR} + break + fi + if test "$wait_sub" -eq 0 + then + REPORT_DEBUG No need to wait for submission to finish. + break fi - break; fi # If this is the contestant submission if test "$child_pid" -eq "$submissionpid" @@ -568,7 +682,8 @@ do fi - REPORT_DEBUG Contestant PID $submissionpid finished with exit code $wstat + FormatExitCode $wstat + REPORT_DEBUG Contestant PID $submissionpid finished with exit code $result contestant_done=1 COMMAND_EXIT_CODE=$wstat @@ -579,39 +694,60 @@ do if test "$kills" != "0" then - REPORT_DEBUG The command was killed because it exceeded the memory limit + REPORT_DEBUG The command was killed because it exceeded the memory limit - setting exit code to ${FAIL_MEMORY_LIMIT_EXCEEDED} REPORT_BRIEF MLE COMMAND_EXIT_CODE=${FAIL_MEMORY_LIMIT_EXCEEDED} GenXML "No - Memory limit exceeded" "" - KillValidator - break + if test "$val_result" = "${EXITCODE_AC}" -o "${KILL_WA_VALIDATOR}" -eq 0 + then + KillValidator + break + fi else # See why we terminated. 137 = 128 + 9 = SIGKILL, which is what ulimit -t sends if test "$COMMAND_EXIT_CODE" -eq 137 -o "$cputime" -gt "$TIMELIMIT_US" then - REPORT_DEBUG The command was killed because it exceeded the CPU Time limit - REPORT_BRIEF TLE COMMAND_EXIT_CODE=${FAIL_TIME_LIMIT_EXCEEDED} + FormatExitCode "$COMMAND_EXIT_CODE" + REPORT_DEBUG The command was killed because it exceeded the CPU Time limit - setting exit code to $result + REPORT_BRIEF TLE GenXML "No - Time limit exceeded" "${cputime}us > ${TIMELIMIT_US}us" - KillValidator - break + if test "$val_result" = "${EXITCODE_AC}" -o "${KILL_WA_VALIDATOR}" -eq 0 + then + KillValidator + break + fi elif test "$COMMAND_EXIT_CODE" -ge 128 then - REPORT_DEBUG The command terminated abnormally due to a signal with exit code $COMMAND_EXIT_CODE signal $((COMMAND_EXIT_CODE - 128)) + FormatExitCode "$COMMAND_EXIT_CODE" + REPORT_DEBUG The command terminated due to a signal with exit code $result else - REPORT_DEBUG The command terminated normally. + REPORT_DEBUG The command terminated normally with exit code $result fi fi REPORT_DEBUG Finished executing "$COMMAND $*" - REPORT_DEBUG "$COMMAND" exited with exit code $COMMAND_EXIT_CODE + FormatExitCode "$COMMAND_EXIT_CODE" + REPORT_DEBUG "$COMMAND" exited with exit code $result - if test "$wstat" -ne 0 + if test "$COMMAND_EXIT_CODE" -ne 0 then - REPORT_DEBUG Contestant finished abnormally - killing validator + REPORT_DEBUG Contestant finished with a non-zero exit code $result REPORT_BRIEF RTE - KillValidator GenXML "No - Run-time Error" "Exit status $wstat" - break + # If validator finished with AC, but submission has a bad exit code, we use that. + # Or, if we didn't kill off the submission, and want to use it's exit code + if test "$val_result" = "${EXITCODE_AC}" -o "${KILL_WA_VALIDATOR}" -eq 0 + then + KillValidator + break + fi + else + # COMMAND_EXIT_CODE is 0, let's see if the validator finished. If + # so, we're done, since the validator already created it's disposition. + if test -n "$val_result" + then + break + fi fi REPORT_DEBUG Waiting for interactive validator to finish... fi @@ -625,6 +761,8 @@ rm -f "$INFIFO" "$OUTFIFO" # TODO: determine how to pass more detailed pc2sandbox.sh results back to PC2... Perhaps in a file... # return the exit code of the command as our exit code +FormatExitCode "$COMMAND_EXIT_CODE" +REPORT_DEBUG Returning exit code $result to PC2 exit $COMMAND_EXIT_CODE # eof pc2sandbox_interactive.sh diff --git a/support/judge_webcgi/cgi-bin/HtmlFunctions.py b/support/judge_webcgi/cgi-bin/HtmlFunctions.py new file mode 100644 index 000000000..2ea16bd8d --- /dev/null +++ b/support/judge_webcgi/cgi-bin/HtmlFunctions.py @@ -0,0 +1,68 @@ +# Functions to support generation of HTML +TESTING = False + +if TESTING : + BANNER_FILE="banner.png" + TABLE_SORT_JS="tablesort.js" + JQUERY="jquery-3.7.1.slim.min.js" +else : + BANNER_FILE="/banner.png" + TABLE_SORT_JS="/scripts/tablesort.js" + JQUERY="/scripts/jquery-3.7.1.slim.min.js" + +def Preamble(hdr) : + if TESTING == False : + print("Content-type: text/html") + print("") + if hdr == None : + headmsg = "Judge" + else : + headmsg = hdr; + print("") + print('') + print("") + print(f"PC² {headmsg}") + +def Styles(css) : + print(f'') + +def StartHTMLDoc() : + print("") + print("") + + +def Scripts() : + print(f'') + print(f'') + +def Header() : + # Need to link bannerfile if not present + print("
") + print(f' ') + print('

PC2 Judging Results

') + print("
") + print("

") + +def HeaderNoBanner(header) : + if header == None : + hdrmsg = "Judging Results" + else : + hdrmsg = header + print("

") + print("

PC2 {hdrmsg}

") + print("
") + print("

") + +def Trailer() : + print("") + print("") + +def StartTable() : + print("

") + +def EndTable() : + print("

") + + + + diff --git a/support/judge_webcgi/cgi-bin/alljudgments.py b/support/judge_webcgi/cgi-bin/alljudgments.py new file mode 100644 index 000000000..c22011828 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/alljudgments.py @@ -0,0 +1,84 @@ +#!/usr/bin/python3 +import json +import urllib.request + +import HtmlFunctions + +TESTING = False + +if TESTING : + CORRECT_PNG = "Correct.png" + COMPILE_ERROR_PNG = "Warning.png" + WRONG_PNG = "Wrong.png" + JUDGE_STYLES="judgestyles.css" +else : + CORRECT_PNG = "../Correct.png" + COMPILE_ERROR_PNG = "../Warning.png" + WRONG_PNG = "../Wrong.png" + JUDGE_STYLES="/css/judgestyles.css" + +JUDGMENTS_SCRIPT="getjudgments.sh" + +JUDGE_HOSTS = ['pc2-aj1', 'pc2-aj2', 'pc2-ccsadmin1'] + +def MyTableStyles() : + print("") + +def TableHeader(): + print(''); + print('Run ID '); + print('Disp'); + print('Judgment '); + print('Problem '); + print('Team '); + print('Test Cases '); + print('Language '); + print('Judge '); + print('Time Judged '); + print(''); + +def getJudgmentsFromHost(host) : + try : + with urllib.request.urlopen(f"http://{host}/cgi-bin/{JUDGMENTS_SCRIPT}") as url : + data = json.load(url) + for j in data : + print(' ') + print(f' Run {j["runid"]}') + if j["judgment"] == "AC" : + icon = CORRECT_PNG + elif j["judgment"] == "CE" : + icon = COMPILE_ERROR_PNG + else : + icon = WRONG_PNG + print(f' ') + print(f' {j["judgment"]}') + print(f' {j["problem"]}') + print(f' team{j["team_number"]}') + print(f' {j["test_info"]}') + print(f' {j["language_id"]}') + print(f' {j["judge"]}') + print(f' {j["runtime"]}') + print(' ') + except Exception as err : + if TESTING : + print(f"getJudgmentsFromHost: Exception: {err}") + +HtmlFunctions.Preamble(None) +HtmlFunctions.Styles(JUDGE_STYLES) +MyTableStyles() +HtmlFunctions.StartHTMLDoc() +HtmlFunctions.Header() +HtmlFunctions.StartTable() +TableHeader() + +for jh in JUDGE_HOSTS : + getJudgmentsFromHost(jh) + +HtmlFunctions.EndTable() +HtmlFunctions.Scripts() +HtmlFunctions.Trailer() + diff --git a/support/judge_webcgi/cgi-bin/getjudgments.sh b/support/judge_webcgi/cgi-bin/getjudgments.sh new file mode 100644 index 000000000..8ab8ccaf5 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/getjudgments.sh @@ -0,0 +1,109 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + +TableRow() +{ + dir="$1" + runid=$2 + problet=$3 + shortname=$4 + langid=$5 + teamnum=$6 + judgment="$7" + runtime="$8" + testinfo="$9" + judge="${10}" + probname="" + probdir="" + if test -n "${shortname}" + then + probdir=${PC2_CDP}/${shortname} + probstatement=${probdir}/problem_statement/problem.en.tex + if test ! -s "{probstatement}" + then + probstatement=${probdir}/problem_statement/problem.tex + fi + if test -s "${probstatement}" + then + probname=`head -1 ${probstatement}` + probname=${probname##*\{} + probname=${probname%\}} + fi + fi + problem="$problet - $probname (${shortname})" + echo ' {' + echo ' "runid":"'$runid'"', + echo ' "href":"http://'$hostname'/cgi-bin/showrun.sh?run='$runid'&dir='$dir'&probdir='$probdir'&probletter='$problet'&probshort='$shortname'&lang='$langid'&submitter='$teamnum'&judgment='$judgment'"', + echo ' "judgment":"'$judgment'"', + echo ' "problem":"'$problem'"', + echo ' "problem_letter":"'$problet'"', + echo ' "problem_short_name":"'$probshort'"', + echo ' "directory":"'$dir'"', + echo ' "problem_dir":"'$probdir'"', + echo ' "team_number":"'$teamnum'"', + echo ' "test_info":"'$testinfo'"', + echo ' "language_id":"'$langid'"', + echo ' "judge":"'$judge'"', + echo ' "runtime":"'$runtime'"' + echo ' }' +} + +hostname=`hostname` +echo "Content-type: application/json" +echo "" + +echo '[' +sep="" +# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid_judge +for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+' | sort --field-separator=_ +1rn` +do + # exdir looks like: ex_188_Y_compression_46103_cpp + # RId P ProbShort team# Lang + # RId = Run ID + # P = problem letter + # Lang = CLICS Language id + saveIFS="$IFS" + IFS="_" + set ${exdir} + IFS="$saveIFS" + if test $# -ge 6 + then + exedir=${PC2_RUN_DIR}/$exdir + runid=$2 + problet=$3 + probshort=$4 + teamnum=$5 + langid=$6 + judge=$7 + if test -z "${judge}" + then + judge="N/A" + fi + GetJudgment "${exedir}" + judgment="${result}" + runtime="${executeDateTime}" + # Get how many total test cases there are + probdir=${PC2_CDP}/${probshort} + if test -n "${probdir}" + then + GetNumberOfTestCases "${probdir}" + numcases=${result} + else + numcases="??" + fi + # Note that GetJudgment also filled in exdata with the last execute data + GetTestCaseNumber "${exdata##./}" + testcaseinfo=$((result+1))/${numcases} + if test -n "$sep" + then + echo " $sep" + else + sep="," + fi + TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" + fi +done +echo ']' +exit 0 diff --git a/support/judge_webcgi/cgi-bin/pc2common.sh b/support/judge_webcgi/cgi-bin/pc2common.sh index d9b6e2aed..edf361bd2 100644 --- a/support/judge_webcgi/cgi-bin/pc2common.sh +++ b/support/judge_webcgi/cgi-bin/pc2common.sh @@ -30,6 +30,8 @@ TEST_OUT_PREFIX="teamoutput" TEST_ERR_PREFIX="teamstderr" TEST_VALOUT_PREFIX="valout" TEST_VALERR_PREFIX="valerr" +COMP_OUT_PREFIX="cstdout" +COMP_ERR_PREFIX="cstderr" # Detailed log of entire judging process SANDBOX_LOG=sandbox.log @@ -219,5 +221,28 @@ DeleteOldestLinks() done } +GetSourceList() +{ + sllang="$1" + sldir="$2" + ext="" + case "$sllang" in + c) ext="c" ;; + cpp) ext="cc cpp cxx c++" ;; + java) ext="java" ;; + python3) ext="py" ;; + kotlin) ext="kt" ;; + esac + result="" + if test -n "$ext" + then + for e in $ext + do + result="$result "`ls -d $sldir/*.$e 2>/dev/null` + done + fi + +} + InitJudgments diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh index 97c0c3391..ca5052c61 100644 --- a/support/judge_webcgi/cgi-bin/showrun.sh +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -11,12 +11,12 @@ LogButton() { # Read first judgment file to get compile time - it's compiled once for all GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" + echo '

' if test -n "${result}" -a -n "${compileTimeMS}" then cat << LBEOF0 -

-

The program took ${compileTimeMS}ms to compile. -

+The program took ${compileTimeMS}ms to compile. +
LBEOF0 fi @@ -40,15 +40,13 @@ LBEOF0 cpusecs="1 second" fi cat << LBEOF1 -
-

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms). -

+The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms). +
LBEOF1 else cat << LBEOF1AA -
-

The CPU Limit for this problem is N/A. -

+The CPU Limit for this problem is N/A. +
LBEOF1AA fi if test -n "${memlim}" @@ -60,17 +58,34 @@ LBEOF1AA memlim=${memlim}MiB fi cat << LBEOF1A -
-

The Memory limit for this problem is ${memlim}. -

+The Memory limit for this problem is ${memlim}. +
LBEOF1A else cat << LBEOF1AAA -
-

The Memory limit for this problem is N/A. -

+The Memory limit for this problem is N/A. +
LBEOF1AAA fi + + GetSourceList $lang ${EXE_DIR_LINK} + srclist="$result" + echo "Submission Source Code: " + # Get source files + sep="" + for sfile in $srclist + do + if test -z "$sep" + then + sep=" " + else + echo -n "$sep" + fi + GenGenericFileLink "$sfile" + done + echo "
" + echo "
" + sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} if test -s "${sandlog}" then @@ -94,8 +109,8 @@ TableHeader() MiB Used Val Time Val Success - Run Out - Run Err + Run stdout + Run stderr Judge In Judge Ans Val Out @@ -104,6 +119,56 @@ TableHeader() EOFTH } +# Usage is: +# GenGenericFileLink full-path-to-file [link-text] +# If no [link-text] supplied, then use basename of file +# This just makes up the href. +# +GenGenericFileLink() +{ + tstfile=$1 + gentxt="$2" + if test -z "$gentxt" + then + gentxt=${tstfile##*/} + fi + tstpath=$tstfile + if test -s "${tstpath}" + then + echo -n ''$gentxt'' + elif test -e "${tstpath}" + then + echo -n "($gentxt Empty)" + else + echo -n "$gentxt Not found" + fi +} + +# Usage is: +# GenGenericFileLinkTd full-path-to-file [link-text] +# If no [link-text] supplied, then use basename of file +# This includes the bracketing ... +# +GenGenericFileLinkTd() +{ + tstfile=$1 + gentxt="$2" + if test -z "$gentxt" + then + gentxt=${tstfile##*/} + fi + tstpath=$dir/$tstfile + if test -s "${tstpath}" + then + echo ' '$gentxt'' + elif test -e "${tstpath}" + then + echo ' (Empty)' + else + echo ' Not found' + fi +} + GenFileLink() { tstcase="$2" @@ -127,8 +192,13 @@ GenFileLinkWithText() linkaddr="$1" linktext="$2" linkcolor="$3" - bytes=`stat -c %s ${linkaddr}` - echo ' '$linktext' ('$bytes' bytes)' + if test -z "${linktext}" + then + echo ' N/A' + else + bytes=`stat -c %s ${linkaddr}` + echo ' '$linktext' ('$bytes' bytes)' + fi } TableRow() @@ -152,7 +222,7 @@ TableRow() memused="$9" memusedbytes="${10}" exesandms="${11}" - + srclist="${12}" # Create link to report/testcase file for testcase number MakeTestcaseFile ${EXE_DIR_LINK} ${tc} tcreport="${result}" @@ -207,8 +277,14 @@ TableRow() fi echo ' '$valtm'ms' echo ' '$valsucc'' - GenFileLink $TEST_OUT_PREFIX $tc - GenFileLink $TEST_ERR_PREFIX $tc + if test "$judgment" != "CE" + then + GenFileLink $TEST_OUT_PREFIX $tc + GenFileLink $TEST_ERR_PREFIX $tc + else + GenGenericFileLinkTd $COMP_OUT_PREFIX.pc2 "View Compiler" + GenGenericFileLinkTd $COMP_ERR_PREFIX.pc2 "View Compiler" + fi GenFileLinkWithText $PROB_DIR_LINK/data/"$jin" "$jinbase" $lcolor GenFileLinkWithText $PROB_DIR_LINK/data/"$jans" "$jansbase" $lcolor GenFileLink $TEST_VALOUT_PREFIX $tc @@ -260,8 +336,17 @@ do valtm=$validateTimeMS valsuc=$validationSuccess MakeBriefcaseFile "$dir" "$tc" - ReadBriefcase < ${result} - TableRow "$tc" "$judgment" "$ec" "$exetm" "$valtm" "$judgein" "$judgeans" "$valsuc" "$mempeak" "$mempeakbytes" "$execpums" + if test -s "${result}" + then + ReadBriefcase < ${result} + else + judgein="" + judgeans="" + mempeak="" + mempeakbytes="" + fi + GetSourceList $lang ${EXE_DIR_LINK} + TableRow "$tc" "$judgment" "$ec" "$exetm" "$valtm" "$judgein" "$judgeans" "$valsuc" "$mempeak" "$mempeakbytes" "$execpums" "$result" done Trailer diff --git a/support/judge_webcgi/css/judgestyles.css b/support/judge_webcgi/css/judgestyles.css new file mode 100644 index 000000000..70a7ee95f --- /dev/null +++ b/support/judge_webcgi/css/judgestyles.css @@ -0,0 +1,94 @@ +body { + font-family: "Arial", "Helvetica", "sans-serif"; +} + +table { + font-family: arial, sans-serif; + border-collapse: collapse; + border-spacing: 10px; + width: 100%; +} + +td, th { + border: 1px solid #dddddd; + text-align: left; + padding: 8px; +} + + td.red { + border-radius: 25px; + text-align: center; + background-color: #f0a0a0; + } + + td.green { + border-radius: 25px; + text-align: center; + background-color: #a0f0a0; + } + + td.cent { + text-align: center; + } + + td.right { + text-align: right; + } + + th.cent { + text-align: center; + } + + th.right { + text-align: right; + } + +tr:nth-child(even) { + background-color: #dddddd; +} + +img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.rspecial { + border: 1px solid #ffffff; + border-radius: 25px; + text-align: center; + background-color: 0xf0a0a0; + margin: 2px; +} + +.judgeicon { + height: 36px; + width: auto; +} + +.tooltip { + position: relative; + display: inline-block; + border-bottom: 3px dashed blue; +} + + .tooltip .tooltiptext { + visibility: hidden; + width: 120px; + background-color: black; + color: white; + text-align: center; + border-radius: 6px; + padding: 5px 0; + /* Position the tooltip */ + position: absolute; + z-index: 1; + } + + .tooltip:hover .tooltiptext { + visibility: visible; + } + +th { + cursor: pointer; +} diff --git a/support/judge_webcgi/scripts/tablesort.js b/support/judge_webcgi/scripts/tablesort.js new file mode 100644 index 000000000..3788b5cf0 --- /dev/null +++ b/support/judge_webcgi/scripts/tablesort.js @@ -0,0 +1,28 @@ +const getCellValue = (tr, idx) => tr.children[idx].innerText || tr.children[idx].textContent; + +const comparer = (idx, asc) => (a, b) => ((v1, v2) => + v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2, undefined, {numeric: true}) + )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx)); + +// do the work... +document.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => { + const table = th.closest('table'); + if (th.querySelector('span')){ + let order_icon = th.querySelector('span'); + // Awesome? hack: use the icon as a sort indicator for the column. we convert + // it to a URI and look at the UTF-8 encoding. Calm yourself. I found this code + // here: https://github.com/VFDouglas/HTML-Order-Table-By-Column/blob/main/index.html + // and hacked it up further. -- JB + let order = encodeURI(order_icon.innerHTML).includes('%E2%96%B2') ? 'desc' : 'asc'; + Array.from(table.querySelectorAll('tr:nth-child(n+2)')) + .sort(comparer(Array.from(th.parentNode.children).indexOf(th), order === 'asc')) + .forEach(tr => table.appendChild(tr) ); + if(order === 'desc') { + // down triangle + order_icon.innerHTML = "▼" + } else { + // up triangle + order_icon.innerHTML = "▲" + } + } +}))); From d7db42fbd372fcb37aa54c0c7992863b4a470d4e Mon Sep 17 00:00:00 2001 From: John Buck Date: Wed, 17 Jul 2024 20:46:02 -0400 Subject: [PATCH 18/55] i_972 Bug fixes and code cleanup Refactor name of judgesExecuteFolder to be just ExecuteFolder since it applies to anyone who can judge. Read the execute-folder property from the system.pc2.yaml file for CDP configuration. Export the execute folder when the contest is exported. Add error logging to indicate why a compile failed Prevent null references (these were harmless, but caused exception traces in the log files) --- .../csus/ecs/pc2/core/execute/Executable.java | 59 +++++----- .../csus/ecs/pc2/core/export/ExportYAML.java | 101 +++++++++--------- .../pc2/core/model/ContestInformation.java | 18 ++-- .../imports/ccs/ContestSnakeYAMLLoader.java | 4 +- .../ecs/pc2/imports/ccs/IContestLoader.java | 2 + .../ecs/pc2/ui/ContestInformationPane.java | 4 +- 6 files changed, 100 insertions(+), 88 deletions(-) diff --git a/src/edu/csus/ecs/pc2/core/execute/Executable.java b/src/edu/csus/ecs/pc2/core/execute/Executable.java index 89a273895..42a11d8c5 100644 --- a/src/edu/csus/ecs/pc2/core/execute/Executable.java +++ b/src/edu/csus/ecs/pc2/core/execute/Executable.java @@ -2841,6 +2841,7 @@ protected boolean compileProgram() { executionData.setCompileResultCode(0); return true; } else { + log.log(Log.INFO, "Expected compiler to generate executable: '" + programName + "' but it did not - Compile failed"); executionData.setCompileExeFileName(""); executionData.setCompileSuccess(false); executionData.setCompileResultCode(2); @@ -3139,9 +3140,7 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb if(executeInfoFileName != null) { newString = replaceString(newString, "{:executeinfofilename}", executeInfoFileName); } else { - // can't happen, but if it does, just use default basename newString = replaceString(newString, "{:executeinfofilename}", Constants.PC2_EXECUTION_RESULTS_NAME_SUFFIX); - log.config("substituteAllStrings() executeInfoFileName is null, using default basename" + Constants.PC2_EXECUTION_RESULTS_NAME_SUFFIX); } String fileName = getJudgeFileName(Utilities.DataFileType.JUDGE_DATA_FILE, dataSetNumber-1); if(fileName == null) { @@ -3415,7 +3414,7 @@ public void setProblem(Problem problem) { * @return the name of the execute directory for this client. */ public String getExecuteDirectoryName() { - String dirName = getContestInformation().getJudgesExecuteFolder(); + String dirName = getContestInformation().getExecuteFolder(); if(StringUtilities.isEmpty(dirName)) { dirName = DEFAULT_EXECUTE_DIRECTORY_TEMPLATE; } @@ -3668,34 +3667,38 @@ private String getJudgeFileName(Utilities.DataFileType type, int setIndex) String result = null; SerializedFile serializedFile = null; - try { - // it's a little more work for external files - if (problem.isUsingExternalDataFiles()) { + // It's possible for this to be null if someone requests the name before the run has started + // executing. + if(problemDataFiles != null) { + try { + // it's a little more work for external files + if (problem.isUsingExternalDataFiles()) { - if(type == Utilities.DataFileType.JUDGE_DATA_FILE) { - serializedFile = problemDataFiles.getJudgesDataFiles()[setIndex]; + if(type == Utilities.DataFileType.JUDGE_DATA_FILE) { + serializedFile = problemDataFiles.getJudgesDataFiles()[setIndex]; + } else { + serializedFile = problemDataFiles.getJudgesAnswerFiles()[setIndex]; + } + //Note: last argument (type) is unused in locateJudgesDataFile + result = Utilities.locateJudgesDataFile(problem, serializedFile, getContestInformation().getJudgeCDPBasePath(), type); } else { - serializedFile = problemDataFiles.getJudgesAnswerFiles()[setIndex]; + // For internal files, the appropriate data files are copied to the FIRST datafile's name in the + // execute folder, so we always return that one. + if(type == Utilities.DataFileType.JUDGE_DATA_FILE) { + result = prefixExecuteDirname(problem.getDataFileName()); + } else { + result = prefixExecuteDirname(problem.getAnswerFileName()); + } } - //Note: last argument (type) is unused in locateJudgesDataFile - result = Utilities.locateJudgesDataFile(problem, serializedFile, getContestInformation().getJudgeCDPBasePath(), type); - } else { - // For internal files, the appropriate data files are copied to the FIRST datafile's name in the - // execute folder, so we always return that one. - if(type == Utilities.DataFileType.JUDGE_DATA_FILE) { - result = prefixExecuteDirname(problem.getDataFileName()); + } catch (Exception e) + { + //if we got far enough to get the serialized file, show the expected name in the log message + if(serializedFile != null) { + log.log(Log.WARNING, "Can not get " + type.toString() + " expected filename (" + serializedFile.getName() + ") for dataset " + (setIndex+1) + ": " + e.getMessage(), e); } else { - result = prefixExecuteDirname(problem.getAnswerFileName()); + log.log(Log.WARNING, "Can not get " + type.toString() + " filename for dataset " + (setIndex+1) + ": " + e.getMessage(), e); } } - } catch (Exception e) - { - //if we got far enough to get the serialized file, show the expected name in the log message - if(serializedFile != null) { - log.log(Log.WARNING, "Can not get " + type.toString() + " expected filename (" + serializedFile.getName() + ") for dataset " + (setIndex+1) + ": " + e.getMessage(), e); - } else { - log.log(Log.WARNING, "Can not get " + type.toString() + " filename for dataset " + (setIndex+1) + ": " + e.getMessage(), e); - } } return(result); } @@ -3776,7 +3779,11 @@ private boolean saveExecuteData(int dataSetNumber) executeDataWriter.println("compileTimeMS='" + executionData.getCompileTimeMS() + "'"); executeDataWriter.println("executeTimeMS='" + executionData.getExecuteTimeMS() + "'"); executeDataWriter.println("validateTimeMS='" + executionData.getvalidateTimeMS() + "'"); - executeDataWriter.println("executionException='" + showNullAsEmpty(executionData.getExecutionException().getMessage()) + "'"); + if(executionData.getExecutionException() != null) { + executeDataWriter.println("executionException='" + showNullAsEmpty(executionData.getExecutionException().getMessage()) + "'"); + } else { + executeDataWriter.println("executionException=''"); + } executeDataWriter.println("runTimeLimitExceeded='" + executionData.isRunTimeLimitExceeded() + "'"); executeDataWriter.println("additionalInformation='" + showNullAsEmpty(executionData.getAdditionalInformation()) + "'"); bWritten = true; diff --git a/src/edu/csus/ecs/pc2/core/export/ExportYAML.java b/src/edu/csus/ecs/pc2/core/export/ExportYAML.java index 3a5188578..74cee2766 100644 --- a/src/edu/csus/ecs/pc2/core/export/ExportYAML.java +++ b/src/edu/csus/ecs/pc2/core/export/ExportYAML.java @@ -40,9 +40,9 @@ /** * Create CCS contest.yaml and problem.yaml files. - * + * * Creates contest.yaml and problem.yaml files along with all the data files per the CCS specification. - * + * * @author pc2@ecs.csus.edu */ // TODO REFACTOR and CI use constants for secret and sample dir names @@ -71,9 +71,9 @@ public class ExportYAML { /** * Write CCS Yaml files to directory. - * + * * Creates files: - * + * *
      * directoryName/contest.yaml
      * directoryName/shortname1/problem.yaml
@@ -82,7 +82,7 @@ public class ExportYAML {
      * directoryName/shortname2/problem.yaml
      * directoryName/shortname3/problem.yaml
      * 
- * + * * @param directoryName * @param contest * @throws IOException @@ -103,7 +103,7 @@ private String getDateTimeString() { /** * Write contest and problem yaml and files to directory. - * + * * @param contest * @param directoryName * @param contestFileName @@ -156,10 +156,10 @@ public void writeContestYAMLFiles(IInternalContest contest, String directoryName contestWriter.println("remaining: " + contestTime.getRemainingTimeStr()); contestWriter.println("running: " + contestTime.isContestRunning()); - + // PC^2 Specific contestWriter.println(IContestLoader.AUTO_STOP_CLOCK_AT_END_KEY + ": " + info.isAutoStopContest()); - + // start-time: 2011-02-04 01:23Z if (info.getScheduledStartDate() == null) { info.setScheduledStartDate(new Date()); @@ -171,13 +171,13 @@ public void writeContestYAMLFiles(IInternalContest contest, String directoryName // TODO CCS scoreboard-freeze: 4:00:00 // scoreboard-freeze: 4:00:00 - + // Bug 1192 - // scoreboard-freeze-length Time length before end of contest when scoreboard will be frozen, in h:mm:ss + // scoreboard-freeze-length Time length before end of contest when scoreboard will be frozen, in h:mm:ss // contestWriter.println("scoreboard-freeze: " + info.getFreezeTime()); contestWriter.println("scoreboard-freeze-length: " + info.getFreezeTime()); - + contestWriter.println(IContestLoader.OUTPUT_PRIVATE_SCORE_DIR_KEY+": " + info.getScoringProperties().getProperty(DefaultScoringAlgorithm.JUDGE_OUTPUT_DIR)); contestWriter.println(IContestLoader.OUTPUT_PUBLIC_SCORE_DIR_KEY+": " + info.getScoringProperties().getProperty(DefaultScoringAlgorithm.PUBLIC_OUTPUT_DIR)); @@ -188,12 +188,13 @@ public void writeContestYAMLFiles(IInternalContest contest, String directoryName if (! StringUtilities.isEmpty(judgeCDPBasePath)){ contestWriter.println(IContestLoader.JUDGE_CONFIG_PATH_KEY +": "+judgeCDPBasePath); } - + contestWriter.println(IContestLoader.SHADOW_MODE_KEY + ": " + (new Boolean(info.isShadowMode()).toString())); contestWriter.println(IContestLoader.CCS_URL_KEY + ": " + (info.getPrimaryCCS_URL())); contestWriter.println(IContestLoader.CCS_LOGIN_KEY + ": " + (info.getPrimaryCCS_user_login())); contestWriter.println(IContestLoader.CCS_PASSWORD_KEY + ": " + (info.getPrimaryCCS_user_pw())); contestWriter.println(IContestLoader.CCS_LAST_EVENT_ID_KEY + ": " + (info.getLastShadowEventID())); + contestWriter.println(IContestLoader.EXECUTE_FOLDER + ": " + (info.getExecuteFolder())); contestWriter.println(); @@ -246,7 +247,7 @@ public void writeContestYAMLFiles(IInternalContest contest, String directoryName contestWriter.println(PAD4 + "compilerCmd: " + quote(language.getCompileCommandLine())); contestWriter.println(PAD4 + "exemask: " + quote(language.getExecutableIdentifierMask())); contestWriter.println(PAD4 + "execCmd: " + quote(language.getProgramExecuteCommandLine())); - + String clicsId = language.getID(); if ( clicsId != null && clicsId.trim().length() > 0){ contestWriter.println(PAD4 + "clics-id: " + quote(clicsId)); @@ -259,7 +260,7 @@ public void writeContestYAMLFiles(IInternalContest contest, String directoryName contestWriter.println(PAD4 + "runner: " + quote(runner)); contestWriter.println(PAD4 + "runner-args: " + quote(runnerArguments)); } - + contestWriter.println(PAD4 + IContestLoader.INTERPRETED_LANGUAGE_KEY + ": " + language.isInterpreted()); contestWriter.println(PAD4 + "use-judge-cmd: " + language.isUsingJudgeProgramExecuteCommandLine()); contestWriter.println(PAD4 + "judge-exec-cmd: " + quote(language.getJudgeProgramExecuteCommandLine())); @@ -267,30 +268,30 @@ public void writeContestYAMLFiles(IInternalContest contest, String directoryName contestWriter.println(); } // end of system.yaml stuff - + Problem[] problems = contest.getProblems(); - + /** - * Write problemset.yaml file. + * Write problemset.yaml file. */ String confiDir = new File(contestFileName).getParent(); - + String problemSetFilename = confiDir + File.separator + IContestLoader.DEFAULT_PROBLEM_SET_YAML_FILENAME; PrintWriter problemSetWriter = new PrintWriter(new FileOutputStream(problemSetFilename, false), true); problemSetWriter.println("# Contest Configuration, Problem Set, version 1.0 "); problemSetWriter.println("# PC^2 Version: " + new VersionInfo().getSystemVersionInfo()); problemSetWriter.println("# Created: " + getDateTimeString()); - + writeProblemSetYaml(problemSetWriter, contest, directoryName, IContestLoader.PROBLEMSET_PROBLEMS_KEY, problems); - + problemSetWriter.flush(); problemSetWriter.close(); problemSetWriter = null; // TODO this should go to system.pc2.yaml Vector accountVector = contest.getAccounts(ClientType.Type.JUDGE); - Account[] judgeAccounts = (Account[]) accountVector.toArray(new Account[accountVector.size()]); + Account[] judgeAccounts = accountVector.toArray(new Account[accountVector.size()]); Arrays.sort(judgeAccounts, new AccountComparator()); int ajCount = 0; @@ -386,11 +387,11 @@ public void writeContestYAMLFiles(IInternalContest contest, String directoryName /** * Write problem set section. - * + * * @param writer * @param contest * @param directoryName - * @param problemsKey problemset for contest.yaml or problems for problemset.yaml + * @param problemsKey problemset for contest.yaml or problems for problemset.yaml * @param problems * @throws IOException */ @@ -399,8 +400,8 @@ private void writeProblemSetYaml(PrintWriter writer, IInternalContest contest, S if (problems.length > 0) { writer.println(problemsKey + ":"); } - - // + + // // problemset: // // - letter: B @@ -454,12 +455,12 @@ private void writeProblemSetYaml(PrintWriter writer, IInternalContest contest, S writer.println(); } - + } /** * Surround by a single quote - * + * * @param string * @return */ @@ -468,7 +469,7 @@ private String quote(String string) { } /** - * + * * @param date * @return empty string if date is null, other wise */ @@ -515,9 +516,9 @@ protected static StringBuffer join(String delimiter, List list) { /** * Create disk file for input SerializedFile. - * + * * Returns true if file is written to disk and is not null. - * + * * @param file * @param outputFileName * @return true if file written to disk, false if external or not written to disk. @@ -533,7 +534,7 @@ boolean createFile(SerializedFile file, String outputFileName) throws IOExceptio /** * Write problem yaml and data files files to directory. - * + * * @param contest * @param directoryName * directory to write files to. @@ -553,7 +554,7 @@ public String[] writeProblemYAML(IInternalContest contest, String directoryName, /** * Write problem YAML to file. - * + * * @param contest * @param problem * @param filename @@ -591,7 +592,7 @@ public String[] writeProblemYAML(IInternalContest contest, Problem problem, Stri problemWriter.println("author: "); problemWriter.println("license: "); problemWriter.println("rights_owner: "); - + problemWriter.println(); problemWriter.println(IContestLoader.HIDE_PROBLEM+": "+!problem.isActive()); @@ -600,7 +601,7 @@ public String[] writeProblemYAML(IInternalContest contest, Problem problem, Stri problemWriter.println(IContestLoader.SHOW_VALIDATION_RESULTS+": "+problem.isShowValidationToJudges()); problemWriter.println(IContestLoader.PROBLEM_LOAD_DATA_FILES_KEY + ": " + (!isExternalFiles(problemDataFiles))); problemWriter.println(IContestLoader.STOP_ON_FIRST_FAILED_TEST_CASE_KEY + ": " + problem.isStopOnFirstFailedTestCase()); - + problemWriter.println(); List groups = problem.getGroups(); if (groups.size() == 0){ @@ -615,7 +616,7 @@ public String[] writeProblemYAML(IInternalContest contest, Problem problem, Stri // datafile: sumit.in problemWriter.println("datafile: " + dataFile); } - + String answerFileName = problem.getAnswerFileName(); if (answerFileName != null) { // answerfile: sumit.ans @@ -628,21 +629,21 @@ public String[] writeProblemYAML(IInternalContest contest, Problem problem, Stri } else { problemWriter.println("# "+ IContestLoader.VALIDATOR_FLAGS_KEY + ": "); } - + problemWriter.println(); - + problemWriter.println(IContestLoader.LIMITS_KEY + ":"); problemWriter.println(PAD4 + "timeout: " + problem.getTimeOutInSeconds()); if (problem.getMemoryLimitMB() > 0) { problemWriter.println(PAD4 + IContestLoader.MEMORY_LIMIT_CLICS + ": " + problem.getMemoryLimitMB()); } - + problemWriter.println(); - + problemWriter.println(IContestLoader.SANDBOX_TYPE_KEY+": "+problem.getSandboxType()); problemWriter.println(IContestLoader.SANDBOX_COMMAND_LINE_KEY+": "+quote(problem.getSandboxCmdLine())); problemWriter.println(IContestLoader.SANDBOX_PROGRAM_NAME_KEY+": "+quote(problem.getSandboxProgramName())); - + problemWriter.println(); if (problem.isValidatedProblem()) { @@ -660,19 +661,19 @@ public String[] writeProblemYAML(IInternalContest contest, Problem problem, Stri } problemWriter.println(); } - + problemWriter.println(IContestLoader.JUDGING_TYPE_KEY + ":"); - + problemWriter.println(PAD4 + IContestLoader.COMPUTER_JUDGING_KEY +": " + problem.isComputerJudged()); if (problem.isComputerJudged()) { - // if computer judged, may or may not be manual judged + // if computer judged, may or may not be manual judged problemWriter.println(PAD4 + IContestLoader.MANUAL_REVIEW_KEY + ": " + problem.isManualReview()); } else { // if not computer judged then MUST be manaul judged problemWriter.println(PAD4 + IContestLoader.MANUAL_REVIEW_KEY + ": true"); } - + problemWriter.println(PAD4 + IContestLoader.SEND_PRELIMINARY_JUDGEMENT_KEY + ": " + problem.isPrelimaryNotification()); problemWriter.println(); @@ -734,12 +735,12 @@ public String[] writeProblemYAML(IInternalContest contest, Problem problem, Stri problemWriter.close(); problemWriter = null; - return (String[]) filesWritten.toArray(new String[filesWritten.size()]); + return filesWritten.toArray(new String[filesWritten.size()]); } /** * Return true if files are not loaded/stored internally. - * + * * @param problemDataFiles * @return */ @@ -768,7 +769,7 @@ private boolean isExternalFiles(ProblemDataFiles problemDataFiles) { /** * Write problem title to (LaTeX) file - * + * * @param filename * @param title * @throws FileNotFoundException @@ -798,9 +799,9 @@ protected void writeProblemTitleToFile(String filename, String title) throws Fil /** * Get problem letter for input integer. - * + * * getProblemLetter(1) is 'A' - * + * * @param id * a one based problem number. * @return @@ -813,7 +814,7 @@ protected String getProblemLetter(int id) { /** * Create a problem short name. - * + * * @param name * Problem full name * @return diff --git a/src/edu/csus/ecs/pc2/core/model/ContestInformation.java b/src/edu/csus/ecs/pc2/core/model/ContestInformation.java index 383d74fd2..d861a7712 100644 --- a/src/edu/csus/ecs/pc2/core/model/ContestInformation.java +++ b/src/edu/csus/ecs/pc2/core/model/ContestInformation.java @@ -99,7 +99,7 @@ public class ContestInformation implements Serializable{ /** * String to append to the execute folder for judged runs. May contain substitution strings. */ - private String judgesExecuteFolder = ""; + private String executeFolder = ""; /** * @@ -262,11 +262,11 @@ public void setJudgesDefaultAnswer(String judgesDefaultAnswer) { } } - public String getJudgesExecuteFolder() { - if(judgesExecuteFolder == null) { - judgesExecuteFolder = ""; + public String getExecuteFolder() { + if(executeFolder == null) { + executeFolder = ""; } - return judgesExecuteFolder ; + return executeFolder ; } /** @@ -274,9 +274,9 @@ public String getJudgesExecuteFolder() { * * @param judgesDefaultAnswer The judgesDefaultAnswer to set. */ - public void setJudgesExecuteFolder(String judgesExecuteFolder) { - if (judgesExecuteFolder != null && judgesExecuteFolder.trim().length() > 0) { - this.judgesExecuteFolder = judgesExecuteFolder.trim(); + public void setExecuteFolder(String executeFolder) { + if (executeFolder != null && executeFolder.trim().length() > 0) { + this.executeFolder = executeFolder.trim(); } } @@ -294,7 +294,7 @@ public boolean isSameAs(ContestInformation contestInformation) { if (!judgesDefaultAnswer.equals(contestInformation.getJudgesDefaultAnswer())) { return false; } - if (!judgesExecuteFolder.equals(contestInformation.getJudgesExecuteFolder())) { + if (!executeFolder.equals(contestInformation.getExecuteFolder())) { return false; } if (!teamDisplayMode.equals(contestInformation.getTeamDisplayMode())) { diff --git a/src/edu/csus/ecs/pc2/imports/ccs/ContestSnakeYAMLLoader.java b/src/edu/csus/ecs/pc2/imports/ccs/ContestSnakeYAMLLoader.java index 51553af79..d5e2e3df2 100644 --- a/src/edu/csus/ecs/pc2/imports/ccs/ContestSnakeYAMLLoader.java +++ b/src/edu/csus/ecs/pc2/imports/ccs/ContestSnakeYAMLLoader.java @@ -479,10 +479,12 @@ public IInternalContest fromYaml(IInternalContest contest, String[] yamlLines, S String ccsPassoword = fetchValue(content, CCS_PASSWORD_KEY, contestInformation.getPrimaryCCS_user_pw()); contestInformation.setPrimaryCCS_user_pw(ccsPassoword); - String lastEventId = fetchValue(content, CCS_LAST_EVENT_ID_KEY, contestInformation.getLastShadowEventID()); contestInformation.setLastShadowEventID(lastEventId); + String executeDir = fetchValue(content, EXECUTE_FOLDER, contestInformation.getExecuteFolder()); + contestInformation.setExecuteFolder(executeDir); + // save ContesInformation to model contest.updateContestInformation(contestInformation); diff --git a/src/edu/csus/ecs/pc2/imports/ccs/IContestLoader.java b/src/edu/csus/ecs/pc2/imports/ccs/IContestLoader.java index 2cc905732..a2317662e 100644 --- a/src/edu/csus/ecs/pc2/imports/ccs/IContestLoader.java +++ b/src/edu/csus/ecs/pc2/imports/ccs/IContestLoader.java @@ -127,6 +127,8 @@ public interface IContestLoader { String JUDGE_CONFIG_PATH_KEY = "judge-config-path"; + String EXECUTE_FOLDER = "execute-folder"; + String TIMEOUT_KEY = "timeout"; final String MEMORY_LIMIT_IN_MEG_KEY = "memory-limit-in-Meg"; diff --git a/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java b/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java index 0832e475e..a4ff20617 100644 --- a/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java +++ b/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java @@ -1002,7 +1002,7 @@ protected ContestInformation getFromFields() { //fill in judging information newContestInformation.setJudgesDefaultAnswer(getJudgesDefaultAnswerTextField().getText()); - newContestInformation.setJudgesExecuteFolder(getJudgesExecuteFolderTextField().getText()); + newContestInformation.setExecuteFolder(getJudgesExecuteFolderTextField().getText()); newContestInformation.setPreliminaryJudgementsTriggerNotifications(getJCheckBoxShowPreliminaryOnNotifications().isSelected()); newContestInformation.setPreliminaryJudgementsUsedByBoard(getJCheckBoxShowPreliminaryOnBoard().isSelected()); newContestInformation.setSendAdditionalRunStatusInformation(getAdditionalRunStatusCheckBox().isSelected()); @@ -1097,7 +1097,7 @@ public void run() { selectDisplayRadioButton(); getJudgesDefaultAnswerTextField().setText(contestInformation.getJudgesDefaultAnswer()); - getJudgesExecuteFolderTextField().setText(contestInformation.getJudgesExecuteFolder()); + getJudgesExecuteFolderTextField().setText(contestInformation.getExecuteFolder()); getJCheckBoxShowPreliminaryOnBoard().setSelected(contestInformation.isPreliminaryJudgementsUsedByBoard()); getJCheckBoxShowPreliminaryOnNotifications().setSelected(contestInformation.isPreliminaryJudgementsTriggerNotifications()); getAdditionalRunStatusCheckBox().setSelected(contestInformation.isSendAdditionalRunStatusInformation()); From e5655bc0bfe119be8e8d02f00c97f375d26b50aa Mon Sep 17 00:00:00 2001 From: John Buck Date: Wed, 17 Jul 2024 21:03:40 -0400 Subject: [PATCH 19/55] i_988 Fix NPE in SubmitSampleRunspane If the SubmitSampleRunsPane was never used, and a run changed, an NPE would result when the IRunListener methods were called. Simply check the value of offending member, submissionList. --- .../csus/ecs/pc2/ui/SubmitSampleRunsPane.java | 76 ++++++++++--------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/src/edu/csus/ecs/pc2/ui/SubmitSampleRunsPane.java b/src/edu/csus/ecs/pc2/ui/SubmitSampleRunsPane.java index 102299cf5..e8535fdd2 100644 --- a/src/edu/csus/ecs/pc2/ui/SubmitSampleRunsPane.java +++ b/src/edu/csus/ecs/pc2/ui/SubmitSampleRunsPane.java @@ -1263,38 +1263,40 @@ public void run() { public void reloadRunList() { - if (filter.isFilterOn()){ - getFilterButton().setForeground(Color.BLUE); - getFilterButton().setToolTipText("Edit filter - filter ON"); - rowCountLabel.setForeground(Color.BLUE); - } else { - getFilterButton().setForeground(Color.BLACK); - getFilterButton().setToolTipText("Edit filter"); - rowCountLabel.setForeground(Color.BLACK); - } + if(submissionList != null) { + if (filter.isFilterOn()){ + getFilterButton().setForeground(Color.BLUE); + getFilterButton().setToolTipText("Edit filter - filter ON"); + rowCountLabel.setForeground(Color.BLUE); + } else { + getFilterButton().setForeground(Color.BLACK); + getFilterButton().setToolTipText("Edit filter"); + rowCountLabel.setForeground(Color.BLACK); + } - for (SubmissionSample sub : submissionList) { - ClientId clientId = null; + for (SubmissionSample sub : submissionList) { + ClientId clientId = null; - Run run = sub.getRun(); - if(run != null) { - RunStates runStates = run.getStatus(); - if (!(runStates.equals(RunStates.NEW) || run.isDeleted())) { - JudgementRecord judgementRecord = run.getJudgementRecord(); - if (judgementRecord != null) { - clientId = judgementRecord.getJudgerClientId(); + Run run = sub.getRun(); + if(run != null) { + RunStates runStates = run.getStatus(); + if (!(runStates.equals(RunStates.NEW) || run.isDeleted())) { + JudgementRecord judgementRecord = run.getJudgementRecord(); + if (judgementRecord != null) { + clientId = judgementRecord.getJudgerClientId(); + } } } + updateRunRow(sub, clientId, false); } - updateRunRow(sub, clientId, false); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + updateRowCount(); + resizeColumnWidth(runTable); + } + }); } - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - updateRowCount(); - resizeColumnWidth(runTable); - } - }); } /** * Run Listener @@ -1370,16 +1372,18 @@ public void runRemoved(RunEvent event) { private SubmissionSample getSubmission(RunEvent event) { - Run run = event.getRun(); - - // We are only interested in runs we submitted - if(run.getSubmitter().equals(getContest().getClientId())) { - for(SubmissionSample sub : submissionList) { - Run subRun = sub.getRun(); - // Check run numbers if this submission has a run - if(subRun != null && subRun.getNumber() == run.getNumber()) { - sub.setRun(run); - return(sub); + if(submissionList != null) { + Run run = event.getRun(); + + // We are only interested in runs we submitted + if(run.getSubmitter().equals(getContest().getClientId())) { + for(SubmissionSample sub : submissionList) { + Run subRun = sub.getRun(); + // Check run numbers if this submission has a run + if(subRun != null && subRun.getNumber() == run.getNumber()) { + sub.setRun(run); + return(sub); + } } } } From d953b75fcbd646b5ff444d2345bf3537bf93fd66 Mon Sep 17 00:00:00 2001 From: John Buck Date: Thu, 18 Jul 2024 15:58:43 -0400 Subject: [PATCH 20/55] i_972 CI Fix possible empty string in substituteAllStrings CI: The substituteAllStrings method could possibly return an empty string in the future. Always copy the original string into the returned string before doing anyway instead of relying on the first substitute to do it. --- .../csus/ecs/pc2/core/execute/Executable.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/edu/csus/ecs/pc2/core/execute/Executable.java b/src/edu/csus/ecs/pc2/core/execute/Executable.java index 42a11d8c5..2923492ce 100644 --- a/src/edu/csus/ecs/pc2/core/execute/Executable.java +++ b/src/edu/csus/ecs/pc2/core/execute/Executable.java @@ -3042,7 +3042,8 @@ public String substituteAllStrings(Run inRun, String origString) { * @return string with values */ public String substituteAllStrings(Run inRun, String origString, int dataSetNumber) { - String newString = ""; + // Make a new copy to start with to avoid issues in the future. + String newString = origString; String nullArgument = "-"; /* this needs to change */ try { @@ -3050,13 +3051,15 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb throw new IllegalArgumentException("Run is null"); } - if (runFiles.getMainFile() == null) { - log.config("substituteAllStrings() main file is null (no contents)"); - return origString; + if(runFiles != null) { + if (runFiles.getMainFile() == null) { + log.config("substituteAllStrings() main file is null (no contents)"); + return newString; + } + newString = replaceString(newString, "{:mainfile}", runFiles.getMainFile().getName()); + newString = replaceString(newString, Constants.CMDSUB_FILES_VARNAME, ExecuteUtilities.getAllSubmittedFilenames(runFiles)); + newString = replaceString(newString, Constants.CMDSUB_BASENAME_VARNAME, removeExtension(runFiles.getMainFile().getName())); } - newString = replaceString(origString, "{:mainfile}", runFiles.getMainFile().getName()); - newString = replaceString(newString, Constants.CMDSUB_FILES_VARNAME, ExecuteUtilities.getAllSubmittedFilenames(runFiles)); - newString = replaceString(newString, Constants.CMDSUB_BASENAME_VARNAME, removeExtension(runFiles.getMainFile().getName())); newString = replaceString(newString, "{:package}", packageName); String validatorCommand = null; From 465e57264a70a63ee1aff9ca8b0a729b07f47b7f Mon Sep 17 00:00:00 2001 From: John Buck Date: Thu, 18 Jul 2024 16:13:07 -0400 Subject: [PATCH 21/55] i_972 Script cleanup and bug fixes Cleaned up interactive script. Made interactive script configurable so we can change the way it judges RTE/WA/AC to match DOMjudge, if necessary. Fixed combined runs webpage to sort runs from all judges. --- scripts/pc2sandbox_interactive.sh | 115 +++++++++---------- support/judge_webcgi/cgi-bin/alljudgments.py | 55 +++++---- support/judge_webcgi/cgi-bin/getjudgments.sh | 2 +- support/judge_webcgi/cgi-bin/pc2common.sh | 15 ++- 4 files changed, 103 insertions(+), 84 deletions(-) diff --git a/scripts/pc2sandbox_interactive.sh b/scripts/pc2sandbox_interactive.sh index 070db2857..db44cd35b 100755 --- a/scripts/pc2sandbox_interactive.sh +++ b/scripts/pc2sandbox_interactive.sh @@ -21,12 +21,10 @@ # finish, and use whatever judgement is appropriate. If contestant finishes with 0, then we use the # vaildator result, else we use the exit code of the submission. KILL_WA_VALIDATOR=1 -# If WAIT_FOR_SUB_ON_AC is 1, we wait for the submission if the validator says "AC". We then -# use the exit code of the submission if it is non-zero to determine disposition, otherise, -# we return AC -# If WAIT_FOR_SUB_ON_AC is 0, we will kill off the submission as soon as we get an AC from the -# validator if the submission is not complete yet. -WAIT_FOR_SUB_ON_AC=1 + +# Always return WA if validator gets WA before submission finishes. This is the DOMjudge compatibility +# option (set it to 1 to be compatible) +ALWAYS_WA_BEFORE_SUB_FINISHES=1 # CPU to run submission on. This is 0 based, so 3 means the 4th CPU DEFAULT_CPU_NUM=3 @@ -240,6 +238,23 @@ GetTimeInMicros() echo $ret } +GetSandboxStats() +{ + # Get cpu time immediately to minimize any usage by this shell + cputime=`grep usage_usec $PC2_SANDBOX_CGROUP_PATH/cpu.stat | cut -d ' ' -f 2` + # Get wall time - we want it as close as possible to when we fetch the cpu time so they stay close + # since the cpu.stat includes the time this script takes AFTER the submission finishes. + endtime=`GetTimeInMicros` + walltime=$((endtime-starttime)) + # Newer kernels support memory.peak, so we have to check if it's there. + if test -e $PC2_SANDBOX_CGROUP_PATH/memory.peak + then + peakmem=`cat $PC2_SANDBOX_CGROUP_PATH/memory.peak` + else + peakmem="N/A" + fi +} + # Show run's resource summary in a nice format, eg. # CPU ms Limit ms Wall ms Memory Used Memory Limit # 3.356 5000.000 4.698 1839104 2147483648 @@ -577,10 +592,15 @@ do # Wait for the next process and put its PID in child_pid wait -n -p child_pid wstat=$? + REPORT_DEBUG Wait returned pid=$child_pid wstat=$wstat # A return code 127 indicates there are no more children. How did that happen? if test $wstat -eq 127 then REPORT_DEBUG No more children found while waiting: Submission PID was $submissionpid and Interactive Validator PID was $intv_pid + if test -d /proc/$submissionpid + then + REPORT_DEBUG The contestant pid /proc/$submissionpid still exists though + fi break fi # If interactive validator finishes @@ -589,39 +609,30 @@ do REPORT_DEBUG Validator finishes with exit code $wstat if test "$contestant_done" -eq 0 then - # If we always kill off the child when the validator finishes with WA, or the validator finishes with "AC" and we dont wait for submission on AC, - # then kill off the child, otherwise we wait for it. - if test "(" ${KILL_WA_VALIDATOR} -eq 1 -a "$wstat" -ne "${EXITCODE_AC}" ")" -o "(" ${WAIT_FOR_SUB_ON_AC} -eq 0 -a "$wstat" -eq ${EXITCODE_AC} ")" + REPORT_DEBUG Waiting for submission pid $submissionpid to finish... + # Wait for child to finish - it has to, one way or the other (TLE or just finish) + wait -n "$submissionpid" + COMMAND_EXIT_CODE=$? + + GetSandboxStats + + if test $COMMAND_EXIT_CODE -eq 127 then - # Only kill it if it still exists - if test -d /proc/$contestantpid - then - REPORT_DEBUG Contestant PID $submissionpid has not finished - killing it "(KILL_WA_VALIDATOR=1)" - # TODO: We should kill and wait for it here and print out the stats - fi - # This just determines if the program ran, not if it's correct. - # The result file has the correctness in it. - # We only do this if the contestant program has not finished yet. - REPORT_DEBUG Indicating that the submission exited with code 0 because we killed it. + REPORT_DEBUG No more children found. Setting submission exit code to 0 COMMAND_EXIT_CODE=0 else - REPORT_DEBUG Contestant PID $submissionpid has not finished - waiting for it "(KILL_WA_VALIDATOR=1)" - # Need to wait for child. Remember validator result. - val_result="$wstat" - wait_sub=1 + FormatExitCode $COMMAND_EXIT_CODE + REPORT_DEBUG Contestant PID $submissionpid finished with exit code $result but after the validator fi + ShowStats ${cputime} ${TIMELIMIT_US} ${walltime} ${peakmem} $((MEMLIMIT*1024*1024)) fi - # Kill everything off if we don't have to wait for submission - if test "$wait_sub" -eq 0 - then - KillChildProcs - fi + KillChildProcs if test "$wstat" -eq $EXITCODE_AC then - # COMMAND_EXIT_CODE will be set to 0 above, or 0 if the submission finished already with EC 0. - # If COMMAND_EXIT_CODE is anything else, we will either be waiting for the submission, or, use + # COMMAND_EXIT_CODE will be set to 0 above, or 0 if the submission previously finished already with EC 0. + # If COMMAND_EXIT_CODE is anything else, we'll use # it's exit code as the result. The GenXML will have been filled in in this case with any errors. if test -z "${COMMAND_EXIT_CODE}" -o "${COMMAND_EXIT_CODE}" = "0" then @@ -637,9 +648,9 @@ do # XML result file will have already been created with the disposition from below. # We will only generate an XML here if the validator says it failed (WA). # COMMAND_EXIT_CODE can be empty if we are waiting for the submission to finish. - if test -z "${COMMAND_EXIT_CODE}" -o "${COMMAND_EXIT_CODE}" = "0" + if test "(" ${ALWAYS_WA_BEFORE_SUB_FINISHES} -eq 1 -a "$contestant_done" -eq 0 ")" -o "(" -z "${COMMAND_EXIT_CODE}" -o "${COMMAND_EXIT_CODE}" = "0" ")" then - REPORT_DEBUG Submission has an exit code of 0 and validator says WA. + REPORT_DEBUG Submission has an exit code of ${COMMAND_EXIT_CODE} and validator says WA. "(ALWAYS_WA_BEFORE_SUB_FINISHES=${ALWAYS_WA_BEFORE_SUB_FINISHES})" # If validator created a feedback file, put the last line in the judgement if test -s "$feedbackfile" then @@ -647,48 +658,35 @@ do else GenXML "No - Wrong answer" "No feedback file" fi + COMMAND_EXIT_CODE=0 fi else REPORT_DEBUG Validator returned code $wstat which is not 42 or 43 - validator error. GenXML "Other - contact staff" "bad validator return code $wstat" - if test "$wait_sub" -ne 0 - then - KillChildProcs - fi COMMAND_EXIT_CODE=${FAIL_INTERACTIVE_ERROR} break fi - if test "$wait_sub" -eq 0 - then - REPORT_DEBUG No need to wait for submission to finish. - break - fi + break fi # If this is the contestant submission if test "$child_pid" -eq "$submissionpid" then - # Get cpu time immediately to minimize any usage by this shell - cputime=`grep usage_usec $PC2_SANDBOX_CGROUP_PATH/cpu.stat | cut -d ' ' -f 2` - # Get wall time - we want it as close as possible to when we fetch the cpu time so they stay close - # since the cpu.stat includes the time this script takes AFTER the submission finishes. - endtime=`GetTimeInMicros` - walltime=$((endtime-starttime)) - # Newer kernels support memory.peak, so we have to check if it's there. - if test -e $PC2_SANDBOX_CGROUP_PATH/memory.peak - then - peakmem=`cat $PC2_SANDBOX_CGROUP_PATH/memory.peak` - else - peakmem="N/A" - fi - + GetSandboxStats FormatExitCode $wstat REPORT_DEBUG Contestant PID $submissionpid finished with exit code $result contestant_done=1 - COMMAND_EXIT_CODE=$wstat ShowStats ${cputime} ${TIMELIMIT_US} ${walltime} ${peakmem} $((MEMLIMIT*1024*1024)) + # If we have already made a definitive judgment based on the validator and we are just waiting + # for the submission to finish, the break out now + if test "$wait_sub" = "2" + then + break + fi + COMMAND_EXIT_CODE=$wstat + # See if we were killed due to memory - this is a kill 9 if it happened kills=`grep oom_kill $PC2_SANDBOX_CGROUP_PATH/memory.events | cut -d ' ' -f 2` @@ -735,8 +733,7 @@ do REPORT_BRIEF RTE GenXML "No - Run-time Error" "Exit status $wstat" # If validator finished with AC, but submission has a bad exit code, we use that. - # Or, if we didn't kill off the submission, and want to use it's exit code - if test "$val_result" = "${EXITCODE_AC}" -o "${KILL_WA_VALIDATOR}" -eq 0 + if test "$val_result" = "${EXITCODE_AC}" then KillValidator break @@ -744,6 +741,8 @@ do else # COMMAND_EXIT_CODE is 0, let's see if the validator finished. If # so, we're done, since the validator already created it's disposition. + # Note: this case should not happen since we wait for the submission above if the validator + # finishes first. if test -n "$val_result" then break diff --git a/support/judge_webcgi/cgi-bin/alljudgments.py b/support/judge_webcgi/cgi-bin/alljudgments.py index c22011828..005a494d1 100644 --- a/support/judge_webcgi/cgi-bin/alljudgments.py +++ b/support/judge_webcgi/cgi-bin/alljudgments.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 import json import urllib.request +import operator import HtmlFunctions @@ -19,7 +20,7 @@ JUDGMENTS_SCRIPT="getjudgments.sh" -JUDGE_HOSTS = ['pc2-aj1', 'pc2-aj2', 'pc2-ccsadmin1'] +JUDGE_HOSTS = ['pc2-aj1', 'pc2-aj2', 'pc2-aj3' ] def MyTableStyles() : print(" + + +
+ +

PC2 Judging Results for Judge 1

+
+

+EOF +} + +Trailer() +{ +cat << EOF2 + + +EOF2 +} + +StartTable() +{ + cat << EOF3 +

+ + + +EOF3 +} + +EndTable() +{ + cat << EOF4 +
Run IDTeamProblemLanguageTime Judged
+

+EOF4 +} + +TableRow() +{ + dir="$1" + runid=$2 + problet=$3 + shortname=$4 + langid=$5 + teamnum=$6 + runtime=`stat -c '%y' $dir` + probname="" + probdir="" + if test -n ${shortname} + then + probdir=${PC2_CDP}/${shortname} + probstatement=${probdir}/problem_statement/problem.en.tex + if test ! -s ${probstatement} + then + probstatement=${probdir}/problem_statement/problem.tex + fi + if test -s ${probstatement} + then + probname=`head -1 ${probstatement}` + probname=${probname##*{} + probname=${probname%\}} + fi + fi + problem="$problet - $probname (${shortname})" + echo ''"Run $runidteam$teamnum$problem$langid$runtime" +} + +###ParseProblemYaml +Preamble +Header +StartTable +# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid +for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+$' | sort --field-separator=_ +1rn` +do + # exdir looks like: ex_188_Y_compression_46103_cpp + # RId P ProbShort team# Lang + # RId = Run ID + # P = problem letter + # Lang = CLICS Language id + saveIFS="$IFS" + IFS="_" + set ${exdir} + IFS="$saveIFS" + if test $# -eq 6 + then + runid=$2 + problet=$3 + probshort=$4 + teamnum=$5 + langid=$6 + TableRow "${PC2_RUN_DIR}/$exdir" $runid $problet $probshort $langid $teamnum + fi +done +EndTable +Trailer +exit 0 diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh new file mode 100644 index 000000000..c4bfbe7b0 --- /dev/null +++ b/support/judge_webcgi/showrun.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# +# Display very curt textual error message +# +Error() +{ + echo "Content-type: text/plain" + echo "" + echo ERROR $* + echo $now Error: $* >> $LOGFILE +} + +Preamble() +{ + echo "Content-type: text/html" + echo "" +} + +Header() +{ +cat << EOF + + + +PC² Judge 1 + + +

+

PC2 Judging Results for Judge 1

+
+

+EOF +} + +Trailer() +{ +cat << EOF2 + + +EOF2 +} + +StartTable() +{ + cat << EOF3 +

+ + + +EOF3 +} + +EndTable() +{ + cat << EOF4 +
Run IDTime Judged
+

+EOF4 +} + +TableRow() +{ + dir="$1" + runid=${dir#../Run} + runtime=`stat -c '%y' $dir` + echo ''"Run $runid$runtime" +} + +Preamble +Header + +# Parse query string into dictionary args +sIFS="$IFS" +IFS='=&' +declare -a parm +parm=($QUERY_STRING) +IFS=$sIFS +declare -A args +for ((i=0; i<${#parm[@]}; i+=2)) +do + args[${parm[i]}]=${parm[i+1]} + echo "${parm[i]} = ${args[${parm[i]}]}
" +done +# Done parsing + +echo '

The run is '${args["run"]} in problem directory ${args["probdir"]}. The execute folder is: ${args["dir"]} +Trailer +exit 0 From 525e503b73175dcd9d8c15e9b01c683de4f7b258 Mon Sep 17 00:00:00 2001 From: John Buck Date: Sun, 30 Jun 2024 21:36:18 -0400 Subject: [PATCH 28/55] i_972 Add script to fetch judgement from execute folder Ongoing work in making human judging of results easier. --- support/judge_webcgi/getjudgment.sh | 114 ++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 support/judge_webcgi/getjudgment.sh diff --git a/support/judge_webcgi/getjudgment.sh b/support/judge_webcgi/getjudgment.sh new file mode 100644 index 000000000..b24514dfc --- /dev/null +++ b/support/judge_webcgi/getjudgment.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# Where to the the result of the first failure. If this file is not created, then the +# run was accepted. (correct) +RESULT_FAILURE_FILE=failure.txt + +# Where judgments are +REJECT_INI=$HOME/pc2/reject.ini + +# Where PC2 puts CLICS validator results +EXECUTE_DATA_PREFIX=executedata + +declare -A Judgments + +InitJudgments() +{ + # Defauls, may be overriden by reject.ini + Judgments["accepted"]="AC" + Judgments["Accepted"]="AC" + Judgments["timelimit"]="TLE" + Judgments["run error"]="RTE" + Judgments["compiler error"]="CE" + if test -s ${REJECT_INIT} + then + while read j + do + if [[ $j = \#* ]] + then + continue + fi + savIFS="$IFS" + IFS='|' + set $j + IFS="$savIFS" + key="$1" + shortcode="$2" + case ${shortcode} in + AC|CE|RTE|WA|TLE) ;; + MLE) shortcode="RTE (MLE)" ;; + *) shortcode="WA (${shortcode})" ;; + esac +# echo Mapping $key to $shortcode + Judgments[$key]="$shortcode" + done < $REJECT_INI + fi +} + +# Takes the judgement string, eg. "Wrong answer" as arg 1 and the validation Result (42 or 42) as arg 2 +MapJudgment() +{ + jm="$1" + vr="$2" + jv=${Judgments[$jm]} + if test -z ${jv} + then + if test ${validationReturnCode} -eq 0 + then + if test ${vr} = "43" + then + jv="WA" + elif test ${vr} = "42" + then + jv="AC" + else + jv="WA (Default)" + fi + else + jv="JE (Validator EC=${validationReturnCode})" + fi + + fi + echo $jv +} + +GetJudgment() +{ + dir=$1 + if ! cd ${dir} + then + echo "Not found" + else + # We got a real live run + # Check out the biggest executedata file + exdata=`ls ${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sort -t. +1rn | head -1` + if test -z "${exdata}" + then + echo "No results" + else + # Source the file + . ./${exdata} + if test ${compileSuccess} = "false" + then + echo "CE" + elif test ${executeSuccess} = "true" + then + if test ${validationSuccess} = "true" + then + MapJudgment "${validationResults}" "${validationReturnCode}" + else + echo "JE (Validator error)" + fi + else + echo "RTE (Execute error)" + fi + fi + fi +} + +InitJudgments + +for file in $* +do + j=`GetJudgment $file` + echo $file: $j +done From 16e0652a3175bb6a8c85ed9044f9a468577a27a4 Mon Sep 17 00:00:00 2001 From: John Buck Date: Mon, 1 Jul 2024 16:39:45 -0400 Subject: [PATCH 29/55] i_972 Update judge web scripts More updates to make judging easier. Add judge number support {:clientid} to execute folder in scripts --- support/judge_webcgi/Correct.png | Bin 0 -> 46323 bytes support/judge_webcgi/Wrong.png | Bin 0 -> 47549 bytes support/judge_webcgi/cgi-bin/cdpcommon.sh | 21 +++ support/judge_webcgi/cgi-bin/judge.sh | 120 ++++++++++++ .../{getjudgment.sh => cgi-bin/pc2common.sh} | 83 +++++--- support/judge_webcgi/cgi-bin/problemyaml.sh | 26 +++ support/judge_webcgi/cgi-bin/showrun.sh | 25 +++ support/judge_webcgi/cgi-bin/webcommon.sh | 154 +++++++++++++++ support/judge_webcgi/judge | 178 ------------------ support/judge_webcgi/showrun.sh | 106 ----------- 10 files changed, 406 insertions(+), 307 deletions(-) create mode 100644 support/judge_webcgi/Correct.png create mode 100644 support/judge_webcgi/Wrong.png create mode 100644 support/judge_webcgi/cgi-bin/cdpcommon.sh create mode 100644 support/judge_webcgi/cgi-bin/judge.sh rename support/judge_webcgi/{getjudgment.sh => cgi-bin/pc2common.sh} (51%) create mode 100644 support/judge_webcgi/cgi-bin/problemyaml.sh create mode 100644 support/judge_webcgi/cgi-bin/showrun.sh create mode 100644 support/judge_webcgi/cgi-bin/webcommon.sh delete mode 100644 support/judge_webcgi/judge delete mode 100644 support/judge_webcgi/showrun.sh diff --git a/support/judge_webcgi/Correct.png b/support/judge_webcgi/Correct.png new file mode 100644 index 0000000000000000000000000000000000000000..6e73ae8871b9ebb40efe1d237d97388b6855c7b6 GIT binary patch literal 46323 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

|;6-?wa-BR;twrttn%Og)F3)$0E7z!E*^cI3 z1=N+VU8?lOzbQ{YWIGBEcxc$3jo%_}U2phTEZ}=ZmmruU$#~3a{~sFOoj4Wc#qyQ~ zS-D64xB%aw<$1oAP|hpVBg$gYzb;!<(eP!D?&(9P209CXrM>CW?~&$Cjt%(Dw>~fT zuskL5RLs!mH+{3CNBrgdMzvc6#MW z@NIE)_%(i51(0t?p**lZ)Jyx8jeJS#hLjaPZR$?aihNXai$&Yd0E)X}TSioT6{`n`8tYed&5SVqy9u zfIG==e^0(2r?Tz0s0?KfRrae`gBbb_^o)?cry0L@Io@7NB$lwFvSrlpO7p;LEBmld z#0o7#USd@nG|JTyZt@%nOFoF(`mXTV=|-S{{&g>#=$`APxm*X<_lankb9t6G!Z&W) zlXn@{f_miqTOCIYq8DTd{E5xgDrBhn8jdeFdBT*KPy#G&*? zz)FpB(%2(^na6qr#v6#pea}Sln}X>C)^q8mQ%zn*-6G)LN{hdUv>(b`BeS;&euqyf zJuw0V(I$1vn$mA3vm?sK4euP7Dg^yU(a7msQ%y35ca8Azd^cBKea^Efi`wqb%rRm} zbsAbhyO?6hXWf<&XZ-GzLZbgvsjhh!dDrqtET-#n0;{M>gynz&O@vg3WiILDbsb)q zURS_AHN}CS=6E}Uq8!CM%;S4wtxLLvFM%Q{b$cofw|#iO;t{S^jj^^qzaxpaLSm`w z;PbWH>=w6-xold4D$sVVOP=59*glihsO@o^DSs~!#+!7H73{xp*y5-r=NdeZqRy|= zTL}N~X_m+!xU!1cWIfZ`+j<2)a|jT?^bFIlVIJzDCa4X$~cMXbjSy>6=|J8;no za(WHFU-nxFya1zV{uz-|T&>{$>FQlMF&sH(#*-fGL%E#3YbO3O#A;Cgqh)nM6{*5M zCHw~K)}H`%ipi%1QOq_RsyLur`Jw1f!r+m0PDhe&47mOio{kk2yX{k=GQC+AeE(iF zf%rDrmU8)8q!N!$98K;d>sF=%xSfn5nGQ+QTF1UEPE#@8x&z-nWp*JiX+=HC`e^%Y zN+US*56jnEEWG!mCfx0KdqSQ6Ag@3UKhk~lc(&-2$JEWl86I@nT;crVCZ))OtuGd+ z#q%OQ5&6|WLm^ST2fli_QKrW-0v{@IA=4&~;Jp{fhzhnrdn{lf3 z`Hfl{0r;Uv*1;fhiAqx|oTS?Ii3Jsv8|oy>vDvR-=xh5IfCB^itE`> z-S5P0p8oTNDZN6*H6pbSxJ*lyu<+Fy$Yc=qs!ht;itib{a zx=nHomyRA*T`6JMLJt39E#uo<_kGJG9b2NMkj8bl9S@x%V5G8NyLdLIx5)SK>knE( zPvz)NhD=+@&7EnAb5UuBg|FL<4MKyIh{_3vOqQMh`oZBd6Di?Rmrf|JQR4s9?zGON zlJ1~tCo~mOmUvXm@?(rkWJM|v_a19?m zF?0yNn*f~kxRwU+C#(g=w|@~J-blA@+UzO~TPu{q`wq7mHP|kRs+2uFXJ4O06i^>% t95lL2a2pBJ@;(LSPX9mth*9m-vvue06_I#VjUWN2C~7K{$Upz*{{ZoLNEZMA literal 0 HcmV?d00001 diff --git a/support/judge_webcgi/Wrong.png b/support/judge_webcgi/Wrong.png new file mode 100644 index 0000000000000000000000000000000000000000..43401f44b4516d7f934179af25ca905ef07fbf9c GIT binary patch literal 47549 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 literal 0 HcmV?d00001 diff --git a/support/judge_webcgi/cgi-bin/cdpcommon.sh b/support/judge_webcgi/cgi-bin/cdpcommon.sh new file mode 100644 index 000000000..e1ced5e2f --- /dev/null +++ b/support/judge_webcgi/cgi-bin/cdpcommon.sh @@ -0,0 +1,21 @@ +# This file is meant to be 'source'd' to give functionality to read the CDP + +CountSamples() +{ + datadir="$1" + if test -d ${datadir} + then + result=`ls $datadir/*.in | wc -l` + else + result=0 + fi +} + +GetNumberOfTestCases() +{ + probdir="$1" + CountSamples $probdir/data/sample + tot=$result + CountSamples $probdir/data/secret + result=$((tot+result)) +} diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh new file mode 100644 index 000000000..5c9d129a0 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -0,0 +1,120 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + +TableHeader() +{ + cat << EOFTH + +Run IDDispJudgmentProblemTeamTest CasesLanguageJudgeTime Judged + +EOFTH +} + +TableRow() +{ + dir="$1" + runid=$2 + problet=$3 + shortname=$4 + langid=$5 + teamnum=$6 + judgment="$7" + runtime="$8" + testinfo="$9" + judge="${10}" + probname="" + probdir="" + if test -n ${shortname} + then + probdir=${PC2_CDP}/${shortname} + probstatement=${probdir}/problem_statement/problem.en.tex + if test ! -s ${probstatement} + then + probstatement=${probdir}/problem_statement/problem.tex + fi + if test -s ${probstatement} + then + probname=`head -1 ${probstatement}` + probname=${probname##*\{} + probname=${probname%\}} + fi + fi + problem="$problet - $probname (${shortname})" + if test "${judgment}" = "AC" + then + jstyle="green" + jicon='' + else + jstyle="red" + jicon='' + fi + echo ' ' + echo ' '"Run $runid" +# echo ' '$judgment"" +# echo '

'$judgment"
" + echo ' '$jicon'' + echo ' '$judgment'' + echo " $problem" + echo " team$teamnum" + echo " $testinfo" + echo " $langid" + echo " $judge" + echo " $runtime" + echo " " +} + +###ParseProblemYaml +Preamble +Header +StartTable +TableHeader + +# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid_judge +for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+' | sort --field-separator=_ +1rn` +do + # exdir looks like: ex_188_Y_compression_46103_cpp + # RId P ProbShort team# Lang + # RId = Run ID + # P = problem letter + # Lang = CLICS Language id + saveIFS="$IFS" + IFS="_" + set ${exdir} + IFS="$saveIFS" + if test $# -ge 6 + then + exedir=${PC2_RUN_DIR}/$exdir + runid=$2 + problet=$3 + probshort=$4 + teamnum=$5 + langid=$6 + judge=$7 + if test -z "${judge}" + then + judge="N/A" + fi + GetJudgment "${exedir}" + judgment="${result}" + runtime="${executeDateTime}" + # Get how many total test cases there are + probdir=${PC2_CDP}/${probshort} + if test -n "${probdir}" + then + GetNumberOfTestCases "${probdir}" + numcases=${result} + else + numcases="??" + fi + # Note that GetJudgment also filled in exdata with the last execute data + GetLastTestCaseNumber "${exdata}" + testcaseinfo=$((result+1))/${numcases} + + TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" + fi +done +EndTable +Trailer +exit 0 diff --git a/support/judge_webcgi/getjudgment.sh b/support/judge_webcgi/cgi-bin/pc2common.sh similarity index 51% rename from support/judge_webcgi/getjudgment.sh rename to support/judge_webcgi/cgi-bin/pc2common.sh index b24514dfc..3db5cd6c4 100644 --- a/support/judge_webcgi/getjudgment.sh +++ b/support/judge_webcgi/cgi-bin/pc2common.sh @@ -1,10 +1,21 @@ -#!/bin/bash -# Where to the the result of the first failure. If this file is not created, then the -# run was accepted. (correct) +# Meant to be "source'd" into bash scripts. + +# The judge's home directory +JUDGE_HOME=/home/icpc + +# Modify constants as necessary for your installation +PC2_RUN_DIR=${JUDGE_HOME}/pc2 +PC2_CDP=${PC2_RUN_DIR}/current/config + +# Where can we find the contest banner png file for webpage headers +BANNER_FILE=banner.png +BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} + +# Where to the the result of script failure before execution RESULT_FAILURE_FILE=failure.txt # Where judgments are -REJECT_INI=$HOME/pc2/reject.ini +REJECT_INI=${JUDGE_HOME}/pc2/reject.ini # Where PC2 puts CLICS validator results EXECUTE_DATA_PREFIX=executedata @@ -19,7 +30,7 @@ InitJudgments() Judgments["timelimit"]="TLE" Judgments["run error"]="RTE" Judgments["compiler error"]="CE" - if test -s ${REJECT_INIT} + if test -s ${REJECT_INI} then while read j do @@ -41,6 +52,7 @@ InitJudgments() # echo Mapping $key to $shortcode Judgments[$key]="$shortcode" done < $REJECT_INI + else echo NO REJECT FILE fi } @@ -49,57 +61,87 @@ MapJudgment() { jm="$1" vr="$2" - jv=${Judgments[$jm]} - if test -z ${jv} + result=${Judgments[$jm]} + if test -z ${result} then if test ${validationReturnCode} -eq 0 then if test ${vr} = "43" then - jv="WA" + resul="WA" elif test ${vr} = "42" then - jv="AC" + result="AC" else - jv="WA (Default)" + result="WA (Default)" fi else - jv="JE (Validator EC=${validationReturnCode})" + result="JE (Validator EC=${validationReturnCode})" fi fi - echo $jv +} + +GetLastJudgmentFile() +{ + lj_exdir="$1" + result=`ls $lj_exdir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1rn | head -1` +} + +GetLastTestCaseNumber() +{ + if test -z "$1" + then + result=0 + else + saveIFS="$IFS" + IFS=. + set ${1} + result="$2" + IFS="$saveIFS" + if test -z "${result}" + then + result=0 + fi + fi } GetJudgment() { dir=$1 + exdata="" if ! cd ${dir} then - echo "Not found" + result="Not found" + elif test -s ${RESULT_FAILURE_FILE} + then + jerr=`cat ${RESULT_FAILURE_FILE}` + result="JE ($jerr)" else # We got a real live run # Check out the biggest executedata file - exdata=`ls ${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sort -t. +1rn | head -1` +# exdata=`ls ${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sort -t. +1rn | head -1` + GetLastJudgmentFile $dir + exdata=${result} if test -z "${exdata}" then - echo "No results" + result="No results" else # Source the file . ./${exdata} if test ${compileSuccess} = "false" then - echo "CE" + result="CE" elif test ${executeSuccess} = "true" then if test ${validationSuccess} = "true" then MapJudgment "${validationResults}" "${validationReturnCode}" else - echo "JE (Validator error)" + result="JE (Validator error)" fi else - echo "RTE (Execute error)" + result="RTE (Execute error)" fi fi fi @@ -107,8 +149,3 @@ GetJudgment() InitJudgments -for file in $* -do - j=`GetJudgment $file` - echo $file: $j -done diff --git a/support/judge_webcgi/cgi-bin/problemyaml.sh b/support/judge_webcgi/cgi-bin/problemyaml.sh new file mode 100644 index 000000000..b1b2154d9 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/problemyaml.sh @@ -0,0 +1,26 @@ +###PROBLEMSET_YAML=problemset.yaml +###PROBLEMSET_YAML_PATH=${PC2_CDP}/${PROBLEMSET_YAML} + +###declare -A problet_to_name + +###ParseProblemYaml() +###{ +### CURDIR="$PWD" +### tmpdir=/tmp/probset$$ +### mkdir $tmpdir +### # Need a copy since web doesnt have access to full path +### cp ${PROBLEMSET_YAML_PATH} $tmpdir +### cd $tmpdir +### csplit --prefix="x$USER" -q ${PROBLEMSET_YAML} "/^ *- /" "{*}" +### for file in $(echo "x$USER"*) +### do +### letter=`sed -n -e 's/^ *letter: \([A-Z]\).*/\1/p' < $file` +### short=`sed -n -e 's/.* short-name: \(.*\)$/\1/p' < $file` +### if test -n "$letter" -a -n "$short" +### then +### problet_to_name[$letter]="$short" +### fi +### done +### cd $CURDIR +### rm -r $tmpdir +###} diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh new file mode 100644 index 000000000..03e31042a --- /dev/null +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -0,0 +1,25 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + + +# Parse query string into dictionary args +# This is not terribly secure. +declare -a parm +sIFS="$IFS" +IFS='=&' +parm=($QUERY_STRING) +IFS=$sIFS +for ((i=0; i<${#parm[@]}; i+=2)) +do + a=${parm[i]} + eval $a=${parm[i+1]} +done +# Done parsing + +Preamble - 'Run '$run +HeaderNoBanner Details for RunId $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" + +Trailer +exit 0 diff --git a/support/judge_webcgi/cgi-bin/webcommon.sh b/support/judge_webcgi/cgi-bin/webcommon.sh new file mode 100644 index 000000000..fef94f341 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/webcommon.sh @@ -0,0 +1,154 @@ +# File meant to be "source'd" to support generating web pages + +# +# Display very curt textual error message +# +Error() +{ + echo "Content-type: text/plain" + echo "" + echo ERROR $* +} + + +Preamble() +{ + echo "Content-type: text/html" + echo "" + if test $# -eq 0 + then + headmsg="Judge" + else + headmsg="$*" + fi + cat << PREEOF + + + +PC² $headmsg + + + +PREEOF +} + +Header() +{ + # Make sure link to banner page is in a place apache can read + if test ! -e ../${BANNER_FILE} + then + ln -s ${BANNER_IMAGE} ../${BANNER_FILE} + fi +cat << HDREOF +
+ +

PC2 Judging Results

+
+

+HDREOF +} + +HeaderNoBanner() +{ + if test $# -eq 0 + then + hdrmsg="Judging Results" + else + hdrmsg="$*" + fi +cat << EOF +

+

PC2 $hdrmsg

+
+

+EOF +} + +Trailer() +{ +cat << EOF2 + + +EOF2 +} + +StartTable() +{ + cat << EOF3 +

+ +EOF3 +} + +EndTable() +{ + cat << EOF4 +
+

+EOF4 +} + diff --git a/support/judge_webcgi/judge b/support/judge_webcgi/judge deleted file mode 100644 index 3d850abff..000000000 --- a/support/judge_webcgi/judge +++ /dev/null @@ -1,178 +0,0 @@ -#!/bin/bash -PC2_RUN_DIR=/home/icpc/pc2 -PC2_CDP=${PC2_RUN_DIR}/current/config -BANNER_FILE=banner.png -BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} -###PROBLEMSET_YAML=problemset.yaml -###PROBLEMSET_YAML_PATH=${PC2_CDP}/${PROBLEMSET_YAML} - -###declare -A problet_to_name - -# -# Display very curt textual error message -# -Error() -{ - echo "Content-type: text/plain" - echo "" - echo ERROR $* - echo $now Error: $* >> $LOGFILE -} - -###ParseProblemYaml() -###{ -### CURDIR="$PWD" -### tmpdir=/tmp/probset$$ -### mkdir $tmpdir -### # Need a copy since web doesnt have access to full path -### cp ${PROBLEMSET_YAML_PATH} $tmpdir -### cd $tmpdir -### csplit --prefix="x$USER" -q ${PROBLEMSET_YAML} "/^ *- /" "{*}" -### for file in $(echo "x$USER"*) -### do -### letter=`sed -n -e 's/^ *letter: \([A-Z]\).*/\1/p' < $file` -### short=`sed -n -e 's/.* short-name: \(.*\)$/\1/p' < $file` -### if test -n "$letter" -a -n "$short" -### then -### problet_to_name[$letter]="$short" -### fi -### done -### cd $CURDIR -### rm -r $tmpdir -###} - -Preamble() -{ - echo "Content-type: text/html" - echo "" -} - -Header() -{ - if test ! -e ../${BANNER_FILE} - then - ln -s ${BANNER_IMAGE} ../${BANNER_FILE} - fi -cat << EOF - - - -PC² Judge 1 - - - -

- -

PC2 Judging Results for Judge 1

-
-

-EOF -} - -Trailer() -{ -cat << EOF2 - - -EOF2 -} - -StartTable() -{ - cat << EOF3 -

- - - -EOF3 -} - -EndTable() -{ - cat << EOF4 -
Run IDTeamProblemLanguageTime Judged
-

-EOF4 -} - -TableRow() -{ - dir="$1" - runid=$2 - problet=$3 - shortname=$4 - langid=$5 - teamnum=$6 - runtime=`stat -c '%y' $dir` - probname="" - probdir="" - if test -n ${shortname} - then - probdir=${PC2_CDP}/${shortname} - probstatement=${probdir}/problem_statement/problem.en.tex - if test ! -s ${probstatement} - then - probstatement=${probdir}/problem_statement/problem.tex - fi - if test -s ${probstatement} - then - probname=`head -1 ${probstatement}` - probname=${probname##*{} - probname=${probname%\}} - fi - fi - problem="$problet - $probname (${shortname})" - echo ''"Run $runidteam$teamnum$problem$langid$runtime" -} - -###ParseProblemYaml -Preamble -Header -StartTable -# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid -for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+$' | sort --field-separator=_ +1rn` -do - # exdir looks like: ex_188_Y_compression_46103_cpp - # RId P ProbShort team# Lang - # RId = Run ID - # P = problem letter - # Lang = CLICS Language id - saveIFS="$IFS" - IFS="_" - set ${exdir} - IFS="$saveIFS" - if test $# -eq 6 - then - runid=$2 - problet=$3 - probshort=$4 - teamnum=$5 - langid=$6 - TableRow "${PC2_RUN_DIR}/$exdir" $runid $problet $probshort $langid $teamnum - fi -done -EndTable -Trailer -exit 0 diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh deleted file mode 100644 index c4bfbe7b0..000000000 --- a/support/judge_webcgi/showrun.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/bin/bash -# -# Display very curt textual error message -# -Error() -{ - echo "Content-type: text/plain" - echo "" - echo ERROR $* - echo $now Error: $* >> $LOGFILE -} - -Preamble() -{ - echo "Content-type: text/html" - echo "" -} - -Header() -{ -cat << EOF - - - -PC² Judge 1 - - -

-

PC2 Judging Results for Judge 1

-
-

-EOF -} - -Trailer() -{ -cat << EOF2 - - -EOF2 -} - -StartTable() -{ - cat << EOF3 -

- - - -EOF3 -} - -EndTable() -{ - cat << EOF4 -
Run IDTime Judged
-

-EOF4 -} - -TableRow() -{ - dir="$1" - runid=${dir#../Run} - runtime=`stat -c '%y' $dir` - echo ''"Run $runid$runtime" -} - -Preamble -Header - -# Parse query string into dictionary args -sIFS="$IFS" -IFS='=&' -declare -a parm -parm=($QUERY_STRING) -IFS=$sIFS -declare -A args -for ((i=0; i<${#parm[@]}; i+=2)) -do - args[${parm[i]}]=${parm[i+1]} - echo "${parm[i]} = ${args[${parm[i]}]}
" -done -# Done parsing - -echo '

The run is '${args["run"]} in problem directory ${args["probdir"]}. The execute folder is: ${args["dir"]} -Trailer -exit 0 From 7ba0ceaa5a27cbac2f8755a14ee6375a1b628987 Mon Sep 17 00:00:00 2001 From: John Buck Date: Tue, 2 Jul 2024 07:11:02 -0400 Subject: [PATCH 30/55] i_972 Updated judge help web scripts Also added Warning icon --- support/judge_webcgi/Warning.png | Bin 0 -> 11345 bytes support/judge_webcgi/cgi-bin/judge.sh | 3 + support/judge_webcgi/cgi-bin/pc2common.sh | 57 +++++----- support/judge_webcgi/cgi-bin/showrun.sh | 94 +++++++++++++++++ support/judge_webcgi/judge.sh | 123 ++++++++++++++++++++++ 5 files changed, 253 insertions(+), 24 deletions(-) create mode 100644 support/judge_webcgi/Warning.png create mode 100644 support/judge_webcgi/judge.sh diff --git a/support/judge_webcgi/Warning.png b/support/judge_webcgi/Warning.png new file mode 100644 index 0000000000000000000000000000000000000000..e33a821d9fdac2891f6d08ee75b1c2bb3d61001f GIT binary patch literal 11345 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 VRFJlT1#k=)q$sN@QzvZ}`hRe3YU}_2 literal 0 HcmV?d00001 diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh index 5c9d129a0..c4bbd86ca 100644 --- a/support/judge_webcgi/cgi-bin/judge.sh +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -46,6 +46,9 @@ TableRow() then jstyle="green" jicon='' + elif test "${judgment}" = "CE" + then + jicon='' else jstyle="red" jicon='' diff --git a/support/judge_webcgi/cgi-bin/pc2common.sh b/support/judge_webcgi/cgi-bin/pc2common.sh index 3db5cd6c4..082b1770b 100644 --- a/support/judge_webcgi/cgi-bin/pc2common.sh +++ b/support/judge_webcgi/cgi-bin/pc2common.sh @@ -19,6 +19,11 @@ REJECT_INI=${JUDGE_HOME}/pc2/reject.ini # Where PC2 puts CLICS validator results EXECUTE_DATA_PREFIX=executedata +# Where PC2 puts run output/error +TEST_OUT_PREFIX="teamoutput" +TEST_ERR_PREFIX="teamstderr" +TEST_VALOUT_PREFIX="valout" +TEST_VALERR_PREFIX="valerr" declare -A Judgments @@ -88,7 +93,7 @@ GetLastJudgmentFile() result=`ls $lj_exdir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1rn | head -1` } -GetLastTestCaseNumber() +GetTestCaseNumber() { if test -z "$1" then @@ -106,6 +111,32 @@ GetLastTestCaseNumber() fi } +GetJudgmentFromFile() +{ + exdata="$1" + if test -z "${exdata}" + then + result="No results" + else + # Source the file + . ${exdata} + if test ${compileSuccess} = "false" + then + result="CE" + elif test ${executeSuccess} = "true" + then + if test ${validationSuccess} = "true" + then + MapJudgment "${validationResults}" "${validationReturnCode}" + else + result="JE (Validator error)" + fi + else + result="RTE (Execute error)" + fi + fi +} + GetJudgment() { dir=$1 @@ -120,30 +151,8 @@ GetJudgment() else # We got a real live run # Check out the biggest executedata file -# exdata=`ls ${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sort -t. +1rn | head -1` GetLastJudgmentFile $dir - exdata=${result} - if test -z "${exdata}" - then - result="No results" - else - # Source the file - . ./${exdata} - if test ${compileSuccess} = "false" - then - result="CE" - elif test ${executeSuccess} = "true" - then - if test ${validationSuccess} = "true" - then - MapJudgment "${validationResults}" "${validationReturnCode}" - else - result="JE (Validator error)" - fi - else - result="RTE (Execute error)" - fi - fi + GetJudgmentFromFile ./${result} fi } diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh index 03e31042a..38b3cf7f7 100644 --- a/support/judge_webcgi/cgi-bin/showrun.sh +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -1,8 +1,82 @@ #!/bin/bash +EXE_DIR_LINK=../exedir$$ +PROB_DIR_LINK=../probdir$$ + . ./pc2common.sh . ./webcommon.sh . ./cdpcommon.sh +TableHeader() +{ + cat << EOFTH + +TestDispJudgmentExitCompile TimeExecute TimeVal TimeVal SuccessRun OutRun ErrJudge InJudge AnsVal OutVal Err + +EOFTH +} + +GenFileLink() +{ + tstcase="$2" + tstcase=$((tstcase-1)) + tstfile=$1.$tstcase.txt + tstpath=$dir/$1.$tstcase.txt + if test -s ${tstpath} + then + bytes=`stat -c %s ${tstpath}` + echo ' View ('$bytes' bytes)' + elif test -e ${tstpath} + then + echo ' (Empty)' + else + echo ' Not found' + fi +} + +TableRow() +{ + tc=$1 + judgment="$2" + ec=$3 + comptm="$4" + exetm="$5" + valtm="$6" + if test "$7" = "true" + then + valsucc=Yes + elif test "$7" = "false" + then + valsucc=No + else + valsucc="N/A" + fi + if test "${judgment}" = "AC" + then + jicon='' + elif test "${judgment}" = "CE" + then + jicon='' + else + jicon='' + fi + + echo ' ' + + echo ' '$tc'' + echo ' '$jicon'' + echo ' '$judgment'' + echo ' '$ec'' + echo ' '$comptm'ms' + echo ' '$exetm'ms' + echo ' '$valtm'ms' + echo ' '$valsucc'' + GenFileLink $TEST_OUT_PREFIX $tc + GenFileLink $TEST_ERR_PREFIX $tc + GenFileLink $TEST_VALOUT_PREFIX $tc + GenFileLink $TEST_VALERR_PREFIX $tc + + echo ' ' +} # Parse query string into dictionary args # This is not terribly secure. @@ -21,5 +95,25 @@ done Preamble - 'Run '$run HeaderNoBanner Details for RunId $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" +StartTable +TableHeader +rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} +ln -s ${dir} ${EXE_DIR_LINK} +ln -s ${probdir} ${PROB_DIR_LINK} +for testcase in `ls $dir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1n` +do + GetTestCaseNumber $testcase + tc=$((result+1)) + # This will also source the execute data + GetJudgmentFromFile $dir/$testcase + judgment=$result + ec=$executeExitValue + comptm=$compileTimeMS + exetm=$executeTimeMS + valtm=$validateTimeMS + valsuc=$validationSuccess + TableRow "$tc" "$judgment" "$ec" "$comptm" "$exetm" "$valtm" "$valsuc" +done + Trailer exit 0 diff --git a/support/judge_webcgi/judge.sh b/support/judge_webcgi/judge.sh new file mode 100644 index 000000000..c4bbd86ca --- /dev/null +++ b/support/judge_webcgi/judge.sh @@ -0,0 +1,123 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + +TableHeader() +{ + cat << EOFTH + +Run IDDispJudgmentProblemTeamTest CasesLanguageJudgeTime Judged + +EOFTH +} + +TableRow() +{ + dir="$1" + runid=$2 + problet=$3 + shortname=$4 + langid=$5 + teamnum=$6 + judgment="$7" + runtime="$8" + testinfo="$9" + judge="${10}" + probname="" + probdir="" + if test -n ${shortname} + then + probdir=${PC2_CDP}/${shortname} + probstatement=${probdir}/problem_statement/problem.en.tex + if test ! -s ${probstatement} + then + probstatement=${probdir}/problem_statement/problem.tex + fi + if test -s ${probstatement} + then + probname=`head -1 ${probstatement}` + probname=${probname##*\{} + probname=${probname%\}} + fi + fi + problem="$problet - $probname (${shortname})" + if test "${judgment}" = "AC" + then + jstyle="green" + jicon='' + elif test "${judgment}" = "CE" + then + jicon='' + else + jstyle="red" + jicon='' + fi + echo ' ' + echo ' '"Run $runid" +# echo ' '$judgment"" +# echo '

'$judgment"
" + echo ' '$jicon'' + echo ' '$judgment'' + echo " $problem" + echo " team$teamnum" + echo " $testinfo" + echo " $langid" + echo " $judge" + echo " $runtime" + echo " " +} + +###ParseProblemYaml +Preamble +Header +StartTable +TableHeader + +# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid_judge +for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+' | sort --field-separator=_ +1rn` +do + # exdir looks like: ex_188_Y_compression_46103_cpp + # RId P ProbShort team# Lang + # RId = Run ID + # P = problem letter + # Lang = CLICS Language id + saveIFS="$IFS" + IFS="_" + set ${exdir} + IFS="$saveIFS" + if test $# -ge 6 + then + exedir=${PC2_RUN_DIR}/$exdir + runid=$2 + problet=$3 + probshort=$4 + teamnum=$5 + langid=$6 + judge=$7 + if test -z "${judge}" + then + judge="N/A" + fi + GetJudgment "${exedir}" + judgment="${result}" + runtime="${executeDateTime}" + # Get how many total test cases there are + probdir=${PC2_CDP}/${probshort} + if test -n "${probdir}" + then + GetNumberOfTestCases "${probdir}" + numcases=${result} + else + numcases="??" + fi + # Note that GetJudgment also filled in exdata with the last execute data + GetLastTestCaseNumber "${exdata}" + testcaseinfo=$((result+1))/${numcases} + + TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" + fi +done +EndTable +Trailer +exit 0 From d40bd84fd53aa1ab160a53066c602478d1239753 Mon Sep 17 00:00:00 2001 From: John Buck Date: Tue, 2 Jul 2024 16:43:06 -0400 Subject: [PATCH 31/55] i_972 More web script updates --- scripts/pc2sandbox.sh | 2 + support/judge_webcgi/judge.sh | 1 + support/judge_webcgi/pc2common.sh | 211 ++++++++++++++++++++++++++++++ support/judge_webcgi/showrun.sh | 209 +++++++++++++++++++++++++++++ support/judge_webcgi/webcommon.sh | 157 ++++++++++++++++++++++ 5 files changed, 580 insertions(+) create mode 100644 support/judge_webcgi/pc2common.sh create mode 100644 support/judge_webcgi/showrun.sh create mode 100644 support/judge_webcgi/webcommon.sh diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh index 08d536e93..f45237d41 100755 --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -379,6 +379,8 @@ REPORT Setting maximum user processes to $MAXPROCS ulimit -u $MAXPROCS # Keep track of details for reports +REPORT_BRIEF ${JUDGEIN} +REPORT_BRIEF ${JUDGEANS} REPORT_BRIEF $cpunum REPORT_BRIEF $$ REPORT_BRIEF $(date "+%F %T.%6N") diff --git a/support/judge_webcgi/judge.sh b/support/judge_webcgi/judge.sh index c4bbd86ca..e1b6bb740 100644 --- a/support/judge_webcgi/judge.sh +++ b/support/judge_webcgi/judge.sh @@ -71,6 +71,7 @@ TableRow() ###ParseProblemYaml Preamble Header +LogButton StartTable TableHeader diff --git a/support/judge_webcgi/pc2common.sh b/support/judge_webcgi/pc2common.sh new file mode 100644 index 000000000..54723de21 --- /dev/null +++ b/support/judge_webcgi/pc2common.sh @@ -0,0 +1,211 @@ +# Meant to be "source'd" into bash scripts. + +# The judge's home directory +JUDGE_HOME=/home/icpc + +# Modify constants as necessary for your installation +PC2_RUN_DIR=${JUDGE_HOME}/pc2 +PC2_CDP=${PC2_RUN_DIR}/current/config + +EXE_LINK_PREFIX=../exedir +PROB_LINK_PREFIX=../probdir + +# How many exedir and probdir links to keep around +NUM_LINK_KEEP=4 + +# Where can we find the contest banner png file for webpage headers +BANNER_FILE=banner.png +BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} + +# Where to the the result of script failure before execution +RESULT_FAILURE_FILE=failure.txt + +# Where judgments are +REJECT_INI=${JUDGE_HOME}/pc2/reject.ini + +# Where PC2 puts CLICS validator results +EXECUTE_DATA_PREFIX=executedata +# Where PC2 puts run output/error +TEST_OUT_PREFIX="teamoutput" +TEST_ERR_PREFIX="teamstderr" +TEST_VALOUT_PREFIX="valout" +TEST_VALERR_PREFIX="valerr" + +# Detailed log of entire judging process +SANDBOX_LOG=sandbox.log + +# Briefcase file prefix +BRIEFCASE_FILE_PREFIX="briefcase" +REPORTS_DIR=reports + +declare -A Judgments + +InitJudgments() +{ + # Defauls, may be overriden by reject.ini + Judgments["accepted"]="AC" + Judgments["Accepted"]="AC" + Judgments["timelimit"]="TLE" + Judgments["run error"]="RTE" + Judgments["compiler error"]="CE" + if test -s ${REJECT_INI} + then + while read j + do + if [[ $j = \#* ]] + then + continue + fi + savIFS="$IFS" + IFS='|' + set $j + IFS="$savIFS" + key="$1" + shortcode="$2" + case ${shortcode} in + AC|CE|RTE|WA|TLE) ;; + MLE) shortcode="RTE (MLE)" ;; + *) shortcode="WA (${shortcode})" ;; + esac +# echo Mapping $key to $shortcode + Judgments[$key]="$shortcode" + done < $REJECT_INI + else echo NO REJECT FILE + fi +} + +# Takes the judgement string, eg. "Wrong answer" as arg 1 and the validation Result (42 or 42) as arg 2 +MapJudgment() +{ + jm="$1" + vr="$2" + result=${Judgments[$jm]} + if test -z ${result} + then + if test ${validationReturnCode} -eq 0 + then + if test ${vr} = "43" + then + resul="WA" + elif test ${vr} = "42" + then + result="AC" + else + result="WA (Default)" + fi + else + result="JE (Validator EC=${validationReturnCode})" + fi + + fi +} + +GetLastJudgmentFile() +{ + lj_exdir="$1" + result=`ls $lj_exdir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1rn | head -1` +} + +GetTestCaseNumber() +{ + if test -z "$1" + then + result=0 + else + saveIFS="$IFS" + IFS=. + set ${1} + result="$2" + IFS="$saveIFS" + if test -z "${result}" + then + result=0 + fi + fi +} + +GetJudgmentFromFile() +{ + exdata="$1" + if test -z "${exdata}" + then + result="No results" + else + # Source the file + . ${exdata} + if test ${compileSuccess} = "false" + then + result="CE" + elif test ${executeSuccess} = "true" + then + if test ${validationSuccess} = "true" + then + MapJudgment "${validationResults}" "${validationReturnCode}" + else + result="JE (Validator error)" + fi + else + result="RTE (Execute error)" + fi + fi +} + +GetJudgment() +{ + dir=$1 + exdata="" + if ! cd ${dir} + then + result="Not found" + elif test -s ${RESULT_FAILURE_FILE} + then + jerr=`cat ${RESULT_FAILURE_FILE}` + result="JE ($jerr)" + else + # We got a real live run + # Check out the biggest executedata file + GetLastJudgmentFile $dir + GetJudgmentFromFile ./${result} + fi +} + +MakeBriefcaseFile() +{ + d="$1" + t="$2" + result=`printf '%s/%s/%s_%03d.log' "$d" "$REPORTS_DIR" "$BRIEFCASE_FILE_PREFIX" "$t"` +} + +# Must redirect from briefcase file +ReadBriefcase() +{ + read judgein + read judgeans + read cpunum + read exepid + read exetime + read execpums cpulimms exewallms mempeak memlim + if [[ $mempeak = [0-9]* ]] + then + mempeak=$((mempeak/(1024*1024))) + fi + if [[ $memlim = [0-9]* ]] + then + memlim=$((memlim/(1024*1024))) + fi +} + +DeleteOldestLinks() +{ + for linkpref in ${EXE_LINK_PREFIX} ${PROB_LINK_PREFIX} + do + dellist=`ls -1td ${linkpref}* | sed 1,${NUM_LINK_KEEP}d` + if test -n "${dellist}" + then + rm -f ${dellist} + fi + done +} + +InitJudgments + diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh new file mode 100644 index 000000000..e88937dea --- /dev/null +++ b/support/judge_webcgi/showrun.sh @@ -0,0 +1,209 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + +EXE_DIR_LINK=${EXE_LINK_PREFIX}$$ +PROB_DIR_LINK=${PROB_LINK_PREFIX}$$ + +# Provide a way to look at the sandbox log +LogButton() +{ + # Read first judgment file to get compile time - it's compiled once for all + GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" + if test -n ${result} -a -n ${compileTimeMS} + then + cat << LBEOF0 +
+

The program took ${compileTimeMS}ms to compile. +

+LBEOF0 + fi + + # Read the first briefcase file (if any) for limits + MakeBriefcaseFile ${EXE_DIR_LINK} 1 + ReadBriefcase < $result + if test -n ${cpulimms} + then + cpulimms=${cpulimms%%.*} + cpusecs="$((cpulimms/1000))" + if test ${cpusecs} != "1" + then + cpusecs="${cpusecs} seconds" + else + cpusecs="1 second" + fi + cat << LBEOF1 +
+

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms). +

+LBEOF1 + fi + if test -n ${memlim} + then + if test ${memlim} = "0" + then + memlim="Unlimited" + else + memlim=${memlim}MB + fi + cat << LBEOF1A +
+

The Memory limit for this problem is ${memlim}. +

+LBEOF1A + fi + sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} + if test -s ${sandlog} + then + cat << LBEOF2 +Click here for the full sandbox log for this run +

+

+LBEOF2 + fi +} + +TableHeader() +{ + cat << EOFTH + +TestDispJudgmentExitExecute TimeVal TimeVal SuccessRun OutRun ErrJudge InJudge AnsVal OutVal Err + +EOFTH +} + +GenFileLink() +{ + tstcase="$2" + tstcase=$((tstcase-1)) + tstfile=$1.$tstcase.txt + tstpath=$dir/$1.$tstcase.txt + if test -s ${tstpath} + then + bytes=`stat -c %s ${tstpath}` + echo ' View ('$bytes' bytes)' + elif test -e ${tstpath} + then + echo ' (Empty)' + else + echo ' Not found' + fi +} + +GenFileLinkWithText() +{ + linkaddr="$1" + linktext="$2" + linkcolor="$3" + bytes=`stat -c %s ${linkaddr}` + echo ' '$linktext' ('$bytes' bytes)' +} + +TableRow() +{ + tc=$1 + judgment="$2" + ec=$3 + exetm="$4" + valtm="$5" + jin="$6" + jans="$7" + if test "$8" = "true" + then + valsucc=Yes + elif test "$8" = "false" + then + valsucc=No + else + valsucc="N/A" + fi + # Strip the stuff of before sample (or secret) + jin=${jin##*/data/} + jans=${jans##*/data/} + # Just the basenames for link text + jinbase=${jin##*/} + jansbase=${jans##*/} + if [[ $jin = sample/* ]] + then + lcolor=#00a0a0 + else + lcolor=#00a000 + fi + if test "${judgment}" = "AC" + then + jicon='' + elif test "${judgment}" = "CE" + then + jicon='' + else + jicon='' + fi + + echo ' ' + + echo ' '$tc'' + echo ' '$jicon'' + echo ' '$judgment'' + echo ' '$ec'' + echo ' '$exetm'ms' + echo ' '$valtm'ms' + echo ' '$valsucc'' + GenFileLink $TEST_OUT_PREFIX $tc + GenFileLink $TEST_ERR_PREFIX $tc + GenFileLinkWithText $PROB_DIR_LINK/data/"$jin" "$jinbase" $lcolor + GenFileLinkWithText $PROB_DIR_LINK/data/"$jans" "$jansbase" $lcolor + GenFileLink $TEST_VALOUT_PREFIX $tc + GenFileLink $TEST_VALERR_PREFIX $tc + + echo ' ' +} + +# Parse query string into dictionary args +# This is not terribly secure. +declare -a parm +sIFS="$IFS" +IFS='=&' +parm=($QUERY_STRING) +IFS=$sIFS +for ((i=0; i<${#parm[@]}; i+=2)) +do + a=${parm[i]} + eval $a=${parm[i+1]} +done +# Done parsing + +DeleteOldestLinks +Preamble - 'Run '$run +HeaderNoBanner Details for Run Id $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" + +# Create links apache can access in our html folder +#rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} +ln -s ${dir} ${EXE_DIR_LINK} +ln -s ${probdir} ${PROB_DIR_LINK} + +LogButton + +StartTable +TableHeader +for testcase in `ls $dir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1n` +do + GetTestCaseNumber $testcase + tc=$((result+1)) + # This will also source the execute data + GetJudgmentFromFile $dir/$testcase + judgment=$result + ec=$executeExitValue +# comptm=$compileTimeMS + exetm=$executeTimeMS + valtm=$validateTimeMS + valsuc=$validationSuccess + MakeBriefcaseFile "$dir" "$tc" + ReadBriefcase < ${result} + TableRow "$tc" "$judgment" "$ec" "$exetm" "$valtm" "$judgein" "$judgeans" "$valsuc" +done + +Trailer + +#rm -f ${EXE_DIR_LINK} ${PROB_DIR_LINK} +exit 0 diff --git a/support/judge_webcgi/webcommon.sh b/support/judge_webcgi/webcommon.sh new file mode 100644 index 000000000..26c65cd62 --- /dev/null +++ b/support/judge_webcgi/webcommon.sh @@ -0,0 +1,157 @@ +# File meant to be "source'd" to support generating web pages + +# +# Display very curt textual error message +# +Error() +{ + echo "Content-type: text/plain" + echo "" + echo ERROR $* +} + + +Preamble() +{ + echo "Content-type: text/html" + echo "" + if test $# -eq 0 + then + headmsg="Judge" + else + headmsg="$*" + fi + cat << PREEOF + + + +PC² $headmsg + + + +PREEOF +} + +Header() +{ + # Make sure link to banner page is in a place apache can read + if test ! -e ../${BANNER_FILE} + then + ln -s ${BANNER_IMAGE} ../${BANNER_FILE} + fi +cat << HDREOF +

+ +

PC2 Judging Results

+
+

+HDREOF +} + +HeaderNoBanner() +{ + if test $# -eq 0 + then + hdrmsg="Judging Results" + else + hdrmsg="$*" + fi +cat << EOF +

+

PC2 $hdrmsg

+
+

+EOF +} + +Trailer() +{ +cat << EOF2 + + +EOF2 +} + +StartTable() +{ + cat << EOF3 +

+ +EOF3 +} + +EndTable() +{ + cat << EOF4 +
+

+EOF4 +} + From be83f3e57937e04da5f3dc8586c508739295aabc Mon Sep 17 00:00:00 2001 From: John Buck Date: Sat, 6 Jul 2024 17:57:47 -0400 Subject: [PATCH 32/55] i_972 Update scripts for more detail Allow display of sandbox cpu time. Allow display of reports/testcase_xxx.log by link Make all links open a new tab as opposed to overwriting the current one. Add memory used column since it is now supported in latest kernel. --- scripts/pc2sandbox.sh | 30 +-- support/judge_webcgi/cgi-bin/judge.sh | 1 + support/judge_webcgi/cgi-bin/pc2common.sh | 63 +++++++ support/judge_webcgi/cgi-bin/showrun.sh | 168 +++++++++++++++-- support/judge_webcgi/cgi-bin/webcommon.sh | 30 +++ support/judge_webcgi/judge.sh | 124 ------------- support/judge_webcgi/pc2common.sh | 211 ---------------------- support/judge_webcgi/showrun.sh | 209 --------------------- support/judge_webcgi/webcommon.sh | 157 ---------------- 9 files changed, 259 insertions(+), 734 deletions(-) delete mode 100644 support/judge_webcgi/judge.sh delete mode 100644 support/judge_webcgi/pc2common.sh delete mode 100644 support/judge_webcgi/showrun.sh delete mode 100644 support/judge_webcgi/webcommon.sh diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh index f45237d41..950bcf432 100755 --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -268,12 +268,15 @@ shift shift shift shift +shift DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" -DEBUG echo -e "\n$0" ${MEMLIMIT} ${TIMELIMIT} xxx xxx $* "< ${JUDGEIN} > $TESTCASE.ans" +RUN_LOCAL_CMD="$0 ${MEMLIMIT} ${TIMELIMIT} xxx xxx ${COMMAND} $* < ${JUDGEIN} > $TESTCASE.ans" +DIFF_OUTPUT_CMD="diff -w ${JUDGEANS} $TESTCASE.ans | more" +DEBUG echo -e "\n${RUN_LOCAL_CMD}" DEBUG echo -e "\nor, without the sandbox by using the following command:" -DEBUG echo -e "\n$* < ${JUDGEIN} > $TESTCASE.ans" +DEBUG echo -e "\n${COMMAND} $* < ${JUDGEIN} > $TESTCASE.ans" DEBUG echo -e "\nAnd compare with the judge's answer:" -DEBUG echo -e "\ndiff -w ${JUDGEANS} $TESTCASE.ans | more\n" +DEBUG echo -e "\n${DIFF_OUTPUT_CMD}\n" #### Debugging - just set expected first args to: 8MB 2seconds ###MEMLIMIT=8 @@ -351,31 +354,33 @@ mkdir -p "$REPORTDIR" REPORTFILE=`printf "$REPORTDIR/testcase_%03d.log" $TESTCASE` BRIEFREPORTFILE=`printf "$REPORTDIR/briefcase_%03d.log" $TESTCASE` DEBUG echo Report file: ${REPORTFILE} Brief Report File: ${BRIEFREORTFILE} +REPORT Test case $TESTCASE: +REPORT Command: "${RUN_LOCAL_CMD}" +REPORT Diff: " ${DIFF_OUTPUT_CMD}" # set the specified memory limit - input is in MB, cgroup v2 requires bytes, so multiply by 1M # but only if > 0. # "max" means unlimited, which is the cgroup v2 default DEBUG echo checking memory limit if [ "$MEMLIMIT" -gt "0" ] ; then - DEBUG echo setting memory limit to $MEMLIMIT MB + REPORT_DEBUG echo Setting memory limit to $MEMLIMIT MB echo $(( $MEMLIMIT * 1024 * 1024 )) > $PC2_SANDBOX_CGROUP_PATH/memory.max echo 1 > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max else - DEBUG echo setting memory limit to max, meaning no limit + REPORT_DEBUG echo Setting memory limit to max, meaning no limit echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.max echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max fi -REPORT Test case $TESTCASE: # We use ulimit to limit CPU time, not cgroups. Time is supplied in seconds. This may have to # be reworked if ms accuracy is needed. The problem is, cgroups do not kill off a process that # exceeds the time limit, ulimit does. TIMELIMIT_US=$((TIMELIMIT * 1000000)) -REPORT Setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" +REPORT_DEBUG Setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" ulimit -t $TIMELIMIT MAXPROCS=$((MAXPROCS+`ps -T -u $USER | wc -l`)) -REPORT Setting maximum user processes to $MAXPROCS +REPORT_DEBUG Setting maximum user processes to $MAXPROCS ulimit -u $MAXPROCS # Keep track of details for reports @@ -398,12 +403,7 @@ then fi # run the command -# the following are the cgroup-tools V1 commands; need to find cgroup-tools v2 commands -# echo Using cgexec to run $COMMAND $* -# cgexec -g cpu,memory:/pc2 $COMMAND $* - -# since we don't know how to use cgroup-tools to execute, just execute it directly (it's a child so it -# should still fall under the cgroup limits). +# execute it directly (it's a child so it should still fall under the cgroup limits). REPORT_DEBUG Executing "setsid taskset $CPUMASK $COMMAND $*" # Set up trap handler to catch wall-clock time exceeded and getting killed by PC2's execute timer @@ -443,6 +443,8 @@ else fi ShowStats ${cputime} ${TIMELIMIT_US} ${walltime} ${peakmem} $((MEMLIMIT*1024*1024)) +REPORT_DEBUG The command exited with code: ${COMMAND_EXIT_CODE} + if test "$kills" != "0" then REPORT_DEBUG The command was killed because it exceeded the memory limit diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh index c4bbd86ca..e1b6bb740 100644 --- a/support/judge_webcgi/cgi-bin/judge.sh +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -71,6 +71,7 @@ TableRow() ###ParseProblemYaml Preamble Header +LogButton StartTable TableHeader diff --git a/support/judge_webcgi/cgi-bin/pc2common.sh b/support/judge_webcgi/cgi-bin/pc2common.sh index 082b1770b..eac62c936 100644 --- a/support/judge_webcgi/cgi-bin/pc2common.sh +++ b/support/judge_webcgi/cgi-bin/pc2common.sh @@ -7,6 +7,12 @@ JUDGE_HOME=/home/icpc PC2_RUN_DIR=${JUDGE_HOME}/pc2 PC2_CDP=${PC2_RUN_DIR}/current/config +EXE_LINK_PREFIX=../exedir +PROB_LINK_PREFIX=../probdir + +# How many exedir and probdir links to keep around +NUM_LINK_KEEP=4 + # Where can we find the contest banner png file for webpage headers BANNER_FILE=banner.png BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} @@ -25,6 +31,15 @@ TEST_ERR_PREFIX="teamstderr" TEST_VALOUT_PREFIX="valout" TEST_VALERR_PREFIX="valerr" +# Detailed log of entire judging process +SANDBOX_LOG=sandbox.log + +# Testcase file prefix +TESTCASE_REPORT_FILE_PREFIX="testcase" +# Briefcase file prefix +BRIEFCASE_FILE_PREFIX="briefcase" +REPORTS_DIR=reports + declare -A Judgments InitJudgments() @@ -156,5 +171,53 @@ GetJudgment() fi } +MakeTestcaseFile() +{ + d="$1" + t="$2" + result=`printf '%s/%s/%s_%03d.log' "$d" "$REPORTS_DIR" "$TESTCASE_REPORT_FILE_PREFIX" "$t"` +} + +MakeBriefcaseFile() +{ + d="$1" + t="$2" + result=`printf '%s/%s/%s_%03d.log' "$d" "$REPORTS_DIR" "$BRIEFCASE_FILE_PREFIX" "$t"` +} + +# Must redirect from briefcase file +ReadBriefcase() +{ + read judgein + read judgeans + read cpunum + read exepid + read exetime + read execpums cpulimms exewallms mempeakbytes memlimbytes + mempeak=$mempeakbytes + memlim=$memlimbytes + # Calculate Mib from bytes, round upto next Mib + if [[ $mempeak = [0-9]* ]] + then + mempeak=$(((mempeak+(1024*1024)-1)/(1024*1024))) + fi + if [[ $memlim = [0-9]* ]] + then + memlim=$(((memlim+(1024*1024)-1)/(1024*1024))) + fi +} + +DeleteOldestLinks() +{ + for linkpref in ${EXE_LINK_PREFIX} ${PROB_LINK_PREFIX} + do + dellist=`ls -1td ${linkpref}* | sed 1,${NUM_LINK_KEEP}d` + if test -n "${dellist}" + then + rm -f ${dellist} + fi + done +} + InitJudgments diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh index 38b3cf7f7..87a848d00 100644 --- a/support/judge_webcgi/cgi-bin/showrun.sh +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -1,16 +1,87 @@ #!/bin/bash -EXE_DIR_LINK=../exedir$$ -PROB_DIR_LINK=../probdir$$ - . ./pc2common.sh . ./webcommon.sh . ./cdpcommon.sh +EXE_DIR_LINK=${EXE_LINK_PREFIX}$$ +PROB_DIR_LINK=${PROB_LINK_PREFIX}$$ + +# Provide a way to look at the sandbox log +LogButton() +{ + # Read first judgment file to get compile time - it's compiled once for all + GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" + if test -n ${result} -a -n ${compileTimeMS} + then + cat << LBEOF0 +

+

The program took ${compileTimeMS}ms to compile. +

+LBEOF0 + fi + + # Read the first briefcase file (if any) for limits + MakeBriefcaseFile ${EXE_DIR_LINK} 1 + ReadBriefcase < $result + if test -n ${cpulimms} + then + cpulimms=${cpulimms%%.*} + cpusecs="$((cpulimms/1000))" + if test ${cpusecs} != "1" + then + cpusecs="${cpusecs} seconds" + else + cpusecs="1 second" + fi + cat << LBEOF1 +
+

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms). +

+LBEOF1 + fi + if test -n ${memlim} + then + if test ${memlim} = "0" + then + memlim="Unlimited" + else + memlim=${memlim}MiB + fi + cat << LBEOF1A +
+

The Memory limit for this problem is ${memlim}. +

+LBEOF1A + fi + sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} + if test -s ${sandlog} + then + cat << LBEOF2 +
Click here for the full sandbox log for this run +

+

+LBEOF2 + fi +} + TableHeader() { cat << EOFTH -TestDispJudgmentExitCompile TimeExecute TimeVal TimeVal SuccessRun OutRun ErrJudge InJudge AnsVal OutVal Err + Test + Disp + Judgment + Exit + Execute Time + MiB Used + Val Time + Val Success + Run Out + Run Err + Judge In + Judge Ans + Val Out + Val Err EOFTH } @@ -24,7 +95,7 @@ GenFileLink() if test -s ${tstpath} then bytes=`stat -c %s ${tstpath}` - echo ' View ('$bytes' bytes)' + echo ' View ('$bytes' bytes)' elif test -e ${tstpath} then echo ' (Empty)' @@ -33,23 +104,56 @@ GenFileLink() fi } +GenFileLinkWithText() +{ + linkaddr="$1" + linktext="$2" + linkcolor="$3" + bytes=`stat -c %s ${linkaddr}` + echo ' '$linktext' ('$bytes' bytes)' +} + TableRow() { tc=$1 judgment="$2" ec=$3 - comptm="$4" - exetm="$5" - valtm="$6" - if test "$7" = "true" + exetm="$4" + valtm="$5" + jin="$6" + jans="$7" + if test "$8" = "true" then valsucc=Yes - elif test "$7" = "false" + elif test "$8" = "false" then valsucc=No else valsucc="N/A" fi + memused="$9" + memusedbytes="${10}" + exesandms="${11}" + + # Create link to report/testcase file for testcase number + MakeTestcaseFile ${EXE_DIR_LINK} ${tc} + tcreport="${result}" + if test ! -s "${tcreport}" + then + tcreport="" + fi + # Strip the stuff of before sample (or secret) + jin=${jin##*/data/} + jans=${jans##*/data/} + # Just the basenames for link text + jinbase=${jin##*/} + jansbase=${jans##*/} + if [[ $jin = sample/* ]] + then + lcolor=#00a0a0 + else + lcolor=#00a000 + fi if test "${judgment}" = "AC" then jicon='' @@ -62,16 +166,33 @@ TableRow() echo ' ' - echo ' '$tc'' + if test -n ${tcreport} + then + echo ' '$tc'' + else + echo ' '$tc'' + fi echo ' '$jicon'' echo ' '$judgment'' echo ' '$ec'' - echo ' '$comptm'ms' - echo ' '$exetm'ms' + if [[ ${exesandms} = [0-9]* ]] + then + echo '

'$exetm'ms'$exesandms'ms in the Sandbox
' + else + echo ' '$exetm'ms' + fi + if [[ ${memusedbytes} = [0-9]* ]] + then + echo '
'$memused'MiB'$memusedbytes' bytes
' + else + echo ' '$memused'' + fi echo ' '$valtm'ms' echo ' '$valsucc'' GenFileLink $TEST_OUT_PREFIX $tc GenFileLink $TEST_ERR_PREFIX $tc + GenFileLinkWithText $PROB_DIR_LINK/data/"$jin" "$jinbase" $lcolor + GenFileLinkWithText $PROB_DIR_LINK/data/"$jans" "$jansbase" $lcolor GenFileLink $TEST_VALOUT_PREFIX $tc GenFileLink $TEST_VALERR_PREFIX $tc @@ -92,14 +213,19 @@ do done # Done parsing +DeleteOldestLinks Preamble - 'Run '$run -HeaderNoBanner Details for RunId $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" +HeaderNoBanner Details for Run Id $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" -StartTable -TableHeader -rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} +# Create links apache can access in our html folder +#rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} ln -s ${dir} ${EXE_DIR_LINK} ln -s ${probdir} ${PROB_DIR_LINK} + +LogButton + +StartTable +TableHeader for testcase in `ls $dir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1n` do GetTestCaseNumber $testcase @@ -108,12 +234,16 @@ do GetJudgmentFromFile $dir/$testcase judgment=$result ec=$executeExitValue - comptm=$compileTimeMS +# comptm=$compileTimeMS exetm=$executeTimeMS valtm=$validateTimeMS valsuc=$validationSuccess - TableRow "$tc" "$judgment" "$ec" "$comptm" "$exetm" "$valtm" "$valsuc" + MakeBriefcaseFile "$dir" "$tc" + ReadBriefcase < ${result} + TableRow "$tc" "$judgment" "$ec" "$exetm" "$valtm" "$judgein" "$judgeans" "$valsuc" "$mempeak" "$mempeakbytes" "$execpums" done Trailer + +#rm -f ${EXE_DIR_LINK} ${PROB_DIR_LINK} exit 0 diff --git a/support/judge_webcgi/cgi-bin/webcommon.sh b/support/judge_webcgi/cgi-bin/webcommon.sh index fef94f341..03fddd8a3 100644 --- a/support/judge_webcgi/cgi-bin/webcommon.sh +++ b/support/judge_webcgi/cgi-bin/webcommon.sh @@ -1,4 +1,7 @@ # File meant to be "source'd" to support generating web pages +TOOLTIP_UL_COLOR="blue" +TOOLTIP_TEXT_COLOR="white" +TOOLTIP_BG_COLOR="black" # # Display very curt textual error message @@ -27,6 +30,9 @@ Preamble() PC² $headmsg diff --git a/support/judge_webcgi/judge.sh b/support/judge_webcgi/judge.sh deleted file mode 100644 index e1b6bb740..000000000 --- a/support/judge_webcgi/judge.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/bin/bash -. ./pc2common.sh -. ./webcommon.sh -. ./cdpcommon.sh - -TableHeader() -{ - cat << EOFTH - -Run IDDispJudgmentProblemTeamTest CasesLanguageJudgeTime Judged - -EOFTH -} - -TableRow() -{ - dir="$1" - runid=$2 - problet=$3 - shortname=$4 - langid=$5 - teamnum=$6 - judgment="$7" - runtime="$8" - testinfo="$9" - judge="${10}" - probname="" - probdir="" - if test -n ${shortname} - then - probdir=${PC2_CDP}/${shortname} - probstatement=${probdir}/problem_statement/problem.en.tex - if test ! -s ${probstatement} - then - probstatement=${probdir}/problem_statement/problem.tex - fi - if test -s ${probstatement} - then - probname=`head -1 ${probstatement}` - probname=${probname##*\{} - probname=${probname%\}} - fi - fi - problem="$problet - $probname (${shortname})" - if test "${judgment}" = "AC" - then - jstyle="green" - jicon='' - elif test "${judgment}" = "CE" - then - jicon='' - else - jstyle="red" - jicon='' - fi - echo ' ' - echo ' '"Run $runid" -# echo ' '$judgment"" -# echo '
'$judgment"
" - echo ' '$jicon'' - echo ' '$judgment'' - echo " $problem" - echo " team$teamnum" - echo " $testinfo" - echo " $langid" - echo " $judge" - echo " $runtime" - echo " " -} - -###ParseProblemYaml -Preamble -Header -LogButton -StartTable -TableHeader - -# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid_judge -for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+' | sort --field-separator=_ +1rn` -do - # exdir looks like: ex_188_Y_compression_46103_cpp - # RId P ProbShort team# Lang - # RId = Run ID - # P = problem letter - # Lang = CLICS Language id - saveIFS="$IFS" - IFS="_" - set ${exdir} - IFS="$saveIFS" - if test $# -ge 6 - then - exedir=${PC2_RUN_DIR}/$exdir - runid=$2 - problet=$3 - probshort=$4 - teamnum=$5 - langid=$6 - judge=$7 - if test -z "${judge}" - then - judge="N/A" - fi - GetJudgment "${exedir}" - judgment="${result}" - runtime="${executeDateTime}" - # Get how many total test cases there are - probdir=${PC2_CDP}/${probshort} - if test -n "${probdir}" - then - GetNumberOfTestCases "${probdir}" - numcases=${result} - else - numcases="??" - fi - # Note that GetJudgment also filled in exdata with the last execute data - GetLastTestCaseNumber "${exdata}" - testcaseinfo=$((result+1))/${numcases} - - TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" - fi -done -EndTable -Trailer -exit 0 diff --git a/support/judge_webcgi/pc2common.sh b/support/judge_webcgi/pc2common.sh deleted file mode 100644 index 54723de21..000000000 --- a/support/judge_webcgi/pc2common.sh +++ /dev/null @@ -1,211 +0,0 @@ -# Meant to be "source'd" into bash scripts. - -# The judge's home directory -JUDGE_HOME=/home/icpc - -# Modify constants as necessary for your installation -PC2_RUN_DIR=${JUDGE_HOME}/pc2 -PC2_CDP=${PC2_RUN_DIR}/current/config - -EXE_LINK_PREFIX=../exedir -PROB_LINK_PREFIX=../probdir - -# How many exedir and probdir links to keep around -NUM_LINK_KEEP=4 - -# Where can we find the contest banner png file for webpage headers -BANNER_FILE=banner.png -BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} - -# Where to the the result of script failure before execution -RESULT_FAILURE_FILE=failure.txt - -# Where judgments are -REJECT_INI=${JUDGE_HOME}/pc2/reject.ini - -# Where PC2 puts CLICS validator results -EXECUTE_DATA_PREFIX=executedata -# Where PC2 puts run output/error -TEST_OUT_PREFIX="teamoutput" -TEST_ERR_PREFIX="teamstderr" -TEST_VALOUT_PREFIX="valout" -TEST_VALERR_PREFIX="valerr" - -# Detailed log of entire judging process -SANDBOX_LOG=sandbox.log - -# Briefcase file prefix -BRIEFCASE_FILE_PREFIX="briefcase" -REPORTS_DIR=reports - -declare -A Judgments - -InitJudgments() -{ - # Defauls, may be overriden by reject.ini - Judgments["accepted"]="AC" - Judgments["Accepted"]="AC" - Judgments["timelimit"]="TLE" - Judgments["run error"]="RTE" - Judgments["compiler error"]="CE" - if test -s ${REJECT_INI} - then - while read j - do - if [[ $j = \#* ]] - then - continue - fi - savIFS="$IFS" - IFS='|' - set $j - IFS="$savIFS" - key="$1" - shortcode="$2" - case ${shortcode} in - AC|CE|RTE|WA|TLE) ;; - MLE) shortcode="RTE (MLE)" ;; - *) shortcode="WA (${shortcode})" ;; - esac -# echo Mapping $key to $shortcode - Judgments[$key]="$shortcode" - done < $REJECT_INI - else echo NO REJECT FILE - fi -} - -# Takes the judgement string, eg. "Wrong answer" as arg 1 and the validation Result (42 or 42) as arg 2 -MapJudgment() -{ - jm="$1" - vr="$2" - result=${Judgments[$jm]} - if test -z ${result} - then - if test ${validationReturnCode} -eq 0 - then - if test ${vr} = "43" - then - resul="WA" - elif test ${vr} = "42" - then - result="AC" - else - result="WA (Default)" - fi - else - result="JE (Validator EC=${validationReturnCode})" - fi - - fi -} - -GetLastJudgmentFile() -{ - lj_exdir="$1" - result=`ls $lj_exdir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1rn | head -1` -} - -GetTestCaseNumber() -{ - if test -z "$1" - then - result=0 - else - saveIFS="$IFS" - IFS=. - set ${1} - result="$2" - IFS="$saveIFS" - if test -z "${result}" - then - result=0 - fi - fi -} - -GetJudgmentFromFile() -{ - exdata="$1" - if test -z "${exdata}" - then - result="No results" - else - # Source the file - . ${exdata} - if test ${compileSuccess} = "false" - then - result="CE" - elif test ${executeSuccess} = "true" - then - if test ${validationSuccess} = "true" - then - MapJudgment "${validationResults}" "${validationReturnCode}" - else - result="JE (Validator error)" - fi - else - result="RTE (Execute error)" - fi - fi -} - -GetJudgment() -{ - dir=$1 - exdata="" - if ! cd ${dir} - then - result="Not found" - elif test -s ${RESULT_FAILURE_FILE} - then - jerr=`cat ${RESULT_FAILURE_FILE}` - result="JE ($jerr)" - else - # We got a real live run - # Check out the biggest executedata file - GetLastJudgmentFile $dir - GetJudgmentFromFile ./${result} - fi -} - -MakeBriefcaseFile() -{ - d="$1" - t="$2" - result=`printf '%s/%s/%s_%03d.log' "$d" "$REPORTS_DIR" "$BRIEFCASE_FILE_PREFIX" "$t"` -} - -# Must redirect from briefcase file -ReadBriefcase() -{ - read judgein - read judgeans - read cpunum - read exepid - read exetime - read execpums cpulimms exewallms mempeak memlim - if [[ $mempeak = [0-9]* ]] - then - mempeak=$((mempeak/(1024*1024))) - fi - if [[ $memlim = [0-9]* ]] - then - memlim=$((memlim/(1024*1024))) - fi -} - -DeleteOldestLinks() -{ - for linkpref in ${EXE_LINK_PREFIX} ${PROB_LINK_PREFIX} - do - dellist=`ls -1td ${linkpref}* | sed 1,${NUM_LINK_KEEP}d` - if test -n "${dellist}" - then - rm -f ${dellist} - fi - done -} - -InitJudgments - diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh deleted file mode 100644 index e88937dea..000000000 --- a/support/judge_webcgi/showrun.sh +++ /dev/null @@ -1,209 +0,0 @@ -#!/bin/bash -. ./pc2common.sh -. ./webcommon.sh -. ./cdpcommon.sh - -EXE_DIR_LINK=${EXE_LINK_PREFIX}$$ -PROB_DIR_LINK=${PROB_LINK_PREFIX}$$ - -# Provide a way to look at the sandbox log -LogButton() -{ - # Read first judgment file to get compile time - it's compiled once for all - GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" - if test -n ${result} -a -n ${compileTimeMS} - then - cat << LBEOF0 -
-

The program took ${compileTimeMS}ms to compile. -

-LBEOF0 - fi - - # Read the first briefcase file (if any) for limits - MakeBriefcaseFile ${EXE_DIR_LINK} 1 - ReadBriefcase < $result - if test -n ${cpulimms} - then - cpulimms=${cpulimms%%.*} - cpusecs="$((cpulimms/1000))" - if test ${cpusecs} != "1" - then - cpusecs="${cpusecs} seconds" - else - cpusecs="1 second" - fi - cat << LBEOF1 -
-

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms). -

-LBEOF1 - fi - if test -n ${memlim} - then - if test ${memlim} = "0" - then - memlim="Unlimited" - else - memlim=${memlim}MB - fi - cat << LBEOF1A -
-

The Memory limit for this problem is ${memlim}. -

-LBEOF1A - fi - sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} - if test -s ${sandlog} - then - cat << LBEOF2 -Click here for the full sandbox log for this run -

-

-LBEOF2 - fi -} - -TableHeader() -{ - cat << EOFTH - -TestDispJudgmentExitExecute TimeVal TimeVal SuccessRun OutRun ErrJudge InJudge AnsVal OutVal Err - -EOFTH -} - -GenFileLink() -{ - tstcase="$2" - tstcase=$((tstcase-1)) - tstfile=$1.$tstcase.txt - tstpath=$dir/$1.$tstcase.txt - if test -s ${tstpath} - then - bytes=`stat -c %s ${tstpath}` - echo ' View ('$bytes' bytes)' - elif test -e ${tstpath} - then - echo ' (Empty)' - else - echo ' Not found' - fi -} - -GenFileLinkWithText() -{ - linkaddr="$1" - linktext="$2" - linkcolor="$3" - bytes=`stat -c %s ${linkaddr}` - echo ' '$linktext' ('$bytes' bytes)' -} - -TableRow() -{ - tc=$1 - judgment="$2" - ec=$3 - exetm="$4" - valtm="$5" - jin="$6" - jans="$7" - if test "$8" = "true" - then - valsucc=Yes - elif test "$8" = "false" - then - valsucc=No - else - valsucc="N/A" - fi - # Strip the stuff of before sample (or secret) - jin=${jin##*/data/} - jans=${jans##*/data/} - # Just the basenames for link text - jinbase=${jin##*/} - jansbase=${jans##*/} - if [[ $jin = sample/* ]] - then - lcolor=#00a0a0 - else - lcolor=#00a000 - fi - if test "${judgment}" = "AC" - then - jicon='' - elif test "${judgment}" = "CE" - then - jicon='' - else - jicon='' - fi - - echo ' ' - - echo ' '$tc'' - echo ' '$jicon'' - echo ' '$judgment'' - echo ' '$ec'' - echo ' '$exetm'ms' - echo ' '$valtm'ms' - echo ' '$valsucc'' - GenFileLink $TEST_OUT_PREFIX $tc - GenFileLink $TEST_ERR_PREFIX $tc - GenFileLinkWithText $PROB_DIR_LINK/data/"$jin" "$jinbase" $lcolor - GenFileLinkWithText $PROB_DIR_LINK/data/"$jans" "$jansbase" $lcolor - GenFileLink $TEST_VALOUT_PREFIX $tc - GenFileLink $TEST_VALERR_PREFIX $tc - - echo ' ' -} - -# Parse query string into dictionary args -# This is not terribly secure. -declare -a parm -sIFS="$IFS" -IFS='=&' -parm=($QUERY_STRING) -IFS=$sIFS -for ((i=0; i<${#parm[@]}; i+=2)) -do - a=${parm[i]} - eval $a=${parm[i+1]} -done -# Done parsing - -DeleteOldestLinks -Preamble - 'Run '$run -HeaderNoBanner Details for Run Id $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" - -# Create links apache can access in our html folder -#rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} -ln -s ${dir} ${EXE_DIR_LINK} -ln -s ${probdir} ${PROB_DIR_LINK} - -LogButton - -StartTable -TableHeader -for testcase in `ls $dir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1n` -do - GetTestCaseNumber $testcase - tc=$((result+1)) - # This will also source the execute data - GetJudgmentFromFile $dir/$testcase - judgment=$result - ec=$executeExitValue -# comptm=$compileTimeMS - exetm=$executeTimeMS - valtm=$validateTimeMS - valsuc=$validationSuccess - MakeBriefcaseFile "$dir" "$tc" - ReadBriefcase < ${result} - TableRow "$tc" "$judgment" "$ec" "$exetm" "$valtm" "$judgein" "$judgeans" "$valsuc" -done - -Trailer - -#rm -f ${EXE_DIR_LINK} ${PROB_DIR_LINK} -exit 0 diff --git a/support/judge_webcgi/webcommon.sh b/support/judge_webcgi/webcommon.sh deleted file mode 100644 index 26c65cd62..000000000 --- a/support/judge_webcgi/webcommon.sh +++ /dev/null @@ -1,157 +0,0 @@ -# File meant to be "source'd" to support generating web pages - -# -# Display very curt textual error message -# -Error() -{ - echo "Content-type: text/plain" - echo "" - echo ERROR $* -} - - -Preamble() -{ - echo "Content-type: text/html" - echo "" - if test $# -eq 0 - then - headmsg="Judge" - else - headmsg="$*" - fi - cat << PREEOF - - - -PC² $headmsg - - - -PREEOF -} - -Header() -{ - # Make sure link to banner page is in a place apache can read - if test ! -e ../${BANNER_FILE} - then - ln -s ${BANNER_IMAGE} ../${BANNER_FILE} - fi -cat << HDREOF -

- -

PC2 Judging Results

-
-

-HDREOF -} - -HeaderNoBanner() -{ - if test $# -eq 0 - then - hdrmsg="Judging Results" - else - hdrmsg="$*" - fi -cat << EOF -

-

PC2 $hdrmsg

-
-

-EOF -} - -Trailer() -{ -cat << EOF2 - - -EOF2 -} - -StartTable() -{ - cat << EOF3 -

- -EOF3 -} - -EndTable() -{ - cat << EOF4 -
-

-EOF4 -} - From dd52e4e5ee55e3b1efb445f2944e3ecdea6ec346 Mon Sep 17 00:00:00 2001 From: John Buck Date: Sun, 7 Jul 2024 14:15:08 -0400 Subject: [PATCH 33/55] i_972 Clean up scripts Interactive sandbox script needed cleaning up and updating. Fix some bash 'if' statements in case arg is empty string. --- scripts/pc2sandbox.sh | 6 +- scripts/pc2sandbox_interactive.sh | 132 +++++++++++++++++----- support/judge_webcgi/cgi-bin/judge.sh | 6 +- support/judge_webcgi/cgi-bin/pc2common.sh | 18 +-- support/judge_webcgi/cgi-bin/showrun.sh | 34 ++++-- 5 files changed, 146 insertions(+), 50 deletions(-) diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh index 950bcf432..9007e4072 100755 --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -244,9 +244,9 @@ if [ "$#" -lt 1 ] ; then exit $FAIL_NO_ARGS_EXIT_CODE fi -if [ "$#" -lt 3 ] ; then - echo $0: expected 3 or more arguments, found: $* - SysFailure Expected 3 or more arguments to $0, found: $* +if [ "$#" -lt 6 ] ; then + echo $0: expected 6 or more arguments, found: $* + SysFailure Expected 6 or more arguments to $0, found: $* exit $FAIL_INSUFFICIENT_ARGS_EXIT_CODE fi diff --git a/scripts/pc2sandbox_interactive.sh b/scripts/pc2sandbox_interactive.sh index d37f66a07..4c45ab5f7 100755 --- a/scripts/pc2sandbox_interactive.sh +++ b/scripts/pc2sandbox_interactive.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Copyright (C) 1989-2023 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. +# Copyright (C) 1989-2024 PC2 Development Team: John Clevenger, Douglas Lane, Samir Ashoo, and Troy Boudreau. # # File: pc2sandbox_interacive.sh # Purpose: a sandbox for pc2 using Linux CGroups v2 and supporting interactive problems @@ -19,6 +19,10 @@ DEFAULT_CPU_NUM=3 CPU_OVERRIDE_FILE=$HOME/pc2_cpu_override +# Where to the the result of the first failure. If this file is not created, then the +# run was accepted. (correct) +RESULT_FAILURE_FILE=failure.txt + # FAIL_RETCODE_BASE is 128 + 64 + xx # 128 = system error, like signal # 64 = biggest used signal @@ -130,6 +134,13 @@ function REPORT() echo "$@" >> $REPORTFILE fi } +function REPORT_BRIEF() +{ + if test -n "$BRIEFREPORTFILE" + then + echo "$@" >> $BRIEFREPORTFILE + fi +} # For per-testcase report and debugging both function REPORT_DEBUG() @@ -163,24 +174,24 @@ SAGE # Function to kill all processes in the cgroupv2 after process has exited KillcgroupProcs() { - if test -n ${PC2_SANDBOX_CGROUP_PATH_KILL} - then + if test -n ${PC2_SANDBOX_CGROUP_PATH_KILL} + then DEBUG echo "Purging cgroup ${PC2_SANDBOX_CGROUP_PATH_KILL} of processes" - echo 1 > ${PC2_SANDBOX_CGROUP_PATH_KILL} - fi + echo 1 > ${PC2_SANDBOX_CGROUP_PATH_KILL} + fi } # Kill active children and stragglers KillChildProcs() { - DEBUG echo "Killing off submission process group $submissionpid and all children" - # Kill off process group - if test -n "$submissionpid" - then - pkill -9 -s $submissionpid - fi - # and... extra stragglers with us as a parent - pkill -9 -P $$ + DEBUG echo "Killing off submission process group $submissionpid and all children" + # Kill off process group + if test -n "$submissionpid" + then + pkill -9 -s $submissionpid + fi + # and... extra stragglers with us as a parent + pkill -9 -P $$ } # Kill off the validator if it is still running @@ -226,6 +237,10 @@ ShowStats() walltime=$3 memused=$4 memlim=$5 + REPORT_BRIEF "$(printf '%d.%03d' $((cpuused / 1000)) $((cpuused % 1000)))" \ + "$(printf '%d.%03d' $((cpulim / 1000)) $((cpulim % 1000)))" \ + "$(printf '%d.%03d' $((walltime / 1000)) $((walltime % 1000)))" \ + ${memused} $((memlim)) REPORT_DEBUG Resources used for this run: REPORT_DEBUG "$(printf ' CPU ms Limit ms Wall ms Memory Used Memory Limit\n')" REPORT_DEBUG "$(printf '%5d.%03d %5d.%03d %6d.%03d %12s %12d\n' $((cpuused / 1000)) $((cpuused % 1000)) \ @@ -234,6 +249,27 @@ ShowStats() ${memused} $((memlim)))" } +# Generate a system failure result file. This will be the FIRST failure as it will not overwrite the file +# if it exists. +SysFailure() +{ + if test ! -s ${RESULT_FAILURE_FILE} + then + (echo system; echo $* ) > ${RESULT_FAILURE_FILE} + fi +} + +# Generate a failure result file. This will be the FIRST failure as it will not overwrite the file +# if it exists. +Failure() +{ + if test ! -s ${RESULT_FAILURE_FILE} + then + echo $* > ${RESULT_FAILURE_FILE} + fi +} + + sent_xml=0 GenXML() @@ -253,6 +289,7 @@ EOF if [ "$#" -lt 1 ] ; then echo $0: Missing command line arguments. Try '"'"$0 --help"'"' for help. + SysFailure No command line arguments to $0 exit $FAIL_NO_ARGS_EXIT_CODE fi @@ -263,6 +300,7 @@ fi if [ "$#" -lt 7 ] ; then echo $0: expected 7 or more arguments, found: $* + SysFailure Expected 7 or more arguments to $0, found: $* exit $FAIL_INSUFFICIENT_ARGS_EXIT_CODE fi @@ -277,6 +315,19 @@ DEBUG echo "+---------------- Test Case ${TESTCASE} ----------------+" DEBUG echo Command line: $0 $* shift 7 +DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" +RUN_LOCAL_CMD="$0 ${MEMLIMIT} ${TIMELIMIT} ${VALIDATOR} ${JUDGEIN} ${JUDGEANS} ${TESTCASE} ${COMMAND} $*" +tcfile=`printf "$REPORTDIR/testcase_%03d.log" $TESTCASE` +REPORT_OUTPUT_CMD="more ${tcfile}" +DEBUG echo -e "\n${RUN_LOCAL_CMD}" +#DEBUG echo -e "\nor, without the sandbox by using the following command:" +#DEBUG echo -e "\n${COMMAND} $* < ${JUDGEIN} > $TESTCASE.ans" +DEBUG echo -e "\nAnd see the run report using the following command:" +DEBUG echo -e "\n${REPORT_OUTPUT_CMD}\n" + +# Skip used arguments +shift 7 + # the rest of the commmand line arguments are the command args for the submission @@ -284,42 +335,49 @@ shift 7 DEBUG echo checking PC2 CGroup V2 installation... if [ ! -d "$PC2_CGROUP_PATH" ]; then - echo $0: expected pc2sandbox CGroups v2 installation in $PC2_CGROUP_PATH + echo $0: expected pc2sandbox CGroups v2 installation in $PC2_CGROUP_PATH + SysFailure CGroups v2 not installed in $PC2_CGROUP_PATH exit $FAIL_INVALID_CGROUP_INSTALLATION fi if [ ! -f "$CGROUP_PATH/cgroup.controllers" ]; then echo $0: missing file cgroup.controllers in $CGROUP_PATH + SysFailure Missing cgroup.controllers in $CGROUP_PATH exit $FAIL_MISSING_CGROUP_CONTROLLERS_FILE fi if [ ! -f "$CGROUP_PATH/cgroup.subtree_control" ]; then echo $0: missing file cgroup.subtree_control in $CGROUP_PATH + SysFailure Missing cgroup.subtree_controll in $CGORUP_PATH exit $FAIL_MISSING_CGROUP_SUBTREE_CONTROL_FILE fi # make sure the cpu and memory controllers are enabled if ! grep -q -F "cpu" "$CGROUP_PATH/cgroup.subtree_control"; then echo $0: cgroup.subtree_control in $CGROUP_PATH does not enable cpu controller + SysFailure CPU controller not enabled in cgroup.subtree_control in $CGROUP_PATH exit $FAIL_CPU_CONTROLLER_NOT_ENABLED fi if ! grep -q -F "memory" "$CGROUP_PATH/cgroup.subtree_control"; then echo $0: cgroup.subtree_control in $CGROUP_PATH does not enable memory controller + SysFailure Memory controller not enabled in cgroup.subtree_control in $CGROUP_PATH exit $FAIL_MEMORY_CONTROLLER_NOT_ENABLED fi if [ ! -e "$VALIDATOR" ]; then echo $0: The interactive validator \'"$VALIDATOR"\' was not found + SysFailure The interactive validator \'"$VALIDATOR"\' was not found exit $FAIL_INTERACTIVE_ERROR fi if [ ! -x "$VALIDATOR" ]; then echo $0: The interactive validator \'"$VALIDATOR"\' is not an executable file + SysFailure The interactive validator \'"$VALIDATOR"\' is not an executable file exit $FAIL_INTERACTIVE_ERROR fi -# we seem to have a valid CGroup installation +# we seem to have a valid CGroup and validator setup DEBUG echo ...done. if test -d $PC2_SANDBOX_CGROUP_PATH @@ -329,6 +387,7 @@ then if ! rmdir $PC2_SANDBOX_CGROUP_PATH then DEBUG echo Cannot purge old sandbox: $PC2_SANDBOX_CGROUP_PATH + SysFailure Cannot purge old sandbox: $PC2_SANDBOX_CGROUP_PATH exit $FAIL_SANDBOX_ERROR fi fi @@ -337,6 +396,7 @@ DEBUG echo Creating sandbox $PC2_SANDBOX_CGROUP_PATH if ! mkdir $PC2_SANDBOX_CGROUP_PATH then DEBUG echo Cannot create $PC2_SANDBOX_CGROUP_PATH + SysFailure Cannot create sandbox: $PC2_SANDBOX_CGROUP_PATH exit $FAIL_INVALID_CGROUP_INSTALLATION fi @@ -344,12 +404,14 @@ fi rm -f "$INFIFO" "$OUTFIFO" if ! mkfifo --mode=$FIFOMODE $INFIFO then - DEBUG Can not create FIFO $INFIFO 1>&2 + DEBUG Can not create input FIFO $INFIFO 1>&2 + SysFailure Cannot create input FIFO: $INFIFO exit $FAIL_INTERACTIVE_ERROR fi if ! mkfifo --mode=$FIFOMODE $OUTFIFO then - DEBUG Can not create FIFO $INFIFO 1>&2 + DEBUG Can not create output FIFO $OUTFIFO 1>&2 + SysFailure Cannot create output FIFO: $OUTFIFO rm $INFIFO exit $FAIL_INTERACTIVE_ERROR fi @@ -359,17 +421,22 @@ mkdir -p "$REPORTDIR" # Set report file to be testcase specific one now REPORTFILE=`printf "$REPORTDIR/testcase_%03d.log" $TESTCASE` +BRIEFREPORTFILE=`printf "$REPORTDIR/briefcase_%03d.log" $TESTCASE` +DEBUG echo Report file: ${REPORTFILE} Brief Report File: ${BRIEFREORTFILE} +REPORT Test case $TESTCASE: +REPORT Command: "${RUN_LOCAL_CMD}" +REPORT Report: " ${REPORT_OUTPUT_CMD}" # set the specified memory limit - input is in MB, cgroup v2 requires bytes, so multiply by 1M # but only if > 0. # "max" means unlimited, which is the cgroup v2 default DEBUG echo checking memory limit if [ "$MEMLIMIT" -gt "0" ] ; then - REPORT Setting memory limit to $MEMLIMIT MB + REPORT_DEBUG Setting memory limit to $MEMLIMIT MiB echo $(( $MEMLIMIT * 1024 * 1024 )) > $PC2_SANDBOX_CGROUP_PATH/memory.max echo 1 > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max else - REPORT Setting memory limit to max, meaning no limit + REPORT_DEBUG Setting memory limit to max, meaning no limit echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.max echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max fi @@ -384,22 +451,29 @@ mkdir -p "$INT_FEEDBACKDIR" # Note that starting of the validator will block until the submission is started since # it will block on the $INFIFO (no one else connected yet) -REPORT Starting: "$VALIDATOR $JUDGEIN $JUDGEANS $INT_FEEDBACKDIR > $INFIFO < $OUTFIFO &" +REPORT_DEBUG Starting: "$VALIDATOR $JUDGEIN $JUDGEANS $INT_FEEDBACKDIR > $INFIFO < $OUTFIFO &" $VALIDATOR $JUDGEIN $JUDGEANS $INT_FEEDBACKDIR > $INFIFO < $OUTFIFO & intv_pid=$! -REPORT Started interactive validator PID $intv_pid +REPORT_DEBUG Started interactive validator PID $intv_pid # We use ulimit to limit CPU time, not cgroups. Time is supplied in seconds. This may have to # be reworked if ms accuracy is needed. The problem is, cgroups do not kill off a process that # exceeds the time limit, ulimit does. TIMELIMIT_US=$((TIMELIMIT * 1000000)) -REPORT Setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" +REPORT_DEBUG Setting cpu limit to $TIMELIMIT_US microseconds "("ulimit -t $TIMELIMIT ")" ulimit -t $TIMELIMIT MAXPROCS=$((MAXPROCS+`ps -T -u $USER | wc -l`)) -REPORT Setting maximum user processes to $MAXPROCS +REPORT_DEBUG Setting maximum user processes to $MAXPROCS ulimit -u $MAXPROCS +# Keep track of details for reports +REPORT_BRIEF ${JUDGEIN} +REPORT_BRIEF ${JUDGEANS} +REPORT_BRIEF $cpunum +REPORT_BRIEF $$ +REPORT_BRIEF $(date "+%F %T.%6N") + # Remember wall time when we started starttime=`GetTimeInMicros` @@ -408,6 +482,7 @@ REPORT Putting $$ into $PC2_SANDBOX_CGROUP_PATH cgroup if ! echo $$ > $PC2_SANDBOX_CGROUP_PATH/cgroup.procs then echo $0: Could not add current process to $PC2_SANDBOX_CGROUP_PATH/cgroup.procs - not executing submission. + SysFailure Could not add current process to $PC2_SANDBOX_CGROUP_PATH/cgroup.procs rm -f "$INFIFO" "$OUTFIFO" exit $FAIL_SANDBOX_ERROR fi @@ -435,19 +510,19 @@ do # A return code 127 indicates there are no more children. How did that happen? if test $wstat -eq 127 then - REPORT No more children found while waiting: Submission PID was $submissionpid and Interactive Validator PID was $intv_pid + REPORT_DEBUG No more children found while waiting: Submission PID was $submissionpid and Interactive Validator PID was $intv_pid break fi # If interactive validator finishes if test "$child_pid" -eq "$intv_pid" then - REPORT Validator finishes with status $wstat + REPORT_DEBUG Validator finishes with exit code $wstat if test "$contestant_done" -eq 0 then # Only kill it if it still exists if test -d /proc/$contestantpid then - REPORT Contestant PID $submissionpid has not finished - killing it + REPORT_DEBUG Contestant PID $submissionpid has not finished - killing it # TODO: We should kill and wait for it here and print out the stats fi # This just determines if the program ran, not if it's correct. @@ -493,7 +568,7 @@ do fi - REPORT Contestant PID $submissionpid finished with status $wstat + REPORT_DEBUG Contestant PID $submissionpid finished with exit code $wstat contestant_done=1 COMMAND_EXIT_CODE=$wstat @@ -505,6 +580,7 @@ do if test "$kills" != "0" then REPORT_DEBUG The command was killed because it exceeded the memory limit + REPORT_BRIEF MLE COMMAND_EXIT_CODE=${FAIL_MEMORY_LIMIT_EXCEEDED} GenXML "No - Memory limit exceeded" "" KillValidator @@ -514,6 +590,7 @@ do if test "$COMMAND_EXIT_CODE" -eq 137 -o "$cputime" -gt "$TIMELIMIT_US" then REPORT_DEBUG The command was killed because it exceeded the CPU Time limit + REPORT_BRIEF TLE COMMAND_EXIT_CODE=${FAIL_TIME_LIMIT_EXCEEDED} GenXML "No - Time limit exceeded" "${cputime}us > ${TIMELIMIT_US}us" KillValidator @@ -531,6 +608,7 @@ do if test "$wstat" -ne 0 then REPORT_DEBUG Contestant finished abnormally - killing validator + REPORT_BRIEF RTE KillValidator GenXML "No - Run-time Error" "Exit status $wstat" break diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh index e1b6bb740..c3b121849 100644 --- a/support/judge_webcgi/cgi-bin/judge.sh +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -26,15 +26,15 @@ TableRow() judge="${10}" probname="" probdir="" - if test -n ${shortname} + if test -n "${shortname}" then probdir=${PC2_CDP}/${shortname} probstatement=${probdir}/problem_statement/problem.en.tex - if test ! -s ${probstatement} + if test ! -s "{probstatement}" then probstatement=${probdir}/problem_statement/problem.tex fi - if test -s ${probstatement} + if test -s "${probstatement}" then probname=`head -1 ${probstatement}` probname=${probname##*\{} diff --git a/support/judge_webcgi/cgi-bin/pc2common.sh b/support/judge_webcgi/cgi-bin/pc2common.sh index eac62c936..d9b6e2aed 100644 --- a/support/judge_webcgi/cgi-bin/pc2common.sh +++ b/support/judge_webcgi/cgi-bin/pc2common.sh @@ -50,7 +50,7 @@ InitJudgments() Judgments["timelimit"]="TLE" Judgments["run error"]="RTE" Judgments["compiler error"]="CE" - if test -s ${REJECT_INI} + if test -s "${REJECT_INI}" then while read j do @@ -82,14 +82,14 @@ MapJudgment() jm="$1" vr="$2" result=${Judgments[$jm]} - if test -z ${result} + if test -z "${result}" then - if test ${validationReturnCode} -eq 0 + if test "${validationReturnCode}" -eq 0 then - if test ${vr} = "43" + if test "${vr}" = "43" then resul="WA" - elif test ${vr} = "42" + elif test "${vr}" = "42" then result="AC" else @@ -135,12 +135,12 @@ GetJudgmentFromFile() else # Source the file . ${exdata} - if test ${compileSuccess} = "false" + if test "${compileSuccess}" = "false" then result="CE" - elif test ${executeSuccess} = "true" + elif test "${executeSuccess}" = "true" then - if test ${validationSuccess} = "true" + if test "${validationSuccess}" = "true" then MapJudgment "${validationResults}" "${validationReturnCode}" else @@ -159,7 +159,7 @@ GetJudgment() if ! cd ${dir} then result="Not found" - elif test -s ${RESULT_FAILURE_FILE} + elif test -s "${RESULT_FAILURE_FILE}" then jerr=`cat ${RESULT_FAILURE_FILE}` result="JE ($jerr)" diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh index 87a848d00..4c1e5b2c1 100644 --- a/support/judge_webcgi/cgi-bin/showrun.sh +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -11,7 +11,7 @@ LogButton() { # Read first judgment file to get compile time - it's compiled once for all GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" - if test -n ${result} -a -n ${compileTimeMS} + if test -n "${result}" -a -n "${compileTimeMS}" then cat << LBEOF0

@@ -22,8 +22,14 @@ LBEOF0 # Read the first briefcase file (if any) for limits MakeBriefcaseFile ${EXE_DIR_LINK} 1 - ReadBriefcase < $result - if test -n ${cpulimms} + if test -s "${result}" + then + ReadBriefcase < $result + else + cpulimms="" + memlim="" + fi + if test -n "${cpulimms}" then cpulimms=${cpulimms%%.*} cpusecs="$((cpulimms/1000))" @@ -38,8 +44,14 @@ LBEOF0

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms).

LBEOF1 + else + cat << LBEOF1AA +
+

The CPU Limit for this problem is N/A. +

+LBEOF1AA fi - if test -n ${memlim} + if test -n "${memlim}" then if test ${memlim} = "0" then @@ -52,9 +64,15 @@ LBEOF1

The Memory limit for this problem is ${memlim}. LBEOF1A + else + cat << LBEOF1AAA +

+

The Memory limit for this problem is N/A. +

+LBEOF1AAA fi sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} - if test -s ${sandlog} + if test -s "${sandlog}" then cat << LBEOF2
Click here for the full sandbox log for this run @@ -92,11 +110,11 @@ GenFileLink() tstcase=$((tstcase-1)) tstfile=$1.$tstcase.txt tstpath=$dir/$1.$tstcase.txt - if test -s ${tstpath} + if test -s "${tstpath}" then bytes=`stat -c %s ${tstpath}` echo ' View ('$bytes' bytes)' - elif test -e ${tstpath} + elif test -e "${tstpath}" then echo ' (Empty)' else @@ -166,7 +184,7 @@ TableRow() echo ' ' - if test -n ${tcreport} + if test -n "${tcreport}" then echo ' '$tc'' else From a14a7f273cb6a098c425623133969ea0d99e4ef8 Mon Sep 17 00:00:00 2001 From: John Buck Date: Sun, 7 Jul 2024 17:20:45 -0400 Subject: [PATCH 34/55] i_972 More judge web page fixes Remove extra space when generating the execute data file. Correct the "whats this" message for the judge execute folder pattern. Add jquery for sorting tables. --- scripts/pc2sandbox.sh | 12 ++-- .../csus/ecs/pc2/core/execute/Executable.java | 2 +- .../ecs/pc2/ui/ContestInformationPane.java | 2 +- support/judge_webcgi/cgi-bin/judge.sh | 28 +++++++++- support/judge_webcgi/cgi-bin/showrun.sh | 3 + support/judge_webcgi/cgi-bin/webcommon.sh | 56 ++++++++++++++++++- .../scripts/jquery-3.7.1.slim.min.js | 2 + 7 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 support/judge_webcgi/scripts/jquery-3.7.1.slim.min.js diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh index 9007e4072..fe3de06a4 100755 --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -263,12 +263,8 @@ TESTCASE=$5 COMMAND=$6 DEBUG echo "+---------------- Test Case ${TESTCASE} ----------------+" DEBUG echo Command line: $0 $* -shift -shift -shift -shift -shift -shift +shift 6 + DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" RUN_LOCAL_CMD="$0 ${MEMLIMIT} ${TIMELIMIT} xxx xxx ${COMMAND} $* < ${JUDGEIN} > $TESTCASE.ans" DIFF_OUTPUT_CMD="diff -w ${JUDGEANS} $TESTCASE.ans | more" @@ -363,11 +359,11 @@ REPORT Diff: " ${DIFF_OUTPUT_CMD}" # "max" means unlimited, which is the cgroup v2 default DEBUG echo checking memory limit if [ "$MEMLIMIT" -gt "0" ] ; then - REPORT_DEBUG echo Setting memory limit to $MEMLIMIT MB + REPORT_DEBUG Setting memory limit to $MEMLIMIT MiB echo $(( $MEMLIMIT * 1024 * 1024 )) > $PC2_SANDBOX_CGROUP_PATH/memory.max echo 1 > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max else - REPORT_DEBUG echo Setting memory limit to max, meaning no limit + REPORT_DEBUG Setting memory limit to max, meaning no limit echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.max echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max fi diff --git a/src/edu/csus/ecs/pc2/core/execute/Executable.java b/src/edu/csus/ecs/pc2/core/execute/Executable.java index ad93794c9..89a273895 100644 --- a/src/edu/csus/ecs/pc2/core/execute/Executable.java +++ b/src/edu/csus/ecs/pc2/core/execute/Executable.java @@ -3770,7 +3770,7 @@ private boolean saveExecuteData(int dataSetNumber) executeDataWriter.println("compileResultCode='" + executionData.getCompileResultCode() + "'"); executeDataWriter.println("executeExitValue='" + executionData.getExecuteExitValue() + "'"); executeDataWriter.println("executeSuccess='" + executionData.isExecuteSucess() + "'"); - executeDataWriter.println("validationReturnCode ='" + executionData.getValidationReturnCode() + "'"); + executeDataWriter.println("validationReturnCode='" + executionData.getValidationReturnCode() + "'"); executeDataWriter.println("validationSuccess='" + executionData.isValidationSuccess() + "'"); executeDataWriter.println("validationResults='" + showNullAsEmpty(executionData.getValidationResults()) + "'"); executeDataWriter.println("compileTimeMS='" + executionData.getCompileTimeMS() + "'"); diff --git a/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java b/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java index ed3742985..0832e475e 100644 --- a/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java +++ b/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java @@ -1461,7 +1461,7 @@ public void mousePressed(MouseEvent e) { + "\n\nAny substitution string available for an executable is allowed here." - + "\n\nSo for example a judge's execute folder string like \"executesite{:siteid}judge{:clientname}_Run_{:runnumber}\" would change the execute folder to something like:" // + + "\n\nSo for example a judge's execute folder string like \"executesite{:siteid}{:clientname}_Run_{:runnumber}\" would change the execute folder to something like:" // + "\n executesite1judge1_Run_220 " // + "\n\nSubstitution values depend on the corresponding data having been loaded into the PC^2 Server; if there is no value defined for a" // diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh index c3b121849..f33a23a86 100644 --- a/support/judge_webcgi/cgi-bin/judge.sh +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -3,15 +3,33 @@ . ./webcommon.sh . ./cdpcommon.sh + TableHeader() { cat << EOFTH -Run IDDispJudgmentProblemTeamTest CasesLanguageJudgeTime Judged +Run ID +Disp +Judgment +Problem +Team +Test Cases +Language +Judge +Time Judged EOFTH } +MyTableStyles() +{ + cat << EOFMYSTYLES +th { + cursor: pointer; +} +EOFMYSTYLES +} + TableRow() { dir="$1" @@ -70,8 +88,11 @@ TableRow() ###ParseProblemYaml Preamble +Styles +MyTableStyles +EndStyles +StartHTMLDoc Header -LogButton StartTable TableHeader @@ -113,12 +134,13 @@ do numcases="??" fi # Note that GetJudgment also filled in exdata with the last execute data - GetLastTestCaseNumber "${exdata}" + GetTestCaseNumber "${exdata##./}" testcaseinfo=$((result+1))/${numcases} TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" fi done EndTable +TableSortScripts Trailer exit 0 diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh index 4c1e5b2c1..97c0c3391 100644 --- a/support/judge_webcgi/cgi-bin/showrun.sh +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -233,6 +233,9 @@ done DeleteOldestLinks Preamble - 'Run '$run +Styles +EndStyles +StartHTMLDoc HeaderNoBanner Details for Run Id $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" # Create links apache can access in our html folder diff --git a/support/judge_webcgi/cgi-bin/webcommon.sh b/support/judge_webcgi/cgi-bin/webcommon.sh index 03fddd8a3..2e513343f 100644 --- a/support/judge_webcgi/cgi-bin/webcommon.sh +++ b/support/judge_webcgi/cgi-bin/webcommon.sh @@ -29,6 +29,12 @@ Preamble() PC² $headmsg +PREEOF +} + +Styles() +{ + cat << EOFSTYLE +EndStyles() +{ + echo "" +} + +StartHTMLDoc() +{ + cat << EOFSHTML -PREEOF +EOFSHTML +} + +TableSortScripts() +{ + cat << EOFSCR + + +EOFSCR } Header() diff --git a/support/judge_webcgi/scripts/jquery-3.7.1.slim.min.js b/support/judge_webcgi/scripts/jquery-3.7.1.slim.min.js new file mode 100644 index 000000000..35906b929 --- /dev/null +++ b/support/judge_webcgi/scripts/jquery-3.7.1.slim.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(ie,e){"use strict";var oe=[],r=Object.getPrototypeOf,ae=oe.slice,g=oe.flat?function(e){return oe.flat.call(e)}:function(e){return oe.concat.apply([],e)},s=oe.push,se=oe.indexOf,n={},i=n.toString,ue=n.hasOwnProperty,o=ue.toString,a=o.call(Object),le={},v=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},y=function(e){return null!=e&&e===e.window},m=ie.document,u={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||m).createElement("script");if(o.text=e,t)for(r in u)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function x(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[i.call(e)]||"object":typeof e}var t="3.7.1 -ajax,-ajax/jsonp,-ajax/load,-ajax/script,-ajax/var/location,-ajax/var/nonce,-ajax/var/rquery,-ajax/xhr,-manipulation/_evalUrl,-deprecated/ajax-event-alias,-effects,-effects/animatedSelector,-effects/Tween",l=/HTML$/i,ce=function(e,t){return new ce.fn.init(e,t)};function c(e){var t=!!e&&"length"in e&&e.length,n=x(e);return!v(e)&&!y(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+ge+")"+ge+"*"),b=new RegExp(ge+"|>"),A=new RegExp(g),D=new RegExp("^"+t+"$"),N={ID:new RegExp("^#("+t+")"),CLASS:new RegExp("^\\.("+t+")"),TAG:new RegExp("^("+t+"|[*])"),ATTR:new RegExp("^"+d),PSEUDO:new RegExp("^"+g),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+ge+"*(even|odd|(([+-]|)(\\d*)n|)"+ge+"*(?:([+-]|)"+ge+"*(\\d+)|))"+ge+"*\\)|)","i"),bool:new RegExp("^(?:"+f+")$","i"),needsContext:new RegExp("^"+ge+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+ge+"*((?:-\\d)?\\d*)"+ge+"*\\)|)(?=[^-]|$)","i")},L=/^(?:input|select|textarea|button)$/i,j=/^h\d$/i,O=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,P=/[+~]/,H=new RegExp("\\\\[\\da-fA-F]{1,6}"+ge+"?|\\\\([^\\r\\n\\f])","g"),q=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},R=function(){V()},M=K(function(e){return!0===e.disabled&&fe(e,"fieldset")},{dir:"parentNode",next:"legend"});try{E.apply(oe=ae.call(ye.childNodes),ye.childNodes),oe[ye.childNodes.length].nodeType}catch(e){E={apply:function(e,t){me.apply(e,ae.call(t))},call:function(e){me.apply(e,ae.call(arguments,1))}}}function I(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,d=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==d&&9!==d&&11!==d)return n;if(!r&&(V(e),e=e||C,T)){if(11!==d&&(u=O.exec(t)))if(i=u[1]){if(9===d){if(!(a=e.getElementById(i)))return n;if(a.id===i)return E.call(n,a),n}else if(f&&(a=f.getElementById(i))&&I.contains(e,a)&&a.id===i)return E.call(n,a),n}else{if(u[2])return E.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&e.getElementsByClassName)return E.apply(n,e.getElementsByClassName(i)),n}if(!(h[t+" "]||p&&p.test(t))){if(c=t,f=e,1===d&&(b.test(t)||m.test(t))){(f=P.test(t)&&X(e.parentNode)||e)==e&&le.scope||((s=e.getAttribute("id"))?s=ce.escapeSelector(s):e.setAttribute("id",s=k)),o=(l=Y(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+G(l[o]);c=l.join(",")}try{return E.apply(n,f.querySelectorAll(c)),n}catch(e){h(t,!0)}finally{s===k&&e.removeAttribute("id")}}}return re(t.replace(ve,"$1"),e,n,r)}function W(){var r=[];return function e(t,n){return r.push(t+" ")>x.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function B(e){return e[k]=!0,e}function F(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function $(t){return function(e){return fe(e,"input")&&e.type===t}}function _(t){return function(e){return(fe(e,"input")||fe(e,"button"))&&e.type===t}}function z(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&M(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function U(a){return B(function(o){return o=+o,B(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function X(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}function V(e){var t,n=e?e.ownerDocument||e:ye;return n!=C&&9===n.nodeType&&n.documentElement&&(r=(C=n).documentElement,T=!ce.isXMLDoc(C),i=r.matches||r.webkitMatchesSelector||r.msMatchesSelector,r.msMatchesSelector&&ye!=C&&(t=C.defaultView)&&t.top!==t&&t.addEventListener("unload",R),le.getById=F(function(e){return r.appendChild(e).id=ce.expando,!C.getElementsByName||!C.getElementsByName(ce.expando).length}),le.disconnectedMatch=F(function(e){return i.call(e,"*")}),le.scope=F(function(){return C.querySelectorAll(":scope")}),le.cssHas=F(function(){try{return C.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),le.getById?(x.filter.ID=function(e){var t=e.replace(H,q);return function(e){return e.getAttribute("id")===t}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&T){var n=t.getElementById(e);return n?[n]:[]}}):(x.filter.ID=function(e){var n=e.replace(H,q);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},x.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&T){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),x.find.TAG=function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):t.querySelectorAll(e)},x.find.CLASS=function(e,t){if("undefined"!=typeof t.getElementsByClassName&&T)return t.getElementsByClassName(e)},p=[],F(function(e){var t;r.appendChild(e).innerHTML="",e.querySelectorAll("[selected]").length||p.push("\\["+ge+"*(?:value|"+f+")"),e.querySelectorAll("[id~="+k+"-]").length||p.push("~="),e.querySelectorAll("a#"+k+"+*").length||p.push(".#.+[+~]"),e.querySelectorAll(":checked").length||p.push(":checked"),(t=C.createElement("input")).setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),r.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&p.push(":enabled",":disabled"),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||p.push("\\["+ge+"*name"+ge+"*="+ge+"*(?:''|\"\")")}),le.cssHas||p.push(":has"),p=p.length&&new RegExp(p.join("|")),l=function(e,t){if(e===t)return a=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!le.sortDetached&&t.compareDocumentPosition(e)===n?e===C||e.ownerDocument==ye&&I.contains(ye,e)?-1:t===C||t.ownerDocument==ye&&I.contains(ye,t)?1:o?se.call(o,e)-se.call(o,t):0:4&n?-1:1)}),C}for(e in I.matches=function(e,t){return I(e,null,null,t)},I.matchesSelector=function(e,t){if(V(e),T&&!h[t+" "]&&(!p||!p.test(t)))try{var n=i.call(e,t);if(n||le.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){h(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(H,q),e[3]=(e[3]||e[4]||e[5]||"").replace(H,q),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||I.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&I.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return N.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&A.test(n)&&(t=Y(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(H,q).toLowerCase();return"*"===e?function(){return!0}:function(e){return fe(e,t)}},CLASS:function(e){var t=s[e+" "];return t||(t=new RegExp("(^|"+ge+")"+e+"("+ge+"|$)"))&&s(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=I.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function T(e,n,r){return v(n)?ce.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?ce.grep(e,function(e){return e===n!==r}):"string"!=typeof n?ce.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(ce.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||E,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:k.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof ce?t[0]:t,ce.merge(this,ce.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:m,!0)),C.test(r[1])&&ce.isPlainObject(t))for(r in t)v(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=m.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):v(e)?void 0!==n.ready?n.ready(e):e(ce):ce.makeArray(e,this)}).prototype=ce.fn,E=ce(m);var S=/^(?:parents|prev(?:Until|All))/,A={children:!0,contents:!0,next:!0,prev:!0};function D(e,t){while((e=e[t])&&1!==e.nodeType);return e}ce.fn.extend({has:function(e){var t=ce(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,Ce=/^$|^module$|\/(?:java|ecma)script/i;re=m.createDocumentFragment().appendChild(m.createElement("div")),(be=m.createElement("input")).setAttribute("type","radio"),be.setAttribute("checked","checked"),be.setAttribute("name","t"),re.appendChild(be),le.checkClone=re.cloneNode(!0).cloneNode(!0).lastChild.checked,re.innerHTML="",le.noCloneChecked=!!re.cloneNode(!0).lastChild.defaultValue,re.innerHTML="",le.option=!!re.lastChild;var Te={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Ee(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&fe(e,t)?ce.merge([e],n):n}function ke(e,t){for(var n=0,r=e.length;n",""]);var Se=/<|&#?\w+;/;function Ae(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),d=[],p=0,h=e.length;p\s*$/g;function Re(e,t){return fe(e,"table")&&fe(11!==t.nodeType?t:t.firstChild,"tr")&&ce(e).children("tbody")[0]||e}function Me(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function Ie(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function We(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(_.hasData(e)&&(s=_.get(e).events))for(i in _.remove(t,"handle events"),s)for(n=0,r=s[i].length;n
",2===yt.childNodes.length),ce.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(le.createHTMLDocument?((r=(t=m.implementation.createHTMLDocument("")).createElement("base")).href=m.location.href,t.head.appendChild(r)):t=m),o=!n&&[],(i=C.exec(e))?[t.createElement(i[1])]:(i=Ae([e],t,o),o&&o.length&&ce(o).remove(),ce.merge([],i.childNodes)));var r,i,o},ce.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=ce.css(e,"position"),c=ce(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=ce.css(e,"top"),u=ce.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),v(t)&&(t=t.call(e,n,ce.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},ce.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){ce.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===ce.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===ce.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=ce(e).offset()).top+=ce.css(e,"borderTopWidth",!0),i.left+=ce.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-ce.css(r,"marginTop",!0),left:t.left-i.left-ce.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===ce.css(e,"position"))e=e.offsetParent;return e||K})}}),ce.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;ce.fn[t]=function(e){return R(this,function(e,t,n){var r;if(y(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),ce.each(["top","left"],function(e,n){ce.cssHooks[n]=Qe(le.pixelPosition,function(e,t){if(t)return t=Ve(e,n),$e.test(t)?ce(e).position()[n]+"px":t})}),ce.each({Height:"height",Width:"width"},function(a,s){ce.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){ce.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return R(this,function(e,t,n){var r;return y(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?ce.css(e,t,i):ce.style(e,t,n,i)},s,n?e:void 0,n)}})}),ce.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.on("mouseenter",e).on("mouseleave",t||e)}}),ce.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){ce.fn[n]=function(e,t){return 0 Date: Sun, 30 Jun 2024 17:06:20 -0400 Subject: [PATCH 35/55] i_972 Add support for better human judge reviewing Modify scripts to generate information to help a human judge figure out why a run failed. Better determination of which CPU to run a submission on in a sandbox. Add some basic Web page scripts to make judge results web pages on each judge machine. Add a configurable execute folder that allows substitute variables. This is good so that the execute folders may be retained for each run {:runnumber} in the execute folder, for example. CI: toString method of ExecutionData was not correct. --- scripts/pc2sandbox.sh | 2 + scripts/pc2sandbox_interactive.sh | 7 +- support/judge_webcgi/judge | 178 ++++++++++++++++++++++++++++++ support/judge_webcgi/showrun.sh | 106 ++++++++++++++++++ 4 files changed, 287 insertions(+), 6 deletions(-) mode change 100755 => 100644 scripts/pc2sandbox.sh mode change 100755 => 100644 scripts/pc2sandbox_interactive.sh create mode 100644 support/judge_webcgi/judge create mode 100644 support/judge_webcgi/showrun.sh diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh old mode 100755 new mode 100644 index fe3de06a4..f6c76ba4b --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -244,6 +244,7 @@ if [ "$#" -lt 1 ] ; then exit $FAIL_NO_ARGS_EXIT_CODE fi + if [ "$#" -lt 6 ] ; then echo $0: expected 6 or more arguments, found: $* SysFailure Expected 6 or more arguments to $0, found: $* @@ -368,6 +369,7 @@ else echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max fi +REPORT Test case $TESTCASE: # We use ulimit to limit CPU time, not cgroups. Time is supplied in seconds. This may have to # be reworked if ms accuracy is needed. The problem is, cgroups do not kill off a process that # exceeds the time limit, ulimit does. diff --git a/scripts/pc2sandbox_interactive.sh b/scripts/pc2sandbox_interactive.sh old mode 100755 new mode 100644 index 4c45ab5f7..ab6ed7f90 --- a/scripts/pc2sandbox_interactive.sh +++ b/scripts/pc2sandbox_interactive.sh @@ -315,6 +315,7 @@ DEBUG echo "+---------------- Test Case ${TESTCASE} ----------------+" DEBUG echo Command line: $0 $* shift 7 +# the rest of the commmand line arguments are the command args for the submission DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" RUN_LOCAL_CMD="$0 ${MEMLIMIT} ${TIMELIMIT} ${VALIDATOR} ${JUDGEIN} ${JUDGEANS} ${TESTCASE} ${COMMAND} $*" tcfile=`printf "$REPORTDIR/testcase_%03d.log" $TESTCASE` @@ -325,12 +326,6 @@ DEBUG echo -e "\n${RUN_LOCAL_CMD}" DEBUG echo -e "\nAnd see the run report using the following command:" DEBUG echo -e "\n${REPORT_OUTPUT_CMD}\n" -# Skip used arguments -shift 7 - -# the rest of the commmand line arguments are the command args for the submission - - # make sure we have CGroups V2 properly installed on this system, including a PC2 structure DEBUG echo checking PC2 CGroup V2 installation... diff --git a/support/judge_webcgi/judge b/support/judge_webcgi/judge new file mode 100644 index 000000000..3d850abff --- /dev/null +++ b/support/judge_webcgi/judge @@ -0,0 +1,178 @@ +#!/bin/bash +PC2_RUN_DIR=/home/icpc/pc2 +PC2_CDP=${PC2_RUN_DIR}/current/config +BANNER_FILE=banner.png +BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} +###PROBLEMSET_YAML=problemset.yaml +###PROBLEMSET_YAML_PATH=${PC2_CDP}/${PROBLEMSET_YAML} + +###declare -A problet_to_name + +# +# Display very curt textual error message +# +Error() +{ + echo "Content-type: text/plain" + echo "" + echo ERROR $* + echo $now Error: $* >> $LOGFILE +} + +###ParseProblemYaml() +###{ +### CURDIR="$PWD" +### tmpdir=/tmp/probset$$ +### mkdir $tmpdir +### # Need a copy since web doesnt have access to full path +### cp ${PROBLEMSET_YAML_PATH} $tmpdir +### cd $tmpdir +### csplit --prefix="x$USER" -q ${PROBLEMSET_YAML} "/^ *- /" "{*}" +### for file in $(echo "x$USER"*) +### do +### letter=`sed -n -e 's/^ *letter: \([A-Z]\).*/\1/p' < $file` +### short=`sed -n -e 's/.* short-name: \(.*\)$/\1/p' < $file` +### if test -n "$letter" -a -n "$short" +### then +### problet_to_name[$letter]="$short" +### fi +### done +### cd $CURDIR +### rm -r $tmpdir +###} + +Preamble() +{ + echo "Content-type: text/html" + echo "" +} + +Header() +{ + if test ! -e ../${BANNER_FILE} + then + ln -s ${BANNER_IMAGE} ../${BANNER_FILE} + fi +cat << EOF + + + +PC² Judge 1 + + + +
+ +

PC2 Judging Results for Judge 1

+
+

+EOF +} + +Trailer() +{ +cat << EOF2 + + +EOF2 +} + +StartTable() +{ + cat << EOF3 +

+ + + +EOF3 +} + +EndTable() +{ + cat << EOF4 +
Run IDTeamProblemLanguageTime Judged
+

+EOF4 +} + +TableRow() +{ + dir="$1" + runid=$2 + problet=$3 + shortname=$4 + langid=$5 + teamnum=$6 + runtime=`stat -c '%y' $dir` + probname="" + probdir="" + if test -n ${shortname} + then + probdir=${PC2_CDP}/${shortname} + probstatement=${probdir}/problem_statement/problem.en.tex + if test ! -s ${probstatement} + then + probstatement=${probdir}/problem_statement/problem.tex + fi + if test -s ${probstatement} + then + probname=`head -1 ${probstatement}` + probname=${probname##*{} + probname=${probname%\}} + fi + fi + problem="$problet - $probname (${shortname})" + echo ''"Run $runidteam$teamnum$problem$langid$runtime" +} + +###ParseProblemYaml +Preamble +Header +StartTable +# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid +for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+$' | sort --field-separator=_ +1rn` +do + # exdir looks like: ex_188_Y_compression_46103_cpp + # RId P ProbShort team# Lang + # RId = Run ID + # P = problem letter + # Lang = CLICS Language id + saveIFS="$IFS" + IFS="_" + set ${exdir} + IFS="$saveIFS" + if test $# -eq 6 + then + runid=$2 + problet=$3 + probshort=$4 + teamnum=$5 + langid=$6 + TableRow "${PC2_RUN_DIR}/$exdir" $runid $problet $probshort $langid $teamnum + fi +done +EndTable +Trailer +exit 0 diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh new file mode 100644 index 000000000..c4bfbe7b0 --- /dev/null +++ b/support/judge_webcgi/showrun.sh @@ -0,0 +1,106 @@ +#!/bin/bash +# +# Display very curt textual error message +# +Error() +{ + echo "Content-type: text/plain" + echo "" + echo ERROR $* + echo $now Error: $* >> $LOGFILE +} + +Preamble() +{ + echo "Content-type: text/html" + echo "" +} + +Header() +{ +cat << EOF + + + +PC² Judge 1 + + +

+

PC2 Judging Results for Judge 1

+
+

+EOF +} + +Trailer() +{ +cat << EOF2 + + +EOF2 +} + +StartTable() +{ + cat << EOF3 +

+ + + +EOF3 +} + +EndTable() +{ + cat << EOF4 +
Run IDTime Judged
+

+EOF4 +} + +TableRow() +{ + dir="$1" + runid=${dir#../Run} + runtime=`stat -c '%y' $dir` + echo ''"Run $runid$runtime" +} + +Preamble +Header + +# Parse query string into dictionary args +sIFS="$IFS" +IFS='=&' +declare -a parm +parm=($QUERY_STRING) +IFS=$sIFS +declare -A args +for ((i=0; i<${#parm[@]}; i+=2)) +do + args[${parm[i]}]=${parm[i+1]} + echo "${parm[i]} = ${args[${parm[i]}]}
" +done +# Done parsing + +echo '

The run is '${args["run"]} in problem directory ${args["probdir"]}. The execute folder is: ${args["dir"]} +Trailer +exit 0 From 113b6957cca94d90aaf47d9a6783977c1ce477d4 Mon Sep 17 00:00:00 2001 From: John Buck Date: Sun, 30 Jun 2024 21:36:18 -0400 Subject: [PATCH 36/55] i_972 Add script to fetch judgement from execute folder Ongoing work in making human judging of results easier. --- support/judge_webcgi/getjudgment.sh | 114 ++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 support/judge_webcgi/getjudgment.sh diff --git a/support/judge_webcgi/getjudgment.sh b/support/judge_webcgi/getjudgment.sh new file mode 100644 index 000000000..b24514dfc --- /dev/null +++ b/support/judge_webcgi/getjudgment.sh @@ -0,0 +1,114 @@ +#!/bin/bash +# Where to the the result of the first failure. If this file is not created, then the +# run was accepted. (correct) +RESULT_FAILURE_FILE=failure.txt + +# Where judgments are +REJECT_INI=$HOME/pc2/reject.ini + +# Where PC2 puts CLICS validator results +EXECUTE_DATA_PREFIX=executedata + +declare -A Judgments + +InitJudgments() +{ + # Defauls, may be overriden by reject.ini + Judgments["accepted"]="AC" + Judgments["Accepted"]="AC" + Judgments["timelimit"]="TLE" + Judgments["run error"]="RTE" + Judgments["compiler error"]="CE" + if test -s ${REJECT_INIT} + then + while read j + do + if [[ $j = \#* ]] + then + continue + fi + savIFS="$IFS" + IFS='|' + set $j + IFS="$savIFS" + key="$1" + shortcode="$2" + case ${shortcode} in + AC|CE|RTE|WA|TLE) ;; + MLE) shortcode="RTE (MLE)" ;; + *) shortcode="WA (${shortcode})" ;; + esac +# echo Mapping $key to $shortcode + Judgments[$key]="$shortcode" + done < $REJECT_INI + fi +} + +# Takes the judgement string, eg. "Wrong answer" as arg 1 and the validation Result (42 or 42) as arg 2 +MapJudgment() +{ + jm="$1" + vr="$2" + jv=${Judgments[$jm]} + if test -z ${jv} + then + if test ${validationReturnCode} -eq 0 + then + if test ${vr} = "43" + then + jv="WA" + elif test ${vr} = "42" + then + jv="AC" + else + jv="WA (Default)" + fi + else + jv="JE (Validator EC=${validationReturnCode})" + fi + + fi + echo $jv +} + +GetJudgment() +{ + dir=$1 + if ! cd ${dir} + then + echo "Not found" + else + # We got a real live run + # Check out the biggest executedata file + exdata=`ls ${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sort -t. +1rn | head -1` + if test -z "${exdata}" + then + echo "No results" + else + # Source the file + . ./${exdata} + if test ${compileSuccess} = "false" + then + echo "CE" + elif test ${executeSuccess} = "true" + then + if test ${validationSuccess} = "true" + then + MapJudgment "${validationResults}" "${validationReturnCode}" + else + echo "JE (Validator error)" + fi + else + echo "RTE (Execute error)" + fi + fi + fi +} + +InitJudgments + +for file in $* +do + j=`GetJudgment $file` + echo $file: $j +done From cac74a4fd6e8249a6b330544f79376e8ba7a8baa Mon Sep 17 00:00:00 2001 From: John Buck Date: Mon, 1 Jul 2024 16:39:45 -0400 Subject: [PATCH 37/55] i_972 Update judge web scripts More updates to make judging easier. Add judge number support {:clientid} to execute folder in scripts --- support/judge_webcgi/getjudgment.sh | 114 ------------------ support/judge_webcgi/judge | 178 ---------------------------- support/judge_webcgi/showrun.sh | 106 ----------------- 3 files changed, 398 deletions(-) delete mode 100644 support/judge_webcgi/getjudgment.sh delete mode 100644 support/judge_webcgi/judge delete mode 100644 support/judge_webcgi/showrun.sh diff --git a/support/judge_webcgi/getjudgment.sh b/support/judge_webcgi/getjudgment.sh deleted file mode 100644 index b24514dfc..000000000 --- a/support/judge_webcgi/getjudgment.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/bin/bash -# Where to the the result of the first failure. If this file is not created, then the -# run was accepted. (correct) -RESULT_FAILURE_FILE=failure.txt - -# Where judgments are -REJECT_INI=$HOME/pc2/reject.ini - -# Where PC2 puts CLICS validator results -EXECUTE_DATA_PREFIX=executedata - -declare -A Judgments - -InitJudgments() -{ - # Defauls, may be overriden by reject.ini - Judgments["accepted"]="AC" - Judgments["Accepted"]="AC" - Judgments["timelimit"]="TLE" - Judgments["run error"]="RTE" - Judgments["compiler error"]="CE" - if test -s ${REJECT_INIT} - then - while read j - do - if [[ $j = \#* ]] - then - continue - fi - savIFS="$IFS" - IFS='|' - set $j - IFS="$savIFS" - key="$1" - shortcode="$2" - case ${shortcode} in - AC|CE|RTE|WA|TLE) ;; - MLE) shortcode="RTE (MLE)" ;; - *) shortcode="WA (${shortcode})" ;; - esac -# echo Mapping $key to $shortcode - Judgments[$key]="$shortcode" - done < $REJECT_INI - fi -} - -# Takes the judgement string, eg. "Wrong answer" as arg 1 and the validation Result (42 or 42) as arg 2 -MapJudgment() -{ - jm="$1" - vr="$2" - jv=${Judgments[$jm]} - if test -z ${jv} - then - if test ${validationReturnCode} -eq 0 - then - if test ${vr} = "43" - then - jv="WA" - elif test ${vr} = "42" - then - jv="AC" - else - jv="WA (Default)" - fi - else - jv="JE (Validator EC=${validationReturnCode})" - fi - - fi - echo $jv -} - -GetJudgment() -{ - dir=$1 - if ! cd ${dir} - then - echo "Not found" - else - # We got a real live run - # Check out the biggest executedata file - exdata=`ls ${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sort -t. +1rn | head -1` - if test -z "${exdata}" - then - echo "No results" - else - # Source the file - . ./${exdata} - if test ${compileSuccess} = "false" - then - echo "CE" - elif test ${executeSuccess} = "true" - then - if test ${validationSuccess} = "true" - then - MapJudgment "${validationResults}" "${validationReturnCode}" - else - echo "JE (Validator error)" - fi - else - echo "RTE (Execute error)" - fi - fi - fi -} - -InitJudgments - -for file in $* -do - j=`GetJudgment $file` - echo $file: $j -done diff --git a/support/judge_webcgi/judge b/support/judge_webcgi/judge deleted file mode 100644 index 3d850abff..000000000 --- a/support/judge_webcgi/judge +++ /dev/null @@ -1,178 +0,0 @@ -#!/bin/bash -PC2_RUN_DIR=/home/icpc/pc2 -PC2_CDP=${PC2_RUN_DIR}/current/config -BANNER_FILE=banner.png -BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} -###PROBLEMSET_YAML=problemset.yaml -###PROBLEMSET_YAML_PATH=${PC2_CDP}/${PROBLEMSET_YAML} - -###declare -A problet_to_name - -# -# Display very curt textual error message -# -Error() -{ - echo "Content-type: text/plain" - echo "" - echo ERROR $* - echo $now Error: $* >> $LOGFILE -} - -###ParseProblemYaml() -###{ -### CURDIR="$PWD" -### tmpdir=/tmp/probset$$ -### mkdir $tmpdir -### # Need a copy since web doesnt have access to full path -### cp ${PROBLEMSET_YAML_PATH} $tmpdir -### cd $tmpdir -### csplit --prefix="x$USER" -q ${PROBLEMSET_YAML} "/^ *- /" "{*}" -### for file in $(echo "x$USER"*) -### do -### letter=`sed -n -e 's/^ *letter: \([A-Z]\).*/\1/p' < $file` -### short=`sed -n -e 's/.* short-name: \(.*\)$/\1/p' < $file` -### if test -n "$letter" -a -n "$short" -### then -### problet_to_name[$letter]="$short" -### fi -### done -### cd $CURDIR -### rm -r $tmpdir -###} - -Preamble() -{ - echo "Content-type: text/html" - echo "" -} - -Header() -{ - if test ! -e ../${BANNER_FILE} - then - ln -s ${BANNER_IMAGE} ../${BANNER_FILE} - fi -cat << EOF - - - -PC² Judge 1 - - - -

- -

PC2 Judging Results for Judge 1

-
-

-EOF -} - -Trailer() -{ -cat << EOF2 - - -EOF2 -} - -StartTable() -{ - cat << EOF3 -

- - - -EOF3 -} - -EndTable() -{ - cat << EOF4 -
Run IDTeamProblemLanguageTime Judged
-

-EOF4 -} - -TableRow() -{ - dir="$1" - runid=$2 - problet=$3 - shortname=$4 - langid=$5 - teamnum=$6 - runtime=`stat -c '%y' $dir` - probname="" - probdir="" - if test -n ${shortname} - then - probdir=${PC2_CDP}/${shortname} - probstatement=${probdir}/problem_statement/problem.en.tex - if test ! -s ${probstatement} - then - probstatement=${probdir}/problem_statement/problem.tex - fi - if test -s ${probstatement} - then - probname=`head -1 ${probstatement}` - probname=${probname##*{} - probname=${probname%\}} - fi - fi - problem="$problet - $probname (${shortname})" - echo ''"Run $runidteam$teamnum$problem$langid$runtime" -} - -###ParseProblemYaml -Preamble -Header -StartTable -# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid -for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+$' | sort --field-separator=_ +1rn` -do - # exdir looks like: ex_188_Y_compression_46103_cpp - # RId P ProbShort team# Lang - # RId = Run ID - # P = problem letter - # Lang = CLICS Language id - saveIFS="$IFS" - IFS="_" - set ${exdir} - IFS="$saveIFS" - if test $# -eq 6 - then - runid=$2 - problet=$3 - probshort=$4 - teamnum=$5 - langid=$6 - TableRow "${PC2_RUN_DIR}/$exdir" $runid $problet $probshort $langid $teamnum - fi -done -EndTable -Trailer -exit 0 diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh deleted file mode 100644 index c4bfbe7b0..000000000 --- a/support/judge_webcgi/showrun.sh +++ /dev/null @@ -1,106 +0,0 @@ -#!/bin/bash -# -# Display very curt textual error message -# -Error() -{ - echo "Content-type: text/plain" - echo "" - echo ERROR $* - echo $now Error: $* >> $LOGFILE -} - -Preamble() -{ - echo "Content-type: text/html" - echo "" -} - -Header() -{ -cat << EOF - - - -PC² Judge 1 - - -

-

PC2 Judging Results for Judge 1

-
-

-EOF -} - -Trailer() -{ -cat << EOF2 - - -EOF2 -} - -StartTable() -{ - cat << EOF3 -

- - - -EOF3 -} - -EndTable() -{ - cat << EOF4 -
Run IDTime Judged
-

-EOF4 -} - -TableRow() -{ - dir="$1" - runid=${dir#../Run} - runtime=`stat -c '%y' $dir` - echo ''"Run $runid$runtime" -} - -Preamble -Header - -# Parse query string into dictionary args -sIFS="$IFS" -IFS='=&' -declare -a parm -parm=($QUERY_STRING) -IFS=$sIFS -declare -A args -for ((i=0; i<${#parm[@]}; i+=2)) -do - args[${parm[i]}]=${parm[i+1]} - echo "${parm[i]} = ${args[${parm[i]}]}
" -done -# Done parsing - -echo '

The run is '${args["run"]} in problem directory ${args["probdir"]}. The execute folder is: ${args["dir"]} -Trailer -exit 0 From 62e9c5dff7b81d26b2647b663735e34d3096b4b4 Mon Sep 17 00:00:00 2001 From: John Buck Date: Tue, 2 Jul 2024 07:11:02 -0400 Subject: [PATCH 38/55] i_972 Updated judge help web scripts Also added Warning icon --- support/judge_webcgi/judge.sh | 123 ++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 support/judge_webcgi/judge.sh diff --git a/support/judge_webcgi/judge.sh b/support/judge_webcgi/judge.sh new file mode 100644 index 000000000..c4bbd86ca --- /dev/null +++ b/support/judge_webcgi/judge.sh @@ -0,0 +1,123 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + +TableHeader() +{ + cat << EOFTH + +Run IDDispJudgmentProblemTeamTest CasesLanguageJudgeTime Judged + +EOFTH +} + +TableRow() +{ + dir="$1" + runid=$2 + problet=$3 + shortname=$4 + langid=$5 + teamnum=$6 + judgment="$7" + runtime="$8" + testinfo="$9" + judge="${10}" + probname="" + probdir="" + if test -n ${shortname} + then + probdir=${PC2_CDP}/${shortname} + probstatement=${probdir}/problem_statement/problem.en.tex + if test ! -s ${probstatement} + then + probstatement=${probdir}/problem_statement/problem.tex + fi + if test -s ${probstatement} + then + probname=`head -1 ${probstatement}` + probname=${probname##*\{} + probname=${probname%\}} + fi + fi + problem="$problet - $probname (${shortname})" + if test "${judgment}" = "AC" + then + jstyle="green" + jicon='' + elif test "${judgment}" = "CE" + then + jicon='' + else + jstyle="red" + jicon='' + fi + echo ' ' + echo ' '"Run $runid" +# echo ' '$judgment"" +# echo '

'$judgment"
" + echo ' '$jicon'' + echo ' '$judgment'' + echo " $problem" + echo " team$teamnum" + echo " $testinfo" + echo " $langid" + echo " $judge" + echo " $runtime" + echo " " +} + +###ParseProblemYaml +Preamble +Header +StartTable +TableHeader + +# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid_judge +for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+' | sort --field-separator=_ +1rn` +do + # exdir looks like: ex_188_Y_compression_46103_cpp + # RId P ProbShort team# Lang + # RId = Run ID + # P = problem letter + # Lang = CLICS Language id + saveIFS="$IFS" + IFS="_" + set ${exdir} + IFS="$saveIFS" + if test $# -ge 6 + then + exedir=${PC2_RUN_DIR}/$exdir + runid=$2 + problet=$3 + probshort=$4 + teamnum=$5 + langid=$6 + judge=$7 + if test -z "${judge}" + then + judge="N/A" + fi + GetJudgment "${exedir}" + judgment="${result}" + runtime="${executeDateTime}" + # Get how many total test cases there are + probdir=${PC2_CDP}/${probshort} + if test -n "${probdir}" + then + GetNumberOfTestCases "${probdir}" + numcases=${result} + else + numcases="??" + fi + # Note that GetJudgment also filled in exdata with the last execute data + GetLastTestCaseNumber "${exdata}" + testcaseinfo=$((result+1))/${numcases} + + TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" + fi +done +EndTable +Trailer +exit 0 From e3238ef205919d47380d8883406aef83800c45fd Mon Sep 17 00:00:00 2001 From: John Buck Date: Tue, 2 Jul 2024 16:43:06 -0400 Subject: [PATCH 39/55] i_972 More web script updates --- support/judge_webcgi/judge.sh | 1 + support/judge_webcgi/pc2common.sh | 211 ++++++++++++++++++++++++++++++ support/judge_webcgi/showrun.sh | 209 +++++++++++++++++++++++++++++ support/judge_webcgi/webcommon.sh | 157 ++++++++++++++++++++++ 4 files changed, 578 insertions(+) create mode 100644 support/judge_webcgi/pc2common.sh create mode 100644 support/judge_webcgi/showrun.sh create mode 100644 support/judge_webcgi/webcommon.sh diff --git a/support/judge_webcgi/judge.sh b/support/judge_webcgi/judge.sh index c4bbd86ca..e1b6bb740 100644 --- a/support/judge_webcgi/judge.sh +++ b/support/judge_webcgi/judge.sh @@ -71,6 +71,7 @@ TableRow() ###ParseProblemYaml Preamble Header +LogButton StartTable TableHeader diff --git a/support/judge_webcgi/pc2common.sh b/support/judge_webcgi/pc2common.sh new file mode 100644 index 000000000..54723de21 --- /dev/null +++ b/support/judge_webcgi/pc2common.sh @@ -0,0 +1,211 @@ +# Meant to be "source'd" into bash scripts. + +# The judge's home directory +JUDGE_HOME=/home/icpc + +# Modify constants as necessary for your installation +PC2_RUN_DIR=${JUDGE_HOME}/pc2 +PC2_CDP=${PC2_RUN_DIR}/current/config + +EXE_LINK_PREFIX=../exedir +PROB_LINK_PREFIX=../probdir + +# How many exedir and probdir links to keep around +NUM_LINK_KEEP=4 + +# Where can we find the contest banner png file for webpage headers +BANNER_FILE=banner.png +BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} + +# Where to the the result of script failure before execution +RESULT_FAILURE_FILE=failure.txt + +# Where judgments are +REJECT_INI=${JUDGE_HOME}/pc2/reject.ini + +# Where PC2 puts CLICS validator results +EXECUTE_DATA_PREFIX=executedata +# Where PC2 puts run output/error +TEST_OUT_PREFIX="teamoutput" +TEST_ERR_PREFIX="teamstderr" +TEST_VALOUT_PREFIX="valout" +TEST_VALERR_PREFIX="valerr" + +# Detailed log of entire judging process +SANDBOX_LOG=sandbox.log + +# Briefcase file prefix +BRIEFCASE_FILE_PREFIX="briefcase" +REPORTS_DIR=reports + +declare -A Judgments + +InitJudgments() +{ + # Defauls, may be overriden by reject.ini + Judgments["accepted"]="AC" + Judgments["Accepted"]="AC" + Judgments["timelimit"]="TLE" + Judgments["run error"]="RTE" + Judgments["compiler error"]="CE" + if test -s ${REJECT_INI} + then + while read j + do + if [[ $j = \#* ]] + then + continue + fi + savIFS="$IFS" + IFS='|' + set $j + IFS="$savIFS" + key="$1" + shortcode="$2" + case ${shortcode} in + AC|CE|RTE|WA|TLE) ;; + MLE) shortcode="RTE (MLE)" ;; + *) shortcode="WA (${shortcode})" ;; + esac +# echo Mapping $key to $shortcode + Judgments[$key]="$shortcode" + done < $REJECT_INI + else echo NO REJECT FILE + fi +} + +# Takes the judgement string, eg. "Wrong answer" as arg 1 and the validation Result (42 or 42) as arg 2 +MapJudgment() +{ + jm="$1" + vr="$2" + result=${Judgments[$jm]} + if test -z ${result} + then + if test ${validationReturnCode} -eq 0 + then + if test ${vr} = "43" + then + resul="WA" + elif test ${vr} = "42" + then + result="AC" + else + result="WA (Default)" + fi + else + result="JE (Validator EC=${validationReturnCode})" + fi + + fi +} + +GetLastJudgmentFile() +{ + lj_exdir="$1" + result=`ls $lj_exdir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1rn | head -1` +} + +GetTestCaseNumber() +{ + if test -z "$1" + then + result=0 + else + saveIFS="$IFS" + IFS=. + set ${1} + result="$2" + IFS="$saveIFS" + if test -z "${result}" + then + result=0 + fi + fi +} + +GetJudgmentFromFile() +{ + exdata="$1" + if test -z "${exdata}" + then + result="No results" + else + # Source the file + . ${exdata} + if test ${compileSuccess} = "false" + then + result="CE" + elif test ${executeSuccess} = "true" + then + if test ${validationSuccess} = "true" + then + MapJudgment "${validationResults}" "${validationReturnCode}" + else + result="JE (Validator error)" + fi + else + result="RTE (Execute error)" + fi + fi +} + +GetJudgment() +{ + dir=$1 + exdata="" + if ! cd ${dir} + then + result="Not found" + elif test -s ${RESULT_FAILURE_FILE} + then + jerr=`cat ${RESULT_FAILURE_FILE}` + result="JE ($jerr)" + else + # We got a real live run + # Check out the biggest executedata file + GetLastJudgmentFile $dir + GetJudgmentFromFile ./${result} + fi +} + +MakeBriefcaseFile() +{ + d="$1" + t="$2" + result=`printf '%s/%s/%s_%03d.log' "$d" "$REPORTS_DIR" "$BRIEFCASE_FILE_PREFIX" "$t"` +} + +# Must redirect from briefcase file +ReadBriefcase() +{ + read judgein + read judgeans + read cpunum + read exepid + read exetime + read execpums cpulimms exewallms mempeak memlim + if [[ $mempeak = [0-9]* ]] + then + mempeak=$((mempeak/(1024*1024))) + fi + if [[ $memlim = [0-9]* ]] + then + memlim=$((memlim/(1024*1024))) + fi +} + +DeleteOldestLinks() +{ + for linkpref in ${EXE_LINK_PREFIX} ${PROB_LINK_PREFIX} + do + dellist=`ls -1td ${linkpref}* | sed 1,${NUM_LINK_KEEP}d` + if test -n "${dellist}" + then + rm -f ${dellist} + fi + done +} + +InitJudgments + diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh new file mode 100644 index 000000000..e88937dea --- /dev/null +++ b/support/judge_webcgi/showrun.sh @@ -0,0 +1,209 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + +EXE_DIR_LINK=${EXE_LINK_PREFIX}$$ +PROB_DIR_LINK=${PROB_LINK_PREFIX}$$ + +# Provide a way to look at the sandbox log +LogButton() +{ + # Read first judgment file to get compile time - it's compiled once for all + GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" + if test -n ${result} -a -n ${compileTimeMS} + then + cat << LBEOF0 +
+

The program took ${compileTimeMS}ms to compile. +

+LBEOF0 + fi + + # Read the first briefcase file (if any) for limits + MakeBriefcaseFile ${EXE_DIR_LINK} 1 + ReadBriefcase < $result + if test -n ${cpulimms} + then + cpulimms=${cpulimms%%.*} + cpusecs="$((cpulimms/1000))" + if test ${cpusecs} != "1" + then + cpusecs="${cpusecs} seconds" + else + cpusecs="1 second" + fi + cat << LBEOF1 +
+

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms). +

+LBEOF1 + fi + if test -n ${memlim} + then + if test ${memlim} = "0" + then + memlim="Unlimited" + else + memlim=${memlim}MB + fi + cat << LBEOF1A +
+

The Memory limit for this problem is ${memlim}. +

+LBEOF1A + fi + sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} + if test -s ${sandlog} + then + cat << LBEOF2 +Click here for the full sandbox log for this run +

+

+LBEOF2 + fi +} + +TableHeader() +{ + cat << EOFTH + +TestDispJudgmentExitExecute TimeVal TimeVal SuccessRun OutRun ErrJudge InJudge AnsVal OutVal Err + +EOFTH +} + +GenFileLink() +{ + tstcase="$2" + tstcase=$((tstcase-1)) + tstfile=$1.$tstcase.txt + tstpath=$dir/$1.$tstcase.txt + if test -s ${tstpath} + then + bytes=`stat -c %s ${tstpath}` + echo ' View ('$bytes' bytes)' + elif test -e ${tstpath} + then + echo ' (Empty)' + else + echo ' Not found' + fi +} + +GenFileLinkWithText() +{ + linkaddr="$1" + linktext="$2" + linkcolor="$3" + bytes=`stat -c %s ${linkaddr}` + echo ' '$linktext' ('$bytes' bytes)' +} + +TableRow() +{ + tc=$1 + judgment="$2" + ec=$3 + exetm="$4" + valtm="$5" + jin="$6" + jans="$7" + if test "$8" = "true" + then + valsucc=Yes + elif test "$8" = "false" + then + valsucc=No + else + valsucc="N/A" + fi + # Strip the stuff of before sample (or secret) + jin=${jin##*/data/} + jans=${jans##*/data/} + # Just the basenames for link text + jinbase=${jin##*/} + jansbase=${jans##*/} + if [[ $jin = sample/* ]] + then + lcolor=#00a0a0 + else + lcolor=#00a000 + fi + if test "${judgment}" = "AC" + then + jicon='' + elif test "${judgment}" = "CE" + then + jicon='' + else + jicon='' + fi + + echo ' ' + + echo ' '$tc'' + echo ' '$jicon'' + echo ' '$judgment'' + echo ' '$ec'' + echo ' '$exetm'ms' + echo ' '$valtm'ms' + echo ' '$valsucc'' + GenFileLink $TEST_OUT_PREFIX $tc + GenFileLink $TEST_ERR_PREFIX $tc + GenFileLinkWithText $PROB_DIR_LINK/data/"$jin" "$jinbase" $lcolor + GenFileLinkWithText $PROB_DIR_LINK/data/"$jans" "$jansbase" $lcolor + GenFileLink $TEST_VALOUT_PREFIX $tc + GenFileLink $TEST_VALERR_PREFIX $tc + + echo ' ' +} + +# Parse query string into dictionary args +# This is not terribly secure. +declare -a parm +sIFS="$IFS" +IFS='=&' +parm=($QUERY_STRING) +IFS=$sIFS +for ((i=0; i<${#parm[@]}; i+=2)) +do + a=${parm[i]} + eval $a=${parm[i+1]} +done +# Done parsing + +DeleteOldestLinks +Preamble - 'Run '$run +HeaderNoBanner Details for Run Id $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" + +# Create links apache can access in our html folder +#rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} +ln -s ${dir} ${EXE_DIR_LINK} +ln -s ${probdir} ${PROB_DIR_LINK} + +LogButton + +StartTable +TableHeader +for testcase in `ls $dir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1n` +do + GetTestCaseNumber $testcase + tc=$((result+1)) + # This will also source the execute data + GetJudgmentFromFile $dir/$testcase + judgment=$result + ec=$executeExitValue +# comptm=$compileTimeMS + exetm=$executeTimeMS + valtm=$validateTimeMS + valsuc=$validationSuccess + MakeBriefcaseFile "$dir" "$tc" + ReadBriefcase < ${result} + TableRow "$tc" "$judgment" "$ec" "$exetm" "$valtm" "$judgein" "$judgeans" "$valsuc" +done + +Trailer + +#rm -f ${EXE_DIR_LINK} ${PROB_DIR_LINK} +exit 0 diff --git a/support/judge_webcgi/webcommon.sh b/support/judge_webcgi/webcommon.sh new file mode 100644 index 000000000..26c65cd62 --- /dev/null +++ b/support/judge_webcgi/webcommon.sh @@ -0,0 +1,157 @@ +# File meant to be "source'd" to support generating web pages + +# +# Display very curt textual error message +# +Error() +{ + echo "Content-type: text/plain" + echo "" + echo ERROR $* +} + + +Preamble() +{ + echo "Content-type: text/html" + echo "" + if test $# -eq 0 + then + headmsg="Judge" + else + headmsg="$*" + fi + cat << PREEOF + + + +PC² $headmsg + + + +PREEOF +} + +Header() +{ + # Make sure link to banner page is in a place apache can read + if test ! -e ../${BANNER_FILE} + then + ln -s ${BANNER_IMAGE} ../${BANNER_FILE} + fi +cat << HDREOF +

+ +

PC2 Judging Results

+
+

+HDREOF +} + +HeaderNoBanner() +{ + if test $# -eq 0 + then + hdrmsg="Judging Results" + else + hdrmsg="$*" + fi +cat << EOF +

+

PC2 $hdrmsg

+
+

+EOF +} + +Trailer() +{ +cat << EOF2 + + +EOF2 +} + +StartTable() +{ + cat << EOF3 +

+ +EOF3 +} + +EndTable() +{ + cat << EOF4 +
+

+EOF4 +} + From 7f207dabe0bdbd14809e57924949fa8dd94b99e0 Mon Sep 17 00:00:00 2001 From: John Buck Date: Sat, 6 Jul 2024 17:57:47 -0400 Subject: [PATCH 40/55] i_972 Update scripts for more detail Allow display of sandbox cpu time. Allow display of reports/testcase_xxx.log by link Make all links open a new tab as opposed to overwriting the current one. Add memory used column since it is now supported in latest kernel. --- scripts/pc2sandbox.sh | 1 - support/judge_webcgi/cgi-bin/judge.sh | 1 + support/judge_webcgi/judge.sh | 124 --------------- support/judge_webcgi/pc2common.sh | 211 -------------------------- support/judge_webcgi/showrun.sh | 209 ------------------------- support/judge_webcgi/webcommon.sh | 157 ------------------- 6 files changed, 1 insertion(+), 702 deletions(-) delete mode 100644 support/judge_webcgi/judge.sh delete mode 100644 support/judge_webcgi/pc2common.sh delete mode 100644 support/judge_webcgi/showrun.sh delete mode 100644 support/judge_webcgi/webcommon.sh diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh index f6c76ba4b..2056d330c 100644 --- a/scripts/pc2sandbox.sh +++ b/scripts/pc2sandbox.sh @@ -369,7 +369,6 @@ else echo "max" > $PC2_SANDBOX_CGROUP_PATH/memory.swap.max fi -REPORT Test case $TESTCASE: # We use ulimit to limit CPU time, not cgroups. Time is supplied in seconds. This may have to # be reworked if ms accuracy is needed. The problem is, cgroups do not kill off a process that # exceeds the time limit, ulimit does. diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh index f33a23a86..390824fde 100644 --- a/support/judge_webcgi/cgi-bin/judge.sh +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -93,6 +93,7 @@ MyTableStyles EndStyles StartHTMLDoc Header +LogButton StartTable TableHeader diff --git a/support/judge_webcgi/judge.sh b/support/judge_webcgi/judge.sh deleted file mode 100644 index e1b6bb740..000000000 --- a/support/judge_webcgi/judge.sh +++ /dev/null @@ -1,124 +0,0 @@ -#!/bin/bash -. ./pc2common.sh -. ./webcommon.sh -. ./cdpcommon.sh - -TableHeader() -{ - cat << EOFTH - -Run IDDispJudgmentProblemTeamTest CasesLanguageJudgeTime Judged - -EOFTH -} - -TableRow() -{ - dir="$1" - runid=$2 - problet=$3 - shortname=$4 - langid=$5 - teamnum=$6 - judgment="$7" - runtime="$8" - testinfo="$9" - judge="${10}" - probname="" - probdir="" - if test -n ${shortname} - then - probdir=${PC2_CDP}/${shortname} - probstatement=${probdir}/problem_statement/problem.en.tex - if test ! -s ${probstatement} - then - probstatement=${probdir}/problem_statement/problem.tex - fi - if test -s ${probstatement} - then - probname=`head -1 ${probstatement}` - probname=${probname##*\{} - probname=${probname%\}} - fi - fi - problem="$problet - $probname (${shortname})" - if test "${judgment}" = "AC" - then - jstyle="green" - jicon='' - elif test "${judgment}" = "CE" - then - jicon='' - else - jstyle="red" - jicon='' - fi - echo ' ' - echo ' '"Run $runid" -# echo ' '$judgment"" -# echo '

'$judgment"
" - echo ' '$jicon'' - echo ' '$judgment'' - echo " $problem" - echo " team$teamnum" - echo " $testinfo" - echo " $langid" - echo " $judge" - echo " $runtime" - echo " " -} - -###ParseProblemYaml -Preamble -Header -LogButton -StartTable -TableHeader - -# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid_judge -for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+' | sort --field-separator=_ +1rn` -do - # exdir looks like: ex_188_Y_compression_46103_cpp - # RId P ProbShort team# Lang - # RId = Run ID - # P = problem letter - # Lang = CLICS Language id - saveIFS="$IFS" - IFS="_" - set ${exdir} - IFS="$saveIFS" - if test $# -ge 6 - then - exedir=${PC2_RUN_DIR}/$exdir - runid=$2 - problet=$3 - probshort=$4 - teamnum=$5 - langid=$6 - judge=$7 - if test -z "${judge}" - then - judge="N/A" - fi - GetJudgment "${exedir}" - judgment="${result}" - runtime="${executeDateTime}" - # Get how many total test cases there are - probdir=${PC2_CDP}/${probshort} - if test -n "${probdir}" - then - GetNumberOfTestCases "${probdir}" - numcases=${result} - else - numcases="??" - fi - # Note that GetJudgment also filled in exdata with the last execute data - GetLastTestCaseNumber "${exdata}" - testcaseinfo=$((result+1))/${numcases} - - TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" - fi -done -EndTable -Trailer -exit 0 diff --git a/support/judge_webcgi/pc2common.sh b/support/judge_webcgi/pc2common.sh deleted file mode 100644 index 54723de21..000000000 --- a/support/judge_webcgi/pc2common.sh +++ /dev/null @@ -1,211 +0,0 @@ -# Meant to be "source'd" into bash scripts. - -# The judge's home directory -JUDGE_HOME=/home/icpc - -# Modify constants as necessary for your installation -PC2_RUN_DIR=${JUDGE_HOME}/pc2 -PC2_CDP=${PC2_RUN_DIR}/current/config - -EXE_LINK_PREFIX=../exedir -PROB_LINK_PREFIX=../probdir - -# How many exedir and probdir links to keep around -NUM_LINK_KEEP=4 - -# Where can we find the contest banner png file for webpage headers -BANNER_FILE=banner.png -BANNER_IMAGE=${PC2_RUN_DIR}/current/contest/${BANNER_FILE} - -# Where to the the result of script failure before execution -RESULT_FAILURE_FILE=failure.txt - -# Where judgments are -REJECT_INI=${JUDGE_HOME}/pc2/reject.ini - -# Where PC2 puts CLICS validator results -EXECUTE_DATA_PREFIX=executedata -# Where PC2 puts run output/error -TEST_OUT_PREFIX="teamoutput" -TEST_ERR_PREFIX="teamstderr" -TEST_VALOUT_PREFIX="valout" -TEST_VALERR_PREFIX="valerr" - -# Detailed log of entire judging process -SANDBOX_LOG=sandbox.log - -# Briefcase file prefix -BRIEFCASE_FILE_PREFIX="briefcase" -REPORTS_DIR=reports - -declare -A Judgments - -InitJudgments() -{ - # Defauls, may be overriden by reject.ini - Judgments["accepted"]="AC" - Judgments["Accepted"]="AC" - Judgments["timelimit"]="TLE" - Judgments["run error"]="RTE" - Judgments["compiler error"]="CE" - if test -s ${REJECT_INI} - then - while read j - do - if [[ $j = \#* ]] - then - continue - fi - savIFS="$IFS" - IFS='|' - set $j - IFS="$savIFS" - key="$1" - shortcode="$2" - case ${shortcode} in - AC|CE|RTE|WA|TLE) ;; - MLE) shortcode="RTE (MLE)" ;; - *) shortcode="WA (${shortcode})" ;; - esac -# echo Mapping $key to $shortcode - Judgments[$key]="$shortcode" - done < $REJECT_INI - else echo NO REJECT FILE - fi -} - -# Takes the judgement string, eg. "Wrong answer" as arg 1 and the validation Result (42 or 42) as arg 2 -MapJudgment() -{ - jm="$1" - vr="$2" - result=${Judgments[$jm]} - if test -z ${result} - then - if test ${validationReturnCode} -eq 0 - then - if test ${vr} = "43" - then - resul="WA" - elif test ${vr} = "42" - then - result="AC" - else - result="WA (Default)" - fi - else - result="JE (Validator EC=${validationReturnCode})" - fi - - fi -} - -GetLastJudgmentFile() -{ - lj_exdir="$1" - result=`ls $lj_exdir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1rn | head -1` -} - -GetTestCaseNumber() -{ - if test -z "$1" - then - result=0 - else - saveIFS="$IFS" - IFS=. - set ${1} - result="$2" - IFS="$saveIFS" - if test -z "${result}" - then - result=0 - fi - fi -} - -GetJudgmentFromFile() -{ - exdata="$1" - if test -z "${exdata}" - then - result="No results" - else - # Source the file - . ${exdata} - if test ${compileSuccess} = "false" - then - result="CE" - elif test ${executeSuccess} = "true" - then - if test ${validationSuccess} = "true" - then - MapJudgment "${validationResults}" "${validationReturnCode}" - else - result="JE (Validator error)" - fi - else - result="RTE (Execute error)" - fi - fi -} - -GetJudgment() -{ - dir=$1 - exdata="" - if ! cd ${dir} - then - result="Not found" - elif test -s ${RESULT_FAILURE_FILE} - then - jerr=`cat ${RESULT_FAILURE_FILE}` - result="JE ($jerr)" - else - # We got a real live run - # Check out the biggest executedata file - GetLastJudgmentFile $dir - GetJudgmentFromFile ./${result} - fi -} - -MakeBriefcaseFile() -{ - d="$1" - t="$2" - result=`printf '%s/%s/%s_%03d.log' "$d" "$REPORTS_DIR" "$BRIEFCASE_FILE_PREFIX" "$t"` -} - -# Must redirect from briefcase file -ReadBriefcase() -{ - read judgein - read judgeans - read cpunum - read exepid - read exetime - read execpums cpulimms exewallms mempeak memlim - if [[ $mempeak = [0-9]* ]] - then - mempeak=$((mempeak/(1024*1024))) - fi - if [[ $memlim = [0-9]* ]] - then - memlim=$((memlim/(1024*1024))) - fi -} - -DeleteOldestLinks() -{ - for linkpref in ${EXE_LINK_PREFIX} ${PROB_LINK_PREFIX} - do - dellist=`ls -1td ${linkpref}* | sed 1,${NUM_LINK_KEEP}d` - if test -n "${dellist}" - then - rm -f ${dellist} - fi - done -} - -InitJudgments - diff --git a/support/judge_webcgi/showrun.sh b/support/judge_webcgi/showrun.sh deleted file mode 100644 index e88937dea..000000000 --- a/support/judge_webcgi/showrun.sh +++ /dev/null @@ -1,209 +0,0 @@ -#!/bin/bash -. ./pc2common.sh -. ./webcommon.sh -. ./cdpcommon.sh - -EXE_DIR_LINK=${EXE_LINK_PREFIX}$$ -PROB_DIR_LINK=${PROB_LINK_PREFIX}$$ - -# Provide a way to look at the sandbox log -LogButton() -{ - # Read first judgment file to get compile time - it's compiled once for all - GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" - if test -n ${result} -a -n ${compileTimeMS} - then - cat << LBEOF0 -
-

The program took ${compileTimeMS}ms to compile. -

-LBEOF0 - fi - - # Read the first briefcase file (if any) for limits - MakeBriefcaseFile ${EXE_DIR_LINK} 1 - ReadBriefcase < $result - if test -n ${cpulimms} - then - cpulimms=${cpulimms%%.*} - cpusecs="$((cpulimms/1000))" - if test ${cpusecs} != "1" - then - cpusecs="${cpusecs} seconds" - else - cpusecs="1 second" - fi - cat << LBEOF1 -
-

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms). -

-LBEOF1 - fi - if test -n ${memlim} - then - if test ${memlim} = "0" - then - memlim="Unlimited" - else - memlim=${memlim}MB - fi - cat << LBEOF1A -
-

The Memory limit for this problem is ${memlim}. -

-LBEOF1A - fi - sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} - if test -s ${sandlog} - then - cat << LBEOF2 -Click here for the full sandbox log for this run -

-

-LBEOF2 - fi -} - -TableHeader() -{ - cat << EOFTH - -TestDispJudgmentExitExecute TimeVal TimeVal SuccessRun OutRun ErrJudge InJudge AnsVal OutVal Err - -EOFTH -} - -GenFileLink() -{ - tstcase="$2" - tstcase=$((tstcase-1)) - tstfile=$1.$tstcase.txt - tstpath=$dir/$1.$tstcase.txt - if test -s ${tstpath} - then - bytes=`stat -c %s ${tstpath}` - echo ' View ('$bytes' bytes)' - elif test -e ${tstpath} - then - echo ' (Empty)' - else - echo ' Not found' - fi -} - -GenFileLinkWithText() -{ - linkaddr="$1" - linktext="$2" - linkcolor="$3" - bytes=`stat -c %s ${linkaddr}` - echo ' '$linktext' ('$bytes' bytes)' -} - -TableRow() -{ - tc=$1 - judgment="$2" - ec=$3 - exetm="$4" - valtm="$5" - jin="$6" - jans="$7" - if test "$8" = "true" - then - valsucc=Yes - elif test "$8" = "false" - then - valsucc=No - else - valsucc="N/A" - fi - # Strip the stuff of before sample (or secret) - jin=${jin##*/data/} - jans=${jans##*/data/} - # Just the basenames for link text - jinbase=${jin##*/} - jansbase=${jans##*/} - if [[ $jin = sample/* ]] - then - lcolor=#00a0a0 - else - lcolor=#00a000 - fi - if test "${judgment}" = "AC" - then - jicon='' - elif test "${judgment}" = "CE" - then - jicon='' - else - jicon='' - fi - - echo ' ' - - echo ' '$tc'' - echo ' '$jicon'' - echo ' '$judgment'' - echo ' '$ec'' - echo ' '$exetm'ms' - echo ' '$valtm'ms' - echo ' '$valsucc'' - GenFileLink $TEST_OUT_PREFIX $tc - GenFileLink $TEST_ERR_PREFIX $tc - GenFileLinkWithText $PROB_DIR_LINK/data/"$jin" "$jinbase" $lcolor - GenFileLinkWithText $PROB_DIR_LINK/data/"$jans" "$jansbase" $lcolor - GenFileLink $TEST_VALOUT_PREFIX $tc - GenFileLink $TEST_VALERR_PREFIX $tc - - echo ' ' -} - -# Parse query string into dictionary args -# This is not terribly secure. -declare -a parm -sIFS="$IFS" -IFS='=&' -parm=($QUERY_STRING) -IFS=$sIFS -for ((i=0; i<${#parm[@]}; i+=2)) -do - a=${parm[i]} - eval $a=${parm[i+1]} -done -# Done parsing - -DeleteOldestLinks -Preamble - 'Run '$run -HeaderNoBanner Details for Run Id $run - $probletter "("$probshort") - team$submitter - Judged: $judgment" - -# Create links apache can access in our html folder -#rm ${EXE_DIR_LINK} ${PROB_DIR_LINK} -ln -s ${dir} ${EXE_DIR_LINK} -ln -s ${probdir} ${PROB_DIR_LINK} - -LogButton - -StartTable -TableHeader -for testcase in `ls $dir/${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1n` -do - GetTestCaseNumber $testcase - tc=$((result+1)) - # This will also source the execute data - GetJudgmentFromFile $dir/$testcase - judgment=$result - ec=$executeExitValue -# comptm=$compileTimeMS - exetm=$executeTimeMS - valtm=$validateTimeMS - valsuc=$validationSuccess - MakeBriefcaseFile "$dir" "$tc" - ReadBriefcase < ${result} - TableRow "$tc" "$judgment" "$ec" "$exetm" "$valtm" "$judgein" "$judgeans" "$valsuc" -done - -Trailer - -#rm -f ${EXE_DIR_LINK} ${PROB_DIR_LINK} -exit 0 diff --git a/support/judge_webcgi/webcommon.sh b/support/judge_webcgi/webcommon.sh deleted file mode 100644 index 26c65cd62..000000000 --- a/support/judge_webcgi/webcommon.sh +++ /dev/null @@ -1,157 +0,0 @@ -# File meant to be "source'd" to support generating web pages - -# -# Display very curt textual error message -# -Error() -{ - echo "Content-type: text/plain" - echo "" - echo ERROR $* -} - - -Preamble() -{ - echo "Content-type: text/html" - echo "" - if test $# -eq 0 - then - headmsg="Judge" - else - headmsg="$*" - fi - cat << PREEOF - - - -PC² $headmsg - - - -PREEOF -} - -Header() -{ - # Make sure link to banner page is in a place apache can read - if test ! -e ../${BANNER_FILE} - then - ln -s ${BANNER_IMAGE} ../${BANNER_FILE} - fi -cat << HDREOF -

- -

PC2 Judging Results

-
-

-HDREOF -} - -HeaderNoBanner() -{ - if test $# -eq 0 - then - hdrmsg="Judging Results" - else - hdrmsg="$*" - fi -cat << EOF -

-

PC2 $hdrmsg

-
-

-EOF -} - -Trailer() -{ -cat << EOF2 - - -EOF2 -} - -StartTable() -{ - cat << EOF3 -

- -EOF3 -} - -EndTable() -{ - cat << EOF4 -
-

-EOF4 -} - From 292e76661de2771913ca7a0c1c4ba887f85c7cbc Mon Sep 17 00:00:00 2001 From: John Buck Date: Sun, 7 Jul 2024 14:15:08 -0400 Subject: [PATCH 41/55] i_972 Clean up scripts Interactive sandbox script needed cleaning up and updating. Fix some bash 'if' statements in case arg is empty string. --- scripts/pc2sandbox_interactive.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scripts/pc2sandbox_interactive.sh b/scripts/pc2sandbox_interactive.sh index ab6ed7f90..d9a35c060 100644 --- a/scripts/pc2sandbox_interactive.sh +++ b/scripts/pc2sandbox_interactive.sh @@ -315,6 +315,19 @@ DEBUG echo "+---------------- Test Case ${TESTCASE} ----------------+" DEBUG echo Command line: $0 $* shift 7 +DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" +RUN_LOCAL_CMD="$0 ${MEMLIMIT} ${TIMELIMIT} ${VALIDATOR} ${JUDGEIN} ${JUDGEANS} ${TESTCASE} ${COMMAND} $*" +tcfile=`printf "$REPORTDIR/testcase_%03d.log" $TESTCASE` +REPORT_OUTPUT_CMD="more ${tcfile}" +DEBUG echo -e "\n${RUN_LOCAL_CMD}" +#DEBUG echo -e "\nor, without the sandbox by using the following command:" +#DEBUG echo -e "\n${COMMAND} $* < ${JUDGEIN} > $TESTCASE.ans" +DEBUG echo -e "\nAnd see the run report using the following command:" +DEBUG echo -e "\n${REPORT_OUTPUT_CMD}\n" + +# Skip used arguments +shift 7 + # the rest of the commmand line arguments are the command args for the submission DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" RUN_LOCAL_CMD="$0 ${MEMLIMIT} ${TIMELIMIT} ${VALIDATOR} ${JUDGEIN} ${JUDGEANS} ${TESTCASE} ${COMMAND} $*" From ab37de345ee409594892b366d9837745a1a7cfde Mon Sep 17 00:00:00 2001 From: John Buck Date: Sun, 7 Jul 2024 17:20:45 -0400 Subject: [PATCH 42/55] i_972 More judge web page fixes Remove extra space when generating the execute data file. Correct the "whats this" message for the judge execute folder pattern. Add jquery for sorting tables. --- support/judge_webcgi/cgi-bin/judge.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/support/judge_webcgi/cgi-bin/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh index 390824fde..f33a23a86 100644 --- a/support/judge_webcgi/cgi-bin/judge.sh +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -93,7 +93,6 @@ MyTableStyles EndStyles StartHTMLDoc Header -LogButton StartTable TableHeader From b203f54f88cff40b12dd098b31a5caedb4eba678 Mon Sep 17 00:00:00 2001 From: John Buck Date: Sun, 14 Jul 2024 15:05:25 -0400 Subject: [PATCH 43/55] i_972 Added scripts to pull from all judges The main web server machine will now pull all submissions from each judge and show them on one page. This is done using a python script that reads a json result from each judge. Added link to display compiler results on compile error Added link for source code of submission. Change Run Out and Run err to Run stdout and Run stderr --- scripts/pc2sandbox_interactive.sh | 202 +++++++++++++++--- support/judge_webcgi/cgi-bin/HtmlFunctions.py | 68 ++++++ support/judge_webcgi/cgi-bin/alljudgments.py | 84 ++++++++ support/judge_webcgi/cgi-bin/getjudgments.sh | 109 ++++++++++ support/judge_webcgi/cgi-bin/pc2common.sh | 25 +++ support/judge_webcgi/cgi-bin/showrun.sh | 133 +++++++++--- support/judge_webcgi/css/judgestyles.css | 94 ++++++++ support/judge_webcgi/scripts/tablesort.js | 28 +++ 8 files changed, 687 insertions(+), 56 deletions(-) create mode 100644 support/judge_webcgi/cgi-bin/HtmlFunctions.py create mode 100644 support/judge_webcgi/cgi-bin/alljudgments.py create mode 100644 support/judge_webcgi/cgi-bin/getjudgments.sh create mode 100644 support/judge_webcgi/css/judgestyles.css create mode 100644 support/judge_webcgi/scripts/tablesort.js diff --git a/scripts/pc2sandbox_interactive.sh b/scripts/pc2sandbox_interactive.sh index d9a35c060..aa4fcfec0 100644 --- a/scripts/pc2sandbox_interactive.sh +++ b/scripts/pc2sandbox_interactive.sh @@ -15,6 +15,19 @@ # # Author: John Buck, based on earlier versions by John Clevenger and Doug Lane +# KILL_WA_VALIDATOR may be set to 1 to kill off a submission if the validator finishes first with +# a WA judgment. In this case, we don't care what the submission does. This matches how DOMjudge +# handles this case. Setting this to 0 will always make it wait for the contestant submission to +# finish, and use whatever judgement is appropriate. If contestant finishes with 0, then we use the +# vaildator result, else we use the exit code of the submission. +KILL_WA_VALIDATOR=1 +# If WAIT_FOR_SUB_ON_AC is 1, we wait for the submission if the validator says "AC". We then +# use the exit code of the submission if it is non-zero to determine disposition, otherise, +# we return AC +# If WAIT_FOR_SUB_ON_AC is 0, we will kill off the submission as soon as we get an AC from the +# validator if the submission is not complete yet. +WAIT_FOR_SUB_ON_AC=1 + # CPU to run submission on. This is 0 based, so 3 means the 4th CPU DEFAULT_CPU_NUM=3 CPU_OVERRIDE_FILE=$HOME/pc2_cpu_override @@ -285,6 +298,59 @@ EOF sent_xml=1 } +# Dont complain to me - this is the order they appear in ./x86_64-linux-gnu/bits/signum-generic.h +declare -A signal_names=( + ["2"]="SIGINT" + ["4"]="SIGILL" + ["6"]="SIGABRT" + ["8"]="SIGFPE" + ["11"]="SIGSEGV" + ["15"]="SIGTERM" + ["1"]="SIGHUP" + ["3"]="SIGQUIT" + ["5"]="SIGTRAP" + ["9"]="SIGKILL" + ["13"]="SIGPIPE" + ["14"]="SIGALRM" +) + +declare -A fail_codes=( + ["$FAIL_EXIT_CODE"]="FAIL_EXIT_CODE" + ["$FAIL_NO_ARGS_EXIT_CODE"]="FAIL_NO_ARGS_EXIT_CODE" + ["$FAIL_INSUFFICIENT_ARGS_EXIT_CODE"]="FAIL_INSUFFICIENT_ARGS_EXIT_CODE" + ["$FAIL_INVALID_CGROUP_INSTALLATION"]="FAIL_INVALID_CGROUP_INSTALLATION" + ["$FAIL_MISSING_CGROUP_CONTROLLERS_FILE"]="FAIL_MISSING_CGROUP_CONTROLLERS_FILE" + ["$FAIL_MISSING_CGROUP_SUBTREE_CONTROL_FILE"]="FAIL_MISSING_CGROUP_SUBTREE_CONTROL_FILE" + ["$FAIL_CPU_CONTROLLER_NOT_ENABLED"]="FAIL_CPU_CONTROLLER_NOT_ENABLED" + ["$FAIL_MEMORY_CONTROLLER_NOT_ENABLED"]="FAIL_MEMORY_CONTROLLER_NOT_ENABLED" + ["$FAIL_MEMORY_LIMIT_EXCEEDED"]="FAIL_MEMORY_LIMIT_EXCEEDED" + ["$FAIL_TIME_LIMIT_EXCEEDED"]="FAIL_TIME_LIMIT_EXCEEDED" + ["$FAIL_WALL_TIME_LIMIT_EXCEEDED"]="FAIL_WALL_TIME_LIMIT_EXCEEDED" + ["$FAIL_SANDBOX_ERROR"]="FAIL_SANDBOX_ERROR" + ["$FAIL_INTERACTIVE_ERROR"]="FAIL_INTERACTIVE_ERROR" +) +FormatExitCode() +{ + result="$1" + if [[ "$result" = [0-9]* ]] + then + failerr=${fail_codes[$result]} + if test -n "$failerr" + then + result="$result ($failerr)" + elif test "$result" -gt 128 -a "$result" -lt 192 + then + sig=$((result-128)) + signame=${signal_names[$sig]} + if test -n "$signame" + then + signame="$signame " + fi + result="$result (Signal $signame$((result-128)))" + fi + fi +} + # ------------------------------------------------------------ if [ "$#" -lt 1 ] ; then @@ -504,6 +570,10 @@ submissionpid=$! # Flag to indicate if contestant submission has terminated contestant_done=0 +# Validator exit status, if it finished before submission +val_result="" +# Indicates if we still have to wait for the submission +wait_sub=0 # Now we wait. We wait for either the child or interactive validator to finish. # If the validator finishes, no matter what, we're done. If the submission is still @@ -527,36 +597,80 @@ do REPORT_DEBUG Validator finishes with exit code $wstat if test "$contestant_done" -eq 0 then - # Only kill it if it still exists - if test -d /proc/$contestantpid + # If we always kill off the child when the validator finishes with WA, or the validator finishes with "AC" and we dont wait for submission on AC, + # then kill off the child, otherwise we wait for it. + if test "(" ${KILL_WA_VALIDATOR} -eq 1 -a "$wstat" -ne "${EXITCODE_AC}" ")" -o "(" ${WAIT_FOR_SUB_ON_AC} -eq 0 -a "$wstat" -eq ${EXITCODE_AC} ")" then - REPORT_DEBUG Contestant PID $submissionpid has not finished - killing it - # TODO: We should kill and wait for it here and print out the stats + # Only kill it if it still exists + if test -d /proc/$contestantpid + then + REPORT_DEBUG Contestant PID $submissionpid has not finished - killing it "(KILL_WA_VALIDATOR=1)" + # TODO: We should kill and wait for it here and print out the stats + fi + # This just determines if the program ran, not if it's correct. + # The result file has the correctness in it. + # We only do this if the contestant program has not finished yet. + REPORT_DEBUG Indicating that the submission exited with code 0 because we killed it. + COMMAND_EXIT_CODE=0 + else + REPORT_DEBUG Contestant PID $submissionpid has not finished - waiting for it "(KILL_WA_VALIDATOR=1)" + # Need to wait for child. Remember validator result. + val_result="$wstat" + wait_sub=1 fi - # This just determines if the program ran, not if it's correct. - # The result file has the correctness in it. - # We only do this if the contestant program has not finished yet. - COMMAND_EXIT_CODE=0 fi - KillChildProcs + # Kill everything off if we don't have to wait for submission + if test "$wait_sub" -eq 0 + then + KillChildProcs + fi if test "$wstat" -eq $EXITCODE_AC then - GenXML Accepted "" + # COMMAND_EXIT_CODE will be set to 0 above, or 0 if the submission finished already with EC 0. + # If COMMAND_EXIT_CODE is anything else, we will either be waiting for the submission, or, use + # it's exit code as the result. The GenXML will have been filled in in this case with any errors. + if test -z "${COMMAND_EXIT_CODE}" -o "${COMMAND_EXIT_CODE}" = "0" + then + REPORT_DEBUG Both the validator and the submission indicate AC + # Set the result to AC. If we have to wait for the submission, this will get overwritten with + # the submissions disposition. + GenXML Accepted "" + fi elif test "$wstat" -eq $EXITCODE_WA then - # If validator created a feedback file, put the last line in the judgement - if test -s "$feedbackfile" + # COMMAND_EXIT_CODE may be set to something here, either 0 from above, or + # an exit code from when the submission finished below. In the latter case, the + # XML result file will have already been created with the disposition from below. + # We will only generate an XML here if the validator says it failed (WA). + # COMMAND_EXIT_CODE can be empty if we are waiting for the submission to finish. + if test -z "${COMMAND_EXIT_CODE}" -o "${COMMAND_EXIT_CODE}" = "0" then - GenXML "No - Wrong answer" `head -n 1 $feedbackfile` - else - GenXML "No - Wrong answer" "No feedback file" + REPORT_DEBUG Submission has an exit code of 0 and validator says WA. + # If validator created a feedback file, put the last line in the judgement + if test -s "$feedbackfile" + then + GenXML "No - Wrong answer" `head -n 1 $feedbackfile` + else + GenXML "No - Wrong answer" "No feedback file" + fi fi else - GenXML "Other - contact staff" "bad return code $wstat" + REPORT_DEBUG Validator returned code $wstat which is not 42 or 43 - validator error. + GenXML "Other - contact staff" "bad validator return code $wstat" + if test "$wait_sub" -ne 0 + then + KillChildProcs + fi + COMMAND_EXIT_CODE=${FAIL_INTERACTIVE_ERROR} + break + fi + if test "$wait_sub" -eq 0 + then + REPORT_DEBUG No need to wait for submission to finish. + break fi - break; fi # If this is the contestant submission if test "$child_pid" -eq "$submissionpid" @@ -576,7 +690,8 @@ do fi - REPORT_DEBUG Contestant PID $submissionpid finished with exit code $wstat + FormatExitCode $wstat + REPORT_DEBUG Contestant PID $submissionpid finished with exit code $result contestant_done=1 COMMAND_EXIT_CODE=$wstat @@ -587,39 +702,60 @@ do if test "$kills" != "0" then - REPORT_DEBUG The command was killed because it exceeded the memory limit + REPORT_DEBUG The command was killed because it exceeded the memory limit - setting exit code to ${FAIL_MEMORY_LIMIT_EXCEEDED} REPORT_BRIEF MLE COMMAND_EXIT_CODE=${FAIL_MEMORY_LIMIT_EXCEEDED} GenXML "No - Memory limit exceeded" "" - KillValidator - break + if test "$val_result" = "${EXITCODE_AC}" -o "${KILL_WA_VALIDATOR}" -eq 0 + then + KillValidator + break + fi else # See why we terminated. 137 = 128 + 9 = SIGKILL, which is what ulimit -t sends if test "$COMMAND_EXIT_CODE" -eq 137 -o "$cputime" -gt "$TIMELIMIT_US" then - REPORT_DEBUG The command was killed because it exceeded the CPU Time limit - REPORT_BRIEF TLE COMMAND_EXIT_CODE=${FAIL_TIME_LIMIT_EXCEEDED} + FormatExitCode "$COMMAND_EXIT_CODE" + REPORT_DEBUG The command was killed because it exceeded the CPU Time limit - setting exit code to $result + REPORT_BRIEF TLE GenXML "No - Time limit exceeded" "${cputime}us > ${TIMELIMIT_US}us" - KillValidator - break + if test "$val_result" = "${EXITCODE_AC}" -o "${KILL_WA_VALIDATOR}" -eq 0 + then + KillValidator + break + fi elif test "$COMMAND_EXIT_CODE" -ge 128 then - REPORT_DEBUG The command terminated abnormally due to a signal with exit code $COMMAND_EXIT_CODE signal $((COMMAND_EXIT_CODE - 128)) + FormatExitCode "$COMMAND_EXIT_CODE" + REPORT_DEBUG The command terminated due to a signal with exit code $result else - REPORT_DEBUG The command terminated normally. + REPORT_DEBUG The command terminated normally with exit code $result fi fi REPORT_DEBUG Finished executing "$COMMAND $*" - REPORT_DEBUG "$COMMAND" exited with exit code $COMMAND_EXIT_CODE + FormatExitCode "$COMMAND_EXIT_CODE" + REPORT_DEBUG "$COMMAND" exited with exit code $result - if test "$wstat" -ne 0 + if test "$COMMAND_EXIT_CODE" -ne 0 then - REPORT_DEBUG Contestant finished abnormally - killing validator + REPORT_DEBUG Contestant finished with a non-zero exit code $result REPORT_BRIEF RTE - KillValidator GenXML "No - Run-time Error" "Exit status $wstat" - break + # If validator finished with AC, but submission has a bad exit code, we use that. + # Or, if we didn't kill off the submission, and want to use it's exit code + if test "$val_result" = "${EXITCODE_AC}" -o "${KILL_WA_VALIDATOR}" -eq 0 + then + KillValidator + break + fi + else + # COMMAND_EXIT_CODE is 0, let's see if the validator finished. If + # so, we're done, since the validator already created it's disposition. + if test -n "$val_result" + then + break + fi fi REPORT_DEBUG Waiting for interactive validator to finish... fi @@ -633,6 +769,8 @@ rm -f "$INFIFO" "$OUTFIFO" # TODO: determine how to pass more detailed pc2sandbox.sh results back to PC2... Perhaps in a file... # return the exit code of the command as our exit code +FormatExitCode "$COMMAND_EXIT_CODE" +REPORT_DEBUG Returning exit code $result to PC2 exit $COMMAND_EXIT_CODE # eof pc2sandbox_interactive.sh diff --git a/support/judge_webcgi/cgi-bin/HtmlFunctions.py b/support/judge_webcgi/cgi-bin/HtmlFunctions.py new file mode 100644 index 000000000..2ea16bd8d --- /dev/null +++ b/support/judge_webcgi/cgi-bin/HtmlFunctions.py @@ -0,0 +1,68 @@ +# Functions to support generation of HTML +TESTING = False + +if TESTING : + BANNER_FILE="banner.png" + TABLE_SORT_JS="tablesort.js" + JQUERY="jquery-3.7.1.slim.min.js" +else : + BANNER_FILE="/banner.png" + TABLE_SORT_JS="/scripts/tablesort.js" + JQUERY="/scripts/jquery-3.7.1.slim.min.js" + +def Preamble(hdr) : + if TESTING == False : + print("Content-type: text/html") + print("") + if hdr == None : + headmsg = "Judge" + else : + headmsg = hdr; + print("") + print('') + print("") + print(f"PC² {headmsg}") + +def Styles(css) : + print(f'') + +def StartHTMLDoc() : + print("") + print("") + + +def Scripts() : + print(f'') + print(f'') + +def Header() : + # Need to link bannerfile if not present + print("

") + print(f' ') + print('

PC2 Judging Results

') + print("
") + print("

") + +def HeaderNoBanner(header) : + if header == None : + hdrmsg = "Judging Results" + else : + hdrmsg = header + print("

") + print("

PC2 {hdrmsg}

") + print("
") + print("

") + +def Trailer() : + print("") + print("") + +def StartTable() : + print("

") + +def EndTable() : + print("

") + + + + diff --git a/support/judge_webcgi/cgi-bin/alljudgments.py b/support/judge_webcgi/cgi-bin/alljudgments.py new file mode 100644 index 000000000..c22011828 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/alljudgments.py @@ -0,0 +1,84 @@ +#!/usr/bin/python3 +import json +import urllib.request + +import HtmlFunctions + +TESTING = False + +if TESTING : + CORRECT_PNG = "Correct.png" + COMPILE_ERROR_PNG = "Warning.png" + WRONG_PNG = "Wrong.png" + JUDGE_STYLES="judgestyles.css" +else : + CORRECT_PNG = "../Correct.png" + COMPILE_ERROR_PNG = "../Warning.png" + WRONG_PNG = "../Wrong.png" + JUDGE_STYLES="/css/judgestyles.css" + +JUDGMENTS_SCRIPT="getjudgments.sh" + +JUDGE_HOSTS = ['pc2-aj1', 'pc2-aj2', 'pc2-ccsadmin1'] + +def MyTableStyles() : + print("") + +def TableHeader(): + print(''); + print('Run ID '); + print('Disp'); + print('Judgment '); + print('Problem '); + print('Team '); + print('Test Cases '); + print('Language '); + print('Judge '); + print('Time Judged '); + print(''); + +def getJudgmentsFromHost(host) : + try : + with urllib.request.urlopen(f"http://{host}/cgi-bin/{JUDGMENTS_SCRIPT}") as url : + data = json.load(url) + for j in data : + print(' ') + print(f' Run {j["runid"]}') + if j["judgment"] == "AC" : + icon = CORRECT_PNG + elif j["judgment"] == "CE" : + icon = COMPILE_ERROR_PNG + else : + icon = WRONG_PNG + print(f' ') + print(f' {j["judgment"]}') + print(f' {j["problem"]}') + print(f' team{j["team_number"]}') + print(f' {j["test_info"]}') + print(f' {j["language_id"]}') + print(f' {j["judge"]}') + print(f' {j["runtime"]}') + print(' ') + except Exception as err : + if TESTING : + print(f"getJudgmentsFromHost: Exception: {err}") + +HtmlFunctions.Preamble(None) +HtmlFunctions.Styles(JUDGE_STYLES) +MyTableStyles() +HtmlFunctions.StartHTMLDoc() +HtmlFunctions.Header() +HtmlFunctions.StartTable() +TableHeader() + +for jh in JUDGE_HOSTS : + getJudgmentsFromHost(jh) + +HtmlFunctions.EndTable() +HtmlFunctions.Scripts() +HtmlFunctions.Trailer() + diff --git a/support/judge_webcgi/cgi-bin/getjudgments.sh b/support/judge_webcgi/cgi-bin/getjudgments.sh new file mode 100644 index 000000000..8ab8ccaf5 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/getjudgments.sh @@ -0,0 +1,109 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + +TableRow() +{ + dir="$1" + runid=$2 + problet=$3 + shortname=$4 + langid=$5 + teamnum=$6 + judgment="$7" + runtime="$8" + testinfo="$9" + judge="${10}" + probname="" + probdir="" + if test -n "${shortname}" + then + probdir=${PC2_CDP}/${shortname} + probstatement=${probdir}/problem_statement/problem.en.tex + if test ! -s "{probstatement}" + then + probstatement=${probdir}/problem_statement/problem.tex + fi + if test -s "${probstatement}" + then + probname=`head -1 ${probstatement}` + probname=${probname##*\{} + probname=${probname%\}} + fi + fi + problem="$problet - $probname (${shortname})" + echo ' {' + echo ' "runid":"'$runid'"', + echo ' "href":"http://'$hostname'/cgi-bin/showrun.sh?run='$runid'&dir='$dir'&probdir='$probdir'&probletter='$problet'&probshort='$shortname'&lang='$langid'&submitter='$teamnum'&judgment='$judgment'"', + echo ' "judgment":"'$judgment'"', + echo ' "problem":"'$problem'"', + echo ' "problem_letter":"'$problet'"', + echo ' "problem_short_name":"'$probshort'"', + echo ' "directory":"'$dir'"', + echo ' "problem_dir":"'$probdir'"', + echo ' "team_number":"'$teamnum'"', + echo ' "test_info":"'$testinfo'"', + echo ' "language_id":"'$langid'"', + echo ' "judge":"'$judge'"', + echo ' "runtime":"'$runtime'"' + echo ' }' +} + +hostname=`hostname` +echo "Content-type: application/json" +echo "" + +echo '[' +sep="" +# Format of the execute folders must be: ex_runid_probletter_probshort_teamnum_langid_judge +for exdir in `ls ${PC2_RUN_DIR} | grep -P '^ex_\d+_[A-Z]_[a-z\d]+_\d+_[a-z\d]+' | sort --field-separator=_ +1rn` +do + # exdir looks like: ex_188_Y_compression_46103_cpp + # RId P ProbShort team# Lang + # RId = Run ID + # P = problem letter + # Lang = CLICS Language id + saveIFS="$IFS" + IFS="_" + set ${exdir} + IFS="$saveIFS" + if test $# -ge 6 + then + exedir=${PC2_RUN_DIR}/$exdir + runid=$2 + problet=$3 + probshort=$4 + teamnum=$5 + langid=$6 + judge=$7 + if test -z "${judge}" + then + judge="N/A" + fi + GetJudgment "${exedir}" + judgment="${result}" + runtime="${executeDateTime}" + # Get how many total test cases there are + probdir=${PC2_CDP}/${probshort} + if test -n "${probdir}" + then + GetNumberOfTestCases "${probdir}" + numcases=${result} + else + numcases="??" + fi + # Note that GetJudgment also filled in exdata with the last execute data + GetTestCaseNumber "${exdata##./}" + testcaseinfo=$((result+1))/${numcases} + if test -n "$sep" + then + echo " $sep" + else + sep="," + fi + TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" + fi +done +echo ']' +exit 0 diff --git a/support/judge_webcgi/cgi-bin/pc2common.sh b/support/judge_webcgi/cgi-bin/pc2common.sh index d9b6e2aed..edf361bd2 100644 --- a/support/judge_webcgi/cgi-bin/pc2common.sh +++ b/support/judge_webcgi/cgi-bin/pc2common.sh @@ -30,6 +30,8 @@ TEST_OUT_PREFIX="teamoutput" TEST_ERR_PREFIX="teamstderr" TEST_VALOUT_PREFIX="valout" TEST_VALERR_PREFIX="valerr" +COMP_OUT_PREFIX="cstdout" +COMP_ERR_PREFIX="cstderr" # Detailed log of entire judging process SANDBOX_LOG=sandbox.log @@ -219,5 +221,28 @@ DeleteOldestLinks() done } +GetSourceList() +{ + sllang="$1" + sldir="$2" + ext="" + case "$sllang" in + c) ext="c" ;; + cpp) ext="cc cpp cxx c++" ;; + java) ext="java" ;; + python3) ext="py" ;; + kotlin) ext="kt" ;; + esac + result="" + if test -n "$ext" + then + for e in $ext + do + result="$result "`ls -d $sldir/*.$e 2>/dev/null` + done + fi + +} + InitJudgments diff --git a/support/judge_webcgi/cgi-bin/showrun.sh b/support/judge_webcgi/cgi-bin/showrun.sh index 97c0c3391..ca5052c61 100644 --- a/support/judge_webcgi/cgi-bin/showrun.sh +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -11,12 +11,12 @@ LogButton() { # Read first judgment file to get compile time - it's compiled once for all GetJudgmentFromFile "$dir/${EXECUTE_DATA_PREFIX}.0.txt" + echo '

' if test -n "${result}" -a -n "${compileTimeMS}" then cat << LBEOF0 -

-

The program took ${compileTimeMS}ms to compile. -

+The program took ${compileTimeMS}ms to compile. +
LBEOF0 fi @@ -40,15 +40,13 @@ LBEOF0 cpusecs="1 second" fi cat << LBEOF1 -
-

The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms). -

+The CPU Limit for this problem is ${cpusecs} (${cpulimms}ms). +
LBEOF1 else cat << LBEOF1AA -
-

The CPU Limit for this problem is N/A. -

+The CPU Limit for this problem is N/A. +
LBEOF1AA fi if test -n "${memlim}" @@ -60,17 +58,34 @@ LBEOF1AA memlim=${memlim}MiB fi cat << LBEOF1A -
-

The Memory limit for this problem is ${memlim}. -

+The Memory limit for this problem is ${memlim}. +
LBEOF1A else cat << LBEOF1AAA -
-

The Memory limit for this problem is N/A. -

+The Memory limit for this problem is N/A. +
LBEOF1AAA fi + + GetSourceList $lang ${EXE_DIR_LINK} + srclist="$result" + echo "Submission Source Code: " + # Get source files + sep="" + for sfile in $srclist + do + if test -z "$sep" + then + sep=" " + else + echo -n "$sep" + fi + GenGenericFileLink "$sfile" + done + echo "
" + echo "
" + sandlog=${EXE_DIR_LINK}/${SANDBOX_LOG} if test -s "${sandlog}" then @@ -94,8 +109,8 @@ TableHeader() MiB Used Val Time Val Success - Run Out - Run Err + Run stdout + Run stderr Judge In Judge Ans Val Out @@ -104,6 +119,56 @@ TableHeader() EOFTH } +# Usage is: +# GenGenericFileLink full-path-to-file [link-text] +# If no [link-text] supplied, then use basename of file +# This just makes up the href. +# +GenGenericFileLink() +{ + tstfile=$1 + gentxt="$2" + if test -z "$gentxt" + then + gentxt=${tstfile##*/} + fi + tstpath=$tstfile + if test -s "${tstpath}" + then + echo -n ''$gentxt'' + elif test -e "${tstpath}" + then + echo -n "($gentxt Empty)" + else + echo -n "$gentxt Not found" + fi +} + +# Usage is: +# GenGenericFileLinkTd full-path-to-file [link-text] +# If no [link-text] supplied, then use basename of file +# This includes the bracketing ... +# +GenGenericFileLinkTd() +{ + tstfile=$1 + gentxt="$2" + if test -z "$gentxt" + then + gentxt=${tstfile##*/} + fi + tstpath=$dir/$tstfile + if test -s "${tstpath}" + then + echo ' '$gentxt'' + elif test -e "${tstpath}" + then + echo ' (Empty)' + else + echo ' Not found' + fi +} + GenFileLink() { tstcase="$2" @@ -127,8 +192,13 @@ GenFileLinkWithText() linkaddr="$1" linktext="$2" linkcolor="$3" - bytes=`stat -c %s ${linkaddr}` - echo ' '$linktext' ('$bytes' bytes)' + if test -z "${linktext}" + then + echo ' N/A' + else + bytes=`stat -c %s ${linkaddr}` + echo ' '$linktext' ('$bytes' bytes)' + fi } TableRow() @@ -152,7 +222,7 @@ TableRow() memused="$9" memusedbytes="${10}" exesandms="${11}" - + srclist="${12}" # Create link to report/testcase file for testcase number MakeTestcaseFile ${EXE_DIR_LINK} ${tc} tcreport="${result}" @@ -207,8 +277,14 @@ TableRow() fi echo ' '$valtm'ms' echo ' '$valsucc'' - GenFileLink $TEST_OUT_PREFIX $tc - GenFileLink $TEST_ERR_PREFIX $tc + if test "$judgment" != "CE" + then + GenFileLink $TEST_OUT_PREFIX $tc + GenFileLink $TEST_ERR_PREFIX $tc + else + GenGenericFileLinkTd $COMP_OUT_PREFIX.pc2 "View Compiler" + GenGenericFileLinkTd $COMP_ERR_PREFIX.pc2 "View Compiler" + fi GenFileLinkWithText $PROB_DIR_LINK/data/"$jin" "$jinbase" $lcolor GenFileLinkWithText $PROB_DIR_LINK/data/"$jans" "$jansbase" $lcolor GenFileLink $TEST_VALOUT_PREFIX $tc @@ -260,8 +336,17 @@ do valtm=$validateTimeMS valsuc=$validationSuccess MakeBriefcaseFile "$dir" "$tc" - ReadBriefcase < ${result} - TableRow "$tc" "$judgment" "$ec" "$exetm" "$valtm" "$judgein" "$judgeans" "$valsuc" "$mempeak" "$mempeakbytes" "$execpums" + if test -s "${result}" + then + ReadBriefcase < ${result} + else + judgein="" + judgeans="" + mempeak="" + mempeakbytes="" + fi + GetSourceList $lang ${EXE_DIR_LINK} + TableRow "$tc" "$judgment" "$ec" "$exetm" "$valtm" "$judgein" "$judgeans" "$valsuc" "$mempeak" "$mempeakbytes" "$execpums" "$result" done Trailer diff --git a/support/judge_webcgi/css/judgestyles.css b/support/judge_webcgi/css/judgestyles.css new file mode 100644 index 000000000..70a7ee95f --- /dev/null +++ b/support/judge_webcgi/css/judgestyles.css @@ -0,0 +1,94 @@ +body { + font-family: "Arial", "Helvetica", "sans-serif"; +} + +table { + font-family: arial, sans-serif; + border-collapse: collapse; + border-spacing: 10px; + width: 100%; +} + +td, th { + border: 1px solid #dddddd; + text-align: left; + padding: 8px; +} + + td.red { + border-radius: 25px; + text-align: center; + background-color: #f0a0a0; + } + + td.green { + border-radius: 25px; + text-align: center; + background-color: #a0f0a0; + } + + td.cent { + text-align: center; + } + + td.right { + text-align: right; + } + + th.cent { + text-align: center; + } + + th.right { + text-align: right; + } + +tr:nth-child(even) { + background-color: #dddddd; +} + +img { + max-width: 100%; + max-height: 100%; + object-fit: contain; +} + +.rspecial { + border: 1px solid #ffffff; + border-radius: 25px; + text-align: center; + background-color: 0xf0a0a0; + margin: 2px; +} + +.judgeicon { + height: 36px; + width: auto; +} + +.tooltip { + position: relative; + display: inline-block; + border-bottom: 3px dashed blue; +} + + .tooltip .tooltiptext { + visibility: hidden; + width: 120px; + background-color: black; + color: white; + text-align: center; + border-radius: 6px; + padding: 5px 0; + /* Position the tooltip */ + position: absolute; + z-index: 1; + } + + .tooltip:hover .tooltiptext { + visibility: visible; + } + +th { + cursor: pointer; +} diff --git a/support/judge_webcgi/scripts/tablesort.js b/support/judge_webcgi/scripts/tablesort.js new file mode 100644 index 000000000..3788b5cf0 --- /dev/null +++ b/support/judge_webcgi/scripts/tablesort.js @@ -0,0 +1,28 @@ +const getCellValue = (tr, idx) => tr.children[idx].innerText || tr.children[idx].textContent; + +const comparer = (idx, asc) => (a, b) => ((v1, v2) => + v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.toString().localeCompare(v2, undefined, {numeric: true}) + )(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx)); + +// do the work... +document.querySelectorAll('th').forEach(th => th.addEventListener('click', (() => { + const table = th.closest('table'); + if (th.querySelector('span')){ + let order_icon = th.querySelector('span'); + // Awesome? hack: use the icon as a sort indicator for the column. we convert + // it to a URI and look at the UTF-8 encoding. Calm yourself. I found this code + // here: https://github.com/VFDouglas/HTML-Order-Table-By-Column/blob/main/index.html + // and hacked it up further. -- JB + let order = encodeURI(order_icon.innerHTML).includes('%E2%96%B2') ? 'desc' : 'asc'; + Array.from(table.querySelectorAll('tr:nth-child(n+2)')) + .sort(comparer(Array.from(th.parentNode.children).indexOf(th), order === 'asc')) + .forEach(tr => table.appendChild(tr) ); + if(order === 'desc') { + // down triangle + order_icon.innerHTML = "▼" + } else { + // up triangle + order_icon.innerHTML = "▲" + } + } +}))); From 8048bf15310592db7d8e0ea3a024b18e4c37e9f6 Mon Sep 17 00:00:00 2001 From: John Buck Date: Wed, 17 Jul 2024 20:46:02 -0400 Subject: [PATCH 44/55] i_972 Bug fixes and code cleanup Refactor name of judgesExecuteFolder to be just ExecuteFolder since it applies to anyone who can judge. Read the execute-folder property from the system.pc2.yaml file for CDP configuration. Export the execute folder when the contest is exported. Add error logging to indicate why a compile failed Prevent null references (these were harmless, but caused exception traces in the log files) --- .../csus/ecs/pc2/core/execute/Executable.java | 59 +++++----- .../csus/ecs/pc2/core/export/ExportYAML.java | 101 +++++++++--------- .../pc2/core/model/ContestInformation.java | 18 ++-- .../imports/ccs/ContestSnakeYAMLLoader.java | 4 +- .../ecs/pc2/imports/ccs/IContestLoader.java | 2 + .../ecs/pc2/ui/ContestInformationPane.java | 4 +- 6 files changed, 100 insertions(+), 88 deletions(-) diff --git a/src/edu/csus/ecs/pc2/core/execute/Executable.java b/src/edu/csus/ecs/pc2/core/execute/Executable.java index 89a273895..42a11d8c5 100644 --- a/src/edu/csus/ecs/pc2/core/execute/Executable.java +++ b/src/edu/csus/ecs/pc2/core/execute/Executable.java @@ -2841,6 +2841,7 @@ protected boolean compileProgram() { executionData.setCompileResultCode(0); return true; } else { + log.log(Log.INFO, "Expected compiler to generate executable: '" + programName + "' but it did not - Compile failed"); executionData.setCompileExeFileName(""); executionData.setCompileSuccess(false); executionData.setCompileResultCode(2); @@ -3139,9 +3140,7 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb if(executeInfoFileName != null) { newString = replaceString(newString, "{:executeinfofilename}", executeInfoFileName); } else { - // can't happen, but if it does, just use default basename newString = replaceString(newString, "{:executeinfofilename}", Constants.PC2_EXECUTION_RESULTS_NAME_SUFFIX); - log.config("substituteAllStrings() executeInfoFileName is null, using default basename" + Constants.PC2_EXECUTION_RESULTS_NAME_SUFFIX); } String fileName = getJudgeFileName(Utilities.DataFileType.JUDGE_DATA_FILE, dataSetNumber-1); if(fileName == null) { @@ -3415,7 +3414,7 @@ public void setProblem(Problem problem) { * @return the name of the execute directory for this client. */ public String getExecuteDirectoryName() { - String dirName = getContestInformation().getJudgesExecuteFolder(); + String dirName = getContestInformation().getExecuteFolder(); if(StringUtilities.isEmpty(dirName)) { dirName = DEFAULT_EXECUTE_DIRECTORY_TEMPLATE; } @@ -3668,34 +3667,38 @@ private String getJudgeFileName(Utilities.DataFileType type, int setIndex) String result = null; SerializedFile serializedFile = null; - try { - // it's a little more work for external files - if (problem.isUsingExternalDataFiles()) { + // It's possible for this to be null if someone requests the name before the run has started + // executing. + if(problemDataFiles != null) { + try { + // it's a little more work for external files + if (problem.isUsingExternalDataFiles()) { - if(type == Utilities.DataFileType.JUDGE_DATA_FILE) { - serializedFile = problemDataFiles.getJudgesDataFiles()[setIndex]; + if(type == Utilities.DataFileType.JUDGE_DATA_FILE) { + serializedFile = problemDataFiles.getJudgesDataFiles()[setIndex]; + } else { + serializedFile = problemDataFiles.getJudgesAnswerFiles()[setIndex]; + } + //Note: last argument (type) is unused in locateJudgesDataFile + result = Utilities.locateJudgesDataFile(problem, serializedFile, getContestInformation().getJudgeCDPBasePath(), type); } else { - serializedFile = problemDataFiles.getJudgesAnswerFiles()[setIndex]; + // For internal files, the appropriate data files are copied to the FIRST datafile's name in the + // execute folder, so we always return that one. + if(type == Utilities.DataFileType.JUDGE_DATA_FILE) { + result = prefixExecuteDirname(problem.getDataFileName()); + } else { + result = prefixExecuteDirname(problem.getAnswerFileName()); + } } - //Note: last argument (type) is unused in locateJudgesDataFile - result = Utilities.locateJudgesDataFile(problem, serializedFile, getContestInformation().getJudgeCDPBasePath(), type); - } else { - // For internal files, the appropriate data files are copied to the FIRST datafile's name in the - // execute folder, so we always return that one. - if(type == Utilities.DataFileType.JUDGE_DATA_FILE) { - result = prefixExecuteDirname(problem.getDataFileName()); + } catch (Exception e) + { + //if we got far enough to get the serialized file, show the expected name in the log message + if(serializedFile != null) { + log.log(Log.WARNING, "Can not get " + type.toString() + " expected filename (" + serializedFile.getName() + ") for dataset " + (setIndex+1) + ": " + e.getMessage(), e); } else { - result = prefixExecuteDirname(problem.getAnswerFileName()); + log.log(Log.WARNING, "Can not get " + type.toString() + " filename for dataset " + (setIndex+1) + ": " + e.getMessage(), e); } } - } catch (Exception e) - { - //if we got far enough to get the serialized file, show the expected name in the log message - if(serializedFile != null) { - log.log(Log.WARNING, "Can not get " + type.toString() + " expected filename (" + serializedFile.getName() + ") for dataset " + (setIndex+1) + ": " + e.getMessage(), e); - } else { - log.log(Log.WARNING, "Can not get " + type.toString() + " filename for dataset " + (setIndex+1) + ": " + e.getMessage(), e); - } } return(result); } @@ -3776,7 +3779,11 @@ private boolean saveExecuteData(int dataSetNumber) executeDataWriter.println("compileTimeMS='" + executionData.getCompileTimeMS() + "'"); executeDataWriter.println("executeTimeMS='" + executionData.getExecuteTimeMS() + "'"); executeDataWriter.println("validateTimeMS='" + executionData.getvalidateTimeMS() + "'"); - executeDataWriter.println("executionException='" + showNullAsEmpty(executionData.getExecutionException().getMessage()) + "'"); + if(executionData.getExecutionException() != null) { + executeDataWriter.println("executionException='" + showNullAsEmpty(executionData.getExecutionException().getMessage()) + "'"); + } else { + executeDataWriter.println("executionException=''"); + } executeDataWriter.println("runTimeLimitExceeded='" + executionData.isRunTimeLimitExceeded() + "'"); executeDataWriter.println("additionalInformation='" + showNullAsEmpty(executionData.getAdditionalInformation()) + "'"); bWritten = true; diff --git a/src/edu/csus/ecs/pc2/core/export/ExportYAML.java b/src/edu/csus/ecs/pc2/core/export/ExportYAML.java index 3a5188578..74cee2766 100644 --- a/src/edu/csus/ecs/pc2/core/export/ExportYAML.java +++ b/src/edu/csus/ecs/pc2/core/export/ExportYAML.java @@ -40,9 +40,9 @@ /** * Create CCS contest.yaml and problem.yaml files. - * + * * Creates contest.yaml and problem.yaml files along with all the data files per the CCS specification. - * + * * @author pc2@ecs.csus.edu */ // TODO REFACTOR and CI use constants for secret and sample dir names @@ -71,9 +71,9 @@ public class ExportYAML { /** * Write CCS Yaml files to directory. - * + * * Creates files: - * + * *
      * directoryName/contest.yaml
      * directoryName/shortname1/problem.yaml
@@ -82,7 +82,7 @@ public class ExportYAML {
      * directoryName/shortname2/problem.yaml
      * directoryName/shortname3/problem.yaml
      * 
- * + * * @param directoryName * @param contest * @throws IOException @@ -103,7 +103,7 @@ private String getDateTimeString() { /** * Write contest and problem yaml and files to directory. - * + * * @param contest * @param directoryName * @param contestFileName @@ -156,10 +156,10 @@ public void writeContestYAMLFiles(IInternalContest contest, String directoryName contestWriter.println("remaining: " + contestTime.getRemainingTimeStr()); contestWriter.println("running: " + contestTime.isContestRunning()); - + // PC^2 Specific contestWriter.println(IContestLoader.AUTO_STOP_CLOCK_AT_END_KEY + ": " + info.isAutoStopContest()); - + // start-time: 2011-02-04 01:23Z if (info.getScheduledStartDate() == null) { info.setScheduledStartDate(new Date()); @@ -171,13 +171,13 @@ public void writeContestYAMLFiles(IInternalContest contest, String directoryName // TODO CCS scoreboard-freeze: 4:00:00 // scoreboard-freeze: 4:00:00 - + // Bug 1192 - // scoreboard-freeze-length Time length before end of contest when scoreboard will be frozen, in h:mm:ss + // scoreboard-freeze-length Time length before end of contest when scoreboard will be frozen, in h:mm:ss // contestWriter.println("scoreboard-freeze: " + info.getFreezeTime()); contestWriter.println("scoreboard-freeze-length: " + info.getFreezeTime()); - + contestWriter.println(IContestLoader.OUTPUT_PRIVATE_SCORE_DIR_KEY+": " + info.getScoringProperties().getProperty(DefaultScoringAlgorithm.JUDGE_OUTPUT_DIR)); contestWriter.println(IContestLoader.OUTPUT_PUBLIC_SCORE_DIR_KEY+": " + info.getScoringProperties().getProperty(DefaultScoringAlgorithm.PUBLIC_OUTPUT_DIR)); @@ -188,12 +188,13 @@ public void writeContestYAMLFiles(IInternalContest contest, String directoryName if (! StringUtilities.isEmpty(judgeCDPBasePath)){ contestWriter.println(IContestLoader.JUDGE_CONFIG_PATH_KEY +": "+judgeCDPBasePath); } - + contestWriter.println(IContestLoader.SHADOW_MODE_KEY + ": " + (new Boolean(info.isShadowMode()).toString())); contestWriter.println(IContestLoader.CCS_URL_KEY + ": " + (info.getPrimaryCCS_URL())); contestWriter.println(IContestLoader.CCS_LOGIN_KEY + ": " + (info.getPrimaryCCS_user_login())); contestWriter.println(IContestLoader.CCS_PASSWORD_KEY + ": " + (info.getPrimaryCCS_user_pw())); contestWriter.println(IContestLoader.CCS_LAST_EVENT_ID_KEY + ": " + (info.getLastShadowEventID())); + contestWriter.println(IContestLoader.EXECUTE_FOLDER + ": " + (info.getExecuteFolder())); contestWriter.println(); @@ -246,7 +247,7 @@ public void writeContestYAMLFiles(IInternalContest contest, String directoryName contestWriter.println(PAD4 + "compilerCmd: " + quote(language.getCompileCommandLine())); contestWriter.println(PAD4 + "exemask: " + quote(language.getExecutableIdentifierMask())); contestWriter.println(PAD4 + "execCmd: " + quote(language.getProgramExecuteCommandLine())); - + String clicsId = language.getID(); if ( clicsId != null && clicsId.trim().length() > 0){ contestWriter.println(PAD4 + "clics-id: " + quote(clicsId)); @@ -259,7 +260,7 @@ public void writeContestYAMLFiles(IInternalContest contest, String directoryName contestWriter.println(PAD4 + "runner: " + quote(runner)); contestWriter.println(PAD4 + "runner-args: " + quote(runnerArguments)); } - + contestWriter.println(PAD4 + IContestLoader.INTERPRETED_LANGUAGE_KEY + ": " + language.isInterpreted()); contestWriter.println(PAD4 + "use-judge-cmd: " + language.isUsingJudgeProgramExecuteCommandLine()); contestWriter.println(PAD4 + "judge-exec-cmd: " + quote(language.getJudgeProgramExecuteCommandLine())); @@ -267,30 +268,30 @@ public void writeContestYAMLFiles(IInternalContest contest, String directoryName contestWriter.println(); } // end of system.yaml stuff - + Problem[] problems = contest.getProblems(); - + /** - * Write problemset.yaml file. + * Write problemset.yaml file. */ String confiDir = new File(contestFileName).getParent(); - + String problemSetFilename = confiDir + File.separator + IContestLoader.DEFAULT_PROBLEM_SET_YAML_FILENAME; PrintWriter problemSetWriter = new PrintWriter(new FileOutputStream(problemSetFilename, false), true); problemSetWriter.println("# Contest Configuration, Problem Set, version 1.0 "); problemSetWriter.println("# PC^2 Version: " + new VersionInfo().getSystemVersionInfo()); problemSetWriter.println("# Created: " + getDateTimeString()); - + writeProblemSetYaml(problemSetWriter, contest, directoryName, IContestLoader.PROBLEMSET_PROBLEMS_KEY, problems); - + problemSetWriter.flush(); problemSetWriter.close(); problemSetWriter = null; // TODO this should go to system.pc2.yaml Vector accountVector = contest.getAccounts(ClientType.Type.JUDGE); - Account[] judgeAccounts = (Account[]) accountVector.toArray(new Account[accountVector.size()]); + Account[] judgeAccounts = accountVector.toArray(new Account[accountVector.size()]); Arrays.sort(judgeAccounts, new AccountComparator()); int ajCount = 0; @@ -386,11 +387,11 @@ public void writeContestYAMLFiles(IInternalContest contest, String directoryName /** * Write problem set section. - * + * * @param writer * @param contest * @param directoryName - * @param problemsKey problemset for contest.yaml or problems for problemset.yaml + * @param problemsKey problemset for contest.yaml or problems for problemset.yaml * @param problems * @throws IOException */ @@ -399,8 +400,8 @@ private void writeProblemSetYaml(PrintWriter writer, IInternalContest contest, S if (problems.length > 0) { writer.println(problemsKey + ":"); } - - // + + // // problemset: // // - letter: B @@ -454,12 +455,12 @@ private void writeProblemSetYaml(PrintWriter writer, IInternalContest contest, S writer.println(); } - + } /** * Surround by a single quote - * + * * @param string * @return */ @@ -468,7 +469,7 @@ private String quote(String string) { } /** - * + * * @param date * @return empty string if date is null, other wise */ @@ -515,9 +516,9 @@ protected static StringBuffer join(String delimiter, List list) { /** * Create disk file for input SerializedFile. - * + * * Returns true if file is written to disk and is not null. - * + * * @param file * @param outputFileName * @return true if file written to disk, false if external or not written to disk. @@ -533,7 +534,7 @@ boolean createFile(SerializedFile file, String outputFileName) throws IOExceptio /** * Write problem yaml and data files files to directory. - * + * * @param contest * @param directoryName * directory to write files to. @@ -553,7 +554,7 @@ public String[] writeProblemYAML(IInternalContest contest, String directoryName, /** * Write problem YAML to file. - * + * * @param contest * @param problem * @param filename @@ -591,7 +592,7 @@ public String[] writeProblemYAML(IInternalContest contest, Problem problem, Stri problemWriter.println("author: "); problemWriter.println("license: "); problemWriter.println("rights_owner: "); - + problemWriter.println(); problemWriter.println(IContestLoader.HIDE_PROBLEM+": "+!problem.isActive()); @@ -600,7 +601,7 @@ public String[] writeProblemYAML(IInternalContest contest, Problem problem, Stri problemWriter.println(IContestLoader.SHOW_VALIDATION_RESULTS+": "+problem.isShowValidationToJudges()); problemWriter.println(IContestLoader.PROBLEM_LOAD_DATA_FILES_KEY + ": " + (!isExternalFiles(problemDataFiles))); problemWriter.println(IContestLoader.STOP_ON_FIRST_FAILED_TEST_CASE_KEY + ": " + problem.isStopOnFirstFailedTestCase()); - + problemWriter.println(); List groups = problem.getGroups(); if (groups.size() == 0){ @@ -615,7 +616,7 @@ public String[] writeProblemYAML(IInternalContest contest, Problem problem, Stri // datafile: sumit.in problemWriter.println("datafile: " + dataFile); } - + String answerFileName = problem.getAnswerFileName(); if (answerFileName != null) { // answerfile: sumit.ans @@ -628,21 +629,21 @@ public String[] writeProblemYAML(IInternalContest contest, Problem problem, Stri } else { problemWriter.println("# "+ IContestLoader.VALIDATOR_FLAGS_KEY + ": "); } - + problemWriter.println(); - + problemWriter.println(IContestLoader.LIMITS_KEY + ":"); problemWriter.println(PAD4 + "timeout: " + problem.getTimeOutInSeconds()); if (problem.getMemoryLimitMB() > 0) { problemWriter.println(PAD4 + IContestLoader.MEMORY_LIMIT_CLICS + ": " + problem.getMemoryLimitMB()); } - + problemWriter.println(); - + problemWriter.println(IContestLoader.SANDBOX_TYPE_KEY+": "+problem.getSandboxType()); problemWriter.println(IContestLoader.SANDBOX_COMMAND_LINE_KEY+": "+quote(problem.getSandboxCmdLine())); problemWriter.println(IContestLoader.SANDBOX_PROGRAM_NAME_KEY+": "+quote(problem.getSandboxProgramName())); - + problemWriter.println(); if (problem.isValidatedProblem()) { @@ -660,19 +661,19 @@ public String[] writeProblemYAML(IInternalContest contest, Problem problem, Stri } problemWriter.println(); } - + problemWriter.println(IContestLoader.JUDGING_TYPE_KEY + ":"); - + problemWriter.println(PAD4 + IContestLoader.COMPUTER_JUDGING_KEY +": " + problem.isComputerJudged()); if (problem.isComputerJudged()) { - // if computer judged, may or may not be manual judged + // if computer judged, may or may not be manual judged problemWriter.println(PAD4 + IContestLoader.MANUAL_REVIEW_KEY + ": " + problem.isManualReview()); } else { // if not computer judged then MUST be manaul judged problemWriter.println(PAD4 + IContestLoader.MANUAL_REVIEW_KEY + ": true"); } - + problemWriter.println(PAD4 + IContestLoader.SEND_PRELIMINARY_JUDGEMENT_KEY + ": " + problem.isPrelimaryNotification()); problemWriter.println(); @@ -734,12 +735,12 @@ public String[] writeProblemYAML(IInternalContest contest, Problem problem, Stri problemWriter.close(); problemWriter = null; - return (String[]) filesWritten.toArray(new String[filesWritten.size()]); + return filesWritten.toArray(new String[filesWritten.size()]); } /** * Return true if files are not loaded/stored internally. - * + * * @param problemDataFiles * @return */ @@ -768,7 +769,7 @@ private boolean isExternalFiles(ProblemDataFiles problemDataFiles) { /** * Write problem title to (LaTeX) file - * + * * @param filename * @param title * @throws FileNotFoundException @@ -798,9 +799,9 @@ protected void writeProblemTitleToFile(String filename, String title) throws Fil /** * Get problem letter for input integer. - * + * * getProblemLetter(1) is 'A' - * + * * @param id * a one based problem number. * @return @@ -813,7 +814,7 @@ protected String getProblemLetter(int id) { /** * Create a problem short name. - * + * * @param name * Problem full name * @return diff --git a/src/edu/csus/ecs/pc2/core/model/ContestInformation.java b/src/edu/csus/ecs/pc2/core/model/ContestInformation.java index 383d74fd2..d861a7712 100644 --- a/src/edu/csus/ecs/pc2/core/model/ContestInformation.java +++ b/src/edu/csus/ecs/pc2/core/model/ContestInformation.java @@ -99,7 +99,7 @@ public class ContestInformation implements Serializable{ /** * String to append to the execute folder for judged runs. May contain substitution strings. */ - private String judgesExecuteFolder = ""; + private String executeFolder = ""; /** * @@ -262,11 +262,11 @@ public void setJudgesDefaultAnswer(String judgesDefaultAnswer) { } } - public String getJudgesExecuteFolder() { - if(judgesExecuteFolder == null) { - judgesExecuteFolder = ""; + public String getExecuteFolder() { + if(executeFolder == null) { + executeFolder = ""; } - return judgesExecuteFolder ; + return executeFolder ; } /** @@ -274,9 +274,9 @@ public String getJudgesExecuteFolder() { * * @param judgesDefaultAnswer The judgesDefaultAnswer to set. */ - public void setJudgesExecuteFolder(String judgesExecuteFolder) { - if (judgesExecuteFolder != null && judgesExecuteFolder.trim().length() > 0) { - this.judgesExecuteFolder = judgesExecuteFolder.trim(); + public void setExecuteFolder(String executeFolder) { + if (executeFolder != null && executeFolder.trim().length() > 0) { + this.executeFolder = executeFolder.trim(); } } @@ -294,7 +294,7 @@ public boolean isSameAs(ContestInformation contestInformation) { if (!judgesDefaultAnswer.equals(contestInformation.getJudgesDefaultAnswer())) { return false; } - if (!judgesExecuteFolder.equals(contestInformation.getJudgesExecuteFolder())) { + if (!executeFolder.equals(contestInformation.getExecuteFolder())) { return false; } if (!teamDisplayMode.equals(contestInformation.getTeamDisplayMode())) { diff --git a/src/edu/csus/ecs/pc2/imports/ccs/ContestSnakeYAMLLoader.java b/src/edu/csus/ecs/pc2/imports/ccs/ContestSnakeYAMLLoader.java index 51553af79..d5e2e3df2 100644 --- a/src/edu/csus/ecs/pc2/imports/ccs/ContestSnakeYAMLLoader.java +++ b/src/edu/csus/ecs/pc2/imports/ccs/ContestSnakeYAMLLoader.java @@ -479,10 +479,12 @@ public IInternalContest fromYaml(IInternalContest contest, String[] yamlLines, S String ccsPassoword = fetchValue(content, CCS_PASSWORD_KEY, contestInformation.getPrimaryCCS_user_pw()); contestInformation.setPrimaryCCS_user_pw(ccsPassoword); - String lastEventId = fetchValue(content, CCS_LAST_EVENT_ID_KEY, contestInformation.getLastShadowEventID()); contestInformation.setLastShadowEventID(lastEventId); + String executeDir = fetchValue(content, EXECUTE_FOLDER, contestInformation.getExecuteFolder()); + contestInformation.setExecuteFolder(executeDir); + // save ContesInformation to model contest.updateContestInformation(contestInformation); diff --git a/src/edu/csus/ecs/pc2/imports/ccs/IContestLoader.java b/src/edu/csus/ecs/pc2/imports/ccs/IContestLoader.java index 2cc905732..a2317662e 100644 --- a/src/edu/csus/ecs/pc2/imports/ccs/IContestLoader.java +++ b/src/edu/csus/ecs/pc2/imports/ccs/IContestLoader.java @@ -127,6 +127,8 @@ public interface IContestLoader { String JUDGE_CONFIG_PATH_KEY = "judge-config-path"; + String EXECUTE_FOLDER = "execute-folder"; + String TIMEOUT_KEY = "timeout"; final String MEMORY_LIMIT_IN_MEG_KEY = "memory-limit-in-Meg"; diff --git a/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java b/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java index 0832e475e..a4ff20617 100644 --- a/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java +++ b/src/edu/csus/ecs/pc2/ui/ContestInformationPane.java @@ -1002,7 +1002,7 @@ protected ContestInformation getFromFields() { //fill in judging information newContestInformation.setJudgesDefaultAnswer(getJudgesDefaultAnswerTextField().getText()); - newContestInformation.setJudgesExecuteFolder(getJudgesExecuteFolderTextField().getText()); + newContestInformation.setExecuteFolder(getJudgesExecuteFolderTextField().getText()); newContestInformation.setPreliminaryJudgementsTriggerNotifications(getJCheckBoxShowPreliminaryOnNotifications().isSelected()); newContestInformation.setPreliminaryJudgementsUsedByBoard(getJCheckBoxShowPreliminaryOnBoard().isSelected()); newContestInformation.setSendAdditionalRunStatusInformation(getAdditionalRunStatusCheckBox().isSelected()); @@ -1097,7 +1097,7 @@ public void run() { selectDisplayRadioButton(); getJudgesDefaultAnswerTextField().setText(contestInformation.getJudgesDefaultAnswer()); - getJudgesExecuteFolderTextField().setText(contestInformation.getJudgesExecuteFolder()); + getJudgesExecuteFolderTextField().setText(contestInformation.getExecuteFolder()); getJCheckBoxShowPreliminaryOnBoard().setSelected(contestInformation.isPreliminaryJudgementsUsedByBoard()); getJCheckBoxShowPreliminaryOnNotifications().setSelected(contestInformation.isPreliminaryJudgementsTriggerNotifications()); getAdditionalRunStatusCheckBox().setSelected(contestInformation.isSendAdditionalRunStatusInformation()); From 83dad7fadac54325c9ff99f3fa66ff6911d8696a Mon Sep 17 00:00:00 2001 From: John Buck Date: Thu, 18 Jul 2024 15:58:43 -0400 Subject: [PATCH 45/55] i_972 CI Fix possible empty string in substituteAllStrings CI: The substituteAllStrings method could possibly return an empty string in the future. Always copy the original string into the returned string before doing anyway instead of relying on the first substitute to do it. --- .../csus/ecs/pc2/core/execute/Executable.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/edu/csus/ecs/pc2/core/execute/Executable.java b/src/edu/csus/ecs/pc2/core/execute/Executable.java index 42a11d8c5..2923492ce 100644 --- a/src/edu/csus/ecs/pc2/core/execute/Executable.java +++ b/src/edu/csus/ecs/pc2/core/execute/Executable.java @@ -3042,7 +3042,8 @@ public String substituteAllStrings(Run inRun, String origString) { * @return string with values */ public String substituteAllStrings(Run inRun, String origString, int dataSetNumber) { - String newString = ""; + // Make a new copy to start with to avoid issues in the future. + String newString = origString; String nullArgument = "-"; /* this needs to change */ try { @@ -3050,13 +3051,15 @@ public String substituteAllStrings(Run inRun, String origString, int dataSetNumb throw new IllegalArgumentException("Run is null"); } - if (runFiles.getMainFile() == null) { - log.config("substituteAllStrings() main file is null (no contents)"); - return origString; + if(runFiles != null) { + if (runFiles.getMainFile() == null) { + log.config("substituteAllStrings() main file is null (no contents)"); + return newString; + } + newString = replaceString(newString, "{:mainfile}", runFiles.getMainFile().getName()); + newString = replaceString(newString, Constants.CMDSUB_FILES_VARNAME, ExecuteUtilities.getAllSubmittedFilenames(runFiles)); + newString = replaceString(newString, Constants.CMDSUB_BASENAME_VARNAME, removeExtension(runFiles.getMainFile().getName())); } - newString = replaceString(origString, "{:mainfile}", runFiles.getMainFile().getName()); - newString = replaceString(newString, Constants.CMDSUB_FILES_VARNAME, ExecuteUtilities.getAllSubmittedFilenames(runFiles)); - newString = replaceString(newString, Constants.CMDSUB_BASENAME_VARNAME, removeExtension(runFiles.getMainFile().getName())); newString = replaceString(newString, "{:package}", packageName); String validatorCommand = null; From b9a7c8acd5b38eccd3759eb557ac4211b7de4f48 Mon Sep 17 00:00:00 2001 From: John Buck Date: Thu, 18 Jul 2024 16:13:07 -0400 Subject: [PATCH 46/55] i_972 Script cleanup and bug fixes Cleaned up interactive script. Made interactive script configurable so we can change the way it judges RTE/WA/AC to match DOMjudge, if necessary. Fixed combined runs webpage to sort runs from all judges. --- scripts/pc2sandbox_interactive.sh | 115 +++++++++---------- support/judge_webcgi/cgi-bin/alljudgments.py | 55 +++++---- support/judge_webcgi/cgi-bin/getjudgments.sh | 2 +- support/judge_webcgi/cgi-bin/pc2common.sh | 15 ++- 4 files changed, 103 insertions(+), 84 deletions(-) diff --git a/scripts/pc2sandbox_interactive.sh b/scripts/pc2sandbox_interactive.sh index aa4fcfec0..e2c6834a0 100644 --- a/scripts/pc2sandbox_interactive.sh +++ b/scripts/pc2sandbox_interactive.sh @@ -21,12 +21,10 @@ # finish, and use whatever judgement is appropriate. If contestant finishes with 0, then we use the # vaildator result, else we use the exit code of the submission. KILL_WA_VALIDATOR=1 -# If WAIT_FOR_SUB_ON_AC is 1, we wait for the submission if the validator says "AC". We then -# use the exit code of the submission if it is non-zero to determine disposition, otherise, -# we return AC -# If WAIT_FOR_SUB_ON_AC is 0, we will kill off the submission as soon as we get an AC from the -# validator if the submission is not complete yet. -WAIT_FOR_SUB_ON_AC=1 + +# Always return WA if validator gets WA before submission finishes. This is the DOMjudge compatibility +# option (set it to 1 to be compatible) +ALWAYS_WA_BEFORE_SUB_FINISHES=1 # CPU to run submission on. This is 0 based, so 3 means the 4th CPU DEFAULT_CPU_NUM=3 @@ -240,6 +238,23 @@ GetTimeInMicros() echo $ret } +GetSandboxStats() +{ + # Get cpu time immediately to minimize any usage by this shell + cputime=`grep usage_usec $PC2_SANDBOX_CGROUP_PATH/cpu.stat | cut -d ' ' -f 2` + # Get wall time - we want it as close as possible to when we fetch the cpu time so they stay close + # since the cpu.stat includes the time this script takes AFTER the submission finishes. + endtime=`GetTimeInMicros` + walltime=$((endtime-starttime)) + # Newer kernels support memory.peak, so we have to check if it's there. + if test -e $PC2_SANDBOX_CGROUP_PATH/memory.peak + then + peakmem=`cat $PC2_SANDBOX_CGROUP_PATH/memory.peak` + else + peakmem="N/A" + fi +} + # Show run's resource summary in a nice format, eg. # CPU ms Limit ms Wall ms Memory Used Memory Limit # 3.356 5000.000 4.698 1839104 2147483648 @@ -585,10 +600,15 @@ do # Wait for the next process and put its PID in child_pid wait -n -p child_pid wstat=$? + REPORT_DEBUG Wait returned pid=$child_pid wstat=$wstat # A return code 127 indicates there are no more children. How did that happen? if test $wstat -eq 127 then REPORT_DEBUG No more children found while waiting: Submission PID was $submissionpid and Interactive Validator PID was $intv_pid + if test -d /proc/$submissionpid + then + REPORT_DEBUG The contestant pid /proc/$submissionpid still exists though + fi break fi # If interactive validator finishes @@ -597,39 +617,30 @@ do REPORT_DEBUG Validator finishes with exit code $wstat if test "$contestant_done" -eq 0 then - # If we always kill off the child when the validator finishes with WA, or the validator finishes with "AC" and we dont wait for submission on AC, - # then kill off the child, otherwise we wait for it. - if test "(" ${KILL_WA_VALIDATOR} -eq 1 -a "$wstat" -ne "${EXITCODE_AC}" ")" -o "(" ${WAIT_FOR_SUB_ON_AC} -eq 0 -a "$wstat" -eq ${EXITCODE_AC} ")" + REPORT_DEBUG Waiting for submission pid $submissionpid to finish... + # Wait for child to finish - it has to, one way or the other (TLE or just finish) + wait -n "$submissionpid" + COMMAND_EXIT_CODE=$? + + GetSandboxStats + + if test $COMMAND_EXIT_CODE -eq 127 then - # Only kill it if it still exists - if test -d /proc/$contestantpid - then - REPORT_DEBUG Contestant PID $submissionpid has not finished - killing it "(KILL_WA_VALIDATOR=1)" - # TODO: We should kill and wait for it here and print out the stats - fi - # This just determines if the program ran, not if it's correct. - # The result file has the correctness in it. - # We only do this if the contestant program has not finished yet. - REPORT_DEBUG Indicating that the submission exited with code 0 because we killed it. + REPORT_DEBUG No more children found. Setting submission exit code to 0 COMMAND_EXIT_CODE=0 else - REPORT_DEBUG Contestant PID $submissionpid has not finished - waiting for it "(KILL_WA_VALIDATOR=1)" - # Need to wait for child. Remember validator result. - val_result="$wstat" - wait_sub=1 + FormatExitCode $COMMAND_EXIT_CODE + REPORT_DEBUG Contestant PID $submissionpid finished with exit code $result but after the validator fi + ShowStats ${cputime} ${TIMELIMIT_US} ${walltime} ${peakmem} $((MEMLIMIT*1024*1024)) fi - # Kill everything off if we don't have to wait for submission - if test "$wait_sub" -eq 0 - then - KillChildProcs - fi + KillChildProcs if test "$wstat" -eq $EXITCODE_AC then - # COMMAND_EXIT_CODE will be set to 0 above, or 0 if the submission finished already with EC 0. - # If COMMAND_EXIT_CODE is anything else, we will either be waiting for the submission, or, use + # COMMAND_EXIT_CODE will be set to 0 above, or 0 if the submission previously finished already with EC 0. + # If COMMAND_EXIT_CODE is anything else, we'll use # it's exit code as the result. The GenXML will have been filled in in this case with any errors. if test -z "${COMMAND_EXIT_CODE}" -o "${COMMAND_EXIT_CODE}" = "0" then @@ -645,9 +656,9 @@ do # XML result file will have already been created with the disposition from below. # We will only generate an XML here if the validator says it failed (WA). # COMMAND_EXIT_CODE can be empty if we are waiting for the submission to finish. - if test -z "${COMMAND_EXIT_CODE}" -o "${COMMAND_EXIT_CODE}" = "0" + if test "(" ${ALWAYS_WA_BEFORE_SUB_FINISHES} -eq 1 -a "$contestant_done" -eq 0 ")" -o "(" -z "${COMMAND_EXIT_CODE}" -o "${COMMAND_EXIT_CODE}" = "0" ")" then - REPORT_DEBUG Submission has an exit code of 0 and validator says WA. + REPORT_DEBUG Submission has an exit code of ${COMMAND_EXIT_CODE} and validator says WA. "(ALWAYS_WA_BEFORE_SUB_FINISHES=${ALWAYS_WA_BEFORE_SUB_FINISHES})" # If validator created a feedback file, put the last line in the judgement if test -s "$feedbackfile" then @@ -655,48 +666,35 @@ do else GenXML "No - Wrong answer" "No feedback file" fi + COMMAND_EXIT_CODE=0 fi else REPORT_DEBUG Validator returned code $wstat which is not 42 or 43 - validator error. GenXML "Other - contact staff" "bad validator return code $wstat" - if test "$wait_sub" -ne 0 - then - KillChildProcs - fi COMMAND_EXIT_CODE=${FAIL_INTERACTIVE_ERROR} break fi - if test "$wait_sub" -eq 0 - then - REPORT_DEBUG No need to wait for submission to finish. - break - fi + break fi # If this is the contestant submission if test "$child_pid" -eq "$submissionpid" then - # Get cpu time immediately to minimize any usage by this shell - cputime=`grep usage_usec $PC2_SANDBOX_CGROUP_PATH/cpu.stat | cut -d ' ' -f 2` - # Get wall time - we want it as close as possible to when we fetch the cpu time so they stay close - # since the cpu.stat includes the time this script takes AFTER the submission finishes. - endtime=`GetTimeInMicros` - walltime=$((endtime-starttime)) - # Newer kernels support memory.peak, so we have to check if it's there. - if test -e $PC2_SANDBOX_CGROUP_PATH/memory.peak - then - peakmem=`cat $PC2_SANDBOX_CGROUP_PATH/memory.peak` - else - peakmem="N/A" - fi - + GetSandboxStats FormatExitCode $wstat REPORT_DEBUG Contestant PID $submissionpid finished with exit code $result contestant_done=1 - COMMAND_EXIT_CODE=$wstat ShowStats ${cputime} ${TIMELIMIT_US} ${walltime} ${peakmem} $((MEMLIMIT*1024*1024)) + # If we have already made a definitive judgment based on the validator and we are just waiting + # for the submission to finish, the break out now + if test "$wait_sub" = "2" + then + break + fi + COMMAND_EXIT_CODE=$wstat + # See if we were killed due to memory - this is a kill 9 if it happened kills=`grep oom_kill $PC2_SANDBOX_CGROUP_PATH/memory.events | cut -d ' ' -f 2` @@ -743,8 +741,7 @@ do REPORT_BRIEF RTE GenXML "No - Run-time Error" "Exit status $wstat" # If validator finished with AC, but submission has a bad exit code, we use that. - # Or, if we didn't kill off the submission, and want to use it's exit code - if test "$val_result" = "${EXITCODE_AC}" -o "${KILL_WA_VALIDATOR}" -eq 0 + if test "$val_result" = "${EXITCODE_AC}" then KillValidator break @@ -752,6 +749,8 @@ do else # COMMAND_EXIT_CODE is 0, let's see if the validator finished. If # so, we're done, since the validator already created it's disposition. + # Note: this case should not happen since we wait for the submission above if the validator + # finishes first. if test -n "$val_result" then break diff --git a/support/judge_webcgi/cgi-bin/alljudgments.py b/support/judge_webcgi/cgi-bin/alljudgments.py index c22011828..005a494d1 100644 --- a/support/judge_webcgi/cgi-bin/alljudgments.py +++ b/support/judge_webcgi/cgi-bin/alljudgments.py @@ -1,6 +1,7 @@ #!/usr/bin/python3 import json import urllib.request +import operator import HtmlFunctions @@ -19,7 +20,7 @@ JUDGMENTS_SCRIPT="getjudgments.sh" -JUDGE_HOSTS = ['pc2-aj1', 'pc2-aj2', 'pc2-ccsadmin1'] +JUDGE_HOSTS = ['pc2-aj1', 'pc2-aj2', 'pc2-aj3' ] def MyTableStyles() : print("