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

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