diff --git a/Frontend/src/components/EditorArea.jsx b/Frontend/src/components/EditorArea.jsx index 89b954b..32e09e8 100644 --- a/Frontend/src/components/EditorArea.jsx +++ b/Frontend/src/components/EditorArea.jsx @@ -9,6 +9,32 @@ import { import Sidebar from "./Sidebar"; import Panel from "./Panel"; // Import Panel component +// Add this function to map file extensions to language identifiers +const getLanguageFromExtension = (extension) => { + const extensionMap = { + 'js': 'javascript', + 'jsx': 'javascript', + 'ts': 'typescript', + 'tsx': 'typescript', + 'py': 'python', + 'java': 'java', + 'c': 'c', + 'cpp': 'cpp', + 'h': 'c', + 'hpp': 'cpp', + 'cs': 'csharp', + 'go': 'go', + 'rb': 'ruby', + 'php': 'php', + 'html': 'html', + 'css': 'css', + 'json': 'json', + 'md': 'markdown' + }; + + return extensionMap[extension] || 'text'; +}; + const EditorArea = ({ sidebarVisible = true, activeView = "explorer", @@ -62,8 +88,8 @@ const EditorArea = ({ // Add a new state for user input const [userInput, setUserInput] = useState(""); - // Add a new state for waiting for input - const [waitingForInput, setWaitingForInput] = useState(false); + // Add socket state to track the connection + const [activeSocket, setActiveSocket] = useState(null); // Focus the input when new file modal opens useEffect(() => { @@ -132,6 +158,16 @@ const EditorArea = ({ } }, [panelVisible]); + // Add this useEffect for cleanup + useEffect(() => { + // Cleanup function to close socket when component unmounts + return () => { + if (activeSocket) { + activeSocket.close(); + } + }; + }, []); + const handleEditorDidMount = (editor) => { editorRef.current = editor; }; @@ -507,7 +543,7 @@ Happy coding!`; width: `calc(100% - ${sidebarVisible ? sidebarWidth : 0}px)` }; - // Modify the handleRunCode function to prompt for input first + // Update the handleRunCode function const handleRunCode = async () => { if (!activeFile) return; @@ -517,49 +553,36 @@ Happy coding!`; setPanelVisible(true); } - // Set state to waiting for input - setWaitingForInput(true); - setActiveRunningFile(activeFile.id); - // Clear previous output and add new command const fileExtension = activeFile.id.split('.').pop().toLowerCase(); const language = getLanguageFromExtension(fileExtension); const newOutput = [ { type: 'command', content: `$ run ${activeFile.id}` }, - { type: 'output', content: 'Waiting for input (press Enter if no input is needed)...' } + { type: 'output', content: 'Submitting code...' } ]; setTerminalOutput(newOutput); - }; - - // Add a new function to handle input submission - const handleInputSubmit = async () => { - if (!activeFile || !waitingForInput) return; - // Set running state - setIsRunning(true); - setWaitingForInput(false); - - // Add message that we're running with the input - setTerminalOutput(prev => [ - ...prev, - { type: 'output', content: userInput ? `Using input: "${userInput}"` : 'Running without input...' } - ]); - - // Use API URL from environment variable - const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080'; - try { - // Now make the API call with the input that was entered + // Close any existing socket + if (activeSocket) { + activeSocket.close(); + setActiveSocket(null); + } + + // Use API URL from environment variable + const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080'; + + // Submit the code to get an execution ID const submitResponse = await fetch(`${apiUrl}/submit`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ - language: getLanguageFromExtension(activeFile.id.split('.').pop().toLowerCase()), + language: language, code: activeFile.content, - input: userInput + input: "" // Explicitly passing empty input, no user input handling }), }); @@ -570,87 +593,167 @@ Happy coding!`; const { id } = await submitResponse.json(); setTerminalOutput(prev => [...prev, { type: 'output', content: `Job submitted with ID: ${id}` }]); - // Step 2: Poll for status until completed or failed - let status = 'pending'; - while (status !== 'completed' && status !== 'failed') { - // Add a small delay between polls - await new Promise(resolve => setTimeout(resolve, 1000)); + // Set active running file + setActiveRunningFile(activeFile.id); + + // Connect to WebSocket with the execution ID + const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsBaseUrl = apiUrl.replace(/^https?:\/\//, ''); + const wsUrl = `${wsProtocol}//${wsBaseUrl}/ws/terminal?id=${id}`; + + setTerminalOutput(prev => [...prev, { type: 'output', content: `Connecting to: ${wsUrl}` }]); + + // Create a new WebSocket + const newSocket = new WebSocket(wsUrl); + + // Set up event handlers + newSocket.onopen = () => { + console.log("WebSocket connected"); + setTerminalOutput(prev => [...prev, { type: 'output', content: 'Connected to execution terminal' }]); + setIsRunning(true); + }; + + newSocket.onmessage = (event) => { + console.log("WebSocket message received:", event.data); + setTerminalOutput(prev => [...prev, { type: 'output', content: event.data }]); - const statusResponse = await fetch(`${apiUrl}/status?id=${id}`); - if (!statusResponse.ok) { - throw new Error(`Status check failed: ${statusResponse.status}`); + // Check if this message is likely asking for input (prompt detection) + const isPrompt = + event.data.includes("input") || + event.data.includes("?") || + event.data.endsWith(":") || + event.data.endsWith("> "); + + if (isPrompt) { + console.log("Input prompt detected, focusing terminal"); + // Force terminal to focus after a prompt is detected + setTimeout(() => { + document.querySelector('.panel-terminal')?.focus(); + }, 100); } + }; + + // Add polling for job status + let statusCheckInterval; + if (id) { + // Start polling the status endpoint every 2 seconds + statusCheckInterval = setInterval(async () => { + try { + const statusResponse = await fetch(`${apiUrl}/status?id=${id}`); + if (statusResponse.ok) { + const statusData = await statusResponse.json(); + + // If the process is completed or failed, stop polling and update UI + if (statusData.status === 'completed' || statusData.status === 'failed') { + clearInterval(statusCheckInterval); + console.log("Process status:", statusData.status); + + // Update the UI to show process is no longer running + setIsRunning(false); + + // Display the final result if WebSocket didn't capture it + if (statusData.output && statusData.output.length > 0) { + setTerminalOutput(prev => { + // Check if the output is already in the terminal + const lastOutput = prev[prev.length - 1]?.content || ""; + if (!lastOutput.includes(statusData.output)) { + return [...prev, { + type: 'output', + content: `\n[System] Final output:\n${statusData.output}` + }]; + } + return prev; + }); + } + + // Close socket if it's still open + if (newSocket && newSocket.readyState === WebSocket.OPEN) { + newSocket.close(); + } + } + } + } catch (error) { + console.error("Status check error:", error); + } + }, 2000); - const statusData = await statusResponse.json(); - status = statusData.status; - - // Update terminal with status (for any status type) - setTerminalOutput(prev => { - // Update the last status message or add a new one - const hasStatus = prev.some(line => line.content.includes('Status:')); - if (hasStatus) { - return prev.map(line => - line.content.includes('Status:') - ? { ...line, content: `Status: ${status}` } - : line - ); - } else { - return [...prev, { type: 'output', content: `Status: ${status}` }]; + // Clean up interval when component unmounts or when socket closes + newSocket.addEventListener('close', () => { + if (statusCheckInterval) { + clearInterval(statusCheckInterval); } }); } - // Get the result for both completed and failed status - const resultResponse = await fetch(`${apiUrl}/result?id=${id}`); - if (!resultResponse.ok) { - throw new Error(`Result fetch failed: ${resultResponse.status}`); - } + newSocket.onclose = (event) => { + console.log("WebSocket closed:", event); + setIsRunning(false); + setActiveSocket(null); + + const reason = event.reason ? `: ${event.reason}` : ''; + const code = event.code ? ` (code: ${event.code})` : ''; + + setTerminalOutput(prev => [...prev, { + type: 'warning', + content: `Terminal connection closed${reason}${code}` + }]); + + // Clean up interval + if (statusCheckInterval) { + clearInterval(statusCheckInterval); + } + }; - const { output } = await resultResponse.json(); + newSocket.onerror = (event) => { + console.error("WebSocket error:", event); + setTerminalOutput(prev => [...prev, { + type: 'warning', + content: `WebSocket error occurred` + }]); + }; - // Format and display output - const outputLines = output.split('\n').map(line => ({ - type: status === 'failed' ? 'warning' : 'output', - content: line - })); - - setTerminalOutput(prev => [ - ...prev, - { - type: status === 'failed' ? 'warning' : 'output', - content: status === 'failed' - ? '------- EXECUTION FAILED -------' - : '------- EXECUTION RESULT -------' - }, - ...outputLines - ]); - - if (status === 'failed') { - console.error('Code execution failed:', output); - } + // Set the active socket after all handlers are defined + setActiveSocket(newSocket); } catch (error) { + console.error("Run code error:", error); setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]); - } finally { - // Set running state to false setIsRunning(false); + + // Also add cleanup in the error handler + if (statusCheckInterval) { + clearInterval(statusCheckInterval); + } } }; - - // Helper function to convert file extension to language identifier for API - const getLanguageFromExtension = (extension) => { - const languageMap = { - 'java': 'java', - 'c': 'c', - 'cpp': 'cpp', - 'py': 'python', - 'js': 'javascript', - 'jsx': 'javascript', - 'ts': 'typescript', - 'tsx': 'typescript' - }; + + // Update handleInputSubmit to ensure the input is sent properly + const handleInputSubmit = () => { + // Log more detail for debugging + console.log("Input submit called, active socket:", !!activeSocket, "userInput:", userInput); - return languageMap[extension] || extension; + if (!activeSocket || !userInput.trim()) { + console.warn("Cannot send input: No active socket or empty input"); + return; + } + + try { + // Add the input to the terminal display + setTerminalOutput(prev => [...prev, { type: 'command', content: `> ${userInput}` }]); + + // Send the input via WebSocket with a newline character to ensure it's processed + console.log("Sending input:", userInput); + activeSocket.send(userInput + "\n"); + + // Clear the input field + setUserInput(""); + } catch (error) { + console.error("Error sending input:", error); + setTerminalOutput(prev => [...prev, { + type: 'warning', + content: `Error sending input: ${error.message}` + }]); + } }; // Update this function to also update parent state @@ -834,18 +937,17 @@ Happy coding!`; document.addEventListener("mouseup", onMouseUp); }} /> - + )} diff --git a/Frontend/src/components/Panel.jsx b/Frontend/src/components/Panel.jsx index 29c7b18..b71f961 100644 --- a/Frontend/src/components/Panel.jsx +++ b/Frontend/src/components/Panel.jsx @@ -1,5 +1,4 @@ -import React from "react"; -import { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import { X } from "lucide-react"; const Panel = ({ @@ -21,9 +20,67 @@ const Panel = ({ setActiveTab(initialTab); }, [initialTab]); + // Update the renderTerminal function to create an interactive terminal const renderTerminal = () => { + const terminalRef = useRef(null); + const [inputBuffer, setInputBuffer] = useState(""); + + // Auto-scroll terminal to bottom when content changes + useEffect(() => { + if (terminalRef.current) { + terminalRef.current.scrollTop = terminalRef.current.scrollHeight; + } + }, [terminalOutput]); + + // Set up keyboard event listeners when terminal is focused + useEffect(() => { + const handleKeyDown = (e) => { + if (!isRunning) return; + + if (e.key === 'Enter') { + // Send current input buffer through WebSocket + if (inputBuffer.trim() && onInputSubmit) { + e.preventDefault(); // Prevent default Enter behavior + + // Important: Set user input and THEN call submit in a sequence + onUserInputChange(inputBuffer); + + // Add a small delay before submitting to ensure state update + setTimeout(() => { + onInputSubmit(); + // Clear buffer after submission is processed + setInputBuffer(""); + }, 10); + } + } else if (e.key === 'Backspace') { + // Handle backspace to remove characters + setInputBuffer(prev => prev.slice(0, -1)); + } else if (e.key.length === 1) { + // Add regular characters to input buffer + setInputBuffer(prev => prev + e.key); + } + }; + + // Add event listener + if (terminalRef.current) { + terminalRef.current.addEventListener('keydown', handleKeyDown); + } + + // Clean up + return () => { + if (terminalRef.current) { + terminalRef.current.removeEventListener('keydown', handleKeyDown); + } + }; + }, [isRunning, inputBuffer, onInputSubmit, onUserInputChange]); + return ( -
+
terminalRef.current?.focus()} // Focus when clicked + > {terminalOutput.length > 0 ? ( // Render output from EditorArea when available <> @@ -32,36 +89,18 @@ const Panel = ({ {line.type === 'command' ? $ : ''} {line.content}
))} - {waitingForInput && ( -
- Input: - onUserInputChange && onUserInputChange(e.target.value)} - placeholder="Enter input for your program here..." - onKeyDown={(e) => { - if (e.key === 'Enter' && onInputSubmit) { - onInputSubmit(); - } - }} - autoFocus - /> + + {/* Show current input with blinking cursor only when connection is active */} + {isRunning && ( +
+ $ {inputBuffer} +
)} ) : ( - // Default terminal content when no output + // Default terminal content <> -
- $ npm start -
-
Starting the development server...
-
Compiled successfully!
-
You can now view vscode-clone in the browser.
-
Local: http://localhost:3000
-
On Your Network: http://192.168.1.5:3000
$
diff --git a/Frontend/src/index.css b/Frontend/src/index.css index 77e00f0..fe108de 100644 --- a/Frontend/src/index.css +++ b/Frontend/src/index.css @@ -418,6 +418,16 @@ body { height: 100%; } +.panel-terminal { + padding: 8px; + font-family: monospace; + overflow-y: auto; + height: calc(100% - 36px); /* Adjust based on your header height */ + background-color: #1e1e1e; + color: #ddd; + outline: none; /* Remove focus outline */ +} + .panel-terminal .terminal-line { white-space: pre-wrap; margin-bottom: 3px; @@ -426,22 +436,20 @@ body { .terminal-line { white-space: pre-wrap; line-height: 1.5; + margin-bottom: 2px; } .terminal-prompt { - color: #0f0; + color: #0a84ff; margin-right: 8px; - color: #569cd6; - margin-right: 6px; } .terminal-output { - color: #888888; - color: #cccccc; + color: #ddd; } .terminal-warning { - color: #ddb100; + color: #ffa500; } .output-line { @@ -463,9 +471,8 @@ body { } @keyframes blink { - 50% { - opacity: 0; - } + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } } .panel-empty-message { @@ -964,9 +971,8 @@ body { } @keyframes blink { - 50% { - opacity: 0; - } + 0%, 100% { opacity: 1; } + 50% { opacity: 0; } } /* Make sure the monaco container adjusts when terminal is shown */ diff --git a/backend/go.mod b/backend/go.mod index 54f54af..39c0626 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -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 diff --git a/backend/go.sum b/backend/go.sum index 47570c2..a1fba2b 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -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= diff --git a/backend/handler/handler.go b/backend/handler/handler.go index 9fa8d48..cd9e4d3 100644 --- a/backend/handler/handler.go +++ b/backend/handler/handler.go @@ -2,12 +2,15 @@ package handler import ( "encoding/json" + "fmt" + "log" "net/http" "sync" "time" "github.com/arnab-afk/monaco/model" "github.com/arnab-afk/monaco/service" + "github.com/gorilla/websocket" ) // Handler manages HTTP requests for code submissions @@ -179,6 +182,63 @@ func (h *Handler) QueueStatsHandler(w http.ResponseWriter, r *http.Request) { }) } +// ConnectTerminal connects a WebSocket to a running execution +func (h *Handler) ConnectTerminal(conn *websocket.Conn, executionID string) { + // Get submission from storage + h.mu.Lock() + submission, found := h.submissions[executionID] + status := "not found" + if found { + status = submission.Status + } + h.mu.Unlock() + + log.Printf("[WS-%s] Terminal connection request, submission status: %s", executionID, status) + + if !found { + log.Printf("[WS-%s] Execution not found", executionID) + conn.WriteMessage(websocket.TextMessage, []byte("Execution not found")) + conn.Close() + return + } + + // If execution is already completed, send stored output and close + if submission.Status == "completed" || submission.Status == "failed" { + log.Printf("[WS-%s] Execution already %s, sending stored output (length: %d)", + executionID, submission.Status, len(submission.Output)) + conn.WriteMessage(websocket.TextMessage, []byte(submission.Output)) + conn.Close() + return + } + + log.Printf("[WS-%s] Registering connection for real-time updates, current status: %s", + executionID, submission.Status) + + // Register this connection with the execution service for real-time updates + h.executionService.RegisterTerminalConnection(executionID, conn) + + // Send initial connection confirmation + initialMsg := fmt.Sprintf("[System] Connected to process (ID: %s, Status: %s)\n", + executionID, submission.Status) + conn.WriteMessage(websocket.TextMessage, []byte(initialMsg)) + + // Handle incoming messages from the terminal (for stdin) + go func() { + for { + _, message, err := conn.ReadMessage() + if err != nil { + log.Printf("[WS-%s] Read error: %v", executionID, err) + h.executionService.UnregisterTerminalConnection(executionID, conn) + break + } + + log.Printf("[WS-%s] Received input from client: %s", executionID, string(message)) + // Send input to the execution if it's waiting for input + h.executionService.SendInput(executionID, string(message)) + } + }() +} + // generateID creates a unique ID for submissions func (h *Handler) generateID() string { return service.GenerateUUID() diff --git a/backend/main.go b/backend/main.go index 61c2041..8cae5b3 100644 --- a/backend/main.go +++ b/backend/main.go @@ -7,6 +7,7 @@ import ( "time" "github.com/arnab-afk/monaco/handler" + "github.com/gorilla/websocket" ) func main() { @@ -46,7 +47,40 @@ func main() { } } - // Register handlers with logging and CORS middleware + // Configure WebSocket upgrader + upgrader := websocket.Upgrader{ + ReadBufferSize: 1024, + WriteBufferSize: 1024, + // Allow connections from any origin + CheckOrigin: func(r *http.Request) bool { + return true + }, + } + + // WebSocket handler for terminal connection + http.HandleFunc("/ws/terminal", func(w http.ResponseWriter, r *http.Request) { + // Get execution ID from query parameters + executionID := r.URL.Query().Get("id") + if executionID == "" { + log.Println("[WS] Missing execution ID") + http.Error(w, "Missing execution ID", http.StatusBadRequest) + return + } + + // Upgrade HTTP connection to WebSocket + conn, err := upgrader.Upgrade(w, r, nil) + if err != nil { + log.Printf("[WS] Failed to upgrade connection: %v", err) + return + } + + log.Printf("[WS] Terminal connection established for execution ID: %s", executionID) + + // Connect this WebSocket to the execution service for real-time streaming + h.ConnectTerminal(conn, executionID) + }) + + // Register REST API handlers with logging and CORS middleware http.HandleFunc("/submit", corsMiddleware(loggingMiddleware(h.SubmitHandler))) http.HandleFunc("/status", corsMiddleware(loggingMiddleware(h.StatusHandler))) http.HandleFunc("/result", corsMiddleware(loggingMiddleware(h.ResultHandler))) diff --git a/backend/service/execution.go b/backend/service/execution.go index d7856f5..38c9d11 100644 --- a/backend/service/execution.go +++ b/backend/service/execution.go @@ -1,6 +1,8 @@ package service import ( + "bytes" + "context" "fmt" "io" "log" @@ -8,25 +10,101 @@ import ( "os/exec" "path/filepath" "regexp" - "strings" "sync" "time" "github.com/arnab-afk/monaco/model" "github.com/arnab-afk/monaco/queue" + "github.com/gorilla/websocket" ) // ExecutionService handles code execution for multiple languages type ExecutionService struct { - mu sync.Mutex - queue *queue.JobQueue + mu sync.Mutex + queue *queue.JobQueue + terminalConnections map[string][]*websocket.Conn // Map of executionID to WebSocket connections + execInputChannels map[string]chan string // Map of executionID to input channels } // NewExecutionService creates a new execution service func NewExecutionService() *ExecutionService { log.Println("Initializing execution service with 3 concurrent workers") return &ExecutionService{ - queue: queue.NewJobQueue(35), // 3 concurrent executions max + queue: queue.NewJobQueue(3), // 3 concurrent executions max + terminalConnections: make(map[string][]*websocket.Conn), + execInputChannels: make(map[string]chan string), + } +} + +// RegisterTerminalConnection registers a WebSocket connection for an execution +func (s *ExecutionService) RegisterTerminalConnection(executionID string, conn *websocket.Conn) { + s.mu.Lock() + defer s.mu.Unlock() + + if _, exists := s.terminalConnections[executionID]; !exists { + s.terminalConnections[executionID] = make([]*websocket.Conn, 0) + } + s.terminalConnections[executionID] = append(s.terminalConnections[executionID], conn) + log.Printf("[WS-%s] Terminal connection registered, total connections: %d", + executionID, len(s.terminalConnections[executionID])) +} + +// UnregisterTerminalConnection removes a WebSocket connection +func (s *ExecutionService) UnregisterTerminalConnection(executionID string, conn *websocket.Conn) { + s.mu.Lock() + defer s.mu.Unlock() + + connections, exists := s.terminalConnections[executionID] + if !exists { + return + } + + // Remove the specific connection + for i, c := range connections { + if c == conn { + s.terminalConnections[executionID] = append(connections[:i], connections[i+1:]...) + break + } + } + + // If no more connections, clean up + if len(s.terminalConnections[executionID]) == 0 { + delete(s.terminalConnections, executionID) + } + + log.Printf("[WS-%s] Terminal connection unregistered", executionID) +} + +// SendOutputToTerminals sends output to all connected terminals for an execution +func (s *ExecutionService) SendOutputToTerminals(executionID string, output string) { + s.mu.Lock() + connections := s.terminalConnections[executionID] + s.mu.Unlock() + + for _, conn := range connections { + if err := conn.WriteMessage(websocket.TextMessage, []byte(output)); err != nil { + log.Printf("[WS-%s] Error sending to terminal: %v", executionID, err) + // Unregister this connection on error + s.UnregisterTerminalConnection(executionID, conn) + } + } +} + +// SendInput sends user input to a running process +func (s *ExecutionService) SendInput(executionID string, input string) { + s.mu.Lock() + inputChan, exists := s.execInputChannels[executionID] + s.mu.Unlock() + + if exists { + select { + case inputChan <- input: + log.Printf("[WS-%s] Sent input to execution: %s", executionID, input) + default: + log.Printf("[WS-%s] Execution not ready for input", executionID) + } + } else { + log.Printf("[WS-%s] No input channel for execution", executionID) } } @@ -110,48 +188,123 @@ func (s *ExecutionService) executeLanguageSpecific(submission *model.CodeSubmiss func (s *ExecutionService) executeWithInput(cmd *exec.Cmd, input string, timeout time.Duration, submissionID string) ([]byte, error) { log.Printf("[TIMEOUT-%s] Setting execution timeout: %v", submissionID, timeout) - // Set up input pipe if input is provided - if input != "" { - stdin, err := cmd.StdinPipe() - if err != nil { - log.Printf("[ERROR-%s] Failed to create stdin pipe: %v", submissionID, err) - return nil, err - } - - // Write input in a goroutine to avoid blocking - go func() { - defer stdin.Close() - io.WriteString(stdin, input) - }() - - log.Printf("[INPUT-%s] Providing input to process", submissionID) + // Create pipes for stdin, stdout, and stderr + stdin, stdinErr := cmd.StdinPipe() + if stdinErr != nil { + return nil, fmt.Errorf("failed to create stdin pipe: %v", stdinErr) } - done := make(chan struct{}) - var output []byte - var err error + stdout, stdoutErr := cmd.StdoutPipe() + if stdoutErr != nil { + return nil, fmt.Errorf("failed to create stdout pipe: %v", stdoutErr) + } - go func() { - log.Printf("[EXEC-%s] Starting command execution: %v", submissionID, cmd.Args) - output, err = cmd.CombinedOutput() - close(done) + stderr, stderrErr := cmd.StderrPipe() + if stderrErr != nil { + return nil, fmt.Errorf("failed to create stderr pipe: %v", stderrErr) + } + + // Create an input channel and register it + inputChan := make(chan string, 10) + s.mu.Lock() + s.execInputChannels[submissionID] = inputChan + s.mu.Unlock() + + // Clean up the input channel when done + defer func() { + s.mu.Lock() + delete(s.execInputChannels, submissionID) + s.mu.Unlock() + close(inputChan) }() + // Start the command + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start process: %v", err) + } + + // Create a buffer to collect all output + var outputBuffer bytes.Buffer + + // Handle stdout in a goroutine + go func() { + buffer := make([]byte, 1024) + for { + n, err := stdout.Read(buffer) + if n > 0 { + data := buffer[:n] + outputBuffer.Write(data) + // Send real-time output to connected terminals + s.SendOutputToTerminals(submissionID, string(data)) + } + if err != nil { + break + } + } + }() + + // Handle stderr in a goroutine + go func() { + buffer := make([]byte, 1024) + for { + n, err := stderr.Read(buffer) + if n > 0 { + data := buffer[:n] + outputBuffer.Write(data) + // Send real-time output to connected terminals + s.SendOutputToTerminals(submissionID, string(data)) + } + if err != nil { + break + } + } + }() + + // Write initial input if provided + if input != "" { + io.WriteString(stdin, input+"\n") + } + + // Process is in a separate context, but it needs to be killed if timeout occurs + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Handle additional input from WebSocket in a goroutine + go func() { + for { + select { + case additionalInput, ok := <-inputChan: + if !ok { + return + } + log.Printf("[INPUT-%s] Received input from WebSocket: %s", submissionID, additionalInput) + io.WriteString(stdin, additionalInput+"\n") + case <-ctx.Done(): + return + } + } + }() + + // Wait for the command to complete with timeout + done := make(chan error, 1) + go func() { + done <- cmd.Wait() + }() + + // Wait for completion or timeout select { case <-time.After(timeout): + cancel() // Stop the input handler log.Printf("[TIMEOUT-%s] Execution timed out after %v seconds", submissionID, timeout.Seconds()) if err := cmd.Process.Kill(); err != nil { log.Printf("[TIMEOUT-%s] Failed to kill process: %v", submissionID, err) - return nil, fmt.Errorf("timeout reached but failed to kill process: %v", err) } - return nil, fmt.Errorf("execution timed out after %v seconds", timeout.Seconds()) - case <-done: - if err != nil { - log.Printf("[EXEC-%s] Command execution failed: %v", submissionID, err) - } else { - log.Printf("[EXEC-%s] Command execution completed successfully", submissionID) - } - return output, err + s.SendOutputToTerminals(submissionID, fmt.Sprintf("\n[System] Process killed after timeout of %v seconds", timeout.Seconds())) + return outputBuffer.Bytes(), fmt.Errorf("execution timed out after %v seconds", timeout.Seconds()) + case err := <-done: + cancel() // Stop the input handler + s.SendOutputToTerminals(submissionID, "\n[System] Process completed") + return outputBuffer.Bytes(), err } } @@ -169,15 +322,9 @@ func (s *ExecutionService) executePython(submission *model.CodeSubmission) { "python:3.9", "python", "-c", submission.Code) log.Printf("[PYTHON-%s] Executing Python code with timeout: 10s", submission.ID) - var output []byte - var err error - if submission.Input != "" { - cmd.Stdin = strings.NewReader(submission.Input) - output, err = cmd.CombinedOutput() - } else { - output, err = s.executeWithTimeout(cmd, 10*time.Second, submission.ID) - } + // Use the enhanced executeWithInput method for all executions + output, err := s.executeWithInput(cmd, submission.Input, 100*time.Second, submission.ID) elapsed := time.Since(startTime) log.Printf("[PYTHON-%s] Python execution completed in %v", submission.ID, elapsed) @@ -255,7 +402,7 @@ func (s *ExecutionService) executeJava(submission *model.CodeSubmission) { log.Printf("[JAVA-%s] Compilation successful", submission.ID) - // Now run the compiled class + // Now run the compiled class with the enhanced executeWithInput method runCmd := exec.Command("docker", "run", "--rm", "-i", "--network=none", // No network access "--memory=400m", // Memory limit @@ -267,17 +414,8 @@ func (s *ExecutionService) executeJava(submission *model.CodeSubmission) { "-Xverify:none", "-Xms64m", "-Xmx256m", "-cp", "/code", className) - // Add input if provided - var output []byte - - if submission.Input != "" { - log.Printf("[JAVA-%s] Executing Java code with input", submission.ID) - runCmd.Stdin = strings.NewReader(submission.Input) - output, err = runCmd.CombinedOutput() - } else { - log.Printf("[JAVA-%s] Executing Java code without input", submission.ID) - output, err = s.executeWithTimeout(runCmd, 15*time.Second, submission.ID) - } + log.Printf("[JAVA-%s] Executing Java code", submission.ID) + output, err := s.executeWithInput(runCmd, submission.Input, 15*time.Second, submission.ID) elapsed := time.Since(startTime) log.Printf("[JAVA-%s] Java execution completed in %v", submission.ID, elapsed) @@ -327,7 +465,7 @@ func (s *ExecutionService) executeC(submission *model.CodeSubmission) { log.Printf("[C-%s] Compilation successful", submission.ID) - // Run C executable + // Run C executable using executeWithInput to support WebSockets runCmd := exec.Command("docker", "run", "--rm", "-i", "--network=none", // No network access "--memory=100m", // Memory limit @@ -336,17 +474,8 @@ func (s *ExecutionService) executeC(submission *model.CodeSubmission) { "-v", tempDir+":/code", // Mount code directory "gcc:latest", "/code/solution") - // Add input if provided - var output []byte - // Don't redeclare err here - use the existing variable - if submission.Input != "" { - log.Printf("[C-%s] Executing C code with input", submission.ID) - runCmd.Stdin = strings.NewReader(submission.Input) - output, err = runCmd.CombinedOutput() // Use the existing err variable - } else { - log.Printf("[C-%s] Executing C code without input", submission.ID) - output, err = s.executeWithTimeout(runCmd, 10*time.Second, submission.ID) // Use the existing err variable - } + log.Printf("[C-%s] Executing C code", submission.ID) + output, err := s.executeWithInput(runCmd, submission.Input, 30*time.Second, submission.ID) elapsed := time.Since(startTime) log.Printf("[C-%s] C execution completed in %v", submission.ID, elapsed) @@ -396,7 +525,7 @@ func (s *ExecutionService) executeCpp(submission *model.CodeSubmission) { log.Printf("[CPP-%s] Compilation successful", submission.ID) - // Run C++ executable + // Run C++ executable using executeWithInput to support WebSockets runCmd := exec.Command("docker", "run", "--rm", "-i", "--network=none", // No network access "--memory=100m", // Memory limit @@ -405,16 +534,8 @@ func (s *ExecutionService) executeCpp(submission *model.CodeSubmission) { "-v", tempDir+":/code", // Mount code directory "gcc:latest", "/code/solution") - // Add input if provided - var output []byte - if submission.Input != "" { - log.Printf("[CPP-%s] Executing C++ code with input", submission.ID) - runCmd.Stdin = strings.NewReader(submission.Input) - output, err = runCmd.CombinedOutput() - } else { - log.Printf("[CPP-%s] Executing C++ code without input", submission.ID) - output, err = s.executeWithTimeout(runCmd, 10*time.Second, submission.ID) - } + log.Printf("[CPP-%s] Executing C++ code", submission.ID) + output, err := s.executeWithInput(runCmd, submission.Input, 100*time.Second, submission.ID) elapsed := time.Since(startTime) log.Printf("[CPP-%s] C++ execution completed in %v", submission.ID, elapsed) diff --git a/backend/tmp/main.exe b/backend/tmp/main.exe index 345d959..ccc2eba 100644 Binary files a/backend/tmp/main.exe and b/backend/tmp/main.exe differ