new backend changes
This commit is contained in:
37
new-backend/Dockerfile
Normal file
37
new-backend/Dockerfile
Normal 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
93
new-backend/README.md
Normal 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
175
new-backend/api/handler.go
Normal 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),
|
||||
})
|
||||
}
|
||||
168
new-backend/config/config.go
Normal file
168
new-backend/config/config.go
Normal 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
|
||||
}
|
||||
615
new-backend/executor/executor.go
Normal file
615
new-backend/executor/executor.go
Normal 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
10
new-backend/go.mod
Normal 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
8
new-backend/go.sum
Normal 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
98
new-backend/main.go
Normal 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")
|
||||
}
|
||||
28
new-backend/models/submission.go
Normal file
28
new-backend/models/submission.go
Normal 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"`
|
||||
}
|
||||
91
new-backend/models/ws_message.go
Normal file
91
new-backend/models/ws_message.go
Normal 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
106
new-backend/utils/utils.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user