You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
438 lines
11 KiB
438 lines
11 KiB
3 years ago
|
#!/usr/bin/python3
|
||
|
|
||
|
import json
|
||
|
import sys
|
||
|
import os
|
||
|
import pipes
|
||
|
import socket
|
||
|
from threading import Thread
|
||
|
import fcntl
|
||
|
import shutil
|
||
|
|
||
|
import traceback
|
||
|
import time
|
||
|
from contextlib import closing
|
||
|
|
||
|
import requests
|
||
|
|
||
|
import queue as queue
|
||
|
from queue import Queue, Empty
|
||
|
|
||
|
taskQ = Queue()
|
||
|
submission = None
|
||
|
|
||
|
# path related function
|
||
|
|
||
|
def uoj_url(uri):
|
||
|
return ("%s://%s%s" % (jconf['uoj_protocol'], jconf['uoj_host'], uri)).rstrip('/')
|
||
|
def uoj_judger_path(path = ''):
|
||
|
return "uoj_judger" + path
|
||
|
|
||
|
# os related funciton
|
||
|
def clean_up_folder(path):
|
||
|
for f in os.listdir(path):
|
||
|
f_path = os.path.join(path, f)
|
||
|
if os.path.isfile(f_path):
|
||
|
os.unlink(f_path)
|
||
|
else:
|
||
|
shutil.rmtree(f_path)
|
||
|
|
||
|
def execute(cmd):
|
||
|
if os.system(cmd):
|
||
|
raise Exception('failed to execute: %s' % cmd)
|
||
|
|
||
|
def freopen(f, g):
|
||
|
os.dup2(g.fileno(), f.fileno())
|
||
|
g.close()
|
||
|
|
||
|
# init
|
||
|
def init():
|
||
|
global jconf
|
||
|
os.chdir(os.path.dirname(os.path.realpath(__file__)))
|
||
|
with open('.conf.json', 'r') as fp:
|
||
|
jconf = json.load(fp)
|
||
|
assert 'uoj_protocol' in jconf
|
||
|
assert 'uoj_host' in jconf
|
||
|
assert 'judger_name' in jconf
|
||
|
assert 'judger_password' in jconf
|
||
|
assert 'socket_port' in jconf
|
||
|
assert 'socket_password' in jconf
|
||
|
|
||
|
# socket server
|
||
|
def socket_server_loop():
|
||
|
SOCK_CLOEXEC = 524288
|
||
|
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM | SOCK_CLOEXEC)) as s:
|
||
|
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||
|
s.bind(('', jconf['socket_port']))
|
||
|
s.listen(5)
|
||
|
|
||
|
while True:
|
||
|
try:
|
||
|
conn, addr = s.accept()
|
||
|
with closing(conn) as conn:
|
||
|
data = conn.recv(1024).decode()
|
||
|
assert data != None
|
||
|
task = json.loads(data)
|
||
|
assert task['password'] == jconf['socket_password']
|
||
|
assert 'cmd' in task
|
||
|
|
||
|
taskQ.put(task)
|
||
|
|
||
|
if task['cmd'] == 'stop':
|
||
|
print('the judge client is closing...')
|
||
|
taskQ.join()
|
||
|
conn.sendall(b'ok')
|
||
|
return 'stop'
|
||
|
except Exception:
|
||
|
print('['+time.asctime()+']', 'connection rejected', file=sys.stderr)
|
||
|
traceback.print_exc()
|
||
|
else:
|
||
|
print('['+time.asctime()+']', 'a new task accomplished', file=sys.stderr)
|
||
|
|
||
|
def start_judger_server():
|
||
|
global socket_server_thread
|
||
|
|
||
|
print_judge_client_status()
|
||
|
print('hello!', file=sys.stderr)
|
||
|
|
||
|
socket_server_thread = Thread(target = socket_server_loop)
|
||
|
socket_server_thread.setDaemon(True)
|
||
|
socket_server_thread.start()
|
||
|
|
||
|
judger_loop()
|
||
|
|
||
|
# report thread
|
||
|
def report_loop():
|
||
|
if 'is_hack' in submission:
|
||
|
return
|
||
|
while not submission_judged:
|
||
|
try:
|
||
|
with open(uoj_judger_path('/result/cur_status.txt'), 'r') as f:
|
||
|
fcntl.flock(f, fcntl.LOCK_SH)
|
||
|
try:
|
||
|
status = f.read(100)
|
||
|
except Exception:
|
||
|
status = None
|
||
|
finally:
|
||
|
fcntl.flock(f, fcntl.LOCK_UN)
|
||
|
|
||
|
if status != None:
|
||
|
data = {}
|
||
|
data['update-status'] = True
|
||
|
data['id'] = submission['id']
|
||
|
if 'is_custom_test' in submission:
|
||
|
data['is_custom_test'] = True
|
||
|
data['status'] = status
|
||
|
uoj_interact(data)
|
||
|
time.sleep(0.2)
|
||
|
except Exception:
|
||
|
pass
|
||
|
|
||
|
# handle task in main thread
|
||
|
def handle_task():
|
||
|
need_restart = False
|
||
|
try:
|
||
|
while True:
|
||
|
task = taskQ.get_nowait()
|
||
|
|
||
|
if task['cmd'] == 'update':
|
||
|
try:
|
||
|
uoj_download('/judger', 'judger_update.zip')
|
||
|
execute('unzip -o judger_update.zip && cd %s && make clean && make' % uoj_judger_path())
|
||
|
except:
|
||
|
print(sys.stderr, "error when update")
|
||
|
if jconf['judger_name'] == 'main_judger':
|
||
|
uoj_sync_judge_client()
|
||
|
need_restart = True
|
||
|
elif task['cmd'] == 'stop':
|
||
|
taskQ.task_done()
|
||
|
socket_server_thread.join()
|
||
|
|
||
|
print_judge_client_status()
|
||
|
print(sys.stderr, "goodbye!")
|
||
|
|
||
|
sys.exit(0)
|
||
|
|
||
|
taskQ.task_done()
|
||
|
except Empty:
|
||
|
pass
|
||
|
|
||
|
if need_restart:
|
||
|
os.execl('./judge_client', './judge_client')
|
||
|
|
||
|
def print_judge_client_status():
|
||
|
print('[' + time.asctime() + ']', end=' ', file=sys.stderr)
|
||
|
if submission != None:
|
||
|
print(submission, end=' ', file=sys.stderr)
|
||
|
print(file=sys.stderr)
|
||
|
|
||
|
# interact with uoj_judger
|
||
|
def get_judger_result():
|
||
|
res = {}
|
||
|
with open(uoj_judger_path('/result/result.txt'), 'r') as fres:
|
||
|
res['score'] = 0
|
||
|
res['time'] = 0
|
||
|
res['memory'] = 0
|
||
|
while True:
|
||
|
line = fres.readline()
|
||
|
if line == '':
|
||
|
break
|
||
|
line = line.strip()
|
||
|
if line == 'details':
|
||
|
res['details'] = fres.read()
|
||
|
break
|
||
|
|
||
|
sp = line.split()
|
||
|
assert len(sp) >= 1
|
||
|
if sp[0] == 'error':
|
||
|
res['error'] = line[len('error') + 1:]
|
||
|
else:
|
||
|
assert len(sp) == 2
|
||
|
res[sp[0]] = sp[1]
|
||
|
res['score'] = int(res['score'])
|
||
|
res['time'] = int(res['time'])
|
||
|
res['memory'] = int(res['memory'])
|
||
|
res['status'] = 'Judged'
|
||
|
return res
|
||
|
|
||
|
def update_problem_data(problem_id, problem_mtime):
|
||
|
try:
|
||
|
if jconf['judger_name'] == 'main_judger':
|
||
|
return
|
||
|
copy_name = uoj_judger_path('/data/%d' % problem_id)
|
||
|
copy_zip_name = uoj_judger_path('/data/%d.zip' % problem_id)
|
||
|
if os.path.isdir(copy_name):
|
||
|
quoted_copy_name = pipes.quote(copy_name)
|
||
|
if os.path.getmtime(copy_name) >= problem_mtime:
|
||
|
execute('touch -a %s' % quoted_copy_name)
|
||
|
return
|
||
|
else:
|
||
|
execute('chmod 700 %s -R && rm -rf %s' % (quoted_copy_name, quoted_copy_name))
|
||
|
del_list = sorted(os.listdir(uoj_judger_path('/data')), key=lambda fname: os.path.getatime(uoj_judger_path('/data/%s' % fname)))[:-99]
|
||
|
for fname in del_list:
|
||
|
quoted_fname = pipes.quote(uoj_judger_path('/data/%s' % fname))
|
||
|
os.system('chmod 700 %s -R && rm -rf %s' % (quoted_fname, quoted_fname))
|
||
|
|
||
|
uoj_download('/problem/%d' % problem_id, copy_zip_name)
|
||
|
execute('cd %s && unzip -q %d.zip && rm %d.zip && chmod -w %d -R' % (uoj_judger_path('/data'), problem_id, problem_id, problem_id))
|
||
|
except Exception:
|
||
|
print_judge_client_status()
|
||
|
traceback.print_exc()
|
||
|
raise Exception('failed to update problem data of #%d' % problem_id)
|
||
|
else:
|
||
|
print_judge_client_status()
|
||
|
print('updated problem data of #%d successfully' % problem_id, file=sys.stderr)
|
||
|
|
||
|
def judge():
|
||
|
global report_thread
|
||
|
global submission_judged
|
||
|
|
||
|
clean_up_folder(uoj_judger_path('/work'))
|
||
|
clean_up_folder(uoj_judger_path('/result'))
|
||
|
update_problem_data(submission['problem_id'], submission['problem_mtime'])
|
||
|
|
||
|
with open(uoj_judger_path('/work/submission.conf'), 'w') as fconf:
|
||
|
uoj_download(submission['content']['file_name'], uoj_judger_path('/work/all.zip'))
|
||
|
execute("cd %s && unzip -q all.zip && rm all.zip" % pipes.quote(uoj_judger_path('/work')))
|
||
|
for k, v in submission['content']['config']:
|
||
|
print(k, v, file=fconf)
|
||
|
|
||
|
if 'is_hack' in submission:
|
||
|
if submission['hack']['input_type'] == 'USE_FORMATTER':
|
||
|
uoj_download(submission['hack']['input'], uoj_judger_path('/work/hack_input_raw.txt'))
|
||
|
execute('%s <%s >%s' % (
|
||
|
pipes.quote(uoj_judger_path('/run/formatter')),
|
||
|
pipes.quote(uoj_judger_path('/work/hack_input_raw.txt')),
|
||
|
pipes.quote(uoj_judger_path('/work/hack_input.txt'))))
|
||
|
else:
|
||
|
uoj_download(submission['hack']['input'], uoj_judger_path('/work/hack_input.txt'))
|
||
|
print('test_new_hack_only on', file=fconf)
|
||
|
elif 'is_custom_test' in submission:
|
||
|
print('custom_test on', file=fconf)
|
||
|
|
||
|
report_thread = Thread(target = report_loop)
|
||
|
report_thread.setDaemon(True)
|
||
|
|
||
|
submission_judged = False
|
||
|
report_thread.start()
|
||
|
execute(pipes.quote(uoj_judger_path('/main_judger')))
|
||
|
submission_judged = True
|
||
|
report_thread.join()
|
||
|
|
||
|
return get_judger_result()
|
||
|
|
||
|
# interact with uoj web server
|
||
|
def uoj_interact(data, files = {}):
|
||
|
data = data.copy()
|
||
|
data.update({
|
||
|
'judger_name': jconf['judger_name'],
|
||
|
'password': jconf['judger_password']
|
||
|
})
|
||
|
return requests.post(uoj_url('/judge/submit'), data=data, files=files).text
|
||
|
def uoj_download(uri, filename):
|
||
|
data = {
|
||
|
'judger_name': jconf['judger_name'],
|
||
|
'password': jconf['judger_password']
|
||
|
}
|
||
|
with open(filename, 'wb') as f:
|
||
|
r = requests.post(uoj_url('/judge/download' + uri), data=data, stream=True)
|
||
|
for chunk in r.iter_content(chunk_size=65536):
|
||
|
if chunk:
|
||
|
f.write(chunk)
|
||
|
def uoj_sync_judge_client():
|
||
|
data = {
|
||
|
'judger_name': jconf['judger_name'],
|
||
|
'password': jconf['judger_password']
|
||
|
}
|
||
|
ret = requests.post(uoj_url('/judge/sync-judge-client'), data=data).text
|
||
|
if ret != "ok":
|
||
|
raise Exception('failed to sync judge clients: %s' % ret)
|
||
|
|
||
|
def send_and_fetch(result = None, fetch_new = True):
|
||
|
global submission
|
||
|
|
||
|
"""send judgement result, and fetch new submission to judge"""
|
||
|
|
||
|
data = {}
|
||
|
files = {}
|
||
|
|
||
|
if not fetch_new:
|
||
|
data['fetch_new'] = False
|
||
|
|
||
|
if result != None:
|
||
|
data['submit'] = True
|
||
|
if 'is_hack' in submission:
|
||
|
data['is_hack'] = True
|
||
|
data['id'] = submission['hack']['id']
|
||
|
if result != False and result['score']:
|
||
|
try:
|
||
|
print("succ hack!", file=sys.stderr)
|
||
|
files = {
|
||
|
('hack_input', open('uoj_judger/work/hack_input.txt', 'r')),
|
||
|
('std_output', open('uoj_judger/work/std_output.txt', 'r'))
|
||
|
}
|
||
|
except Exception:
|
||
|
print_judge_client_status()
|
||
|
traceback.print_exc()
|
||
|
result = False
|
||
|
elif 'is_custom_test' in submission:
|
||
|
data['is_custom_test'] = True
|
||
|
data['id'] = submission['id']
|
||
|
else:
|
||
|
data['id'] = submission['id']
|
||
|
|
||
|
if result == False:
|
||
|
result = {
|
||
|
'score': 0,
|
||
|
'error': 'Judgement Failed',
|
||
|
'details': 'Unknown Error'
|
||
|
}
|
||
|
result['status'] = 'Judged'
|
||
|
data['result'] = json.dumps(result, ensure_ascii=False)
|
||
|
|
||
|
while True:
|
||
|
try:
|
||
|
ret = uoj_interact(data, files)
|
||
|
print(ret)
|
||
|
except Exception:
|
||
|
print_judge_client_status()
|
||
|
traceback.print_exc()
|
||
|
else:
|
||
|
break
|
||
|
time.sleep(2)
|
||
|
|
||
|
try:
|
||
|
submission = json.loads(ret)
|
||
|
except Exception as e:
|
||
|
submission = None
|
||
|
return False
|
||
|
else:
|
||
|
return True
|
||
|
|
||
|
# judge client
|
||
|
def judger_loop():
|
||
|
ok = False
|
||
|
while True:
|
||
|
fetch_new = True
|
||
|
|
||
|
if ok and not (taskQ.empty() and socket_server_thread.isAlive()):
|
||
|
fetch_new = False
|
||
|
|
||
|
if not ok:
|
||
|
while True:
|
||
|
if not taskQ.empty():
|
||
|
handle_task()
|
||
|
if not socket_server_thread.isAlive():
|
||
|
raise Exception('socket server exited unexpectedly')
|
||
|
|
||
|
if send_and_fetch():
|
||
|
break
|
||
|
|
||
|
print('['+time.asctime()+']', 'Nothing to judge...')
|
||
|
time.sleep(2)
|
||
|
|
||
|
ok = True
|
||
|
print_judge_client_status()
|
||
|
print('judging', file=sys.stderr)
|
||
|
|
||
|
try:
|
||
|
res = judge()
|
||
|
except Exception:
|
||
|
print_judge_client_status()
|
||
|
traceback.print_exc()
|
||
|
res = False
|
||
|
|
||
|
ok = send_and_fetch(result=res,fetch_new=fetch_new)
|
||
|
|
||
|
# main function
|
||
|
def main():
|
||
|
init()
|
||
|
|
||
|
if len(sys.argv) == 1:
|
||
|
start_judger_server()
|
||
|
if len(sys.argv) == 2:
|
||
|
if sys.argv[1] == 'start':
|
||
|
pid = os.fork()
|
||
|
if pid == -1:
|
||
|
raise Exception('fork failed')
|
||
|
elif pid > 0:
|
||
|
return
|
||
|
else:
|
||
|
freopen(sys.stdout, open(os.devnull, 'wb'))
|
||
|
freopen(sys.stderr, open('log/judge.log', 'ab', buffering=0))
|
||
|
start_judger_server()
|
||
|
elif sys.argv[1] == 'update':
|
||
|
try:
|
||
|
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
||
|
s.connect(('127.0.0.1', jconf['socket_port']))
|
||
|
s.sendall(json.dumps({
|
||
|
'password': jconf['socket_password'],
|
||
|
'cmd': 'update'
|
||
|
}).encode())
|
||
|
return
|
||
|
except Exception:
|
||
|
traceback.print_exc()
|
||
|
raise Exception('update failed')
|
||
|
elif sys.argv[1] == 'stop':
|
||
|
try:
|
||
|
with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
|
||
|
s.connect(('127.0.0.1', jconf['socket_port']))
|
||
|
s.sendall(json.dumps({
|
||
|
'password': jconf['socket_password'],
|
||
|
'cmd': 'stop'
|
||
|
}).encode())
|
||
|
if s.recv(10).decode() != 'ok':
|
||
|
raise Exception('stop failed')
|
||
|
return
|
||
|
except Exception:
|
||
|
traceback.print_exc()
|
||
|
raise Exception('stop failed')
|
||
|
raise Exception('invalid argument')
|
||
|
|
||
|
try:
|
||
|
main()
|
||
|
except Exception:
|
||
|
print_judge_client_status()
|
||
|
traceback.print_exc()
|
||
|
sys.exit(1)
|