Compare commits
3 Commits
socket
...
socket_rew
| Author | SHA1 | Date | |
|---|---|---|---|
| 4453e69e68 | |||
| c143efa70e | |||
| 86dcfa2a4a |
@@ -1,24 +1,13 @@
|
||||
# VS Code Clone Project
|
||||
# VSCode Clone with React and Vite
|
||||
|
||||
## Authors
|
||||
- Arnab Bhowmik
|
||||
- Ishika Bhoyar
|
||||
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.
|
||||
|
||||
## 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.
|
||||
## Features
|
||||
|
||||
## 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.
|
||||
- **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.
|
||||
|
||||
## 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.
|
||||
## Project Structure
|
||||
|
||||
@@ -8,32 +8,7 @@ import {
|
||||
} from "lucide-react";
|
||||
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';
|
||||
};
|
||||
import WebSocketTerminal from "./WebSocketTerminal"; // Import WebSocket Terminal
|
||||
|
||||
const EditorArea = ({
|
||||
sidebarVisible = true,
|
||||
@@ -88,8 +63,12 @@ const EditorArea = ({
|
||||
|
||||
// Add a new state for user input
|
||||
const [userInput, setUserInput] = useState("");
|
||||
// Add socket state to track the connection
|
||||
const [activeSocket, setActiveSocket] = useState(null);
|
||||
// Add a new state for waiting for input
|
||||
const [waitingForInput, setWaitingForInput] = useState(false);
|
||||
// Add a new state for tracking the active submission ID
|
||||
const [activeRunningSubmissionId, setActiveRunningSubmissionId] = useState(null);
|
||||
// Add a state to toggle between regular and WebSocket terminals
|
||||
const [useWebSocket, setUseWebSocket] = useState(false);
|
||||
|
||||
// Focus the input when new file modal opens
|
||||
useEffect(() => {
|
||||
@@ -158,41 +137,6 @@ 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;
|
||||
};
|
||||
@@ -540,31 +484,21 @@ const EditorArea = ({
|
||||
case "README.md":
|
||||
return `# VS Code Clone Project
|
||||
|
||||
## Authors
|
||||
- Arnab Bhowmik
|
||||
- Ishika Bhoyar
|
||||
## Overview
|
||||
This is a simple VS Code clone built with React and Monaco Editor.
|
||||
|
||||
## 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.
|
||||
`;
|
||||
## Features
|
||||
- File tree navigation
|
||||
- Tab management
|
||||
- Code editing with Monaco Editor
|
||||
- Syntax highlighting
|
||||
|
||||
## 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 "";
|
||||
}
|
||||
@@ -578,7 +512,7 @@ This project is a VS Code Clone built with React and Monaco Editor. It features
|
||||
width: `calc(100% - ${sidebarVisible ? sidebarWidth : 0}px)`
|
||||
};
|
||||
|
||||
// Update the handleRunCode function
|
||||
// Modified handleRunCode to start execution immediately
|
||||
const handleRunCode = async () => {
|
||||
if (!activeFile) return;
|
||||
|
||||
@@ -588,36 +522,47 @@ This project is a VS Code Clone built with React and Monaco Editor. It features
|
||||
setPanelVisible(true);
|
||||
}
|
||||
|
||||
// Clear previous output and add new command
|
||||
// Reset states
|
||||
setIsRunning(true);
|
||||
setWaitingForInput(false);
|
||||
setActiveRunningFile(activeFile.id);
|
||||
setActiveRunningSubmissionId(null);
|
||||
setUserInput('');
|
||||
|
||||
// Get language from file extension
|
||||
const fileExtension = activeFile.id.split('.').pop().toLowerCase();
|
||||
const language = getLanguageFromExtension(fileExtension);
|
||||
|
||||
// If using WebSocket mode, we'll use the WebSocketTerminal component
|
||||
if (useWebSocket) {
|
||||
// Just set the running state, the WebSocketTerminal will handle the rest
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular HTTP mode - use polling
|
||||
// Clear previous output and add new command
|
||||
const newOutput = [
|
||||
{ type: 'command', content: `$ run ${activeFile.id}` },
|
||||
{ type: 'output', content: 'Submitting code...' }
|
||||
{ type: 'output', content: '------- PROGRAM EXECUTION -------' },
|
||||
{ type: 'output', content: `Language: ${language}` },
|
||||
{ type: 'output', content: 'Executing code...' }
|
||||
];
|
||||
setTerminalOutput(newOutput);
|
||||
|
||||
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';
|
||||
|
||||
// Submit the code to get an execution ID
|
||||
try {
|
||||
// Submit the code for execution immediately
|
||||
const submitResponse = await fetch(`${apiUrl}/submit`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
language: language,
|
||||
language: getLanguageFromExtension(fileExtension),
|
||||
code: activeFile.content,
|
||||
input: "" // Explicitly passing empty input, no user input handling
|
||||
input: '' // No initial input
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -626,191 +571,263 @@ This project is a VS Code Clone built with React and Monaco Editor. It features
|
||||
}
|
||||
|
||||
const { id } = await submitResponse.json();
|
||||
setActiveRunningSubmissionId(id);
|
||||
setTerminalOutput(prev => [...prev, { type: 'output', content: `Job submitted with ID: ${id}` }]);
|
||||
|
||||
// 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) {
|
||||
const statusData = await statusResponse.json();
|
||||
|
||||
// If the process is completed or failed, stop polling and update UI
|
||||
if (statusData.status === 'completed' || statusData.status === 'failed') {
|
||||
clearInterval(statusCheckInterval);
|
||||
console.log("Process status:", statusData.status);
|
||||
|
||||
// Update the UI to show process is no longer running
|
||||
setIsRunning(false);
|
||||
|
||||
// Display the final result if WebSocket didn't capture it
|
||||
if (statusData.output && statusData.output.length > 0) {
|
||||
setTerminalOutput(prev => {
|
||||
// Check if the output is already in the terminal
|
||||
const lastOutput = prev[prev.length - 1]?.content || "";
|
||||
if (!lastOutput.includes(statusData.output)) {
|
||||
return [...prev, {
|
||||
type: 'output',
|
||||
content: `\n[System] Final output:\n${statusData.output}`
|
||||
}];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
|
||||
// Close socket if it's still open
|
||||
if (newSocket && newSocket.readyState === WebSocket.OPEN) {
|
||||
newSocket.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start polling for status and output
|
||||
pollForStatusAndOutput(id);
|
||||
} catch (error) {
|
||||
console.error("Status check error:", error);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// Clean up interval when component unmounts or when socket closes
|
||||
newSocket.addEventListener('close', () => {
|
||||
if (statusCheckInterval) {
|
||||
clearInterval(statusCheckInterval);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
newSocket.onerror = (event) => {
|
||||
console.error("WebSocket error:", event);
|
||||
setTerminalOutput(prev => [...prev, {
|
||||
type: 'warning',
|
||||
content: `WebSocket error occurred`
|
||||
}]);
|
||||
};
|
||||
|
||||
// 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}` }]);
|
||||
setIsRunning(false);
|
||||
|
||||
// Also add cleanup in the error handler
|
||||
if (statusCheckInterval) {
|
||||
clearInterval(statusCheckInterval);
|
||||
}
|
||||
setActiveRunningSubmissionId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 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;
|
||||
// Toggle between WebSocket and HTTP modes
|
||||
const toggleWebSocketMode = () => {
|
||||
setUseWebSocket(!useWebSocket);
|
||||
};
|
||||
|
||||
console.log("Input submit called, active socket state:",
|
||||
activeSocket ? activeSocket.readyState : "no socket",
|
||||
"input:", textToSend);
|
||||
// Simplified handleInputSubmit to only handle interactive input
|
||||
const handleInputSubmit = async () => {
|
||||
if (!waitingForInput || !activeRunningSubmissionId) return;
|
||||
|
||||
if (!activeSocket) {
|
||||
console.warn("Cannot send input: No active socket");
|
||||
setTerminalOutput(prev => [...prev, {
|
||||
type: 'warning',
|
||||
content: `Cannot send input: No active connection`
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
// Store the input value before clearing it
|
||||
const inputValue = userInput;
|
||||
|
||||
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;
|
||||
}
|
||||
// Clear the input field and reset waiting state immediately for better UX
|
||||
setUserInput('');
|
||||
setWaitingForInput(false);
|
||||
|
||||
if (!textToSend.trim()) {
|
||||
console.warn("Cannot send empty input");
|
||||
return;
|
||||
}
|
||||
// Add the input to the terminal immediately
|
||||
setTerminalOutput(prev => [
|
||||
...prev,
|
||||
{ type: 'input', content: inputValue }
|
||||
]);
|
||||
|
||||
// Use API URL from environment variable
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
||||
|
||||
try {
|
||||
// Add the input to the terminal display
|
||||
setTerminalOutput(prev => [...prev, { type: 'command', content: `> ${textToSend}` }]);
|
||||
// Submit input to the running program
|
||||
const submitInputResponse = await fetch(`${apiUrl}/submit-input`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: activeRunningSubmissionId,
|
||||
input: inputValue
|
||||
}),
|
||||
});
|
||||
|
||||
// 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}`
|
||||
}]);
|
||||
if (!submitInputResponse.ok) {
|
||||
throw new Error(`Server error: ${submitInputResponse.status}`);
|
||||
}
|
||||
|
||||
// Wait for a moment to allow the program to process the input
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Poll for status and check if we need more input
|
||||
pollForStatusAndOutput(activeRunningSubmissionId);
|
||||
|
||||
} catch (error) {
|
||||
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
|
||||
setIsRunning(false);
|
||||
setActiveRunningSubmissionId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Add a function to poll for status and output
|
||||
const pollForStatusAndOutput = async (id) => {
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
||||
|
||||
try {
|
||||
// Step 2: Poll for status until completed, failed, or waiting_for_input
|
||||
let status = 'pending';
|
||||
while (status !== 'completed' && status !== 'failed' && status !== 'waiting_for_input') {
|
||||
// Add a small delay between polls
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const statusResponse = await fetch(`${apiUrl}/status?id=${id}`);
|
||||
if (!statusResponse.ok) {
|
||||
throw new Error(`Status check failed: ${statusResponse.status}`);
|
||||
}
|
||||
|
||||
const statusData = await statusResponse.json();
|
||||
status = statusData.status;
|
||||
|
||||
// Update terminal with status (for any status type)
|
||||
setTerminalOutput(prev => {
|
||||
// Update the last status message or add a new one
|
||||
const hasStatus = prev.some(line => line.content.includes('Status:'));
|
||||
if (hasStatus) {
|
||||
return prev.map(line =>
|
||||
line.content.includes('Status:')
|
||||
? { ...line, content: `Status: ${status}` }
|
||||
: line
|
||||
);
|
||||
} else {
|
||||
return [...prev, { type: 'output', content: `Status: ${status}` }];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if we're waiting for input
|
||||
if (status === 'waiting_for_input') {
|
||||
// Get the current output to display to the user
|
||||
const resultResponse = await fetch(`${apiUrl}/result?id=${id}`);
|
||||
if (resultResponse.ok) {
|
||||
const { output } = await resultResponse.json();
|
||||
|
||||
// Process the output to show what's happened so far
|
||||
const outputLines = [];
|
||||
let promptText = '';
|
||||
|
||||
// Split by lines and process each line
|
||||
const lines = output.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
if (line.startsWith('[Input] ')) {
|
||||
// This is an input line
|
||||
outputLines.push({
|
||||
type: 'input',
|
||||
content: line.substring(8) // Remove the '[Input] ' prefix
|
||||
});
|
||||
} else if (line === '[WAITING_FOR_INPUT]') {
|
||||
// This is a marker for waiting for input
|
||||
// If there's a line before this, it's likely the prompt
|
||||
if (i > 0 && lines[i-1].trim() !== '') {
|
||||
promptText = lines[i-1];
|
||||
}
|
||||
continue;
|
||||
} else if (line.trim() !== '') {
|
||||
// This is a regular output line
|
||||
outputLines.push({
|
||||
type: 'output',
|
||||
content: line
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update the terminal with the current output
|
||||
if (outputLines.length > 0) {
|
||||
setTerminalOutput(prev => {
|
||||
// Keep only the essential lines to avoid duplication
|
||||
const essentialLines = prev.filter(line =>
|
||||
line.type === 'command' ||
|
||||
line.content.includes('PROGRAM EXECUTION') ||
|
||||
line.content.includes('Language:') ||
|
||||
line.content.includes('Job submitted') ||
|
||||
line.content.includes('Status:') ||
|
||||
line.content === 'Executing code...'
|
||||
);
|
||||
return [...essentialLines, ...outputLines];
|
||||
});
|
||||
}
|
||||
|
||||
// Now set the waiting for input state
|
||||
setWaitingForInput(true);
|
||||
|
||||
// Add a message indicating we're waiting for input
|
||||
setTerminalOutput(prev => {
|
||||
// Remove any existing waiting message
|
||||
const filteredPrev = prev.filter(line =>
|
||||
line.content !== 'Waiting for input...'
|
||||
);
|
||||
|
||||
// Add the prompt text if available
|
||||
if (promptText) {
|
||||
return [...filteredPrev, {
|
||||
type: 'prompt',
|
||||
content: promptText
|
||||
}, {
|
||||
type: 'output',
|
||||
content: 'Waiting for input...'
|
||||
}];
|
||||
} else {
|
||||
return [...filteredPrev, {
|
||||
type: 'output',
|
||||
content: 'Waiting for input...'
|
||||
}];
|
||||
}
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the result for both completed and failed status
|
||||
const resultResponse = await fetch(`${apiUrl}/result?id=${id}`);
|
||||
if (!resultResponse.ok) {
|
||||
throw new Error(`Result fetch failed: ${resultResponse.status}`);
|
||||
}
|
||||
|
||||
const { output } = await resultResponse.json();
|
||||
|
||||
// Format and display output
|
||||
const outputLines = [];
|
||||
|
||||
// Add a header
|
||||
outputLines.push({
|
||||
type: status === 'failed' ? 'warning' : 'output',
|
||||
content: status === 'failed'
|
||||
? '------- EXECUTION FAILED -------'
|
||||
: '------- EXECUTION RESULT -------'
|
||||
});
|
||||
|
||||
// Process the output line by line
|
||||
output.split('\n').forEach(line => {
|
||||
// Check if this is an input line
|
||||
if (line.startsWith('[Input] ')) {
|
||||
outputLines.push({
|
||||
type: 'input',
|
||||
content: line.substring(8) // Remove the '[Input] ' prefix
|
||||
});
|
||||
} else {
|
||||
outputLines.push({
|
||||
type: status === 'failed' ? 'warning' : 'output',
|
||||
content: line
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setTerminalOutput(prev => [
|
||||
...prev,
|
||||
...outputLines
|
||||
]);
|
||||
|
||||
if (status === 'failed') {
|
||||
console.error('Code execution failed:', output);
|
||||
}
|
||||
|
||||
// Reset state
|
||||
setIsRunning(false);
|
||||
setWaitingForInput(false);
|
||||
setActiveRunningSubmissionId(null);
|
||||
|
||||
} catch (error) {
|
||||
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
|
||||
setIsRunning(false);
|
||||
setWaitingForInput(false);
|
||||
setActiveRunningSubmissionId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// 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'
|
||||
};
|
||||
|
||||
return languageMap[extension] || extension;
|
||||
};
|
||||
|
||||
// Update this function to also update parent state
|
||||
@@ -927,15 +944,38 @@ This project is a VS Code Clone built with React and Monaco Editor. It features
|
||||
title="Run code"
|
||||
>
|
||||
{isRunning ? <Loader size={16} className="animate-spin" /> : <Play size={16} />}
|
||||
|
||||
</button>
|
||||
<button
|
||||
className="terminal-toggle-button"
|
||||
onClick={togglePanel} // Use the new function
|
||||
onClick={togglePanel}
|
||||
title="Toggle terminal"
|
||||
>
|
||||
<Terminal size={16} />
|
||||
</button>
|
||||
<button
|
||||
className={`websocket-toggle-button ${useWebSocket ? 'active' : ''}`}
|
||||
onClick={toggleWebSocketMode}
|
||||
title={`${useWebSocket ? 'Disable' : 'Enable'} WebSocket mode`}
|
||||
>
|
||||
<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"
|
||||
>
|
||||
<path d="M5 12s2-3 5-3 5 3 5 3-2 3-5 3-5-3-5-3z"></path>
|
||||
<circle cx="12" cy="12" r="1"></circle>
|
||||
<path d="M2 4l3 3"></path>
|
||||
<path d="M22 4l-3 3"></path>
|
||||
<path d="M2 20l3-3"></path>
|
||||
<path d="M22 20l-3-3"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -994,10 +1034,20 @@ This project is a VS Code Clone built with React and Monaco Editor. It features
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
}}
|
||||
/>
|
||||
{useWebSocket && activeFile ? (
|
||||
<div style={{ height: panelHeight + 'px' }}>
|
||||
<WebSocketTerminal
|
||||
code={activeFile.content}
|
||||
language={getLanguageFromExtension(activeFile.id.split('.').pop().toLowerCase())}
|
||||
onClose={togglePanel}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Panel
|
||||
height={panelHeight}
|
||||
terminalOutput={terminalOutput}
|
||||
isRunning={isRunning}
|
||||
waitingForInput={waitingForInput}
|
||||
activeRunningFile={activeRunningFile}
|
||||
initialTab="terminal"
|
||||
onClose={togglePanel}
|
||||
@@ -1005,6 +1055,7 @@ This project is a VS Code Clone built with React and Monaco Editor. It features
|
||||
onUserInputChange={setUserInput}
|
||||
onInputSubmit={handleInputSubmit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
150
Frontend/src/components/Navbar.jsx
Normal file
150
Frontend/src/components/Navbar.jsx
Normal file
@@ -0,0 +1,150 @@
|
||||
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,5 +1,6 @@
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { X, Maximize2, ChevronDown, Plus } from "lucide-react";
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
const Panel = ({
|
||||
height,
|
||||
@@ -11,106 +12,86 @@ const Panel = ({
|
||||
onClose,
|
||||
userInput = "",
|
||||
onUserInputChange,
|
||||
onInputSubmit,
|
||||
onInputSubmit
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState(initialTab);
|
||||
const terminalRef = useRef(null);
|
||||
const [inputBuffer, setInputBuffer] = useState("");
|
||||
|
||||
// Update active tab when initialTab changes
|
||||
// Set active tab when initialTab changes
|
||||
useEffect(() => {
|
||||
setActiveTab(initialTab);
|
||||
}, [initialTab]);
|
||||
|
||||
// Auto-scroll terminal to the bottom when content changes
|
||||
useEffect(() => {
|
||||
if (terminalRef.current) {
|
||||
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
|
||||
}
|
||||
}, [terminalOutput]);
|
||||
|
||||
// Handle keyboard input for the terminal
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (!isRunning) return;
|
||||
|
||||
if (e.key === "Enter") {
|
||||
if (inputBuffer.trim() && onInputSubmit) {
|
||||
e.preventDefault();
|
||||
// Update parent's userInput state directly and call submit in the same function
|
||||
// instead of using setTimeout which creates a race condition
|
||||
onUserInputChange(inputBuffer);
|
||||
onInputSubmit(inputBuffer); // Pass inputBuffer directly to avoid race condition
|
||||
setInputBuffer("");
|
||||
}
|
||||
} else if (e.key === "Backspace") {
|
||||
setInputBuffer((prev) => prev.slice(0, -1));
|
||||
} else if (e.key.length === 1) {
|
||||
setInputBuffer((prev) => prev + e.key);
|
||||
}
|
||||
};
|
||||
|
||||
const terminalElement = terminalRef.current;
|
||||
terminalElement?.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
terminalElement?.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [isRunning, inputBuffer, onInputSubmit, onUserInputChange]);
|
||||
|
||||
// Render the terminal tab
|
||||
const renderTerminal = () => (
|
||||
<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";
|
||||
|
||||
const renderTerminal = () => {
|
||||
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>}
|
||||
<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' : line.type === 'input' ? 'terminal-input-line' : line.type === 'prompt' ? 'terminal-prompt-line' : 'terminal-output'}`}>
|
||||
{line.type === 'command' ? <span className="terminal-prompt">$</span> : ''}
|
||||
{line.type === 'input' ? <span className="terminal-input-marker">[Input]</span> : ''}
|
||||
{line.type === 'prompt' ? <span className="terminal-prompt-marker">></span> : ''}
|
||||
{line.content}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{isRunning && (
|
||||
<div className="terminal-line terminal-input-line">
|
||||
<span className="terminal-prompt">$</span> {inputBuffer}
|
||||
<span className="terminal-cursor"></span>
|
||||
))}
|
||||
{waitingForInput && (
|
||||
<div className="terminal-line terminal-input-container">
|
||||
<div className="terminal-input-header">
|
||||
<span className="terminal-input-marker">Input Required:</span>
|
||||
</div>
|
||||
<div className="terminal-input-wrapper">
|
||||
<div className="terminal-input-prompt">></div>
|
||||
<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();
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="terminal-input-help">
|
||||
Press Enter to submit input
|
||||
</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">
|
||||
<span className="terminal-prompt">$</span>
|
||||
<span className="terminal-cursor"></span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// Render other tabs
|
||||
const renderProblems = () => (
|
||||
const renderProblems = () => {
|
||||
return (
|
||||
<div className="panel-problems">
|
||||
<div className="panel-empty-message">No problems have been detected in the workspace.</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOutput = () => (
|
||||
const renderOutput = () => {
|
||||
return (
|
||||
<div className="panel-output">
|
||||
<div className="output-line">[Extension Host] Extension host started.</div>
|
||||
<div className="output-line">[Language Server] Language server started.</div>
|
||||
@@ -119,27 +100,8 @@ 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":
|
||||
@@ -148,12 +110,6 @@ 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>;
|
||||
}
|
||||
@@ -162,29 +118,76 @@ const Panel = ({
|
||||
return (
|
||||
<div className="panel" style={{ height: `${height}px` }}>
|
||||
<div className="panel-tabs">
|
||||
{["problems", "output", "debug", "terminal", "ports", "comments"].map((tab) => (
|
||||
<div
|
||||
key={tab}
|
||||
className={`panel-tab ${activeTab === tab ? "active" : ""}`}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`panel-tab ${activeTab === "problems" ? "active" : ""}`}
|
||||
onClick={() => setActiveTab("problems")}
|
||||
>
|
||||
<span className="tab-name">{tab.toUpperCase()}</span>
|
||||
<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>
|
||||
</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={16} />
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,3 +198,4 @@ const Panel = ({
|
||||
};
|
||||
|
||||
export default Panel;
|
||||
|
||||
|
||||
@@ -75,6 +75,21 @@ 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 && (
|
||||
@@ -169,38 +184,10 @@ 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
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
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"
|
||||
@@ -17,7 +16,6 @@ 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>
|
||||
@@ -27,7 +25,6 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
||||
<span>main</span>
|
||||
</div>
|
||||
|
||||
{/* Error Indicator */}
|
||||
<div className="status-item">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -39,72 +36,30 @@ 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>
|
||||
|
||||
{/* Warning Indicator */}
|
||||
<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="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"
|
||||
>
|
||||
<button className="status-item status-button" onClick={togglePanel}>
|
||||
<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"
|
||||
@@ -116,7 +71,6 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
||||
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>
|
||||
@@ -126,7 +80,6 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
||||
<span>Connected</span>
|
||||
</div>
|
||||
|
||||
{/* Bell Icon */}
|
||||
<div className="status-item">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -138,7 +91,6 @@ 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>
|
||||
@@ -146,7 +98,8 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusBar
|
||||
|
||||
export default StatusBar;
|
||||
239
Frontend/src/components/WebSocketTerminal.jsx
Normal file
239
Frontend/src/components/WebSocketTerminal.jsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
const WebSocketTerminal = ({ code, language, onClose }) => {
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [output, setOutput] = useState([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [submissionId, setSubmissionId] = useState(null);
|
||||
const wsRef = useRef(null);
|
||||
const outputRef = useRef(null);
|
||||
|
||||
// Auto-scroll to bottom of output
|
||||
useEffect(() => {
|
||||
if (outputRef.current) {
|
||||
outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
||||
}
|
||||
}, [output]);
|
||||
|
||||
// Connect to WebSocket on component mount
|
||||
useEffect(() => {
|
||||
// Use API URL from environment variable
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
||||
const wsUrl = apiUrl.replace('http://', 'ws://').replace('https://', 'wss://');
|
||||
|
||||
// Create WebSocket connection
|
||||
wsRef.current = new WebSocket(`${wsUrl}/ws`);
|
||||
|
||||
// Connection opened
|
||||
wsRef.current.addEventListener('open', () => {
|
||||
setConnected(true);
|
||||
setOutput(prev => [...prev, { type: 'system', content: 'Connected to server' }]);
|
||||
|
||||
// Send the code submission
|
||||
const submission = {
|
||||
language,
|
||||
code
|
||||
};
|
||||
wsRef.current.send(JSON.stringify(submission));
|
||||
});
|
||||
|
||||
// Listen for messages
|
||||
wsRef.current.addEventListener('message', (event) => {
|
||||
const message = event.data;
|
||||
|
||||
// Check if this is a submission ID message
|
||||
if (message.startsWith('Submission ID: ')) {
|
||||
const id = message.substring('Submission ID: '.length);
|
||||
setSubmissionId(id);
|
||||
setOutput(prev => [...prev, { type: 'system', content: `Execution started with ID: ${id}` }]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular output
|
||||
setOutput(prev => [...prev, { type: 'output', content: message }]);
|
||||
});
|
||||
|
||||
// Connection closed
|
||||
wsRef.current.addEventListener('close', () => {
|
||||
setConnected(false);
|
||||
setOutput(prev => [...prev, { type: 'system', content: 'Disconnected from server' }]);
|
||||
});
|
||||
|
||||
// Connection error
|
||||
wsRef.current.addEventListener('error', (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
setOutput(prev => [...prev, { type: 'error', content: 'Connection error' }]);
|
||||
});
|
||||
|
||||
// Clean up on unmount
|
||||
return () => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
}
|
||||
};
|
||||
}, [code, language]);
|
||||
|
||||
// Handle input submission
|
||||
const handleInputSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (!input.trim() || !connected) return;
|
||||
|
||||
// Send input to server
|
||||
wsRef.current.send(input);
|
||||
|
||||
// Add input to output display
|
||||
setOutput(prev => [...prev, { type: 'input', content: input }]);
|
||||
|
||||
// Clear input field
|
||||
setInput('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="websocket-terminal">
|
||||
<div className="terminal-header">
|
||||
<div className="terminal-title">
|
||||
{connected ? 'Connected' : 'Disconnected'}
|
||||
{submissionId && ` - Execution ID: ${submissionId}`}
|
||||
</div>
|
||||
<button className="terminal-close" onClick={onClose}>×</button>
|
||||
</div>
|
||||
|
||||
<div className="terminal-output" ref={outputRef}>
|
||||
{output.map((line, index) => (
|
||||
<div key={index} className={`terminal-line ${line.type}`}>
|
||||
{line.type === 'input' && <span className="input-prefix">> </span>}
|
||||
{line.content}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form className="terminal-input-form" onSubmit={handleInputSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Enter input..."
|
||||
disabled={!connected}
|
||||
className="terminal-input-field"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!connected}
|
||||
className="terminal-input-submit"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<style jsx>{`
|
||||
.websocket-terminal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
font-family: 'Consolas', monospace;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background-color: #252526;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.terminal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #d4d4d4;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.terminal-line {
|
||||
margin-bottom: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.terminal-line.system {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.terminal-line.error {
|
||||
color: #f44747;
|
||||
}
|
||||
|
||||
.terminal-line.input {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.input-prefix {
|
||||
color: #569cd6;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.terminal-input-form {
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
background-color: #252526;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
|
||||
.terminal-input-field {
|
||||
flex: 1;
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
border: 1px solid #3c3c3c;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
font-family: 'Consolas', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.terminal-input-field:focus {
|
||||
outline: none;
|
||||
border-color: #007acc;
|
||||
}
|
||||
|
||||
.terminal-input-submit {
|
||||
margin-left: 8px;
|
||||
background-color: #0e639c;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.terminal-input-submit:hover {
|
||||
background-color: #1177bb;
|
||||
}
|
||||
|
||||
.terminal-input-submit:disabled {
|
||||
background-color: #3c3c3c;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebSocketTerminal;
|
||||
@@ -57,12 +57,12 @@ body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--vscode-activityBar-background);
|
||||
z-index: 10; /* Lower z-index than the StatusBar */
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
width: 50px;
|
||||
height: 100%;
|
||||
position: fixed; /* Change to fixed to avoid layout issues */
|
||||
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: 10px;
|
||||
font-size: 13px;
|
||||
padding: 8px;
|
||||
padding: 10px;
|
||||
font-family: 'Consolas', 'Courier New', monospace;
|
||||
@@ -418,16 +418,6 @@ 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;
|
||||
@@ -436,20 +426,46 @@ body {
|
||||
.terminal-line {
|
||||
white-space: pre-wrap;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.terminal-prompt {
|
||||
color: #0a84ff;
|
||||
color: #0f0;
|
||||
margin-right: 8px;
|
||||
color: #569cd6;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.terminal-input-marker {
|
||||
color: #4ec9b0;
|
||||
margin-right: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.terminal-prompt-marker {
|
||||
color: #569cd6;
|
||||
margin-right: 8px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.terminal-prompt-line {
|
||||
color: #569cd6;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.terminal-input-line {
|
||||
color: #4ec9b0;
|
||||
background-color: rgba(78, 201, 176, 0.1);
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
color: #ddd;
|
||||
color: #888888;
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.terminal-warning {
|
||||
color: #ffa500;
|
||||
color: #ddb100;
|
||||
}
|
||||
|
||||
.output-line {
|
||||
@@ -471,8 +487,9 @@ body {
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.panel-empty-message {
|
||||
@@ -869,7 +886,7 @@ body {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.terminal-toggle-button {
|
||||
.terminal-toggle-button, .websocket-toggle-button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: #cccccc;
|
||||
@@ -880,7 +897,12 @@ body {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.terminal-toggle-button:hover {
|
||||
.terminal-toggle-button:hover, .websocket-toggle-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.websocket-toggle-button.active {
|
||||
color: #4ec9b0;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -932,18 +954,159 @@ body {
|
||||
}
|
||||
|
||||
.terminal-input {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: inherit;
|
||||
background-color: rgba(78, 201, 176, 0.1);
|
||||
border: 1px solid rgba(78, 201, 176, 0.3);
|
||||
border-radius: 3px;
|
||||
color: #4ec9b0;
|
||||
font-family: monospace;
|
||||
font-size: inherit;
|
||||
margin-left: 8px;
|
||||
outline: none;
|
||||
width: calc(100% - 60px);
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.terminal-input:focus {
|
||||
outline: none;
|
||||
border-color: rgba(78, 201, 176, 0.6);
|
||||
}
|
||||
|
||||
.terminal-input-container {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: rgba(78, 201, 176, 0.05);
|
||||
border-radius: 5px;
|
||||
border-left: 3px solid #4ec9b0;
|
||||
}
|
||||
|
||||
.terminal-input-header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.terminal-input-wrapper {
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.terminal-input-prompt {
|
||||
color: #4ec9b0;
|
||||
font-weight: bold;
|
||||
margin-right: 8px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.terminal-input-help {
|
||||
font-size: 12px;
|
||||
color: #888888;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* WebSocket Terminal */
|
||||
.websocket-terminal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
font-family: 'Consolas', monospace;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background-color: #252526;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.terminal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #d4d4d4;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.terminal-line {
|
||||
margin-bottom: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.terminal-line.system {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.terminal-line.error {
|
||||
color: #f44747;
|
||||
}
|
||||
|
||||
.terminal-line.input {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.input-prefix {
|
||||
color: #569cd6;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.terminal-input-form {
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
background-color: #252526;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
|
||||
.terminal-input-field {
|
||||
flex: 1;
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
border: 1px solid #3c3c3c;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
font-family: 'Consolas', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.terminal-input-field:focus {
|
||||
outline: none;
|
||||
border-color: #007acc;
|
||||
}
|
||||
|
||||
.terminal-input-submit {
|
||||
margin-left: 8px;
|
||||
background-color: #0e639c;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.terminal-input-submit:hover {
|
||||
background-color: #1177bb;
|
||||
}
|
||||
|
||||
.terminal-input-submit:disabled {
|
||||
background-color: #3c3c3c;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.terminal-line.info {
|
||||
@@ -971,8 +1134,9 @@ body {
|
||||
}
|
||||
|
||||
@keyframes blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0; }
|
||||
50% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Make sure the monaco container adjusts when terminal is shown */
|
||||
|
||||
297
Readme.md
297
Readme.md
@@ -1,240 +1,125 @@
|
||||
# Monaco Code Execution Engine
|
||||
# Monaco Online Code Compiler
|
||||
|
||||
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.
|
||||
A full-featured online code compiler with a VS Code-like interface. This project allows users to write, edit, and execute code in multiple programming languages directly in the browser.
|
||||
|
||||
## Features
|
||||
|
||||
- **Multi-language support**: Run code in Python, Java, C, and C++
|
||||
- **Secure execution**: All code runs in isolated Docker containers
|
||||
- **Resource limits**: Memory, CPU, and file descriptor limits to prevent abuse
|
||||
- **Concurrent processing**: Efficient job queue for handling multiple requests
|
||||
- **Simple REST API**: Easy to integrate with any frontend
|
||||
- **Interactive terminal**: Real-time code execution with input/output via WebSockets
|
||||
- **VS Code-like interface**: Modern editor with syntax highlighting and file management
|
||||
- **VS Code-like Interface**: Familiar editor experience with syntax highlighting, tabs, and file explorer
|
||||
- **Multi-language Support**: Run code in Python, JavaScript, Go, Java, C, and C++
|
||||
- **Input/Output Handling**: Enter input for your programs and see the output in real-time
|
||||
- **Secure Execution**: Code runs in isolated Docker containers on the backend
|
||||
- **File Management**: Create, edit, and organize files and folders
|
||||
|
||||
## Architecture
|
||||
## Project Structure
|
||||
|
||||
Monaco consists of several components:
|
||||
- **Frontend**: React-based UI with Monaco Editor
|
||||
- **Backend**: Go-based code execution service with Docker integration
|
||||
- HTTP Handlers (internal/api/handlers): Processes API requests
|
||||
- Execution Service (internal/executor): Manages code execution in containers
|
||||
- Job Queue (internal/queue): Handles concurrent execution of code submissions
|
||||
- Submission Model (internal/models): Defines the data structure for code submissions
|
||||
|
||||
### Backend Components
|
||||
## Getting Started
|
||||
|
||||
- **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
|
||||
### Prerequisites
|
||||
|
||||
### Frontend Components
|
||||
- Node.js 18+ for the frontend
|
||||
- Go 1.22+ for the backend
|
||||
- Docker for code execution
|
||||
|
||||
- **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
|
||||
### Running the Frontend
|
||||
|
||||
### 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.
|
||||
The frontend will be available at http://localhost:5173
|
||||
|
||||
### API Reference
|
||||
### Running the Backend
|
||||
|
||||
### REST Endpoints
|
||||
```POST /submit```
|
||||
|
||||
Submits code for execution
|
||||
```json
|
||||
{
|
||||
"language": "python",
|
||||
"code": "print('Hello, World!')",
|
||||
"input": ""
|
||||
}
|
||||
```bash
|
||||
cd backend
|
||||
go build -o monaco ./cmd/server
|
||||
./monaco
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1"
|
||||
}
|
||||
```
|
||||
The backend API will be available at http://localhost:8080
|
||||
|
||||
```GET /status?id={submissionId}```
|
||||
## Using the Online Compiler
|
||||
|
||||
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
|
||||
}
|
||||
```
|
||||
1. **Create a File**: Click the "+" button in the editor tabs or use the file explorer
|
||||
2. **Write Code**: Use the Monaco editor to write your code
|
||||
3. **Run Code**: Click the "Play" button in the top right corner
|
||||
4. **Enter Input**: If your program requires input, enter it in the terminal panel
|
||||
5. **View Output**: See the execution results in the terminal panel
|
||||
|
||||
```GET /result?id={submissionId}```
|
||||
## Supported Languages
|
||||
|
||||
Gets the execution result of a submission.
|
||||
- **Python** (.py)
|
||||
- **JavaScript** (.js)
|
||||
- **Go** (.go)
|
||||
- **Java** (.java)
|
||||
- **C** (.c)
|
||||
- **C++** (.cpp)
|
||||
|
||||
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"
|
||||
}
|
||||
```
|
||||
## Examples
|
||||
|
||||
```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
|
||||
```python
|
||||
name = input("Enter your name: ")
|
||||
print(f"Hello, {name}!")
|
||||
for i in range(5):
|
||||
print(f"Count: {i}")
|
||||
```
|
||||
|
||||
### Security Considerations
|
||||
All code execution happens within isolated Docker containers with:
|
||||
### JavaScript
|
||||
|
||||
- No network access (```--network=none```)
|
||||
- Limited CPU and memory resources
|
||||
- Limited file system access
|
||||
- No persistent storage
|
||||
- Execution time limits (10-15 seconds)
|
||||
```javascript
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
### 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.
|
||||
rl.question('Enter your name: ', (name) => {
|
||||
console.log(`Hello, ${name}!`);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
console.log(`Count: ${i}`);
|
||||
}
|
||||
rl.close();
|
||||
});
|
||||
```
|
||||
|
||||
### Contributing
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
### Go
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Print("Enter your name: ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
name, _ := reader.ReadString('\n')
|
||||
name = strings.TrimSpace(name)
|
||||
fmt.Printf("Hello, %s!\n", name)
|
||||
for i := 0; i < 5; i++ {
|
||||
fmt.Printf("Count: %d\n", i)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- All code is executed in isolated Docker containers
|
||||
- Network access is disabled
|
||||
- Memory and CPU limits are enforced
|
||||
- Execution timeouts prevent infinite loops
|
||||
@@ -20,7 +20,7 @@
|
||||
Monaco is a secure, containerized code execution backend service designed to run user-submitted code in multiple programming languages. It features a job queue system to manage execution resources, containerized execution environments for security, and a RESTful API for submission and monitoring.
|
||||
|
||||
**Key Features:**
|
||||
- Multi-language support (Python, Java, C, C++)
|
||||
- Multi-language support (Python, JavaScript, Go, Java, C, C++)
|
||||
- Secure containerized execution using Docker
|
||||
- Resource limiting to prevent abuse
|
||||
- Job queuing for managing concurrent executions
|
||||
@@ -34,10 +34,10 @@ Monaco is a secure, containerized code execution backend service designed to run
|
||||
|
||||
Monaco follows a layered architecture with the following key components:
|
||||
|
||||
1. **HTTP Handlers** (handler package) - Processes incoming HTTP requests
|
||||
2. **Execution Service** (service package) - Manages code execution in containers
|
||||
3. **Job Queue** (queue package) - Controls concurrent execution
|
||||
4. **Data Models** (model package) - Defines data structures
|
||||
1. **HTTP Handlers** (internal/api/handlers) - Processes incoming HTTP requests
|
||||
2. **Execution Service** (internal/executor) - Manages code execution in containers
|
||||
3. **Job Queue** (internal/queue) - Controls concurrent execution
|
||||
4. **Data Models** (internal/models) - Defines data structures
|
||||
|
||||
### Request Flow
|
||||
|
||||
@@ -64,6 +64,8 @@ Client Request → HTTP Handlers → Execution Service → Job Queue → Docker
|
||||
- Docker Engine
|
||||
- Docker images for supported languages:
|
||||
- `python:3.9`
|
||||
- `node:18-alpine`
|
||||
- `golang:1.22-alpine`
|
||||
- `eclipse-temurin:11-jdk-alpine`
|
||||
- `gcc:latest`
|
||||
|
||||
@@ -82,7 +84,7 @@ Client Request → HTTP Handlers → Execution Service → Job Queue → Docker
|
||||
|
||||
3. Build the application:
|
||||
```bash
|
||||
go build -o monaco main.go
|
||||
go build -o monaco ./cmd/server
|
||||
```
|
||||
|
||||
4. Run the service:
|
||||
@@ -103,7 +105,7 @@ Submits code for execution.
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"language": "python", // Required: "python", "java", "c", or "cpp"
|
||||
"language": "python", // Required: "python", "javascript", "go", "java", "c", or "cpp"
|
||||
"code": "print('Hello, World!')", // Required: source code to execute
|
||||
"input": "optional input string" // Optional: input to stdin
|
||||
}
|
||||
@@ -256,6 +258,17 @@ The queue tracks and reports:
|
||||
- **Input Handling**: Direct stdin piping
|
||||
- **Limitations**: No file I/O, no package imports outside standard library
|
||||
|
||||
### JavaScript
|
||||
- **Version**: Node.js 18 (Alpine)
|
||||
- **Input Handling**: File-based input redirection
|
||||
- **Limitations**: No file I/O, no package imports outside standard library
|
||||
|
||||
### Go
|
||||
- **Version**: Go 1.22 (Alpine)
|
||||
- **Compilation**: Standard Go build process
|
||||
- **Input Handling**: Direct stdin piping
|
||||
- **Limitations**: No file I/O, no external dependencies
|
||||
|
||||
### Java
|
||||
- **Version**: Java 11 (Eclipse Temurin)
|
||||
- **Class Detection**: Extracts class name from code using regex
|
||||
|
||||
38
backend/cmd/server/main.go
Normal file
38
backend/cmd/server/main.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Configure logging
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
log.Println("Starting Monaco code execution backend...")
|
||||
|
||||
// Initialize router with all routes
|
||||
router := api.SetupRoutes()
|
||||
|
||||
// Start the server
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: router,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
log.Printf("Server started at :%s", port)
|
||||
log.Fatal(server.ListenAndServe())
|
||||
}
|
||||
155
backend/examples/examples.md
Normal file
155
backend/examples/examples.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# Monaco Code Execution Examples
|
||||
|
||||
This document provides examples of code submissions for each supported language.
|
||||
|
||||
## Python
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "python",
|
||||
"code": "name = input('Enter your name: ')\nprint(f'Hello, {name}!')\nfor i in range(5):\n print(f'Count: {i}')",
|
||||
"input": "World"
|
||||
}
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Enter your name: Hello, World!
|
||||
Count: 0
|
||||
Count: 1
|
||||
Count: 2
|
||||
Count: 3
|
||||
Count: 4
|
||||
```
|
||||
|
||||
## JavaScript
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "javascript",
|
||||
"code": "const readline = require('readline');\nconst rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout\n});\n\nrl.question('Enter your name: ', (name) => {\n console.log(`Hello, ${name}!`);\n for (let i = 0; i < 5; i++) {\n console.log(`Count: ${i}`);\n }\n rl.close();\n});",
|
||||
"input": "World"
|
||||
}
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Enter your name: Hello, World!
|
||||
Count: 0
|
||||
Count: 1
|
||||
Count: 2
|
||||
Count: 3
|
||||
Count: 4
|
||||
```
|
||||
|
||||
## Go
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "go",
|
||||
"code": "package main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\nfunc main() {\n\tfmt.Print(\"Enter your name: \")\n\treader := bufio.NewReader(os.Stdin)\n\tname, _ := reader.ReadString('\\n')\n\tname = strings.TrimSpace(name)\n\tfmt.Printf(\"Hello, %s!\\n\", name)\n\tfor i := 0; i < 5; i++ {\n\t\tfmt.Printf(\"Count: %d\\n\", i)\n\t}\n}",
|
||||
"input": "World"
|
||||
}
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Enter your name: Hello, World!
|
||||
Count: 0
|
||||
Count: 1
|
||||
Count: 2
|
||||
Count: 3
|
||||
Count: 4
|
||||
```
|
||||
|
||||
## Java
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "java",
|
||||
"code": "import java.util.Scanner;\n\npublic class Main {\n public static void main(String[] args) {\n Scanner scanner = new Scanner(System.in);\n System.out.print(\"Enter your name: \");\n String name = scanner.nextLine();\n System.out.println(\"Hello, \" + name + \"!\");\n for (int i = 0; i < 5; i++) {\n System.out.println(\"Count: \" + i);\n }\n scanner.close();\n }\n}",
|
||||
"input": "World"
|
||||
}
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Enter your name: Hello, World!
|
||||
Count: 0
|
||||
Count: 1
|
||||
Count: 2
|
||||
Count: 3
|
||||
Count: 4
|
||||
```
|
||||
|
||||
## C
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "c",
|
||||
"code": "#include <stdio.h>\n\nint main() {\n char name[100];\n printf(\"Enter your name: \");\n scanf(\"%s\", name);\n printf(\"Hello, %s!\\n\", name);\n for (int i = 0; i < 5; i++) {\n printf(\"Count: %d\\n\", i);\n }\n return 0;\n}",
|
||||
"input": "World"
|
||||
}
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Enter your name: Hello, World!
|
||||
Count: 0
|
||||
Count: 1
|
||||
Count: 2
|
||||
Count: 3
|
||||
Count: 4
|
||||
```
|
||||
|
||||
## C++
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "cpp",
|
||||
"code": "#include <iostream>\n#include <string>\n\nint main() {\n std::string name;\n std::cout << \"Enter your name: \";\n std::cin >> name;\n std::cout << \"Hello, \" << name << \"!\" << std::endl;\n for (int i = 0; i < 5; i++) {\n std::cout << \"Count: \" << i << std::endl;\n }\n return 0;\n}",
|
||||
"input": "World"
|
||||
}
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Enter your name: Hello, World!
|
||||
Count: 0
|
||||
Count: 1
|
||||
Count: 2
|
||||
Count: 3
|
||||
Count: 4
|
||||
```
|
||||
|
||||
## Testing with cURL
|
||||
|
||||
You can test these examples using cURL:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/submit \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"language": "python",
|
||||
"code": "name = input(\"Enter your name: \")\nprint(f\"Hello, {name}!\")\nfor i in range(5):\n print(f\"Count: {i}\")",
|
||||
"input": "World"
|
||||
}'
|
||||
```
|
||||
|
||||
This will return a submission ID:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1"
|
||||
}
|
||||
```
|
||||
|
||||
You can then check the status and result:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/status?id=6423259c-ee14-c5aa-1c90-d5e989f92aa1
|
||||
```
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/result?id=6423259c-ee14-c5aa-1c90-d5e989f92aa1
|
||||
```
|
||||
@@ -2,15 +2,12 @@ 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
|
||||
@@ -182,63 +179,6 @@ 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()
|
||||
|
||||
85
backend/handler/websocket.go
Normal file
85
backend/handler/websocket.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/arnab-afk/monaco/model"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
// Allow all origins for development
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
// WebSocketHandler handles WebSocket connections for code execution
|
||||
func (h *Handler) WebSocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Upgrade the HTTP connection to a WebSocket connection
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to upgrade connection: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Read the initial message containing the code submission
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("Failed to read message: %v", err)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the message as a code submission
|
||||
var submission model.CodeSubmission
|
||||
if err := json.Unmarshal(message, &submission); err != nil {
|
||||
log.Printf("Failed to parse message: %v", err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Invalid submission format"))
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the submission
|
||||
if submission.Code == "" {
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Code is required"))
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Set default language if not provided
|
||||
if submission.Language == "" {
|
||||
submission.Language = "python" // Default to Python
|
||||
}
|
||||
|
||||
// Validate language
|
||||
supportedLanguages := map[string]bool{
|
||||
"python": true,
|
||||
"java": true,
|
||||
"c": true,
|
||||
"cpp": true,
|
||||
}
|
||||
|
||||
if !supportedLanguages[submission.Language] {
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Unsupported language: "+submission.Language))
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a unique ID for the submission
|
||||
submission.ID = h.generateID()
|
||||
submission.Status = "pending"
|
||||
|
||||
// Store the submission
|
||||
h.mu.Lock()
|
||||
h.submissions[submission.ID] = &submission
|
||||
h.mu.Unlock()
|
||||
|
||||
// Send the submission ID to the client
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Submission ID: "+submission.ID))
|
||||
|
||||
// Execute the code with WebSocket communication
|
||||
h.executionService.HandleWebSocket(conn, &submission)
|
||||
}
|
||||
260
backend/internal/api/handlers/handlers.go
Normal file
260
backend/internal/api/handlers/handlers.go
Normal file
@@ -0,0 +1,260 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/executor"
|
||||
"github.com/arnab-afk/monaco/internal/models"
|
||||
)
|
||||
|
||||
// Handler manages HTTP requests for code submissions
|
||||
type Handler struct {
|
||||
executionService *executor.ExecutionService
|
||||
mu sync.Mutex
|
||||
submissions map[string]*models.CodeSubmission
|
||||
}
|
||||
|
||||
// NewHandler creates a new handler instance
|
||||
func NewHandler() *Handler {
|
||||
return &Handler{
|
||||
executionService: executor.NewExecutionService(),
|
||||
submissions: make(map[string]*models.CodeSubmission),
|
||||
}
|
||||
}
|
||||
|
||||
// SubmitHandler handles code submission requests
|
||||
func (h *Handler) SubmitHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow POST method
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the request body
|
||||
var submission models.CodeSubmission
|
||||
if err := json.NewDecoder(r.Body).Decode(&submission); err != nil {
|
||||
http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the submission
|
||||
if submission.Code == "" {
|
||||
http.Error(w, "Code is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if submission.Language == "" {
|
||||
http.Error(w, "Language is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a unique ID for the submission
|
||||
h.mu.Lock()
|
||||
submission.ID = executor.GenerateUUID()
|
||||
submission.Status = "pending"
|
||||
h.submissions[submission.ID] = &submission
|
||||
h.mu.Unlock()
|
||||
|
||||
// Execute the code in a goroutine
|
||||
go h.executionService.ExecuteCode(&submission)
|
||||
|
||||
// Return the submission ID
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
json.NewEncoder(w).Encode(map[string]string{"id": submission.ID})
|
||||
}
|
||||
|
||||
// StatusHandler handles status check requests
|
||||
func (h *Handler) StatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow GET method
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the submission ID from the query parameters
|
||||
id := r.URL.Query().Get("id")
|
||||
if id == "" {
|
||||
http.Error(w, "ID is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the submission from the map
|
||||
h.mu.Lock()
|
||||
submission, exists := h.submissions[id]
|
||||
h.mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
http.Error(w, "Submission not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Return the submission status
|
||||
response := map[string]interface{}{
|
||||
"id": submission.ID,
|
||||
"status": submission.Status,
|
||||
}
|
||||
|
||||
// Add time information based on status
|
||||
if !submission.QueuedAt.IsZero() {
|
||||
response["queuedAt"] = submission.QueuedAt.Format(time.RFC3339)
|
||||
}
|
||||
if !submission.StartedAt.IsZero() {
|
||||
response["startedAt"] = submission.StartedAt.Format(time.RFC3339)
|
||||
}
|
||||
if !submission.CompletedAt.IsZero() {
|
||||
response["completedAt"] = submission.CompletedAt.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// ResultHandler handles result requests
|
||||
func (h *Handler) ResultHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow GET method
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the submission ID from the query parameters
|
||||
id := r.URL.Query().Get("id")
|
||||
if id == "" {
|
||||
http.Error(w, "ID is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the submission from the map
|
||||
h.mu.Lock()
|
||||
submission, exists := h.submissions[id]
|
||||
h.mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
http.Error(w, "Submission not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Return the submission result
|
||||
response := map[string]interface{}{
|
||||
"id": submission.ID,
|
||||
"status": submission.Status,
|
||||
"language": submission.Language,
|
||||
"output": submission.Output,
|
||||
"input": submission.Input,
|
||||
}
|
||||
|
||||
// Add error information if available
|
||||
if submission.Error != "" {
|
||||
response["error"] = submission.Error
|
||||
}
|
||||
|
||||
// Add time information
|
||||
if !submission.QueuedAt.IsZero() {
|
||||
response["queuedAt"] = submission.QueuedAt.Format(time.RFC3339)
|
||||
}
|
||||
if !submission.StartedAt.IsZero() {
|
||||
response["startedAt"] = submission.StartedAt.Format(time.RFC3339)
|
||||
}
|
||||
if !submission.CompletedAt.IsZero() {
|
||||
response["completedAt"] = submission.CompletedAt.Format(time.RFC3339)
|
||||
if !submission.StartedAt.IsZero() {
|
||||
response["executionTime"] = submission.CompletedAt.Sub(submission.StartedAt).Milliseconds()
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// QueueStatsHandler provides information about the job queue
|
||||
func (h *Handler) QueueStatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow GET method
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the queue statistics
|
||||
stats := h.executionService.GetQueueStats()
|
||||
|
||||
// Return the queue statistics
|
||||
response := map[string]interface{}{
|
||||
"queue_stats": stats,
|
||||
"submissions": len(h.submissions),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// SubmitInputHandler handles interactive input submission
|
||||
func (h *Handler) SubmitInputHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow POST method
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the request body
|
||||
var inputRequest struct {
|
||||
ID string `json:"id"`
|
||||
Input string `json:"input"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&inputRequest); err != nil {
|
||||
http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the request
|
||||
if inputRequest.ID == "" {
|
||||
http.Error(w, "ID is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the submission from the map
|
||||
h.mu.Lock()
|
||||
submission, exists := h.submissions[inputRequest.ID]
|
||||
h.mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
http.Error(w, "Submission not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the submission is waiting for input or running
|
||||
// We're more lenient here to handle race conditions
|
||||
if submission.Status != "waiting_for_input" && submission.Status != "running" {
|
||||
http.Error(w, fmt.Sprintf("Submission is not waiting for input (status: %s)", submission.Status), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Send the input to the execution service
|
||||
h.executionService.SubmitInput(submission, inputRequest.Input)
|
||||
|
||||
// Return success response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "input_submitted"})
|
||||
}
|
||||
|
||||
// HealthCheckHandler handles health check requests
|
||||
func (h *Handler) HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow GET method
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Return a simple health check response
|
||||
response := map[string]interface{}{
|
||||
"status": "ok",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
70
backend/internal/api/handlers/handlers_test.go
Normal file
70
backend/internal/api/handlers/handlers_test.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSubmitHandler(t *testing.T) {
|
||||
h := NewHandler()
|
||||
|
||||
// Create a test request
|
||||
reqBody := map[string]string{
|
||||
"language": "python",
|
||||
"code": "print('Hello, World!')",
|
||||
"input": "",
|
||||
}
|
||||
reqJSON, _ := json.Marshal(reqBody)
|
||||
req, err := http.NewRequest("POST", "/submit", bytes.NewBuffer(reqJSON))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Create a response recorder
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// Call the handler
|
||||
h.SubmitHandler(rr, req)
|
||||
|
||||
// Check the status code
|
||||
assert.Equal(t, http.StatusAccepted, rr.Code)
|
||||
|
||||
// Check the response body
|
||||
var response map[string]string
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, response, "id")
|
||||
assert.NotEmpty(t, response["id"])
|
||||
}
|
||||
|
||||
func TestHealthCheckHandler(t *testing.T) {
|
||||
h := NewHandler()
|
||||
|
||||
// Create a test request
|
||||
req, err := http.NewRequest("GET", "/health", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a response recorder
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// Call the handler
|
||||
h.HealthCheckHandler(rr, req)
|
||||
|
||||
// Check the status code
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
// Check the response body
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ok", response["status"])
|
||||
assert.Contains(t, response, "timestamp")
|
||||
}
|
||||
49
backend/internal/api/handlers/middleware.go
Normal file
49
backend/internal/api/handlers/middleware.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoggingMiddleware logs HTTP requests
|
||||
func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
log.Printf("[HTTP] %s %s %s", r.Method, r.URL.Path, r.RemoteAddr)
|
||||
next.ServeHTTP(w, r)
|
||||
log.Printf("[HTTP] %s %s completed in %v", r.Method, r.URL.Path, time.Since(startTime))
|
||||
})
|
||||
}
|
||||
|
||||
// CORSMiddleware adds CORS headers to responses
|
||||
func CORSMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Set CORS headers
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
// Handle preflight requests
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the next handler
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RecoveryMiddleware recovers from panics
|
||||
func RecoveryMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Printf("[PANIC] %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
261
backend/internal/api/handlers/websocket.go
Normal file
261
backend/internal/api/handlers/websocket.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/executor"
|
||||
"github.com/arnab-afk/monaco/internal/models"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// WebSocketTerminal represents a terminal session over WebSocket
|
||||
type WebSocketTerminal struct {
|
||||
ID string
|
||||
Conn *websocket.Conn
|
||||
InputChan chan string
|
||||
OutputChan chan string
|
||||
Done chan struct{}
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var (
|
||||
// Configure the upgrader
|
||||
upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
// Allow all origins for development
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
// Active terminal sessions
|
||||
terminals = make(map[string]*WebSocketTerminal)
|
||||
terminalsMu sync.Mutex
|
||||
)
|
||||
|
||||
// TerminalHandler handles WebSocket connections for terminal sessions
|
||||
func (h *Handler) TerminalHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Upgrade the HTTP connection to a WebSocket connection
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to upgrade connection: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a unique ID for this terminal session
|
||||
terminalID := executor.GenerateUUID()
|
||||
|
||||
// Create channels for communication
|
||||
inputChan := make(chan string)
|
||||
outputChan := make(chan string)
|
||||
done := make(chan struct{})
|
||||
|
||||
// Create a new terminal session
|
||||
terminal := &WebSocketTerminal{
|
||||
ID: terminalID,
|
||||
Conn: conn,
|
||||
InputChan: inputChan,
|
||||
OutputChan: outputChan,
|
||||
Done: done,
|
||||
}
|
||||
|
||||
// Store the terminal session
|
||||
terminalsMu.Lock()
|
||||
terminals[terminalID] = terminal
|
||||
terminalsMu.Unlock()
|
||||
|
||||
// Send the terminal ID to the client
|
||||
if err := conn.WriteJSON(map[string]string{"type": "terminal_id", "id": terminalID}); err != nil {
|
||||
log.Printf("Failed to send terminal ID: %v", err)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle incoming messages (input from the client)
|
||||
go func() {
|
||||
defer func() {
|
||||
close(done)
|
||||
conn.Close()
|
||||
|
||||
// Remove the terminal from the map
|
||||
terminalsMu.Lock()
|
||||
delete(terminals, terminalID)
|
||||
terminalsMu.Unlock()
|
||||
|
||||
log.Printf("Terminal session %s closed", terminalID)
|
||||
}()
|
||||
|
||||
for {
|
||||
// Read message from the WebSocket
|
||||
messageType, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
log.Printf("WebSocket error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle different message types
|
||||
if messageType == websocket.TextMessage {
|
||||
// Parse the message
|
||||
input := string(message)
|
||||
|
||||
// Send the input to the execution service
|
||||
select {
|
||||
case inputChan <- input:
|
||||
// Input sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle outgoing messages (output to the client)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case output := <-outputChan:
|
||||
// Send the output to the client
|
||||
err := conn.WriteMessage(websocket.TextMessage, []byte(output))
|
||||
if err != nil {
|
||||
log.Printf("Failed to write message: %v", err)
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Keep the connection alive with ping/pong
|
||||
go func() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ExecuteCodeWebSocket executes code and streams the output over WebSocket
|
||||
func (h *Handler) ExecuteCodeWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
// Upgrade the HTTP connection to a WebSocket connection
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to upgrade connection: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Read the initial message containing the code to execute
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("Failed to read message: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the message into a code submission
|
||||
var submission models.CodeSubmission
|
||||
if err := submission.UnmarshalJSON(message); err != nil {
|
||||
log.Printf("Failed to parse submission: %v", err)
|
||||
conn.WriteJSON(map[string]string{"error": "Invalid submission format"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a unique ID for the submission
|
||||
submission.ID = executor.GenerateUUID()
|
||||
submission.Status = "pending"
|
||||
|
||||
// Store the submission
|
||||
h.mu.Lock()
|
||||
h.submissions[submission.ID] = &submission
|
||||
h.mu.Unlock()
|
||||
|
||||
// Create channels for communication
|
||||
inputChan := make(chan string)
|
||||
outputChan := make(chan string)
|
||||
done := make(chan struct{})
|
||||
|
||||
// Set up the execution service to use these channels
|
||||
h.executionService.SetupWebSocketChannels(&submission, inputChan, outputChan)
|
||||
|
||||
// Send the submission ID to the client
|
||||
if err := conn.WriteJSON(map[string]string{"type": "submission_id", "id": submission.ID}); err != nil {
|
||||
log.Printf("Failed to send submission ID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Execute the code in a goroutine
|
||||
go func() {
|
||||
h.executionService.ExecuteCodeWebSocket(&submission)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Handle incoming messages (input from the client)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
// Read message from the WebSocket
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
log.Printf("WebSocket error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Send the input to the execution service
|
||||
select {
|
||||
case inputChan <- string(message):
|
||||
// Input sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle outgoing messages (output to the client)
|
||||
for {
|
||||
select {
|
||||
case output := <-outputChan:
|
||||
// Send the output to the client
|
||||
err := conn.WriteMessage(websocket.TextMessage, []byte(output))
|
||||
if err != nil {
|
||||
log.Printf("Failed to write message: %v", err)
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
// Execution completed
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetTerminal returns a terminal session by ID
|
||||
func GetTerminal(id string) (*WebSocketTerminal, error) {
|
||||
terminalsMu.Lock()
|
||||
defer terminalsMu.Unlock()
|
||||
|
||||
terminal, exists := terminals[id]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("terminal not found: %s", id)
|
||||
}
|
||||
|
||||
return terminal, nil
|
||||
}
|
||||
32
backend/internal/api/routes.go
Normal file
32
backend/internal/api/routes.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/api/handlers"
|
||||
)
|
||||
|
||||
// SetupRoutes configures all API routes
|
||||
func SetupRoutes() http.Handler {
|
||||
// Create a new handler
|
||||
h := handlers.NewHandler()
|
||||
|
||||
// Create a new router
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Apply middleware to all routes
|
||||
var handler http.Handler = mux
|
||||
handler = handlers.RecoveryMiddleware(handler)
|
||||
handler = handlers.LoggingMiddleware(handler)
|
||||
handler = handlers.CORSMiddleware(handler)
|
||||
|
||||
// Register routes
|
||||
mux.HandleFunc("/submit", h.SubmitHandler)
|
||||
mux.HandleFunc("/status", h.StatusHandler)
|
||||
mux.HandleFunc("/result", h.ResultHandler)
|
||||
mux.HandleFunc("/submit-input", h.SubmitInputHandler)
|
||||
mux.HandleFunc("/queue-stats", h.QueueStatsHandler)
|
||||
mux.HandleFunc("/health", h.HealthCheckHandler)
|
||||
|
||||
return handler
|
||||
}
|
||||
683
backend/internal/executor/executor.go
Normal file
683
backend/internal/executor/executor.go
Normal file
@@ -0,0 +1,683 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/models"
|
||||
"github.com/arnab-afk/monaco/internal/queue"
|
||||
)
|
||||
|
||||
// ExecutionService manages code execution
|
||||
type ExecutionService struct {
|
||||
queue *queue.JobQueue
|
||||
mu sync.Mutex
|
||||
// Map of submission ID to input channel for interactive programs
|
||||
inputChannels map[string]chan string
|
||||
// WebSocket channels for real-time communication
|
||||
wsInputChannels map[string]chan string
|
||||
wsOutputChannels map[string]chan string
|
||||
}
|
||||
|
||||
// CodeExecutionJob represents a code execution job
|
||||
type CodeExecutionJob struct {
|
||||
service *ExecutionService
|
||||
submission *models.CodeSubmission
|
||||
}
|
||||
|
||||
// NewExecutionService creates a new execution service
|
||||
func NewExecutionService() *ExecutionService {
|
||||
return &ExecutionService{
|
||||
queue: queue.NewJobQueue(5), // 5 concurrent workers
|
||||
inputChannels: make(map[string]chan string),
|
||||
wsInputChannels: make(map[string]chan string),
|
||||
wsOutputChannels: make(map[string]chan string),
|
||||
}
|
||||
}
|
||||
|
||||
// NewCodeExecutionJob creates a new code execution job
|
||||
func NewCodeExecutionJob(service *ExecutionService, submission *models.CodeSubmission) *CodeExecutionJob {
|
||||
return &CodeExecutionJob{
|
||||
service: service,
|
||||
submission: submission,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute runs the code execution job
|
||||
func (j *CodeExecutionJob) Execute() {
|
||||
submission := j.submission
|
||||
submission.Status = "running"
|
||||
submission.StartedAt = time.Now()
|
||||
|
||||
log.Printf("[JOB-%s] Starting execution for language: %s", submission.ID, submission.Language)
|
||||
|
||||
j.service.executeLanguageSpecific(submission)
|
||||
|
||||
submission.CompletedAt = time.Now()
|
||||
log.Printf("[JOB-%s] Execution completed in %v", submission.ID, submission.CompletedAt.Sub(submission.StartedAt))
|
||||
}
|
||||
|
||||
// ExecuteCode adds the submission to the execution queue
|
||||
func (s *ExecutionService) ExecuteCode(submission *models.CodeSubmission) {
|
||||
submission.Status = "queued"
|
||||
submission.QueuedAt = time.Now()
|
||||
|
||||
log.Printf("[SUBMISSION-%s] Code submission queued for language: %s", submission.ID, submission.Language)
|
||||
|
||||
// Create and add the job to the queue
|
||||
job := NewCodeExecutionJob(s, submission)
|
||||
s.queue.AddJob(job)
|
||||
}
|
||||
|
||||
// executeLanguageSpecific executes code based on the language
|
||||
func (s *ExecutionService) executeLanguageSpecific(submission *models.CodeSubmission) {
|
||||
switch strings.ToLower(submission.Language) {
|
||||
case "python":
|
||||
s.executePython(submission)
|
||||
case "javascript", "js":
|
||||
s.executeJavaScript(submission)
|
||||
case "go", "golang":
|
||||
s.executeGo(submission)
|
||||
case "java":
|
||||
s.executeJava(submission)
|
||||
case "c":
|
||||
s.executeC(submission)
|
||||
case "cpp", "c++":
|
||||
s.executeCpp(submission)
|
||||
default:
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Unsupported language: %s", submission.Language)
|
||||
log.Printf("[EXEC-%s] ERROR: Unsupported language: %s", submission.ID, submission.Language)
|
||||
}
|
||||
}
|
||||
|
||||
// executePython runs Python code in a container
|
||||
func (s *ExecutionService) executePython(submission *models.CodeSubmission) {
|
||||
log.Printf("[PYTHON-%s] Preparing Python execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Create a temporary file for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-python-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "code.py")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we should use interactive mode
|
||||
if strings.Contains(submission.Code, "input(") {
|
||||
// This code likely requires interactive input
|
||||
submission.IsInteractive = true
|
||||
s.executePythonInteractive(submission, tempDir)
|
||||
return
|
||||
}
|
||||
|
||||
// Non-interactive mode
|
||||
// Create a file for input if provided
|
||||
inputPath := ""
|
||||
if submission.Input != "" {
|
||||
inputPath = filepath.Join(tempDir, "input.txt")
|
||||
if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write input file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if inputPath != "" {
|
||||
cmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"--ulimit", "nofile=64:64", // File descriptor limits
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"python:3.9",
|
||||
"sh", "-c", "cat /code/input.txt | python /code/code.py")
|
||||
} else {
|
||||
cmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"--ulimit", "nofile=64:64", // File descriptor limits
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"python:3.9",
|
||||
"python", "/code/code.py")
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[PYTHON-%s] Python execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err, ctx.Err() != nil)
|
||||
}
|
||||
|
||||
// executeJavaScript runs JavaScript code in a container
|
||||
func (s *ExecutionService) executeJavaScript(submission *models.CodeSubmission) {
|
||||
log.Printf("[JS-%s] Preparing JavaScript execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Create a temporary file for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-js-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "code.js")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we should use interactive mode
|
||||
if strings.Contains(submission.Code, "readline") && strings.Contains(submission.Code, "question") {
|
||||
// This code likely requires interactive input
|
||||
submission.IsInteractive = true
|
||||
s.executeJavaScriptInteractive(submission, tempDir)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a file for input if provided
|
||||
inputPath := ""
|
||||
if submission.Input != "" {
|
||||
inputPath = filepath.Join(tempDir, "input.txt")
|
||||
if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write input file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if inputPath != "" {
|
||||
// Create a wrapper script to handle input
|
||||
wrapperPath := filepath.Join(tempDir, "wrapper.js")
|
||||
wrapperCode := `
|
||||
const fs = require('fs');
|
||||
const input = fs.readFileSync('/code/input.txt', 'utf8');
|
||||
// Redirect input to stdin
|
||||
process.stdin.push(input);
|
||||
process.stdin.push(null);
|
||||
// Load and run the user code
|
||||
require('./code.js');
|
||||
`
|
||||
if err := os.WriteFile(wrapperPath, []byte(wrapperCode), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write wrapper file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"node:18-alpine",
|
||||
"node", "/code/wrapper.js")
|
||||
} else {
|
||||
cmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"node:18-alpine",
|
||||
"node", "/code/code.js")
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[JS-%s] JavaScript execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err, ctx.Err() != nil)
|
||||
}
|
||||
|
||||
// executeGo runs Go code in a container
|
||||
func (s *ExecutionService) executeGo(submission *models.CodeSubmission) {
|
||||
log.Printf("[GO-%s] Preparing Go execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Create a temporary file for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-go-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "main.go")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a file for input if provided
|
||||
inputPath := ""
|
||||
if submission.Input != "" {
|
||||
inputPath = filepath.Join(tempDir, "input.txt")
|
||||
if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write input file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// First compile the Go code
|
||||
compileCmd := exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"golang:1.22-alpine",
|
||||
"go", "build", "-o", "/code/app", "/code/main.go")
|
||||
|
||||
compileOutput, compileErr := compileCmd.CombinedOutput()
|
||||
if compileErr != nil {
|
||||
log.Printf("[GO-%s] Compilation failed: %v", submission.ID, compileErr)
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Compilation error: %s", compileOutput)
|
||||
return
|
||||
}
|
||||
|
||||
// Then run the compiled binary
|
||||
var runCmd *exec.Cmd
|
||||
if inputPath != "" {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"golang:1.22-alpine",
|
||||
"sh", "-c", "cat /code/input.txt | /code/app")
|
||||
} else {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"golang:1.22-alpine",
|
||||
"/code/app")
|
||||
}
|
||||
|
||||
output, err := runCmd.CombinedOutput()
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[GO-%s] Go execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err, ctx.Err() != nil)
|
||||
}
|
||||
|
||||
// executeJava runs Java code in a container
|
||||
func (s *ExecutionService) executeJava(submission *models.CodeSubmission) {
|
||||
log.Printf("[JAVA-%s] Preparing Java execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Create a temporary file for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-java-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Extract class name from the code
|
||||
className := extractJavaClassName(submission.Code)
|
||||
if className == "" {
|
||||
className = "Main" // Default class name
|
||||
}
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, className+".java")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a file for input if provided
|
||||
inputPath := ""
|
||||
if submission.Input != "" {
|
||||
inputPath = filepath.Join(tempDir, "input.txt")
|
||||
if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write input file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// First compile the Java code
|
||||
compileCmd := exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"eclipse-temurin:11-jdk-alpine",
|
||||
"javac", "/code/"+className+".java")
|
||||
|
||||
compileOutput, compileErr := compileCmd.CombinedOutput()
|
||||
if compileErr != nil {
|
||||
log.Printf("[JAVA-%s] Compilation failed: %v", submission.ID, compileErr)
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Compilation error: %s", compileOutput)
|
||||
return
|
||||
}
|
||||
|
||||
// Then run the compiled class
|
||||
var runCmd *exec.Cmd
|
||||
if inputPath != "" {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=400m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=50000", // 50% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"eclipse-temurin:11-jdk-alpine",
|
||||
"sh", "-c", "cd /code && cat input.txt | java -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Xverify:none -Xms64m -Xmx256m "+className)
|
||||
} else {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=400m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=50000", // 50% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"eclipse-temurin:11-jdk-alpine",
|
||||
"java", "-XX:+TieredCompilation", "-XX:TieredStopAtLevel=1", "-Xverify:none", "-Xms64m", "-Xmx256m", "-cp", "/code", className)
|
||||
}
|
||||
|
||||
output, err := runCmd.CombinedOutput()
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[JAVA-%s] Java execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err, ctx.Err() != nil)
|
||||
}
|
||||
|
||||
// executeC runs C code in a container
|
||||
func (s *ExecutionService) executeC(submission *models.CodeSubmission) {
|
||||
log.Printf("[C-%s] Preparing C execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Create a temporary file for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-c-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "code.c")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a file for input if provided
|
||||
inputPath := ""
|
||||
if submission.Input != "" {
|
||||
inputPath = filepath.Join(tempDir, "input.txt")
|
||||
if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write input file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// First compile the C code
|
||||
compileCmd := exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest",
|
||||
"gcc", "-o", "/code/app", "/code/code.c")
|
||||
|
||||
compileOutput, compileErr := compileCmd.CombinedOutput()
|
||||
if compileErr != nil {
|
||||
log.Printf("[C-%s] Compilation failed: %v", submission.ID, compileErr)
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Compilation error: %s", compileOutput)
|
||||
return
|
||||
}
|
||||
|
||||
// Then run the compiled binary
|
||||
var runCmd *exec.Cmd
|
||||
if inputPath != "" {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest",
|
||||
"sh", "-c", "cat /code/input.txt | /code/app")
|
||||
} else {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest",
|
||||
"/code/app")
|
||||
}
|
||||
|
||||
output, err := runCmd.CombinedOutput()
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[C-%s] C execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err, ctx.Err() != nil)
|
||||
}
|
||||
|
||||
// executeCpp runs C++ code in a container
|
||||
func (s *ExecutionService) executeCpp(submission *models.CodeSubmission) {
|
||||
log.Printf("[CPP-%s] Preparing C++ execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Create a temporary file for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-cpp-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "code.cpp")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a file for input if provided
|
||||
inputPath := ""
|
||||
if submission.Input != "" {
|
||||
inputPath = filepath.Join(tempDir, "input.txt")
|
||||
if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write input file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// First compile the C++ code
|
||||
compileCmd := exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest",
|
||||
"g++", "-o", "/code/app", "/code/code.cpp")
|
||||
|
||||
compileOutput, compileErr := compileCmd.CombinedOutput()
|
||||
if compileErr != nil {
|
||||
log.Printf("[CPP-%s] Compilation failed: %v", submission.ID, compileErr)
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Compilation error: %s", compileOutput)
|
||||
return
|
||||
}
|
||||
|
||||
// Then run the compiled binary
|
||||
var runCmd *exec.Cmd
|
||||
if inputPath != "" {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest",
|
||||
"sh", "-c", "cat /code/input.txt | /code/app")
|
||||
} else {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest",
|
||||
"/code/app")
|
||||
}
|
||||
|
||||
output, err := runCmd.CombinedOutput()
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[CPP-%s] C++ execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err, ctx.Err() != nil)
|
||||
}
|
||||
|
||||
// updateSubmissionResult updates the submission with the execution result
|
||||
func (s *ExecutionService) updateSubmissionResult(submission *models.CodeSubmission, output []byte, err error, timedOut bool) {
|
||||
// Format the output to include the input if provided
|
||||
formattedOutput := ""
|
||||
if submission.Input != "" {
|
||||
// Only add input lines that were actually used
|
||||
inputLines := strings.Split(submission.Input, "\n")
|
||||
for _, line := range inputLines {
|
||||
if line != "" {
|
||||
// Don't add the input marker for empty lines
|
||||
formattedOutput += "[Input] " + line + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the actual output
|
||||
rawOutput := string(output)
|
||||
|
||||
if timedOut {
|
||||
submission.Status = "failed"
|
||||
submission.Error = "Execution timed out"
|
||||
submission.Output = formattedOutput + rawOutput
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = err.Error()
|
||||
submission.Output = formattedOutput + rawOutput
|
||||
return
|
||||
}
|
||||
|
||||
submission.Status = "completed"
|
||||
submission.Output = formattedOutput + rawOutput
|
||||
}
|
||||
|
||||
// SubmitInput submits input to a running interactive program
|
||||
func (s *ExecutionService) SubmitInput(submission *models.CodeSubmission, input string) {
|
||||
s.mu.Lock()
|
||||
inputChan, exists := s.inputChannels[submission.ID]
|
||||
s.mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
log.Printf("[ERROR] No input channel found for submission %s", submission.ID)
|
||||
return
|
||||
}
|
||||
|
||||
// Send the input to the channel
|
||||
inputChan <- input
|
||||
|
||||
// Update the submission status
|
||||
submission.Status = "running"
|
||||
submission.Output += "[Input] " + input + "\n"
|
||||
}
|
||||
|
||||
// GetQueueStats returns statistics about the job queue
|
||||
func (s *ExecutionService) GetQueueStats() models.QueueStats {
|
||||
return s.queue.GetStats()
|
||||
}
|
||||
|
||||
// GenerateUUID generates a unique ID for submissions
|
||||
func GenerateUUID() string {
|
||||
b := make([]byte, 16)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// extractJavaClassName extracts the class name from Java code
|
||||
func extractJavaClassName(code string) string {
|
||||
// Simple regex-like extraction
|
||||
lines := strings.Split(code, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "public class ") {
|
||||
parts := strings.Split(line, " ")
|
||||
if len(parts) > 2 {
|
||||
className := parts[2]
|
||||
// Remove any { or implements/extends
|
||||
className = strings.Split(className, "{")[0]
|
||||
className = strings.Split(className, " ")[0]
|
||||
return strings.TrimSpace(className)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
366
backend/internal/executor/interactive.go
Normal file
366
backend/internal/executor/interactive.go
Normal file
@@ -0,0 +1,366 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/models"
|
||||
)
|
||||
|
||||
// executePythonInteractive runs Python code in interactive mode
|
||||
func (s *ExecutionService) executePythonInteractive(submission *models.CodeSubmission, tempDir string) {
|
||||
log.Printf("[PYTHON-%s] Running Python in interactive mode", submission.ID)
|
||||
|
||||
// Create an input channel for this submission
|
||||
inputChan := make(chan string)
|
||||
s.mu.Lock()
|
||||
s.inputChannels[submission.ID] = inputChan
|
||||
s.mu.Unlock()
|
||||
|
||||
// Clean up when done
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
delete(s.inputChannels, submission.ID)
|
||||
close(inputChan)
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
|
||||
// Create a wrapper script that handles interactive input
|
||||
wrapperPath := filepath.Join(tempDir, "wrapper.py")
|
||||
wrapperCode := `
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
# Load the user's code
|
||||
with open('/code/code.py', 'r') as f:
|
||||
user_code = f.read()
|
||||
|
||||
# Replace the built-in input function
|
||||
original_input = input
|
||||
|
||||
def custom_input(prompt=''):
|
||||
# Print the prompt without newline
|
||||
sys.stdout.write(prompt)
|
||||
sys.stdout.flush()
|
||||
|
||||
# Signal that we're waiting for input
|
||||
sys.stdout.write('\n[WAITING_FOR_INPUT]\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
# Wait for input from the parent process
|
||||
# Use a blocking read that won't raise EOFError
|
||||
line = ''
|
||||
while True:
|
||||
try:
|
||||
char = sys.stdin.read(1)
|
||||
if char == '\n':
|
||||
break
|
||||
if char:
|
||||
line += char
|
||||
except:
|
||||
# If any error occurs, wait a bit and try again
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
# Echo the input as if the user typed it
|
||||
sys.stdout.write(line + '\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
return line
|
||||
|
||||
# Replace the built-in input function
|
||||
input = custom_input
|
||||
|
||||
# Execute the user's code
|
||||
try:
|
||||
# Use globals and locals to ensure proper variable scope
|
||||
exec(user_code, globals(), globals())
|
||||
except Exception as e:
|
||||
# Print detailed error information
|
||||
sys.stdout.write(f'\nError: {str(e)}\n')
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
sys.stdout.flush()
|
||||
`
|
||||
|
||||
if err := os.WriteFile(wrapperPath, []byte(wrapperCode), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write wrapper file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) // Longer timeout for interactive
|
||||
defer cancel()
|
||||
|
||||
// Start the container
|
||||
cmd := exec.CommandContext(ctx, "docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"--ulimit", "nofile=64:64", // File descriptor limits
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"python:3.9",
|
||||
"python", "/code/wrapper.py")
|
||||
|
||||
// Get pipes for stdin and stdout
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdin pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdout pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to start command: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Set status to running
|
||||
submission.Status = "running"
|
||||
|
||||
// Read output in a goroutine
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Check if the program is waiting for input
|
||||
if line == "[WAITING_FOR_INPUT]" {
|
||||
// Update status to waiting for input
|
||||
submission.Status = "waiting_for_input"
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the output to the submission
|
||||
submission.Output += line + "\n"
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle input in a goroutine
|
||||
go func() {
|
||||
for input := range inputChan {
|
||||
// Write the input to stdin
|
||||
_, err := stdin.Write([]byte(input + "\n"))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to write to stdin: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the command to complete
|
||||
err = cmd.Wait()
|
||||
|
||||
// Update the submission status
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = "Execution timed out"
|
||||
} else {
|
||||
submission.Status = "failed"
|
||||
submission.Error = err.Error()
|
||||
}
|
||||
} else {
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
submission.CompletedAt = time.Now()
|
||||
log.Printf("[PYTHON-%s] Interactive execution completed", submission.ID)
|
||||
}
|
||||
|
||||
// executeJavaScriptInteractive runs JavaScript code in interactive mode
|
||||
func (s *ExecutionService) executeJavaScriptInteractive(submission *models.CodeSubmission, tempDir string) {
|
||||
log.Printf("[JS-%s] Running JavaScript in interactive mode", submission.ID)
|
||||
|
||||
// Create an input channel for this submission
|
||||
inputChan := make(chan string)
|
||||
s.mu.Lock()
|
||||
s.inputChannels[submission.ID] = inputChan
|
||||
s.mu.Unlock()
|
||||
|
||||
// Clean up when done
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
delete(s.inputChannels, submission.ID)
|
||||
close(inputChan)
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
|
||||
// Create a wrapper script that handles interactive input
|
||||
wrapperPath := filepath.Join(tempDir, "wrapper.js")
|
||||
wrapperCode := `
|
||||
const fs = require('fs');
|
||||
const readline = require('readline');
|
||||
|
||||
// Load the user's code
|
||||
const userCode = fs.readFileSync('/code/code.js', 'utf8');
|
||||
|
||||
// Create a custom readline interface
|
||||
const originalReadline = readline.createInterface;
|
||||
readline.createInterface = function(options) {
|
||||
// Create a custom interface that intercepts input
|
||||
const rl = originalReadline({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
terminal: false
|
||||
});
|
||||
|
||||
// Override the question method
|
||||
const originalQuestion = rl.question;
|
||||
rl.question = function(query, callback) {
|
||||
// Print the prompt
|
||||
process.stdout.write(query);
|
||||
|
||||
// Signal that we're waiting for input
|
||||
process.stdout.write('\n[WAITING_FOR_INPUT]\n');
|
||||
process.stdout.flush();
|
||||
|
||||
// Set up a more robust input handler
|
||||
const onLine = (answer) => {
|
||||
// Echo the input as if the user typed it
|
||||
process.stdout.write(answer + '\n');
|
||||
process.stdout.flush();
|
||||
callback(answer);
|
||||
};
|
||||
|
||||
// Handle input with error recovery
|
||||
rl.once('line', onLine);
|
||||
|
||||
// Add error handler
|
||||
rl.once('error', (err) => {
|
||||
console.error('Input error:', err.message);
|
||||
// Provide a default answer in case of error
|
||||
callback('');
|
||||
});
|
||||
};
|
||||
|
||||
return rl;
|
||||
};
|
||||
|
||||
// Capture uncaught exceptions
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('Uncaught Exception:', err.message);
|
||||
console.error(err.stack);
|
||||
});
|
||||
|
||||
// Execute the user's code
|
||||
try {
|
||||
eval(userCode);
|
||||
} catch (e) {
|
||||
console.error('Error:', e.message);
|
||||
console.error(e.stack);
|
||||
}
|
||||
`
|
||||
|
||||
if err := os.WriteFile(wrapperPath, []byte(wrapperCode), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write wrapper file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) // Longer timeout for interactive
|
||||
defer cancel()
|
||||
|
||||
// Start the container
|
||||
cmd := exec.CommandContext(ctx, "docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"node:18-alpine",
|
||||
"node", "/code/wrapper.js")
|
||||
|
||||
// Get pipes for stdin and stdout
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdin pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdout pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to start command: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Set status to running
|
||||
submission.Status = "running"
|
||||
|
||||
// Read output in a goroutine
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Check if the program is waiting for input
|
||||
if line == "[WAITING_FOR_INPUT]" {
|
||||
// Update status to waiting for input
|
||||
submission.Status = "waiting_for_input"
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the output to the submission
|
||||
submission.Output += line + "\n"
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle input in a goroutine
|
||||
go func() {
|
||||
for input := range inputChan {
|
||||
// Write the input to stdin
|
||||
_, err := stdin.Write([]byte(input + "\n"))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to write to stdin: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the command to complete
|
||||
err = cmd.Wait()
|
||||
|
||||
// Update the submission status
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = "Execution timed out"
|
||||
} else {
|
||||
submission.Status = "failed"
|
||||
submission.Error = err.Error()
|
||||
}
|
||||
} else {
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
submission.CompletedAt = time.Now()
|
||||
log.Printf("[JS-%s] Interactive execution completed", submission.ID)
|
||||
}
|
||||
376
backend/internal/executor/websocket.go
Normal file
376
backend/internal/executor/websocket.go
Normal file
@@ -0,0 +1,376 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/models"
|
||||
)
|
||||
|
||||
// WebSocketSession represents a WebSocket execution session
|
||||
type WebSocketSession struct {
|
||||
Submission *models.CodeSubmission
|
||||
InputChan chan string
|
||||
OutputChan chan string
|
||||
Done chan struct{}
|
||||
}
|
||||
|
||||
// SetupWebSocketChannels sets up the channels for WebSocket communication
|
||||
func (s *ExecutionService) SetupWebSocketChannels(submission *models.CodeSubmission, inputChan chan string, outputChan chan string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Store the channels in the service
|
||||
s.wsInputChannels[submission.ID] = inputChan
|
||||
s.wsOutputChannels[submission.ID] = outputChan
|
||||
}
|
||||
|
||||
// ExecuteCodeWebSocket executes code and streams the output over WebSocket
|
||||
func (s *ExecutionService) ExecuteCodeWebSocket(submission *models.CodeSubmission) {
|
||||
log.Printf("[WS-%s] Starting WebSocket execution for %s code", submission.ID, submission.Language)
|
||||
|
||||
// Update submission status
|
||||
submission.Status = "running"
|
||||
submission.StartedAt = time.Now()
|
||||
|
||||
// Execute the code based on the language
|
||||
switch strings.ToLower(submission.Language) {
|
||||
case "python":
|
||||
s.executePythonWebSocket(submission)
|
||||
case "javascript":
|
||||
s.executeJavaScriptWebSocket(submission)
|
||||
case "go":
|
||||
s.executeGoWebSocket(submission)
|
||||
case "java":
|
||||
s.executeJavaWebSocket(submission)
|
||||
case "c":
|
||||
s.executeCWebSocket(submission)
|
||||
case "cpp":
|
||||
s.executeCppWebSocket(submission)
|
||||
default:
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Unsupported language: %s", submission.Language)
|
||||
submission.CompletedAt = time.Now()
|
||||
}
|
||||
|
||||
log.Printf("[WS-%s] Execution completed with status: %s", submission.ID, submission.Status)
|
||||
}
|
||||
|
||||
// executePythonWebSocket executes Python code with WebSocket communication
|
||||
func (s *ExecutionService) executePythonWebSocket(submission *models.CodeSubmission) {
|
||||
log.Printf("[WS-PYTHON-%s] Preparing Python WebSocket execution", submission.ID)
|
||||
|
||||
// Create a temporary directory for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-ws-python-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "code.py")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the input and output channels
|
||||
s.mu.Lock()
|
||||
inputChan := s.wsInputChannels[submission.ID]
|
||||
outputChan := s.wsOutputChannels[submission.ID]
|
||||
s.mu.Unlock()
|
||||
|
||||
// Create a context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Run the code in a Docker container
|
||||
cmd := exec.CommandContext(ctx, "docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"--ulimit", "nofile=64:64", // File descriptor limits
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"python:3.9",
|
||||
"python", "/code/code.py")
|
||||
|
||||
// Get pipes for stdin and stdout
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdin pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdout pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stderr pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to start command: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a done channel to signal when the command is complete
|
||||
done := make(chan struct{})
|
||||
|
||||
// Read from stdout and send to the output channel
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
select {
|
||||
case outputChan <- line + "\n":
|
||||
// Output sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from stderr and send to the output channel
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
select {
|
||||
case outputChan <- "ERROR: " + line + "\n":
|
||||
// Error sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from the input channel and write to stdin
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case input := <-inputChan:
|
||||
// Write the input to stdin
|
||||
_, err := io.WriteString(stdin, input+"\n")
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to write to stdin: %v", submission.ID, err)
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the command to complete
|
||||
err = cmd.Wait()
|
||||
close(done)
|
||||
|
||||
// Update the submission status
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = "Execution timed out"
|
||||
} else {
|
||||
submission.Status = "failed"
|
||||
submission.Error = err.Error()
|
||||
}
|
||||
} else {
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
submission.CompletedAt = time.Now()
|
||||
log.Printf("[WS-PYTHON-%s] WebSocket execution completed", submission.ID)
|
||||
}
|
||||
|
||||
// executeJavaScriptWebSocket executes JavaScript code with WebSocket communication
|
||||
func (s *ExecutionService) executeJavaScriptWebSocket(submission *models.CodeSubmission) {
|
||||
log.Printf("[WS-JS-%s] Preparing JavaScript WebSocket execution", submission.ID)
|
||||
|
||||
// Create a temporary directory for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-ws-js-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "code.js")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the input and output channels
|
||||
s.mu.Lock()
|
||||
inputChan := s.wsInputChannels[submission.ID]
|
||||
outputChan := s.wsOutputChannels[submission.ID]
|
||||
s.mu.Unlock()
|
||||
|
||||
// Create a context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Run the code in a Docker container
|
||||
cmd := exec.CommandContext(ctx, "docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"node:18-alpine",
|
||||
"node", "/code/code.js")
|
||||
|
||||
// Get pipes for stdin and stdout
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdin pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdout pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stderr pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to start command: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a done channel to signal when the command is complete
|
||||
done := make(chan struct{})
|
||||
|
||||
// Read from stdout and send to the output channel
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
select {
|
||||
case outputChan <- line + "\n":
|
||||
// Output sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from stderr and send to the output channel
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
select {
|
||||
case outputChan <- "ERROR: " + line + "\n":
|
||||
// Error sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from the input channel and write to stdin
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case input := <-inputChan:
|
||||
// Write the input to stdin
|
||||
_, err := io.WriteString(stdin, input+"\n")
|
||||
if err != nil {
|
||||
log.Printf("[WS-JS-%s] Failed to write to stdin: %v", submission.ID, err)
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the command to complete
|
||||
err = cmd.Wait()
|
||||
close(done)
|
||||
|
||||
// Update the submission status
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = "Execution timed out"
|
||||
} else {
|
||||
submission.Status = "failed"
|
||||
submission.Error = err.Error()
|
||||
}
|
||||
} else {
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
submission.CompletedAt = time.Now()
|
||||
log.Printf("[WS-JS-%s] WebSocket execution completed", submission.ID)
|
||||
}
|
||||
|
||||
// executeGoWebSocket executes Go code with WebSocket communication
|
||||
func (s *ExecutionService) executeGoWebSocket(submission *models.CodeSubmission) {
|
||||
// Implementation similar to executePythonWebSocket but for Go
|
||||
// For brevity, this is left as a placeholder
|
||||
submission.Status = "failed"
|
||||
submission.Error = "WebSocket execution for Go not implemented yet"
|
||||
}
|
||||
|
||||
// executeJavaWebSocket executes Java code with WebSocket communication
|
||||
func (s *ExecutionService) executeJavaWebSocket(submission *models.CodeSubmission) {
|
||||
// Implementation similar to executePythonWebSocket but for Java
|
||||
// For brevity, this is left as a placeholder
|
||||
submission.Status = "failed"
|
||||
submission.Error = "WebSocket execution for Java not implemented yet"
|
||||
}
|
||||
|
||||
// executeCWebSocket executes C code with WebSocket communication
|
||||
func (s *ExecutionService) executeCWebSocket(submission *models.CodeSubmission) {
|
||||
// Implementation similar to executePythonWebSocket but for C
|
||||
// For brevity, this is left as a placeholder
|
||||
submission.Status = "failed"
|
||||
submission.Error = "WebSocket execution for C not implemented yet"
|
||||
}
|
||||
|
||||
// executeCppWebSocket executes C++ code with WebSocket communication
|
||||
func (s *ExecutionService) executeCppWebSocket(submission *models.CodeSubmission) {
|
||||
// Implementation similar to executePythonWebSocket but for C++
|
||||
// For brevity, this is left as a placeholder
|
||||
submission.Status = "failed"
|
||||
submission.Error = "WebSocket execution for C++ not implemented yet"
|
||||
}
|
||||
36
backend/internal/models/submission.go
Normal file
36
backend/internal/models/submission.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// CodeSubmission represents a code submission for execution
|
||||
type CodeSubmission struct {
|
||||
ID string `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Language string `json:"language"`
|
||||
Input string `json:"input"`
|
||||
Status string `json:"status"` // "pending", "queued", "running", "waiting_for_input", "completed", "failed"
|
||||
QueuedAt time.Time `json:"queuedAt,omitempty"`
|
||||
StartedAt time.Time `json:"startedAt,omitempty"`
|
||||
CompletedAt time.Time `json:"completedAt,omitempty"`
|
||||
Output string `json:"output,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
IsInteractive bool `json:"isInteractive,omitempty"` // Whether the program requires interactive input
|
||||
CurrentPrompt string `json:"currentPrompt,omitempty"` // Current input prompt if waiting for input
|
||||
}
|
||||
|
||||
// ExecutionResult represents the result of code execution
|
||||
type ExecutionResult struct {
|
||||
Output string `json:"output"`
|
||||
Error string `json:"error"`
|
||||
ExitCode int `json:"exitCode"`
|
||||
ExecutionMS int64 `json:"executionMs"`
|
||||
}
|
||||
|
||||
// QueueStats represents statistics about the job queue
|
||||
type QueueStats struct {
|
||||
QueueLength int `json:"queueLength"`
|
||||
RunningJobs int `json:"runningJobs"`
|
||||
CompletedJobs int `json:"completedJobs"`
|
||||
FailedJobs int `json:"failedJobs"`
|
||||
TotalProcessed int `json:"totalProcessed"`
|
||||
}
|
||||
112
backend/internal/queue/queue.go
Normal file
112
backend/internal/queue/queue.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/models"
|
||||
)
|
||||
|
||||
// Job represents a job to be executed
|
||||
type Job interface {
|
||||
Execute()
|
||||
}
|
||||
|
||||
// JobQueue manages the execution of jobs
|
||||
type JobQueue struct {
|
||||
queue chan Job
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
runningJobs int
|
||||
completedJobs int
|
||||
failedJobs int
|
||||
totalProcessed int
|
||||
workerCount int
|
||||
}
|
||||
|
||||
// NewJobQueue creates a new job queue with the specified number of workers
|
||||
func NewJobQueue(workerCount int) *JobQueue {
|
||||
q := &JobQueue{
|
||||
queue: make(chan Job, 100), // Buffer size of 100 jobs
|
||||
workerCount: workerCount,
|
||||
}
|
||||
|
||||
// Start workers
|
||||
for i := 0; i < workerCount; i++ {
|
||||
q.wg.Add(1)
|
||||
go q.worker(i)
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
// worker processes jobs from the queue
|
||||
func (q *JobQueue) worker(id int) {
|
||||
defer q.wg.Done()
|
||||
|
||||
log.Printf("[WORKER-%d] Started", id)
|
||||
|
||||
for job := range q.queue {
|
||||
// Update stats
|
||||
q.mu.Lock()
|
||||
q.runningJobs++
|
||||
q.mu.Unlock()
|
||||
|
||||
// Execute the job
|
||||
startTime := time.Now()
|
||||
log.Printf("[WORKER-%d] Processing job", id)
|
||||
|
||||
// Execute the job and handle panics
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[WORKER-%d] Panic in job execution: %v", id, r)
|
||||
q.mu.Lock()
|
||||
q.failedJobs++
|
||||
q.runningJobs--
|
||||
q.totalProcessed++
|
||||
q.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
job.Execute()
|
||||
}()
|
||||
|
||||
// Update stats if no panic occurred
|
||||
q.mu.Lock()
|
||||
q.completedJobs++
|
||||
q.runningJobs--
|
||||
q.totalProcessed++
|
||||
q.mu.Unlock()
|
||||
|
||||
log.Printf("[WORKER-%d] Job completed in %v", id, time.Since(startTime))
|
||||
}
|
||||
|
||||
log.Printf("[WORKER-%d] Stopped", id)
|
||||
}
|
||||
|
||||
// AddJob adds a job to the queue
|
||||
func (q *JobQueue) AddJob(job Job) {
|
||||
q.queue <- job
|
||||
}
|
||||
|
||||
// GetStats returns statistics about the job queue
|
||||
func (q *JobQueue) GetStats() models.QueueStats {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
return models.QueueStats{
|
||||
QueueLength: len(q.queue),
|
||||
RunningJobs: q.runningJobs,
|
||||
CompletedJobs: q.completedJobs,
|
||||
FailedJobs: q.failedJobs,
|
||||
TotalProcessed: q.totalProcessed,
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown stops the job queue
|
||||
func (q *JobQueue) Shutdown() {
|
||||
close(q.queue)
|
||||
q.wg.Wait()
|
||||
}
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/handler"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -47,44 +46,12 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// Register 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)))
|
||||
http.HandleFunc("/queue-stats", corsMiddleware(loggingMiddleware(h.QueueStatsHandler)))
|
||||
http.HandleFunc("/ws", corsMiddleware(h.WebSocketHandler)) // WebSocket doesn't need logging middleware
|
||||
|
||||
port := ":8080"
|
||||
log.Printf("Server started at %s", port)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -22,8 +23,8 @@ import (
|
||||
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
|
||||
wsConnections map[string]*websocket.Conn // Map of submission ID to WebSocket connection
|
||||
wsInputChannels map[string]chan string // Map of submission ID to input channel
|
||||
}
|
||||
|
||||
// NewExecutionService creates a new execution service
|
||||
@@ -31,80 +32,8 @@ func NewExecutionService() *ExecutionService {
|
||||
log.Println("Initializing execution service with 3 concurrent workers")
|
||||
return &ExecutionService{
|
||||
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)
|
||||
wsConnections: make(map[string]*websocket.Conn),
|
||||
wsInputChannels: make(map[string]chan string),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,123 +117,48 @@ 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)
|
||||
|
||||
// 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 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle stderr in a goroutine
|
||||
go func() {
|
||||
buffer := make([]byte, 1024)
|
||||
for {
|
||||
n, err := stderr.Read(buffer)
|
||||
if n > 0 {
|
||||
data := buffer[:n]
|
||||
outputBuffer.Write(data)
|
||||
// Send real-time output to connected terminals
|
||||
s.SendOutputToTerminals(submissionID, string(data))
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Write initial input if provided
|
||||
// Set up input pipe if input is provided
|
||||
if input != "" {
|
||||
io.WriteString(stdin, input+"\n")
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR-%s] Failed to create stdin pipe: %v", submissionID, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 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
|
||||
// Write input in a goroutine to avoid blocking
|
||||
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
|
||||
}
|
||||
}
|
||||
defer stdin.Close()
|
||||
io.WriteString(stdin, input)
|
||||
}()
|
||||
|
||||
// Wait for the command to complete with timeout
|
||||
done := make(chan error, 1)
|
||||
log.Printf("[INPUT-%s] Providing input to process", submissionID)
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
var output []byte
|
||||
var err error
|
||||
|
||||
go func() {
|
||||
done <- cmd.Wait()
|
||||
log.Printf("[EXEC-%s] Starting command execution: %v", submissionID, cmd.Args)
|
||||
output, err = cmd.CombinedOutput()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// 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)
|
||||
}
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,9 +176,15 @@ 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
|
||||
|
||||
// Use the enhanced executeWithInput method for all executions
|
||||
output, err := s.executeWithInput(cmd, submission.Input, 100*time.Second, submission.ID)
|
||||
if submission.Input != "" {
|
||||
cmd.Stdin = strings.NewReader(submission.Input)
|
||||
output, err = cmd.CombinedOutput()
|
||||
} else {
|
||||
output, err = s.executeWithTimeout(cmd, 10*time.Second, submission.ID)
|
||||
}
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[PYTHON-%s] Python execution completed in %v", submission.ID, elapsed)
|
||||
@@ -402,7 +262,7 @@ func (s *ExecutionService) executeJava(submission *model.CodeSubmission) {
|
||||
|
||||
log.Printf("[JAVA-%s] Compilation successful", submission.ID)
|
||||
|
||||
// Now run the compiled class with the enhanced executeWithInput method
|
||||
// Now run the compiled class
|
||||
runCmd := exec.Command("docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=400m", // Memory limit
|
||||
@@ -414,8 +274,17 @@ func (s *ExecutionService) executeJava(submission *model.CodeSubmission) {
|
||||
"-Xverify:none", "-Xms64m", "-Xmx256m",
|
||||
"-cp", "/code", className)
|
||||
|
||||
log.Printf("[JAVA-%s] Executing Java code", submission.ID)
|
||||
output, err := s.executeWithInput(runCmd, submission.Input, 15*time.Second, submission.ID)
|
||||
// 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)
|
||||
}
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[JAVA-%s] Java execution completed in %v", submission.ID, elapsed)
|
||||
@@ -465,7 +334,7 @@ func (s *ExecutionService) executeC(submission *model.CodeSubmission) {
|
||||
|
||||
log.Printf("[C-%s] Compilation successful", submission.ID)
|
||||
|
||||
// Run C executable using executeWithInput to support WebSockets
|
||||
// Run C executable
|
||||
runCmd := exec.Command("docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
@@ -474,8 +343,17 @@ func (s *ExecutionService) executeC(submission *model.CodeSubmission) {
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest", "/code/solution")
|
||||
|
||||
log.Printf("[C-%s] Executing C code", submission.ID)
|
||||
output, err := s.executeWithInput(runCmd, submission.Input, 30*time.Second, submission.ID)
|
||||
// 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
|
||||
}
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[C-%s] C execution completed in %v", submission.ID, elapsed)
|
||||
@@ -525,7 +403,7 @@ func (s *ExecutionService) executeCpp(submission *model.CodeSubmission) {
|
||||
|
||||
log.Printf("[CPP-%s] Compilation successful", submission.ID)
|
||||
|
||||
// Run C++ executable using executeWithInput to support WebSockets
|
||||
// Run C++ executable
|
||||
runCmd := exec.Command("docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
@@ -534,8 +412,16 @@ func (s *ExecutionService) executeCpp(submission *model.CodeSubmission) {
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest", "/code/solution")
|
||||
|
||||
log.Printf("[CPP-%s] Executing C++ code", submission.ID)
|
||||
output, err := s.executeWithInput(runCmd, submission.Input, 100*time.Second, submission.ID)
|
||||
// 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)
|
||||
}
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[CPP-%s] C++ execution completed in %v", submission.ID, elapsed)
|
||||
@@ -604,3 +490,223 @@ func (s *ExecutionService) GetQueueStats() map[string]int {
|
||||
stats["queue_length"], stats["running_jobs"], stats["max_workers"])
|
||||
return stats
|
||||
}
|
||||
|
||||
// HandleWebSocket handles a WebSocket connection for a code submission
|
||||
func (s *ExecutionService) HandleWebSocket(conn *websocket.Conn, submission *model.CodeSubmission) {
|
||||
// Store the WebSocket connection
|
||||
s.mu.Lock()
|
||||
s.wsConnections[submission.ID] = conn
|
||||
|
||||
// Create an input channel for this submission
|
||||
inputChan := make(chan string, 10) // Buffer size of 10
|
||||
s.wsInputChannels[submission.ID] = inputChan
|
||||
s.mu.Unlock()
|
||||
|
||||
// Clean up when done
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
delete(s.wsConnections, submission.ID)
|
||||
delete(s.wsInputChannels, submission.ID)
|
||||
s.mu.Unlock()
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
// Start a goroutine to read input from the WebSocket
|
||||
go func() {
|
||||
for {
|
||||
// Read message from WebSocket
|
||||
messageType, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("[WS-%s] Error reading message: %v", submission.ID, err)
|
||||
break
|
||||
}
|
||||
|
||||
// Only process text messages
|
||||
if messageType == websocket.TextMessage {
|
||||
// Send the input to the input channel
|
||||
inputChan <- string(message)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Execute the code
|
||||
submission.Status = "running"
|
||||
submission.StartedAt = time.Now()
|
||||
|
||||
log.Printf("[WS-JOB-%s] Starting WebSocket execution for language: %s",
|
||||
submission.ID, submission.Language)
|
||||
|
||||
// Execute the code based on the language
|
||||
s.executeLanguageSpecificWithWebSocket(submission, inputChan, conn)
|
||||
}
|
||||
|
||||
// executeLanguageSpecificWithWebSocket runs code in the appropriate language with WebSocket I/O
|
||||
func (s *ExecutionService) executeLanguageSpecificWithWebSocket(submission *model.CodeSubmission, inputChan chan string, conn *websocket.Conn) {
|
||||
log.Printf("[WS-EXEC-%s] Selecting execution environment for language: %s",
|
||||
submission.ID, submission.Language)
|
||||
|
||||
switch submission.Language {
|
||||
case "python":
|
||||
log.Printf("[WS-EXEC-%s] Executing Python code", submission.ID)
|
||||
s.executePythonWithWebSocket(submission, inputChan, conn)
|
||||
case "java":
|
||||
log.Printf("[WS-EXEC-%s] Executing Java code", submission.ID)
|
||||
s.executeJavaWithWebSocket(submission, inputChan, conn)
|
||||
case "c":
|
||||
log.Printf("[WS-EXEC-%s] Executing C code", submission.ID)
|
||||
s.executeCWithWebSocket(submission, inputChan, conn)
|
||||
case "cpp":
|
||||
log.Printf("[WS-EXEC-%s] Executing C++ code", submission.ID)
|
||||
s.executeCppWithWebSocket(submission, inputChan, conn)
|
||||
default:
|
||||
log.Printf("[WS-EXEC-%s] ERROR: Unsupported language: %s", submission.ID, submission.Language)
|
||||
submission.Status = "failed"
|
||||
output := "Unsupported language: " + submission.Language
|
||||
submission.Output = output
|
||||
|
||||
// Send error message to WebSocket
|
||||
conn.WriteMessage(websocket.TextMessage, []byte(output))
|
||||
}
|
||||
|
||||
// Update submission status
|
||||
submission.CompletedAt = time.Now()
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
// executePythonWithWebSocket runs Python code with WebSocket for I/O
|
||||
func (s *ExecutionService) executePythonWithWebSocket(submission *model.CodeSubmission, inputChan chan string, conn *websocket.Conn) {
|
||||
log.Printf("[WS-PYTHON-%s] Preparing Python WebSocket execution", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Send initial message to client
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Starting Python execution...\n"))
|
||||
|
||||
// Create a command to run Python in a Docker container
|
||||
cmd := exec.Command("docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"--ulimit", "nofile=64:64", // File descriptor limits
|
||||
"python:3.9", "python", "-c", submission.Code)
|
||||
|
||||
// Get stdin pipe
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to create stdin pipe: %v", submission.ID, err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Failed to create stdin pipe\n"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get stdout and stderr pipes
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to create stdout pipe: %v", submission.ID, err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Failed to create stdout pipe\n"))
|
||||
return
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to create stderr pipe: %v", submission.ID, err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Failed to create stderr pipe\n"))
|
||||
return
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to start command: %v", submission.ID, err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("Error: Failed to start command: %v\n", err)))
|
||||
return
|
||||
}
|
||||
|
||||
// Create a context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create a channel to signal when the command is done
|
||||
done := make(chan struct{})
|
||||
|
||||
// Start a goroutine to handle command completion
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Command failed: %v", submission.ID, err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("\nExecution failed: %v\n", err)))
|
||||
} else {
|
||||
log.Printf("[WS-PYTHON-%s] Command completed successfully", submission.ID)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("\nExecution completed successfully\n"))
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Start a goroutine to read from stdout and stderr
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(io.MultiReader(stdout, stderr))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
log.Printf("[WS-PYTHON-%s] Output: %s", submission.ID, line)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte(line+"\n"))
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle input from the WebSocket
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case input := <-inputChan:
|
||||
log.Printf("[WS-PYTHON-%s] Received input: %s", submission.ID, input)
|
||||
// Write the input to stdin
|
||||
_, err := io.WriteString(stdin, input+"\n")
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to write to stdin: %v", submission.ID, err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the command to complete or timeout
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("[WS-PYTHON-%s] Execution timed out after 30 seconds", submission.ID)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("\nExecution timed out after 30 seconds\n"))
|
||||
cmd.Process.Kill()
|
||||
case <-done:
|
||||
// Command completed
|
||||
}
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[WS-PYTHON-%s] Python execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
// Update submission result
|
||||
submission.CompletedAt = time.Now()
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
// executeJavaWithWebSocket runs Java code with WebSocket for I/O
|
||||
func (s *ExecutionService) executeJavaWithWebSocket(submission *model.CodeSubmission, inputChan chan string, conn *websocket.Conn) {
|
||||
// For now, just send a message that this is not implemented
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Java WebSocket execution not yet implemented\n"))
|
||||
submission.Status = "failed"
|
||||
submission.Output = "Java WebSocket execution not yet implemented"
|
||||
}
|
||||
|
||||
// executeCWithWebSocket runs C code with WebSocket for I/O
|
||||
func (s *ExecutionService) executeCWithWebSocket(submission *model.CodeSubmission, inputChan chan string, conn *websocket.Conn) {
|
||||
// For now, just send a message that this is not implemented
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("C WebSocket execution not yet implemented\n"))
|
||||
submission.Status = "failed"
|
||||
submission.Output = "C WebSocket execution not yet implemented"
|
||||
}
|
||||
|
||||
// executeCppWithWebSocket runs C++ code with WebSocket for I/O
|
||||
func (s *ExecutionService) executeCppWithWebSocket(submission *model.CodeSubmission, inputChan chan string, conn *websocket.Conn) {
|
||||
// For now, just send a message that this is not implemented
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("C++ WebSocket execution not yet implemented\n"))
|
||||
submission.Status = "failed"
|
||||
submission.Output = "C++ WebSocket execution not yet implemented"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
|
||||
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
|
||||
Binary file not shown.
3
examples/basic_input.py
Normal file
3
examples/basic_input.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# Very basic input example
|
||||
name = input("What is your name? ")
|
||||
print(f"Hello, {name}!")
|
||||
41
examples/interactive_calculator.js
Normal file
41
examples/interactive_calculator.js
Normal file
@@ -0,0 +1,41 @@
|
||||
// Interactive Calculator Example
|
||||
// This demonstrates how the interactive input/output works
|
||||
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
function calculator() {
|
||||
console.log("Welcome to the Interactive Calculator!");
|
||||
console.log("Enter 'q' to quit at any time.");
|
||||
|
||||
function promptUser() {
|
||||
rl.question("Enter an expression (e.g., 2 + 3): ", (expression) => {
|
||||
if (expression.toLowerCase() === 'q') {
|
||||
console.log("Thank you for using the Interactive Calculator!");
|
||||
rl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Safely evaluate the expression
|
||||
const result = eval(expression);
|
||||
console.log(`Result: ${result}`);
|
||||
} catch (e) {
|
||||
console.log(`Error: ${e.message}`);
|
||||
console.log("Please try again with a valid expression.");
|
||||
}
|
||||
|
||||
// Continue prompting
|
||||
promptUser();
|
||||
});
|
||||
}
|
||||
|
||||
// Start the prompt loop
|
||||
promptUser();
|
||||
}
|
||||
|
||||
// Run the calculator
|
||||
calculator();
|
||||
24
examples/interactive_calculator.py
Normal file
24
examples/interactive_calculator.py
Normal file
@@ -0,0 +1,24 @@
|
||||
# Interactive Calculator Example
|
||||
# This demonstrates how the interactive input/output works
|
||||
|
||||
def calculator():
|
||||
print("Welcome to the Interactive Calculator!")
|
||||
print("Enter 'q' to quit at any time.")
|
||||
|
||||
while True:
|
||||
expression = input("Enter an expression (e.g., 2 + 3): ")
|
||||
|
||||
if expression.lower() == 'q':
|
||||
print("Thank you for using the Interactive Calculator!")
|
||||
break
|
||||
|
||||
try:
|
||||
# Safely evaluate the expression
|
||||
result = eval(expression)
|
||||
print(f"Result: {result}")
|
||||
except Exception as e:
|
||||
print(f"Error: {str(e)}")
|
||||
print("Please try again with a valid expression.")
|
||||
|
||||
# Run the calculator
|
||||
calculator()
|
||||
22
examples/interactive_javascript.js
Normal file
22
examples/interactive_javascript.js
Normal file
@@ -0,0 +1,22 @@
|
||||
// Interactive JavaScript Example
|
||||
// This example demonstrates interactive input/output
|
||||
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
rl.question('Enter your name: ', (name) => {
|
||||
console.log(`Hello, ${name}!`);
|
||||
|
||||
rl.question('Enter your age: ', (age) => {
|
||||
console.log(`You are ${age} years old.`);
|
||||
|
||||
rl.question('What is your favorite color? ', (color) => {
|
||||
console.log(`Your favorite color is ${color}.`);
|
||||
console.log('Thank you for using the interactive example!');
|
||||
rl.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
13
examples/interactive_python.py
Normal file
13
examples/interactive_python.py
Normal file
@@ -0,0 +1,13 @@
|
||||
# Interactive Python Example
|
||||
# This example demonstrates interactive input/output
|
||||
|
||||
name = input("Enter your name: ")
|
||||
print(f"Hello, {name}!")
|
||||
|
||||
age = input("Enter your age: ")
|
||||
print(f"You are {age} years old.")
|
||||
|
||||
favorite_color = input("What is your favorite color? ")
|
||||
print(f"Your favorite color is {favorite_color}.")
|
||||
|
||||
print("Thank you for using the interactive example!")
|
||||
5
examples/simple_input.py
Normal file
5
examples/simple_input.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# Simple input example
|
||||
name = input("Enter your name: ")
|
||||
print(f"Hello, {name}!")
|
||||
for i in range(5):
|
||||
print(f"Count: {i}")
|
||||
Reference in New Issue
Block a user