From cb6bd0afb1406fb21faaec7545e560e5a24e8c8c Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Thu, 4 Apr 2024 19:47:46 +0200 Subject: [PATCH 01/15] First refactors --- src/sinol_make/commands/run/__init__.py | 49 ++++++++------------ src/sinol_make/helpers/compile.py | 3 +- src/sinol_make/helpers/package_util.py | 1 + src/sinol_make/structs/package_structs.py | 7 +++ src/sinol_make/task_type/__init__.py | 16 +++++++ src/sinol_make/task_type/base.py | 56 +++++++++++++++++++++++ src/sinol_make/task_type/normal.py | 51 +++++++++++++++++++++ tests/commands/run/test_integration.py | 5 +- 8 files changed, 153 insertions(+), 35 deletions(-) create mode 100644 src/sinol_make/structs/package_structs.py create mode 100644 src/sinol_make/task_type/__init__.py create mode 100644 src/sinol_make/task_type/base.py create mode 100644 src/sinol_make/task_type/normal.py diff --git a/src/sinol_make/commands/run/__init__.py b/src/sinol_make/commands/run/__init__.py index 79612732..982303c5 100644 --- a/src/sinol_make/commands/run/__init__.py +++ b/src/sinol_make/commands/run/__init__.py @@ -5,13 +5,14 @@ import signal import threading import time +import traceback import psutil import glob import shutil from io import StringIO from typing import Dict -from sinol_make import contest_types, oiejq +from sinol_make import contest_types, oiejq, task_type from sinol_make.structs.run_structs import ExecutionData, PrintData from sinol_make.structs.cache_structs import CacheTest, CacheFile from sinol_make.helpers.parsers import add_compilation_arguments @@ -357,6 +358,8 @@ def compile_solutions(self, solutions, is_checker=False): def compile(self, solution, use_extras = False, is_checker = False): + os.makedirs(paths.get_compilation_log_path(), exist_ok=True) + os.makedirs(paths.get_executables_path(), exist_ok=True) compile_log_file = paths.get_compilation_log_path("%s.compile_log" % package_util.get_file_name(solution)) source_file = os.path.join(os.getcwd(), "prog", self.get_solution_from_exe(solution)) output = paths.get_executables_path(package_util.get_executable(solution)) @@ -506,7 +509,8 @@ def sigint_handler(signum, frame): result.Status = Status.ML else: try: - correct, result.Points = self.check_output(name, input_file_path, output_file_path, output, answer_file_path) + correct, result.Points = self.task_type.check_output(input_file_path, output_file_path, + output, answer_file_path) if not correct: result.Status = Status.WA except CheckerOutputException as e: @@ -613,8 +617,8 @@ def sigint_handler(signum, frame): result.Status = Status.ML else: try: - correct, result.Points = self.check_output(name, input_file_path, output_file_path, output, - answer_file_path) + correct, result.Points = self.task_type.check_output(input_file_path, output_file_path, + output, answer_file_path) if correct: result.Status = Status.OK else: @@ -800,7 +804,7 @@ def validate_expected_scores(self, results): if group not in self.scores: util.exit_with_error(f'Group {group} doesn\'t have points specified in config file.') - if self.checker is None: + if self.task_type.has_checker(): for solution in results.keys(): new_expected_scores[solution] = { "expected": results[solution], @@ -1025,6 +1029,7 @@ def set_constants(self): self.ID = package_util.get_task_id() self.SOURCE_EXTENSIONS = ['.c', '.cpp', '.py', '.java'] self.SOLUTIONS_RE = package_util.get_solutions_re(self.ID) + self.task_type = task_type.get_task_type() def validate_arguments(self, args): @@ -1165,25 +1170,13 @@ def compile_checker(self): if not checker_compilation[0]: util.exit_with_error('Checker compilation failed.') - def check_had_checker(self, has_checker): - """ - Checks if there was a checker and if it is now removed (or the other way around) and if so, removes tests cache. - In theory, removing cache after adding a checker is redundant, because during its compilation, the cache is - removed. - """ - had_checker = os.path.exists(paths.get_cache_path("checker")) - if (had_checker and not has_checker) or (not had_checker and has_checker): - cache.remove_results_cache() - if has_checker: - with open(paths.get_cache_path("checker"), "w") as f: - f.write("") - else: - try: - os.remove(paths.get_cache_path("checker")) - except FileNotFoundError: - pass + def compile_additional_files(self, files): + for name, args, kwargs in files: + print(f'Compiling {name}...') + self.compile(*args, **kwargs) + def run(self, args): args = util.init_package_command(args) @@ -1208,14 +1201,7 @@ def run(self, args): cache.process_extra_execution_files(self.config.get("extra_execution_files", {}), self.ID) cache.remove_results_if_contest_type_changed(self.config.get("sinol_contest_type", "default")) - checker = package_util.get_files_matching_pattern(self.ID, f'{self.ID}chk.*') - if len(checker) != 0: - print(util.info("Checker found: %s" % os.path.basename(checker[0]))) - self.checker = checker[0] - self.compile_checker() - else: - self.checker = None - self.check_had_checker(self.checker is not None) + self.compile_additional_files(self.task_type.get_files_to_compile()) lib = package_util.get_files_matching_pattern(self.ID, f'{self.ID}lib.*') self.has_lib = len(lib) != 0 @@ -1243,7 +1229,8 @@ def run(self, args): self.config = util.try_fix_config(self.config) try: validation_results = self.validate_expected_scores(results) - except Exception: + except Exception as e: + print(traceback.format_exc()) util.exit_with_error("Validating expected scores failed. " "This probably means that `sinol_expected_scores` is broken. " "Delete it and run `sinol-make run --apply-suggestions` again.") diff --git a/src/sinol_make/helpers/compile.py b/src/sinol_make/helpers/compile.py index 07500090..0b60aebf 100644 --- a/src/sinol_make/helpers/compile.py +++ b/src/sinol_make/helpers/compile.py @@ -113,8 +113,7 @@ def compile(program, output, compilers: Compilers = None, compile_log=None, comp def compile_file(file_path: str, name: str, compilers: Compilers, compilation_flags='default', - use_fsanitize=False, additional_flags=None) \ - -> Tuple[Union[str, None], str]: + use_fsanitize=False, additional_flags=None) -> Tuple[Union[str, None], str]: """ Compile a file :param file_path: Path to the file to compile diff --git a/src/sinol_make/helpers/package_util.py b/src/sinol_make/helpers/package_util.py index d155e746..edaff6d7 100644 --- a/src/sinol_make/helpers/package_util.py +++ b/src/sinol_make/helpers/package_util.py @@ -8,6 +8,7 @@ from sinol_make import util from sinol_make.helpers import paths +from sinol_make.structs.package_structs import TaskType def get_task_id() -> str: diff --git a/src/sinol_make/structs/package_structs.py b/src/sinol_make/structs/package_structs.py new file mode 100644 index 00000000..3fb0f249 --- /dev/null +++ b/src/sinol_make/structs/package_structs.py @@ -0,0 +1,7 @@ +from enum import Enum + + +class TaskType(Enum): + NORMAL = 1 + INTERACTIVE_IO = 2 + ENCDEC = 3 diff --git a/src/sinol_make/task_type/__init__.py b/src/sinol_make/task_type/__init__.py new file mode 100644 index 00000000..89726bca --- /dev/null +++ b/src/sinol_make/task_type/__init__.py @@ -0,0 +1,16 @@ +import os + +from sinol_make.helpers import package_util +from sinol_make.task_type.base import BaseTaskType +from sinol_make.task_type.normal import NormalTask + + +def get_task_type() -> BaseTaskType: + if 'encdec' in os.listdir(os.getcwd()): + # Encdec is not actually supported by sinol-make, as it isn't yet merged in OIOIOI. + # (And probably never will) + raise NotImplementedError("Encdec is not supported by sinol-make.") + task_id = package_util.get_task_id() + if package_util.any_files_matching_pattern(task_id, f"{task_id}soc.*"): + pass + return NormalTask(task_id) diff --git a/src/sinol_make/task_type/base.py b/src/sinol_make/task_type/base.py new file mode 100644 index 00000000..45f6740b --- /dev/null +++ b/src/sinol_make/task_type/base.py @@ -0,0 +1,56 @@ +from typing import List, Dict, Any, Tuple + +from sinol_make import util +from sinol_make.interfaces.Errors import CheckerOutputException + + +class BaseTaskType: + def __init__(self, task_id): + self.task_id = task_id + self._has_checker = False + + def get_files_to_compile(self) -> List[Tuple[str, List[str], Dict[str, Any]]]: + """ + Returns a list of tuples where: + - the first element is what will be printed in `Compiling {first_element}` + - the second element is a list of *args passed to `run.compile` + - the third element is a dictionary of **kwargs passed to `run.compile` + """ + pass + + def has_checker(self) -> bool: + return self._has_checker + + def _run_checker(self, input_file, output_file_path, answer_file_path) -> List[str]: + return [] + + def _parse_checker_output(self, checker_output: List[str]) -> Tuple[bool, int]: + """ + Parse the output of the checker + :return: tuple of (is_correct, score) + """ + if len(checker_output) == 0: + raise CheckerOutputException("Checker output is empty.") + + if checker_output[0].strip() == "OK": + points = 100 + if len(checker_output) >= 3: + try: + points = int(checker_output[2].strip()) + except ValueError: + pass + + return True, points + else: + return False, 0 + + def check_output(self, input_file, output_file_path, output, answer_file_path) -> Tuple[bool, int]: + if self._has_checker: + with open(output_file_path, "w") as output_file: + output_file.write("\n".join(output) + "\n") + checker_output = self._run_checker(input_file, output_file_path, answer_file_path) + return self._parse_checker_output(checker_output) + else: + with open(answer_file_path, "r") as answer_file: + correct = util.lines_diff(output, answer_file.readlines()) + return correct, 100 if correct else 0 diff --git a/src/sinol_make/task_type/normal.py b/src/sinol_make/task_type/normal.py new file mode 100644 index 00000000..79f9cbae --- /dev/null +++ b/src/sinol_make/task_type/normal.py @@ -0,0 +1,51 @@ +import os +import subprocess +from typing import List + +from sinol_make.helpers import package_util, paths, cache +from sinol_make.task_type.base import BaseTaskType + + +class NormalTask(BaseTaskType): + def __init__(self, task_id): + super().__init__(task_id) + self.checker = None + self.checker_exe = None + + def _check_had_checker(self, has_checker): + """ + Checks if there was a checker and if it is now removed (or the other way around) and if so, removes tests cache. + In theory, removing cache after adding a checker is redundant, because during its compilation, the cache is + removed. + """ + had_checker = os.path.exists(paths.get_cache_path("checker")) + if (had_checker and not has_checker) or (not had_checker and has_checker): + cache.remove_results_cache() + if has_checker: + with open(paths.get_cache_path("checker"), "w") as f: + f.write("") + else: + try: + os.remove(paths.get_cache_path("checker")) + except FileNotFoundError: + pass + + def get_files_to_compile(self): + super().get_files_to_compile() + checkers = package_util.get_files_matching_pattern(self.task_id, f'{self.task_id}chk.*') + if checkers: + self._has_checker = True + self.checker = checkers[0] + self._check_had_checker(True) + self.checker_exe = paths.get_executables_path(package_util.get_executable(self.checker)) + return [("checker", [self.checker], {"is_checker": True})] + else: + self._has_checker = False + self._check_had_checker(False) + return [] + + def _run_checker(self, input_file, output_file_path, answer_file_path) -> List[str]: + command = [self.checker_exe, input_file, output_file_path, answer_file_path] + process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) + process.wait() + return process.communicate()[0].decode("utf-8").splitlines() diff --git a/tests/commands/run/test_integration.py b/tests/commands/run/test_integration.py index da17eb97..54990fb5 100644 --- a/tests/commands/run/test_integration.py +++ b/tests/commands/run/test_integration.py @@ -651,7 +651,7 @@ def test_results_caching_checker_changed(create_package, time_tool): f.write("// Changed checker source code.\n" + checker_source) # Compile checker check if test results are removed. - command.compile_checker() + command.compile_additional_files([("checker", ["chkchk.cpp"], {"is_checker": True})]) task_id = package_util.get_task_id() solutions = package_util.get_solutions(task_id, None) for solution in solutions: @@ -771,7 +771,8 @@ def test_ghost_checker(create_package): shutil.copytree(paths.get_cache_path(), os.path.join(os.getcwd(), ".cache-copy")) command = Command() - command.check_had_checker(False) + command.set_constants() + command.task_type._check_had_checker(False) for solution in os.listdir(paths.get_cache_path("md5sums")): cache_file: CacheFile = cache.get_cache_file(solution) From fdd01adcafa23e4ab160ed7cce030795995351ed Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Thu, 4 Apr 2024 21:58:30 +0200 Subject: [PATCH 02/15] Refactor running --- src/sinol_make/commands/run/__init__.py | 14 +- src/sinol_make/task_type/base.py | 192 ++++++++++++++++++++++++ 2 files changed, 198 insertions(+), 8 deletions(-) diff --git a/src/sinol_make/commands/run/__init__.py b/src/sinol_make/commands/run/__init__.py index 982303c5..66ec89aa 100644 --- a/src/sinol_make/commands/run/__init__.py +++ b/src/sinol_make/commands/run/__init__.py @@ -639,14 +639,12 @@ def run_solution(self, data_for_execution: ExecutionData): file_no_ext = paths.get_executions_path(name, package_util.extract_test_id(test, self.ID)) output_file = file_no_ext + ".out" result_file = file_no_ext + ".res" - hard_time_limit_in_s = math.ceil(2 * time_limit / 1000.0) + hard_time_limit = math.ceil(2 * time_limit / 1000.0) + + oiejq = self.timetool_name == 'oiejq' + return self.task_type.run(oiejq, timetool_path, executable, result_file, test, output_file, + self.get_output_file(test), time_limit, memory_limit, hard_time_limit, execution_dir) - if self.timetool_name == 'oiejq': - return self.execute_oiejq(name, timetool_path, executable, result_file, test, output_file, self.get_output_file(test), - time_limit, memory_limit, hard_time_limit_in_s, execution_dir) - elif self.timetool_name == 'time': - return self.execute_time(name, executable, result_file, test, output_file, self.get_output_file(test), - time_limit, memory_limit, hard_time_limit_in_s, execution_dir) def run_solutions(self, compiled_commands, names, solutions, executables_dir): """ @@ -1160,7 +1158,7 @@ def check_errors(self, results: Dict[str, Dict[str, Dict[str, ExecutionResult]]] if results[solution][group][test].Error is not None: error_msg += f'Solution {solution} had an error on test {test}: {results[solution][group][test].Error}\n' if error_msg != "": - util.exit_with_error(error_msg) + print(util.error(error_msg)) def compile_checker(self): checker_basename = os.path.basename(self.checker) diff --git a/src/sinol_make/task_type/base.py b/src/sinol_make/task_type/base.py index 45f6740b..4d128405 100644 --- a/src/sinol_make/task_type/base.py +++ b/src/sinol_make/task_type/base.py @@ -1,7 +1,14 @@ +import os +import sys +import time +import signal +import psutil +import subprocess from typing import List, Dict, Any, Tuple from sinol_make import util from sinol_make.interfaces.Errors import CheckerOutputException +from sinol_make.structs.status_structs import ExecutionResult, Status class BaseTaskType: @@ -54,3 +61,188 @@ def check_output(self, input_file, output_file_path, output, answer_file_path) - with open(answer_file_path, "r") as answer_file: correct = util.lines_diff(output, answer_file.readlines()) return correct, 100 if correct else 0 + + def _prepare_oiejq_env(self, env: Dict[str, str], memory_limit) -> Dict[str, str]: + env['MEM_LIMIT'] = f'{memory_limit}K' + env['MEASURE_MEM'] = '1' + env['UNDER_OIEJQ'] = '1' + return env + + def _wrap_with_oiejq(self, command: str, oiejq_path: str) -> str: + return f'"{oiejq_path}" {command}' + + def _wrap_with_time(self, command: List[str], result_file_path: str) -> List[str]: + if sys.platform == 'darwin': + time_name = 'gtime' + elif sys.platform == 'linux': + time_name = '/usr/bin/time' + elif sys.platform == 'win32' or sys.platform == 'cygwin': + raise Exception("Measuring time with GNU time on Windows is not supported.") + else: + raise Exception(f"Unknown platform: {sys.platform}") + + return [time_name, '-f', '%U\\\\n%M\\\\n%x', '-o', f'"{result_file_path}"'] + command + + def _parse_time_output(self, result_file_path: str): + result = ExecutionResult() + program_exit_code = None + with open(result_file_path, "r") as result_file: + lines = result_file.readlines() + if len(lines) == 3: + """ + If programs runs successfully, the output looks like this: + - first line is CPU time in seconds + - second line is memory in KB + - third line is exit code + This format is defined by -f flag in time command. + """ + result.Time = round(float(lines[0].strip()) * 1000) + result.Memory = int(lines[1].strip()) + program_exit_code = int(lines[2].strip()) + elif len(lines) > 0 and "Command terminated by signal " in lines[0]: + """ + If there was a runtime error, the first line is the error message with signal number. + For example: + Command terminated by signal 11 + """ + program_exit_code = int(lines[0].strip().split(" ")[-1]) + else: + result.Status = Status.RE + result.Error = "Unexpected output from time command: " + "".join(lines) + return result, program_exit_code + + def _parse_time(self, time_str): + if len(time_str) < 3: return -1 + return int(time_str[:-2]) + + def _parse_memory(self, memory_str): + if len(memory_str) < 3: return -1 + return int(memory_str[:-2]) + + def _parse_oiejq_output(self, result_file_path: str): + result = ExecutionResult() + with open(result_file_path, "r") as result_file: + for line in result_file: + line = line.strip() + if ": " in line: + (key, value) = line.split(": ")[:2] + if key == "Time": + result.Time = self._parse_time(value) + elif key == "Memory": + result.Memory = self._parse_memory(value) + else: + setattr(result, key, value) + return result + + def _run_subprocess(self, oiejq, executable, memory_limit, hard_time_limit, *args, **kwargs): + process = subprocess.Popen(*args, **kwargs) + + def sigint_handler(signum, frame): + try: + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + except ProcessLookupError: + pass + sys.exit(1) + + signal.signal(signal.SIGINT, sigint_handler) + timeout = False + mem_limit_exceeded = False + + if oiejq: + mem_limit_exceeded = False + try: + process.wait(timeout=hard_time_limit) + except subprocess.TimeoutExpired: + timeout = True + try: + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + except ProcessLookupError: + pass + process.communicate() + else: # time + start_time = time.time() + while process.poll() is None: + try: + time_process = psutil.Process(process.pid) + executable_process = None + for child in time_process.children(): + if child.name() == executable: + executable_process = child + break + if executable_process is not None and executable_process.memory_info().rss > memory_limit * 1024: + try: + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + except ProcessLookupError: + pass + mem_limit_exceeded = True + break + except psutil.NoSuchProcess: + pass + + if time.time() - start_time > hard_time_limit: + try: + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + except ProcessLookupError: + pass + timeout = True + break + + return timeout, mem_limit_exceeded + + def run(self, oiejq: bool, timetool_path, executable, result_file_path, input_file_path, output_file_path, + answer_file_path, time_limit, memory_limit, hard_time_limit, execution_dir) -> ExecutionResult: + env = os.environ.copy() + result = ExecutionResult() + if oiejq: + command = self._wrap_with_oiejq(f'"{executable}"', timetool_path) + env = self._prepare_oiejq_env(env, memory_limit) + with open(input_file_path, "r") as input_file, open(output_file_path, "w") as output_file, \ + open(result_file_path, "w") as result_file: + timeout, mem_limit_exceeded = self._run_subprocess(oiejq, executable, memory_limit, hard_time_limit, + command, shell=True, stdin=input_file, + stdout=output_file, stderr=result_file, env=env, + preexec_fn=os.setsid, cwd=execution_dir) + result = self._parse_oiejq_output(result_file_path) + else: + command = self._wrap_with_time([f'"{executable}"'], result_file_path) + with open(input_file_path, "r") as input_file, open(output_file_path, "w") as output_file: + timeout, mem_limit_exceeded = self._run_subprocess(oiejq, executable, memory_limit, hard_time_limit, + ' '.join(command), shell=True, stdin=input_file, stdout=output_file, + stderr=subprocess.DEVNULL, preexec_fn=os.setsid, + cwd=execution_dir) + if not timeout: + result, program_exit_code = self._parse_time_output(result_file_path) + if program_exit_code is not None and program_exit_code != 0: + result.Status = Status.RE + result.Error = f"Program exited with code {program_exit_code}." + return result + + with open(output_file_path, "r") as output_file: + output = output_file.readlines() + + def getattrd(obj, attr, default): + if getattr(obj, attr, None) is None: + return default + return getattr(obj, attr) + + if timeout: + result.Status = Status.TL + elif mem_limit_exceeded: + result.Status = Status.ML + result.Memory = memory_limit + 1 # Add one so that the memory is red in the table + elif getattrd(result, 'Time', 0) > time_limit: + result.Status = Status.TL + elif getattrd(result, 'Memory', 0) > memory_limit: + result.Status = Status.ML + else: + try: + correct, result.Points = self.check_output(input_file_path, output_file_path, output, answer_file_path) + if correct: + result.Status = Status.OK + else: + result.Status = Status.WA + except CheckerOutputException as e: + result.Status = Status.CE + result.Error = str(e) + + return result From 7842a37f728646124aecdf5813bb58956b6f8695 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Fri, 5 Apr 2024 17:01:46 +0200 Subject: [PATCH 03/15] Bump version --- src/sinol_make/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sinol_make/__init__.py b/src/sinol_make/__init__.py index f7aa5f70..886f5c6b 100644 --- a/src/sinol_make/__init__.py +++ b/src/sinol_make/__init__.py @@ -6,7 +6,7 @@ from sinol_make import util, oiejq -__version__ = "1.5.29" +__version__ = "1.7.0.dev1" def configure_parsers(): From 65aee453fc20ea74ce81ae9e1c5cef8260b95f2e Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Fri, 5 Apr 2024 19:38:30 +0200 Subject: [PATCH 04/15] Interactive with time --- src/sinol_make/commands/run/__init__.py | 11 ++- src/sinol_make/structs/run_structs.py | 2 + src/sinol_make/task_type/__init__.py | 3 +- src/sinol_make/task_type/base.py | 72 +++++++++----- src/sinol_make/task_type/interactive_io.py | 108 +++++++++++++++++++++ src/sinol_make/task_type/normal.py | 26 +++++ 6 files changed, 193 insertions(+), 29 deletions(-) create mode 100644 src/sinol_make/task_type/interactive_io.py diff --git a/src/sinol_make/commands/run/__init__.py b/src/sinol_make/commands/run/__init__.py index 66ec89aa..3b6af7b1 100644 --- a/src/sinol_make/commands/run/__init__.py +++ b/src/sinol_make/commands/run/__init__.py @@ -635,15 +635,15 @@ def run_solution(self, data_for_execution: ExecutionData): Run an execution and return the result as ExecutionResult object. """ - (name, executable, test, time_limit, memory_limit, timetool_path, execution_dir) = data_for_execution + (name, executable, test, output_test, time_limit, memory_limit, timetool_path, execution_dir) = data_for_execution file_no_ext = paths.get_executions_path(name, package_util.extract_test_id(test, self.ID)) output_file = file_no_ext + ".out" result_file = file_no_ext + ".res" hard_time_limit = math.ceil(2 * time_limit / 1000.0) oiejq = self.timetool_name == 'oiejq' - return self.task_type.run(oiejq, timetool_path, executable, result_file, test, output_file, - self.get_output_file(test), time_limit, memory_limit, hard_time_limit, execution_dir) + return self.task_type.run(oiejq, timetool_path, executable, result_file, os.path.join(os.getcwd(), test), output_file, + output_test, time_limit, memory_limit, hard_time_limit, execution_dir) def run_solutions(self, compiled_commands, names, solutions, executables_dir): @@ -679,8 +679,9 @@ def run_solutions(self, compiled_commands, names, solutions, executables_dir): test_result.time_tool == self.timetool_name: all_results[name][self.get_group(test)][test] = test_result.result else: - executions.append((name, executable, test, test_time_limit, test_memory_limit, - self.timetool_path, os.path.dirname(executable))) + executions.append((name, executable, test, + os.path.join(os.getcwd(), self.get_output_file(test)), test_time_limit, + test_memory_limit, self.timetool_path, os.path.dirname(executable))) all_results[name][self.get_group(test)][test] = ExecutionResult(Status.PENDING) os.makedirs(paths.get_executions_path(name), exist_ok=True) else: diff --git a/src/sinol_make/structs/run_structs.py b/src/sinol_make/structs/run_structs.py index 2488b1ee..3f981057 100644 --- a/src/sinol_make/structs/run_structs.py +++ b/src/sinol_make/structs/run_structs.py @@ -12,6 +12,8 @@ class ExecutionData: executable: str # Filename of the test test: str + #Filename of the output file + output_test: str # Time limit for this test in milliseconds time_limit: int # Memory limit in KB diff --git a/src/sinol_make/task_type/__init__.py b/src/sinol_make/task_type/__init__.py index 89726bca..aa525f67 100644 --- a/src/sinol_make/task_type/__init__.py +++ b/src/sinol_make/task_type/__init__.py @@ -2,6 +2,7 @@ from sinol_make.helpers import package_util from sinol_make.task_type.base import BaseTaskType +from sinol_make.task_type.interactive_io import InteractiveIOTask from sinol_make.task_type.normal import NormalTask @@ -12,5 +13,5 @@ def get_task_type() -> BaseTaskType: raise NotImplementedError("Encdec is not supported by sinol-make.") task_id = package_util.get_task_id() if package_util.any_files_matching_pattern(task_id, f"{task_id}soc.*"): - pass + return InteractiveIOTask(task_id) return NormalTask(task_id) diff --git a/src/sinol_make/task_type/base.py b/src/sinol_make/task_type/base.py index 4d128405..edd029f6 100644 --- a/src/sinol_make/task_type/base.py +++ b/src/sinol_make/task_type/base.py @@ -4,7 +4,7 @@ import signal import psutil import subprocess -from typing import List, Dict, Any, Tuple +from typing import List, Dict, Any, Tuple, Union from sinol_make import util from sinol_make.interfaces.Errors import CheckerOutputException @@ -134,17 +134,30 @@ def _parse_oiejq_output(self, result_file_path: str): setattr(result, key, value) return result - def _run_subprocess(self, oiejq, executable, memory_limit, hard_time_limit, *args, **kwargs): + def _run_subprocess(self, oiejq: bool, sigint_handler, executable, memory_limit, hard_time_limit, *args, **kwargs): + # print("oiejq", oiejq) + # print("executable", executable) + # print("memory_limit", memory_limit) + # print("hard_time_limit", hard_time_limit) + # print(args, kwargs) + # stdin_stat = os.fstat(kwargs['stdin']) + # print("stdin", kwargs['stdin'], "stdin_stat", stdin_stat) + # stdout_stat = os.fstat(kwargs['stdout']) + # print("stdout", kwargs['stdout'], "stdout_stat", stdout_stat) process = subprocess.Popen(*args, **kwargs) + if 'pass_fds' in kwargs: + for fd in kwargs['pass_fds']: + os.close(fd) - def sigint_handler(signum, frame): - try: - os.killpg(os.getpgid(process.pid), signal.SIGTERM) - except ProcessLookupError: - pass - sys.exit(1) + if sigint_handler: + def sigint_handler(signum, frame): + try: + os.killpg(os.getpgid(process.pid), signal.SIGTERM) + except ProcessLookupError: + pass + sys.exit(1) - signal.signal(signal.SIGINT, sigint_handler) + signal.signal(signal.SIGINT, sigint_handler) timeout = False mem_limit_exceeded = False @@ -189,34 +202,47 @@ def sigint_handler(signum, frame): return timeout, mem_limit_exceeded + def _run_program_oiejq(self, command, env, executable, result_file_path, input_file_path, output_file_path, + answer_file_path, time_limit, memory_limit, hard_time_limit, execution_dir): + raise NotImplementedError() + + def _run_program_time(self, command, env, executable, result_file_path, input_file_path, output_file_path, + answer_file_path, time_limit, memory_limit, hard_time_limit, execution_dir): + raise NotImplementedError() + + def _update_result_RE(self, result, program_exit_code): + pass + + def _parse_additional_time(self, result_file_path) -> Union[ExecutionResult, None]: + return None + def run(self, oiejq: bool, timetool_path, executable, result_file_path, input_file_path, output_file_path, answer_file_path, time_limit, memory_limit, hard_time_limit, execution_dir) -> ExecutionResult: env = os.environ.copy() result = ExecutionResult() if oiejq: - command = self._wrap_with_oiejq(f'"{executable}"', timetool_path) - env = self._prepare_oiejq_env(env, memory_limit) - with open(input_file_path, "r") as input_file, open(output_file_path, "w") as output_file, \ - open(result_file_path, "w") as result_file: - timeout, mem_limit_exceeded = self._run_subprocess(oiejq, executable, memory_limit, hard_time_limit, - command, shell=True, stdin=input_file, - stdout=output_file, stderr=result_file, env=env, - preexec_fn=os.setsid, cwd=execution_dir) + timeout, mem_limit_exceeded = self._run_program_oiejq(timetool_path, env, executable, result_file_path, + input_file_path, output_file_path, answer_file_path, + time_limit, memory_limit, hard_time_limit, + execution_dir) result = self._parse_oiejq_output(result_file_path) else: - command = self._wrap_with_time([f'"{executable}"'], result_file_path) - with open(input_file_path, "r") as input_file, open(output_file_path, "w") as output_file: - timeout, mem_limit_exceeded = self._run_subprocess(oiejq, executable, memory_limit, hard_time_limit, - ' '.join(command), shell=True, stdin=input_file, stdout=output_file, - stderr=subprocess.DEVNULL, preexec_fn=os.setsid, - cwd=execution_dir) + timeout, mem_limit_exceeded = self._run_program_time(timetool_path, env, executable, result_file_path, + input_file_path, output_file_path, answer_file_path, + time_limit, memory_limit, hard_time_limit, + execution_dir) if not timeout: result, program_exit_code = self._parse_time_output(result_file_path) if program_exit_code is not None and program_exit_code != 0: result.Status = Status.RE result.Error = f"Program exited with code {program_exit_code}." + self._update_result_RE(result, program_exit_code) return result + additional_result = self._parse_additional_time(result_file_path) + if additional_result is not None: + return additional_result + with open(output_file_path, "r") as output_file: output = output_file.readlines() diff --git a/src/sinol_make/task_type/interactive_io.py b/src/sinol_make/task_type/interactive_io.py new file mode 100644 index 00000000..976e8f95 --- /dev/null +++ b/src/sinol_make/task_type/interactive_io.py @@ -0,0 +1,108 @@ +import os +import signal +from threading import Thread +from typing import Tuple, Union + +from sinol_make.helpers import package_util, paths +from sinol_make.structs.status_structs import ExecutionResult, Status +from sinol_make.task_type import BaseTaskType + + +class InteractiveIOTask(BaseTaskType): + def __init__(self, task_id): + super().__init__(task_id) + self.interactor = None + self.interactor_exe = None + + def get_files_to_compile(self): + super().get_files_to_compile() + interactors = package_util.get_files_matching_pattern(self.task_id, f'{self.task_id}soc.*') + if interactors: + self.interactor = interactors[0] + self.interactor_exe = paths.get_executables_path(package_util.get_executable(self.interactor)) + return [("interactor", [self.interactor], {})] + + def _get_interactor_result_file(self, sol_result_file_path: str): + dirname = os.path.dirname(sol_result_file_path) + basename_no_ext = os.path.basename(sol_result_file_path).split(".")[0] + ext = os.path.basename(sol_result_file_path).split(".")[1] + return os.path.join(dirname, f"{basename_no_ext}_interactor.{ext}") + + def check_output(self, input_file, output_file_path, output, answer_file_path) -> Tuple[bool, int]: + with open(output_file_path, "r") as output_file: + output = output_file.read().splitlines() + return self._parse_checker_output(output) + + def _update_result_RE(self, result, program_exit_code): + if program_exit_code == signal.Signals.SIGPIPE: + result.Error = "Interactor exited prematurely" + + def _parse_additional_time(self, result_file_path) -> Union[ExecutionResult, None]: + result, program_exit_code = self._parse_time_output( + self._get_interactor_result_file(result_file_path) + ) + if program_exit_code is not None and program_exit_code != 0: + result.Status = Status.RE + if program_exit_code == signal.Signals.SIGPIPE: + result.Error = "Solution exited prematurely" + else: + result.Error = f"Program exited with code {program_exit_code}." + return result + return None + + def _run_program_time(self, command, env, executable, result_file_path, input_file_path, output_file_path, + answer_file_path, time_limit, memory_limit, hard_time_limit, execution_dir): + r1, w1 = os.pipe() + r2, w2 = os.pipe() + for fd in (r1, w1, r2, w2): + os.set_inheritable(fd, True) + + command_sol = self._wrap_with_time( + [f'"{executable}"'], + result_file_path + ) + command_interactor = self._wrap_with_time( + [f'"{self.interactor_exe}"', f'"{input_file_path}"', f'"{answer_file_path}"'], + self._get_interactor_result_file(result_file_path) + ) + + def thread_wrapper(result, *args, **kwargs): + result.append(self._run_subprocess(*args, **kwargs)) + + with open(output_file_path, "w") as output_file: + sol_result = [] + solution = Thread( + target=thread_wrapper, + args=(sol_result, False, False, executable, memory_limit, hard_time_limit, ' '.join(command_sol),), + kwargs={ + "shell": True, + "stdin": r1, + "stdout": w2, + "preexec_fn": os.setsid, + "cwd": execution_dir, + "pass_fds": (r1, w2,) + } + ) + interactor_result = [] + interactor = Thread( + target=thread_wrapper, + args=(interactor_result, False, False, self.interactor_exe, memory_limit, hard_time_limit, + ' '.join(command_interactor),), + kwargs={ + "shell": True, + "stdin": r2, + "stdout": w1, + "stderr": output_file, + "preexec_fn": os.setsid, + "cwd": execution_dir, + "pass_fds": (r2, w1,) + } + ) + solution.start() + interactor.start() + # for fd in (r1, w1, r2, w2): + # os.close(fd) + solution.join() + interactor.join() + + return sol_result[0][0], sol_result[0][1] diff --git a/src/sinol_make/task_type/normal.py b/src/sinol_make/task_type/normal.py index 79f9cbae..324ba51d 100644 --- a/src/sinol_make/task_type/normal.py +++ b/src/sinol_make/task_type/normal.py @@ -44,8 +44,34 @@ def get_files_to_compile(self): self._check_had_checker(False) return [] + def _run_checker(self, input_file, output_file_path, answer_file_path) -> List[str]: command = [self.checker_exe, input_file, output_file_path, answer_file_path] process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) process.wait() return process.communicate()[0].decode("utf-8").splitlines() + + def _run_program_oiejq(self, timetool_path, env, executable, result_file_path, input_file_path, output_file_path, + answer_file_path, time_limit, memory_limit, hard_time_limit, execution_dir): + + command = self._wrap_with_oiejq(f'"{executable}"', timetool_path) + env = self._prepare_oiejq_env(env, memory_limit) + with open(input_file_path, "r") as input_file, open(output_file_path, "w") as output_file, \ + open(result_file_path, "w") as result_file: + timeout, mem_limit_exceeded = self._run_subprocess(True, True, executable, memory_limit, hard_time_limit, + command, shell=True, stdin=input_file, + stdout=output_file, stderr=result_file, env=env, + preexec_fn=os.setsid, cwd=execution_dir) + return timeout, mem_limit_exceeded + + def _run_program_time(self, timetool_path, env, executable, result_file_path, input_file_path, output_file_path, + answer_file_path, time_limit, memory_limit, hard_time_limit, execution_dir): + + command = self._wrap_with_time([f'"{executable}"'], result_file_path) + with open(input_file_path, "r") as input_file, open(output_file_path, "w") as output_file: + timeout, mem_limit_exceeded = self._run_subprocess(False, True, executable, memory_limit, hard_time_limit, + ' '.join(command), shell=True, stdin=input_file, + stdout=output_file, + stderr=subprocess.DEVNULL, preexec_fn=os.setsid, + cwd=execution_dir) + return timeout, mem_limit_exceeded From aeb7ca8fd6aa05b9dfe0460ae88708a7e5c1896c Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Fri, 5 Apr 2024 19:38:46 +0200 Subject: [PATCH 05/15] package with interactive task via io --- tests/packages/interactive_io/config.yml | 20 +++++++++++++++ tests/packages/interactive_io/in/.gitkeep | 0 tests/packages/interactive_io/out/.gitkeep | 0 tests/packages/interactive_io/prog/iio.cpp | 9 +++++++ tests/packages/interactive_io/prog/iio2.cpp | 7 ++++++ tests/packages/interactive_io/prog/iio3.cpp | 9 +++++++ .../packages/interactive_io/prog/iioingen.cpp | 12 +++++++++ tests/packages/interactive_io/prog/iiosoc.cpp | 25 +++++++++++++++++++ 8 files changed, 82 insertions(+) create mode 100644 tests/packages/interactive_io/config.yml create mode 100644 tests/packages/interactive_io/in/.gitkeep create mode 100644 tests/packages/interactive_io/out/.gitkeep create mode 100644 tests/packages/interactive_io/prog/iio.cpp create mode 100644 tests/packages/interactive_io/prog/iio2.cpp create mode 100644 tests/packages/interactive_io/prog/iio3.cpp create mode 100644 tests/packages/interactive_io/prog/iioingen.cpp create mode 100644 tests/packages/interactive_io/prog/iiosoc.cpp diff --git a/tests/packages/interactive_io/config.yml b/tests/packages/interactive_io/config.yml new file mode 100644 index 00000000..dc6de905 --- /dev/null +++ b/tests/packages/interactive_io/config.yml @@ -0,0 +1,20 @@ +title: Interactive task via IO +sinol_task_id: iio +memory_limit: 10240 +time_limit: 5000 +sinol_expected_scores: + iio.cpp: + expected: + 0: {points: 0, status: OK} + 1: {points: 100, status: OK} + points: 100 + iio2.cpp: + expected: + 0: {points: 0, status: WA} + 1: {points: 0, status: RE} + points: 0 + iio3.cpp: + expected: + 0: {points: 0, status: OK} + 1: {points: 50, status: OK} + points: 50 diff --git a/tests/packages/interactive_io/in/.gitkeep b/tests/packages/interactive_io/in/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/interactive_io/out/.gitkeep b/tests/packages/interactive_io/out/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/packages/interactive_io/prog/iio.cpp b/tests/packages/interactive_io/prog/iio.cpp new file mode 100644 index 00000000..177f9501 --- /dev/null +++ b/tests/packages/interactive_io/prog/iio.cpp @@ -0,0 +1,9 @@ +#include + +using namespace std; + +int main() { + int a; + cin >> a; + cout << a + 42 << "\n" << flush; +} diff --git a/tests/packages/interactive_io/prog/iio2.cpp b/tests/packages/interactive_io/prog/iio2.cpp new file mode 100644 index 00000000..ae779674 --- /dev/null +++ b/tests/packages/interactive_io/prog/iio2.cpp @@ -0,0 +1,7 @@ +#include + +using namespace std; + +int main() { + return 0; +} diff --git a/tests/packages/interactive_io/prog/iio3.cpp b/tests/packages/interactive_io/prog/iio3.cpp new file mode 100644 index 00000000..f335f14c --- /dev/null +++ b/tests/packages/interactive_io/prog/iio3.cpp @@ -0,0 +1,9 @@ +#include + +using namespace std; + +int main() { + int a; + cin >> a; + cout << a + 10 << "\n" << flush; +} diff --git a/tests/packages/interactive_io/prog/iioingen.cpp b/tests/packages/interactive_io/prog/iioingen.cpp new file mode 100644 index 00000000..24214ea3 --- /dev/null +++ b/tests/packages/interactive_io/prog/iioingen.cpp @@ -0,0 +1,12 @@ +#include + +using namespace std; + +int main() { + ofstream f("iio0.in"); + f << "10\n"; + f.close(); + f.open("iio1.in"); + f << "42\n"; + f.close(); +} diff --git a/tests/packages/interactive_io/prog/iiosoc.cpp b/tests/packages/interactive_io/prog/iiosoc.cpp new file mode 100644 index 00000000..d93e2cc2 --- /dev/null +++ b/tests/packages/interactive_io/prog/iiosoc.cpp @@ -0,0 +1,25 @@ +#include + +using namespace std; + +int main(int argc, char const *argv[]) { + if (argc != 3) { + cerr << "Usage: ./a.out " << endl; + return 1; + } + ifstream ifs(argv[1]); + int a; + ifs >> a; + cout << a << "\n" << flush; + int ans; + cin >> ans; + if (ans == a + 42) { + cerr << "OK\n"; + } + else if (ans == a + 10) { + cerr << "OK\nwrong diff\n50"; + } + else { + cerr << "WRONG\n"; + } +} From 3c1c8d608c7472c121d2580a0a206245315b6c18 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Mon, 6 May 2024 20:59:44 +0200 Subject: [PATCH 06/15] Close fds --- src/sinol_make/task_type/interactive_io.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sinol_make/task_type/interactive_io.py b/src/sinol_make/task_type/interactive_io.py index 976e8f95..28b3dc81 100644 --- a/src/sinol_make/task_type/interactive_io.py +++ b/src/sinol_make/task_type/interactive_io.py @@ -100,8 +100,8 @@ def thread_wrapper(result, *args, **kwargs): ) solution.start() interactor.start() - # for fd in (r1, w1, r2, w2): - # os.close(fd) + for fd in (r1, w1, r2, w2): + os.close(fd) solution.join() interactor.join() From cd596680221dec5112d3c103fa715c67f5cc6442 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Mon, 6 May 2024 21:49:27 +0200 Subject: [PATCH 07/15] Initial support for sio2jail --- setup.cfg | 1 + src/sinol_make/commands/run/__init__.py | 2 +- src/sinol_make/task_type/base.py | 7 +- src/sinol_make/task_type/interactive_io.py | 156 ++++++++++++++++----- 4 files changed, 128 insertions(+), 38 deletions(-) diff --git a/setup.cfg b/setup.cfg index 542bc314..85505e9f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,6 +44,7 @@ tests = [options.entry_points] console_scripts = sinol-make = sinol_make:main + sm = sinol_make:main [tool:pytest] testpaths = diff --git a/src/sinol_make/commands/run/__init__.py b/src/sinol_make/commands/run/__init__.py index e78ac24e..e585431c 100644 --- a/src/sinol_make/commands/run/__init__.py +++ b/src/sinol_make/commands/run/__init__.py @@ -1153,7 +1153,7 @@ def check_are_any_tests_to_run(self): if len(example_tests) == len(self.tests): print(util.warning('Running only on example tests.')) - if not self.has_lib: + if self.task_type.require_outputs() and not self.has_lib: self.validate_existence_of_outputs() else: util.exit_with_error('There are no tests to run.') diff --git a/src/sinol_make/task_type/base.py b/src/sinol_make/task_type/base.py index edd029f6..4a1c7f3d 100644 --- a/src/sinol_make/task_type/base.py +++ b/src/sinol_make/task_type/base.py @@ -251,6 +251,8 @@ def getattrd(obj, attr, default): return default return getattr(obj, attr) + if result.Status == Status.RE: + return result if timeout: result.Status = Status.TL elif mem_limit_exceeded: @@ -268,7 +270,10 @@ def getattrd(obj, attr, default): else: result.Status = Status.WA except CheckerOutputException as e: - result.Status = Status.CE + result.Status = Status.RE result.Error = str(e) return result + + def require_outputs(self): + return True diff --git a/src/sinol_make/task_type/interactive_io.py b/src/sinol_make/task_type/interactive_io.py index 28b3dc81..7201dfbe 100644 --- a/src/sinol_make/task_type/interactive_io.py +++ b/src/sinol_make/task_type/interactive_io.py @@ -3,6 +3,7 @@ from threading import Thread from typing import Tuple, Union +from sinol_make import oiejq from sinol_make.helpers import package_util, paths from sinol_make.structs.status_structs import ExecutionResult, Status from sinol_make.task_type import BaseTaskType @@ -62,47 +63,130 @@ def _run_program_time(self, command, env, executable, result_file_path, input_fi result_file_path ) command_interactor = self._wrap_with_time( - [f'"{self.interactor_exe}"', f'"{input_file_path}"', f'"{answer_file_path}"'], + [f'"{self.interactor_exe}"', f'"{input_file_path}"', f'"{output_file_path}"'], self._get_interactor_result_file(result_file_path) ) def thread_wrapper(result, *args, **kwargs): result.append(self._run_subprocess(*args, **kwargs)) - with open(output_file_path, "w") as output_file: - sol_result = [] - solution = Thread( - target=thread_wrapper, - args=(sol_result, False, False, executable, memory_limit, hard_time_limit, ' '.join(command_sol),), - kwargs={ - "shell": True, - "stdin": r1, - "stdout": w2, - "preexec_fn": os.setsid, - "cwd": execution_dir, - "pass_fds": (r1, w2,) - } - ) - interactor_result = [] - interactor = Thread( - target=thread_wrapper, - args=(interactor_result, False, False, self.interactor_exe, memory_limit, hard_time_limit, - ' '.join(command_interactor),), - kwargs={ - "shell": True, - "stdin": r2, - "stdout": w1, - "stderr": output_file, - "preexec_fn": os.setsid, - "cwd": execution_dir, - "pass_fds": (r2, w1,) - } - ) - solution.start() - interactor.start() - for fd in (r1, w1, r2, w2): - os.close(fd) - solution.join() - interactor.join() + sol_result = [] + solution = Thread( + target=thread_wrapper, + args=(sol_result, False, False, executable, memory_limit, hard_time_limit, ' '.join(command_sol),), + kwargs={ + "shell": True, + "stdin": r1, + "stdout": w2, + "preexec_fn": os.setsid, + "cwd": execution_dir, + "pass_fds": (r1, w2,), + } + ) + interactor_result = [] + interactor = Thread( + target=thread_wrapper, + args=(interactor_result, False, False, self.interactor_exe, memory_limit, hard_time_limit, + ' '.join(command_interactor),), + kwargs={ + "shell": True, + "stdin": r2, + "stdout": w1, + "preexec_fn": os.setsid, + "cwd": execution_dir, + "pass_fds": (r2, w1,) + } + ) + solution.start() + interactor.start() + solution.join() + interactor.join() return sol_result[0][0], sol_result[0][1] + + def _wrap_with_sio2jail(self, command, result_file_path, sio2jail_path, mem_limit): + return [f'"{sio2jail_path}"', "--mount-namespace", "off", "--pid-namespace", "off", "--uts-namespace", "off", + "--ipc-namespace", "off", "--net-namespace", "off", "--capability-drop", "off", + "--user-namespace", "off", "-s", "-m", str(mem_limit), "-f", "3", "-o", "oiaug", "--"] + \ + command + [f'3>"{result_file_path}"'] + + def _run_program_oiejq(self, command, env, executable, result_file_path, input_file_path, output_file_path, answer_file_path, time_limit, memory_limit, hard_time_limit, execution_dir): + r1, w1 = os.pipe() + r2, w2 = os.pipe() + for fd in (r1, w1, r2, w2): + os.set_inheritable(fd, True) + + oiejq_path = oiejq.get_oiejq_path() + sio2jail_path = os.path.join(os.path.dirname(oiejq_path), "sio2jail") + command_sol = self._wrap_with_sio2jail( + [f'"{executable}"'], + result_file_path, + sio2jail_path, + memory_limit + ) + + command_interactor = self._wrap_with_sio2jail( + [f'"{self.interactor_exe}"', f'"{input_file_path}"', f'"{output_file_path}"'], + self._get_interactor_result_file(result_file_path), + sio2jail_path, + memory_limit + ) + + def thread_wrapper(result, *args, **kwargs): + result.append(self._run_subprocess(*args, **kwargs)) + + sol_result = [] + solution = Thread( + target=thread_wrapper, + args=(sol_result, True, False, executable, memory_limit, hard_time_limit, ' '.join(command_sol),), + kwargs={ + "shell": True, + "stdin": r1, + "stdout": w2, + "preexec_fn": os.setsid, + "cwd": execution_dir, + "pass_fds": (r1, w2,) + } + ) + interactor_result = [] + interactor = Thread( + target=thread_wrapper, + args=(interactor_result, True, False, self.interactor_exe, memory_limit, hard_time_limit, + ' '.join(command_interactor),), + kwargs={ + "shell": True, + "stdin": r2, + "stdout": w1, + "preexec_fn": os.setsid, + "cwd": execution_dir, + "pass_fds": (r2, w1,) + } + ) + solution.start() + interactor.start() + solution.join() + interactor.join() + + print(open(self._get_interactor_result_file(result_file_path), "r").read()) + return sol_result[0][0], sol_result[0][1] + + def _parse_oiejq_output(self, result_file_path: str): + result = ExecutionResult() + with open(result_file_path, "r") as result_file: + try: + line = result_file.readline() + status, code, time, _, mem, _ = line.split() + except ValueError: + result.Status = Status.RE + result.Error = "Invalid output format: " + line + return result + + result.Time = round(float(time * 1000)) + result.Memory = int(mem) + if int(code) != 0: + result.Status = Status.RE + result.Error = f"Program exited with code {code}." + return result + + def require_outputs(self): + return False From 4367cff67e2060ff6b58531eb2549ff467404fc4 Mon Sep 17 00:00:00 2001 From: MasloMaslane Date: Wed, 29 May 2024 11:10:59 +0200 Subject: [PATCH 08/15] Better parsing --- src/sinol_make/helpers/package_util.py | 2 +- src/sinol_make/task_type/interactive_io.py | 41 +++++++++++++++------- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/sinol_make/helpers/package_util.py b/src/sinol_make/helpers/package_util.py index e2e453b9..69459139 100644 --- a/src/sinol_make/helpers/package_util.py +++ b/src/sinol_make/helpers/package_util.py @@ -55,7 +55,7 @@ def get_test_key(test, task_id): def get_config(): try: with open(os.path.join(os.getcwd(), "config.yml"), "r") as config_file: - return yaml.load(config_file, Loader=yaml.FullLoader) + return yaml.load(config_file, Loader=yaml.FullLoader) or {} except FileNotFoundError: # Potentially redundant with util:exit_if_not_package util.exit_with_error("You are not in a package directory (couldn't find config.yml in current directory).") diff --git a/src/sinol_make/task_type/interactive_io.py b/src/sinol_make/task_type/interactive_io.py index 7201dfbe..b660c38f 100644 --- a/src/sinol_make/task_type/interactive_io.py +++ b/src/sinol_make/task_type/interactive_io.py @@ -38,18 +38,28 @@ def _update_result_RE(self, result, program_exit_code): if program_exit_code == signal.Signals.SIGPIPE: result.Error = "Interactor exited prematurely" + def _check_errors(self, result, interactor_exit_code, program_exit_code): + if interactor_exit_code != 0 and interactor_exit_code != signal.Signals.SIGPIPE: + result.Status = Status.RE + result.Error = f"Interactor exited with code {interactor_exit_code}." + return result + elif program_exit_code != 0 and program_exit_code != signal.Signals.SIGPIPE: + result.Status = Status.RE + result.Error = f"Solution exited with code {program_exit_code}." + return result + elif interactor_exit_code == signal.Signals.SIGPIPE: + result.Status = Status.RE + result.Error = "Interactor exited prematurely" + return result + else: + return None + def _parse_additional_time(self, result_file_path) -> Union[ExecutionResult, None]: - result, program_exit_code = self._parse_time_output( + result, program_exit_code = self._parse_time_output(result_file_path) + _, interactor_exit_code = self._parse_time_output( self._get_interactor_result_file(result_file_path) ) - if program_exit_code is not None and program_exit_code != 0: - result.Status = Status.RE - if program_exit_code == signal.Signals.SIGPIPE: - result.Error = "Solution exited prematurely" - else: - result.Error = f"Program exited with code {program_exit_code}." - return result - return None + return self._check_errors(result, interactor_exit_code, program_exit_code) def _run_program_time(self, command, env, executable, result_file_path, input_file_path, output_file_path, answer_file_path, time_limit, memory_limit, hard_time_limit, execution_dir): @@ -181,11 +191,18 @@ def _parse_oiejq_output(self, result_file_path: str): result.Error = "Invalid output format: " + line return result + with open(self._get_interactor_result_file(result_file_path), "r") as result_file: + try: + line = result_file.readline() + status_interactor, code_interactor, time_interactor, _, mem_interactor, _ = line.split() + except ValueError: + result.Status = Status.RE + result.Error = "Invalid interactor output format: " + line + return result + + result = self._check_errors(result, int(code_interactor), int(code)) result.Time = round(float(time * 1000)) result.Memory = int(mem) - if int(code) != 0: - result.Status = Status.RE - result.Error = f"Program exited with code {code}." return result def require_outputs(self): From e8f2dd67daf21354aa81bc95ddb7b4807cf42992 Mon Sep 17 00:00:00 2001 From: MasloMaslane Date: Wed, 29 May 2024 11:29:56 +0200 Subject: [PATCH 09/15] sio2jail fixes --- src/sinol_make/task_type/base.py | 13 +++++++++---- src/sinol_make/task_type/interactive_io.py | 7 +++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/sinol_make/task_type/base.py b/src/sinol_make/task_type/base.py index 4a1c7f3d..0794a97f 100644 --- a/src/sinol_make/task_type/base.py +++ b/src/sinol_make/task_type/base.py @@ -85,7 +85,7 @@ def _wrap_with_time(self, command: List[str], result_file_path: str) -> List[str def _parse_time_output(self, result_file_path: str): result = ExecutionResult() - program_exit_code = None + program_exit_code = 0 with open(result_file_path, "r") as result_file: lines = result_file.readlines() if len(lines) == 3: @@ -99,11 +99,13 @@ def _parse_time_output(self, result_file_path: str): result.Time = round(float(lines[0].strip()) * 1000) result.Memory = int(lines[1].strip()) program_exit_code = int(lines[2].strip()) - elif len(lines) > 0 and "Command terminated by signal " in lines[0]: + elif len(lines) > 0 and ("Command terminated by signal " in lines[0] or "Command exited with non-zero status" in lines[0]): """ If there was a runtime error, the first line is the error message with signal number. For example: Command terminated by signal 11 + or + Command exited with non-zero status 1 """ program_exit_code = int(lines[0].strip().split(" ")[-1]) else: @@ -243,8 +245,11 @@ def run(self, oiejq: bool, timetool_path, executable, result_file_path, input_fi if additional_result is not None: return additional_result - with open(output_file_path, "r") as output_file: - output = output_file.readlines() + try: + with open(output_file_path, "r") as output_file: + output = output_file.readlines() + except FileNotFoundError: + output = [] def getattrd(obj, attr, default): if getattr(obj, attr, None) is None: diff --git a/src/sinol_make/task_type/interactive_io.py b/src/sinol_make/task_type/interactive_io.py index b660c38f..2d6a97f9 100644 --- a/src/sinol_make/task_type/interactive_io.py +++ b/src/sinol_make/task_type/interactive_io.py @@ -200,6 +200,13 @@ def _parse_oiejq_output(self, result_file_path: str): result.Error = "Invalid interactor output format: " + line return result + code = int(code) + code_interactor = int(code_interactor) + if code > 128: + code -= 128 + if code_interactor > 128: + code_interactor -= 128 + result = self._check_errors(result, int(code_interactor), int(code)) result.Time = round(float(time * 1000)) result.Memory = int(mem) From fbb264ea331f1870ac7d34d97546526b76484531 Mon Sep 17 00:00:00 2001 From: MasloMaslane Date: Wed, 29 May 2024 12:17:04 +0200 Subject: [PATCH 10/15] Fix tests --- tests/commands/run/test_unit.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/commands/run/test_unit.py b/tests/commands/run/test_unit.py index 262194a1..5e66f8db 100644 --- a/tests/commands/run/test_unit.py +++ b/tests/commands/run/test_unit.py @@ -34,12 +34,13 @@ def test_execution(create_package, time_tool): create_ins_outs(package_path) test = package_util.get_tests("abc", None)[0] + output_test = command.get_output_file(test) with open(os.path.join(package_path, "config.yml"), "r") as config_file: config = yaml.load(config_file, Loader=yaml.FullLoader) os.makedirs(paths.get_executions_path(solution), exist_ok=True) - result = command.run_solution((solution, paths.get_executables_path(executable), test, config['time_limit'], + result = command.run_solution((solution, paths.get_executables_path(executable), test, output_test, config['time_limit'], config['memory_limit'], oiejq.get_oiejq_path(), paths.get_executions_path())) assert result.Status == Status.OK From cf5482dd986a9feda1d9f0290f3fc4ef45d8f385 Mon Sep 17 00:00:00 2001 From: MasloMaslane Date: Wed, 29 May 2024 12:24:56 +0200 Subject: [PATCH 11/15] No outgen in interactive --- src/sinol_make/commands/gen/__init__.py | 4 +++- src/sinol_make/commands/outgen/__init__.py | 4 ++++ src/sinol_make/task_type/base.py | 3 +++ src/sinol_make/task_type/interactive_io.py | 3 +++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/sinol_make/commands/gen/__init__.py b/src/sinol_make/commands/gen/__init__.py index 18719100..7dbd1850 100644 --- a/src/sinol_make/commands/gen/__init__.py +++ b/src/sinol_make/commands/gen/__init__.py @@ -5,6 +5,7 @@ from sinol_make.commands.outgen import Command as OutgenCommand from sinol_make.helpers import parsers from sinol_make.interfaces.BaseCommand import BaseCommand +from sinol_make.task_type import get_task_type class Command(BaseCommand): @@ -40,6 +41,7 @@ def configure_subparser(self, subparser): def run(self, args: argparse.Namespace): args = util.init_package_command(args) + task_type = get_task_type() self.args = args self.ins = args.only_inputs @@ -53,6 +55,6 @@ def run(self, args: argparse.Namespace): command = IngenCommand() command.run(args) - if self.outs: + if self.outs and task_type.run_outgen(): command = OutgenCommand() command.run(args) diff --git a/src/sinol_make/commands/outgen/__init__.py b/src/sinol_make/commands/outgen/__init__.py index 3a188484..b708b3e6 100644 --- a/src/sinol_make/commands/outgen/__init__.py +++ b/src/sinol_make/commands/outgen/__init__.py @@ -10,6 +10,7 @@ from sinol_make.structs.gen_structs import OutputGenerationArguments from sinol_make.helpers import parsers, package_util, cache, paths from sinol_make.interfaces.BaseCommand import BaseCommand +from sinol_make.task_type import get_task_type class Command(BaseCommand): @@ -106,6 +107,9 @@ def clean_cache(self, inputs): def run(self, args: argparse.Namespace): args = util.init_package_command(args) + task_type = get_task_type() + if not task_type.run_outgen(): + util.exit_with_error('This task type does not support output generation.') self.args = args self.task_id = package_util.get_task_id() diff --git a/src/sinol_make/task_type/base.py b/src/sinol_make/task_type/base.py index 0794a97f..e24399ea 100644 --- a/src/sinol_make/task_type/base.py +++ b/src/sinol_make/task_type/base.py @@ -16,6 +16,9 @@ def __init__(self, task_id): self.task_id = task_id self._has_checker = False + def run_outgen(self): + return True + def get_files_to_compile(self) -> List[Tuple[str, List[str], Dict[str, Any]]]: """ Returns a list of tuples where: diff --git a/src/sinol_make/task_type/interactive_io.py b/src/sinol_make/task_type/interactive_io.py index 2d6a97f9..41f201be 100644 --- a/src/sinol_make/task_type/interactive_io.py +++ b/src/sinol_make/task_type/interactive_io.py @@ -15,6 +15,9 @@ def __init__(self, task_id): self.interactor = None self.interactor_exe = None + def run_outgen(self): + return False + def get_files_to_compile(self): super().get_files_to_compile() interactors = package_util.get_files_matching_pattern(self.task_id, f'{self.task_id}soc.*') From 7e50d705a0ab56aaeecaefb866be877dfc9cf022 Mon Sep 17 00:00:00 2001 From: MasloMaslane Date: Wed, 29 May 2024 12:26:48 +0200 Subject: [PATCH 12/15] Exit on failed interactor compilation --- src/sinol_make/commands/run/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sinol_make/commands/run/__init__.py b/src/sinol_make/commands/run/__init__.py index e585431c..cd183f9f 100644 --- a/src/sinol_make/commands/run/__init__.py +++ b/src/sinol_make/commands/run/__init__.py @@ -1186,7 +1186,8 @@ def compile_checker(self): def compile_additional_files(self, files): for name, args, kwargs in files: print(f'Compiling {name}...') - self.compile(*args, **kwargs) + if not self.compile(*args, **kwargs): + util.exit_with_error(f'Compilation of {name} failed.') def run(self, args): args = util.init_package_command(args) From b992a7f00f82876fade3eccf01a8088ab4b8c417 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Thu, 30 May 2024 22:32:02 +0200 Subject: [PATCH 13/15] Remove cache on interactor change --- src/sinol_make/commands/run/__init__.py | 10 +++++----- src/sinol_make/helpers/cache.py | 6 +++--- src/sinol_make/helpers/compile.py | 6 +++--- src/sinol_make/task_type/interactive_io.py | 2 +- src/sinol_make/task_type/normal.py | 2 +- tests/commands/run/test_integration.py | 6 +++--- tests/helpers/test_cache.py | 2 +- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/sinol_make/commands/run/__init__.py b/src/sinol_make/commands/run/__init__.py index cd183f9f..450e3344 100644 --- a/src/sinol_make/commands/run/__init__.py +++ b/src/sinol_make/commands/run/__init__.py @@ -354,17 +354,17 @@ def get_groups(self, tests): return sorted(list(set([self.get_group(test) for test in tests]))) - def compile_solutions(self, solutions, is_checker=False): + def compile_solutions(self, solutions, remove_all_cache=False): os.makedirs(paths.get_compilation_log_path(), exist_ok=True) os.makedirs(paths.get_executables_path(), exist_ok=True) print("Compiling %d solutions..." % len(solutions)) - args = [(solution, True, is_checker) for solution in solutions] + args = [(solution, True, remove_all_cache) for solution in solutions] with mp.Pool(self.cpus) as pool: compilation_results = pool.starmap(self.compile, args) return compilation_results - def compile(self, solution, use_extras = False, is_checker = False): + def compile(self, solution, use_extras=False, remove_all_cache=False): os.makedirs(paths.get_compilation_log_path(), exist_ok=True) os.makedirs(paths.get_executables_path(), exist_ok=True) compile_log_file = paths.get_compilation_log_path("%s.compile_log" % package_util.get_file_name(solution)) @@ -387,7 +387,7 @@ def compile(self, solution, use_extras = False, is_checker = False): try: with open(compile_log_file, "w") as compile_log: compile.compile(source_file, output, self.compilers, compile_log, self.args.compile_mode, - extra_compilation_args, extra_compilation_files, is_checker=is_checker) + extra_compilation_args, extra_compilation_files, remove_all_cache=remove_all_cache) print(util.info("Compilation of file %s was successful." % package_util.get_file_name(solution))) return True @@ -1177,7 +1177,7 @@ def compile_checker(self): checker_basename = os.path.basename(self.checker) self.checker_executable = paths.get_executables_path(checker_basename + ".e") - checker_compilation = self.compile_solutions([self.checker], is_checker=True) + checker_compilation = self.compile_solutions([self.checker], remove_all_cache=True) if not checker_compilation[0]: util.exit_with_error('Checker compilation failed.') diff --git a/src/sinol_make/helpers/cache.py b/src/sinol_make/helpers/cache.py index cca819fd..016b553e 100644 --- a/src/sinol_make/helpers/cache.py +++ b/src/sinol_make/helpers/cache.py @@ -53,7 +53,7 @@ def check_compiled(file_path: str, compilation_flags: str, sanitizers: bool) -> return None -def save_compiled(file_path: str, exe_path: str, compilation_flags: str, sanitizers: bool, is_checker: bool = False): +def save_compiled(file_path: str, exe_path: str, compilation_flags: str, sanitizers: bool, remove_all_cache: bool = False): """ Save the compiled executable path to cache in `.cache/md5sums/`, which contains the md5sum of the file and the path to the executable. @@ -61,11 +61,11 @@ def save_compiled(file_path: str, exe_path: str, compilation_flags: str, sanitiz :param exe_path: Path to the compiled executable :param compilation_flags: Compilation flags used :param sanitizers: Whether -fsanitize=undefined,address was used - :param is_checker: Whether the compiled file is a checker. If True, all cached tests are removed. + :param remove_all_cache: Whether to remove all cached files. If True, all cached tests are removed. """ info = CacheFile(util.get_file_md5(file_path), exe_path, compilation_flags, sanitizers) info.save(file_path) - if is_checker: + if remove_all_cache: remove_results_cache() diff --git a/src/sinol_make/helpers/compile.py b/src/sinol_make/helpers/compile.py index e8ccd563..a929242b 100644 --- a/src/sinol_make/helpers/compile.py +++ b/src/sinol_make/helpers/compile.py @@ -14,7 +14,7 @@ def compile(program, output, compilers: Compilers = None, compile_log=None, compilation_flags='default', - extra_compilation_args=None, extra_compilation_files=None, is_checker=False, use_fsanitize=False): + extra_compilation_args=None, extra_compilation_files=None, remove_all_cache=False, use_fsanitize=False): """ Compile a program. :param program: Path to the program to compile @@ -24,7 +24,7 @@ def compile(program, output, compilers: Compilers = None, compile_log=None, comp :param compilation_flags: Group of compilation flags to use :param extra_compilation_args: Extra compilation arguments :param extra_compilation_files: Extra compilation files - :param is_checker: Set to True if compiling a checker. This will remove all cached test results. + :param remove_all_cache: Set to True if you want to remove all cached test results. :param use_fsanitize: Whether to use fsanitize when compiling C/C++ programs. Sanitizes address and undefined behavior. """ if extra_compilation_args is None: @@ -114,7 +114,7 @@ def compile(program, output, compilers: Compilers = None, compile_log=None, comp if process.returncode != 0: raise CompilationError('Compilation failed') else: - save_compiled(program, output, compilation_flags, use_fsanitize, is_checker) + save_compiled(program, output, compilation_flags, use_fsanitize, remove_all_cache) return True diff --git a/src/sinol_make/task_type/interactive_io.py b/src/sinol_make/task_type/interactive_io.py index 41f201be..2524fde6 100644 --- a/src/sinol_make/task_type/interactive_io.py +++ b/src/sinol_make/task_type/interactive_io.py @@ -24,7 +24,7 @@ def get_files_to_compile(self): if interactors: self.interactor = interactors[0] self.interactor_exe = paths.get_executables_path(package_util.get_executable(self.interactor)) - return [("interactor", [self.interactor], {})] + return [("interactor", [self.interactor], {'remove_all_cache': True})] def _get_interactor_result_file(self, sol_result_file_path: str): dirname = os.path.dirname(sol_result_file_path) diff --git a/src/sinol_make/task_type/normal.py b/src/sinol_make/task_type/normal.py index 324ba51d..59c050f3 100644 --- a/src/sinol_make/task_type/normal.py +++ b/src/sinol_make/task_type/normal.py @@ -38,7 +38,7 @@ def get_files_to_compile(self): self.checker = checkers[0] self._check_had_checker(True) self.checker_exe = paths.get_executables_path(package_util.get_executable(self.checker)) - return [("checker", [self.checker], {"is_checker": True})] + return [("checker", [self.checker], {"remove_all_cache": True})] else: self._has_checker = False self._check_had_checker(False) diff --git a/tests/commands/run/test_integration.py b/tests/commands/run/test_integration.py index 8058d56b..806966ff 100644 --- a/tests/commands/run/test_integration.py +++ b/tests/commands/run/test_integration.py @@ -339,8 +339,8 @@ def test_missing_output_files(capsys, create_package): assert 'There are tests without outputs.' in out assert 'Run outgen to fix this issue or add the --no-outputs flag to ignore the issue.' in out assert 'An error occurred while running the command.' not in out - - + + @pytest.mark.parametrize("create_package", [get_simple_package_path(), get_verify_status_package_path()], indirect=True) def test_missing_output_files_allow_missing(capsys, create_package): """ @@ -680,7 +680,7 @@ def test_results_caching_checker_changed(create_package, time_tool): f.write("// Changed checker source code.\n" + checker_source) # Compile checker check if test results are removed. - command.compile_additional_files([("checker", ["chkchk.cpp"], {"is_checker": True})]) + command.compile_additional_files([("checker", ["chkchk.cpp"], {"remove_all_cache": True})]) task_id = package_util.get_task_id() solutions = package_util.get_solutions(task_id, None) for solution in solutions: diff --git a/tests/helpers/test_cache.py b/tests/helpers/test_cache.py index 52c37c77..f1cdac9e 100644 --- a/tests/helpers/test_cache.py +++ b/tests/helpers/test_cache.py @@ -81,7 +81,7 @@ def test_cache(): cache_file.save("abc.cpp") assert cache.get_cache_file("abc.cpp") == cache_file cache.save_compiled("abc.cpp", "abc.e", "default", False, - is_checker=True) + remove_all_cache=True) assert cache.get_cache_file("abc.cpp").tests == {} # Test that cache is cleared when extra compilation files change From b1093d79a54fa30524fb919e0a993db401c56518 Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Thu, 30 May 2024 23:35:26 +0200 Subject: [PATCH 14/15] Refactor and cleanup --- src/sinol_make/commands/run/__init__.py | 263 ------------------ src/sinol_make/task_type/base.py | 28 +- src/sinol_make/task_type/interactive_io.py | 68 ++--- tests/packages/interactive_io/prog/iiosoc.cpp | 7 +- 4 files changed, 45 insertions(+), 321 deletions(-) diff --git a/src/sinol_make/commands/run/__init__.py b/src/sinol_make/commands/run/__init__.py index 450e3344..9e3f7791 100644 --- a/src/sinol_make/commands/run/__init__.py +++ b/src/sinol_make/commands/run/__init__.py @@ -310,16 +310,6 @@ def configure_subparser(self, subparser): parsers.add_compilation_arguments(parser) return parser - def parse_time(self, time_str): - if len(time_str) < 3: return -1 - return int(time_str[:-2]) - - - def parse_memory(self, memory_str): - if len(memory_str) < 3: return -1 - return int(memory_str[:-2]) - - def extract_file_name(self, file_path): return os.path.split(file_path)[1] @@ -335,9 +325,6 @@ def get_solution_from_exe(self, executable): return file + ext util.exit_with_error("Source file not found for executable %s" % executable) - def get_executables(self, args_solutions): - return [package_util.get_executable(solution) for solution in package_util.get_solutions(self.ID, args_solutions)] - def get_possible_score(self, groups): possible_score = 0 @@ -397,246 +384,6 @@ def compile(self, solution, use_extras=False, remove_all_cache=False): compile.print_compile_log(compile_log_file) return False - def check_output_diff(self, output_file, answer_file): - """ - Checks whether the output file and the answer file are the same. - """ - return util.file_diff(output_file, answer_file) - - def check_output_checker(self, name, input_file, output_file, answer_file): - """ - Checks if the output file is correct with the checker. - Returns True if the output file is correct, False otherwise and number of points. - """ - command = [self.checker_executable, input_file, output_file, answer_file] - process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) - process.wait() - checker_output = process.communicate()[0].decode("utf-8").splitlines() - - if len(checker_output) == 0: - raise CheckerOutputException("Checker output is empty.") - - if checker_output[0].strip() == "OK": - points = 100 - if len(checker_output) >= 3: - try: - points = int(checker_output[2].strip()) - except ValueError: - pass - - return True, points - else: - return False, 0 - - - def check_output(self, name, input_file, output_file_path, output, answer_file_path): - """ - Checks if the output file is correct. - Returns a tuple (is correct, number of points). - """ - try: - has_checker = self.checker is not None - except AttributeError: - has_checker = False - - if not has_checker: - with open(answer_file_path, "r") as answer_file: - correct = util.lines_diff(output, answer_file.readlines()) - return correct, 100 if correct else 0 - else: - with open(output_file_path, "w") as output_file: - output_file.write("\n".join(output) + "\n") - return self.check_output_checker(name, input_file, output_file_path, answer_file_path) - - def execute_oiejq(self, name, timetool_path, executable, result_file_path, input_file_path, output_file_path, answer_file_path, - time_limit, memory_limit, hard_time_limit, execution_dir): - command = f'"{timetool_path}" "{executable}"' - env = os.environ.copy() - env["MEM_LIMIT"] = f'{memory_limit}K' - env["MEASURE_MEM"] = "1" - env["UNDER_OIEJQ"] = "1" - - timeout = False - with open(input_file_path, "r") as input_file, open(output_file_path, "w") as output_file, \ - open(result_file_path, "w") as result_file: - process = subprocess.Popen(command, shell=True, stdin=input_file, stdout=output_file, - stderr=result_file, env=env, preexec_fn=os.setsid, cwd=execution_dir) - - def sigint_handler(signum, frame): - try: - os.killpg(os.getpgid(process.pid), signal.SIGTERM) - except ProcessLookupError: - pass - sys.exit(1) - signal.signal(signal.SIGINT, sigint_handler) - - try: - process.wait(timeout=hard_time_limit) - except subprocess.TimeoutExpired: - timeout = True - try: - os.killpg(os.getpgid(process.pid), signal.SIGTERM) - except ProcessLookupError: - pass - process.communicate() - - with open(result_file_path, "r") as result_file: - lines = result_file.read() - with open(output_file_path, "r") as output_file: - output = output_file.read() - result = ExecutionResult() - - if not timeout: - lines = lines.splitlines() - output = output.splitlines() - - for line in lines: - line = line.strip() - if ": " in line: - (key, value) = line.split(": ")[:2] - if key == "Time": - result.Time = self.parse_time(value) - elif key == "Memory": - result.Memory = self.parse_memory(value) - else: - setattr(result, key, value) - - if timeout: - result.Status = Status.TL - elif getattr(result, "Time") is not None and result.Time > time_limit: - result.Status = Status.TL - elif getattr(result, "Memory") is not None and result.Memory > memory_limit: - result.Status = Status.ML - elif getattr(result, "Status") is None: - result.Status = Status.RE - elif result.Status == "OK": # Here OK is a string, because it is set while parsing oiejq's output. - if result.Time > time_limit: - result.Status = Status.TL - elif result.Memory > memory_limit: - result.Status = Status.ML - else: - try: - correct, result.Points = self.task_type.check_output(input_file_path, output_file_path, - output, answer_file_path) - if not correct: - result.Status = Status.WA - except CheckerOutputException as e: - result.Status = Status.CE - result.Error = e.message - else: - result.Status = result.Status[:2] - - return result - - - def execute_time(self, name, executable, result_file_path, input_file_path, output_file_path, answer_file_path, - time_limit, memory_limit, hard_time_limit, execution_dir): - if sys.platform == 'darwin': - time_name = 'gtime' - elif sys.platform == 'linux': - time_name = 'time' - elif sys.platform == 'win32' or sys.platform == 'cygwin': - raise Exception("Measuring time with GNU time on Windows is not supported.") - - command = [f'{time_name}', '-f', '%U\\n%M\\n%x', '-o', result_file_path, executable] - timeout = False - mem_limit_exceeded = False - with open(input_file_path, "r") as input_file, open(output_file_path, "w") as output_file: - process = subprocess.Popen(command, stdin=input_file, stdout=output_file, stderr=subprocess.DEVNULL, - preexec_fn=os.setsid, cwd=execution_dir) - - def sigint_handler(signum, frame): - try: - os.killpg(os.getpgid(process.pid), signal.SIGTERM) - except ProcessLookupError: - pass - sys.exit(1) - signal.signal(signal.SIGINT, sigint_handler) - - start_time = time.time() - while process.poll() is None: - try: - time_process = psutil.Process(process.pid) - executable_process = None - for child in time_process.children(): - if child.name() == executable: - executable_process = child - break - if executable_process is not None and executable_process.memory_info().rss > memory_limit * 1024: - try: - os.killpg(os.getpgid(process.pid), signal.SIGTERM) - except ProcessLookupError: - pass - mem_limit_exceeded = True - break - except psutil.NoSuchProcess: - pass - - if time.time() - start_time > hard_time_limit: - try: - os.killpg(os.getpgid(process.pid), signal.SIGTERM) - except ProcessLookupError: - pass - timeout = True - break - - with open(output_file_path, "r") as output_file: - output = output_file.read() - result = ExecutionResult() - program_exit_code = None - if not timeout: - output = output.splitlines() - with open(result_file_path, "r") as result_file: - lines = result_file.readlines() - if len(lines) == 3: - """ - If programs runs successfully, the output looks like this: - - first line is CPU time in seconds - - second line is memory in KB - - third line is exit code - This format is defined by -f flag in time command. - """ - result.Time = round(float(lines[0].strip()) * 1000) - result.Memory = int(lines[1].strip()) - program_exit_code = int(lines[2].strip()) - elif len(lines) > 0 and "Command terminated by signal " in lines[0]: - """ - If there was a runtime error, the first line is the error message with signal number. - For example: - Command terminated by signal 11 - """ - program_exit_code = int(lines[0].strip().split(" ")[-1]) - elif not mem_limit_exceeded: - result.Status = Status.RE - result.Error = "Unexpected output from time command: " + "".join(lines) - return result - - if program_exit_code is not None and program_exit_code != 0: - result.Status = Status.RE - elif timeout: - result.Status = Status.TL - elif mem_limit_exceeded: - result.Memory = memory_limit + 1 # Add one so that the memory is red in the table - result.Status = Status.ML - elif result.Time > time_limit: - result.Status = Status.TL - elif result.Memory > memory_limit: - result.Status = Status.ML - else: - try: - correct, result.Points = self.task_type.check_output(input_file_path, output_file_path, - output, answer_file_path) - if correct: - result.Status = Status.OK - else: - result.Status = Status.WA - except CheckerOutputException as e: - result.Status = Status.CE - result.Error = e.message - - return result - - def run_solution(self, data_for_execution: ExecutionData): """ Run an execution and return the result as ExecutionResult object. @@ -1173,16 +920,6 @@ def check_errors(self, results: Dict[str, Dict[str, Dict[str, ExecutionResult]]] if error_msg != "": print(util.error(error_msg)) - def compile_checker(self): - checker_basename = os.path.basename(self.checker) - self.checker_executable = paths.get_executables_path(checker_basename + ".e") - - checker_compilation = self.compile_solutions([self.checker], remove_all_cache=True) - if not checker_compilation[0]: - util.exit_with_error('Checker compilation failed.') - - - def compile_additional_files(self, files): for name, args, kwargs in files: print(f'Compiling {name}...') diff --git a/src/sinol_make/task_type/base.py b/src/sinol_make/task_type/base.py index e24399ea..149d0e40 100644 --- a/src/sinol_make/task_type/base.py +++ b/src/sinol_make/task_type/base.py @@ -19,6 +19,9 @@ def __init__(self, task_id): def run_outgen(self): return True + def require_outputs(self): + return True + def get_files_to_compile(self) -> List[Tuple[str, List[str], Dict[str, Any]]]: """ Returns a list of tuples where: @@ -34,6 +37,9 @@ def has_checker(self) -> bool: def _run_checker(self, input_file, output_file_path, answer_file_path) -> List[str]: return [] + def _raise_empty_output(self): + raise CheckerOutputException("Checker output is empty.") + def _parse_checker_output(self, checker_output: List[str]) -> Tuple[bool, int]: """ Parse the output of the checker @@ -140,15 +146,6 @@ def _parse_oiejq_output(self, result_file_path: str): return result def _run_subprocess(self, oiejq: bool, sigint_handler, executable, memory_limit, hard_time_limit, *args, **kwargs): - # print("oiejq", oiejq) - # print("executable", executable) - # print("memory_limit", memory_limit) - # print("hard_time_limit", hard_time_limit) - # print(args, kwargs) - # stdin_stat = os.fstat(kwargs['stdin']) - # print("stdin", kwargs['stdin'], "stdin_stat", stdin_stat) - # stdout_stat = os.fstat(kwargs['stdout']) - # print("stdout", kwargs['stdout'], "stdout_stat", stdout_stat) process = subprocess.Popen(*args, **kwargs) if 'pass_fds' in kwargs: for fd in kwargs['pass_fds']: @@ -215,9 +212,6 @@ def _run_program_time(self, command, env, executable, result_file_path, input_fi answer_file_path, time_limit, memory_limit, hard_time_limit, execution_dir): raise NotImplementedError() - def _update_result_RE(self, result, program_exit_code): - pass - def _parse_additional_time(self, result_file_path) -> Union[ExecutionResult, None]: return None @@ -238,16 +232,11 @@ def run(self, oiejq: bool, timetool_path, executable, result_file_path, input_fi execution_dir) if not timeout: result, program_exit_code = self._parse_time_output(result_file_path) - if program_exit_code is not None and program_exit_code != 0: + if program_exit_code is not None and program_exit_code != 0 and result.Status != Status.RE: result.Status = Status.RE result.Error = f"Program exited with code {program_exit_code}." - self._update_result_RE(result, program_exit_code) return result - additional_result = self._parse_additional_time(result_file_path) - if additional_result is not None: - return additional_result - try: with open(output_file_path, "r") as output_file: output = output_file.readlines() @@ -282,6 +271,3 @@ def getattrd(obj, attr, default): result.Error = str(e) return result - - def require_outputs(self): - return True diff --git a/src/sinol_make/task_type/interactive_io.py b/src/sinol_make/task_type/interactive_io.py index 2524fde6..e83f7066 100644 --- a/src/sinol_make/task_type/interactive_io.py +++ b/src/sinol_make/task_type/interactive_io.py @@ -5,6 +5,7 @@ from sinol_make import oiejq from sinol_make.helpers import package_util, paths +from sinol_make.interfaces.Errors import CheckerOutputException from sinol_make.structs.status_structs import ExecutionResult, Status from sinol_make.task_type import BaseTaskType @@ -18,6 +19,9 @@ def __init__(self, task_id): def run_outgen(self): return False + def require_outputs(self): + return False + def get_files_to_compile(self): super().get_files_to_compile() interactors = package_util.get_files_matching_pattern(self.task_id, f'{self.task_id}soc.*') @@ -32,15 +36,16 @@ def _get_interactor_result_file(self, sol_result_file_path: str): ext = os.path.basename(sol_result_file_path).split(".")[1] return os.path.join(dirname, f"{basename_no_ext}_interactor.{ext}") + def _raise_empty_output(self): + raise CheckerOutputException("Interactor output is empty.") + def check_output(self, input_file, output_file_path, output, answer_file_path) -> Tuple[bool, int]: + if not os.path.exists(output_file_path): + self._raise_empty_output() with open(output_file_path, "r") as output_file: output = output_file.read().splitlines() return self._parse_checker_output(output) - def _update_result_RE(self, result, program_exit_code): - if program_exit_code == signal.Signals.SIGPIPE: - result.Error = "Interactor exited prematurely" - def _check_errors(self, result, interactor_exit_code, program_exit_code): if interactor_exit_code != 0 and interactor_exit_code != signal.Signals.SIGPIPE: result.Status = Status.RE @@ -52,24 +57,31 @@ def _check_errors(self, result, interactor_exit_code, program_exit_code): return result elif interactor_exit_code == signal.Signals.SIGPIPE: result.Status = Status.RE - result.Error = "Interactor exited prematurely" + result.Error = "Solution exited prematurely" return result else: - return None + return result - def _parse_additional_time(self, result_file_path) -> Union[ExecutionResult, None]: - result, program_exit_code = self._parse_time_output(result_file_path) - _, interactor_exit_code = self._parse_time_output( + def _parse_time_output(self, result_file_path) -> Tuple[Union[ExecutionResult, None], int]: + result, program_exit_code = super()._parse_time_output(result_file_path) + _, interactor_exit_code = super()._parse_time_output( self._get_interactor_result_file(result_file_path) ) - return self._check_errors(result, interactor_exit_code, program_exit_code) + return self._check_errors(result, interactor_exit_code, program_exit_code), program_exit_code - def _run_program_time(self, command, env, executable, result_file_path, input_file_path, output_file_path, - answer_file_path, time_limit, memory_limit, hard_time_limit, execution_dir): + def _get_pipes(self): r1, w1 = os.pipe() r2, w2 = os.pipe() for fd in (r1, w1, r2, w2): os.set_inheritable(fd, True) + return r1, w1, r2, w2 + + def _thread_wrapper(self, result, *args, **kwargs): + result.append(self._run_subprocess(*args, **kwargs)) + + def _run_program_time(self, command, env, executable, result_file_path, input_file_path, output_file_path, + answer_file_path, time_limit, memory_limit, hard_time_limit, execution_dir): + r1, w1, r2, w2 = self._get_pipes() command_sol = self._wrap_with_time( [f'"{executable}"'], @@ -80,12 +92,9 @@ def _run_program_time(self, command, env, executable, result_file_path, input_fi self._get_interactor_result_file(result_file_path) ) - def thread_wrapper(result, *args, **kwargs): - result.append(self._run_subprocess(*args, **kwargs)) - sol_result = [] solution = Thread( - target=thread_wrapper, + target=self._thread_wrapper, args=(sol_result, False, False, executable, memory_limit, hard_time_limit, ' '.join(command_sol),), kwargs={ "shell": True, @@ -98,9 +107,9 @@ def thread_wrapper(result, *args, **kwargs): ) interactor_result = [] interactor = Thread( - target=thread_wrapper, + target=self._thread_wrapper, args=(interactor_result, False, False, self.interactor_exe, memory_limit, hard_time_limit, - ' '.join(command_interactor),), + ' '.join(command_interactor),), kwargs={ "shell": True, "stdin": r2, @@ -121,13 +130,11 @@ def _wrap_with_sio2jail(self, command, result_file_path, sio2jail_path, mem_limi return [f'"{sio2jail_path}"', "--mount-namespace", "off", "--pid-namespace", "off", "--uts-namespace", "off", "--ipc-namespace", "off", "--net-namespace", "off", "--capability-drop", "off", "--user-namespace", "off", "-s", "-m", str(mem_limit), "-f", "3", "-o", "oiaug", "--"] + \ - command + [f'3>"{result_file_path}"'] + command + [f'3>"{result_file_path}"'] - def _run_program_oiejq(self, command, env, executable, result_file_path, input_file_path, output_file_path, answer_file_path, time_limit, memory_limit, hard_time_limit, execution_dir): - r1, w1 = os.pipe() - r2, w2 = os.pipe() - for fd in (r1, w1, r2, w2): - os.set_inheritable(fd, True) + def _run_program_oiejq(self, command, env, executable, result_file_path, input_file_path, output_file_path, + answer_file_path, time_limit, memory_limit, hard_time_limit, execution_dir): + r1, w1, r2, w2 = self._get_pipes() oiejq_path = oiejq.get_oiejq_path() sio2jail_path = os.path.join(os.path.dirname(oiejq_path), "sio2jail") @@ -145,12 +152,9 @@ def _run_program_oiejq(self, command, env, executable, result_file_path, input_f memory_limit ) - def thread_wrapper(result, *args, **kwargs): - result.append(self._run_subprocess(*args, **kwargs)) - sol_result = [] solution = Thread( - target=thread_wrapper, + target=self._thread_wrapper, args=(sol_result, True, False, executable, memory_limit, hard_time_limit, ' '.join(command_sol),), kwargs={ "shell": True, @@ -163,9 +167,9 @@ def thread_wrapper(result, *args, **kwargs): ) interactor_result = [] interactor = Thread( - target=thread_wrapper, + target=self._thread_wrapper, args=(interactor_result, True, False, self.interactor_exe, memory_limit, hard_time_limit, - ' '.join(command_interactor),), + ' '.join(command_interactor),), kwargs={ "shell": True, "stdin": r2, @@ -180,7 +184,6 @@ def thread_wrapper(result, *args, **kwargs): solution.join() interactor.join() - print(open(self._get_interactor_result_file(result_file_path), "r").read()) return sol_result[0][0], sol_result[0][1] def _parse_oiejq_output(self, result_file_path: str): @@ -214,6 +217,3 @@ def _parse_oiejq_output(self, result_file_path: str): result.Time = round(float(time * 1000)) result.Memory = int(mem) return result - - def require_outputs(self): - return False diff --git a/tests/packages/interactive_io/prog/iiosoc.cpp b/tests/packages/interactive_io/prog/iiosoc.cpp index d93e2cc2..8e8d365e 100644 --- a/tests/packages/interactive_io/prog/iiosoc.cpp +++ b/tests/packages/interactive_io/prog/iiosoc.cpp @@ -8,18 +8,19 @@ int main(int argc, char const *argv[]) { return 1; } ifstream ifs(argv[1]); + ofstream out(argv[2]); int a; ifs >> a; cout << a << "\n" << flush; int ans; cin >> ans; if (ans == a + 42) { - cerr << "OK\n"; + out << "OK\n"; } else if (ans == a + 10) { - cerr << "OK\nwrong diff\n50"; + out << "OK\nwrong diff\n50"; } else { - cerr << "WRONG\n"; + out << "WRONG\n" << ans; } } From e65f25c6ce2f85ea036fadcbd0d284d0333d51af Mon Sep 17 00:00:00 2001 From: Mateusz Masiarz Date: Sun, 30 Jun 2024 16:41:34 +0200 Subject: [PATCH 15/15] Stuff --- src/sinol_make/commands/run/__init__.py | 1 + src/sinol_make/task_type/base.py | 7 +- src/sinol_make/task_type/interactive_io.py | 8 +- tests/packages/abc/abc.tgz | Bin 0 -> 5964 bytes tests/packages/interactive_io/config.yml | 2 +- tests/packages/interactive_io/in/iio0.in | 1 + tests/packages/interactive_io/in/iio1.in | 1 + tests/packages/interactive_io/prog/iio.cpp | 2 +- tests/packages/interactive_io/prog/iio2.cpp | 79 +++++++++++++ tests/packages/interactive_io/prog/iio3.cpp | 2 +- tests/packages/interactive_io/prog/iiosoc.cpp | 108 +++++++++++++++++- tests/packages/interactive_io/run.sh | 9 ++ 12 files changed, 206 insertions(+), 14 deletions(-) create mode 100644 tests/packages/abc/abc.tgz create mode 100644 tests/packages/interactive_io/in/iio0.in create mode 100644 tests/packages/interactive_io/in/iio1.in create mode 100644 tests/packages/interactive_io/run.sh diff --git a/src/sinol_make/commands/run/__init__.py b/src/sinol_make/commands/run/__init__.py index 9e3f7791..2ede35f0 100644 --- a/src/sinol_make/commands/run/__init__.py +++ b/src/sinol_make/commands/run/__init__.py @@ -447,6 +447,7 @@ def run_solutions(self, compiled_commands, names, solutions, executables_dir): print_data = PrintData(0) has_terminal, terminal_width, terminal_height = util.get_terminal_size() + has_terminal = False if has_terminal: run_event = threading.Event() diff --git a/src/sinol_make/task_type/base.py b/src/sinol_make/task_type/base.py index 149d0e40..a3465beb 100644 --- a/src/sinol_make/task_type/base.py +++ b/src/sinol_make/task_type/base.py @@ -146,10 +146,11 @@ def _parse_oiejq_output(self, result_file_path: str): return result def _run_subprocess(self, oiejq: bool, sigint_handler, executable, memory_limit, hard_time_limit, *args, **kwargs): + fds_to_close = kwargs.pop('fds_to_close', []) process = subprocess.Popen(*args, **kwargs) - if 'pass_fds' in kwargs: - for fd in kwargs['pass_fds']: - os.close(fd) + for fd in fds_to_close: + print("Closing fd " + str(fd)) + os.close(fd) if sigint_handler: def sigint_handler(signum, frame): diff --git a/src/sinol_make/task_type/interactive_io.py b/src/sinol_make/task_type/interactive_io.py index e83f7066..f4fff910 100644 --- a/src/sinol_make/task_type/interactive_io.py +++ b/src/sinol_make/task_type/interactive_io.py @@ -102,7 +102,7 @@ def _run_program_time(self, command, env, executable, result_file_path, input_fi "stdout": w2, "preexec_fn": os.setsid, "cwd": execution_dir, - "pass_fds": (r1, w2,), + "fds_to_close": (r1, w2,), } ) interactor_result = [] @@ -116,7 +116,7 @@ def _run_program_time(self, command, env, executable, result_file_path, input_fi "stdout": w1, "preexec_fn": os.setsid, "cwd": execution_dir, - "pass_fds": (r2, w1,) + "fds_to_close": (r2, w1,) } ) solution.start() @@ -162,7 +162,7 @@ def _run_program_oiejq(self, command, env, executable, result_file_path, input_f "stdout": w2, "preexec_fn": os.setsid, "cwd": execution_dir, - "pass_fds": (r1, w2,) + "fds_to_close": (r1, w2,) } ) interactor_result = [] @@ -176,7 +176,7 @@ def _run_program_oiejq(self, command, env, executable, result_file_path, input_f "stdout": w1, "preexec_fn": os.setsid, "cwd": execution_dir, - "pass_fds": (r2, w1,) + "fds_to_close": (r2, w1,) } ) solution.start() diff --git a/tests/packages/abc/abc.tgz b/tests/packages/abc/abc.tgz new file mode 100644 index 0000000000000000000000000000000000000000..a72fbb3724c50927afd583e170cba75a13b48d40 GIT binary patch literal 5964 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 uq7Tok%l`v@0q@QL$N&IlRJ)h} literal 0 HcmV?d00001 diff --git a/tests/packages/interactive_io/config.yml b/tests/packages/interactive_io/config.yml index dc6de905..b9186b41 100644 --- a/tests/packages/interactive_io/config.yml +++ b/tests/packages/interactive_io/config.yml @@ -10,7 +10,7 @@ sinol_expected_scores: points: 100 iio2.cpp: expected: - 0: {points: 0, status: WA} + 0: {points: 0, status: RE} 1: {points: 0, status: RE} points: 0 iio3.cpp: diff --git a/tests/packages/interactive_io/in/iio0.in b/tests/packages/interactive_io/in/iio0.in new file mode 100644 index 00000000..0cfbf088 --- /dev/null +++ b/tests/packages/interactive_io/in/iio0.in @@ -0,0 +1 @@ +2 diff --git a/tests/packages/interactive_io/in/iio1.in b/tests/packages/interactive_io/in/iio1.in new file mode 100644 index 00000000..00750edc --- /dev/null +++ b/tests/packages/interactive_io/in/iio1.in @@ -0,0 +1 @@ +3 diff --git a/tests/packages/interactive_io/prog/iio.cpp b/tests/packages/interactive_io/prog/iio.cpp index 177f9501..2b9e911e 100644 --- a/tests/packages/interactive_io/prog/iio.cpp +++ b/tests/packages/interactive_io/prog/iio.cpp @@ -5,5 +5,5 @@ using namespace std; int main() { int a; cin >> a; - cout << a + 42 << "\n" << flush; + cout << a + 1 << "\n" << flush; } diff --git a/tests/packages/interactive_io/prog/iio2.cpp b/tests/packages/interactive_io/prog/iio2.cpp index ae779674..c01e8d3a 100644 --- a/tests/packages/interactive_io/prog/iio2.cpp +++ b/tests/packages/interactive_io/prog/iio2.cpp @@ -1,7 +1,86 @@ #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +std::string exec(const char* cmd) { + std::array buffer; + std::string result; + std::unique_ptr pipe(popen(cmd, "r"), pclose); + if (!pipe) { + throw std::runtime_error("popen() failed!"); + } + while (fgets(buffer.data(), static_cast(buffer.size()), pipe.get()) != nullptr) { + result += buffer.data(); + } + return result; +} using namespace std; +ofstream out("xd"); + +#define MAX_PATH_LENGTH 1024 + +void print_open_descriptors(void) +{ + const char* path = "/proc/self/fd"; + + // Iterate over all symlinks in `path`. + // They represent open file descriptors of our process. + DIR* dr = opendir(path); + if (dr == NULL) + out << "Could not open dir: " << path << "\n"; + + struct dirent* entry; + while ((entry = readdir(dr)) != NULL) { + if (entry->d_type != DT_LNK) + continue; + + // Make a c-string with the full path of the entry. + char subpath[MAX_PATH_LENGTH]; + int ret = snprintf(subpath, sizeof(subpath), "%s/%s", path, entry->d_name); + if (ret < 0 || ret >= (int)sizeof(subpath)) + out << "Error in snprintf\n"; + + // Read what the symlink points to. + char symlink_target[MAX_PATH_LENGTH]; + ssize_t ret2 = readlink(subpath, symlink_target, sizeof(symlink_target) - 1); + if (ret2 == -1) + out << "Could not read symlink: " << subpath << "\n"; + symlink_target[ret2] = '\0'; + + // Skip an additional open descriptor to `path` that we have until closedir(). + if (strncmp(symlink_target, "/proc", 5) == 0) + continue; + + out << "Pid " << getpid() << " file descriptor " << entry->d_name << " -> " << symlink_target << "\n"; + } + + string res = exec("ls -l /proc/*/fd"); + out << res << "\n"; + res = exec("ps aux"); + out << res << "\n"; + + closedir(dr); +} + int main() { + print_open_descriptors(); + // sleep for a second + this_thread::sleep_for(2s); return 0; } diff --git a/tests/packages/interactive_io/prog/iio3.cpp b/tests/packages/interactive_io/prog/iio3.cpp index f335f14c..791e6adc 100644 --- a/tests/packages/interactive_io/prog/iio3.cpp +++ b/tests/packages/interactive_io/prog/iio3.cpp @@ -5,5 +5,5 @@ using namespace std; int main() { int a; cin >> a; - cout << a + 10 << "\n" << flush; + cout << a + 2 << "\n" << flush; } diff --git a/tests/packages/interactive_io/prog/iiosoc.cpp b/tests/packages/interactive_io/prog/iiosoc.cpp index 8e8d365e..a0781bc8 100644 --- a/tests/packages/interactive_io/prog/iiosoc.cpp +++ b/tests/packages/interactive_io/prog/iiosoc.cpp @@ -1,26 +1,126 @@ #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +std::string exec(const char* cmd) { + std::array buffer; + std::string result; + std::unique_ptr pipe(popen(cmd, "r"), pclose); + if (!pipe) { + throw std::runtime_error("popen() failed!"); + } + while (fgets(buffer.data(), static_cast(buffer.size()), pipe.get()) != nullptr) { + result += buffer.data(); + } + return result; +} using namespace std; +ofstream out; + +#define MAX_PATH_LENGTH 1024 + +void print_open_descriptors(void) +{ + const char* path = "/proc/self/fd"; + + // Iterate over all symlinks in `path`. + // They represent open file descriptors of our process. + DIR* dr = opendir(path); + if (dr == NULL) + out << "Could not open dir: " << path << "\n"; + + struct dirent* entry; + while ((entry = readdir(dr)) != NULL) { + if (entry->d_type != DT_LNK) + continue; + + // Make a c-string with the full path of the entry. + char subpath[MAX_PATH_LENGTH]; + int ret = snprintf(subpath, sizeof(subpath), "%s/%s", path, entry->d_name); + if (ret < 0 || ret >= (int)sizeof(subpath)) + out << "Error in snprintf\n"; + + // Read what the symlink points to. + char symlink_target[MAX_PATH_LENGTH]; + ssize_t ret2 = readlink(subpath, symlink_target, sizeof(symlink_target) - 1); + if (ret2 == -1) + out << "Could not read symlink: " << subpath << "\n"; + symlink_target[ret2] = '\0'; + + // Skip an additional open descriptor to `path` that we have until closedir(). + if (strncmp(symlink_target, "/proc", 5) == 0) + continue; + + out << "Pid " << getpid() << " file descriptor " << entry->d_name << " -> " << symlink_target << "\n"; + } + + string res = exec("ls -l /proc/*/fd"); + out << res << "\n"; + res = exec("ps aux"); + out << res << "\n"; + + closedir(dr); +} + int main(int argc, char const *argv[]) { if (argc != 3) { cerr << "Usage: ./a.out " << endl; return 1; } ifstream ifs(argv[1]); - ofstream out(argv[2]); + out.open(argv[2]); int a; ifs >> a; cout << a << "\n" << flush; int ans; +// char c; + int ret = fcntl(0, F_GETFD); + errno = 0; + bool closed = ret == -1 && errno == EBADF; +// scanf("%d", &ans); +// ssize_t read_ret = read(0, &c, sizeof(c)); +// if (read_ret != sizeof(c)) { +// out << "WRONG\n" << "read_ret: " << read_ret << "\n"; +// return 0; +// } +// ans = c - '0'; cin >> ans; - if (ans == a + 42) { + if (cin.eof()) { + out << "WRONG\nEOF\n"; + return 0; + } + ret = fcntl(0, F_GETFD); + errno = 0; + bool closed2 = ret == -1 && errno == EBADF; +// cin >> ans; + int b; + cin >> b; + if (ans == a + 1) { out << "OK\n"; } - else if (ans == a + 10) { + else if (ans == a + 2) { out << "OK\nwrong diff\n50"; } else { - out << "WRONG\n" << ans; + out << "WRONG\n" << ans << " " << "\n"; +// out << "WRONG\n" << ans << " " << closed << " " << closed2 << " " << b << "\n"; +// +// print_open_descriptors(); } } diff --git a/tests/packages/interactive_io/run.sh b/tests/packages/interactive_io/run.sh new file mode 100644 index 00000000..7a1ed862 --- /dev/null +++ b/tests/packages/interactive_io/run.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +while true; do + rm -rf .cache + sm run -s prog/iio2.cpp -t in/iio0.in + if [ $? -ne 0 ]; then + break + fi +done