33import argparse
44import importlib .machinery
55import importlib .util
6+ import logging
7+ from logging .handlers import RotatingFileHandler
68import os
79import sys
10+ import traceback
811import typing as ty
912import shutil
1013from pathlib import Path
3235IS_SUBMISSION = os .path .isfile ("/etc/is_submission" )
3336MAX_TEST_OUTPUT_LENGTH = 1024 * 64
3437
38+ _LOG_PATH = "/var/log/ref-task.log"
39+
40+
41+ class _SecureRotatingFileHandler (RotatingFileHandler ):
42+ """RotatingFileHandler that enforces 0600 permissions on every file it creates."""
43+
44+ def _open (self ):
45+ old = os .umask (0o077 )
46+ try :
47+ return super ()._open ()
48+ finally :
49+ os .umask (old )
50+
51+
52+ _error_logger = logging .getLogger ("ref.task" )
53+ _error_logger .setLevel (logging .ERROR )
54+ _log_handler = _SecureRotatingFileHandler (
55+ _LOG_PATH , maxBytes = 1024 * 1024 , backupCount = 1
56+ )
57+ _log_handler .setFormatter (logging .Formatter ("%(asctime)s %(levelname)s %(message)s" ))
58+ _error_logger .addHandler (_log_handler )
59+
3560
3661def finalize_request (req ):
3762 signer = TimedSerializer (KEY , salt = "from-container-to-web" )
@@ -40,6 +65,19 @@ def finalize_request(req):
4065 return req
4166
4267
68+ def server_post (url : str , ** kwargs ) -> requests .Response :
69+ """POST to the submission server, handling connection errors gracefully."""
70+ try :
71+ return requests .post (url , ** kwargs )
72+ except requests .exceptions .RequestException :
73+ _error_logger .error ("Request to %s failed:\n %s" , url , traceback .format_exc ())
74+ print_err ("[!] Failed to connect to the submission server." )
75+ print_err (
76+ "[!] Please try again later. If this problem persists, contact a supervisor."
77+ )
78+ exit (1 )
79+
80+
4381def handle_response (resp , expected_status = (200 ,)) -> ty .Tuple [int , ty .Dict ]:
4482 """
4583 Process a response of a "requests" request.
@@ -101,7 +139,7 @@ def cmd_reset(_):
101139 )
102140 req = {}
103141 req = finalize_request (req )
104- res = requests . post ("http://ssh-reverse-proxy:8000/api/instance/reset" , json = req )
142+ res = server_post ("http://ssh-reverse-proxy:8000/api/instance/reset" , json = req )
105143 handle_response (res )
106144
107145
@@ -178,23 +216,24 @@ def flush(self) -> None:
178216 return captured_output .getvalue (), test_results
179217
180218
181- def cmd_submit (_ ):
219+ def cmd_submit (args : argparse . Namespace ):
182220 print_ok ("[+] Submitting instance.." , flush = True )
183221
184222 test_output , test_results = _run_tests (result_will_be_submitted = True )
185223 any_test_failed = any ([not t .success for t in test_results ])
186224
187- if any_test_failed :
188- print_warn (
189- "[!] Failing tests may indicate that your solution is erroneous or not complete yet."
190- )
191- print_warn ("[!] Are you sure you want to submit? [y/n] " , end = "" )
192- if not user_answered_yes ():
193- exit (0 )
194- else :
195- print_ok ("[+] Are you sure you want to submit? [y/n] " , end = "" )
196- if not user_answered_yes ():
197- exit (0 )
225+ if not args .yes :
226+ if any_test_failed :
227+ print_warn (
228+ "[!] Failing tests may indicate that your solution is erroneous or not complete yet."
229+ )
230+ print_warn ("[!] Are you sure you want to submit? [y/n] " , end = "" )
231+ if not user_answered_yes ():
232+ exit (0 )
233+ else :
234+ print_ok ("[+] Are you sure you want to submit? [y/n] " , end = "" )
235+ if not user_answered_yes ():
236+ exit (0 )
198237
199238 if len (test_output ) > MAX_TEST_OUTPUT_LENGTH :
200239 print_err (
@@ -213,7 +252,7 @@ def cmd_submit(_):
213252 req = {"output" : test_output , "test_results" : [asdict (e ) for e in test_results ]}
214253
215254 req = finalize_request (req )
216- res = requests . post ("http://ssh-reverse-proxy:8000/api/instance/submit" , json = req )
255+ res = server_post ("http://ssh-reverse-proxy:8000/api/instance/submit" , json = req )
217256 _ , ret = handle_response (res )
218257 print_ok (ret )
219258
@@ -274,6 +313,12 @@ def main():
274313 "submit" ,
275314 help = "Submit the current state of your work for grading. Your whole instance is submitted." ,
276315 )
316+ submit_parser .add_argument (
317+ "-y" ,
318+ "--yes" ,
319+ action = "store_true" ,
320+ help = "Skip confirmation prompt and submit immediately." ,
321+ )
277322 submit_parser .set_defaults (func = cmd_submit )
278323
279324 check_parser = subparsers .add_parser (
0 commit comments