@@ -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.
|
## Frontend Functionalities
|
||||||
- **Sidebar**: Displays file explorer, search results, and source control information.
|
- Built with React and Monaco Editor.
|
||||||
- **Editor Area**: Code editor with syntax highlighting and multiple tabs.
|
- File tree navigation for managing files and folders.
|
||||||
- **Panel**: Terminal, Problems, and Output views.
|
- Tab management for opening multiple files simultaneously.
|
||||||
- **Status Bar**: Displays status information and provides quick actions.
|
- 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.
|
||||||
|
|||||||
@@ -9,6 +9,32 @@ import {
|
|||||||
import Sidebar from "./Sidebar";
|
import Sidebar from "./Sidebar";
|
||||||
import Panel from "./Panel"; // Import Panel component
|
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 = ({
|
const EditorArea = ({
|
||||||
sidebarVisible = true,
|
sidebarVisible = true,
|
||||||
activeView = "explorer",
|
activeView = "explorer",
|
||||||
@@ -62,8 +88,8 @@ const EditorArea = ({
|
|||||||
|
|
||||||
// Add a new state for user input
|
// Add a new state for user input
|
||||||
const [userInput, setUserInput] = useState("");
|
const [userInput, setUserInput] = useState("");
|
||||||
// Add a new state for waiting for input
|
// Add socket state to track the connection
|
||||||
const [waitingForInput, setWaitingForInput] = useState(false);
|
const [activeSocket, setActiveSocket] = useState(null);
|
||||||
|
|
||||||
// Focus the input when new file modal opens
|
// Focus the input when new file modal opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -132,6 +158,41 @@ const EditorArea = ({
|
|||||||
}
|
}
|
||||||
}, [panelVisible]);
|
}, [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) => {
|
const handleEditorDidMount = (editor) => {
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
};
|
};
|
||||||
@@ -479,21 +540,31 @@ const EditorArea = ({
|
|||||||
case "README.md":
|
case "README.md":
|
||||||
return `# VS Code Clone Project
|
return `# VS Code Clone Project
|
||||||
|
|
||||||
## Overview
|
## Authors
|
||||||
This is a simple VS Code clone built with React and Monaco Editor.
|
- Arnab Bhowmik
|
||||||
|
- Ishika Bhoyar
|
||||||
|
|
||||||
## Features
|
## Description
|
||||||
- File tree navigation
|
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.
|
||||||
- Tab management
|
|
||||||
- Code editing with Monaco Editor
|
## Frontend Functionalities
|
||||||
- Syntax highlighting
|
- 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:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -507,7 +578,7 @@ Happy coding!`;
|
|||||||
width: `calc(100% - ${sidebarVisible ? sidebarWidth : 0}px)`
|
width: `calc(100% - ${sidebarVisible ? sidebarWidth : 0}px)`
|
||||||
};
|
};
|
||||||
|
|
||||||
// Modify the handleRunCode function to prompt for input first
|
// Update the handleRunCode function
|
||||||
const handleRunCode = async () => {
|
const handleRunCode = async () => {
|
||||||
if (!activeFile) return;
|
if (!activeFile) return;
|
||||||
|
|
||||||
@@ -517,49 +588,36 @@ Happy coding!`;
|
|||||||
setPanelVisible(true);
|
setPanelVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set state to waiting for input
|
|
||||||
setWaitingForInput(true);
|
|
||||||
setActiveRunningFile(activeFile.id);
|
|
||||||
|
|
||||||
// Clear previous output and add new command
|
// Clear previous output and add new command
|
||||||
const fileExtension = activeFile.id.split('.').pop().toLowerCase();
|
const fileExtension = activeFile.id.split('.').pop().toLowerCase();
|
||||||
const language = getLanguageFromExtension(fileExtension);
|
const language = getLanguageFromExtension(fileExtension);
|
||||||
|
|
||||||
const newOutput = [
|
const newOutput = [
|
||||||
{ type: 'command', content: `$ run ${activeFile.id}` },
|
{ 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);
|
setTerminalOutput(newOutput);
|
||||||
};
|
|
||||||
|
|
||||||
// Add a new function to handle input submission
|
try {
|
||||||
const handleInputSubmit = async () => {
|
// Close any existing socket
|
||||||
if (!activeFile || !waitingForInput) return;
|
if (activeSocket) {
|
||||||
|
activeSocket.close();
|
||||||
// Set running state
|
setActiveSocket(null);
|
||||||
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
|
// Use API URL from environment variable
|
||||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
||||||
|
|
||||||
try {
|
// Submit the code to get an execution ID
|
||||||
// Now make the API call with the input that was entered
|
|
||||||
const submitResponse = await fetch(`${apiUrl}/submit`, {
|
const submitResponse = await fetch(`${apiUrl}/submit`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
language: getLanguageFromExtension(activeFile.id.split('.').pop().toLowerCase()),
|
language: language,
|
||||||
code: activeFile.content,
|
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();
|
const { id } = await submitResponse.json();
|
||||||
setTerminalOutput(prev => [...prev, { type: 'output', content: `Job submitted with ID: ${id}` }]);
|
setTerminalOutput(prev => [...prev, { type: 'output', content: `Job submitted with ID: ${id}` }]);
|
||||||
|
|
||||||
// Step 2: Poll for status until completed or failed
|
// Set active running file
|
||||||
let status = 'pending';
|
setActiveRunningFile(activeFile.id);
|
||||||
while (status !== 'completed' && status !== 'failed') {
|
|
||||||
// Add a small delay between polls
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
|
// 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}`);
|
const statusResponse = await fetch(`${apiUrl}/status?id=${id}`);
|
||||||
if (!statusResponse.ok) {
|
if (statusResponse.ok) {
|
||||||
throw new Error(`Status check failed: ${statusResponse.status}`);
|
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();
|
// Close socket if it's still open
|
||||||
status = statusData.status;
|
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)
|
// Clean up interval when component unmounts or when socket closes
|
||||||
setTerminalOutput(prev => {
|
newSocket.addEventListener('close', () => {
|
||||||
// Update the last status message or add a new one
|
if (statusCheckInterval) {
|
||||||
const hasStatus = prev.some(line => line.content.includes('Status:'));
|
clearInterval(statusCheckInterval);
|
||||||
if (hasStatus) {
|
|
||||||
return prev.map(line =>
|
|
||||||
line.content.includes('Status:')
|
|
||||||
? { ...line, content: `Status: ${status}` }
|
|
||||||
: line
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return [...prev, { type: 'output', content: `Status: ${status}` }];
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the result for both completed and failed status
|
newSocket.onclose = (event) => {
|
||||||
const resultResponse = await fetch(`${apiUrl}/result?id=${id}`);
|
console.log("WebSocket closed:", event);
|
||||||
if (!resultResponse.ok) {
|
setIsRunning(false);
|
||||||
throw new Error(`Result fetch failed: ${resultResponse.status}`);
|
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
|
// Set the active socket after all handlers are defined
|
||||||
const outputLines = output.split('\n').map(line => ({
|
setActiveSocket(newSocket);
|
||||||
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) {
|
} catch (error) {
|
||||||
|
console.error("Run code error:", error);
|
||||||
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
|
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
|
||||||
} finally {
|
|
||||||
// Set running state to false
|
|
||||||
setIsRunning(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
|
// Update handleInputSubmit to ensure the input is sent properly
|
||||||
const getLanguageFromExtension = (extension) => {
|
const handleInputSubmit = (input) => {
|
||||||
const languageMap = {
|
// Use the direct input parameter instead of relying on userInput state
|
||||||
'java': 'java',
|
const textToSend = input || userInput;
|
||||||
'c': 'c',
|
|
||||||
'cpp': 'cpp',
|
|
||||||
'py': 'python',
|
|
||||||
'js': 'javascript',
|
|
||||||
'jsx': 'javascript',
|
|
||||||
'ts': 'typescript',
|
|
||||||
'tsx': 'typescript'
|
|
||||||
};
|
|
||||||
|
|
||||||
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
|
// Update this function to also update parent state
|
||||||
@@ -838,7 +998,6 @@ Happy coding!`;
|
|||||||
height={panelHeight}
|
height={panelHeight}
|
||||||
terminalOutput={terminalOutput}
|
terminalOutput={terminalOutput}
|
||||||
isRunning={isRunning}
|
isRunning={isRunning}
|
||||||
waitingForInput={waitingForInput}
|
|
||||||
activeRunningFile={activeRunningFile}
|
activeRunningFile={activeRunningFile}
|
||||||
initialTab="terminal"
|
initialTab="terminal"
|
||||||
onClose={togglePanel}
|
onClose={togglePanel}
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { useState, useEffect } from "react";
|
import { X, Maximize2, ChevronDown, Plus } from "lucide-react";
|
||||||
import { X } from "lucide-react";
|
|
||||||
|
|
||||||
const Panel = ({
|
const Panel = ({
|
||||||
height,
|
height,
|
||||||
@@ -12,75 +11,106 @@ const Panel = ({
|
|||||||
onClose,
|
onClose,
|
||||||
userInput = "",
|
userInput = "",
|
||||||
onUserInputChange,
|
onUserInputChange,
|
||||||
onInputSubmit
|
onInputSubmit,
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState(initialTab);
|
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(() => {
|
useEffect(() => {
|
||||||
setActiveTab(initialTab);
|
setActiveTab(initialTab);
|
||||||
}, [initialTab]);
|
}, [initialTab]);
|
||||||
|
|
||||||
const renderTerminal = () => {
|
// Auto-scroll terminal to the bottom when content changes
|
||||||
return (
|
useEffect(() => {
|
||||||
<div className="panel-terminal">
|
if (terminalRef.current) {
|
||||||
{terminalOutput.length > 0 ? (
|
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
}}
|
}, [terminalOutput]);
|
||||||
autoFocus
|
|
||||||
/>
|
// 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>
|
</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">
|
<div className="terminal-line">
|
||||||
<span className="terminal-prompt">$</span>
|
<span className="terminal-prompt">$</span>
|
||||||
|
<span className="terminal-cursor"></span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const renderProblems = () => {
|
// Render other tabs
|
||||||
return (
|
const renderProblems = () => (
|
||||||
<div className="panel-problems">
|
<div className="panel-problems">
|
||||||
<div className="panel-empty-message">No problems have been detected in the workspace.</div>
|
<div className="panel-empty-message">No problems have been detected in the workspace.</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const renderOutput = () => {
|
const renderOutput = () => (
|
||||||
return (
|
|
||||||
<div className="panel-output">
|
<div className="panel-output">
|
||||||
<div className="output-line">[Extension Host] Extension host started.</div>
|
<div className="output-line">[Extension Host] Extension host started.</div>
|
||||||
<div className="output-line">[Language Server] Language server started.</div>
|
<div className="output-line">[Language Server] Language server started.</div>
|
||||||
@@ -89,8 +119,27 @@ const Panel = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</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 = () => {
|
const getTabContent = () => {
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case "terminal":
|
case "terminal":
|
||||||
@@ -99,6 +148,12 @@ const Panel = ({
|
|||||||
return renderProblems();
|
return renderProblems();
|
||||||
case "output":
|
case "output":
|
||||||
return renderOutput();
|
return renderOutput();
|
||||||
|
case "debug":
|
||||||
|
return renderDebugConsole();
|
||||||
|
case "ports":
|
||||||
|
return renderPorts();
|
||||||
|
case "comments":
|
||||||
|
return renderComments();
|
||||||
default:
|
default:
|
||||||
return <div>Unknown tab</div>;
|
return <div>Unknown tab</div>;
|
||||||
}
|
}
|
||||||
@@ -107,76 +162,29 @@ const Panel = ({
|
|||||||
return (
|
return (
|
||||||
<div className="panel" style={{ height: `${height}px` }}>
|
<div className="panel" style={{ height: `${height}px` }}>
|
||||||
<div className="panel-tabs">
|
<div className="panel-tabs">
|
||||||
|
{["problems", "output", "debug", "terminal", "ports", "comments"].map((tab) => (
|
||||||
<div
|
<div
|
||||||
className={`panel-tab ${activeTab === "problems" ? "active" : ""}`}
|
key={tab}
|
||||||
onClick={() => setActiveTab("problems")}
|
className={`panel-tab ${activeTab === tab ? "active" : ""}`}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
>
|
>
|
||||||
<span className="tab-icon">
|
<span className="tab-name">{tab.toUpperCase()}</span>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
{/* Add close button */}
|
|
||||||
<div className="panel-actions">
|
<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}>
|
<button className="panel-close-btn" onClick={onClose}>
|
||||||
<X size={14} />
|
<X size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,4 +195,3 @@ const Panel = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default Panel;
|
export default Panel;
|
||||||
|
|
||||||
|
|||||||
@@ -75,21 +75,6 @@ const Sidebar = ({
|
|||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</span>
|
</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>
|
<span className="folder-name">{name}</span>
|
||||||
</div>
|
</div>
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
@@ -184,10 +169,38 @@ const Sidebar = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFileIcon = (fileName) => {
|
const getFileIcon = (fileName) => {
|
||||||
const extension = fileName.split('.').pop().toLowerCase();
|
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)) {
|
if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
"use client"
|
|
||||||
|
|
||||||
const StatusBar = ({ togglePanel, panelVisible }) => {
|
const StatusBar = ({ togglePanel, panelVisible }) => {
|
||||||
return (
|
return (
|
||||||
<div className="status-bar">
|
<div className="status-bar">
|
||||||
|
{/* Left Section of the Status Bar */}
|
||||||
<div className="status-bar-left">
|
<div className="status-bar-left">
|
||||||
|
{/* Branch Indicator */}
|
||||||
<div className="status-item">
|
<div className="status-item">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -16,6 +17,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
|||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
|
aria-label="Branch Icon"
|
||||||
>
|
>
|
||||||
<line x1="6" y1="3" x2="6" y2="15"></line>
|
<line x1="6" y1="3" x2="6" y2="15"></line>
|
||||||
<circle cx="18" cy="6" r="3"></circle>
|
<circle cx="18" cy="6" r="3"></circle>
|
||||||
@@ -25,6 +27,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
|||||||
<span>main</span>
|
<span>main</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Error Indicator */}
|
||||||
<div className="status-item">
|
<div className="status-item">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -36,30 +39,14 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
|||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
|
aria-label="Error Icon"
|
||||||
>
|
>
|
||||||
<polyline points="20 6 9 17 4 12"></polyline>
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
<span>0 errors</span>
|
<span>0 errors</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button className="status-item status-button" onClick={togglePanel}>
|
{/* Warning Indicator */}
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className="status-item">
|
<div className="status-item">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -71,6 +58,65 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
|||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="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="M5 12.55a11 11 0 0 1 14.08 0"></path>
|
||||||
<path d="M1.42 9a16 16 0 0 1 21.16 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>
|
<span>Connected</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Bell Icon */}
|
||||||
<div className="status-item">
|
<div className="status-item">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -91,6 +138,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
|||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="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="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>
|
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
|
||||||
@@ -98,8 +146,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default StatusBar
|
|
||||||
|
|
||||||
|
export default StatusBar;
|
||||||
@@ -57,12 +57,12 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: var(--vscode-activityBar-background);
|
background-color: var(--vscode-activityBar-background);
|
||||||
z-index: 10;
|
z-index: 10; /* Lower z-index than the StatusBar */
|
||||||
width: 50px;
|
position: fixed;
|
||||||
height: 100%;
|
|
||||||
position: fixed; /* Change to fixed to avoid layout issues */
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
height: calc(100% - 22px); /* Subtract the height of the StatusBar */
|
||||||
|
width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-bar button {
|
.activity-bar button {
|
||||||
@@ -404,7 +404,7 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-family: "Consolas", "Courier New", monospace;
|
font-family: "Consolas", "Courier New", monospace;
|
||||||
font-size: 13px;
|
font-size: 10px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-family: 'Consolas', 'Courier New', monospace;
|
font-family: 'Consolas', 'Courier New', monospace;
|
||||||
@@ -418,6 +418,16 @@ body {
|
|||||||
height: 100%;
|
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 {
|
.panel-terminal .terminal-line {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
@@ -426,22 +436,20 @@ body {
|
|||||||
.terminal-line {
|
.terminal-line {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-prompt {
|
.terminal-prompt {
|
||||||
color: #0f0;
|
color: #0a84ff;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
color: #569cd6;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-output {
|
.terminal-output {
|
||||||
color: #888888;
|
color: #ddd;
|
||||||
color: #cccccc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-warning {
|
.terminal-warning {
|
||||||
color: #ddb100;
|
color: #ffa500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.output-line {
|
.output-line {
|
||||||
@@ -463,9 +471,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink {
|
@keyframes blink {
|
||||||
50% {
|
0%, 100% { opacity: 1; }
|
||||||
opacity: 0;
|
50% { opacity: 0; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-empty-message {
|
.panel-empty-message {
|
||||||
@@ -964,9 +971,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink {
|
@keyframes blink {
|
||||||
50% {
|
0%, 100% { opacity: 1; }
|
||||||
opacity: 0;
|
50% { opacity: 0; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make sure the monaco container adjusts when terminal is shown */
|
/* Make sure the monaco container adjusts when terminal is shown */
|
||||||
|
|||||||
238
Readme.md
238
Readme.md
@@ -1,22 +1,240 @@
|
|||||||
# Monaco Code Execution Engine
|
# 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
|
## Features
|
||||||
- Multi-language support: Run code in Python, Java, C, and C++
|
|
||||||
- Secure execution: All code runs in isolated Docker containers
|
- **Multi-language support**: Run code in Python, Java, C, and C++
|
||||||
- Resource limits: Memory, CPU, and file descriptor limits to prevent abuse
|
- **Secure execution**: All code runs in isolated Docker containers
|
||||||
- Concurrent processing: Efficient job queue for handling multiple requests
|
- **Resource limits**: Memory, CPU, and file descriptor limits to prevent abuse
|
||||||
- Simple REST API: Easy to integrate with any frontend
|
- **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
|
## Architecture
|
||||||
|
|
||||||
Monaco consists of several components:
|
Monaco consists of several components:
|
||||||
|
|
||||||
- HTTP Handlers (handler/handler.go): Processes API requests
|
### Backend Components
|
||||||
- Execution Service (service/execution.go): Manages code execution in containers
|
|
||||||
- Job Queue (queue/queue.go): Handles concurrent execution of code submissions
|
- **HTTP Handlers** (`handler/handler.go`): Processes API requests and WebSocket connections
|
||||||
- Submission Model (model/submission.go): Defines the data structure for code submissions
|
- **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
|
## Requirements
|
||||||
|
|
||||||
|
- **Backend**:
|
||||||
- Go 1.22.3 or higher
|
- Go 1.22.3 or higher
|
||||||
- Docker
|
- Docker
|
||||||
- Network connectivity for container image pulling
|
- 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.
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ require github.com/stretchr/testify v1.9.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
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/kr/pretty v0.3.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
|||||||
@@ -2,12 +2,15 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/arnab-afk/monaco/model"
|
"github.com/arnab-afk/monaco/model"
|
||||||
"github.com/arnab-afk/monaco/service"
|
"github.com/arnab-afk/monaco/service"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler manages HTTP requests for code submissions
|
// 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
|
// generateID creates a unique ID for submissions
|
||||||
func (h *Handler) generateID() string {
|
func (h *Handler) generateID() string {
|
||||||
return service.GenerateUUID()
|
return service.GenerateUUID()
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/arnab-afk/monaco/handler"
|
"github.com/arnab-afk/monaco/handler"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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("/submit", corsMiddleware(loggingMiddleware(h.SubmitHandler)))
|
||||||
http.HandleFunc("/status", corsMiddleware(loggingMiddleware(h.StatusHandler)))
|
http.HandleFunc("/status", corsMiddleware(loggingMiddleware(h.StatusHandler)))
|
||||||
http.HandleFunc("/result", corsMiddleware(loggingMiddleware(h.ResultHandler)))
|
http.HandleFunc("/result", corsMiddleware(loggingMiddleware(h.ResultHandler)))
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
@@ -8,25 +10,101 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/arnab-afk/monaco/model"
|
"github.com/arnab-afk/monaco/model"
|
||||||
"github.com/arnab-afk/monaco/queue"
|
"github.com/arnab-afk/monaco/queue"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ExecutionService handles code execution for multiple languages
|
// ExecutionService handles code execution for multiple languages
|
||||||
type ExecutionService struct {
|
type ExecutionService struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
queue *queue.JobQueue
|
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
|
// NewExecutionService creates a new execution service
|
||||||
func NewExecutionService() *ExecutionService {
|
func NewExecutionService() *ExecutionService {
|
||||||
log.Println("Initializing execution service with 3 concurrent workers")
|
log.Println("Initializing execution service with 3 concurrent workers")
|
||||||
return &ExecutionService{
|
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) {
|
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)
|
log.Printf("[TIMEOUT-%s] Setting execution timeout: %v", submissionID, timeout)
|
||||||
|
|
||||||
// Set up input pipe if input is provided
|
// Create pipes for stdin, stdout, and stderr
|
||||||
if input != "" {
|
stdin, stdinErr := cmd.StdinPipe()
|
||||||
stdin, err := 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 {
|
if err != nil {
|
||||||
log.Printf("[ERROR-%s] Failed to create stdin pipe: %v", submissionID, err)
|
break
|
||||||
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)
|
// Handle stderr in a goroutine
|
||||||
}
|
|
||||||
|
|
||||||
done := make(chan struct{})
|
|
||||||
var output []byte
|
|
||||||
var err error
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
log.Printf("[EXEC-%s] Starting command execution: %v", submissionID, cmd.Args)
|
buffer := make([]byte, 1024)
|
||||||
output, err = cmd.CombinedOutput()
|
for {
|
||||||
close(done)
|
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 {
|
select {
|
||||||
case <-time.After(timeout):
|
case <-time.After(timeout):
|
||||||
|
cancel() // Stop the input handler
|
||||||
log.Printf("[TIMEOUT-%s] Execution timed out after %v seconds", submissionID, timeout.Seconds())
|
log.Printf("[TIMEOUT-%s] Execution timed out after %v seconds", submissionID, timeout.Seconds())
|
||||||
if err := cmd.Process.Kill(); err != nil {
|
if err := cmd.Process.Kill(); err != nil {
|
||||||
log.Printf("[TIMEOUT-%s] Failed to kill process: %v", submissionID, err)
|
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())
|
s.SendOutputToTerminals(submissionID, fmt.Sprintf("\n[System] Process killed after timeout of %v seconds", timeout.Seconds()))
|
||||||
case <-done:
|
return outputBuffer.Bytes(), fmt.Errorf("execution timed out after %v seconds", timeout.Seconds())
|
||||||
if err != nil {
|
case err := <-done:
|
||||||
log.Printf("[EXEC-%s] Command execution failed: %v", submissionID, err)
|
cancel() // Stop the input handler
|
||||||
} else {
|
s.SendOutputToTerminals(submissionID, "\n[System] Process completed")
|
||||||
log.Printf("[EXEC-%s] Command execution completed successfully", submissionID)
|
return outputBuffer.Bytes(), err
|
||||||
}
|
|
||||||
return output, err
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,15 +322,9 @@ func (s *ExecutionService) executePython(submission *model.CodeSubmission) {
|
|||||||
"python:3.9", "python", "-c", submission.Code)
|
"python:3.9", "python", "-c", submission.Code)
|
||||||
|
|
||||||
log.Printf("[PYTHON-%s] Executing Python code with timeout: 10s", submission.ID)
|
log.Printf("[PYTHON-%s] Executing Python code with timeout: 10s", submission.ID)
|
||||||
var output []byte
|
|
||||||
var err error
|
|
||||||
|
|
||||||
if submission.Input != "" {
|
// Use the enhanced executeWithInput method for all executions
|
||||||
cmd.Stdin = strings.NewReader(submission.Input)
|
output, err := s.executeWithInput(cmd, submission.Input, 100*time.Second, submission.ID)
|
||||||
output, err = cmd.CombinedOutput()
|
|
||||||
} else {
|
|
||||||
output, err = s.executeWithTimeout(cmd, 10*time.Second, submission.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
elapsed := time.Since(startTime)
|
elapsed := time.Since(startTime)
|
||||||
log.Printf("[PYTHON-%s] Python execution completed in %v", submission.ID, elapsed)
|
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)
|
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",
|
runCmd := exec.Command("docker", "run", "--rm", "-i",
|
||||||
"--network=none", // No network access
|
"--network=none", // No network access
|
||||||
"--memory=400m", // Memory limit
|
"--memory=400m", // Memory limit
|
||||||
@@ -267,17 +414,8 @@ func (s *ExecutionService) executeJava(submission *model.CodeSubmission) {
|
|||||||
"-Xverify:none", "-Xms64m", "-Xmx256m",
|
"-Xverify:none", "-Xms64m", "-Xmx256m",
|
||||||
"-cp", "/code", className)
|
"-cp", "/code", className)
|
||||||
|
|
||||||
// Add input if provided
|
log.Printf("[JAVA-%s] Executing Java code", submission.ID)
|
||||||
var output []byte
|
output, err := s.executeWithInput(runCmd, submission.Input, 15*time.Second, submission.ID)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
elapsed := time.Since(startTime)
|
elapsed := time.Since(startTime)
|
||||||
log.Printf("[JAVA-%s] Java execution completed in %v", submission.ID, elapsed)
|
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)
|
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",
|
runCmd := exec.Command("docker", "run", "--rm", "-i",
|
||||||
"--network=none", // No network access
|
"--network=none", // No network access
|
||||||
"--memory=100m", // Memory limit
|
"--memory=100m", // Memory limit
|
||||||
@@ -336,17 +474,8 @@ func (s *ExecutionService) executeC(submission *model.CodeSubmission) {
|
|||||||
"-v", tempDir+":/code", // Mount code directory
|
"-v", tempDir+":/code", // Mount code directory
|
||||||
"gcc:latest", "/code/solution")
|
"gcc:latest", "/code/solution")
|
||||||
|
|
||||||
// Add input if provided
|
log.Printf("[C-%s] Executing C code", submission.ID)
|
||||||
var output []byte
|
output, err := s.executeWithInput(runCmd, submission.Input, 30*time.Second, submission.ID)
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
elapsed := time.Since(startTime)
|
elapsed := time.Since(startTime)
|
||||||
log.Printf("[C-%s] C execution completed in %v", submission.ID, elapsed)
|
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)
|
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",
|
runCmd := exec.Command("docker", "run", "--rm", "-i",
|
||||||
"--network=none", // No network access
|
"--network=none", // No network access
|
||||||
"--memory=100m", // Memory limit
|
"--memory=100m", // Memory limit
|
||||||
@@ -405,16 +534,8 @@ func (s *ExecutionService) executeCpp(submission *model.CodeSubmission) {
|
|||||||
"-v", tempDir+":/code", // Mount code directory
|
"-v", tempDir+":/code", // Mount code directory
|
||||||
"gcc:latest", "/code/solution")
|
"gcc:latest", "/code/solution")
|
||||||
|
|
||||||
// Add input if provided
|
log.Printf("[CPP-%s] Executing C++ code", submission.ID)
|
||||||
var output []byte
|
output, err := s.executeWithInput(runCmd, submission.Input, 100*time.Second, submission.ID)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
elapsed := time.Since(startTime)
|
elapsed := time.Since(startTime)
|
||||||
log.Printf("[CPP-%s] C++ execution completed in %v", submission.ID, elapsed)
|
log.Printf("[CPP-%s] C++ execution completed in %v", submission.ID, elapsed)
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user