diff --git a/Dockerfile.alplne b/Dockerfile.alplne new file mode 100644 index 00000000..aab39a5a --- /dev/null +++ b/Dockerfile.alplne @@ -0,0 +1,93 @@ +FROM alpine:latest + +ENV PYTHONUNBUFFERED=1 + +# Switch to edge repository for latest packages +RUN echo "https://dl-cdn.alpinelinux.org/alpine/edge/main" > /etc/apk/repositories && \ + echo "https://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \ + echo "https://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories + +# Install basic tools and build dependencies in smaller chunks +RUN set -ex && \ + apk add --no-cache gcc g++ make musl-dev curl wget tar gzip + +RUN set -ex && \ + apk add --no-cache libffi-dev openssl-dev zlib-dev bzip2-dev xz-dev readline-dev sqlite-dev + +RUN set -ex && \ + apk add --no-cache autoconf bison yaml-dev gdbm-dev ncurses-dev git bash ca-certificates + +RUN set -ex && \ + apk add --no-cache chromium lsof iptables ip6tables + +# Install available packages from Alpine Edge +RUN set -ex && \ + apk add --no-cache \ + openjdk21 \ + ruby \ + php82 php82-cli php82-common php82-phar php82-json php82-openssl php82-curl php82-dev \ + go mono + +# Install Kotlin manually +ARG KOTLIN_VERSION=2.1.0 +RUN cd /opt && \ + wget https://github.com/JetBrains/kotlin/releases/download/v${KOTLIN_VERSION}/kotlin-compiler-${KOTLIN_VERSION}.zip && \ + unzip kotlin-compiler-${KOTLIN_VERSION}.zip && \ + mv kotlinc /opt/kotlin && \ + ln -s /opt/kotlin/bin/kotlin /usr/local/bin/kotlin && \ + ln -s /opt/kotlin/bin/kotlinc /usr/local/bin/kotlinc && \ + rm kotlin-compiler-${KOTLIN_VERSION}.zip + + +RUN apk add --no-cache nodejs npm +ENV PATH="/opt/node/bin:$PATH" +RUN npm install -g typescript ts-node + +RUN apk add --no-cache python3 + +# Create symbolic links and fix paths +RUN ln -sf python3 /usr/bin/python && \ + ln -sf pip3 /usr/bin/pip && \ + ln -s /usr/bin/php82 /usr/bin/php + +# Remove unneeded GCC internals to save space +RUN set -ex && \ + find /usr/libexec/gcc -name "cc1obj" -delete 2>/dev/null || true && \ + find /usr/libexec/gcc -name "lto1" -delete 2>/dev/null || true && \ + find /usr/libexec/gcc -name "lto-wrapper" -delete 2>/dev/null || true && \ + rm -f /usr/bin/x86_64-alpine-linux-musl-gcj 2>/dev/null || true + +# Copy application files +COPY . /usr/bin/ +COPY start.sh /usr/bin/ + +# Install npm dependencies +RUN npm --prefix /usr/bin/ install + +# Create non-root user for security +RUN addgroup -S -g 2000 runner && \ + adduser -S -D -u 2000 -s /sbin/nologin -h /tmp -G runner runner + +# Set up environment variables for all languages +ENV PATH="/opt/node/bin:/opt/swift/usr/bin:$PATH" +ENV JAVA_HOME="/usr/lib/jvm/java-21-openjdk" +ENV GOPATH="/tmp/go" +ENV GOCACHE="/tmp/go-cache" + +EXPOSE 8080 + +# Health check to verify installations +RUN echo "=== Language Versions ===" && \ + echo "Node: $(node --version)" && \ + echo "Python: $(python --version)" && \ + echo "Java: $(java --version | head -1)" && \ + echo "Ruby: $(ruby --version)" && \ + echo "Go: $(go version)" && \ + echo "PHP: $(php --version | head -1)" && \ + echo "TypeScript: $(tsc --version)" && \ + echo "C#: $(mono --version) | head -1" && \ + echo "Kotlin: $(kotlin -version)" && \ + echo "========================" + +# USER runner +CMD sh /usr/bin/start.sh diff --git a/configs/language.config.js b/configs/language.config.js index 23d77bb8..79e3a177 100644 --- a/configs/language.config.js +++ b/configs/language.config.js @@ -1,4 +1,4 @@ -const { CPP, C, PYTHON, JAVA, NODEJS, RUBY, PROMPTV1, PROMPTV2, PROMPTV3 } = require('../enums/supportedLanguages') +const { CPP, C, PYTHON, JAVA, NODEJS, RUBY, GO, PHP, TYPESCRIPT, CSHARP, KOTLIN, PROMPTV1, PROMPTV2, PROMPTV3 } = require('../enums/supportedLanguages') const ONE_MB = 1024 // ulimit uses Kilobyte as base unit const ALLOWED_RAM = process.env.ALLOWED_RAM || 512 @@ -45,6 +45,42 @@ const LANGUAGES_CONFIG = { filename: 'solution.rb', memory: ALLOWED_RAM * ONE_MB, }, + [GO]: { + compile: 'go build -o solution solution.go', + run: './solution', + timeout: 4, + filename: 'solution.go', + memory: 1024 * ONE_MB, // Go programs can be memory-intensive, so we allocate 1GB + }, + [PHP]: { + compile: 'php -l solution.php', + run: 'php solution.php', + timeout: 8, + filename: 'solution.php', + memory: ALLOWED_RAM * ONE_MB, + }, + [TYPESCRIPT]: { + compile: 'tsc solution.ts --outDir .', + run: 'node solution.js', + timeout: 10, + filename: 'solution.ts', + memory: 786432, // Similar to Node.js since it compiles to JS + }, + [CSHARP]: { + compile: 'mcs -out:solution.exe solution.cs', + run: 'mono solution.exe', + timeout: 6, + filename: 'solution.cs', + memory: ALLOWED_RAM * ONE_MB * 1.5, + requiresProjectFile: false, // Not needed for Mono + }, + [KOTLIN]: { + compile: 'kotlinc solution.kt -include-runtime -d solution.jar', + run: 'java -jar solution.jar', + timeout: 6, + filename: 'solution.kt', + memory: 1024 * ONE_MB //Kotlin/JVM needs slightly more memory + }, [PROMPTV1]: { model: 'gpt-4-1106-preview', }, diff --git a/enums/supportedLanguages.js b/enums/supportedLanguages.js index 3e4a817a..40702063 100644 --- a/enums/supportedLanguages.js +++ b/enums/supportedLanguages.js @@ -5,6 +5,11 @@ module.exports = { JAVA: 'java', NODEJS: 'nodejs', RUBY: 'ruby', + GO: 'go', + PHP: 'php', + TYPESCRIPT: 'typescript', + CSHARP: 'csharp', + KOTLIN: 'kotlin', PROMPTV1: 'promptv1', PROMPTV2: 'promptv2', PROMPTV3: 'promptv3', diff --git a/services/code.service.js b/services/code.service.js index cf93b08d..08781ce6 100644 --- a/services/code.service.js +++ b/services/code.service.js @@ -1,3 +1,4 @@ + /* globals gc */ const util = require('util') const exec = util.promisify(require('child_process').exec) @@ -228,11 +229,12 @@ const _executeCode = async (req, res, response) => { // Check if there is no compilation error if (response.compileMessage === '') { let command - if (language === 'java') { + if (language === 'java' || language === 'kotlin') { // Remove ulimit as a temp fix - command = `cd /tmp/ && timeout ${langConfig.timeout}s ${langConfig.run}` + command = `cd /tmp/ && /usr/bin/time -f "%M" -o /tmp/memory_report.txt timeout ${langConfig.timeout}s ${langConfig.run}` } else { - command = `cd /tmp/ && ulimit -v ${langConfig.memory} && ulimit -m ${langConfig.memory} && timeout ${langConfig.timeout}s ${langConfig.run}` + // Execute command with memory limits and resource monitoring for non-Java languages + command = `cd /tmp/ && ulimit -v ${langConfig.memory} && ulimit -m ${langConfig.memory} && /usr/bin/time -f "%M" -o /tmp/memory_report.txt timeout ${langConfig.timeout}s ${langConfig.run}` } // Check if there is any input that is to be provided to code execution @@ -244,14 +246,35 @@ const _executeCode = async (req, res, response) => { } const outputLog = await _runScript(command, res, true) + + let memoryKB = null + try { + const path = '/tmp/memory_report.txt' + await fs.promises.access(path, fs.constants.F_OK) + const memoryReport = await fs.promises.readFile(path, 'utf8') + memoryKB = parseInt(memoryReport.trim(), 10) + } catch (err) { + console.error(`Memory report not found or failed to read: ${err.message}`) + } + + if (memoryKB) { + response.memory = memoryKB + } else { + response.memory = null; + } + + console.log('Memory used:', response.memory); + response.output = outputLog.error !== undefined ? _prepareErrorMessage(outputLog, language, command) : outputLog.result.stdout + if (outputLog.error) { response.error = 1 } - } else { + } + else { response.error = 1 } } catch (e) { @@ -399,7 +422,7 @@ const _getAiScore = async (langConfig, question, response, points, userAnswer, r const _executeStatement = (db, sql) => { return new Promise((resolve, reject) => { - db.all(sql, function(err, rows) { + db.all(sql, function (err, rows) { if (err) { reject(err); } else { @@ -887,14 +910,14 @@ const _postCleanUp = async (type, staticServerInstance = undefined, jasmineServe await _cleanUpDir(appConfig.multifile.workingDir, appConfig.multifile.submissionFileDownloadPath) switch (type) { case FRONTEND_STATIC_JASMINE: - if(staticServerInstance) { + if (staticServerInstance) { staticServerInstance.close(() => { logger.info('Exiting static server in post cleanup') }) } break case FRONTEND_REACT_JASMINE: - if(jasmineServer) { + if (jasmineServer) { logger.info('Exiting react setup server in post cleanup') process.kill(-jasmineServer.pid) } @@ -917,7 +940,7 @@ const _executeMultiFile = async (req, res, response) => { let result if (req?.non_editable_files) { const isValidSubmission = await _checkIntegrity(req.non_editable_files) - if(!isValidSubmission) throw new Error(`A non editable file has been modified, exiting...`) + if (!isValidSubmission) throw new Error(`A non editable file has been modified, exiting...`) } switch (req.type) { case FRONTEND_STATIC_JASMINE: diff --git a/tests/data/testJson.js b/tests/data/testJson.js index 823b5b35..51480db1 100644 --- a/tests/data/testJson.js +++ b/tests/data/testJson.js @@ -1,18 +1,20 @@ + const testCases = [ { name: 'cpp : hello world', reqObject: { language: 'cpp', - script: - '#include\n' + - 'using namespace std;\n' + - 'int main(){\n' + - ' cout << "hello world";\n' + - 'return 0;\n' + - '}\n', + script: ` +#include +using namespace std; +int main(){ + cout << "hello world"; + return 0; +}`, }, expectedResponse: { val: 'hello world', + approxMemoryUses: 2744, status: 200, error: 0, }, @@ -21,20 +23,21 @@ const testCases = [ name: 'cpp : print stdin', reqObject: { language: 'cpp', - script: - '#include\n\n' + - 'using namespace std;\n' + - 'int main(){\n\n' + - ' int a;\n' + - ' while(cin >> a){\n' + - ' cout << a << endl;\n' + - ' }\n' + - ' return 0;\n\n' + - '}\n', + script: ` +#include +using namespace std; +int main(){ + int a; + while(cin >> a){ + cout << a << endl; + } + return 0; +}`, stdin: '1 2 3', }, expectedResponse: { val: '1\n2\n3\n', + approxMemoryUses: 2680, status: 200, error: 0, }, @@ -44,10 +47,11 @@ const testCases = [ name: 'nodejs : hello world', reqObject: { language: 'nodejs', - script: 'console.log(\'hello world\')', + script: `console.log('hello world')`, }, expectedResponse: { val: 'hello world\n', + approxMemoryUses: 44540, status: 200, error: 0, }, @@ -56,16 +60,16 @@ const testCases = [ name: 'nodejs : print stdin', reqObject: { language: 'nodejs', - script: - 'process.stdin.setEncoding(\'utf8\'); \n ' + - 'process.stdin.on(\'data\', (input) => { \n ' + - ' console.log(input); \n ' + - ' \n ' + - '}); \n ', + script: ` +process.stdin.setEncoding('utf8'); +process.stdin.on('data', (input) => { +console.log(input.trim()); +});`, stdin: '1 2 3', }, expectedResponse: { val: '1 2 3\n', + approxMemoryUses: 44888, status: 200, error: 0, }, @@ -74,10 +78,12 @@ const testCases = [ name: 'python : hello world', reqObject: { language: 'python', - script: 'print(\'hello world\')', + script: + `print('hello world')`, }, expectedResponse: { val: 'hello world\n', + approxMemoryUses: 7344, status: 200, error: 0, }, @@ -86,19 +92,15 @@ const testCases = [ name: 'python : print stdin', reqObject: { language: 'python', - script: - 'try:\n' + - ' while(True):\n' + - ' line = input()\n' + - ' if not line:\n' + - ' break\n' + - ' print(line)\n' + - 'except EOFError:\n' + - ' pass', + script: ` +import sys +for line in sys.stdin: + print(line.strip())`, stdin: '1 2 3', }, expectedResponse: { val: '1 2 3\n', + approxMemoryUses: 7228, status: 200, error: 0, }, @@ -108,14 +110,15 @@ const testCases = [ reqObject: { language: 'c', script: - '#include\n\n' + - 'int main(){\n\n' + - ' printf("hello world");\n' + - ' return 0;\n' + - '}\n', + `#include +int main(){ +printf("hello world"); +return 0; +} `, }, expectedResponse: { val: 'hello world', + approxMemoryUses: 900, status: 200, error: 0, }, @@ -124,37 +127,40 @@ const testCases = [ name: 'c : print stdin', reqObject: { language: 'c', - script: - '#include \n' + - 'int main() {\n' + - ' int number;\n' + - ' while (scanf("%d", &number) == 1) {\n' + - ' printf("%d\\n", number);\n' + - ' } \n' + - ' return 0;\n' + - '}', + script: ` +#include +int main() { + int number; + while (scanf("%d", &number) == 1) { + printf("%d\\n", number); +} + return 0; +} `, stdin: '1 2 3', }, expectedResponse: { val: '1\n2\n3\n', + approxMemoryUses: 924, status: 200, error: 0, }, }, { - name: 'java : print stdin', + name: 'java : hello world', reqObject: { language: 'java', script: - 'import java.util.Scanner;\n' + - 'public class Solution {\n' + - ' public static void main(String[] args) {\n' + - ' System.out.println("hello world");\n' + - ' }\n' + - '}\n', + ` +import java.util.Scanner; +public class Solution { + public static void main(String[] args) { + System.out.println("hello world"); + } +}`, }, expectedResponse: { val: 'hello world\n', + approxMemoryUses: 33000, status: 200, error: 0, }, @@ -163,22 +169,23 @@ const testCases = [ name: 'java : print stdin', reqObject: { language: 'java', - script: - 'import java.util.Scanner;\n' + - 'public class Solution {\n' + - ' public static void main(String[] args) {\n' + - ' Scanner scanner = new Scanner(System.in);\n' + - ' while (scanner.hasNextInt()) {\n' + - ' int number = scanner.nextInt();\n' + - ' System.out.println(number);\n' + - ' } \n' + - ' scanner.close();\n' + - ' }\n' + - '}\n', + script: ` +import java.util.Scanner; +public class Solution { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + while (scanner.hasNextInt()) { + int number = scanner.nextInt(); + System.out.println(number); + } + scanner.close(); + } +} `, stdin: '1 2 3', }, expectedResponse: { val: '1\n2\n3\n', + approxMemoryUses: 35900, status: 200, error: 0, }, @@ -188,10 +195,11 @@ const testCases = [ reqObject: { language: 'ruby', script: - 'print "hello world"' + `print "hello world"`, }, expectedResponse: { val: 'hello world', + approxMemoryUses: 11790, status: 200, error: 0, }, @@ -200,13 +208,14 @@ const testCases = [ name: 'ruby : print stdin', reqObject: { language: 'ruby', - script: - 'user_input = gets.chomp\n' + - 'puts user_input', - stdin: '10\n' + script: ` +user_input = gets.chomp +puts user_input`, + stdin: '10', }, expectedResponse: { val: '10\n', + approxMemoryUses: 11800, status: 200, error: 0, }, @@ -215,7 +224,8 @@ const testCases = [ name: 'TLE test', reqObject: { language: 'nodejs', - script: 'for(let i=0 ; ; ){i++}', + script: + `for(let i = 0 ;; ) { i++ } `, }, expectedResponse: { val: 'Time limit exceeded', @@ -227,7 +237,8 @@ const testCases = [ name: 'MLE test', reqObject: { language: 'python', - script: 'one_gb_data = bytearray(1000 * 1024 * 1024)', + script: + `one_gb_data = bytearray(1000 * 1024 * 1024)`, }, expectedResponse: { val: 'Memory limit exceeded', @@ -239,19 +250,19 @@ const testCases = [ name: 'MLE test 2', reqObject: { language: 'python', - script: - 'import time\n' + - 'def consume_memory(target_mb, duration_sec):\n' + - ' float_size = 8\n' + - ' floats_per_mb = (1024 * 1024) // float_size\n' + - ' total_floats = target_mb * floats_per_mb\n' + - ' iterations = int(duration_sec / 0.1)\n' + - ' floats_per_iteration = total_floats // iterations\n' + - ' memory_hog = []\n' + - ' for _ in range(iterations):\n' + - ' memory_hog.extend([0.0] * floats_per_iteration)\n' + - ' time.sleep(0.1)\n' + - 'consume_memory(1000, 1)\n', + script: ` +import time +def consume_memory(target_mb, duration_sec): + float_size = 8 + floats_per_mb = (1024 * 1024) // float_size + total_floats = target_mb * floats_per_mb + iterations = int(duration_sec / 0.1) + floats_per_iteration = total_floats // iterations + memory_hog = [] + for _ in range(iterations): + memory_hog.extend([0.0] * floats_per_iteration) + time.sleep(0.1) +consume_memory(1000, 1)`, }, expectedResponse: { val: 'Memory limit exceeded', @@ -263,10 +274,11 @@ const testCases = [ name: 'MLE test 3', reqObject: { language: 'python', - script: - 'a = [100]\n' + - 'for i in a:\n' + - ' a.append(i)\n', + script: ` +a = [100] +for i in a: + a.append(i) + `, }, expectedResponse: { val: 'Memory limit exceeded', @@ -298,6 +310,147 @@ const testCases = [ error: 0, }, }, + { + name: 'c :Heap memory allocation', + reqObject: { + language: 'c', + script: ` +#include +#include +int main() { + size_t memory_size = 50 * 1024 * 1024; + char *memory_block = malloc(memory_size); + if (memory_block == NULL) { + printf("Failed to allocate memory\\n"); + return 1; + } + printf("Memory allocation done\\n"); + for (size_t i = 0; i < memory_size; i += 4096) { + memory_block[i] = (char)(i % 256); + } + printf("Memory touched and initialized\\n"); + free(memory_block); + printf("Memory freed\\n"); + return 0; +} `, + }, + expectedResponse: { + val: 'Memory allocation done\nMemory touched and initialized\nMemory freed\n', + status: 200, + error: 0, + approxMemoryUses: 51000, + }, + }, + { + name: 'c :Stack memory allocation', + reqObject: { + language: 'c', + script: ` +#include +#include +#define ONE_MB (1024 * 1024) +void stack_allocate(int remaining_bytes, int depth) { + if (remaining_bytes <= 0) { + printf("Memory allocated on stack\\n"); + return; + } + char buffer[ONE_MB]; + memset(buffer, 0, ONE_MB); // Touch the memory + stack_allocate(remaining_bytes - ONE_MB, depth + 1); +} +int main() { + stack_allocate(6 * ONE_MB, 0); + return 0; +} `, + }, + expectedResponse: { + val: 'Memory allocated on stack\n', + status: 200, + error: 0, + approxMemoryUses: 6500, // Max stack uses limit is ~10 MB + }, + }, + { + name: "Go: Basic Hello World", + reqObject: { + language: "go", + script: ` +package main +import "fmt" + +func main() { + fmt.Println("Language: Go") +}` + }, + expectedResponse: { + val: "Language: Go\n", + status: 200, + error: 0 + } + }, + { + name: "PHP: Basic Hello World", + reqObject: { + language: "php", + script: ` +` + }, + expectedResponse: { + val: "\nLanguage: PHP", + status: 200, + error: 0 + } + }, + { + name: "TypeScript: Basic Hello World", + reqObject: { + language: "typescript", + script: ` +function main(): void { + console.log("Language: TypeScript"); +} + +main();` + }, + expectedResponse: { + val: "Language: TypeScript\n", + status: 200, + error: 0 + } + }, + { + name: "C#: Basic Hello World", + reqObject: { + language: "csharp", + script: ` +using System; +class Solution { + static void Main(string[] args) { + Console.WriteLine("Language: C#"); + } +}` + }, + expectedResponse: { + val: "Language: C#\n", + status: 200, + error: 0 + } + }, + { + name: "Kotlin: Basic Hello World", + reqObject: { + language: "kotlin", + script: ` +fun main() { + println("Language: Kotlin") +}` + }, + expectedResponse: { + val: "Language: Kotlin\n", + status: 200, + error: 0 + } + } ] module.exports = { testCases } diff --git a/tests/test.js b/tests/test.js index 7cd19621..5321549f 100644 --- a/tests/test.js +++ b/tests/test.js @@ -1,9 +1,13 @@ + const axios = require('axios') const { testCases } = require('./data/testJson') const { describe, expect, it } = require('@jest/globals') const ENDPOINT = process.env.ENDPOINT || 'http://localhost:3000/api/execute/' +// Memory comparison tolerance in percent (e.g., 20 means ±20%) +const MEMORY_TOLERANCE_PERCENT = 20 + describe('Tests', () => { for (const testCase of testCases) { it(testCase.name, async () => { @@ -15,6 +19,13 @@ describe('Tests', () => { expect(response.data.output.points).toBeDefined() } else { expect(response).toHaveProperty('data.output', testCase.expectedResponse.val) + if (testCase.expectedResponse.approxMemoryUses) { + const actual = response.data.memory + const expected = testCase.expectedResponse.approxMemoryUses + const tolerance = (MEMORY_TOLERANCE_PERCENT / 100) * expected + expect(actual).toBeGreaterThanOrEqual(expected - tolerance, `Memory usage should be within tolerance (min: ${expected - tolerance})`) + expect(actual).toBeLessThanOrEqual(expected + tolerance, `Memory usage should be within tolerance (max: ${expected + tolerance})`) + } } expect(response).toHaveProperty('status', testCase.expectedResponse.status) expect(response).toHaveProperty('data.error', testCase.expectedResponse.error)