def test_006(self): """Send a ~100B random binary file to the storage through FileCacher as a string. FC should cache the content locally. """ self.content = "".join( chr(random.randint(0, 255)) for unused_i in xrange(100)) logger.info(" I am sending the ~100B binary file to FileCacher") try: data = self.file_cacher.put_file_content(self.content, u"Test #005") except Exception as error: self.test_end(False, "Error received: %r." % error) return if not os.path.exists(os.path.join(self.cache_base_path, data)): self.test_end(False, "File not stored in local cache.") elif open(os.path.join(self.cache_base_path, data), "rb").read() != self.content: self.test_end( False, "Local cache's content differ " "from original file.") else: self.cache_path = os.path.join(self.cache_base_path, data) self.digest = data self.test_end(True, "Data sent and cached without error.")
def test_008(self): """Put a ~100MB file into the storage (using a specially crafted file-like object). """ logger.info(" I am sending the ~100MB binary file to FileCacher") rand_file = RandomFile(100000000) try: data = self.file_cacher.put_file_from_fobj(rand_file, u"Test #007") except Exception as error: self.test_end(False, "Error received: %r." % error) return if rand_file.dim != 0: self.test_end(False, "The input file wasn't read completely.") my_digest = rand_file.digest rand_file.close() if not os.path.exists(os.path.join(self.cache_base_path, data)): self.test_end(False, "File not stored in local cache.") elif my_digest != data: self.test_end(False, "File received with wrong hash.") else: self.cache_path = os.path.join(self.cache_base_path, data) self.digest = data self.test_end(True, "Data sent and cached without error.")
def prepare(self): """Initialization for the test code - make sure that the cache is empty before testing. """ logger.info("Please delete directory %s before." % self.cache_base_path)
def get_params_for_user(self, user_dict): """Given the dictionary of information of a user (extracted from contest.yaml), it fills another dictionary with the parameters required by User.import_from_dict(). """ params = {} params["username"] = user_dict["username"] logger.info("Loading parameters for user %s." % params['username']) if self.modif == 'test': params["password"] = '******' params["ip"] = '0.0.0.0' else: params["password"] = user_dict["password"] params["ip"] = user_dict.get("ip", "0.0.0.0") name = user_dict.get("nome", "") surname = user_dict.get("cognome", user_dict["username"]) params["first_name"] = name params["last_name"] = surname params["hidden"] = "True" == user_dict.get("fake", "False") params["timezone"] = 0.0 params["messages"] = [] params["questions"] = [] params["submissions"] = [] logger.info("User parameters loaded.") return params
def test_006(self): """Send a ~100B random binary file to the storage through FileCacher as a string. FC should cache the content locally. """ self.content = "".join(chr(random.randint(0, 255)) for unused_i in xrange(100)) logger.info(" I am sending the ~100B binary file to FileCacher") try: data = self.file_cacher.put_file_content(self.content, u"Test #005") except Exception as error: self.test_end(False, "Error received: %r." % error) return if not os.path.exists(os.path.join(self.cache_base_path, data)): self.test_end(False, "File not stored in local cache.") elif open(os.path.join(self.cache_base_path, data), "rb").read() != self.content: self.test_end(False, "Local cache's content differ " "from original file.") else: self.cache_path = os.path.join(self.cache_base_path, data) self.digest = data self.test_end(True, "Data sent and cached without error.")
def test_003(self): """Get file from FileCacher. """ logger.info(" I am retrieving the file from FileCacher " + "after deleting the cache.") os.unlink(self.cache_path) try: data = self.file_cacher.get_file(self.digest) except Exception as error: self.test_end(False, "Error received: %r." % error) return received = data.read() data.close() if received != self.content: self.test_end(False, "Content differ.") elif not os.path.exists(self.cache_path): self.test_end(False, "File not stored in local cache.") elif open(self.cache_path).read() != self.content: self.test_end(False, "Local cache's content differ " + "from original file.") else: self.test_end(True, "Content object received " + "and cached correctly.")
def get_user(self, username): """See docstring in class Loader. """ logger.info("Loading parameters for user %s." % username) conf = self.users_conf[username] assert username == conf['username'] args = {} load(conf, args, "username") load(conf, args, "password") load(conf, args, "ip") load(conf, args, ["first_name", "nome"]) load(conf, args, ["last_name", "cognome"]) if "first_name" not in args: args["first_name"] = "" if "last_name" not in args: args["last_name"] = args["username"] load(conf, args, ["hidden", "fake"], conv=lambda a: a == "True") logger.info("User parameters loaded.") return User(**args)
def test_009(self): """Get the ~100MB file from FileCacher. """ logger.info(" I am retrieving the ~100MB file from FileCacher " + "after deleting the cache.") os.unlink(self.cache_path) hash_file = HashingFile() try: self.file_cacher.get_file_to_fobj(self.digest, hash_file) except Exception as error: self.test_end(False, "Error received: %r." % error) return my_digest = hash_file.digest hash_file.close() try: if self.digest != my_digest: self.test_end(False, "Content differs.") elif not os.path.exists(self.cache_path): self.test_end(False, "File not stored in local cache.") else: self.test_end(True, "Content object received " + "and cached correctly.") finally: self.file_cacher.delete(self.digest)
def echo_callback(self, data, error=None): """Callback for check. """ current = time.time() logger.debug("Checker.echo_callback") if error is not None: return try: service, time_ = data.split() time_ = float(time_) name, shard = service.split(",") shard = int(shard) service = ServiceCoord(name, shard) if service not in self.waiting_for or current - time_ > 10: logger.warning("Got late reply (%5.3lf s) from %s." % (current - time_, service)) else: if time_ - self.waiting_for[service] > 0.001: logger.warning("Someone cheated on the timestamp?!") logger.info("Got reply (%5.3lf s) from %s." % (current - time_, service)) del self.waiting_for[service] except KeyError: logger.error("Echo answer mis-shapen.")
def release_worker(self, shard): """To be called by ES when it receives a notification that a job finished. Note: if the worker is scheduled to be disabled, then we disable it, and notify the ES to discard the outcome obtained by the worker. shard (int): the worker to release. returns (bool): if the result is to be ignored. """ if self._job[shard] == WorkerPool.WORKER_INACTIVE: err_msg = "Trying to release worker while it's inactive." logger.error(err_msg) raise ValueError(err_msg) ret = self._ignore[shard] self._start_time[shard] = None self._side_data[shard] = None self._ignore[shard] = False if self._schedule_disabling[shard]: self._job[shard] = WorkerPool.WORKER_DISABLED self._schedule_disabling[shard] = False logger.info("Worker %s released and disabled." % shard) else: self._job[shard] = WorkerPool.WORKER_INACTIVE logger.debug("Worker %s released." % shard) return ret
def search_jobs_not_done(self): """Look in the database for submissions that have not been compiled or evaluated for no good reasons. Put the missing job in the queue. """ new_jobs = 0 with SessionGen(commit=False) as session: contest = session.query(Contest).\ filter_by(id=self.contest_id).first() # Only adding submission not compiled/evaluated that have # not yet reached the limit of tries. for submission in contest.get_submissions(): if to_compile(submission): if self.push_in_queue( (EvaluationService.JOB_TYPE_COMPILATION, submission.id), EvaluationService.JOB_PRIORITY_HIGH, submission.timestamp): new_jobs += 1 elif to_evaluate(submission): if self.push_in_queue( (EvaluationService.JOB_TYPE_EVALUATION, submission.id), EvaluationService.JOB_PRIORITY_MEDIUM, submission.timestamp): new_jobs += 1 if new_jobs > 0: logger.info("Found %s submissions with jobs to do." % new_jobs) # Run forever. return True
def execute_job(self, job_dict): job = Job.import_from_dict_with_type(job_dict) if self.work_lock.acquire(False): try: logger.operation = "job '%s'" % (job.info) logger.info("Request received") job.shard = self.shard self.task_type = get_task_type(job, self.file_cacher) self.task_type.execute_job() logger.info("Request finished.") return job.export_to_dict() except: err_msg = "Worker failed on operation `%s'" % logger.operation logger.error("%s\n%s" % (err_msg, traceback.format_exc())) raise JobException(err_msg) finally: self.task_type = None self.session = None logger.operation = "" self.work_lock.release() else: err_msg = "Request '%s' received, " \ "but declined because of acquired lock" % \ (job.info) logger.warning(err_msg) raise JobException(err_msg)
def submit(self, timestamp, username, password, t_id, t_short, files, language): """Execute the request for a submission. timestamp (int): seconds from the start. username (string): username issuing the submission. password (string): password of username. t_id (string): id of the task. t_short (string): short name of the task. files ([dict]): list of dictionaries with keys 'filename' and 'digest'. language (string): the extension the files should have. """ logger.info("%s - Submitting for %s on task %s." % (to_time(timestamp), username, t_short)) submit_files = [] for f in files: filename_to_send = f["filename"] if language != None: filename_to_send = f["filename"].replace("%%l", language) submit_files.append([ f["filename"], filename_to_send, os.path.join(self.import_source, "files", f["digest"]) ]) browser = Browser() browser.set_handle_robots(False) step(LoginRequest(browser, username, password, base_url=self.cws_address)) step(SubmitMultifileRequest(browser, (int(t_id), t_short), files=files, base_url=self.cws_address))
def dispatch_operations(self): """Look at the operations still to do in the queue and tries to dispatch them """ pending = len(self.operation_queue) if pending > 0: logger.info("%s operations still pending." % pending) failed_rankings = set([]) new_queue = [] for method, args in self.operation_queue: if args[0] in failed_rankings: new_queue.append((method, args)) continue try: method(*args) except: logger.info("Ranking %s not connected or generic error." % args[0][0]) new_queue.append((method, args)) failed_rankings.add(args[0]) self.operation_queue = new_queue # We want this to run forever. return True
def submission_tokened(self, submission_id): """This RPC inform ScoringService that the user has played the token on a submission. submission_id (int): the id of the submission that changed. timestamp (int): the time of the token. """ with SessionGen(commit=False) as session: submission = Submission.get_from_id(submission_id, session) if submission is None: logger.error("[submission_tokened] Received token request for " "unexistent submission id %s." % submission_id) raise KeyError if submission.user.hidden: logger.info("[submission_tokened] Token for submission %d " "not sent because user is hidden." % submission_id) return # Mark submission as tokened. self.submissions_tokened.add(submission_id) # Update RWS. self.rankings_send_token(submission)
def import_contest(self, path): """Import a contest into the system, returning a dictionary that can be passed to Contest.import_from_dict(). """ params, tasks, users = self.get_params_for_contest(path) for i, task in enumerate(tasks): task_params = self.get_params_for_task(os.path.join(path, task), num=i) params["tasks"].append(task_params) if self.user_number is None: for user in users: user_params = self.get_params_for_user(user) params["users"].append(user_params) else: logger.info("Generating %s random users." % self.user_number) for i in xrange(self.user_number): user = User("User %d" % (i), "Last name %d" % (i), "user%03d" % (i)) if self.modif == 'test': user.password = '******' params["users"].append(user.export_to_dict()) return params
def submit(self, timestamp, username, password, t_id, t_short, files, language): """Execute the request for a submission. timestamp (int): seconds from the start. username (string): username issuing the submission. password (string): password of username. t_id (string): id of the task. t_short (string): short name of the task. files ([dict]): list of dictionaries with keys 'filename' and 'digest'. language (string): the extension the files should have. """ logger.info("%s - Submitting for %s on task %s." % (to_time(timestamp), username, t_short)) if len(files) != 1: logger.error("We cannot submit more than one file.") return # Copying submission files into a temporary directory with the # correct name. Otherwise, SubmissionRequest does not know how # to interpret the file (and which language are they in). temp_dir = tempfile.mkdtemp(dir=config.temp_dir) for file_ in files: temp_filename = os.path.join(temp_dir, file_["filename"].replace("%l", language)) shutil.copy(os.path.join(self.import_source, "files", files[0]["digest"]), temp_filename) file_["filename"] = temp_filename filename = os.path.join(files[0]["filename"]) browser = Browser() browser.set_handle_robots(False) step(LoginRequest(browser, username, password, base_url=self.cws_address)) step(SubmitRequest(browser, (int(t_id), t_short), filename=filename, base_url=self.cws_address)) shutil.rmtree(temp_dir)
def test_003(self): """Get file from FileCacher. """ logger.info(" I am retrieving the file from FileCacher " + "after deleting the cache.") os.unlink(self.cache_path) try: data = self.file_cacher.get_file(self.digest) except Exception as error: self.test_end(False, "Error received: %r." % error) return received = data.read() data.close() if received != self.content: self.test_end(False, "Content differ.") elif not os.path.exists(self.cache_path): self.test_end(False, "File not stored in local cache.") elif open(self.cache_path).read() != self.content: self.test_end( False, "Local cache's content differ " + "from original file.") else: self.test_end(True, "Content object received " + "and cached correctly.")
def score_old_submissions(self): """The submissions in the submission_ids_to_score set are evaluated submissions that we can assign a score to, and this method scores a bunch of these at a time. This method keeps getting called while the set is non-empty. (Exactly the same happens for the submissions to token.) Note: doing this way (instead of putting everything in the __init__) prevent freezing the service at the beginning in the case of many old submissions. """ self.scoring_old_submission = True to_score = len(self.submission_ids_to_score) to_token = len(self.submission_ids_to_token) to_score_now = to_score if to_score < 4 else 4 to_token_now = to_token if to_token < 16 else 16 logger.info("Old submission yet to score/token: %s/%s." % (to_score, to_token)) for unused_i in xrange(to_score_now): self.new_evaluation(self.submission_ids_to_score.pop()) if to_score - to_score_now > 0: return True for unused_i in xrange(to_token_now): submission_id, timestamp = self.submission_ids_to_token.pop() self.submission_tokened(submission_id, timestamp) if to_token - to_token_now > 0: return True logger.info("Finished loading old submissions.") self.scoring_old_submission = False return False
def test_009(self): """Get the ~100MB file from FileCacher. """ logger.info(" I am retrieving the ~100MB file from FileCacher " + "after deleting the cache.") os.unlink(self.cache_path) hash_file = HashingFile() try: self.file_cacher.get_file_to_fobj(self.digest, hash_file) except Exception as error: self.test_end(False, "Error received: %r." % error) return my_digest = hash_file.digest hash_file.close() try: if self.digest != my_digest: self.test_end(False, "Content differs.") elif not os.path.exists(self.cache_path): self.test_end(False, "File not stored in local cache.") else: self.test_end( True, "Content object received " + "and cached correctly.") finally: self.file_cacher.delete(self.digest)
def get_user(self, conf): """Produce a User object. Given an object of the first list returned by get_contest, construct a full User object and return it. Access the data on the filesystem if needed. return (User): the User object corresponding to the given dict. """ logger.info("Loading parameters for user %s." % conf['username']) args = {} load(conf, args, "username") load(conf, args, "password") load(conf, args, "ip") load(conf, args, "nome", "first_name") load(conf, args, "cognome", "last_name") if "first_name" not in args: args["first_name"] = "" if "last_name" not in args: args["last_name"] = args["username"] load(conf, args, "fake", "hidden", lambda a: a == "True") logger.info("User parameters loaded.") return User(**args)
def token(self, timestamp, username, password, t_id, t_short, submission_num): """Execute the request for releasing test a submission. timestamp (int): seconds from the start. username (string): username issuing the submission. password (string): password of username. t_id (string): id of the task. t_short (string): short name of the task. submission_num (string): id of the submission to release test. """ logger.info("%s - Playing token for %s on task %s" % (to_time(timestamp), username, t_short)) browser = Browser() browser.set_handle_robots(False) step( LoginRequest(browser, username, password, base_url=self.cws_address)) step( TokenRequest(browser, (int(t_id), t_short), submission_num=submission_num, base_url=self.cws_address))
def user_test_compilation_ended(self, user_test): """Actions to be performed when we have a user test that has ended compilation. In particular: we queue evaluation if compilation was ok; we requeue compilation if it failed. user_test (UserTest): the user test. """ # Compilation was ok, so we evaluate if user_test.compilation_outcome == "ok": self.push_in_queue( (EvaluationService.JOB_TYPE_TEST_EVALUATION, user_test.id), EvaluationService.JOB_PRIORITY_MEDIUM, user_test.timestamp, ) # If instead user test failed compilation, we don't evaluatate elif user_test.compilation_outcome == "fail": logger.info("User test %d did not compile. Not going " "to evaluate." % user_test.id) # If compilation failed for our fault, we requeue or not elif user_test.compilation_outcome is None: if user_test.compilation_tries > EvaluationService.MAX_TEST_COMPILATION_TRIES: logger.error( "Maximum tries reached for the " "compilation of user test %d. I will " "not try again." % (user_test.id) ) else: # Note: lower priority (MEDIUM instead of HIGH) for # compilations that are probably failing again self.push_in_queue( (EvaluationService.JOB_TYPE_TEST_COMPILATION, user_test.id), EvaluationService.JOB_PRIORITY_MEDIUM, user_test.timestamp, )
def toggle_autorestart(self, service): """If the service is scheduled for autorestart, disable it, otherwise enable it. service (string): format: name,shard. return (bool/None): current status of will_restart. """ # If the contest_id is not set, we cannot autorestart. if self.contest_id is None: return None # Decode name,shard try: idx = service.rindex(",") except ValueError: logger.error("Unable to decode service string.") name = service[:idx] try: shard = int(service[idx + 1:]) except ValueError: logger.error("Unable to decode service shard.") service = ServiceCoord(name, shard) self._will_restart[service] = not self._will_restart[service] logger.info("Will restart %s,%s is now %s." % (service.name, service.shard, self._will_restart[service])) return self._will_restart[service]
def score_old_submissions(self): """The submissions in the submission_results_to_score set are evaluated submissions that we can assign a score to, and this method scores a bunch of these at a time. This method keeps getting called while the set is non-empty. (Exactly the same happens for the submissions to token.) Note: doing this way (instead of putting everything in the __init__) prevent freezing the service at the beginning in the case of many old submissions. """ self.scoring_old_submission = True to_score = len(self.submission_results_to_score) to_token = len(self.submissions_to_token) to_score_now = to_score if to_score < 4 else 4 to_token_now = to_token if to_token < 16 else 16 logger.info("Old submission yet to score/token: %s/%s." % (to_score, to_token)) for unused_i in xrange(to_score_now): submission_id, dataset_id = self.submission_results_to_score.pop() self.new_evaluation(submission_id, dataset_id) if to_score - to_score_now > 0: return True for unused_i in xrange(to_token_now): submission_id = self.submissions_to_token.pop() self.submission_tokened(submission_id) if to_token - to_token_now > 0: return True logger.info("Finished loading old submissions.") self.scoring_old_submission = False return False
def invalidate_submission(self, submission_id=None, dataset_id=None, user_id=None, task_id=None): """Request for invalidating some scores. Invalidate the scores of the SubmissionResults that: - belong to submission_id or, if None, to any submission of user_id and/or task_id or, if both None, to any submission of the contest this service is running for. - belong to dataset_id or, if None, to any dataset of task_id or, if None, to any dataset of any task of the contest this service is running for. submission_id (int): id of the submission to invalidate, or None. dataset_id (int): id of the dataset to invalidate, or None. user_id (int): id of the user to invalidate, or None. task_id (int): id of the task to invalidate, or None. """ logger.info("Invalidation request received.") # Validate arguments # TODO Check that all these objects belong to this contest. with SessionGen(commit=True) as session: submission_results = get_submission_results( # Give contest_id only if all others are None. self.contest_id if {user_id, task_id, submission_id, dataset_id} == {None} else None, user_id, task_id, submission_id, dataset_id, session) logger.info("Submission results to invalidate scores for: %d." % len(submission_results)) if len(submission_results) == 0: return new_submission_results_to_score = set() for submission_result in submission_results: # If the submission is not evaluated, it does not have # a score to invalidate, and, when evaluated, # ScoringService will be prompted to score it. So in # that case we do not have to do anything. if submission_result.evaluated(): submission_result.invalidate_score() new_submission_results_to_score.add( (submission_result.submission_id, submission_result.dataset_id)) old_s = len(self.submission_results_to_score) old_t = len(self.submissions_to_token) self.submission_results_to_score |= new_submission_results_to_score if old_s + old_t == 0: self.add_timeout(self.score_old_submissions, None, 0.5, immediately=False)
def export_submissions(self): """Export submissions' source files. """ logger.info("Exporting submissions.") queue_file = codecs.open(os.path.join(self.spool_dir, "queue"), "w", encoding="utf-8") # FIXME - The enumeration of submission should be time-increasing for submission in self.contest.get_submissions(): if submission.user.hidden: continue logger.info("Exporting submission %s." % submission.id) username = submission.user.username task = submission.task.name timestamp = submission.timestamp # Get source files to the spool directory. file_digest = submission.files["%s.%s" % (task, "%l")].digest upload_filename = os.path.join( self.upload_dir, username, "%s.%d.%s" % (task, timestamp, submission.language)) self.file_cacher.get_file(file_digest, path=upload_filename) upload_filename = os.path.join( self.upload_dir, username, "%s.%s" % (task, submission.language)) self.file_cacher.get_file(file_digest, path=upload_filename) print >> queue_file, "./upload/%s/%s.%d.%s" % \ (username, task, timestamp, submission.language) # Write results file for the submission. if submission.evaluated(): res_file = codecs.open(os.path.join( self.spool_dir, "%d.%s.%s.%s.res" % (timestamp, username, task, submission.language)), "w", encoding="utf-8") res2_file = codecs.open(os.path.join( self.spool_dir, "%s.%s.%s.res" % (username, task, submission.language)), "w", encoding="utf-8") total = 0.0 for num, evaluation in enumerate(submission.evaluations): outcome = float(evaluation.outcome) total += outcome line = "Executing on file n. %2d %s (%.4f)" % \ (num, evaluation.text, outcome) print >> res_file, line print >> res2_file, line line = "Score: %.6f" % total print >> res_file, line print >> res2_file, line res_file.close() res2_file.close() print >> queue_file queue_file.close()
def post(self): submission_id = self.get_argument("submission_id", "") # Decrypt submission_id. try: submission_id = decrypt_number(submission_id) except ValueError: # We reply with Forbidden if the given ID cannot be # decrypted. logger.warning( "User %s tried to play a token " "on an undecryptable submission_id." % self.current_user.username ) raise tornado.web.HTTPError(403) # Find submission and check it is of the current user. submission = Submission.get_from_id(submission_id, self.sql_session) if submission is None or submission.user != self.current_user: logger.warning( "User %s tried to play a token " "on an unexisting submission_id." % self.current_user.username ) raise tornado.web.HTTPError(404) # Don't trust the user, check again if (s)he can really play # the token. timestamp = int(time.time()) if self.contest.tokens_available(self.current_user.username, submission.task.name, timestamp)[0] <= 0: logger.warning("User %s tried to play a token " "when it shouldn't." % self.current_user.username) # Add "no luck" notification self.application.service.add_notification( self.current_user.username, timestamp, self._("Token request discarded"), self._("Your request has been discarded because you have no " "tokens available."), ) self.redirect("/tasks/%s" % encrypt_number(submission.task.id)) return token = Token(timestamp, submission) self.sql_session.add(token) self.sql_session.commit() # Inform ScoringService and eventually the ranking that the # token has been played. self.application.service.scoring_service.submission_tokened(submission_id=submission_id, timestamp=timestamp) logger.info("Token played by user %s on task %s." % (self.current_user.username, submission.task.name)) # Add "All ok" notification self.application.service.add_notification( self.current_user.username, timestamp, self._("Token request received"), self._("Your request has been received " "and applied to the submission."), ) self.redirect("/tasks/%s" % encrypt_number(submission.task.id))
def evaluation_step_before_run( self, sandbox, command, executables_to_get, files_to_get, time_limit=0, memory_limit=0, allow_path=None, stdin_redirect=None, stdout_redirect=None, wait=False, ): """First part of an evaluation step, until the running. return: exit code already translated if wait is True, the process if wait is False. """ # Record the usage of the sandbox. self.sandbox_paths = ":".join([self.sandbox_paths, sandbox.path]) # Copy all necessary files. for filename, digest in executables_to_get.iteritems(): sandbox.create_file_from_storage(filename, digest, executable=True) for filename, digest in files_to_get.iteritems(): sandbox.create_file_from_storage(filename, digest) if allow_path is None: allow_path = [] # Set sandbox parameters suitable for evaluation. sandbox.chdir = sandbox.path sandbox.filter_syscalls = 2 sandbox.timeout = time_limit sandbox.wallclock_timeout = 2 * time_limit sandbox.address_space = memory_limit * 1024 sandbox.file_check = 1 sandbox.allow_path = allow_path sandbox.stdin_file = stdin_redirect sandbox.stdout_file = stdout_redirect stdout_filename = os.path.join(sandbox.path, "stdout.txt") stderr_filename = os.path.join(sandbox.path, "stderr.txt") if sandbox.stdout_file is None: sandbox.stdout_file = stdout_filename sandbox.stderr_file = stderr_filename # These syscalls and paths are used by executables generated # by fpc. sandbox.allow_path += ["/proc/self/exe"] sandbox.allow_syscall += ["getrlimit", "rt_sigaction", "ugetrlimit"] # This one seems to be used for a C++ executable. sandbox.allow_path += ["/proc/meminfo"] # Actually run the evaluation command. logger.info("Starting evaluation step.") return sandbox.execute_without_std(command, wait=wait)
def main(): parser = argparse.ArgumentParser( description="Updater of CMS contest dumps.") parser.add_argument( "-V", "--to-version", action="store", type=int, default=-1, help="Update to given version number") parser.add_argument( "path", help="location of the dump or of the 'contest.json' file") args = parser.parse_args() path = args.path to_version = args.to_version if to_version == -1: to_version = model_version if not path.endswith("contest.json"): path = os.path.join(path, "contest.json") if not os.path.exists(path): logger.critical( "The given path doesn't exist or doesn't contain a contest " "dump in a format CMS is able to understand.") return with io.open(path, 'rb') as fin: data = json.load(fin, encoding="utf-8") # If no "_version" field is found we assume it's a v1.0 # export (before the new dump format was introduced). dump_version = data.get("_version", 0) if dump_version == to_version: logger.info( "The dump you're trying to update is already stored using " "the most recent format supported by this version of CMS.") return if dump_version > to_version: logger.critical( "The dump you're trying to update is stored using a format " "that's more recent than the one supported by this version " "of CMS. You probably need to update CMS to handle it.") return for version in range(dump_version, to_version): # Update from version to version+1 updater = __import__( "cmscontrib.updaters.update_%d" % (version + 1), globals(), locals(), ["Updater"]).Updater(data) data = updater.run() data["_version"] = version + 1 assert data["_version"] == to_version with io.open(path, 'wb') as fout: json.dump(data, fout, encoding="utf-8", indent=4, sort_keys=True)
def dataset_updated(self, task_id): """This function updates RWS with new data about a task. It should be called after the live dataset of a task is changed. task_id (int): id of the task whose dataset has changed. """ with SessionGen(commit=False) as session: task = Task.get_from_id(task_id, session) dataset_id = task.active_dataset_id logger.info("Dataset update for task %d (dataset now is %d)." % ( task_id, dataset_id)) submission_ids = get_submissions(self.contest_id, task_id=task_id) subchanges = [] with SessionGen(commit=False) as session: for submission_id in submission_ids: submission = Submission.get_from_id(submission_id, session) submission_result = SubmissionResult.get_from_id( (submission_id, dataset_id), session) if submission_result is None: # Not yet compiled, evaluated or scored. score = None ranking_score_details = None else: score = submission_result.score try: ranking_score_details = json.loads( submission_result.ranking_score_details) except (json.decoder.JSONDecodeError, TypeError): # It may be blank. ranking_score_details = None # Data to send to remote rankings. subchange_id = "%s%ss" % \ (int(make_timestamp(submission.timestamp)), submission_id) subchange_put_data = { "submission": encode_id(submission_id), "time": int(make_timestamp(submission.timestamp))} # We're sending the unrounded score to RWS if score is not None: subchange_put_data["score"] = score if ranking_score_details is not None: subchange_put_data["extra"] = ranking_score_details subchanges.append((subchange_id, subchange_put_data)) # Adding operations to the queue. with self.operation_queue_lock: for ranking in self.rankings: for subchange_id, data in subchanges: self.subchange_queue.setdefault( ranking, dict())[encode_id(subchange_id)] = data
def reinitialize(self): """Inform the service that something in the data of the contest has changed (users, tasks, the contest itself) and we need to do it over again. This should be almost like restarting the service. """ logger.info("Reinitializing rankings.") self.rankings_initialize()
def export_submissions(self): """Export submissions' source files. """ logger.info("Exporting submissions.") queue_file = codecs.open(os.path.join(self.spool_dir, "queue"), "w", encoding="utf-8") for submission in self.submissions: logger.info("Exporting submission %s." % submission.id) username = submission.user.username task = submission.task.name timestamp = submission.timestamp # Get source files to the spool directory. file_digest = submission.files["%s.%s" % (task, "%l")].digest upload_filename = os.path.join( self.upload_dir, username, "%s.%d.%s" % (task, timestamp, submission.language)) self.file_cacher.get_file(file_digest, path=upload_filename) upload_filename = os.path.join( self.upload_dir, username, "%s.%s" % (task, submission.language)) self.file_cacher.get_file(file_digest, path=upload_filename) print >> queue_file, "./upload/%s/%s.%d.%s" % \ (username, task, timestamp, submission.language) # Write results file for the submission. if submission.evaluated(): res_file = codecs.open(os.path.join( self.spool_dir, "%d.%s.%s.%s.res" % (timestamp, username, task, submission.language)), "w", encoding="utf-8") res2_file = codecs.open(os.path.join( self.spool_dir, "%s.%s.%s.res" % (username, task, submission.language)), "w", encoding="utf-8") total = 0.0 for num, evaluation in enumerate(submission.evaluations): outcome = float(evaluation.outcome) total += outcome line = "Executing on file n. %2d %s (%.4f)" % \ (num, evaluation.text, outcome) print >> res_file, line print >> res2_file, line line = "Score: %.6f" % total print >> res_file, line print >> res2_file, line res_file.close() res2_file.close() print >> queue_file queue_file.close()
def do_export(self): """Run the actual export code. """ logger.operation = "exporting contest %d" % self.contest_id logger.info("Starting export.") export_dir = self.export_target archive_info = get_archive_info(self.export_target) if archive_info["write_mode"] != "": # We are able to write to this archive. if os.path.exists(self.export_target): logger.critical("The specified file already exists, " "I won't overwrite it.") return False export_dir = os.path.join(tempfile.mkdtemp(), archive_info["basename"]) logger.info("Creating dir structure.") try: os.mkdir(export_dir) except OSError: logger.critical("The specified directory already exists, " "I won't overwrite it.") return False files_dir = os.path.join(export_dir, "files") descr_dir = os.path.join(export_dir, "descriptions") os.mkdir(files_dir) os.mkdir(descr_dir) with SessionGen(commit=False) as session: contest = Contest.get_from_id(self.contest_id, session) # Export files. logger.info("Exporting files.") files = contest.enumerate_files(self.skip_submissions, self.skip_user_tests, light=self.light) for _file in files: if not self.safe_get_file(_file, os.path.join(files_dir, _file), os.path.join(descr_dir, _file)): return False # Export the contest in JSON format. logger.info("Exporting the contest in JSON format.") with open(os.path.join(export_dir, "contest.json"), "w") as fout: json.dump(contest.export_to_dict(self.skip_submissions, self.skip_user_tests), fout, indent=4) # If the admin requested export to file, we do that. if archive_info["write_mode"] != "": archive = tarfile.open(self.export_target, archive_info["write_mode"]) archive.add(export_dir, arcname=archive_info["basename"]) archive.close() shutil.rmtree(export_dir) logger.info("Export finished.") logger.operation = "" return True
def invalidate_submission(self, submission_id=None, user_id=None, task_id=None): """Request for invalidating some scores. Invalidate the scores of the Submission whose ID is submission_id or, if None, of those whose user is user_id and/or whose task is task_id or, if both None, of those that belong to the contest this service is running for. submission_id (int): id of the submission to invalidate, or None. user_id (int): id of the user to invalidate, or None. task_id (int): id of the task to invalidate, or None. """ logger.info("Invalidation request received.") # Validate arguments # TODO Check that all these objects belong to this contest. with SessionGen(commit=True) as session: submissions = get_submissions( # Give contest_id only if all others are None. self.contest_id if {user_id, task_id, submission_id} == {None} else None, user_id, task_id, submission_id, session) logger.info("Submissions to invalidate scores for: %d." % len(submissions)) if len(submissions) == 0: return new_submissions_to_score = set() for submission in submissions: # If the submission is not evaluated, it does not have # a score to invalidate, and, when evaluated, # ScoringService will be prompted to score it. So in # that case we do not have to do anything. if submission.evaluated(): submission.invalidate_score() new_submissions_to_score.add(submission.id) old_s = len(self.submissions_to_score) old_t = len(self.submissions_to_token) self.submissions_to_score |= new_submissions_to_score if old_s + old_t == 0: self.add_timeout(self.score_old_submissions, None, 0.5, immediately=False)
def acquire_worker(self, job, side_data=None): """Tries to assign a job to an available worker. If no workers are available then this returns None, otherwise this returns the chosen worker. job (job): the job to assign to a worker side_data (object): object to attach to the worker for later use returns (int): None if no workers are available, the worker assigned to the job otherwise """ # We look for an available worker try: shard = self.find_worker(WorkerPool.WORKER_INACTIVE, require_connection=True, random_worker=True) except LookupError: return None # Then we fill the info for future memory self._job[shard] = job self._start_time[shard] = make_datetime() self._side_data[shard] = side_data logger.debug("Worker %s acquired." % shard) # And finally we ask the worker to do the job action, object_id = job timestamp = side_data[1] queue_time = self._start_time[shard] - timestamp logger.info( "Asking worker %s to %s submission/user test %d " " (%s after submission)." % (shard, action, object_id, queue_time) ) with SessionGen(commit=False) as session: if action == EvaluationService.JOB_TYPE_COMPILATION: submission = Submission.get_from_id(object_id, session) job_ = CompilationJob.from_submission(submission) elif action == EvaluationService.JOB_TYPE_EVALUATION: submission = Submission.get_from_id(object_id, session) job_ = EvaluationJob.from_submission(submission) elif action == EvaluationService.JOB_TYPE_TEST_COMPILATION: user_test = UserTest.get_from_id(object_id, session) job_ = CompilationJob.from_user_test(user_test) elif action == EvaluationService.JOB_TYPE_TEST_EVALUATION: user_test = UserTest.get_from_id(object_id, session) job_ = EvaluationJob.from_user_test(user_test) job_.get_output = True job_.only_execution = True self._worker[shard].execute_job( job_dict=job_.export_to_dict(), callback=self._service.action_finished.im_func, plus=(action, object_id, side_data, shard), ) return shard
def ignore_job(self): """RPC that inform the worker that its result for the current action will be discarded. The worker will try to return as soon as possible even if this means that the result are inconsistent. """ # We remember to quit as soon as possible. logger.info("Trying to interrupt job as requested.") self._ignore_job = True
def test_005(self): """Get unexisting file from FileCacher. """ logger.info(" I am retrieving an unexisting file from FileCacher.") try: self.file_cacher.get_file(self.digest) except Exception as error: self.test_end(True, "Correctly received an error: %r." % error) else: self.test_end(False, "Did not receive error.")
def reinitialize(self): """Inform the service that something in the data of the contest has changed (users, tasks, the contest itself) and we need to do it over again. This should be almost like restarting the service. """ logger.info("Reinitializing rankings.") self._initialize_scorers() with self.operation_queue_lock: for ranking in self.rankings: self.initialize_queue.add(ranking)
def get_params_for_contest(self, path): """Given the path of a contest, extract the data from its contest.yaml file, and create a dictionary with the parameter required by Contest.import_from_dict(). Returns that dictionary and the two pieces of data that must be processed with get_params_for_task and get_params_for_users. path (string): the input directory. return (dict): data of the contest. """ path = os.path.realpath(path) name = os.path.split(path)[1] conf = yaml.load( codecs.open(os.path.join(path, "contest.yaml"), "r", "utf-8")) logger.info("Loading parameters for contest %s." % name) params = {} params["name"] = name assert name == conf["nome_breve"] params["description"] = conf["nome"] params["token_initial"] = conf.get("token_initial", None) params["token_max"] = conf.get("token_max", None) params["token_total"] = conf.get("token_total", None) params["token_min_interval"] = conf.get("token_min_interval", 0) params["token_gen_time"] = conf.get("token_gen_time", 0) params["token_gen_number"] = conf.get("token_gen_number", 0) if params["token_gen_time"] is None or \ params["token_gen_number"] is None: params["token_gen_time"] = 1 params["token_gen_number"] = 0 if self.modif == 'zero_time': params["start"] = 0 params["stop"] = 0 elif self.modif == 'test': params["start"] = 0 params["stop"] = 2000000000 else: params["start"] = conf.get("inizio", 0) params["stop"] = conf.get("fine", 0) logger.info("Contest parameters loaded.") params["tasks"] = [] params["users"] = [] params["announcements"] = [] return params, conf["problemi"], conf["utenti"]
def ignore_job(self): """RPC that inform the worker that its result for the current action will be discarded. The worker will try to return as soon as possible even if this means that the result are inconsistent. """ # We inform the task_type to quit as soon as possible. logger.info("Trying to interrupt job as requested.") try: self.task_type.ignore_job = True except AttributeError: pass # Job concluded right under our nose, that's ok too.
def search_jobs_not_done(self): """Look in the database for submissions that have not been scored for no good reasons. Put the missing job in the queue. """ # Do this only if we are not still loading old submission # (from the start of the service). if self.scoring_old_submission: return True with SessionGen(commit=False) as session: new_submission_ids_to_score = set([]) new_submission_ids_to_token = set([]) contest = session.query(Contest).\ filter_by(id=self.contest_id).first() for submission in contest.get_submissions(): for dataset in get_autojudge_datasets(submission.task): # If a submission result does not yet exist, then we don't # need to score it. r = SubmissionResult.get_from_id( (submission.id, dataset.id), session) if r is None: continue x = (r.submission_id, r.dataset_id) if r is not None and (r.evaluated() or r.compilation_outcome == "fail") \ and x not in self.submission_ids_scored: new_submission_ids_to_score.add(x) if r.submission.tokened() and r.submission_id not in \ self.submission_ids_tokened: new_submission_ids_to_token.add( (r.submission_id, make_timestamp(r.submission.token.timestamp))) new_s = len(new_submission_ids_to_score) old_s = len(self.submission_ids_to_score) new_t = len(new_submission_ids_to_token) old_t = len(self.submission_ids_to_token) logger.info("Submissions found to score/token: %d, %d." % (new_s, new_t)) if new_s + new_t > 0: self.submission_ids_to_score |= new_submission_ids_to_score self.submission_ids_to_token |= new_submission_ids_to_token if old_s + old_t == 0: self.add_timeout(self.score_old_submissions, None, 0.5, immediately=False) # Run forever. return True
def dump_database(self, export_dir): """Dump the whole database. This is never used; however, this part is retained for historical reasons. """ # Warning: this part depends on the specific database used. logger.info("Dumping SQL database.") (engine, connection) = config.database.split(':', 1) db_exportfile = os.path.join(export_dir, "database_dump.sql") # Export procedure for PostgreSQL. if engine == 'postgresql': db_regex = re.compile('//(\w*):(\w*)@(\w*)/(\w*)') db_match = db_regex.match(connection) if db_match is not None: username, password, host, database = db_match.groups() os.environ['PGPASSWORD'] = password export_res = os.system('pg_dump -h %s -U %s -w %s -x " \ "--attribute-inserts > %s' % (host, username, database, db_exportfile)) del os.environ['PGPASSWORD'] if export_res != 0: logger.critical("Database export failed.") return False else: logger.critical("Cannot obtain parameters for " "database connection.") return False # Export procedure for SQLite. elif engine == 'sqlite': db_regex = re.compile('///(.*)') db_match = db_regex.match(connection) if db_match is not None: dbfile, = db_match.groups() export_res = os.system('sqlite3 %s .dump > %s' % (dbfile, db_exportfile)) if export_res != 0: logger.critical("Database export failed.") return False else: logger.critical("Cannot obtain parameters for " "database connection.") return False else: logger.critical("Database engine not supported. :-(") return False return True
def submission_tokened(self, submission_id): """This RPC inform ScoringService that the user has played the token on a submission. submission_id (int): the id of the submission that changed. timestamp (int): the time of the token. """ with SessionGen(commit=False) as session: submission = Submission.get_from_id(submission_id, session) if submission is None: logger.error("[submission_tokened] Received token request for " "unexistent submission id %s." % submission_id) raise KeyError elif submission.user.hidden: logger.info("[submission_tokened] Token for submission %d " "not sent because user is hidden." % submission_id) return # Mark submission as tokened. self.submissions_tokened.add(submission_id) # Data to send to remote rankings. submission_put_data = { "user": encode_id(submission.user.username), "task": encode_id(submission.task.name), "time": int(make_timestamp(submission.timestamp)) } subchange_id = "%s%st" % \ (int(make_timestamp(submission.token.timestamp)), submission_id) subchange_put_data = { "submission": encode_id(submission_id), "time": int(make_timestamp(submission.token.timestamp)), "token": True } # Adding operations to the queue. with self.operation_queue_lock: for ranking in self.rankings: self.submission_queue.setdefault( ranking, dict())[encode_id(submission_id)] = \ submission_put_data self.subchange_queue.setdefault( ranking, dict())[encode_id(subchange_id)] = \ subchange_put_data
def precache_files(self, contest_id): """RPC to ask the worker to precache of files in the contest. contest_id (int): the id of the contest """ # Lock is not needed if the admins correctly placed cache and # temp directories in the same filesystem. This is what # usually happens since they are children of the same, # cms-created, directory. logger.info("Precaching files for contest %d." % contest_id) with SessionGen(commit=False) as session: contest = Contest.get_from_id(contest_id, session) for digest in contest.enumerate_files(skip_submissions=True): self.file_cacher.get_file(digest) logger.info("Precaching finished.")
def test_002(self): """Check the size of the file. """ logger.info(" I am checking the size of the ~100B binary file") try: size = self.file_cacher.get_size(self.digest) except Exception as error: self.test_end(False, "Error received: %r." % error) if size == self.size: self.test_end(True, "The size is correct.") else: self.test_end( False, "The size is wrong: %d instead of %d" % (size, self.size))
def replay(self): """Start replaying the events in source on the CWS at the specified address. """ with self.speed_lock: index = 0 if self.start_from is not None: while index < len(self.events) \ and float(self.events[index][0]) < self.start_from: index += 1 self.start = time.time() - self.start_from else: self.start = time.time() while index < len(self.events): timestamp, username, password, task_id, task_name, type_, data \ = self.events[index] to_wait = (timestamp / self.speed - (time.time() - self.start)) while to_wait > .5: if 0 < to_wait % 10 <= .5: logger.info("Next event in %d seconds." % int(to_wait)) time.sleep(.5) to_wait = (timestamp / self.speed - (time.time() - self.start)) if to_wait > 0: time.sleep(to_wait) if type_ == "s": # Submit. files, language = data self.submit(timestamp=timestamp, username=username, password=password, t_id=task_id, t_short=task_name, files=files, language=language) elif type_ == "t": # Token. self.token(timestamp=timestamp, username=username, password=password, t_id=task_id, t_short=task_name, submission_num=data) else: logger.warning("Unexpected type `%s', ignoring." % type_) index += 1
def evaluation_step_before_run(sandbox, command, time_limit=0, memory_limit=0, allow_path=None, stdin_redirect=None, stdout_redirect=None, wait=False): """First part of an evaluation step, until the running. return: exit code already translated if wait is True, the process if wait is False. """ # Set sandbox parameters suitable for evaluation. sandbox.chdir = sandbox.path sandbox.filter_syscalls = 2 sandbox.timeout = time_limit sandbox.wallclock_timeout = 2 * time_limit sandbox.address_space = memory_limit * 1024 sandbox.file_check = 1 if allow_path is None: allow_path = [] sandbox.allow_path = allow_path sandbox.stdin_file = stdin_redirect sandbox.stdout_file = stdout_redirect stdout_filename = os.path.join(sandbox.path, "stdout.txt") stderr_filename = os.path.join(sandbox.path, "stderr.txt") if sandbox.stdout_file is None: sandbox.stdout_file = stdout_filename sandbox.stderr_file = stderr_filename # These syscalls and paths are used by executables generated # by fpc. sandbox.allow_path += [ "/proc/self/exe", "/etc/timezone", "/usr/share/zoneinfo/" ] sandbox.allow_syscall += [ "getrlimit", "rt_sigaction", "ugetrlimit", "time" ] # This one seems to be used for a C++ executable. sandbox.allow_path += ["/proc/meminfo"] # This is used by freopen in Ubuntu 12.04. sandbox.allow_syscall += ["dup3"] # Actually run the evaluation command. logger.info("Starting execution step.") return sandbox.execute_without_std(command, wait=wait)
def _fetch_write_chunk(self): """Send a chunk of the file to the browser. """ data = self.temp_file.read(FileCacher.CHUNK_SIZE) length = len(data) self.size += length / 1024.0 / 1024.0 self.write(data) if length < FileCacher.CHUNK_SIZE: self.temp_file.close() os.unlink(self.temp_filename) duration = time.time() - self.start_time logger.info("%.3lf seconds for %.3lf MB, %.3lf MB/s" % (duration, self.size, self.size / duration)) self.finish() return False return True
def post(self): username = self.get_argument("username", "") password = self.get_argument("password", "") user = self.sql_session.query(User)\ .filter(User.auth_type == "Password")\ .filter(User.contest == self.contest)\ .filter(User.username == username).first() filtered_user = filter_ascii(username) filtered_pass = filter_ascii(password) if user is None or user.password != password: logger.info("Login error: user=%s pass=%s remote_ip=%s." % (filtered_user, filtered_pass, self.request.remote_ip)) self.redirect("/?login_error=true") return self.try_user_login(user)
def invalidate_submission(self, submission_id=None, user_id=None, task_id=None): """Request for invalidating the scores of some submissions. The scores to be cleared are the one regarding 1) a submission or 2) all submissions of a user or 3) all submissions of a task or 4) all submission (if all parameters are None). submission_id (int): id of the submission to invalidate, or None. user_id (int): id of the user we want to invalidate, or None. task_id (int): id of the task we want to invalidate, or None. """ logger.info("Invalidation request received.") submission_ids = get_submissions(self.contest_id, submission_id, user_id, task_id) logger.info("Submissions to invalidate: %s" % len(submission_ids)) if len(submission_ids) == 0: return new_submission_ids = [] with SessionGen(commit=True) as session: for submission_id in submission_ids: submission = Submission.get_from_id(submission_id, session) # If the submission is not evaluated, it does not have # a score to invalidate, and, when evaluated, # ScoringService will be prompted to score it. So in # that case we do not have to do anything. if submission.evaluated(): submission.invalidate_score() new_submission_ids.append(submission_id) old_s = len(self.submission_ids_to_score) old_t = len(self.submission_ids_to_token) self.submission_ids_to_score |= new_submission_ids if old_s + old_t == 0: self.add_timeout(self.score_old_submissions, None, 0.5, immediately=False)
def submit(self, timestamp, username, password, t_id, t_short, files, language): """Execute the request for a submission. timestamp (int): seconds from the start. username (string): username issuing the submission. password (string): password of username. t_id (string): id of the task. t_short (string): short name of the task. files ([dict]): list of dictionaries with keys 'filename' and 'digest'. language (string): the extension the files should have. """ logger.info("%s - Submitting for %s on task %s." % (to_time(timestamp), username, t_short)) if len(files) != 1: logger.error("We cannot submit more than one file.") return # Copying submission files into a temporary directory with the # correct name. Otherwise, SubmissionRequest does not know how # to interpret the file (and which language are they in). temp_dir = tempfile.mkdtemp(dir=config.temp_dir) for file_ in files: temp_filename = os.path.join( temp_dir, file_["filename"].replace("%l", language)) shutil.copy( os.path.join(self.import_source, "files", files[0]["digest"]), temp_filename) file_["filename"] = temp_filename filename = os.path.join(files[0]["filename"]) browser = Browser() browser.set_handle_robots(False) step( LoginRequest(browser, username, password, base_url=self.cws_address)) step( SubmitRequest(browser, (int(t_id), t_short), filename=filename, base_url=self.cws_address)) shutil.rmtree(temp_dir)