Compare commits
48 Commits
socket_rew
...
leetcode
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eabfbf806e | ||
|
|
e4f193613a | ||
| 210f531990 | |||
| e9553dd3af | |||
| 458b2ca06f | |||
| 232c6ec530 | |||
| 20680719f5 | |||
| c7d65c612f | |||
| e5827cfa42 | |||
| 720a37fa82 | |||
| 9474d2f633 | |||
| 5902300c95 | |||
| eb2873a3b9 | |||
|
|
25900803c3 | ||
|
|
5104fcbde0 | ||
|
|
402235bdec | ||
| 2af996b83e | |||
|
|
d501b53ab6 | ||
|
|
ac12360822 | ||
|
|
4654b93b15 | ||
|
|
2867f3bf42 | ||
|
|
6d77a6b8ce | ||
|
|
233be39b7f | ||
|
|
6964f370cb | ||
|
|
e12bbcfc6a | ||
|
|
e61bd7cfca | ||
|
|
a6bd8eeebb | ||
|
|
c529a48f31 | ||
|
|
86bc89c12e | ||
|
|
6802cefcaa | ||
|
|
85c9b99ef6 | ||
|
|
142ff262ac | ||
|
|
7eb42b20dd | ||
|
|
6128348417 | ||
|
|
b860db0b41 | ||
|
|
75ffb94cca | ||
| 56e3086cd9 | |||
| 4a737744df | |||
| 1cbb4f3c35 | |||
|
|
918b323cda | ||
|
|
208655c9bc | ||
|
|
48a14f674d | ||
|
|
697c4b8460 | ||
|
|
99e12a7355 | ||
|
|
3cc73e786a | ||
|
|
80a713cc56 | ||
|
|
648391e6ba | ||
| 3a75000e12 |
56
Dockerfile
Normal file
56
Dockerfile
Normal 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;"'
|
||||||
@@ -1,13 +1,24 @@
|
|||||||
# VSCode Clone with React and Vite
|
# VS Code Clone Project
|
||||||
|
|
||||||
This project is a VSCode-like code editor built with React and Vite. It features a customizable UI with an activity bar, sidebar, editor area, panel, and status bar, mimicking the look and feel of Visual Studio Code.
|
## Authors
|
||||||
|
- Arnab Bhowmik
|
||||||
|
- Ishika Bhoyar
|
||||||
|
|
||||||
## Features
|
## Description
|
||||||
|
This project is a VS Code Clone built with React and Monaco Editor. It features a file tree navigation, tab management, code editing with syntax highlighting, and a terminal panel for running code. It mimics the core functionalities of Visual Studio Code in a browser-based environment.
|
||||||
|
|
||||||
- **Activity Bar**: Switch between different views like Explorer, Search, Source Control, etc.
|
## Frontend Functionalities
|
||||||
- **Sidebar**: Displays file explorer, search results, and source control information.
|
- Built with React and Monaco Editor.
|
||||||
- **Editor Area**: Code editor with syntax highlighting and multiple tabs.
|
- File tree navigation for managing files and folders.
|
||||||
- **Panel**: Terminal, Problems, and Output views.
|
- Tab management for opening multiple files simultaneously.
|
||||||
- **Status Bar**: Displays status information and provides quick actions.
|
- Code editing with syntax highlighting and language support.
|
||||||
|
- Terminal panel for running code and viewing output.
|
||||||
|
- Persistent file structure and content using localStorage.
|
||||||
|
|
||||||
## Project Structure
|
## Backend Functionalities
|
||||||
|
- Built with Go and Docker for secure code execution.
|
||||||
|
- Supports multiple programming languages (Python, Java, C/C++).
|
||||||
|
- Executes code in isolated Docker containers with resource limits.
|
||||||
|
- RESTful API for submitting code, checking status, and retrieving results.
|
||||||
|
- Job queue system for managing concurrent executions.
|
||||||
|
- Enforces timeouts and resource limits for security and performance.
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
<title>VSCode</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
BIN
Frontend/public/favicon.ico
Normal file
BIN
Frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -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 |
@@ -1,10 +1,18 @@
|
|||||||
import VSCodeUI from "./components/VSCodeUI.jsx"
|
import CodeChallenge from "./components/CodeChallenge.jsx"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
750
Frontend/src/components/CodeChallenge.jsx
Normal file
750
Frontend/src/components/CodeChallenge.jsx
Normal 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;
|
||||||
@@ -9,6 +9,32 @@ import {
|
|||||||
import Sidebar from "./Sidebar";
|
import Sidebar from "./Sidebar";
|
||||||
import Panel from "./Panel"; // Import Panel component
|
import Panel from "./Panel"; // Import Panel component
|
||||||
|
|
||||||
|
// Add this function to map file extensions to language identifiers
|
||||||
|
const getLanguageFromExtension = (extension) => {
|
||||||
|
const extensionMap = {
|
||||||
|
'js': 'javascript',
|
||||||
|
'jsx': 'javascript',
|
||||||
|
'ts': 'typescript',
|
||||||
|
'tsx': 'typescript',
|
||||||
|
'py': 'python',
|
||||||
|
'java': 'java',
|
||||||
|
'c': 'c',
|
||||||
|
'cpp': 'cpp',
|
||||||
|
'h': 'c',
|
||||||
|
'hpp': 'cpp',
|
||||||
|
'cs': 'csharp',
|
||||||
|
'go': 'go',
|
||||||
|
'rb': 'ruby',
|
||||||
|
'php': 'php',
|
||||||
|
'html': 'html',
|
||||||
|
'css': 'css',
|
||||||
|
'json': 'json',
|
||||||
|
'md': 'markdown'
|
||||||
|
};
|
||||||
|
|
||||||
|
return extensionMap[extension] || 'text';
|
||||||
|
};
|
||||||
|
|
||||||
const EditorArea = ({
|
const EditorArea = ({
|
||||||
sidebarVisible = true,
|
sidebarVisible = true,
|
||||||
activeView = "explorer",
|
activeView = "explorer",
|
||||||
@@ -62,8 +88,8 @@ const EditorArea = ({
|
|||||||
|
|
||||||
// Add a new state for user input
|
// Add a new state for user input
|
||||||
const [userInput, setUserInput] = useState("");
|
const [userInput, setUserInput] = useState("");
|
||||||
// Add a new state for waiting for input
|
// Add socket state to track the connection
|
||||||
const [waitingForInput, setWaitingForInput] = useState(false);
|
const [activeSocket, setActiveSocket] = useState(null);
|
||||||
|
|
||||||
// Focus the input when new file modal opens
|
// Focus the input when new file modal opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -110,11 +136,6 @@ const EditorArea = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem("vscode-clone-files", JSON.stringify(files));
|
localStorage.setItem("vscode-clone-files", JSON.stringify(files));
|
||||||
}, [files]);
|
}, [files]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem("vscode-clone-structure", JSON.stringify(fileStructure));
|
|
||||||
}, [fileStructure]);
|
|
||||||
|
|
||||||
// Add this effect to handle editor resize when sidebar changes
|
// Add this effect to handle editor resize when sidebar changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Force editor to readjust layout when sidebar visibility changes
|
// Force editor to readjust layout when sidebar visibility changes
|
||||||
@@ -132,6 +153,41 @@ const EditorArea = ({
|
|||||||
}
|
}
|
||||||
}, [panelVisible]);
|
}, [panelVisible]);
|
||||||
|
|
||||||
|
// Add this useEffect for cleanup
|
||||||
|
useEffect(() => {
|
||||||
|
// Cleanup function to close socket when component unmounts
|
||||||
|
return () => {
|
||||||
|
if (activeSocket) {
|
||||||
|
activeSocket.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Add interval to poll execution status
|
||||||
|
useEffect(() => {
|
||||||
|
const checkInterval = setInterval(() => {
|
||||||
|
// Poll execution status
|
||||||
|
if (activeSocket && activeRunningFile) {
|
||||||
|
// Check if socket is still connected
|
||||||
|
if (activeSocket.readyState !== WebSocket.OPEN) {
|
||||||
|
console.warn("Socket not in OPEN state:", activeSocket.readyState);
|
||||||
|
setTerminalOutput(prev => [...prev, {
|
||||||
|
type: 'warning',
|
||||||
|
content: `Terminal connection lost, attempting to reconnect...`
|
||||||
|
}]);
|
||||||
|
// Could implement reconnection logic here
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// Clean up interval when component unmounts
|
||||||
|
return () => {
|
||||||
|
if (checkInterval) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [activeSocket, activeRunningFile]);
|
||||||
|
|
||||||
const handleEditorDidMount = (editor) => {
|
const handleEditorDidMount = (editor) => {
|
||||||
editorRef.current = editor;
|
editorRef.current = editor;
|
||||||
};
|
};
|
||||||
@@ -479,21 +535,31 @@ const EditorArea = ({
|
|||||||
case "README.md":
|
case "README.md":
|
||||||
return `# VS Code Clone Project
|
return `# VS Code Clone Project
|
||||||
|
|
||||||
## Overview
|
## Authors
|
||||||
This is a simple VS Code clone built with React and Monaco Editor.
|
- Arnab Bhowmik
|
||||||
|
- Ishika Bhoyar
|
||||||
|
|
||||||
## Features
|
## Description
|
||||||
- File tree navigation
|
This project is a VS Code Clone built with React and Monaco Editor. It features a file tree navigation, tab management, code editing with syntax highlighting, and a terminal panel for running code. It mimics the core functionalities of Visual Studio Code in a browser-based environment.
|
||||||
- Tab management
|
|
||||||
- Code editing with Monaco Editor
|
## Frontend Functionalities
|
||||||
- Syntax highlighting
|
- Built with React and Monaco Editor.
|
||||||
|
- File tree navigation for managing files and folders.
|
||||||
|
- Tab management for opening multiple files simultaneously.
|
||||||
|
- Code editing with syntax highlighting and language support.
|
||||||
|
- Terminal panel for running code and viewing output.
|
||||||
|
- Persistent file structure and content using localStorage.
|
||||||
|
|
||||||
|
## Backend Functionalities
|
||||||
|
- Built with Go and Docker for secure code execution.
|
||||||
|
- Supports multiple programming languages (Python, Java, C/C++).
|
||||||
|
- Executes code in isolated Docker containers with resource limits.
|
||||||
|
- RESTful API for submitting code, checking status, and retrieving results.
|
||||||
|
- Job queue system for managing concurrent executions.
|
||||||
|
- Enforces timeouts and resource limits for security and performance.
|
||||||
|
`;
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
1. Create a new file using the + button in the sidebar
|
|
||||||
2. Edit your code in the editor
|
|
||||||
3. Save changes using the save button
|
|
||||||
|
|
||||||
Happy coding!`;
|
|
||||||
default:
|
default:
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
@@ -507,7 +573,7 @@ Happy coding!`;
|
|||||||
width: `calc(100% - ${sidebarVisible ? sidebarWidth : 0}px)`
|
width: `calc(100% - ${sidebarVisible ? sidebarWidth : 0}px)`
|
||||||
};
|
};
|
||||||
|
|
||||||
// Modify the handleRunCode function to prompt for input first
|
// Update the handleRunCode function
|
||||||
const handleRunCode = async () => {
|
const handleRunCode = async () => {
|
||||||
if (!activeFile) return;
|
if (!activeFile) return;
|
||||||
|
|
||||||
@@ -517,49 +583,36 @@ Happy coding!`;
|
|||||||
setPanelVisible(true);
|
setPanelVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set state to waiting for input
|
|
||||||
setWaitingForInput(true);
|
|
||||||
setActiveRunningFile(activeFile.id);
|
|
||||||
|
|
||||||
// Clear previous output and add new command
|
// Clear previous output and add new command
|
||||||
const fileExtension = activeFile.id.split('.').pop().toLowerCase();
|
const fileExtension = activeFile.id.split('.').pop().toLowerCase();
|
||||||
const language = getLanguageFromExtension(fileExtension);
|
const language = getLanguageFromExtension(fileExtension);
|
||||||
|
|
||||||
const newOutput = [
|
const newOutput = [
|
||||||
{ type: 'command', content: `$ run ${activeFile.id}` },
|
{ type: 'command', content: `$ run ${activeFile.id}` },
|
||||||
{ type: 'output', content: 'Waiting for input (press Enter if no input is needed)...' }
|
{ type: 'output', content: 'Submitting code...' }
|
||||||
];
|
];
|
||||||
setTerminalOutput(newOutput);
|
setTerminalOutput(newOutput);
|
||||||
};
|
|
||||||
|
|
||||||
// Add a new function to handle input submission
|
|
||||||
const handleInputSubmit = async () => {
|
|
||||||
if (!activeFile || !waitingForInput) return;
|
|
||||||
|
|
||||||
// Set running state
|
|
||||||
setIsRunning(true);
|
|
||||||
setWaitingForInput(false);
|
|
||||||
|
|
||||||
// Add message that we're running with the input
|
|
||||||
setTerminalOutput(prev => [
|
|
||||||
...prev,
|
|
||||||
{ type: 'output', content: userInput ? `Using input: "${userInput}"` : 'Running without input...' }
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Use API URL from environment variable
|
|
||||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Now make the API call with the input that was entered
|
// Close any existing socket
|
||||||
const submitResponse = await fetch(`${apiUrl}/submit`, {
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
language: getLanguageFromExtension(activeFile.id.split('.').pop().toLowerCase()),
|
language: language,
|
||||||
code: activeFile.content,
|
code: activeFile.content,
|
||||||
input: userInput
|
input: "" // Explicitly passing empty input, no user input handling
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -570,87 +623,270 @@ Happy coding!`;
|
|||||||
const { id } = await submitResponse.json();
|
const { id } = await submitResponse.json();
|
||||||
setTerminalOutput(prev => [...prev, { type: 'output', content: `Job submitted with ID: ${id}` }]);
|
setTerminalOutput(prev => [...prev, { type: 'output', content: `Job submitted with ID: ${id}` }]);
|
||||||
|
|
||||||
// Step 2: Poll for status until completed or failed
|
// Set active running file
|
||||||
let status = 'pending';
|
setActiveRunningFile(activeFile.id);
|
||||||
while (status !== 'completed' && status !== 'failed') {
|
|
||||||
// Add a small delay between polls
|
// Connect to WebSocket with the execution ID
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const wsBaseUrl = apiUrl.replace(/^https?:\/\//, '');
|
||||||
|
const wsUrl = `${wsProtocol}//${wsBaseUrl}/api/ws/terminal/${id}`;
|
||||||
|
|
||||||
|
setTerminalOutput(prev => [...prev, { type: 'output', content: `Connecting to: ${wsUrl}` }]);
|
||||||
|
|
||||||
|
// Create a new WebSocket
|
||||||
|
const newSocket = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
// Set up event handlers
|
||||||
|
newSocket.onopen = () => {
|
||||||
|
console.log("WebSocket connected");
|
||||||
|
setTerminalOutput(prev => [...prev, { type: 'output', content: 'Connected to execution terminal' }]);
|
||||||
|
setIsRunning(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
newSocket.onmessage = (event) => {
|
||||||
|
console.log("WebSocket message received:", event.data);
|
||||||
|
|
||||||
const statusResponse = await fetch(`${apiUrl}/status?id=${id}`);
|
try {
|
||||||
if (!statusResponse.ok) {
|
const message = JSON.parse(event.data);
|
||||||
throw new Error(`Status check failed: ${statusResponse.status}`);
|
|
||||||
|
// Handle different message types
|
||||||
|
switch (message.type) {
|
||||||
|
case 'output':
|
||||||
|
setTerminalOutput(prev => [...prev, {
|
||||||
|
type: 'output',
|
||||||
|
content: message.content.text,
|
||||||
|
isError: message.content.isError
|
||||||
|
}]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'status':
|
||||||
|
const status = message.content.status;
|
||||||
|
setTerminalOutput(prev => [...prev, {
|
||||||
|
type: 'status',
|
||||||
|
content: `Status: ${status}`
|
||||||
|
}]);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
case 'system':
|
||||||
|
setTerminalOutput(prev => [...prev, {
|
||||||
|
type: 'system',
|
||||||
|
content: message.content
|
||||||
|
}]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'error':
|
||||||
|
setTerminalOutput(prev => [...prev, {
|
||||||
|
type: 'error',
|
||||||
|
content: `Error: ${message.content.message}`
|
||||||
|
}]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// For raw or unknown messages
|
||||||
|
setTerminalOutput(prev => [...prev, {
|
||||||
|
type: 'output',
|
||||||
|
content: event.data
|
||||||
|
}]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
} 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
|
||||||
|
}]);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
const statusData = await statusResponse.json();
|
// Clean up interval when component unmounts or when socket closes
|
||||||
status = statusData.status;
|
newSocket.addEventListener('close', () => {
|
||||||
|
if (statusCheckInterval) {
|
||||||
// Update terminal with status (for any status type)
|
clearInterval(statusCheckInterval);
|
||||||
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}` }];
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the result for both completed and failed status
|
newSocket.onclose = (event) => {
|
||||||
const resultResponse = await fetch(`${apiUrl}/result?id=${id}`);
|
console.log("WebSocket closed:", event);
|
||||||
if (!resultResponse.ok) {
|
|
||||||
throw new Error(`Result fetch failed: ${resultResponse.status}`);
|
const reason = event.reason ? `: ${event.reason}` : '';
|
||||||
}
|
const code = event.code ? ` (code: ${event.code})` : '';
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setActiveSocket(null);
|
||||||
|
|
||||||
|
// Clean up interval
|
||||||
|
if (statusCheckInterval) {
|
||||||
|
clearInterval(statusCheckInterval);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const { output } = await resultResponse.json();
|
newSocket.onerror = (event) => {
|
||||||
|
console.error("WebSocket error:", event);
|
||||||
|
setTerminalOutput(prev => [...prev, {
|
||||||
|
type: 'warning',
|
||||||
|
content: `WebSocket error occurred`
|
||||||
|
}]);
|
||||||
|
};
|
||||||
|
|
||||||
// Format and display output
|
// Set the active socket after all handlers are defined
|
||||||
const outputLines = output.split('\n').map(line => ({
|
setActiveSocket(newSocket);
|
||||||
type: status === 'failed' ? 'warning' : 'output',
|
|
||||||
content: line
|
|
||||||
}));
|
|
||||||
|
|
||||||
setTerminalOutput(prev => [
|
|
||||||
...prev,
|
|
||||||
{
|
|
||||||
type: status === 'failed' ? 'warning' : 'output',
|
|
||||||
content: status === 'failed'
|
|
||||||
? '------- EXECUTION FAILED -------'
|
|
||||||
: '------- EXECUTION RESULT -------'
|
|
||||||
},
|
|
||||||
...outputLines
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (status === 'failed') {
|
|
||||||
console.error('Code execution failed:', output);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error("Run code error:", error);
|
||||||
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
|
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
|
||||||
} finally {
|
|
||||||
// Set running state to false
|
|
||||||
setIsRunning(false);
|
setIsRunning(false);
|
||||||
|
|
||||||
|
// Also add cleanup in the error handler
|
||||||
|
if (statusCheckInterval) {
|
||||||
|
clearInterval(statusCheckInterval);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper function to convert file extension to language identifier for API
|
// Update handleInputSubmit to ensure the input is sent properly
|
||||||
const getLanguageFromExtension = (extension) => {
|
const handleInputSubmit = (input) => {
|
||||||
const languageMap = {
|
// Use the direct input parameter instead of relying on userInput state
|
||||||
'java': 'java',
|
const textToSend = input || userInput;
|
||||||
'c': 'c',
|
|
||||||
'cpp': 'cpp',
|
|
||||||
'py': 'python',
|
|
||||||
'js': 'javascript',
|
|
||||||
'jsx': 'javascript',
|
|
||||||
'ts': 'typescript',
|
|
||||||
'tsx': 'typescript'
|
|
||||||
};
|
|
||||||
|
|
||||||
return languageMap[extension] || extension;
|
console.log("Input submit called, active socket state:",
|
||||||
|
activeSocket ? activeSocket.readyState : "no socket",
|
||||||
|
"input:", textToSend);
|
||||||
|
|
||||||
|
if (!activeSocket) {
|
||||||
|
console.warn("Cannot send input: No active socket");
|
||||||
|
setTerminalOutput(prev => [...prev, {
|
||||||
|
type: 'warning',
|
||||||
|
content: `Cannot send input: No active connection`
|
||||||
|
}]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeSocket.readyState !== WebSocket.OPEN) {
|
||||||
|
console.warn("Socket not in OPEN state:", activeSocket.readyState);
|
||||||
|
setTerminalOutput(prev => [...prev, {
|
||||||
|
type: 'warning',
|
||||||
|
content: `Cannot send input: Connection not open (state: ${activeSocket.readyState})`
|
||||||
|
}]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// Update this function to also update parent state
|
||||||
@@ -834,18 +1070,17 @@ Happy coding!`;
|
|||||||
document.addEventListener("mouseup", onMouseUp);
|
document.addEventListener("mouseup", onMouseUp);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Panel
|
<Panel
|
||||||
height={panelHeight}
|
height={panelHeight}
|
||||||
terminalOutput={terminalOutput}
|
terminalOutput={terminalOutput}
|
||||||
isRunning={isRunning}
|
isRunning={isRunning}
|
||||||
waitingForInput={waitingForInput}
|
activeRunningFile={activeRunningFile}
|
||||||
activeRunningFile={activeRunningFile}
|
initialTab="terminal"
|
||||||
initialTab="terminal"
|
onClose={togglePanel}
|
||||||
onClose={togglePanel}
|
userInput={userInput}
|
||||||
userInput={userInput}
|
onUserInputChange={setUserInput}
|
||||||
onUserInputChange={setUserInput}
|
onInputSubmit={handleInputSubmit}
|
||||||
onInputSubmit={handleInputSubmit}
|
/>
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
import React from "react"
|
|
||||||
"use client"
|
|
||||||
|
|
||||||
import Link from "next/link"
|
|
||||||
import { ChevronDown } from "lucide-react"
|
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
|
||||||
import {
|
|
||||||
NavigationMenu,
|
|
||||||
NavigationMenuContent,
|
|
||||||
NavigationMenuItem,
|
|
||||||
NavigationMenuLink,
|
|
||||||
NavigationMenuList,
|
|
||||||
NavigationMenuTrigger,
|
|
||||||
} from "@/components/ui/navigation-menu"
|
|
||||||
|
|
||||||
export function Navbar() {
|
|
||||||
return (
|
|
||||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
||||||
<div className="container flex h-16 items-center justify-between">
|
|
||||||
<Link href="/" className="flex items-center space-x-2">
|
|
||||||
<span className="text-xl font-bold">*Azzle</span>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<NavigationMenu className="hidden md:flex">
|
|
||||||
<NavigationMenuList>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NavigationMenuTrigger>
|
|
||||||
Demo <ChevronDown className="h-4 w-4" />
|
|
||||||
</NavigationMenuTrigger>
|
|
||||||
<NavigationMenuContent>
|
|
||||||
<div className="grid gap-3 p-6 w-[400px]">
|
|
||||||
<NavigationMenuLink asChild>
|
|
||||||
<Link
|
|
||||||
href="/demo/features"
|
|
||||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
|
||||||
>
|
|
||||||
<div className="text-sm font-medium leading-none">Features</div>
|
|
||||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
|
||||||
Explore all the features our platform has to offer
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
</NavigationMenuLink>
|
|
||||||
<NavigationMenuLink asChild>
|
|
||||||
<Link
|
|
||||||
href="/demo/pricing"
|
|
||||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
|
||||||
>
|
|
||||||
<div className="text-sm font-medium leading-none">Pricing</div>
|
|
||||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
|
||||||
View our flexible pricing plans
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
</NavigationMenuLink>
|
|
||||||
</div>
|
|
||||||
</NavigationMenuContent>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<Link href="/about" legacyBehavior passHref>
|
|
||||||
<NavigationMenuLink className="group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50">
|
|
||||||
About
|
|
||||||
</NavigationMenuLink>
|
|
||||||
</Link>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NavigationMenuTrigger>
|
|
||||||
Services <ChevronDown className="h-4 w-4" />
|
|
||||||
</NavigationMenuTrigger>
|
|
||||||
<NavigationMenuContent>
|
|
||||||
<div className="grid gap-3 p-6 w-[400px]">
|
|
||||||
<NavigationMenuLink asChild>
|
|
||||||
<Link
|
|
||||||
href="/services/consulting"
|
|
||||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
|
||||||
>
|
|
||||||
<div className="text-sm font-medium leading-none">Consulting</div>
|
|
||||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
|
||||||
Expert guidance for your business needs
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
</NavigationMenuLink>
|
|
||||||
<NavigationMenuLink asChild>
|
|
||||||
<Link
|
|
||||||
href="/services/implementation"
|
|
||||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
|
||||||
>
|
|
||||||
<div className="text-sm font-medium leading-none">Implementation</div>
|
|
||||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
|
||||||
Full-service implementation and support
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
</NavigationMenuLink>
|
|
||||||
</div>
|
|
||||||
</NavigationMenuContent>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<NavigationMenuTrigger>
|
|
||||||
Pages <ChevronDown className="h-4 w-4" />
|
|
||||||
</NavigationMenuTrigger>
|
|
||||||
<NavigationMenuContent>
|
|
||||||
<div className="grid gap-3 p-6 w-[400px]">
|
|
||||||
<NavigationMenuLink asChild>
|
|
||||||
<Link
|
|
||||||
href="/blog"
|
|
||||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
|
||||||
>
|
|
||||||
<div className="text-sm font-medium leading-none">Blog</div>
|
|
||||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
|
||||||
Read our latest articles and updates
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
</NavigationMenuLink>
|
|
||||||
<NavigationMenuLink asChild>
|
|
||||||
<Link
|
|
||||||
href="/resources"
|
|
||||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
|
||||||
>
|
|
||||||
<div className="text-sm font-medium leading-none">Resources</div>
|
|
||||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
|
||||||
Helpful guides and documentation
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
</NavigationMenuLink>
|
|
||||||
</div>
|
|
||||||
</NavigationMenuContent>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
<NavigationMenuItem>
|
|
||||||
<Link href="/contact" legacyBehavior passHref>
|
|
||||||
<NavigationMenuLink className="group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50">
|
|
||||||
Contact
|
|
||||||
</NavigationMenuLink>
|
|
||||||
</Link>
|
|
||||||
</NavigationMenuItem>
|
|
||||||
</NavigationMenuList>
|
|
||||||
</NavigationMenu>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<Button variant="ghost" asChild>
|
|
||||||
<Link href="/login">Login</Link>
|
|
||||||
</Button>
|
|
||||||
<Button asChild>
|
|
||||||
<Link href="/signup">Sign up free</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Navbar
|
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import React from "react";
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
import { useState, useEffect } from "react";
|
import { X, Maximize2, ChevronDown, Plus } from "lucide-react";
|
||||||
import { X } from "lucide-react";
|
|
||||||
|
|
||||||
const Panel = ({
|
const Panel = ({
|
||||||
height,
|
height,
|
||||||
terminalOutput = [],
|
terminalOutput = [],
|
||||||
isRunning = false,
|
isRunning = false,
|
||||||
@@ -12,85 +11,135 @@ const Panel = ({
|
|||||||
onClose,
|
onClose,
|
||||||
userInput = "",
|
userInput = "",
|
||||||
onUserInputChange,
|
onUserInputChange,
|
||||||
onInputSubmit
|
onInputSubmit,
|
||||||
}) => {
|
}) => {
|
||||||
const [activeTab, setActiveTab] = useState(initialTab);
|
const [activeTab, setActiveTab] = useState(initialTab);
|
||||||
|
const terminalRef = useRef(null);
|
||||||
|
const [inputBuffer, setInputBuffer] = useState("");
|
||||||
|
|
||||||
// Set active tab when initialTab changes
|
// Update active tab when initialTab changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setActiveTab(initialTab);
|
setActiveTab(initialTab);
|
||||||
}, [initialTab]);
|
}, [initialTab]);
|
||||||
|
|
||||||
const renderTerminal = () => {
|
// Auto-scroll terminal to the bottom when content changes
|
||||||
return (
|
useEffect(() => {
|
||||||
<div className="panel-terminal">
|
if (terminalRef.current) {
|
||||||
{terminalOutput.length > 0 ? (
|
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
|
||||||
// Render output from EditorArea when available
|
}
|
||||||
<>
|
}, [terminalOutput]);
|
||||||
{terminalOutput.map((line, index) => (
|
|
||||||
<div key={index} className={`terminal-line ${line.type === 'warning' ? 'terminal-warning' : 'terminal-output'}`}>
|
// Handle keyboard input for the terminal
|
||||||
{line.type === 'command' ? <span className="terminal-prompt">$</span> : ''} {line.content}
|
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>
|
</div>
|
||||||
))}
|
);
|
||||||
{waitingForInput && (
|
})}
|
||||||
<div className="terminal-line">
|
|
||||||
<span className="terminal-prompt">Input:</span>
|
{isRunning && (
|
||||||
<input
|
<div className="terminal-line terminal-input-line">
|
||||||
type="text"
|
<span className="terminal-prompt">$</span> {inputBuffer}
|
||||||
className="terminal-input"
|
<span className="terminal-cursor"></span>
|
||||||
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>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
// Default terminal content when no output
|
|
||||||
<>
|
|
||||||
<div className="terminal-line">
|
|
||||||
<span className="terminal-prompt">$</span> npm start
|
|
||||||
</div>
|
</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">
|
||||||
<div className="terminal-line terminal-output">On Your Network: http://192.168.1.5:3000</div>
|
<span className="terminal-prompt">$</span>
|
||||||
<div className="terminal-line">
|
<span className="terminal-cursor"></span>
|
||||||
<span className="terminal-prompt">$</span>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</div>
|
||||||
)}
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderProblems = () => {
|
// Render other tabs
|
||||||
return (
|
const renderProblems = () => (
|
||||||
<div className="panel-problems">
|
<div className="panel-problems">
|
||||||
<div className="panel-empty-message">No problems have been detected in the workspace.</div>
|
<div className="panel-empty-message">No problems have been detected in the workspace.</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
const renderOutput = () => {
|
const renderOutput = () => (
|
||||||
return (
|
<div className="panel-output">
|
||||||
<div className="panel-output">
|
<div className="output-line">[Extension Host] Extension host started.</div>
|
||||||
<div className="output-line">[Extension Host] Extension host started.</div>
|
<div className="output-line">[Language Server] Language server started.</div>
|
||||||
<div className="output-line">[Language Server] Language server started.</div>
|
{activeRunningFile && (
|
||||||
{activeRunningFile && (
|
<div className="output-line">[Running] {activeRunningFile}</div>
|
||||||
<div className="output-line">[Running] {activeRunningFile}</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
const renderDebugConsole = () => (
|
||||||
|
<div className="panel-debug-console">
|
||||||
|
<div className="debug-line">Debug session not yet started.</div>
|
||||||
|
<div className="debug-line">Press F5 to start debugging.</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderPorts = () => (
|
||||||
|
<div className="panel-ports">
|
||||||
|
<div className="ports-line">No forwarded ports detected.</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderComments = () => (
|
||||||
|
<div className="panel-comments">
|
||||||
|
<div className="comments-line">No comments have been added to this workspace.</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get content for the active tab
|
||||||
const getTabContent = () => {
|
const getTabContent = () => {
|
||||||
switch (activeTab) {
|
switch (activeTab) {
|
||||||
case "terminal":
|
case "terminal":
|
||||||
@@ -99,6 +148,12 @@ const Panel = ({
|
|||||||
return renderProblems();
|
return renderProblems();
|
||||||
case "output":
|
case "output":
|
||||||
return renderOutput();
|
return renderOutput();
|
||||||
|
case "debug":
|
||||||
|
return renderDebugConsole();
|
||||||
|
case "ports":
|
||||||
|
return renderPorts();
|
||||||
|
case "comments":
|
||||||
|
return renderComments();
|
||||||
default:
|
default:
|
||||||
return <div>Unknown tab</div>;
|
return <div>Unknown tab</div>;
|
||||||
}
|
}
|
||||||
@@ -107,76 +162,29 @@ const Panel = ({
|
|||||||
return (
|
return (
|
||||||
<div className="panel" style={{ height: `${height}px` }}>
|
<div className="panel" style={{ height: `${height}px` }}>
|
||||||
<div className="panel-tabs">
|
<div className="panel-tabs">
|
||||||
<div
|
{["problems", "output", "debug", "terminal", "ports", "comments"].map((tab) => (
|
||||||
className={`panel-tab ${activeTab === "problems" ? "active" : ""}`}
|
<div
|
||||||
onClick={() => setActiveTab("problems")}
|
key={tab}
|
||||||
>
|
className={`panel-tab ${activeTab === tab ? "active" : ""}`}
|
||||||
<span className="tab-icon">
|
onClick={() => setActiveTab(tab)}
|
||||||
<svg
|
>
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<span className="tab-name">{tab.toUpperCase()}</span>
|
||||||
width="16"
|
</div>
|
||||||
height="16"
|
))}
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
|
||||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
|
||||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span className="tab-name">Problems</span>
|
|
||||||
</div>
|
|
||||||
<div className={`panel-tab ${activeTab === "output" ? "active" : ""}`} onClick={() => setActiveTab("output")}>
|
|
||||||
<span className="tab-icon">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<circle cx="12" cy="12" r="10"></circle>
|
|
||||||
<line x1="12" y1="16" x2="12" y2="12"></line>
|
|
||||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span className="tab-name">Output</span>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`panel-tab ${activeTab === "terminal" ? "active" : ""}`}
|
|
||||||
onClick={() => setActiveTab("terminal")}
|
|
||||||
>
|
|
||||||
<span className="tab-icon">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<polyline points="4 17 10 11 4 5"></polyline>
|
|
||||||
<line x1="12" y1="19" x2="20" y2="19"></line>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span className="tab-name">Terminal</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Add close button */}
|
|
||||||
<div className="panel-actions">
|
<div className="panel-actions">
|
||||||
|
{/* <button className="panel-action-btn">
|
||||||
|
<span className="current-terminal">node - frontend</span>
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</button>
|
||||||
|
<button className="panel-action-btn">
|
||||||
|
<Plus size={16} />
|
||||||
|
</button>
|
||||||
|
<button className="panel-action-btn">
|
||||||
|
<Maximize2 size={16} />
|
||||||
|
</button> */}
|
||||||
<button className="panel-close-btn" onClick={onClose}>
|
<button className="panel-close-btn" onClick={onClose}>
|
||||||
<X size={14} />
|
<X size={16} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,5 +194,4 @@ const Panel = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Panel;
|
export default Panel;
|
||||||
|
|
||||||
@@ -31,10 +31,10 @@ const Sidebar = ({
|
|||||||
const renderExplorer = () => {
|
const renderExplorer = () => {
|
||||||
const renderFileTree = (structure, path = "") => {
|
const renderFileTree = (structure, path = "") => {
|
||||||
if (!structure) return null;
|
if (!structure) return null;
|
||||||
|
|
||||||
return Object.entries(structure).map(([name, item]) => {
|
return Object.entries(structure).map(([name, item]) => {
|
||||||
const currentPath = path ? `${path}/${name}` : name;
|
const currentPath = path ? `${path}/${name}` : name;
|
||||||
|
|
||||||
if (item.type === "folder") {
|
if (item.type === "folder") {
|
||||||
const isExpanded = expandedFolders[currentPath];
|
const isExpanded = expandedFolders[currentPath];
|
||||||
return (
|
return (
|
||||||
@@ -75,21 +75,6 @@ const Sidebar = ({
|
|||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="folder-icon">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="#75beff"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
>
|
|
||||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span className="folder-name">{name}</span>
|
<span className="folder-name">{name}</span>
|
||||||
</div>
|
</div>
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
@@ -137,7 +122,7 @@ const Sidebar = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sidebar-section">
|
<div className="sidebar-section">
|
||||||
<div className="sidebar-title">
|
<div className="sidebar-title">
|
||||||
@@ -184,10 +169,38 @@ const Sidebar = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getFileIcon = (fileName) => {
|
const getFileIcon = (fileName) => {
|
||||||
const extension = fileName.split('.').pop().toLowerCase();
|
const extension = fileName.split('.').pop().toLowerCase();
|
||||||
|
|
||||||
|
if (fileName.toLowerCase() === 'readme.md') {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="#007acc" /* Blue color for the circle */
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" fill="none" stroke="#007acc" />
|
||||||
|
<text
|
||||||
|
x="12"
|
||||||
|
y="15"
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize="10"
|
||||||
|
fill="#007acc"
|
||||||
|
fontFamily="Arial, sans-serif"
|
||||||
|
fontWeight="bold"
|
||||||
|
>
|
||||||
|
i
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) {
|
if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
@@ -240,7 +253,7 @@ const Sidebar = ({
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
"use client"
|
|
||||||
|
|
||||||
const StatusBar = ({ togglePanel, panelVisible }) => {
|
const StatusBar = ({ togglePanel, panelVisible }) => {
|
||||||
return (
|
return (
|
||||||
<div className="status-bar">
|
<div className="status-bar">
|
||||||
|
{/* Left Section of the Status Bar */}
|
||||||
<div className="status-bar-left">
|
<div className="status-bar-left">
|
||||||
|
{/* Branch Indicator */}
|
||||||
<div className="status-item">
|
<div className="status-item">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -16,6 +17,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
|||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
|
aria-label="Branch Icon"
|
||||||
>
|
>
|
||||||
<line x1="6" y1="3" x2="6" y2="15"></line>
|
<line x1="6" y1="3" x2="6" y2="15"></line>
|
||||||
<circle cx="18" cy="6" r="3"></circle>
|
<circle cx="18" cy="6" r="3"></circle>
|
||||||
@@ -25,6 +27,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
|||||||
<span>main</span>
|
<span>main</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Error Indicator */}
|
||||||
<div className="status-item">
|
<div className="status-item">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -36,30 +39,14 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
|||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
|
aria-label="Error Icon"
|
||||||
>
|
>
|
||||||
<polyline points="20 6 9 17 4 12"></polyline>
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
</svg>
|
</svg>
|
||||||
<span>0 errors</span>
|
<span>0 errors</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button className="status-item status-button" onClick={togglePanel}>
|
{/* Warning Indicator */}
|
||||||
<span>{panelVisible ? "Hide Terminal" : "Show Terminal"}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="status-bar-right">
|
|
||||||
<div className="status-item">
|
|
||||||
<span>Ln 1, Col 1</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="status-item">
|
|
||||||
<span>Spaces: 2</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="status-item">
|
|
||||||
<span>UTF-8</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="status-item">
|
<div className="status-item">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -71,6 +58,65 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
|||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
|
aria-label="Warning Icon"
|
||||||
|
>
|
||||||
|
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
||||||
|
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||||
|
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||||
|
</svg>
|
||||||
|
<span>0 warnings</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Toggle Terminal Button */}
|
||||||
|
<button
|
||||||
|
className="status-item status-button"
|
||||||
|
onClick={togglePanel}
|
||||||
|
aria-label="Toggle Terminal"
|
||||||
|
>
|
||||||
|
<span>{panelVisible ? "Hide Terminal" : "Show Terminal"}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Section of the Status Bar */}
|
||||||
|
<div className="status-bar-right">
|
||||||
|
{/* Line and Column Indicator */}
|
||||||
|
<div className="status-item">
|
||||||
|
<span>Ln 1, Col 1</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Spaces Indicator */}
|
||||||
|
<div className="status-item">
|
||||||
|
<span>Spaces: 2</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Encoding Indicator */}
|
||||||
|
<div className="status-item">
|
||||||
|
<span>UTF-8</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Language Mode */}
|
||||||
|
<div className="status-item">
|
||||||
|
<span>JavaScript</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* EOL (End of Line) Indicator */}
|
||||||
|
<div className="status-item">
|
||||||
|
<span>LF</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Connection Status */}
|
||||||
|
<div className="status-item">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
aria-label="Connection Icon"
|
||||||
>
|
>
|
||||||
<path d="M5 12.55a11 11 0 0 1 14.08 0"></path>
|
<path d="M5 12.55a11 11 0 0 1 14.08 0"></path>
|
||||||
<path d="M1.42 9a16 16 0 0 1 21.16 0"></path>
|
<path d="M1.42 9a16 16 0 0 1 21.16 0"></path>
|
||||||
@@ -80,6 +126,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
|||||||
<span>Connected</span>
|
<span>Connected</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Bell Icon */}
|
||||||
<div className="status-item">
|
<div className="status-item">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -91,6 +138,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
|||||||
strokeWidth="2"
|
strokeWidth="2"
|
||||||
strokeLinecap="round"
|
strokeLinecap="round"
|
||||||
strokeLinejoin="round"
|
strokeLinejoin="round"
|
||||||
|
aria-label="Bell Icon"
|
||||||
>
|
>
|
||||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
|
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
|
||||||
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
|
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
|
||||||
@@ -98,8 +146,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default StatusBar
|
|
||||||
|
|
||||||
|
export default StatusBar;
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
:root {
|
:root {
|
||||||
--vscode-background: #1e1e1e;
|
--vscode-background: #000000;
|
||||||
--vscode-foreground: #d4d4d4;
|
--vscode-foreground: #d4d4d4;
|
||||||
--vscode-activityBar-background: #333333;
|
--vscode-activityBar-background: #333333;
|
||||||
--vscode-activityBar-foreground: #ffffff;
|
--vscode-activityBar-foreground: #ffffff;
|
||||||
--vscode-activityBar-inactiveForeground: #ffffff80;
|
--vscode-activityBar-inactiveForeground: #ffffff80;
|
||||||
--vscode-sideBar-background: #252526;
|
--vscode-sideBar-background: #252526;
|
||||||
--vscode-sideBar-foreground: #cccccc;
|
--vscode-sideBar-foreground: #cccccc;
|
||||||
--vscode-editor-background: #1e1e1e;
|
--vscode-editor-background: #000000;
|
||||||
--vscode-statusBar-background: #007acc;
|
--vscode-statusBar-background: #007acc;
|
||||||
--vscode-statusBar-foreground: #ffffff;
|
--vscode-statusBar-foreground: #ffffff;
|
||||||
--vscode-panel-background: #1e1e1e;
|
--vscode-panel-background: #000000;
|
||||||
--vscode-panel-border: #80808059;
|
--vscode-panel-border: #80808059;
|
||||||
--vscode-tab-activeBackground: #1e1e1e;
|
--vscode-tab-activeBackground: #000000;
|
||||||
--vscode-tab-inactiveBackground: #2d2d2d;
|
--vscode-tab-inactiveBackground: #2d2d2d;
|
||||||
--vscode-tab-activeForeground: #ffffff;
|
--vscode-tab-activeForeground: #ffffff;
|
||||||
--vscode-tab-inactiveForeground: #ffffff80;
|
--vscode-tab-inactiveForeground: #ffffff80;
|
||||||
@@ -57,12 +57,12 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: var(--vscode-activityBar-background);
|
background-color: var(--vscode-activityBar-background);
|
||||||
z-index: 10;
|
z-index: 10; /* Lower z-index than the StatusBar */
|
||||||
width: 50px;
|
position: fixed;
|
||||||
height: 100%;
|
|
||||||
position: fixed; /* Change to fixed to avoid layout issues */
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
height: calc(100% - 22px); /* Subtract the height of the StatusBar */
|
||||||
|
width: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-bar button {
|
.activity-bar button {
|
||||||
@@ -404,7 +404,7 @@ body {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-family: "Consolas", "Courier New", monospace;
|
font-family: "Consolas", "Courier New", monospace;
|
||||||
font-size: 13px;
|
font-size: 10px;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-family: 'Consolas', 'Courier New', monospace;
|
font-family: 'Consolas', 'Courier New', monospace;
|
||||||
@@ -418,30 +418,59 @@ body {
|
|||||||
height: 100%;
|
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 {
|
.panel-terminal .terminal-line {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
|
word-wrap: break-word; /* Ensure long words don't cause horizontal overflow */
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-line {
|
.terminal-line {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
margin-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-prompt {
|
.terminal-prompt {
|
||||||
color: #0f0;
|
color: #0a84ff;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
color: #569cd6;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-output {
|
.terminal-output {
|
||||||
color: #888888;
|
color: #ddd;
|
||||||
color: #cccccc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-warning {
|
.terminal-warning {
|
||||||
color: #ddb100;
|
color: #ffa500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.output-line {
|
.output-line {
|
||||||
@@ -463,9 +492,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink {
|
@keyframes blink {
|
||||||
50% {
|
0%, 100% { opacity: 1; }
|
||||||
opacity: 0;
|
50% { opacity: 0; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-empty-message {
|
.panel-empty-message {
|
||||||
@@ -964,9 +992,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blink {
|
@keyframes blink {
|
||||||
50% {
|
0%, 100% { opacity: 1; }
|
||||||
opacity: 0;
|
50% { opacity: 0; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Make sure the monaco container adjusts when terminal is shown */
|
/* Make sure the monaco container adjusts when terminal is shown */
|
||||||
@@ -1019,4 +1046,409 @@ body {
|
|||||||
|
|
||||||
.panel-close-btn:hover {
|
.panel-close-btn:hover {
|
||||||
opacity: 1;
|
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;
|
||||||
}
|
}
|
||||||
244
Readme.md
244
Readme.md
@@ -1,22 +1,240 @@
|
|||||||
# Monaco Code Execution Engine
|
# Monaco Code Execution Engine
|
||||||
Monaco is a secure, containerized code execution engine that allows you to run code in multiple programming languages through a simple REST API.
|
|
||||||
|
Monaco is a secure, containerized code execution engine that allows you to run code in multiple programming languages through a simple REST API and WebSocket connections for real-time terminal interaction.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
- Multi-language support: Run code in Python, Java, C, and C++
|
|
||||||
- Secure execution: All code runs in isolated Docker containers
|
- **Multi-language support**: Run code in Python, Java, C, and C++
|
||||||
- Resource limits: Memory, CPU, and file descriptor limits to prevent abuse
|
- **Secure execution**: All code runs in isolated Docker containers
|
||||||
- Concurrent processing: Efficient job queue for handling multiple requests
|
- **Resource limits**: Memory, CPU, and file descriptor limits to prevent abuse
|
||||||
- Simple REST API: Easy to integrate with any frontend
|
- **Concurrent processing**: Efficient job queue for handling multiple requests
|
||||||
|
- **Simple REST API**: Easy to integrate with any frontend
|
||||||
|
- **Interactive terminal**: Real-time code execution with input/output via WebSockets
|
||||||
|
- **VS Code-like interface**: Modern editor with syntax highlighting and file management
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Monaco consists of several components:
|
Monaco consists of several components:
|
||||||
|
|
||||||
- HTTP Handlers (handler/handler.go): Processes API requests
|
### Backend Components
|
||||||
- Execution Service (service/execution.go): Manages code execution in containers
|
|
||||||
- Job Queue (queue/queue.go): Handles concurrent execution of code submissions
|
- **HTTP Handlers** (`handler/handler.go`): Processes API requests and WebSocket connections
|
||||||
- Submission Model (model/submission.go): Defines the data structure for code submissions
|
- **Execution Service** (`service/execution.go`): Manages code execution in containers
|
||||||
|
- **Job Queue** (`queue/queue.go`): Handles concurrent execution of code submissions
|
||||||
|
- **Submission Model** (`model/submission.go`): Defines the data structure for code submissions
|
||||||
|
|
||||||
|
### Frontend Components
|
||||||
|
|
||||||
|
- **Editor Area** (`EditorArea.jsx`): Main code editor with Monaco editor integration
|
||||||
|
- **Terminal Panel** (`Panel.jsx`): Interactive terminal for code execution and input
|
||||||
|
- **Sidebar** (`Sidebar.jsx`): File explorer and project structure navigation
|
||||||
|
- **Status Bar** (`StatusBar.jsx`): Information display and quick actions
|
||||||
|
|
||||||
|
### Communication Flow
|
||||||
|
|
||||||
|
1. Frontend submits code to backend via REST API
|
||||||
|
2. Backend assigns a unique ID and queues the execution
|
||||||
|
3. Frontend connects to WebSocket endpoint with the execution ID
|
||||||
|
4. Backend sends real-time execution output through WebSocket
|
||||||
|
5. Frontend can send user input back through WebSocket
|
||||||
|
6. Results are stored and retrievable via REST endpoints
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
- Go 1.22.3 or higher
|
|
||||||
- Docker
|
- **Backend**:
|
||||||
- Network connectivity for container image pulling
|
- Go 1.22.3 or higher
|
||||||
|
- Docker
|
||||||
|
- Network connectivity for container image pulling
|
||||||
|
- **Frontend**:
|
||||||
|
- Node.js and npm/yarn
|
||||||
|
- Modern web browser
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### Backend Setup
|
||||||
|
|
||||||
|
1. Clone the repository:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/arnab-afk/monaco.git
|
||||||
|
cd monaco/backend
|
||||||
|
|
||||||
|
2.Install Go dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go mod download
|
||||||
|
```
|
||||||
|
|
||||||
|
3.Build the application:
|
||||||
|
```bash
|
||||||
|
go build -o monaco
|
||||||
|
```
|
||||||
|
|
||||||
|
4.Run the service
|
||||||
|
```bash
|
||||||
|
./monaco
|
||||||
|
```
|
||||||
|
|
||||||
|
The backend service will start on port 8080 by default.
|
||||||
|
|
||||||
|
### Frontend Setup
|
||||||
|
1. Navigate to the Frontend directory:
|
||||||
|
```bash
|
||||||
|
cd Frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Install dependencies:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Set up environment variables: Create a ```.env``` or ```.env.local.``` file with:
|
||||||
|
```bash
|
||||||
|
VITE_API_URL=http://localhost:8080
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Start the development server:
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The frontend will be available at http://localhost:5173 by default.
|
||||||
|
|
||||||
|
### API Reference
|
||||||
|
|
||||||
|
### REST Endpoints
|
||||||
|
```POST /submit```
|
||||||
|
|
||||||
|
Submits code for execution
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"language": "python",
|
||||||
|
"code": "print('Hello, World!')",
|
||||||
|
"input": ""
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```GET /status?id={submissionId}```
|
||||||
|
|
||||||
|
Checks the status of submission:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1",
|
||||||
|
"status": "completed",
|
||||||
|
"queuedAt": "2025-03-25T14:30:00Z",
|
||||||
|
"startedAt": "2025-03-25T14:30:01Z",
|
||||||
|
"completedAt": "2025-03-25T14:30:02Z",
|
||||||
|
"executionTime": 1000
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```GET /result?id={submissionId}```
|
||||||
|
|
||||||
|
Gets the execution result of a submission.
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1",
|
||||||
|
"status": "completed",
|
||||||
|
"language": "python",
|
||||||
|
"output": "Hello, World!",
|
||||||
|
"queuedAt": "2025-03-25T14:30:00Z",
|
||||||
|
"startedAt": "2025-03-25T14:30:01Z",
|
||||||
|
"completedAt": "2025-03-25T14:30:02Z",
|
||||||
|
"executionTime": 1000,
|
||||||
|
"executionTimeFormatted": "1.0s",
|
||||||
|
"totalTime": 2000,
|
||||||
|
"totalTimeFormatted": "2.0s"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```GET /queue-stats```
|
||||||
|
Gets the statistics about the job queue.
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"queue_stats": {
|
||||||
|
"queue_length": 5,
|
||||||
|
"max_workers": 3,
|
||||||
|
"running_jobs": 3
|
||||||
|
},
|
||||||
|
"submissions": 42
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### WebSocket Endpoints
|
||||||
|
```ws://localhost:8080/ws/terminal?id={submissionId}```
|
||||||
|
|
||||||
|
Establishes a real-time connection for terminal interaction.
|
||||||
|
|
||||||
|
- The server sends execution output as plain text messages.
|
||||||
|
- The client can send input as plain text messages (with newline).
|
||||||
|
- Connection automatically closes when execution completes or fails.
|
||||||
|
|
||||||
|
### Terminal Input Handling
|
||||||
|
The system supports interactive programs requiring user input:
|
||||||
|
|
||||||
|
1. The frontend detects possible input prompts by looking for patterns
|
||||||
|
2. When detected, it focuses the terminal and allows user input
|
||||||
|
3. User input is captured in the terminal component's inputBuffer
|
||||||
|
4. When the user presses Enter, the input is:
|
||||||
|
- Sent to the backend via WebSocket.
|
||||||
|
- Displayed in the terminal.
|
||||||
|
- Buffer is cleared for next input.
|
||||||
|
5. The input is processed by the running program in real-time.
|
||||||
|
|
||||||
|
|
||||||
|
Troubleshooting tips:
|
||||||
|
|
||||||
|
- Ensure WebSocket connection is established before sending input
|
||||||
|
- Check for WebSocket errors in console
|
||||||
|
- Verify input reaches the backend by checking server logs
|
||||||
|
- Ensure newline characters are properly appended to input.
|
||||||
|
|
||||||
|
### Language Support
|
||||||
|
### Python
|
||||||
|
- **Version**: Python 3.9
|
||||||
|
- **Input Handling**: Direct stdin piping
|
||||||
|
- **Limitations**: No file I/O, no package imports outside standard library
|
||||||
|
- **Resource Limits**: 100MB memory, 10% CPU
|
||||||
|
### Java
|
||||||
|
- **Version**: Java 11 (Eclipse Temurin)
|
||||||
|
- **Class Detection**: Extracts class name from code using regex.
|
||||||
|
- **Memory Settings**: 64MB min heap, 256MB max heap
|
||||||
|
- **Resource Limits**: 400MB memory, 50% CPU
|
||||||
|
C
|
||||||
|
- **Version**: Latest GCC
|
||||||
|
- **Compilation Flags**: Default GCC settings
|
||||||
|
- **Resource Limits**: 100MB memory, 10% CPU
|
||||||
|
|
||||||
|
### C++
|
||||||
|
- **Version**: Latest G++
|
||||||
|
- **Standard**: C++17
|
||||||
|
- **Resource Limits**: 100MB memory, 10% CPU
|
||||||
|
|
||||||
|
### Security Considerations
|
||||||
|
All code execution happens within isolated Docker containers with:
|
||||||
|
|
||||||
|
- No network access (```--network=none```)
|
||||||
|
- Limited CPU and memory resources
|
||||||
|
- Limited file system access
|
||||||
|
- No persistent storage
|
||||||
|
- Execution time limits (10-15 seconds)
|
||||||
|
|
||||||
|
### Debugging
|
||||||
|
Check backend logs for execution details
|
||||||
|
Use browser developer tools to debug WebSocket connections
|
||||||
|
Terminal panel shows WebSocket connection status and errors
|
||||||
|
Check Docker logs for container-related issues.
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||||
|
|
||||||
|
|||||||
@@ -1,487 +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, 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** (handler package) - Processes incoming HTTP requests
|
|
||||||
2. **Execution Service** (service package) - Manages code execution in containers
|
|
||||||
3. **Job Queue** (queue package) - Controls concurrent execution
|
|
||||||
4. **Data Models** (model package) - Defines data structures
|
|
||||||
|
|
||||||
### 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`
|
|
||||||
- `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 main.go
|
|
||||||
```
|
|
||||||
|
|
||||||
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", "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
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
@@ -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())
|
|
||||||
1
backend/dist/artifacts.json
vendored
1
backend/dist/artifacts.json
vendored
File diff suppressed because one or more lines are too long
131
backend/dist/config.yaml
vendored
131
backend/dist/config.yaml
vendored
@@ -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
|
|
||||||
1
backend/dist/metadata.json
vendored
1
backend/dist/metadata.json
vendored
@@ -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"}}
|
|
||||||
@@ -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.
BIN
backend/dist/monaco_darwin_amd64_v1/monaco
vendored
BIN
backend/dist/monaco_darwin_amd64_v1/monaco
vendored
Binary file not shown.
BIN
backend/dist/monaco_darwin_arm64_v8.0/monaco
vendored
BIN
backend/dist/monaco_darwin_arm64_v8.0/monaco
vendored
Binary file not shown.
BIN
backend/dist/monaco_linux_386_sse2/monaco
vendored
BIN
backend/dist/monaco_linux_386_sse2/monaco
vendored
Binary file not shown.
BIN
backend/dist/monaco_linux_amd64_v1/monaco
vendored
BIN
backend/dist/monaco_linux_amd64_v1/monaco
vendored
Binary file not shown.
BIN
backend/dist/monaco_linux_arm64_v8.0/monaco
vendored
BIN
backend/dist/monaco_linux_arm64_v8.0/monaco
vendored
Binary file not shown.
BIN
backend/dist/monaco_windows_386_sse2/monaco.exe
vendored
BIN
backend/dist/monaco_windows_386_sse2/monaco.exe
vendored
Binary file not shown.
BIN
backend/dist/monaco_windows_amd64_v1/monaco.exe
vendored
BIN
backend/dist/monaco_windows_amd64_v1/monaco.exe
vendored
Binary file not shown.
BIN
backend/dist/monaco_windows_arm64_v8.0/monaco.exe
vendored
BIN
backend/dist/monaco_windows_arm64_v8.0/monaco.exe
vendored
Binary file not shown.
@@ -1,14 +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/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
|
|
||||||
)
|
|
||||||
@@ -1,23 +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/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=
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -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"])
|
|
||||||
}
|
|
||||||
@@ -1,58 +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)))
|
|
||||||
|
|
||||||
port := ":8080"
|
|
||||||
log.Printf("Server started at %s", port)
|
|
||||||
log.Fatal(http.ListenAndServe(port, nil))
|
|
||||||
}
|
|
||||||
@@ -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"`
|
|
||||||
}
|
|
||||||
@@ -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"`)
|
|
||||||
}
|
|
||||||
BIN
backend/monaco
BIN
backend/monaco
Binary file not shown.
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
@@ -1,485 +0,0 @@
|
|||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/arnab-afk/monaco/model"
|
|
||||||
"github.com/arnab-afk/monaco/queue"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ExecutionService handles code execution for multiple languages
|
|
||||||
type ExecutionService struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
queue *queue.JobQueue
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewExecutionService creates a new execution service
|
|
||||||
func NewExecutionService() *ExecutionService {
|
|
||||||
log.Println("Initializing execution service with 3 concurrent workers")
|
|
||||||
return &ExecutionService{
|
|
||||||
queue: queue.NewJobQueue(35), // 3 concurrent executions max
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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:])
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
@@ -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!")
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
@@ -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 1
|
|
||||||
37
new-backend/Dockerfile
Normal file
37
new-backend/Dockerfile
Normal 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"]
|
||||||
74
new-backend/Dockerfile.tunnel
Normal file
74
new-backend/Dockerfile.tunnel
Normal 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"]
|
||||||
66
new-backend/Dockerfile.tunnel.new
Normal file
66
new-backend/Dockerfile.tunnel.new
Normal 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
21
new-backend/Makefile
Normal 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
93
new-backend/README.md
Normal 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
|
||||||
46
new-backend/README.tunnel.md
Normal file
46
new-backend/README.tunnel.md
Normal 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
175
new-backend/api/handler.go
Normal 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
15
new-backend/config.json
Normal 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"
|
||||||
|
}
|
||||||
13
new-backend/config.json.new
Normal file
13
new-backend/config.json.new
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
168
new-backend/config/config.go
Normal file
168
new-backend/config/config.go
Normal 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
|
||||||
|
}
|
||||||
1
new-backend/credentials.json
Normal file
1
new-backend/credentials.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"AccountTag":"453afb9373a00a55876e6127cf0efd97","TunnelSecret":"02VClcBt+1nxxu8ioUzw/UizXtKKh4X7UUpneVbfB/Y=","TunnelID":"5d2682ef-0b5b-47e5-b0fa-ad48968ce016"}
|
||||||
28
new-backend/docker-compose.tunnel.yml
Normal file
28
new-backend/docker-compose.tunnel.yml
Normal 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
|
||||||
695
new-backend/executor/executor.go
Normal file
695
new-backend/executor/executor.go
Normal 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
10
new-backend/go.mod
Normal 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
8
new-backend/go.sum
Normal 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
BIN
backend/tmp/main.exe → new-backend/main
Normal file → Executable file
Binary file not shown.
98
new-backend/main.go
Normal file
98
new-backend/main.go
Normal 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")
|
||||||
|
}
|
||||||
28
new-backend/models/submission.go
Normal file
28
new-backend/models/submission.go
Normal 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"`
|
||||||
|
}
|
||||||
91
new-backend/models/ws_message.go
Normal file
91
new-backend/models/ws_message.go
Normal 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
16
new-backend/start.sh
Normal 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
|
||||||
21
new-backend/supervisord.conf
Normal file
21
new-backend/supervisord.conf
Normal 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
106
new-backend/utils/utils.go
Normal 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
29
nginx.conf
Normal 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
26
nginx/nginx.conf
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user