Compare commits
7 Commits
v1.0.0
...
socket_rew
| Author | SHA1 | Date | |
|---|---|---|---|
| 4453e69e68 | |||
| c143efa70e | |||
| 86dcfa2a4a | |||
| 305650925e | |||
| d3e0ef95d4 | |||
| 809505ec84 | |||
| 73fa23e434 |
47
.github/workflows/main.yml
vendored
Normal file
47
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Release Monaco
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Triggers on version tags like v1.0.0, v1.1.0-beta, etc.
|
||||
|
||||
jobs:
|
||||
build-and-release:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go-version: [1.22.3] # Adjust to your Go version
|
||||
os: [linux, darwin, windows]
|
||||
arch: [amd64, arm64]
|
||||
exclude:
|
||||
- os: darwin
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
|
||||
- name: Build binary
|
||||
env:
|
||||
GOOS: ${{ matrix.os }}
|
||||
GOARCH: ${{ matrix.arch }}
|
||||
run: |
|
||||
cd backend
|
||||
go mod tidy
|
||||
GOOS=$GOOS GOARCH=$GOARCH go build -o monaco-${{ matrix.os }}-${{ matrix.arch }} .
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: |
|
||||
backend/monaco-${{ matrix.os }}-${{ matrix.arch }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -1 +1 @@
|
||||
VITE_API_URL="https://monacoapi.thearnab.tech"
|
||||
VITE_API_URL="http://localhost:8080"
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import Sidebar from "./Sidebar";
|
||||
import Panel from "./Panel"; // Import Panel component
|
||||
import WebSocketTerminal from "./WebSocketTerminal"; // Import WebSocket Terminal
|
||||
|
||||
const EditorArea = ({
|
||||
sidebarVisible = true,
|
||||
@@ -60,6 +61,15 @@ const EditorArea = ({
|
||||
const [terminalOutput, setTerminalOutput] = useState([]);
|
||||
const [activeRunningFile, setActiveRunningFile] = useState(null);
|
||||
|
||||
// Add a new state for user input
|
||||
const [userInput, setUserInput] = useState("");
|
||||
// Add a new state for waiting for input
|
||||
const [waitingForInput, setWaitingForInput] = useState(false);
|
||||
// Add 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(() => {
|
||||
if (isNewFileModalOpen && newFileInputRef.current) {
|
||||
@@ -502,7 +512,7 @@ Happy coding!`;
|
||||
width: `calc(100% - ${sidebarVisible ? sidebarWidth : 0}px)`
|
||||
};
|
||||
|
||||
// Update the run code function to work with backend
|
||||
// Modified handleRunCode to start execution immediately
|
||||
const handleRunCode = async () => {
|
||||
if (!activeFile) return;
|
||||
|
||||
@@ -512,16 +522,30 @@ Happy coding!`;
|
||||
setPanelVisible(true);
|
||||
}
|
||||
|
||||
// Set running state
|
||||
// Reset states
|
||||
setIsRunning(true);
|
||||
setWaitingForInput(false);
|
||||
setActiveRunningFile(activeFile.id);
|
||||
setActiveRunningSubmissionId(null);
|
||||
setUserInput('');
|
||||
|
||||
// Clear previous output and add new command
|
||||
// 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: 'command', content: `$ run ${activeFile.id}` },
|
||||
{ type: 'output', content: '------- PROGRAM EXECUTION -------' },
|
||||
{ type: 'output', content: `Language: ${language}` },
|
||||
{ type: 'output', content: 'Executing code...' }
|
||||
];
|
||||
setTerminalOutput(newOutput);
|
||||
|
||||
@@ -529,15 +553,16 @@ Happy coding!`;
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
||||
|
||||
try {
|
||||
// Step 1: Submit code to backend
|
||||
// Submit the code for execution immediately
|
||||
const submitResponse = await fetch(`${apiUrl}/submit`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
language: language,
|
||||
code: activeFile.content
|
||||
language: getLanguageFromExtension(fileExtension),
|
||||
code: activeFile.content,
|
||||
input: '' // No initial input
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -546,11 +571,81 @@ Happy coding!`;
|
||||
}
|
||||
|
||||
const { id } = await submitResponse.json();
|
||||
setActiveRunningSubmissionId(id);
|
||||
setTerminalOutput(prev => [...prev, { type: 'output', content: `Job submitted with ID: ${id}` }]);
|
||||
|
||||
// Step 2: Poll for status until completed or failed
|
||||
// Start polling for status and output
|
||||
pollForStatusAndOutput(id);
|
||||
} catch (error) {
|
||||
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
|
||||
setIsRunning(false);
|
||||
setActiveRunningSubmissionId(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle between WebSocket and HTTP modes
|
||||
const toggleWebSocketMode = () => {
|
||||
setUseWebSocket(!useWebSocket);
|
||||
};
|
||||
|
||||
// Simplified handleInputSubmit to only handle interactive input
|
||||
const handleInputSubmit = async () => {
|
||||
if (!waitingForInput || !activeRunningSubmissionId) return;
|
||||
|
||||
// Store the input value before clearing it
|
||||
const inputValue = userInput;
|
||||
|
||||
// Clear the input field and reset waiting state immediately for better UX
|
||||
setUserInput('');
|
||||
setWaitingForInput(false);
|
||||
|
||||
// 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 {
|
||||
// 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
|
||||
}),
|
||||
});
|
||||
|
||||
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') {
|
||||
while (status !== 'completed' && status !== 'failed' && status !== 'waiting_for_input') {
|
||||
// Add a small delay between polls
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
@@ -578,6 +673,90 @@ Happy coding!`;
|
||||
});
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -587,19 +766,34 @@ Happy coding!`;
|
||||
const { output } = await resultResponse.json();
|
||||
|
||||
// Format and display output
|
||||
const outputLines = output.split('\n').map(line => ({
|
||||
type: status === 'failed' ? 'warning' : 'output',
|
||||
content: line
|
||||
}));
|
||||
const outputLines = [];
|
||||
|
||||
setTerminalOutput(prev => [
|
||||
...prev,
|
||||
{
|
||||
// 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
|
||||
]);
|
||||
|
||||
@@ -607,11 +801,16 @@ Happy coding!`;
|
||||
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}` }]);
|
||||
} finally {
|
||||
// Set running state to false
|
||||
setIsRunning(false);
|
||||
setWaitingForInput(false);
|
||||
setActiveRunningSubmissionId(null);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -640,6 +839,37 @@ Happy coding!`;
|
||||
}
|
||||
};
|
||||
|
||||
// Add this function above the return statement
|
||||
const handleDownloadFile = () => {
|
||||
if (!activeFile) return;
|
||||
|
||||
// Create a blob with the file content
|
||||
const blob = new Blob([activeFile.content], { type: 'text/plain' });
|
||||
|
||||
// Create a URL for the blob
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create a temporary anchor element
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
||||
// Get just the filename without path
|
||||
const fileName = activeFile.id.includes('/') ?
|
||||
activeFile.id.split('/').pop() :
|
||||
activeFile.id;
|
||||
|
||||
// Set the download attribute with the filename
|
||||
a.download = fileName;
|
||||
|
||||
// Append to the document, click it, and then remove it
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
// Release the object URL
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="editor-container">
|
||||
{sidebarVisible && (
|
||||
@@ -714,15 +944,38 @@ Happy coding!`;
|
||||
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>
|
||||
@@ -781,17 +1034,32 @@ Happy coding!`;
|
||||
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} // Use the new function
|
||||
onClose={togglePanel}
|
||||
userInput={userInput}
|
||||
onUserInputChange={setUserInput}
|
||||
onInputSubmit={handleInputSubmit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Modify the editor-actions div to include the download button */}
|
||||
<div className="editor-actions">
|
||||
<button
|
||||
className="editor-action-button"
|
||||
@@ -801,6 +1069,30 @@ Happy coding!`;
|
||||
>
|
||||
<Save size={16} />
|
||||
</button>
|
||||
|
||||
{/* Add download button */}
|
||||
<button
|
||||
className="editor-action-button"
|
||||
onClick={handleDownloadFile}
|
||||
disabled={!activeTab}
|
||||
title="Download file"
|
||||
>
|
||||
<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="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isNewFileModalOpen && (
|
||||
@@ -860,6 +1152,61 @@ Happy coding!`;
|
||||
<Edit size={14} className="mr-1" />
|
||||
Rename
|
||||
</div>
|
||||
|
||||
{/* Add download option - only show for files */}
|
||||
{contextMenuTarget?.type === 'file' && (
|
||||
<div className="context-menu-item" onClick={() => {
|
||||
// Find the file in the files array
|
||||
const file = files.find(f => f.id === contextMenuTarget.path);
|
||||
if (file) {
|
||||
// Create a blob with the file content
|
||||
const blob = new Blob([file.content], { type: 'text/plain' });
|
||||
|
||||
// Create a URL for the blob
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
// Create a temporary anchor element
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
|
||||
// Get just the filename without path
|
||||
const fileName = file.id.includes('/') ?
|
||||
file.id.split('/').pop() :
|
||||
file.id;
|
||||
|
||||
// Set the download attribute with the filename
|
||||
a.download = fileName;
|
||||
|
||||
// Append to the document, click it, and then remove it
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
|
||||
// Release the object URL
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
closeContextMenu();
|
||||
}}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="mr-1"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
|
||||
<polyline points="7 10 12 15 17 10"></polyline>
|
||||
<line x1="12" y1="15" x2="12" y2="3"></line>
|
||||
</svg>
|
||||
Download
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="context-menu-item delete" onClick={() => {
|
||||
deleteItem(contextMenuTarget.path, contextMenuTarget.type);
|
||||
closeContextMenu();
|
||||
|
||||
@@ -6,9 +6,13 @@ const Panel = ({
|
||||
height,
|
||||
terminalOutput = [],
|
||||
isRunning = false,
|
||||
waitingForInput = false,
|
||||
activeRunningFile = null,
|
||||
initialTab = "terminal",
|
||||
onClose
|
||||
onClose,
|
||||
userInput = "",
|
||||
onUserInputChange,
|
||||
onInputSubmit
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState(initialTab);
|
||||
|
||||
@@ -24,13 +28,37 @@ const Panel = ({
|
||||
// Render output from EditorArea when available
|
||||
<>
|
||||
{terminalOutput.map((line, index) => (
|
||||
<div key={index} className={`terminal-line ${line.type === 'warning' ? 'terminal-warning' : 'terminal-output'}`}>
|
||||
{line.type === 'command' ? <span className="terminal-prompt">$</span> : ''} {line.content}
|
||||
<div 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">
|
||||
<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>
|
||||
)}
|
||||
</>
|
||||
|
||||
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;
|
||||
@@ -435,6 +435,30 @@ body {
|
||||
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: #888888;
|
||||
color: #cccccc;
|
||||
@@ -862,7 +886,7 @@ body {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.terminal-toggle-button {
|
||||
.terminal-toggle-button, .websocket-toggle-button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: #cccccc;
|
||||
@@ -873,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;
|
||||
}
|
||||
|
||||
@@ -924,6 +953,162 @@ body {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.terminal-input {
|
||||
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 {
|
||||
color: #75beff;
|
||||
}
|
||||
|
||||
137
Readme.md
137
Readme.md
@@ -1,22 +1,125 @@
|
||||
# Monaco Code Execution Engine
|
||||
Monaco is a secure, containerized code execution engine that allows you to run code in multiple programming languages through a simple REST API.
|
||||
# Monaco Online Code Compiler
|
||||
|
||||
A full-featured online code compiler with a VS Code-like interface. This project allows users to write, edit, and execute code in multiple programming languages directly in the browser.
|
||||
|
||||
## Features
|
||||
- Multi-language support: Run code in Python, Java, C, and C++
|
||||
- Secure execution: All code runs in isolated Docker containers
|
||||
- Resource limits: Memory, CPU, and file descriptor limits to prevent abuse
|
||||
- Concurrent processing: Efficient job queue for handling multiple requests
|
||||
- Simple REST API: Easy to integrate with any frontend
|
||||
|
||||
## Architecture
|
||||
Monaco consists of several components:
|
||||
- **VS Code-like Interface**: Familiar editor experience with syntax highlighting, tabs, and file explorer
|
||||
- **Multi-language Support**: Run code in Python, JavaScript, Go, Java, C, and C++
|
||||
- **Input/Output Handling**: Enter input for your programs and see the output in real-time
|
||||
- **Secure Execution**: Code runs in isolated Docker containers on the backend
|
||||
- **File Management**: Create, edit, and organize files and folders
|
||||
|
||||
- HTTP Handlers (handler/handler.go): Processes API requests
|
||||
- Execution Service (service/execution.go): Manages code execution in containers
|
||||
- Job Queue (queue/queue.go): Handles concurrent execution of code submissions
|
||||
- Submission Model (model/submission.go): Defines the data structure for code submissions
|
||||
## Project Structure
|
||||
|
||||
## Requirements
|
||||
- Go 1.22.3 or higher
|
||||
- Docker
|
||||
- Network connectivity for container image pulling
|
||||
- **Frontend**: React-based UI with Monaco Editor
|
||||
- **Backend**: Go-based code execution service with Docker integration
|
||||
- HTTP Handlers (internal/api/handlers): Processes API requests
|
||||
- Execution Service (internal/executor): Manages code execution in containers
|
||||
- Job Queue (internal/queue): Handles concurrent execution of code submissions
|
||||
- Submission Model (internal/models): Defines the data structure for code submissions
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Node.js 18+ for the frontend
|
||||
- Go 1.22+ for the backend
|
||||
- Docker for code execution
|
||||
|
||||
### Running the Frontend
|
||||
|
||||
```bash
|
||||
cd Frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The frontend will be available at http://localhost:5173
|
||||
|
||||
### Running the Backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
go build -o monaco ./cmd/server
|
||||
./monaco
|
||||
```
|
||||
|
||||
The backend API will be available at http://localhost:8080
|
||||
|
||||
## Using the Online Compiler
|
||||
|
||||
1. **Create a File**: Click the "+" button in the editor tabs or use the file explorer
|
||||
2. **Write Code**: Use the Monaco editor to write your code
|
||||
3. **Run Code**: Click the "Play" button in the top right corner
|
||||
4. **Enter Input**: If your program requires input, enter it in the terminal panel
|
||||
5. **View Output**: See the execution results in the terminal panel
|
||||
|
||||
## Supported Languages
|
||||
|
||||
- **Python** (.py)
|
||||
- **JavaScript** (.js)
|
||||
- **Go** (.go)
|
||||
- **Java** (.java)
|
||||
- **C** (.c)
|
||||
- **C++** (.cpp)
|
||||
|
||||
## Examples
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
name = input("Enter your name: ")
|
||||
print(f"Hello, {name}!")
|
||||
for i in range(5):
|
||||
print(f"Count: {i}")
|
||||
```
|
||||
|
||||
### JavaScript
|
||||
|
||||
```javascript
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
rl.question('Enter your name: ', (name) => {
|
||||
console.log(`Hello, ${name}!`);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
console.log(`Count: ${i}`);
|
||||
}
|
||||
rl.close();
|
||||
});
|
||||
```
|
||||
|
||||
### Go
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Print("Enter your name: ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
name, _ := reader.ReadString('\n')
|
||||
name = strings.TrimSpace(name)
|
||||
fmt.Printf("Hello, %s!\n", name)
|
||||
for i := 0; i < 5; i++ {
|
||||
fmt.Printf("Count: %d\n", i)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- All code is executed in isolated Docker containers
|
||||
- Network access is disabled
|
||||
- Memory and CPU limits are enforced
|
||||
- Execution timeouts prevent infinite loops
|
||||
@@ -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())
|
||||
}
|
||||
56
backend/data.py
Normal file
56
backend/data.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import os
|
||||
import aiohttp
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# Base URL template
|
||||
BASE_URL = "https://bhuvan-app3.nrsc.gov.in/isroeodatadownloadutility/tiledownloadnew_cfr_new.php?f=nices_ssm2_{}_{}.zip&se=NICES&u=arnabafk"
|
||||
|
||||
# Directory to save files
|
||||
SAVE_DIR = "data"
|
||||
os.makedirs(SAVE_DIR, exist_ok=True)
|
||||
|
||||
async def download_file(session, file_url, file_path):
|
||||
"""Download a file asynchronously."""
|
||||
print(f"Downloading {file_url}...")
|
||||
try:
|
||||
async with session.get(file_url) as response:
|
||||
if response.status == 200:
|
||||
with open(file_path, 'wb') as file:
|
||||
while chunk := await response.content.read(1024):
|
||||
file.write(chunk)
|
||||
print(f"Downloaded: {file_path}")
|
||||
else:
|
||||
print(f"Failed to download: {file_path}, Status Code: {response.status}")
|
||||
except Exception as e:
|
||||
print(f"Error downloading {file_url}: {e}")
|
||||
|
||||
async def fetch_data_for_year(session, year):
|
||||
"""Fetch and download data for a given year."""
|
||||
year_dir = os.path.join(SAVE_DIR, str(year))
|
||||
os.makedirs(year_dir, exist_ok=True)
|
||||
|
||||
start_date = datetime(year, 1, 1)
|
||||
end_date = datetime(year, 12, 31)
|
||||
delta = timedelta(days=2)
|
||||
tasks = []
|
||||
|
||||
date = start_date
|
||||
while date <= end_date:
|
||||
date_str = date.strftime("%Y%m%d")
|
||||
file_url = BASE_URL.format(date_str, "NICES")
|
||||
file_name = f"nices_ssm2_{date_str}.zip"
|
||||
file_path = os.path.join(year_dir, file_name)
|
||||
|
||||
tasks.append(download_file(session, file_url, file_path))
|
||||
date += delta
|
||||
|
||||
await asyncio.gather(*tasks)
|
||||
|
||||
async def main():
|
||||
"""Main function to download data for multiple years."""
|
||||
async with aiohttp.ClientSession() as session:
|
||||
await asyncio.gather(*(fetch_data_for_year(session, year) for year in range(2002, 2025)))
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
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
|
||||
```
|
||||
@@ -6,6 +6,7 @@ require github.com/stretchr/testify v1.9.0
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
|
||||
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()
|
||||
}
|
||||
@@ -51,6 +51,7 @@ func main() {
|
||||
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,6 +1,8 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -14,19 +16,24 @@ import (
|
||||
|
||||
"github.com/arnab-afk/monaco/model"
|
||||
"github.com/arnab-afk/monaco/queue"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// ExecutionService handles code execution for multiple languages
|
||||
type ExecutionService struct {
|
||||
mu sync.Mutex
|
||||
queue *queue.JobQueue
|
||||
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
|
||||
func NewExecutionService() *ExecutionService {
|
||||
log.Println("Initializing execution service with 3 concurrent workers")
|
||||
return &ExecutionService{
|
||||
queue: queue.NewJobQueue(35), // 3 concurrent executions max
|
||||
queue: queue.NewJobQueue(3), // 3 concurrent executions max
|
||||
wsConnections: make(map[string]*websocket.Conn),
|
||||
wsInputChannels: make(map[string]chan string),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,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
|
||||
BIN
backend/tmp/main.exe
Normal file
BIN
backend/tmp/main.exe
Normal file
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