Judge0Tsting / app /jobs /isolate_job.rb
AdityaSharmaTech's picture
Upload 177 files
def1299 verified
class IsolateJob < ApplicationJob
retry_on RuntimeError, wait: 0.1.seconds, attempts: 100
queue_as ENV["JUDGE0_VERSION"].to_sym
STDIN_FILE_NAME = "stdin.txt"
STDOUT_FILE_NAME = "stdout.txt"
STDERR_FILE_NAME = "stderr.txt"
METADATA_FILE_NAME = "metadata.txt"
ADDITIONAL_FILES_ARCHIVE_FILE_NAME = "additional_files.zip"
attr_reader :submission, :cgroups,
:box_id, :workdir, :boxdir, :tmpdir,
:source_file, :stdin_file, :stdout_file,
:stderr_file, :metadata_file, :additional_files_archive_file
def perform(submission_id)
@submission = Submission.find(submission_id)
submission.update(status: Status.process, started_at: DateTime.now, execution_host: ENV["HOSTNAME"])
time = []
memory = []
submission.number_of_runs.times do
initialize_workdir
if compile == :failure
cleanup
return
end
run
verify
time << submission.time
memory << submission.memory
cleanup
break if submission.status != Status.ac
end
submission.time = time.inject(&:+).to_f / time.size
submission.memory = memory.inject(&:+).to_f / memory.size
submission.save
rescue Exception => e
raise e.message unless submission
submission.update(message: e.message, status: Status.boxerr, finished_at: DateTime.now)
cleanup(raise_exception = false)
ensure
call_callback
end
private
def initialize_workdir
@box_id = submission.id%2147483647
@cgroups = (!submission.enable_per_process_and_thread_time_limit || !submission.enable_per_process_and_thread_memory_limit) ? "--cg" : ""
@workdir = `isolate #{cgroups} -b #{box_id} --init`.chomp
@boxdir = workdir + "/box"
@tmpdir = workdir + "/tmp"
@source_file = boxdir + "/" + submission.language.source_file.to_s
@stdin_file = workdir + "/" + STDIN_FILE_NAME
@stdout_file = workdir + "/" + STDOUT_FILE_NAME
@stderr_file = workdir + "/" + STDERR_FILE_NAME
@metadata_file = workdir + "/" + METADATA_FILE_NAME
@additional_files_archive_file = boxdir + "/" + ADDITIONAL_FILES_ARCHIVE_FILE_NAME
[stdin_file, stdout_file, stderr_file, metadata_file].each do |f|
initialize_file(f)
end
File.open(source_file, "wb") { |f| f.write(submission.source_code) } unless submission.is_project
File.open(stdin_file, "wb") { |f| f.write(submission.stdin) }
extract_archive
end
def initialize_file(file)
`sudo touch #{file} && sudo chown $(whoami): #{file}`
end
def extract_archive
return unless submission.additional_files?
File.open(additional_files_archive_file, "wb") { |f| f.write(submission.additional_files) }
command = "isolate #{cgroups} \
-s \
-b #{box_id} \
--stderr-to-stdout \
-t 2 \
-x 1 \
-w 4 \
-k #{Config::MAX_STACK_LIMIT} \
-p#{Config::MAX_MAX_PROCESSES_AND_OR_THREADS} \
#{submission.enable_per_process_and_thread_time_limit ? (cgroups.present? ? "--no-cg-timing" : "") : "--cg-timing"} \
#{submission.enable_per_process_and_thread_memory_limit ? "-m " : "--cg-mem="}#{Config::MAX_MEMORY_LIMIT} \
-f #{Config::MAX_EXTRACT_SIZE} \
--run \
-- /usr/bin/unzip -n -qq #{ADDITIONAL_FILES_ARCHIVE_FILE_NAME} \
"
puts "[#{DateTime.now}] Extracting archive for submission #{submission.token} (#{submission.id}):"
puts command.gsub(/\s+/, " ")
puts
`#{command}`
File.delete(additional_files_archive_file)
end
def compile
unless submission.is_project
return :success unless submission.language.compile_cmd
end
compile_script = boxdir + "/" + "compile.sh"
acceptable_project_compile_scripts = [compile_script, boxdir + "/" + "compile"]
if submission.is_project
compile_file_exists = false
acceptable_project_compile_scripts.each do |f|
if File.file?(f)
compile_script = f
compile_file_exists = true
break
end
end
unless compile_file_exists
return :success # If compile script does not exist then this project does not need to be compiled.
end
else
# gsub can be skipped if compile script is used, but is kept for additional security.
compiler_options = submission.compiler_options.to_s.strip.encode("UTF-8", invalid: :replace).gsub(/[$&;<>|`]/, "")
File.open(compile_script, "w") { |f| f.write("#{submission.language.compile_cmd % compiler_options}") }
end
compile_output_file = workdir + "/" + "compile_output.txt"
initialize_file(compile_output_file)
command = "isolate #{cgroups} \
-s \
-b #{box_id} \
-M #{metadata_file} \
--stderr-to-stdout \
-i /dev/null \
-t #{Config::MAX_CPU_TIME_LIMIT} \
-x 0 \
-w #{Config::MAX_WALL_TIME_LIMIT} \
-k #{Config::MAX_STACK_LIMIT} \
-p#{Config::MAX_MAX_PROCESSES_AND_OR_THREADS} \
#{submission.enable_per_process_and_thread_time_limit ? (cgroups.present? ? "--no-cg-timing" : "") : "--cg-timing"} \
#{submission.enable_per_process_and_thread_memory_limit ? "-m " : "--cg-mem="}#{Config::MAX_MEMORY_LIMIT} \
-f #{Config::MAX_MAX_FILE_SIZE} \
-E HOME=/tmp \
-E PATH=\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\" \
-E LANG -E LANGUAGE -E LC_ALL -E JUDGE0_HOMEPAGE -E JUDGE0_SOURCE_CODE -E JUDGE0_MAINTAINER -E JUDGE0_VERSION \
-d /etc:noexec \
--run \
-- /bin/bash $(basename #{compile_script}) > #{compile_output_file} \
"
puts "[#{DateTime.now}] Compiling submission #{submission.token} (#{submission.id}):"
puts command.gsub(/\s+/, " ")
puts
`#{command}`
process_status = $?
compile_output = File.read(compile_output_file)
compile_output = nil if compile_output.empty?
submission.compile_output = compile_output
metadata = get_metadata
reset_metadata_file
files_to_remove = [compile_output_file]
files_to_remove << compile_script unless submission.is_project
files_to_remove.each do |f|
`sudo rm -rf #{f}`
end
return :success if process_status.success?
if metadata[:status] == "TO"
submission.compile_output = "Compilation time limit exceeded."
end
submission.finished_at = DateTime.now
submission.time = nil
submission.wall_time = nil
submission.memory = nil
submission.stdout = nil
submission.stderr = nil
submission.exit_code = nil
submission.exit_signal = nil
submission.message = nil
submission.status = Status.ce
submission.save
return :failure
end
def run
run_script = boxdir + "/" + "run.sh"
acceptable_project_run_scripts = [run_script, boxdir + "/" + "run"]
acceptable_project_run_scripts.each do |f|
if File.file?(f)
run_script = f
break
end
end
unless submission.is_project
# gsub is mandatory!
command_line_arguments = submission.command_line_arguments.to_s.strip.encode("UTF-8", invalid: :replace).gsub(/[$&;<>|`]/, "")
File.open(run_script, "w") { |f| f.write("#{submission.language.run_cmd} #{command_line_arguments}")}
end
command = "isolate #{cgroups} \
-s \
-b #{box_id} \
-M #{metadata_file} \
#{submission.redirect_stderr_to_stdout ? "--stderr-to-stdout" : ""} \
#{submission.enable_network ? "--share-net" : ""} \
-t #{submission.cpu_time_limit} \
-x #{submission.cpu_extra_time} \
-w #{submission.wall_time_limit} \
-k #{submission.stack_limit} \
-p#{submission.max_processes_and_or_threads} \
#{submission.enable_per_process_and_thread_time_limit ? (cgroups.present? ? "--no-cg-timing" : "") : "--cg-timing"} \
#{submission.enable_per_process_and_thread_memory_limit ? "-m " : "--cg-mem="}#{submission.memory_limit} \
-f #{submission.max_file_size} \
-E HOME=/tmp \
-E PATH=\"/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\" \
-E LANG -E LANGUAGE -E LC_ALL -E JUDGE0_HOMEPAGE -E JUDGE0_SOURCE_CODE -E JUDGE0_MAINTAINER -E JUDGE0_VERSION \
-d /etc:noexec \
--run \
-- /bin/bash $(basename #{run_script}) \
< #{stdin_file} > #{stdout_file} 2> #{stderr_file} \
"
puts "[#{DateTime.now}] Running submission #{submission.token} (#{submission.id}):"
puts command.gsub(/\s+/, " ")
puts
`#{command}`
`sudo rm #{run_script}` unless submission.is_project
end
def verify
submission.finished_at = DateTime.now
metadata = get_metadata
program_stdout = File.read(stdout_file)
program_stdout = nil if program_stdout.empty?
program_stderr = File.read(stderr_file)
program_stderr = nil if program_stderr.empty?
submission.time = metadata[:time]
submission.wall_time = metadata[:"time-wall"]
submission.memory = (cgroups.present? ? metadata[:"cg-mem"] : metadata[:"max-rss"])
submission.stdout = program_stdout
submission.stderr = program_stderr
submission.exit_code = metadata[:exitcode].try(:to_i) || 0
submission.exit_signal = metadata[:exitsig].try(:to_i)
submission.message = metadata[:message]
submission.status = determine_status(metadata[:status], submission.exit_signal)
# After adding support for compiler_options and command_line_arguments
# status "Exec Format Error" will no longer occur because compile and run
# is done inside a dynamically created bash script, thus isolate doesn't call
# execve directily on submission.language.compile_cmd or submission.langauge.run_cmd.
# Consequence of running compile and run through bash script is that when
# target binary is not found then submission gets status "Runtime Error (NZEC)".
#
# I think this is for now O.K. behaviour, but I will leave this if block
# here until I am 100% sure that "Exec Format Error" can be deprecated.
if submission.status == Status.boxerr &&
(
submission.message.to_s.match(/^execve\(.+\): Exec format error$/) ||
submission.message.to_s.match(/^execve\(.+\): No such file or directory$/) ||
submission.message.to_s.match(/^execve\(.+\): Permission denied$/)
)
submission.status = Status.exeerr
end
end
def cleanup(raise_exception = true)
fix_permissions
`sudo rm -rf #{boxdir}/* #{tmpdir}/*`
[stdin_file, stdout_file, stderr_file, metadata_file].each do |f|
`sudo rm -rf #{f}`
end
`isolate #{cgroups} -b #{box_id} --cleanup`
raise "Cleanup of sandbox #{box_id} failed." if raise_exception && Dir.exists?(workdir)
end
def reset_metadata_file
`sudo rm -rf #{metadata_file}`
initialize_file(metadata_file)
end
def fix_permissions
`sudo chown -R $(whoami): #{boxdir}`
end
def call_callback
return unless submission.callback_url.present?
serialized_submission = ActiveModelSerializers::SerializableResource.new(
submission,
{
serializer: SubmissionSerializer,
base64_encoded: true,
fields: SubmissionSerializer.default_fields
}
).to_json
Config::CALLBACKS_MAX_TRIES.times do
begin
response = HTTParty.put(
submission.callback_url,
body: serialized_submission,
headers: {
"Content-Type" => "application/json"
},
timeout: Config::CALLBACKS_TIMEOUT
)
break
rescue Exception => e
end
end
rescue Exception => e
end
def get_metadata
metadata = File.read(metadata_file).split("\n").collect do |e|
{ e.split(":").first.to_sym => e.split(":")[1..-1].join(":") }
end.reduce({}, :merge)
return metadata
end
def determine_status(status, exit_signal)
if status == "TO"
return Status.tle
elsif status == "SG"
return Status.find_runtime_error_by_status_code(exit_signal)
elsif status == "RE"
return Status.nzec
elsif status == "XX"
return Status.boxerr
elsif submission.expected_output.nil? || strip(submission.expected_output) == strip(submission.stdout)
return Status.ac
else
return Status.wa
end
end
def strip(text)
return nil unless text
text.split("\n").collect(&:rstrip).join("\n").rstrip
rescue ArgumentError
return text
end
end