diff --git a/scripts/pc2sandbox.sh b/scripts/pc2sandbox.sh old mode 100755 new mode 100644 index 30afd9aeb..2c1577130 --- 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: $* +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 @@ -163,14 +257,30 @@ fi MEMLIMIT=$1 TIMELIMIT=$2 -COMMAND=$3 -shift -shift -shift - -#### Debugging - just set expected first 3 args to: 16MB 5seconds +JUDGEIN=$3 +JUDGEANS=$4 +TESTCASE=$5 +COMMAND=$6 +DEBUG echo "+---------------- Test Case ${TESTCASE} ----------------+" +DEBUG echo Command line: $0 $* +shift 6 + +DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" +scriptname=`basename $0` +RUN_LOCAL_CMD="./${scriptname} ${MEMLIMIT} ${TIMELIMIT} xxx xxx $TESTCASE ${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${COMMAND} $* < ${JUDGEIN} > $TESTCASE.ans" +DEBUG echo -e "\nAnd compare with the judge's answer:" +DEBUG echo -e "\n${DIFF_OUTPUT_CMD}\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,19 +340,31 @@ 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} +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 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 - 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 @@ -245,32 +373,35 @@ fi # 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_DEBUG 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_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` #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 # 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). -DEBUG echo Executing "setsid taskset $CPUMASK $COMMAND $*" +# 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 trap HandleTerminateFromPC2 15 @@ -309,25 +440,30 @@ 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 - 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 old mode 100755 new mode 100644 index 6213b4e1b..5b0b49ed1 --- 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 @@ -15,6 +15,25 @@ # # 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 + +# 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 +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 @@ -37,14 +56,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<> $REPORTFILE fi } +function REPORT_BRIEF() +{ + if test -n "$BRIEFREPORTFILE" + then + echo "$@" >> $BRIEFREPORTFILE + fi +} # For per-testcase report and debugging both function REPORT_DEBUG() @@ -131,24 +185,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 @@ -184,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 @@ -194,32 +265,112 @@ 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)) \ $((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 +} + + sent_xml=0 GenXML() { rm -rf "$INT_RESULTFILE" msg1="$1" - msg2="$2" + shift + msg2="$*" cat << EOF > $INT_RESULTFILE -$msg2 + 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 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 @@ -230,6 +381,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 @@ -240,51 +392,68 @@ 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 +DEBUG echo -e "\nYou can run this by hand in the sandbox by using the following command:" +scriptname=`basename $0` +RUN_LOCAL_CMD="./${scriptname} ${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 "\nAnd see the run report using the following command:" +DEBUG echo -e "\n${REPORT_OUTPUT_CMD}\n" + # make sure we have CGroups V2 properly installed on this system, including a PC2 structure 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 @@ -294,6 +463,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 @@ -302,6 +472,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 @@ -309,12 +480,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 @@ -324,17 +497,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 @@ -349,22 +527,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` @@ -373,6 +558,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 @@ -386,6 +572,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 @@ -397,108 +587,161 @@ 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 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 + if test -d /proc/$submissionpid + then + REPORT_DEBUG The contestant pid /proc/$submissionpid still exists though + fi 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 + 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 - REPORT Contestant PID $submissionpid has not finished - killing it - # TODO: We should kill and wait for it here and print out the stats + REPORT_DEBUG No more children found. Setting submission exit code to 0 + COMMAND_EXIT_CODE=0 + else + FormatExitCode $COMMAND_EXIT_CODE + REPORT_DEBUG Contestant PID $submissionpid finished with exit code $result but after the validator 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 + ShowStats ${cputime} ${TIMELIMIT_US} ${walltime} ${peakmem} $((MEMLIMIT*1024*1024)) fi KillChildProcs if test "$wstat" -eq $EXITCODE_AC then - GenXML Accepted "" + # 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 + 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 "(" ${ALWAYS_WA_BEFORE_SUB_FINISHES} -eq 1 -a "$contestant_done" -eq 0 ")" -o "(" -z "${COMMAND_EXIT_CODE}" -o "${COMMAND_EXIT_CODE}" = "0" ")" then - GenXML "No - Wrong answer" `tail -1 $feedbackfile` - else - GenXML "No - Wrong answer" "No feedback file" + 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 + GenXML "No - Wrong answer" `head -n 1 $feedbackfile` + else + GenXML "No - Wrong answer" "No feedback file" + fi + COMMAND_EXIT_CODE=0 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" + COMMAND_EXIT_CODE=${FAIL_INTERACTIVE_ERROR} + break fi - break; + 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 - REPORT Contestant PID $submissionpid finished with status $wstat + 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` 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 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 - KillValidator + REPORT_DEBUG Contestant finished with a non-zero exit code $result + REPORT_BRIEF RTE GenXML "No - Run-time Error" "Exit status $wstat" - break + # If validator finished with AC, but submission has a bad exit code, we use that. + if test "$val_result" = "${EXITCODE_AC}" + 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. + # 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 + fi fi REPORT_DEBUG Waiting for interactive validator to finish... fi @@ -512,6 +755,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/src/edu/csus/ecs/pc2/core/CommandVariableReplacer.java b/src/edu/csus/ecs/pc2/core/CommandVariableReplacer.java index 6a9557c49..03370a987 100644 --- a/src/edu/csus/ecs/pc2/core/CommandVariableReplacer.java +++ b/src/edu/csus/ecs/pc2/core/CommandVariableReplacer.java @@ -1,10 +1,12 @@ -// 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; import java.io.File; import edu.csus.ecs.pc2.VersionInfo; import edu.csus.ecs.pc2.core.execute.ExecutionData; +import edu.csus.ecs.pc2.core.log.Log; +import edu.csus.ecs.pc2.core.log.StaticLog; import edu.csus.ecs.pc2.core.model.Account; import edu.csus.ecs.pc2.core.model.ClientId; import edu.csus.ecs.pc2.core.model.IInternalContest; @@ -17,7 +19,7 @@ /** * Replace command line variables. - * + * * @author pc2@ecs.csus.edu * @version $Id$ */ @@ -33,7 +35,7 @@ public class CommandVariableReplacer { /** * All files, main file and additional files. - * + * */ public static final String FILELIST = "{:filelist}"; @@ -51,33 +53,64 @@ public class CommandVariableReplacer { * Run Id (or number). */ public static final String RUNID = "{:runid}"; - + public static final String RUNNUMBER = "{:runnumber}"; + /** * Internal run id. - * - * If run comes from another CCS this is the internal + * + * If run comes from another CCS this is the internal * pc2 submission number for this run. */ public static final String INTERNAL_RUNID = "{:irunid}"; - + /** * CCS short problem name. */ public static final String PROBLEMSHORTNAME = "{:problemshort}"; + /** + * CCS problem letter. + */ + public static final String PROBLEMLETTER = "{:problemletter}"; + + /** + * CCS problem index (1 based). + */ + public static final String PROBLEMINDEX = "{:problem}"; + /** * Language name. */ public static final String LANGUAGENAME = "{:languagename}"; + /** + * Language letter. + */ + public static final String LANGUAGELETTER = "{:languageletter}"; + + /** + * CLICS Language id. + */ + public static final String LANGUAGEID = "{:languageid}"; + /** * Client Id. */ public static final String CLIENTID = "{:clientid}"; /** - * Team Id (client id). - * + * Client full name, eg team2 + */ + public static final String CLIENTNAME = "{:clientname}"; + + /** + * Client's site id. + */ + public static final String CLIENTSITE = "{:clientsite}"; + + /** + * Team Id. + * */ public static final String TEAMID = "{:teamid}"; @@ -98,7 +131,7 @@ public class CommandVariableReplacer { /** * Client type. - * + * */ public static final String CLIENTTYPE = "{:clienttype}"; @@ -106,11 +139,18 @@ public class CommandVariableReplacer { * Language number. */ public static final String LANGUAGE = "{:language}"; - + + /** + * Site + */ + public static final String SITEID = "{:siteid}"; + public static final String[] VARIABLE_NAMES = {// BASENAME, // CLIENTID, // CLIENTTYPE, // + CLIENTNAME, // + CLIENTSITE, ELAPSEDMINUTES, // ELAPSEDMS, // ELAPSEDSECONDS, // @@ -123,28 +163,28 @@ public class CommandVariableReplacer { PROBLEMSHORTNAME, // RUNID, // TEAMID, // + SITEID, // "{:ansfile}", // "{:executetime}", // "{:exitvalue}", // "{:infile}", // - "{:languageletter}", // + LANGUAGELETTER, // "{:outfile}", // "{:pc2home}", // - "{:problemletter}", // - "{:problem}", // - "{:siteid}", // + PROBLEMLETTER, // + PROBLEMINDEX, // "{:timelimit}", // "{:validator}", // }; - + /** * Replace all instances of beforeString with afterString. - * + * * If before string is not found, then returns original string. - * + * * @param origString * string to be modified * @param beforeString @@ -156,7 +196,7 @@ public class CommandVariableReplacer { public static String replaceString(String origString, String beforeString, String afterString) { // SOMEDAY replace this with Java String replace method. - + if (origString == null || afterString == null) { return origString; } @@ -179,9 +219,9 @@ public static String replaceString(String origString, String beforeString, Strin /** * Replace beforeString with int. - * + * * For details see {@link #replaceString(String, String, String)} - * + * * @param origString * string to be modified * @param beforeString @@ -197,9 +237,9 @@ protected String replaceString(String origString, String beforeString, int after /** * return string with all field variables filled with values. - * + * * Each variable will be filled in with values. - * + * *
      *             valid fields are:
      *              {:mainfile} - submitted file (hello.java)
@@ -214,7 +254,7 @@ protected String replaceString(String origString, String beforeString, int after
      *              {:ansfile}
      *              {:pc2home}
      * 
- * + * * @param run * submitted by team * @param origString @@ -223,7 +263,7 @@ protected String replaceString(String origString, String beforeString, int after * @param problemDataFiles * @return string with values */ - public String substituteExecutableVariables(IInternalContest contest, Run run, RunFiles runFiles, String origString, // + public String substituteExecutableVariables(IInternalContest contest, Run run, RunFiles runFiles, String origString, // ExecutionData executionData, ProblemDataFiles problemDataFiles) { String newString = ""; String nullArgument = "-"; /* this needs to change */ @@ -266,21 +306,21 @@ public String substituteExecutableVariables(IInternalContest contest, Run run, R Language language = contest.getLanguage(run.getLanguageId()); int index = getLanguageIndex(contest, language); if (index > 0) { - + newString = replaceString(newString, LANGUAGE, index); - newString = replaceString(newString, "{:languageletter}", Utilities.convertNumber(index)); + newString = replaceString(newString, LANGUAGELETTER, Utilities.convertNumber(index)); } } if (run.getProblemId() != null) { int index = getProblemIndex(contest, problem); if (index > 0) { - newString = replaceString(newString, "{:problem}", index); - newString = replaceString(newString, "{:problemletter}", Utilities.convertNumber(index)); + newString = replaceString(newString, PROBLEMINDEX, index); + newString = replaceString(newString, PROBLEMLETTER, Utilities.convertNumber(index)); } } if (run.getSubmitter() != null) { newString = replaceString(newString, TEAMID, run.getSubmitter().getClientNumber()); - newString = replaceString(newString, "{:siteid}", run.getSubmitter().getSiteNumber()); + newString = replaceString(newString, SITEID, run.getSubmitter().getSiteNumber()); } if (problem != null) { @@ -317,9 +357,9 @@ public String substituteExecutableVariables(IInternalContest contest, Run run, R /** * Return the problem index (starting at/base one)). - * + * * Does not count problems that are not active. - * + * * @param contest * @param inProblem * @return -1 if problem not found or inactive, else 1 or greater as rank for problem. @@ -340,7 +380,7 @@ public int getProblemIndex(IInternalContest contest, Problem inProblem) { /** * Return the language index (starting at base one). - * + * * @param contest * @param inLanguage * @return -2 if language not found or inactive, else 1 or greater rank for language. @@ -361,10 +401,10 @@ public int getLanguageIndex(IInternalContest contest, Language inLanguage) { /** * Return string minus last extension. - * + * * Finds last . (period) in input string, strips that period and all other characters after that last period. If no period is found in string, will return a copy of the original string.
* Unlike the Unix basename program, no extension is supplied. - * + * * @param original * the input string * @return a string with all text after last . removed @@ -403,12 +443,12 @@ public static String replaceInteger(String origString, String beforeString, int public static String replaceLong(String origString, String beforeString, long longValue) { return replaceString(origString, beforeString, Long.toString(longValue)); } - + /** * Create command after substituting various variables. - * + * * See substitution constants. {@value CommandVariableReplacer#OPTIONS} will substitute: - * + * *
      * -p <problem short-name>, string
      * -l <language name>, string
@@ -418,18 +458,18 @@ public static String replaceLong(String origString, String beforeString, long lo
      * -i <run id> unique key for the run, integer
      * -w <team password>, string
      * 
- * + * * @param command * @param run * @param runFiles * @param runDir - * @param contest + * @param contest * @return a file with value substituted for variables. * @throws Exception */ public String substituteVariables(String command, IInternalContest contest, Run run, RunFiles runFiles, String runDir, // ExecutionData executionData, ProblemDataFiles problemDataFiles) throws Exception { - + String mainfileName = getMainFileName(runDir, runFiles); String fileList = mainfileName; @@ -514,11 +554,11 @@ public String substituteVariables(String command, IInternalContest contest, Run return newCommand; } - + /** * Returns full path for main file. * @param runDir if null just returns mainfilename - * @param runFiles + * @param runFiles * @return name of main file, if runDir is not null returns rundir + FS + mainfilename. */ public String getMainFileName(String runDir, RunFiles runFiles) { @@ -529,5 +569,90 @@ public String getMainFileName(String runDir, RunFiles runFiles) { } } + /** + * Replaces substitute variables in the execute folder string. + * This needs a special version (and can't use substituteAllStrings) because some substitute + * variables do not make sense or are not available at the time the execute folder is needed, + * such as the test case, package, mainfile, infile, outfile, etc. + * + * return string with execute folder relevent field variables filled with values. + * + * Each variable will be filled in with values. + * + *
+     *             valid fields are:
+     *              {: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
+     *              {:runnumber} - the run number
+     * 
+ * + * @param inRun + * submitted by team + * @param origString + * - original string to be substituted. + * @return string with values + */ + public String substituteExecuteFolderVariables(IInternalContest contest, Log log, Run inRun, String origString) { + // Make a new copy to start with to avoid issues in the future. + String newString = origString; + + try { + if (inRun == null) { + throw new IllegalArgumentException("Run is null"); + } + + // SOMEDAY LanguageId and ProblemId are now a long string not an int, + // what should we do? + + if (inRun.getLanguageId() != null) { + Language language = contest.getLanguage(inRun.getLanguageId()); + int index = getLanguageIndex(contest, language); + if (index > 0) { + newString = replaceString(newString, LANGUAGE, index); + newString = replaceString(newString, LANGUAGELETTER, Utilities.convertNumber(index)); + newString = replaceString(newString, LANGUAGENAME, language.getDisplayName().toLowerCase().replaceAll(" ", "_")); + newString = replaceString(newString, LANGUAGEID, language.getID()); + } + } + if (inRun.getProblemId() != null) { + Problem problem = contest.getProblem(inRun.getProblemId()); + if(problem != null) { + int index = getProblemIndex(contest, problem); + newString = replaceString(newString, PROBLEMINDEX, index); + newString = replaceString(newString, PROBLEMLETTER, problem.getLetter()); + newString = replaceString(newString, PROBLEMSHORTNAME, problem.getShortName()); + } + } + if (inRun.getSubmitter() != null) { + 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()); + newString = replaceString(newString, RUNNUMBER, Integer.toString(inRun.getNumber())); + + } catch (Exception e) { + if(log == null) { + log = StaticLog.getLog(); + } + if(log != null) { + log.log(Log.CONFIG, "Exception substituting execute folder variables ", e); + } + // carrying on not required to save exception + } + + return newString; + } } 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..df4a2624c 100644 --- a/src/edu/csus/ecs/pc2/core/execute/Executable.java +++ b/src/edu/csus/ecs/pc2/core/execute/Executable.java @@ -27,6 +27,7 @@ import javax.swing.SwingUtilities; import edu.csus.ecs.pc2.VersionInfo; +import edu.csus.ecs.pc2.core.CommandVariableReplacer; import edu.csus.ecs.pc2.core.Constants; import edu.csus.ecs.pc2.core.IInternalController; import edu.csus.ecs.pc2.core.IniFile; @@ -81,6 +82,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 +177,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 @@ -274,6 +277,8 @@ public class Executable extends Plugin implements IExecutable, IExecutableNotify //setting this to True will override the prohibition on invoking a Sandbox when running on Windows. private boolean debugAllowInteractiveInvocationOnWindows = false; + // for execute folder, and maybe in the future, for the other substitute vars + CommandVariableReplacer variableReplacer = new CommandVariableReplacer(); public Executable(IInternalContest inContest, IInternalController inController, Run run, RunFiles runFiles, IExecutableMonitor msgFrame) { super(); @@ -323,6 +328,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 +338,6 @@ private void initialize() { } executeDirectoryName = getExecuteDirectoryName(); - log = controller.getLog(); - if (executorId.getClientType() != ClientType.Type.TEAM) { this.problemDataFiles = contest.getProblemDataFile(problem); } @@ -528,6 +534,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 +549,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 +620,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 @@ -2830,6 +2844,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); @@ -2988,16 +3003,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 +3030,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 @@ -3018,7 +3045,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 { @@ -3026,13 +3054,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; @@ -3059,9 +3089,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 +3102,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,18 +3116,20 @@ 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, "{:problemshort}", problem.getShortName()); - } } } if (inRun.getSubmitter() != null) { 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()); + newString = replaceString(newString, "{:runnumber}", Integer.toString(inRun.getNumber())); if (problem != null) { + newString = replaceString(newString, "{:problemletter}", problem.getLetter()); + newString = replaceString(newString, "{:problemshort}", problem.getShortName()); if (problem.getDataFileName() != null && !problem.getDataFileName().equals("")) { newString = replaceString(newString, "{:infile}", problem.getDataFileName()); } else { @@ -3111,9 +3146,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) { @@ -3174,6 +3207,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 +3411,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().getExecuteFolder(); + if(StringUtilities.isEmpty(dirName)) { + dirName = DEFAULT_EXECUTE_DIRECTORY_TEMPLATE; + } + dirName = variableReplacer.substituteExecuteFolderVariables(contest, log, run, dirName) + getExecuteDirectoryNameSuffix(); + return(dirName); } /** @@ -3632,34 +3671,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); } @@ -3700,4 +3743,57 @@ 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() + "'"); + 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; + } 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/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 0338325e3..d861a7712 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 executeFolder = ""; + + /** + * * @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 getExecuteFolder() { + if(executeFolder == null) { + executeFolder = ""; + } + return executeFolder ; + } + + /** + * judgesDefaultAnswer must be a non-zero length trimmed string. + * + * @param judgesDefaultAnswer The judgesDefaultAnswer to set. + */ + public void setExecuteFolder(String executeFolder) { + if (executeFolder != null && executeFolder.trim().length() > 0) { + this.executeFolder = executeFolder.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 (!executeFolder.equals(contestInformation.getExecuteFolder())) { + 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/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 e173da27b..192c0b31f 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.setExecuteFolder(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.getExecuteFolder()); 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,89 @@ 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\nThe recognized substitution variables include:" // + + "\n {:clientid} - this client's id number, eg. 1" + + "\n {:clientname} - this client's name, eg judge1" + + "\n {:clientsite} - this client's site" + + "\n {:languageid} - CLICS language id, eg cpp" + + "\n {:language} - index into languages (1 based)" + + "\n {:languageletter} - index converted to letter, eg 1=A, 2=B" + + "\n {:languagename} - Display name of language (spaces converted to _)" + + "\n {:problem} - Index into problem table" + + "\n {:problemletter} - A,B,C..." + + "\n {:problemshort} - problem short name" + + "\n {:runnumber} - the run number" + + "\n {:siteid} - team's site" + + "\n {:teamid} - team's id number" + + + "\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" // + + "\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 +1488,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 +1499,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes jCheckBoxShowPreliminaryOnNotifications - * + * * @return javax.swing.JCheckBox */ private JCheckBox getJCheckBoxShowPreliminaryOnNotifications() { @@ -1378,6 +1512,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 +1523,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes additionalRunStatusCheckBox - * + * * @return javax.swing.JCheckBox */ private JCheckBox getAdditionalRunStatusCheckBox() { @@ -1399,6 +1534,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 +1558,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 +1577,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 +1614,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { /** * This method initializes runSubmissionInterfaceCommandTextField - * + * * @return javax.swing.JTextField */ private JTextField getRunSubmissionInterfaceCommandTextField() { @@ -1484,6 +1623,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/src/edu/csus/ecs/pc2/ui/DevelopmentPane.java b/src/edu/csus/ecs/pc2/ui/DevelopmentPane.java index 6a698f7e6..c684bb118 100644 --- a/src/edu/csus/ecs/pc2/ui/DevelopmentPane.java +++ b/src/edu/csus/ecs/pc2/ui/DevelopmentPane.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.ui; import java.awt.BorderLayout; @@ -10,7 +10,7 @@ /** * Developer tools pane. - * + * * @author pc2@ecs.csus.edu */ public class DevelopmentPane extends JPanePlugin { @@ -18,7 +18,7 @@ public class DevelopmentPane extends JPanePlugin { private static final long serialVersionUID = -6902750741500529883L; private PluginPane pluginPane = new PluginPane(); - + private ContestScheduledStartClockPane contestScheduledStartClockPane = new ContestScheduledStartClockPane(); private ReportPane reportPane = new ReportPane(); @@ -26,15 +26,15 @@ public class DevelopmentPane extends JPanePlugin { private OptionsPane optionsPane = new OptionsPane(); private LogSettingsPane logSettingsPane = new LogSettingsPane(); - + private ContestPreloadPane contestPreloadPane = new ContestPreloadPane(); private ContestClockAllPane contestClockAllPane = new ContestClockAllPane(); - - private SubmitSubmissionsPane submitSubmissionsPane = new SubmitSubmissionsPane(); - + + private SubmitSampleRunsPane submitSampleRunsPane = new SubmitSampleRunsPane(); + private DisplayPermissionsPane displayPermissionsPane = new DisplayPermissionsPane(); - + public DevelopmentPane() { super(); setLayout(new BorderLayout(0, 0)); @@ -43,23 +43,23 @@ public DevelopmentPane() { add(tabbedPane); tabbedPane.addTab("Plugins", null, pluginPane, null); - - tabbedPane.addTab("Submitter", null, submitSubmissionsPane, null); - + + tabbedPane.addTab("Submit Judge's Submissions", null, submitSampleRunsPane, null); + tabbedPane.addTab("Scheduled Start Countdown", null, contestScheduledStartClockPane, null); - + tabbedPane.addTab("All Countdown Timers", null, contestClockAllPane, null); - + tabbedPane.addTab("Reports", null, reportPane, null); - + tabbedPane.addTab("Sample Contests", null, contestPreloadPane, null); tabbedPane.addTab("Log Settings", null, logSettingsPane, null); tabbedPane.addTab("Options", null, optionsPane, null); - + tabbedPane.addTab("Check Permissions", null, displayPermissionsPane, null); - + } @Override @@ -77,9 +77,9 @@ public void setContestAndController(IInternalContest inContest, IInternalControl contestPreloadPane.setContestAndController(inContest, inController); contestScheduledStartClockPane.setContestAndController(inContest, inController); contestClockAllPane.setContestAndController(inContest, inController); - submitSubmissionsPane.setContestAndController(inContest, inController); + submitSampleRunsPane.setContestAndController(inContest, inController); displayPermissionsPane.setContestAndController(inContest, inController); } - + } // @jve:decl-index=0:visual-constraint="10,10" diff --git a/src/edu/csus/ecs/pc2/ui/DisplayPermissionsPane.java b/src/edu/csus/ecs/pc2/ui/DisplayPermissionsPane.java index 2da0d4e52..96a4fb56b 100644 --- a/src/edu/csus/ecs/pc2/ui/DisplayPermissionsPane.java +++ b/src/edu/csus/ecs/pc2/ui/DisplayPermissionsPane.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.ui; import java.awt.BorderLayout; @@ -14,13 +14,14 @@ import edu.csus.ecs.pc2.core.IInternalController; import edu.csus.ecs.pc2.core.model.Account; import edu.csus.ecs.pc2.core.model.ClientId; +import edu.csus.ecs.pc2.core.model.ClientType; import edu.csus.ecs.pc2.core.model.IInternalContest; import edu.csus.ecs.pc2.core.security.Permission; /** * A UI that provides a button to display the PC2 {@link Permission}s associated with the currently logged-in account. - * - * + * + * * @author John Clevenger, PC^2 Development Team (pc2@ecs.csus.edu). */ public class DisplayPermissionsPane extends JPanePlugin { @@ -54,35 +55,42 @@ private void displayPermissions() { //find out who I am ClientId clientId = getContest().getClientId(); - + //get my account Account myAccount = getContest().getAccount(clientId); - - //get the list of permissions granted to my account - Permission.Type[] myPermissions = myAccount.getPermissionList().getList(); - - //construct a String listing each permission - String msg = "Permissions (" + myPermissions.length + "): \n"; - - if (myPermissions.length==0) { - msg += " \n"; + + String msg; + + // Since the server does not have an Account, we make a special check here to avoid a null + // reference. + if(myAccount == null && clientId.getClientType() == ClientType.Type.SERVER) { + msg = "The contest server has any permission it needs."; } else { + //get the list of permissions granted to my account + Permission.Type[] myPermissions = myAccount.getPermissionList().getList(); - ArrayList perms = new ArrayList(); - - for (Permission.Type permission : myPermissions) { - perms.add(permission.toString()); - } - perms.sort(null); - - for (String perm : perms) { - msg += " " + perm + "\n"; + //construct a String listing each permission + msg = "Permissions (" + myPermissions.length + "): \n"; + + if (myPermissions.length==0) { + msg += " \n"; + } else { + + ArrayList perms = new ArrayList(); + + for (Permission.Type permission : myPermissions) { + perms.add(permission.toString()); + } + perms.sort(null); + + for (String perm : perms) { + msg += " " + perm + "\n"; + } } } - //display the permision list JOptionPane.showMessageDialog(this, msg, "Current Permissions", JOptionPane.INFORMATION_MESSAGE); - + } @Override diff --git a/src/edu/csus/ecs/pc2/ui/PluginLoadPane.java b/src/edu/csus/ecs/pc2/ui/PluginLoadPane.java index 152656f0b..41db05f1a 100644 --- a/src/edu/csus/ecs/pc2/ui/PluginLoadPane.java +++ b/src/edu/csus/ecs/pc2/ui/PluginLoadPane.java @@ -18,14 +18,14 @@ /** * Pane to allow user to choose and open/start plugin window or pane. - * + * * @author pc2@ecs.csus.edu */ public class PluginLoadPane extends JPanePlugin { /** - * + * */ private static final long serialVersionUID = -1303011559658754807L; @@ -47,7 +47,7 @@ public void setParentTabbedPane(JTabbedPane parentTabbedPane) { /** * This method initializes - * + * */ public PluginLoadPane() { super(); @@ -56,7 +56,7 @@ public PluginLoadPane() { /** * This method initializes this - * + * */ private void initialize() { this.setLayout(null); @@ -74,7 +74,7 @@ public String getPluginTitle() { /** * This method initializes pluginComboBox - * + * * @return javax.swing.JComboBox */ private JComboBox getPluginComboBox() { @@ -87,7 +87,7 @@ private JComboBox getPluginComboBox() { /** * This method initializes openNewPluginButton - * + * * @return javax.swing.JButton */ private JButton getOpenNewPluginButton() { @@ -96,6 +96,7 @@ private JButton getOpenNewPluginButton() { openNewPluginButton.setBounds(new Rectangle(41, 147, 134, 36)); openNewPluginButton.setText("Add Tab"); openNewPluginButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { addNewTab(); } @@ -119,7 +120,7 @@ protected void addNewTab() { /** * This method initializes addPluginInNewWindow - * + * * @return javax.swing.JButton */ private JButton getAddPluginInNewWindow() { @@ -128,6 +129,7 @@ private JButton getAddPluginInNewWindow() { addPluginInNewWindow.setBounds(new Rectangle(317, 147, 182, 36)); addPluginInNewWindow.setText("Open in new Window"); addPluginInNewWindow.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { addNewPluginWindow(); } @@ -144,7 +146,7 @@ protected void addNewPluginWindow() { } /** - * + * * @author pc2@ecs.csus.edu * @version $Id$ */ @@ -177,7 +179,7 @@ public void setContestAndController(IInternalContest inContest, IInternalControl private JPanePlugin[] getPluginList() { Vector plugins = new Vector(); - + plugins.add(new AccountsPane()); plugins.add(new AccountsTablePane()); plugins.add(new AutoJudgesPane()); @@ -187,6 +189,7 @@ private JPanePlugin[] getPluginList() { plugins.add(new ConnectionsPane()); plugins.add(new ConnectionsTablePane()); plugins.add(new ContestClockPane()); + plugins.add(new ContestInformationPane()); plugins.add(new ContestScheduledStartClockPane()); plugins.add(new ContestTimesPane()); plugins.add(new EventFeedServerPane()); @@ -216,10 +219,10 @@ private JPanePlugin[] getPluginList() { plugins.add(new ViewPropertiesPane()); plugins.add(new NSAStandingsPane()); plugins.add(new QuickJudgePane()); - + plugins.add(new ResultsComparePane()); - JPanePlugin[] pluginList = (JPanePlugin[]) plugins.toArray(new JPanePlugin[plugins.size()]); + JPanePlugin[] pluginList = plugins.toArray(new JPanePlugin[plugins.size()]); Arrays.sort(pluginList, new JPluginPaneNameComparator()); @@ -229,6 +232,7 @@ private JPanePlugin[] getPluginList() { private void loadPluginList() { SwingUtilities.invokeLater(new Runnable() { + @Override public void run() { pluginComboBox.removeAllItems(); for (JPanePlugin plugin : getPluginList()) { diff --git a/src/edu/csus/ecs/pc2/ui/PluginPane.java b/src/edu/csus/ecs/pc2/ui/PluginPane.java index e8beda913..6ca9c590f 100644 --- a/src/edu/csus/ecs/pc2/ui/PluginPane.java +++ b/src/edu/csus/ecs/pc2/ui/PluginPane.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.ui; import java.awt.BorderLayout; @@ -14,7 +14,7 @@ /** * Pane for Plugin Frame (tabbed panes). - * + * * @author pc2@ecs.csus.edu * @version $Id$ */ @@ -23,7 +23,7 @@ public class PluginPane extends JPanePlugin { /** - * + * */ private static final long serialVersionUID = 4327701084703859112L; @@ -35,7 +35,7 @@ public class PluginPane extends JPanePlugin { /** * This method initializes - * + * */ public PluginPane() { super(); @@ -44,7 +44,7 @@ public PluginPane() { /** * This method initializes this - * + * */ private void initialize() { this.setLayout(new BorderLayout()); @@ -56,13 +56,15 @@ private void initialize() { } + @Override public String getPluginTitle() { return "Plugin View"; } + @Override public void setContestAndController(IInternalContest inContest, IInternalController inController) { super.setContestAndController(inContest, inController); - + PluginLoadPane pluginLoadPane = new PluginLoadPane(); pluginLoadPane.setParentFrame(getParentFrame()); pluginLoadPane.setParentTabbedPane(getPluginTabbedPane()); @@ -72,7 +74,7 @@ public void setContestAndController(IInternalContest inContest, IInternalControl /** * This method initializes pluginTabbedPane - * + * * @return javax.swing.JTabbedPane */ private JTabbedPane getPluginTabbedPane() { @@ -84,13 +86,13 @@ private JTabbedPane getPluginTabbedPane() { /** * This method initializes infoPane - * + * * @return javax.swing.JPanel */ private JPanel getInfoPane() { if (infoPane == null) { infoLabel = new JLabel(); - infoLabel.setText("JLabel"); + infoLabel.setText("Dynamically Load Plugins"); infoLabel.setHorizontalAlignment(SwingConstants.CENTER); infoLabel.setHorizontalTextPosition(SwingConstants.CENTER); infoPane = new JPanel(); 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); + } } } } diff --git a/src/edu/csus/ecs/pc2/ui/ViewPropertiesPane.java b/src/edu/csus/ecs/pc2/ui/ViewPropertiesPane.java index e90410cb4..d512b80d7 100644 --- a/src/edu/csus/ecs/pc2/ui/ViewPropertiesPane.java +++ b/src/edu/csus/ecs/pc2/ui/ViewPropertiesPane.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.ui; import java.awt.BorderLayout; @@ -25,7 +25,7 @@ /** * View this clients properties. - * + * * @author pc2@ecs.csus.edu * @version $Id$ */ @@ -34,7 +34,7 @@ public class ViewPropertiesPane extends JPanePlugin { /** - * + * */ private static final long serialVersionUID = 2554693130978453347L; @@ -54,9 +54,12 @@ public class ViewPropertiesPane extends JPanePlugin { private JButton closeButton = null; + private JPanel permissionSummaryPane = null; + + /** * This method initializes - * + * */ public ViewPropertiesPane() { super(); @@ -65,11 +68,12 @@ public ViewPropertiesPane() { /** * This method initializes this - * + * */ private void initialize() { this.setLayout(new BorderLayout()); this.setSize(new Dimension(396, 208)); + this.add(getPermissionSummaryPane(), BorderLayout.NORTH); this.add(getPermissionsScrollPane(), BorderLayout.CENTER); this.add(getButtonPane(), BorderLayout.SOUTH); @@ -82,7 +86,7 @@ public String getPluginTitle() { /** * This method initializes permissionsJList - * + * * @return javax.swing.JList */ private JCheckBoxJList getPermissionsJList() { @@ -91,6 +95,7 @@ private JCheckBoxJList getPermissionsJList() { permissionsJList.setModel(defaultListModel); // ListSelectionListeners are called before JCheckBoxes get updated permissionsJList.addPropertyChangeListener("change", new PropertyChangeListener() { + @Override public void propertyChange(PropertyChangeEvent evt) { showPermissionCount(permissionsJList.getSelectedIndices().length + " permissions selected"); } @@ -104,7 +109,7 @@ public void setContestAndController(IInternalContest inContest, IInternalControl super.setContestAndController(inContest, inController); account = inContest.getAccount(inContest.getClientId()); populatePermissions(account); - + if (getParentFrame() != null){ getParentFrame().setTitle("Permissions/Abilities for " + inContest.getClientId()); } @@ -174,15 +179,16 @@ private String[] getPermissionDescriptions() { public void showPermissionCount(final String message) { SwingUtilities.invokeLater(new Runnable() { + @Override public void run() { - permissionCountLabel.setText(message); + getPermissionCountLabel().setText(message); } }); } /** * This method initializes permissionsScrollPane - * + * * @return javax.swing.JScrollPane */ private JScrollPane getPermissionsScrollPane() { @@ -195,7 +201,7 @@ private JScrollPane getPermissionsScrollPane() { /** * This method initializes buttonPane - * + * * @return javax.swing.JPanel */ private JPanel getButtonPane() { @@ -203,14 +209,17 @@ private JPanel getButtonPane() { buttonPane = new JPanel(); buttonPane.setLayout(new FlowLayout()); buttonPane.setPreferredSize(new Dimension(35, 35)); - buttonPane.add(getCloseButton(), null); + // do not add close button if in a tabbed control + if(getParentFrame() != null) { + buttonPane.add(getCloseButton(), null); + } } return buttonPane; } /** * This method initializes closeButton - * + * * @return javax.swing.JButton */ private JButton getCloseButton() { @@ -218,6 +227,7 @@ private JButton getCloseButton() { closeButton = new JButton(); closeButton.setText("Close"); closeButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { closeWindow(); } @@ -232,4 +242,27 @@ protected void closeWindow() { } } + /** + * The summary pane is used to show things like how many permissions are selected. + * it appears at the top of the frame + * + * @return the pane containing the summary info + */ + private JPanel getPermissionSummaryPane() { + if(permissionSummaryPane == null) { + permissionSummaryPane = new JPanel(); + permissionSummaryPane.setLayout(new FlowLayout()); + permissionSummaryPane.setPreferredSize(new Dimension(25, 25)); + permissionSummaryPane.add(getPermissionCountLabel(), null); + } + return(permissionSummaryPane); + } + + private JLabel getPermissionCountLabel() { + if(permissionCountLabel == null) { + permissionCountLabel = new JLabel("No permissions selected"); + } + return(permissionCountLabel); + } + } // @jve:decl-index=0:visual-constraint="10,10" diff --git a/src/edu/csus/ecs/pc2/ui/admin/AdministratorView.java b/src/edu/csus/ecs/pc2/ui/admin/AdministratorView.java index b5865b047..619c9814f 100644 --- a/src/edu/csus/ecs/pc2/ui/admin/AdministratorView.java +++ b/src/edu/csus/ecs/pc2/ui/admin/AdministratorView.java @@ -23,6 +23,7 @@ import edu.csus.ecs.pc2.VersionInfo; import edu.csus.ecs.pc2.core.IInternalController; import edu.csus.ecs.pc2.core.IniFile; +import edu.csus.ecs.pc2.core.StringUtilities; import edu.csus.ecs.pc2.core.Utilities; import edu.csus.ecs.pc2.core.log.Log; import edu.csus.ecs.pc2.core.log.StaticLog; @@ -84,6 +85,8 @@ public class AdministratorView extends JFrame implements UIPlugin, ChangeListene private static final long serialVersionUID = 1L; + private static final String SAMPLE_SUBMIT_PANE_KEY = "admin.sampleSubmitPane"; + private IInternalContest contest; private IInternalController controller; // @jve:decl-index=0: @@ -333,8 +336,10 @@ public void run() { QuickJudgePane quickJudgePane = new QuickJudgePane(); addUIPlugin(getRunContestTabbedPane(), "Judging Utilities", quickJudgePane); - SubmitSampleRunsPane sampleRunsPane = new SubmitSampleRunsPane(); - addUIPlugin(getRunContestTabbedPane(), "Submit Samples", sampleRunsPane); + if(StringUtilities.getBooleanValue(IniFile.getValue(SAMPLE_SUBMIT_PANE_KEY), false)) { + SubmitSampleRunsPane sampleRunsPane = new SubmitSampleRunsPane(); + addUIPlugin(getRunContestTabbedPane(), "Submit Samples", sampleRunsPane); + } if (!controller.isSuppressLoginsPaneDisplay()) { LoginsTablePane loginsTablePane = new LoginsTablePane(); diff --git a/src/edu/csus/ecs/pc2/ui/judge/JudgeView.java b/src/edu/csus/ecs/pc2/ui/judge/JudgeView.java index 62b4a86f7..d64a3ba53 100644 --- a/src/edu/csus/ecs/pc2/ui/judge/JudgeView.java +++ b/src/edu/csus/ecs/pc2/ui/judge/JudgeView.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.ui.judge; import java.awt.BorderLayout; @@ -15,6 +15,7 @@ import edu.csus.ecs.pc2.VersionInfo; import edu.csus.ecs.pc2.core.IInternalController; import edu.csus.ecs.pc2.core.IniFile; +import edu.csus.ecs.pc2.core.StringUtilities; import edu.csus.ecs.pc2.core.Utilities; import edu.csus.ecs.pc2.core.exception.MultipleIssuesException; import edu.csus.ecs.pc2.core.log.Log; @@ -39,20 +40,23 @@ import edu.csus.ecs.pc2.ui.SubmissionBiffPane; import edu.csus.ecs.pc2.ui.SubmitClarificationPane; import edu.csus.ecs.pc2.ui.SubmitRunPane; +import edu.csus.ecs.pc2.ui.SubmitSampleRunsPane; import edu.csus.ecs.pc2.ui.UIPlugin; /** * Judge GUI. - * + * * @author pc2@ecs.csus.edu */ public class JudgeView extends JFrame implements UIPlugin { /** - * + * */ private static final long serialVersionUID = 5365837218548110171L; + private static final String SAMPLE_SUBMIT_PANE_KEY = "judge.sampleSubmitPane"; + private IInternalContest contest; private IInternalController controller; @@ -97,6 +101,7 @@ private void initialize() { FrameUtilities.centerFrame(this); this.addWindowListener(new java.awt.event.WindowAdapter() { + @Override public void windowClosing(java.awt.event.WindowEvent e) { promptAndExit(); } @@ -105,7 +110,7 @@ public void windowClosing(java.awt.event.WindowEvent e) { } private void overRideLookAndFeel(){ - // TODO eventually move this method to on location + // TODO eventually move this method to on location String value = IniFile.getValue("client.plaf"); if (value != null && value.equalsIgnoreCase("java")){ FrameUtilities.setJavaLookAndFeel(); @@ -124,7 +129,7 @@ protected void promptAndExit() { /** * This method initializes mainTabbedPane - * + * * @return javax.swing.JTabbedPane */ private JTabbedPane getMainTabbedPane() { @@ -149,8 +154,9 @@ protected JudgeView getThisFrame() { private void setFrameTitle(final boolean contestStarted) { final Frame thisFrame = this; SwingUtilities.invokeLater(new Runnable() { + @Override public void run() { - + FrameUtilities.setFrameTitle(thisFrame, contest.getTitle(), contestStarted, new VersionInfo()); if (contestStarted) { contestClockDisplay.fireClockStateChange(contest.getContestTime()); @@ -166,17 +172,19 @@ public void run() { FrameUtilities.regularCursor(this); } - + + @Override public void setContestAndController(IInternalContest inContest, IInternalController inController) { this.contest = inContest; this.controller = inController; SwingUtilities.invokeLater(new Runnable() { + @Override public void run() { - + controller.startLogWindow(contest); contest.addContestTimeListener(new ContestTimeListenerImplementation()); @@ -200,16 +208,21 @@ public void run() { ClarificationsTablePane clarificationsTablePane = new ClarificationsTablePane(); addUIPlugin(getMainTabbedPane(), "All clarifications", clarificationsTablePane); - + SubmitRunPane submitRunPane = new SubmitRunPane(); addUIPlugin(getMainTabbedPane(), "Test Run", submitRunPane); - + SubmitClarificationPane submitClarificationPane = new SubmitClarificationPane(); addUIPlugin(getMainTabbedPane(), "Generate Clarification", submitClarificationPane); OptionsPane optionsPanel = new OptionsPane(); addUIPlugin(getMainTabbedPane(), "Options", optionsPanel); - + + if(StringUtilities.getBooleanValue(IniFile.getValue(SAMPLE_SUBMIT_PANE_KEY), false)) { + SubmitSampleRunsPane sampleRunsPane = new SubmitSampleRunsPane(); + addUIPlugin(getMainTabbedPane(), "Submit Samples", sampleRunsPane); + } + ImBoredPane boredPane = new ImBoredPane(); addUIPlugin(getMainTabbedPane(), "I\'m Bored", boredPane); @@ -217,7 +230,7 @@ public void run() { contestClockDisplay = new ContestClockDisplay(controller.getLog(), contest.getContestTime(), contest.getSiteNumber(), true, null); contestClockDisplay.addLabeltoUpdateList(clockLabel, DisplayTimes.REMAINING_TIME, contest.getSiteNumber()); controller.register(contestClockDisplay); - + if (Utilities.isDebugMode()) { try { @@ -266,14 +279,14 @@ public void run() { JOptionPane.showMessageDialog(getParent(), message, "Cannot perform Judging", JOptionPane.ERROR_MESSAGE); System.exit(1); } - + SubmissionBiffPane submissionBiffPane = new SubmissionBiffPane(); getJudgeBiffPane().add(submissionBiffPane, java.awt.BorderLayout.CENTER); submissionBiffPane.setContestAndController(contest, controller); controller.register(submissionBiffPane); - - setVisible(true); - //the following was fixed under Bug 800 + + setVisible(true); + //the following was fixed under Bug 800 // //TODO This needs to be resolved. The submitClarifcaitonPane is bleeding through the other tabs // getMainTabbedPane().setSelectedComponent(submitClarificationPane); // getMainTabbedPane().doLayout(); @@ -282,7 +295,7 @@ public void run() { // getMainTabbedPane().setSelectedComponent(newRunsPane); if (Utilities.isDebugMode()){ - + try { PacketMonitorPane pane = new PacketMonitorPane(); addUIPlugin(getMainTabbedPane(), "Packets", pane); @@ -290,7 +303,7 @@ public void run() { logException(e); } } - + AboutPane aboutPane = new AboutPane(); addUIPlugin(getMainTabbedPane(), "About", aboutPane); @@ -304,12 +317,13 @@ public void run() { } catch (Exception e) { logException(e); } - + } }); } + @Override public String getPluginTitle() { return "Judge Main GUI"; } @@ -320,7 +334,7 @@ protected void showLog(boolean showLogWindow) { /** * This method initializes messagePane - * + * * @return javax.swing.JPanel */ private JPanel getMessagePane() { @@ -339,7 +353,7 @@ private JPanel getMessagePane() { /** * This method initializes centerPane - * + * * @return javax.swing.JPanel */ private JPanel getCenterPane() { @@ -353,7 +367,7 @@ private JPanel getCenterPane() { /** * This method initializes mainPane - * + * * @return javax.swing.JPanel */ private JPanel getMainPane() { @@ -368,7 +382,7 @@ private JPanel getMainPane() { /** * This method initializes exitPane - * + * * @return javax.swing.JPanel */ private JPanel getExitPane() { @@ -381,7 +395,7 @@ private JPanel getExitPane() { /** * This method initializes exitButton - * + * * @return javax.swing.JButton */ private JButton getExitButton() { @@ -390,6 +404,7 @@ private JButton getExitButton() { exitButton.setText("Exit"); exitButton.setToolTipText("Click here to Shutdown PC^2"); exitButton.addActionListener(new java.awt.event.ActionListener() { + @Override public void actionPerformed(java.awt.event.ActionEvent e) { promptAndExit(); } @@ -401,6 +416,7 @@ public void actionPerformed(java.awt.event.ActionEvent e) { private void showMessage(final String string) { SwingUtilities.invokeLater(new Runnable() { + @Override public void run() { messageLabel.setText(string); messageLabel.setToolTipText(string); @@ -414,21 +430,24 @@ protected boolean isThisSite(int siteNumber) { } /** - * + * * @author pc2@ecs.csus.edu * @version $Id$ */ class ContestTimeListenerImplementation implements IContestTimeListener { + @Override public void contestTimeAdded(ContestTimeEvent event) { contestTimeChanged(event); } + @Override public void contestTimeRemoved(ContestTimeEvent event) { contestTimeChanged(event); } + @Override public void contestTimeChanged(ContestTimeEvent event) { ContestTime contestTime = event.getContestTime(); if (isThisSite(contestTime.getSiteNumber())) { @@ -436,14 +455,17 @@ public void contestTimeChanged(ContestTimeEvent event) { } } + @Override public void contestStarted(ContestTimeEvent event) { contestTimeChanged(event); } + @Override public void contestStopped(ContestTimeEvent event) { contestTimeChanged(event); } + @Override public void refreshAll(ContestTimeEvent event) { contestTimeChanged(event); } @@ -461,7 +483,7 @@ public void contestAutoStarted(ContestTimeEvent event) { /** * This method initializes northPane - * + * * @return javax.swing.JPanel */ private JPanel getNorthPane() { @@ -477,7 +499,7 @@ private JPanel getNorthPane() { /** * This method initializes judgeBiffPane - * + * * @return javax.swing.JPanel */ private JPanel getJudgeBiffPane() { @@ -498,7 +520,7 @@ public static void setAlreadyJudgingRun(boolean alreadyJudgingRun) { // if anybody was waiting for us to not be judging, wake 1 of them now if (!alreadyJudgingRun) { /** - * notify is called outside of a synchronized block: + * notify is called outside of a synchronized block: * Exception in thread "AWT-EventQueue-0" java.lang.IllegalMonitorStateException: current thread not owner */ synchronized (JudgeView.getAlreadyJudgingRun()) { @@ -513,7 +535,7 @@ public static Boolean getAlreadyJudgingRun() { /** * This method initializes clockPane - * + * * @return javax.swing.JPanel */ private JPanel getClockPane() { @@ -542,7 +564,7 @@ private void logException(Exception e) { e.printStackTrace(System.err); } } - + } // @jve:decl-index=0:visual-constraint="10,10" diff --git a/support/judge_webcgi/Correct.png b/support/judge_webcgi/Correct.png new file mode 100644 index 000000000..6e73ae887 Binary files /dev/null and b/support/judge_webcgi/Correct.png differ diff --git a/support/judge_webcgi/Warning.png b/support/judge_webcgi/Warning.png new file mode 100644 index 000000000..e33a821d9 Binary files /dev/null and b/support/judge_webcgi/Warning.png differ diff --git a/support/judge_webcgi/Wrong.png b/support/judge_webcgi/Wrong.png new file mode 100644 index 000000000..43401f44b Binary files /dev/null and b/support/judge_webcgi/Wrong.png differ diff --git a/support/judge_webcgi/apache2_site/README b/support/judge_webcgi/apache2_site/README new file mode 100644 index 000000000..61ec61a16 --- /dev/null +++ b/support/judge_webcgi/apache2_site/README @@ -0,0 +1,23 @@ +John Buck, August 12, 2024 + +Before starting, make sure that the /home/icpc/html folder is setup with the proper permissions; +that is, /home/icpc/html must be owned by icpc.www-data and mode 775 (apache has to be able to +write to this folder to create symlinks). +The rest of the folders/files under /home/icpc/html just have to be readable. + +To setup Apache2 to service the judgments: + +1) Install apache2: apt-get install apache2 +2) Enable the CGI module: a2enmod cgi +3) Copy the judge.conf in this folder to: /etc/apache2/sites-available +4) Enable the site: a2ensite judge +5) You may want to disable the default site: a2dissite 000-default +6) Restart apache: apachectl restart +7) Create the /home/icpc/html/.htpasswd file with any user(s) you want to have access to the page: + (as root): + htpasswd -c /home/icpc/html/.htpasswd pc2 pc2password + This would only allow user "pc2" with password: "pc2password" to access the: + http:///cgi-bin/alljudgments.py page. + + + diff --git a/support/judge_webcgi/apache2_site/judge.conf b/support/judge_webcgi/apache2_site/judge.conf new file mode 100644 index 000000000..6f2568221 --- /dev/null +++ b/support/judge_webcgi/apache2_site/judge.conf @@ -0,0 +1,60 @@ + + # The ServerName directive sets the request scheme, hostname and port that + # the server uses to identify itself. This is used when creating + # redirection URLs. In the context of virtual hosts, the ServerName + # specifies what hostname must appear in the request's Host: header to + # match this virtual host. For the default virtual host (this file) this + # value is not decisive as it is used as a last resort host regardless. + # However, you must set it for any further virtual host explicitly. + #ServerName www.example.com + + ServerAdmin icpc@localhost + DocumentRoot /home/icpc/html + + # Available loglevels: trace8, ..., trace1, debug, info, notice, warn, + # error, crit, alert, emerg. + # It is also possible to configure the loglevel for particular + # modules, e.g. + #LogLevel info ssl:warn + + ErrorLog ${APACHE_LOG_DIR}/icpc_error.log + CustomLog ${APACHE_LOG_DIR}/icpc_access.log combined + + # For most configuration files from conf-available/, which are + # enabled or disabled at a global level, it is possible to + # include a line for only one particular virtual host. For example the + # following line enables the CGI configuration for this host only + # after it has been globally disabled with "a2disconf". + #Include conf-available/serve-cgi-bin.conf + ScriptAlias /cgi-bin/ /home/icpc/html/cgi-bin/ + + Options +ExecCGI +FollowSymlinks + SetHandler cgi-script + AcceptPathInfo Default + Satisfy Any + # Main page requires login + + Order Allow,Deny + AllowOverride AuthConfig + Allow from all + AuthName "PC2 Judging Results" + AuthUserFile "/home/icpc/html/.htpasswd" + AuthType Basic + Require valid-user + +# AllowOverride all +# Order Allow,Deny +# Require all granted +## Require ip 71.125.58.28 +# Allow from all + + + + Options FollowSymLinks + AllowOverride None + Require all granted + + + + +# vim: syntax=apache ts=4 sw=4 sts=4 sr noet 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..86a0a2fb1 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/alljudgments.py @@ -0,0 +1,109 @@ +#!/usr/bin/python3 +import json +import urllib.request +import operator +import subprocess + +import HtmlFunctions + +# For looking up all auto-judges +HOSTS_FILE="/etc/hosts" +JUDGE_PREFIX="pc2-aj" + +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" + +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 : + return(json.load(url)) + except Exception as err : + if TESTING : + print(f"getJudgmentsFromHost: Exception: {err}") + return None + +def addDataToTable(data) : + 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(' ') + +HtmlFunctions.Preamble(None) +HtmlFunctions.Styles(JUDGE_STYLES) +MyTableStyles() +HtmlFunctions.StartHTMLDoc() +HtmlFunctions.Header() + +alldata=[] + +procJudges = subprocess.Popen("egrep '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}[[:space:]]" + JUDGE_PREFIX + "[0-9][0-9]*' " + HOSTS_FILE, + shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) +for hostent in procJudges.stdout.readlines() : + hostinfo = hostent.decode().split(); + # we only accept lines that have: IP hostname + if len(hostinfo) == 2 : + # get the judge hostname, eg. pc2-aj1 + jh = hostinfo[1] + newData = getJudgmentsFromHost(jh) + if newData != None : + alldata = alldata + newData + +if TESTING : + retval = p.wait() + print(f"egrep for {JUDGE_PREFIX} in {HOSTS_FILE} Return code {retval}") + +HtmlFunctions.StartTable() +TableHeader() +alldata.sort(key = lambda n: int(n['runid']), reverse=True) +addDataToTable(alldata) + +HtmlFunctions.EndTable() +HtmlFunctions.Scripts() +HtmlFunctions.Trailer() + 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/getjudgments.sh b/support/judge_webcgi/cgi-bin/getjudgments.sh new file mode 100644 index 000000000..62b813075 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/getjudgments.sh @@ -0,0 +1,155 @@ +#!/bin/bash + +# Set to 1 to search through all test cases looking for first failure, otherwise +# it will use the last one. In practice, it makes little difference in terms of +# how long it takes, so might as well set it to 1 +USE_ALL_TESTCASES=1 + +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + +CACHEDIR=${JUDGE_HOME}/html/cache +LASTFILE=${CACHEDIR}/lastfile + +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` + +# Create cache dir, if it does not exist +mkdir -p ${CACHEDIR} +# Create last update time file if it does not exist +if test ! -s ${LASTFILE} +then + lastdate="2024-01-01 00:00:01" +else + lastdate=`cat ${LASTFILE}` +fi +newdate="${lastdate}" + +#echo $LASTFILE contains date $lastdate 1>&2 + +for exedir in `find ${PC2_RUN_DIR} -name 'ex_[0-9]*_[A-Z]_[a-z][a-z0-9]*_[0-9]*_[a-z0-9]*_[a-z][a-z0-9]*' -a -newermt "${lastdate}" -a -type d` +do + # Strip leading PC2_RUN_DIR + exdir=${exedir##*/} + # 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 + # Update last update time if newer + filetime=`stat --format %y $exedir` + if [[ -z "$newdate" || "${filetime}" > "${newdate}" ]] + then + newdate="${filetime}" + fi + cachefile=${CACHEDIR}/$exdir + runid=$2 + problet=$3 + probshort=$4 + teamnum=$5 + langid=$6 + judge=$7 + if test -z "${judge}" + then + judge="N/A" + fi + if test ${USE_ALL_TESTCASES} -eq 1 + then + GetJudgment "${exedir}" + else + GetLastJudgment "${exedir}" + fi + 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} + TableRow "$exedir" $runid $problet $probshort $langid $teamnum "$judgment" "$runtime" "$testcaseinfo" "$judge" > $cachefile + fi +done +if [[ -z "$lastdate" || "${newdate}" > "${lastdate}" ]] +then + echo "${newdate}" > ${LASTFILE} +# echo Updated $LASTFILE with $newdate 1>&2 +fi + +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 ${CACHEDIR} | grep -P '^ex_\d+_[A-Z]_[a-z\-\d]+_\d+_[a-z\d]+' | sort --field-separator=_ +1rn` +do + if test -n "$sep" + then + echo " $sep" + else + sep="," + fi + cat ${CACHEDIR}/$exdir +done +echo ']' +exit 0 diff --git a/support/judge_webcgi/cgi-bin/getjudgments_nocache.sh b/support/judge_webcgi/cgi-bin/getjudgments_nocache.sh new file mode 100644 index 000000000..d9c0bdb17 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/getjudgments_nocache.sh @@ -0,0 +1,120 @@ +#!/bin/bash + +# Set to 1 to search through all test cases looking for first failure, otherwise +# it will use the last one. In practice, it makes little difference in terms of +# how long it takes, so might as well set it to 1 +USE_ALL_TESTCASES=1 + +. ./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 + if test ${USE_ALL_TESTCASES} -eq 1 + then + GetJudgment "${exedir}" + else + GetLastJudgment "${exedir}" + fi + 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/judge.sh b/support/judge_webcgi/cgi-bin/judge.sh new file mode 100644 index 000000000..f33a23a86 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/judge.sh @@ -0,0 +1,146 @@ +#!/bin/bash +. ./pc2common.sh +. ./webcommon.sh +. ./cdpcommon.sh + + +TableHeader() +{ + cat << EOFTH + +Run ID +Disp +Judgment +Problem +Team +Test Cases +Language +Judge +Time Judged + +EOFTH +} + +MyTableStyles() +{ + cat << EOFMYSTYLES +th { + cursor: pointer; +} +EOFMYSTYLES +} + +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 +Styles +MyTableStyles +EndStyles +StartHTMLDoc +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 + 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/pc2common.sh b/support/judge_webcgi/cgi-bin/pc2common.sh new file mode 100644 index 000000000..053f8529c --- /dev/null +++ b/support/judge_webcgi/cgi-bin/pc2common.sh @@ -0,0 +1,285 @@ +# 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" +COMP_OUT_PREFIX="cstdout" +COMP_ERR_PREFIX="cstderr" + +# 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() +{ + # 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 +} + + +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` +} + + +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 + # Have to check all judgements, in order + jresult="AC" + for jfile in `ls ${EXECUTE_DATA_PREFIX}.[0-9]*.txt 2>/dev/null | sed -e 's;^.*/;;' | sort -t. +1n` + do + GetJudgmentFromFile ./${jfile} + if test -n "$result" -a "$result" != "AC" + then + jresult="$result" + break + fi + done + result="$jresult" + fi +} + +GetLastJudgment() +{ + 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 +} + + +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 +} + +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 new file mode 100644 index 000000000..ca5052c61 --- /dev/null +++ b/support/judge_webcgi/cgi-bin/showrun.sh @@ -0,0 +1,355 @@ +#!/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" + echo '

' + 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 + if test -s "${result}" + then + ReadBriefcase < $result + else + cpulimms="" + memlim="" + fi + 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 + else + cat << LBEOF1AA +The CPU Limit for this problem is N/A. +
+LBEOF1AA + 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 + else + cat << LBEOF1AAA +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 + cat << LBEOF2 +Click here for the full sandbox log for this run +

+

+LBEOF2 + fi +} + +TableHeader() +{ + cat << EOFTH + + Test + Disp + Judgment + Exit + Execute Time + MiB Used + Val Time + Val Success + Run stdout + Run stderr + Judge In + Judge Ans + Val Out + Val Err + +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" + 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" + if test -z "${linktext}" + then + echo ' N/A' + else + bytes=`stat -c %s ${linkaddr}` + echo ' '$linktext' ('$bytes' bytes)' + fi +} + +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 + memused="$9" + memusedbytes="${10}" + exesandms="${11}" + srclist="${12}" + # 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='' + elif test "${judgment}" = "CE" + then + jicon='' + else + jicon='' + fi + + echo ' ' + + if test -n "${tcreport}" + then + echo ' '$tc'' + else + echo ' '$tc'' + fi + echo ' '$jicon'' + echo ' '$judgment'' + echo ' '$ec'' + 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'' + 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 + 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 +Styles +EndStyles +StartHTMLDoc +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" + 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 + +#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 new file mode 100644 index 000000000..2e513343f --- /dev/null +++ b/support/judge_webcgi/cgi-bin/webcommon.sh @@ -0,0 +1,236 @@ +# 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 +# +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 +} + +Styles() +{ + cat << EOFSTYLE +" +} + +StartHTMLDoc() +{ + cat << EOFSHTML + + +EOFSHTML +} + +TableSortScripts() +{ + cat << EOFSCR + + +EOFSCR +} + +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/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/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 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 = "▲" + } + } +}))); diff --git a/test/edu/csus/ecs/pc2/core/CommandVariableReplacerTest.java b/test/edu/csus/ecs/pc2/core/CommandVariableReplacerTest.java index f5b62abd1..58f87a81e 100644 --- a/test/edu/csus/ecs/pc2/core/CommandVariableReplacerTest.java +++ b/test/edu/csus/ecs/pc2/core/CommandVariableReplacerTest.java @@ -15,7 +15,7 @@ /** * Unit tests. - * + * * @author pc2@ecs.csus.edu * @version $Id$ */ @@ -38,7 +38,7 @@ protected void setUp() throws Exception { } public void testMostFields() throws Exception { - + String [] dataLines = { "{:basename}::Sumit", // "{:clientid}::9", // @@ -79,10 +79,10 @@ public void testMostFields() throws Exception { Run run = sample.createRun(contest, teamId, language, problem, 0); run.setNumber(45); run.setElapsedMS(elapsed); - + ExecutionData executionData = null; String filename = getSamplesSourceFilename("Sumit.java"); - + RunFiles runFiles = new RunFiles(run, filename); ProblemDataFiles problemDataFiles = null; @@ -91,19 +91,19 @@ public void testMostFields() throws Exception { // String result = commandVariableReplacer.substituteAllStrings(contest, run, runFiles, name, executionData, problemDataFiles); // System.out.println("\""+name+"::"+result+"\", //"); // } - + for (String line : dataLines) { String [] list = line.split("::"); String name = list[0]; String expectedString = list[1]; String actual = commandVariableReplacer.substituteVariables(name, contest, run, runFiles, null, executionData, problemDataFiles); - + assertEquals(name+" variable ", expectedString, actual); } } public void testOtherFields() throws Exception { - + String [] dataLines = { "{:basename}::Sumit", // "{:clientid}::9", // @@ -144,15 +144,15 @@ public void testOtherFields() throws Exception { Run run = sample.createRun(contest, teamId, language, problem, 0); run.setNumber(45); run.setElapsedMS(elapsed); - + problem.setTimeOutInSeconds(3412); - + ExecutionData executionData = new ExecutionData(); executionData.setExecuteExitValue(999); executionData.setExecuteTimeMS(112233); - + String filename = getSamplesSourceFilename("Sumit.java"); - + RunFiles runFiles = new RunFiles(run, filename); ProblemDataFiles problemDataFiles = null; @@ -161,7 +161,48 @@ public void testOtherFields() throws Exception { String name = list[0]; String expectedString = list[1]; String actual = commandVariableReplacer.substituteVariables(name, contest, run, runFiles, null, executionData, problemDataFiles); - + + assertEquals(name+" variable ", expectedString, actual); + } + } + + public void testExecuteFolderFields() throws Exception { + + String [] dataLines = { + "{:clientname}::server0", // + "{:clientid}::0", // + "{:clientsite}::1", // + "{:runnumber}::1029", // + "{:siteid}::1", // + "{:teamid}::9", // + "{:problem}::3", // + "{:problemletter}::C", // + "{:problemshort}::dancingladies", + "{:language}::4", // + "{:languageletter}::D", + "{:languagename}::perl", // + "{:languageid}::peeeeerl", + "executesite{:siteid}{:clientname}::executesite1server0", + "ex_{:runnumber}_{:problemletter}_{:problemshort}_{:teamid}_{:languageid}_{:clientname}::ex_1029_C_dancingladies_9_peeeeerl_server0" + }; + + Problem problem = contest.getProblems()[2]; + Language language = contest.getLanguages()[3]; + Account account = SampleContest.getTeamAccounts(contest)[8]; + ClientId teamId = account.getClientId(); + problem.setShortName("dancingladies"); + + Run run = sample.createRun(contest, teamId, language, problem, 0); + run.setNumber(1029); + run.setElapsedMS(1000); + + language.setID("peeeeerl"); + for (String line : dataLines) { + String [] list = line.split("::"); + String name = list[0]; + String expectedString = list[1]; + String actual = commandVariableReplacer.substituteExecuteFolderVariables(contest, null, run, name); + assertEquals(name+" variable ", expectedString, actual); } }