diff --git a/grader b/grader --- a/grader +++ b/grader @@ -57,11 +57,14 @@ the problem name must be specified by the next argument. additional options: + --all-sub re-grade every submissions instead of just the latest submission of each user. - --all-sub re-grade every submissions instead of just the latest submission of each user. sub: re-grader the specified submission. The submission ID to be re-graded must be specified by the next argument. + options: + --err-log log error to a file in the log dir + (3) create stop-file to stop running grader in queue mode (4) You are here. USAGE @@ -134,6 +137,8 @@ options[:all_sub] = (ARGV.delete('--all-sub') != nil) + options[:err_log] = (ARGV.delete('--err-log') != nil) + return options end @@ -420,6 +425,13 @@ #set loggin environment ENV['GRADER_LOGGING'] = log_file_name +if options[:err_log] + err_file_name = log_file_name + '.err' + $stderr.reopen(err_file_name,"a") + log "STDERR log to file [#{err_file_name}]" + warn "start logging for grader PID #{Process.pid} on #{Time.now.in_time_zone}" +end + # register exit handler to report inactive, and terminated at_exit do diff --git a/installer/install.sh b/installer/install.sh --- a/installer/install.sh +++ b/installer/install.sh @@ -2,7 +2,7 @@ echo "This script will install and configure Cafe grader." -$RUBY_VERSION="2.2.0" +$RUBY_VERSION="2.1.2" echo "This will install Ruby $RUBY_VERSION under RVM" echo "Installing required apts" @@ -14,11 +14,11 @@ zlib1g zlib1g-dev libssl-dev libyaml-dev libsqlite3-dev \ sqlite3 libxml2-dev libxslt-dev autoconf libc6-dev \ ncurses-dev automake libtool bison subversion \ - pkg-config curl nodejs unzip + pkg-config curl nodejs unzip pyflakes ruby default-jdk echo "Installing RVM" curl -k -L https://get.rvm.io | bash -s stable -~/.rvm/scripts/rvm +source ~/.rvm/scripts/rvm echo "Installing Ruby $RUBY_VERSION in RVM" @@ -157,6 +157,15 @@ echo "require File.join(File.dirname(__FILE__),'../lib/boot')" >> scripts/config/environment.rb echo "require File.dirname(__FILE__) + \"/env_#{GRADER_ENV}.rb\"" >> scripts/config/environment.rb +# compiling box +MACHINE_TYPE=`uname -m` +if [ ${MACHINE_TYPE} == 'x86_64' ]; then + gcc -std=c99 -o scripts/std-script/box scripts/std-script/box64-new.c +else + g++ -o scripts/std-script/box scripts/std-script/box.cc +fi + + cd .. echo "Now you are ready to run cafe grader...." diff --git a/lib/engine.rb b/lib/engine.rb --- a/lib/engine.rb +++ b/lib/engine.rb @@ -118,8 +118,10 @@ talk grading_dir Dir.chdir grading_dir - cmd = "#{problem_home}/script/judge #{language} #{fname}" + script_name = "#{problem_home}/script/judge" + cmd = "#{script_name} #{language} #{fname}" talk "CMD: #{cmd}" + warn "ERROR: file does not exists #{script_name}" unless File.exists? script_name system(cmd) end diff --git a/lib/runner.rb b/lib/runner.rb --- a/lib/runner.rb +++ b/lib/runner.rb @@ -45,7 +45,7 @@ end def grade_submission(submission) - puts "Submission: #{submission.id} by #{submission.user.full_name}" + puts "Submission: #{submission.id} by #{submission.try(:user).try(:full_name)}" @engine.grade(submission) end diff --git a/lib/submission_helper.rb b/lib/submission_helper.rb --- a/lib/submission_helper.rb +++ b/lib/submission_helper.rb @@ -65,7 +65,8 @@ end result_fname = "#{test_result_dir}/result" - comment_fname = "#{test_result_dir}/comment" + comment_fname = "#{test_result_dir}/comment" + runstat_fname = "#{test_result_dir}/run_stat" if FileTest.exist?(result_fname) comment = "" begin @@ -85,9 +86,22 @@ comment += "" end - return {:points => result, - :comment => comment, - :cmp_msg => cmp_msg} + begin + runstat_file = File.open(runstat_fname) + max_runtime = runstat_file.readline.to_f + peak_memory = runstat_file.readline.to_i + rescue + max_runtime = -1 + peak_memory = -1 + end + + + return {points: result, + comment: comment, + cmp_msg: cmp_msg, + max_runtime: max_runtime, + peak_memory: peak_memory + } else if FileTest.exist?("#{test_result_dir}/a.out") return {:points => 0, @@ -108,6 +122,10 @@ submission.points = points comment = @config.report_comment(result[:comment]) + submission.peak_memory = result[:peak_memory] + submission.max_runtime = result[:max_runtime] + submission.effective_code_length =submission.source.length + # # TODO: FIX THIS MESSAGE # diff --git a/lib/test_request_helper.rb b/lib/test_request_helper.rb --- a/lib/test_request_helper.rb +++ b/lib/test_request_helper.rb @@ -198,7 +198,7 @@ end # extract memory usage - if res = /s(.*)m/.match(running_stat_line) + if res = /s(.*)kbytes/.match(running_stat_line) memory_used = res[1].to_i else memory_used = -1 diff --git a/std-script/compile b/std-script/compile --- a/std-script/compile +++ b/std-script/compile @@ -26,10 +26,18 @@ C_COMPILER = "/usr/bin/gcc" CPLUSPLUS_COMPILER = "/usr/bin/g++" PASCAL_COMPILER = "/usr/bin/fpc" +JAVA_COMPILER = "/usr/bin/javac" +RUBY_INTERPRETER = "/usr/bin/ruby" +PYTHON_INTERPRETER = "/usr/bin/python" +PYTHON_CHECKER = "/usr/bin/pyflakes" +PHP_INTERPRETER = "/usr/bin/php" C_OPTIONS = "-O2 -s -static -std=c99 -DCONTEST -lm -Wall" -CPLUSPLUS_OPTIONS = "-O2 -s -static -DCONTEST -lm -Wall" +CPLUSPLUS_OPTIONS = "-O2 -s -std=c++11 -static -DCONTEST -lm -Wall" PASCAL_OPTIONS = "-O1 -XS -dCONTEST" +JAVA_OPTIONS = "" +PYTHON_OPTIONS = "" +PHP_OPTIONS = "-l" # Check for the correct number of arguments. Otherwise, print usage. if ARGV.length == 0 or ARGV.length > 4 @@ -84,20 +92,87 @@ # Compile. case params[:prog_lang] - when "c" - command = "#{C_COMPILER} #{params[:source_file]} -o #{params[:output_file]} #{C_OPTIONS} 2> #{params[:message_file]}" - system(command) +when "c" + command = "#{C_COMPILER} #{params[:source_file]} -o #{params[:output_file]} #{C_OPTIONS}" + system(command, err: params[:message_file]) + +when "c++" + command = "#{CPLUSPLUS_COMPILER} #{params[:source_file]} -o #{params[:output_file]} #{CPLUSPLUS_OPTIONS}" + system(command, err: params[:message_file]) + +when "pas" + command = "#{PASCAL_COMPILER} #{params[:source_file]} -ooutpas #{PASCAL_OPTIONS}" + system(command,out: params[:message_file]) + FileUtils.mv("output", params[:output_file]) + +when "java" + #rename the file to the public class name + + #get the class name + classname = 'DUMMY' + source = Array.new + File.foreach(params[:source_file],'r:UTF-8') do |line| + line.encode!('UTF-8','UTF-8',invalid: :replace, replace: '') + md = /\s*public\s*class\s*(\w*)/.match(line) + classname=md[1] if md + source << line unless line =~ /\s*package\s*\w+\s*\;/ + end + File.open("#{classname}.java","w") do |file| + source.each do |s| + file.puts s + end + end + #system("cp #{params[:source_file]} #{classname}.java") + command = "#{JAVA_COMPILER} -encoding utf8 #{classname}.java" + system(command, err: params[:message_file]) + if File.exists?(classname + ".class") + File.open(params[:output_file],"w") {|file| file.write("#{classname}")} + end + if classname == 'DUMMY' + File.open(params[:message_file],"w") {|file| file.write("Cannot find any public class in the source code\n")} + end - when "c++" - command = "#{CPLUSPLUS_COMPILER} #{params[:source_file]} -o #{params[:output_file]} #{CPLUSPLUS_OPTIONS} 2> #{params[:message_file]}" - system(command) - - when "pas" - command = "#{PASCAL_COMPILER} #{params[:source_file]} -ooutpas #{PASCAL_OPTIONS} > #{params[:message_file]}" - system(command) - FileUtils.mv("output", params[:output_file]) - - else +when "ruby" + command = "#{RUBY_INTERPRETER} -c #{params[:source_file]}" + if system(command, err: params[:message_file]) + File.open(params[:output_file],"w") do |out_file| + out_file.puts "#!#{RUBY_INTERPRETER}" + File.open(params[:source_file],"r").each do |line| + out_file.print line + end + end + File.chmod(0755, params[:output_file]) + end + +when "python" + command = "#{PYTHON_CHECKER} #{params[:source_file]}" + if system(command, out: params[:message_file]) + #compile to python bytecode + command = "#{PYTHON_INTERPRETER} -m py_compile #{params[:source_file]}" + puts "compile: #{command}" + system(command) + puts "pwd: " + Dir.pwd + Dir.new('.').each {|file| puts file} + File.open(params[:output_file],"w") do |out_file| + out_file.puts "#!#{PYTHON_INTERPRETER} #{params[:source_file]}c" + end + File.chmod(0755, params[:output_file]) + FileUtils.cp("#{params[:source_file]}c",params[:output_file]) + end + +when "php" + command = "#{PHP_INTERPRETER} #{PHP_OPTIONS} #{params[:source_file]}" + if system(command, err: params[:message_file]) + File.open(params[:output_file],"w") do |out_file| + out_file.puts "#!#{PHP_INTERPRETER}" + File.open(params[:source_file],"r").each do |line| + out_file.print line + end + end + File.chmod(0755, params[:output_file]) + end + +else talk("ERROR: Invalid language specified!") open(params[:message_file],"w") do |f| f.puts "ERROR: Invalid language specified!" diff --git a/std-script/grade b/std-script/grade --- a/std-script/grade +++ b/std-script/grade @@ -31,6 +31,17 @@ end end +def extract_time(t) + #puts "TIME: #{t}" + if (result=/^(.*)r(.*)u(.*)s(.*)kbytes/.match(t)) + {:real => result[1], :user => result[2], :sys => result[3], :mem => result[4]} + else + #{:real => 0, :user => 0, :sys => 0} + #puts "ERROR READING RUNNING TIME: #{t}" + raise "Error reading running time: #{t}" + end +end + problem_home = ENV['PROBLEM_HOME'] require "#{problem_home}/script/test_dsl.rb" load "#{problem_home}/test_cases/all_tests.cfg" @@ -43,6 +54,8 @@ all_score = 0 all_comment = '' +peak_memory = -1 +max_runtime = -1 (1..(problem.runs.length-1)).each do |k| log "grade run #{k}" run = problem.runs[k] @@ -58,10 +71,15 @@ else result_file = File.new(result_file_name, "r") result_file_lines = result_file.readlines - if result_file_lines.length>=2 + if result_file_lines.length>=3 current_run_score = result_file_lines[1].to_i run_comment += result_file_lines[0] run_comment_short += char_comment(result_file_lines[0].chomp) + + #update max runtime & memory + run_stat = extract_time result_file_lines[2] + peak_memory = [peak_memory,run_stat[:mem].to_i].max + max_runtime = [max_runtime,run_stat[:user].to_f + run_stat[:sys].to_f].max else current_run_score = 0 run_comment += "result file for test #{test_num} error\n" @@ -104,3 +122,13 @@ comment_file = File.new("comment", "w") comment_file.write "#{all_comment}\n" comment_file.close + + +File.open("run_stat","w") do |file| + file.puts max_runtime + file.puts peak_memory +end + +puts "#{all_score} #{all_comment}" +log "score = #{all_score}\ncomment = #{all_comment}" +log "max_runtime = #{max_runtime}\npeak_memory = #{peak_memory}" diff --git a/std-script/judge b/std-script/judge --- a/std-script/judge +++ b/std-script/judge @@ -53,12 +53,13 @@ end language = ARGV[0] -if language != "c" && language != "c++" && language != "pas" +if language != "c" && language != "c++" && language != "pas" && language != "java" && language != "ruby" && language != "python" && language != "php" log "You specified a language that is not supported: #{language}." exit(127) end source_file = ARGV[1] +ENV['SOURCE_NAME'] = source_file if File.exist?(source_file) == false log "The source file does not exist." exit(127) @@ -110,6 +111,8 @@ else call_and_log("Cannot move the compiled program to #{test_result_dir}") { FileUtils.mv("a.out",test_result_dir) + if language == "java" then Dir["*.class"].each { |file| FileUtils.mv(file,test_result_dir)} end + if language == "python" then Dir["*.pyc"].each { |file| FileUtils.mv(file,test_result_dir)} end } FileUtils.rm_rf("#{sandbox_dir}/.") end @@ -133,10 +136,12 @@ call_and_log("Cannot copy the compiled program into #{sandbox_dir}") { FileUtils.cp("#{test_result_dir}/a.out", sandbox_dir, :preserve => true) + if language == "java" then Dir["#{test_result_dir}/*.class"].each { |file| FileUtils.cp(file,sandbox_dir)} end + if language == "python" then Dir["#{test_result_dir}/*.pyc"].each { |file| FileUtils.cp(file,sandbox_dir)} end } begin - execute("#{problem_home}/script/run #{language} #{test_num}", "Error occured during execution of the run script") + execute("#{problem_home}/script/run #{language} #{test_num} ", "Error occured during execution of the run script") rescue # do nothing end diff --git a/std-script/run b/std-script/run --- a/std-script/run +++ b/std-script/run @@ -43,10 +43,13 @@ end problem_home = ENV['PROBLEM_HOME'] +source_name = ENV['SOURCE_NAME'] require "#{problem_home}/script/test_dsl.rb" load "#{problem_home}/test_cases/all_tests.cfg" problem = Problem.get_instance +sandbox_dir = Dir.getwd + if problem.well_formed? == false log "The problem specification is not well formed." exit(127) @@ -81,16 +84,42 @@ # Hide PROBLEM_HOME ENV['PROBLEM_HOME'] = nil +ENV['SOURCE_NAME'] = nil # Run the program. #run_command = "/usr/bin/time -f \"#{time_output_format}\" 2>run_result #{problem_home}/script/box_new -a 2 -f -t #{time_limit} -m #{mem_limit} -i #{input_file_name} -o output.txt #{program_name}" +# -run_command = "#{problem_home}/script/box -a 2 -f -T -t #{time_limit} -m #{mem_limit} -i #{input_file_name} -o output.txt #{program_name} 2>run_result" +JAVA_OPTION = "-s set_robust_list -s futex -s clone -s getppid -s clone -s wait4 -p /usr/bin/ -p ./" +RUBY_OPTION = "-p /usr/lib64/ -p /usr/local/lib64/ -p /usr/local/lib/ -p /lib64/ -p /dev/urandom -p #{sandbox_dir}/#{program_name} -p #{sandbox_dir}/ -s set_robust_list -s sched_getaffinity -s clock_gettime -s sigaltstack -s pipe2 -s clone -s futex -s openat -s pipe" +PYTHON_OPTION = "-p /usr/lib64/ -p /usr/local/lib64/ -p /usr/local/lib/ -p /usr/bin/ -p /lib64/ -p #{sandbox_dir}/#{program_name} -p ./#{program_name} -p #{sandbox_dir}/#{source_name} -s set_robust_list -s openat -s recvmsg -s connect -s socket -s sendto -s futex -E PYTHONNOUSERSITE=yes" +PHP_OPTION = "-p /usr/lib64/ -p/lib64/ -p /usr/bin/ -p #{sandbox_dir}/#{program_name} -p ./#{program_name} -p /usr/share/ -s setfsuid -s setfsgid -s openat -s set_robust_list -s futex -s clone -s socket -s connect" + +case language + when "java" + # for java, extract the classname + # wne have to add additional systemcall and we don't check the mem limit (dunno how to fix...) + classname = 'DUMMY' + File.open(program_name,"r").each do |line| + classname = line + end + #for java, we cannot really check the memory limit... + run_command = "#{problem_home}/script/box -a 3 -f -T -t #{time_limit} #{JAVA_OPTION} -i #{input_file_name} -o output.txt /usr/bin/java -A -Xmx#{mem_limit}k -A #{classname} " + when "ruby" + run_command = "#{problem_home}/script/box -a 2 -f -T -t #{time_limit*=2} -m #{mem_limit} #{RUBY_OPTION} -i #{input_file_name} -o output.txt /usr/bin/ruby #{program_name} " + when "python" + run_command = "#{problem_home}/script/box -a 2 -f -T -t #{time_limit*=2} -m #{mem_limit} #{PYTHON_OPTION} -i #{input_file_name} -o output.txt /usr/bin/python #{program_name} " + when "php" + run_command = "#{problem_home}/script/box -a 2 -f -T -t #{time_limit*=2} -m #{[128*1024,mem_limit].max} #{PHP_OPTION} -i #{input_file_name} -o output.txt /usr/bin/php -A -d -A memory_limit=#{mem_limit}k -A #{program_name} " + else # for c++, pascal, we do the normal checking + run_command = "#{problem_home}/script/box -a 2 -f -T -t #{time_limit} -m #{mem_limit} -i #{input_file_name} -o output.txt #{program_name} " +end + log "Running test #{test_num}..." log run_command -log -system(run_command) +log +system(run_command,err: 'run_result') # Restore PROBLEM_HOME ENV['PROBLEM_HOME'] = problem_home @@ -132,12 +161,13 @@ exit(0) } + if run_result[0][0,2] != "OK" log "There was a runtime error." report.call(run_result[0], 0, "No comment.\n") end -if running_time[:user].to_f + running_time[:sys].to_f > time_limit +if running_time[:user].to_f > time_limit log "Time limit exceeded." report.call("Time limit exceeded", 0, "No comment.\n") end diff --git a/templates/check.float b/templates/check.float new file mode 100755 --- /dev/null +++ b/templates/check.float @@ -0,0 +1,66 @@ +#!/usr/bin/env ruby + +problem_home = ENV['PROBLEM_HOME'] +require "#{problem_home}/script/test_dsl.rb" + +if ARGV.length < 2 + puts "Usage: check []" + exit(0) +end + +language = ARGV[0] +test_num = ARGV[1].to_i +if ARGV.length >= 3 + output_file_name = ARGV[2] +else + output_file_name = "output.txt" +end + +load "#{problem_home}/test_cases/all_tests.cfg" +problem = Problem.get_instance + +output_file = File.new(output_file_name, "r") +answer_file = File.new("#{problem_home}/test_cases/#{test_num}/answer-#{test_num}.txt") +result_file = File.new("check_result", "w") + +output_file_content = output_file.read +answer_file_content = answer_file.read + +report_correct = lambda { + result_file.write "Correct\n" + result_file.write problem.get_score(test_num) + result_file.write "\n" + result_file.close + exit(0) +} + +report_wrong = lambda { + result_file.write "Incorrect\n" + result_file.write "0\n" + result_file.close + exit(0) +} + +################## +# Your code here # +################## + +########### THIS IS FOR CHECKING FLOAT with EPSILON error ########## + +EPSILON = 0.000001 + +out_items = output_file_content.split +ans_items = answer_file_content.split + +if out_items.length != ans_items.length + report_wrong.call +else + out_items.length.times do |i| + out_value = out_items[i].to_f + ans_value = ans_items[i].to_f + if (out_value - ans_value).abs > EPSILON * [out_value.abs,ans_value.abs].max + report_wrong.call + end + end + report_correct.call +end