working socket integration
This commit is contained in:
@@ -6,6 +6,7 @@ require github.com/stretchr/testify v1.9.0
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
|
||||
85
backend/handler/websocket.go
Normal file
85
backend/handler/websocket.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/arnab-afk/monaco/model"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
// Allow all origins for development
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
// WebSocketHandler handles WebSocket connections for code execution
|
||||
func (h *Handler) WebSocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Upgrade the HTTP connection to a WebSocket connection
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to upgrade connection: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Read the initial message containing the code submission
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("Failed to read message: %v", err)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the message as a code submission
|
||||
var submission model.CodeSubmission
|
||||
if err := json.Unmarshal(message, &submission); err != nil {
|
||||
log.Printf("Failed to parse message: %v", err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Invalid submission format"))
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the submission
|
||||
if submission.Code == "" {
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Code is required"))
|
||||
conn.Close()
|
||||
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] {
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Unsupported language: "+submission.Language))
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a unique ID for the submission
|
||||
submission.ID = h.generateID()
|
||||
submission.Status = "pending"
|
||||
|
||||
// Store the submission
|
||||
h.mu.Lock()
|
||||
h.submissions[submission.ID] = &submission
|
||||
h.mu.Unlock()
|
||||
|
||||
// Send the submission ID to the client
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Submission ID: "+submission.ID))
|
||||
|
||||
// Execute the code with WebSocket communication
|
||||
h.executionService.HandleWebSocket(conn, &submission)
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -225,9 +226,10 @@ func (h *Handler) SubmitInputHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the submission is waiting for input
|
||||
if submission.Status != "waiting_for_input" {
|
||||
http.Error(w, "Submission is not waiting for input", http.StatusBadRequest)
|
||||
// Check if the submission is waiting for input or running
|
||||
// We're more lenient here to handle race conditions
|
||||
if submission.Status != "waiting_for_input" && submission.Status != "running" {
|
||||
http.Error(w, fmt.Sprintf("Submission is not waiting for input (status: %s)", submission.Status), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
261
backend/internal/api/handlers/websocket.go
Normal file
261
backend/internal/api/handlers/websocket.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/executor"
|
||||
"github.com/arnab-afk/monaco/internal/models"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// WebSocketTerminal represents a terminal session over WebSocket
|
||||
type WebSocketTerminal struct {
|
||||
ID string
|
||||
Conn *websocket.Conn
|
||||
InputChan chan string
|
||||
OutputChan chan string
|
||||
Done chan struct{}
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var (
|
||||
// Configure the upgrader
|
||||
upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
// Allow all origins for development
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
// Active terminal sessions
|
||||
terminals = make(map[string]*WebSocketTerminal)
|
||||
terminalsMu sync.Mutex
|
||||
)
|
||||
|
||||
// TerminalHandler handles WebSocket connections for terminal sessions
|
||||
func (h *Handler) TerminalHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Upgrade the HTTP connection to a WebSocket connection
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to upgrade connection: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a unique ID for this terminal session
|
||||
terminalID := executor.GenerateUUID()
|
||||
|
||||
// Create channels for communication
|
||||
inputChan := make(chan string)
|
||||
outputChan := make(chan string)
|
||||
done := make(chan struct{})
|
||||
|
||||
// Create a new terminal session
|
||||
terminal := &WebSocketTerminal{
|
||||
ID: terminalID,
|
||||
Conn: conn,
|
||||
InputChan: inputChan,
|
||||
OutputChan: outputChan,
|
||||
Done: done,
|
||||
}
|
||||
|
||||
// Store the terminal session
|
||||
terminalsMu.Lock()
|
||||
terminals[terminalID] = terminal
|
||||
terminalsMu.Unlock()
|
||||
|
||||
// Send the terminal ID to the client
|
||||
if err := conn.WriteJSON(map[string]string{"type": "terminal_id", "id": terminalID}); err != nil {
|
||||
log.Printf("Failed to send terminal ID: %v", err)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle incoming messages (input from the client)
|
||||
go func() {
|
||||
defer func() {
|
||||
close(done)
|
||||
conn.Close()
|
||||
|
||||
// Remove the terminal from the map
|
||||
terminalsMu.Lock()
|
||||
delete(terminals, terminalID)
|
||||
terminalsMu.Unlock()
|
||||
|
||||
log.Printf("Terminal session %s closed", terminalID)
|
||||
}()
|
||||
|
||||
for {
|
||||
// Read message from the WebSocket
|
||||
messageType, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
log.Printf("WebSocket error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle different message types
|
||||
if messageType == websocket.TextMessage {
|
||||
// Parse the message
|
||||
input := string(message)
|
||||
|
||||
// Send the input to the execution service
|
||||
select {
|
||||
case inputChan <- input:
|
||||
// Input sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle outgoing messages (output to the client)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case output := <-outputChan:
|
||||
// Send the output to the client
|
||||
err := conn.WriteMessage(websocket.TextMessage, []byte(output))
|
||||
if err != nil {
|
||||
log.Printf("Failed to write message: %v", err)
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Keep the connection alive with ping/pong
|
||||
go func() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ExecuteCodeWebSocket executes code and streams the output over WebSocket
|
||||
func (h *Handler) ExecuteCodeWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
// Upgrade the HTTP connection to a WebSocket connection
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to upgrade connection: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Read the initial message containing the code to execute
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("Failed to read message: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the message into a code submission
|
||||
var submission models.CodeSubmission
|
||||
if err := submission.UnmarshalJSON(message); err != nil {
|
||||
log.Printf("Failed to parse submission: %v", err)
|
||||
conn.WriteJSON(map[string]string{"error": "Invalid submission format"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a unique ID for the submission
|
||||
submission.ID = executor.GenerateUUID()
|
||||
submission.Status = "pending"
|
||||
|
||||
// Store the submission
|
||||
h.mu.Lock()
|
||||
h.submissions[submission.ID] = &submission
|
||||
h.mu.Unlock()
|
||||
|
||||
// Create channels for communication
|
||||
inputChan := make(chan string)
|
||||
outputChan := make(chan string)
|
||||
done := make(chan struct{})
|
||||
|
||||
// Set up the execution service to use these channels
|
||||
h.executionService.SetupWebSocketChannels(&submission, inputChan, outputChan)
|
||||
|
||||
// Send the submission ID to the client
|
||||
if err := conn.WriteJSON(map[string]string{"type": "submission_id", "id": submission.ID}); err != nil {
|
||||
log.Printf("Failed to send submission ID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Execute the code in a goroutine
|
||||
go func() {
|
||||
h.executionService.ExecuteCodeWebSocket(&submission)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Handle incoming messages (input from the client)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
// Read message from the WebSocket
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
log.Printf("WebSocket error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Send the input to the execution service
|
||||
select {
|
||||
case inputChan <- string(message):
|
||||
// Input sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle outgoing messages (output to the client)
|
||||
for {
|
||||
select {
|
||||
case output := <-outputChan:
|
||||
// Send the output to the client
|
||||
err := conn.WriteMessage(websocket.TextMessage, []byte(output))
|
||||
if err != nil {
|
||||
log.Printf("Failed to write message: %v", err)
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
// Execution completed
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetTerminal returns a terminal session by ID
|
||||
func GetTerminal(id string) (*WebSocketTerminal, error) {
|
||||
terminalsMu.Lock()
|
||||
defer terminalsMu.Unlock()
|
||||
|
||||
terminal, exists := terminals[id]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("terminal not found: %s", id)
|
||||
}
|
||||
|
||||
return terminal, nil
|
||||
}
|
||||
@@ -23,6 +23,9 @@ type ExecutionService struct {
|
||||
mu sync.Mutex
|
||||
// Map of submission ID to input channel for interactive programs
|
||||
inputChannels map[string]chan string
|
||||
// WebSocket channels for real-time communication
|
||||
wsInputChannels map[string]chan string
|
||||
wsOutputChannels map[string]chan string
|
||||
}
|
||||
|
||||
// CodeExecutionJob represents a code execution job
|
||||
@@ -34,8 +37,10 @@ type CodeExecutionJob struct {
|
||||
// NewExecutionService creates a new execution service
|
||||
func NewExecutionService() *ExecutionService {
|
||||
return &ExecutionService{
|
||||
queue: queue.NewJobQueue(5), // 5 concurrent workers
|
||||
inputChannels: make(map[string]chan string),
|
||||
queue: queue.NewJobQueue(5), // 5 concurrent workers
|
||||
inputChannels: make(map[string]chan string),
|
||||
wsInputChannels: make(map[string]chan string),
|
||||
wsOutputChannels: make(map[string]chan string),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +122,15 @@ func (s *ExecutionService) executePython(submission *models.CodeSubmission) {
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we should use interactive mode
|
||||
if strings.Contains(submission.Code, "input(") {
|
||||
// This code likely requires interactive input
|
||||
submission.IsInteractive = true
|
||||
s.executePythonInteractive(submission, tempDir)
|
||||
return
|
||||
}
|
||||
|
||||
// Non-interactive mode
|
||||
// Create a file for input if provided
|
||||
inputPath := ""
|
||||
if submission.Input != "" {
|
||||
@@ -184,6 +198,14 @@ func (s *ExecutionService) executeJavaScript(submission *models.CodeSubmission)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we should use interactive mode
|
||||
if strings.Contains(submission.Code, "readline") && strings.Contains(submission.Code, "question") {
|
||||
// This code likely requires interactive input
|
||||
submission.IsInteractive = true
|
||||
s.executeJavaScriptInteractive(submission, tempDir)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a file for input if provided
|
||||
inputPath := ""
|
||||
if submission.Input != "" {
|
||||
|
||||
366
backend/internal/executor/interactive.go
Normal file
366
backend/internal/executor/interactive.go
Normal file
@@ -0,0 +1,366 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/models"
|
||||
)
|
||||
|
||||
// executePythonInteractive runs Python code in interactive mode
|
||||
func (s *ExecutionService) executePythonInteractive(submission *models.CodeSubmission, tempDir string) {
|
||||
log.Printf("[PYTHON-%s] Running Python in interactive mode", submission.ID)
|
||||
|
||||
// Create an input channel for this submission
|
||||
inputChan := make(chan string)
|
||||
s.mu.Lock()
|
||||
s.inputChannels[submission.ID] = inputChan
|
||||
s.mu.Unlock()
|
||||
|
||||
// Clean up when done
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
delete(s.inputChannels, submission.ID)
|
||||
close(inputChan)
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
|
||||
// Create a wrapper script that handles interactive input
|
||||
wrapperPath := filepath.Join(tempDir, "wrapper.py")
|
||||
wrapperCode := `
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
# Load the user's code
|
||||
with open('/code/code.py', 'r') as f:
|
||||
user_code = f.read()
|
||||
|
||||
# Replace the built-in input function
|
||||
original_input = input
|
||||
|
||||
def custom_input(prompt=''):
|
||||
# Print the prompt without newline
|
||||
sys.stdout.write(prompt)
|
||||
sys.stdout.flush()
|
||||
|
||||
# Signal that we're waiting for input
|
||||
sys.stdout.write('\n[WAITING_FOR_INPUT]\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
# Wait for input from the parent process
|
||||
# Use a blocking read that won't raise EOFError
|
||||
line = ''
|
||||
while True:
|
||||
try:
|
||||
char = sys.stdin.read(1)
|
||||
if char == '\n':
|
||||
break
|
||||
if char:
|
||||
line += char
|
||||
except:
|
||||
# If any error occurs, wait a bit and try again
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
# Echo the input as if the user typed it
|
||||
sys.stdout.write(line + '\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
return line
|
||||
|
||||
# Replace the built-in input function
|
||||
input = custom_input
|
||||
|
||||
# Execute the user's code
|
||||
try:
|
||||
# Use globals and locals to ensure proper variable scope
|
||||
exec(user_code, globals(), globals())
|
||||
except Exception as e:
|
||||
# Print detailed error information
|
||||
sys.stdout.write(f'\nError: {str(e)}\n')
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
sys.stdout.flush()
|
||||
`
|
||||
|
||||
if err := os.WriteFile(wrapperPath, []byte(wrapperCode), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write wrapper file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) // Longer timeout for interactive
|
||||
defer cancel()
|
||||
|
||||
// Start the container
|
||||
cmd := exec.CommandContext(ctx, "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
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"python:3.9",
|
||||
"python", "/code/wrapper.py")
|
||||
|
||||
// Get pipes for stdin and stdout
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdin pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdout pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to start command: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Set status to running
|
||||
submission.Status = "running"
|
||||
|
||||
// Read output in a goroutine
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Check if the program is waiting for input
|
||||
if line == "[WAITING_FOR_INPUT]" {
|
||||
// Update status to waiting for input
|
||||
submission.Status = "waiting_for_input"
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the output to the submission
|
||||
submission.Output += line + "\n"
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle input in a goroutine
|
||||
go func() {
|
||||
for input := range inputChan {
|
||||
// Write the input to stdin
|
||||
_, err := stdin.Write([]byte(input + "\n"))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to write to stdin: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the command to complete
|
||||
err = cmd.Wait()
|
||||
|
||||
// Update the submission status
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = "Execution timed out"
|
||||
} else {
|
||||
submission.Status = "failed"
|
||||
submission.Error = err.Error()
|
||||
}
|
||||
} else {
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
submission.CompletedAt = time.Now()
|
||||
log.Printf("[PYTHON-%s] Interactive execution completed", submission.ID)
|
||||
}
|
||||
|
||||
// executeJavaScriptInteractive runs JavaScript code in interactive mode
|
||||
func (s *ExecutionService) executeJavaScriptInteractive(submission *models.CodeSubmission, tempDir string) {
|
||||
log.Printf("[JS-%s] Running JavaScript in interactive mode", submission.ID)
|
||||
|
||||
// Create an input channel for this submission
|
||||
inputChan := make(chan string)
|
||||
s.mu.Lock()
|
||||
s.inputChannels[submission.ID] = inputChan
|
||||
s.mu.Unlock()
|
||||
|
||||
// Clean up when done
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
delete(s.inputChannels, submission.ID)
|
||||
close(inputChan)
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
|
||||
// Create a wrapper script that handles interactive input
|
||||
wrapperPath := filepath.Join(tempDir, "wrapper.js")
|
||||
wrapperCode := `
|
||||
const fs = require('fs');
|
||||
const readline = require('readline');
|
||||
|
||||
// Load the user's code
|
||||
const userCode = fs.readFileSync('/code/code.js', 'utf8');
|
||||
|
||||
// Create a custom readline interface
|
||||
const originalReadline = readline.createInterface;
|
||||
readline.createInterface = function(options) {
|
||||
// Create a custom interface that intercepts input
|
||||
const rl = originalReadline({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
terminal: false
|
||||
});
|
||||
|
||||
// Override the question method
|
||||
const originalQuestion = rl.question;
|
||||
rl.question = function(query, callback) {
|
||||
// Print the prompt
|
||||
process.stdout.write(query);
|
||||
|
||||
// Signal that we're waiting for input
|
||||
process.stdout.write('\n[WAITING_FOR_INPUT]\n');
|
||||
process.stdout.flush();
|
||||
|
||||
// Set up a more robust input handler
|
||||
const onLine = (answer) => {
|
||||
// Echo the input as if the user typed it
|
||||
process.stdout.write(answer + '\n');
|
||||
process.stdout.flush();
|
||||
callback(answer);
|
||||
};
|
||||
|
||||
// Handle input with error recovery
|
||||
rl.once('line', onLine);
|
||||
|
||||
// Add error handler
|
||||
rl.once('error', (err) => {
|
||||
console.error('Input error:', err.message);
|
||||
// Provide a default answer in case of error
|
||||
callback('');
|
||||
});
|
||||
};
|
||||
|
||||
return rl;
|
||||
};
|
||||
|
||||
// Capture uncaught exceptions
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('Uncaught Exception:', err.message);
|
||||
console.error(err.stack);
|
||||
});
|
||||
|
||||
// Execute the user's code
|
||||
try {
|
||||
eval(userCode);
|
||||
} catch (e) {
|
||||
console.error('Error:', e.message);
|
||||
console.error(e.stack);
|
||||
}
|
||||
`
|
||||
|
||||
if err := os.WriteFile(wrapperPath, []byte(wrapperCode), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write wrapper file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) // Longer timeout for interactive
|
||||
defer cancel()
|
||||
|
||||
// Start the container
|
||||
cmd := exec.CommandContext(ctx, "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
|
||||
"node:18-alpine",
|
||||
"node", "/code/wrapper.js")
|
||||
|
||||
// Get pipes for stdin and stdout
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdin pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdout pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to start command: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Set status to running
|
||||
submission.Status = "running"
|
||||
|
||||
// Read output in a goroutine
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Check if the program is waiting for input
|
||||
if line == "[WAITING_FOR_INPUT]" {
|
||||
// Update status to waiting for input
|
||||
submission.Status = "waiting_for_input"
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the output to the submission
|
||||
submission.Output += line + "\n"
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle input in a goroutine
|
||||
go func() {
|
||||
for input := range inputChan {
|
||||
// Write the input to stdin
|
||||
_, err := stdin.Write([]byte(input + "\n"))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to write to stdin: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the command to complete
|
||||
err = cmd.Wait()
|
||||
|
||||
// Update the submission status
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = "Execution timed out"
|
||||
} else {
|
||||
submission.Status = "failed"
|
||||
submission.Error = err.Error()
|
||||
}
|
||||
} else {
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
submission.CompletedAt = time.Now()
|
||||
log.Printf("[JS-%s] Interactive execution completed", submission.ID)
|
||||
}
|
||||
376
backend/internal/executor/websocket.go
Normal file
376
backend/internal/executor/websocket.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/models"
|
||||
)
|
||||
|
||||
// WebSocketSession represents a WebSocket execution session
|
||||
type WebSocketSession struct {
|
||||
Submission *models.CodeSubmission
|
||||
InputChan chan string
|
||||
OutputChan chan string
|
||||
Done chan struct{}
|
||||
}
|
||||
|
||||
// SetupWebSocketChannels sets up the channels for WebSocket communication
|
||||
func (s *ExecutionService) SetupWebSocketChannels(submission *models.CodeSubmission, inputChan chan string, outputChan chan string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Store the channels in the service
|
||||
s.wsInputChannels[submission.ID] = inputChan
|
||||
s.wsOutputChannels[submission.ID] = outputChan
|
||||
}
|
||||
|
||||
// ExecuteCodeWebSocket executes code and streams the output over WebSocket
|
||||
func (s *ExecutionService) ExecuteCodeWebSocket(submission *models.CodeSubmission) {
|
||||
log.Printf("[WS-%s] Starting WebSocket execution for %s code", submission.ID, submission.Language)
|
||||
|
||||
// Update submission status
|
||||
submission.Status = "running"
|
||||
submission.StartedAt = time.Now()
|
||||
|
||||
// Execute the code based on the language
|
||||
switch strings.ToLower(submission.Language) {
|
||||
case "python":
|
||||
s.executePythonWebSocket(submission)
|
||||
case "javascript":
|
||||
s.executeJavaScriptWebSocket(submission)
|
||||
case "go":
|
||||
s.executeGoWebSocket(submission)
|
||||
case "java":
|
||||
s.executeJavaWebSocket(submission)
|
||||
case "c":
|
||||
s.executeCWebSocket(submission)
|
||||
case "cpp":
|
||||
s.executeCppWebSocket(submission)
|
||||
default:
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Unsupported language: %s", submission.Language)
|
||||
submission.CompletedAt = time.Now()
|
||||
}
|
||||
|
||||
log.Printf("[WS-%s] Execution completed with status: %s", submission.ID, submission.Status)
|
||||
}
|
||||
|
||||
// executePythonWebSocket executes Python code with WebSocket communication
|
||||
func (s *ExecutionService) executePythonWebSocket(submission *models.CodeSubmission) {
|
||||
log.Printf("[WS-PYTHON-%s] Preparing Python WebSocket execution", submission.ID)
|
||||
|
||||
// Create a temporary directory for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-ws-python-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "code.py")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the input and output channels
|
||||
s.mu.Lock()
|
||||
inputChan := s.wsInputChannels[submission.ID]
|
||||
outputChan := s.wsOutputChannels[submission.ID]
|
||||
s.mu.Unlock()
|
||||
|
||||
// Create a context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Run the code in a Docker container
|
||||
cmd := exec.CommandContext(ctx, "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
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"python:3.9",
|
||||
"python", "/code/code.py")
|
||||
|
||||
// Get pipes for stdin and stdout
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdin pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdout pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stderr pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to start command: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a done channel to signal when the command is complete
|
||||
done := make(chan struct{})
|
||||
|
||||
// Read from stdout and send to the output channel
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
select {
|
||||
case outputChan <- line + "\n":
|
||||
// Output sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from stderr and send to the output channel
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
select {
|
||||
case outputChan <- "ERROR: " + line + "\n":
|
||||
// Error sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from the input channel and write to stdin
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case input := <-inputChan:
|
||||
// Write the input to stdin
|
||||
_, err := io.WriteString(stdin, input+"\n")
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to write to stdin: %v", submission.ID, err)
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the command to complete
|
||||
err = cmd.Wait()
|
||||
close(done)
|
||||
|
||||
// Update the submission status
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = "Execution timed out"
|
||||
} else {
|
||||
submission.Status = "failed"
|
||||
submission.Error = err.Error()
|
||||
}
|
||||
} else {
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
submission.CompletedAt = time.Now()
|
||||
log.Printf("[WS-PYTHON-%s] WebSocket execution completed", submission.ID)
|
||||
}
|
||||
|
||||
// executeJavaScriptWebSocket executes JavaScript code with WebSocket communication
|
||||
func (s *ExecutionService) executeJavaScriptWebSocket(submission *models.CodeSubmission) {
|
||||
log.Printf("[WS-JS-%s] Preparing JavaScript WebSocket execution", submission.ID)
|
||||
|
||||
// Create a temporary directory for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-ws-js-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "code.js")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the input and output channels
|
||||
s.mu.Lock()
|
||||
inputChan := s.wsInputChannels[submission.ID]
|
||||
outputChan := s.wsOutputChannels[submission.ID]
|
||||
s.mu.Unlock()
|
||||
|
||||
// Create a context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Run the code in a Docker container
|
||||
cmd := exec.CommandContext(ctx, "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
|
||||
"node:18-alpine",
|
||||
"node", "/code/code.js")
|
||||
|
||||
// Get pipes for stdin and stdout
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdin pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdout pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stderr pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to start command: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a done channel to signal when the command is complete
|
||||
done := make(chan struct{})
|
||||
|
||||
// Read from stdout and send to the output channel
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
select {
|
||||
case outputChan <- line + "\n":
|
||||
// Output sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from stderr and send to the output channel
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
select {
|
||||
case outputChan <- "ERROR: " + line + "\n":
|
||||
// Error sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from the input channel and write to stdin
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case input := <-inputChan:
|
||||
// Write the input to stdin
|
||||
_, err := io.WriteString(stdin, input+"\n")
|
||||
if err != nil {
|
||||
log.Printf("[WS-JS-%s] Failed to write to stdin: %v", submission.ID, err)
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the command to complete
|
||||
err = cmd.Wait()
|
||||
close(done)
|
||||
|
||||
// Update the submission status
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = "Execution timed out"
|
||||
} else {
|
||||
submission.Status = "failed"
|
||||
submission.Error = err.Error()
|
||||
}
|
||||
} else {
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
submission.CompletedAt = time.Now()
|
||||
log.Printf("[WS-JS-%s] WebSocket execution completed", submission.ID)
|
||||
}
|
||||
|
||||
// executeGoWebSocket executes Go code with WebSocket communication
|
||||
func (s *ExecutionService) executeGoWebSocket(submission *models.CodeSubmission) {
|
||||
// Implementation similar to executePythonWebSocket but for Go
|
||||
// For brevity, this is left as a placeholder
|
||||
submission.Status = "failed"
|
||||
submission.Error = "WebSocket execution for Go not implemented yet"
|
||||
}
|
||||
|
||||
// executeJavaWebSocket executes Java code with WebSocket communication
|
||||
func (s *ExecutionService) executeJavaWebSocket(submission *models.CodeSubmission) {
|
||||
// Implementation similar to executePythonWebSocket but for Java
|
||||
// For brevity, this is left as a placeholder
|
||||
submission.Status = "failed"
|
||||
submission.Error = "WebSocket execution for Java not implemented yet"
|
||||
}
|
||||
|
||||
// executeCWebSocket executes C code with WebSocket communication
|
||||
func (s *ExecutionService) executeCWebSocket(submission *models.CodeSubmission) {
|
||||
// Implementation similar to executePythonWebSocket but for C
|
||||
// For brevity, this is left as a placeholder
|
||||
submission.Status = "failed"
|
||||
submission.Error = "WebSocket execution for C not implemented yet"
|
||||
}
|
||||
|
||||
// executeCppWebSocket executes C++ code with WebSocket communication
|
||||
func (s *ExecutionService) executeCppWebSocket(submission *models.CodeSubmission) {
|
||||
// Implementation similar to executePythonWebSocket but for C++
|
||||
// For brevity, this is left as a placeholder
|
||||
submission.Status = "failed"
|
||||
submission.Error = "WebSocket execution for C++ not implemented yet"
|
||||
}
|
||||
@@ -51,6 +51,7 @@ func main() {
|
||||
http.HandleFunc("/status", corsMiddleware(loggingMiddleware(h.StatusHandler)))
|
||||
http.HandleFunc("/result", corsMiddleware(loggingMiddleware(h.ResultHandler)))
|
||||
http.HandleFunc("/queue-stats", corsMiddleware(loggingMiddleware(h.QueueStatsHandler)))
|
||||
http.HandleFunc("/ws", corsMiddleware(h.WebSocketHandler)) // WebSocket doesn't need logging middleware
|
||||
|
||||
port := ":8080"
|
||||
log.Printf("Server started at %s", port)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -14,19 +16,24 @@ import (
|
||||
|
||||
"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
|
||||
mu sync.Mutex
|
||||
queue *queue.JobQueue
|
||||
wsConnections map[string]*websocket.Conn // Map of submission ID to WebSocket connection
|
||||
wsInputChannels map[string]chan string // Map of submission ID to input channel
|
||||
}
|
||||
|
||||
// NewExecutionService creates a new execution service
|
||||
func NewExecutionService() *ExecutionService {
|
||||
log.Println("Initializing execution service with 3 concurrent workers")
|
||||
return &ExecutionService{
|
||||
queue: queue.NewJobQueue(35), // 3 concurrent executions max
|
||||
queue: queue.NewJobQueue(3), // 3 concurrent executions max
|
||||
wsConnections: make(map[string]*websocket.Conn),
|
||||
wsInputChannels: make(map[string]chan string),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,3 +490,223 @@ func (s *ExecutionService) GetQueueStats() map[string]int {
|
||||
stats["queue_length"], stats["running_jobs"], stats["max_workers"])
|
||||
return stats
|
||||
}
|
||||
|
||||
// HandleWebSocket handles a WebSocket connection for a code submission
|
||||
func (s *ExecutionService) HandleWebSocket(conn *websocket.Conn, submission *model.CodeSubmission) {
|
||||
// Store the WebSocket connection
|
||||
s.mu.Lock()
|
||||
s.wsConnections[submission.ID] = conn
|
||||
|
||||
// Create an input channel for this submission
|
||||
inputChan := make(chan string, 10) // Buffer size of 10
|
||||
s.wsInputChannels[submission.ID] = inputChan
|
||||
s.mu.Unlock()
|
||||
|
||||
// Clean up when done
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
delete(s.wsConnections, submission.ID)
|
||||
delete(s.wsInputChannels, submission.ID)
|
||||
s.mu.Unlock()
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
// Start a goroutine to read input from the WebSocket
|
||||
go func() {
|
||||
for {
|
||||
// Read message from WebSocket
|
||||
messageType, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("[WS-%s] Error reading message: %v", submission.ID, err)
|
||||
break
|
||||
}
|
||||
|
||||
// Only process text messages
|
||||
if messageType == websocket.TextMessage {
|
||||
// Send the input to the input channel
|
||||
inputChan <- string(message)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Execute the code
|
||||
submission.Status = "running"
|
||||
submission.StartedAt = time.Now()
|
||||
|
||||
log.Printf("[WS-JOB-%s] Starting WebSocket execution for language: %s",
|
||||
submission.ID, submission.Language)
|
||||
|
||||
// Execute the code based on the language
|
||||
s.executeLanguageSpecificWithWebSocket(submission, inputChan, conn)
|
||||
}
|
||||
|
||||
// executeLanguageSpecificWithWebSocket runs code in the appropriate language with WebSocket I/O
|
||||
func (s *ExecutionService) executeLanguageSpecificWithWebSocket(submission *model.CodeSubmission, inputChan chan string, conn *websocket.Conn) {
|
||||
log.Printf("[WS-EXEC-%s] Selecting execution environment for language: %s",
|
||||
submission.ID, submission.Language)
|
||||
|
||||
switch submission.Language {
|
||||
case "python":
|
||||
log.Printf("[WS-EXEC-%s] Executing Python code", submission.ID)
|
||||
s.executePythonWithWebSocket(submission, inputChan, conn)
|
||||
case "java":
|
||||
log.Printf("[WS-EXEC-%s] Executing Java code", submission.ID)
|
||||
s.executeJavaWithWebSocket(submission, inputChan, conn)
|
||||
case "c":
|
||||
log.Printf("[WS-EXEC-%s] Executing C code", submission.ID)
|
||||
s.executeCWithWebSocket(submission, inputChan, conn)
|
||||
case "cpp":
|
||||
log.Printf("[WS-EXEC-%s] Executing C++ code", submission.ID)
|
||||
s.executeCppWithWebSocket(submission, inputChan, conn)
|
||||
default:
|
||||
log.Printf("[WS-EXEC-%s] ERROR: Unsupported language: %s", submission.ID, submission.Language)
|
||||
submission.Status = "failed"
|
||||
output := "Unsupported language: " + submission.Language
|
||||
submission.Output = output
|
||||
|
||||
// Send error message to WebSocket
|
||||
conn.WriteMessage(websocket.TextMessage, []byte(output))
|
||||
}
|
||||
|
||||
// Update submission status
|
||||
submission.CompletedAt = time.Now()
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
// executePythonWithWebSocket runs Python code with WebSocket for I/O
|
||||
func (s *ExecutionService) executePythonWithWebSocket(submission *model.CodeSubmission, inputChan chan string, conn *websocket.Conn) {
|
||||
log.Printf("[WS-PYTHON-%s] Preparing Python WebSocket execution", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Send initial message to client
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Starting Python execution...\n"))
|
||||
|
||||
// Create a command to run Python in a Docker container
|
||||
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)
|
||||
|
||||
// Get stdin pipe
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to create stdin pipe: %v", submission.ID, err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Failed to create stdin pipe\n"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get stdout and stderr pipes
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to create stdout pipe: %v", submission.ID, err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Failed to create stdout pipe\n"))
|
||||
return
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to create stderr pipe: %v", submission.ID, err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Failed to create stderr pipe\n"))
|
||||
return
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to start command: %v", submission.ID, err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("Error: Failed to start command: %v\n", err)))
|
||||
return
|
||||
}
|
||||
|
||||
// Create a context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create a channel to signal when the command is done
|
||||
done := make(chan struct{})
|
||||
|
||||
// Start a goroutine to handle command completion
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Command failed: %v", submission.ID, err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("\nExecution failed: %v\n", err)))
|
||||
} else {
|
||||
log.Printf("[WS-PYTHON-%s] Command completed successfully", submission.ID)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("\nExecution completed successfully\n"))
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Start a goroutine to read from stdout and stderr
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(io.MultiReader(stdout, stderr))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
log.Printf("[WS-PYTHON-%s] Output: %s", submission.ID, line)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte(line+"\n"))
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle input from the WebSocket
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case input := <-inputChan:
|
||||
log.Printf("[WS-PYTHON-%s] Received input: %s", submission.ID, input)
|
||||
// Write the input to stdin
|
||||
_, err := io.WriteString(stdin, input+"\n")
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to write to stdin: %v", submission.ID, err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the command to complete or timeout
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("[WS-PYTHON-%s] Execution timed out after 30 seconds", submission.ID)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("\nExecution timed out after 30 seconds\n"))
|
||||
cmd.Process.Kill()
|
||||
case <-done:
|
||||
// Command completed
|
||||
}
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[WS-PYTHON-%s] Python execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
// Update submission result
|
||||
submission.CompletedAt = time.Now()
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
// executeJavaWithWebSocket runs Java code with WebSocket for I/O
|
||||
func (s *ExecutionService) executeJavaWithWebSocket(submission *model.CodeSubmission, inputChan chan string, conn *websocket.Conn) {
|
||||
// For now, just send a message that this is not implemented
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Java WebSocket execution not yet implemented\n"))
|
||||
submission.Status = "failed"
|
||||
submission.Output = "Java WebSocket execution not yet implemented"
|
||||
}
|
||||
|
||||
// executeCWithWebSocket runs C code with WebSocket for I/O
|
||||
func (s *ExecutionService) executeCWithWebSocket(submission *model.CodeSubmission, inputChan chan string, conn *websocket.Conn) {
|
||||
// For now, just send a message that this is not implemented
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("C WebSocket execution not yet implemented\n"))
|
||||
submission.Status = "failed"
|
||||
submission.Output = "C WebSocket execution not yet implemented"
|
||||
}
|
||||
|
||||
// executeCppWithWebSocket runs C++ code with WebSocket for I/O
|
||||
func (s *ExecutionService) executeCppWithWebSocket(submission *model.CodeSubmission, inputChan chan string, conn *websocket.Conn) {
|
||||
// For now, just send a message that this is not implemented
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("C++ WebSocket execution not yet implemented\n"))
|
||||
submission.Status = "failed"
|
||||
submission.Output = "C++ WebSocket execution not yet implemented"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
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
|
||||
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 1exit status 1exit status 1exit status 1
|
||||
BIN
backend/tmp/main.exe
Normal file
BIN
backend/tmp/main.exe
Normal file
Binary file not shown.
Reference in New Issue
Block a user