Рубрики
Без рубрики

Как разработать онлайн-компилятор кода с использованием Java и Docker

В этом руководстве мы рассмотрим обзор того, как создать простой и эффективный онлайн-код… Помечено как java, docker, бессерверное программирование.

В этом руководстве мы рассмотрим обзор того, как создать простой и эффективный онлайн-компилятор кода для конкурентного программирования и собеседований по кодированию, используя Java (Весенняя загрузка) и Docker .

Перед началом работы вы можете ознакомиться с исходным кодом проекта по следующей ссылке Перед началом работы вы можете ознакомиться с исходным кодом проекта по следующей ссылке

Почему Java и Docker?

Что ж, для java это личный выбор, мне действительно нравится работать с этим языком, но вы можете выбрать любой другой язык программирования, который вам больше нравится.

Чтобы отделить среды выполнения от разных исходных кодов (чтобы избежать вредоносного кода, влияющего на вашу машину) и ограничить ресурсы, нам нужно использовать виртуальные машины или контейнеры, но в чем разница между этими двумя вариантами?

Виртуальная машина против контейнера

Виртуальная машина (VM) – это эмуляция компьютерной системы. Проще говоря, это позволяет запускать то, что кажется множеством отдельных компьютеров, на оборудовании, которое на самом деле является одним компьютером.

С помощью контейнеров вместо виртуализации базового компьютера, такого как виртуальная машина (VM), виртуализируется только операционная система.

Контейнеры располагаются поверх физического сервера и его основной операционной системы — обычно Linux или Windows. Каждый контейнер совместно использует ядро хост-ОС, а также, как правило, двоичные файлы и библиотеки. Общие компоненты доступны только для чтения.

Итак, теперь ясно, что использование контейнеров – лучший выбор для нас, поскольку они легче виртуальных машин, а docker – наиболее используемое решение для контейнеризации, вот почему я выбрал docker для этого проекта.

API-интерфейс

Во-первых, нам нужно разработать API, допустим, мы хотим предоставить онлайн-компилятор для 4 языков программирования (Java, C, C++ и Python). Таким образом, API должен выглядеть примерно так: четыре контроллера, один для Java, один для C, один для C++ и еще один для Python. Вызов этих контроллеров осуществляется через POST-запросы к следующим URL-адресам:

  • localhost:8080/компилятор/ java
  • локальный хост:8080/компилятор/ c
  • localhost:8080/компилятор/ cpp
  • localhost:8080/компилятор/ python

В качестве входных данных мы ожидаем 5 полей:

  • результат : ожидаемый результат.
  • Исходный код : исходный код на java, c, c++ или python.
  • ограничение по времени : ограничение по времени в секундах, которое исходный код не должен превышать во время его выполнения (должно составлять от 0 до 15 секунд).
  • ограничение памяти :: ограничение памяти в Мб, которое исходный код не должен превышать во время его выполнения (должно быть от 0 до 1000 МБ).
  • входной файл : входные данные, записанные в отдельные строки (необязательно).
// Python Compiler
    @RequestMapping(
            value = "python",
            method = RequestMethod.POST
    )
    public ResponseEntity compile_python(@RequestPart(value = "output", required = true) MultipartFile output,
                                            @RequestPart(value = "sourceCode", required = true) MultipartFile sourceCode,
                                            @RequestParam(value = "inputFile", required = false) MultipartFile inputFile,
                                            @RequestParam(value = "timeLimit", required = true) int timeLimit,
                                            @RequestParam(value = "memoryLimit", required = true) int memoryLimit
    ) throws Exception {
        return compiler(output, sourceCode, inputFile, timeLimit, memoryLimit, Langage.Python);
    }

    // C Compiler
    @RequestMapping(
            value = "c",
            method = RequestMethod.POST
    )
    public ResponseEntity compile_c(@RequestPart(value = "output", required = true) MultipartFile output,
                                          @RequestPart(value = "sourceCode", required = true) MultipartFile sourceCode,
                                          @RequestParam(value = "inputFile", required = false) MultipartFile inputFile,
                                          @RequestParam(value = "timeLimit", required = true) int timeLimit,
                                          @RequestParam(value = "memoryLimit", required = true) int memoryLimit
    ) throws Exception {
        return compiler(output, sourceCode, inputFile, timeLimit, memoryLimit, Langage.C);
    }

    // C++ Compiler
    @RequestMapping(
            value = "cpp",
            method = RequestMethod.POST
    )
    public ResponseEntity compile_cpp(@RequestPart(value = "output", required = true) MultipartFile output,
                                            @RequestPart(value = "sourceCode", required = true) MultipartFile sourceCode,
                                            @RequestParam(value = "inputFile", required = false) MultipartFile inputFile,
                                            @RequestParam(value = "timeLimit", required = true) int timeLimit,
                                            @RequestParam(value = "memoryLimit", required = true) int memoryLimit
    ) throws Exception {
        return compiler(output, sourceCode, inputFile, timeLimit, memoryLimit, Langage.Cpp);
    }

    // Java Compiler
    @RequestMapping(
            value = "java",
            method = RequestMethod.POST
    )
    public ResponseEntity compile_java(@RequestPart(value = "output", required = true) MultipartFile output,
                                          @RequestPart(value = "sourceCode", required = true) MultipartFile sourceCode,
                                          @RequestParam(value = "inputFile", required = false) MultipartFile inputFile,
                                          @RequestParam(value = "timeLimit", required = true) int timeLimit,
                                          @RequestParam(value = "memoryLimit", required = true) int memoryLimit
    ) throws Exception {
        return compiler(output, sourceCode, inputFile, timeLimit, memoryLimit, Langage.Java);
    }

Какого типа ответа следует ожидать пользователю?

Что ж, давайте уделим этому вопросу минутку. Если вы занимаетесь конкурентным программированием на таких платформах, как Codeforces, Leetcode или других, вы можете увидеть, что существует 6 типов вердиктов ( Принято , Неправильный ответ , Ошибка компиляции , Ошибка времени выполнения , Ошибка нехватки памяти , и Превышен лимит времени/| ).

Компиляция исходного кода внутри контейнера docker

Идея здесь состоит в том, чтобы взять исходный код, предоставленный пользователем, и создать образ docker в зависимости от языка, а затем запустить контейнер этого образа для компиляции и выполнения исходного кода. В зависимости от возвращаемого кода контейнера мы можем вынести вердикт, который мы обсуждали ранее, но нам нужно убедиться, что контейнер не превышает лимит времени, чтобы избежать бесконечных выполнений, ни лимит памяти (чтобы избежать вредоносного кода).

// Compile method
    private ResponseEntity compiler(
            MultipartFile output,
            MultipartFile sourceCode,
            MultipartFile inputFile,
            int timeLimit,
            int memoryLimit,
            Langage langage
    ) throws Exception {

        String folder = "utility";
        String file = "main";
        if(langage == Langage.C) {
            folder += "_c";
            file += ".c";
        } else if(langage == Langage.Java) {
            file += ".java";
        } else if(langage == Langage.Cpp) {
            folder += "_cpp";
            file += ".cpp";
        } else {
            folder += "_py";
            file += ".py";
        }

        if(memoryLimit < 0 || memoryLimit > 1000)
            return ResponseEntity
                    .badRequest()
                    .body("Error memoryLimit must be between 0Mb and 1000Mb");

        if(timeLimit < 0 || timeLimit > 15)
            return ResponseEntity
                    .badRequest()
                    .body("Error timeLimit must be between 0 Sec and 15 Sec");

        LocalDateTime date = LocalDateTime.now();

        createEntrypointFile(sourceCode, inputFile, timeLimit, memoryLimit, langage);

        logger.info("entrypoint.sh file has been created");

        saveUploadedFiles(sourceCode, folder + "/" + file);
        saveUploadedFiles(output, folder + "/" + output.getOriginalFilename());
        if(inputFile != null)
            saveUploadedFiles(inputFile, folder + "/" + inputFile.getOriginalFilename());
        logger.info("Files have been uploaded");

        String imageName = "compile" + new Date().getTime();

        logger.info("Building the docker image");
        String[] dockerCommand = new String[] {"docker", "image", "build", folder, "-t", imageName};
        ProcessBuilder probuilder = new ProcessBuilder(dockerCommand);
        Process process = probuilder.start();
        int status = process.waitFor();
        if(status == 0)
            logger.info("Docker image has been built");
        else
            logger.info("Error while building image");

        logger.info("Running the container");
        dockerCommand = new String[] {"docker", "run", "--rm", imageName};
        probuilder = new ProcessBuilder(dockerCommand);
        process = probuilder.start();
        status = process.waitFor();
        logger.info("End of the execution of the container");

        BufferedReader outputReader = new BufferedReader(new InputStreamReader(output.getInputStream()));
        StringBuilder outputBuilder = new StringBuilder();
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
        StringBuilder builder = new StringBuilder();

        boolean ans = runCode(outputReader, outputBuilder, reader, builder);

        String result = builder.toString();

        // delete files
        deleteFile(folder, file);
        new File(folder + "/" + output.getOriginalFilename()).delete();
        if(inputFile != null)
            new File(folder + "/" + inputFile.getOriginalFilename()).delete();
        logger.info("files have been deleted");

        String statusResponse = statusResponse(status, ans);

        logger.info("Status response is " + statusResponse);

        return ResponseEntity
                .status(HttpStatus.OK)
                .body(new Response(builder.toString(), outputBuilder.toString(), statusResponse, ans, date));
    }

Точка входа в контейнер

Чтобы настроить точку входа образа docker, мы создаем во время выполнения файл точки входа, который содержит ограничение по времени, ограничение по памяти и команду выполнения.

Это пример того, как сгенерировать entrypoint.sh файл для выполнения на java:

// create Java entrypoint.sh file
    private void createJavaEntrypointFile(String fileName, int timeLimit, int memoryLimit, MultipartFile inputFile) {
        String executionCommand = inputFile == null
                ? "timeout --signal=SIGTERM " + timeLimit + " java " + fileName.substring(0,fileName.length() - 5) + "\n"
                : "timeout --signal=SIGTERM " + timeLimit + " java " + fileName.substring(0,fileName.length() - 5) + " < " + inputFile.getOriginalFilename() + "\n";
        String content = "#!/usr/bin/env bash\n" +
                "mv main.java " + fileName+ "\n" +
                "javac " + fileName + "\n" +
                "ret=$?\n" +
                "if [ $ret -ne 0 ]\n" +
                "then\n" +
                "  exit 2\n" +
                "fi\n" +
                "ulimit -s " + memoryLimit + "\n" +
                 executionCommand +
                "exit $?\n";
        OutputStream os = null;
        try {
            os = new FileOutputStream(new File("utility/entrypoint.sh"));
            os.write(content.getBytes(), 0, content.length());
        } catch (IOException e) {
            e.printStackTrace();
        }finally{
            try {
                os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

Разговоры дешевы, покажи мне код 😃

Это был просто обзор, вместо того, чтобы объяснять, как все это кодировать, я предлагаю вам перейти по ссылке моего репозитория Github https://github.com/zakariamaaraki/RemoteCodeCompiler

Если у вас есть какие-либо вопросы, пожалуйста, не стесняйтесь задавать их!

Оригинал: “https://dev.to/zakariamaaraki/how-to-develop-an-online-code-compiler-using-java-and-docker-1gpi”