+6

Phần kết: Xây dựng hệ thống thi lập trình trực tuyến

image.png

Mở đầu

Sau hai phần trước em, chưa hài lòng về hệ thống hiện tại em tiếp tục đi tìm các hướng đi mới. Thật sự có các open source của mảng này làm rất ổn áp rồi, chủ yếu là mục đích học tập nên em vẫn muốn làm lại cái "bánh xe" 😂😂.

Open source: RemoteCodeCompiler

An online code compiler supporting 11 programming languages (Java, Kotlin, Scala, C, C++, C#, Golang, Python, Ruby, Rust and Haskell) for competitive programming and coding interviews.

Đây là đoạn giới thiệu của nó trên github. Nôm na nó là 1 open source được viết bằng Java dùng để compiler code online.

Sau một hồi tìm kiếm em thấy thằng này có vẻ phù hợp tiêu chí không code giới hạn tài nguyên bằng C, và hoạt động cũng đơn giản.

Cách hoạt động

image.png

  1. Khi gửi file code lên nó sẽ khởi tạo 1 container để compiler (ở thời điểm em đọc mấy tháng trước nó không có cái này, không biết mục đích nó bổ sung thêm thằng này làm gì?)
  2. Sau khi compiler xong nó sẽ khởi tạo 1 container dùng để chạy file vừa được compiler và phản hồi lại kết quả.

Chi tiết cách họ compiler và giới hạn tài nguyên

Thay vì dùng C thì họ dùng bash script để làm điều đó. Cụ thể khi ta upload file input, output và các thông số giới hạn thời gian, bộ nhớ họ sẽ tạo 1 file entrypoint.sh với nội dung như sau:

#!/usr/bin/env bash
rename=${compiler.rename}
compile=${compiler.compile}
if [ "$rename" = true ];
then
    mv ${compiler.defaultName} ${compiler.fileName}
fi
if [ "$compile" = true ];
then
    ${compiler.compilationCommand} 1> /dev/null
    ret=$?
    if [ $ret -ne 0 ];
    then
        exit ${compiler.compilationErrorStatusCode}
    fi
fi
ulimit -s ${compiler.memoryLimit}
timeout -s SIGTERM ${compiler.timeLimit} ${compiler.executionCommand}
exit $?
  • Giải thích:
    • Các ${} là template khi họ generate các chỗ này sẽ được truyền giá trị vào.
    • rename = true thì sẽ đổi tên bằng command mv
    • compile = true thì sẽ chạy command compiler tương ứng của ngôn ngữ đó (tại vì một số ngôn ngữ không cần compiler)
    • Sử dụng command ulimit để giới hạn bộ nhớ, command timeout để giới hạn thời gian
    • Dựa vào exit code để phân biệt các lỗi runtime, compiler, vượt quá thời gian hay bộ nhớ.

Vậy công việc còn lại là chạy file entrypoint.sh này bằng các ngôn ngữ lập trình bắt exit code nếu không lỗi thì so sánh với đáp án đúng.

Triển khai ý tưởng

1. Viết code generate file entrypoint

Với template string của js thì việc tạo ra file entrypoint ez =))


const generateEntryPointFile = async(compiler, path) => {
    const content = 
    
    `#!/usr/bin/env bash
    rename=${compiler.rename}
    compile=${compiler.compile}
    if [ "$rename" = true ];
    then
        mv ${compiler.defaultName} ${compiler.fileName}
    fi
    if [ "$compile" = true ];
    then
        ${compiler.compilationCommand} 1> /dev/null
        ret=$?
        if [ $ret -ne 0 ];
        then
            exit ${compiler.compilationErrorStatusCode}
        fi
    fi
    ulimit -s ${compiler.memoryLimit}
    timeout -s SIGTERM ${compiler.timeLimit} ${compiler.executionCommand}
    exit $?`

    await fs.writeFile(`${path}/entrypoint.sh`, content);
}

2. Định nghĩa các command compiler

ENUM_COMPILATION_CMD : Các comand compiler

ENUM_EXCECUTION_CMD: Các command để chạy code

ENUM_MESSAGE: exit code tương ứng với ý nghĩa.

Ví dụ sau này muốn update thì chỉ việc bổ sung command vào 😂

const ENUM_COMPILATION_CMD = {
    "c": "gcc main.c  -o exec",
    "cpp": "g++ main.cpp  -o exec",
    "java": "javac main.java",
    "python": "",
    "js": ""
}

const ENUM_EXCECUTION_CMD = {
    "c": "./exec",
    "cpp": "./exec",
    "java": "java Main",
    "python" : "python3 main.py",
    "js": "node main.js"
}

const ENUM_MESSAGE = {
    "0": "Accepted",
    "1": "Wrong Answer",
    "139" : "Out Of Memory",
    "124": "Time Limit Exceeded",
    "96": "Compilation Error"
}

3. Tạo các object compiler truyền vào hàm generate entrypoint

const compiler = (input, memoryLimit, timeLimit, lang) => {
    let compile = true;
    let executionCommand = ENUM_EXCECUTION_CMD[lang];
    if(ENUM_COMPILATION_CMD[lang] == "") compile = false;

    let compiler = {
        rename: false,
        compile,
        compilationCommand: ENUM_COMPILATION_CMD[lang],
        compilationErrorStatusCode: 96,
        executionCommand,
        memoryLimit,
        timeLimit,
    }

    return compiler;
}

4. Chạy container và lấy kết quả

--cpus=1: giới hạn cpu của container

--rm: Sau khi chạy xong thì cho bay màu

const runContainer = (name, folder) => {
  return new Promise(async (resolve, reject) => {
    const cmdBuildImage = `docker build -t ${name} ${folder}`;
    const cmdRunContainer = `docker run --cpus=1 --rm ${name}`;

    const { stdout, stderr } = await exec(cmdBuildImage);

    if (stdout.includes("Successfully")) {
      const ls = spawn("docker", ["run", "--cpus=1", "--rm", name]);

      let result = {};

      ls.stdout.on("data", (data) => {
        result.output = data.toString();
      });

      ls.stderr.on("data", (data) => {
        result.stack = data.toString();
      });

      ls.on("close", (code) => {
        console.log(`child process close all stdio with code ${code}`);
      });

      ls.on("exit", (code) => {
        result.code = code;
        resolve(result);
      });
    }
  });
};

Một số vấn đề khi triển khai

1. Việc so sánh kết quả

Ta đọc ouput của container và dùng hàm đọc file để so sánh kết quả nó không good cho lắm, nên sau em lưu đáp án đúng vào redis giúp tốc độ đọc nhanh hơn.

SET output:id
{
    "01": "1 2 3"
    "02": "1 2 3 4 5",
    "03": "1"
}

Sau đó em quyết định so sánh luôn trong file entrypoint.sh

raw=$(ulimit -s ${compiler.memoryLimit} && timeout -s SIGTERM ${compiler.timeLimit} ${compiler.executionCommand} < "${inputFileName}")
ans=$(cat "out1.txt")
if [ "$raw" == "$ans" ]

2. Việc chạy nhiều test đồng thời

Thay vì phải khởi mỗi container cho 1 test thì em chuyển hết file inputoutput vào 1 container rồi dùng for của bash để chạy

count=$(find . -name "in*.txt" |wc -l)`;
for i in \`seq 1 $count\`
do
  run_code $i $1
done

sau đó viết hàm run_code đối số truyền vào số thứ tự của file input

run_code(){
  start=$(date +%s.%N)
  raw=$(ulimit -s ${compiler.memoryLimit} && timeout -s SIGTERM ${compiler.timeLimit} ${compiler.executionCommand} < "${inputFileName}")
  code=$?
  if [ $code -eq 0 ]
  then
    ans=$(cat "out\${1}.txt")
    ...
    
    if [ "$raw" == "$ans" ]
    then
     ...
      return 1
    else
      ...
      return 1
    fi
  else
    ...
  fi
  printf '%s|' "$JSON"
}

Vấn đề: Nếu chạy vòng lặp thì nó sẽ bị chạy "tuần tự" (mỗi test để giới hạn 2s thì 10 cái test nếu xui mất 20s) vậy ta cần chạy nó "đồng thời"

Google thì solution là thêm & vào để nó chạy "đồng thời"

count=$(find . -name "in*.txt" |wc -l)`;
for i in \`seq 1 $count\`
do
  run_code $i $1 &
done

3. Nhập từ bàn phím với javascript

Việc nhập vào từ bàn phím với ngôn ngữ js thì ta có thể sử dụng readline nhưng em để ý một số thằng ví dụ như leetcode nó không cần người dùng có thằng này mà nó vẫn hiểu ảo ma vch =))

//Leetcode
function solution(a, b){
// code here
}

Vấn đề: Như trên ta chỉ cần viết code vào trong function, việc truyền biến vào thì leetcode sẽ lo. Làm sao để có thể truyền động các đối số như họ ?

Ban đầu em nghĩ đến việc for các đối số rồi generate các hàm readline, nhưng biết sao được các đối số là kiểu dữ liệu nào để parse cho đúng (vì input ghi trong file text thì nó sẽ auto kiểu string). Rồi còn kiểu mảng, kiểu object truyền kiểu gì ?

Trường hợp này file text không chơi được với js vậy mình dùng json. Vậy nó sẽ có dạng

{
  "in": [2]
}

Khi dùng json thì ta có thể dùng bất kì kiểu dữ liệu nào ta thích number, string, array, object.

Vậy truyền nó vào hàm solution kiểu gì ? Đáp dòng này vào cuối file code js của người dùng là được 😂

process.stdin.resume();
process.stdin.setEncoding("utf8");

process.stdin.on("data", function (input) {
    let data = JSON.parse(input);
    solution.apply(null, data.in);
});

Kết

  • Hệ thống có vẻ hoạt động đúng yêu cầu đề ra.
  • Nó chưa hoàn hảo như em kỳ vọng vì em muốn giới hạn thêm một số tài nguyên nữa nhưng không thể. Chắc có lẽ đấy là lý do người ta dùng C để giới hạn tài nguyên.
  • Hành trình theo đổi cái hệ thống này em cũng học được nhiều cái hay 😂😂

À còn thằng domjudge cũng sịn 😄


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí