diff --git a/judge/scripts/config/.gitignore b/judge/scripts/config/.gitignore new file mode 100644 --- /dev/null +++ b/judge/scripts/config/.gitignore @@ -0,0 +1,2 @@ +env*.rb + diff --git a/judge/scripts/config/env_exam.rb.SAMPLE b/judge/scripts/config/env_exam.rb.SAMPLE new file mode 100644 --- /dev/null +++ b/judge/scripts/config/env_exam.rb.SAMPLE @@ -0,0 +1,20 @@ +# +# See documentation in lib/configuration.rb +# +Grader::Initializer.run do |config| + + config.problems_dir = GRADER_ROOT + "/../ev" + config.user_result_dir = GRADER_ROOT + "/../result" + + config.talkative = true + config.logging = true + config.log_dir = GRADER_ROOT + "/../log" + + config.report_grader = true + + config.test_request_input_base_dir = RAILS_ROOT + "/data/test_request/input" + config.test_request_output_base_dir = RAILS_ROOT + "/data/test_request/output" + config.test_request_problem_templates_dir = config.problems_dir + "/test_request" + + config.comment_report_style = :short +end diff --git a/judge/scripts/config/env_grading.rb.SAMPLE b/judge/scripts/config/env_grading.rb.SAMPLE new file mode 100644 --- /dev/null +++ b/judge/scripts/config/env_grading.rb.SAMPLE @@ -0,0 +1,19 @@ +# +# See documentation in lib/configuration.rb +# +Grader::Initializer.run do |config| + config.problems_dir = GRADER_ROOT + "/../ev" + config.user_result_dir = GRADER_ROOT + "/../result" + + config.talkative = true + config.logging = true + config.log_dir = GRADER_ROOT + "/../log" + + config.report_grader = true + + config.test_request_input_base_dir = RAILS_ROOT + "/data/test_request/input" + config.test_request_output_base_dir = RAILS_ROOT + "/data/test_request/output" + config.test_request_problem_templates_dir = config.problems_dir + "/test_request" + + config.comment_report_style = :full +end diff --git a/judge/scripts/config/env_test.rb.SAMPLE b/judge/scripts/config/env_test.rb.SAMPLE new file mode 100644 --- /dev/null +++ b/judge/scripts/config/env_test.rb.SAMPLE @@ -0,0 +1,29 @@ +# +# See documentation in lib/configuration.rb +# +Grader::Initializer.run do |config| + config.problems_dir = GRADER_ROOT + "/test/sandbox/ev" + config.user_result_dir = GRADER_ROOT + "/test/sandbox/result" + + config.talkative = false + + config.report_grader = false + + config.rails_env = 'test' + + config.comment_report_style = :full + + config.test_request_input_base_dir = GRADER_ROOT + "/test/data/test_request/input" + config.test_request_output_base_dir = GRADER_ROOT + "/test/sandbox/test_request/output" + config.test_request_problem_templates_dir = GRADER_ROOT + "/test/data/test_request/problems" + + # + # These options are for testing + # + class << config + attr_accessor :test_data_dir, :test_sandbox_dir + end + + config.test_data_dir = GRADER_ROOT + "/test/data" + config.test_sandbox_dir = GRADER_ROOT + "/test/sandbox" +end diff --git a/judge/scripts/config/environment.rb.SAMPLE b/judge/scripts/config/environment.rb.SAMPLE new file mode 100644 --- /dev/null +++ b/judge/scripts/config/environment.rb.SAMPLE @@ -0,0 +1,10 @@ +# Rails app directory +RAILS_ROOT = "/home/jittat/web_grader" + +GRADER_ROOT = "/home/jittat/grader/scripts" + +# This load all required codes +require File.join(File.dirname(__FILE__),'../lib/boot') + +# load the required environment file +require File.dirname(__FILE__) + "/env_#{GRADER_ENV}.rb" diff --git a/judge/scripts/grader b/judge/scripts/grader new file mode 100755 --- /dev/null +++ b/judge/scripts/grader @@ -0,0 +1,217 @@ +#!/usr/bin/ruby + +def stop_grader(id) + if id==:all + File.open(File.dirname(__FILE__) + "/stop.all",'w').close + else + File.open(File.dirname(__FILE__) + "/stop.#{id}",'w').close + end +end + +def check_stopfile + FileTest.exist?(File.dirname(__FILE__) + "/stop.all") or + FileTest.exist?(File.dirname(__FILE__) + "/stop.#{Process.pid}") +end + +def clear_stopfile + if FileTest.exist?(File.dirname(__FILE__) + "/stop.#{Process.pid}") + system("rm " + File.dirname(__FILE__) + "/stop.#{Process.pid}") + end +end + +def config + Grader::Configuration.get_instance +end + +def log_file_name + if !File.exists?(config.log_dir) + raise "Log directory does not exist: #{config.log_dir}" + end + config.log_dir + + "/#{GRADER_ENV}_#{config.grader_mode}.#{Process.pid}" +end + +def log(str) + if config.talkative + puts str + end + if config.logging + fp = File.open(log_file_name,"a") + fp.puts("GRADER: #{Time.new.strftime("%H:%M")} #{str}") + fp.close + end +end + +def display_manual + puts <= 1) and (ARGV[0]=='stop') + if ARGV.length==1 + puts "you should specify pid-list or 'all'" + display_manual + elsif (ARGV.length==2) and (ARGV[1]=='all') + stop_grader(:all) + puts "A global stop file ('stop.all') created." + puts "You should remove it manually later." + else + (1..ARGV.length-1).each do |i| + stop_grader(ARGV[i]) + end + puts "stop file(s) created" + end + exit(0) +end + +if check_stopfile + puts "Stop file exists. Terminated." + clear_stopfile + exit(0) +end + +grader_mode = 'queue' +if ARGV.length >= 1 + GRADER_ENV = ARGV[0] + if ARGV.length >=2 + grader_mode = ARGV[1] + end +else + GRADER_ENV = 'exam' +end + +puts "environment: #{GRADER_ENV}" +require File.join(File.dirname(__FILE__),'config/environment') + +# add grader_mode to config +# this is needed because method log needs it. TODO: clean this up +class << config + attr_accessor :grader_mode +end +config.grader_mode = grader_mode + +# reading rails environment +log 'Reading rails environment' + +RAILS_ENV = config.rails_env +require RAILS_ROOT + '/config/environment' + +# register grader process +if config.report_grader + grader_proc = GraderProcess.register(config.grader_hostname, + Process.pid, + grader_mode) +else + grader_proc = nil +end + +#set loggin environment +ENV['GRADER_LOGGING'] = log_file_name + +# register exit handler to report inactive, and terminated +at_exit do + if grader_proc!=nil + grader_proc.report_inactive + grader_proc.terminate + end +end + +# +# MAIN LOOP +# + +case grader_mode +when "queue", "test_request" + log "Grader: #{grader_mode}" + if grader_mode=="queue" + engine = Grader::Engine.new + else + engine = Grader::Engine.new(Grader::TestRequestRoomMaker.new, + Grader::TestRequestReporter.new) + end + + runner = Grader::Runner.new(engine, grader_proc) + while true + + if check_stopfile # created by calling grader stop + clear_stopfile + log "stopped (with stop file)" + break + end + + if grader_mode=="queue" + task = runner.grade_oldest_task + else + task = runner.grade_oldest_test_request + end + if task==nil + sleep(1) + end + end + +when "prob" + engine = Grader::Engine.new + runner = Grader::Runner.new(engine, grader_proc) + + grader_proc.report_active if grader_proc!=nil + + ARGV.shift + ARGV.shift + + ARGV.each do |prob_name| + prob = Problem.find_by_name(prob_name) + if prob==nil + puts "cannot find problem: #{prob_name}" + else + runner.grade_problem(prob) + end + end + +when "sub" + engine = Grader::Engine.new + runner = Grader::Runner.new(engine, grader_proc) + + grader_proc.report_active if grader_proc!=nil + + ARGV.shift + ARGV.shift + + ARGV.each do |sub_id| + puts "Grading #{sub_id}" + begin + submission = Submission.find(sub_id.to_i) + rescue ActiveRecord::RecordNotFound + puts "Record not found" + submission = nil + end + + if submission!=nil + runner.grade_submission(submission) + end + end + +else + display_manual + exit(0) +end + diff --git a/judge/scripts/grader_id b/judge/scripts/grader_id new file mode 100755 --- /dev/null +++ b/judge/scripts/grader_id @@ -0,0 +1,110 @@ +#!/usr/bin/ruby + +def talk(str) + if TALKATIVE + puts str + end +end + +def execute(command, error_message="") + if not system(command) + puts "ERROR: #{error_message}" + exit(127) + end +end + +def save_source(submission,dir,fname) + f = File.open("#{dir}/#{fname}","w") + f.write(submission.source) + f.close +end + +def call_judge(problem_home,language,problem_out_dir,fname) + ENV['PROBLEM_HOME'] = problem_home + Dir.chdir problem_out_dir + cmd = "#{problem_home}/script/judge #{language} #{fname}" +# puts "CMD: #{cmd}" + system(cmd) +end + +def read_result(test_result_dir) + cmp_msg_fname = "#{test_result_dir}/compiler_message" + cmp_msg = File.open(cmp_msg_fname).read + + result_fname = "#{test_result_dir}/result" + comment_fname = "#{test_result_dir}/comment" + if FileTest.exist?(result_fname) + result = File.open(result_fname).readline.to_i + comment = File.open(comment_fname).readline.chomp + return {:points => result, + :comment => comment, + :cmp_msg => cmp_msg} + else + return {:points => 0, + :comment => 'compile error', + :cmp_msg => cmp_msg} + end +end + +def save_result(submission,result) + submission.graded_at = Time.now + submission.points = result[:points] + submission.grader_comment = report_comment(result[:comment]) + submission.compiler_message = result[:cmp_msg] + submission.save +end + +def grade(submission_id) + sub = Submission.find(submission_id) + user = sub.user + problem = sub.problem + + language = sub.language.name + lang_ext = sub.language.ext + # FIX THIS + talk 'some hack on language' + if language == 'cpp' + language = 'c++' + end + + user_dir = "#{USER_RESULT_DIR}/#{user.login}" + Dir.mkdir(user_dir) if !FileTest.exist?(user_dir) + + problem_out_dir = "#{user_dir}/#{problem.name}" + Dir.mkdir(problem_out_dir) if !FileTest.exist?(problem_out_dir) + + problem_home = "#{PROBLEMS_DIR}/#{problem.name}" + source_name = "#{problem.name}.#{lang_ext}" + + save_source(sub,problem_out_dir,source_name) + call_judge(problem_home,language,problem_out_dir,source_name) + save_result(sub,read_result("#{problem_out_dir}/test-result")) +end + +def stop_grader + File.open(File.dirname(__FILE__) + '/stop','w') +end + +def check_stopfile + FileTest.exist?(File.dirname(__FILE__) + '/stop') +end + +def clear_stopfile + system("rm " + File.dirname(__FILE__) + '/stop') +end + +# reading environment and options +GRADER_ENV = 'exam' +puts "environment: #{GRADER_ENV}" +require File.dirname(__FILE__) + "/environment.rb" + +#main program +talk 'Reading rails environment' + +RAILS_ENV = 'development' +require RAILS_APP_DIR + '/config/environment' + +current_dir = `pwd` +grade(ARGV[0].to_i) + + diff --git a/judge/scripts/import_problem b/judge/scripts/import_problem new file mode 100755 --- /dev/null +++ b/judge/scripts/import_problem @@ -0,0 +1,164 @@ +#!/usr/bin/ruby + +# import_problem: +# * creates a directory for a problem in the current directory, +# * copy testdata in the old format and create standard testcase config file + +require 'erb' +require 'fileutils' +require File.join(File.dirname(__FILE__),'lib/import_helper') + +def input_filename(dir,i) + "#{dir}/input-#{i}.txt" +end + +def answer_filename(dir,i) + "#{dir}/answer-#{i}.txt" +end + +def build_testrun_info_from_dir(num_testruns,importing_test_dir) + filenames = Dir["#{importing_test_dir}/*.in"].collect do |filename| + File.basename((/(.*)\.in/.match(filename))[1]) + end + build_testrun_info(num_testruns,filenames) +end + +def copy_testcase(importing_test_dir,fname,dir,i) + system("cp #{importing_test_dir}/#{fname}.in #{input_filename(dir,i)}") + system("cp #{importing_test_dir}/#{fname}.sol #{answer_filename(dir,i)}") +end + +def process_options(options) + i = 4 + while ii+1 + i += 1 + end + if ARGV[i]=='-m' + options[:mem_limit] = ARGV[i+1].to_i if ARGV.length>i+1 + i += 1 + end + i += 1 + end +end + +SCRIPT_DIR = File.dirname(__FILE__) + +# print usage +if (ARGV.length < 4) or (ARGV[3][0,1]=="-") + puts < 1, :mem_limit => 16} +process_options(options) + +testrun_info = build_testrun_info_from_dir(num_testruns, testcase_dir) + +# start working +puts "creating directories" + +system("mkdir #{problem}") +system("mkdir #{problem}/script") +system("mkdir #{problem}/test_cases") +#system("cp #{GRADER_DIR}/std-script/* #{problem}/script") + +puts "copying testcases" + +tr_num = 0 + +num_testcases = 0 + +testrun_info.each do |testrun| + tr_num += 1 + puts "testrun: #{tr_num}" + + testrun.each do |testcase_info| + testcase_num, testcase_fname = testcase_info + + puts "copy #{testcase_fname} to #{testcase_num}" + + system("mkdir #{problem}/test_cases/#{testcase_num}") + copy_testcase("#{testcase_dir}",testcase_fname,"#{problem}/test_cases/#{testcase_num}",testcase_num) + + num_testcases += 1 + end +end + +# generating all_tests.cfg +puts "generating testcase config file" + +template = File.open(SCRIPT_DIR + "/templates/all_tests.cfg.erb").read +all_test_cfg = ERB.new(template) + +cfg_file = File.open("#{problem}/test_cases/all_tests.cfg","w") +cfg_file.puts all_test_cfg.result +cfg_file.close + + +# copy check script +if res = /^wrapper:(.*)$/.match(check_script) + # wrapper script + check_script_fname = res[1] + script_name = File.basename(check_script_fname) + check_wrapper_template = File.open(SCRIPT_DIR + "/templates/check_wrapper").read + check_wrapper = ERB.new(check_wrapper_template) + + check_file = File.open("#{problem}/script/check","w") + check_file.puts check_wrapper.result + check_file.close + + File.chmod(0755,"#{problem}/script/check") + + system("cp #{check_script_fname} #{problem}/script/#{script_name}") +else + if File.exists?(SCRIPT_DIR + "/templates/check.#{check_script}") + check_script_fname = SCRIPT_DIR + "/templates/check.#{check_script}" + else + check_script_fname = check_script + end + system("cp #{check_script_fname} #{problem}/script/check") +end + +# generating test_request directory +puts "generating test_request template" +FileUtils.mkdir_p("test_request/#{problem}/script") +FileUtils.mkdir_p("test_request/#{problem}/test_cases/1") + +template = File.open(SCRIPT_DIR + "/templates/test_request_all_tests.cfg.erb").read +test_request_all_test_cfg = ERB.new(template) + +cfg_file = File.open("test_request/#{problem}/test_cases/all_tests.cfg","w") +cfg_file.puts test_request_all_test_cfg.result +cfg_file.close + +system("cp #{SCRIPT_DIR}/templates/check_empty test_request/#{problem}/script/check") +system("cp #{SCRIPT_DIR}/templates/answer-1.txt test_request/#{problem}/test_cases/1") + +puts "done" diff --git a/judge/scripts/lib/boot.rb b/judge/scripts/lib/boot.rb new file mode 100644 --- /dev/null +++ b/judge/scripts/lib/boot.rb @@ -0,0 +1,10 @@ + +require File.join(File.dirname(__FILE__), 'configuration') +require File.join(File.dirname(__FILE__), 'initializer') + +require File.join(File.dirname(__FILE__), 'submission_helper') +require File.join(File.dirname(__FILE__), 'test_request_helper') + +require File.join(File.dirname(__FILE__), 'engine') +require File.join(File.dirname(__FILE__), 'runner') + diff --git a/judge/scripts/lib/configuration.rb b/judge/scripts/lib/configuration.rb new file mode 100644 --- /dev/null +++ b/judge/scripts/lib/configuration.rb @@ -0,0 +1,84 @@ +module Grader + + # This singleton class holds basic configurations for grader. When + # running in each mode, grader uses resources from different + # directories and outputs differently. Usually the attributes name + # are descriptive; below we explain more on each attributes. + class Configuration + # Rails' environment: "development", "production" + attr_accessor :rails_env + + # Grader looks for problem [prob] in problem_dir/[prob], and store + # execution results for submission [x] of user [u] in directory + # user_result_dir/[u]/[x] + attr_accessor :problems_dir + attr_accessor :user_result_dir + + # If report_grader=true, the grader would add a row in model + # GraderProcess. It would report itself with grader_hostname and + # process id. + attr_accessor :report_grader + attr_accessor :grader_hostname + + # If talkative=true, grader would report status to console. If + # logging=true, grader would report status to a log file located + # in log_dir, in a file name mode.options.pid. TODO: defined + # log file naming. + attr_accessor :talkative + attr_accessor :logging + attr_accessor :log_dir + + # These are directories related to the test interface. + attr_accessor :test_request_input_base_dir + attr_accessor :test_request_output_base_dir + attr_accessor :test_request_problem_templates_dir + + # Comment received from the grading script will be filtered + # through Configuration#report_comment. How this method behave + # depends on this option; right now only two formats, :short and + # :long + attr_accessor :comment_report_style + + def report_comment(comment) + case comment_report_style + when :short + if comment.chomp =~ /^[\[\]P]+$/ # all P's + 'passed' + elsif comment.chomp =~ /[Cc]ompil.*[Ee]rror/ + 'compilation error' + else + 'failed' + end + + when :full + comment.chomp + end + end + + # Codes for singleton + private_class_method :new + + @@instance = nil + + def self.get_instance + if @@instance==nil + @@instance = new + end + @@instance + end + + private + def initialize + @talkative = false + @log_file = nil + @report_grader = false + @grader_hostname = `hostname`.chomp + + @rails_env = 'development' + + @comment_report_style = :full + end + + end + +end diff --git a/judge/scripts/lib/engine.rb b/judge/scripts/lib/engine.rb new file mode 100644 --- /dev/null +++ b/judge/scripts/lib/engine.rb @@ -0,0 +1,138 @@ +require 'fileutils' + +module Grader + + # + # A grader engine grades a submission, against anything: a test + # data, or a user submitted test data. It uses two helpers objects: + # room_maker and reporter. + # + class Engine + + attr_writer :room_maker + attr_writer :reporter + + def initialize(room_maker=nil, reporter=nil) + @config = Grader::Configuration.get_instance + + @room_maker = room_maker || Grader::SubmissionRoomMaker.new + @reporter = reporter || Grader::SubmissionReporter.new + end + + # takes a submission, asks room_maker to produce grading directories, + # calls grader scripts, and asks reporter to save the result + def grade(submission) + current_dir = `pwd`.chomp + + user = submission.user + problem = submission.problem + + # TODO: will have to create real exception for this + if user==nil or problem == nil + @reporter.report_error(submission,"Grading error: problem with submission") + #raise "engine: user or problem is nil" + end + + # TODO: this is another hack so that output only task can be judged + if submission.language!=nil + language = submission.language.name + lang_ext = submission.language.ext + else + language = 'c' + lang_ext = 'c' + end + + # FIX THIS + talk 'some hack on language' + if language == 'cpp' + language = 'c++' + end + + # COMMENT: should it be only source.ext? + if problem!=nil + source_name = "#{problem.name}.#{lang_ext}" + else + source_name = "source.#{lang_ext}" + end + + begin + grading_dir = @room_maker.produce_grading_room(submission) + @room_maker.save_source(submission,source_name) + problem_home = @room_maker.find_problem_home(submission) + + # puts "GRADING DIR: #{grading_dir}" + # puts "PROBLEM DIR: #{problem_home}" + + copy_log = copy_script(problem_home) + + call_judge(problem_home,language,grading_dir,source_name) + + @reporter.report(submission,"#{grading_dir}/test-result") + + clear_script(copy_log,problem_home) + + rescue RuntimeError => msg + @reporter.report_error(submission,"Grading error: #{msg}") + + ensure + @room_maker.clean_up(submission) + Dir.chdir(current_dir) # this is really important + end + end + + protected + + def talk(str) + if @config.talkative + puts str + end + end + + def call_judge(problem_home,language,grading_dir,fname) + ENV['PROBLEM_HOME'] = problem_home + + talk grading_dir + Dir.chdir grading_dir + cmd = "#{problem_home}/script/judge #{language} #{fname}" + talk "CMD: #{cmd}" + system(cmd) + end + + def get_std_script_dir + GRADER_ROOT + '/std-script' + end + + def copy_script(problem_home) + script_dir = "#{problem_home}/script" + std_script_dir = get_std_script_dir + + raise "std-script directory not found" if !FileTest.exist?(std_script_dir) + + scripts = Dir[std_script_dir + '/*'] + + copied = [] + + scripts.each do |s| + fname = File.basename(s) + if !FileTest.exist?("#{script_dir}/#{fname}") + copied << fname + system("cp #{s} #{script_dir}") + end + end + + return copied + end + + def clear_script(log,problem_home) + log.each do |s| + system("rm #{problem_home}/script/#{s}") + end + end + + def mkdir_if_does_not_exist(dirname) + Dir.mkdir(dirname) if !FileTest.exist?(dirname) + end + + end + +end diff --git a/judge/scripts/lib/import_helper.rb b/judge/scripts/lib/import_helper.rb new file mode 100644 --- /dev/null +++ b/judge/scripts/lib/import_helper.rb @@ -0,0 +1,28 @@ + +def filter_filename_for_testrun(testrun, filename_list) + l = [] + regex = Regexp.new("^(#{testrun}[a-z]*|#{testrun}-.*)$") + filename_list.each do |filename| + if regex.match(filename) + l << filename + end + end + l +end + +def build_testrun_info(num_testruns, input_filename_list) + info = [] + num_testcases = 0 + num_testruns.times do |i| + r = i+1 + testrun_info = [] + filenames = filter_filename_for_testrun(r, input_filename_list) + filenames.each do |fname| + num_testcases += 1 + testrun_info << [num_testcases,fname] + end + info << testrun_info + end + info +end + diff --git a/judge/scripts/lib/initializer.rb b/judge/scripts/lib/initializer.rb new file mode 100644 --- /dev/null +++ b/judge/scripts/lib/initializer.rb @@ -0,0 +1,13 @@ + +module Grader + + class Initializer + + def self.run(&block) + config = Grader::Configuration.get_instance + yield config + end + + end + +end diff --git a/judge/scripts/lib/runner.rb b/judge/scripts/lib/runner.rb new file mode 100644 --- /dev/null +++ b/judge/scripts/lib/runner.rb @@ -0,0 +1,58 @@ +# +# A runner drives the engine into various tasks. +# + +module Grader + + class Runner + + def initialize(engine, grader_process=nil) + @engine = engine + @grader_process = grader_process + end + + def grade_oldest_task + task = Task.get_inqueue_and_change_status(Task::STATUS_GRADING) + if task!=nil + @grader_process.report_active(task) if @grader_process!=nil + + submission = Submission.find(task.submission_id) + @engine.grade(submission) + task.status_complete! + @grader_process.report_inactive(task) if @grader_process!=nil + end + return task + end + + def grade_problem(problem) + users = User.find(:all) + users.each do |u| + puts "user: #{u.login}" + last_sub = Submission.find_last_by_user_and_problem(u.id,problem.id) + if last_sub!=nil + @engine.grade(last_sub) + end + end + end + + def grade_submission(submission) + puts "Submission: #{submission.id} by #{submission.user.full_name}" + @engine.grade(submission) + end + + def grade_oldest_test_request + test_request = TestRequest.get_inqueue_and_change_status(Task::STATUS_GRADING) + if test_request!=nil + @grader_process.report_active(test_request) if @grader_process!=nil + + @engine.grade(test_request) + test_request.status_complete! + @grader_process.report_inactive(test_request) if @grader_process!=nil + end + return test_request + end + + end + +end + diff --git a/judge/scripts/lib/submission_helper.rb b/judge/scripts/lib/submission_helper.rb new file mode 100644 --- /dev/null +++ b/judge/scripts/lib/submission_helper.rb @@ -0,0 +1,123 @@ +module Grader + + class SubmissionRoomMaker + def initialize + @config = Grader::Configuration.get_instance + end + + def produce_grading_room(submission) + user = submission.user + problem = submission.problem + grading_room = "#{@config.user_result_dir}/" + + "#{user.login}/#{problem.name}/#{submission.id}" + + FileUtils.mkdir_p(grading_room) + grading_room + end + + def find_problem_home(submission) + problem = submission.problem + "#{@config.problems_dir}/#{problem.name}" + end + + def save_source(submission,source_name) + dir = self.produce_grading_room(submission) + f = File.open("#{dir}/#{source_name}","w") + f.write(submission.source) + f.close + end + + def clean_up(submission) + end + end + + class SubmissionReporter + def initialize + @config = Grader::Configuration.get_instance + end + + def report(sub,test_result_dir) + save_result(sub,read_result(test_result_dir)) + end + + def report_error(sub,msg) + save_result(sub,{:points => 0, + :comment => "Grading error: #{msg}" }) + end + + protected + def read_result(test_result_dir) + cmp_msg_fname = "#{test_result_dir}/compiler_message" + if FileTest.exist?(cmp_msg_fname) + cmp_file = File.open(cmp_msg_fname) + cmp_msg = cmp_file.read + cmp_file.close + else + cmp_msg = "" + end + + result_fname = "#{test_result_dir}/result" + comment_fname = "#{test_result_dir}/comment" + if FileTest.exist?(result_fname) + comment = "" + begin + result_file = File.open(result_fname) + result = result_file.readline.to_i + result_file.close + rescue + result = 0 + comment = "error reading result file." + end + + begin + comment_file = File.open(comment_fname) + comment += comment_file.readline.chomp + comment_file.close + rescue + comment += "" + end + + return {:points => result, + :comment => comment, + :cmp_msg => cmp_msg} + else + if FileTest.exist?("#{test_result_dir}/a.out") + return {:points => 0, + :comment => 'error during grading', + :cmp_msg => cmp_msg} + else + return {:points => 0, + :comment => 'compilation error', + :cmp_msg => cmp_msg} + end + end + end + + def save_result(submission,result) + problem = submission.problem + submission.graded_at = Time.now.gmtime + points = result[:points] + submission.points = points + comment = @config.report_comment(result[:comment]) + + # + # TODO: FIX THIS MESSAGE + # + if problem == nil + submission.grader_comment = 'PASSED: ' + comment + '(problem is nil)' + elsif points == problem.full_score + #submission.grader_comment = 'PASSED: ' + comment + submission.grader_comment = comment + elsif result[:comment].chomp =~ /^[\[\]P]+$/ + submission.grader_comment = 'PASSED: ' + comment + '(inconsistent score)' + else + #submission.grader_comment = 'FAILED: ' + comment + submission.grader_comment = comment + end + submission.compiler_message = result[:cmp_msg] or '' + submission.save + end + + end + +end diff --git a/judge/scripts/lib/test_request_helper.rb b/judge/scripts/lib/test_request_helper.rb new file mode 100644 --- /dev/null +++ b/judge/scripts/lib/test_request_helper.rb @@ -0,0 +1,242 @@ +# +# This part contains various test_request helpers for interfacing +# with Grader::Engine. There are TestRequestRoomMaker and +# TestRequestReporter. + +module Grader + + # + # A TestRequestRoomMaker is a helper object for Engine + # - finds grading room: in user_result_dir/(user)/test_request/ ... + # - prepare problem configuration for grading --- basically it copy + # all config files, and copy user's input into the testcase + # directory. First, it finds the template from problem template + # directory; if it can't find a template, it'll use the template + # from default template. + class TestRequestRoomMaker + def initialize + @config = Grader::Configuration.get_instance + end + + def produce_grading_room(test_request) + grading_room = grading_room_dir(test_request) + FileUtils.mkdir_p(grading_room) + + # + # Also copy additional submitted file to this directory as well. + # The program would see this file only if it is copied + # to the sandbox directory later. The run script should do it. + # + if FileTest.exists?("#{test_request.input_file_name}.files") + cmd = "cp #{test_request.input_file_name}.files/* #{grading_room}" + system(cmd) + end + + grading_room + end + + def find_problem_home(test_request) + problem_name = test_request.problem_name + + template_dir = "#{@config.test_request_problem_templates_dir}/" + problem_name + + raise "Test Request: error template not found" if !File.exists?(template_dir) + + problem_home = problem_home_dir(test_request) + FileUtils.mkdir_p(problem_home) + + copy_problem_template(template_dir,problem_home) + link_input_file(test_request,problem_home) + + problem_home + end + + def save_source(test_request,source_name) + dir = self.produce_grading_room(test_request) + submission = test_request.submission + f = File.open("#{dir}/#{source_name}","w") + f.write(submission.source) + f.close + end + + def clean_up(test_request) + problem_home = problem_home_dir(test_request) + remove_data_files(problem_home) + end + + protected + def grading_room_dir(test_request) + problem_name = test_request.problem_name + user = test_request.user + grading_room = "#{@config.user_result_dir}" + + "/#{user.login}/test_request" + + "/#{problem_name}/#{test_request.id}" + grading_room + end + + def problem_home_dir(test_request) + problem_name = test_request.problem_name + user = test_request.user + "#{@config.user_result_dir}" + + "/#{user.login}/test_request/#{problem_name}" + end + + def copy_problem_template(template_dir,problem_home) + cmd = "cp -R #{template_dir}/* #{problem_home}" + system_and_raise_when_fail(cmd,"Test Request: cannot copy problem template") + end + + def link_input_file(test_request,problem_home) + input_fname = "#{test_request.input_file_name}" + if !File.exists?(input_fname) + raise "Test Request: input file not found." + end + + input_fname_problem_home = "#{problem_home}/test_cases/1/input-1.txt" + if File.exists?(input_fname_problem_home) + FileUtils.rm([input_fname_problem_home], :force => true) + end + + cmd = "ln -s #{input_fname} #{input_fname_problem_home}" + system_and_raise_when_fail(cmd,"Test Request: cannot link input file") + end + + def remove_data_files(problem_home) + if File.exists?("#{problem_home}/test_cases/1/input-1.txt") + cmd = "rm #{problem_home}/test_cases/1/*" + system_and_raise_when_fail(cmd,"Test Request: cannot remove data files") + end + end + + def system_and_raise_when_fail(cmd,msg) + if !system(cmd) + raise msg + end + end + + end + + class TestRequestReporter + def initialize + @config = Grader::Configuration.get_instance + end + + def report(test_request,test_result_dir) + save_result(test_request,read_result(test_result_dir)) + end + + def report_error(test_request, msg) + save_result(test_request, {:running_stat => { + :msg => "#{msg}", + :running_time => nil, + :exit_status => "Some error occured. Program did not run", + :memory_usage => nil + }}) + end + + protected + def read_result(test_result_dir) + # TODO: + cmp_msg_fname = "#{test_result_dir}/compiler_message" + cmp_file = File.open(cmp_msg_fname) + cmp_msg = cmp_file.read + cmp_file.close + + result_file_name = "#{test_result_dir}/1/result" + + if File.exists?(result_file_name) + output_file_name = "#{test_result_dir}/1/output.txt" + results = File.open("#{test_result_dir}/1/result").readlines + stat = extract_running_stat(results) + + return { + :output_file_name => output_file_name, + :running_stat => stat, + :comment => "", + :cmp_msg => cmp_msg} + else + return { + :running_stat => nil, + :comment => "Compilation error", + :cmp_msg => cmp_msg} + end + end + + def extract_running_stat(results) + running_stat_line = results[-1] + + # extract exit status line + run_stat = "" + if !(/[Cc]orrect/.match(results[0])) + run_stat = results[0].chomp + else + run_stat = 'Program exited normally' + end + + # extract running time + if res = /r(.*)u(.*)s/.match(running_stat_line) + seconds = (res[1].to_f + res[2].to_f) + time_stat = "Time used: #{seconds} sec." + else + seconds = nil + time_stat = "Time used: n/a sec." + end + + # extract memory usage + if res = /s(.*)m/.match(running_stat_line) + memory_used = res[1].to_i + else + memory_used = -1 + end + + return { + :msg => "#{run_stat}\n#{time_stat}", + :running_time => seconds, + :exit_status => run_stat, + :memory_usage => memory_used + } + end + + def save_result(test_request,result) + if result[:output_file_name]!=nil + test_request.output_file_name = link_output_file(test_request, + result[:output_file_name]) + end + test_request.graded_at = Time.now + test_request.compiler_message = (result[:cmp_msg] or '') + test_request.grader_comment = (result[:comment] or '') + if result[:running_stat]!=nil + test_request.running_stat = (result[:running_stat][:msg] or '') + test_request.running_time = (result[:running_stat][:running_time] or nil) + test_request.exit_status = result[:running_stat][:exit_status] + test_request.memory_usage = result[:running_stat][:memory_usage] + else + test_request.running_stat = '' + end + test_request.save + end + + protected + def link_output_file(test_request, fname) + target_file_name = random_output_file_name(test_request.user, + test_request.problem) + FileUtils.mkdir_p(File.dirname(target_file_name)) + cmd = "ln -s #{fname} #{target_file_name}" + if !system(cmd) + raise "TestRequestReporter: cannot move output file" + end + return target_file_name + end + + def random_output_file_name(user,problem) + problem_name = TestRequest.name_of(problem) + begin + tmpname = "#{@config.test_request_output_base_dir}" + + "/#{user.login}/#{problem_name}/#{rand(10000)}" + end while File.exists?(tmpname) + tmpname + end + + end + +end diff --git a/judge/scripts/new_problem b/judge/scripts/new_problem new file mode 100755 --- /dev/null +++ b/judge/scripts/new_problem @@ -0,0 +1,73 @@ +#!/usr/bin/ruby + +# new_problem: +# * creates a directory for a problem in the current directory, +# * create standard testcase config file + +require 'erb' + +def process_options(options) + i = 2 + while ii+1 + i += 1 + end + if ARGV[i]=='-m' + options[:mem_limit] = ARGV[i+1].to_i if ARGV.length>i+1 + i += 1 + end + i += 1 + end +end + + +puts "This script is out of dated, shall be fixed soon" +puts "Right now, you can create raw_ev and import" +exit(0) + +GRADER_DIR = File.dirname(__FILE__) + +# print usage +if ARGV.length < 2 + puts < 1, :mem_limit => 16} +process_options(options) + +# start working +puts "creating directories" + +system("mkdir #{problem}") +system("mkdir #{problem}/script") +system("mkdir #{problem}/test_cases") + +puts "creating testcases directories" + +1.upto(num_testcases) do |i| + system("mkdir #{problem}/test_cases/#{i}") +end + +# generating all_tests.cfg +puts "generating testcase config file" + +template = File.open(File.dirname(__FILE__) + "/templates/all_tests.cfg.erb").read +all_test_cfg = ERB.new(template) + +cfg_file = File.open("#{problem}/test_cases/all_tests.cfg","w") +cfg_file.puts all_test_cfg.result +cfg_file.close + +puts "done" diff --git a/judge/scripts/std-script/box.cc b/judge/scripts/std-script/box.cc new file mode 100644 --- /dev/null +++ b/judge/scripts/std-script/box.cc @@ -0,0 +1,684 @@ +/* + * A Simple Testing Sandbox + * + * (c) 2001--2004 Martin Mares + */ + +#define _LARGEFILE64_SOURCE +//#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NONRET __attribute__((noreturn)) +#define UNUSED __attribute__((unused)) + +static int filter_syscalls; /* 0=off, 1=liberal, 2=totalitarian */ +static double timeout; +static int pass_environ; +static int use_wall_clock; +static int file_access; +static int verbose; +static int memory_limit; +static int allow_times; +static char *redir_stdin, *redir_stdout; +static char *set_cwd; + +static pid_t box_pid; +static int is_ptraced; +static volatile int timer_tick; +static time_t start_time; +static int ticks_per_sec; +static int page_size; + +#if defined(__GLIBC__) && __GLIBC__ == 2 && __GLIBC_MINOR__ > 0 +/* glibc 2.1 or newer -> has lseek64 */ +#define long_seek(f,o,w) lseek64(f,o,w) +#else +/* Touching clandestine places in glibc */ +extern loff_t llseek(int fd, loff_t pos, int whence); +#define long_seek(f,o,w) llseek(f,o,w) +#endif + +int max_mem_used = 0; + +void print_running_stat(double wall_time, + double user_time, + double system_time, + int mem_usage) +{ + fprintf(stderr,"%.4lfr%.4lfu%.4lfs%dm\n", + wall_time, user_time, system_time, mem_usage); +} + +static void NONRET +box_exit(void) +{ + if (box_pid > 0) { + if (is_ptraced) + ptrace(PTRACE_KILL, box_pid); + kill(-box_pid, SIGKILL); + kill(box_pid, SIGKILL); + } + + struct timeval total; + int wall; + struct rusage rus; + int stat; + pid_t p; + + // wait so that we can get information + p = wait4(box_pid, &stat, WUNTRACED, &rus); + if (p < 0) { + fprintf(stderr,"wait4: error\n"); + print_running_stat(0,0,0,max_mem_used); + } else if (p != box_pid) { + fprintf(stderr,"wait4: unknown pid %d exited!\n", p); + print_running_stat(0,0,0,max_mem_used); + } else { + if (!WIFEXITED(stat)) + fprintf(stderr,"wait4: unknown status\n"); + struct timeval total; + int wall; + wall = time(NULL) - start_time; + timeradd(&rus.ru_utime, &rus.ru_stime, &total); + + print_running_stat((double)wall, + (double) rus.ru_utime.tv_sec + + ((double) rus.ru_utime.tv_usec/1000000.0), + (double) rus.ru_stime.tv_sec + + ((double) rus.ru_stime.tv_usec/1000000.0), + max_mem_used); + } + exit(1); +} + +static void NONRET __attribute__((format(printf,1,2))) +die(char *msg, ...) +{ + va_list args; + va_start(args, msg); + vfprintf(stderr, msg, args); + fputc('\n', stderr); + box_exit(); +} + +static void __attribute__((format(printf,1,2))) +log(char *msg, ...) +{ + va_list args; + va_start(args, msg); + if (verbose) + { + vfprintf(stderr, msg, args); + fflush(stderr); + } + va_end(args); +} + +static void +valid_filename(unsigned long addr) +{ + char namebuf[4096], *p, *end; + static int mem_fd; + + if (!file_access) + die("File access forbidden."); + if (file_access >= 9) + return; + + if (!mem_fd) + { + sprintf(namebuf, "/proc/%d/mem", (int) box_pid); + mem_fd = open(namebuf, O_RDONLY); + if (mem_fd < 0) + die("open(%s): %m", namebuf); + } + p = end = namebuf; + do + { + if (p >= end) + { + int remains = PAGE_SIZE - (addr & (PAGE_SIZE-1)); + int l = namebuf + sizeof(namebuf) - end; + if (l > remains) + l = remains; + if (!l) + die("Access to file with name too long."); + if (long_seek(mem_fd, addr, SEEK_SET) < 0) + die("long_seek(mem): %m"); + remains = read(mem_fd, end, l); + if (remains < 0) + die("read(mem): %m"); + if (!remains) + die("Access to file with name out of memory."); + end += l; + addr += l; + } + } + while (*p++); + + log("[%s] ", namebuf); + if (file_access >= 3) + return; + if (!strchr(namebuf, '/') && strcmp(namebuf, "..")) + return; + if (file_access >= 2) + { + if ((!strncmp(namebuf, "/etc/", 5) || + !strncmp(namebuf, "/lib/", 5) || + !strncmp(namebuf, "/usr/lib/", 9)) + && !strstr(namebuf, "..")) + return; + if (!strcmp(namebuf, "/dev/null") || + !strcmp(namebuf, "/dev/zero") || + !strcmp(namebuf, "/proc/meminfo") || + !strcmp(namebuf, "/proc/self/stat") || + !strncmp(namebuf, "/usr/share/zoneinfo/", 20)) + return; + } + die("Forbidden access to file `%s'.", namebuf); +} + +static int +valid_syscall(struct user *u) +{ + switch (u->regs.orig_eax) + { + case __NR_execve: + { + static int exec_counter; + return !exec_counter++; + } + case __NR_open: + case __NR_creat: + case __NR_unlink: + case __NR_oldstat: + case __NR_access: + case __NR_oldlstat: + case __NR_truncate: + case __NR_stat: + case __NR_lstat: + case __NR_truncate64: + case __NR_stat64: + case __NR_lstat64: + valid_filename(u->regs.ebx); + return 1; + case __NR_exit: + case __NR_read: + case __NR_write: + case __NR_close: + case __NR_lseek: + case __NR_getpid: + case __NR_getuid: + case __NR_oldfstat: + case __NR_dup: + case __NR_brk: + case __NR_getgid: + case __NR_geteuid: + case __NR_getegid: + case __NR_dup2: + case __NR_ftruncate: + case __NR_fstat: + case __NR_personality: + case __NR__llseek: + case __NR_readv: + case __NR_writev: + case __NR_getresuid: +#ifdef __NR_pread64 + case __NR_pread64: + case __NR_pwrite64: +#else + case __NR_pread: + case __NR_pwrite: +#endif + case __NR_ftruncate64: + case __NR_fstat64: + case __NR_fcntl: + case __NR_fcntl64: + case __NR_mmap: + case __NR_munmap: + case __NR_ioctl: + case __NR_uname: + case 252: + case 243: +// added for free pascal + case __NR_ugetrlimit: + case __NR_readlink: + return 1; + // case __NR_time: + case __NR_alarm: + // case __NR_pause: + case __NR_signal: + case __NR_fchmod: + case __NR_sigaction: + case __NR_sgetmask: + case __NR_ssetmask: + case __NR_sigsuspend: + case __NR_sigpending: + case __NR_getrlimit: + case __NR_getrusage: + case __NR_gettimeofday: + case __NR_select: + case __NR_readdir: + case __NR_setitimer: + case __NR_getitimer: + case __NR_sigreturn: + case __NR_mprotect: + case __NR_sigprocmask: + case __NR_getdents: + case __NR_getdents64: + case __NR__newselect: + case __NR_fdatasync: + case __NR_mremap: + case __NR_poll: + case __NR_getcwd: + case __NR_nanosleep: + case __NR_rt_sigreturn: + case __NR_rt_sigaction: + case __NR_rt_sigprocmask: + case __NR_rt_sigpending: + case __NR_rt_sigtimedwait: + case __NR_rt_sigqueueinfo: + case __NR_rt_sigsuspend: + case __NR_mmap2: + case __NR__sysctl: + return (filter_syscalls == 1); + case __NR_times: + case __NR_time: + return allow_times; + case __NR_kill: + if (u->regs.ebx == box_pid) + die("Commited suicide by signal %d.", (int)u->regs.ecx); + return 0; + default: + return 0; + } +} + +static void +signal_alarm(int unused UNUSED) +{ + /* Time limit checks are synchronous, so we only schedule them there. */ + timer_tick = 1; + + //NOTE: do not use alarm, changed to setitimer for precision + // alarm(1); +} + +static void +signal_int(int unused UNUSED) +{ + /* Interrupts are fatal, so no synchronization requirements. */ + die("Interrupted."); +} + +static void +check_timeout(void) +{ + double sec; + + if (use_wall_clock) + sec = (double)(time(NULL) - start_time); + else + { + char buf[4096], *x; + int c, utime, stime; + static int proc_status_fd; + if (!proc_status_fd) + { + sprintf(buf, "/proc/%d/stat", (int) box_pid); + proc_status_fd = open(buf, O_RDONLY); + if (proc_status_fd < 0) + die("open(%s): %m", buf); + } + lseek(proc_status_fd, 0, SEEK_SET); + if ((c = read(proc_status_fd, buf, sizeof(buf)-1)) < 0) + die("read on /proc/$pid/stat: %m"); + if (c >= (int) sizeof(buf) - 1) + die("/proc/$pid/stat too long"); + buf[c] = 0; + x = buf; + while (*x && *x != ' ') + x++; + while (*x == ' ') + x++; + if (*x++ != '(') + die("proc syntax error 1"); + while (*x && (*x != ')' || x[1] != ' ')) + x++; + while (*x == ')' || *x == ' ') + x++; + if (sscanf(x, "%*c %*d %*d %*d %*d %*d %*d %*d %*d %*d %*d %d %d", &utime, &stime) != 2) + die("proc syntax error 2"); + //printf("%s - %d\n",x,ticks_per_sec); + sec = ((double)(utime + stime))/(double)ticks_per_sec; + } + if (verbose > 1) + fprintf(stderr, "[timecheck: %d seconds]\n", sec); + if (sec > timeout) { + die("Time limit exceeded.",sec,timeout); + } +} + +static void +check_memory_usage() +{ + char proc_fname[100]; + sprintf(proc_fname,"/proc/%d/statm",box_pid); + //printf("proc fname: %s\n",proc_fname); + FILE *fp = fopen(proc_fname,"r"); + if(fp!=NULL) { + char line[1000]; + fgets(line,999,fp); + //printf("%s\n",line); + int m; + + if(sscanf(line,"%d",&m)==1) { + m = (m*page_size+1023)/1024; + if(m>max_mem_used) + max_mem_used = m; + } + + fclose(fp); + } +} + +static void +boxkeeper(void) +{ + int syscall_count = 0; + struct sigaction sa; + + is_ptraced = 1; + bzero(&sa, sizeof(sa)); + sa.sa_handler = signal_int; + sigaction(SIGINT, &sa, NULL); + start_time = time(NULL); + ticks_per_sec = sysconf(_SC_CLK_TCK); + page_size = getpagesize(); + if (ticks_per_sec <= 0) + die("Invalid ticks_per_sec!"); + + check_memory_usage(); + + sa.sa_handler = signal_alarm; + sigaction(SIGALRM, &sa, NULL); + //alarm(1); + + struct itimerval val; + val.it_interval.tv_sec = 0; + val.it_interval.tv_usec = 50000; + val.it_value.tv_sec = 0; + val.it_value.tv_usec = 50000; + setitimer(ITIMER_REAL,&val,NULL); + + /* + --- add alarm handler no matter what.. + if (timeout) + { + sa.sa_handler = signal_alarm; + sigaction(SIGALRM, &sa, NULL); + alarm(1); + } + */ + + for(;;) + { + struct rusage rus; + int stat; + pid_t p; + + if (timer_tick) + { + check_timeout(); + check_memory_usage(); + timer_tick = 0; + } + p = wait4(box_pid, &stat, WUNTRACED, &rus); + + if (p < 0) + { + if (errno == EINTR) + continue; + die("wait4: %m"); + } + if (p != box_pid) + die("wait4: unknown pid %d exited!", p); + if (WIFEXITED(stat)) + { + struct timeval total; + int wall; + wall = time(NULL) - start_time; + timeradd(&rus.ru_utime, &rus.ru_stime, &total); + + box_pid = 0; + if (WEXITSTATUS(stat)) + fprintf(stderr,"Exited with error status %d.\n", WEXITSTATUS(stat)); + else if ((use_wall_clock ? + wall : + (double) total.tv_sec + + ((double) total.tv_usec/1000000.0)) > timeout) + fprintf(stderr,"Time limit exceeded.\n"); + else + // report OK and statistics + fprintf(stderr,"OK\n"); + + print_running_stat((double) wall, + (double) rus.ru_utime.tv_sec + + ((double) rus.ru_utime.tv_usec/1000000.0), + (double) rus.ru_stime.tv_sec + + ((double) rus.ru_stime.tv_usec/1000000.0), + max_mem_used); +/* + (%.4lf sec real (%d), %d sec wall, %d syscalls, %d kb)\n", + (double) total.tv_sec + ((double)total.tv_usec / 1000000.0), + (int) total.tv_usec, + wall, + syscall_count, + max_mem_used); +*/ + exit(0); + } + if (WIFSIGNALED(stat)) + { + box_pid = 0; + fprintf(stderr,"Caught fatal signal %d.\n", WTERMSIG(stat)); + + struct timeval total; + int wall; + wall = time(NULL) - start_time; + timeradd(&rus.ru_utime, &rus.ru_stime, &total); + print_running_stat((double) wall, + (double) rus.ru_utime.tv_sec + + ((double) rus.ru_utime.tv_usec/1000000.0), + (double) rus.ru_stime.tv_sec + + ((double) rus.ru_stime.tv_usec/1000000.0), + max_mem_used); + exit(0); + } + if (WIFSTOPPED(stat)) + { + int sig = WSTOPSIG(stat); + if (sig == SIGTRAP) + { + struct user u; + static int stop_count = -1; + if (ptrace(PTRACE_GETREGS, box_pid, NULL, &u) < 0) + die("ptrace(PTRACE_GETREGS): %m"); + stop_count++; + if (!stop_count) /* Traceme request */ + log(">> Traceme request caught\n"); + else if (stop_count & 1) /* Syscall entry */ + { + log(">> Syscall %3ld (%08lx,%08lx,%08lx) ", u.regs.orig_eax, u.regs.ebx, u.regs.ecx, u.regs.edx); + syscall_count++; + if (!valid_syscall(&u)) + { + /* + * Unfortunately, PTRACE_KILL kills _after_ the syscall completes, + * so we have to change it to something harmless (e.g., an undefined + * syscall) and make the program continue. + */ + unsigned int sys = u.regs.orig_eax; + u.regs.orig_eax = 0xffffffff; + if (ptrace(PTRACE_SETREGS, box_pid, NULL, &u) < 0) + die("ptrace(PTRACE_SETREGS): %m"); + die("Forbidden syscall %d.", sys); + } + } + else /* Syscall return */ + log("= %ld\n", u.regs.eax); + ptrace(PTRACE_SYSCALL, box_pid, 0, 0); + } + else if (sig != SIGSTOP && sig != SIGXCPU && sig != SIGXFSZ) + { + log(">> Signal %d\n", sig); + ptrace(PTRACE_SYSCALL, box_pid, 0, sig); + } + else + die("Received signal %d.", sig); + } + else + die("wait4: unknown status %x, giving up!", stat); + } +} + +static void +box_inside(int argc, char **argv) +{ + struct rlimit rl; + char *args[argc+1]; + char *env[1] = { NULL }; + + memcpy(args, argv, argc * sizeof(char *)); + args[argc] = NULL; + if (set_cwd && chdir(set_cwd)) + die("chdir: %m"); + if (redir_stdin) + { + close(0); + if (open(redir_stdin, O_RDONLY) != 0) + die("open(\"%s\"): %m", redir_stdin); + } + if (redir_stdout) + { + close(1); + if (open(redir_stdout, O_WRONLY | O_CREAT | O_TRUNC, 0666) != 1) + die("open(\"%s\"): %m", redir_stdout); + } + dup2(1, 2); + setpgrp(); + if (memory_limit) + { + rl.rlim_cur = rl.rlim_max = memory_limit * 1024; + if (setrlimit(RLIMIT_AS, &rl) < 0) + die("setrlimit: %m"); + } + rl.rlim_cur = rl.rlim_max = 64; + if (setrlimit(RLIMIT_NOFILE, &rl) < 0) + die("setrlimit: %m"); + if (filter_syscalls && ptrace(PTRACE_TRACEME) < 0) + die("ptrace(PTRACE_TRACEME): %m"); + execve(args[0], args, (pass_environ ? environ : env)); + die("execve(\"%s\"): %m", args[0]); +} + +static void +usage(void) +{ + fprintf(stderr, "Invalid arguments!\n"); + printf("\ +Usage: box [] -- \n\ +\n\ +Options:\n\ +-a \tSet file access level (0=none, 1=cwd, 2=/etc,/lib,..., 3=whole fs, 9=no checks; needs -f)\n\ +-c \tChange directory to first\n\ +-e\t\tPass full environment of parent process\n\ +-f\t\tFilter system calls (-ff=very restricted)\n\ +-i \tRedirect stdin from \n\ +-m \tLimit address space to KB\n\ +-o \tRedirect stdout to \n\ +-t