working socket integration
This commit is contained in:
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user