Merge pull request #1 from Arnab-Afk/socket

Socket Integration
This commit is contained in:
2025-03-30 21:36:59 +05:30
committed by GitHub
14 changed files with 1099 additions and 570 deletions

View File

@@ -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.

View File

@@ -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...' }
]);
try {
// 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';
try {
// Now make the API call with the input that was entered
// 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 }]);
// 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) {
throw new Error(`Status check failed: ${statusResponse.status}`);
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;
});
}
const statusData = await statusResponse.json();
status = statusData.status;
// Close socket if it's still open
if (newSocket && newSocket.readyState === WebSocket.OPEN) {
newSocket.close();
}
}
}
} catch (error) {
console.error("Status check error:", error);
}
}, 2000);
// 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
@@ -838,7 +998,6 @@ Happy coding!`;
height={panelHeight}
terminalOutput={terminalOutput}
isRunning={isRunning}
waitingForInput={waitingForInput}
activeRunningFile={activeRunningFile}
initialTab="terminal"
onClose={togglePanel}

View File

@@ -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 (
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<div className="container flex h-16 items-center justify-between">
<Link href="/" className="flex items-center space-x-2">
<span className="text-xl font-bold">*Azzle</span>
</Link>
<NavigationMenu className="hidden md:flex">
<NavigationMenuList>
<NavigationMenuItem>
<NavigationMenuTrigger>
Demo <ChevronDown className="h-4 w-4" />
</NavigationMenuTrigger>
<NavigationMenuContent>
<div className="grid gap-3 p-6 w-[400px]">
<NavigationMenuLink asChild>
<Link
href="/demo/features"
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div className="text-sm font-medium leading-none">Features</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
Explore all the features our platform has to offer
</p>
</Link>
</NavigationMenuLink>
<NavigationMenuLink asChild>
<Link
href="/demo/pricing"
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div className="text-sm font-medium leading-none">Pricing</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
View our flexible pricing plans
</p>
</Link>
</NavigationMenuLink>
</div>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<Link href="/about" legacyBehavior passHref>
<NavigationMenuLink className="group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50">
About
</NavigationMenuLink>
</Link>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>
Services <ChevronDown className="h-4 w-4" />
</NavigationMenuTrigger>
<NavigationMenuContent>
<div className="grid gap-3 p-6 w-[400px]">
<NavigationMenuLink asChild>
<Link
href="/services/consulting"
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div className="text-sm font-medium leading-none">Consulting</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
Expert guidance for your business needs
</p>
</Link>
</NavigationMenuLink>
<NavigationMenuLink asChild>
<Link
href="/services/implementation"
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div className="text-sm font-medium leading-none">Implementation</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
Full-service implementation and support
</p>
</Link>
</NavigationMenuLink>
</div>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<NavigationMenuTrigger>
Pages <ChevronDown className="h-4 w-4" />
</NavigationMenuTrigger>
<NavigationMenuContent>
<div className="grid gap-3 p-6 w-[400px]">
<NavigationMenuLink asChild>
<Link
href="/blog"
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div className="text-sm font-medium leading-none">Blog</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
Read our latest articles and updates
</p>
</Link>
</NavigationMenuLink>
<NavigationMenuLink asChild>
<Link
href="/resources"
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
>
<div className="text-sm font-medium leading-none">Resources</div>
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
Helpful guides and documentation
</p>
</Link>
</NavigationMenuLink>
</div>
</NavigationMenuContent>
</NavigationMenuItem>
<NavigationMenuItem>
<Link href="/contact" legacyBehavior passHref>
<NavigationMenuLink className="group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50">
Contact
</NavigationMenuLink>
</Link>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
<div className="flex items-center space-x-4">
<Button variant="ghost" asChild>
<Link href="/login">Login</Link>
</Button>
<Button asChild>
<Link href="/signup">Sign up free</Link>
</Button>
</div>
</div>
</header>
)
}
export default Navbar

View File

@@ -1,6 +1,5 @@
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 = ({
height,
@@ -12,75 +11,106 @@ 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 (
<div className="panel-terminal">
{terminalOutput.length > 0 ? (
// Render output from EditorArea when available
<>
{terminalOutput.map((line, index) => (
<div key={index} className={`terminal-line ${line.type === 'warning' ? 'terminal-warning' : 'terminal-output'}`}>
{line.type === 'command' ? <span className="terminal-prompt">$</span> : ''} {line.content}
</div>
))}
{waitingForInput && (
<div className="terminal-line">
<span className="terminal-prompt">Input:</span>
<input
type="text"
className="terminal-input"
value={userInput}
onChange={(e) => onUserInputChange && onUserInputChange(e.target.value)}
placeholder="Enter input for your program here..."
onKeyDown={(e) => {
if (e.key === 'Enter' && onInputSubmit) {
onInputSubmit();
// Auto-scroll terminal to the bottom when content changes
useEffect(() => {
if (terminalRef.current) {
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
}
}}
autoFocus
/>
}, [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 = () => (
<div
className="panel-terminal"
ref={terminalRef}
tabIndex={0} // Make div focusable
onClick={() => 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 (
<div key={index} className={`terminal-line ${typeClass}`}>
{line.timestamp && (
<span className="terminal-timestamp">{line.timestamp} </span>
)}
{line.type === "command" && <span className="terminal-prompt">$</span>}
{line.content}
</div>
);
})}
{isRunning && (
<div className="terminal-line terminal-input-line">
<span className="terminal-prompt">$</span> {inputBuffer}
<span className="terminal-cursor"></span>
</div>
)}
</>
) : (
// Default terminal content when no output
<>
<div className="terminal-line">
<span className="terminal-prompt">$</span> npm start
</div>
<div className="terminal-line terminal-output">Starting the development server...</div>
<div className="terminal-line terminal-output">Compiled successfully!</div>
<div className="terminal-line terminal-output">You can now view vscode-clone in the browser.</div>
<div className="terminal-line terminal-output">Local: http://localhost:3000</div>
<div className="terminal-line terminal-output">On Your Network: http://192.168.1.5:3000</div>
<div className="terminal-line">
<span className="terminal-prompt">$</span>
<span className="terminal-cursor"></span>
</div>
</>
)}
</div>
);
};
const renderProblems = () => {
return (
// Render other tabs
const renderProblems = () => (
<div className="panel-problems">
<div className="panel-empty-message">No problems have been detected in the workspace.</div>
</div>
);
};
const renderOutput = () => {
return (
const renderOutput = () => (
<div className="panel-output">
<div className="output-line">[Extension Host] Extension host started.</div>
<div className="output-line">[Language Server] Language server started.</div>
@@ -89,8 +119,27 @@ const Panel = ({
)}
</div>
);
};
const renderDebugConsole = () => (
<div className="panel-debug-console">
<div className="debug-line">Debug session not yet started.</div>
<div className="debug-line">Press F5 to start debugging.</div>
</div>
);
const renderPorts = () => (
<div className="panel-ports">
<div className="ports-line">No forwarded ports detected.</div>
</div>
);
const renderComments = () => (
<div className="panel-comments">
<div className="comments-line">No comments have been added to this workspace.</div>
</div>
);
// 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 <div>Unknown tab</div>;
}
@@ -107,76 +162,29 @@ const Panel = ({
return (
<div className="panel" style={{ height: `${height}px` }}>
<div className="panel-tabs">
{["problems", "output", "debug", "terminal", "ports", "comments"].map((tab) => (
<div
className={`panel-tab ${activeTab === "problems" ? "active" : ""}`}
onClick={() => setActiveTab("problems")}
key={tab}
className={`panel-tab ${activeTab === tab ? "active" : ""}`}
onClick={() => setActiveTab(tab)}
>
<span className="tab-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
</span>
<span className="tab-name">Problems</span>
</div>
<div className={`panel-tab ${activeTab === "output" ? "active" : ""}`} onClick={() => setActiveTab("output")}>
<span className="tab-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>
</span>
<span className="tab-name">Output</span>
</div>
<div
className={`panel-tab ${activeTab === "terminal" ? "active" : ""}`}
onClick={() => setActiveTab("terminal")}
>
<span className="tab-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="4 17 10 11 4 5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
</span>
<span className="tab-name">Terminal</span>
<span className="tab-name">{tab.toUpperCase()}</span>
</div>
))}
{/* Add close button */}
<div className="panel-actions">
{/* <button className="panel-action-btn">
<span className="current-terminal">node - frontend</span>
<ChevronDown size={16} />
</button>
<button className="panel-action-btn">
<Plus size={16} />
</button>
<button className="panel-action-btn">
<Maximize2 size={16} />
</button> */}
<button className="panel-close-btn" onClick={onClose}>
<X size={14} />
<X size={16} />
</button>
</div>
</div>
@@ -187,4 +195,3 @@ const Panel = ({
};
export default Panel;

View File

@@ -75,21 +75,6 @@ const Sidebar = ({
</svg>
)}
</span>
<span className="folder-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="#75beff"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
</svg>
</span>
<span className="folder-name">{name}</span>
</div>
{isExpanded && (
@@ -184,10 +169,38 @@ const Sidebar = ({
</div>
);
};
const getFileIcon = (fileName) => {
const extension = fileName.split('.').pop().toLowerCase();
if (fileName.toLowerCase() === 'readme.md') {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="#007acc" /* Blue color for the circle */
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10" fill="none" stroke="#007acc" />
<text
x="12"
y="15"
textAnchor="middle"
fontSize="10"
fill="#007acc"
fontFamily="Arial, sans-serif"
fontWeight="bold"
>
i
</text>
</svg>
);
}
if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) {
return (
<svg

View File

@@ -1,10 +1,11 @@
import React from "react";
"use client"
const StatusBar = ({ togglePanel, panelVisible }) => {
return (
<div className="status-bar">
{/* Left Section of the Status Bar */}
<div className="status-bar-left">
{/* Branch Indicator */}
<div className="status-item">
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -16,6 +17,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Branch Icon"
>
<line x1="6" y1="3" x2="6" y2="15"></line>
<circle cx="18" cy="6" r="3"></circle>
@@ -25,6 +27,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
<span>main</span>
</div>
{/* Error Indicator */}
<div className="status-item">
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -36,30 +39,14 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Error Icon"
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span>0 errors</span>
</div>
<button className="status-item status-button" onClick={togglePanel}>
<span>{panelVisible ? "Hide Terminal" : "Show Terminal"}</span>
</button>
</div>
<div className="status-bar-right">
<div className="status-item">
<span>Ln 1, Col 1</span>
</div>
<div className="status-item">
<span>Spaces: 2</span>
</div>
<div className="status-item">
<span>UTF-8</span>
</div>
{/* Warning Indicator */}
<div className="status-item">
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -71,6 +58,65 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Warning Icon"
>
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
<line x1="12" y1="9" x2="12" y2="13"></line>
<line x1="12" y1="17" x2="12.01" y2="17"></line>
</svg>
<span>0 warnings</span>
</div>
{/* Toggle Terminal Button */}
<button
className="status-item status-button"
onClick={togglePanel}
aria-label="Toggle Terminal"
>
<span>{panelVisible ? "Hide Terminal" : "Show Terminal"}</span>
</button>
</div>
{/* Right Section of the Status Bar */}
<div className="status-bar-right">
{/* Line and Column Indicator */}
<div className="status-item">
<span>Ln 1, Col 1</span>
</div>
{/* Spaces Indicator */}
<div className="status-item">
<span>Spaces: 2</span>
</div>
{/* Encoding Indicator */}
<div className="status-item">
<span>UTF-8</span>
</div>
{/* Language Mode */}
<div className="status-item">
<span>JavaScript</span>
</div>
{/* EOL (End of Line) Indicator */}
<div className="status-item">
<span>LF</span>
</div>
{/* Connection Status */}
<div className="status-item">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Connection Icon"
>
<path d="M5 12.55a11 11 0 0 1 14.08 0"></path>
<path d="M1.42 9a16 16 0 0 1 21.16 0"></path>
@@ -80,6 +126,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
<span>Connected</span>
</div>
{/* Bell Icon */}
<div className="status-item">
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -91,6 +138,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-label="Bell Icon"
>
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
@@ -98,8 +146,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
</div>
</div>
</div>
)
}
export default StatusBar
);
};
export default StatusBar;

View File

@@ -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 */

238
Readme.md
View File

@@ -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
- **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.

View File

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

View File

@@ -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=

View File

@@ -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()

View File

@@ -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)))

View File

@@ -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
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()
// 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)
}
stdout, stdoutErr := cmd.StdoutPipe()
if stdoutErr != nil {
return nil, fmt.Errorf("failed to create stdout pipe: %v", stdoutErr)
}
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 {
log.Printf("[ERROR-%s] Failed to create stdin pipe: %v", submissionID, err)
return nil, err
break
}
}
// 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)
}
done := make(chan struct{})
var output []byte
var err error
// Handle stderr in a goroutine
go func() {
log.Printf("[EXEC-%s] Starting command execution: %v", submissionID, cmd.Args)
output, err = cmd.CombinedOutput()
close(done)
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)

Binary file not shown.