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

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
}