48 Commits

Author SHA1 Message Date
ishikabhoyar
eabfbf806e Refactor footer padding and enhance scrollbar styles for terminal and content areas 2025-08-17 17:07:38 +05:30
ishikabhoyar
e4f193613a Add footer component and styles to prevent overlap with content 2025-08-17 17:03:16 +05:30
210f531990 Update configuration defaults for server and executor settings 2025-08-15 14:53:57 +05:30
e9553dd3af Add DNS routing setup for cloudflared tunnel in Dockerfile 2025-08-15 14:23:29 +05:30
458b2ca06f Add credentials.json and update cloudflared configuration in Dockerfile 2025-08-15 14:19:58 +05:30
232c6ec530 Update supervisord configuration for cloudflared to use tunnel URL and modify tunnel ID in config.json 2025-08-15 14:17:18 +05:30
20680719f5 Update Dockerfile and docker-compose to enhance cloudflared configuration and startup process 2025-08-15 14:09:36 +05:30
c7d65c612f Refactor Dockerfile and docker-compose to use supervisord for process management and update cloudflared installation method 2025-08-15 14:05:00 +05:30
e5827cfa42 Add supervisord configuration for backend and cloudflared services 2025-08-14 23:55:57 +05:30
720a37fa82 Refactor Dockerfile to create startup script dynamically, ensuring correct line endings and improved backend initialization 2025-08-14 23:52:05 +05:30
9474d2f633 Add Dockerfile and Makefile for cloudflared tunnel setup; include startup script 2025-08-14 23:49:19 +05:30
5902300c95 Refactor Dockerfile to streamline cloudflared installation and improve image layering 2025-08-14 23:42:45 +05:30
eb2873a3b9 Add Docker setup for Monaco backend with Cloudflare tunnel support 2025-08-14 21:15:53 +05:30
ishikabhoyar
25900803c3 Increase timeout settings for Python, C, C++, JavaScript, and Go language configurations to improve execution reliability 2025-08-08 13:39:49 +05:30
ishikabhoyar
5104fcbde0 java timeout 2025-08-08 13:37:52 +05:30
ishikabhoyar
402235bdec Remove unused vite.svg file to declutter the project 2025-08-08 13:37:30 +05:30
2af996b83e Add Dockerfile and Nginx configuration for multi-stage build setup 2025-07-23 19:31:13 +05:30
ishikabhoyar
d501b53ab6 Add favicon.ico and link it in index.html for improved branding 2025-07-23 17:25:21 +05:30
ishikabhoyar
ac12360822 Remove CodeChallenge component CSS file to streamline styling management 2025-07-23 17:17:12 +05:30
ishikabhoyar
4654b93b15 Enhance styling in CodeChallenge component for improved UI and readability 2025-07-23 17:16:23 +05:30
ishikabhoyar
2867f3bf42 Enhance CodeChallenge component with improved styling for problem examples and refined WebSocket handling for execution state management 2025-07-23 16:04:06 +05:30
ishikabhoyar
6d77a6b8ce Refactor language identifier mapping and remove unused language options in CodeChallenge component 2025-07-22 15:52:06 +05:30
ishikabhoyar
233be39b7f Add language identifier mapping and starter code templates for multiple languages in CodeChallenge component 2025-07-22 15:47:24 +05:30
ishikabhoyar
6964f370cb Enhance WebSocket message handling to support new message types and improve error reporting 2025-07-22 15:32:59 +05:30
ishikabhoyar
e12bbcfc6a Implement Code Challenge component with styling and WebSocket integration 2025-07-22 15:26:54 +05:30
ishikabhoyar
e61bd7cfca Add new backend implementation in main file 2025-06-22 12:46:46 +05:30
ishikabhoyar
a6bd8eeebb Refactor WebSocket input handling to support structured messages and improve error logging 2025-06-22 12:44:00 +05:30
ishikabhoyar
c529a48f31 Enhance C code execution by adding a wrapper to disable stdout buffering 2025-06-22 12:11:13 +05:30
ishikabhoyar
86bc89c12e new backend changes 2025-06-22 11:30:47 +05:30
ishikabhoyar
6802cefcaa Remove unused Monaco distribution files for Windows and macOS 2025-06-22 11:17:30 +05:30
ishikabhoyar
85c9b99ef6 Remove unused Monaco distribution files for various platforms 2025-06-22 11:16:58 +05:30
ishikabhoyar
142ff262ac Remove unused Monaco distribution files 2025-06-22 11:16:50 +05:30
ishikabhoyar
7eb42b20dd Remove unused backend distribution files and configuration 2025-06-22 11:16:40 +05:30
ishikabhoyar
6128348417 Remove backend documentation and test files 2025-06-22 11:16:22 +05:30
ishikabhoyar
b860db0b41 Remove backend main.go and go.sum files 2025-06-22 11:16:12 +05:30
ishikabhoyar
75ffb94cca Remove unused data fetching and Go module files 2025-06-22 11:16:03 +05:30
56e3086cd9 Merge pull request #1 from Arnab-Afk/socket
Socket Integration
2025-03-30 21:36:59 +05:30
4a737744df Update README.md to include WebSocket support and enhance installation instructions 2025-03-30 21:34:56 +05:30
1cbb4f3c35 Implement execution status polling and enhance input handling in EditorArea and Panel components 2025-03-30 21:34:45 +05:30
ishikabhoyar
918b323cda Add warning indicator and language mode to StatusBar component 2025-03-30 11:15:26 +05:30
ishikabhoyar
208655c9bc Refactor StatusBar component and adjust CSS for improved layout and styling 2025-03-30 02:06:27 +05:30
ishikabhoyar
48a14f674d Refactor Panel and StatusBar components by adding comments for clarity and improving accessibility with aria-labels 2025-03-30 01:59:12 +05:30
ishikabhoyar
697c4b8460 Enhance Panel component with additional tabs and keyboard input handling for terminal 2025-03-30 01:51:21 +05:30
ishikabhoyar
99e12a7355 Add custom icon for README.md file in Sidebar component 2025-03-30 01:26:27 +05:30
ishikabhoyar
3cc73e786a Refactor Sidebar component by removing unused folder icon and cleaning up code 2025-03-30 01:18:15 +05:30
ishikabhoyar
80a713cc56 Remove Navbar component and associated styles 2025-03-30 01:11:21 +05:30
ishikabhoyar
648391e6ba Update README and EditorArea components with project description and functionalities 2025-03-30 00:45:05 +05:30
3a75000e12 Implement WebSocket support for terminal connections and enhance terminal UI 2025-03-30 00:13:32 +05:30
97 changed files with 4088 additions and 6804 deletions

56
Dockerfile Normal file
View File

@@ -0,0 +1,56 @@
# Stage 1: Build the React frontend
FROM node:18-alpine AS frontend-builder
WORKDIR /app/frontend
# Define a build-time argument for the API URL with a default value
ARG VITE_API_URL=""
# Set it as an environment variable for the build command
ENV VITE_API_URL=$VITE_API_URL
# Copy package files and install dependencies
COPY Frontend/package.json Frontend/package-lock.json* Frontend/yarn.lock* ./
RUN yarn install --frozen-lockfile
# Copy the rest of the frontend source code
COPY Frontend/ ./
# Build the static files. Vite will use the VITE_API_URL env var.
RUN yarn build
# Stage 2: Build the Go backend
FROM golang:1.19-alpine AS backend-builder
WORKDIR /app/backend
# Install git for dependency fetching
RUN apk update && apk add --no-cache git
# Copy go module files and download dependencies
COPY new-backend/go.mod new-backend/go.sum ./
RUN go mod download
# Copy the backend source code
COPY new-backend/ ./
# Build the Go binary
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o /monaco-backend .
# Stage 3: Create the final image with Nginx
FROM nginx:1.25-alpine
# Install Docker client for the backend
RUN apk update && apk add --no-cache docker-cli
# Copy the Go backend binary
COPY --from=backend-builder /monaco-backend /usr/local/bin/monaco-backend
# Copy the built frontend files to the Nginx html directory
COPY --from=frontend-builder /app/frontend/dist /usr/share/nginx/html
# Copy the Nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf
# Expose the public port for Nginx
EXPOSE 80
# Start both the backend and Nginx
CMD sh -c 'monaco-backend & nginx -g "daemon off;"'

View File

@@ -1,13 +1,24 @@
# VSCode Clone with React and Vite
# VS Code Clone Project
This project is a VSCode-like code editor built with React and Vite. It features a customizable UI with an activity bar, sidebar, editor area, panel, and status bar, mimicking the look and feel of Visual Studio Code.
## Authors
- Arnab Bhowmik
- Ishika Bhoyar
## Features
## Description
This project is a VS Code Clone built with React and Monaco Editor. It features a file tree navigation, tab management, code editing with syntax highlighting, and a terminal panel for running code. It mimics the core functionalities of Visual Studio Code in a browser-based environment.
- **Activity Bar**: Switch between different views like Explorer, Search, Source Control, etc.
- **Sidebar**: Displays file explorer, search results, and source control information.
- **Editor Area**: Code editor with syntax highlighting and multiple tabs.
- **Panel**: Terminal, Problems, and Output views.
- **Status Bar**: Displays status information and provides quick actions.
## Frontend Functionalities
- Built with React and Monaco Editor.
- File tree navigation for managing files and folders.
- Tab management for opening multiple files simultaneously.
- Code editing with syntax highlighting and language support.
- Terminal panel for running code and viewing output.
- Persistent file structure and content using localStorage.
## Project Structure
## Backend Functionalities
- Built with Go and Docker for secure code execution.
- Supports multiple programming languages (Python, Java, C/C++).
- Executes code in isolated Docker containers with resource limits.
- RESTful API for submitting code, checking status, and retrieving results.
- Job queue system for managing concurrent executions.
- Enforces timeouts and resource limits for security and performance.

View File

@@ -3,6 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/public/favicon.ico" type="image/x-icon" />
<title>VSCode</title>
</head>
<body>

BIN
Frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,10 +1,18 @@
import VSCodeUI from "./components/VSCodeUI.jsx"
import CodeChallenge from "./components/CodeChallenge.jsx"
import "./index.css"
function App() {
return (
<div className="App">
<VSCodeUI />
<CodeChallenge />
<footer className="footer-bar fixed bottom-0 left-0 right-0 border-t border-slate-200/40 dark:border-gray-800/20 bg-black">
<div className="flex items-center justify-center h-7">
<span className="text-xs text-slate-400 dark:text-gray-400 flex items-center">
Copyright © 2025. Made with
by Ishika and Arnab.
</span>
</div>
</footer>
</div>
)
}

View File

@@ -0,0 +1,750 @@
import React, { useState, useEffect, useRef } from 'react';
import Editor from "@monaco-editor/react";
import { Play, Send } from 'lucide-react';
const CodeChallenge = () => {
const [activeQuestion, setActiveQuestion] = useState("Q.1");
const [language, setLanguage] = useState("JavaScript");
const [code, setCode] = useState("");
const [isRunning, setIsRunning] = useState(false);
const [terminalOutput, setTerminalOutput] = useState([]);
const [autoSelected, setAutoSelected] = useState(true);
const [activeSocket, setActiveSocket] = useState(null);
const [submissionId, setSubmissionId] = useState(null);
const socketRef = useRef(null);
// Map frontend language names to backend language identifiers
const getLanguageIdentifier = (uiLanguage) => {
const languageMap = {
'javascript': 'javascript',
'python': 'python',
'java': 'java',
'c++': 'cpp',
'c': 'c'
};
// Important: make sure we convert to lowercase to match the backend's expected format
return languageMap[uiLanguage.toLowerCase()] || uiLanguage.toLowerCase();
};
// Reset execution state to allow rerunning
const resetExecutionState = () => {
setIsRunning(false);
// Properly close the socket if it exists and is open
if (socketRef.current) {
if (socketRef.current.readyState === WebSocket.OPEN) {
socketRef.current.close();
}
socketRef.current = null;
}
// Ensure activeSocket is also nullified
setActiveSocket(null);
console.log('Execution state reset, buttons should be enabled');
};
// Example problem data
const problems = {
"Q.1": {
id: "two-sum",
title: "Two Sum",
description: "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.",
constraints: "You may assume that each input would have exactly one solution, and you may not use the same element twice.",
examples: [
{
input: "nums = [2,7,11,15], target = 9",
output: "[0,1]",
explanation: "Because nums[0] + nums[1] == 9, we return [0, 1]."
},
{
input: "nums = [3,2,4], target = 6",
output: "[1,2]"
},
{
input: "nums = [3,3], target = 6",
output: "[0,1]"
}
],
starterCode: `/**
* @param {number[]} nums
* @param {number} target
* @return {number[]}
*/
var twoSum = function(nums, target) {
// Write your solution here
};`
},
"Q.2": {
id: "palindrome-number",
title: "Palindrome Number",
description: "Given an integer x, return true if x is a palindrome, and false otherwise.",
examples: [
{
input: "x = 121",
output: "true"
},
{
input: "x = -121",
output: "false",
explanation: "From left to right, it reads -121. From right to left, it reads 121-. Therefore it is not a palindrome."
}
],
starterCode: `/**
* @param {number} x
* @return {boolean}
*/
var isPalindrome = function(x) {
// Write your solution here
};`
},
"Q.3": {
id: "valid-parentheses",
title: "Valid Parentheses",
description: "Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.",
constraints: "An input string is valid if: Open brackets must be closed by the same type of brackets. Open brackets must be closed in the correct order.",
examples: [
{
input: 's = "()"',
output: "true"
},
{
input: 's = "()[]{}"',
output: "true"
},
{
input: 's = "(]"',
output: "false"
}
],
starterCode: `/**
* @param {string} s
* @return {boolean}
*/
var isValid = function(s) {
// Write your solution here
};`
}
};
// Get appropriate starter code based on language
const getStarterCode = (problem, lang) => {
// Language-specific starter code templates
const templates = {
'JavaScript': problem.starterCode,
'C': `#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
// ${problem.title} solution
int main() {
// Write your solution here
return 0;
}`,
'Python': `# ${problem.title}
def solution():
# Write your solution here
# Use input() for user input in Python
# Example: name = input("Enter your name: ")
pass
if __name__ == "__main__":
solution()`,
'Java': `public class Solution {
// ${problem.title}
public static void main(String[] args) {
// Write your solution here
}
}`,
'C++': `#include <iostream>
#include <vector>
using namespace std;
// ${problem.title} solution
int main() {
// Write your solution here
return 0;
}`
};
return templates[lang] || problem.starterCode;
};
// Set initial code based on active problem
useEffect(() => {
if (problems[activeQuestion]) {
setCode(getStarterCode(problems[activeQuestion], language));
}
}, [activeQuestion, language]);
// Cleanup WebSocket connection on unmount
useEffect(() => {
return () => {
if (socketRef.current) {
socketRef.current.close();
}
};
}, []);
// Set a safety timeout to ensure buttons are re-enabled if execution hangs
useEffect(() => {
let safetyTimer = null;
if (isRunning) {
// If execution is running for more than 30 seconds, reset state
safetyTimer = setTimeout(() => {
console.log('Safety timeout reached, re-enabling buttons');
resetExecutionState();
}, 30000);
}
return () => {
if (safetyTimer) clearTimeout(safetyTimer);
};
}, [isRunning]);
// Connect to WebSocket
const connectToWebSocket = (id) => {
console.log('Connecting to WebSocket with ID:', id);
// Force close any existing connections
if (socketRef.current) {
console.log('Closing existing socket, state:', socketRef.current.readyState);
socketRef.current.onclose = null; // Remove existing handler to avoid conflicts
socketRef.current.onerror = null;
socketRef.current.onmessage = null;
if (socketRef.current.readyState !== WebSocket.CLOSED) {
socketRef.current.close();
}
socketRef.current = null;
}
if (activeSocket) {
console.log('Clearing active socket reference');
setActiveSocket(null);
}
console.log('Creating new WebSocket connection');
const wsUrl = `ws://localhost:8080/api/ws/terminal/${id}`;
const socket = new WebSocket(wsUrl);
socket.onopen = () => {
console.log('WebSocket connection established');
setActiveSocket(socket);
};
socket.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
console.log('WebSocket message received:', message, 'Current isRunning state:', isRunning);
switch (message.type) {
case 'output':
// Handle output message based on the format seen in the screenshot
setTerminalOutput(prev => [
...prev,
{
type: message.content.isError ? 'error' : 'output',
content: message.content.text
}
]);
break;
case 'input_prompt':
// Handle input prompt message (e.g., "Enter your name:")
setTerminalOutput(prev => [
...prev,
{ type: 'output', content: message.content }
]);
break;
case 'status':
let statusText = '';
let statusValue = '';
if (typeof message.content === 'object') {
statusText = `Status: ${message.content.status}`;
statusValue = message.content.status;
} else {
statusText = `Status: ${message.content}`;
statusValue = message.content;
}
setTerminalOutput(prev => [
...prev,
{ type: 'system', content: statusText }
]);
// If status contains "completed" or "failed", stop running
if (statusValue.includes('completed') || statusValue.includes('failed')) {
console.log('Execution completed or failed, stopping');
setTimeout(() => {
setIsRunning(false);
}, 500); // Small delay to ensure UI updates properly
}
break;
case 'error':
let errorContent = '';
if (typeof message.content === 'object' && message.content.message) {
errorContent = message.content.message;
} else {
errorContent = String(message.content);
}
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: errorContent }
]);
console.log('Error received, enabling buttons');
setTimeout(() => {
setIsRunning(false);
}, 500); // Small delay to ensure UI updates properly
break;
case 'system':
const systemContent = String(message.content);
setTerminalOutput(prev => [
...prev,
{ type: 'system', content: systemContent }
]);
// Check for connection closing message which indicates execution is complete
if (systemContent.includes('Connection will close') ||
systemContent.includes('completed successfully') ||
systemContent.includes('Execution completed')) {
console.log('System message indicates completion, enabling buttons');
setTimeout(() => {
setIsRunning(false);
}, 500);
}
break;
default:
// Handle any other message types or direct string content
console.log('Unknown message type:', message);
if (typeof message === 'object') {
setTerminalOutput(prev => [
...prev,
{ type: 'output', content: JSON.stringify(message) }
]);
}
}
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
};
socket.onerror = (error) => {
console.error('WebSocket error:', error);
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: 'WebSocket connection error' }
]);
console.log('WebSocket error, enabling buttons');
setTimeout(() => {
setIsRunning(false);
}, 500); // Small delay to ensure UI updates properly
};
socket.onclose = () => {
console.log('WebSocket connection closed');
setActiveSocket(null);
// Ensure buttons are re-enabled when the connection closes
setTimeout(() => {
resetExecutionState();
}, 100);
};
// Set the socket reference early to ensure we can clean it up if needed
socketRef.current = socket;
return socket;
};
// Handle code execution
const runCode = async () => {
console.log('Run button clicked, current state:', {
isRunning,
socketState: activeSocket ? activeSocket.readyState : 'no socket',
socketRefState: socketRef.current ? socketRef.current.readyState : 'no socket ref'
});
// First make sure previous connections are fully closed
resetExecutionState();
// Increase the delay to ensure clean state before starting new execution
setTimeout(async () => {
// Double-check socket state before proceeding
if (activeSocket || socketRef.current) {
console.warn('Socket still exists after reset, forcing cleanup');
if (activeSocket && activeSocket.readyState !== WebSocket.CLOSED) {
activeSocket.close();
}
if (socketRef.current && socketRef.current.readyState !== WebSocket.CLOSED) {
socketRef.current.close();
}
socketRef.current = null;
setActiveSocket(null);
// Extra delay to ensure socket is fully closed
await new Promise(resolve => setTimeout(resolve, 100));
}
setIsRunning(true);
setTerminalOutput([
{ type: 'system', content: `Running ${problems[activeQuestion].id}...` }
]);
try {
// Submit code to the backend
const response = await fetch('http://localhost:8080/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: code,
language: getLanguageIdentifier(language),
input: '',
}),
});
if (!response.ok) {
throw new Error(`Error: ${response.statusText}`);
}
const data = await response.json();
setSubmissionId(data.id);
// Connect to WebSocket for real-time updates
connectToWebSocket(data.id);
} catch (error) {
console.error('Error submitting code:', error);
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: `Error: ${error.message}` }
]);
resetExecutionState();
}
}, 200); // Increased delay to ensure clean state
};
// Handle code submission
const submitCode = async () => {
setIsRunning(true);
setTerminalOutput([
{ type: 'system', content: `Submitting solution for ${problems[activeQuestion].id}...` }
]);
try {
// Submit code to the backend
const response = await fetch('http://localhost:8080/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: code,
language: getLanguageIdentifier(language),
input: '',
problemId: problems[activeQuestion].id
}),
});
if (!response.ok) {
throw new Error(`Error: ${response.statusText}`);
}
const data = await response.json();
setSubmissionId(data.id);
// Connect to WebSocket for real-time updates
connectToWebSocket(data.id);
} catch (error) {
console.error('Error submitting solution:', error);
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: `Error: ${error.message}` }
]);
setIsRunning(false);
}
};
// Render the current problem
const renderProblem = () => {
const problem = problems[activeQuestion];
if (!problem) return null;
return (
<div className="problem-container">
<h1>{problem.title}</h1>
<div className="problem-description">
<p>{problem.description}</p>
{problem.constraints && <p>{problem.constraints}</p>}
</div>
{/* Test cases section removed */}
</div>
);
};
// Add this useEffect to monitor socket state
useEffect(() => {
// If we have an active socket but aren't running, we should clean up
if (activeSocket && !isRunning) {
console.log('Cleaning up inactive socket');
if (activeSocket.readyState === WebSocket.OPEN) {
activeSocket.close();
}
setActiveSocket(null);
}
}, [activeSocket, isRunning]);
return (
<div className="code-challenge-container">
<header className="code-challenge-header">
<h1>OnScreen Test</h1>
<button className="sign-in-btn">Sign In</button>
</header>
{/* <div className="code-challenge-problem-nav">
<h3 className="problem-number">1. {problems["Q.1"].title}</h3>
</div> */}
<div className="code-challenge-main">
<div className="problem-tabs">
<button
className={activeQuestion === "Q.1" ? "tab-active" : ""}
onClick={() => setActiveQuestion("Q.1")}
>
Q.1
</button>
<button
className={activeQuestion === "Q.2" ? "tab-active" : ""}
onClick={() => setActiveQuestion("Q.2")}
>
Q.2
</button>
<button
className={activeQuestion === "Q.3" ? "tab-active" : ""}
onClick={() => setActiveQuestion("Q.3")}
>
Q.3
</button>
</div>
<div className="problem-content">
{renderProblem()}
</div>
<div className="editor-section">
<div className="editor-header">
<div className="editor-controls">
<select
value={language}
onChange={(e) => setLanguage(e.target.value)}
className="language-selector"
>
<option value="JavaScript">JavaScript</option>
<option value="Python">Python</option>
<option value="Java">Java</option>
<option value="C++">C++</option>
<option value="C">C</option>
</select>
</div>
<div className="editor-actions">
<button
className="run-btn"
onClick={runCode}
disabled={isRunning}
title={isRunning ? "Code execution in progress..." : "Run code"}
>
{isRunning ? (
<>
<span className="loading-spinner"></span> Running...
</>
) : (
<>
<Play size={16} /> Run
</>
)}
</button>
<button
className="submit-btn"
onClick={submitCode}
disabled={isRunning}
title={isRunning ? "Code execution in progress..." : "Submit solution"}
>
{isRunning ? (
<>
<span className="loading-spinner"></span> Submitting...
</>
) : (
<>
<Send size={16} /> Submit
</>
)}
</button>
</div>
</div>
<div className="editor-container">
<Editor
height="100%"
defaultLanguage="python"
language={language.toLowerCase() === 'c++' ? 'cpp' : language.toLowerCase()}
value={code}
onChange={(value) => setCode(value)}
theme="hc-black"
options={{
fontSize: 14,
minimap: { enabled: false },
scrollBeyondLastLine: false,
automaticLayout: true,
}}
/>
</div>
</div>
</div>
<div className="terminal-section">
<div className="terminal-header">
<span>Terminal</span>
{/* <div className="terminal-controls">
<button className="terminal-btn">⊞</button>
<button className="terminal-btn">□</button>
<button className="terminal-btn">✕</button>
</div> */}
</div>
<div className="terminal-content">
{terminalOutput.map((line, index) => (
<div
key={index}
className={`terminal-line ${line.type}`}
>
{line.content}
</div>
))}
<div className="terminal-prompt">
<span className="prompt-symbol">$</span>
<input
type="text"
className="terminal-input"
placeholder="Type here..."
disabled={!isRunning}
// Update the ref callback
ref={(inputEl) => {
// Auto-focus input when isRunning changes to true
if (inputEl && isRunning) {
inputEl.focus();
// Clear any previous input
inputEl.value = '';
}
}}
onKeyDown={(e) => { // Change from onKeyPress to onKeyDown for better cross-browser support
if (e.key === 'Enter') {
e.preventDefault(); // Prevent default to avoid form submissions
const input = e.target.value.trim();
if (!input) return; // Skip empty input
if (activeSocket && activeSocket.readyState === WebSocket.OPEN) {
try {
// Send input to server
activeSocket.send(JSON.stringify({
"type": "input",
"content": input
}));
// Add input to terminal output
setTerminalOutput(prev => [
...prev,
{ type: 'system', content: `$ ${input}` }
]);
// Clear the input field
e.target.value = '';
} catch (error) {
console.error("Error sending input:", error);
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: `Failed to send input: ${error.message}` }
]);
}
} else {
// Better error message with socket state information
const socketState = activeSocket ?
['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'][activeSocket.readyState] :
'NO_SOCKET';
console.log(`Cannot send input: Socket state is ${socketState}`);
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: `Cannot send input: connection not available (${socketState})` }
]);
}
}
}}
onKeyPress={(e) => {
if (e.key === 'Enter' && activeSocket && activeSocket.readyState === WebSocket.OPEN) {
const input = e.target.value;
// Send input to WebSocket with the correct format
try {
activeSocket.send(JSON.stringify({
"type": "input",
"content": input
}));
// Add input to terminal output
setTerminalOutput(prev => [
...prev,
{ type: 'system', content: `$ ${input}` }
]);
// Clear the input field
e.target.value = '';
} catch (error) {
console.error("Error sending input:", error);
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: `Failed to send input: ${error.message}` }
]);
}
} else if (e.key === 'Enter') {
// Inform user if socket isn't available
if (!activeSocket || activeSocket.readyState !== WebSocket.OPEN) {
setTerminalOutput(prev => [
...prev,
{ type: 'error', content: `Cannot send input: connection closed` }
]);
}
}
}}
/>
</div>
</div>
</div>
</div>
);
};
export default CodeChallenge;

View File

@@ -8,7 +8,32 @@ import {
} from "lucide-react";
import Sidebar from "./Sidebar";
import Panel from "./Panel"; // Import Panel component
import WebSocketTerminal from "./WebSocketTerminal"; // Import WebSocket Terminal
// Add this function to map file extensions to language identifiers
const getLanguageFromExtension = (extension) => {
const extensionMap = {
'js': 'javascript',
'jsx': 'javascript',
'ts': 'typescript',
'tsx': 'typescript',
'py': 'python',
'java': 'java',
'c': 'c',
'cpp': 'cpp',
'h': 'c',
'hpp': 'cpp',
'cs': 'csharp',
'go': 'go',
'rb': 'ruby',
'php': 'php',
'html': 'html',
'css': 'css',
'json': 'json',
'md': 'markdown'
};
return extensionMap[extension] || 'text';
};
const EditorArea = ({
sidebarVisible = true,
@@ -63,12 +88,8 @@ const EditorArea = ({
// Add a new state for user input
const [userInput, setUserInput] = useState("");
// Add a new state for waiting for input
const [waitingForInput, setWaitingForInput] = useState(false);
// Add 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);
// Add socket state to track the connection
const [activeSocket, setActiveSocket] = useState(null);
// Focus the input when new file modal opens
useEffect(() => {
@@ -115,11 +136,6 @@ const EditorArea = ({
useEffect(() => {
localStorage.setItem("vscode-clone-files", JSON.stringify(files));
}, [files]);
useEffect(() => {
localStorage.setItem("vscode-clone-structure", JSON.stringify(fileStructure));
}, [fileStructure]);
// Add this effect to handle editor resize when sidebar changes
useEffect(() => {
// Force editor to readjust layout when sidebar visibility changes
@@ -137,6 +153,41 @@ const EditorArea = ({
}
}, [panelVisible]);
// Add this useEffect for cleanup
useEffect(() => {
// Cleanup function to close socket when component unmounts
return () => {
if (activeSocket) {
activeSocket.close();
}
};
}, []);
// Add interval to poll execution status
useEffect(() => {
const checkInterval = setInterval(() => {
// Poll execution status
if (activeSocket && activeRunningFile) {
// Check if socket is still connected
if (activeSocket.readyState !== WebSocket.OPEN) {
console.warn("Socket not in OPEN state:", activeSocket.readyState);
setTerminalOutput(prev => [...prev, {
type: 'warning',
content: `Terminal connection lost, attempting to reconnect...`
}]);
// Could implement reconnection logic here
}
}
}, 5000);
// Clean up interval when component unmounts
return () => {
if (checkInterval) {
clearInterval(checkInterval);
}
};
}, [activeSocket, activeRunningFile]);
const handleEditorDidMount = (editor) => {
editorRef.current = editor;
};
@@ -484,21 +535,31 @@ const EditorArea = ({
case "README.md":
return `# VS Code Clone Project
## Overview
This is a simple VS Code clone built with React and Monaco Editor.
## Authors
- Arnab Bhowmik
- Ishika Bhoyar
## Features
- File tree navigation
- Tab management
- Code editing with Monaco Editor
- Syntax highlighting
## Description
This project is a VS Code Clone built with React and Monaco Editor. It features a file tree navigation, tab management, code editing with syntax highlighting, and a terminal panel for running code. It mimics the core functionalities of Visual Studio Code in a browser-based environment.
## Frontend Functionalities
- Built with React and Monaco Editor.
- File tree navigation for managing files and folders.
- Tab management for opening multiple files simultaneously.
- Code editing with syntax highlighting and language support.
- Terminal panel for running code and viewing output.
- Persistent file structure and content using localStorage.
## Backend Functionalities
- Built with Go and Docker for secure code execution.
- Supports multiple programming languages (Python, Java, C/C++).
- Executes code in isolated Docker containers with resource limits.
- RESTful API for submitting code, checking status, and retrieving results.
- Job queue system for managing concurrent executions.
- Enforces timeouts and resource limits for security and performance.
`;
## Getting Started
1. Create a new file using the + button in the sidebar
2. Edit your code in the editor
3. Save changes using the save button
Happy coding!`;
default:
return "";
}
@@ -512,7 +573,7 @@ Happy coding!`;
width: `calc(100% - ${sidebarVisible ? sidebarWidth : 0}px)`
};
// Modified handleRunCode to start execution immediately
// Update the handleRunCode function
const handleRunCode = async () => {
if (!activeFile) return;
@@ -522,47 +583,36 @@ Happy coding!`;
setPanelVisible(true);
}
// Reset states
setIsRunning(true);
setWaitingForInput(false);
setActiveRunningFile(activeFile.id);
setActiveRunningSubmissionId(null);
setUserInput('');
// Get language from file extension
// Clear previous output and add new command
const fileExtension = activeFile.id.split('.').pop().toLowerCase();
const language = getLanguageFromExtension(fileExtension);
// If using WebSocket mode, we'll use the WebSocketTerminal component
if (useWebSocket) {
// Just set the running state, the WebSocketTerminal will handle the rest
return;
}
// Regular HTTP mode - use polling
// Clear previous output and add new command
const newOutput = [
{ type: 'command', content: `$ run ${activeFile.id}` },
{ type: 'output', content: '------- PROGRAM EXECUTION -------' },
{ type: 'output', content: `Language: ${language}` },
{ type: 'output', content: 'Executing code...' }
{ type: 'output', content: 'Submitting code...' }
];
setTerminalOutput(newOutput);
// Use API URL from environment variable
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
try {
// Submit the code for execution immediately
const submitResponse = await fetch(`${apiUrl}/submit`, {
// Close any existing socket
if (activeSocket) {
activeSocket.close();
setActiveSocket(null);
}
// Use API URL from environment variable
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
// Submit the code to get an execution ID
const submitResponse = await fetch(`${apiUrl}/api/submit`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
language: getLanguageFromExtension(fileExtension),
language: language,
code: activeFile.content,
input: '' // No initial input
input: "" // Explicitly passing empty input, no user input handling
}),
});
@@ -571,263 +621,272 @@ Happy coding!`;
}
const { id } = await submitResponse.json();
setActiveRunningSubmissionId(id);
setTerminalOutput(prev => [...prev, { type: 'output', content: `Job submitted with ID: ${id}` }]);
// Start polling for status and output
pollForStatusAndOutput(id);
} catch (error) {
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
setIsRunning(false);
setActiveRunningSubmissionId(null);
}
};
// Set active running file
setActiveRunningFile(activeFile.id);
// Toggle between WebSocket and HTTP modes
const toggleWebSocketMode = () => {
setUseWebSocket(!useWebSocket);
};
// Connect to WebSocket with the execution ID
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsBaseUrl = apiUrl.replace(/^https?:\/\//, '');
const wsUrl = `${wsProtocol}//${wsBaseUrl}/api/ws/terminal/${id}`;
// Simplified handleInputSubmit to only handle interactive input
const handleInputSubmit = async () => {
if (!waitingForInput || !activeRunningSubmissionId) return;
setTerminalOutput(prev => [...prev, { type: 'output', content: `Connecting to: ${wsUrl}` }]);
// Store the input value before clearing it
const inputValue = userInput;
// Create a new WebSocket
const newSocket = new WebSocket(wsUrl);
// Clear the input field and reset waiting state immediately for better UX
setUserInput('');
setWaitingForInput(false);
// Set up event handlers
newSocket.onopen = () => {
console.log("WebSocket connected");
setTerminalOutput(prev => [...prev, { type: 'output', content: 'Connected to execution terminal' }]);
setIsRunning(true);
};
// Add the input to the terminal immediately
setTerminalOutput(prev => [
...prev,
{ type: 'input', content: inputValue }
]);
newSocket.onmessage = (event) => {
console.log("WebSocket message received:", event.data);
// Use API URL from environment variable
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
try {
const message = JSON.parse(event.data);
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
}),
});
// Handle different message types
switch (message.type) {
case 'output':
setTerminalOutput(prev => [...prev, {
type: 'output',
content: message.content.text,
isError: message.content.isError
}]);
break;
if (!submitInputResponse.ok) {
throw new Error(`Server error: ${submitInputResponse.status}`);
}
case 'status':
const status = message.content.status;
setTerminalOutput(prev => [...prev, {
type: 'status',
content: `Status: ${status}`
}]);
// Wait for a moment to allow the program to process the input
await new Promise(resolve => setTimeout(resolve, 500));
// Update running state based on status
if (status === 'completed' || status === 'failed') {
// Don't immediately set isRunning to false - we'll wait for the socket to close or delay
}
break;
// Poll for status and check if we need more input
pollForStatusAndOutput(activeRunningSubmissionId);
case 'system':
setTerminalOutput(prev => [...prev, {
type: 'system',
content: message.content
}]);
break;
} catch (error) {
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
setIsRunning(false);
setActiveRunningSubmissionId(null);
}
};
case 'error':
setTerminalOutput(prev => [...prev, {
type: 'error',
content: `Error: ${message.content.message}`
}]);
break;
// Add a function to poll for status and output
const pollForStatusAndOutput = async (id) => {
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
default:
// For raw or unknown messages
setTerminalOutput(prev => [...prev, {
type: 'output',
content: event.data
}]);
}
try {
// Step 2: Poll for status until completed, failed, or waiting_for_input
let status = 'pending';
while (status !== 'completed' && status !== 'failed' && status !== 'waiting_for_input') {
// Add a small delay between polls
await new Promise(resolve => setTimeout(resolve, 1000));
// Check if this message is likely asking for input (prompt detection)
if (message.type === 'output' && !message.content.isError &&
(message.content.text.includes("?") ||
message.content.text.endsWith(":") ||
message.content.text.endsWith("> "))) {
console.log("Input prompt detected, focusing terminal");
// Force terminal to focus after a prompt is detected
setTimeout(() => {
document.querySelector('.panel-terminal')?.focus();
}, 100);
}
const statusResponse = await fetch(`${apiUrl}/status?id=${id}`);
if (!statusResponse.ok) {
throw new Error(`Status check failed: ${statusResponse.status}`);
} catch (err) {
// Handle case where message isn't valid JSON
console.warn("Failed to parse WebSocket message:", err);
setTerminalOutput(prev => [...prev, {
type: 'output',
content: event.data
}]);
}
};
const statusData = await statusResponse.json();
status = statusData.status;
// Add polling for job status
let statusCheckInterval;
if (id) {
// Start polling the status endpoint every 2 seconds
statusCheckInterval = setInterval(async () => {
try {
const statusResponse = await fetch(`${apiUrl}/api/status/${id}`);
if (statusResponse.ok) {
const statusData = await statusResponse.json();
// Update terminal with status (for any status type)
setTerminalOutput(prev => {
// Update the last status message or add a new one
const hasStatus = prev.some(line => line.content.includes('Status:'));
if (hasStatus) {
return prev.map(line =>
line.content.includes('Status:')
? { ...line, content: `Status: ${status}` }
: line
);
} else {
return [...prev, { type: 'output', content: `Status: ${status}` }];
// If the process is completed or failed, stop polling and update UI
if (statusData.status === 'completed' || statusData.status === 'failed') {
clearInterval(statusCheckInterval);
console.log("Process status:", statusData.status);
// Update the UI to show process is no longer running
setIsRunning(false);
// Display the final result if WebSocket didn't capture it
if (statusData.output && statusData.output.length > 0) {
setTerminalOutput(prev => {
// Check if the output is already in the terminal
const lastOutput = prev[prev.length - 1]?.content || "";
if (!lastOutput.includes(statusData.output)) {
return [...prev, {
type: 'output',
content: `\n[System] Final output:\n${statusData.output}`
}];
}
return prev;
});
}
// Close socket if it's still open
if (newSocket && newSocket.readyState === WebSocket.OPEN) {
newSocket.close();
}
}
}
} catch (error) {
console.error("Status check error:", error);
}
}, 2000);
// Clean up interval when component unmounts or when socket closes
newSocket.addEventListener('close', () => {
if (statusCheckInterval) {
clearInterval(statusCheckInterval);
}
});
}
// 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();
newSocket.onclose = (event) => {
console.log("WebSocket closed:", event);
// Process the output to show what's happened so far
const outputLines = [];
let promptText = '';
const reason = event.reason ? `: ${event.reason}` : '';
const code = event.code ? ` (code: ${event.code})` : '';
// Split by lines and process each line
const lines = output.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Don't mark as not running if this is expected close (after execution completes)
// Code 1000 is normal closure, 1005 is no status code
const isExpectedClose = event.code === 1000 || event.code === 1005;
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];
// Only set running to false if it wasn't an expected close
if (!isExpectedClose) {
setIsRunning(false);
// Add a graceful reconnection message
setTerminalOutput(prev => [...prev, {
type: 'warning',
content: `Terminal connection closed${reason}${code}`
}]);
// Attempt reconnection for certain close codes (unexpected closes)
if (activeRunningFile && event.code !== 1000) {
setTerminalOutput(prev => [...prev, {
type: 'info',
content: `Attempting to reconnect...`
}]);
// Reconnection delay
setTimeout(() => {
// Attempt to reconnect for the same file
if (activeRunningFile) {
console.log("Attempting to reconnect for", activeRunningFile);
// You could call your run function here again
}
continue;
} else if (line.trim() !== '') {
// This is a regular output line
outputLines.push({
type: 'output',
content: line
});
}
}, 3000);
}
// Update the terminal with the current output
if (outputLines.length > 0) {
setTerminalOutput(prev => {
// Keep only the essential lines to avoid duplication
const essentialLines = prev.filter(line =>
line.type === 'command' ||
line.content.includes('PROGRAM EXECUTION') ||
line.content.includes('Language:') ||
line.content.includes('Job submitted') ||
line.content.includes('Status:') ||
line.content === 'Executing code...'
);
return [...essentialLines, ...outputLines];
});
}
// Now set the waiting for input state
setWaitingForInput(true);
// Add a message indicating we're waiting for input
setTerminalOutput(prev => {
// Remove any existing waiting message
const filteredPrev = prev.filter(line =>
line.content !== 'Waiting for input...'
);
// Add the prompt text if available
if (promptText) {
return [...filteredPrev, {
type: 'prompt',
content: promptText
}, {
type: 'output',
content: 'Waiting for input...'
}];
} else {
return [...filteredPrev, {
type: 'output',
content: 'Waiting for input...'
}];
}
});
}
return;
}
// Get the result for both completed and failed status
const resultResponse = await fetch(`${apiUrl}/result?id=${id}`);
if (!resultResponse.ok) {
throw new Error(`Result fetch failed: ${resultResponse.status}`);
}
setActiveSocket(null);
const { output } = await resultResponse.json();
// Format and display output
const outputLines = [];
// Add a header
outputLines.push({
type: status === 'failed' ? 'warning' : 'output',
content: status === 'failed'
? '------- EXECUTION FAILED -------'
: '------- EXECUTION RESULT -------'
});
// Process the output line by line
output.split('\n').forEach(line => {
// Check if this is an input line
if (line.startsWith('[Input] ')) {
outputLines.push({
type: 'input',
content: line.substring(8) // Remove the '[Input] ' prefix
});
} else {
outputLines.push({
type: status === 'failed' ? 'warning' : 'output',
content: line
});
// Clean up interval
if (statusCheckInterval) {
clearInterval(statusCheckInterval);
}
});
};
setTerminalOutput(prev => [
...prev,
...outputLines
]);
newSocket.onerror = (event) => {
console.error("WebSocket error:", event);
setTerminalOutput(prev => [...prev, {
type: 'warning',
content: `WebSocket error occurred`
}]);
};
if (status === 'failed') {
console.error('Code execution failed:', output);
}
// Reset state
setIsRunning(false);
setWaitingForInput(false);
setActiveRunningSubmissionId(null);
// Set the active socket after all handlers are defined
setActiveSocket(newSocket);
} catch (error) {
console.error("Run code error:", error);
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
setIsRunning(false);
setWaitingForInput(false);
setActiveRunningSubmissionId(null);
// Also add cleanup in the error handler
if (statusCheckInterval) {
clearInterval(statusCheckInterval);
}
}
};
// Helper function to convert file extension to language identifier for API
const getLanguageFromExtension = (extension) => {
const languageMap = {
'java': 'java',
'c': 'c',
'cpp': 'cpp',
'py': 'python',
'js': 'javascript',
'jsx': 'javascript',
'ts': 'typescript',
'tsx': 'typescript'
};
// Update handleInputSubmit to ensure the input is sent properly
const handleInputSubmit = (input) => {
// Use the direct input parameter instead of relying on userInput state
const textToSend = input || userInput;
return languageMap[extension] || extension;
console.log("Input submit called, active socket state:",
activeSocket ? activeSocket.readyState : "no socket",
"input:", textToSend);
if (!activeSocket) {
console.warn("Cannot send input: No active socket");
setTerminalOutput(prev => [...prev, {
type: 'warning',
content: `Cannot send input: No active connection`
}]);
return;
}
if (activeSocket.readyState !== WebSocket.OPEN) {
console.warn("Socket not in OPEN state:", activeSocket.readyState);
setTerminalOutput(prev => [...prev, {
type: 'warning',
content: `Cannot send input: Connection not open (state: ${activeSocket.readyState})`
}]);
return;
}
try {
// Add the input to the terminal display
setTerminalOutput(prev => [...prev, { type: 'command', content: `> ${textToSend}` }]);
// Send the input via WebSocket
console.log("Sending input:", textToSend);
// Instead of just sending the raw input, send a formatted input message
// This helps the backend identify it as user input rather than a command
activeSocket.send(JSON.stringify({
type: "input",
content: textToSend
}));
// Clear the input field
setUserInput("");
} catch (error) {
console.error("Error sending input:", error);
setTerminalOutput(prev => [...prev, {
type: 'warning',
content: `Error sending input: ${error.message}`
}]);
}
};
// Update this function to also update parent state
@@ -944,38 +1003,15 @@ Happy coding!`;
title="Run code"
>
{isRunning ? <Loader size={16} className="animate-spin" /> : <Play size={16} />}
</button>
<button
className="terminal-toggle-button"
onClick={togglePanel}
onClick={togglePanel} // Use the new function
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>
@@ -1034,28 +1070,17 @@ 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}
userInput={userInput}
onUserInputChange={setUserInput}
onInputSubmit={handleInputSubmit}
/>
)}
<Panel
height={panelHeight}
terminalOutput={terminalOutput}
isRunning={isRunning}
activeRunningFile={activeRunningFile}
initialTab="terminal"
onClose={togglePanel}
userInput={userInput}
onUserInputChange={setUserInput}
onInputSubmit={handleInputSubmit}
/>
</>
)}

View File

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

View File

@@ -1,6 +1,5 @@
import React from "react";
import { useState, useEffect } from "react";
import { X } from "lucide-react";
import React, { useState, useEffect, useRef } from "react";
import { X, Maximize2, ChevronDown, Plus } from "lucide-react";
const Panel = ({
height,
@@ -12,96 +11,135 @@ const Panel = ({
onClose,
userInput = "",
onUserInputChange,
onInputSubmit
onInputSubmit,
}) => {
const [activeTab, setActiveTab] = useState(initialTab);
const terminalRef = useRef(null);
const [inputBuffer, setInputBuffer] = useState("");
// Set active tab when initialTab changes
// Update active tab when initialTab changes
useEffect(() => {
setActiveTab(initialTab);
}, [initialTab]);
const renderTerminal = () => {
return (
<div className="panel-terminal">
{terminalOutput.length > 0 ? (
// Render output from EditorArea when available
<>
{terminalOutput.map((line, index) => (
<div key={index} className={`terminal-line ${line.type === 'warning' ? 'terminal-warning' : 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">&gt;</span> : ''}
// Auto-scroll terminal to the bottom when content changes
useEffect(() => {
if (terminalRef.current) {
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
}
}, [terminalOutput]);
// Handle keyboard input for the terminal
useEffect(() => {
const handleKeyDown = (e) => {
if (!isRunning) return;
if (e.key === "Enter") {
if (inputBuffer.trim() && onInputSubmit) {
e.preventDefault();
// Update parent's userInput state directly and call submit in the same function
// instead of using setTimeout which creates a race condition
onUserInputChange(inputBuffer);
onInputSubmit(inputBuffer); // Pass inputBuffer directly to avoid race condition
setInputBuffer("");
}
} else if (e.key === "Backspace") {
setInputBuffer((prev) => prev.slice(0, -1));
} else if (e.key.length === 1) {
setInputBuffer((prev) => prev + e.key);
}
};
const terminalElement = terminalRef.current;
terminalElement?.addEventListener("keydown", handleKeyDown);
return () => {
terminalElement?.removeEventListener("keydown", handleKeyDown);
};
}, [isRunning, inputBuffer, onInputSubmit, onUserInputChange]);
// Render the terminal tab
const renderTerminal = () => (
<div
className="panel-terminal"
ref={terminalRef}
tabIndex={0} // Make div focusable
onClick={() => terminalRef.current?.focus()} // Focus when clicked
>
{terminalOutput.length > 0 ? (
<>
{terminalOutput.map((line, index) => {
const typeClass =
line.type === "warning"
? "terminal-warning"
: line.type === "error"
? "terminal-error"
: "terminal-output";
return (
<div key={index} className={`terminal-line ${typeClass}`}>
{line.timestamp && (
<span className="terminal-timestamp">{line.timestamp} </span>
)}
{line.type === "command" && <span className="terminal-prompt">$</span>}
{line.content}
</div>
))}
{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">&gt;</div>
<input
type="text"
className="terminal-input"
value={userInput}
onChange={(e) => onUserInputChange && onUserInputChange(e.target.value)}
placeholder="Enter input for your program here..."
onKeyDown={(e) => {
if (e.key === 'Enter' && onInputSubmit) {
onInputSubmit();
}
}}
autoFocus
/>
</div>
<div className="terminal-input-help">
Press Enter to submit input
</div>
</div>
)}
</>
) : (
// Default terminal content when no output
<>
<div className="terminal-line">
<span className="terminal-prompt">$</span> npm start
);
})}
{isRunning && (
<div className="terminal-line terminal-input-line">
<span className="terminal-prompt">$</span> {inputBuffer}
<span className="terminal-cursor"></span>
</div>
<div className="terminal-line terminal-output">Starting the development server...</div>
<div className="terminal-line terminal-output">Compiled successfully!</div>
<div className="terminal-line terminal-output">You can now view vscode-clone in the browser.</div>
<div className="terminal-line terminal-output">Local: http://localhost:3000</div>
<div className="terminal-line terminal-output">On Your Network: http://192.168.1.5:3000</div>
<div className="terminal-line">
<span className="terminal-prompt">$</span>
</div>
</>
)}
</div>
);
};
)}
</>
) : (
<div className="terminal-line">
<span className="terminal-prompt">$</span>
<span className="terminal-cursor"></span>
</div>
)}
</div>
);
const renderProblems = () => {
return (
<div className="panel-problems">
<div className="panel-empty-message">No problems have been detected in the workspace.</div>
</div>
);
};
// Render other tabs
const renderProblems = () => (
<div className="panel-problems">
<div className="panel-empty-message">No problems have been detected in the workspace.</div>
</div>
);
const renderOutput = () => {
return (
<div className="panel-output">
<div className="output-line">[Extension Host] Extension host started.</div>
<div className="output-line">[Language Server] Language server started.</div>
{activeRunningFile && (
<div className="output-line">[Running] {activeRunningFile}</div>
)}
</div>
);
};
const renderOutput = () => (
<div className="panel-output">
<div className="output-line">[Extension Host] Extension host started.</div>
<div className="output-line">[Language Server] Language server started.</div>
{activeRunningFile && (
<div className="output-line">[Running] {activeRunningFile}</div>
)}
</div>
);
const renderDebugConsole = () => (
<div className="panel-debug-console">
<div className="debug-line">Debug session not yet started.</div>
<div className="debug-line">Press F5 to start debugging.</div>
</div>
);
const renderPorts = () => (
<div className="panel-ports">
<div className="ports-line">No forwarded ports detected.</div>
</div>
);
const renderComments = () => (
<div className="panel-comments">
<div className="comments-line">No comments have been added to this workspace.</div>
</div>
);
// Get content for the active tab
const getTabContent = () => {
switch (activeTab) {
case "terminal":
@@ -110,6 +148,12 @@ const Panel = ({
return renderProblems();
case "output":
return renderOutput();
case "debug":
return renderDebugConsole();
case "ports":
return renderPorts();
case "comments":
return renderComments();
default:
return <div>Unknown tab</div>;
}
@@ -118,76 +162,29 @@ const Panel = ({
return (
<div className="panel" style={{ height: `${height}px` }}>
<div className="panel-tabs">
<div
className={`panel-tab ${activeTab === "problems" ? "active" : ""}`}
onClick={() => setActiveTab("problems")}
>
<span className="tab-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
</svg>
</span>
<span className="tab-name">Problems</span>
</div>
<div className={`panel-tab ${activeTab === "output" ? "active" : ""}`} onClick={() => setActiveTab("output")}>
<span className="tab-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>
</span>
<span className="tab-name">Output</span>
</div>
<div
className={`panel-tab ${activeTab === "terminal" ? "active" : ""}`}
onClick={() => setActiveTab("terminal")}
>
<span className="tab-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="4 17 10 11 4 5"></polyline>
<line x1="12" y1="19" x2="20" y2="19"></line>
</svg>
</span>
<span className="tab-name">Terminal</span>
</div>
{["problems", "output", "debug", "terminal", "ports", "comments"].map((tab) => (
<div
key={tab}
className={`panel-tab ${activeTab === tab ? "active" : ""}`}
onClick={() => setActiveTab(tab)}
>
<span className="tab-name">{tab.toUpperCase()}</span>
</div>
))}
{/* Add close button */}
<div className="panel-actions">
{/* <button className="panel-action-btn">
<span className="current-terminal">node - frontend</span>
<ChevronDown size={16} />
</button>
<button className="panel-action-btn">
<Plus size={16} />
</button>
<button className="panel-action-btn">
<Maximize2 size={16} />
</button> */}
<button className="panel-close-btn" onClick={onClose}>
<X size={14} />
<X size={16} />
</button>
</div>
</div>
@@ -198,4 +195,3 @@ const Panel = ({
};
export default Panel;

View File

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

View File

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

View File

@@ -1,239 +0,0 @@
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">&gt; </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;

View File

@@ -1,17 +1,17 @@
:root {
--vscode-background: #1e1e1e;
--vscode-background: #000000;
--vscode-foreground: #d4d4d4;
--vscode-activityBar-background: #333333;
--vscode-activityBar-foreground: #ffffff;
--vscode-activityBar-inactiveForeground: #ffffff80;
--vscode-sideBar-background: #252526;
--vscode-sideBar-foreground: #cccccc;
--vscode-editor-background: #1e1e1e;
--vscode-editor-background: #000000;
--vscode-statusBar-background: #007acc;
--vscode-statusBar-foreground: #ffffff;
--vscode-panel-background: #1e1e1e;
--vscode-panel-background: #000000;
--vscode-panel-border: #80808059;
--vscode-tab-activeBackground: #1e1e1e;
--vscode-tab-activeBackground: #000000;
--vscode-tab-inactiveBackground: #2d2d2d;
--vscode-tab-activeForeground: #ffffff;
--vscode-tab-inactiveForeground: #ffffff80;
@@ -57,12 +57,12 @@ body {
display: flex;
flex-direction: column;
background-color: var(--vscode-activityBar-background);
z-index: 10;
width: 50px;
height: 100%;
position: fixed; /* Change to fixed to avoid layout issues */
z-index: 10; /* Lower z-index than the StatusBar */
position: fixed;
top: 0;
left: 0;
height: calc(100% - 22px); /* Subtract the height of the StatusBar */
width: 50px;
}
.activity-bar button {
@@ -404,7 +404,7 @@ body {
flex: 1;
overflow: auto;
font-family: "Consolas", "Courier New", monospace;
font-size: 13px;
font-size: 10px;
padding: 8px;
padding: 10px;
font-family: 'Consolas', 'Courier New', monospace;
@@ -418,54 +418,59 @@ body {
height: 100%;
}
.panel-terminal {
padding: 8px;
font-family: monospace;
overflow-y: auto;
overflow-x: hidden;
height: calc(100% - 36px); /* Adjust based on your header height */
background-color: #1e1e1e;
color: #ddd;
outline: none; /* Remove focus outline */
scrollbar-width: thin;
scrollbar-color: #555555 #1e1e1e;
}
.panel-terminal::-webkit-scrollbar {
width: 8px;
}
.panel-terminal::-webkit-scrollbar-track {
background: #1e1e1e;
}
.panel-terminal::-webkit-scrollbar-thumb {
background: #555555;
border-radius: 4px;
}
.panel-terminal::-webkit-scrollbar-thumb:hover {
background: #666666;
}
.panel-terminal .terminal-line {
white-space: pre-wrap;
margin-bottom: 3px;
word-wrap: break-word; /* Ensure long words don't cause horizontal overflow */
}
.terminal-line {
white-space: pre-wrap;
line-height: 1.5;
margin-bottom: 2px;
}
.terminal-prompt {
color: #0f0;
color: #0a84ff;
margin-right: 8px;
color: #569cd6;
margin-right: 6px;
}
.terminal-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;
color: #ddd;
}
.terminal-warning {
color: #ddb100;
color: #ffa500;
}
.output-line {
@@ -487,9 +492,8 @@ body {
}
@keyframes blink {
50% {
opacity: 0;
}
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
.panel-empty-message {
@@ -886,7 +890,7 @@ body {
margin-right: 4px;
}
.terminal-toggle-button, .websocket-toggle-button {
.terminal-toggle-button {
background-color: transparent;
border: none;
color: #cccccc;
@@ -897,12 +901,7 @@ body {
align-items: center;
}
.terminal-toggle-button:hover, .websocket-toggle-button:hover {
opacity: 1;
}
.websocket-toggle-button.active {
color: #4ec9b0;
.terminal-toggle-button:hover {
opacity: 1;
}
@@ -954,159 +953,18 @@ body {
}
.terminal-input {
background-color: rgba(78, 201, 176, 0.1);
border: 1px solid rgba(78, 201, 176, 0.3);
border-radius: 3px;
color: #4ec9b0;
background-color: transparent;
border: none;
color: inherit;
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 {
@@ -1134,9 +992,8 @@ body {
}
@keyframes blink {
50% {
opacity: 0;
}
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
/* Make sure the monaco container adjusts when terminal is shown */
@@ -1190,3 +1047,408 @@ body {
.panel-close-btn:hover {
opacity: 1;
}
/* Code Challenge Component Styles */
.code-challenge-container {
display: flex;
flex-direction: column;
height: 100vh;
background-color: var(--vscode-background);
color: var(--vscode-foreground);
}
.code-challenge-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
background-color: var(--vscode-background);
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
}
.code-challenge-header h1 {
margin: 0;
font-size: 25px;
font-weight: 400;
font-weight: bold;
}
.sign-in-btn {
background-color: transparent;
color: var(--vscode-foreground);
border: 1px solid rgba(128, 128, 128, 0.5);
padding: 4px 12px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
}
.sign-in-btn:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.code-challenge-problem-nav {
padding: 8px 16px;
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
}
.problem-number {
margin: 0;
font-size: 16px;
font-weight: 400;
}
.code-challenge-main {
display: flex;
height: 60vh;
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
}
.problem-tabs {
display: flex;
flex-direction: column;
width: 80px;
border-right: 1px solid rgba(128, 128, 128, 0.35);
}
.problem-tabs button {
padding: 16px;
background-color: transparent;
color: var(--vscode-foreground);
border: none;
text-align: left;
cursor: pointer;
border-bottom: 1px solid rgba(128, 128, 128, 0.2);
}
.problem-tabs button.tab-active {
background-color: var(--vscode-background);
font-weight: 500;
border-left: 2px solid #007acc;
}
.problem-tabs button:hover:not(.tab-active) {
background-color: rgba(255, 255, 255, 0.05);
}
.problem-content {
flex: 1;
overflow-y: auto;
padding: 16px;
border-right: 1px solid rgba(128, 128, 128, 0.35);
width: 50%;
}
.problem-container h1 {
margin-top: 0;
font-size: 22px;
margin-bottom: 16px;
}
.problem-description {
margin-bottom: 20px;
font-size: 14px;
line-height: 1.5;
}
.problem-examples h2 {
font-size: 16px;
margin-top: 24px;
margin-bottom: 12px;
}
.example-box {
background-color: rgba(0, 0, 0, 0.3);
padding: 12px;
border-radius: 6px;
margin-bottom: 12px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
line-height: 1.5;
}
.editor-section {
width: 50%;
display: flex;
flex-direction: column;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background-color: #252526;
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
}
.editor-controls {
display: flex;
align-items: center;
}
.language-selector {
background-color: #3c3c3c;
color: #d4d4d4;
border: 1px solid #3c3c3c;
border-radius: 4px;
padding: 4px 8px;
font-size: 13px;
margin-right: 8px;
}
.auto-btn {
background-color: #3c3c3c;
color: #d4d4d4;
border: none;
border-radius: 4px;
padding: 4px 12px;
font-size: 13px;
cursor: pointer;
}
.auto-selected {
background-color: #4d4d4d;
}
.editor-actions {
display: flex;
gap: 8px;
}
.run-btn {
display: flex;
align-items: center;
gap: 4px;
background-color: #3c3c3c;
color: #d4d4d4;
border: none;
border-radius: 4px;
padding: 4px 12px;
font-size: 13px;
cursor: pointer;
}
.submit-btn {
display: flex;
align-items: center;
gap: 4px;
background-color: #0e639c;
color: #ffffff;
border: none;
border-radius: 4px;
padding: 4px 12px;
font-size: 13px;
cursor: pointer;
}
.run-btn:hover {
background-color: #4d4d4d;
}
.submit-btn:hover {
background-color: #1177bb;
}
.editor-container {
flex: 1;
}
.terminal-section {
flex: 1;
display: flex;
flex-direction: column;
background-color: var(--vscode-panel-background);
padding-bottom: 30px; /* Reduced padding to keep buttons just above footer */
}
.terminal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 12px;
background-color: #252526;
font-size: 13px;
}
.terminal-controls {
display: flex;
gap: 4px;
}
.terminal-btn {
background-color: transparent;
color: #d4d4d4;
border: none;
cursor: pointer;
font-size: 12px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.terminal-content {
flex: 1;
padding: 8px 12px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
overflow-y: auto;
overflow-x: hidden;
white-space: pre-wrap;
max-height: calc(100% - 10px); /* Ensure content doesn't expand beyond container */
scrollbar-width: thin;
scrollbar-color: #555555 #1e1e1e;
}
.terminal-content::-webkit-scrollbar {
width: 8px;
}
.terminal-content::-webkit-scrollbar-track {
background: #1e1e1e;
}
.terminal-content::-webkit-scrollbar-thumb {
background: #555555;
border-radius: 4px;
}
.terminal-content::-webkit-scrollbar-thumb:hover {
background: #666666;
}
.terminal-line {
margin-bottom: 4px;
line-height: 1.4;
}
.terminal-line.system {
color: #569cd6;
}
.terminal-line.error {
color: #f48771;
}
.terminal-prompt {
display: flex;
align-items: center;
margin-top: 8px;
}
.prompt-symbol {
color: #569cd6;
margin-right: 8px;
}
.terminal-input {
background-color: transparent;
color: #d4d4d4;
border: none;
outline: none;
flex: 1;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
}
/* Footer Styles */
.fixed {
position: fixed;
}
.bottom-0 {
bottom: 0;
}
.left-0 {
left: 0;
}
.right-0 {
right: 0;
}
.border-t {
border-top-width: 1px;
border-top-style: solid;
}
.border-slate-200\/40 {
border-color: rgba(226, 232, 240, 0.4);
}
.dark\:border-gray-800\/20 {
border-color: rgba(31, 41, 55, 0.2);
}
.bg-white {
background-color: #ffffff;
}
.dark\:bg-\[\#070c1f\] {
background-color: #000000;
}
.footer-bar {
background-color: #000000;
z-index: 1000;
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
}
.flex {
display: flex;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.h-7 {
height: 1.75rem;
}
.text-xs {
font-size: 0.75rem;
line-height: 1rem;
}
.text-slate-500 {
color: #64748b;
}
.dark\:text-gray-500 {
color: #6b7280;
}
.text-slate-400 {
color: #94a3b8;
}
.dark\:text-gray-400 {
color: #9ca3af;
}
.text-red-400 {
color: #f87171;
}
.dark\:text-red-500 {
color: #ef4444;
}
.mx-0\.5 {
margin-left: 0.125rem;
margin-right: 0.125rem;
}
/* Make sure the footer appears on top of other elements */
footer {
z-index: 1000;
}

317
Readme.md
View File

@@ -1,125 +1,240 @@
# Monaco Online Code Compiler
# Monaco Code Execution Engine
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.
Monaco is a secure, containerized code execution engine that allows you to run code in multiple programming languages through a simple REST API and WebSocket connections for real-time terminal interaction.
## Features
- **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
- **Multi-language support**: Run code in Python, Java, C, and C++
- **Secure execution**: All code runs in isolated Docker containers
- **Resource limits**: Memory, CPU, and file descriptor limits to prevent abuse
- **Concurrent processing**: Efficient job queue for handling multiple requests
- **Simple REST API**: Easy to integrate with any frontend
- **Interactive terminal**: Real-time code execution with input/output via WebSockets
- **VS Code-like interface**: Modern editor with syntax highlighting and file management
## Project Structure
## Architecture
- **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
Monaco consists of several components:
## Getting Started
### Backend Components
### Prerequisites
- **HTTP Handlers** (`handler/handler.go`): Processes API requests and WebSocket connections
- **Execution Service** (`service/execution.go`): Manages code execution in containers
- **Job Queue** (`queue/queue.go`): Handles concurrent execution of code submissions
- **Submission Model** (`model/submission.go`): Defines the data structure for code submissions
- Node.js 18+ for the frontend
- Go 1.22+ for the backend
- Docker for code execution
### Frontend Components
### Running the Frontend
- **Editor Area** (`EditorArea.jsx`): Main code editor with Monaco editor integration
- **Terminal Panel** (`Panel.jsx`): Interactive terminal for code execution and input
- **Sidebar** (`Sidebar.jsx`): File explorer and project structure navigation
- **Status Bar** (`StatusBar.jsx`): Information display and quick actions
### Communication Flow
1. Frontend submits code to backend via REST API
2. Backend assigns a unique ID and queues the execution
3. Frontend connects to WebSocket endpoint with the execution ID
4. Backend sends real-time execution output through WebSocket
5. Frontend can send user input back through WebSocket
6. Results are stored and retrievable via REST endpoints
## Requirements
- **Backend**:
- Go 1.22.3 or higher
- Docker
- Network connectivity for container image pulling
- **Frontend**:
- Node.js and npm/yarn
- Modern web browser
## Installation
### Backend Setup
1. Clone the repository:
```bash
git clone https://github.com/arnab-afk/monaco.git
cd monaco/backend
2.Install Go dependencies:
```bash
go mod download
```
3.Build the application:
```bash
go build -o monaco
```
4.Run the service
```bash
./monaco
```
The backend service will start on port 8080 by default.
### Frontend Setup
1. Navigate to the Frontend directory:
```bash
cd Frontend
```
2. Install dependencies:
```bash
npm install
```
3. Set up environment variables: Create a ```.env``` or ```.env.local.``` file with:
```bash
VITE_API_URL=http://localhost:8080
```
4. Start the development server:
```bash
npm run dev
```
The frontend will be available at http://localhost:5173
The frontend will be available at http://localhost:5173 by default.
### Running the Backend
### API Reference
```bash
cd backend
go build -o monaco ./cmd/server
./monaco
```
### REST Endpoints
```POST /submit```
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)
}
Submits code for execution
```json
{
"language": "python",
"code": "print('Hello, World!')",
"input": ""
}
```
## Security Considerations
Response:
```json
{
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1"
}
```
```GET /status?id={submissionId}```
Checks the status of submission:
```json
{
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1",
"status": "completed",
"queuedAt": "2025-03-25T14:30:00Z",
"startedAt": "2025-03-25T14:30:01Z",
"completedAt": "2025-03-25T14:30:02Z",
"executionTime": 1000
}
```
```GET /result?id={submissionId}```
Gets the execution result of a submission.
Response:
```json
{
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1",
"status": "completed",
"language": "python",
"output": "Hello, World!",
"queuedAt": "2025-03-25T14:30:00Z",
"startedAt": "2025-03-25T14:30:01Z",
"completedAt": "2025-03-25T14:30:02Z",
"executionTime": 1000,
"executionTimeFormatted": "1.0s",
"totalTime": 2000,
"totalTimeFormatted": "2.0s"
}
```
```GET /queue-stats```
Gets the statistics about the job queue.
Response:
```json
{
"queue_stats": {
"queue_length": 5,
"max_workers": 3,
"running_jobs": 3
},
"submissions": 42
}
```
### WebSocket Endpoints
```ws://localhost:8080/ws/terminal?id={submissionId}```
Establishes a real-time connection for terminal interaction.
- The server sends execution output as plain text messages.
- The client can send input as plain text messages (with newline).
- Connection automatically closes when execution completes or fails.
### Terminal Input Handling
The system supports interactive programs requiring user input:
1. The frontend detects possible input prompts by looking for patterns
2. When detected, it focuses the terminal and allows user input
3. User input is captured in the terminal component's inputBuffer
4. When the user presses Enter, the input is:
- Sent to the backend via WebSocket.
- Displayed in the terminal.
- Buffer is cleared for next input.
5. The input is processed by the running program in real-time.
Troubleshooting tips:
- Ensure WebSocket connection is established before sending input
- Check for WebSocket errors in console
- Verify input reaches the backend by checking server logs
- Ensure newline characters are properly appended to input.
### Language Support
### Python
- **Version**: Python 3.9
- **Input Handling**: Direct stdin piping
- **Limitations**: No file I/O, no package imports outside standard library
- **Resource Limits**: 100MB memory, 10% CPU
### Java
- **Version**: Java 11 (Eclipse Temurin)
- **Class Detection**: Extracts class name from code using regex.
- **Memory Settings**: 64MB min heap, 256MB max heap
- **Resource Limits**: 400MB memory, 50% CPU
C
- **Version**: Latest GCC
- **Compilation Flags**: Default GCC settings
- **Resource Limits**: 100MB memory, 10% CPU
### C++
- **Version**: Latest G++
- **Standard**: C++17
- **Resource Limits**: 100MB memory, 10% CPU
### Security Considerations
All code execution happens within isolated Docker containers with:
- No network access (```--network=none```)
- Limited CPU and memory resources
- Limited file system access
- No persistent storage
- Execution time limits (10-15 seconds)
### Debugging
Check backend logs for execution details
Use browser developer tools to debug WebSocket connections
Terminal panel shows WebSocket connection status and errors
Check Docker logs for container-related issues.
### Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- All code is executed in isolated Docker containers
- Network access is disabled
- Memory and CPU limits are enforced
- Execution timeouts prevent infinite loops

View File

@@ -1,500 +0,0 @@
# Monaco Backend - Code Execution Service
## Table of Contents
1. Introduction
2. Architecture
3. Installation
4. API Reference
5. Code Execution
6. Job Queue System
7. Language Support
8. Security Considerations
9. Configuration
10. Testing
11. Performance Tuning
12. Troubleshooting
## Introduction
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, JavaScript, Go, Java, C, C++)
- Secure containerized execution using Docker
- Resource limiting to prevent abuse
- Job queuing for managing concurrent executions
- Detailed execution statistics and monitoring
- Support for user input via stdin
- CORS support for browser-based clients
## Architecture
### Component Overview
Monaco follows a layered architecture with the following key components:
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
1. Client submits code via `/submit` endpoint
2. Request is validated and assigned a unique ID
3. Submission is added to the job queue
4. Worker picks job from queue when available
5. Code is executed in appropriate Docker container
6. Results are stored and available via `/result` endpoint
### Dependency Diagram
```
Client Request → HTTP Handlers → Execution Service → Job Queue → Docker Containers
Data Models
```
## Installation
### Prerequisites
- Go 1.22+
- Docker Engine
- Docker images for supported languages:
- `python:3.9`
- `node:18-alpine`
- `golang:1.22-alpine`
- `eclipse-temurin:11-jdk-alpine`
- `gcc:latest`
### Setup Instructions
1. Clone the repository:
```bash
git clone https://github.com/arnab-afk/monaco.git
cd monaco/backend
```
2. Install Go dependencies:
```bash
go mod download
```
3. Build the application:
```bash
go build -o monaco ./cmd/server
```
4. Run the service:
```bash
./monaco
```
The service will start on port 8080 by default.
## API Reference
### Endpoints
#### `POST /submit`
Submits code for execution.
**Request Body:**
```json
{
"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
}
```
**Response:**
```json
{
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1" // Unique ID for this submission
}
```
**Status Codes:**
- 202 Accepted - Code accepted for execution
- 400 Bad Request - Invalid request (e.g., unsupported language)
#### `GET /status?id={submissionId}`
Checks the status of a submission.
**Response:**
```json
{
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1",
"status": "completed", // "pending", "queued", "running", "completed", "failed"
"queuedAt": "2025-03-25T14:30:00Z",
"startedAt": "2025-03-25T14:30:01Z", // Only present if status is "running", "completed", or "failed"
"completedAt": "2025-03-25T14:30:02Z", // Only present if status is "completed" or "failed"
"executionTime": 1000 // Execution time in milliseconds (only if completed)
}
```
**Status Codes:**
- 200 OK - Status retrieved successfully
- 400 Bad Request - Missing ID parameter
- 404 Not Found - Submission with given ID not found
#### `GET /result?id={submissionId}`
Gets the execution result of a submission.
**Response:**
```json
{
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1",
"status": "completed",
"language": "python",
"output": "Hello, World!",
"queuedAt": "2025-03-25T14:30:00Z",
"startedAt": "2025-03-25T14:30:01Z",
"completedAt": "2025-03-25T14:30:02Z",
"executionTime": 1000,
"executionTimeFormatted": "1.0s",
"totalTime": 2000,
"totalTimeFormatted": "2.0s"
}
```
**Status Codes:**
- 200 OK - Result retrieved successfully
- 400 Bad Request - Missing ID parameter
- 404 Not Found - Submission with given ID not found
#### `GET /queue-stats`
Gets statistics about the job queue.
**Response:**
```json
{
"queue_stats": {
"queue_length": 5,
"max_workers": 3,
"running_jobs": 3
},
"submissions": 42
}
```
## Code Execution
### Execution Process
1. **Language Detection**: The system identifies the programming language of the submission.
2. **Environment Setup**: A temporary directory is created for compiled languages.
3. **Container Setup**: Docker containers are configured with resource limits.
4. **Compilation**: For compiled languages (Java, C, C++), code is compiled first.
5. **Execution**: The program is executed with the provided input.
6. **Resource Monitoring**: Memory and CPU usage are limited during execution.
7. **Result Collection**: Output and errors are captured and stored.
### Language-Specific Processing
#### Python
- Directly executes Python code using the `-c` flag
- Uses `python:3.9` Docker image
- Resource limits: 100MB memory, 10% CPU
#### Java
- Detects class name using regex pattern matching
- Compiles with `javac` and runs with optimized JVM settings
- Uses `eclipse-temurin:11-jdk-alpine` Docker image
- Resource limits: 400MB memory, 50% CPU
- JVM flags: `-XX:+TieredCompilation`, `-XX:TieredStopAtLevel=1`, `-Xverify:none`
#### C/C++
- Saves code to a file in a temporary directory
- Compiles with `gcc`/`g++` and runs the executable
- Uses `gcc:latest` Docker image
- Resource limits: 100MB memory, 10% CPU
### Timeout Handling
All executions have a timeout limit:
- Python: 10 seconds
- Java: 15 seconds
- C/C++: 10 seconds
If execution exceeds this limit, the process is killed and an error is returned.
## Job Queue System
### Worker Pool
Monaco uses a worker pool to manage concurrent code executions:
- Default pool size: 20 workers (configurable)
- Maximum queue capacity: 100 jobs
- FIFO (First-In-First-Out) processing order
### Job Lifecycle
1. **Creation**: Job created when code is submitted
2. **Queuing**: Job added to queue with `queued` status
3. **Execution**: Worker picks job from queue and changes status to `running`
4. **Completion**: Job finishes with either `completed` or `failed` status
### Performance Metrics
The queue tracks and reports:
- Current queue length
- Number of running jobs
- Maximum worker count
- Total number of submissions
## Language Support
### Python
- **Version**: Python 3.9
- **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
- **Memory Settings**: 64MB min heap, 256MB max heap
- **Best Practices**: Use `public class` with the main method
### C
- **Version**: Latest GCC
- **Compilation Flags**: Default GCC settings
- **Execution**: Compiled binary
### C++
- **Version**: Latest G++
- **Standard**: C++17
- **Execution**: Compiled binary
## Security Considerations
### Containerization
All code execution happens within isolated Docker containers with:
- No network access (`--network=none`)
- Limited CPU and memory resources
- Limited file system access
- No persistent storage
### Resource Limiting
- **Memory Limits**: 100-400MB depending on language
- **CPU Limits**: 10-50% of CPU depending on language
- **File Descriptors**: Limited to 64 for Python
- **Execution Time**: Enforced timeouts (10-15 seconds)
### Known Limitations
- Container escape vulnerabilities
- Docker daemon security depends on host configuration
- Resource limits can be circumvented with certain techniques
## Configuration
The service can be configured through environment variables:
- `PORT`: HTTP port (default: 8080)
- `MAX_WORKERS`: Maximum concurrent executions (default: 3)
- `QUEUE_SIZE`: Maximum queue size (default: 100)
- `DEFAULT_LANGUAGE`: Default language if none specified (default: "python")
## Testing
### Unit Tests
Run unit tests with:
```bash
go test ./...
```
# Monaco Backend Test Plan
## Overview
This test plan outlines the testing approach for the Monaco code execution backend service.
## Test Environment
- Development: Local workstations with Docker and Go
- Testing: Dedicated test server with Docker
- Production-like: Staging environment with similar resources to production
## Test Types
### Unit Tests
- **Purpose**: Verify individual components work as expected
- **Components to Test**:
- Handler package
- Queue package
- Execution service
- Models
- **Tools**: Go testing framework
### Integration Tests
- **Purpose**: Verify components work together correctly
- **Focus Areas**:
- API endpoints
- End-to-end code execution flow
- Error handling
- **Tools**: Go testing framework, HTTP test utilities
### Load Tests
- **Purpose**: Verify system performance under load
- **Scenarios**:
- Concurrent submissions
- Mixed language workloads
- Queue saturation
- **Metrics**:
- Request throughput
- Response times
- Success rates
- Resource utilization
- **Tools**: Custom Python test scripts
## Test Data
- Simple programs in each language
- Programs with input requirements
- Programs with compile errors
- Programs with runtime errors
- Programs with timeouts
## Test Execution
1. Run unit tests on every code change
2. Run integration tests before merging to main branch
3. Run load tests weekly and before major releases
## Success Criteria
- All unit tests pass
- Integration tests complete successfully
- Load tests show acceptable performance metrics:
- 95% of requests complete successfully
- 95th percentile response time < 5 seconds
- System can handle 20 concurrent users
## Reporting
- Test results stored in CI/CD pipeline
- Performance metrics graphed over time
- Issues logged in GitHub issues
### Load Testing
A Python script (`test.py`) is included for load testing:
```bash
python test.py
```
This script sends 500 requests concurrently and reports performance metrics.
### Manual Testing with Curl
#### Python Example
```bash
curl -X POST http://localhost:8080/submit \
-H "Content-Type: application/json" \
-d '{
"language": "python",
"code": "print(\"Hello, World!\")\nfor i in range(5):\n print(f\"Number: {i}\")",
"input": ""
}'
```
#### Java Example
```bash
curl -X POST http://localhost:8080/submit \
-H "Content-Type: application/json" \
-d '{
"language": "java",
"code": "public class Solution {\n public static void main(String[] args) {\n System.out.println(\"Hello, World!\");\n for (int i = 0; i < 5; i++) {\n System.out.println(\"Number: \" + i);\n }\n }\n}",
"input": ""
}'
```
## Performance Tuning
### Optimizing Worker Count
The optimal worker count depends on:
- CPU cores available
- Memory available
- Docker container startup time
For most single-server deployments, 3-5 workers is optimal.
### Memory Considerations
Each language has different memory requirements:
- Python: ~50-100MB per instance
- Java: ~200-400MB per instance
- C/C++: ~50-100MB per instance
Calculate total memory needs as: `(Python instances × 100MB) + (Java instances × 400MB) + (C/C++ instances × 100MB)`
### Disk Space Management
Temporary files are cleaned up after execution, but with high request volumes, ensure adequate disk space for concurrent operations (approximately 1-5MB per request for compiled languages).
## Troubleshooting
### Common Issues
#### Docker Connection Errors
```
Error: Cannot connect to the Docker daemon
```
**Solution**: Ensure Docker daemon is running with `systemctl start docker` or `docker --version`
#### Permissions Issues
```
Error: Permission denied while trying to connect to the Docker daemon socket
```
**Solution**: Add user to docker group: `sudo usermod -aG docker $USER`
#### Container Resource Limits
```
Error: Container killed due to memory limit
```
**Solution**: Increase memory limits in execution service or optimize submitted code
#### File Not Found Errors
```
Error: Failed to write Java file
```
**Solution**: Check temporary directory permissions and disk space
### Logs
The service provides structured logs with prefixes for easier filtering:
- `[HTTP]` - API requests
- `[QUEUE]` - Queue operations
- `[WORKER-n]` - Worker activities
- `[EXEC-id]` - Execution details
- `[PYTHON/JAVA/C/CPP-id]` - Language-specific logs
- `[TIMEOUT-id]` - Timeout events
- `[RESULT-id]` - Execution results
---
## License
This project is licensed under the MIT License - see the LICENSE file for details.
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.

View File

@@ -1,38 +0,0 @@
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())
}

View File

@@ -1,56 +0,0 @@
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())

File diff suppressed because one or more lines are too long

View File

@@ -1,131 +0,0 @@
project_name: monaco
release:
github:
owner: Arnab-Afk
name: monaco
name_template: '{{.Tag}}'
builds:
- id: monaco
goos:
- linux
- darwin
- windows
goarch:
- amd64
- arm64
- "386"
goamd64:
- v1
go386:
- sse2
goarm:
- "6"
goarm64:
- v8.0
gomips:
- hardfloat
goppc64:
- power8
goriscv64:
- rva20u64
targets:
- linux_amd64_v1
- linux_arm64_v8.0
- linux_386_sse2
- darwin_amd64_v1
- darwin_arm64_v8.0
- windows_amd64_v1
- windows_arm64_v8.0
- windows_386_sse2
dir: .
main: .
binary: monaco
builder: go
tool: go
command: build
ldflags:
- -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser
archives:
- id: default
builds_info:
mode: 493
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}{{ with .Mips }}_{{ . }}{{ end }}{{ if not (eq .Amd64 "v1") }}{{ .Amd64 }}{{ end }}'
formats:
- tar.gz
files:
- src: license*
- src: LICENSE*
- src: readme*
- src: README*
- src: changelog*
- src: CHANGELOG*
snapshot:
version_template: '{{ .Version }}-SNAPSHOT-{{ .ShortCommit }}'
checksum:
name_template: '{{ .ProjectName }}_{{ .Version }}_checksums.txt'
algorithm: sha256
changelog:
format: '{{ .SHA }} {{ .Message }}'
dist: dist
env_files:
github_token: ~/.config/goreleaser/github_token
gitlab_token: ~/.config/goreleaser/gitlab_token
gitea_token: ~/.config/goreleaser/gitea_token
source:
name_template: '{{ .ProjectName }}-{{ .Version }}'
format: tar.gz
gomod:
gobinary: go
announce:
twitter:
message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}'
mastodon:
message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}'
server: ""
reddit:
title_template: '{{ .ProjectName }} {{ .Tag }} is out!'
url_template: '{{ .ReleaseURL }}'
slack:
message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}'
username: GoReleaser
discord:
message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}'
author: GoReleaser
color: "3888754"
icon_url: https://goreleaser.com/static/avatar.png
teams:
title_template: '{{ .ProjectName }} {{ .Tag }} is out!'
message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}'
color: '#2D313E'
icon_url: https://goreleaser.com/static/avatar.png
smtp:
subject_template: '{{ .ProjectName }} {{ .Tag }} is out!'
body_template: 'You can view details from: {{ .ReleaseURL }}'
mattermost:
message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}'
title_template: '{{ .ProjectName }} {{ .Tag }} is out!'
username: GoReleaser
linkedin:
message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}'
telegram:
message_template: '{{ mdv2escape .ProjectName }} {{ mdv2escape .Tag }} is out{{ mdv2escape "!" }} Check it out at {{ mdv2escape .ReleaseURL }}'
parse_mode: MarkdownV2
webhook:
message_template: '{ "message": "{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}"}'
content_type: application/json; charset=utf-8
expected_status_codes:
- 200
- 201
- 202
- 204
opencollective:
title_template: '{{ .Tag }}'
message_template: '{{ .ProjectName }} {{ .Tag }} is out!<br/>Check it out at <a href="{{ .ReleaseURL }}">{{ .ReleaseURL }}</a>'
bluesky:
message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}'
git:
tag_sort: -version:refname
github_urls:
download: https://github.com
gitlab_urls:
download: https://gitlab.com

View File

@@ -1 +0,0 @@
{"project_name":"monaco","tag":"v0.0.0","previous_tag":"","version":"0.0.0-SNAPSHOT-574f754","commit":"574f7549406a4faa0d84d53eb201ae7ebd1edc1a","date":"2025-03-26T20:50:22.2203996+05:30","runtime":{"goos":"windows","goarch":"amd64"}}

View File

@@ -1,8 +0,0 @@
41202196ac39bef7e0fdb394f838967c82dd4429245c5e4c720d849bc8b73c2d monaco_0.0.0-SNAPSHOT-574f754_darwin_amd64.tar.gz
c69d1632f0c2bb6df9fa5bf33ac892596bf69cd994ee022dfc4659a2c8df4000 monaco_0.0.0-SNAPSHOT-574f754_darwin_arm64.tar.gz
b1b7bd54911686aa5c8539e9fbb4b5c4ac7ba5609eb9df32976edf14821acd59 monaco_0.0.0-SNAPSHOT-574f754_linux_386.tar.gz
b078183b95e9088a3cd9814113a98197a44cf93bb14e27a148f2ccc5d5a2db27 monaco_0.0.0-SNAPSHOT-574f754_linux_amd64.tar.gz
9fed0feae759d5731b4ea347d239cc559008bde1ff3c4afdcc27a89260696c92 monaco_0.0.0-SNAPSHOT-574f754_linux_arm64.tar.gz
dc8e6b76317db38904f1899d8895ed4a361cca931224b48875ed1cef2604ce59 monaco_0.0.0-SNAPSHOT-574f754_windows_386.tar.gz
86b9667a338fa80bb6a175fbb881f7eb02f2a31378c1a76f3b92671f2c24f845 monaco_0.0.0-SNAPSHOT-574f754_windows_amd64.tar.gz
642cf9946261558515db88540cf7ecb92859c73afd1600c0441605dd68518757 monaco_0.0.0-SNAPSHOT-574f754_windows_arm64.tar.gz

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,155 +0,0 @@
# 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
```

View File

@@ -1,15 +0,0 @@
module github.com/arnab-afk/monaco
go 1.22.3
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
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View File

@@ -1,25 +0,0 @@
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=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,185 +0,0 @@
package handler
import (
"encoding/json"
"net/http"
"sync"
"time"
"github.com/arnab-afk/monaco/model"
"github.com/arnab-afk/monaco/service"
)
// Handler manages HTTP requests for code submissions
type Handler struct {
executionService *service.ExecutionService
mu sync.Mutex
submissions map[string]*model.CodeSubmission
}
// NewHandler creates a new handler instance
func NewHandler() *Handler {
return &Handler{
executionService: service.NewExecutionService(),
submissions: make(map[string]*model.CodeSubmission),
}
}
// SubmitHandler handles code submission requests
func (h *Handler) SubmitHandler(w http.ResponseWriter, r *http.Request) {
var submission model.CodeSubmission
if err := json.NewDecoder(r.Body).Decode(&submission); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
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] {
http.Error(w, "Unsupported language: "+submission.Language, http.StatusBadRequest)
return
}
h.mu.Lock()
submission.ID = h.generateID()
submission.Status = "pending"
h.submissions[submission.ID] = &submission
h.mu.Unlock()
go h.executionService.ExecuteCode(&submission)
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) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "ID is required", http.StatusBadRequest)
return
}
h.mu.Lock()
submission, exists := h.submissions[id]
h.mu.Unlock()
if !exists {
http.Error(w, "Submission not found", http.StatusNotFound)
return
}
// Return status with time information
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.Status == "running" && !submission.StartedAt.IsZero() {
response["startedAt"] = submission.StartedAt.Format(time.RFC3339)
response["runningFor"] = time.Since(submission.StartedAt).String()
}
if submission.Status == "completed" || submission.Status == "failed" {
if !submission.CompletedAt.IsZero() && !submission.StartedAt.IsZero() {
response["executionTime"] = submission.CompletedAt.Sub(submission.StartedAt).Milliseconds()
response["completedAt"] = submission.CompletedAt.Format(time.RFC3339)
}
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, "Failed to serialize response: "+err.Error(), http.StatusInternalServerError)
return
}
}
// ResultHandler handles result requests
func (h *Handler) ResultHandler(w http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id")
if id == "" {
http.Error(w, "ID is required", http.StatusBadRequest)
return
}
h.mu.Lock()
submission, exists := h.submissions[id]
h.mu.Unlock()
if !exists {
http.Error(w, "Submission not found", http.StatusNotFound)
return
}
// Prepare response with safe time handling
response := map[string]interface{}{
"id": submission.ID,
"status": submission.Status,
"output": submission.Output,
"language": submission.Language,
}
// Only include time fields if they're set
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)
// Calculate times only if we have valid timestamps
if !submission.StartedAt.IsZero() {
executionTime := submission.CompletedAt.Sub(submission.StartedAt)
response["executionTime"] = executionTime.Milliseconds() // Use milliseconds for frontend
response["executionTimeFormatted"] = executionTime.String()
}
if !submission.QueuedAt.IsZero() {
totalTime := submission.CompletedAt.Sub(submission.QueuedAt)
response["totalTime"] = totalTime.Milliseconds() // Use milliseconds for frontend
response["totalTimeFormatted"] = totalTime.String()
}
}
// Return full submission details
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
http.Error(w, "Failed to serialize response: "+err.Error(), http.StatusInternalServerError)
return
}
}
// QueueStatsHandler provides information about the job queue
func (h *Handler) QueueStatsHandler(w http.ResponseWriter, r *http.Request) {
stats := h.executionService.GetQueueStats()
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"queue_stats": stats,
"submissions": len(h.submissions),
})
}
// generateID creates a unique ID for submissions
func (h *Handler) generateID() string {
return service.GenerateUUID()
}

View File

@@ -1,154 +0,0 @@
package handler
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/arnab-afk/monaco/model"
"github.com/stretchr/testify/assert"
)
func TestSubmitHandler(t *testing.T) {
h := NewHandler()
// Test valid Python submission
body := map[string]string{
"language": "python",
"code": "print('Hello, World!')",
}
bodyBytes, _ := json.Marshal(body)
req := httptest.NewRequest("POST", "/submit", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
h.SubmitHandler(w, req)
assert.Equal(t, http.StatusAccepted, w.Code)
var response map[string]string
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.NotEmpty(t, response["id"])
// Test invalid language
body["language"] = "invalid"
bodyBytes, _ = json.Marshal(body)
req = httptest.NewRequest("POST", "/submit", bytes.NewReader(bodyBytes))
req.Header.Set("Content-Type", "application/json")
w = httptest.NewRecorder()
h.SubmitHandler(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "Unsupported language")
}
func TestStatusHandler(t *testing.T) {
h := NewHandler()
// Create a test submission
submission := &model.CodeSubmission{
ID: "test-id",
Language: "python",
Code: "print('Hello')",
Status: "completed",
QueuedAt: time.Now().Add(-2 * time.Second),
StartedAt: time.Now().Add(-1 * time.Second),
CompletedAt: time.Now(),
Output: "Hello",
}
h.submissions["test-id"] = submission
// Test valid status request
req := httptest.NewRequest("GET", "/status?id=test-id", nil)
w := httptest.NewRecorder()
h.StatusHandler(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "test-id", response["id"])
assert.Equal(t, "completed", response["status"])
// Test missing ID
req = httptest.NewRequest("GET", "/status", nil)
w = httptest.NewRecorder()
h.StatusHandler(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Contains(t, w.Body.String(), "ID is required")
// Test non-existent ID
req = httptest.NewRequest("GET", "/status?id=nonexistent", nil)
w = httptest.NewRecorder()
h.StatusHandler(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
assert.Contains(t, w.Body.String(), "Submission not found")
}
func TestResultHandler(t *testing.T) {
h := NewHandler()
// Create a test submission
submission := &model.CodeSubmission{
ID: "test-id",
Language: "python",
Code: "print('Hello')",
Status: "completed",
QueuedAt: time.Now().Add(-2 * time.Second),
StartedAt: time.Now().Add(-1 * time.Second),
CompletedAt: time.Now(),
Output: "Hello",
}
h.submissions["test-id"] = submission
// Test valid result request
req := httptest.NewRequest("GET", "/result?id=test-id", nil)
w := httptest.NewRecorder()
h.ResultHandler(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "test-id", response["id"])
assert.Equal(t, "completed", response["status"])
assert.Equal(t, "Hello", response["output"])
}
func TestQueueStatsHandler(t *testing.T) {
h := NewHandler()
// Add some test submissions
h.submissions["test-id1"] = &model.CodeSubmission{ID: "test-id1"}
h.submissions["test-id2"] = &model.CodeSubmission{ID: "test-id2"}
req := httptest.NewRequest("GET", "/queue-stats", nil)
w := httptest.NewRecorder()
h.QueueStatsHandler(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
stats, ok := response["queue_stats"].(map[string]interface{})
assert.True(t, ok)
assert.Contains(t, stats, "queue_length")
assert.Contains(t, stats, "max_workers")
assert.Contains(t, stats, "running_jobs")
assert.Equal(t, float64(2), response["submissions"])
}

View File

@@ -1,85 +0,0 @@
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)
}

View File

@@ -1,260 +0,0 @@
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)
}

View File

@@ -1,70 +0,0 @@
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")
}

View File

@@ -1,49 +0,0 @@
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)
})
}

View File

@@ -1,261 +0,0 @@
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
}

View File

@@ -1,32 +0,0 @@
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
}

View File

@@ -1,683 +0,0 @@
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 ""
}

View File

@@ -1,366 +0,0 @@
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)
}

View File

@@ -1,376 +0,0 @@
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"
}

View File

@@ -1,36 +0,0 @@
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"`
}

View File

@@ -1,112 +0,0 @@
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()
}

View File

@@ -1,59 +0,0 @@
package main
import (
"log"
"net/http"
"os"
"time"
"github.com/arnab-afk/monaco/handler"
)
func main() {
// Configure logging with timestamps and file locations
log.SetFlags(log.LstdFlags | log.Lshortfile)
log.SetOutput(os.Stdout)
log.Println("Starting Monaco code execution backend...")
h := handler.NewHandler()
// Create a middleware for request logging
loggingMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
log.Printf("[HTTP] %s %s %s", r.Method, r.URL.Path, r.RemoteAddr)
next(w, r)
log.Printf("[HTTP] %s %s completed in %v", r.Method, r.URL.Path, time.Since(startTime))
}
}
// Create a middleware for CORS - allow all origins
corsMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Set CORS headers to allow any origin
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
w.Header().Set("Access-Control-Max-Age", "3600")
// Handle preflight OPTIONS requests
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
next(w, r)
}
}
// Register handlers with logging and CORS middleware
http.HandleFunc("/submit", corsMiddleware(loggingMiddleware(h.SubmitHandler)))
http.HandleFunc("/status", corsMiddleware(loggingMiddleware(h.StatusHandler)))
http.HandleFunc("/result", corsMiddleware(loggingMiddleware(h.ResultHandler)))
http.HandleFunc("/queue-stats", corsMiddleware(loggingMiddleware(h.QueueStatsHandler)))
http.HandleFunc("/ws", corsMiddleware(h.WebSocketHandler)) // WebSocket doesn't need logging middleware
port := ":8080"
log.Printf("Server started at %s", port)
log.Fatal(http.ListenAndServe(port, nil))
}

View File

@@ -1,16 +0,0 @@
package model
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"` // Added input field for stdin
Status string `json:"status"` // "queued", "running", "completed", "failed"
QueuedAt time.Time `json:"queuedAt"`
StartedAt time.Time `json:"startedAt,omitempty"`
CompletedAt time.Time `json:"completedAt,omitempty"`
Output string `json:"output"`
}

View File

@@ -1,71 +0,0 @@
package model
import (
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestCodeSubmissionSerialization(t *testing.T) {
// Create a submission
now := time.Now()
submission := CodeSubmission{
ID: "test-id",
Code: "print('Hello, World!')",
Language: "python",
Input: "test input",
Status: "completed",
QueuedAt: now.Add(-2 * time.Second),
StartedAt: now.Add(-1 * time.Second),
CompletedAt: now,
Output: "Hello, World!",
}
// Serialize to JSON
jsonBytes, err := json.Marshal(submission)
assert.NoError(t, err)
assert.NotNil(t, jsonBytes)
// Deserialize back
var decoded CodeSubmission
err = json.Unmarshal(jsonBytes, &decoded)
assert.NoError(t, err)
// Verify fields match
assert.Equal(t, submission.ID, decoded.ID)
assert.Equal(t, submission.Code, decoded.Code)
assert.Equal(t, submission.Language, decoded.Language)
assert.Equal(t, submission.Input, decoded.Input)
assert.Equal(t, submission.Status, decoded.Status)
assert.Equal(t, submission.Output, decoded.Output)
// Time fields need special handling due to JSON serialization
assert.Equal(t, submission.QueuedAt.Format(time.RFC3339), decoded.QueuedAt.Format(time.RFC3339))
assert.Equal(t, submission.StartedAt.Format(time.RFC3339), decoded.StartedAt.Format(time.RFC3339))
assert.Equal(t, submission.CompletedAt.Format(time.RFC3339), decoded.CompletedAt.Format(time.RFC3339))
}
func TestCodeSubmissionDefaults(t *testing.T) {
// Test that zero time values work correctly
submission := CodeSubmission{
ID: "test-id",
Code: "print('Hello')",
Language: "python",
Status: "pending",
}
assert.True(t, submission.QueuedAt.IsZero())
assert.True(t, submission.StartedAt.IsZero())
assert.True(t, submission.CompletedAt.IsZero())
// Test JSON marshaling with zero time values
jsonBytes, err := json.Marshal(submission)
assert.NoError(t, err)
// The zero time values should still be included in the JSON
jsonStr := string(jsonBytes)
assert.Contains(t, jsonStr, `"id":"test-id"`)
assert.Contains(t, jsonStr, `"status":"pending"`)
}

Binary file not shown.

View File

@@ -1,96 +0,0 @@
package queue
import (
"log"
"sync"
"time"
)
// Job represents a task that can be executed
type Job interface {
Execute()
}
// JobQueue manages the execution of jobs with limited concurrency
type JobQueue struct {
jobs chan Job
maxWorkers int
wg sync.WaitGroup
running int
mu sync.Mutex
}
// NewJobQueue creates a new job queue with specified max concurrent workers
func NewJobQueue(maxWorkers int) *JobQueue {
log.Printf("[QUEUE] Initializing job queue with %d workers and buffer size 100", maxWorkers)
jq := &JobQueue{
jobs: make(chan Job, 100), // Buffer size of 100 jobs
maxWorkers: maxWorkers,
}
jq.start()
return jq
}
// start initializes the worker pool
func (jq *JobQueue) start() {
// Start the workers
for i := 0; i < jq.maxWorkers; i++ {
workerId := i + 1
log.Printf("[WORKER-%d] Starting worker", workerId)
jq.wg.Add(1)
go func(id int) {
defer jq.wg.Done()
for job := range jq.jobs {
jq.mu.Lock()
jq.running++
queueLen := len(jq.jobs)
jq.mu.Unlock()
log.Printf("[WORKER-%d] Processing job (running: %d, queued: %d)",
id, jq.running, queueLen)
startTime := time.Now()
job.Execute()
elapsed := time.Since(startTime)
jq.mu.Lock()
jq.running--
jq.mu.Unlock()
log.Printf("[WORKER-%d] Completed job in %v (running: %d, queued: %d)",
id, elapsed, jq.running, len(jq.jobs))
}
log.Printf("[WORKER-%d] Worker shutting down", id)
}(workerId)
}
}
// Enqueue adds a job to the queue
func (jq *JobQueue) Enqueue(job Job) {
jq.mu.Lock()
queueLen := len(jq.jobs)
jq.mu.Unlock()
log.Printf("[QUEUE] Job enqueued (queue length: %d, running: %d)", queueLen, jq.running)
jq.jobs <- job
}
// Stop gracefully shuts down the job queue
func (jq *JobQueue) Stop() {
log.Println("[QUEUE] Stopping job queue, waiting for running jobs to complete")
close(jq.jobs)
jq.wg.Wait()
log.Println("[QUEUE] Job queue shutdown complete")
}
// QueueStats returns statistics about the queue
func (jq *JobQueue) QueueStats() map[string]int {
jq.mu.Lock()
defer jq.mu.Unlock()
return map[string]int{
"queue_length": len(jq.jobs),
"max_workers": jq.maxWorkers,
"running_jobs": jq.running,
}
}

View File

@@ -1,112 +0,0 @@
package queue
import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// Mock job for testing
type MockJob struct {
executed bool
executeTime time.Duration
mu sync.Mutex
}
func (j *MockJob) Execute() {
j.mu.Lock()
defer j.mu.Unlock()
time.Sleep(j.executeTime)
j.executed = true
}
func (j *MockJob) IsExecuted() bool {
j.mu.Lock()
defer j.mu.Unlock()
return j.executed
}
func TestJobQueueCreation(t *testing.T) {
// Test with different numbers of workers
jq := NewJobQueue(5)
assert.NotNil(t, jq)
assert.Equal(t, 5, jq.maxWorkers)
stats := jq.QueueStats()
assert.Equal(t, 0, stats["queue_length"])
assert.Equal(t, 5, stats["max_workers"])
assert.Equal(t, 0, stats["running_jobs"])
}
func TestJobExecution(t *testing.T) {
jq := NewJobQueue(2)
// Create test jobs
job1 := &MockJob{executeTime: 10 * time.Millisecond}
job2 := &MockJob{executeTime: 10 * time.Millisecond}
// Enqueue jobs
jq.Enqueue(job1)
jq.Enqueue(job2)
// Wait for execution
time.Sleep(50 * time.Millisecond)
// Verify both jobs executed
assert.True(t, job1.IsExecuted())
assert.True(t, job2.IsExecuted())
}
func TestConcurrentJobsExecution(t *testing.T) {
// Test that only maxWorkers jobs run concurrently
jq := NewJobQueue(2)
var mu sync.Mutex
runningCount := 0
maxObservedRunning := 0
wg := sync.WaitGroup{}
// Create long running jobs to test concurrency
for i := 0; i < 5; i++ {
wg.Add(1)
job := &MockJob{
executeTime: 100 * time.Millisecond,
}
// Wrap the job to monitor concurrency
wrappedJob := JobFunc(func() {
mu.Lock()
runningCount++
if runningCount > maxObservedRunning {
maxObservedRunning = runningCount
}
mu.Unlock()
job.Execute()
mu.Lock()
runningCount--
mu.Unlock()
wg.Done()
})
jq.Enqueue(wrappedJob)
}
wg.Wait()
jq.Stop()
// Verify max concurrent jobs is respected
assert.LessOrEqual(t, maxObservedRunning, 2)
}
// Define JobFunc type for easier job creation in tests
type JobFunc func()
func (f JobFunc) Execute() {
f()
}

View File

@@ -1,712 +0,0 @@
package service
import (
"bufio"
"context"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/arnab-afk/monaco/model"
"github.com/arnab-afk/monaco/queue"
"github.com/gorilla/websocket"
)
// ExecutionService handles code execution for multiple languages
type ExecutionService struct {
mu sync.Mutex
queue *queue.JobQueue
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(3), // 3 concurrent executions max
wsConnections: make(map[string]*websocket.Conn),
wsInputChannels: make(map[string]chan string),
}
}
// CodeExecutionJob represents a job to execute code
type CodeExecutionJob struct {
service *ExecutionService
submission *model.CodeSubmission
}
// NewCodeExecutionJob creates a new code execution job
func NewCodeExecutionJob(service *ExecutionService, submission *model.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)
}
// ExecuteCode adds the submission to the execution queue
func (s *ExecutionService) ExecuteCode(submission *model.CodeSubmission) {
submission.Status = "queued"
submission.QueuedAt = time.Now()
log.Printf("[SUBMISSION-%s] Code submission queued for language: %s (Queue length: %d)",
submission.ID, submission.Language, s.queue.QueueStats()["queue_length"])
// Log if input is provided
if len(submission.Input) > 0 {
inputLen := len(submission.Input)
previewLen := 30
if inputLen > previewLen {
log.Printf("[INPUT-%s] Input provided (%d bytes): %s...",
submission.ID, inputLen, submission.Input[:previewLen])
} else {
log.Printf("[INPUT-%s] Input provided (%d bytes): %s",
submission.ID, inputLen, submission.Input)
}
}
job := NewCodeExecutionJob(s, submission)
s.queue.Enqueue(job)
}
// executeLanguageSpecific runs code in the appropriate language container
func (s *ExecutionService) executeLanguageSpecific(submission *model.CodeSubmission) {
log.Printf("[EXEC-%s] Selecting execution environment for language: %s",
submission.ID, submission.Language)
switch submission.Language {
case "python":
log.Printf("[EXEC-%s] Executing Python code", submission.ID)
s.executePython(submission)
case "java":
log.Printf("[EXEC-%s] Executing Java code", submission.ID)
s.executeJava(submission)
case "c":
log.Printf("[EXEC-%s] Executing C code", submission.ID)
s.executeC(submission)
case "cpp":
log.Printf("[EXEC-%s] Executing C++ code", submission.ID)
s.executeCpp(submission)
default:
log.Printf("[EXEC-%s] ERROR: Unsupported language: %s", submission.ID, submission.Language)
submission.Status = "failed"
submission.Output = "Unsupported language: " + submission.Language
}
}
// executeWithInput runs a command with a timeout and provides input
func (s *ExecutionService) executeWithInput(cmd *exec.Cmd, input string, timeout time.Duration, submissionID string) ([]byte, error) {
log.Printf("[TIMEOUT-%s] Setting execution timeout: %v", submissionID, timeout)
// Set up input pipe if input is provided
if input != "" {
stdin, err := cmd.StdinPipe()
if err != nil {
log.Printf("[ERROR-%s] Failed to create stdin pipe: %v", submissionID, err)
return nil, err
}
// Write input in a goroutine to avoid blocking
go func() {
defer stdin.Close()
io.WriteString(stdin, input)
}()
log.Printf("[INPUT-%s] Providing input to process", submissionID)
}
done := make(chan struct{})
var output []byte
var err error
go func() {
log.Printf("[EXEC-%s] Starting command execution: %v", submissionID, cmd.Args)
output, err = cmd.CombinedOutput()
close(done)
}()
select {
case <-time.After(timeout):
log.Printf("[TIMEOUT-%s] Execution timed out after %v seconds", submissionID, timeout.Seconds())
if err := cmd.Process.Kill(); err != nil {
log.Printf("[TIMEOUT-%s] Failed to kill process: %v", submissionID, err)
return nil, fmt.Errorf("timeout reached but failed to kill process: %v", err)
}
return nil, fmt.Errorf("execution timed out after %v seconds", timeout.Seconds())
case <-done:
if err != nil {
log.Printf("[EXEC-%s] Command execution failed: %v", submissionID, err)
} else {
log.Printf("[EXEC-%s] Command execution completed successfully", submissionID)
}
return output, err
}
}
// executePython runs Python code in a container
func (s *ExecutionService) executePython(submission *model.CodeSubmission) {
log.Printf("[PYTHON-%s] Preparing Python execution environment", submission.ID)
startTime := time.Now()
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)
log.Printf("[PYTHON-%s] Executing Python code with timeout: 10s", submission.ID)
var output []byte
var err error
if submission.Input != "" {
cmd.Stdin = strings.NewReader(submission.Input)
output, err = cmd.CombinedOutput()
} else {
output, err = s.executeWithTimeout(cmd, 10*time.Second, submission.ID)
}
elapsed := time.Since(startTime)
log.Printf("[PYTHON-%s] Python execution completed in %v", submission.ID, elapsed)
s.updateSubmissionResult(submission, output, err)
}
// extractClassName extracts the Java class name from code
func extractClassName(code string) string {
// Default class name as fallback
defaultClass := "Solution"
// Look for public class
re := regexp.MustCompile(`public\s+class\s+(\w+)`)
matches := re.FindStringSubmatch(code)
if len(matches) > 1 {
return matches[1]
}
// Look for any class if no public class
re = regexp.MustCompile(`class\s+(\w+)`)
matches = re.FindStringSubmatch(code)
if len(matches) > 1 {
return matches[1]
}
return defaultClass
}
// executeJava runs Java code in a container
func (s *ExecutionService) executeJava(submission *model.CodeSubmission) {
log.Printf("[JAVA-%s] Preparing Java execution environment", submission.ID)
startTime := time.Now()
// Extract class name from code
className := extractClassName(submission.Code)
log.Printf("[JAVA-%s] Detected class name: %s", submission.ID, className)
// Create temp directory for Java files
tempDir, err := os.MkdirTemp("", "java-execution-"+submission.ID)
if err != nil {
log.Printf("[JAVA-%s] Failed to create temp directory: %v", submission.ID, err)
submission.Status = "failed"
submission.Output = "Failed to create temp directory: " + err.Error()
return
}
defer os.RemoveAll(tempDir)
log.Printf("[JAVA-%s] Created temp directory: %s", submission.ID, tempDir)
// Write Java code to file with detected class name
javaFilePath := filepath.Join(tempDir, className+".java")
if err := os.WriteFile(javaFilePath, []byte(submission.Code), 0644); err != nil {
log.Printf("[JAVA-%s] Failed to write Java file: %v", submission.ID, err)
submission.Status = "failed"
submission.Output = "Failed to write Java file: " + err.Error()
return
}
log.Printf("[JAVA-%s] Wrote code to file: %s", submission.ID, javaFilePath)
// First compile without running
compileCmd := exec.Command("docker", "run", "--rm",
"-v", tempDir+":/code", // Mount code directory
"eclipse-temurin:11-jdk-alpine",
"javac", "/code/"+className+".java")
log.Printf("[JAVA-%s] Compiling Java code", submission.ID)
compileOutput, compileErr := compileCmd.CombinedOutput()
if compileErr != nil {
log.Printf("[JAVA-%s] Compilation failed: %v", submission.ID, compileErr)
submission.Status = "failed"
submission.Output = "Compilation error:\n" + string(compileOutput)
return
}
log.Printf("[JAVA-%s] Compilation successful", submission.ID)
// Now run the compiled class
runCmd := exec.Command("docker", "run", "--rm", "-i",
"--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)
// Add input if provided
var output []byte
if submission.Input != "" {
log.Printf("[JAVA-%s] Executing Java code with input", submission.ID)
runCmd.Stdin = strings.NewReader(submission.Input)
output, err = runCmd.CombinedOutput()
} else {
log.Printf("[JAVA-%s] Executing Java code without input", submission.ID)
output, err = s.executeWithTimeout(runCmd, 15*time.Second, submission.ID)
}
elapsed := time.Since(startTime)
log.Printf("[JAVA-%s] Java execution completed in %v", submission.ID, elapsed)
s.updateSubmissionResult(submission, output, err)
}
// executeC runs C code in a container with improved file handling
func (s *ExecutionService) executeC(submission *model.CodeSubmission) {
log.Printf("[C-%s] Preparing C execution environment", submission.ID)
startTime := time.Now()
// Create unique temp directory for C files
tempDir, err := os.MkdirTemp("", "c-execution-"+submission.ID)
if err != nil {
log.Printf("[C-%s] Failed to create temp directory: %v", submission.ID, err)
submission.Status = "failed"
submission.Output = "Failed to create temp directory: " + err.Error()
return
}
defer os.RemoveAll(tempDir)
log.Printf("[C-%s] Created temp directory: %s", submission.ID, tempDir)
// Write C code to file
cFilePath := filepath.Join(tempDir, "solution.c")
if err := os.WriteFile(cFilePath, []byte(submission.Code), 0644); err != nil {
log.Printf("[C-%s] Failed to write C file: %v", submission.ID, err)
submission.Status = "failed"
submission.Output = "Failed to write C file: " + err.Error()
return
}
log.Printf("[C-%s] Wrote code to file: %s", submission.ID, cFilePath)
// Compile C code first
compileCmd := exec.Command("docker", "run", "--rm",
"-v", tempDir+":/code", // Mount code directory
"gcc:latest", "gcc", "-o", "/code/solution", "/code/solution.c")
compileOutput, compileErr := compileCmd.CombinedOutput()
if compileErr != nil {
log.Printf("[C-%s] Compilation failed: %v", submission.ID, compileErr)
submission.Status = "failed"
submission.Output = "Compilation error:\n" + string(compileOutput)
return
}
log.Printf("[C-%s] Compilation successful", submission.ID)
// Run C executable
runCmd := 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
"-v", tempDir+":/code", // Mount code directory
"gcc:latest", "/code/solution")
// Add input if provided
var output []byte
// Don't redeclare err here - use the existing variable
if submission.Input != "" {
log.Printf("[C-%s] Executing C code with input", submission.ID)
runCmd.Stdin = strings.NewReader(submission.Input)
output, err = runCmd.CombinedOutput() // Use the existing err variable
} else {
log.Printf("[C-%s] Executing C code without input", submission.ID)
output, err = s.executeWithTimeout(runCmd, 10*time.Second, submission.ID) // Use the existing err variable
}
elapsed := time.Since(startTime)
log.Printf("[C-%s] C execution completed in %v", submission.ID, elapsed)
s.updateSubmissionResult(submission, output, err)
}
// executeCpp runs C++ code in a container with improved file handling
func (s *ExecutionService) executeCpp(submission *model.CodeSubmission) {
log.Printf("[CPP-%s] Preparing C++ execution environment", submission.ID)
startTime := time.Now()
// Create unique temp directory for C++ files
tempDir, err := os.MkdirTemp("", "cpp-execution-"+submission.ID)
if err != nil {
log.Printf("[CPP-%s] Failed to create temp directory: %v", submission.ID, err)
submission.Status = "failed"
submission.Output = "Failed to create temp directory: " + err.Error()
return
}
defer os.RemoveAll(tempDir)
log.Printf("[CPP-%s] Created temp directory: %s", submission.ID, tempDir)
// Write C++ code to file
cppFilePath := filepath.Join(tempDir, "solution.cpp")
if err := os.WriteFile(cppFilePath, []byte(submission.Code), 0644); err != nil {
log.Printf("[CPP-%s] Failed to write C++ file: %v", submission.ID, err)
submission.Status = "failed"
submission.Output = "Failed to write C++ file: " + err.Error()
return
}
log.Printf("[CPP-%s] Wrote code to file: %s", submission.ID, cppFilePath)
// Compile C++ code first
compileCmd := exec.Command("docker", "run", "--rm",
"-v", tempDir+":/code", // Mount code directory
"gcc:latest", "g++", "-o", "/code/solution", "/code/solution.cpp")
compileOutput, compileErr := compileCmd.CombinedOutput()
if compileErr != nil {
log.Printf("[CPP-%s] Compilation failed: %v", submission.ID, compileErr)
submission.Status = "failed"
submission.Output = "Compilation error:\n" + string(compileOutput)
return
}
log.Printf("[CPP-%s] Compilation successful", submission.ID)
// Run C++ executable
runCmd := 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
"-v", tempDir+":/code", // Mount code directory
"gcc:latest", "/code/solution")
// Add input if provided
var output []byte
if submission.Input != "" {
log.Printf("[CPP-%s] Executing C++ code with input", submission.ID)
runCmd.Stdin = strings.NewReader(submission.Input)
output, err = runCmd.CombinedOutput()
} else {
log.Printf("[CPP-%s] Executing C++ code without input", submission.ID)
output, err = s.executeWithTimeout(runCmd, 10*time.Second, submission.ID)
}
elapsed := time.Since(startTime)
log.Printf("[CPP-%s] C++ execution completed in %v", submission.ID, elapsed)
s.updateSubmissionResult(submission, output, err)
}
// executeWithTimeout runs a command with a timeout
func (s *ExecutionService) executeWithTimeout(cmd *exec.Cmd, timeout time.Duration, submissionID string) ([]byte, error) {
log.Printf("[TIMEOUT-%s] Setting execution timeout: %v", submissionID, timeout)
done := make(chan error, 1)
var output []byte
var err error
go func() {
log.Printf("[EXEC-%s] Starting command execution: %v", submissionID, cmd.Args)
output, err = cmd.CombinedOutput()
done <- err
}()
select {
case <-time.After(timeout):
log.Printf("[TIMEOUT-%s] Execution timed out after %v seconds", submissionID, timeout.Seconds())
if err := cmd.Process.Kill(); err != nil {
log.Printf("[TIMEOUT-%s] Failed to kill process: %v", submissionID, err)
return nil, fmt.Errorf("timeout reached but failed to kill process: %v", err)
}
return nil, fmt.Errorf("execution timed out after %v seconds", timeout.Seconds())
case err := <-done:
if err != nil {
log.Printf("[EXEC-%s] Command execution failed: %v", submissionID, err)
} else {
log.Printf("[EXEC-%s] Command execution completed successfully", submissionID)
}
return output, err
}
}
// updateSubmissionResult updates the submission with execution results
func (s *ExecutionService) updateSubmissionResult(submission *model.CodeSubmission, output []byte, err error) {
s.mu.Lock()
defer s.mu.Unlock()
submission.CompletedAt = time.Now()
executionTime := submission.CompletedAt.Sub(submission.StartedAt)
totalTime := submission.CompletedAt.Sub(submission.QueuedAt)
if err != nil {
submission.Status = "failed"
submission.Output = string(output) + "\n" + err.Error()
log.Printf("[RESULT-%s] Execution FAILED in %v (total time: %v, including queue: %v)",
submission.ID, executionTime, totalTime, totalTime-executionTime)
} else {
submission.Status = "completed"
submission.Output = string(output)
log.Printf("[RESULT-%s] Execution COMPLETED in %v (total time: %v, including queue: %v)",
submission.ID, executionTime, totalTime, totalTime-executionTime)
}
}
// GetQueueStats returns statistics about the job queue
func (s *ExecutionService) GetQueueStats() map[string]int {
stats := s.queue.QueueStats()
log.Printf("[QUEUE] Stats - Jobs in queue: %d, Running jobs: %d, Max workers: %d",
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"
}

View File

@@ -1,115 +0,0 @@
package service
import (
"os"
"testing"
"time"
"github.com/arnab-afk/monaco/model"
"github.com/stretchr/testify/assert"
)
// TestExecutionServiceCreation tests that the service is created properly
func TestExecutionServiceCreation(t *testing.T) {
service := NewExecutionService()
assert.NotNil(t, service)
assert.NotNil(t, service.queue)
}
// TestExtractClassName tests the class name extraction for Java code
func TestExtractClassName(t *testing.T) {
tests := []struct {
name string
code string
expected string
}{
{
name: "Public class",
code: "public class MyClass { public static void main(String[] args) {} }",
expected: "MyClass",
},
{
name: "Regular class",
code: "class RegularClass { public static void main(String[] args) {} }",
expected: "RegularClass",
},
{
name: "Multiple classes",
code: "class Class1 {} public class MainClass {} class Class2 {}",
expected: "MainClass",
},
{
name: "No class",
code: "// Just a comment",
expected: "Solution", // Default class name
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := extractClassName(tt.code)
assert.Equal(t, tt.expected, result)
})
}
}
// MockDockerExec is a function that can be used to mock Docker exec commands
type MockDockerExec func(cmd string, args ...string) ([]byte, error)
// TestUpdateSubmissionResult tests the submission result update logic
func TestUpdateSubmissionResult(t *testing.T) {
service := NewExecutionService()
// Test successful execution
submission := &model.CodeSubmission{
ID: "test-id",
Status: "running",
StartedAt: time.Now().Add(-500 * time.Millisecond),
QueuedAt: time.Now().Add(-1 * time.Second),
}
output := []byte("Hello, World!")
service.updateSubmissionResult(submission, output, nil)
assert.Equal(t, "completed", submission.Status)
assert.Equal(t, "Hello, World!", submission.Output)
assert.False(t, submission.CompletedAt.IsZero())
// Test failed execution
submission = &model.CodeSubmission{
ID: "test-id-2",
Status: "running",
StartedAt: time.Now().Add(-500 * time.Millisecond),
QueuedAt: time.Now().Add(-1 * time.Second),
}
output = []byte("Compilation error")
err := os.ErrInvalid // Any error will do for testing
service.updateSubmissionResult(submission, output, err)
assert.Equal(t, "failed", submission.Status)
assert.Contains(t, submission.Output, "Compilation error")
assert.Contains(t, submission.Output, err.Error())
assert.False(t, submission.CompletedAt.IsZero())
}
// TestCodeExecutionJob tests the job execution logic
func TestCodeExecutionJob(t *testing.T) {
service := NewExecutionService()
submission := &model.CodeSubmission{
ID: "test-id",
Language: "python",
Code: "print('test')",
Status: "queued",
QueuedAt: time.Now(),
}
job := NewCodeExecutionJob(service, submission)
assert.NotNil(t, job)
assert.Equal(t, submission, job.submission)
assert.Equal(t, service, job.service)
// We can't easily test the actual execution because it depends on Docker
// In a real test environment, you would mock the Docker calls
}

View File

@@ -1,17 +0,0 @@
package service
import (
"crypto/rand"
"fmt"
"time"
)
// GenerateUUID creates a random UUID
func GenerateUUID() string {
b := make([]byte, 16)
_, err := rand.Read(b)
if err != nil {
return fmt.Sprintf("%d", time.Now().UnixNano())
}
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}

View File

@@ -1,96 +0,0 @@
import requests
import concurrent.futures
import time
# Define the endpoint URLs
POST_URL = "http://localhost:8080/submit"
GET_URL = "http://localhost:8080/result?id={}"
# Define the request bodies
cpp_payload = {
"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 return 0;\n}""",
"input": "Alice"
}
java_payload = {
"language": "java",
"code": """import java.util.Scanner;\n\npublic class Solution {\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 scanner.close();\n }\n}""",
"input": "Jane"
}
def send_request(index):
"""Sends a POST request and returns the task ID."""
payload = cpp_payload if index % 2 == 0 else java_payload
for _ in range(3): # Retry up to 3 times
try:
response = requests.post(POST_URL, json=payload, timeout=10)
if response.status_code == 200:
task_id = response.json().get("id")
print(f"Request {index} sent. Task ID: {task_id}")
return task_id
except requests.exceptions.RequestException as e:
print(f"Request {index} failed: {e}")
time.sleep(1)
return None
def get_result(task_id):
"""Polls the result endpoint until completion."""
if not task_id:
return None
max_retries = 50 # Prevent infinite loop
retries = 0
while retries < max_retries:
try:
response = requests.get(GET_URL.format(task_id), timeout=10)
if response.status_code == 200:
result = response.json()
if result.get("status") == "completed":
print(f"Task {task_id} completed.")
return result
time.sleep(1) # Poll every second
retries += 1
except requests.exceptions.RequestException as e:
print(f"Error fetching result for {task_id}: {e}")
print(f"Task {task_id} did not complete in time.")
return None
def main():
start_time = time.time()
task_ids = []
print("Sending 500 requests...")
# Send 500 requests concurrently
with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
futures = {executor.submit(send_request, i): i for i in range(500)}
for future in concurrent.futures.as_completed(futures):
task_id = future.result()
if task_id:
task_ids.append(task_id)
print(f"Sent {len(task_ids)} requests. Waiting for results...")
# Fetch results
results = []
with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
futures = {executor.submit(get_result, task_id): task_id for task_id in task_ids}
for future in concurrent.futures.as_completed(futures):
result = future.result()
if result:
results.append(result)
# Calculate execution stats
total_time = time.time() - start_time
waiting_times = [r["totalTime"] for r in results if "totalTime" in r]
avg_waiting_time = sum(waiting_times) / len(waiting_times) if waiting_times else 0
print("\nExecution Stats:")
print(f"Total Execution Time: {total_time:.2f}s")
print(f"Total Requests Processed: {len(results)}/{len(task_ids)}")
print(f"Average Waiting Time: {avg_waiting_time:.2f}ms")
print(f"Min Waiting Time: {min(waiting_times, default=0)}ms")
print(f"Max Waiting Time: {max(waiting_times, default=0)}ms")
if __name__ == "__main__":
main()

View File

@@ -1,195 +0,0 @@
package tests
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/arnab-afk/monaco/handler"
"github.com/stretchr/testify/assert"
)
func setupTestServer() *httptest.Server {
h := handler.NewHandler()
mux := http.NewServeMux()
mux.HandleFunc("/submit", h.SubmitHandler)
mux.HandleFunc("/status", h.StatusHandler)
mux.HandleFunc("/result", h.ResultHandler)
mux.HandleFunc("/queue-stats", h.QueueStatsHandler)
return httptest.NewServer(mux)
}
func TestAPIIntegration(t *testing.T) {
server := setupTestServer()
defer server.Close()
// Test: Submit code, check status, and get results
// 1. Submit a Python job
submitURL := server.URL + "/submit"
body := map[string]string{
"language": "python",
"code": "print('Hello, Integration Test!')",
}
bodyBytes, _ := json.Marshal(body)
resp, err := http.Post(submitURL, "application/json", bytes.NewReader(bodyBytes))
assert.NoError(t, err)
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
// Get the job ID
var submitResp map[string]string
json.NewDecoder(resp.Body).Decode(&submitResp)
resp.Body.Close()
jobID := submitResp["id"]
assert.NotEmpty(t, jobID)
// 2. Check status
statusURL := server.URL + "/status?id=" + jobID
// Wait for job to complete (try multiple times)
var statusResp map[string]interface{}
maxRetries := 10
for i := 0; i < maxRetries; i++ {
resp, err = http.Get(statusURL)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
json.NewDecoder(resp.Body).Decode(&statusResp)
resp.Body.Close()
// If job completed or failed, break
status, _ := statusResp["status"].(string)
if status == "completed" || status == "failed" {
break
}
// Wait before next retry
time.Sleep(200 * time.Millisecond)
}
// 3. Get results
resultURL := server.URL + "/result?id=" + jobID
resp, err = http.Get(resultURL)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var resultResp map[string]interface{}
json.NewDecoder(resp.Body).Decode(&resultResp)
resp.Body.Close()
assert.Equal(t, jobID, resultResp["id"])
assert.Contains(t, resultResp["output"], "Hello, Integration Test!")
// 4. Check queue stats
statsURL := server.URL + "/queue-stats"
resp, err = http.Get(statsURL)
assert.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
var statsResp map[string]interface{}
json.NewDecoder(resp.Body).Decode(&statsResp)
resp.Body.Close()
assert.Contains(t, statsResp, "queue_stats")
assert.Contains(t, statsResp, "submissions")
}
func TestMultipleLanguageSubmissions(t *testing.T) {
server := setupTestServer()
defer server.Close()
// Test submissions for different languages
languages := []string{"python", "java", "c", "cpp"}
codes := map[string]string{
"python": "print('Hello from Python')",
"java": "public class Solution { public static void main(String[] args) { System.out.println(\"Hello from Java\"); } }",
"c": "#include <stdio.h>\nint main() { printf(\"Hello from C\\n\"); return 0; }",
"cpp": "#include <iostream>\nint main() { std::cout << \"Hello from C++\" << std::endl; return 0; }",
}
submitURL := server.URL + "/submit"
for _, lang := range languages {
body := map[string]string{
"language": lang,
"code": codes[lang],
}
bodyBytes, _ := json.Marshal(body)
resp, err := http.Post(submitURL, "application/json", bytes.NewReader(bodyBytes))
assert.NoError(t, err)
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
var submitResp map[string]string
json.NewDecoder(resp.Body).Decode(&submitResp)
resp.Body.Close()
jobID := submitResp["id"]
assert.NotEmpty(t, jobID)
// We don't wait for completion in this test
// This is just to verify submission acceptance for all languages
}
}
func TestInputHandling(t *testing.T) {
server := setupTestServer()
defer server.Close()
// Test code submission with input
submitURL := server.URL + "/submit"
body := map[string]string{
"language": "python",
"code": "name = input('Enter name: ')\nprint('Hello, ' + name + '!')",
"input": "Integration Test",
}
bodyBytes, _ := json.Marshal(body)
resp, err := http.Post(submitURL, "application/json", bytes.NewReader(bodyBytes))
assert.NoError(t, err)
assert.Equal(t, http.StatusAccepted, resp.StatusCode)
var submitResp map[string]string
json.NewDecoder(resp.Body).Decode(&submitResp)
resp.Body.Close()
jobID := submitResp["id"]
assert.NotEmpty(t, jobID)
// Wait for job to complete and check result
resultURL := server.URL + "/result?id=" + jobID
// Poll for results
var resultResp map[string]interface{}
maxRetries := 10
for i := 0; i < maxRetries; i++ {
time.Sleep(300 * time.Millisecond)
resp, err = http.Get(resultURL)
assert.NoError(t, err)
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
continue
}
json.NewDecoder(resp.Body).Decode(&resultResp)
resp.Body.Close()
status, _ := resultResp["status"].(string)
if status == "completed" || status == "failed" {
break
}
}
// Verify output contains the greeting with input
assert.Contains(t, resultResp["output"], "Hello, Integration Test!")
}

View File

@@ -1,278 +0,0 @@
import requests
import concurrent.futures
import time
import statistics
import matplotlib.pyplot as plt
import numpy as np
# Define the endpoint URLs
POST_URL = "http://localhost:8080/submit"
GET_URL_STATUS = "http://localhost:8080/status?id={}"
GET_URL_RESULT = "http://localhost:8080/result?id={}"
GET_URL_STATS = "http://localhost:8080/queue-stats"
# Test payloads for different languages
PAYLOADS = {
"python": {
"language": "python",
"code": "print('Hello, Load Test!')",
},
"java": {
"language": "java",
"code": "public class Solution { public static void main(String[] args) { System.out.println(\"Hello, Load Test!\"); } }",
},
"c": {
"language": "c",
"code": "#include <stdio.h>\nint main() { printf(\"Hello, Load Test!\\n\"); return 0; }",
},
"cpp": {
"language": "cpp",
"code": "#include <iostream>\nint main() { std::cout << \"Hello, Load Test!\" << std::endl; return 0; }",
}
}
def send_request(language, index):
"""Sends a POST request and returns (task_id, time_taken)."""
payload = PAYLOADS[language]
start_time = time.time()
try:
response = requests.post(POST_URL, json=payload, timeout=10)
end_time = time.time()
if response.status_code == 202:
return response.json().get("id"), end_time - start_time
else:
print(f"Request {index} failed with status {response.status_code}")
return None, end_time - start_time
except requests.exceptions.RequestException as e:
end_time = time.time()
print(f"Request {index} error: {e}")
return None, end_time - start_time
def wait_for_result(task_id, index):
"""Waits for a result and returns (result, time_taken)."""
if not task_id:
return None, 0
start_time = time.time()
max_retries = 30
retry_interval = 0.5 # seconds
for _ in range(max_retries):
try:
response = requests.get(GET_URL_RESULT.format(task_id), timeout=10)
if response.status_code == 200:
result = response.json()
if result.get("status") in ["completed", "failed"]:
end_time = time.time()
return result, end_time - start_time
time.sleep(retry_interval)
except requests.exceptions.RequestException as e:
print(f"Error checking result for task {index}: {e}")
end_time = time.time()
print(f"Timed out waiting for result of task {index}")
return None, end_time - start_time
def run_test(concurrency, requests_per_language):
"""Runs a load test with the specified parameters."""
languages = list(PAYLOADS.keys())
all_results = {lang: [] for lang in languages}
submit_times = {lang: [] for lang in languages}
wait_times = {lang: [] for lang in languages}
success_rates = {lang: 0 for lang in languages}
# Keep track of all submissions for each language
total_per_language = {lang: 0 for lang in languages}
successful_per_language = {lang: 0 for lang in languages}
start_time = time.time()
# Create a list of tasks
tasks = []
for lang in languages:
for i in range(requests_per_language):
tasks.append((lang, i))
print(f"Running load test with {concurrency} concurrent connections")
print(f"Sending {requests_per_language} requests per language ({len(languages)} languages)")
# Submit all tasks
task_ids = {}
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
future_to_task = {executor.submit(send_request, lang, i): (lang, i) for lang, i in tasks}
for future in concurrent.futures.as_completed(future_to_task):
lang, i = future_to_task[future]
total_per_language[lang] += 1
try:
task_id, submit_time = future.result()
if task_id:
task_ids[(lang, i)] = task_id
submit_times[lang].append(submit_time)
except Exception as e:
print(f"Error submitting {lang} task {i}: {e}")
print(f"Submitted {len(task_ids)} tasks successfully")
# Wait for all results
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
future_to_task = {executor.submit(wait_for_result, task_ids.get((lang, i)), i): (lang, i)
for lang, i in tasks if (lang, i) in task_ids}
for future in concurrent.futures.as_completed(future_to_task):
lang, i = future_to_task[future]
try:
result, wait_time = future.result()
if result and result.get("status") == "completed":
successful_per_language[lang] += 1
all_results[lang].append(result)
wait_times[lang].append(wait_time)
except Exception as e:
print(f"Error waiting for {lang} task {i}: {e}")
end_time = time.time()
total_time = end_time - start_time
# Calculate success rates
for lang in languages:
if total_per_language[lang] > 0:
success_rates[lang] = (successful_per_language[lang] / total_per_language[lang]) * 100
else:
success_rates[lang] = 0
# Calculate statistics
stats = {
"total_time": total_time,
"requests_per_second": len(task_ids) / total_time if total_time > 0 else 0,
"success_rate": sum(success_rates.values()) / len(success_rates) if success_rates else 0,
"submit_times": {
lang: {
"avg": statistics.mean(times) if times else 0,
"min": min(times) if times else 0,
"max": max(times) if times else 0,
"p95": np.percentile(times, 95) if times else 0
} for lang, times in submit_times.items()
},
"wait_times": {
lang: {
"avg": statistics.mean(times) if times else 0,
"min": min(times) if times else 0,
"max": max(times) if times else 0,
"p95": np.percentile(times, 95) if times else 0
} for lang, times in wait_times.items()
},
"success_rates": success_rates
}
return stats, all_results
def print_stats(stats):
"""Prints test statistics."""
print("\n=== Load Test Results ===")
print(f"Total time: {stats['total_time']:.2f}s")
print(f"Requests per second: {stats['requests_per_second']:.2f}")
print(f"Overall success rate: {stats['success_rate']:.2f}%")
print("\n== Submit Times (seconds) ==")
for lang, times in stats["submit_times"].items():
print(f"{lang:<6}: avg={times['avg']:.4f}, min={times['min']:.4f}, max={times['max']:.4f}, p95={times['p95']:.4f}")
print("\n== Wait Times (seconds) ==")
for lang, times in stats["wait_times"].items():
print(f"{lang:<6}: avg={times['avg']:.4f}, min={times['min']:.4f}, max={times['max']:.4f}, p95={times['p95']:.4f}")
print("\n== Success Rates ==")
for lang, rate in stats["success_rates"].items():
print(f"{lang:<6}: {rate:.2f}%")
def plot_results(stats):
"""Creates visualizations of test results."""
languages = list(stats["submit_times"].keys())
# Plot submit times
plt.figure(figsize=(12, 10))
plt.subplot(2, 2, 1)
plt.title("Average Submit Time by Language")
avg_times = [stats["submit_times"][lang]["avg"] for lang in languages]
plt.bar(languages, avg_times)
plt.ylabel("Time (seconds)")
plt.subplot(2, 2, 2)
plt.title("Average Wait Time by Language")
avg_wait_times = [stats["wait_times"][lang]["avg"] for lang in languages]
plt.bar(languages, avg_wait_times)
plt.ylabel("Time (seconds)")
plt.subplot(2, 2, 3)
plt.title("Success Rate by Language")
success_rates = [stats["success_rates"][lang] for lang in languages]
plt.bar(languages, success_rates)
plt.ylabel("Success Rate (%)")
plt.ylim(0, 100)
plt.subplot(2, 2, 4)
plt.title("95th Percentile Wait Time by Language")
p95_times = [stats["wait_times"][lang]["p95"] for lang in languages]
plt.bar(languages, p95_times)
plt.ylabel("Time (seconds)")
plt.tight_layout()
plt.savefig("load_test_results.png")
print("Results saved to load_test_results.png")
def main():
# Run tests with different concurrency levels
concurrency_levels = [10, 20, 30]
requests_per_language = 10
all_stats = []
for concurrency in concurrency_levels:
stats, results = run_test(concurrency, requests_per_language)
all_stats.append((concurrency, stats))
print_stats(stats)
# Create comparison visualization
plt.figure(figsize=(12, 8))
plt.subplot(2, 2, 1)
plt.title("Requests per Second vs Concurrency")
plt.plot([s[0] for s in all_stats], [s[1]["requests_per_second"] for s in all_stats], "o-")
plt.xlabel("Concurrency Level")
plt.ylabel("Requests per Second")
plt.subplot(2, 2, 2)
plt.title("Success Rate vs Concurrency")
plt.plot([s[0] for s in all_stats], [s[1]["success_rate"] for s in all_stats], "o-")
plt.xlabel("Concurrency Level")
plt.ylabel("Success Rate (%)")
plt.ylim(0, 100)
plt.subplot(2, 2, 3)
plt.title("Average Submit Time vs Concurrency")
for lang in PAYLOADS.keys():
plt.plot([s[0] for s in all_stats],
[s[1]["submit_times"][lang]["avg"] for s in all_stats],
"o-", label=lang)
plt.xlabel("Concurrency Level")
plt.ylabel("Average Submit Time (s)")
plt.legend()
plt.subplot(2, 2, 4)
plt.title("Average Wait Time vs Concurrency")
for lang in PAYLOADS.keys():
plt.plot([s[0] for s in all_stats],
[s[1]["wait_times"][lang]["avg"] for s in all_stats],
"o-", label=lang)
plt.xlabel("Concurrency Level")
plt.ylabel("Average Wait Time (s)")
plt.legend()
plt.tight_layout()
plt.savefig("concurrency_comparison.png")
print("Concurrency comparison saved to concurrency_comparison.png")
# Plot detailed results for the highest concurrency test
plot_results(all_stats[-1][1])
if __name__ == "__main__":
main()

View File

@@ -1 +0,0 @@
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

View File

@@ -1,3 +0,0 @@
# Very basic input example
name = input("What is your name? ")
print(f"Hello, {name}!")

View File

@@ -1,41 +0,0 @@
// 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();

View File

@@ -1,24 +0,0 @@
# 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()

View File

@@ -1,22 +0,0 @@
// 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();
});
});
});

View File

@@ -1,13 +0,0 @@
# 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!")

View File

@@ -1,5 +0,0 @@
# Simple input example
name = input("Enter your name: ")
print(f"Hello, {name}!")
for i in range(5):
print(f"Count: {i}")

37
new-backend/Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
FROM golang:1.19-alpine AS builder
# Install git and required dependencies
RUN apk update && apk add --no-cache git
# Set working directory
WORKDIR /app
# Copy go mod and sum files
COPY go.mod go.sum* ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the application with optimizations
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o monaco-backend .
# Use a smaller image for the final container
FROM alpine:latest
# Install Docker client (required for container-in-container execution)
RUN apk update && apk add --no-cache docker-cli
# Create a non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Copy the binary from builder
COPY --from=builder /app/monaco-backend /monaco-backend
# Use non-root user
USER appuser
# Run the binary
ENTRYPOINT ["/monaco-backend"]

View File

@@ -0,0 +1,74 @@
FROM golang:1.19-alpine AS builder
# Install git and required dependencies
RUN apk update && apk add --no-cache git
# Set working directory
WORKDIR /app
# Copy go mod and sum files
COPY go.mod go.sum* ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the application with optimizations
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o monaco-backend .
# Final stage
FROM alpine:latest
# Install Docker client and supervisor
RUN apk update && apk add --no-cache docker-cli supervisor wget
# Get cloudflared directly from GitHub (more reliable than the tarball)
RUN wget -O cloudflared https://github.com/cloudflare/cloudflared/releases/download/2023.5.0/cloudflared-linux-amd64 && \
chmod +x cloudflared && \
mv cloudflared /usr/local/bin/
# Create directories for cloudflared
RUN mkdir -p /etc/cloudflared
# Copy the certificate file and config
COPY cert.pem /etc/cloudflared/cert.pem
COPY credentials.json /etc/cloudflared/credentials.json
COPY config.json /etc/cloudflared/config.json
# Setup DNS routing for the tunnel (only needs to be done once)
RUN cloudflared tunnel route dns 5d2682ef-0b5b-47e5-b0fa-ad48968ce016 api.ishikabhoyar.tech || echo "DNS routing already set up or failed - continuing anyway"
# Copy the binary from builder
COPY --from=builder /app/monaco-backend /monaco-backend
# Create supervisord config
RUN mkdir -p /etc/supervisor/conf.d/
RUN echo "[supervisord]" > /etc/supervisor/conf.d/supervisord.conf && \
echo "nodaemon=true" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "user=root" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "[program:backend]" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "command=/monaco-backend" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "autostart=true" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "autorestart=true" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "stdout_logfile=/dev/stdout" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "stdout_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "stderr_logfile=/dev/stderr" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "stderr_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "[program:cloudflared]" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "command=cloudflared tunnel --config /etc/cloudflared/config.json run" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "autostart=true" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "autorestart=true" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "stdout_logfile=/dev/stdout" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "stdout_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "stderr_logfile=/dev/stderr" >> /etc/supervisor/conf.d/supervisord.conf && \
echo "stderr_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf
# Expose port for local access
EXPOSE 8080
# Use supervisord to manage processes
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@@ -0,0 +1,66 @@
FROM golang:1.19-alpine AS builder
# Install git and required dependencies
RUN apk update && apk add --no-cache git
# Set working directory
WORKDIR /app
# Copy go mod and sum files
COPY go.mod go.sum* ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the application with optimizations
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o monaco-backend .
# Final stage
FROM alpine:latest
# Install Docker client and tools
RUN apk update && apk add --no-cache docker-cli bash wget
# Get cloudflared directly
RUN wget -O cloudflared https://github.com/cloudflare/cloudflared/releases/download/2023.7.3/cloudflared-linux-amd64 && \
chmod +x cloudflared && \
mv cloudflared /usr/local/bin/
# Create directories for cloudflared
RUN mkdir -p /etc/cloudflared
# Copy the certificate file and config
COPY cert.pem /etc/cloudflared/cert.pem
COPY config.json /etc/cloudflared/config.json
# Copy the binary from builder
COPY --from=builder /app/monaco-backend /monaco-backend
# Create a simple bash script to run both services
RUN echo '#!/bin/bash\n\
# Start the backend in the background\n\
/monaco-backend &\n\
BACKEND_PID=$!\n\
echo "Backend started with PID: $BACKEND_PID"\n\
\n\
# Wait for backend to start\n\
echo "Waiting for backend to initialize..."\n\
sleep 5\n\
\n\
# Start cloudflared with proper arguments\n\
echo "Starting Cloudflare tunnel to api.ishikabhoyar.tech..."\n\
# Use the specific tunnel name from config.json\n\
cloudflared tunnel run monaco-backend-tunnel\n\
\n\
# If cloudflared exits, kill the backend\n\
kill $BACKEND_PID\n\
' > /start.sh && chmod +x /start.sh
# Expose port for local access
EXPOSE 8080
# Run the startup script
CMD ["/start.sh"]

21
new-backend/Makefile Normal file
View File

@@ -0,0 +1,21 @@
.PHONY: build run run-detached stop logs
# Build the image
build:
docker-compose -f docker-compose.tunnel.yml build
# Run the container
run: build
docker-compose -f docker-compose.tunnel.yml up
# Run in detached mode
run-detached: build
docker-compose -f docker-compose.tunnel.yml up -d
# Stop the container
stop:
docker-compose -f docker-compose.tunnel.yml down
# View logs
logs:
docker-compose -f docker-compose.tunnel.yml logs -f

93
new-backend/README.md Normal file
View File

@@ -0,0 +1,93 @@
# Monaco Code Execution Backend
A modern, secure, and efficient code execution backend inspired by online code editors like Programiz. This backend is written in Go and uses Docker containers for secure code execution.
## Features
- **Multi-language Support**: Execute code in Python, Java, C, C++, JavaScript, and Go
- **Real-time Output**: Stream code execution output via WebSockets
- **Interactive Input**: Send input to running programs via WebSockets
- **Secure Execution**: All code runs in isolated Docker containers
- **Resource Limits**: Memory, CPU, and execution time limits
- **Scalable Architecture**: Concurrent execution with configurable worker pools
## Requirements
- Go 1.19+
- Docker
- Git (for development)
## Getting Started
### Running Locally
1. Clone the repository:
```bash
git clone https://github.com/your-username/monaco.git
cd monaco/new-backend
```
2. Install dependencies:
```bash
go mod download
```
3. Build and run:
```bash
go run main.go
```
The server will start on `http://localhost:8080` by default.
### Using Docker
Build and run using Docker:
```bash
docker build -t monaco-backend .
docker run -p 8080:8080 -v /var/run/docker.sock:/var/run/docker.sock monaco-backend
```
Note: Mounting the Docker socket is necessary for container-in-container execution.
## API Endpoints
- `POST /api/submit`: Submit code for execution
- `GET /api/status/{id}`: Get execution status
- `GET /api/result/{id}`: Get complete execution result
- `GET /api/languages`: List supported languages
- `GET /api/health`: Health check endpoint
- `WS /api/ws/terminal/{id}`: WebSocket for real-time output
## WebSocket Communication
The `/api/ws/terminal/{id}` endpoint supports these message types:
- `output`: Code execution output
- `input`: User input to the program
- `input_prompt`: Input prompt detected
- `status`: Execution status updates
- `error`: Error messages
## Configuration
Configuration is handled through environment variables:
- `PORT`: Server port (default: 8080)
- `CONCURRENT_EXECUTIONS`: Number of concurrent executions (default: 5)
- `QUEUE_CAPACITY`: Execution queue capacity (default: 100)
- `DEFAULT_TIMEOUT`: Default execution timeout in seconds (default: 30)
See `config/config.go` for more configuration options.
## Security Considerations
- All code execution happens in isolated Docker containers
- Network access is disabled in execution containers
- Memory and CPU limits are enforced
- Process limits prevent fork bombs
- Execution timeouts prevent infinite loops
## License
MIT

View File

@@ -0,0 +1,46 @@
# Backend with Cloudflare Tunnel
This setup runs the Monaco backend service and establishes a Cloudflare tunnel, exposing the service to the internet securely via api.ishikabhoyar.tech.
## Prerequisites
- Docker and Docker Compose installed
- The Cloudflare tunnel certificate (cert.pem) in the same directory as the Dockerfile.tunnel
## Files
- `Dockerfile.tunnel`: Dockerfile that builds the backend and sets up Cloudflare tunnel
- `cert.pem`: Cloudflare tunnel certificate
- `config.json`: Cloudflare tunnel configuration that routes traffic to api.ishikabhoyar.tech
- `docker-compose.tunnel.yml`: Docker Compose configuration for easy deployment
## How to Run
```bash
# Build and start the container
docker-compose -f docker-compose.tunnel.yml up -d
# Check logs
docker-compose -f docker-compose.tunnel.yml logs -f
```
## How it Works
1. The Dockerfile builds the Go backend application
2. It installs the Cloudflare tunnel client (cloudflared)
3. On container start:
- The backend server starts on port 8080
- The Cloudflare tunnel connects to Cloudflare's edge network using the config.json
- External traffic to api.ishikabhoyar.tech is routed through the tunnel to the backend
- The cloudflared runs entirely within the container, isolated from any host cloudflared instance
## Environment Variables
You can customize the behavior by modifying the environment variables in the docker-compose.tunnel.yml file:
- `PORT`: The port the backend server listens on (default: 8080)
- `CONCURRENT_EXECUTIONS`: Number of concurrent code executions (default: 5)
- `QUEUE_CAPACITY`: Maximum queue capacity for code executions (default: 100)
- `DEFAULT_TIMEOUT`: Default timeout for code execution in seconds (default: 30)
- `SANDBOX_NETWORK_DISABLED`: Whether to disable network in sandbox containers (default: true)
- `SANDBOX_PIDS_LIMIT`: Process ID limit for sandbox containers (default: 50)

175
new-backend/api/handler.go Normal file
View File

@@ -0,0 +1,175 @@
package api
import (
"encoding/json"
"log"
"net/http"
"time"
"github.com/google/uuid"
"github.com/gorilla/mux"
"github.com/gorilla/websocket"
"github.com/ishikabhoyar/monaco/new-backend/executor"
"github.com/ishikabhoyar/monaco/new-backend/models"
)
// Handler manages all API routes
type Handler struct {
executor *executor.CodeExecutor
upgrader websocket.Upgrader
}
// NewHandler creates a new API handler
func NewHandler(executor *executor.CodeExecutor) *Handler {
return &Handler{
executor: executor,
upgrader: websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true // Allow all origins for development
},
HandshakeTimeout: 10 * time.Second,
},
}
}
// RegisterRoutes sets up all API routes
func (h *Handler) RegisterRoutes(router *mux.Router) {
// Code execution endpoints
router.HandleFunc("/api/submit", h.SubmitCodeHandler).Methods("POST")
router.HandleFunc("/api/status/{id}", h.StatusHandler).Methods("GET")
router.HandleFunc("/api/result/{id}", h.ResultHandler).Methods("GET")
// WebSocket endpoint for real-time output
router.HandleFunc("/api/ws/terminal/{id}", h.TerminalWebSocketHandler)
// Language support endpoint
router.HandleFunc("/api/languages", h.SupportedLanguagesHandler).Methods("GET")
// Health check
router.HandleFunc("/api/health", h.HealthCheckHandler).Methods("GET")
}
// SubmitCodeHandler handles code submission requests
func (h *Handler) SubmitCodeHandler(w http.ResponseWriter, r *http.Request) {
// Parse request
var submission models.CodeSubmission
if err := json.NewDecoder(r.Body).Decode(&submission); err != nil {
http.Error(w, "Invalid request format", http.StatusBadRequest)
return
}
// Validate request
if submission.Code == "" {
http.Error(w, "Code cannot be empty", http.StatusBadRequest)
return
}
if submission.Language == "" {
http.Error(w, "Language must be specified", http.StatusBadRequest)
return
}
// Generate ID if not provided
if submission.ID == "" {
submission.ID = uuid.New().String()
}
// Submit code for execution
id := h.executor.SubmitCode(&submission)
// Return response
response := models.SubmissionResponse{
ID: id,
Status: "queued",
Message: "Code submission accepted and queued for execution",
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
// StatusHandler returns the current status of a code execution
func (h *Handler) StatusHandler(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
submission, exists := h.executor.GetSubmission(id)
if !exists {
http.Error(w, "Submission not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"id": submission.ID,
"status": submission.Status,
})
}
// ResultHandler returns the complete result of a code execution
func (h *Handler) ResultHandler(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
submission, exists := h.executor.GetSubmission(id)
if !exists {
http.Error(w, "Submission not found", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(submission)
}
// TerminalWebSocketHandler handles WebSocket connections for real-time output
func (h *Handler) TerminalWebSocketHandler(w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
// Check if submission exists
if _, exists := h.executor.GetSubmission(id); !exists {
http.Error(w, "Submission not found", http.StatusNotFound)
return
}
// Upgrade connection to WebSocket
conn, err := h.upgrader.Upgrade(w, r, nil)
if err != nil {
log.Printf("WebSocket upgrade failed: %v", err)
return
}
log.Printf("WebSocket connection established for submission %s", id)
// Register connection
h.executor.RegisterTerminalConnection(id, conn)
// Connection will be handled by the executor
}
// SupportedLanguagesHandler returns a list of supported languages
func (h *Handler) SupportedLanguagesHandler(w http.ResponseWriter, r *http.Request) {
// This is a placeholder - in a real implementation, you'd get this from the config
languages := []map[string]string{
{"id": "python", "name": "Python", "version": "3.9"},
{"id": "java", "name": "Java", "version": "11"},
{"id": "c", "name": "C", "version": "GCC 10.2"},
{"id": "cpp", "name": "C++", "version": "GCC 10.2"},
{"id": "javascript", "name": "JavaScript", "version": "Node.js 16"},
{"id": "golang", "name": "Go", "version": "1.19"},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(languages)
}
// HealthCheckHandler provides a simple health check endpoint
func (h *Handler) HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "ok",
"time": time.Now().Format(time.RFC3339),
})
}

15
new-backend/config.json Normal file
View File

@@ -0,0 +1,15 @@
{
"tunnel": "5d2682ef-0b5b-47e5-b0fa-ad48968ce016",
"credentials-file": "/etc/cloudflared/credentials.json",
"ingress": [
{
"hostname": "api.ishikabhoyar.tech",
"service": "http://localhost:8080"
},
{
"service": "http_status:404"
}
],
"protocol": "http2",
"loglevel": "info"
}

View File

@@ -0,0 +1,13 @@
{
"tunnel": "monaco-backend-tunnel",
"credentials-file": "/etc/cloudflared/cert.pem",
"ingress": [
{
"hostname": "api.ishikabhoyar.tech",
"service": "http://localhost:8080"
},
{
"service": "http_status:404"
}
]
}

View File

@@ -0,0 +1,168 @@
package config
import (
"os"
"strconv"
"time"
)
// Config holds all configuration for the application
type Config struct {
Server ServerConfig
Executor ExecutorConfig
Languages map[string]LanguageConfig
Sandbox SandboxConfig
}
// ServerConfig holds server-related configurations
type ServerConfig struct {
Port string
ReadTimeout time.Duration
WriteTimeout time.Duration
IdleTimeout time.Duration
}
// ExecutorConfig holds executor-related configurations
type ExecutorConfig struct {
ConcurrentExecutions int
QueueCapacity int
DefaultTimeout time.Duration
}
// LanguageConfig holds language-specific configurations
type LanguageConfig struct {
Name string
Image string
MemoryLimit string
CPULimit string
TimeoutSec int
CompileCmd []string
RunCmd []string
FileExt string
VersionCmd []string
}
// SandboxConfig holds sandbox-related configurations
type SandboxConfig struct {
NetworkDisabled bool
MemorySwapLimit string
PidsLimit int64
}
// GetConfig returns the application configuration
func GetConfig() *Config {
return &Config{
Server: ServerConfig{
Port: getEnv("PORT", "8080"),
ReadTimeout: time.Duration(getEnvAsInt("READ_TIMEOUT", 15)) * time.Second,
WriteTimeout: time.Duration(getEnvAsInt("WRITE_TIMEOUT", 15)) * time.Second,
IdleTimeout: time.Duration(getEnvAsInt("IDLE_TIMEOUT", 90)) * time.Second,
},
Executor: ExecutorConfig{
ConcurrentExecutions: getEnvAsInt("CONCURRENT_EXECUTIONS", 100),
QueueCapacity: getEnvAsInt("QUEUE_CAPACITY", 1000),
DefaultTimeout: time.Duration(getEnvAsInt("DEFAULT_TIMEOUT", 30)) * time.Second,
},
Languages: getLanguageConfigs(),
Sandbox: SandboxConfig{
NetworkDisabled: getEnvAsBool("SANDBOX_NETWORK_DISABLED", true),
MemorySwapLimit: getEnv("SANDBOX_MEMORY_SWAP_LIMIT", "0"),
PidsLimit: int64(getEnvAsInt("SANDBOX_PIDS_LIMIT", 50)),
},
}
}
// getLanguageConfigs returns configurations for all supported languages
func getLanguageConfigs() map[string]LanguageConfig {
return map[string]LanguageConfig{
"python": {
Name: "Python",
Image: "python:3.9-slim",
MemoryLimit: "100m",
CPULimit: "0.1",
TimeoutSec: 90,
RunCmd: []string{"python", "-c"},
FileExt: ".py",
VersionCmd: []string{"python", "--version"},
},
"java": {
Name: "Java",
Image: "eclipse-temurin:11-jdk",
MemoryLimit: "400m",
CPULimit: "0.5",
TimeoutSec: 100,
CompileCmd: []string{"javac"},
RunCmd: []string{"java"},
FileExt: ".java",
VersionCmd: []string{"java", "-version"},
},
"c": {
Name: "C",
Image: "gcc:latest",
MemoryLimit: "100m",
CPULimit: "0.1",
TimeoutSec: 90,
CompileCmd: []string{"gcc", "-o", "program"},
RunCmd: []string{"./program"},
FileExt: ".c",
VersionCmd: []string{"gcc", "--version"},
},
"cpp": {
Name: "C++",
Image: "gcc:latest",
MemoryLimit: "100m",
CPULimit: "0.1",
TimeoutSec: 90,
CompileCmd: []string{"g++", "-o", "program"},
RunCmd: []string{"./program"},
FileExt: ".cpp",
VersionCmd: []string{"g++", "--version"},
},
"javascript": {
Name: "JavaScript",
Image: "node:16-alpine",
MemoryLimit: "100m",
CPULimit: "0.1",
TimeoutSec: 90,
RunCmd: []string{"node", "-e"},
FileExt: ".js",
VersionCmd: []string{"node", "--version"},
},
"golang": {
Name: "Go",
Image: "golang:1.19-alpine",
MemoryLimit: "100m",
CPULimit: "0.1",
TimeoutSec: 90,
CompileCmd: []string{"go", "build", "-o", "program"},
RunCmd: []string{"./program"},
FileExt: ".go",
VersionCmd: []string{"go", "version"},
},
}
}
// Helper functions to get environment variables with defaults
func getEnv(key, defaultValue string) string {
value := os.Getenv(key)
if value == "" {
return defaultValue
}
return value
}
func getEnvAsInt(key string, defaultValue int) int {
valueStr := getEnv(key, "")
if value, err := strconv.Atoi(valueStr); err == nil {
return value
}
return defaultValue
}
func getEnvAsBool(key string, defaultValue bool) bool {
valueStr := getEnv(key, "")
if value, err := strconv.ParseBool(valueStr); err == nil {
return value
}
return defaultValue
}

View File

@@ -0,0 +1 @@
{"AccountTag":"453afb9373a00a55876e6127cf0efd97","TunnelSecret":"02VClcBt+1nxxu8ioUzw/UizXtKKh4X7UUpneVbfB/Y=","TunnelID":"5d2682ef-0b5b-47e5-b0fa-ad48968ce016"}

View File

@@ -0,0 +1,28 @@
services:
backend:
build:
context: .
dockerfile: Dockerfile.tunnel
restart: unless-stopped
volumes:
- //var/run/docker.sock:/var/run/docker.sock
# Port is only exposed locally, traffic comes through the tunnel
ports:
- "127.0.0.1:8080:8080"
environment:
- PORT=8080
- CONCURRENT_EXECUTIONS=5
- QUEUE_CAPACITY=100
- DEFAULT_TIMEOUT=30
- SANDBOX_NETWORK_DISABLED=true
- SANDBOX_PIDS_LIMIT=50
# Define cloudflared environment variables
- TUNNEL_ORIGIN_CERT=/etc/cloudflared/cert.pem
- NO_AUTOUPDATE=true
# Isolated network to prevent conflicts with host cloudflared
networks:
- monaco-backend-network
networks:
monaco-backend-network:
driver: bridge

View File

@@ -0,0 +1,695 @@
package executor
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"github.com/ishikabhoyar/monaco/new-backend/config"
"github.com/ishikabhoyar/monaco/new-backend/models"
)
// CodeExecutor handles code execution for all languages
type CodeExecutor struct {
config *config.Config
execQueue chan *models.CodeSubmission
submissions map[string]*models.CodeSubmission
submissionsMutex sync.RWMutex
terminalConnections map[string][]*websocket.Conn
terminalMutex sync.RWMutex
inputChannels map[string]chan string
inputMutex sync.RWMutex
}
// NewCodeExecutor creates a new code executor with specified capacity
func NewCodeExecutor(cfg *config.Config) *CodeExecutor {
executor := &CodeExecutor{
config: cfg,
execQueue: make(chan *models.CodeSubmission, cfg.Executor.QueueCapacity),
submissions: make(map[string]*models.CodeSubmission),
terminalConnections: make(map[string][]*websocket.Conn),
inputChannels: make(map[string]chan string),
}
// Start worker goroutines
for i := 0; i < cfg.Executor.ConcurrentExecutions; i++ {
go executor.worker(i)
}
log.Printf("Started %d code execution workers", cfg.Executor.ConcurrentExecutions)
return executor
}
// SubmitCode adds a code submission to the execution queue
func (e *CodeExecutor) SubmitCode(submission *models.CodeSubmission) string {
// Generate ID if not provided
if submission.ID == "" {
submission.ID = uuid.New().String()
}
submission.Status = "queued"
submission.QueuedAt = time.Now()
// Store submission
e.submissionsMutex.Lock()
e.submissions[submission.ID] = submission
e.submissionsMutex.Unlock()
// Send to execution queue
e.execQueue <- submission
log.Printf("Submission queued: %s, language: %s", submission.ID, submission.Language)
return submission.ID
}
// GetSubmission returns a submission by ID
func (e *CodeExecutor) GetSubmission(id string) (*models.CodeSubmission, bool) {
e.submissionsMutex.RLock()
defer e.submissionsMutex.RUnlock()
submission, exists := e.submissions[id]
return submission, exists
}
// RegisterTerminalConnection registers a WebSocket connection for streaming output
func (e *CodeExecutor) RegisterTerminalConnection(submissionID string, conn *websocket.Conn) {
e.terminalMutex.Lock()
defer e.terminalMutex.Unlock()
e.terminalConnections[submissionID] = append(e.terminalConnections[submissionID], conn)
log.Printf("WebSocket connection registered for submission %s (total: %d)",
submissionID, len(e.terminalConnections[submissionID]))
// Set up a reader to handle input from WebSocket
go e.handleTerminalInput(submissionID, conn)
}
// UnregisterTerminalConnection removes a WebSocket connection
func (e *CodeExecutor) UnregisterTerminalConnection(submissionID string, conn *websocket.Conn) {
e.terminalMutex.Lock()
defer e.terminalMutex.Unlock()
connections := e.terminalConnections[submissionID]
for i, c := range connections {
if c == conn {
// Remove the connection
e.terminalConnections[submissionID] = append(connections[:i], connections[i+1:]...)
break
}
}
// Clean up if no more connections
if len(e.terminalConnections[submissionID]) == 0 {
delete(e.terminalConnections, submissionID)
}
log.Printf("WebSocket connection unregistered for submission %s", submissionID)
}
// handleTerminalInput reads input from the WebSocket and forwards it to the running process
func (e *CodeExecutor) handleTerminalInput(submissionID string, conn *websocket.Conn) {
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Printf("Error reading WebSocket message: %v", err)
break
}
// Try to parse the message as JSON first
var inputMessage struct {
Type string `json:"type"`
Content string `json:"content"`
}
inputText := string(message)
if err := json.Unmarshal(message, &inputMessage); err == nil && inputMessage.Type == "input" {
// It's a structured input message
inputText = inputMessage.Content
}
// Now get the input channel
e.inputMutex.Lock()
inputChan, exists := e.inputChannels[submissionID]
e.inputMutex.Unlock()
if exists {
select {
case inputChan <- inputText:
log.Printf("Input sent to process: %s", inputText)
default:
log.Printf("Failed to send input: channel full or closed")
}
} else {
log.Printf("No input channel for submission %s", submissionID)
}
}
// When connection is closed, unregister it
e.UnregisterTerminalConnection(submissionID, conn)
}
// sendToTerminals sends output to all registered WebSocket connections
func (e *CodeExecutor) sendToTerminals(submissionID string, message models.WebSocketMessage) {
e.terminalMutex.RLock()
connections := e.terminalConnections[submissionID]
e.terminalMutex.RUnlock()
if len(connections) == 0 {
return
}
for _, conn := range connections {
err := conn.WriteJSON(message)
if err != nil {
log.Printf("WebSocket write error: %v", err)
// Consider unregistering the connection on error
}
}
}
// worker processes code execution jobs from the queue
func (e *CodeExecutor) worker(id int) {
log.Printf("Worker %d started", id)
for submission := range e.execQueue {
log.Printf("Worker %d processing submission %s (%s)", id, submission.ID, submission.Language)
// Update status to running
submission.Status = "running"
submission.StartedAt = time.Now()
e.sendToTerminals(submission.ID, models.NewStatusMessage("running", "", ""))
// Execute the code according to language
e.executeCode(submission)
// Update completion time
submission.CompletedAt = time.Now()
executionTime := submission.CompletedAt.Sub(submission.StartedAt).Seconds()
submission.ExecutionTime = executionTime
// Send completion status
e.sendToTerminals(submission.ID, models.NewStatusMessage(submission.Status, "", ""))
// Send a notification that terminal will close soon
e.sendToTerminals(submission.ID, models.NewSystemMessage("Connection will close in 5 seconds"))
// Add delay to keep the connection open longer
time.Sleep(5 * time.Second)
log.Printf("Worker %d completed submission %s in %.2f seconds", id, submission.ID, executionTime)
}
}
// executeCode orchestrates the execution of code for different languages
func (e *CodeExecutor) executeCode(submission *models.CodeSubmission) {
langConfig, exists := e.config.Languages[strings.ToLower(submission.Language)]
if !exists {
submission.Status = "failed"
submission.Output = "Unsupported language: " + submission.Language
return
}
// Create a temporary directory for this submission
tempDir, err := os.MkdirTemp("", fmt.Sprintf("%s-code-%s-", submission.Language, submission.ID))
if err != nil {
submission.Status = "failed"
submission.Output = "Failed to create execution environment: " + err.Error()
return
}
defer os.RemoveAll(tempDir)
// Choose execution strategy based on language
switch strings.ToLower(submission.Language) {
case "python":
e.executePython(submission, tempDir, langConfig)
case "java":
e.executeJava(submission, tempDir, langConfig)
case "c":
e.executeC(submission, tempDir, langConfig)
case "cpp":
e.executeCpp(submission, tempDir, langConfig)
case "javascript":
e.executeJavaScript(submission, tempDir, langConfig)
case "golang":
e.executeGolang(submission, tempDir, langConfig)
default:
submission.Status = "failed"
submission.Output = "Unsupported language: " + submission.Language
}
}
// executePython executes Python code
func (e *CodeExecutor) executePython(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
// Write code to file
codeFile := filepath.Join(tempDir, "code"+langConfig.FileExt)
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
// Setup Docker run command with unbuffered Python output
cmd := exec.Command(
"docker", "run", "--rm", "-i",
"--network=none",
"--memory="+langConfig.MemoryLimit,
"--cpu-quota="+fmt.Sprintf("%d", int(float64(100000)*0.1)), // 10% CPU
"--pids-limit=20",
"-v", tempDir+":/code",
"-e", "PYTHONUNBUFFERED=1", // Force Python to be unbuffered
langConfig.Image,
"python", "-u", "/code/code.py", // Add -u flag for unbuffered I/O
)
// Execute with increased timeout for interactive programs
e.executeWithIO(cmd, submission, time.Duration(langConfig.TimeoutSec)*time.Second)
}
// executeJava executes Java code
func (e *CodeExecutor) executeJava(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
// Extract class name from code
className := extractJavaClassName(submission.Code)
// Write code to file
codeFile := filepath.Join(tempDir, className+langConfig.FileExt)
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
// Compile Java code
compileCmd := exec.Command(
"docker", "run", "--rm",
"-v", tempDir+":/code",
langConfig.Image,
"javac", "/code/"+className+".java",
)
compileOutput, compileErr := compileCmd.CombinedOutput()
if compileErr != nil {
submission.Status = "failed"
submission.Output = "Compilation error:\n" + string(compileOutput)
e.sendToTerminals(submission.ID, models.NewOutputMessage(string(compileOutput), true))
return
}
// Setup Docker run command for execution
cmd := exec.Command(
"docker", "run", "--rm", "-i",
"--network=none",
"--memory="+langConfig.MemoryLimit,
"--cpu-quota="+fmt.Sprintf("%d", int(float64(100000)*0.5)), // 50% CPU
"--pids-limit=20",
"-v", tempDir+":/code",
langConfig.Image,
"java", "-XX:+TieredCompilation", "-XX:TieredStopAtLevel=1",
"-Xms64m", "-Xmx256m",
"-cp", "/code", className,
)
// Execute the code with input handling
e.executeWithIO(cmd, submission, time.Duration(langConfig.TimeoutSec)*time.Second)
}
// executeC executes C code
func (e *CodeExecutor) executeC(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
// Write code to file
codeFile := filepath.Join(tempDir, "code"+langConfig.FileExt)
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
// Create a wrapper script that will include setbuf to disable buffering
wrapperCode := `#include <stdio.h>
// Forward declaration of user's main function
int user_main();
int main() {
// Disable buffering completely for stdout
setbuf(stdout, NULL);
// Call the user's code
return user_main();
}
// User's code begins here
`
// Modify the user's code to be a function called from our wrapper
modifiedCode := submission.Code
// Replace main function with our wrapper
mainRegex := regexp.MustCompile(`int\s+main\s*\([^)]*\)\s*{`)
if mainRegex.MatchString(modifiedCode) {
// Rename user's main to user_main
modifiedCode = mainRegex.ReplaceAllString(modifiedCode, "int user_main() {")
// Combine wrapper with modified user code
finalCode := wrapperCode + modifiedCode
// Write the final code with wrapper to file
if err := os.WriteFile(codeFile, []byte(finalCode), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
} else {
// If no main function found, create a minimal program that includes the user code
finalCode := `#include <stdio.h>
int main() {
// Disable buffering completely for stdout
setbuf(stdout, NULL);
// Execute the user's code
` + submission.Code + `
return 0;
}
`
// Write the final code to file
if err := os.WriteFile(codeFile, []byte(finalCode), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
}
// Compile C code
compileCmd := exec.Command(
"docker", "run", "--rm",
"-v", tempDir+":/code",
langConfig.Image,
"gcc", "-o", "/code/program", "/code/code.c",
)
compileOutput, compileErr := compileCmd.CombinedOutput()
if compileErr != nil {
submission.Status = "failed"
submission.Output = "Compilation error:\n" + string(compileOutput)
e.sendToTerminals(submission.ID, models.NewOutputMessage(string(compileOutput), true))
return
}
// Setup Docker run command
cmd := exec.Command(
"docker", "run", "--rm", "-i",
"--network=none",
"--memory="+langConfig.MemoryLimit,
"--cpu-quota="+fmt.Sprintf("%d", int(float64(100000)*0.1)), // 10% CPU
"--pids-limit=20",
"-v", tempDir+":/code",
langConfig.Image,
"/code/program",
)
// Execute the code with input handling
e.executeWithIO(cmd, submission, time.Duration(langConfig.TimeoutSec)*time.Second)
}
// executeCpp executes C++ code
func (e *CodeExecutor) executeCpp(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
// Write code to file
codeFile := filepath.Join(tempDir, "code"+langConfig.FileExt)
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
// Compile C++ code
compileCmd := exec.Command(
"docker", "run", "--rm",
"-v", tempDir+":/code",
langConfig.Image,
"g++", "-o", "/code/program", "/code/code.cpp",
)
compileOutput, compileErr := compileCmd.CombinedOutput()
if compileErr != nil {
submission.Status = "failed"
submission.Output = "Compilation error:\n" + string(compileOutput)
e.sendToTerminals(submission.ID, models.NewOutputMessage(string(compileOutput), true))
return
}
// Setup Docker run command
cmd := exec.Command(
"docker", "run", "--rm", "-i",
"--network=none",
"--memory="+langConfig.MemoryLimit,
"--cpu-quota="+fmt.Sprintf("%d", int(float64(100000)*0.1)), // 10% CPU
"--pids-limit=20",
"-v", tempDir+":/code",
langConfig.Image,
"/code/program",
)
// Execute the code with input handling
e.executeWithIO(cmd, submission, time.Duration(langConfig.TimeoutSec)*time.Second)
}
// executeJavaScript executes JavaScript code
func (e *CodeExecutor) executeJavaScript(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
// Write code to file
codeFile := filepath.Join(tempDir, "code"+langConfig.FileExt)
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
// Setup Docker run command
cmd := exec.Command(
"docker", "run", "--rm", "-i",
"--network=none",
"--memory="+langConfig.MemoryLimit,
"--cpu-quota="+fmt.Sprintf("%d", int(float64(100000)*0.1)), // 10% CPU
"--pids-limit=20",
"-v", tempDir+":/code",
langConfig.Image,
"node", "/code/code.js",
)
// Execute the code with input handling
e.executeWithIO(cmd, submission, time.Duration(langConfig.TimeoutSec)*time.Second)
}
// executeGolang executes Go code
func (e *CodeExecutor) executeGolang(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
// Write code to file
codeFile := filepath.Join(tempDir, "code"+langConfig.FileExt)
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
// Setup Docker run command to compile and run in one step
cmd := exec.Command(
"docker", "run", "--rm", "-i",
"--network=none",
"--memory="+langConfig.MemoryLimit,
"--cpu-quota="+fmt.Sprintf("%d", int(float64(100000)*0.1)), // 10% CPU
"--pids-limit=20",
"-v", tempDir+":/code",
"-w", "/code",
langConfig.Image,
"go", "run", "/code/code.go",
)
// Execute the code with input handling
e.executeWithIO(cmd, submission, time.Duration(langConfig.TimeoutSec)*time.Second)
}
// executeWithIO runs a command with input/output handling through WebSockets
func (e *CodeExecutor) executeWithIO(cmd *exec.Cmd, submission *models.CodeSubmission, timeout time.Duration) {
// Create pipes for stdin, stdout and stderr
stdin, err := cmd.StdinPipe()
if err != nil {
submission.Status = "failed"
submission.Output = "Failed to create stdin pipe: " + err.Error()
return
}
stdout, err := cmd.StdoutPipe()
if err != nil {
submission.Status = "failed"
submission.Output = "Failed to create stdout pipe: " + err.Error()
return
}
stderr, err := cmd.StderrPipe()
if err != nil {
submission.Status = "failed"
submission.Output = "Failed to create stderr pipe: " + err.Error()
return
}
// Create an input channel for this submission
inputChan := make(chan string, 10)
e.inputMutex.Lock()
e.inputChannels[submission.ID] = inputChan
e.inputMutex.Unlock()
// Clean up when done
defer func() {
e.inputMutex.Lock()
delete(e.inputChannels, submission.ID)
e.inputMutex.Unlock()
close(inputChan)
}()
// Start the command
if err := cmd.Start(); err != nil {
submission.Status = "failed"
submission.Output = "Failed to start process: " + err.Error()
return
}
// Output buffer to collect all output
var outputBuffer bytes.Buffer
// Send initial input if provided
if submission.Input != "" {
io.WriteString(stdin, submission.Input+"\n")
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
// Handle stdout in a goroutine
go func() {
buffer := make([]byte, 1024)
for {
n, err := stdout.Read(buffer)
if n > 0 {
data := buffer[:n]
outputBuffer.Write(data)
// Send real-time output to terminals
e.sendToTerminals(submission.ID, models.NewOutputMessage(string(data), false))
}
if err != nil {
if err != io.EOF {
log.Printf("Stdout read error: %v", err)
}
break
}
}
}()
// Handle stderr in a goroutine
go func() {
buffer := make([]byte, 1024)
for {
n, err := stderr.Read(buffer)
if n > 0 {
data := buffer[:n]
outputBuffer.Write(data)
// Send real-time error output to terminals
e.sendToTerminals(submission.ID, models.NewOutputMessage(string(data), true))
}
if err != nil {
if err != io.EOF {
log.Printf("Stderr read error: %v", err)
}
break
}
}
}()
// Listen for input from WebSocket
go func() {
for {
select {
case input, ok := <-inputChan:
if !ok {
return
}
log.Printf("Received input from WebSocket: %s", input)
// Write input with a single newline - don't add extra newlines
_, err := stdin.Write([]byte(input + "\n"))
if err != nil {
log.Printf("Error writing to stdin: %v", err)
e.sendToTerminals(submission.ID, models.NewErrorMessage("input_error", "Failed to send input to process"))
}
case <-ctx.Done():
return
}
}
}()
// Wait for command to complete or timeout
done := make(chan error)
go func() {
done <- cmd.Wait()
}()
// Wait for completion or timeout
select {
case <-ctx.Done():
// Process timed out
if ctx.Err() == context.DeadlineExceeded {
log.Printf("Process timed out for submission %s", submission.ID)
submission.Status = "failed"
submission.Output = outputBuffer.String() + "\nExecution timed out after " + timeout.String()
e.sendToTerminals(submission.ID, models.NewErrorMessage("timeout", "Execution timed out after "+timeout.String()))
// Attempt to kill the process
if err := cmd.Process.Kill(); err != nil {
log.Printf("Failed to kill process: %v", err)
}
}
case err := <-done:
// Process completed
if err != nil {
log.Printf("Process error: %v", err)
submission.Status = "failed"
// Don't overwrite output, as stderr has already been captured
} else {
submission.Status = "completed"
}
}
// Store the complete output
submission.Output = outputBuffer.String()
}
// Helper function to extract Java class name
func extractJavaClassName(code string) string {
// Default class name as fallback
defaultClass := "Solution"
// Look for public class
re := regexp.MustCompile(`public\s+class\s+(\w+)`)
matches := re.FindStringSubmatch(code)
if len(matches) > 1 {
return matches[1]
}
// Look for any class if no public class
re = regexp.MustCompile(`class\s+(\w+)`)
matches = re.FindStringSubmatch(code)
if len(matches) > 1 {
return matches[1]
}
return defaultClass
}

10
new-backend/go.mod Normal file
View File

@@ -0,0 +1,10 @@
module github.com/ishikabhoyar/monaco/new-backend
go 1.19
require (
github.com/google/uuid v1.3.0
github.com/gorilla/mux v1.8.0
github.com/gorilla/websocket v1.5.0
github.com/rs/cors v1.8.3
)

8
new-backend/go.sum Normal file
View File

@@ -0,0 +1,8 @@
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/rs/cors v1.8.3 h1:O+qNyWn7Z+F9M0ILBHgMVPuB1xTOucVd5gtaYyXBpRo=
github.com/rs/cors v1.8.3/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=

BIN
backend/tmp/main.exe → new-backend/main Normal file → Executable file

Binary file not shown.

98
new-backend/main.go Normal file
View File

@@ -0,0 +1,98 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gorilla/mux"
"github.com/ishikabhoyar/monaco/new-backend/api"
"github.com/ishikabhoyar/monaco/new-backend/config"
"github.com/ishikabhoyar/monaco/new-backend/executor"
"github.com/ishikabhoyar/monaco/new-backend/utils"
"github.com/rs/cors"
)
func main() {
// Configure logging
log.SetFlags(log.LstdFlags | log.Lshortfile | log.Lmicroseconds)
log.Println("Starting Monaco Code Execution Server...")
// Check if Docker is available
if !utils.DockerAvailable() {
log.Fatal("Docker is required but not available on this system")
}
// Load configuration
cfg := config.GetConfig()
log.Printf("Loaded configuration (max workers: %d, queue capacity: %d)",
cfg.Executor.ConcurrentExecutions, cfg.Executor.QueueCapacity)
// Initialize code executor
codeExecutor := executor.NewCodeExecutor(cfg)
log.Println("Code executor initialized")
// Initialize API handler
handler := api.NewHandler(codeExecutor)
// Setup router with middleware
router := mux.NewRouter()
// Register API routes
handler.RegisterRoutes(router)
// Add a simple welcome route
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Monaco Code Execution Server v1.0.0")
})
// Configure CORS
corsHandler := cors.New(cors.Options{
AllowedOrigins: []string{"*"}, // For development - restrict in production
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 300, // Maximum cache time for preflight requests
})
// Create server with timeouts
server := &http.Server{
Addr: ":" + cfg.Server.Port,
Handler: corsHandler.Handler(router),
ReadTimeout: cfg.Server.ReadTimeout,
WriteTimeout: cfg.Server.WriteTimeout,
IdleTimeout: cfg.Server.IdleTimeout,
}
// Channel for graceful shutdown
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
// Start server in a goroutine
go func() {
log.Printf("Server listening on port %s", cfg.Server.Port)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Error starting server: %v", err)
}
}()
// Wait for interrupt signal
<-stop
log.Println("Shutting down server...")
// Create context with timeout for graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Shutdown server gracefully
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server shutdown error: %v", err)
}
log.Println("Server stopped gracefully")
}

View File

@@ -0,0 +1,28 @@
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,omitempty"`
Status string `json:"status"` // "queued", "running", "completed", "failed"
QueuedAt time.Time `json:"queuedAt"`
StartedAt time.Time `json:"startedAt,omitempty"`
CompletedAt time.Time `json:"completedAt,omitempty"`
Output string `json:"output"`
Memory string `json:"memory,omitempty"` // Memory usage statistics
CPU string `json:"cpu,omitempty"` // CPU usage statistics
ExecutionTime float64 `json:"executionTime,omitempty"` // Execution time in seconds
}
// SubmissionResponse is the response returned after submitting code
type SubmissionResponse struct {
ID string `json:"id"`
Status string `json:"status"`
Message string `json:"message"`
}

View File

@@ -0,0 +1,91 @@
package models
// WebSocketMessage represents a message sent over WebSockets
type WebSocketMessage struct {
Type string `json:"type"`
Content interface{} `json:"content"`
}
// OutputMessage is sent when program produces output
type OutputMessage struct {
Text string `json:"text"`
IsError bool `json:"isError"`
}
// InputMessage is sent when user provides input
type InputMessage struct {
Text string `json:"text"`
}
// StatusUpdateMessage is sent when execution status changes
type StatusUpdateMessage struct {
Status string `json:"status"`
Memory string `json:"memory,omitempty"`
CPU string `json:"cpu,omitempty"`
}
// ErrorMessage is sent when an error occurs
type ErrorMessage struct {
ErrorType string `json:"errorType"`
Message string `json:"message"`
}
// NewOutputMessage creates a standard output message
func NewOutputMessage(content string, isError bool) WebSocketMessage {
return WebSocketMessage{
Type: "output",
Content: OutputMessage{
Text: content,
IsError: isError,
},
}
}
// NewInputPromptMessage creates an input prompt message
func NewInputPromptMessage(prompt string) WebSocketMessage {
return WebSocketMessage{
Type: "input_prompt",
Content: prompt,
}
}
// NewInputMessage creates a user input message
func NewInputMessage(input string) WebSocketMessage {
return WebSocketMessage{
Type: "input",
Content: InputMessage{
Text: input,
},
}
}
// NewStatusMessage creates a status update message
func NewStatusMessage(status, memory, cpu string) WebSocketMessage {
return WebSocketMessage{
Type: "status",
Content: StatusUpdateMessage{
Status: status,
Memory: memory,
CPU: cpu,
},
}
}
// NewErrorMessage creates an error message
func NewErrorMessage(errorType, message string) WebSocketMessage {
return WebSocketMessage{
Type: "error",
Content: ErrorMessage{
ErrorType: errorType,
Message: message,
},
}
}
// NewSystemMessage creates a system notification message
func NewSystemMessage(message string) WebSocketMessage {
return WebSocketMessage{
Type: "system",
Content: message,
}
}

16
new-backend/start.sh Normal file
View File

@@ -0,0 +1,16 @@
#!/bin/sh
# Start the backend
/monaco-backend &
BACKEND_PID=$!
echo "Backend started with PID: $BACKEND_PID"
# Wait for backend to start
echo "Waiting for backend to initialize..."
sleep 5
# Start cloudflared tunnel using config file
echo "Starting Cloudflare tunnel to api.ishikabhoyar.tech..."
cloudflared tunnel --no-autoupdate run --config /etc/cloudflared/config.json
# If cloudflared exits, kill the backend too
kill $BACKEND_PID

View File

@@ -0,0 +1,21 @@
[supervisord]
nodaemon=true
user=root
[program:backend]
command=/monaco-backend
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:cloudflared]
command=cloudflared tunnel --no-autoupdate run --config /etc/cloudflared/config.json
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0

106
new-backend/utils/utils.go Normal file
View File

@@ -0,0 +1,106 @@
package utils
import (
"log"
"os"
"os/exec"
"regexp"
"strings"
)
// DockerAvailable checks if Docker is available on the system
func DockerAvailable() bool {
cmd := exec.Command("docker", "--version")
if err := cmd.Run(); err != nil {
log.Printf("Docker not available: %v", err)
return false
}
return true
}
// PullDockerImage pulls a Docker image if it doesn't exist
func PullDockerImage(image string) error {
// Check if image exists
checkCmd := exec.Command("docker", "image", "inspect", image)
if err := checkCmd.Run(); err == nil {
// Image exists
return nil
}
// Pull the image
log.Printf("Pulling Docker image: %s", image)
pullCmd := exec.Command("docker", "pull", image)
pullCmd.Stdout = os.Stdout
pullCmd.Stderr = os.Stderr
return pullCmd.Run()
}
// ExtractJavaClassName extracts the class name from Java code
func ExtractJavaClassName(code string) string {
// Default class name as fallback
defaultClass := "Solution"
// Look for public class
re := regexp.MustCompile(`public\s+class\s+(\w+)`)
matches := re.FindStringSubmatch(code)
if len(matches) > 1 {
return matches[1]
}
// Look for any class if no public class
re = regexp.MustCompile(`class\s+(\w+)`)
matches = re.FindStringSubmatch(code)
if len(matches) > 1 {
return matches[1]
}
return defaultClass
}
// IsInputPrompt determines if a string is likely an input prompt
func IsInputPrompt(text string) bool {
// Early exit for empty or very long text
text = strings.TrimSpace(text)
if text == "" || len(text) > 100 {
return false
}
// Common prompt endings
if strings.HasSuffix(text, ":") || strings.HasSuffix(text, ">") ||
strings.HasSuffix(text, "?") || strings.HasSuffix(text, "...") {
return true
}
// Common prompt words
promptWords := []string{"input", "enter", "type", "provide"}
for _, word := range promptWords {
if strings.Contains(strings.ToLower(text), word) {
return true
}
}
return false
}
// SanitizeDockerArgs ensures safe Docker command arguments
func SanitizeDockerArgs(args []string) []string {
// This is a simplified version - in production, you'd want more robust checks
sanitized := make([]string, 0, len(args))
// Disallow certain dangerous flags
dangerousFlags := map[string]bool{
"--privileged": true,
"--net=host": true,
"--pid=host": true,
"--ipc=host": true,
"--userns=host": true,
}
for _, arg := range args {
if _, isDangerous := dangerousFlags[arg]; !isDangerous {
sanitized = append(sanitized, arg)
}
}
return sanitized
}

29
nginx.conf Normal file
View File

@@ -0,0 +1,29 @@
events {}
http {
include /etc/nginx/mime.types; # <-- ADD THIS LINE
server {
listen 80;
server_name localhost;
# Serve the static frontend files
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# Proxy API requests to the Go backend
location /api/ {
proxy_pass http://localhost:8081;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

26
nginx/nginx.conf Normal file
View File

@@ -0,0 +1,26 @@
events {}
http {
include /etc/nginx/mime.types; # <-- ADD THIS LINE
server {
listen 80;
server_name localhost;
# Serve the static frontend files
location / {
root /usr/share/nginx/html;
index index.html index.htm;
try_files $uri $uri/ /index.html;
}
# Proxy API requests to the Go backend
location /api/ {
proxy_pass http://localhost:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}