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/__init__.py b/src/sinol_make/__init__.py index 5196abf8..6c944236 100644 --- a/src/sinol_make/__init__.py +++ b/src/sinol_make/__init__.py @@ -7,7 +7,7 @@ from sinol_make import util, oiejq -__version__ = "1.6.0" +__version__ = "1.7.0.dev1" def configure_parsers(): 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/commands/run/__init__.py b/src/sinol_make/commands/run/__init__.py index 161b9e5a..2ede35f0 100644 --- a/src/sinol_make/commands/run/__init__.py +++ b/src/sinol_make/commands/run/__init__.py @@ -5,6 +5,7 @@ import signal import threading import time +import traceback import psutil import glob import shutil @@ -17,7 +18,7 @@ from io import StringIO from typing import Dict -from sinol_make import contest_types, oiejq, util +from sinol_make import contest_types, oiejq, task_type, util from sinol_make.structs.run_structs import ExecutionData, PrintData from sinol_make.structs.cache_structs import CacheTest, CacheFile from sinol_make.interfaces.BaseCommand import BaseCommand @@ -309,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] @@ -334,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 @@ -353,17 +341,19 @@ 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)) source_file = os.path.join(os.getcwd(), "prog", self.get_solution_from_exe(solution)) output = paths.get_executables_path(package_util.get_executable(solution)) @@ -384,7 +374,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 @@ -394,262 +384,21 @@ def compile(self, solution, use_extras = False, is_checker = 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.check_output(name, 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.check_output(name, 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. """ - (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_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, os.path.join(os.getcwd(), test), output_file, + output_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): """ @@ -684,8 +433,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: @@ -697,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() @@ -807,7 +558,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], @@ -1032,6 +783,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): @@ -1149,7 +901,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.') @@ -1167,34 +919,13 @@ 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) - - 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) - 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 + print(util.error(error_msg)) + def compile_additional_files(self, files): + for name, args, kwargs in files: + print(f'Compiling {name}...') + 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) @@ -1220,14 +951,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("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 @@ -1260,7 +984,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/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 d3dc8958..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,13 +114,12 @@ 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 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 98d582b8..69459139 100644 --- a/src/sinol_make/helpers/package_util.py +++ b/src/sinol_make/helpers/package_util.py @@ -10,6 +10,7 @@ from sinol_make.helpers.func_cache import cache_result from sinol_make import util from sinol_make.helpers import paths +from sinol_make.structs.package_structs import TaskType @cache_result(cwd=True) @@ -54,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/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/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 new file mode 100644 index 00000000..aa525f67 --- /dev/null +++ b/src/sinol_make/task_type/__init__.py @@ -0,0 +1,17 @@ +import os + +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 + + +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.*"): + 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 new file mode 100644 index 00000000..a3465beb --- /dev/null +++ b/src/sinol_make/task_type/base.py @@ -0,0 +1,274 @@ +import os +import sys +import time +import signal +import psutil +import subprocess +from typing import List, Dict, Any, Tuple, Union + +from sinol_make import util +from sinol_make.interfaces.Errors import CheckerOutputException +from sinol_make.structs.status_structs import ExecutionResult, Status + + +class BaseTaskType: + def __init__(self, task_id): + self.task_id = task_id + self._has_checker = False + + 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: + - 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 _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 + :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 + + 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 = 0 + 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] 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: + 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: bool, sigint_handler, executable, memory_limit, hard_time_limit, *args, **kwargs): + fds_to_close = kwargs.pop('fds_to_close', []) + process = subprocess.Popen(*args, **kwargs) + for fd in fds_to_close: + print("Closing fd " + str(fd)) + os.close(fd) + + 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) + 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_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 _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: + 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: + 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 and result.Status != Status.RE: + result.Status = Status.RE + result.Error = f"Program exited with code {program_exit_code}." + return result + + 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: + return default + return getattr(obj, attr) + + if result.Status == Status.RE: + return result + 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.RE + result.Error = str(e) + + return result 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..f4fff910 --- /dev/null +++ b/src/sinol_make/task_type/interactive_io.py @@ -0,0 +1,219 @@ +import os +import signal +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.interfaces.Errors import CheckerOutputException +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 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.*') + if interactors: + self.interactor = interactors[0] + self.interactor_exe = paths.get_executables_path(package_util.get_executable(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) + 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 _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 _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 = "Solution exited prematurely" + return result + else: + return result + + 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), program_exit_code + + 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}"'], + result_file_path + ) + command_interactor = self._wrap_with_time( + [f'"{self.interactor_exe}"', f'"{input_file_path}"', f'"{output_file_path}"'], + self._get_interactor_result_file(result_file_path) + ) + + sol_result = [] + solution = Thread( + target=self._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, + "fds_to_close": (r1, w2,), + } + ) + interactor_result = [] + interactor = Thread( + target=self._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, + "fds_to_close": (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, r2, w2 = self._get_pipes() + + 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 + ) + + sol_result = [] + solution = Thread( + target=self._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, + "fds_to_close": (r1, w2,) + } + ) + interactor_result = [] + interactor = Thread( + target=self._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, + "fds_to_close": (r2, w1,) + } + ) + solution.start() + interactor.start() + solution.join() + interactor.join() + + 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 + + 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 + + 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) + return result diff --git a/src/sinol_make/task_type/normal.py b/src/sinol_make/task_type/normal.py new file mode 100644 index 00000000..59c050f3 --- /dev/null +++ b/src/sinol_make/task_type/normal.py @@ -0,0 +1,77 @@ +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], {"remove_all_cache": 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() + + 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 diff --git a/tests/commands/run/test_integration.py b/tests/commands/run/test_integration.py index 69b23b24..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_checker() + 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: @@ -800,7 +800,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) 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 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 diff --git a/tests/packages/abc/abc.tgz b/tests/packages/abc/abc.tgz new file mode 100644 index 00000000..a72fbb37 Binary files /dev/null and b/tests/packages/abc/abc.tgz differ diff --git a/tests/packages/interactive_io/config.yml b/tests/packages/interactive_io/config.yml new file mode 100644 index 00000000..b9186b41 --- /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: RE} + 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/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/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..2b9e911e --- /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 + 1 << "\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..c01e8d3a --- /dev/null +++ b/tests/packages/interactive_io/prog/iio2.cpp @@ -0,0 +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 new file mode 100644 index 00000000..791e6adc --- /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 + 2 << "\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..a0781bc8 --- /dev/null +++ b/tests/packages/interactive_io/prog/iiosoc.cpp @@ -0,0 +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]); + 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 (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 + 2) { + out << "OK\nwrong diff\n50"; + } + else { + 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