working socket integration

This commit is contained in:
2025-04-21 21:03:59 +05:30
parent c143efa70e
commit 4453e69e68
22 changed files with 2070 additions and 60 deletions

View File

@@ -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

View File

@@ -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=

View 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)
}

View File

@@ -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
}

View 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
}

View File

@@ -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 != "" {

View 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)
}

View 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"
}

View File

@@ -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)

View File

@@ -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"
}

View File

@@ -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

Binary file not shown.