diff --git a/backend/Readme.md b/backend/Readme.md index 7b017c7..fc4150f 100644 --- a/backend/Readme.md +++ b/backend/Readme.md @@ -20,7 +20,7 @@ Monaco is a secure, containerized code execution backend service designed to run user-submitted code in multiple programming languages. It features a job queue system to manage execution resources, containerized execution environments for security, and a RESTful API for submission and monitoring. **Key Features:** -- Multi-language support (Python, Java, C, C++) +- Multi-language support (Python, JavaScript, Go, Java, C, C++) - Secure containerized execution using Docker - Resource limiting to prevent abuse - Job queuing for managing concurrent executions @@ -34,10 +34,10 @@ Monaco is a secure, containerized code execution backend service designed to run Monaco follows a layered architecture with the following key components: -1. **HTTP Handlers** (handler package) - Processes incoming HTTP requests -2. **Execution Service** (service package) - Manages code execution in containers -3. **Job Queue** (queue package) - Controls concurrent execution -4. **Data Models** (model package) - Defines data structures +1. **HTTP Handlers** (internal/api/handlers) - Processes incoming HTTP requests +2. **Execution Service** (internal/executor) - Manages code execution in containers +3. **Job Queue** (internal/queue) - Controls concurrent execution +4. **Data Models** (internal/models) - Defines data structures ### Request Flow @@ -60,10 +60,12 @@ Client Request → HTTP Handlers → Execution Service → Job Queue → Docker ### Prerequisites -- Go 1.22+ +- Go 1.22+ - Docker Engine - Docker images for supported languages: - `python:3.9` + - `node:18-alpine` + - `golang:1.22-alpine` - `eclipse-temurin:11-jdk-alpine` - `gcc:latest` @@ -82,7 +84,7 @@ Client Request → HTTP Handlers → Execution Service → Job Queue → Docker 3. Build the application: ```bash - go build -o monaco main.go + go build -o monaco ./cmd/server ``` 4. Run the service: @@ -103,7 +105,7 @@ Submits code for execution. **Request Body:** ```json { - "language": "python", // Required: "python", "java", "c", or "cpp" + "language": "python", // Required: "python", "javascript", "go", "java", "c", or "cpp" "code": "print('Hello, World!')", // Required: source code to execute "input": "optional input string" // Optional: input to stdin } @@ -127,7 +129,7 @@ Checks the status of a submission. **Response:** ```json { - "id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1", + "id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1", "status": "completed", // "pending", "queued", "running", "completed", "failed" "queuedAt": "2025-03-25T14:30:00Z", "startedAt": "2025-03-25T14:30:01Z", // Only present if status is "running", "completed", or "failed" @@ -256,6 +258,17 @@ The queue tracks and reports: - **Input Handling**: Direct stdin piping - **Limitations**: No file I/O, no package imports outside standard library +### JavaScript +- **Version**: Node.js 18 (Alpine) +- **Input Handling**: File-based input redirection +- **Limitations**: No file I/O, no package imports outside standard library + +### Go +- **Version**: Go 1.22 (Alpine) +- **Compilation**: Standard Go build process +- **Input Handling**: Direct stdin piping +- **Limitations**: No file I/O, no external dependencies + ### Java - **Version**: Java 11 (Eclipse Temurin) - **Class Detection**: Extracts class name from code using regex diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go new file mode 100644 index 0000000..26030db --- /dev/null +++ b/backend/cmd/server/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "log" + "net/http" + "os" + "time" + + "github.com/arnab-afk/monaco/internal/api" +) + +func main() { + // Configure logging + log.SetFlags(log.LstdFlags | log.Lshortfile) + log.SetOutput(os.Stdout) + + log.Println("Starting Monaco code execution backend...") + + // Initialize router with all routes + router := api.SetupRoutes() + + // Start the server + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + server := &http.Server{ + Addr: ":" + port, + Handler: router, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 120 * time.Second, + } + + log.Printf("Server started at :%s", port) + log.Fatal(server.ListenAndServe()) +} diff --git a/backend/examples/examples.md b/backend/examples/examples.md new file mode 100644 index 0000000..e951c69 --- /dev/null +++ b/backend/examples/examples.md @@ -0,0 +1,155 @@ +# Monaco Code Execution Examples + +This document provides examples of code submissions for each supported language. + +## Python + +```json +{ + "language": "python", + "code": "name = input('Enter your name: ')\nprint(f'Hello, {name}!')\nfor i in range(5):\n print(f'Count: {i}')", + "input": "World" +} +``` + +Expected output: +``` +Enter your name: Hello, World! +Count: 0 +Count: 1 +Count: 2 +Count: 3 +Count: 4 +``` + +## JavaScript + +```json +{ + "language": "javascript", + "code": "const readline = require('readline');\nconst rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout\n});\n\nrl.question('Enter your name: ', (name) => {\n console.log(`Hello, ${name}!`);\n for (let i = 0; i < 5; i++) {\n console.log(`Count: ${i}`);\n }\n rl.close();\n});", + "input": "World" +} +``` + +Expected output: +``` +Enter your name: Hello, World! +Count: 0 +Count: 1 +Count: 2 +Count: 3 +Count: 4 +``` + +## Go + +```json +{ + "language": "go", + "code": "package main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\nfunc main() {\n\tfmt.Print(\"Enter your name: \")\n\treader := bufio.NewReader(os.Stdin)\n\tname, _ := reader.ReadString('\\n')\n\tname = strings.TrimSpace(name)\n\tfmt.Printf(\"Hello, %s!\\n\", name)\n\tfor i := 0; i < 5; i++ {\n\t\tfmt.Printf(\"Count: %d\\n\", i)\n\t}\n}", + "input": "World" +} +``` + +Expected output: +``` +Enter your name: Hello, World! +Count: 0 +Count: 1 +Count: 2 +Count: 3 +Count: 4 +``` + +## Java + +```json +{ + "language": "java", + "code": "import java.util.Scanner;\n\npublic class Main {\n public static void main(String[] args) {\n Scanner scanner = new Scanner(System.in);\n System.out.print(\"Enter your name: \");\n String name = scanner.nextLine();\n System.out.println(\"Hello, \" + name + \"!\");\n for (int i = 0; i < 5; i++) {\n System.out.println(\"Count: \" + i);\n }\n scanner.close();\n }\n}", + "input": "World" +} +``` + +Expected output: +``` +Enter your name: Hello, World! +Count: 0 +Count: 1 +Count: 2 +Count: 3 +Count: 4 +``` + +## C + +```json +{ + "language": "c", + "code": "#include \n\nint main() {\n char name[100];\n printf(\"Enter your name: \");\n scanf(\"%s\", name);\n printf(\"Hello, %s!\\n\", name);\n for (int i = 0; i < 5; i++) {\n printf(\"Count: %d\\n\", i);\n }\n return 0;\n}", + "input": "World" +} +``` + +Expected output: +``` +Enter your name: Hello, World! +Count: 0 +Count: 1 +Count: 2 +Count: 3 +Count: 4 +``` + +## C++ + +```json +{ + "language": "cpp", + "code": "#include \n#include \n\nint main() {\n std::string name;\n std::cout << \"Enter your name: \";\n std::cin >> name;\n std::cout << \"Hello, \" << name << \"!\" << std::endl;\n for (int i = 0; i < 5; i++) {\n std::cout << \"Count: \" << i << std::endl;\n }\n return 0;\n}", + "input": "World" +} +``` + +Expected output: +``` +Enter your name: Hello, World! +Count: 0 +Count: 1 +Count: 2 +Count: 3 +Count: 4 +``` + +## Testing with cURL + +You can test these examples using cURL: + +```bash +curl -X POST http://localhost:8080/submit \ + -H "Content-Type: application/json" \ + -d '{ + "language": "python", + "code": "name = input(\"Enter your name: \")\nprint(f\"Hello, {name}!\")\nfor i in range(5):\n print(f\"Count: {i}\")", + "input": "World" + }' +``` + +This will return a submission ID: + +```json +{ + "id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1" +} +``` + +You can then check the status and result: + +```bash +curl http://localhost:8080/status?id=6423259c-ee14-c5aa-1c90-d5e989f92aa1 +``` + +```bash +curl http://localhost:8080/result?id=6423259c-ee14-c5aa-1c90-d5e989f92aa1 +``` diff --git a/backend/internal/api/handlers/handlers.go b/backend/internal/api/handlers/handlers.go new file mode 100644 index 0000000..8ce1373 --- /dev/null +++ b/backend/internal/api/handlers/handlers.go @@ -0,0 +1,209 @@ +package handlers + +import ( + "encoding/json" + "log" + "net/http" + "sync" + "time" + + "github.com/arnab-afk/monaco/internal/executor" + "github.com/arnab-afk/monaco/internal/models" +) + +// Handler manages HTTP requests for code submissions +type Handler struct { + executionService *executor.ExecutionService + mu sync.Mutex + submissions map[string]*models.CodeSubmission +} + +// NewHandler creates a new handler instance +func NewHandler() *Handler { + return &Handler{ + executionService: executor.NewExecutionService(), + submissions: make(map[string]*models.CodeSubmission), + } +} + +// SubmitHandler handles code submission requests +func (h *Handler) SubmitHandler(w http.ResponseWriter, r *http.Request) { + // Only allow POST method + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse the request body + var submission models.CodeSubmission + if err := json.NewDecoder(r.Body).Decode(&submission); err != nil { + http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest) + return + } + + // Validate the submission + if submission.Code == "" { + http.Error(w, "Code is required", http.StatusBadRequest) + return + } + if submission.Language == "" { + http.Error(w, "Language is required", http.StatusBadRequest) + return + } + + // Generate a unique ID for the submission + h.mu.Lock() + submission.ID = executor.GenerateUUID() + submission.Status = "pending" + h.submissions[submission.ID] = &submission + h.mu.Unlock() + + // Execute the code in a goroutine + go h.executionService.ExecuteCode(&submission) + + // Return the submission ID + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusAccepted) + json.NewEncoder(w).Encode(map[string]string{"id": submission.ID}) +} + +// StatusHandler handles status check requests +func (h *Handler) StatusHandler(w http.ResponseWriter, r *http.Request) { + // Only allow GET method + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Get the submission ID from the query parameters + id := r.URL.Query().Get("id") + if id == "" { + http.Error(w, "ID is required", http.StatusBadRequest) + return + } + + // Get the submission from the map + h.mu.Lock() + submission, exists := h.submissions[id] + h.mu.Unlock() + + if !exists { + http.Error(w, "Submission not found", http.StatusNotFound) + return + } + + // Return the submission status + response := map[string]interface{}{ + "id": submission.ID, + "status": submission.Status, + } + + // Add time information based on status + if !submission.QueuedAt.IsZero() { + response["queuedAt"] = submission.QueuedAt.Format(time.RFC3339) + } + if !submission.StartedAt.IsZero() { + response["startedAt"] = submission.StartedAt.Format(time.RFC3339) + } + if !submission.CompletedAt.IsZero() { + response["completedAt"] = submission.CompletedAt.Format(time.RFC3339) + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// ResultHandler handles result requests +func (h *Handler) ResultHandler(w http.ResponseWriter, r *http.Request) { + // Only allow GET method + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Get the submission ID from the query parameters + id := r.URL.Query().Get("id") + if id == "" { + http.Error(w, "ID is required", http.StatusBadRequest) + return + } + + // Get the submission from the map + h.mu.Lock() + submission, exists := h.submissions[id] + h.mu.Unlock() + + if !exists { + http.Error(w, "Submission not found", http.StatusNotFound) + return + } + + // Return the submission result + response := map[string]interface{}{ + "id": submission.ID, + "status": submission.Status, + "language": submission.Language, + "output": submission.Output, + } + + // Add error information if available + if submission.Error != "" { + response["error"] = submission.Error + } + + // Add time information + if !submission.QueuedAt.IsZero() { + response["queuedAt"] = submission.QueuedAt.Format(time.RFC3339) + } + if !submission.StartedAt.IsZero() { + response["startedAt"] = submission.StartedAt.Format(time.RFC3339) + } + if !submission.CompletedAt.IsZero() { + response["completedAt"] = submission.CompletedAt.Format(time.RFC3339) + if !submission.StartedAt.IsZero() { + response["executionTime"] = submission.CompletedAt.Sub(submission.StartedAt).Milliseconds() + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// QueueStatsHandler provides information about the job queue +func (h *Handler) QueueStatsHandler(w http.ResponseWriter, r *http.Request) { + // Only allow GET method + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Get the queue statistics + stats := h.executionService.GetQueueStats() + + // Return the queue statistics + response := map[string]interface{}{ + "queue_stats": stats, + "submissions": len(h.submissions), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} + +// HealthCheckHandler handles health check requests +func (h *Handler) HealthCheckHandler(w http.ResponseWriter, r *http.Request) { + // Only allow GET method + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Return a simple health check response + response := map[string]interface{}{ + "status": "ok", + "timestamp": time.Now().Format(time.RFC3339), + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) +} diff --git a/backend/internal/api/handlers/handlers_test.go b/backend/internal/api/handlers/handlers_test.go new file mode 100644 index 0000000..020e4f0 --- /dev/null +++ b/backend/internal/api/handlers/handlers_test.go @@ -0,0 +1,70 @@ +package handlers + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSubmitHandler(t *testing.T) { + h := NewHandler() + + // Create a test request + reqBody := map[string]string{ + "language": "python", + "code": "print('Hello, World!')", + "input": "", + } + reqJSON, _ := json.Marshal(reqBody) + req, err := http.NewRequest("POST", "/submit", bytes.NewBuffer(reqJSON)) + if err != nil { + t.Fatal(err) + } + req.Header.Set("Content-Type", "application/json") + + // Create a response recorder + rr := httptest.NewRecorder() + + // Call the handler + h.SubmitHandler(rr, req) + + // Check the status code + assert.Equal(t, http.StatusAccepted, rr.Code) + + // Check the response body + var response map[string]string + err = json.Unmarshal(rr.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Contains(t, response, "id") + assert.NotEmpty(t, response["id"]) +} + +func TestHealthCheckHandler(t *testing.T) { + h := NewHandler() + + // Create a test request + req, err := http.NewRequest("GET", "/health", nil) + if err != nil { + t.Fatal(err) + } + + // Create a response recorder + rr := httptest.NewRecorder() + + // Call the handler + h.HealthCheckHandler(rr, req) + + // Check the status code + assert.Equal(t, http.StatusOK, rr.Code) + + // Check the response body + var response map[string]interface{} + err = json.Unmarshal(rr.Body.Bytes(), &response) + assert.NoError(t, err) + assert.Equal(t, "ok", response["status"]) + assert.Contains(t, response, "timestamp") +} diff --git a/backend/internal/api/handlers/middleware.go b/backend/internal/api/handlers/middleware.go new file mode 100644 index 0000000..7712937 --- /dev/null +++ b/backend/internal/api/handlers/middleware.go @@ -0,0 +1,49 @@ +package handlers + +import ( + "log" + "net/http" + "time" +) + +// LoggingMiddleware logs HTTP requests +func LoggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() + log.Printf("[HTTP] %s %s %s", r.Method, r.URL.Path, r.RemoteAddr) + next.ServeHTTP(w, r) + log.Printf("[HTTP] %s %s completed in %v", r.Method, r.URL.Path, time.Since(startTime)) + }) +} + +// CORSMiddleware adds CORS headers to responses +func CORSMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Set CORS headers + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + + // Handle preflight requests + if r.Method == http.MethodOptions { + w.WriteHeader(http.StatusOK) + return + } + + // Call the next handler + next.ServeHTTP(w, r) + }) +} + +// RecoveryMiddleware recovers from panics +func RecoveryMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + log.Printf("[PANIC] %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + } + }() + next.ServeHTTP(w, r) + }) +} diff --git a/backend/internal/api/routes.go b/backend/internal/api/routes.go new file mode 100644 index 0000000..7187207 --- /dev/null +++ b/backend/internal/api/routes.go @@ -0,0 +1,31 @@ +package api + +import ( + "net/http" + + "github.com/arnab-afk/monaco/internal/api/handlers" +) + +// SetupRoutes configures all API routes +func SetupRoutes() http.Handler { + // Create a new handler + h := handlers.NewHandler() + + // Create a new router + mux := http.NewServeMux() + + // Apply middleware to all routes + var handler http.Handler = mux + handler = handlers.RecoveryMiddleware(handler) + handler = handlers.LoggingMiddleware(handler) + handler = handlers.CORSMiddleware(handler) + + // Register routes + mux.HandleFunc("/submit", h.SubmitHandler) + mux.HandleFunc("/status", h.StatusHandler) + mux.HandleFunc("/result", h.ResultHandler) + mux.HandleFunc("/queue-stats", h.QueueStatsHandler) + mux.HandleFunc("/health", h.HealthCheckHandler) + + return handler +} diff --git a/backend/internal/executor/executor.go b/backend/internal/executor/executor.go new file mode 100644 index 0000000..6dc9958 --- /dev/null +++ b/backend/internal/executor/executor.go @@ -0,0 +1,637 @@ +package executor + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/arnab-afk/monaco/internal/models" + "github.com/arnab-afk/monaco/internal/queue" +) + +// ExecutionService manages code execution +type ExecutionService struct { + queue *queue.JobQueue +} + +// CodeExecutionJob represents a code execution job +type CodeExecutionJob struct { + service *ExecutionService + submission *models.CodeSubmission +} + +// NewExecutionService creates a new execution service +func NewExecutionService() *ExecutionService { + return &ExecutionService{ + queue: queue.NewJobQueue(5), // 5 concurrent workers + } +} + +// NewCodeExecutionJob creates a new code execution job +func NewCodeExecutionJob(service *ExecutionService, submission *models.CodeSubmission) *CodeExecutionJob { + return &CodeExecutionJob{ + service: service, + submission: submission, + } +} + +// Execute runs the code execution job +func (j *CodeExecutionJob) Execute() { + submission := j.submission + submission.Status = "running" + submission.StartedAt = time.Now() + + log.Printf("[JOB-%s] Starting execution for language: %s", submission.ID, submission.Language) + + j.service.executeLanguageSpecific(submission) + + submission.CompletedAt = time.Now() + log.Printf("[JOB-%s] Execution completed in %v", submission.ID, submission.CompletedAt.Sub(submission.StartedAt)) +} + +// ExecuteCode adds the submission to the execution queue +func (s *ExecutionService) ExecuteCode(submission *models.CodeSubmission) { + submission.Status = "queued" + submission.QueuedAt = time.Now() + + log.Printf("[SUBMISSION-%s] Code submission queued for language: %s", submission.ID, submission.Language) + + // Create and add the job to the queue + job := NewCodeExecutionJob(s, submission) + s.queue.AddJob(job) +} + +// executeLanguageSpecific executes code based on the language +func (s *ExecutionService) executeLanguageSpecific(submission *models.CodeSubmission) { + switch strings.ToLower(submission.Language) { + case "python": + s.executePython(submission) + case "javascript", "js": + s.executeJavaScript(submission) + case "go", "golang": + s.executeGo(submission) + case "java": + s.executeJava(submission) + case "c": + s.executeC(submission) + case "cpp", "c++": + s.executeCpp(submission) + default: + submission.Status = "failed" + submission.Error = fmt.Sprintf("Unsupported language: %s", submission.Language) + log.Printf("[EXEC-%s] ERROR: Unsupported language: %s", submission.ID, submission.Language) + } +} + +// executePython runs Python code in a container +func (s *ExecutionService) executePython(submission *models.CodeSubmission) { + log.Printf("[PYTHON-%s] Preparing Python execution environment", submission.ID) + startTime := time.Now() + + // Create a temporary file for the code + tempDir, err := os.MkdirTemp("", "monaco-python-*") + if err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err) + return + } + defer os.RemoveAll(tempDir) + + // Write the code to a file + codePath := filepath.Join(tempDir, "code.py") + if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to write code file: %v", err) + return + } + + // Create a file for input if provided + inputPath := "" + if submission.Input != "" { + inputPath = filepath.Join(tempDir, "input.txt") + if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to write input file: %v", err) + return + } + } + + // Run the code in a Docker container + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var cmd *exec.Cmd + if inputPath != "" { + cmd = exec.CommandContext(ctx, "docker", "run", "--rm", + "--network=none", // No network access + "--memory=100m", // Memory limit + "--cpu-period=100000", // CPU quota period + "--cpu-quota=10000", // 10% CPU + "--ulimit", "nofile=64:64", // File descriptor limits + "-v", tempDir+":/code", // Mount code directory + "python:3.9", + "sh", "-c", "cat /code/input.txt | python /code/code.py") + } else { + cmd = exec.CommandContext(ctx, "docker", "run", "--rm", + "--network=none", // No network access + "--memory=100m", // Memory limit + "--cpu-period=100000", // CPU quota period + "--cpu-quota=10000", // 10% CPU + "--ulimit", "nofile=64:64", // File descriptor limits + "-v", tempDir+":/code", // Mount code directory + "python:3.9", + "python", "/code/code.py") + } + + output, err := cmd.CombinedOutput() + elapsed := time.Since(startTime) + log.Printf("[PYTHON-%s] Python execution completed in %v", submission.ID, elapsed) + + s.updateSubmissionResult(submission, output, err, ctx.Err() != nil) +} + +// executeJavaScript runs JavaScript code in a container +func (s *ExecutionService) executeJavaScript(submission *models.CodeSubmission) { + log.Printf("[JS-%s] Preparing JavaScript execution environment", submission.ID) + startTime := time.Now() + + // Create a temporary file for the code + tempDir, err := os.MkdirTemp("", "monaco-js-*") + if err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err) + return + } + defer os.RemoveAll(tempDir) + + // Write the code to a file + codePath := filepath.Join(tempDir, "code.js") + if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to write code file: %v", err) + return + } + + // Create a file for input if provided + inputPath := "" + if submission.Input != "" { + inputPath = filepath.Join(tempDir, "input.txt") + if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to write input file: %v", err) + return + } + } + + // Run the code in a Docker container + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var cmd *exec.Cmd + if inputPath != "" { + // Create a wrapper script to handle input + wrapperPath := filepath.Join(tempDir, "wrapper.js") + wrapperCode := ` +const fs = require('fs'); +const input = fs.readFileSync('/code/input.txt', 'utf8'); +// Redirect input to stdin +process.stdin.push(input); +process.stdin.push(null); +// Load and run the user code +require('./code.js'); +` + if err := os.WriteFile(wrapperPath, []byte(wrapperCode), 0644); err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to write wrapper file: %v", err) + return + } + + cmd = exec.CommandContext(ctx, "docker", "run", "--rm", + "--network=none", // No network access + "--memory=100m", // Memory limit + "--cpu-period=100000", // CPU quota period + "--cpu-quota=10000", // 10% CPU + "-v", tempDir+":/code", // Mount code directory + "node:18-alpine", + "node", "/code/wrapper.js") + } else { + cmd = exec.CommandContext(ctx, "docker", "run", "--rm", + "--network=none", // No network access + "--memory=100m", // Memory limit + "--cpu-period=100000", // CPU quota period + "--cpu-quota=10000", // 10% CPU + "-v", tempDir+":/code", // Mount code directory + "node:18-alpine", + "node", "/code/code.js") + } + + output, err := cmd.CombinedOutput() + elapsed := time.Since(startTime) + log.Printf("[JS-%s] JavaScript execution completed in %v", submission.ID, elapsed) + + s.updateSubmissionResult(submission, output, err, ctx.Err() != nil) +} + +// executeGo runs Go code in a container +func (s *ExecutionService) executeGo(submission *models.CodeSubmission) { + log.Printf("[GO-%s] Preparing Go execution environment", submission.ID) + startTime := time.Now() + + // Create a temporary file for the code + tempDir, err := os.MkdirTemp("", "monaco-go-*") + if err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err) + return + } + defer os.RemoveAll(tempDir) + + // Write the code to a file + codePath := filepath.Join(tempDir, "main.go") + if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to write code file: %v", err) + return + } + + // Create a file for input if provided + inputPath := "" + if submission.Input != "" { + inputPath = filepath.Join(tempDir, "input.txt") + if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to write input file: %v", err) + return + } + } + + // Run the code in a Docker container + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + // First compile the Go code + compileCmd := exec.CommandContext(ctx, "docker", "run", "--rm", + "-v", tempDir+":/code", // Mount code directory + "golang:1.22-alpine", + "go", "build", "-o", "/code/app", "/code/main.go") + + compileOutput, compileErr := compileCmd.CombinedOutput() + if compileErr != nil { + log.Printf("[GO-%s] Compilation failed: %v", submission.ID, compileErr) + submission.Status = "failed" + submission.Error = fmt.Sprintf("Compilation error: %s", compileOutput) + return + } + + // Then run the compiled binary + var runCmd *exec.Cmd + if inputPath != "" { + runCmd = exec.CommandContext(ctx, "docker", "run", "--rm", + "--network=none", // No network access + "--memory=100m", // Memory limit + "--cpu-period=100000", // CPU quota period + "--cpu-quota=10000", // 10% CPU + "-v", tempDir+":/code", // Mount code directory + "golang:1.22-alpine", + "sh", "-c", "cat /code/input.txt | /code/app") + } else { + runCmd = exec.CommandContext(ctx, "docker", "run", "--rm", + "--network=none", // No network access + "--memory=100m", // Memory limit + "--cpu-period=100000", // CPU quota period + "--cpu-quota=10000", // 10% CPU + "-v", tempDir+":/code", // Mount code directory + "golang:1.22-alpine", + "/code/app") + } + + output, err := runCmd.CombinedOutput() + elapsed := time.Since(startTime) + log.Printf("[GO-%s] Go execution completed in %v", submission.ID, elapsed) + + s.updateSubmissionResult(submission, output, err, ctx.Err() != nil) +} + +// executeJava runs Java code in a container +func (s *ExecutionService) executeJava(submission *models.CodeSubmission) { + log.Printf("[JAVA-%s] Preparing Java execution environment", submission.ID) + startTime := time.Now() + + // Create a temporary file for the code + tempDir, err := os.MkdirTemp("", "monaco-java-*") + if err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err) + return + } + defer os.RemoveAll(tempDir) + + // Extract class name from the code + className := extractJavaClassName(submission.Code) + if className == "" { + className = "Main" // Default class name + } + + // Write the code to a file + codePath := filepath.Join(tempDir, className+".java") + if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to write code file: %v", err) + return + } + + // Create a file for input if provided + inputPath := "" + if submission.Input != "" { + inputPath = filepath.Join(tempDir, "input.txt") + if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to write input file: %v", err) + return + } + } + + // Run the code in a Docker container + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + // First compile the Java code + compileCmd := exec.CommandContext(ctx, "docker", "run", "--rm", + "-v", tempDir+":/code", // Mount code directory + "eclipse-temurin:11-jdk-alpine", + "javac", "/code/"+className+".java") + + compileOutput, compileErr := compileCmd.CombinedOutput() + if compileErr != nil { + log.Printf("[JAVA-%s] Compilation failed: %v", submission.ID, compileErr) + submission.Status = "failed" + submission.Error = fmt.Sprintf("Compilation error: %s", compileOutput) + return + } + + // Then run the compiled class + var runCmd *exec.Cmd + if inputPath != "" { + runCmd = exec.CommandContext(ctx, "docker", "run", "--rm", + "--network=none", // No network access + "--memory=400m", // Memory limit + "--cpu-period=100000", // CPU quota period + "--cpu-quota=50000", // 50% CPU + "-v", tempDir+":/code", // Mount code directory + "eclipse-temurin:11-jdk-alpine", + "sh", "-c", "cd /code && cat input.txt | java -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Xverify:none -Xms64m -Xmx256m "+className) + } else { + runCmd = exec.CommandContext(ctx, "docker", "run", "--rm", + "--network=none", // No network access + "--memory=400m", // Memory limit + "--cpu-period=100000", // CPU quota period + "--cpu-quota=50000", // 50% CPU + "-v", tempDir+":/code", // Mount code directory + "eclipse-temurin:11-jdk-alpine", + "java", "-XX:+TieredCompilation", "-XX:TieredStopAtLevel=1", "-Xverify:none", "-Xms64m", "-Xmx256m", "-cp", "/code", className) + } + + output, err := runCmd.CombinedOutput() + elapsed := time.Since(startTime) + log.Printf("[JAVA-%s] Java execution completed in %v", submission.ID, elapsed) + + s.updateSubmissionResult(submission, output, err, ctx.Err() != nil) +} + +// executeC runs C code in a container +func (s *ExecutionService) executeC(submission *models.CodeSubmission) { + log.Printf("[C-%s] Preparing C execution environment", submission.ID) + startTime := time.Now() + + // Create a temporary file for the code + tempDir, err := os.MkdirTemp("", "monaco-c-*") + if err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err) + return + } + defer os.RemoveAll(tempDir) + + // Write the code to a file + codePath := filepath.Join(tempDir, "code.c") + if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to write code file: %v", err) + return + } + + // Create a file for input if provided + inputPath := "" + if submission.Input != "" { + inputPath = filepath.Join(tempDir, "input.txt") + if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to write input file: %v", err) + return + } + } + + // Run the code in a Docker container + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + // First compile the C code + compileCmd := exec.CommandContext(ctx, "docker", "run", "--rm", + "-v", tempDir+":/code", // Mount code directory + "gcc:latest", + "gcc", "-o", "/code/app", "/code/code.c") + + compileOutput, compileErr := compileCmd.CombinedOutput() + if compileErr != nil { + log.Printf("[C-%s] Compilation failed: %v", submission.ID, compileErr) + submission.Status = "failed" + submission.Error = fmt.Sprintf("Compilation error: %s", compileOutput) + return + } + + // Then run the compiled binary + var runCmd *exec.Cmd + if inputPath != "" { + runCmd = exec.CommandContext(ctx, "docker", "run", "--rm", + "--network=none", // No network access + "--memory=100m", // Memory limit + "--cpu-period=100000", // CPU quota period + "--cpu-quota=10000", // 10% CPU + "-v", tempDir+":/code", // Mount code directory + "gcc:latest", + "sh", "-c", "cat /code/input.txt | /code/app") + } else { + runCmd = exec.CommandContext(ctx, "docker", "run", "--rm", + "--network=none", // No network access + "--memory=100m", // Memory limit + "--cpu-period=100000", // CPU quota period + "--cpu-quota=10000", // 10% CPU + "-v", tempDir+":/code", // Mount code directory + "gcc:latest", + "/code/app") + } + + output, err := runCmd.CombinedOutput() + elapsed := time.Since(startTime) + log.Printf("[C-%s] C execution completed in %v", submission.ID, elapsed) + + s.updateSubmissionResult(submission, output, err, ctx.Err() != nil) +} + +// executeCpp runs C++ code in a container +func (s *ExecutionService) executeCpp(submission *models.CodeSubmission) { + log.Printf("[CPP-%s] Preparing C++ execution environment", submission.ID) + startTime := time.Now() + + // Create a temporary file for the code + tempDir, err := os.MkdirTemp("", "monaco-cpp-*") + if err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err) + return + } + defer os.RemoveAll(tempDir) + + // Write the code to a file + codePath := filepath.Join(tempDir, "code.cpp") + if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to write code file: %v", err) + return + } + + // Create a file for input if provided + inputPath := "" + if submission.Input != "" { + inputPath = filepath.Join(tempDir, "input.txt") + if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil { + submission.Status = "failed" + submission.Error = fmt.Sprintf("Failed to write input file: %v", err) + return + } + } + + // Run the code in a Docker container + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + defer cancel() + + // First compile the C++ code + compileCmd := exec.CommandContext(ctx, "docker", "run", "--rm", + "-v", tempDir+":/code", // Mount code directory + "gcc:latest", + "g++", "-o", "/code/app", "/code/code.cpp") + + compileOutput, compileErr := compileCmd.CombinedOutput() + if compileErr != nil { + log.Printf("[CPP-%s] Compilation failed: %v", submission.ID, compileErr) + submission.Status = "failed" + submission.Error = fmt.Sprintf("Compilation error: %s", compileOutput) + return + } + + // Then run the compiled binary + var runCmd *exec.Cmd + if inputPath != "" { + runCmd = exec.CommandContext(ctx, "docker", "run", "--rm", + "--network=none", // No network access + "--memory=100m", // Memory limit + "--cpu-period=100000", // CPU quota period + "--cpu-quota=10000", // 10% CPU + "-v", tempDir+":/code", // Mount code directory + "gcc:latest", + "sh", "-c", "cat /code/input.txt | /code/app") + } else { + runCmd = exec.CommandContext(ctx, "docker", "run", "--rm", + "--network=none", // No network access + "--memory=100m", // Memory limit + "--cpu-period=100000", // CPU quota period + "--cpu-quota=10000", // 10% CPU + "-v", tempDir+":/code", // Mount code directory + "gcc:latest", + "/code/app") + } + + output, err := runCmd.CombinedOutput() + elapsed := time.Since(startTime) + log.Printf("[CPP-%s] C++ execution completed in %v", submission.ID, elapsed) + + s.updateSubmissionResult(submission, output, err, ctx.Err() != nil) +} + +// updateSubmissionResult updates the submission with the execution result +func (s *ExecutionService) updateSubmissionResult(submission *models.CodeSubmission, output []byte, err error, timedOut bool) { + // Format the output to include the input if provided + formattedOutput := "" + if submission.Input != "" { + // Only add input lines that were actually used + inputLines := strings.Split(submission.Input, "\n") + for _, line := range inputLines { + if line != "" { + // Don't add the input marker for empty lines + formattedOutput += "[Input] " + line + "\n" + } + } + } + + // Add the actual output + rawOutput := string(output) + + if timedOut { + submission.Status = "failed" + submission.Error = "Execution timed out" + submission.Output = formattedOutput + rawOutput + return + } + + if err != nil { + submission.Status = "failed" + submission.Error = err.Error() + submission.Output = formattedOutput + rawOutput + return + } + + submission.Status = "completed" + submission.Output = formattedOutput + rawOutput +} + +// GetQueueStats returns statistics about the job queue +func (s *ExecutionService) GetQueueStats() models.QueueStats { + return s.queue.GetStats() +} + +// GenerateUUID generates a unique ID for submissions +func GenerateUUID() string { + b := make([]byte, 16) + _, err := rand.Read(b) + if err != nil { + return fmt.Sprintf("%d", time.Now().UnixNano()) + } + return hex.EncodeToString(b) +} + +// extractJavaClassName extracts the class name from Java code +func extractJavaClassName(code string) string { + // Simple regex-like extraction + lines := strings.Split(code, "\n") + for _, line := range lines { + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "public class ") { + parts := strings.Split(line, " ") + if len(parts) > 2 { + className := parts[2] + // Remove any { or implements/extends + className = strings.Split(className, "{")[0] + className = strings.Split(className, " ")[0] + return strings.TrimSpace(className) + } + } + } + return "" +} diff --git a/backend/internal/models/submission.go b/backend/internal/models/submission.go new file mode 100644 index 0000000..a7c3f33 --- /dev/null +++ b/backend/internal/models/submission.go @@ -0,0 +1,34 @@ +package models + +import "time" + +// CodeSubmission represents a code submission for execution +type CodeSubmission struct { + ID string `json:"id"` + Code string `json:"code"` + Language string `json:"language"` + Input string `json:"input"` + Status string `json:"status"` // "pending", "queued", "running", "completed", "failed" + QueuedAt time.Time `json:"queuedAt,omitempty"` + StartedAt time.Time `json:"startedAt,omitempty"` + CompletedAt time.Time `json:"completedAt,omitempty"` + Output string `json:"output,omitempty"` + Error string `json:"error,omitempty"` +} + +// ExecutionResult represents the result of code execution +type ExecutionResult struct { + Output string `json:"output"` + Error string `json:"error"` + ExitCode int `json:"exitCode"` + ExecutionMS int64 `json:"executionMs"` +} + +// QueueStats represents statistics about the job queue +type QueueStats struct { + QueueLength int `json:"queueLength"` + RunningJobs int `json:"runningJobs"` + CompletedJobs int `json:"completedJobs"` + FailedJobs int `json:"failedJobs"` + TotalProcessed int `json:"totalProcessed"` +} diff --git a/backend/internal/queue/queue.go b/backend/internal/queue/queue.go new file mode 100644 index 0000000..38da0f0 --- /dev/null +++ b/backend/internal/queue/queue.go @@ -0,0 +1,112 @@ +package queue + +import ( + "log" + "sync" + "time" + + "github.com/arnab-afk/monaco/internal/models" +) + +// Job represents a job to be executed +type Job interface { + Execute() +} + +// JobQueue manages the execution of jobs +type JobQueue struct { + queue chan Job + wg sync.WaitGroup + mu sync.Mutex + runningJobs int + completedJobs int + failedJobs int + totalProcessed int + workerCount int +} + +// NewJobQueue creates a new job queue with the specified number of workers +func NewJobQueue(workerCount int) *JobQueue { + q := &JobQueue{ + queue: make(chan Job, 100), // Buffer size of 100 jobs + workerCount: workerCount, + } + + // Start workers + for i := 0; i < workerCount; i++ { + q.wg.Add(1) + go q.worker(i) + } + + return q +} + +// worker processes jobs from the queue +func (q *JobQueue) worker(id int) { + defer q.wg.Done() + + log.Printf("[WORKER-%d] Started", id) + + for job := range q.queue { + // Update stats + q.mu.Lock() + q.runningJobs++ + q.mu.Unlock() + + // Execute the job + startTime := time.Now() + log.Printf("[WORKER-%d] Processing job", id) + + // Execute the job and handle panics + func() { + defer func() { + if r := recover(); r != nil { + log.Printf("[WORKER-%d] Panic in job execution: %v", id, r) + q.mu.Lock() + q.failedJobs++ + q.runningJobs-- + q.totalProcessed++ + q.mu.Unlock() + } + }() + + job.Execute() + }() + + // Update stats if no panic occurred + q.mu.Lock() + q.completedJobs++ + q.runningJobs-- + q.totalProcessed++ + q.mu.Unlock() + + log.Printf("[WORKER-%d] Job completed in %v", id, time.Since(startTime)) + } + + log.Printf("[WORKER-%d] Stopped", id) +} + +// AddJob adds a job to the queue +func (q *JobQueue) AddJob(job Job) { + q.queue <- job +} + +// GetStats returns statistics about the job queue +func (q *JobQueue) GetStats() models.QueueStats { + q.mu.Lock() + defer q.mu.Unlock() + + return models.QueueStats{ + QueueLength: len(q.queue), + RunningJobs: q.runningJobs, + CompletedJobs: q.completedJobs, + FailedJobs: q.failedJobs, + TotalProcessed: q.totalProcessed, + } +} + +// Shutdown stops the job queue +func (q *JobQueue) Shutdown() { + close(q.queue) + q.wg.Wait() +} diff --git a/backend/tmp/main.exe b/backend/tmp/main.exe index 345d959..90300e6 100644 Binary files a/backend/tmp/main.exe and b/backend/tmp/main.exe differ