diff --git a/Frontend/README.md b/Frontend/README.md index 10f1307..03e7d9f 100644 --- a/Frontend/README.md +++ b/Frontend/README.md @@ -1,13 +1,24 @@ -# VSCode Clone with React and Vite +# VS Code Clone Project -This project is a VSCode-like code editor built with React and Vite. It features a customizable UI with an activity bar, sidebar, editor area, panel, and status bar, mimicking the look and feel of Visual Studio Code. +## Authors +- Arnab Bhowmik +- Ishika Bhoyar -## Features +## Description +This project is a VS Code Clone built with React and Monaco Editor. It features a file tree navigation, tab management, code editing with syntax highlighting, and a terminal panel for running code. It mimics the core functionalities of Visual Studio Code in a browser-based environment. -- **Activity Bar**: Switch between different views like Explorer, Search, Source Control, etc. -- **Sidebar**: Displays file explorer, search results, and source control information. -- **Editor Area**: Code editor with syntax highlighting and multiple tabs. -- **Panel**: Terminal, Problems, and Output views. -- **Status Bar**: Displays status information and provides quick actions. +## Frontend Functionalities +- Built with React and Monaco Editor. +- File tree navigation for managing files and folders. +- Tab management for opening multiple files simultaneously. +- Code editing with syntax highlighting and language support. +- Terminal panel for running code and viewing output. +- Persistent file structure and content using localStorage. -## Project Structure +## Backend Functionalities +- Built with Go and Docker for secure code execution. +- Supports multiple programming languages (Python, Java, C/C++). +- Executes code in isolated Docker containers with resource limits. +- RESTful API for submitting code, checking status, and retrieving results. +- Job queue system for managing concurrent executions. +- Enforces timeouts and resource limits for security and performance. diff --git a/Frontend/src/components/EditorArea.jsx b/Frontend/src/components/EditorArea.jsx index 89b954b..caf0b2c 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,41 @@ const EditorArea = ({ } }, [panelVisible]); + // Add this useEffect for cleanup + useEffect(() => { + // Cleanup function to close socket when component unmounts + return () => { + if (activeSocket) { + activeSocket.close(); + } + }; + }, []); + + // Add interval to poll execution status + useEffect(() => { + const checkInterval = setInterval(() => { + // Poll execution status + if (activeSocket && activeRunningFile) { + // Check if socket is still connected + if (activeSocket.readyState !== WebSocket.OPEN) { + console.warn("Socket not in OPEN state:", activeSocket.readyState); + setTerminalOutput(prev => [...prev, { + type: 'warning', + content: `Terminal connection lost, attempting to reconnect...` + }]); + // Could implement reconnection logic here + } + } + }, 5000); + + // Clean up interval when component unmounts + return () => { + if (checkInterval) { + clearInterval(checkInterval); + } + }; + }, [activeSocket, activeRunningFile]); + const handleEditorDidMount = (editor) => { editorRef.current = editor; }; @@ -479,21 +540,31 @@ const EditorArea = ({ case "README.md": return `# VS Code Clone Project -## Overview -This is a simple VS Code clone built with React and Monaco Editor. +## Authors +- Arnab Bhowmik +- Ishika Bhoyar -## Features -- File tree navigation -- Tab management -- Code editing with Monaco Editor -- Syntax highlighting +## Description +This project is a VS Code Clone built with React and Monaco Editor. It features a file tree navigation, tab management, code editing with syntax highlighting, and a terminal panel for running code. It mimics the core functionalities of Visual Studio Code in a browser-based environment. + +## Frontend Functionalities +- Built with React and Monaco Editor. +- File tree navigation for managing files and folders. +- Tab management for opening multiple files simultaneously. +- Code editing with syntax highlighting and language support. +- Terminal panel for running code and viewing output. +- Persistent file structure and content using localStorage. + +## Backend Functionalities +- Built with Go and Docker for secure code execution. +- Supports multiple programming languages (Python, Java, C/C++). +- Executes code in isolated Docker containers with resource limits. +- RESTful API for submitting code, checking status, and retrieving results. +- Job queue system for managing concurrent executions. +- Enforces timeouts and resource limits for security and performance. +`; -## Getting Started -1. Create a new file using the + button in the sidebar -2. Edit your code in the editor -3. Save changes using the save button -Happy coding!`; default: return ""; } @@ -507,7 +578,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 +588,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 +628,189 @@ 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 = (input) => { + // Use the direct input parameter instead of relying on userInput state + const textToSend = input || userInput; - return languageMap[extension] || extension; + console.log("Input submit called, active socket state:", + activeSocket ? activeSocket.readyState : "no socket", + "input:", textToSend); + + if (!activeSocket) { + console.warn("Cannot send input: No active socket"); + setTerminalOutput(prev => [...prev, { + type: 'warning', + content: `Cannot send input: No active connection` + }]); + return; + } + + if (activeSocket.readyState !== WebSocket.OPEN) { + console.warn("Socket not in OPEN state:", activeSocket.readyState); + setTerminalOutput(prev => [...prev, { + type: 'warning', + content: `Cannot send input: Connection not open (state: ${activeSocket.readyState})` + }]); + return; + } + + if (!textToSend.trim()) { + console.warn("Cannot send empty input"); + return; + } + + try { + // Add the input to the terminal display + setTerminalOutput(prev => [...prev, { type: 'command', content: `> ${textToSend}` }]); + + // Send the input via WebSocket with a newline character + console.log("Sending input:", textToSend); + activeSocket.send(textToSend + "\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 +994,17 @@ Happy coding!`; document.addEventListener("mouseup", onMouseUp); }} /> - + )} diff --git a/Frontend/src/components/Navbar.jsx b/Frontend/src/components/Navbar.jsx deleted file mode 100644 index 3ab701c..0000000 --- a/Frontend/src/components/Navbar.jsx +++ /dev/null @@ -1,150 +0,0 @@ -import React from "react" -"use client" - -import Link from "next/link" -import { ChevronDown } from "lucide-react" - -import { Button } from "@/components/ui/button" -import { - NavigationMenu, - NavigationMenuContent, - NavigationMenuItem, - NavigationMenuLink, - NavigationMenuList, - NavigationMenuTrigger, -} from "@/components/ui/navigation-menu" - -export function Navbar() { - return ( -
-
- - *Azzle - - - - - - - Demo - - -
- - -
Features
-

- Explore all the features our platform has to offer -

- -
- - -
Pricing
-

- View our flexible pricing plans -

- -
-
-
-
- - - - About - - - - - - Services - - -
- - -
Consulting
-

- Expert guidance for your business needs -

- -
- - -
Implementation
-

- Full-service implementation and support -

- -
-
-
-
- - - Pages - - -
- - -
Blog
-

- Read our latest articles and updates -

- -
- - -
Resources
-

- Helpful guides and documentation -

- -
-
-
-
- - - - Contact - - - -
-
- -
- - -
-
-
- ) -} - -export default Navbar \ No newline at end of file diff --git a/Frontend/src/components/Panel.jsx b/Frontend/src/components/Panel.jsx index 29c7b18..a76a640 100644 --- a/Frontend/src/components/Panel.jsx +++ b/Frontend/src/components/Panel.jsx @@ -1,8 +1,7 @@ -import React from "react"; -import { useState, useEffect } from "react"; -import { X } from "lucide-react"; +import React, { useState, useEffect, useRef } from "react"; +import { X, Maximize2, ChevronDown, Plus } from "lucide-react"; -const Panel = ({ +const Panel = ({ height, terminalOutput = [], isRunning = false, @@ -12,85 +11,135 @@ const Panel = ({ onClose, userInput = "", onUserInputChange, - onInputSubmit + onInputSubmit, }) => { const [activeTab, setActiveTab] = useState(initialTab); + const terminalRef = useRef(null); + const [inputBuffer, setInputBuffer] = useState(""); - // Set active tab when initialTab changes + // Update active tab when initialTab changes useEffect(() => { setActiveTab(initialTab); }, [initialTab]); - const renderTerminal = () => { - return ( -
- {terminalOutput.length > 0 ? ( - // Render output from EditorArea when available - <> - {terminalOutput.map((line, index) => ( -
- {line.type === 'command' ? $ : ''} {line.content} + // Auto-scroll terminal to the bottom when content changes + useEffect(() => { + if (terminalRef.current) { + terminalRef.current.scrollTop = terminalRef.current.scrollHeight; + } + }, [terminalOutput]); + + // Handle keyboard input for the terminal + useEffect(() => { + const handleKeyDown = (e) => { + if (!isRunning) return; + + if (e.key === "Enter") { + if (inputBuffer.trim() && onInputSubmit) { + e.preventDefault(); + // Update parent's userInput state directly and call submit in the same function + // instead of using setTimeout which creates a race condition + onUserInputChange(inputBuffer); + onInputSubmit(inputBuffer); // Pass inputBuffer directly to avoid race condition + setInputBuffer(""); + } + } else if (e.key === "Backspace") { + setInputBuffer((prev) => prev.slice(0, -1)); + } else if (e.key.length === 1) { + setInputBuffer((prev) => prev + e.key); + } + }; + + const terminalElement = terminalRef.current; + terminalElement?.addEventListener("keydown", handleKeyDown); + + return () => { + terminalElement?.removeEventListener("keydown", handleKeyDown); + }; + }, [isRunning, inputBuffer, onInputSubmit, onUserInputChange]); + + // Render the terminal tab + const renderTerminal = () => ( +
terminalRef.current?.focus()} // Focus when clicked + > + {terminalOutput.length > 0 ? ( + <> + {terminalOutput.map((line, index) => { + const typeClass = + line.type === "warning" + ? "terminal-warning" + : line.type === "error" + ? "terminal-error" + : "terminal-output"; + + return ( +
+ {line.timestamp && ( + {line.timestamp} + )} + {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 - /> -
- )} - - ) : ( - // Default terminal content when no output - <> -
- $ npm start + ); + })} + + {isRunning && ( +
+ $ {inputBuffer} +
-
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
-
- $ -
- - )} -
- ); - }; + )} + + ) : ( +
+ $ + +
+ )} +
+ ); - const renderProblems = () => { - return ( -
-
No problems have been detected in the workspace.
-
- ); - }; + // Render other tabs + const renderProblems = () => ( +
+
No problems have been detected in the workspace.
+
+ ); - const renderOutput = () => { - return ( -
-
[Extension Host] Extension host started.
-
[Language Server] Language server started.
- {activeRunningFile && ( -
[Running] {activeRunningFile}
- )} -
- ); - }; + const renderOutput = () => ( +
+
[Extension Host] Extension host started.
+
[Language Server] Language server started.
+ {activeRunningFile && ( +
[Running] {activeRunningFile}
+ )} +
+ ); + const renderDebugConsole = () => ( +
+
Debug session not yet started.
+
Press F5 to start debugging.
+
+ ); + + const renderPorts = () => ( +
+
No forwarded ports detected.
+
+ ); + + const renderComments = () => ( +
+
No comments have been added to this workspace.
+
+ ); + + // Get content for the active tab const getTabContent = () => { switch (activeTab) { case "terminal": @@ -99,6 +148,12 @@ const Panel = ({ return renderProblems(); case "output": return renderOutput(); + case "debug": + return renderDebugConsole(); + case "ports": + return renderPorts(); + case "comments": + return renderComments(); default: return
Unknown tab
; } @@ -107,76 +162,29 @@ const Panel = ({ return (
-
setActiveTab("problems")} - > - - - - - - - - Problems -
-
setActiveTab("output")}> - - - - - - - - Output -
-
setActiveTab("terminal")} - > - - - - - - - Terminal -
+ {["problems", "output", "debug", "terminal", "ports", "comments"].map((tab) => ( +
setActiveTab(tab)} + > + {tab.toUpperCase()} +
+ ))} - {/* Add close button */}
+ {/* + + */}
@@ -186,5 +194,4 @@ const Panel = ({ ); }; -export default Panel; - +export default Panel; \ No newline at end of file diff --git a/Frontend/src/components/Sidebar.jsx b/Frontend/src/components/Sidebar.jsx index 1f26a69..c0002d1 100644 --- a/Frontend/src/components/Sidebar.jsx +++ b/Frontend/src/components/Sidebar.jsx @@ -31,10 +31,10 @@ const Sidebar = ({ const renderExplorer = () => { const renderFileTree = (structure, path = "") => { if (!structure) return null; - + return Object.entries(structure).map(([name, item]) => { const currentPath = path ? `${path}/${name}` : name; - + if (item.type === "folder") { const isExpanded = expandedFolders[currentPath]; return ( @@ -75,21 +75,6 @@ const Sidebar = ({ )} - - - - - {name}
{isExpanded && ( @@ -137,7 +122,7 @@ const Sidebar = ({ } }); }; - + return (
@@ -184,10 +169,38 @@ const Sidebar = ({
); }; - const getFileIcon = (fileName) => { const extension = fileName.split('.').pop().toLowerCase(); - + + if (fileName.toLowerCase() === 'readme.md') { + return ( + + + + i + + + ); + } + if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) { return ( ); } - + return ( { return (
+ {/* Left Section of the Status Bar */}
+ {/* Branch Indicator */}
{ strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" + aria-label="Branch Icon" > @@ -25,6 +27,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => { main
+ {/* Error Indicator */}
{ strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" + aria-label="Error Icon" > 0 errors
- -
- -
-
- Ln 1, Col 1 -
- -
- Spaces: 2 -
- -
- UTF-8 -
- + {/* Warning Indicator */}
{ strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" + aria-label="Warning Icon" + > + + + + + 0 warnings +
+ + {/* Toggle Terminal Button */} + +
+ + {/* Right Section of the Status Bar */} +
+ {/* Line and Column Indicator */} +
+ Ln 1, Col 1 +
+ + {/* Spaces Indicator */} +
+ Spaces: 2 +
+ + {/* Encoding Indicator */} +
+ UTF-8 +
+ + {/* Language Mode */} +
+ JavaScript +
+ + {/* EOL (End of Line) Indicator */} +
+ LF +
+ + {/* Connection Status */} +
+ @@ -80,6 +126,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => { Connected
+ {/* Bell Icon */}
{ strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" + aria-label="Bell Icon" > @@ -98,8 +146,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
- ) -} - -export default StatusBar + ); +}; +export default StatusBar; \ No newline at end of file diff --git a/Frontend/src/index.css b/Frontend/src/index.css index 77e00f0..c762840 100644 --- a/Frontend/src/index.css +++ b/Frontend/src/index.css @@ -57,12 +57,12 @@ body { display: flex; flex-direction: column; background-color: var(--vscode-activityBar-background); - z-index: 10; - width: 50px; - height: 100%; - position: fixed; /* Change to fixed to avoid layout issues */ + z-index: 10; /* Lower z-index than the StatusBar */ + position: fixed; top: 0; left: 0; + height: calc(100% - 22px); /* Subtract the height of the StatusBar */ + width: 50px; } .activity-bar button { @@ -404,7 +404,7 @@ body { flex: 1; overflow: auto; font-family: "Consolas", "Courier New", monospace; - font-size: 13px; + font-size: 10px; padding: 8px; padding: 10px; font-family: 'Consolas', 'Courier New', monospace; @@ -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/Readme.md b/Readme.md index 84f49a0..e90c531 100644 --- a/Readme.md +++ b/Readme.md @@ -1,22 +1,240 @@ # Monaco Code Execution Engine -Monaco is a secure, containerized code execution engine that allows you to run code in multiple programming languages through a simple REST API. + +Monaco is a secure, containerized code execution engine that allows you to run code in multiple programming languages through a simple REST API and WebSocket connections for real-time terminal interaction. ## Features -- Multi-language support: Run code in Python, Java, C, and C++ -- Secure execution: All code runs in isolated Docker containers -- Resource limits: Memory, CPU, and file descriptor limits to prevent abuse -- Concurrent processing: Efficient job queue for handling multiple requests -- Simple REST API: Easy to integrate with any frontend + +- **Multi-language support**: Run code in Python, Java, C, and C++ +- **Secure execution**: All code runs in isolated Docker containers +- **Resource limits**: Memory, CPU, and file descriptor limits to prevent abuse +- **Concurrent processing**: Efficient job queue for handling multiple requests +- **Simple REST API**: Easy to integrate with any frontend +- **Interactive terminal**: Real-time code execution with input/output via WebSockets +- **VS Code-like interface**: Modern editor with syntax highlighting and file management ## Architecture + Monaco consists of several components: -- HTTP Handlers (handler/handler.go): Processes API requests -- Execution Service (service/execution.go): Manages code execution in containers -- Job Queue (queue/queue.go): Handles concurrent execution of code submissions -- Submission Model (model/submission.go): Defines the data structure for code submissions +### Backend Components + +- **HTTP Handlers** (`handler/handler.go`): Processes API requests and WebSocket connections +- **Execution Service** (`service/execution.go`): Manages code execution in containers +- **Job Queue** (`queue/queue.go`): Handles concurrent execution of code submissions +- **Submission Model** (`model/submission.go`): Defines the data structure for code submissions + +### Frontend Components + +- **Editor Area** (`EditorArea.jsx`): Main code editor with Monaco editor integration +- **Terminal Panel** (`Panel.jsx`): Interactive terminal for code execution and input +- **Sidebar** (`Sidebar.jsx`): File explorer and project structure navigation +- **Status Bar** (`StatusBar.jsx`): Information display and quick actions + +### Communication Flow + +1. Frontend submits code to backend via REST API +2. Backend assigns a unique ID and queues the execution +3. Frontend connects to WebSocket endpoint with the execution ID +4. Backend sends real-time execution output through WebSocket +5. Frontend can send user input back through WebSocket +6. Results are stored and retrievable via REST endpoints ## Requirements -- Go 1.22.3 or higher -- Docker -- Network connectivity for container image pulling \ No newline at end of file + +- **Backend**: + - Go 1.22.3 or higher + - Docker + - Network connectivity for container image pulling +- **Frontend**: + - Node.js and npm/yarn + - Modern web browser + +## Installation + +### Backend Setup + +1. Clone the repository: + ```bash + git clone https://github.com/arnab-afk/monaco.git + cd monaco/backend + +2.Install Go dependencies: + +```bash + go mod download +``` + +3.Build the application: +```bash +go build -o monaco +``` + +4.Run the service +```bash + ./monaco +``` + +The backend service will start on port 8080 by default. + +### Frontend Setup +1. Navigate to the Frontend directory: +```bash +cd Frontend +``` + +2. Install dependencies: +```bash +npm install +``` + +3. Set up environment variables: Create a ```.env``` or ```.env.local.``` file with: +```bash +VITE_API_URL=http://localhost:8080 +``` + +4. Start the development server: +```bash +npm run dev +``` + +The frontend will be available at http://localhost:5173 by default. + +### API Reference + +### REST Endpoints +```POST /submit``` + +Submits code for execution +```json +{ + "language": "python", + "code": "print('Hello, World!')", + "input": "" +} +``` + +Response: +```json +{ + "id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1" +} +``` + +```GET /status?id={submissionId}``` + +Checks the status of submission: +```json +{ + "id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1", + "status": "completed", + "queuedAt": "2025-03-25T14:30:00Z", + "startedAt": "2025-03-25T14:30:01Z", + "completedAt": "2025-03-25T14:30:02Z", + "executionTime": 1000 +} +``` + +```GET /result?id={submissionId}``` + +Gets the execution result of a submission. + +Response: +```json +{ + "id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1", + "status": "completed", + "language": "python", + "output": "Hello, World!", + "queuedAt": "2025-03-25T14:30:00Z", + "startedAt": "2025-03-25T14:30:01Z", + "completedAt": "2025-03-25T14:30:02Z", + "executionTime": 1000, + "executionTimeFormatted": "1.0s", + "totalTime": 2000, + "totalTimeFormatted": "2.0s" +} +``` + +```GET /queue-stats``` +Gets the statistics about the job queue. + +Response: +```json +{ + "queue_stats": { + "queue_length": 5, + "max_workers": 3, + "running_jobs": 3 + }, + "submissions": 42 +} +``` + +### WebSocket Endpoints +```ws://localhost:8080/ws/terminal?id={submissionId}``` + +Establishes a real-time connection for terminal interaction. + +- The server sends execution output as plain text messages. +- The client can send input as plain text messages (with newline). +- Connection automatically closes when execution completes or fails. + +### Terminal Input Handling +The system supports interactive programs requiring user input: + +1. The frontend detects possible input prompts by looking for patterns +2. When detected, it focuses the terminal and allows user input +3. User input is captured in the terminal component's inputBuffer +4. When the user presses Enter, the input is: + - Sent to the backend via WebSocket. + - Displayed in the terminal. + - Buffer is cleared for next input. +5. The input is processed by the running program in real-time. + + +Troubleshooting tips: + +- Ensure WebSocket connection is established before sending input +- Check for WebSocket errors in console +- Verify input reaches the backend by checking server logs +- Ensure newline characters are properly appended to input. + +### Language Support +### Python +- **Version**: Python 3.9 +- **Input Handling**: Direct stdin piping +- **Limitations**: No file I/O, no package imports outside standard library +- **Resource Limits**: 100MB memory, 10% CPU +### Java +- **Version**: Java 11 (Eclipse Temurin) +- **Class Detection**: Extracts class name from code using regex. +- **Memory Settings**: 64MB min heap, 256MB max heap +- **Resource Limits**: 400MB memory, 50% CPU +C +- **Version**: Latest GCC +- **Compilation Flags**: Default GCC settings +- **Resource Limits**: 100MB memory, 10% CPU + +### C++ +- **Version**: Latest G++ +- **Standard**: C++17 +- **Resource Limits**: 100MB memory, 10% CPU + +### Security Considerations +All code execution happens within isolated Docker containers with: + +- No network access (```--network=none```) +- Limited CPU and memory resources +- Limited file system access +- No persistent storage +- Execution time limits (10-15 seconds) + +### Debugging +Check backend logs for execution details +Use browser developer tools to debug WebSocket connections +Terminal panel shows WebSocket connection status and errors +Check Docker logs for container-related issues. + +### Contributing +Contributions are welcome! Please feel free to submit a Pull Request. + 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..2eedd68 100644 Binary files a/backend/tmp/main.exe and b/backend/tmp/main.exe differ