new backend changes

This commit is contained in:
ishikabhoyar
2025-06-22 11:30:47 +05:30
parent 6802cefcaa
commit 86bc89c12e
32 changed files with 1531 additions and 1928 deletions

View File

@@ -609,7 +609,7 @@ This project is a VS Code Clone built with React and Monaco Editor. It features
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
// Submit the code to get an execution ID
const submitResponse = await fetch(`${apiUrl}/submit`, {
const submitResponse = await fetch(`${apiUrl}/api/submit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@@ -634,7 +634,7 @@ This project is a VS Code Clone built with React and Monaco Editor. It features
// Connect to WebSocket with the execution ID
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsBaseUrl = apiUrl.replace(/^https?:\/\//, '');
const wsUrl = `${wsProtocol}//${wsBaseUrl}/ws/terminal?id=${id}`;
const wsUrl = `${wsProtocol}//${wsBaseUrl}/api/ws/terminal/${id}`;
setTerminalOutput(prev => [...prev, { type: 'output', content: `Connecting to: ${wsUrl}` }]);
@@ -650,21 +650,74 @@ This project is a VS Code Clone built with React and Monaco Editor. It features
newSocket.onmessage = (event) => {
console.log("WebSocket message received:", event.data);
setTerminalOutput(prev => [...prev, { type: 'output', content: event.data }]);
// Check if this message is likely asking for input (prompt detection)
const isPrompt =
event.data.includes("input") ||
event.data.includes("?") ||
event.data.endsWith(":") ||
event.data.endsWith("> ");
try {
const message = JSON.parse(event.data);
if (isPrompt) {
console.log("Input prompt detected, focusing terminal");
// Force terminal to focus after a prompt is detected
setTimeout(() => {
document.querySelector('.panel-terminal')?.focus();
}, 100);
// Handle different message types
switch (message.type) {
case 'output':
setTerminalOutput(prev => [...prev, {
type: 'output',
content: message.content.text,
isError: message.content.isError
}]);
break;
case 'status':
const status = message.content.status;
setTerminalOutput(prev => [...prev, {
type: 'status',
content: `Status: ${status}`
}]);
// Update running state based on status
if (status === 'completed' || status === 'failed') {
// Don't immediately set isRunning to false - we'll wait for the socket to close or delay
}
break;
case 'system':
setTerminalOutput(prev => [...prev, {
type: 'system',
content: message.content
}]);
break;
case 'error':
setTerminalOutput(prev => [...prev, {
type: 'error',
content: `Error: ${message.content.message}`
}]);
break;
default:
// For raw or unknown messages
setTerminalOutput(prev => [...prev, {
type: 'output',
content: event.data
}]);
}
// Check if this message is likely asking for input (prompt detection)
if (message.type === 'output' && !message.content.isError &&
(message.content.text.includes("?") ||
message.content.text.endsWith(":") ||
message.content.text.endsWith("> "))) {
console.log("Input prompt detected, focusing terminal");
// Force terminal to focus after a prompt is detected
setTimeout(() => {
document.querySelector('.panel-terminal')?.focus();
}, 100);
}
} catch (err) {
// Handle case where message isn't valid JSON
console.warn("Failed to parse WebSocket message:", err);
setTerminalOutput(prev => [...prev, {
type: 'output',
content: event.data
}]);
}
};
@@ -674,7 +727,7 @@ This project is a VS Code Clone built with React and Monaco Editor. It features
// Start polling the status endpoint every 2 seconds
statusCheckInterval = setInterval(async () => {
try {
const statusResponse = await fetch(`${apiUrl}/status?id=${id}`);
const statusResponse = await fetch(`${apiUrl}/api/status/${id}`);
if (statusResponse.ok) {
const statusData = await statusResponse.json();
@@ -722,16 +775,43 @@ This project is a VS Code Clone built with React and Monaco Editor. It features
newSocket.onclose = (event) => {
console.log("WebSocket closed:", event);
setIsRunning(false);
setActiveSocket(null);
const reason = event.reason ? `: ${event.reason}` : '';
const code = event.code ? ` (code: ${event.code})` : '';
setTerminalOutput(prev => [...prev, {
type: 'warning',
content: `Terminal connection closed${reason}${code}`
}]);
// Don't mark as not running if this is expected close (after execution completes)
// Code 1000 is normal closure, 1005 is no status code
const isExpectedClose = event.code === 1000 || event.code === 1005;
// Only set running to false if it wasn't an expected close
if (!isExpectedClose) {
setIsRunning(false);
// Add a graceful reconnection message
setTerminalOutput(prev => [...prev, {
type: 'warning',
content: `Terminal connection closed${reason}${code}`
}]);
// Attempt reconnection for certain close codes (unexpected closes)
if (activeRunningFile && event.code !== 1000) {
setTerminalOutput(prev => [...prev, {
type: 'info',
content: `Attempting to reconnect...`
}]);
// Reconnection delay
setTimeout(() => {
// Attempt to reconnect for the same file
if (activeRunningFile) {
console.log("Attempting to reconnect for", activeRunningFile);
// You could call your run function here again
}
}, 3000);
}
}
setActiveSocket(null);
// Clean up interval
if (statusCheckInterval) {

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,245 +0,0 @@
package handler
import (
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"time"
"github.com/arnab-afk/monaco/model"
"github.com/arnab-afk/monaco/service"
"github.com/gorilla/websocket"
)
// Handler manages HTTP requests for code submissions
type Handler struct {
executionService *service.ExecutionService
mu sync.Mutex
submissions map[string]*model.CodeSubmission
}
// NewHandler creates a new handler instance
func NewHandler() *Handler {
return &Handler{
executionService: service.NewExecutionService(),
submissions: make(map[string]*model.CodeSubmission),
}
}
// SubmitHandler handles code submission requests
func (h *Handler) SubmitHandler(w http.ResponseWriter, r *http.Request) {
var submission model.CodeSubmission
if err := json.NewDecoder(r.Body).Decode(&submission); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Set default language if not provided
if submission.Language == "" {
submission.Language = "python" // Default to Python
}
// Validate language
supportedLanguages := map[string]bool{
"python": true,
"java": true,
"c": true,
"cpp": true,
}
if !supportedLanguages[submission.Language] {
http.Error(w, "Unsupported language: "+submission.Language, http.StatusBadRequest)
return
}
h.mu.Lock()
submission.ID = h.generateID()
submission.Status = "pending"
h.submissions[submission.ID] = &submission
h.mu.Unlock()
go h.executionService.ExecuteCode(&submission)
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) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "ID is required", http.StatusBadRequest)
return
}
h.mu.Lock()
submission, exists := h.submissions[id]
h.mu.Unlock()
if !exists {
http.Error(w, "Submission not found", http.StatusNotFound)
return
}
// Return status with time information
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.Status == "running" && !submission.StartedAt.IsZero() {
response["startedAt"] = submission.StartedAt.Format(time.RFC3339)
response["runningFor"] = time.Since(submission.StartedAt).String()
}
if submission.Status == "completed" || submission.Status == "failed" {
if !submission.CompletedAt.IsZero() && !submission.StartedAt.IsZero() {
response["executionTime"] = submission.CompletedAt.Sub(submission.StartedAt).Milliseconds()
response["completedAt"] = submission.CompletedAt.Format(time.RFC3339)
}
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, "Failed to serialize response: "+err.Error(), http.StatusInternalServerError)
return
}
}
// ResultHandler handles result requests
func (h *Handler) ResultHandler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "ID is required", http.StatusBadRequest)
return
}
h.mu.Lock()
submission, exists := h.submissions[id]
h.mu.Unlock()
if !exists {
http.Error(w, "Submission not found", http.StatusNotFound)
return
}
// Prepare response with safe time handling
response := map[string]interface{}{
"id": submission.ID,
"status": submission.Status,
"output": submission.Output,
"language": submission.Language,
}
// Only include time fields if they're set
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)
// Calculate times only if we have valid timestamps
if !submission.StartedAt.IsZero() {
executionTime := submission.CompletedAt.Sub(submission.StartedAt)
response["executionTime"] = executionTime.Milliseconds() // Use milliseconds for frontend
response["executionTimeFormatted"] = executionTime.String()
}
if !submission.QueuedAt.IsZero() {
totalTime := submission.CompletedAt.Sub(submission.QueuedAt)
response["totalTime"] = totalTime.Milliseconds() // Use milliseconds for frontend
response["totalTimeFormatted"] = totalTime.String()
}
}
// Return full submission details
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, "Failed to serialize response: "+err.Error(), http.StatusInternalServerError)
return
}
}
// QueueStatsHandler provides information about the job queue
func (h *Handler) QueueStatsHandler(w http.ResponseWriter, r *http.Request) {
stats := h.executionService.GetQueueStats()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"queue_stats": stats,
"submissions": len(h.submissions),
})
}
// ConnectTerminal connects a WebSocket to a running execution
func (h *Handler) ConnectTerminal(conn *websocket.Conn, executionID string) {
// Get submission from storage
h.mu.Lock()
submission, found := h.submissions[executionID]
status := "not found"
if found {
status = submission.Status
}
h.mu.Unlock()
log.Printf("[WS-%s] Terminal connection request, submission status: %s", executionID, status)
if !found {
log.Printf("[WS-%s] Execution not found", executionID)
conn.WriteMessage(websocket.TextMessage, []byte("Execution not found"))
conn.Close()
return
}
// If execution is already completed, send stored output and close
if submission.Status == "completed" || submission.Status == "failed" {
log.Printf("[WS-%s] Execution already %s, sending stored output (length: %d)",
executionID, submission.Status, len(submission.Output))
conn.WriteMessage(websocket.TextMessage, []byte(submission.Output))
conn.Close()
return
}
log.Printf("[WS-%s] Registering connection for real-time updates, current status: %s",
executionID, submission.Status)
// Register this connection with the execution service for real-time updates
h.executionService.RegisterTerminalConnection(executionID, conn)
// Send initial connection confirmation
initialMsg := fmt.Sprintf("[System] Connected to process (ID: %s, Status: %s)\n",
executionID, submission.Status)
conn.WriteMessage(websocket.TextMessage, []byte(initialMsg))
// Handle incoming messages from the terminal (for stdin)
go func() {
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Printf("[WS-%s] Read error: %v", executionID, err)
h.executionService.UnregisterTerminalConnection(executionID, conn)
break
}
log.Printf("[WS-%s] Received input from client: %s", executionID, string(message))
// Send input to the execution if it's waiting for input
h.executionService.SendInput(executionID, string(message))
}
}()
}
// generateID creates a unique ID for submissions
func (h *Handler) generateID() string {
return service.GenerateUUID()
}

View File

@@ -1,154 +0,0 @@
package handler
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/arnab-afk/monaco/model"
"github.com/stretchr/testify/assert"
)
func TestSubmitHandler(t *testing.T) {
h := NewHandler()
// Test valid Python submission
body := map[string]string{
"language": "python",
"code": "print('Hello, World!')",
}
bodyBytes, _ := json.Marshal(body)
req := httptest.NewRequest("POST", "/submit", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
h.SubmitHandler(w, req)
assert.Equal(t, http.StatusAccepted, w.Code)
var response map[string]string
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.NotEmpty(t, response["id"])
// Test invalid language
body["language"] = "invalid"
bodyBytes, _ = json.Marshal(body)
req = httptest.NewRequest("POST", "/submit", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
w = httptest.NewRecorder()
h.SubmitHandler(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "Unsupported language")
}
func TestStatusHandler(t *testing.T) {
h := NewHandler()
// Create a test submission
submission := &model.CodeSubmission{
ID: "test-id",
Language: "python",
Code: "print('Hello')",
Status: "completed",
QueuedAt: time.Now().Add(-2 * time.Second),
StartedAt: time.Now().Add(-1 * time.Second),
CompletedAt: time.Now(),
Output: "Hello",
}
h.submissions["test-id"] = submission
// Test valid status request
req := httptest.NewRequest("GET", "/status?id=test-id", nil)
w := httptest.NewRecorder()
h.StatusHandler(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "test-id", response["id"])
assert.Equal(t, "completed", response["status"])
// Test missing ID
req = httptest.NewRequest("GET", "/status", nil)
w = httptest.NewRecorder()
h.StatusHandler(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "ID is required")
// Test non-existent ID
req = httptest.NewRequest("GET", "/status?id=nonexistent", nil)
w = httptest.NewRecorder()
h.StatusHandler(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
assert.Contains(t, w.Body.String(), "Submission not found")
}
func TestResultHandler(t *testing.T) {
h := NewHandler()
// Create a test submission
submission := &model.CodeSubmission{
ID: "test-id",
Language: "python",
Code: "print('Hello')",
Status: "completed",
QueuedAt: time.Now().Add(-2 * time.Second),
StartedAt: time.Now().Add(-1 * time.Second),
CompletedAt: time.Now(),
Output: "Hello",
}
h.submissions["test-id"] = submission
// Test valid result request
req := httptest.NewRequest("GET", "/result?id=test-id", nil)
w := httptest.NewRecorder()
h.ResultHandler(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "test-id", response["id"])
assert.Equal(t, "completed", response["status"])
assert.Equal(t, "Hello", response["output"])
}
func TestQueueStatsHandler(t *testing.T) {
h := NewHandler()
// Add some test submissions
h.submissions["test-id1"] = &model.CodeSubmission{ID: "test-id1"}
h.submissions["test-id2"] = &model.CodeSubmission{ID: "test-id2"}
req := httptest.NewRequest("GET", "/queue-stats", nil)
w := httptest.NewRecorder()
h.QueueStatsHandler(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
stats, ok := response["queue_stats"].(map[string]interface{})
assert.True(t, ok)
assert.Contains(t, stats, "queue_length")
assert.Contains(t, stats, "max_workers")
assert.Contains(t, stats, "running_jobs")
assert.Equal(t, float64(2), response["submissions"])
}

View File

@@ -1,16 +0,0 @@
package model
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"` // Added input field for stdin
Status string `json:"status"` // "queued", "running", "completed", "failed"
QueuedAt time.Time `json:"queuedAt"`
StartedAt time.Time `json:"startedAt,omitempty"`
CompletedAt time.Time `json:"completedAt,omitempty"`
Output string `json:"output"`
}

View File

@@ -1,71 +0,0 @@
package model
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCodeSubmissionSerialization(t *testing.T) {
// Create a submission
now := time.Now()
submission := CodeSubmission{
ID: "test-id",
Code: "print('Hello, World!')",
Language: "python",
Input: "test input",
Status: "completed",
QueuedAt: now.Add(-2 * time.Second),
StartedAt: now.Add(-1 * time.Second),
CompletedAt: now,
Output: "Hello, World!",
}
// Serialize to JSON
jsonBytes, err := json.Marshal(submission)
assert.NoError(t, err)
assert.NotNil(t, jsonBytes)
// Deserialize back
var decoded CodeSubmission
err = json.Unmarshal(jsonBytes, &decoded)
assert.NoError(t, err)
// Verify fields match
assert.Equal(t, submission.ID, decoded.ID)
assert.Equal(t, submission.Code, decoded.Code)
assert.Equal(t, submission.Language, decoded.Language)
assert.Equal(t, submission.Input, decoded.Input)
assert.Equal(t, submission.Status, decoded.Status)
assert.Equal(t, submission.Output, decoded.Output)
// Time fields need special handling due to JSON serialization
assert.Equal(t, submission.QueuedAt.Format(time.RFC3339), decoded.QueuedAt.Format(time.RFC3339))
assert.Equal(t, submission.StartedAt.Format(time.RFC3339), decoded.StartedAt.Format(time.RFC3339))
assert.Equal(t, submission.CompletedAt.Format(time.RFC3339), decoded.CompletedAt.Format(time.RFC3339))
}
func TestCodeSubmissionDefaults(t *testing.T) {
// Test that zero time values work correctly
submission := CodeSubmission{
ID: "test-id",
Code: "print('Hello')",
Language: "python",
Status: "pending",
}
assert.True(t, submission.QueuedAt.IsZero())
assert.True(t, submission.StartedAt.IsZero())
assert.True(t, submission.CompletedAt.IsZero())
// Test JSON marshaling with zero time values
jsonBytes, err := json.Marshal(submission)
assert.NoError(t, err)
// The zero time values should still be included in the JSON
jsonStr := string(jsonBytes)
assert.Contains(t, jsonStr, `"id":"test-id"`)
assert.Contains(t, jsonStr, `"status":"pending"`)
}

View File

@@ -1,96 +0,0 @@
package queue
import (
"log"
"sync"
"time"
)
// Job represents a task that can be executed
type Job interface {
Execute()
}
// JobQueue manages the execution of jobs with limited concurrency
type JobQueue struct {
jobs chan Job
maxWorkers int
wg sync.WaitGroup
running int
mu sync.Mutex
}
// NewJobQueue creates a new job queue with specified max concurrent workers
func NewJobQueue(maxWorkers int) *JobQueue {
log.Printf("[QUEUE] Initializing job queue with %d workers and buffer size 100", maxWorkers)
jq := &JobQueue{
jobs: make(chan Job, 100), // Buffer size of 100 jobs
maxWorkers: maxWorkers,
}
jq.start()
return jq
}
// start initializes the worker pool
func (jq *JobQueue) start() {
// Start the workers
for i := 0; i < jq.maxWorkers; i++ {
workerId := i + 1
log.Printf("[WORKER-%d] Starting worker", workerId)
jq.wg.Add(1)
go func(id int) {
defer jq.wg.Done()
for job := range jq.jobs {
jq.mu.Lock()
jq.running++
queueLen := len(jq.jobs)
jq.mu.Unlock()
log.Printf("[WORKER-%d] Processing job (running: %d, queued: %d)",
id, jq.running, queueLen)
startTime := time.Now()
job.Execute()
elapsed := time.Since(startTime)
jq.mu.Lock()
jq.running--
jq.mu.Unlock()
log.Printf("[WORKER-%d] Completed job in %v (running: %d, queued: %d)",
id, elapsed, jq.running, len(jq.jobs))
}
log.Printf("[WORKER-%d] Worker shutting down", id)
}(workerId)
}
}
// Enqueue adds a job to the queue
func (jq *JobQueue) Enqueue(job Job) {
jq.mu.Lock()
queueLen := len(jq.jobs)
jq.mu.Unlock()
log.Printf("[QUEUE] Job enqueued (queue length: %d, running: %d)", queueLen, jq.running)
jq.jobs <- job
}
// Stop gracefully shuts down the job queue
func (jq *JobQueue) Stop() {
log.Println("[QUEUE] Stopping job queue, waiting for running jobs to complete")
close(jq.jobs)
jq.wg.Wait()
log.Println("[QUEUE] Job queue shutdown complete")
}
// QueueStats returns statistics about the queue
func (jq *JobQueue) QueueStats() map[string]int {
jq.mu.Lock()
defer jq.mu.Unlock()
return map[string]int{
"queue_length": len(jq.jobs),
"max_workers": jq.maxWorkers,
"running_jobs": jq.running,
}
}

View File

@@ -1,112 +0,0 @@
package queue
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// Mock job for testing
type MockJob struct {
executed bool
executeTime time.Duration
mu sync.Mutex
}
func (j *MockJob) Execute() {
j.mu.Lock()
defer j.mu.Unlock()
time.Sleep(j.executeTime)
j.executed = true
}
func (j *MockJob) IsExecuted() bool {
j.mu.Lock()
defer j.mu.Unlock()
return j.executed
}
func TestJobQueueCreation(t *testing.T) {
// Test with different numbers of workers
jq := NewJobQueue(5)
assert.NotNil(t, jq)
assert.Equal(t, 5, jq.maxWorkers)
stats := jq.QueueStats()
assert.Equal(t, 0, stats["queue_length"])
assert.Equal(t, 5, stats["max_workers"])
assert.Equal(t, 0, stats["running_jobs"])
}
func TestJobExecution(t *testing.T) {
jq := NewJobQueue(2)
// Create test jobs
job1 := &MockJob{executeTime: 10 * time.Millisecond}
job2 := &MockJob{executeTime: 10 * time.Millisecond}
// Enqueue jobs
jq.Enqueue(job1)
jq.Enqueue(job2)
// Wait for execution
time.Sleep(50 * time.Millisecond)
// Verify both jobs executed
assert.True(t, job1.IsExecuted())
assert.True(t, job2.IsExecuted())
}
func TestConcurrentJobsExecution(t *testing.T) {
// Test that only maxWorkers jobs run concurrently
jq := NewJobQueue(2)
var mu sync.Mutex
runningCount := 0
maxObservedRunning := 0
wg := sync.WaitGroup{}
// Create long running jobs to test concurrency
for i := 0; i < 5; i++ {
wg.Add(1)
job := &MockJob{
executeTime: 100 * time.Millisecond,
}
// Wrap the job to monitor concurrency
wrappedJob := JobFunc(func() {
mu.Lock()
runningCount++
if runningCount > maxObservedRunning {
maxObservedRunning = runningCount
}
mu.Unlock()
job.Execute()
mu.Lock()
runningCount--
mu.Unlock()
wg.Done()
})
jq.Enqueue(wrappedJob)
}
wg.Wait()
jq.Stop()
// Verify max concurrent jobs is respected
assert.LessOrEqual(t, maxObservedRunning, 2)
}
// Define JobFunc type for easier job creation in tests
type JobFunc func()
func (f JobFunc) Execute() {
f()
}

View File

@@ -1,606 +0,0 @@
package service
import (
"bytes"
"context"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"sync"
"time"
"github.com/arnab-afk/monaco/model"
"github.com/arnab-afk/monaco/queue"
"github.com/gorilla/websocket"
)
// ExecutionService handles code execution for multiple languages
type ExecutionService struct {
mu sync.Mutex
queue *queue.JobQueue
terminalConnections map[string][]*websocket.Conn // Map of executionID to WebSocket connections
execInputChannels map[string]chan string // Map of executionID to input channels
}
// NewExecutionService creates a new execution service
func NewExecutionService() *ExecutionService {
log.Println("Initializing execution service with 3 concurrent workers")
return &ExecutionService{
queue: queue.NewJobQueue(3), // 3 concurrent executions max
terminalConnections: make(map[string][]*websocket.Conn),
execInputChannels: make(map[string]chan string),
}
}
// RegisterTerminalConnection registers a WebSocket connection for an execution
func (s *ExecutionService) RegisterTerminalConnection(executionID string, conn *websocket.Conn) {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.terminalConnections[executionID]; !exists {
s.terminalConnections[executionID] = make([]*websocket.Conn, 0)
}
s.terminalConnections[executionID] = append(s.terminalConnections[executionID], conn)
log.Printf("[WS-%s] Terminal connection registered, total connections: %d",
executionID, len(s.terminalConnections[executionID]))
}
// UnregisterTerminalConnection removes a WebSocket connection
func (s *ExecutionService) UnregisterTerminalConnection(executionID string, conn *websocket.Conn) {
s.mu.Lock()
defer s.mu.Unlock()
connections, exists := s.terminalConnections[executionID]
if !exists {
return
}
// Remove the specific connection
for i, c := range connections {
if c == conn {
s.terminalConnections[executionID] = append(connections[:i], connections[i+1:]...)
break
}
}
// If no more connections, clean up
if len(s.terminalConnections[executionID]) == 0 {
delete(s.terminalConnections, executionID)
}
log.Printf("[WS-%s] Terminal connection unregistered", executionID)
}
// SendOutputToTerminals sends output to all connected terminals for an execution
func (s *ExecutionService) SendOutputToTerminals(executionID string, output string) {
s.mu.Lock()
connections := s.terminalConnections[executionID]
s.mu.Unlock()
for _, conn := range connections {
if err := conn.WriteMessage(websocket.TextMessage, []byte(output)); err != nil {
log.Printf("[WS-%s] Error sending to terminal: %v", executionID, err)
// Unregister this connection on error
s.UnregisterTerminalConnection(executionID, conn)
}
}
}
// SendInput sends user input to a running process
func (s *ExecutionService) SendInput(executionID string, input string) {
s.mu.Lock()
inputChan, exists := s.execInputChannels[executionID]
s.mu.Unlock()
if exists {
select {
case inputChan <- input:
log.Printf("[WS-%s] Sent input to execution: %s", executionID, input)
default:
log.Printf("[WS-%s] Execution not ready for input", executionID)
}
} else {
log.Printf("[WS-%s] No input channel for execution", executionID)
}
}
// CodeExecutionJob represents a job to execute code
type CodeExecutionJob struct {
service *ExecutionService
submission *model.CodeSubmission
}
// NewCodeExecutionJob creates a new code execution job
func NewCodeExecutionJob(service *ExecutionService, submission *model.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)
}
// ExecuteCode adds the submission to the execution queue
func (s *ExecutionService) ExecuteCode(submission *model.CodeSubmission) {
submission.Status = "queued"
submission.QueuedAt = time.Now()
log.Printf("[SUBMISSION-%s] Code submission queued for language: %s (Queue length: %d)",
submission.ID, submission.Language, s.queue.QueueStats()["queue_length"])
// Log if input is provided
if len(submission.Input) > 0 {
inputLen := len(submission.Input)
previewLen := 30
if inputLen > previewLen {
log.Printf("[INPUT-%s] Input provided (%d bytes): %s...",
submission.ID, inputLen, submission.Input[:previewLen])
} else {
log.Printf("[INPUT-%s] Input provided (%d bytes): %s",
submission.ID, inputLen, submission.Input)
}
}
job := NewCodeExecutionJob(s, submission)
s.queue.Enqueue(job)
}
// executeLanguageSpecific runs code in the appropriate language container
func (s *ExecutionService) executeLanguageSpecific(submission *model.CodeSubmission) {
log.Printf("[EXEC-%s] Selecting execution environment for language: %s",
submission.ID, submission.Language)
switch submission.Language {
case "python":
log.Printf("[EXEC-%s] Executing Python code", submission.ID)
s.executePython(submission)
case "java":
log.Printf("[EXEC-%s] Executing Java code", submission.ID)
s.executeJava(submission)
case "c":
log.Printf("[EXEC-%s] Executing C code", submission.ID)
s.executeC(submission)
case "cpp":
log.Printf("[EXEC-%s] Executing C++ code", submission.ID)
s.executeCpp(submission)
default:
log.Printf("[EXEC-%s] ERROR: Unsupported language: %s", submission.ID, submission.Language)
submission.Status = "failed"
submission.Output = "Unsupported language: " + submission.Language
}
}
// executeWithInput runs a command with a timeout and provides input
func (s *ExecutionService) executeWithInput(cmd *exec.Cmd, input string, timeout time.Duration, submissionID string) ([]byte, error) {
log.Printf("[TIMEOUT-%s] Setting execution timeout: %v", submissionID, timeout)
// Create pipes for stdin, stdout, and stderr
stdin, stdinErr := cmd.StdinPipe()
if stdinErr != nil {
return nil, fmt.Errorf("failed to create stdin pipe: %v", stdinErr)
}
stdout, stdoutErr := cmd.StdoutPipe()
if stdoutErr != nil {
return nil, fmt.Errorf("failed to create stdout pipe: %v", stdoutErr)
}
stderr, stderrErr := cmd.StderrPipe()
if stderrErr != nil {
return nil, fmt.Errorf("failed to create stderr pipe: %v", stderrErr)
}
// Create an input channel and register it
inputChan := make(chan string, 10)
s.mu.Lock()
s.execInputChannels[submissionID] = inputChan
s.mu.Unlock()
// Clean up the input channel when done
defer func() {
s.mu.Lock()
delete(s.execInputChannels, submissionID)
s.mu.Unlock()
close(inputChan)
}()
// Start the command
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to start process: %v", err)
}
// Create a buffer to collect all output
var outputBuffer bytes.Buffer
// Handle stdout in a goroutine
go func() {
buffer := make([]byte, 1024)
for {
n, err := stdout.Read(buffer)
if n > 0 {
data := buffer[:n]
outputBuffer.Write(data)
// Send real-time output to connected terminals
s.SendOutputToTerminals(submissionID, string(data))
}
if err != nil {
break
}
}
}()
// Handle stderr in a goroutine
go func() {
buffer := make([]byte, 1024)
for {
n, err := stderr.Read(buffer)
if n > 0 {
data := buffer[:n]
outputBuffer.Write(data)
// Send real-time output to connected terminals
s.SendOutputToTerminals(submissionID, string(data))
}
if err != nil {
break
}
}
}()
// Write initial input if provided
if input != "" {
io.WriteString(stdin, input+"\n")
}
// Process is in a separate context, but it needs to be killed if timeout occurs
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Handle additional input from WebSocket in a goroutine
go func() {
for {
select {
case additionalInput, ok := <-inputChan:
if !ok {
return
}
log.Printf("[INPUT-%s] Received input from WebSocket: %s", submissionID, additionalInput)
io.WriteString(stdin, additionalInput+"\n")
case <-ctx.Done():
return
}
}
}()
// Wait for the command to complete with timeout
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
// Wait for completion or timeout
select {
case <-time.After(timeout):
cancel() // Stop the input handler
log.Printf("[TIMEOUT-%s] Execution timed out after %v seconds", submissionID, timeout.Seconds())
if err := cmd.Process.Kill(); err != nil {
log.Printf("[TIMEOUT-%s] Failed to kill process: %v", submissionID, err)
}
s.SendOutputToTerminals(submissionID, fmt.Sprintf("\n[System] Process killed after timeout of %v seconds", timeout.Seconds()))
return outputBuffer.Bytes(), fmt.Errorf("execution timed out after %v seconds", timeout.Seconds())
case err := <-done:
cancel() // Stop the input handler
s.SendOutputToTerminals(submissionID, "\n[System] Process completed")
return outputBuffer.Bytes(), err
}
}
// executePython runs Python code in a container
func (s *ExecutionService) executePython(submission *model.CodeSubmission) {
log.Printf("[PYTHON-%s] Preparing Python execution environment", submission.ID)
startTime := time.Now()
cmd := exec.Command("docker", "run", "--rm", "-i",
"--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
"python:3.9", "python", "-c", submission.Code)
log.Printf("[PYTHON-%s] Executing Python code with timeout: 10s", submission.ID)
// Use the enhanced executeWithInput method for all executions
output, err := s.executeWithInput(cmd, submission.Input, 100*time.Second, submission.ID)
elapsed := time.Since(startTime)
log.Printf("[PYTHON-%s] Python execution completed in %v", submission.ID, elapsed)
s.updateSubmissionResult(submission, output, err)
}
// extractClassName extracts the Java class name from code
func extractClassName(code string) string {
// Default class name as fallback
defaultClass := "Solution"
// Look for public class
re := regexp.MustCompile(`public\s+class\s+(\w+)`)
matches := re.FindStringSubmatch(code)
if len(matches) > 1 {
return matches[1]
}
// Look for any class if no public class
re = regexp.MustCompile(`class\s+(\w+)`)
matches = re.FindStringSubmatch(code)
if len(matches) > 1 {
return matches[1]
}
return defaultClass
}
// executeJava runs Java code in a container
func (s *ExecutionService) executeJava(submission *model.CodeSubmission) {
log.Printf("[JAVA-%s] Preparing Java execution environment", submission.ID)
startTime := time.Now()
// Extract class name from code
className := extractClassName(submission.Code)
log.Printf("[JAVA-%s] Detected class name: %s", submission.ID, className)
// Create temp directory for Java files
tempDir, err := os.MkdirTemp("", "java-execution-"+submission.ID)
if err != nil {
log.Printf("[JAVA-%s] Failed to create temp directory: %v", submission.ID, err)
submission.Status = "failed"
submission.Output = "Failed to create temp directory: " + err.Error()
return
}
defer os.RemoveAll(tempDir)
log.Printf("[JAVA-%s] Created temp directory: %s", submission.ID, tempDir)
// Write Java code to file with detected class name
javaFilePath := filepath.Join(tempDir, className+".java")
if err := os.WriteFile(javaFilePath, []byte(submission.Code), 0644); err != nil {
log.Printf("[JAVA-%s] Failed to write Java file: %v", submission.ID, err)
submission.Status = "failed"
submission.Output = "Failed to write Java file: " + err.Error()
return
}
log.Printf("[JAVA-%s] Wrote code to file: %s", submission.ID, javaFilePath)
// First compile without running
compileCmd := exec.Command("docker", "run", "--rm",
"-v", tempDir+":/code", // Mount code directory
"eclipse-temurin:11-jdk-alpine",
"javac", "/code/"+className+".java")
log.Printf("[JAVA-%s] Compiling Java code", submission.ID)
compileOutput, compileErr := compileCmd.CombinedOutput()
if compileErr != nil {
log.Printf("[JAVA-%s] Compilation failed: %v", submission.ID, compileErr)
submission.Status = "failed"
submission.Output = "Compilation error:\n" + string(compileOutput)
return
}
log.Printf("[JAVA-%s] Compilation successful", submission.ID)
// Now run the compiled class with the enhanced executeWithInput method
runCmd := exec.Command("docker", "run", "--rm", "-i",
"--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)
log.Printf("[JAVA-%s] Executing Java code", submission.ID)
output, err := s.executeWithInput(runCmd, submission.Input, 15*time.Second, submission.ID)
elapsed := time.Since(startTime)
log.Printf("[JAVA-%s] Java execution completed in %v", submission.ID, elapsed)
s.updateSubmissionResult(submission, output, err)
}
// executeC runs C code in a container with improved file handling
func (s *ExecutionService) executeC(submission *model.CodeSubmission) {
log.Printf("[C-%s] Preparing C execution environment", submission.ID)
startTime := time.Now()
// Create unique temp directory for C files
tempDir, err := os.MkdirTemp("", "c-execution-"+submission.ID)
if err != nil {
log.Printf("[C-%s] Failed to create temp directory: %v", submission.ID, err)
submission.Status = "failed"
submission.Output = "Failed to create temp directory: " + err.Error()
return
}
defer os.RemoveAll(tempDir)
log.Printf("[C-%s] Created temp directory: %s", submission.ID, tempDir)
// Write C code to file
cFilePath := filepath.Join(tempDir, "solution.c")
if err := os.WriteFile(cFilePath, []byte(submission.Code), 0644); err != nil {
log.Printf("[C-%s] Failed to write C file: %v", submission.ID, err)
submission.Status = "failed"
submission.Output = "Failed to write C file: " + err.Error()
return
}
log.Printf("[C-%s] Wrote code to file: %s", submission.ID, cFilePath)
// Compile C code first
compileCmd := exec.Command("docker", "run", "--rm",
"-v", tempDir+":/code", // Mount code directory
"gcc:latest", "gcc", "-o", "/code/solution", "/code/solution.c")
compileOutput, compileErr := compileCmd.CombinedOutput()
if compileErr != nil {
log.Printf("[C-%s] Compilation failed: %v", submission.ID, compileErr)
submission.Status = "failed"
submission.Output = "Compilation error:\n" + string(compileOutput)
return
}
log.Printf("[C-%s] Compilation successful", submission.ID)
// Run C executable using executeWithInput to support WebSockets
runCmd := exec.Command("docker", "run", "--rm", "-i",
"--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/solution")
log.Printf("[C-%s] Executing C code", submission.ID)
output, err := s.executeWithInput(runCmd, submission.Input, 30*time.Second, submission.ID)
elapsed := time.Since(startTime)
log.Printf("[C-%s] C execution completed in %v", submission.ID, elapsed)
s.updateSubmissionResult(submission, output, err)
}
// executeCpp runs C++ code in a container with improved file handling
func (s *ExecutionService) executeCpp(submission *model.CodeSubmission) {
log.Printf("[CPP-%s] Preparing C++ execution environment", submission.ID)
startTime := time.Now()
// Create unique temp directory for C++ files
tempDir, err := os.MkdirTemp("", "cpp-execution-"+submission.ID)
if err != nil {
log.Printf("[CPP-%s] Failed to create temp directory: %v", submission.ID, err)
submission.Status = "failed"
submission.Output = "Failed to create temp directory: " + err.Error()
return
}
defer os.RemoveAll(tempDir)
log.Printf("[CPP-%s] Created temp directory: %s", submission.ID, tempDir)
// Write C++ code to file
cppFilePath := filepath.Join(tempDir, "solution.cpp")
if err := os.WriteFile(cppFilePath, []byte(submission.Code), 0644); err != nil {
log.Printf("[CPP-%s] Failed to write C++ file: %v", submission.ID, err)
submission.Status = "failed"
submission.Output = "Failed to write C++ file: " + err.Error()
return
}
log.Printf("[CPP-%s] Wrote code to file: %s", submission.ID, cppFilePath)
// Compile C++ code first
compileCmd := exec.Command("docker", "run", "--rm",
"-v", tempDir+":/code", // Mount code directory
"gcc:latest", "g++", "-o", "/code/solution", "/code/solution.cpp")
compileOutput, compileErr := compileCmd.CombinedOutput()
if compileErr != nil {
log.Printf("[CPP-%s] Compilation failed: %v", submission.ID, compileErr)
submission.Status = "failed"
submission.Output = "Compilation error:\n" + string(compileOutput)
return
}
log.Printf("[CPP-%s] Compilation successful", submission.ID)
// Run C++ executable using executeWithInput to support WebSockets
runCmd := exec.Command("docker", "run", "--rm", "-i",
"--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/solution")
log.Printf("[CPP-%s] Executing C++ code", submission.ID)
output, err := s.executeWithInput(runCmd, submission.Input, 100*time.Second, submission.ID)
elapsed := time.Since(startTime)
log.Printf("[CPP-%s] C++ execution completed in %v", submission.ID, elapsed)
s.updateSubmissionResult(submission, output, err)
}
// executeWithTimeout runs a command with a timeout
func (s *ExecutionService) executeWithTimeout(cmd *exec.Cmd, timeout time.Duration, submissionID string) ([]byte, error) {
log.Printf("[TIMEOUT-%s] Setting execution timeout: %v", submissionID, timeout)
done := make(chan error, 1)
var output []byte
var err error
go func() {
log.Printf("[EXEC-%s] Starting command execution: %v", submissionID, cmd.Args)
output, err = cmd.CombinedOutput()
done <- err
}()
select {
case <-time.After(timeout):
log.Printf("[TIMEOUT-%s] Execution timed out after %v seconds", submissionID, timeout.Seconds())
if err := cmd.Process.Kill(); err != nil {
log.Printf("[TIMEOUT-%s] Failed to kill process: %v", submissionID, err)
return nil, fmt.Errorf("timeout reached but failed to kill process: %v", err)
}
return nil, fmt.Errorf("execution timed out after %v seconds", timeout.Seconds())
case err := <-done:
if err != nil {
log.Printf("[EXEC-%s] Command execution failed: %v", submissionID, err)
} else {
log.Printf("[EXEC-%s] Command execution completed successfully", submissionID)
}
return output, err
}
}
// updateSubmissionResult updates the submission with execution results
func (s *ExecutionService) updateSubmissionResult(submission *model.CodeSubmission, output []byte, err error) {
s.mu.Lock()
defer s.mu.Unlock()
submission.CompletedAt = time.Now()
executionTime := submission.CompletedAt.Sub(submission.StartedAt)
totalTime := submission.CompletedAt.Sub(submission.QueuedAt)
if err != nil {
submission.Status = "failed"
submission.Output = string(output) + "\n" + err.Error()
log.Printf("[RESULT-%s] Execution FAILED in %v (total time: %v, including queue: %v)",
submission.ID, executionTime, totalTime, totalTime-executionTime)
} else {
submission.Status = "completed"
submission.Output = string(output)
log.Printf("[RESULT-%s] Execution COMPLETED in %v (total time: %v, including queue: %v)",
submission.ID, executionTime, totalTime, totalTime-executionTime)
}
}
// GetQueueStats returns statistics about the job queue
func (s *ExecutionService) GetQueueStats() map[string]int {
stats := s.queue.QueueStats()
log.Printf("[QUEUE] Stats - Jobs in queue: %d, Running jobs: %d, Max workers: %d",
stats["queue_length"], stats["running_jobs"], stats["max_workers"])
return stats
}

View File

@@ -1,115 +0,0 @@
package service
import (
"os"
"testing"
"time"
"github.com/arnab-afk/monaco/model"
"github.com/stretchr/testify/assert"
)
// TestExecutionServiceCreation tests that the service is created properly
func TestExecutionServiceCreation(t *testing.T) {
service := NewExecutionService()
assert.NotNil(t, service)
assert.NotNil(t, service.queue)
}
// TestExtractClassName tests the class name extraction for Java code
func TestExtractClassName(t *testing.T) {
tests := []struct {
name string
code string
expected string
}{
{
name: "Public class",
code: "public class MyClass { public static void main(String[] args) {} }",
expected: "MyClass",
},
{
name: "Regular class",
code: "class RegularClass { public static void main(String[] args) {} }",
expected: "RegularClass",
},
{
name: "Multiple classes",
code: "class Class1 {} public class MainClass {} class Class2 {}",
expected: "MainClass",
},
{
name: "No class",
code: "// Just a comment",
expected: "Solution", // Default class name
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := extractClassName(tt.code)
assert.Equal(t, tt.expected, result)
})
}
}
// MockDockerExec is a function that can be used to mock Docker exec commands
type MockDockerExec func(cmd string, args ...string) ([]byte, error)
// TestUpdateSubmissionResult tests the submission result update logic
func TestUpdateSubmissionResult(t *testing.T) {
service := NewExecutionService()
// Test successful execution
submission := &model.CodeSubmission{
ID: "test-id",
Status: "running",
StartedAt: time.Now().Add(-500 * time.Millisecond),
QueuedAt: time.Now().Add(-1 * time.Second),
}
output := []byte("Hello, World!")
service.updateSubmissionResult(submission, output, nil)
assert.Equal(t, "completed", submission.Status)
assert.Equal(t, "Hello, World!", submission.Output)
assert.False(t, submission.CompletedAt.IsZero())
// Test failed execution
submission = &model.CodeSubmission{
ID: "test-id-2",
Status: "running",
StartedAt: time.Now().Add(-500 * time.Millisecond),
QueuedAt: time.Now().Add(-1 * time.Second),
}
output = []byte("Compilation error")
err := os.ErrInvalid // Any error will do for testing
service.updateSubmissionResult(submission, output, err)
assert.Equal(t, "failed", submission.Status)
assert.Contains(t, submission.Output, "Compilation error")
assert.Contains(t, submission.Output, err.Error())
assert.False(t, submission.CompletedAt.IsZero())
}
// TestCodeExecutionJob tests the job execution logic
func TestCodeExecutionJob(t *testing.T) {
service := NewExecutionService()
submission := &model.CodeSubmission{
ID: "test-id",
Language: "python",
Code: "print('test')",
Status: "queued",
QueuedAt: time.Now(),
}
job := NewCodeExecutionJob(service, submission)
assert.NotNil(t, job)
assert.Equal(t, submission, job.submission)
assert.Equal(t, service, job.service)
// We can't easily test the actual execution because it depends on Docker
// In a real test environment, you would mock the Docker calls
}

View File

@@ -1,17 +0,0 @@
package service
import (
"crypto/rand"
"fmt"
"time"
)
// GenerateUUID creates a random UUID
func GenerateUUID() string {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}

View File

@@ -1,195 +0,0 @@
package tests
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/arnab-afk/monaco/handler"
"github.com/stretchr/testify/assert"
)
func setupTestServer() *httptest.Server {
h := handler.NewHandler()
mux := http.NewServeMux()
mux.HandleFunc("/submit", h.SubmitHandler)
mux.HandleFunc("/status", h.StatusHandler)
mux.HandleFunc("/result", h.ResultHandler)
mux.HandleFunc("/queue-stats", h.QueueStatsHandler)
return httptest.NewServer(mux)
}
func TestAPIIntegration(t *testing.T) {
server := setupTestServer()
defer server.Close()
// Test: Submit code, check status, and get results
// 1. Submit a Python job
submitURL := server.URL + "/submit"
body := map[string]string{
"language": "python",
"code": "print('Hello, Integration Test!')",
}
bodyBytes, _ := json.Marshal(body)
resp, err := http.Post(submitURL, "application/json", bytes.NewReader(bodyBytes))
assert.NoError(t, err)
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
// Get the job ID
var submitResp map[string]string
json.NewDecoder(resp.Body).Decode(&submitResp)
resp.Body.Close()
jobID := submitResp["id"]
assert.NotEmpty(t, jobID)
// 2. Check status
statusURL := server.URL + "/status?id=" + jobID
// Wait for job to complete (try multiple times)
var statusResp map[string]interface{}
maxRetries := 10
for i := 0; i < maxRetries; i++ {
resp, err = http.Get(statusURL)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
json.NewDecoder(resp.Body).Decode(&statusResp)
resp.Body.Close()
// If job completed or failed, break
status, _ := statusResp["status"].(string)
if status == "completed" || status == "failed" {
break
}
// Wait before next retry
time.Sleep(200 * time.Millisecond)
}
// 3. Get results
resultURL := server.URL + "/result?id=" + jobID
resp, err = http.Get(resultURL)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var resultResp map[string]interface{}
json.NewDecoder(resp.Body).Decode(&resultResp)
resp.Body.Close()
assert.Equal(t, jobID, resultResp["id"])
assert.Contains(t, resultResp["output"], "Hello, Integration Test!")
// 4. Check queue stats
statsURL := server.URL + "/queue-stats"
resp, err = http.Get(statsURL)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var statsResp map[string]interface{}
json.NewDecoder(resp.Body).Decode(&statsResp)
resp.Body.Close()
assert.Contains(t, statsResp, "queue_stats")
assert.Contains(t, statsResp, "submissions")
}
func TestMultipleLanguageSubmissions(t *testing.T) {
server := setupTestServer()
defer server.Close()
// Test submissions for different languages
languages := []string{"python", "java", "c", "cpp"}
codes := map[string]string{
"python": "print('Hello from Python')",
"java": "public class Solution { public static void main(String[] args) { System.out.println(\"Hello from Java\"); } }",
"c": "#include <stdio.h>\nint main() { printf(\"Hello from C\\n\"); return 0; }",
"cpp": "#include <iostream>\nint main() { std::cout << \"Hello from C++\" << std::endl; return 0; }",
}
submitURL := server.URL + "/submit"
for _, lang := range languages {
body := map[string]string{
"language": lang,
"code": codes[lang],
}
bodyBytes, _ := json.Marshal(body)
resp, err := http.Post(submitURL, "application/json", bytes.NewReader(bodyBytes))
assert.NoError(t, err)
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
var submitResp map[string]string
json.NewDecoder(resp.Body).Decode(&submitResp)
resp.Body.Close()
jobID := submitResp["id"]
assert.NotEmpty(t, jobID)
// We don't wait for completion in this test
// This is just to verify submission acceptance for all languages
}
}
func TestInputHandling(t *testing.T) {
server := setupTestServer()
defer server.Close()
// Test code submission with input
submitURL := server.URL + "/submit"
body := map[string]string{
"language": "python",
"code": "name = input('Enter name: ')\nprint('Hello, ' + name + '!')",
"input": "Integration Test",
}
bodyBytes, _ := json.Marshal(body)
resp, err := http.Post(submitURL, "application/json", bytes.NewReader(bodyBytes))
assert.NoError(t, err)
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
var submitResp map[string]string
json.NewDecoder(resp.Body).Decode(&submitResp)
resp.Body.Close()
jobID := submitResp["id"]
assert.NotEmpty(t, jobID)
// Wait for job to complete and check result
resultURL := server.URL + "/result?id=" + jobID
// Poll for results
var resultResp map[string]interface{}
maxRetries := 10
for i := 0; i < maxRetries; i++ {
time.Sleep(300 * time.Millisecond)
resp, err = http.Get(resultURL)
assert.NoError(t, err)
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
continue
}
json.NewDecoder(resp.Body).Decode(&resultResp)
resp.Body.Close()
status, _ := resultResp["status"].(string)
if status == "completed" || status == "failed" {
break
}
}
// Verify output contains the greeting with input
assert.Contains(t, resultResp["output"], "Hello, Integration Test!")
}

View File

@@ -1,278 +0,0 @@
import requests
import concurrent.futures
import time
import statistics
import matplotlib.pyplot as plt
import numpy as np
# Define the endpoint URLs
POST_URL = "http://localhost:8080/submit"
GET_URL_STATUS = "http://localhost:8080/status?id={}"
GET_URL_RESULT = "http://localhost:8080/result?id={}"
GET_URL_STATS = "http://localhost:8080/queue-stats"
# Test payloads for different languages
PAYLOADS = {
"python": {
"language": "python",
"code": "print('Hello, Load Test!')",
},
"java": {
"language": "java",
"code": "public class Solution { public static void main(String[] args) { System.out.println(\"Hello, Load Test!\"); } }",
},
"c": {
"language": "c",
"code": "#include <stdio.h>\nint main() { printf(\"Hello, Load Test!\\n\"); return 0; }",
},
"cpp": {
"language": "cpp",
"code": "#include <iostream>\nint main() { std::cout << \"Hello, Load Test!\" << std::endl; return 0; }",
}
}
def send_request(language, index):
"""Sends a POST request and returns (task_id, time_taken)."""
payload = PAYLOADS[language]
start_time = time.time()
try:
response = requests.post(POST_URL, json=payload, timeout=10)
end_time = time.time()
if response.status_code == 202:
return response.json().get("id"), end_time - start_time
else:
print(f"Request {index} failed with status {response.status_code}")
return None, end_time - start_time
except requests.exceptions.RequestException as e:
end_time = time.time()
print(f"Request {index} error: {e}")
return None, end_time - start_time
def wait_for_result(task_id, index):
"""Waits for a result and returns (result, time_taken)."""
if not task_id:
return None, 0
start_time = time.time()
max_retries = 30
retry_interval = 0.5 # seconds
for _ in range(max_retries):
try:
response = requests.get(GET_URL_RESULT.format(task_id), timeout=10)
if response.status_code == 200:
result = response.json()
if result.get("status") in ["completed", "failed"]:
end_time = time.time()
return result, end_time - start_time
time.sleep(retry_interval)
except requests.exceptions.RequestException as e:
print(f"Error checking result for task {index}: {e}")
end_time = time.time()
print(f"Timed out waiting for result of task {index}")
return None, end_time - start_time
def run_test(concurrency, requests_per_language):
"""Runs a load test with the specified parameters."""
languages = list(PAYLOADS.keys())
all_results = {lang: [] for lang in languages}
submit_times = {lang: [] for lang in languages}
wait_times = {lang: [] for lang in languages}
success_rates = {lang: 0 for lang in languages}
# Keep track of all submissions for each language
total_per_language = {lang: 0 for lang in languages}
successful_per_language = {lang: 0 for lang in languages}
start_time = time.time()
# Create a list of tasks
tasks = []
for lang in languages:
for i in range(requests_per_language):
tasks.append((lang, i))
print(f"Running load test with {concurrency} concurrent connections")
print(f"Sending {requests_per_language} requests per language ({len(languages)} languages)")
# Submit all tasks
task_ids = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
future_to_task = {executor.submit(send_request, lang, i): (lang, i) for lang, i in tasks}
for future in concurrent.futures.as_completed(future_to_task):
lang, i = future_to_task[future]
total_per_language[lang] += 1
try:
task_id, submit_time = future.result()
if task_id:
task_ids[(lang, i)] = task_id
submit_times[lang].append(submit_time)
except Exception as e:
print(f"Error submitting {lang} task {i}: {e}")
print(f"Submitted {len(task_ids)} tasks successfully")
# Wait for all results
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
future_to_task = {executor.submit(wait_for_result, task_ids.get((lang, i)), i): (lang, i)
for lang, i in tasks if (lang, i) in task_ids}
for future in concurrent.futures.as_completed(future_to_task):
lang, i = future_to_task[future]
try:
result, wait_time = future.result()
if result and result.get("status") == "completed":
successful_per_language[lang] += 1
all_results[lang].append(result)
wait_times[lang].append(wait_time)
except Exception as e:
print(f"Error waiting for {lang} task {i}: {e}")
end_time = time.time()
total_time = end_time - start_time
# Calculate success rates
for lang in languages:
if total_per_language[lang] > 0:
success_rates[lang] = (successful_per_language[lang] / total_per_language[lang]) * 100
else:
success_rates[lang] = 0
# Calculate statistics
stats = {
"total_time": total_time,
"requests_per_second": len(task_ids) / total_time if total_time > 0 else 0,
"success_rate": sum(success_rates.values()) / len(success_rates) if success_rates else 0,
"submit_times": {
lang: {
"avg": statistics.mean(times) if times else 0,
"min": min(times) if times else 0,
"max": max(times) if times else 0,
"p95": np.percentile(times, 95) if times else 0
} for lang, times in submit_times.items()
},
"wait_times": {
lang: {
"avg": statistics.mean(times) if times else 0,
"min": min(times) if times else 0,
"max": max(times) if times else 0,
"p95": np.percentile(times, 95) if times else 0
} for lang, times in wait_times.items()
},
"success_rates": success_rates
}
return stats, all_results
def print_stats(stats):
"""Prints test statistics."""
print("\n=== Load Test Results ===")
print(f"Total time: {stats['total_time']:.2f}s")
print(f"Requests per second: {stats['requests_per_second']:.2f}")
print(f"Overall success rate: {stats['success_rate']:.2f}%")
print("\n== Submit Times (seconds) ==")
for lang, times in stats["submit_times"].items():
print(f"{lang:<6}: avg={times['avg']:.4f}, min={times['min']:.4f}, max={times['max']:.4f}, p95={times['p95']:.4f}")
print("\n== Wait Times (seconds) ==")
for lang, times in stats["wait_times"].items():
print(f"{lang:<6}: avg={times['avg']:.4f}, min={times['min']:.4f}, max={times['max']:.4f}, p95={times['p95']:.4f}")
print("\n== Success Rates ==")
for lang, rate in stats["success_rates"].items():
print(f"{lang:<6}: {rate:.2f}%")
def plot_results(stats):
"""Creates visualizations of test results."""
languages = list(stats["submit_times"].keys())
# Plot submit times
plt.figure(figsize=(12, 10))
plt.subplot(2, 2, 1)
plt.title("Average Submit Time by Language")
avg_times = [stats["submit_times"][lang]["avg"] for lang in languages]
plt.bar(languages, avg_times)
plt.ylabel("Time (seconds)")
plt.subplot(2, 2, 2)
plt.title("Average Wait Time by Language")
avg_wait_times = [stats["wait_times"][lang]["avg"] for lang in languages]
plt.bar(languages, avg_wait_times)
plt.ylabel("Time (seconds)")
plt.subplot(2, 2, 3)
plt.title("Success Rate by Language")
success_rates = [stats["success_rates"][lang] for lang in languages]
plt.bar(languages, success_rates)
plt.ylabel("Success Rate (%)")
plt.ylim(0, 100)
plt.subplot(2, 2, 4)
plt.title("95th Percentile Wait Time by Language")
p95_times = [stats["wait_times"][lang]["p95"] for lang in languages]
plt.bar(languages, p95_times)
plt.ylabel("Time (seconds)")
plt.tight_layout()
plt.savefig("load_test_results.png")
print("Results saved to load_test_results.png")
def main():
# Run tests with different concurrency levels
concurrency_levels = [10, 20, 30]
requests_per_language = 10
all_stats = []
for concurrency in concurrency_levels:
stats, results = run_test(concurrency, requests_per_language)
all_stats.append((concurrency, stats))
print_stats(stats)
# Create comparison visualization
plt.figure(figsize=(12, 8))
plt.subplot(2, 2, 1)
plt.title("Requests per Second vs Concurrency")
plt.plot([s[0] for s in all_stats], [s[1]["requests_per_second"] for s in all_stats], "o-")
plt.xlabel("Concurrency Level")
plt.ylabel("Requests per Second")
plt.subplot(2, 2, 2)
plt.title("Success Rate vs Concurrency")
plt.plot([s[0] for s in all_stats], [s[1]["success_rate"] for s in all_stats], "o-")
plt.xlabel("Concurrency Level")
plt.ylabel("Success Rate (%)")
plt.ylim(0, 100)
plt.subplot(2, 2, 3)
plt.title("Average Submit Time vs Concurrency")
for lang in PAYLOADS.keys():
plt.plot([s[0] for s in all_stats],
[s[1]["submit_times"][lang]["avg"] for s in all_stats],
"o-", label=lang)
plt.xlabel("Concurrency Level")
plt.ylabel("Average Submit Time (s)")
plt.legend()
plt.subplot(2, 2, 4)
plt.title("Average Wait Time vs Concurrency")
for lang in PAYLOADS.keys():
plt.plot([s[0] for s in all_stats],
[s[1]["wait_times"][lang]["avg"] for s in all_stats],
"o-", label=lang)
plt.xlabel("Concurrency Level")
plt.ylabel("Average Wait Time (s)")
plt.legend()
plt.tight_layout()
plt.savefig("concurrency_comparison.png")
print("Concurrency comparison saved to concurrency_comparison.png")
# Plot detailed results for the highest concurrency test
plot_results(all_stats[-1][1])
if __name__ == "__main__":
main()

View File

@@ -1 +0,0 @@
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1

Binary file not shown.

37
new-backend/Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
FROM golang:1.19-alpine AS builder
# Install git and required dependencies
RUN apk update && apk add --no-cache git
# Set working directory
WORKDIR /app
# Copy go mod and sum files
COPY go.mod go.sum* ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the application with optimizations
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o monaco-backend .
# Use a smaller image for the final container
FROM alpine:latest
# Install Docker client (required for container-in-container execution)
RUN apk update && apk add --no-cache docker-cli
# Create a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Copy the binary from builder
COPY --from=builder /app/monaco-backend /monaco-backend
# Use non-root user
USER appuser
# Run the binary
ENTRYPOINT ["/monaco-backend"]

93
new-backend/README.md Normal file
View File

@@ -0,0 +1,93 @@
# Monaco Code Execution Backend
A modern, secure, and efficient code execution backend inspired by online code editors like Programiz. This backend is written in Go and uses Docker containers for secure code execution.
## Features
- **Multi-language Support**: Execute code in Python, Java, C, C++, JavaScript, and Go
- **Real-time Output**: Stream code execution output via WebSockets
- **Interactive Input**: Send input to running programs via WebSockets
- **Secure Execution**: All code runs in isolated Docker containers
- **Resource Limits**: Memory, CPU, and execution time limits
- **Scalable Architecture**: Concurrent execution with configurable worker pools
## Requirements
- Go 1.19+
- Docker
- Git (for development)
## Getting Started
### Running Locally
1. Clone the repository:
```bash
git clone https://github.com/your-username/monaco.git
cd monaco/new-backend
```
2. Install dependencies:
```bash
go mod download
```
3. Build and run:
```bash
go run main.go
```
The server will start on `http://localhost:8080` by default.
### Using Docker
Build and run using Docker:
```bash
docker build -t monaco-backend .
docker run -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock monaco-backend
```
Note: Mounting the Docker socket is necessary for container-in-container execution.
## API Endpoints
- `POST /api/submit`: Submit code for execution
- `GET /api/status/{id}`: Get execution status
- `GET /api/result/{id}`: Get complete execution result
- `GET /api/languages`: List supported languages
- `GET /api/health`: Health check endpoint
- `WS /api/ws/terminal/{id}`: WebSocket for real-time output
## WebSocket Communication
The `/api/ws/terminal/{id}` endpoint supports these message types:
- `output`: Code execution output
- `input`: User input to the program
- `input_prompt`: Input prompt detected
- `status`: Execution status updates
- `error`: Error messages
## Configuration
Configuration is handled through environment variables:
- `PORT`: Server port (default: 8080)
- `CONCURRENT_EXECUTIONS`: Number of concurrent executions (default: 5)
- `QUEUE_CAPACITY`: Execution queue capacity (default: 100)
- `DEFAULT_TIMEOUT`: Default execution timeout in seconds (default: 30)
See `config/config.go` for more configuration options.
## Security Considerations
- All code execution happens in isolated Docker containers
- Network access is disabled in execution containers
- Memory and CPU limits are enforced
- Process limits prevent fork bombs
- Execution timeouts prevent infinite loops
## License
MIT

175
new-backend/api/handler.go Normal file
View File

@@ -0,0 +1,175 @@
package api
import (
"encoding/json"
"log"
"net/http"
"time"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/ishikabhoyar/monaco/new-backend/executor"
"github.com/ishikabhoyar/monaco/new-backend/models"
)
// Handler manages all API routes
type Handler struct {
executor *executor.CodeExecutor
upgrader websocket.Upgrader
}
// NewHandler creates a new API handler
func NewHandler(executor *executor.CodeExecutor) *Handler {
return &Handler{
executor: executor,
upgrader: websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // Allow all origins for development
},
HandshakeTimeout: 10 * time.Second,
},
}
}
// RegisterRoutes sets up all API routes
func (h *Handler) RegisterRoutes(router *mux.Router) {
// Code execution endpoints
router.HandleFunc("/api/submit", h.SubmitCodeHandler).Methods("POST")
router.HandleFunc("/api/status/{id}", h.StatusHandler).Methods("GET")
router.HandleFunc("/api/result/{id}", h.ResultHandler).Methods("GET")
// WebSocket endpoint for real-time output
router.HandleFunc("/api/ws/terminal/{id}", h.TerminalWebSocketHandler)
// Language support endpoint
router.HandleFunc("/api/languages", h.SupportedLanguagesHandler).Methods("GET")
// Health check
router.HandleFunc("/api/health", h.HealthCheckHandler).Methods("GET")
}
// SubmitCodeHandler handles code submission requests
func (h *Handler) SubmitCodeHandler(w http.ResponseWriter, r *http.Request) {
// Parse request
var submission models.CodeSubmission
if err := json.NewDecoder(r.Body).Decode(&submission); err != nil {
http.Error(w, "Invalid request format", http.StatusBadRequest)
return
}
// Validate request
if submission.Code == "" {
http.Error(w, "Code cannot be empty", http.StatusBadRequest)
return
}
if submission.Language == "" {
http.Error(w, "Language must be specified", http.StatusBadRequest)
return
}
// Generate ID if not provided
if submission.ID == "" {
submission.ID = uuid.New().String()
}
// Submit code for execution
id := h.executor.SubmitCode(&submission)
// Return response
response := models.SubmissionResponse{
ID: id,
Status: "queued",
Message: "Code submission accepted and queued for execution",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// StatusHandler returns the current status of a code execution
func (h *Handler) StatusHandler(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
submission, exists := h.executor.GetSubmission(id)
if !exists {
http.Error(w, "Submission not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"id": submission.ID,
"status": submission.Status,
})
}
// ResultHandler returns the complete result of a code execution
func (h *Handler) ResultHandler(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
submission, exists := h.executor.GetSubmission(id)
if !exists {
http.Error(w, "Submission not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(submission)
}
// TerminalWebSocketHandler handles WebSocket connections for real-time output
func (h *Handler) TerminalWebSocketHandler(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
// Check if submission exists
if _, exists := h.executor.GetSubmission(id); !exists {
http.Error(w, "Submission not found", http.StatusNotFound)
return
}
// Upgrade connection to WebSocket
conn, err := h.upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade failed: %v", err)
return
}
log.Printf("WebSocket connection established for submission %s", id)
// Register connection
h.executor.RegisterTerminalConnection(id, conn)
// Connection will be handled by the executor
}
// SupportedLanguagesHandler returns a list of supported languages
func (h *Handler) SupportedLanguagesHandler(w http.ResponseWriter, r *http.Request) {
// This is a placeholder - in a real implementation, you'd get this from the config
languages := []map[string]string{
{"id": "python", "name": "Python", "version": "3.9"},
{"id": "java", "name": "Java", "version": "11"},
{"id": "c", "name": "C", "version": "GCC 10.2"},
{"id": "cpp", "name": "C++", "version": "GCC 10.2"},
{"id": "javascript", "name": "JavaScript", "version": "Node.js 16"},
{"id": "golang", "name": "Go", "version": "1.19"},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(languages)
}
// HealthCheckHandler provides a simple health check endpoint
func (h *Handler) HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "ok",
"time": time.Now().Format(time.RFC3339),
})
}

View File

@@ -0,0 +1,168 @@
package config
import (
"os"
"strconv"
"time"
)
// Config holds all configuration for the application
type Config struct {
Server ServerConfig
Executor ExecutorConfig
Languages map[string]LanguageConfig
Sandbox SandboxConfig
}
// ServerConfig holds server-related configurations
type ServerConfig struct {
Port string
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
}
// ExecutorConfig holds executor-related configurations
type ExecutorConfig struct {
ConcurrentExecutions int
QueueCapacity int
DefaultTimeout time.Duration
}
// LanguageConfig holds language-specific configurations
type LanguageConfig struct {
Name string
Image string
MemoryLimit string
CPULimit string
TimeoutSec int
CompileCmd []string
RunCmd []string
FileExt string
VersionCmd []string
}
// SandboxConfig holds sandbox-related configurations
type SandboxConfig struct {
NetworkDisabled bool
MemorySwapLimit string
PidsLimit int64
}
// GetConfig returns the application configuration
func GetConfig() *Config {
return &Config{
Server: ServerConfig{
Port: getEnv("PORT", "8080"),
ReadTimeout: time.Duration(getEnvAsInt("READ_TIMEOUT", 15)) * time.Second,
WriteTimeout: time.Duration(getEnvAsInt("WRITE_TIMEOUT", 15)) * time.Second,
IdleTimeout: time.Duration(getEnvAsInt("IDLE_TIMEOUT", 60)) * time.Second,
},
Executor: ExecutorConfig{
ConcurrentExecutions: getEnvAsInt("CONCURRENT_EXECUTIONS", 5),
QueueCapacity: getEnvAsInt("QUEUE_CAPACITY", 100),
DefaultTimeout: time.Duration(getEnvAsInt("DEFAULT_TIMEOUT", 30)) * time.Second,
},
Languages: getLanguageConfigs(),
Sandbox: SandboxConfig{
NetworkDisabled: getEnvAsBool("SANDBOX_NETWORK_DISABLED", true),
MemorySwapLimit: getEnv("SANDBOX_MEMORY_SWAP_LIMIT", "0"),
PidsLimit: int64(getEnvAsInt("SANDBOX_PIDS_LIMIT", 50)),
},
}
}
// getLanguageConfigs returns configurations for all supported languages
func getLanguageConfigs() map[string]LanguageConfig {
return map[string]LanguageConfig{
"python": {
Name: "Python",
Image: "python:3.9-slim",
MemoryLimit: "100m",
CPULimit: "0.1",
TimeoutSec: 30,
RunCmd: []string{"python", "-c"},
FileExt: ".py",
VersionCmd: []string{"python", "--version"},
},
"java": {
Name: "Java",
Image: "eclipse-temurin:11-jdk",
MemoryLimit: "400m",
CPULimit: "0.5",
TimeoutSec: 60,
CompileCmd: []string{"javac"},
RunCmd: []string{"java"},
FileExt: ".java",
VersionCmd: []string{"java", "-version"},
},
"c": {
Name: "C",
Image: "gcc:latest",
MemoryLimit: "100m",
CPULimit: "0.1",
TimeoutSec: 30,
CompileCmd: []string{"gcc", "-o", "program"},
RunCmd: []string{"./program"},
FileExt: ".c",
VersionCmd: []string{"gcc", "--version"},
},
"cpp": {
Name: "C++",
Image: "gcc:latest",
MemoryLimit: "100m",
CPULimit: "0.1",
TimeoutSec: 30,
CompileCmd: []string{"g++", "-o", "program"},
RunCmd: []string{"./program"},
FileExt: ".cpp",
VersionCmd: []string{"g++", "--version"},
},
"javascript": {
Name: "JavaScript",
Image: "node:16-alpine",
MemoryLimit: "100m",
CPULimit: "0.1",
TimeoutSec: 30,
RunCmd: []string{"node", "-e"},
FileExt: ".js",
VersionCmd: []string{"node", "--version"},
},
"golang": {
Name: "Go",
Image: "golang:1.19-alpine",
MemoryLimit: "100m",
CPULimit: "0.1",
TimeoutSec: 30,
CompileCmd: []string{"go", "build", "-o", "program"},
RunCmd: []string{"./program"},
FileExt: ".go",
VersionCmd: []string{"go", "version"},
},
}
}
// Helper functions to get environment variables with defaults
func getEnv(key, defaultValue string) string {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
return value
}
func getEnvAsInt(key string, defaultValue int) int {
valueStr := getEnv(key, "")
if value, err := strconv.Atoi(valueStr); err == nil {
return value
}
return defaultValue
}
func getEnvAsBool(key string, defaultValue bool) bool {
valueStr := getEnv(key, "")
if value, err := strconv.ParseBool(valueStr); err == nil {
return value
}
return defaultValue
}

View File

@@ -0,0 +1,615 @@
package executor
import (
"bytes"
"context"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/ishikabhoyar/monaco/new-backend/config"
"github.com/ishikabhoyar/monaco/new-backend/models"
)
// CodeExecutor handles code execution for all languages
type CodeExecutor struct {
config *config.Config
execQueue chan *models.CodeSubmission
submissions map[string]*models.CodeSubmission
submissionsMutex sync.RWMutex
terminalConnections map[string][]*websocket.Conn
terminalMutex sync.RWMutex
inputChannels map[string]chan string
inputMutex sync.RWMutex
}
// NewCodeExecutor creates a new code executor with specified capacity
func NewCodeExecutor(cfg *config.Config) *CodeExecutor {
executor := &CodeExecutor{
config: cfg,
execQueue: make(chan *models.CodeSubmission, cfg.Executor.QueueCapacity),
submissions: make(map[string]*models.CodeSubmission),
terminalConnections: make(map[string][]*websocket.Conn),
inputChannels: make(map[string]chan string),
}
// Start worker goroutines
for i := 0; i < cfg.Executor.ConcurrentExecutions; i++ {
go executor.worker(i)
}
log.Printf("Started %d code execution workers", cfg.Executor.ConcurrentExecutions)
return executor
}
// SubmitCode adds a code submission to the execution queue
func (e *CodeExecutor) SubmitCode(submission *models.CodeSubmission) string {
// Generate ID if not provided
if submission.ID == "" {
submission.ID = uuid.New().String()
}
submission.Status = "queued"
submission.QueuedAt = time.Now()
// Store submission
e.submissionsMutex.Lock()
e.submissions[submission.ID] = submission
e.submissionsMutex.Unlock()
// Send to execution queue
e.execQueue <- submission
log.Printf("Submission queued: %s, language: %s", submission.ID, submission.Language)
return submission.ID
}
// GetSubmission returns a submission by ID
func (e *CodeExecutor) GetSubmission(id string) (*models.CodeSubmission, bool) {
e.submissionsMutex.RLock()
defer e.submissionsMutex.RUnlock()
submission, exists := e.submissions[id]
return submission, exists
}
// RegisterTerminalConnection registers a WebSocket connection for streaming output
func (e *CodeExecutor) RegisterTerminalConnection(submissionID string, conn *websocket.Conn) {
e.terminalMutex.Lock()
defer e.terminalMutex.Unlock()
e.terminalConnections[submissionID] = append(e.terminalConnections[submissionID], conn)
log.Printf("WebSocket connection registered for submission %s (total: %d)",
submissionID, len(e.terminalConnections[submissionID]))
// Set up a reader to handle input from WebSocket
go e.handleTerminalInput(submissionID, conn)
}
// UnregisterTerminalConnection removes a WebSocket connection
func (e *CodeExecutor) UnregisterTerminalConnection(submissionID string, conn *websocket.Conn) {
e.terminalMutex.Lock()
defer e.terminalMutex.Unlock()
connections := e.terminalConnections[submissionID]
for i, c := range connections {
if c == conn {
// Remove the connection
e.terminalConnections[submissionID] = append(connections[:i], connections[i+1:]...)
break
}
}
// Clean up if no more connections
if len(e.terminalConnections[submissionID]) == 0 {
delete(e.terminalConnections, submissionID)
}
log.Printf("WebSocket connection unregistered for submission %s", submissionID)
}
// handleTerminalInput reads input from the WebSocket and forwards it to the running process
func (e *CodeExecutor) handleTerminalInput(submissionID string, conn *websocket.Conn) {
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Printf("WebSocket read error: %v", err)
break
}
// If there's an input channel, send the input
e.inputMutex.RLock()
if inputChan, exists := e.inputChannels[submissionID]; exists {
select {
case inputChan <- string(message):
log.Printf("Input sent to process: %s", string(message))
default:
log.Printf("Input channel is full or closed, input ignored")
}
}
e.inputMutex.RUnlock()
}
// When connection is closed, unregister it
e.UnregisterTerminalConnection(submissionID, conn)
}
// sendToTerminals sends output to all registered WebSocket connections
func (e *CodeExecutor) sendToTerminals(submissionID string, message models.WebSocketMessage) {
e.terminalMutex.RLock()
connections := e.terminalConnections[submissionID]
e.terminalMutex.RUnlock()
if len(connections) == 0 {
return
}
for _, conn := range connections {
err := conn.WriteJSON(message)
if err != nil {
log.Printf("WebSocket write error: %v", err)
// Consider unregistering the connection on error
}
}
}
// worker processes code execution jobs from the queue
func (e *CodeExecutor) worker(id int) {
log.Printf("Worker %d started", id)
for submission := range e.execQueue {
log.Printf("Worker %d processing submission %s (%s)", id, submission.ID, submission.Language)
// Update status to running
submission.Status = "running"
submission.StartedAt = time.Now()
e.sendToTerminals(submission.ID, models.NewStatusMessage("running", "", ""))
// Execute the code according to language
e.executeCode(submission)
// Update completion time
submission.CompletedAt = time.Now()
executionTime := submission.CompletedAt.Sub(submission.StartedAt).Seconds()
submission.ExecutionTime = executionTime
// Send completion status
e.sendToTerminals(submission.ID, models.NewStatusMessage(submission.Status, "", ""))
// Send a notification that terminal will close soon
e.sendToTerminals(submission.ID, models.NewSystemMessage("Connection will close in 5 seconds"))
// Add delay to keep the connection open longer
time.Sleep(5 * time.Second)
log.Printf("Worker %d completed submission %s in %.2f seconds", id, submission.ID, executionTime)
}
}
// executeCode orchestrates the execution of code for different languages
func (e *CodeExecutor) executeCode(submission *models.CodeSubmission) {
langConfig, exists := e.config.Languages[strings.ToLower(submission.Language)]
if !exists {
submission.Status = "failed"
submission.Output = "Unsupported language: " + submission.Language
return
}
// Create a temporary directory for this submission
tempDir, err := os.MkdirTemp("", fmt.Sprintf("%s-code-%s-", submission.Language, submission.ID))
if err != nil {
submission.Status = "failed"
submission.Output = "Failed to create execution environment: " + err.Error()
return
}
defer os.RemoveAll(tempDir)
// Choose execution strategy based on language
switch strings.ToLower(submission.Language) {
case "python":
e.executePython(submission, tempDir, langConfig)
case "java":
e.executeJava(submission, tempDir, langConfig)
case "c":
e.executeC(submission, tempDir, langConfig)
case "cpp":
e.executeCpp(submission, tempDir, langConfig)
case "javascript":
e.executeJavaScript(submission, tempDir, langConfig)
case "golang":
e.executeGolang(submission, tempDir, langConfig)
default:
submission.Status = "failed"
submission.Output = "Unsupported language: " + submission.Language
}
}
// executePython executes Python code
func (e *CodeExecutor) executePython(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
// Write code to file
codeFile := filepath.Join(tempDir, "code"+langConfig.FileExt)
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
// Setup Docker run command
cmd := exec.Command(
"docker", "run", "--rm", "-i",
"--network=none",
"--memory="+langConfig.MemoryLimit,
"--cpu-quota="+fmt.Sprintf("%d", int(float64(100000)*0.1)), // 10% CPU
"--pids-limit=20",
"-v", tempDir+":/code",
langConfig.Image,
"python", "/code/code.py",
)
// Execute the code with input handling
e.executeWithIO(cmd, submission, time.Duration(langConfig.TimeoutSec)*time.Second)
}
// executeJava executes Java code
func (e *CodeExecutor) executeJava(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
// Extract class name from code
className := extractJavaClassName(submission.Code)
// Write code to file
codeFile := filepath.Join(tempDir, className+langConfig.FileExt)
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
// Compile Java code
compileCmd := exec.Command(
"docker", "run", "--rm",
"-v", tempDir+":/code",
langConfig.Image,
"javac", "/code/"+className+".java",
)
compileOutput, compileErr := compileCmd.CombinedOutput()
if compileErr != nil {
submission.Status = "failed"
submission.Output = "Compilation error:\n" + string(compileOutput)
e.sendToTerminals(submission.ID, models.NewOutputMessage(string(compileOutput), true))
return
}
// Setup Docker run command for execution
cmd := exec.Command(
"docker", "run", "--rm", "-i",
"--network=none",
"--memory="+langConfig.MemoryLimit,
"--cpu-quota="+fmt.Sprintf("%d", int(float64(100000)*0.5)), // 50% CPU
"--pids-limit=20",
"-v", tempDir+":/code",
langConfig.Image,
"java", "-XX:+TieredCompilation", "-XX:TieredStopAtLevel=1",
"-Xms64m", "-Xmx256m",
"-cp", "/code", className,
)
// Execute the code with input handling
e.executeWithIO(cmd, submission, time.Duration(langConfig.TimeoutSec)*time.Second)
}
// executeC executes C code
func (e *CodeExecutor) executeC(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
// Write code to file
codeFile := filepath.Join(tempDir, "code"+langConfig.FileExt)
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
// Compile C code
compileCmd := exec.Command(
"docker", "run", "--rm",
"-v", tempDir+":/code",
langConfig.Image,
"gcc", "-o", "/code/program", "/code/code.c",
)
compileOutput, compileErr := compileCmd.CombinedOutput()
if compileErr != nil {
submission.Status = "failed"
submission.Output = "Compilation error:\n" + string(compileOutput)
e.sendToTerminals(submission.ID, models.NewOutputMessage(string(compileOutput), true))
return
}
// Setup Docker run command
cmd := exec.Command(
"docker", "run", "--rm", "-i",
"--network=none",
"--memory="+langConfig.MemoryLimit,
"--cpu-quota="+fmt.Sprintf("%d", int(float64(100000)*0.1)), // 10% CPU
"--pids-limit=20",
"-v", tempDir+":/code",
langConfig.Image,
"/code/program",
)
// Execute the code with input handling
e.executeWithIO(cmd, submission, time.Duration(langConfig.TimeoutSec)*time.Second)
}
// executeCpp executes C++ code
func (e *CodeExecutor) executeCpp(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
// Write code to file
codeFile := filepath.Join(tempDir, "code"+langConfig.FileExt)
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
// Compile C++ code
compileCmd := exec.Command(
"docker", "run", "--rm",
"-v", tempDir+":/code",
langConfig.Image,
"g++", "-o", "/code/program", "/code/code.cpp",
)
compileOutput, compileErr := compileCmd.CombinedOutput()
if compileErr != nil {
submission.Status = "failed"
submission.Output = "Compilation error:\n" + string(compileOutput)
e.sendToTerminals(submission.ID, models.NewOutputMessage(string(compileOutput), true))
return
}
// Setup Docker run command
cmd := exec.Command(
"docker", "run", "--rm", "-i",
"--network=none",
"--memory="+langConfig.MemoryLimit,
"--cpu-quota="+fmt.Sprintf("%d", int(float64(100000)*0.1)), // 10% CPU
"--pids-limit=20",
"-v", tempDir+":/code",
langConfig.Image,
"/code/program",
)
// Execute the code with input handling
e.executeWithIO(cmd, submission, time.Duration(langConfig.TimeoutSec)*time.Second)
}
// executeJavaScript executes JavaScript code
func (e *CodeExecutor) executeJavaScript(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
// Write code to file
codeFile := filepath.Join(tempDir, "code"+langConfig.FileExt)
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
// Setup Docker run command
cmd := exec.Command(
"docker", "run", "--rm", "-i",
"--network=none",
"--memory="+langConfig.MemoryLimit,
"--cpu-quota="+fmt.Sprintf("%d", int(float64(100000)*0.1)), // 10% CPU
"--pids-limit=20",
"-v", tempDir+":/code",
langConfig.Image,
"node", "/code/code.js",
)
// Execute the code with input handling
e.executeWithIO(cmd, submission, time.Duration(langConfig.TimeoutSec)*time.Second)
}
// executeGolang executes Go code
func (e *CodeExecutor) executeGolang(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
// Write code to file
codeFile := filepath.Join(tempDir, "code"+langConfig.FileExt)
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
// Setup Docker run command to compile and run in one step
cmd := exec.Command(
"docker", "run", "--rm", "-i",
"--network=none",
"--memory="+langConfig.MemoryLimit,
"--cpu-quota="+fmt.Sprintf("%d", int(float64(100000)*0.1)), // 10% CPU
"--pids-limit=20",
"-v", tempDir+":/code",
"-w", "/code",
langConfig.Image,
"go", "run", "/code/code.go",
)
// Execute the code with input handling
e.executeWithIO(cmd, submission, time.Duration(langConfig.TimeoutSec)*time.Second)
}
// executeWithIO runs a command with input/output handling through WebSockets
func (e *CodeExecutor) executeWithIO(cmd *exec.Cmd, submission *models.CodeSubmission, timeout time.Duration) {
// Create pipes for stdin, stdout and stderr
stdin, err := cmd.StdinPipe()
if err != nil {
submission.Status = "failed"
submission.Output = "Failed to create stdin pipe: " + err.Error()
return
}
stdout, err := cmd.StdoutPipe()
if err != nil {
submission.Status = "failed"
submission.Output = "Failed to create stdout pipe: " + err.Error()
return
}
stderr, err := cmd.StderrPipe()
if err != nil {
submission.Status = "failed"
submission.Output = "Failed to create stderr pipe: " + err.Error()
return
}
// Create an input channel for this submission
inputChan := make(chan string, 10)
e.inputMutex.Lock()
e.inputChannels[submission.ID] = inputChan
e.inputMutex.Unlock()
// Clean up when done
defer func() {
e.inputMutex.Lock()
delete(e.inputChannels, submission.ID)
e.inputMutex.Unlock()
close(inputChan)
}()
// Start the command
if err := cmd.Start(); err != nil {
submission.Status = "failed"
submission.Output = "Failed to start process: " + err.Error()
return
}
// Output buffer to collect all output
var outputBuffer bytes.Buffer
// Send initial input if provided
if submission.Input != "" {
io.WriteString(stdin, submission.Input+"\n")
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// Handle stdout in a goroutine
go func() {
buffer := make([]byte, 1024)
for {
n, err := stdout.Read(buffer)
if n > 0 {
data := buffer[:n]
outputBuffer.Write(data)
// Send real-time output to terminals
e.sendToTerminals(submission.ID, models.NewOutputMessage(string(data), false))
}
if err != nil {
if err != io.EOF {
log.Printf("Stdout read error: %v", err)
}
break
}
}
}()
// Handle stderr in a goroutine
go func() {
buffer := make([]byte, 1024)
for {
n, err := stderr.Read(buffer)
if n > 0 {
data := buffer[:n]
outputBuffer.Write(data)
// Send real-time error output to terminals
e.sendToTerminals(submission.ID, models.NewOutputMessage(string(data), true))
}
if err != nil {
if err != io.EOF {
log.Printf("Stderr read error: %v", err)
}
break
}
}
}()
// Listen for input from WebSocket
go func() {
for {
select {
case input, ok := <-inputChan:
if !ok {
return
}
stdin.Write([]byte(input + "\n"))
case <-ctx.Done():
return
}
}
}()
// Wait for command to complete or timeout
done := make(chan error)
go func() {
done <- cmd.Wait()
}()
// Wait for completion or timeout
select {
case <-ctx.Done():
// Process timed out
if ctx.Err() == context.DeadlineExceeded {
log.Printf("Process timed out for submission %s", submission.ID)
submission.Status = "failed"
submission.Output = outputBuffer.String() + "\nExecution timed out after " + timeout.String()
e.sendToTerminals(submission.ID, models.NewErrorMessage("timeout", "Execution timed out after "+timeout.String()))
// Attempt to kill the process
if err := cmd.Process.Kill(); err != nil {
log.Printf("Failed to kill process: %v", err)
}
}
case err := <-done:
// Process completed
if err != nil {
log.Printf("Process error: %v", err)
submission.Status = "failed"
// Don't overwrite output, as stderr has already been captured
} else {
submission.Status = "completed"
}
}
// Store the complete output
submission.Output = outputBuffer.String()
}
// Helper function to extract Java class name
func extractJavaClassName(code string) string {
// Default class name as fallback
defaultClass := "Solution"
// Look for public class
re := regexp.MustCompile(`public\s+class\s+(\w+)`)
matches := re.FindStringSubmatch(code)
if len(matches) > 1 {
return matches[1]
}
// Look for any class if no public class
re = regexp.MustCompile(`class\s+(\w+)`)
matches = re.FindStringSubmatch(code)
if len(matches) > 1 {
return matches[1]
}
return defaultClass
}

10
new-backend/go.mod Normal file
View File

@@ -0,0 +1,10 @@
module github.com/ishikabhoyar/monaco/new-backend
go 1.19
require (
github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0
github.com/rs/cors v1.8.3
)

8
new-backend/go.sum Normal file
View File

@@ -0,0 +1,8 @@
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo=
github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=

98
new-backend/main.go Normal file
View File

@@ -0,0 +1,98 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gorilla/mux"
"github.com/ishikabhoyar/monaco/new-backend/api"
"github.com/ishikabhoyar/monaco/new-backend/config"
"github.com/ishikabhoyar/monaco/new-backend/executor"
"github.com/ishikabhoyar/monaco/new-backend/utils"
"github.com/rs/cors"
)
func main() {
// Configure logging
log.SetFlags(log.LstdFlags | log.Lshortfile | log.Lmicroseconds)
log.Println("Starting Monaco Code Execution Server...")
// Check if Docker is available
if !utils.DockerAvailable() {
log.Fatal("Docker is required but not available on this system")
}
// Load configuration
cfg := config.GetConfig()
log.Printf("Loaded configuration (max workers: %d, queue capacity: %d)",
cfg.Executor.ConcurrentExecutions, cfg.Executor.QueueCapacity)
// Initialize code executor
codeExecutor := executor.NewCodeExecutor(cfg)
log.Println("Code executor initialized")
// Initialize API handler
handler := api.NewHandler(codeExecutor)
// Setup router with middleware
router := mux.NewRouter()
// Register API routes
handler.RegisterRoutes(router)
// Add a simple welcome route
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Monaco Code Execution Server v1.0.0")
})
// Configure CORS
corsHandler := cors.New(cors.Options{
AllowedOrigins: []string{"*"}, // For development - restrict in production
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 300, // Maximum cache time for preflight requests
})
// Create server with timeouts
server := &http.Server{
Addr: ":" + cfg.Server.Port,
Handler: corsHandler.Handler(router),
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
IdleTimeout: cfg.Server.IdleTimeout,
}
// Channel for graceful shutdown
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
// Start server in a goroutine
go func() {
log.Printf("Server listening on port %s", cfg.Server.Port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Error starting server: %v", err)
}
}()
// Wait for interrupt signal
<-stop
log.Println("Shutting down server...")
// Create context with timeout for graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Shutdown server gracefully
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server shutdown error: %v", err)
}
log.Println("Server stopped gracefully")
}

View File

@@ -0,0 +1,28 @@
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,omitempty"`
Status string `json:"status"` // "queued", "running", "completed", "failed"
QueuedAt time.Time `json:"queuedAt"`
StartedAt time.Time `json:"startedAt,omitempty"`
CompletedAt time.Time `json:"completedAt,omitempty"`
Output string `json:"output"`
Memory string `json:"memory,omitempty"` // Memory usage statistics
CPU string `json:"cpu,omitempty"` // CPU usage statistics
ExecutionTime float64 `json:"executionTime,omitempty"` // Execution time in seconds
}
// SubmissionResponse is the response returned after submitting code
type SubmissionResponse struct {
ID string `json:"id"`
Status string `json:"status"`
Message string `json:"message"`
}

View File

@@ -0,0 +1,91 @@
package models
// WebSocketMessage represents a message sent over WebSockets
type WebSocketMessage struct {
Type string `json:"type"`
Content interface{} `json:"content"`
}
// OutputMessage is sent when program produces output
type OutputMessage struct {
Text string `json:"text"`
IsError bool `json:"isError"`
}
// InputMessage is sent when user provides input
type InputMessage struct {
Text string `json:"text"`
}
// StatusUpdateMessage is sent when execution status changes
type StatusUpdateMessage struct {
Status string `json:"status"`
Memory string `json:"memory,omitempty"`
CPU string `json:"cpu,omitempty"`
}
// ErrorMessage is sent when an error occurs
type ErrorMessage struct {
ErrorType string `json:"errorType"`
Message string `json:"message"`
}
// NewOutputMessage creates a standard output message
func NewOutputMessage(content string, isError bool) WebSocketMessage {
return WebSocketMessage{
Type: "output",
Content: OutputMessage{
Text: content,
IsError: isError,
},
}
}
// NewInputPromptMessage creates an input prompt message
func NewInputPromptMessage(prompt string) WebSocketMessage {
return WebSocketMessage{
Type: "input_prompt",
Content: prompt,
}
}
// NewInputMessage creates a user input message
func NewInputMessage(input string) WebSocketMessage {
return WebSocketMessage{
Type: "input",
Content: InputMessage{
Text: input,
},
}
}
// NewStatusMessage creates a status update message
func NewStatusMessage(status, memory, cpu string) WebSocketMessage {
return WebSocketMessage{
Type: "status",
Content: StatusUpdateMessage{
Status: status,
Memory: memory,
CPU: cpu,
},
}
}
// NewErrorMessage creates an error message
func NewErrorMessage(errorType, message string) WebSocketMessage {
return WebSocketMessage{
Type: "error",
Content: ErrorMessage{
ErrorType: errorType,
Message: message,
},
}
}
// NewSystemMessage creates a system notification message
func NewSystemMessage(message string) WebSocketMessage {
return WebSocketMessage{
Type: "system",
Content: message,
}
}

106
new-backend/utils/utils.go Normal file
View File

@@ -0,0 +1,106 @@
package utils
import (
"log"
"os"
"os/exec"
"regexp"
"strings"
)
// DockerAvailable checks if Docker is available on the system
func DockerAvailable() bool {
cmd := exec.Command("docker", "--version")
if err := cmd.Run(); err != nil {
log.Printf("Docker not available: %v", err)
return false
}
return true
}
// PullDockerImage pulls a Docker image if it doesn't exist
func PullDockerImage(image string) error {
// Check if image exists
checkCmd := exec.Command("docker", "image", "inspect", image)
if err := checkCmd.Run(); err == nil {
// Image exists
return nil
}
// Pull the image
log.Printf("Pulling Docker image: %s", image)
pullCmd := exec.Command("docker", "pull", image)
pullCmd.Stdout = os.Stdout
pullCmd.Stderr = os.Stderr
return pullCmd.Run()
}
// ExtractJavaClassName extracts the class name from Java code
func ExtractJavaClassName(code string) string {
// Default class name as fallback
defaultClass := "Solution"
// Look for public class
re := regexp.MustCompile(`public\s+class\s+(\w+)`)
matches := re.FindStringSubmatch(code)
if len(matches) > 1 {
return matches[1]
}
// Look for any class if no public class
re = regexp.MustCompile(`class\s+(\w+)`)
matches = re.FindStringSubmatch(code)
if len(matches) > 1 {
return matches[1]
}
return defaultClass
}
// IsInputPrompt determines if a string is likely an input prompt
func IsInputPrompt(text string) bool {
// Early exit for empty or very long text
text = strings.TrimSpace(text)
if text == "" || len(text) > 100 {
return false
}
// Common prompt endings
if strings.HasSuffix(text, ":") || strings.HasSuffix(text, ">") ||
strings.HasSuffix(text, "?") || strings.HasSuffix(text, "...") {
return true
}
// Common prompt words
promptWords := []string{"input", "enter", "type", "provide"}
for _, word := range promptWords {
if strings.Contains(strings.ToLower(text), word) {
return true
}
}
return false
}
// SanitizeDockerArgs ensures safe Docker command arguments
func SanitizeDockerArgs(args []string) []string {
// This is a simplified version - in production, you'd want more robust checks
sanitized := make([]string, 0, len(args))
// Disallow certain dangerous flags
dangerousFlags := map[string]bool{
"--privileged": true,
"--net=host": true,
"--pid=host": true,
"--ipc=host": true,
"--userns=host": true,
}
for _, arg := range args {
if _, isDangerous := dangerousFlags[arg]; !isDangerous {
sanitized = append(sanitized, arg)
}
}
return sanitized
}