diff --git a/Frontend/src/components/EditorArea.jsx b/Frontend/src/components/EditorArea.jsx index 89b954b..cae1e98 100644 --- a/Frontend/src/components/EditorArea.jsx +++ b/Frontend/src/components/EditorArea.jsx @@ -1,31 +1,31 @@ import React from "react"; import { useState, useRef, useEffect } from "react"; import Editor from "@monaco-editor/react"; -import { +import { X, Plus, Save, FileCode, FileText, Folder, ChevronDown, ChevronRight, - File, FilePlus, FolderPlus, Trash2, Edit, MoreHorizontal, Play, + File, FilePlus, FolderPlus, Trash2, Edit, MoreHorizontal, Play, Terminal, Loader } from "lucide-react"; import Sidebar from "./Sidebar"; import Panel from "./Panel"; // Import Panel component -const EditorArea = ({ - sidebarVisible = true, +const EditorArea = ({ + sidebarVisible = true, activeView = "explorer", panelVisible, - setPanelVisible + setPanelVisible }) => { // Store files with their content in state - start with just README.md const [files, setFiles] = useState([ { id: "README.md", language: "markdown", content: getDefaultCode("README.md") }, ]); - + const [activeTab, setActiveTab] = useState(files[0]?.id || ""); const [isNewFileModalOpen, setIsNewFileModalOpen] = useState(false); const [newFileName, setNewFileName] = useState(""); const [newFileType, setNewFileType] = useState("javascript"); const [unsavedChanges, setUnsavedChanges] = useState({}); - + // Sidebar state - now receives visibility from props const [sidebarWidth, setSidebarWidth] = useState(250); const [expandedFolders, setExpandedFolders] = useState({}); @@ -45,7 +45,7 @@ const EditorArea = ({ const [showContextMenu, setShowContextMenu] = useState(false); const [contextMenuPosition, setContextMenuPosition] = useState({ x: 0, y: 0 }); const [contextMenuTarget, setContextMenuTarget] = useState(null); - + const editorRef = useRef(null); const newFileInputRef = useRef(null); const renameInputRef = useRef(null); @@ -83,7 +83,7 @@ const EditorArea = ({ useEffect(() => { const savedFiles = localStorage.getItem("vscode-clone-files"); const savedFileStructure = localStorage.getItem("vscode-clone-structure"); - + if (savedFiles) { try { const parsedFiles = JSON.parse(savedFiles); @@ -95,7 +95,7 @@ const EditorArea = ({ console.error("Failed to load saved files:", error); } } - + if (savedFileStructure) { try { const parsedStructure = JSON.parse(savedFileStructure); @@ -110,7 +110,7 @@ const EditorArea = ({ useEffect(() => { localStorage.setItem("vscode-clone-files", JSON.stringify(files)); }, [files]); - + useEffect(() => { localStorage.setItem("vscode-clone-structure", JSON.stringify(fileStructure)); }, [fileStructure]); @@ -142,31 +142,31 @@ const EditorArea = ({ ...prev, [activeTab]: true })); - + // Update the file content in the files array - setFiles(files.map(file => + setFiles(files.map(file => file.id === activeTab ? { ...file, content: value } : file )); }; const handleCloseTab = (e, fileId) => { e.stopPropagation(); - + if (unsavedChanges[fileId]) { if (!confirm(`You have unsaved changes in ${fileId}. Close anyway?`)) { return; } } - + // Remove the file from the files array const newFiles = files.filter(file => file.id !== fileId); setFiles(newFiles); - + // Update unsavedChanges const newUnsavedChanges = { ...unsavedChanges }; delete newUnsavedChanges[fileId]; setUnsavedChanges(newUnsavedChanges); - + // If the active tab is closed, set a new active tab if (activeTab === fileId && newFiles.length > 0) { setActiveTab(newFiles[0].id); @@ -175,17 +175,17 @@ const EditorArea = ({ const handleCreateNewFile = (e, path = '') => { e?.preventDefault(); - + if (!newFileName) return; - + const filePath = path ? `${path}/${newFileName}` : newFileName; - + // Check if file already exists if (files.some(file => file.id === filePath)) { alert(`A file named "${filePath}" already exists.`); return; } - + // Determine language based on file extension let language = newFileType; const extension = newFileName.split('.').pop().toLowerCase(); @@ -200,20 +200,20 @@ const EditorArea = ({ } else if (['md', 'markdown'].includes(extension)) { language = 'markdown'; } - + // Create new file const newFile = { id: filePath, language, content: '' }; - + setFiles([...files, newFile]); setActiveTab(filePath); - + // Update file structure updateFileStructure(filePath, 'file', language); - + setNewFileName(''); setIsNewFileModalOpen(false); }; @@ -222,7 +222,7 @@ const EditorArea = ({ const parts = path.split('/'); const fileName = parts.pop(); let current = fileStructure; - + // Navigate to the correct folder if (parts.length > 0) { for (const part of parts) { @@ -232,14 +232,14 @@ const EditorArea = ({ current = current[part].children; } } - + // Add the new item to the structure if (type === 'file') { current[fileName] = { type: 'file', language, id: path }; } else if (type === 'folder') { current[fileName] = { type: 'folder', children: {} }; } - + // Update the state with the new structure setFileStructure({...fileStructure}); }; @@ -247,10 +247,10 @@ const EditorArea = ({ const createNewFolder = (path = '') => { const folderName = prompt("Enter folder name:"); if (!folderName) return; - + const folderPath = path ? `${path}/${folderName}` : folderName; updateFileStructure(folderPath, 'folder'); - + // If the folder is inside another folder, expand the parent if (path) { setExpandedFolders({ @@ -266,7 +266,7 @@ const EditorArea = ({ ...prev, [activeTab]: false })); - + // In a real app, you would save to the server here console.log(`File ${activeTab} saved!`); }; @@ -281,14 +281,14 @@ const EditorArea = ({ const openFile = (fileId) => { // Check if file exists in files array const fileExists = files.some(file => file.id === fileId); - + if (!fileExists) { // Determine language from file structure let language = 'text'; const parts = fileId.split('/'); const fileName = parts.pop(); const extension = fileName.split('.').pop().toLowerCase(); - + if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) { language = 'javascript'; } else if (['css', 'scss', 'less'].includes(extension)) { @@ -300,17 +300,17 @@ const EditorArea = ({ } else if (['md', 'markdown'].includes(extension)) { language = 'markdown'; } - + // Create new file entry const newFile = { id: fileId, language, content: '' }; - + setFiles([...files, newFile]); } - + setActiveTab(fileId); }; @@ -329,82 +329,82 @@ const EditorArea = ({ const deleteItem = (path, type) => { const confirmDelete = confirm(`Are you sure you want to delete ${path}?`); if (!confirmDelete) return; - + if (type === 'file') { // Remove from files array setFiles(files.filter(file => file.id !== path)); - + // If it was active, set a new active tab if (activeTab === path) { const newActiveTab = files.find(file => file.id !== path)?.id || ''; setActiveTab(newActiveTab); } - + // Remove from unsavedChanges const newUnsavedChanges = { ...unsavedChanges }; delete newUnsavedChanges[path]; setUnsavedChanges(newUnsavedChanges); } - + // Remove from file structure const parts = path.split('/'); const itemName = parts.pop(); let current = fileStructure; let parent = null; - + // Navigate to the correct folder if (parts.length > 0) { for (const part of parts) { parent = current; current = current[part].children; } - + // Delete the item delete current[itemName]; } else { // Delete top-level item delete fileStructure[itemName]; } - + // Update the state setFileStructure({...fileStructure}); }; const startRenaming = (path, type) => { setRenamePath(path); - + const parts = path.split('/'); const currentName = parts.pop(); setRenameValue(currentName); - + setIsRenaming(true); }; const handleRename = (e) => { e.preventDefault(); - + if (!renameValue.trim()) return; - + const parts = renamePath.split('/'); const oldName = parts.pop(); const parentPath = parts.join('/'); const newPath = parentPath ? `${parentPath}/${renameValue}` : renameValue; - + // Check if this would overwrite an existing file or folder const parts2 = newPath.split('/'); const newName = parts2.pop(); let current = fileStructure; - + // Navigate to parent folder for (let i = 0; i < parts2.length; i++) { current = current[parts2[i]].children; } - + if (current[newName] && renamePath !== newPath) { alert(`An item named "${newName}" already exists at this location.`); return; } - + // Get the object data const pathParts = renamePath.split('/'); let curr = fileStructure; @@ -412,10 +412,10 @@ const EditorArea = ({ curr = curr[pathParts[i]].children; } const item = curr[pathParts[pathParts.length - 1]]; - + // Delete from old location delete curr[pathParts[pathParts.length - 1]]; - + // Add to new location const newParts = newPath.split('/'); curr = fileStructure; @@ -423,7 +423,7 @@ const EditorArea = ({ curr = curr[newParts[i]].children; } curr[newParts[newParts.length - 1]] = item; - + // If it's a file, update the files array if (item.type === 'file') { const fileIndex = files.findIndex(file => file.id === renamePath); @@ -434,12 +434,12 @@ const EditorArea = ({ id: newPath }; setFiles(updatedFiles); - + // Update active tab if necessary if (activeTab === renamePath) { setActiveTab(newPath); } - + // Update unsavedChanges if (unsavedChanges[renamePath]) { const newUnsavedChanges = { ...unsavedChanges }; @@ -449,7 +449,7 @@ const EditorArea = ({ } } } - + setFileStructure({...fileStructure}); setIsRenaming(false); }; @@ -500,7 +500,7 @@ Happy coding!`; } const activeFile = files.find(file => file.id === activeTab); - + // Calculate editor area style based on sidebar visibility const editorAreaStyle = { marginLeft: sidebarVisible ? `${sidebarWidth}px` : '0px', @@ -510,23 +510,25 @@ Happy coding!`; // Modify the handleRunCode function to prompt for input first const handleRunCode = async () => { if (!activeFile) return; - + // Show the panel setShowPanel(true); if (setPanelVisible) { 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: '------- PROGRAM EXECUTION -------' }, + { type: 'output', content: `Language: ${language}` }, { type: 'output', content: 'Waiting for input (press Enter if no input is needed)...' } ]; setTerminalOutput(newOutput); @@ -535,16 +537,23 @@ Happy coding!`; // 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...' } - ]); + if (userInput) { + setTerminalOutput(prev => [ + ...prev, + { type: 'input', content: userInput } + ]); + } else { + setTerminalOutput(prev => [ + ...prev, + { type: 'output', content: 'Running without input...' } + ]); + } // Use API URL from environment variable const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080'; @@ -562,36 +571,36 @@ Happy coding!`; input: userInput }), }); - + if (!submitResponse.ok) { throw new Error(`Server error: ${submitResponse.status}`); } - + 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)); - + const statusResponse = await fetch(`${apiUrl}/status?id=${id}`); if (!statusResponse.ok) { throw new Error(`Status check failed: ${statusResponse.status}`); } - + 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}` } + return prev.map(line => + line.content.includes('Status:') + ? { ...line, content: `Status: ${status}` } : line ); } else { @@ -599,36 +608,51 @@ Happy coding!`; } }); } - + // 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}`); } - + const { output } = await resultResponse.json(); - + // Format and display output - const outputLines = output.split('\n').map(line => ({ - type: status === 'failed' ? 'warning' : 'output', - content: line - })); - + const outputLines = []; + + // Add a header + outputLines.push({ + type: status === 'failed' ? 'warning' : 'output', + content: status === 'failed' + ? '------- EXECUTION FAILED -------' + : '------- EXECUTION RESULT -------' + }); + + // Process the output line by line + output.split('\n').forEach(line => { + // Check if this is an input line + if (line.startsWith('[Input] ')) { + outputLines.push({ + type: 'input', + content: line.substring(8) // Remove the '[Input] ' prefix + }); + } else { + outputLines.push({ + 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); } - + } catch (error) { setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]); } finally { @@ -636,7 +660,7 @@ Happy coding!`; setIsRunning(false); } }; - + // Helper function to convert file extension to language identifier for API const getLanguageFromExtension = (extension) => { const languageMap = { @@ -649,7 +673,7 @@ Happy coding!`; 'ts': 'typescript', 'tsx': 'typescript' }; - + return languageMap[extension] || extension; }; @@ -665,30 +689,30 @@ Happy coding!`; // Add this function above the return statement const handleDownloadFile = () => { if (!activeFile) return; - + // Create a blob with the file content const blob = new Blob([activeFile.content], { type: 'text/plain' }); - + // Create a URL for the blob const url = URL.createObjectURL(blob); - + // Create a temporary anchor element const a = document.createElement('a'); a.href = url; - + // Get just the filename without path - const fileName = activeFile.id.includes('/') ? - activeFile.id.split('/').pop() : + const fileName = activeFile.id.includes('/') ? + activeFile.id.split('/').pop() : activeFile.id; - + // Set the download attribute with the filename a.download = fileName; - + // Append to the document, click it, and then remove it document.body.appendChild(a); a.click(); document.body.removeChild(a); - + // Release the object URL URL.revokeObjectURL(url); }; @@ -716,16 +740,16 @@ Happy coding!`; createNewFolder={createNewFolder} /> )} - +
{files.map((file) => { // Extract just the filename without path for display - const displayName = file.id.includes('/') ? - file.id.split('/').pop() : + const displayName = file.id.includes('/') ? + file.id.split('/').pop() : file.id; - + return (
-
); })} -
- + {/* Run controls */}
{activeFile && ( <> - -
-
{activeFile ? ( - - - + {/* Add download button */} -
)} - + {/* Context Menu */} {showContextMenu && ( -
)}
- + {showContextMenu && ( -
{terminalOutput.map((line, index) => ( -
- {line.type === 'command' ? $ : ''} {line.content} +
+ {line.type === 'command' ? $ : ''} + {line.type === 'input' ? [Input] : ''} + {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 - /> +
+
+ Input Required: +
+
+ onUserInputChange && onUserInputChange(e.target.value)} + placeholder="Enter input for your program here..." + onKeyDown={(e) => { + if (e.key === 'Enter' && onInputSubmit) { + onInputSubmit(); + } + }} + autoFocus + /> +
+
+ Press Enter to submit input +
)} diff --git a/Frontend/src/index.css b/Frontend/src/index.css index 77e00f0..368269c 100644 --- a/Frontend/src/index.css +++ b/Frontend/src/index.css @@ -435,6 +435,19 @@ body { margin-right: 6px; } +.terminal-input-marker { + color: #4ec9b0; + margin-right: 8px; + font-weight: bold; +} + +.terminal-input-line { + color: #4ec9b0; + background-color: rgba(78, 201, 176, 0.1); + padding: 2px 8px; + border-radius: 3px; +} + .terminal-output { color: #888888; color: #cccccc; @@ -925,18 +938,43 @@ body { } .terminal-input { - background-color: transparent; - border: none; - color: inherit; + background-color: rgba(78, 201, 176, 0.1); + border: 1px solid rgba(78, 201, 176, 0.3); + border-radius: 3px; + color: #4ec9b0; font-family: monospace; font-size: inherit; margin-left: 8px; outline: none; width: calc(100% - 60px); + padding: 4px 8px; } .terminal-input:focus { outline: none; + border-color: rgba(78, 201, 176, 0.6); +} + +.terminal-input-container { + margin: 10px 0; + padding: 10px; + background-color: rgba(78, 201, 176, 0.05); + border-radius: 5px; + border-left: 3px solid #4ec9b0; +} + +.terminal-input-header { + margin-bottom: 8px; +} + +.terminal-input-wrapper { + margin-bottom: 8px; +} + +.terminal-input-help { + font-size: 12px; + color: #888888; + font-style: italic; } .terminal-line.info { diff --git a/Readme.md b/Readme.md index 84f49a0..e95c884 100644 --- a/Readme.md +++ b/Readme.md @@ -1,22 +1,125 @@ -# 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 Online Code Compiler + +A full-featured online code compiler with a VS Code-like interface. This project allows users to write, edit, and execute code in multiple programming languages directly in the browser. ## 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 -## Architecture -Monaco consists of several components: +- **VS Code-like Interface**: Familiar editor experience with syntax highlighting, tabs, and file explorer +- **Multi-language Support**: Run code in Python, JavaScript, Go, Java, C, and C++ +- **Input/Output Handling**: Enter input for your programs and see the output in real-time +- **Secure Execution**: Code runs in isolated Docker containers on the backend +- **File Management**: Create, edit, and organize files and folders -- 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 +## Project Structure -## Requirements -- Go 1.22.3 or higher -- Docker -- Network connectivity for container image pulling \ No newline at end of file +- **Frontend**: React-based UI with Monaco Editor +- **Backend**: Go-based code execution service with Docker integration + - HTTP Handlers (internal/api/handlers): Processes API requests + - Execution Service (internal/executor): Manages code execution in containers + - Job Queue (internal/queue): Handles concurrent execution of code submissions + - Submission Model (internal/models): Defines the data structure for code submissions + +## Getting Started + +### Prerequisites + +- Node.js 18+ for the frontend +- Go 1.22+ for the backend +- Docker for code execution + +### Running the Frontend + +```bash +cd Frontend +npm install +npm run dev +``` + +The frontend will be available at http://localhost:5173 + +### Running the Backend + +```bash +cd backend +go build -o monaco ./cmd/server +./monaco +``` + +The backend API will be available at http://localhost:8080 + +## Using the Online Compiler + +1. **Create a File**: Click the "+" button in the editor tabs or use the file explorer +2. **Write Code**: Use the Monaco editor to write your code +3. **Run Code**: Click the "Play" button in the top right corner +4. **Enter Input**: If your program requires input, enter it in the terminal panel +5. **View Output**: See the execution results in the terminal panel + +## Supported Languages + +- **Python** (.py) +- **JavaScript** (.js) +- **Go** (.go) +- **Java** (.java) +- **C** (.c) +- **C++** (.cpp) + +## Examples + +### Python + +```python +name = input("Enter your name: ") +print(f"Hello, {name}!") +for i in range(5): + print(f"Count: {i}") +``` + +### JavaScript + +```javascript +const readline = require('readline'); +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}); + +rl.question('Enter your name: ', (name) => { + console.log(`Hello, ${name}!`); + for (let i = 0; i < 5; i++) { + console.log(`Count: ${i}`); + } + rl.close(); +}); +``` + +### Go + +```go +package main + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +func main() { + fmt.Print("Enter your name: ") + reader := bufio.NewReader(os.Stdin) + name, _ := reader.ReadString('\n') + name = strings.TrimSpace(name) + fmt.Printf("Hello, %s!\n", name) + for i := 0; i < 5; i++ { + fmt.Printf("Count: %d\n", i) + } +} +``` + +## Security Considerations + +- All code is executed in isolated Docker containers +- Network access is disabled +- Memory and CPU limits are enforced +- Execution timeouts prevent infinite loops \ No newline at end of file diff --git a/backend/internal/api/handlers/handlers.go b/backend/internal/api/handlers/handlers.go index 8ce1373..1a41248 100644 --- a/backend/internal/api/handlers/handlers.go +++ b/backend/internal/api/handlers/handlers.go @@ -2,7 +2,6 @@ package handlers import ( "encoding/json" - "log" "net/http" "sync" "time" @@ -144,6 +143,7 @@ func (h *Handler) ResultHandler(w http.ResponseWriter, r *http.Request) { "status": submission.Status, "language": submission.Language, "output": submission.Output, + "input": submission.Input, } // Add error information if available @@ -190,6 +190,55 @@ func (h *Handler) QueueStatsHandler(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(response) } +// SubmitInputHandler handles interactive input submission +func (h *Handler) SubmitInputHandler(w http.ResponseWriter, r *http.Request) { + // Only allow POST method + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + // Parse the request body + var inputRequest struct { + ID string `json:"id"` + Input string `json:"input"` + } + + if err := json.NewDecoder(r.Body).Decode(&inputRequest); err != nil { + http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest) + return + } + + // Validate the request + if inputRequest.ID == "" { + http.Error(w, "ID is required", http.StatusBadRequest) + return + } + + // Get the submission from the map + h.mu.Lock() + submission, exists := h.submissions[inputRequest.ID] + h.mu.Unlock() + + if !exists { + http.Error(w, "Submission not found", http.StatusNotFound) + return + } + + // Check if the submission is waiting for input + if submission.Status != "waiting_for_input" { + http.Error(w, "Submission is not waiting for input", http.StatusBadRequest) + return + } + + // Send the input to the execution service + h.executionService.SubmitInput(submission, inputRequest.Input) + + // Return success response + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(map[string]string{"status": "input_submitted"}) +} + // HealthCheckHandler handles health check requests func (h *Handler) HealthCheckHandler(w http.ResponseWriter, r *http.Request) { // Only allow GET method diff --git a/backend/internal/api/routes.go b/backend/internal/api/routes.go index 7187207..2e9b59a 100644 --- a/backend/internal/api/routes.go +++ b/backend/internal/api/routes.go @@ -24,6 +24,7 @@ func SetupRoutes() http.Handler { mux.HandleFunc("/submit", h.SubmitHandler) mux.HandleFunc("/status", h.StatusHandler) mux.HandleFunc("/result", h.ResultHandler) + mux.HandleFunc("/submit-input", h.SubmitInputHandler) mux.HandleFunc("/queue-stats", h.QueueStatsHandler) mux.HandleFunc("/health", h.HealthCheckHandler) diff --git a/backend/internal/executor/executor.go b/backend/internal/executor/executor.go index 6dc9958..276112c 100644 --- a/backend/internal/executor/executor.go +++ b/backend/internal/executor/executor.go @@ -10,6 +10,7 @@ import ( "os/exec" "path/filepath" "strings" + "sync" "time" "github.com/arnab-afk/monaco/internal/models" @@ -19,6 +20,9 @@ import ( // ExecutionService manages code execution type ExecutionService struct { queue *queue.JobQueue + mu sync.Mutex + // Map of submission ID to input channel for interactive programs + inputChannels map[string]chan string } // CodeExecutionJob represents a code execution job @@ -30,7 +34,8 @@ type CodeExecutionJob struct { // NewExecutionService creates a new execution service func NewExecutionService() *ExecutionService { return &ExecutionService{ - queue: queue.NewJobQueue(5), // 5 concurrent workers + queue: queue.NewJobQueue(5), // 5 concurrent workers + inputChannels: make(map[string]chan string), } } @@ -601,6 +606,25 @@ func (s *ExecutionService) updateSubmissionResult(submission *models.CodeSubmiss submission.Output = formattedOutput + rawOutput } +// SubmitInput submits input to a running interactive program +func (s *ExecutionService) SubmitInput(submission *models.CodeSubmission, input string) { + s.mu.Lock() + inputChan, exists := s.inputChannels[submission.ID] + s.mu.Unlock() + + if !exists { + log.Printf("[ERROR] No input channel found for submission %s", submission.ID) + return + } + + // Send the input to the channel + inputChan <- input + + // Update the submission status + submission.Status = "running" + submission.Output += "[Input] " + input + "\n" +} + // GetQueueStats returns statistics about the job queue func (s *ExecutionService) GetQueueStats() models.QueueStats { return s.queue.GetStats() diff --git a/backend/internal/models/submission.go b/backend/internal/models/submission.go index a7c3f33..13e99b0 100644 --- a/backend/internal/models/submission.go +++ b/backend/internal/models/submission.go @@ -4,16 +4,18 @@ import "time" // CodeSubmission represents a code submission for execution type CodeSubmission struct { - ID string `json:"id"` - Code string `json:"code"` - Language string `json:"language"` - Input string `json:"input"` - Status string `json:"status"` // "pending", "queued", "running", "completed", "failed" - QueuedAt time.Time `json:"queuedAt,omitempty"` - StartedAt time.Time `json:"startedAt,omitempty"` - CompletedAt time.Time `json:"completedAt,omitempty"` - Output string `json:"output,omitempty"` - Error string `json:"error,omitempty"` + ID string `json:"id"` + Code string `json:"code"` + Language string `json:"language"` + Input string `json:"input"` + Status string `json:"status"` // "pending", "queued", "running", "waiting_for_input", "completed", "failed" + QueuedAt time.Time `json:"queuedAt,omitempty"` + StartedAt time.Time `json:"startedAt,omitempty"` + CompletedAt time.Time `json:"completedAt,omitempty"` + Output string `json:"output,omitempty"` + Error string `json:"error,omitempty"` + IsInteractive bool `json:"isInteractive,omitempty"` // Whether the program requires interactive input + CurrentPrompt string `json:"currentPrompt,omitempty"` // Current input prompt if waiting for input } // ExecutionResult represents the result of code execution diff --git a/backend/tmp/main.exe b/backend/tmp/main.exe deleted file mode 100644 index 90300e6..0000000 Binary files a/backend/tmp/main.exe and /dev/null differ