Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try to add proxy-judge feature #34

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
a21b192
Add basic proxy judge feature
OmeletWithoutEgg Jun 19, 2024
8f99ba6
Add parameters for proxy judge
OmeletWithoutEgg Jun 19, 2024
b580273
Add another type of proxy judge
OmeletWithoutEgg Jun 19, 2024
e1b51f3
Change proxy judge type to enum and rewrite related logics
OmeletWithoutEgg Jun 20, 2024
2d30a5b
Update proxy judge job
OmeletWithoutEgg Jun 21, 2024
c04af57
Fix missing bracket in problem edit
OmeletWithoutEgg Jun 21, 2024
9730cb2
Add different languages to proxy judge file
OmeletWithoutEgg Jun 21, 2024
7ef72d3
Add basic description in problem form
OmeletWithoutEgg Jun 21, 2024
4807d6c
Decouple proxy judge submit and fetch result
adrien1018 Jun 24, 2024
f16aea4
Revise problem settings UI
adrien1018 Jun 24, 2024
cd3dad7
Fix collation in migration
adrien1018 Jun 24, 2024
0887001
Rename to proxyjudge
adrien1018 Jun 25, 2024
5c01d76
Complete the code of decoupling proxyjudge jobs
OmeletWithoutEgg Jul 6, 2024
e1a60b2
Use fetch_results inferface
adrien1018 Jul 18, 2024
6576db7
Rename CF to codeforces
adrien1018 Jul 22, 2024
df3e536
Make proxyjudge work
adrien1018 Jul 22, 2024
ec0abfe
Disable edit & rejudge in proxy judge problems
adrien1018 Jul 22, 2024
afa202f
Add new proxy judge type QOJ
OmeletWithoutEgg Aug 29, 2024
555b9a4
Set ban_compiler when updating problems with proxyjudge
OmeletWithoutEgg Oct 29, 2024
5bffd98
Use nonce to identify QOJ proxy-judge submissions
OmeletWithoutEgg Nov 2, 2024
4acc7c7
Try to fix encoding error when using mechanize
OmeletWithoutEgg Nov 9, 2024
7285812
Fix tioj-proxy-nonce exposing
OmeletWithoutEgg Nov 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ group :doc do
end

gem 'mechanize'
gem 'rest-client' # for proxy judge

# friendly id for SEO
gem 'friendly_id'
Expand Down
10 changes: 9 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ GEM
actionpack (>= 5.2)
activesupport (>= 5.2)
hashie (5.0.0)
http-accept (1.7.0)
http-cookie (1.0.5)
domain_name (~> 0.5)
i18n (1.14.4)
Expand Down Expand Up @@ -243,6 +244,7 @@ GEM
timeout
net-smtp (0.5.0)
net-protocol
netrc (0.11.0)
nio4r (2.7.1)
nkf (0.2.0)
nokogiri (1.16.3)
Expand Down Expand Up @@ -324,6 +326,11 @@ GEM
responders (3.1.1)
actionpack (>= 5.2)
railties (>= 5.2)
rest-client (2.1.0)
http-accept (>= 1.7.0, < 2.0)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rexml (3.2.6)
ruby-vips (2.2.1)
ffi (~> 1.12)
Expand Down Expand Up @@ -362,7 +369,7 @@ GEM
activesupport (>= 5.2)
sprockets (>= 3.0.0)
ssrf_filter (1.1.2)
stringio (3.1.0)
stringio (3.1.1)
terser (1.2.0)
execjs (>= 0.3.0, < 3)
thor (1.3.1)
Expand Down Expand Up @@ -427,6 +434,7 @@ DEPENDENCIES
rdoc (~> 6)
redcarpet
redis (~> 4)
rest-client
rubyzip
sassc-rails
sdoc
Expand Down
4 changes: 2 additions & 2 deletions app/channels/fetch_channel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ def report_queued(data)
Submission.where(id: data[:submission_ids]).update_all(updated_at: Time.now)
# requeue dead submissions
retry_op do |is_first|
Submission.where(result: ["received", "Validating"], updated_at: ..40.second.ago).update_all(result: "queued")
Submission.where(result: ["received", "Validating"], updated_at: ..40.second.ago, proxyjudge_type: :none).update_all(result: "queued")
end
end

def fetch_submission(data)
n_retry = 5
for i in 1..n_retry
submission = Submission.where(result: "queued").order(priority: :desc, id: :asc).first
submission = Submission.where(result: "queued", proxyjudge_type: :none).order(priority: :desc, id: :asc).first
flag = false
if submission
retry_op(3) do |is_first|
Expand Down
17 changes: 17 additions & 0 deletions app/controllers/problems_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ def delete_submissions
end

def rejudge
if @problem.proxyjudge_any?
redirect_back fallback_location: root_path, alert: 'This action is disabled in proxy judge problems.'
return
end
subs = Submission.where(problem_id: params[:id], contest_id: @contest ? @contest.id : nil)
sub_ids = subs.pluck(:id)
SubmissionTestdataResult.where(submission_id: sub_ids).delete_all
Expand Down Expand Up @@ -103,6 +107,7 @@ def edit
def create
params[:problem][:compiler_ids] ||= []
@problem = Problem.new(check_params())
set_proxyjudge_ban_compiler
@ban_compiler_ids = params[:problem][:compiler_ids].map(&:to_i).to_set
respond_to do |format|
if @problem.save
Expand All @@ -120,6 +125,7 @@ def update
@ban_compiler_ids = params[:problem][:compiler_ids].map(&:to_i).to_set
respond_to do |format|
@problem.attributes = check_params()
set_proxyjudge_ban_compiler
pre_ids = @problem.subtasks.collect(&:id)
changed = @problem.subtasks.any? {|x| x.score_changed? || x.td_list_changed?}
changed ||= @problem.score_precision_changed?
Expand Down Expand Up @@ -219,6 +225,15 @@ def check_params
params
end

def set_proxyjudge_ban_compiler
if @problem.proxyjudge_any? then
valid_compiler_names = @problem.proxyjudge_class::PROXY_COMPILERS.keys()
proxyjudge_ban_compilers = Compiler.where.not(name: valid_compiler_names)
@problem.compilers = \
(@problem.compilers.to_set | proxyjudge_ban_compilers.to_set).to_a
end
end

# Never trust parameters from the scary internet, only allow the white list through.
def problem_params
params.require(:problem).permit(
Expand Down Expand Up @@ -256,6 +271,8 @@ def problem_params
:ranklist_display_score,
:strict_mode,
:skip_group,
:proxyjudge_type,
:proxyjudge_args,
sample_testdata_attributes:
[
:id,
Expand Down
20 changes: 19 additions & 1 deletion app/controllers/submissions_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class SubmissionsController < ApplicationController
before_action :check_compiler, only: [:create, :update]
before_action :normalize_code, only: [:create, :update]
before_action :set_show_attrs, only: [:show, :show_old]
before_action :check_proxyjudge, only: [:edit, :update, :rejudge]
layout :set_contest_layout, only: [:show, :index, :new, :edit]

def rejudge
Expand Down Expand Up @@ -97,10 +98,20 @@ def create
@submission.contest = @contest
@submission.generate_subtask_result
@submission.priority = @contest ? Submission::PRIORITY[:contest] : Submission::PRIORITY[:normal]
@submission.proxyjudge_type = @problem.proxyjudge_type

if @problem.proxyjudge_any?
@submission.proxyjudge_nonce = SecureRandom.hex(32)
end

respond_to do |format|
if @submission.save
redirect_url = helpers.contest_adaptive_polymorphic_path([@submission], strip_prefix: false)
ActionCable.server.broadcast('fetch', {type: 'notify', action: 'new', submission_id: @submission.id})
if @problem.proxyjudge_any?
SubmitProxyJudgeJob.perform_later(@submission, @problem)
else
ActionCable.server.broadcast('fetch', {type: 'notify', action: 'new', submission_id: @submission.id})
end
helpers.notify_contest_channel(@submission.contest_id, @submission.user_id)
format.html { redirect_to redirect_url, notice: 'Submission was successfully created.' }
format.json { render action: 'show', status: :created, location: redirect_url }
Expand Down Expand Up @@ -272,6 +283,13 @@ def normalize_code
params[:submission][:code_length] = code.bytesize
end

def check_proxyjudge
if @problem.proxyjudge_any?
redirect_back fallback_location: root_path, alert: 'This action is disabled in proxy judge problems.'
return
end
end

# Never trust parameters from the scary internet, only allow the white list through.
def submission_params
params.require(:submission).permit(
Expand Down
9 changes: 9 additions & 0 deletions app/helpers/problems_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ def specjudge_type_desc_map
}
end

def proxyjudge_type_desc_map
{
"none" => "None (TIOJ native)",
"codeforces" => "Codeforces",
"poj" => "PKU JudgeOnline (POJ)",
"qoj" => "QOJ",
}
end

def interlib_type_desc_map
{
"none" => "No interactive library",
Expand Down
6 changes: 5 additions & 1 deletion app/javascript/pages/problems/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@ export function initProblemForm() {

setGroupVisibility('interlib', $('#problem_interlib_type').val());
setGroupVisibility('specjudge', $('#problem_specjudge_type').val());
setGroupVisibility('proxyjudge', $('#problem_proxyjudge_type').val());
setGroupVisibility('summary', $('#problem_summary_type').val());
$('#problem_interlib_type').on('change', function() {
setGroupVisibility('interlib', this.value);
});
$('#problem_specjudge_type').on('change', function() {
setGroupVisibility('specjudge', this.value);
});
$('#problem_proxyjudge_type').on('change', function() {
setGroupVisibility('proxyjudge', this.value);
});
$('#problem_summary_type').on('change', function() {
setGroupVisibility('summary', this.value);
});
}
}
7 changes: 7 additions & 0 deletions app/jobs/application_job.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
class ApplicationJob < ActiveJob::Base
# Automatically retry jobs that encountered a deadlock
# retry_on ActiveRecord::Deadlocked

# Most jobs are safe to ignore if the underlying records are no longer available
# discard_on ActiveJob::DeserializationError
end
173 changes: 173 additions & 0 deletions app/jobs/judges/POJ.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
require 'rest-client'

class Judges::POJ
# ref: http://poj.org/page?id=1000
# "C", "C++" are MS VC++ 2008 Express and "GCC", "G++" are MinGW GCC 4.4.0
PROXY_COMPILERS = {
"c++98" => "G++",
"c99" => "GCC",
}.freeze

LANGUAGES = %w(G++ GCC Java Pascal C++ C Fortran)
REDIRECT_HANDLER = lambda do |response, request, result, &block|
if response.code == 302
response
else
response.return!(request, result, &block)
end
end

SUBMISSION_REGEX = %r{
<tr\salign=center>
<td>(?<run_id>.*?)</td>
<td><a\shref=userstatus\?user_id=.*?>(?<username>.*?)</a></td>
<td><a\shref=problem\?id=.*?>(?<problem_id>.*?)</a></td>
<td>(<a.*?>)?<font\scolor=.*?>(?<verdict>.*?)</font>(</a>)?</td>
<td>(?<memory>.*?)K?</td>
<td>(?<time>.*?)(MS)?</td>
<td>(?<language>.*?)</td>
<td>(?<code_length>.*?)B</td>
<td>(?<submit_time>.*?)</td>
</tr>
}xm
DETAIL_REGEX = %r{
<tr><td><b>.*?</b>.*?<a.*?>(?<problem_id>.*?)</a></td>
<td.*?></td>
<td><b>.*?</b>.*?<a.*?>(?<username>.*?)</a></td>
</tr>.*?
<tr><td><b>.*?</b>.*?((?<memory>[0-9]+)K|N/A)</td>
<td.*?></td>
<td><b>.*?</b>.*?((?<time>[0-9]+)MS|N/A)</td>
</tr>.*?
<tr><td><b>.*?</b>.*?(?<language>\S+?)</td>
<td.*?></td>
<td><b>.*?</b>.*?((<a.*?>)?<font.*?>(?<verdict>.*?)</font>(</a>)?)?</td></tr>
}xm

attr_reader :username

def initialize
if not @username
account = Rails.configuration.x.settings.dig(:proxyjudge).dig(:poj)
@username = account.dig(:username)
@password = account.dig(:password)
@cookies = nil
end
end

def login
return unless @cookies.nil?
login_response = RestClient.post(
'http://poj.org/login',
{
B1: 'login',
url: '/',
user_id1: @username,
password1: @password
},
&REDIRECT_HANDLER
)
@cookies = login_response.cookies
end

def submit!(problem_id, compiler_name, source_code, _)
if not PROXY_COMPILERS.include?(compiler_name) then
raise 'Unknown compiler for POJ proxy judge'
end
proxy_language = PROXY_COMPILERS[compiler_name]

login
submit_response = RestClient.post(
'http://poj.org/submit',
{
submit: 'Submit',
problem_id: problem_id,
language: LANGUAGES.index(proxy_language),
source: [source_code].pack('m0'),
encoded: 1
},
cookies: @cookies,
&REDIRECT_HANDLER
)
return submit_response.code == 302
end

def fetch_results
response = RestClient.get "http://poj.org/status?user_id=#{@username}&size=100"
# find all matches with MatchData
submission_data = response.to_enum(:scan, SUBMISSION_REGEX).map {$~}

submission_ids = submission_data.map{ |match| match[:run_id] }
identified_submissions = Submission.where(proxyjudge_id: submission_ids, proxyjudge_type: :poj).map{|x| [x.proxyjudge_id, x]}.to_h
unidentified_submissions = Submission.where(proxyjudge_id: nil, proxyjudge_type: :poj).map{|x| [x.proxyjudge_nonce, x]}.to_h
submission_ids -= identified_submissions.keys
submission_id_set = submission_ids.to_set

result = false
submission_ids.each do |submission_id|
next if submission_id_set.empty?
detail = fetch_submission_detail(submission_id)
nonce = detail.scan(/tioj-proxy nonce=([0-9a-f]{64})/).last&.first
next if nonce.nil? || !unidentified_submissions.include?(nonce)
submission_id_set.delete(submission_id)
sub = unidentified_submissions[nonce]
status = get_status_dict_from_detail(detail)
result = true if status.nil?
next if status.nil?
sub.update(**status, proxyjudge_id: submission_id)
ActionCable.server.broadcast("submission_#{sub.id}_overall", status.update(id: sub.id))
end
update_hash = submission_data.filter_map do |x|
status = get_status_dict(x)
sub = identified_submissions[x[:run_id]]
next if sub.nil?
result = true if status.nil?
expected = {result: sub.result, score: sub.score, total_time: sub.total_time, total_memory: sub.total_memory}
status.update(id: sub.id) if status && status != expected
end
Submission.import(
update_hash.map {|x| x.update(compiler_id: -1, code_content_id: -1)},
on_duplicate_key_update: [:result, :score, :total_time, :total_memory], validate: false)
update_hash.each{ |x|
ActionCable.server.broadcast("submission_#{x[:id]}_overall", x)
}
result
end

private

def get_status_dict(status)
verdict = status[:verdict]
return nil if verdict.nil? || verdict.empty? || verdict.include?("ing")
verdict = format_verdict(verdict)
{
result: verdict,
score: verdict == 'AC' ? 100 : 0,
total_time: status[:time]&.to_i || 0,
total_memory: status[:memory]&.to_i || 0,
}
end

def get_status_dict_from_detail(response)
get_status_dict(DETAIL_REGEX.match(response))
end

# submission_id is the ID of POJ (proxyjudge_id)
def fetch_submission_detail(submission_id)
login
RestClient.get("http://poj.org/showsource?solution_id=#{submission_id}", cookies: @cookies)
end

def format_verdict(verdict)
verdict = verdict.split.map(&:capitalize).join(' ')
case verdict
when /Accepted/ then "AC"
when /Wrong Answer/ then "WA"
when /Time Limit Exceeded/ then "TLE"
when /Runtime Error/ then "RE"
when /Memory Limit Exceeded/ then "MLE"
when /Compilation Error/ then "CE"
else "JE"
end
end
end
Loading