judge_client 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. #!/usr/bin/python3
  2. import json
  3. import sys
  4. import os
  5. import pipes
  6. import socket
  7. from threading import Thread
  8. import fcntl
  9. import shutil
  10. import traceback
  11. import time
  12. from contextlib import closing
  13. import requests
  14. import queue as queue
  15. from queue import Queue, Empty
  16. taskQ = Queue()
  17. submission = None
  18. # path related function
  19. def uoj_url(uri):
  20. return ("%s://%s%s" % (jconf['uoj_protocol'], jconf['uoj_host'], uri)).rstrip('/')
  21. def uoj_judger_path(path = ''):
  22. return "uoj_judger" + path
  23. # os related funciton
  24. def clean_up_folder(path):
  25. for f in os.listdir(path):
  26. f_path = os.path.join(path, f)
  27. if os.path.isfile(f_path):
  28. os.unlink(f_path)
  29. else:
  30. shutil.rmtree(f_path)
  31. def execute(cmd):
  32. if os.system(cmd):
  33. raise Exception('failed to execute: %s' % cmd)
  34. def freopen(f, g):
  35. os.dup2(g.fileno(), f.fileno())
  36. g.close()
  37. # init
  38. def init():
  39. global jconf
  40. os.chdir(os.path.dirname(os.path.realpath(__file__)))
  41. with open('.conf.json', 'r') as fp:
  42. jconf = json.load(fp)
  43. assert 'uoj_protocol' in jconf
  44. assert 'uoj_host' in jconf
  45. assert 'judger_name' in jconf
  46. assert 'judger_password' in jconf
  47. assert 'socket_port' in jconf
  48. assert 'socket_password' in jconf
  49. # socket server
  50. def socket_server_loop():
  51. SOCK_CLOEXEC = 524288
  52. with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM | SOCK_CLOEXEC)) as s:
  53. s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  54. s.bind(('', jconf['socket_port']))
  55. s.listen(5)
  56. while True:
  57. try:
  58. conn, addr = s.accept()
  59. with closing(conn) as conn:
  60. data = conn.recv(1024).decode()
  61. assert data != None
  62. task = json.loads(data)
  63. assert task['password'] == jconf['socket_password']
  64. assert 'cmd' in task
  65. taskQ.put(task)
  66. if task['cmd'] == 'stop':
  67. print('the judge client is closing...')
  68. taskQ.join()
  69. conn.sendall(b'ok')
  70. return 'stop'
  71. except Exception:
  72. print('['+time.asctime()+']', 'connection rejected', file=sys.stderr)
  73. traceback.print_exc()
  74. else:
  75. print('['+time.asctime()+']', 'a new task accomplished', file=sys.stderr)
  76. def start_judger_server():
  77. global socket_server_thread
  78. print_judge_client_status()
  79. print('hello!', file=sys.stderr)
  80. socket_server_thread = Thread(target = socket_server_loop)
  81. socket_server_thread.setDaemon(True)
  82. socket_server_thread.start()
  83. judger_loop()
  84. # report thread
  85. def report_loop():
  86. if 'is_hack' in submission:
  87. return
  88. while not submission_judged:
  89. try:
  90. with open(uoj_judger_path('/result/cur_status.txt'), 'r') as f:
  91. fcntl.flock(f, fcntl.LOCK_SH)
  92. try:
  93. status = f.read(100)
  94. except Exception:
  95. status = None
  96. finally:
  97. fcntl.flock(f, fcntl.LOCK_UN)
  98. if status != None:
  99. data = {}
  100. data['update-status'] = True
  101. data['id'] = submission['id']
  102. if 'is_custom_test' in submission:
  103. data['is_custom_test'] = True
  104. data['status'] = status
  105. uoj_interact(data)
  106. time.sleep(0.2)
  107. except Exception:
  108. pass
  109. # handle task in main thread
  110. def handle_task():
  111. need_restart = False
  112. try:
  113. while True:
  114. task = taskQ.get_nowait()
  115. if task['cmd'] == 'update':
  116. try:
  117. uoj_download('/judger', 'judger_update.zip')
  118. execute('unzip -o judger_update.zip && cd %s && make clean && make' % uoj_judger_path())
  119. except:
  120. print(sys.stderr, "error when update")
  121. if jconf['judger_name'] == 'main_judger':
  122. uoj_sync_judge_client()
  123. need_restart = True
  124. elif task['cmd'] == 'stop':
  125. taskQ.task_done()
  126. socket_server_thread.join()
  127. print_judge_client_status()
  128. print(sys.stderr, "goodbye!")
  129. sys.exit(0)
  130. taskQ.task_done()
  131. except Empty:
  132. pass
  133. if need_restart:
  134. os.execl('./judge_client', './judge_client')
  135. def print_judge_client_status():
  136. print('[' + time.asctime() + ']', end=' ', file=sys.stderr)
  137. if submission != None:
  138. print(submission, end=' ', file=sys.stderr)
  139. print(file=sys.stderr)
  140. # interact with uoj_judger
  141. def get_judger_result():
  142. res = {}
  143. with open(uoj_judger_path('/result/result.txt'), 'r') as fres:
  144. res['score'] = 0
  145. res['time'] = 0
  146. res['memory'] = 0
  147. while True:
  148. line = fres.readline()
  149. if line == '':
  150. break
  151. line = line.strip()
  152. if line == 'details':
  153. res['details'] = fres.read()
  154. break
  155. sp = line.split()
  156. assert len(sp) >= 1
  157. if sp[0] == 'error':
  158. res['error'] = line[len('error') + 1:]
  159. else:
  160. assert len(sp) == 2
  161. res[sp[0]] = sp[1]
  162. res['score'] = int(res['score'])
  163. res['time'] = int(res['time'])
  164. res['memory'] = int(res['memory'])
  165. res['status'] = 'Judged'
  166. return res
  167. def update_problem_data(problem_id, problem_mtime):
  168. try:
  169. if jconf['judger_name'] == 'main_judger':
  170. return
  171. copy_name = uoj_judger_path('/data/%d' % problem_id)
  172. copy_zip_name = uoj_judger_path('/data/%d.zip' % problem_id)
  173. if os.path.isdir(copy_name):
  174. quoted_copy_name = pipes.quote(copy_name)
  175. if os.path.getmtime(copy_name) >= problem_mtime:
  176. execute('touch -a %s' % quoted_copy_name)
  177. return
  178. else:
  179. execute('chmod 700 %s -R && rm -rf %s' % (quoted_copy_name, quoted_copy_name))
  180. del_list = sorted(os.listdir(uoj_judger_path('/data')), key=lambda fname: os.path.getatime(uoj_judger_path('/data/%s' % fname)))[:-99]
  181. for fname in del_list:
  182. quoted_fname = pipes.quote(uoj_judger_path('/data/%s' % fname))
  183. os.system('chmod 700 %s -R && rm -rf %s' % (quoted_fname, quoted_fname))
  184. uoj_download('/problem/%d' % problem_id, copy_zip_name)
  185. execute('cd %s && unzip -q %d.zip && rm %d.zip && chmod -w %d -R' % (uoj_judger_path('/data'), problem_id, problem_id, problem_id))
  186. except Exception:
  187. print_judge_client_status()
  188. traceback.print_exc()
  189. raise Exception('failed to update problem data of #%d' % problem_id)
  190. else:
  191. print_judge_client_status()
  192. print('updated problem data of #%d successfully' % problem_id, file=sys.stderr)
  193. def judge():
  194. global report_thread
  195. global submission_judged
  196. clean_up_folder(uoj_judger_path('/work'))
  197. clean_up_folder(uoj_judger_path('/result'))
  198. update_problem_data(submission['problem_id'], submission['problem_mtime'])
  199. with open(uoj_judger_path('/work/submission.conf'), 'w') as fconf:
  200. uoj_download(submission['content']['file_name'], uoj_judger_path('/work/all.zip'))
  201. execute("cd %s && unzip -q all.zip && rm all.zip" % pipes.quote(uoj_judger_path('/work')))
  202. for k, v in submission['content']['config']:
  203. print(k, v, file=fconf)
  204. if 'is_hack' in submission:
  205. if submission['hack']['input_type'] == 'USE_FORMATTER':
  206. uoj_download(submission['hack']['input'], uoj_judger_path('/work/hack_input_raw.txt'))
  207. execute('%s <%s >%s' % (
  208. pipes.quote(uoj_judger_path('/run/formatter')),
  209. pipes.quote(uoj_judger_path('/work/hack_input_raw.txt')),
  210. pipes.quote(uoj_judger_path('/work/hack_input.txt'))))
  211. else:
  212. uoj_download(submission['hack']['input'], uoj_judger_path('/work/hack_input.txt'))
  213. print('test_new_hack_only on', file=fconf)
  214. elif 'is_custom_test' in submission:
  215. print('custom_test on', file=fconf)
  216. report_thread = Thread(target = report_loop)
  217. report_thread.setDaemon(True)
  218. submission_judged = False
  219. report_thread.start()
  220. execute(pipes.quote(uoj_judger_path('/main_judger')))
  221. submission_judged = True
  222. report_thread.join()
  223. return get_judger_result()
  224. # interact with uoj web server
  225. def uoj_interact(data, files = {}):
  226. data = data.copy()
  227. data.update({
  228. 'judger_name': jconf['judger_name'],
  229. 'password': jconf['judger_password']
  230. })
  231. return requests.post(uoj_url('/judge/submit'), data=data, files=files).text
  232. def uoj_download(uri, filename):
  233. data = {
  234. 'judger_name': jconf['judger_name'],
  235. 'password': jconf['judger_password']
  236. }
  237. with open(filename, 'wb') as f:
  238. r = requests.post(uoj_url('/judge/download' + uri), data=data, stream=True)
  239. for chunk in r.iter_content(chunk_size=65536):
  240. if chunk:
  241. f.write(chunk)
  242. def uoj_sync_judge_client():
  243. data = {
  244. 'judger_name': jconf['judger_name'],
  245. 'password': jconf['judger_password']
  246. }
  247. ret = requests.post(uoj_url('/judge/sync-judge-client'), data=data).text
  248. if ret != "ok":
  249. raise Exception('failed to sync judge clients: %s' % ret)
  250. def send_and_fetch(result = None, fetch_new = True):
  251. global submission
  252. """send judgement result, and fetch new submission to judge"""
  253. data = {}
  254. files = {}
  255. if not fetch_new:
  256. data['fetch_new'] = False
  257. if result != None:
  258. data['submit'] = True
  259. if 'is_hack' in submission:
  260. data['is_hack'] = True
  261. data['id'] = submission['hack']['id']
  262. if result != False and result['score']:
  263. try:
  264. print("succ hack!", file=sys.stderr)
  265. files = {
  266. ('hack_input', open('uoj_judger/work/hack_input.txt', 'r')),
  267. ('std_output', open('uoj_judger/work/std_output.txt', 'r'))
  268. }
  269. except Exception:
  270. print_judge_client_status()
  271. traceback.print_exc()
  272. result = False
  273. elif 'is_custom_test' in submission:
  274. data['is_custom_test'] = True
  275. data['id'] = submission['id']
  276. else:
  277. data['id'] = submission['id']
  278. if result == False:
  279. result = {
  280. 'score': 0,
  281. 'error': 'Judgement Failed',
  282. 'details': 'Unknown Error'
  283. }
  284. result['status'] = 'Judged'
  285. data['result'] = json.dumps(result, ensure_ascii=False)
  286. while True:
  287. try:
  288. ret = uoj_interact(data, files)
  289. print(ret)
  290. except Exception:
  291. print_judge_client_status()
  292. traceback.print_exc()
  293. else:
  294. break
  295. time.sleep(2)
  296. try:
  297. submission = json.loads(ret)
  298. except Exception as e:
  299. submission = None
  300. return False
  301. else:
  302. return True
  303. # judge client
  304. def judger_loop():
  305. ok = False
  306. while True:
  307. fetch_new = True
  308. if ok and not (taskQ.empty() and socket_server_thread.isAlive()):
  309. fetch_new = False
  310. if not ok:
  311. while True:
  312. if not taskQ.empty():
  313. handle_task()
  314. if not socket_server_thread.isAlive():
  315. raise Exception('socket server exited unexpectedly')
  316. if send_and_fetch():
  317. break
  318. print('['+time.asctime()+']', 'Nothing to judge...')
  319. time.sleep(2)
  320. ok = True
  321. print_judge_client_status()
  322. print('judging', file=sys.stderr)
  323. try:
  324. res = judge()
  325. except Exception:
  326. print_judge_client_status()
  327. traceback.print_exc()
  328. res = False
  329. ok = send_and_fetch(result=res,fetch_new=fetch_new)
  330. # main function
  331. def main():
  332. init()
  333. if len(sys.argv) == 1:
  334. start_judger_server()
  335. if len(sys.argv) == 2:
  336. if sys.argv[1] == 'start':
  337. pid = os.fork()
  338. if pid == -1:
  339. raise Exception('fork failed')
  340. elif pid > 0:
  341. return
  342. else:
  343. freopen(sys.stdout, open(os.devnull, 'wb'))
  344. freopen(sys.stderr, open('log/judge.log', 'ab', buffering=0))
  345. start_judger_server()
  346. elif sys.argv[1] == 'update':
  347. try:
  348. with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
  349. s.connect(('127.0.0.1', jconf['socket_port']))
  350. s.sendall(json.dumps({
  351. 'password': jconf['socket_password'],
  352. 'cmd': 'update'
  353. }).encode())
  354. return
  355. except Exception:
  356. traceback.print_exc()
  357. raise Exception('update failed')
  358. elif sys.argv[1] == 'stop':
  359. try:
  360. with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as s:
  361. s.connect(('127.0.0.1', jconf['socket_port']))
  362. s.sendall(json.dumps({
  363. 'password': jconf['socket_password'],
  364. 'cmd': 'stop'
  365. }).encode())
  366. if s.recv(10).decode() != 'ok':
  367. raise Exception('stop failed')
  368. return
  369. except Exception:
  370. traceback.print_exc()
  371. raise Exception('stop failed')
  372. raise Exception('invalid argument')
  373. try:
  374. main()
  375. except Exception:
  376. print_judge_client_status()
  377. traceback.print_exc()
  378. sys.exit(1)