def evaluate_output(task_name, input_path, output_path): """ Given an input of a task, evaluate the correctness of the output :param task_name: Name of the task :param input_path: Path to the user's input file :param output_path: Path to the user's output file :return: The stdout of the checker """ try: # call the checker and store the output start_time = time.monotonic() output = gevent.subprocess.check_output([ ContestManager.tasks[task_name]["checker"], StorageManager.get_absolute_path(input_path), StorageManager.get_absolute_path(output_path) ]) if time.monotonic() > start_time + 1: Logger.warning( "TASK", "Evaluation of output %s " "for task %s, with input %s, took %f " "seconds" % (output_path, task_name, input_path, time.monotonic() - start_time)) except: # TODO log the stdout and stderr of the checker Logger.error( "TASK", "Error while evaluating output %s " "for task %s, with input %s: %s" % (output_path, task_name, input_path, traceback.format_exc())) raise Logger.info( "TASK", "Evaluated output %s for task %s, with input %s" % (output_path, task_name, input_path)) return output
def submit(self, output, source): """ POST /submit """ input = Database.get_input(output["input"]) if input is None: Logger.warning("DB_CONSISTENCY_ERROR", "Input %s not found in the db" % output["input"]) self.raise_exc(BadRequest, "WRONG_INPUT", "The provided input in invalid") if output["input"] != source["input"]: Logger.warning("POSSIBLE_CHEAT", "Trying to submit wrong pair source-output") self.raise_exc(Forbidden, "WRONG_OUTPUT_SOURCE", "The provided pair of source-output is invalid") score = ContestHandler.compute_score(input["task"], output["result"]) Database.begin() try: submission_id = Database.gen_id() if not Database.add_submission(submission_id, input["id"], output["id"], source["id"], score, autocommit=False): self.raise_exc(BadRequest, "INTERNAL_ERROR", "Error inserting the submission") ContestHandler.update_user_score(input["token"], input["task"], score) Database.set_user_attempt(input["token"], input["task"], None, autocommit=False) Database.commit() except sqlite3.IntegrityError as ex: Database.rollback() # provide a better error message if the input has already been # submitted if "UNIQUE constraint failed: submissions.input" in str(ex): self.raise_exc(Forbidden, "ALREADY_SUBMITTED", "This input has already been submitted") raise except: Database.rollback() raise Logger.info( "CONTEST", "User %s has submitted %s on %s" % (input["token"], submission_id, input["task"])) return InfoHandler.patch_submission( Database.get_submission(submission_id))
def wsgi_app(self, environ, start_response): route = self.router.bind_to_environ(environ) request = Request(environ) try: endpoint, args = route.match() except HTTPException: Logger.warning("HTTP_ERROR", "%s %s %s 404" % (BaseHandler.get_ip(request), request.method, request.url)) return NotFound() controller, action = endpoint.split("#") return self.handlers[controller].handle(action, args, request)
def get_input(task_name, attempt): """ Fetch an input from the queue and properly rename it :param task_name: Name of the task :param attempt: Number of the attempt for the user :return: A pair, the first element is the id of the input file, the second the path """ if ContestManager.input_queue[task_name].empty(): Logger.warning("TASK", "Empty queue for task %s!" % task_name) input = ContestManager.input_queue[task_name].get() path = StorageManager.new_input_file(input["id"], task_name, attempt) StorageManager.rename_file(input["path"], path) return input["id"], path
def drop_contest(self, admin_token): """ POST /admin/drop_contest """ if not os.path.exists(Config.encrypted_file): self.raise_exc(NotFound, "NOT_FOUND", "No packs found") Logger.warning("DROP_CONTEST", "Started dropping contest") with open(Config.encrypted_file, "rb") as f: pack = f.read() db_token = Database.get_meta("admin_token") # contest has been extracted but the token is wrong if db_token is not None and db_token != admin_token: self.raise_exc(Forbidden, "FORBIDDEN", "Wrong token") # contest has not been extracted if db_token is None: try: password = crypto.recover_file_password_from_token(admin_token) crypto.decode(password, pack) except nacl.exceptions.CryptoError: # pack password is wrong self.raise_exc(Forbidden, "FORBIDDEN", "Wrong pack token") metadata = ruamel.yaml.safe_load(crypto.metadata(pack).strip(b"\x00")) if not metadata.get("deletable"): self.raise_exc(Forbidden, "FORBIDDEN", "Contest not deletable") shutil.rmtree(Config.storedir, ignore_errors=True) shutil.rmtree(Config.statementdir, ignore_errors=True) shutil.rmtree(Config.contest_path, ignore_errors=True) for f in (Config.encrypted_file, Config.decrypted_file): try: os.remove(f) except FileNotFoundError: pass Database.disconnect_database() for f in glob.glob(Config.db + "*"): os.remove(f) Database.connect_to_database() Logger.warning("DROP_CONTEST", "Contest dropped") return {}
def main(): parser = argparse.ArgumentParser() parser.add_argument("-c", "--config", help="Path to the config file", default="config/config.yaml") args = parser.parse_args() if args.config: Config.set_config_file(args.config) Logger.set_log_level(Config.log_level) Logger.connect_to_database() Database.connect_to_database() try: ContestManager.read_from_disk(remove_enc=False) except: Logger.warning("CONTEST", "Failed to read the contest from disk...") server = Server() server.run()
def _validate_admin_token(token, ip): """ Ensure the admin token is valid :param token: Token to check :param ip: IP of the client """ correct_token = Database.get_meta("admin_token") if correct_token is None: ContestManager.extract_contest(token) ContestManager.read_from_disk() correct_token = token if token != correct_token: Logger.warning("LOGIN_ADMIN", "Admin login failed from %s" % ip) BaseHandler.raise_exc(Forbidden, "FORBIDDEN", "Invalid admin token!") else: if Database.register_admin_ip(ip): Logger.info("LOGIN_ADMIN", "An admin has connected from a new ip: %s" % ip)
def internet_detected(self, token): """ POST /internet_detected """ Logger.warning("INTERNET_DETECTED", "User %s has been detected with internet!" % token)
def worker(task_name): """ Method that stays in the background and generates inputs """ task = ContestManager.tasks[task_name] queue = ContestManager.input_queue[task_name] while True: try: id = Database.gen_id() path = StorageManager.new_input_file(id, task_name, "invalid") seed = int(sha256(id.encode()).hexdigest(), 16) % (2**31) stdout = os.open( StorageManager.get_absolute_path(path), os.O_WRONLY | os.O_CREAT, 0o644, ) try: start_time = time.monotonic() # generate the input and store the stdout into a file retcode = gevent.subprocess.call( [task["generator"], str(seed), "0"], stdout=stdout) if time.monotonic() > start_time + 1: Logger.warning( "TASK", "Generation of input %s for task %s took %f seconds" % (seed, task_name, time.monotonic() - start_time), ) finally: os.close(stdout) if retcode != 0: Logger.error( "TASK", "Error %d generating input %s (%d) for task %s" % (retcode, id, seed, task_name), ) # skip the input continue # if there is a validator in the task use it to check if the # generated input is valid if "validator" in task: stdin = os.open(StorageManager.get_absolute_path(path), os.O_RDONLY) try: start_time = time.monotonic() # execute the validator piping the input file to stdin retcode = gevent.subprocess.call( [task["validator"], "0"], stdin=stdin) if time.monotonic() > start_time + 1: Logger.warning( "TASK", "Validation of input %s for task %s took %f " "seconds" % (seed, task_name, time.monotonic() - start_time), ) finally: os.close(stdin) if retcode != 0: Logger.error( "TASK", "Error %d validating input %s (%d) for task %s" % (retcode, id, seed, task_name), ) # skip the input continue Logger.debug( "TASK", "Generated input %s (%d) for task %s" % (id, seed, task_name), ) # this method is blocking if the queue is full queue.put({"id": id, "path": path}) except: Logger.error( "TASK", "Exception while creating an input file: " + traceback.format_exc(), )
def read_from_disk(remove_enc=True): """ Load a task from the disk and load the data into the database """ try: contest = ContestManager.import_contest(Config.contest_path) except FileNotFoundError as ex: error = ( "Contest not found, you probably need to unzip it. Missing file %s" % ex.filename) Logger.warning("CONTEST", error) shutil.rmtree(Config.statementdir, ignore_errors=True) shutil.rmtree(Config.web_statementdir, ignore_errors=True) shutil.rmtree(Config.contest_path, ignore_errors=True) if remove_enc: with suppress(Exception): os.remove(Config.encrypted_file) with suppress(Exception): os.remove(Config.decrypted_file) Database.del_meta("admin_token") BaseHandler.raise_exc(UnprocessableEntity, "CONTEST", error) if not Database.get_meta("contest_imported", default=False, type=bool): Database.begin() try: Database.set_meta("contest_duration", contest["duration"], autocommit=False) Database.set_meta("contest_name", contest.get("name", "Contest"), autocommit=False) Database.set_meta( "contest_description", contest.get("description", ""), autocommit=False, ) Database.set_meta( "window_duration", # if None the contest is not USACO-style contest.get("window_duration"), autocommit=False, ) count = 0 for task in contest["tasks"]: Database.add_task( task["name"], task["description"], task["statement_path"], task["max_score"], count, autocommit=False, ) count += 1 for user in contest["users"]: Database.add_user(user["token"], user["name"], user["surname"], autocommit=False) for user in Database.get_users(): for task in Database.get_tasks(): Database.add_user_task(user["token"], task["name"], autocommit=False) Database.set_meta("contest_imported", True, autocommit=False) Database.commit() except: Database.rollback() raise else: # TODO: check that the contest is still the same pass # store the task in the ContestManager singleton ContestManager.tasks = dict( (task["name"], task) for task in contest["tasks"]) ContestManager.has_contest = True # create the queues for the task inputs for task in ContestManager.tasks: ContestManager.input_queue[task] = gevent.queue.Queue( Config.queue_size) gevent.spawn(ContestManager.worker, task)