Exemplo n.º 1
0
    def test_run_command_exec_memory(self):
        factor_input = "1234567890123456789012345678901"
        run_result = Runner.run_command(sandbox=Sandbox(), command="factor {}".format(factor_input), timeout=1.0)
        factor_output = "{}: 7742394596501 159455563099482401".format(factor_input)
        self.assertEqual(run_result.output.decode().strip(), factor_output)
        self.assertGreater(run_result.exec_memory, 1 << 20)  # More than 1MB
        self.assertLess(run_result.exec_memory, 1 << 23)     # And less than 8MB

        sandbox = Sandbox()
        sandbox.put_file(os.path.join(self.PATH_FIXTURES, "..", "sandbox/mem_allocator.cpp"))
        run_result = Runner.run_command(sandbox=sandbox, timeout=10.0, privileged=True,
                                        command="g++ -O2 -std=c++17 -o mem_allocator mem_allocator.cpp")
        self.assertEqual(run_result.exit_code, 0)
        self.assertTrue(sandbox.has_file("mem_allocator"))

        run_result = Runner.run_command(sandbox=sandbox, command="./mem_allocator heap 50000000", timeout=1.0)
        self.assertEqual(run_result.exit_code, 0)
        self.assertGreater(run_result.exec_memory, 50000000)
        self.assertLess(run_result.exec_memory, 55000000)  # Allowing up to 5MB overhead

        run_result = Runner.run_command(sandbox=sandbox, command="./mem_allocator heap 250000000", timeout=1.0)
        self.assertEqual(run_result.exit_code, 0)
        self.assertGreater(run_result.exec_memory, 250000000)
        self.assertLess(run_result.exec_memory, 255000000)  # Allowing up to 5MB overhead

        run_result = Runner.run_command(sandbox=sandbox, command="./mem_allocator stack 10000000", timeout=1.0)
        self.assertEqual(run_result.exit_code, 0)
        self.assertGreater(run_result.exec_memory, 10000000)
        self.assertLess(run_result.exec_memory, 15000000)  # Allowing up to 5MB overhead

        run_result = Runner.run_command(sandbox=sandbox, command="./mem_allocator stack 50000000", timeout=1.0)
        self.assertEqual(run_result.exit_code, 0)
        self.assertGreater(run_result.exec_memory, 50000000)
        self.assertLess(run_result.exec_memory, 55000000)  # Allowing up to 5MB overhead
Exemplo n.º 2
0
    def compile_java(path_source, path_executable):
        # Remove "package" directives (we don't need them here)
        with open(path_source, "rt") as inp:
            lines_left = [line for line in inp.readlines() if not line.strip().startswith("package")]
        with open(path_source, "wt") as out:
            out.writelines(lines_left)

        sandbox = Sandbox()

        # Try compiling the Java file using a random class name
        # The compilation will almost certainly fail, but we'll figure out the name of the main class.
        class_name = ''.join(random.choices(string.ascii_lowercase, k=8))
        sandbox.put_file(path_source, class_name + ".java")

        command = Compiler.COMPILE_COMMAND_JAVA.format(source=class_name + ".java")
        run_result = Runner.run_command(
            sandbox=sandbox, command=command, timeout=config.MAX_COMPILATION_TIME, print_stderr=True, privileged=True
        )

        # If the compilation *does not* fail then there is no public class in the file
        if run_result.exit_code == 0 and run_result.output.decode() == "":
            return "No public class provided."

        # If the compilation fails as we expect (the public class being named differently
        # than the file it is in), try again using the name the compiler gives us.
        if "is public, should be declared in a file named" in run_result.output.decode():
            class_name = run_result.output.decode().split(" is public")[0].split()[-1]
            sandbox.put_file(path_source, class_name + ".java")
            command = Compiler.COMPILE_COMMAND_JAVA.format(source=class_name + ".java")
            run_result = Runner.run_command(
                sandbox=sandbox, command=command, timeout=config.MAX_COMPILATION_TIME, print_stderr=True, privileged=True)

        # Check for standard errors (time limit, internal error or compilation error)
        if run_result.exec_time > config.MAX_COMPILATION_TIME:
            return "Compilation exceeded the time limit of {0:.2f} seconds.".format(config.MAX_COMPILATION_TIME)
        if run_result.exit_code != 0:
            return "Compilation error: " + run_result.output.decode()

        # Do a sanity check that we have at least one class file with the public class
        if not sandbox.has_file(class_name + ".class"):
            return "An unexpected problem with the compilation arose, please report to the admin."

        # At this point everything seems to be fine and the code should be compiled into class files
        # Create a jar with them so we can execute it later on.
        command = Compiler.COMPILE_COMMAND_JAVA_JAR.format(class_name=class_name)
        run_result = Runner.run_command(
            sandbox=sandbox, command=command, timeout=config.MAX_COMPILATION_TIME, print_stderr=True, privileged=True)

        if run_result.exec_time > config.MAX_COMPILATION_TIME:
            return "Compilation exceeded the time limit of {0:.2f} seconds.".format(config.MAX_COMPILATION_TIME)
        if run_result.exit_code != 0:
            return "Compilation error: " + run_result.output.decode()

        sandbox.get_file("result.jar", path_executable)
        return ""
Exemplo n.º 3
0
    def compile_cpp(path_source, path_executable):
        sandbox = Sandbox()
        sandbox.put_file(path_source)
        name_source = os.path.basename(path_source)
        name_executable = os.path.basename(path_executable)

        command = Compiler.COMPILE_COMMAND_CPP.format(executable=name_executable, source=name_source)
        run_result = Runner.run_command(
            sandbox=sandbox, command=command, timeout=config.MAX_COMPILATION_TIME, print_stderr=True, privileged=True
        )

        if run_result.exec_time > config.MAX_COMPILATION_TIME - 0.1:
            return "Compilation exceeded the time limit of {0:.2f} seconds.".format(config.MAX_COMPILATION_TIME)
        if run_result.exit_code != 0:
            return "Compilation error: {}".format(run_result.output.decode())

        sandbox.get_file(name_executable, path_executable)
        return ""
Exemplo n.º 4
0
    def compile_python(path_source, path_executable):
        name_source = os.path.basename(path_source)

        sandbox = Sandbox()
        sandbox.put_file(path_source, name_source)

        command = Compiler.COMPILE_COMMAND_PYTHON.format(source=name_source)
        run_result = Runner.run_command(
            sandbox=sandbox, command=command, timeout=config.MAX_COMPILATION_TIME, print_stderr=True, privileged=True)

        if run_result.output.decode() != "":
            return "Compilation error: " + run_result.output.decode()

        if run_result.exec_time > config.MAX_COMPILATION_TIME:
            return "Compilation exceeded the time limit of {0:.2f} seconds.".format(config.MAX_COMPILATION_TIME)

        if run_result.exit_code != 0:
            return "Compilation exited with a non-zero exit code: {}".format(run_result.exit_code)

        # The file seems to be parsed correctly, so place it as an executable
        sandbox.get_file(name_source, path_executable)
        return ""
Exemplo n.º 5
0
    def test_timing_command_wrapper(self):
        sandbox = Sandbox()
        sandbox.put_file(os.path.join(self.PATH_FIXTURES, "handle_sigterm.py"))

        # With enough time the program completes successfully
        command = "pypy3 handle_sigterm.py"
        stdout_bytes, stderr_bytes = Runner.run(
            sandbox=sandbox, command=COMMAND_WRAPPER.format(command=command, timeout=0.4)
        )
        self.assertNotEqual(stdout_bytes.decode(), "")
        exit_code, exec_time, exec_memory = parse_exec_info(stderr_bytes.decode(), 0.4)
        self.assertEqual(exit_code, 0)
        self.assertTrue(0.0 <= exec_time <= 0.1)  # This is CPU time
        self.assertTrue(2**20 <= exec_memory <= 2**25)  # Takes between 1MB and 32MB

        # If it runs longer than the timeout, it gets killed before printing anything
        command = "pypy3 handle_sigterm.py"
        stdout_bytes, stderr_bytes = Runner.run(
            sandbox=sandbox, command=COMMAND_WRAPPER.format(command=command, timeout=0.2)
        )
        self.assertEqual(stdout_bytes.decode(), "")
        exit_code, exec_time, exec_memory = parse_exec_info(stderr_bytes.decode(), 0.2)
        self.assertEqual(exit_code, SIGKILL)
        self.assertTrue(0.2 <= exec_time <= 0.22)  # This is clock time
        self.assertTrue(2**20 <= exec_memory <= 2**25)  # Takes between 1MB and 32MB

        # Catching SIGTERM signal doesn't help
        command = "pypy3 handle_sigterm.py --handle"
        stdout_bytes, stderr_bytes = Runner.run(
            sandbox=sandbox, command=COMMAND_WRAPPER.format(command=command, timeout=0.2)
        )
        self.assertEqual(stdout_bytes.decode(), "")
        exit_code, exec_time, exec_memory = parse_exec_info(stderr_bytes.decode(), 0.2)
        self.assertEqual(exit_code, SIGKILL)
        self.assertTrue(0.2 <= exec_time <= 0.22)  # This is clock time
        self.assertTrue(2**20 <= exec_memory <= 2**25)  # Takes between 1MB and 32MB
Exemplo n.º 6
0
    def run_program(sandbox: Sandbox,
                    executable_path,
                    memory_limit,
                    timeout,
                    input_bytes=None,
                    print_stderr=False,
                    args=None,
                    privileged=False) -> RunResult:
        # Copy the executable to the sandbox directory
        sandbox.put_file(executable_path)

        # Generate the command which runs the executable (it is language-specific)
        executable_name = os.path.basename(executable_path)
        executable_language = common.get_language_by_exec_name(executable_name)
        command = Runner.get_run_command(executable_language, executable_name,
                                         memory_limit)

        # Run the program and measure its time and memory consumption
        run_result = Runner.run_command(sandbox=sandbox,
                                        command=command,
                                        timeout=timeout,
                                        input_bytes=input_bytes,
                                        print_stderr=print_stderr,
                                        args=args,
                                        privileged=privileged)

        # Calculate final time and memory (offset for VM start-up time)
        run_result.exec_time = max(
            0,
            run_result.exec_time - common.get_time_offset(executable_language))
        run_result.exec_memory = max(
            0, run_result.exec_memory -
            common.get_memory_offset(executable_language))

        # The caller function should populate the rest of the fields of the RunResult
        return run_result
Exemplo n.º 7
0
def execute_standard(submit_id, test: TestInfo,
                     run_config: RunConfig) -> RunResult:
    # Prepare input data (provided to the program through stdin)
    with open(test.inpPath, mode="rb") as inp:
        input_bytes = inp.read()

    # Run the solution inside a sandbox and delete the sandbox to free up the worker
    sandbox = Sandbox()
    run_result = Runner.run_program(sandbox=sandbox,
                                    executable_path=run_config.executable_path,
                                    memory_limit=run_config.memory_limit,
                                    timeout=run_config.timeout,
                                    input_bytes=input_bytes,
                                    print_stderr=False)
    del sandbox

    # If there is a checker, run it as well
    if run_config.checker_path is not None:
        # Create a temporary file and write the output there
        out_file = NamedTemporaryFile(mode="w+b", delete=True)
        with open(out_file.name, "wb") as out:
            out.write(run_result.output)
        out_file.seek(0)

        # Create execution config for the checker and run it
        sandbox = Sandbox()
        sandbox.put_file(test.inpPath, target_name="input.txt")
        sandbox.put_file(out_file.name, target_name="output.txt")
        sandbox.put_file(test.solPath, target_name="solution.txt")
        checker_result = Runner.run_program(
            sandbox=sandbox,
            executable_path=run_config.checker_path,
            memory_limit=config.MAX_EXECUTION_MEMORY,
            timeout=config.CHECKER_TIMEOUT,
            print_stderr=True,
            args=["input.txt", "output.txt", "solution.txt"])
        del sandbox

        # Close and delete temporary file with user's output
        out_file.close()

        if checker_result.exit_code != 0:
            message = "Checker returned non-zero exit code. Checker's output: '{}'".format(
                checker_result.output)
            logger.error("[Submission {id}] Internal Error: {error}".format(
                id=submit_id, error=message))
            return RunResult(status=TestStatus.INTERNAL_ERROR, error=message)

        run_result.output = checker_result.output

    return run_result
Exemplo n.º 8
0
class TestSandbox(TestCase):
    PATH_FIXTURES = os.path.abspath("tests/fixtures/sandbox/")

    @classmethod
    def setUpClass(cls):
        initializer.init()

    @classmethod
    def tearDownClass(cls):
        pass

    def setUp(self) -> None:
        self.sandbox = None

    def tearDown(self) -> None:
        if self.sandbox is not None:
            self.sandbox.wait(0.1)
            del self.sandbox
            self.sandbox = None

    @staticmethod
    def sandbox_helper(sandbox: Sandbox, command, privileged=False):
        stdout, stderr = TemporaryFile("wb+"), TemporaryFile("wb+")
        sandbox.execute(command=command,
                        stdin_fd=None,
                        stdout_fd=stdout,
                        stderr_fd=stderr,
                        privileged=privileged)

        stdout.flush()
        stdout.seek(0)
        stdout_text = stdout.read().decode().strip()
        stdout.close()
        stderr.flush()
        stderr.seek(0)
        stderr_text = stderr.read().decode().strip()
        stderr.close()

        # If running java or javac or jar the JVM prints an annoying message:
        # "Picked up JAVA_TOOL_OPTIONS: <actual options set by sandbox environment>
        # Remove it from the stderr if it is there
        if any(java in command for java in ["java", "javac", "jar"]):
            stdout_text = "\n".join([
                line for line in stdout_text.splitlines()
                if not line.startswith("Picked up JAVA_TOOL_OPTIONS")
            ])
            stderr_text = "\n".join([
                line for line in stderr_text.splitlines()
                if not line.startswith("Picked up JAVA_TOOL_OPTIONS")
            ])
        return stdout_text, stderr_text

    # ================================= #
    #           Chroot Setup            #
    # ================================= #
    def test_working_directory_is_empty(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="ls")
        self.assertEqual("", stderr)
        self.assertEqual("", stdout)

    def test_working_directory_is_home(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="pwd")
        self.assertEqual("", stderr)
        self.assertEqual("/home", stdout)

    def test_chroot_has_proper_fs_tree(self):
        # Root directory has a proper chroot structure
        # List all entries in "/" folder (but skip "total...", thus not using -la)
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="ls -ld /* /.*")
        self.assertEqual("", stderr)

        # All required system directories are present
        for mount_dir in [
                "bin", "dev", "etc", "lib", "lib64", "proc", "sys", "usr"
        ]:
            self.assertIn(mount_dir, stdout)

        # All dirs (".", "..", "/home" and mounted directories) have the correct permissions
        self.assertEqual(stdout.count("xr-xr-x"), len(stdout.splitlines()))

    def test_sys_structure_is_mounted(self):
        # There are files in the mounted directories
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="cat /proc/uptime")
        self.assertEqual("", stderr)
        self.assertNotEqual("", stdout)

        # Sanity check that an error is printed on a missing file
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="cat /proc/foobarbaz")
        self.assertNotEqual("", stderr)
        self.assertEqual("", stdout)

    def test_cannot_chroot_second_time(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="chroot ..")
        self.assertIn("Operation not permitted", stderr)
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="sudo chroot ..")
        self.assertIn("is not allowed to execute '/usr/sbin/chroot ..'",
                      stderr)

    # ================================= #
    #         File System Access        #
    # ================================= #
    def test_cant_touch_this(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="touch /proc/uptime")
        self.assertNotEqual("", stderr)
        self.assertEqual("", stdout)

    def test_cannot_make_directories(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="mkdir foo")
        self.assertNotEqual("", stderr)
        self.assertEqual("", stdout)

    def test_cannot_remove_directories(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="cd .. && rmdir home")
        self.assertNotEqual("", stderr)
        self.assertEqual("", stdout)

    def test_cannot_create_files(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="touch foo.txt")
        self.assertNotEqual("", stderr)
        self.assertEqual("", stdout)

    def test_cannot_redirect_to_files(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="echo bla > foo.txt")
        self.assertNotEqual("", stderr)
        self.assertEqual("", stdout)

    def test_cannot_rm_rf(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="rm -rf /")
        self.assertNotEqual("", stderr)
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="sudo rm -rf /")
        self.assertNotEqual("", stderr)

    def test_cp(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="cp /bin/bash .")
        self.assertIn("Permission denied", stderr)

    def test_mv(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="mv /bin/bash .")
        self.assertIn("Permission denied", stderr)

    def test_create_symlink(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="ln -s /bin/bash bash")
        self.assertIn("Permission denied", stderr)

    def test_mount(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="mount /bin /usr")
        self.assertIn("only root can do that", stderr)

    # ================================= #
    #           Network Access          #
    # ================================= #
    @mock.patch("config.MAX_EXECUTION_TIME", 1.0)
    def test_no_ping_dns_resolving(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="ping www.google.com")
        self.assertIn("Temporary failure in name resolution", stderr)

    @mock.patch("config.MAX_EXECUTION_TIME", 1.0)
    def test_no_ping_to_ip_address(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="ping 8.8.8.8")
        self.assertIn("Operation not permitted", stderr)

    @mock.patch("config.MAX_EXECUTION_TIME", 1.0)
    def test_no_wget_dns_resolving(self):
        stdout, stderr = self.sandbox_helper(
            sandbox=Sandbox(), command="wget www.google.com/robots.txt")
        self.assertIn("Temporary failure in name resolution", stderr)

    @mock.patch("config.MAX_EXECUTION_TIME", 1.0)
    def test_no_wget_from_ip_address(self):
        stdout, stderr = self.sandbox_helper(
            sandbox=Sandbox(), command="wget 216.58.212.4/robots.txt")
        # The process should reach the MAX_EXECUTION_TIME and be killed (being stuck on "Connecting to...")
        self.assertEqual("Connecting to 216.58.212.4:80...",
                         stderr.splitlines()[-1])

    @mock.patch("config.MAX_EXECUTION_TIME", 1.0)
    def test_no_localhost_access(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="ping localhost")
        self.assertIn("Operation not permitted", stderr)
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="ping 127.0.0.1")
        self.assertIn("Operation not permitted", stderr)

    # ================================= #
    #        User and process info      #
    # ================================= #
    def test_priority_in_boundaries(self):
        self.assertGreaterEqual(config.PROCESS_PRIORITY_REAL,
                                os.sched_get_priority_min(os.SCHED_RR))
        self.assertLessEqual(config.PROCESS_PRIORITY_REAL,
                             os.sched_get_priority_max(os.SCHED_RR))

    def test_cannot_run_commands_with_sudo(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="sudo ls -la")
        self.assertNotEqual("", stderr)
        self.assertEqual("", stdout)

    def test_prlimit_output(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="prlimit")

        expected = {
            "AS": [
                config.MAX_EXECUTION_MEMORY, config.MAX_EXECUTION_MEMORY,
                "bytes"
            ],
            "CPU":
            [config.MAX_EXECUTION_TIME, config.MAX_EXECUTION_TIME, "seconds"],
            "DATA": [
                config.MAX_EXECUTION_MEMORY, config.MAX_EXECUTION_MEMORY,
                "bytes"
            ],
            "FSIZE": [
                config.MAX_EXECUTION_OUTPUT, config.MAX_EXECUTION_OUTPUT,
                "bytes"
            ],
            "NOFILE": [config.MAX_OPEN_FILES, config.MAX_OPEN_FILES, "files"],
            "NPROC": [config.MAX_PROCESSES, config.MAX_PROCESSES, "processes"],
            "RSS": [
                config.MAX_EXECUTION_MEMORY, config.MAX_EXECUTION_MEMORY,
                "bytes"
            ],
            "STACK":
            [config.MAX_EXECUTION_STACK, config.MAX_EXECUTION_STACK, "bytes"],
        }

        lines = stdout.splitlines()
        for line in lines:
            tokens = line.split()
            if tokens[0] in expected:
                self.assertEqual(int(tokens[-3]), expected[tokens[0]][0])
                self.assertEqual(int(tokens[-2]), expected[tokens[0]][1])
                self.assertEqual(tokens[-1], expected[tokens[0]][2])

    def test_niceness_level(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="nice")
        self.assertEqual(str(config.PROCESS_PRIORITY_NICE), stdout)

    def test_worker_id(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="id -u")
        self.assertGreaterEqual(int(stdout), 1000)

    def test_worker_user(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="whoami")
        self.assertTrue("worker" in stdout)

    def test_scheduling_algorithm(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="chrt -p $$")
        self.assertIn("scheduling policy: SCHED_OTHER",
                      stdout.splitlines()[0])  # Standard UNIX scheduler

    def test_process_info(self):
        stdout, stderr = self.sandbox_helper(
            sandbox=Sandbox(),
            command="ps -o uid,pid,ppid,cls,pri,ni,rtprio -p $$")
        process_info = stdout.splitlines()[1].split()
        self.assertGreaterEqual(int(process_info[0]), 1000)  # User ID (again)
        self.assertEqual(
            process_info[3], "TS"
        )  # Scheduling algorithm, RR = real-time round robin, TS = standard
        self.assertEqual(process_info[5],
                         str(config.PROCESS_PRIORITY_NICE))  # Nice level
        self.assertEqual(process_info[6], "-")  # Priority

    # ================================= #
    #            Sandbox API            #
    # ================================= #
    def test_has_file(self):
        self.sandbox = Sandbox()
        self.assertFalse(self.sandbox.has_file("foo.txt"))
        self.assertTrue(self.sandbox.has_file("../usr/bin/timeout"))

    def test_get_file(self):
        self.sandbox = Sandbox()
        self.assertFalse(os.path.isfile("./time_binary"))
        self.sandbox.get_file("../usr/bin/timeout", "./timeout_binary")
        self.assertTrue(os.path.isfile("./timeout_binary"))
        os.remove("./timeout_binary")

    def test_put_file(self):
        self.sandbox = Sandbox()
        self.assertFalse(self.sandbox.has_file("foo.txt"))
        self.sandbox.put_file("install_steps.txt", "foo.txt")
        self.assertTrue(self.sandbox.has_file("foo.txt"))

    def test_put_file_with_permissions_write(self):
        self.sandbox = Sandbox()
        self.assertFalse(self.sandbox.has_file("foo.txt"))
        self.sandbox.put_file("install_steps.txt", "foo.txt")
        self.assertTrue(self.sandbox.has_file("foo.txt"))
        stdout, stderr = self.sandbox_helper(sandbox=self.sandbox,
                                             command="echo bar > foo.txt")
        self.assertNotEqual(stderr, "")  # No permissions to write
        self.sandbox.put_file("install_steps.txt", "foo.txt", 0o777)
        stdout, stderr = self.sandbox_helper(sandbox=self.sandbox,
                                             command="echo bar > foo.txt")
        self.assertEqual(stderr, "")  # This time has permissions
        stdout, stderr = self.sandbox_helper(sandbox=self.sandbox,
                                             command="cat foo.txt")
        self.assertEqual(stdout,
                         "bar")  # Double check by printing the file's contents

    def test_put_file_with_permissions_exec(self):
        self.sandbox = Sandbox()
        self.sandbox.put_file("/bin/ls", "ls_binary")
        stdout, stderr = self.sandbox_helper(sandbox=self.sandbox,
                                             command="./ls_binary ..")
        self.assertEqual(stderr, "")
        self.assertIn("bin", stdout)  # Should list parent folder

        self.sandbox.put_file("/bin/ls", "ls_binary", 0o766)
        stdout, stderr = self.sandbox_helper(sandbox=self.sandbox,
                                             command="./ls_binary ..")
        self.assertIn("Permission denied",
                      stderr)  # Should be unable to execute
        self.assertEqual(stdout, "")

    def test_del_file(self):
        self.sandbox = Sandbox()
        self.assertFalse(self.sandbox.has_file("foo.txt"))
        self.sandbox.put_file("install_steps.txt", "foo.txt")
        self.assertTrue(self.sandbox.has_file("foo.txt"))
        self.sandbox.del_file("foo.txt")
        self.assertFalse(self.sandbox.has_file("foo.txt"))

    def test_read_file(self):
        self.sandbox = Sandbox()
        self.sandbox.put_file("install_steps.txt", "foo.txt")
        contents = self.sandbox.read_file("foo.txt").decode()
        self.assertIn("python", contents)

    def test_execute_blocking(self):
        self.sandbox = Sandbox()
        output = TemporaryFile(mode="w+b")
        start_time = perf_counter()
        self.sandbox.execute(command="sleep 0.2 ; echo foo",
                             stdin_fd=None,
                             stdout_fd=output,
                             stderr_fd=None,
                             blocking=True)
        self.assertGreaterEqual(perf_counter() - start_time, 0.2)
        self.assertEqual(output.tell(), 4)  # Already printed "foo\n"

    def test_execute_non_blocking(self):
        self.sandbox = Sandbox()
        output = TemporaryFile(mode="w+b")
        start_time = perf_counter()
        self.sandbox.execute(command="sleep 0.2 ; echo foo",
                             stdin_fd=None,
                             stdout_fd=output,
                             stderr_fd=None,
                             blocking=False)
        self.assertLess(perf_counter() - start_time, 0.1)
        self.assertEqual(output.tell(), 0)  # Haven't yet printed anything
        sleep(0.3)
        self.assertEqual(output.tell(), 4)  # But printing it eventually

    def test_privileged_execution(self):
        self.sandbox = Sandbox()
        self.sandbox.put_file("/bin/ls", "ls_binary", 0o744)

        stdout, stderr = self.sandbox_helper(sandbox=self.sandbox,
                                             command="./ls_binary ..")
        self.assertIn("Permission denied",
                      stderr)  # Should be unable to execute
        self.assertEqual("", stdout)
        stdout, stderr = self.sandbox_helper(sandbox=self.sandbox,
                                             command="./ls_binary ..",
                                             privileged=True)
        self.assertEqual("",
                         stderr)  # But privileged user should be able to do it
        self.assertIn("bin", stdout)

    def test_privileged_deletion(self):
        self.sandbox = Sandbox()
        self.sandbox.put_file("/bin/ls", "ls_binary", 0o744)

        stdout, stderr = self.sandbox_helper(sandbox=self.sandbox,
                                             command="rm ls_binary")
        self.assertIn("Permission denied",
                      stderr)  # Should be unable to execute
        self.assertTrue(self.sandbox.has_file("ls_binary"))
        stdout, stderr = self.sandbox_helper(sandbox=self.sandbox,
                                             command="rm ls_binary",
                                             privileged=True)
        self.assertEqual("",
                         stderr)  # But privileged user should be able to do it
        self.assertFalse(self.sandbox.has_file("ls_binary"))

    # ================================= #
    #          Applied ulimits          #
    # ================================= #
    def test_output_limit(self):
        self.sandbox = Sandbox()

        file_size = 1000000  # 1MB
        output = NamedTemporaryFile(mode="w+", delete=True)
        for i in range(file_size // 10):
            output.write("test test\n")
        output.flush()
        self.sandbox.put_file(output.name, "foo.txt")

        target_size = 0
        while target_size + file_size <= config.MAX_EXECUTION_OUTPUT:
            target_size += file_size
        stdout, stderr = self.sandbox_helper(
            sandbox=self.sandbox,
            command="for i in {{1..{}}}; do cat foo.txt; done;".format(
                target_size // file_size))
        self.assertEqual("", stderr)
        self.assertEqual(len(stdout), target_size - 1)

        target_size += file_size
        stdout, stderr = self.sandbox_helper(
            sandbox=self.sandbox,
            command="for i in {{1..{}}}; do cat foo.txt; done;".format(
                target_size // file_size))
        self.assertIn("File size limit exceeded", stderr)
        self.assertEqual(len(stdout), config.MAX_EXECUTION_OUTPUT)

    def test_no_input_limit(self):
        self.sandbox = Sandbox()

        file_size = 50000000  # 50MB
        output = NamedTemporaryFile(mode="w+", delete=True)
        message = "Without IT I'm just espr\n"
        for i in range(file_size // len(message)):
            output.write(message)
        output.flush()
        self.sandbox.put_file(output.name, "foo.txt")

        stdout, stderr = self.sandbox_helper(
            sandbox=self.sandbox, command="wc -c < foo.txt && wc -l < foo.txt")
        self.assertEqual("", stderr)
        self.assertEqual(len(stdout.splitlines()), 2)
        self.assertEqual(int(stdout.splitlines()[0]), file_size)
        self.assertEqual(int(stdout.splitlines()[1]),
                         file_size // len(message))

    @mock.patch("config.MAX_EXECUTION_TIME", 0.5)
    def test_hard_timeout(self):
        start_time = perf_counter()
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="sleep 3; echo foo")
        self.assertEqual("", stdout)
        self.assertEqual("", stderr)
        exec_time = perf_counter() - start_time
        self.assertGreaterEqual(exec_time, 0.5)
        self.assertLess(exec_time, 0.7)

    @mock.patch("config.MAX_EXECUTION_TIME", 1.0)
    def test_fork_bomb(self):
        # Run to see if it crashes the system and how many processes it spawns
        start_time = perf_counter()

        stdout, stderr = TemporaryFile(mode="w+"), TemporaryFile(mode="w+")
        self.sandbox = Sandbox()
        self.sandbox.execute(command=":(){ :|:& };:",
                             stdin_fd=None,
                             stdout_fd=stdout,
                             stderr_fd=stderr,
                             blocking=False)

        # Check number of processes by this worker and its CPU usage continuously
        # (but sleep for 0.01 seconds so we don't do it more than 100 times)
        iteration = 0
        max_cpu = 0.0
        max_processes = 0
        while perf_counter() - start_time < config.MAX_EXECUTION_TIME:
            if iteration % 2 == 0:
                ps_info = os.popen("ps -U {}".format(
                    self.sandbox._worker.name)).read()
                max_processes = max(max_processes,
                                    len(ps_info.splitlines()) - 1)
            else:
                cpu_info = os.popen(
                    "top -b -n 1 -u {} | awk 'NR>7 {{ sum += $9; }} END {{ print sum; }}'"
                    .format(self.sandbox._worker.name)).read()
                max_cpu = max(max_cpu, float(cpu_info))
            iteration += 1
            sleep(0.01)

        self.assertLess(perf_counter() - start_time, 1.2)
        self.assertLessEqual(max_processes, config.MAX_PROCESSES)
        self.assertLessEqual(max_cpu, 100.0)

        stdout.seek(0)
        self.assertEqual("", stdout.read())
        stderr.seek(0)
        self.assertIn("fork: retry: Resource temporarily unavailable",
                      stderr.read())

        # At this point the sandbox is still running (as the fork bomb processes are detached)
        # Make sure that wait() kills it entirely
        self.assertTrue(self.sandbox.is_running())
        self.sandbox.wait(0.1)
        self.assertFalse(self.sandbox.is_running())

    @mock.patch("config.MAX_EXECUTION_TIME", 0.3)
    def test_sandbox_wait_kills_sleepers(self):
        stdout, stderr = TemporaryFile(mode="w+"), TemporaryFile(mode="w+")
        self.sandbox = Sandbox()
        self.sandbox.execute(command=":(){ :|:& };:",
                             stdin_fd=None,
                             stdout_fd=stdout,
                             stderr_fd=stderr,
                             blocking=False)

        # While the program is within its time limit it is at max processes
        sleep(0.2)
        self.assertTrue(self.sandbox.is_running())
        ps_info = os.popen("ps -U {}".format(self.sandbox._worker.name)).read()
        self.assertEqual(len(ps_info.splitlines()) - 1, config.MAX_PROCESSES)

        # What's worse, even after that they are still alive
        # (as they don't use much CPU, so are not affected by MAX_EXECUTION_TIME)
        sleep(0.2)
        self.assertTrue(self.sandbox.is_running())
        ps_info = os.popen("ps -U {}".format(self.sandbox._worker.name)).read()
        self.assertEqual(len(ps_info.splitlines()) - 1, config.MAX_PROCESSES)

        # However, wait() should terminate everything
        self.sandbox.wait(0.1)
        self.assertFalse(self.sandbox.is_running())
        ps_info = os.popen("ps -U {}".format(self.sandbox._worker.name)).read()
        self.assertEqual(len(ps_info.splitlines()) - 1, 0)

    def test_cpu_usage(self):
        self.sandbox = Sandbox()
        self.sandbox.put_file(os.path.join(self.PATH_FIXTURES, "factor.py"))
        stdout, stderr = self.sandbox_helper(
            sandbox=self.sandbox,
            command="/usr/bin/time --format '%U %P' pypy3 factor.py")
        self.assertIn("1000000000000037", stdout)
        self.assertEqual(len(stderr.splitlines()), 1)
        user_time = float(stderr.split()[0])
        percent_cpu = int(stderr.split()[1].split('%')[0])
        # Took at least half a second
        self.assertGreater(user_time, 0.5)
        # CPU usage was around 100%
        self.assertTrue(95 < percent_cpu <= 100)

    def test_memory_usage_heap(self):
        self.sandbox = Sandbox()
        self.sandbox.put_file(
            os.path.join(self.PATH_FIXTURES, "mem_allocator.cpp"))
        self.sandbox_helper(
            sandbox=self.sandbox,
            command=
            "g++ -O2 -std=c++17 -w -s -o mem_allocator mem_allocator.cpp",
            privileged=True)
        self.assertTrue(self.sandbox.has_file("mem_allocator"))

        command = "/usr/bin/time --quiet --format='%M' /bin/bash -c \"./mem_allocator heap {}\"" +\
                  " ; code=$? ; >&2 echo $code ; exit $code"

        targets = [10000000, 100000000, 500000000, 1000000000,
                   2000000000]  # 10MB, 100MB, 500MB, 1GB, 2GB
        for target in targets:
            stdout, stderr = self.sandbox_helper(
                sandbox=self.sandbox, command=command.format(target))
            exit_code, exec_memory = int(stderr.splitlines()[-1]), int(
                stderr.splitlines()[-2])
            self.assertEqual(exit_code, 0)
            self.assertTrue(target <= exec_memory * 1024 <= target +
                            5000000)  # Up to 5MB overhead for C++ libraries

        # Twenty megabytes less than the threshold is okay
        target = config.MAX_EXECUTION_MEMORY - 20000000
        stdout, stderr = self.sandbox_helper(sandbox=self.sandbox,
                                             command=command.format(target))
        exit_code, exec_memory = int(stderr.splitlines()[-1]), int(
            stderr.splitlines()[-2])
        self.assertEqual(exit_code, 0)
        self.assertTrue(target <= exec_memory * 1024 <= target +
                        5000000)  # Up to 5MB overhead for C++ libraries

        # Allocating around the threshold is no longer okay
        target = config.MAX_EXECUTION_MEMORY
        stdout, stderr = self.sandbox_helper(sandbox=self.sandbox,
                                             command=command.format(target))
        exit_code, exec_memory = int(stderr.splitlines()[-1]), int(
            stderr.splitlines()[-2])
        self.assertNotEqual(exit_code, 0)
        self.assertTrue(
            exec_memory * 1024 <= config.MAX_EXECUTION_MEMORY + 1024)

    def test_memory_usage_stack(self):
        self.sandbox = Sandbox()
        self.sandbox.put_file(
            os.path.join(self.PATH_FIXTURES, "mem_allocator.cpp"))
        self.sandbox_helper(
            sandbox=self.sandbox,
            command=
            "g++ -O2 -std=c++17 -w -s -o mem_allocator mem_allocator.cpp",
            privileged=True)
        self.assertTrue(self.sandbox.has_file("mem_allocator"))

        command = "/usr/bin/time --quiet --format='%M' /bin/bash -c \"./mem_allocator stack {}\"" +\
                  " ; code=$? ; >&2 echo $code ; exit $code"

        # Test different target stack sizes (should all be okay)
        targets = [1000000, 10000000, 50000000]  # 1MB, 10MB, 50MB
        for target in targets:
            stdout, stderr = self.sandbox_helper(
                sandbox=self.sandbox, command=command.format(target))
            exit_code, exec_memory = int(stderr.splitlines()[-1]), int(
                stderr.splitlines()[-2])
            self.assertEqual(exit_code, 0)
            self.assertTrue(target <= exec_memory * 1024 <= target +
                            5000000)  # Up to 5MB overhead for C++ libraries

        # Half a megabyte less than the threshold is okay
        target = config.MAX_EXECUTION_STACK - 500000
        stdout, stderr = self.sandbox_helper(sandbox=self.sandbox,
                                             command=command.format(target))
        exit_code, exec_memory = int(stderr.splitlines()[-1]), int(
            stderr.splitlines()[-2])
        self.assertEqual(exit_code, 0)
        self.assertTrue(target <= exec_memory * 1024 <= target +
                        5000000)  # Up to 5MB overhead for C++ libraries

        # Half a megabyte more than the threshold is not okay
        target = config.MAX_EXECUTION_STACK + 500000
        stdout, stderr = self.sandbox_helper(sandbox=self.sandbox,
                                             command=command.format(target))
        exit_code, exec_memory = int(stderr.splitlines()[-1]), int(
            stderr.splitlines()[-2])
        self.assertNotEqual(exit_code, 0)
        self.assertTrue(target <= exec_memory * 1024 <= target +
                        5000000)  # Up to 5MB overhead for C++ libraries

    # ================================= #
    #      High-level prerequisites     #
    # ================================= #
    def test_languages_are_available(self):
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="g++")
        self.assertIn("g++: fatal error: no input files", stderr)

        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="java")
        self.assertIn("Usage: java [options]", stderr)

        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="javac")
        self.assertIn("Usage: javac <options> <source files>", stdout)

        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(), command="jar")
        self.assertIn("Usage: jar [OPTION...]", stderr)

        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command="pypy3 --version")
        self.assertIn("Python 3.", stdout)

    def test_time_command_available(self):
        stdout, stderr = self.sandbox_helper(
            sandbox=Sandbox(), command="ls -la /usr/bin | grep -w time")
        self.assertEqual("", stderr)
        self.assertTrue(stdout.startswith("-rwx") and stdout.endswith("time"))

        command = "/usr/bin/time --quiet --format='%U %S %e %M' /bin/bash -c 'sleep 0.33; echo foo'" + \
                  " ; code=$? ; >&2 echo $code ; exit $code"
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command=command)
        self.assertEqual(stdout, "foo")
        self.assertEqual(int(stderr.splitlines()[-1]), 0)  # Exit code
        self.assertLess(float(stderr.splitlines()[-2].split()[0]),
                        0.1)  # User time
        self.assertLess(float(stderr.splitlines()[-2].split()[1]),
                        0.1)  # Kernel time
        self.assertAlmostEqual(float(stderr.splitlines()[-2].split()[2]),
                               0.33,
                               delta=0.1)  # Clock time

    def test_timeout_command_available(self):
        stdout, stderr = self.sandbox_helper(
            sandbox=Sandbox(), command="ls -la /usr/bin | grep -w timeout")
        self.assertEqual("", stderr)
        self.assertTrue(
            stdout.startswith("-rwx") and stdout.endswith("timeout"))

        start_time = perf_counter()
        command = "/usr/bin/timeout 0.3s /bin/bash -c 'sleep 1.0; echo foo'"
        stdout, stderr = self.sandbox_helper(sandbox=Sandbox(),
                                             command=command)
        self.assertEqual(stdout, "")  # No output, killed before that
        self.assertLess(perf_counter() - start_time,
                        0.5)  # Killed shortly after the timeout

    # Up to MAX_PARALLEL_WORKERS run in parallel.
    # We expect if we run less than or equal to MAX_PARALLEL_WORKERS to take clock time equal to the longest
    # of them. If we run even a single one more, we expect the clock time to be roughly twice as long.
    def dummy_sleep_helper(self):
        start_time = perf_counter()
        sandbox = Sandbox()
        waiting_time = perf_counter() - start_time
        self.sandbox_helper(sandbox=sandbox, command="sleep 0.5 ; echo foo")
        return waiting_time

    def test_workers_under_limit(self):
        start_time = perf_counter()

        pool = ThreadPoolExecutor(max_workers=config.MAX_PARALLEL_WORKERS)
        futures = [
            pool.submit(self.dummy_sleep_helper)
            for _ in range(config.MAX_PARALLEL_WORKERS)
        ]

        # Each of the processes runs in ~0.5s, but since we schedule them through a thread pool
        # we should reach this point much earlier.
        self.assertLess(perf_counter() - start_time, 0.1)

        # Wait for all workers to complete and get the maximum waiting time for a Sandbox object
        max_waiting_time = 0.0
        for future in futures:
            max_waiting_time = max(max_waiting_time, future.result())

        # Expecting none of the workers to wait for another one to finish, this get a Sandbox object immediately
        self.assertLess(max_waiting_time, 0.2)
        # Waiting all of them to complete takes at least 0.5 seconds
        self.assertGreaterEqual(perf_counter() - start_time, 0.5)
        # But not much more than 0.5 seconds
        self.assertLess(perf_counter() - start_time, 0.7)

    def test_workers_over_limit(self):
        start_time = perf_counter()

        pool = ThreadPoolExecutor(max_workers=config.MAX_PARALLEL_WORKERS + 1)
        futures = [
            pool.submit(self.dummy_sleep_helper)
            for _ in range(config.MAX_PARALLEL_WORKERS + 1)
        ]

        # Each of the processes runs in ~0.5s, but since we schedule them through a thread pool
        # we should reach this point much earlier. One of the threads should be blocked on waiting
        # for a sandbox, though.
        self.assertLess(perf_counter() - start_time, 0.1)

        # Wait for all workers to complete and get the maximum waiting time for a Sandbox object
        max_waiting_time = 0.0
        for future in futures:
            max_waiting_time = max(max_waiting_time, future.result())

        # Expecting one of the workers to wait for another one to finish before getting a Sandbox object
        self.assertGreaterEqual(max_waiting_time, 0.5)
        self.assertLess(max_waiting_time, 0.7)
        # Waiting all of them to complete takes at least 1 second (twice as much)
        self.assertGreaterEqual(perf_counter() - start_time, 1.0)
        # But not much more than 1 second
        self.assertLess(perf_counter() - start_time, 1.4)
Exemplo n.º 9
0
def execute_two_player_game(submit_id, test: TestInfo, run_config: RunConfig,
                            player_one_executable_path,
                            player_two_executable_path) -> RunResult:

    log_file = "interaction.log"
    tester_executable_name = "tester." + run_config.tester_path.split(".")[-1]
    player_one_executable_name = "solution1." + player_one_executable_path.split(
        ".")[-1]
    player_two_executable_name = "solution2." + player_two_executable_path.split(
        ".")[-1]

    args = {
        "tester_run_command":
        Runner.get_run_command(
            language=common.get_language_by_exec_name(tester_executable_name),
            executable=tester_executable_name,
            memory_limit=config.MAX_TESTER_MEMORY),
        "player_one_run_command":
        Runner.get_run_command(language=common.get_language_by_exec_name(
            player_one_executable_name),
                               executable=player_one_executable_name,
                               memory_limit=run_config.memory_limit),
        "player_two_run_command":
        Runner.get_run_command(language=common.get_language_by_exec_name(
            player_two_executable_name),
                               executable=player_two_executable_name,
                               memory_limit=run_config.memory_limit),
        "tester_timeout":
        config.MAX_GAME_LENGTH,
        "solution_timeout":
        run_config.timeout,
        "time_limit":
        run_config.time_limit,
        "memory_limit":
        run_config.memory_limit,
        "log_file":
        log_file
    }

    empty_file = NamedTemporaryFile(mode="w+t", delete=False)
    empty_file_path = os.path.abspath(empty_file.name)

    # Copy all the needed files to the sandbox directory:
    #   1) wrapper.py
    #   2) the tester executable
    #   3) player one's solution executable
    #   4) player two's solution executable
    #   5) the input file (read by the tester)
    #   6) game log (empty file with write permissions)

    sandbox = Sandbox()
    sandbox.put_file(os.path.join(config.ROOT_DIR, "wrapper.py"))
    sandbox.put_file(run_config.tester_path,
                     target_name=tester_executable_name)
    sandbox.put_file(player_one_executable_path,
                     target_name=player_one_executable_name)
    sandbox.put_file(player_two_executable_path,
                     target_name=player_two_executable_name)
    sandbox.put_file(test.inpPath, target_name="input.txt", mode=0o777)
    sandbox.put_file(empty_file_path, target_name=log_file, mode=0o777)

    # Run the executable, while measuring CPU and Memory consumption
    run_result = Runner.run_program(
        sandbox=sandbox,
        executable_path=os.path.join(config.ROOT_DIR, "interactor_2P.py"),
        memory_limit=config.MAX_EXECUTION_MEMORY,
        timeout=config.MAX_GAME_LENGTH,
        input_bytes=json.dumps(args, indent=4, sort_keys=True).encode())
    interaction_log = sandbox.read_file(log_file)
    del sandbox

    # Record the game log to the /replays folder
    replay_id = save_replay_log(interaction_log)

    # The interactor crashed or got killed
    # Don't check memory limit, as it can be caused by child processes (including solution)
    if run_result.exec_time >= config.MAX_GAME_LENGTH:
        message = "Interactor took too much time to complete ({:.3f}s).".format(
            run_result.exec_time)
        logger.error("Submit {id} | {message}".format(id=submit_id,
                                                      message=message))
        return RunResult(status=TestStatus.INTERNAL_ERROR,
                         error=message,
                         replay_id=replay_id)
    if run_result.exit_code != 0:
        message = "Interactor exited with non-zero exit code ({}).".format(
            run_result.exit_code)
        logger.error("Submit {id} | {message}".format(id=submit_id,
                                                      message=message))
        return RunResult(status=TestStatus.INTERNAL_ERROR,
                         error=message,
                         replay_id=replay_id)

    # Parse the response from the interactor
    try:
        results = json.loads(run_result.output.decode(config.OUTPUT_ENCODING))
    except ValueError:
        message = "Could not decode interactor's output: {}!".format(
            run_result.output.decode())
        logger.error("Submit {id} | {message}".format(id=submit_id,
                                                      message=message))
        return RunResult(status=TestStatus.INTERNAL_ERROR,
                         error=message,
                         replay_id=replay_id)

    # print("RESULTS:\n{}".format(json.dumps(results, indent=4, sort_keys=True)))

    # Okay, let's assume the interactor was okay. Now check if the tester crashed.
    if results["internal_error"]:
        message = "Tester crashed or some other internal error happened. Result from interactor:\n{}".format(
            json.dumps(results, indent=4, sort_keys=True))
        logger.error("Submit {id} | {message}".format(id=submit_id,
                                                      message=message))
        return RunResult(status=TestStatus.INTERNAL_ERROR,
                         error=message,
                         replay_id=replay_id)

    # Everything with the system seems okay.
    # Leave the caller function decide what the test status will be
    return RunResult(status=TestStatus.ACCEPTED,
                     output=run_result.output,
                     replay_id=replay_id)
Exemplo n.º 10
0
def execute_interactive(submit_id, test: TestInfo,
                        run_config: RunConfig) -> RunResult:
    log_file = "interaction.log"
    tester_executable_name = "tester." + run_config.tester_path.split(".")[-1]
    solution_executable_name = "solution." + run_config.executable_path.split(
        ".")[-1]

    # Both the tester and the solution get extra time in order to account for time tester
    # processes input/output. The tester gets additional time to make sure it can print
    # the log and results properly after the solution completes its execution.
    tester_communication_time = min(1.0, run_config.time_limit * 2)
    tester_postprocessing_time = 1.0
    solution_timeout = run_config.timeout + tester_communication_time
    tester_timeout = solution_timeout + tester_postprocessing_time

    args = {
        "tester_timeout":
        tester_timeout,
        "solution_timeout":
        solution_timeout,
        "tester_run_command":
        Runner.get_run_command(
            language=common.get_language_by_exec_name(tester_executable_name),
            executable=tester_executable_name,
            memory_limit=config.MAX_TESTER_MEMORY),
        "solution_run_command":
        Runner.get_run_command(language=common.get_language_by_exec_name(
            solution_executable_name),
                               executable=solution_executable_name,
                               memory_limit=run_config.memory_limit),
        "log_file":
        log_file
    }
    # print(json.dumps(args, indent=4, sort_keys=True))

    empty_file = NamedTemporaryFile(mode="w+t", delete=False)
    empty_file_path = os.path.abspath(empty_file.name)

    # Copy all the needed files to the sandbox directory:
    #   1) wrapper.py
    #   2) the tester executable
    #   3) the solution executable
    #   4) the input file (read by the tester)
    #   5) game log (empty file with write permissions)

    sandbox = Sandbox()
    sandbox.put_file(os.path.join(config.ROOT_DIR, "wrapper.py"))
    sandbox.put_file(run_config.tester_path,
                     target_name=tester_executable_name)
    sandbox.put_file(run_config.executable_path,
                     target_name=solution_executable_name)
    sandbox.put_file(test.inpPath, target_name="input.txt", mode=0o777)
    sandbox.put_file(empty_file_path, target_name=log_file, mode=0o777)

    # Run the executable, while measuring CPU and Memory consumption
    run_result = Runner.run_program(
        sandbox=sandbox,
        executable_path=os.path.join(config.ROOT_DIR, "interactor_1P.py"),
        memory_limit=config.MAX_EXECUTION_MEMORY,
        timeout=config.MAX_GAME_LENGTH,
        input_bytes=json.dumps(args, indent=4, sort_keys=True).encode())
    interaction_log = sandbox.read_file(log_file)
    del sandbox

    # Record the game log to the /replays folder
    replay_id = save_replay_log(interaction_log)

    # The interactor crashed or got killed
    # Don't check memory limit, as it can be caused by child processes (including solution)
    if run_result.exec_time >= config.MAX_GAME_LENGTH:
        message = "Interactor took too much time to complete ({:.3f}s).".format(
            run_result.exec_time)
        logger.error("Submit {id} | {message}".format(id=submit_id,
                                                      message=message))
        return RunResult(status=TestStatus.INTERNAL_ERROR,
                         error=message,
                         replay_id=replay_id)
    if run_result.exit_code != 0:
        message = "Interactor exited with non-zero exit code ({}).".format(
            run_result.exit_code)
        logger.error("Submit {id} | {message}".format(id=submit_id,
                                                      message=message))
        return RunResult(status=TestStatus.INTERNAL_ERROR,
                         error=message,
                         replay_id=replay_id)

    # Parse the response from the interactor
    try:
        results = json.loads(run_result.output.decode(config.OUTPUT_ENCODING))
    except ValueError:
        message = "Could not decode interactor's output: {}!".format(
            run_result.output.decode())
        logger.error("Submit {id} | {message}".format(id=submit_id,
                                                      message=message))
        return RunResult(status=TestStatus.INTERNAL_ERROR,
                         error=message,
                         replay_id=replay_id)

    # print("RESULTS:\n{}".format(json.dumps(results, indent=4, sort_keys=True)))

    # Okay, let's assume the interactor was okay. Now check if the tester crashed.
    if results["internal_error"] or results["tester_exit_code"] != 0:
        message = "Tester crashed or some other internal error happened. Result from interactor:\n{}".format(
            json.dumps(results, indent=4, sort_keys=True))
        logger.error("Submit {id} | {message}".format(id=submit_id,
                                                      message=message))
        return RunResult(status=TestStatus.INTERNAL_ERROR,
                         error=message,
                         replay_id=replay_id)

    # Everything with the system seems okay.
    # Get the score and the solution's exit_code, exec_time, and exec_memory
    exit_code = int(results["solution_exit_code"])
    # Calculate final time and memory (offset for VM start-up time)
    solution_language = common.get_language_by_exec_name(
        run_config.executable_path)
    exec_time = max(
        0,
        float(results["solution_exec_time"]) -
        common.get_time_offset(solution_language))
    exec_memory = max(
        0,
        float(results["solution_exec_memory"]) -
        common.get_memory_offset(solution_language))
    # Get tester's output
    output = ("" if "tester_message" not in results else
              results["tester_message"]).encode()

    # Leave the caller function decide what the test status will be
    return RunResult(exit_code=exit_code,
                     exec_time=exec_time,
                     exec_memory=exec_memory,
                     output=output,
                     replay_id=replay_id)