new backend changes
This commit is contained in:
@@ -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) {
|
||||
|
||||
BIN
backend/dist/monaco_darwin_arm64_v8.0/monaco
vendored
BIN
backend/dist/monaco_darwin_arm64_v8.0/monaco
vendored
Binary file not shown.
BIN
backend/dist/monaco_linux_386_sse2/monaco
vendored
BIN
backend/dist/monaco_linux_386_sse2/monaco
vendored
Binary file not shown.
BIN
backend/dist/monaco_linux_amd64_v1/monaco
vendored
BIN
backend/dist/monaco_linux_amd64_v1/monaco
vendored
Binary file not shown.
BIN
backend/dist/monaco_linux_arm64_v8.0/monaco
vendored
BIN
backend/dist/monaco_linux_arm64_v8.0/monaco
vendored
Binary file not shown.
BIN
backend/dist/monaco_windows_386_sse2/monaco.exe
vendored
BIN
backend/dist/monaco_windows_386_sse2/monaco.exe
vendored
Binary file not shown.
BIN
backend/dist/monaco_windows_amd64_v1/monaco.exe
vendored
BIN
backend/dist/monaco_windows_amd64_v1/monaco.exe
vendored
Binary file not shown.
BIN
backend/dist/monaco_windows_arm64_v8.0/monaco.exe
vendored
BIN
backend/dist/monaco_windows_arm64_v8.0/monaco.exe
vendored
Binary file not shown.
@@ -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()
|
||||
}
|
||||
@@ -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"])
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -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"`)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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:])
|
||||
}
|
||||
@@ -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!")
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
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