Compare commits
84 Commits
socket_rew
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e35d086b0 | |||
| 917de3e401 | |||
| 2e089e192d | |||
| 38cf325e22 | |||
| fff316d5ec | |||
| 16620111cf | |||
|
|
fbbcabbeab | ||
|
|
35d40da727 | ||
| ebaef13845 | |||
| b075e5b3d3 | |||
|
|
1d3b1c74e1 | ||
|
|
8ec0935486 | ||
|
|
33b9e2fe38 | ||
|
|
c579f972b8 | ||
|
|
0c844c3122 | ||
| 356d532beb | |||
|
|
2508731ec7 | ||
| 6ef2edb768 | |||
| fbafab5d51 | |||
| a433c5067f | |||
|
|
ec33604a6f | ||
|
|
dbd8770f20 | ||
|
|
9fa0528ff0 | ||
|
|
e82554215b | ||
|
|
abc15efabd | ||
|
|
b18dc5f21b | ||
|
|
336ad17240 | ||
|
|
9d180e13b1 | ||
|
|
6b98938245 | ||
|
|
9a1dee00a1 | ||
|
|
9d6729e63c | ||
|
|
4daafa726d | ||
| 47f73681af | |||
|
|
304761e258 | ||
|
|
453f44a43a | ||
|
|
e8e6011524 | ||
|
|
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 +1,2 @@
|
||||
VITE_API_URL="http://localhost:8080"
|
||||
VITE_FACULTY_API_URL="http://localhost:5000/api"
|
||||
7
Frontend/.env.example
Normal file
7
Frontend/.env.example
Normal file
@@ -0,0 +1,7 @@
|
||||
# Monaco Frontend Environment Variables
|
||||
|
||||
# Backend API URL (Monaco code execution server)
|
||||
VITE_API_URL=http://localhost:8080
|
||||
|
||||
# Faculty API URL (Faculty/Student management backend)
|
||||
VITE_FACULTY_API_URL=http://localhost:5000/api
|
||||
35
Frontend/Dockerfile.tunnel
Normal file
35
Frontend/Dockerfile.tunnel
Normal file
@@ -0,0 +1,35 @@
|
||||
# Final stage
|
||||
FROM alpine:latest
|
||||
|
||||
# Install supervisor and wget
|
||||
RUN apk update && apk add --no-cache supervisor wget
|
||||
|
||||
# Get cloudflared directly from GitHub
|
||||
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 cloudflared config and credentials
|
||||
COPY credentials.json /etc/cloudflared/credentials.json
|
||||
COPY config.json /etc/cloudflared/config.json
|
||||
|
||||
# Create supervisord config (only cloudflared, no frontend inside container)
|
||||
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: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
|
||||
|
||||
# Use supervisord to manage cloudflared
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||
@@ -1,13 +1,24 @@
|
||||
# VSCode Clone with React and Vite
|
||||
# VS Code Clone Project
|
||||
|
||||
This project is a VSCode-like code editor built with React and Vite. It features a customizable UI with an activity bar, sidebar, editor area, panel, and status bar, mimicking the look and feel of Visual Studio Code.
|
||||
## Authors
|
||||
- Arnab Bhowmik
|
||||
- Ishika Bhoyar
|
||||
|
||||
## Features
|
||||
## Description
|
||||
This project is a VS Code Clone built with React and Monaco Editor. It features a file tree navigation, tab management, code editing with syntax highlighting, and a terminal panel for running code. It mimics the core functionalities of Visual Studio Code in a browser-based environment.
|
||||
|
||||
- **Activity Bar**: Switch between different views like Explorer, Search, Source Control, etc.
|
||||
- **Sidebar**: Displays file explorer, search results, and source control information.
|
||||
- **Editor Area**: Code editor with syntax highlighting and multiple tabs.
|
||||
- **Panel**: Terminal, Problems, and Output views.
|
||||
- **Status Bar**: Displays status information and provides quick actions.
|
||||
## Frontend Functionalities
|
||||
- Built with React and Monaco Editor.
|
||||
- File tree navigation for managing files and folders.
|
||||
- Tab management for opening multiple files simultaneously.
|
||||
- Code editing with syntax highlighting and language support.
|
||||
- Terminal panel for running code and viewing output.
|
||||
- Persistent file structure and content using localStorage.
|
||||
|
||||
## Project Structure
|
||||
## Backend Functionalities
|
||||
- Built with Go and Docker for secure code execution.
|
||||
- Supports multiple programming languages (Python, Java, C/C++).
|
||||
- Executes code in isolated Docker containers with resource limits.
|
||||
- RESTful API for submitting code, checking status, and retrieving results.
|
||||
- Job queue system for managing concurrent executions.
|
||||
- Enforces timeouts and resource limits for security and performance.
|
||||
|
||||
15
Frontend/config.json
Normal file
15
Frontend/config.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"tunnel": "8c559c7c-42bb-4b9d-96a2-99cefd274b06",
|
||||
"credentials-file": "/etc/cloudflared/credentials.json",
|
||||
"ingress": [
|
||||
{
|
||||
"hostname": "monaco.ishikabhoyar.tech",
|
||||
"service": "http://host.docker.internal:8001"
|
||||
},
|
||||
{
|
||||
"service": "http_status:404"
|
||||
}
|
||||
],
|
||||
"protocol": "http2",
|
||||
"loglevel": "info"
|
||||
}
|
||||
5
Frontend/credentials.json
Normal file
5
Frontend/credentials.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"AccountTag": "453afb9373a00a55876e6127cf0efd97",
|
||||
"TunnelSecret": "afJ6YY25rj9+G6qqHy+2jss4h+QKfw6YntijRZvo4ZQ=",
|
||||
"TunnelID": "8c559c7c-42bb-4b9d-96a2-99cefd274b06"
|
||||
}
|
||||
10
Frontend/docker-compose.tunnel.yml
Normal file
10
Frontend/docker-compose.tunnel.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
tunnel:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.tunnel
|
||||
restart: unless-stopped
|
||||
network_mode: "host"
|
||||
environment:
|
||||
# Define cloudflared environment variables
|
||||
- NO_AUTOUPDATE=true
|
||||
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/public/favicon.ico" type="image/x-icon" />
|
||||
<title>VSCode</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
75
Frontend/package-lock.json
generated
75
Frontend/package-lock.json
generated
@@ -9,11 +9,13 @@
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@react-oauth/google": "^0.12.2",
|
||||
"lucide-react": "^0.483.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-resizable-panels": "^2.1.7"
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^7.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
@@ -1041,6 +1043,16 @@
|
||||
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-oauth/google": {
|
||||
"version": "0.12.2",
|
||||
"resolved": "https://registry.npmjs.org/@react-oauth/google/-/google-0.12.2.tgz",
|
||||
"integrity": "sha512-d1GVm2uD4E44EJft2RbKtp8Z1fp/gK8Lb6KHgs3pHlM0PxCXGLaq8LLYQYENnN4xPWO1gkL4apBtlPKzpLvZwg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.36.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.36.0.tgz",
|
||||
@@ -1602,6 +1614,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
|
||||
"integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
@@ -2656,9 +2677,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -2676,7 +2697,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -2745,6 +2766,44 @@
|
||||
"react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc"
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.9.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.4.tgz",
|
||||
"integrity": "sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cookie": "^1.0.1",
|
||||
"set-cookie-parser": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.9.4",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.4.tgz",
|
||||
"integrity": "sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.9.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-from": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||
@@ -2810,6 +2869,12 @@
|
||||
"semver": "bin/semver.js"
|
||||
}
|
||||
},
|
||||
"node_modules/set-cookie-parser": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||
"integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
|
||||
@@ -11,11 +11,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@react-oauth/google": "^0.12.2",
|
||||
"lucide-react": "^0.483.0",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-resizable-panels": "^2.1.7"
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"react-router-dom": "^7.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.21.0",
|
||||
|
||||
BIN
Frontend/public/BG-login(2).jpg
Normal file
BIN
Frontend/public/BG-login(2).jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 677 KiB |
BIN
Frontend/public/Bottom.png
Normal file
BIN
Frontend/public/Bottom.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
BIN
Frontend/public/Vidyavihar@3x.png
Normal file
BIN
Frontend/public/Vidyavihar@3x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.9 KiB |
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 |
BIN
Frontend/public/kjsce2x.png
Normal file
BIN
Frontend/public/kjsce2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 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,12 +1,50 @@
|
||||
import VSCodeUI from "./components/VSCodeUI.jsx"
|
||||
import "./index.css"
|
||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||
import { GoogleOAuthProvider } from '@react-oauth/google';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
import Login from './components/Login';
|
||||
import TestList from './components/TestList';
|
||||
import CodeChallenge from "./components/CodeChallenge.jsx";
|
||||
import Header from './components/Header';
|
||||
import Footer from './components/Footer';
|
||||
import ProtectedRoute from './components/ProtectedRoute';
|
||||
import "./index.css";
|
||||
|
||||
function App() {
|
||||
// Google OAuth Client ID - in production, this should be in environment variables
|
||||
const GOOGLE_CLIENT_ID = "586378657128-smg8t52eqbji66c3eg967f70hsr54q5r.apps.googleusercontent.com";
|
||||
|
||||
return (
|
||||
<GoogleOAuthProvider clientId={GOOGLE_CLIENT_ID}>
|
||||
<AuthProvider>
|
||||
<Router>
|
||||
<div className="App">
|
||||
<VSCodeUI />
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route
|
||||
path="/tests"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<TestList />
|
||||
<Footer />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/editor"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<CodeChallenge />
|
||||
<Footer />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route path="/" element={<Navigate to="/tests" replace />} />
|
||||
</Routes>
|
||||
</div>
|
||||
)
|
||||
</Router>
|
||||
</AuthProvider>
|
||||
</GoogleOAuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
1147
Frontend/src/components/CodeChallenge.jsx
Normal file
1147
Frontend/src/components/CodeChallenge.jsx
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,32 @@ import {
|
||||
} from "lucide-react";
|
||||
import Sidebar from "./Sidebar";
|
||||
import Panel from "./Panel"; // Import Panel component
|
||||
import WebSocketTerminal from "./WebSocketTerminal"; // Import WebSocket Terminal
|
||||
|
||||
// Add this function to map file extensions to language identifiers
|
||||
const getLanguageFromExtension = (extension) => {
|
||||
const extensionMap = {
|
||||
'js': 'javascript',
|
||||
'jsx': 'javascript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript',
|
||||
'py': 'python',
|
||||
'java': 'java',
|
||||
'c': 'c',
|
||||
'cpp': 'cpp',
|
||||
'h': 'c',
|
||||
'hpp': 'cpp',
|
||||
'cs': 'csharp',
|
||||
'go': 'go',
|
||||
'rb': 'ruby',
|
||||
'php': 'php',
|
||||
'html': 'html',
|
||||
'css': 'css',
|
||||
'json': 'json',
|
||||
'md': 'markdown'
|
||||
};
|
||||
|
||||
return extensionMap[extension] || 'text';
|
||||
};
|
||||
|
||||
const EditorArea = ({
|
||||
sidebarVisible = true,
|
||||
@@ -63,12 +88,8 @@ const EditorArea = ({
|
||||
|
||||
// Add a new state for user input
|
||||
const [userInput, setUserInput] = useState("");
|
||||
// Add a new state for waiting for input
|
||||
const [waitingForInput, setWaitingForInput] = useState(false);
|
||||
// Add a new state for tracking the active submission ID
|
||||
const [activeRunningSubmissionId, setActiveRunningSubmissionId] = useState(null);
|
||||
// Add a state to toggle between regular and WebSocket terminals
|
||||
const [useWebSocket, setUseWebSocket] = useState(false);
|
||||
// Add socket state to track the connection
|
||||
const [activeSocket, setActiveSocket] = useState(null);
|
||||
|
||||
// Focus the input when new file modal opens
|
||||
useEffect(() => {
|
||||
@@ -115,11 +136,6 @@ const EditorArea = ({
|
||||
useEffect(() => {
|
||||
localStorage.setItem("vscode-clone-files", JSON.stringify(files));
|
||||
}, [files]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("vscode-clone-structure", JSON.stringify(fileStructure));
|
||||
}, [fileStructure]);
|
||||
|
||||
// Add this effect to handle editor resize when sidebar changes
|
||||
useEffect(() => {
|
||||
// Force editor to readjust layout when sidebar visibility changes
|
||||
@@ -137,6 +153,41 @@ const EditorArea = ({
|
||||
}
|
||||
}, [panelVisible]);
|
||||
|
||||
// Add this useEffect for cleanup
|
||||
useEffect(() => {
|
||||
// Cleanup function to close socket when component unmounts
|
||||
return () => {
|
||||
if (activeSocket) {
|
||||
activeSocket.close();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Add interval to poll execution status
|
||||
useEffect(() => {
|
||||
const checkInterval = setInterval(() => {
|
||||
// Poll execution status
|
||||
if (activeSocket && activeRunningFile) {
|
||||
// Check if socket is still connected
|
||||
if (activeSocket.readyState !== WebSocket.OPEN) {
|
||||
console.warn("Socket not in OPEN state:", activeSocket.readyState);
|
||||
setTerminalOutput(prev => [...prev, {
|
||||
type: 'warning',
|
||||
content: `Terminal connection lost, attempting to reconnect...`
|
||||
}]);
|
||||
// Could implement reconnection logic here
|
||||
}
|
||||
}
|
||||
}, 5000);
|
||||
|
||||
// Clean up interval when component unmounts
|
||||
return () => {
|
||||
if (checkInterval) {
|
||||
clearInterval(checkInterval);
|
||||
}
|
||||
};
|
||||
}, [activeSocket, activeRunningFile]);
|
||||
|
||||
const handleEditorDidMount = (editor) => {
|
||||
editorRef.current = editor;
|
||||
};
|
||||
@@ -484,21 +535,31 @@ const EditorArea = ({
|
||||
case "README.md":
|
||||
return `# VS Code Clone Project
|
||||
|
||||
## Overview
|
||||
This is a simple VS Code clone built with React and Monaco Editor.
|
||||
## Authors
|
||||
- Arnab Bhowmik
|
||||
- Ishika Bhoyar
|
||||
|
||||
## Features
|
||||
- File tree navigation
|
||||
- Tab management
|
||||
- Code editing with Monaco Editor
|
||||
- Syntax highlighting
|
||||
## Description
|
||||
This project is a VS Code Clone built with React and Monaco Editor. It features a file tree navigation, tab management, code editing with syntax highlighting, and a terminal panel for running code. It mimics the core functionalities of Visual Studio Code in a browser-based environment.
|
||||
|
||||
## Frontend Functionalities
|
||||
- Built with React and Monaco Editor.
|
||||
- File tree navigation for managing files and folders.
|
||||
- Tab management for opening multiple files simultaneously.
|
||||
- Code editing with syntax highlighting and language support.
|
||||
- Terminal panel for running code and viewing output.
|
||||
- Persistent file structure and content using localStorage.
|
||||
|
||||
## Backend Functionalities
|
||||
- Built with Go and Docker for secure code execution.
|
||||
- Supports multiple programming languages (Python, Java, C/C++).
|
||||
- Executes code in isolated Docker containers with resource limits.
|
||||
- RESTful API for submitting code, checking status, and retrieving results.
|
||||
- Job queue system for managing concurrent executions.
|
||||
- Enforces timeouts and resource limits for security and performance.
|
||||
`;
|
||||
|
||||
## Getting Started
|
||||
1. Create a new file using the + button in the sidebar
|
||||
2. Edit your code in the editor
|
||||
3. Save changes using the save button
|
||||
|
||||
Happy coding!`;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -512,7 +573,7 @@ Happy coding!`;
|
||||
width: `calc(100% - ${sidebarVisible ? sidebarWidth : 0}px)`
|
||||
};
|
||||
|
||||
// Modified handleRunCode to start execution immediately
|
||||
// Update the handleRunCode function
|
||||
const handleRunCode = async () => {
|
||||
if (!activeFile) return;
|
||||
|
||||
@@ -522,47 +583,36 @@ Happy coding!`;
|
||||
setPanelVisible(true);
|
||||
}
|
||||
|
||||
// Reset states
|
||||
setIsRunning(true);
|
||||
setWaitingForInput(false);
|
||||
setActiveRunningFile(activeFile.id);
|
||||
setActiveRunningSubmissionId(null);
|
||||
setUserInput('');
|
||||
|
||||
// Get language from file extension
|
||||
// Clear previous output and add new command
|
||||
const fileExtension = activeFile.id.split('.').pop().toLowerCase();
|
||||
const language = getLanguageFromExtension(fileExtension);
|
||||
|
||||
// If using WebSocket mode, we'll use the WebSocketTerminal component
|
||||
if (useWebSocket) {
|
||||
// Just set the running state, the WebSocketTerminal will handle the rest
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular HTTP mode - use polling
|
||||
// Clear previous output and add new command
|
||||
const newOutput = [
|
||||
{ type: 'command', content: `$ run ${activeFile.id}` },
|
||||
{ type: 'output', content: '------- PROGRAM EXECUTION -------' },
|
||||
{ type: 'output', content: `Language: ${language}` },
|
||||
{ type: 'output', content: 'Executing code...' }
|
||||
{ type: 'output', content: 'Submitting code...' }
|
||||
];
|
||||
setTerminalOutput(newOutput);
|
||||
|
||||
try {
|
||||
// Close any existing socket
|
||||
if (activeSocket) {
|
||||
activeSocket.close();
|
||||
setActiveSocket(null);
|
||||
}
|
||||
|
||||
// Use API URL from environment variable
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
||||
|
||||
try {
|
||||
// Submit the code for execution immediately
|
||||
const submitResponse = await fetch(`${apiUrl}/submit`, {
|
||||
// Submit the code to get an execution ID
|
||||
const submitResponse = await fetch(`${apiUrl}/api/submit`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
language: getLanguageFromExtension(fileExtension),
|
||||
language: language,
|
||||
code: activeFile.content,
|
||||
input: '' // No initial input
|
||||
input: "" // Explicitly passing empty input, no user input handling
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -571,265 +621,274 @@ Happy coding!`;
|
||||
}
|
||||
|
||||
const { id } = await submitResponse.json();
|
||||
setActiveRunningSubmissionId(id);
|
||||
setTerminalOutput(prev => [...prev, { type: 'output', content: `Job submitted with ID: ${id}` }]);
|
||||
|
||||
// Start polling for status and output
|
||||
pollForStatusAndOutput(id);
|
||||
} catch (error) {
|
||||
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
|
||||
setIsRunning(false);
|
||||
setActiveRunningSubmissionId(null);
|
||||
}
|
||||
// Set active running file
|
||||
setActiveRunningFile(activeFile.id);
|
||||
|
||||
// Connect to WebSocket with the execution ID
|
||||
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsBaseUrl = apiUrl.replace(/^https?:\/\//, '');
|
||||
const wsUrl = `${wsProtocol}//${wsBaseUrl}/api/ws/terminal/${id}`;
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
// Toggle between WebSocket and HTTP modes
|
||||
const toggleWebSocketMode = () => {
|
||||
setUseWebSocket(!useWebSocket);
|
||||
};
|
||||
|
||||
// Simplified handleInputSubmit to only handle interactive input
|
||||
const handleInputSubmit = async () => {
|
||||
if (!waitingForInput || !activeRunningSubmissionId) return;
|
||||
|
||||
// Store the input value before clearing it
|
||||
const inputValue = userInput;
|
||||
|
||||
// Clear the input field and reset waiting state immediately for better UX
|
||||
setUserInput('');
|
||||
setWaitingForInput(false);
|
||||
|
||||
// Add the input to the terminal immediately
|
||||
setTerminalOutput(prev => [
|
||||
...prev,
|
||||
{ type: 'input', content: inputValue }
|
||||
]);
|
||||
|
||||
// Use API URL from environment variable
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
||||
newSocket.onmessage = (event) => {
|
||||
console.log("WebSocket message received:", event.data);
|
||||
|
||||
try {
|
||||
// Submit input to the running program
|
||||
const submitInputResponse = await fetch(`${apiUrl}/submit-input`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: activeRunningSubmissionId,
|
||||
input: inputValue
|
||||
}),
|
||||
});
|
||||
const message = JSON.parse(event.data);
|
||||
|
||||
if (!submitInputResponse.ok) {
|
||||
throw new Error(`Server error: ${submitInputResponse.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
|
||||
}]);
|
||||
}
|
||||
|
||||
// Wait for a moment to allow the program to process the input
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Poll for status and check if we need more input
|
||||
pollForStatusAndOutput(activeRunningSubmissionId);
|
||||
|
||||
} catch (error) {
|
||||
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
|
||||
setIsRunning(false);
|
||||
setActiveRunningSubmissionId(null);
|
||||
} 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 a function to poll for status and output
|
||||
const pollForStatusAndOutput = async (id) => {
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
||||
|
||||
// Add polling for job status
|
||||
let statusCheckInterval;
|
||||
if (id) {
|
||||
// Start polling the status endpoint every 2 seconds
|
||||
statusCheckInterval = setInterval(async () => {
|
||||
try {
|
||||
// Step 2: Poll for status until completed, failed, or waiting_for_input
|
||||
let status = 'pending';
|
||||
while (status !== 'completed' && status !== 'failed' && status !== 'waiting_for_input') {
|
||||
// Add a small delay between polls
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const statusResponse = await fetch(`${apiUrl}/status?id=${id}`);
|
||||
if (!statusResponse.ok) {
|
||||
throw new Error(`Status check failed: ${statusResponse.status}`);
|
||||
}
|
||||
|
||||
const statusResponse = await fetch(`${apiUrl}/api/status/${id}`);
|
||||
if (statusResponse.ok) {
|
||||
const statusData = await statusResponse.json();
|
||||
status = statusData.status;
|
||||
|
||||
// Update terminal with status (for any status type)
|
||||
// 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 => {
|
||||
// Update the last status message or add a new one
|
||||
const hasStatus = prev.some(line => line.content.includes('Status:'));
|
||||
if (hasStatus) {
|
||||
return prev.map(line =>
|
||||
line.content.includes('Status:')
|
||||
? { ...line, content: `Status: ${status}` }
|
||||
: line
|
||||
);
|
||||
} else {
|
||||
return [...prev, { type: 'output', content: `Status: ${status}` }];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Check if we're waiting for input
|
||||
if (status === 'waiting_for_input') {
|
||||
// Get the current output to display to the user
|
||||
const resultResponse = await fetch(`${apiUrl}/result?id=${id}`);
|
||||
if (resultResponse.ok) {
|
||||
const { output } = await resultResponse.json();
|
||||
|
||||
// Process the output to show what's happened so far
|
||||
const outputLines = [];
|
||||
let promptText = '';
|
||||
|
||||
// Split by lines and process each line
|
||||
const lines = output.split('\n');
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
if (line.startsWith('[Input] ')) {
|
||||
// This is an input line
|
||||
outputLines.push({
|
||||
type: 'input',
|
||||
content: line.substring(8) // Remove the '[Input] ' prefix
|
||||
});
|
||||
} else if (line === '[WAITING_FOR_INPUT]') {
|
||||
// This is a marker for waiting for input
|
||||
// If there's a line before this, it's likely the prompt
|
||||
if (i > 0 && lines[i-1].trim() !== '') {
|
||||
promptText = lines[i-1];
|
||||
}
|
||||
continue;
|
||||
} else if (line.trim() !== '') {
|
||||
// This is a regular output line
|
||||
outputLines.push({
|
||||
// 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: line
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update the terminal with the current output
|
||||
if (outputLines.length > 0) {
|
||||
setTerminalOutput(prev => {
|
||||
// Keep only the essential lines to avoid duplication
|
||||
const essentialLines = prev.filter(line =>
|
||||
line.type === 'command' ||
|
||||
line.content.includes('PROGRAM EXECUTION') ||
|
||||
line.content.includes('Language:') ||
|
||||
line.content.includes('Job submitted') ||
|
||||
line.content.includes('Status:') ||
|
||||
line.content === 'Executing code...'
|
||||
);
|
||||
return [...essentialLines, ...outputLines];
|
||||
});
|
||||
}
|
||||
|
||||
// Now set the waiting for input state
|
||||
setWaitingForInput(true);
|
||||
|
||||
// Add a message indicating we're waiting for input
|
||||
setTerminalOutput(prev => {
|
||||
// Remove any existing waiting message
|
||||
const filteredPrev = prev.filter(line =>
|
||||
line.content !== 'Waiting for input...'
|
||||
);
|
||||
|
||||
// Add the prompt text if available
|
||||
if (promptText) {
|
||||
return [...filteredPrev, {
|
||||
type: 'prompt',
|
||||
content: promptText
|
||||
}, {
|
||||
type: 'output',
|
||||
content: 'Waiting for input...'
|
||||
}];
|
||||
} else {
|
||||
return [...filteredPrev, {
|
||||
type: 'output',
|
||||
content: 'Waiting for input...'
|
||||
content: `\n[System] Final output:\n${statusData.output}`
|
||||
}];
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
|
||||
// Close socket if it's still open
|
||||
if (newSocket && newSocket.readyState === WebSocket.OPEN) {
|
||||
newSocket.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Status check error:", error);
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
// Clean up interval when component unmounts or when socket closes
|
||||
newSocket.addEventListener('close', () => {
|
||||
if (statusCheckInterval) {
|
||||
clearInterval(statusCheckInterval);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
newSocket.onclose = (event) => {
|
||||
console.log("WebSocket closed:", event);
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
newSocket.onerror = (event) => {
|
||||
console.error("WebSocket error:", event);
|
||||
setTerminalOutput(prev => [...prev, {
|
||||
type: 'warning',
|
||||
content: `WebSocket error occurred`
|
||||
}]);
|
||||
};
|
||||
|
||||
// Set the active socket after all handlers are defined
|
||||
setActiveSocket(newSocket);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Run code error:", error);
|
||||
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
|
||||
setIsRunning(false);
|
||||
|
||||
// Also add cleanup in the error handler
|
||||
if (statusCheckInterval) {
|
||||
clearInterval(statusCheckInterval);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Update handleInputSubmit to ensure the input is sent properly
|
||||
const handleInputSubmit = (input) => {
|
||||
// Use the direct input parameter instead of relying on userInput state
|
||||
const textToSend = input || userInput;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Get the result for both completed and failed status
|
||||
const resultResponse = await fetch(`${apiUrl}/result?id=${id}`);
|
||||
if (!resultResponse.ok) {
|
||||
throw new Error(`Result fetch failed: ${resultResponse.status}`);
|
||||
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;
|
||||
}
|
||||
|
||||
const { output } = await resultResponse.json();
|
||||
try {
|
||||
// Add the input to the terminal display
|
||||
setTerminalOutput(prev => [...prev, { type: 'command', content: `> ${textToSend}` }]);
|
||||
|
||||
// Format and display output
|
||||
const outputLines = [];
|
||||
// Send the input via WebSocket
|
||||
console.log("Sending input:", textToSend);
|
||||
|
||||
// Add a header
|
||||
outputLines.push({
|
||||
type: status === 'failed' ? 'warning' : 'output',
|
||||
content: status === 'failed'
|
||||
? '------- EXECUTION FAILED -------'
|
||||
: '------- EXECUTION RESULT -------'
|
||||
});
|
||||
|
||||
// Process the output line by line
|
||||
output.split('\n').forEach(line => {
|
||||
// Check if this is an input line
|
||||
if (line.startsWith('[Input] ')) {
|
||||
outputLines.push({
|
||||
type: 'input',
|
||||
content: line.substring(8) // Remove the '[Input] ' prefix
|
||||
});
|
||||
} else {
|
||||
outputLines.push({
|
||||
type: status === 'failed' ? 'warning' : 'output',
|
||||
content: line
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setTerminalOutput(prev => [
|
||||
...prev,
|
||||
...outputLines
|
||||
]);
|
||||
|
||||
if (status === 'failed') {
|
||||
console.error('Code execution failed:', output);
|
||||
}
|
||||
|
||||
// Reset state
|
||||
setIsRunning(false);
|
||||
setWaitingForInput(false);
|
||||
setActiveRunningSubmissionId(null);
|
||||
// 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) {
|
||||
setTerminalOutput(prev => [...prev, { type: 'warning', content: `Error: ${error.message}` }]);
|
||||
setIsRunning(false);
|
||||
setWaitingForInput(false);
|
||||
setActiveRunningSubmissionId(null);
|
||||
console.error("Error sending input:", error);
|
||||
setTerminalOutput(prev => [...prev, {
|
||||
type: 'warning',
|
||||
content: `Error sending input: ${error.message}`
|
||||
}]);
|
||||
}
|
||||
};
|
||||
|
||||
// Helper function to convert file extension to language identifier for API
|
||||
const getLanguageFromExtension = (extension) => {
|
||||
const languageMap = {
|
||||
'java': 'java',
|
||||
'c': 'c',
|
||||
'cpp': 'cpp',
|
||||
'py': 'python',
|
||||
'js': 'javascript',
|
||||
'jsx': 'javascript',
|
||||
'ts': 'typescript',
|
||||
'tsx': 'typescript'
|
||||
};
|
||||
|
||||
return languageMap[extension] || extension;
|
||||
};
|
||||
|
||||
// Update this function to also update parent state
|
||||
const togglePanel = () => {
|
||||
const newState = !showPanel;
|
||||
@@ -944,38 +1003,15 @@ Happy coding!`;
|
||||
title="Run code"
|
||||
>
|
||||
{isRunning ? <Loader size={16} className="animate-spin" /> : <Play size={16} />}
|
||||
|
||||
</button>
|
||||
<button
|
||||
className="terminal-toggle-button"
|
||||
onClick={togglePanel}
|
||||
onClick={togglePanel} // Use the new function
|
||||
title="Toggle terminal"
|
||||
>
|
||||
<Terminal size={16} />
|
||||
</button>
|
||||
<button
|
||||
className={`websocket-toggle-button ${useWebSocket ? 'active' : ''}`}
|
||||
onClick={toggleWebSocketMode}
|
||||
title={`${useWebSocket ? 'Disable' : 'Enable'} WebSocket mode`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M5 12s2-3 5-3 5 3 5 3-2 3-5 3-5-3-5-3z"></path>
|
||||
<circle cx="12" cy="12" r="1"></circle>
|
||||
<path d="M2 4l3 3"></path>
|
||||
<path d="M22 4l-3 3"></path>
|
||||
<path d="M2 20l3-3"></path>
|
||||
<path d="M22 20l-3-3"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -1034,20 +1070,10 @@ Happy coding!`;
|
||||
document.addEventListener("mouseup", onMouseUp);
|
||||
}}
|
||||
/>
|
||||
{useWebSocket && activeFile ? (
|
||||
<div style={{ height: panelHeight + 'px' }}>
|
||||
<WebSocketTerminal
|
||||
code={activeFile.content}
|
||||
language={getLanguageFromExtension(activeFile.id.split('.').pop().toLowerCase())}
|
||||
onClose={togglePanel}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Panel
|
||||
height={panelHeight}
|
||||
terminalOutput={terminalOutput}
|
||||
isRunning={isRunning}
|
||||
waitingForInput={waitingForInput}
|
||||
activeRunningFile={activeRunningFile}
|
||||
initialTab="terminal"
|
||||
onClose={togglePanel}
|
||||
@@ -1055,7 +1081,6 @@ Happy coding!`;
|
||||
onUserInputChange={setUserInput}
|
||||
onInputSubmit={handleInputSubmit}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
14
Frontend/src/components/Footer.jsx
Normal file
14
Frontend/src/components/Footer.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
const Footer = () => {
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
41
Frontend/src/components/Header.jsx
Normal file
41
Frontend/src/components/Header.jsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const Header = () => {
|
||||
const { user, logout } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
return (
|
||||
<header className="bg-gray-800 border-b border-gray-700 px-4 py-2 flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<h1 className="text-white font-bold text-lg">Monaco Editor</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
{user?.picture && (
|
||||
<img
|
||||
src={user.picture}
|
||||
alt="Profile"
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
)}
|
||||
<span className="text-gray-300 text-sm">
|
||||
Welcome, {user?.name || user?.email}
|
||||
</span>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="bg-red-600 hover:bg-red-700 text-white text-sm px-3 py-1 rounded transition-colors duration-200"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
161
Frontend/src/components/Login.jsx
Normal file
161
Frontend/src/components/Login.jsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { GoogleLogin } from '@react-oauth/google';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import ThemeToggle from './ThemeToggle';
|
||||
|
||||
const Login = () => {
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const navigate = useNavigate();
|
||||
const { login } = useAuth();
|
||||
|
||||
// Check if user is already logged in
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('monaco_token');
|
||||
if (token) {
|
||||
navigate('/tests');
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
const handleLoginSuccess = async (credentialResponse) => {
|
||||
console.log('Google login success:', credentialResponse);
|
||||
|
||||
if (credentialResponse.credential) {
|
||||
try {
|
||||
console.log('Processing Google credential...');
|
||||
|
||||
// For demo purposes, we'll decode the JWT token to get user info
|
||||
// In a real app, you would send this to your backend for verification
|
||||
const base64Url = credentialResponse.credential.split('.')[1];
|
||||
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
|
||||
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(''));
|
||||
|
||||
const userInfo = JSON.parse(jsonPayload);
|
||||
console.log('User info:', userInfo);
|
||||
|
||||
const success = await login(userInfo.email, credentialResponse.credential, userInfo);
|
||||
|
||||
if (success) {
|
||||
navigate('/tests');
|
||||
} else {
|
||||
throw new Error('Authentication failed');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error during login:', error);
|
||||
setError(`Authentication failed: ${error.message || 'Please try again.'}`);
|
||||
}
|
||||
} else {
|
||||
console.error('No credential received from Google');
|
||||
setError('No credential received from Google. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLoginError = () => {
|
||||
console.error('Google login failed - checking network and configuration...');
|
||||
|
||||
// Check if we're online
|
||||
if (!navigator.onLine) {
|
||||
setError('You appear to be offline. Please check your internet connection and try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if Google Identity Services script loaded
|
||||
if (typeof window !== 'undefined' && !window.google) {
|
||||
console.error('Google Identity Services script not loaded');
|
||||
setError('Google authentication service is not available. Please refresh the page and try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
setError('Google login failed. This might be due to network connectivity issues, browser compatibility, or Google account configuration. Please try again or contact support if the problem persists.');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-container">
|
||||
{/* Theme Toggle - Top Right */}
|
||||
<div className="theme-toggle">
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
|
||||
{/* Left Side - Background Image */}
|
||||
<div className="login-left">
|
||||
<img
|
||||
src="/BG-login(2).jpg"
|
||||
alt="Login Background"
|
||||
className="login-bg-image"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Right Side - Login Form */}
|
||||
<div className="login-right">
|
||||
<div className="login-form-container">
|
||||
{/* Logos */}
|
||||
<div className="login-logos">
|
||||
<img
|
||||
src="/Vidyavihar@3x.png"
|
||||
alt="KJSCE"
|
||||
className="login-logo"
|
||||
/>
|
||||
<img
|
||||
src="/kjsce2x.png"
|
||||
alt="Somaiya Vidyavihar"
|
||||
className="login-logo"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<h1 className="login-title">
|
||||
Welcome To Monaco Editor
|
||||
</h1>
|
||||
<p className="login-subtitle">
|
||||
Please sign in with your Google account to continue.
|
||||
</p>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="login-error">
|
||||
<p style={{ fontSize: '0.875rem', margin: 0 }}>{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Google Login Button */}
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: '1.5rem' }}>
|
||||
<GoogleLogin
|
||||
onSuccess={handleLoginSuccess}
|
||||
onError={handleLoginError}
|
||||
useOneTap
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="login-footer">
|
||||
<p className="login-footer-text">
|
||||
Need help?{' '}
|
||||
<button className="login-footer-link">
|
||||
Contact admin
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="login-demo-note">
|
||||
<p className="login-demo-text">
|
||||
Sign in with your Google account to access the Monaco Editor
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Trust Logo */}
|
||||
<div className="login-trust-logo">
|
||||
<img
|
||||
src="/Bottom.png"
|
||||
alt="Somaiya Trust"
|
||||
className="trust-logo-img"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
@@ -1,150 +0,0 @@
|
||||
import React from "react"
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { ChevronDown } from "lucide-react"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
} from "@/components/ui/navigation-menu"
|
||||
|
||||
export function Navbar() {
|
||||
return (
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-16 items-center justify-between">
|
||||
<Link href="/" className="flex items-center space-x-2">
|
||||
<span className="text-xl font-bold">*Azzle</span>
|
||||
</Link>
|
||||
|
||||
<NavigationMenu className="hidden md:flex">
|
||||
<NavigationMenuList>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>
|
||||
Demo <ChevronDown className="h-4 w-4" />
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<div className="grid gap-3 p-6 w-[400px]">
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/demo/features"
|
||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">Features</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||
Explore all the features our platform has to offer
|
||||
</p>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/demo/pricing"
|
||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">Pricing</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||
View our flexible pricing plans
|
||||
</p>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</div>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<Link href="/about" legacyBehavior passHref>
|
||||
<NavigationMenuLink className="group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50">
|
||||
About
|
||||
</NavigationMenuLink>
|
||||
</Link>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>
|
||||
Services <ChevronDown className="h-4 w-4" />
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<div className="grid gap-3 p-6 w-[400px]">
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/services/consulting"
|
||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">Consulting</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||
Expert guidance for your business needs
|
||||
</p>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/services/implementation"
|
||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">Implementation</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||
Full-service implementation and support
|
||||
</p>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</div>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>
|
||||
Pages <ChevronDown className="h-4 w-4" />
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<div className="grid gap-3 p-6 w-[400px]">
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/blog"
|
||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">Blog</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||
Read our latest articles and updates
|
||||
</p>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
<NavigationMenuLink asChild>
|
||||
<Link
|
||||
href="/resources"
|
||||
className="block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground"
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">Resources</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||
Helpful guides and documentation
|
||||
</p>
|
||||
</Link>
|
||||
</NavigationMenuLink>
|
||||
</div>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem>
|
||||
<Link href="/contact" legacyBehavior passHref>
|
||||
<NavigationMenuLink className="group inline-flex h-10 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus:outline-none disabled:pointer-events-none disabled:opacity-50 data-[active]:bg-accent/50 data-[state=open]:bg-accent/50">
|
||||
Contact
|
||||
</NavigationMenuLink>
|
||||
</Link>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
|
||||
<div className="flex items-center space-x-4">
|
||||
<Button variant="ghost" asChild>
|
||||
<Link href="/login">Login</Link>
|
||||
</Button>
|
||||
<Button asChild>
|
||||
<Link href="/signup">Sign up free</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Navbar
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { X } from "lucide-react";
|
||||
import React, { useState, useEffect, useRef } from "react";
|
||||
import { X, Maximize2, ChevronDown, Plus } from "lucide-react";
|
||||
|
||||
const Panel = ({
|
||||
height,
|
||||
@@ -12,86 +11,106 @@ const Panel = ({
|
||||
onClose,
|
||||
userInput = "",
|
||||
onUserInputChange,
|
||||
onInputSubmit
|
||||
onInputSubmit,
|
||||
}) => {
|
||||
const [activeTab, setActiveTab] = useState(initialTab);
|
||||
const terminalRef = useRef(null);
|
||||
const [inputBuffer, setInputBuffer] = useState("");
|
||||
|
||||
// Set active tab when initialTab changes
|
||||
// Update active tab when initialTab changes
|
||||
useEffect(() => {
|
||||
setActiveTab(initialTab);
|
||||
}, [initialTab]);
|
||||
|
||||
const renderTerminal = () => {
|
||||
return (
|
||||
<div className="panel-terminal">
|
||||
// Auto-scroll terminal to the bottom when content changes
|
||||
useEffect(() => {
|
||||
if (terminalRef.current) {
|
||||
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
|
||||
}
|
||||
}, [terminalOutput]);
|
||||
|
||||
// Handle keyboard input for the terminal
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (!isRunning) return;
|
||||
|
||||
if (e.key === "Enter") {
|
||||
if (inputBuffer.trim() && onInputSubmit) {
|
||||
e.preventDefault();
|
||||
// Update parent's userInput state directly and call submit in the same function
|
||||
// instead of using setTimeout which creates a race condition
|
||||
onUserInputChange(inputBuffer);
|
||||
onInputSubmit(inputBuffer); // Pass inputBuffer directly to avoid race condition
|
||||
setInputBuffer("");
|
||||
}
|
||||
} else if (e.key === "Backspace") {
|
||||
setInputBuffer((prev) => prev.slice(0, -1));
|
||||
} else if (e.key.length === 1) {
|
||||
setInputBuffer((prev) => prev + e.key);
|
||||
}
|
||||
};
|
||||
|
||||
const terminalElement = terminalRef.current;
|
||||
terminalElement?.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
terminalElement?.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [isRunning, inputBuffer, onInputSubmit, onUserInputChange]);
|
||||
|
||||
// Render the terminal tab
|
||||
const renderTerminal = () => (
|
||||
<div
|
||||
className="panel-terminal"
|
||||
ref={terminalRef}
|
||||
tabIndex={0} // Make div focusable
|
||||
onClick={() => terminalRef.current?.focus()} // Focus when clicked
|
||||
>
|
||||
{terminalOutput.length > 0 ? (
|
||||
// Render output from EditorArea when available
|
||||
<>
|
||||
{terminalOutput.map((line, index) => (
|
||||
<div key={index} className={`terminal-line ${line.type === 'warning' ? 'terminal-warning' : line.type === 'input' ? 'terminal-input-line' : line.type === 'prompt' ? 'terminal-prompt-line' : 'terminal-output'}`}>
|
||||
{line.type === 'command' ? <span className="terminal-prompt">$</span> : ''}
|
||||
{line.type === 'input' ? <span className="terminal-input-marker">[Input]</span> : ''}
|
||||
{line.type === 'prompt' ? <span className="terminal-prompt-marker">></span> : ''}
|
||||
{terminalOutput.map((line, index) => {
|
||||
const typeClass =
|
||||
line.type === "warning"
|
||||
? "terminal-warning"
|
||||
: line.type === "error"
|
||||
? "terminal-error"
|
||||
: "terminal-output";
|
||||
|
||||
return (
|
||||
<div key={index} className={`terminal-line ${typeClass}`}>
|
||||
{line.timestamp && (
|
||||
<span className="terminal-timestamp">{line.timestamp} </span>
|
||||
)}
|
||||
{line.type === "command" && <span className="terminal-prompt">$</span>}
|
||||
{line.content}
|
||||
</div>
|
||||
))}
|
||||
{waitingForInput && (
|
||||
<div className="terminal-line terminal-input-container">
|
||||
<div className="terminal-input-header">
|
||||
<span className="terminal-input-marker">Input Required:</span>
|
||||
</div>
|
||||
<div className="terminal-input-wrapper">
|
||||
<div className="terminal-input-prompt">></div>
|
||||
<input
|
||||
type="text"
|
||||
className="terminal-input"
|
||||
value={userInput}
|
||||
onChange={(e) => onUserInputChange && onUserInputChange(e.target.value)}
|
||||
placeholder="Enter input for your program here..."
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && onInputSubmit) {
|
||||
onInputSubmit();
|
||||
}
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="terminal-input-help">
|
||||
Press Enter to submit input
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{isRunning && (
|
||||
<div className="terminal-line terminal-input-line">
|
||||
<span className="terminal-prompt">$</span> {inputBuffer}
|
||||
<span className="terminal-cursor"></span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
// Default terminal content when no output
|
||||
<>
|
||||
<div className="terminal-line">
|
||||
<span className="terminal-prompt">$</span> npm start
|
||||
</div>
|
||||
<div className="terminal-line terminal-output">Starting the development server...</div>
|
||||
<div className="terminal-line terminal-output">Compiled successfully!</div>
|
||||
<div className="terminal-line terminal-output">You can now view vscode-clone in the browser.</div>
|
||||
<div className="terminal-line terminal-output">Local: http://localhost:3000</div>
|
||||
<div className="terminal-line terminal-output">On Your Network: http://192.168.1.5:3000</div>
|
||||
<div className="terminal-line">
|
||||
<span className="terminal-prompt">$</span>
|
||||
<span className="terminal-cursor"></span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderProblems = () => {
|
||||
return (
|
||||
// Render other tabs
|
||||
const renderProblems = () => (
|
||||
<div className="panel-problems">
|
||||
<div className="panel-empty-message">No problems have been detected in the workspace.</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderOutput = () => {
|
||||
return (
|
||||
const renderOutput = () => (
|
||||
<div className="panel-output">
|
||||
<div className="output-line">[Extension Host] Extension host started.</div>
|
||||
<div className="output-line">[Language Server] Language server started.</div>
|
||||
@@ -100,8 +119,27 @@ const Panel = ({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDebugConsole = () => (
|
||||
<div className="panel-debug-console">
|
||||
<div className="debug-line">Debug session not yet started.</div>
|
||||
<div className="debug-line">Press F5 to start debugging.</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderPorts = () => (
|
||||
<div className="panel-ports">
|
||||
<div className="ports-line">No forwarded ports detected.</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderComments = () => (
|
||||
<div className="panel-comments">
|
||||
<div className="comments-line">No comments have been added to this workspace.</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Get content for the active tab
|
||||
const getTabContent = () => {
|
||||
switch (activeTab) {
|
||||
case "terminal":
|
||||
@@ -110,6 +148,12 @@ const Panel = ({
|
||||
return renderProblems();
|
||||
case "output":
|
||||
return renderOutput();
|
||||
case "debug":
|
||||
return renderDebugConsole();
|
||||
case "ports":
|
||||
return renderPorts();
|
||||
case "comments":
|
||||
return renderComments();
|
||||
default:
|
||||
return <div>Unknown tab</div>;
|
||||
}
|
||||
@@ -118,76 +162,29 @@ const Panel = ({
|
||||
return (
|
||||
<div className="panel" style={{ height: `${height}px` }}>
|
||||
<div className="panel-tabs">
|
||||
{["problems", "output", "debug", "terminal", "ports", "comments"].map((tab) => (
|
||||
<div
|
||||
className={`panel-tab ${activeTab === "problems" ? "active" : ""}`}
|
||||
onClick={() => setActiveTab("problems")}
|
||||
key={tab}
|
||||
className={`panel-tab ${activeTab === tab ? "active" : ""}`}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
>
|
||||
<span className="tab-icon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="8" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<span className="tab-name">Problems</span>
|
||||
</div>
|
||||
<div className={`panel-tab ${activeTab === "output" ? "active" : ""}`} onClick={() => setActiveTab("output")}>
|
||||
<span className="tab-icon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10"></circle>
|
||||
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<span className="tab-name">Output</span>
|
||||
</div>
|
||||
<div
|
||||
className={`panel-tab ${activeTab === "terminal" ? "active" : ""}`}
|
||||
onClick={() => setActiveTab("terminal")}
|
||||
>
|
||||
<span className="tab-icon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<polyline points="4 17 10 11 4 5"></polyline>
|
||||
<line x1="12" y1="19" x2="20" y2="19"></line>
|
||||
</svg>
|
||||
</span>
|
||||
<span className="tab-name">Terminal</span>
|
||||
<span className="tab-name">{tab.toUpperCase()}</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Add close button */}
|
||||
<div className="panel-actions">
|
||||
{/* <button className="panel-action-btn">
|
||||
<span className="current-terminal">node - frontend</span>
|
||||
<ChevronDown size={16} />
|
||||
</button>
|
||||
<button className="panel-action-btn">
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
<button className="panel-action-btn">
|
||||
<Maximize2 size={16} />
|
||||
</button> */}
|
||||
<button className="panel-close-btn" onClick={onClose}>
|
||||
<X size={14} />
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -198,4 +195,3 @@ const Panel = ({
|
||||
};
|
||||
|
||||
export default Panel;
|
||||
|
||||
|
||||
21
Frontend/src/components/ProtectedRoute.jsx
Normal file
21
Frontend/src/components/ProtectedRoute.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-black">
|
||||
<div className="text-center">
|
||||
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-white"></div>
|
||||
<p className="mt-4 text-white">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return isAuthenticated ? children : <Navigate to="/login" replace />;
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
@@ -75,21 +75,6 @@ const Sidebar = ({
|
||||
</svg>
|
||||
)}
|
||||
</span>
|
||||
<span className="folder-icon">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#75beff"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
</span>
|
||||
<span className="folder-name">{name}</span>
|
||||
</div>
|
||||
{isExpanded && (
|
||||
@@ -184,10 +169,38 @@ const Sidebar = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getFileIcon = (fileName) => {
|
||||
const extension = fileName.split('.').pop().toLowerCase();
|
||||
|
||||
if (fileName.toLowerCase() === 'readme.md') {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#007acc" /* Blue color for the circle */
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" fill="none" stroke="#007acc" />
|
||||
<text
|
||||
x="12"
|
||||
y="15"
|
||||
textAnchor="middle"
|
||||
fontSize="10"
|
||||
fill="#007acc"
|
||||
fontFamily="Arial, sans-serif"
|
||||
fontWeight="bold"
|
||||
>
|
||||
i
|
||||
</text>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
if (['jsx', 'js', 'ts', 'tsx'].includes(extension)) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import React from "react";
|
||||
"use client"
|
||||
|
||||
const StatusBar = ({ togglePanel, panelVisible }) => {
|
||||
return (
|
||||
<div className="status-bar">
|
||||
{/* Left Section of the Status Bar */}
|
||||
<div className="status-bar-left">
|
||||
{/* Branch Indicator */}
|
||||
<div className="status-item">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -16,6 +17,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-label="Branch Icon"
|
||||
>
|
||||
<line x1="6" y1="3" x2="6" y2="15"></line>
|
||||
<circle cx="18" cy="6" r="3"></circle>
|
||||
@@ -25,6 +27,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
||||
<span>main</span>
|
||||
</div>
|
||||
|
||||
{/* Error Indicator */}
|
||||
<div className="status-item">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -36,30 +39,14 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-label="Error Icon"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
<span>0 errors</span>
|
||||
</div>
|
||||
|
||||
<button className="status-item status-button" onClick={togglePanel}>
|
||||
<span>{panelVisible ? "Hide Terminal" : "Show Terminal"}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="status-bar-right">
|
||||
<div className="status-item">
|
||||
<span>Ln 1, Col 1</span>
|
||||
</div>
|
||||
|
||||
<div className="status-item">
|
||||
<span>Spaces: 2</span>
|
||||
</div>
|
||||
|
||||
<div className="status-item">
|
||||
<span>UTF-8</span>
|
||||
</div>
|
||||
|
||||
{/* Warning Indicator */}
|
||||
<div className="status-item">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -71,6 +58,65 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-label="Warning Icon"
|
||||
>
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path>
|
||||
<line x1="12" y1="9" x2="12" y2="13"></line>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||
</svg>
|
||||
<span>0 warnings</span>
|
||||
</div>
|
||||
|
||||
{/* Toggle Terminal Button */}
|
||||
<button
|
||||
className="status-item status-button"
|
||||
onClick={togglePanel}
|
||||
aria-label="Toggle Terminal"
|
||||
>
|
||||
<span>{panelVisible ? "Hide Terminal" : "Show Terminal"}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Right Section of the Status Bar */}
|
||||
<div className="status-bar-right">
|
||||
{/* Line and Column Indicator */}
|
||||
<div className="status-item">
|
||||
<span>Ln 1, Col 1</span>
|
||||
</div>
|
||||
|
||||
{/* Spaces Indicator */}
|
||||
<div className="status-item">
|
||||
<span>Spaces: 2</span>
|
||||
</div>
|
||||
|
||||
{/* Encoding Indicator */}
|
||||
<div className="status-item">
|
||||
<span>UTF-8</span>
|
||||
</div>
|
||||
|
||||
{/* Language Mode */}
|
||||
<div className="status-item">
|
||||
<span>JavaScript</span>
|
||||
</div>
|
||||
|
||||
{/* EOL (End of Line) Indicator */}
|
||||
<div className="status-item">
|
||||
<span>LF</span>
|
||||
</div>
|
||||
|
||||
{/* Connection Status */}
|
||||
<div className="status-item">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-label="Connection Icon"
|
||||
>
|
||||
<path d="M5 12.55a11 11 0 0 1 14.08 0"></path>
|
||||
<path d="M1.42 9a16 16 0 0 1 21.16 0"></path>
|
||||
@@ -80,6 +126,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
||||
<span>Connected</span>
|
||||
</div>
|
||||
|
||||
{/* Bell Icon */}
|
||||
<div className="status-item">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -91,6 +138,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-label="Bell Icon"
|
||||
>
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
|
||||
@@ -98,8 +146,7 @@ const StatusBar = ({ togglePanel, panelVisible }) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatusBar
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusBar;
|
||||
688
Frontend/src/components/TestList.css
Normal file
688
Frontend/src/components/TestList.css
Normal file
@@ -0,0 +1,688 @@
|
||||
/* TestList.css */
|
||||
.test-list-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
}
|
||||
|
||||
.dark .test-list-container {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2d3748 100%);
|
||||
}
|
||||
|
||||
.test-list-header {
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
.dark .test-list-header {
|
||||
background: #1f2937;
|
||||
border-bottom-color: #374151;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.header-content {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.dark .header-title {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dark .header-subtitle {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
background: #f3f4f6;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark .filter-tabs {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.filter-tab {
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #6b7280;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.filter-tab:hover {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.dark .filter-tab {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.dark .filter-tab:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.filter-tab.active {
|
||||
background: white;
|
||||
color: #2563eb;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dark .filter-tab.active {
|
||||
background: #4b5563;
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.test-list-content {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1rem;
|
||||
}
|
||||
|
||||
.tests-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.tests-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.tests-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.test-card {
|
||||
background: white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.test-card:hover {
|
||||
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
|
||||
border-color: #93c5fd;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.dark .test-card {
|
||||
background: #1f2937;
|
||||
border-color: #374151;
|
||||
}
|
||||
|
||||
.dark .test-card:hover {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.test-card-stripe {
|
||||
height: 0.5rem;
|
||||
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%);
|
||||
}
|
||||
|
||||
.test-card-content {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.test-card-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.test-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.test-card:hover .test-title {
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.dark .test-title {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark .test-card:hover .test-title {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.status-active .status-dot {
|
||||
background: #10b981;
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.status-active {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.dark .status-active {
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
.status-upcoming {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.status-upcoming .status-dot {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.dark .status-upcoming {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
.status-closed {
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.status-closed .status-dot {
|
||||
background: #9ca3af;
|
||||
}
|
||||
|
||||
.dark .status-closed {
|
||||
background: #374151;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.test-description {
|
||||
color: #6b7280;
|
||||
margin-bottom: 1.5rem;
|
||||
min-height: 3rem;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .test-description {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.test-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.test-detail {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.dark .test-detail {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.test-detail-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon-blue {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.icon-purple {
|
||||
color: #8b5cf6;
|
||||
}
|
||||
|
||||
.icon-amber {
|
||||
color: #f59e0b;
|
||||
}
|
||||
|
||||
.test-button {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.test-button-active {
|
||||
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 6px rgba(59, 130, 246, 0.3);
|
||||
}
|
||||
|
||||
.test-button-active:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 8px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.test-button-disabled {
|
||||
background: #e5e7eb;
|
||||
color: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dark .test-button-disabled {
|
||||
background: #374151;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.loading-container,
|
||||
.error-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||
}
|
||||
|
||||
.dark .loading-container,
|
||||
.dark .error-container {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2d3748 100%);
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: 0.25rem solid #e5e7eb;
|
||||
border-top-color: #3b82f6;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
color: #6b7280;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.dark .loading-text {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.error-box {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.5rem;
|
||||
max-width: 28rem;
|
||||
}
|
||||
|
||||
.dark .error-box {
|
||||
background: rgba(220, 38, 38, 0.1);
|
||||
border-color: #991b1b;
|
||||
}
|
||||
|
||||
.error-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
color: #dc2626;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.error-title {
|
||||
font-weight: 600;
|
||||
color: #991b1b;
|
||||
margin: 0 0 0.25rem 0;
|
||||
}
|
||||
|
||||
.dark .error-title {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #dc2626;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dark .error-message {
|
||||
color: #fca5a5;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 1rem;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
border-radius: 50%;
|
||||
background: #e5e7eb;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.dark .empty-icon {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.empty-icon svg {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.empty-title {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #111827;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
.dark .empty-title {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dark .empty-message {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1rem;
|
||||
z-index: 1000;
|
||||
animation: fadeIn 0.3s;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
box-shadow: 0 20px 25px rgba(0, 0, 0, 0.3);
|
||||
width: 100%;
|
||||
max-width: 28rem;
|
||||
animation: slideUp 0.3s;
|
||||
}
|
||||
|
||||
.dark .modal-content {
|
||||
background: #1f2937;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.dark .modal-header {
|
||||
border-bottom-color: #374151;
|
||||
}
|
||||
|
||||
.modal-header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.modal-icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border-radius: 50%;
|
||||
background: #dbeafe;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dark .modal-icon {
|
||||
background: rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.modal-icon svg {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
.dark .modal-icon svg {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: #111827;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dark .modal-title {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-subtitle {
|
||||
font-size: 0.875rem;
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dark .modal-subtitle {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-label {
|
||||
display: block;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.dark .modal-label {
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.modal-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.modal-input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.dark .modal-input {
|
||||
background: #374151;
|
||||
border-color: #4b5563;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark .modal-input:focus {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding: 1.5rem;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.dark .modal-footer {
|
||||
border-top-color: #374151;
|
||||
}
|
||||
|
||||
.modal-button {
|
||||
flex: 1;
|
||||
padding: 0.625rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.modal-button-cancel {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.modal-button-cancel:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.dark .modal-button-cancel {
|
||||
background: #374151;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
.dark .modal-button-cancel:hover {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
.modal-button-submit {
|
||||
background: linear-gradient(90deg, #3b82f6 0%, #8b5cf6 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.modal-button-submit:hover:not(:disabled) {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.modal-button-submit:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.dark .modal-button-submit:disabled {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
transform: translateY(1.25rem);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
354
Frontend/src/components/TestList.jsx
Normal file
354
Frontend/src/components/TestList.jsx
Normal file
@@ -0,0 +1,354 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import './TestList.css';
|
||||
|
||||
const TestList = () => {
|
||||
const [tests, setTests] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
const [showPasswordModal, setShowPasswordModal] = useState(false);
|
||||
const [selectedTest, setSelectedTest] = useState(null);
|
||||
const [password, setPassword] = useState('');
|
||||
const [filterStatus, setFilterStatus] = useState('all');
|
||||
const navigate = useNavigate();
|
||||
const { token } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
fetchTests();
|
||||
}, []);
|
||||
|
||||
const fetchTests = async () => {
|
||||
try {
|
||||
console.log('Fetching tests with token:', token?.substring(0, 50) + '...');
|
||||
const API_URL = import.meta.env.VITE_FACULTY_API_URL || 'http://localhost:5000/api';
|
||||
const response = await fetch(`${API_URL}/students/tests`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('monaco_user');
|
||||
localStorage.removeItem('monaco_token');
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
console.log('Tests received:', data.tests);
|
||||
data.tests.forEach(test => {
|
||||
console.log(`Test: ${test.title}, Status: ${test.status}, Start: ${test.start_time}, End: ${test.end_time}`);
|
||||
});
|
||||
setTests(data.tests);
|
||||
} else {
|
||||
setError(data.message || 'Failed to fetch tests');
|
||||
}
|
||||
} catch (error) {
|
||||
setError('Failed to fetch tests');
|
||||
console.error('Error:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleStartTest = async (test) => {
|
||||
try {
|
||||
const API_URL = import.meta.env.VITE_FACULTY_API_URL || 'http://localhost:5000/api';
|
||||
const response = await fetch(`${API_URL}/students/tests/${test.id}/questions`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
localStorage.setItem('currentTest', JSON.stringify({
|
||||
id: test.id,
|
||||
title: test.title,
|
||||
description: test.description,
|
||||
duration_minutes: test.duration_minutes,
|
||||
start_time: test.start_time,
|
||||
end_time: test.end_time,
|
||||
questions: data.questions,
|
||||
currentQuestionIndex: 0
|
||||
}));
|
||||
navigate('/editor');
|
||||
} else {
|
||||
setError(data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
setError('Failed to start test');
|
||||
console.error('Error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePasswordSubmit = async () => {
|
||||
if (!selectedTest || !password) return;
|
||||
|
||||
try {
|
||||
const API_URL = import.meta.env.VITE_FACULTY_API_URL || 'http://localhost:5000/api';
|
||||
const response = await fetch(`${API_URL}/students/tests/${selectedTest.id}/verify-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
setShowPasswordModal(false);
|
||||
setPassword('');
|
||||
handleStartTest(selectedTest);
|
||||
} else {
|
||||
setError('Invalid password');
|
||||
}
|
||||
} catch (error) {
|
||||
setError('Failed to verify password');
|
||||
console.error('Error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestClick = (test) => {
|
||||
if (test.password_required) {
|
||||
setSelectedTest(test);
|
||||
setShowPasswordModal(true);
|
||||
} else {
|
||||
handleStartTest(test);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredTests = tests.filter(test => {
|
||||
if (filterStatus === 'all') return true;
|
||||
return test.status.toLowerCase() === filterStatus.toLowerCase();
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="loading-container">
|
||||
<div className="loading-content">
|
||||
<div className="spinner"></div>
|
||||
<p className="loading-text">Loading tests...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="error-container">
|
||||
<div className="error-box">
|
||||
<div className="error-content">
|
||||
<svg className="error-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h3 className="error-title">Error</h3>
|
||||
<p className="error-message">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="test-list-container">
|
||||
{/* Header Section */}
|
||||
<div className="test-list-header">
|
||||
<div className="header-content">
|
||||
<div>
|
||||
<h1 className="header-title">
|
||||
📝 Available Tests
|
||||
</h1>
|
||||
<p className="header-subtitle">
|
||||
Select a test to start your coding challenge
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Filter Tabs */}
|
||||
<div className="filter-tabs">
|
||||
<button
|
||||
onClick={() => setFilterStatus('all')}
|
||||
className={`filter-tab ${filterStatus === 'all' ? 'active' : ''}`}
|
||||
>
|
||||
All Tests
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilterStatus('active')}
|
||||
className={`filter-tab ${filterStatus === 'active' ? 'active' : ''}`}
|
||||
>
|
||||
Active
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilterStatus('upcoming')}
|
||||
className={`filter-tab ${filterStatus === 'upcoming' ? 'active' : ''}`}
|
||||
>
|
||||
Upcoming
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tests Grid */}
|
||||
<div className="test-list-content">
|
||||
{filteredTests.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<div className="empty-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 className="empty-title">No tests available</h3>
|
||||
<p className="empty-message">Check back later for new tests</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="tests-grid">
|
||||
{filteredTests.map(test => (
|
||||
<div key={test.id} className="test-card">
|
||||
{/* Status Badge */}
|
||||
<div className="test-card-stripe"></div>
|
||||
|
||||
<div className="test-card-content">
|
||||
{/* Header */}
|
||||
<div className="test-card-header">
|
||||
<h2 className="test-title">
|
||||
{test.title}
|
||||
</h2>
|
||||
<span className={`status-badge status-${test.status?.toLowerCase() || 'closed'}`}>
|
||||
<span className="status-dot"></span>
|
||||
{test.status}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<p className="test-description">
|
||||
{test.description || 'No description available'}
|
||||
</p>
|
||||
|
||||
{/* Test Details */}
|
||||
<div className="test-details">
|
||||
<div className="test-detail">
|
||||
<svg className="test-detail-icon icon-blue" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span><strong>{test.duration_minutes}</strong> minutes</span>
|
||||
</div>
|
||||
|
||||
{test.total_questions && (
|
||||
<div className="test-detail">
|
||||
<svg className="test-detail-icon icon-purple" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
<span><strong>{test.total_questions}</strong> questions</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{test.password_required && (
|
||||
<div className="test-detail icon-amber">
|
||||
<svg className="test-detail-icon icon-amber" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<span><strong>Password required</strong></span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Action Button */}
|
||||
<button
|
||||
onClick={() => handleTestClick(test)}
|
||||
disabled={test.status !== 'Active'}
|
||||
className={`test-button ${
|
||||
test.status === 'Active' ? 'test-button-active' : 'test-button-disabled'
|
||||
}`}
|
||||
>
|
||||
{test.status === 'Active' ? (
|
||||
<>
|
||||
<span>Start Test</span>
|
||||
<svg style={{width: '1.25rem', height: '1.25rem'}} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M13 7l5 5m0 0l-5 5m5-5H6" />
|
||||
</svg>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg style={{width: '1.25rem', height: '1.25rem'}} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
<span>Not Available</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Password Modal */}
|
||||
{showPasswordModal && (
|
||||
<div className="modal-overlay">
|
||||
<div className="modal-content">
|
||||
{/* Modal Header */}
|
||||
<div className="modal-header">
|
||||
<div className="modal-header-content">
|
||||
<div className="modal-icon">
|
||||
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="modal-title">Protected Test</h2>
|
||||
<p className="modal-subtitle">Enter password to continue</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className="modal-body">
|
||||
<label className="modal-label">
|
||||
Test Password
|
||||
</label>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handlePasswordSubmit()}
|
||||
className="modal-input"
|
||||
placeholder="Enter password"
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="modal-footer">
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowPasswordModal(false);
|
||||
setPassword('');
|
||||
setError(null);
|
||||
}}
|
||||
className="modal-button modal-button-cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handlePasswordSubmit}
|
||||
disabled={!password}
|
||||
className="modal-button modal-button-submit"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TestList;
|
||||
61
Frontend/src/components/ThemeToggle.jsx
Normal file
61
Frontend/src/components/ThemeToggle.jsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const ThemeToggle = () => {
|
||||
const [theme, setTheme] = useState('dark');
|
||||
|
||||
useEffect(() => {
|
||||
// Check if there's a saved theme preference
|
||||
const savedTheme = localStorage.getItem('monaco_theme');
|
||||
if (savedTheme) {
|
||||
setTheme(savedTheme);
|
||||
} else {
|
||||
// Check system preference
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
setTheme(prefersDark ? 'dark' : 'light');
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Update the HTML class and CSS variables
|
||||
const root = document.documentElement;
|
||||
root.className = theme;
|
||||
|
||||
// Save theme preference
|
||||
localStorage.setItem('monaco_theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="relative p-2 rounded-md border border-gray-600 bg-gray-800 hover:bg-gray-700 transition-colors"
|
||||
aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} theme`}
|
||||
>
|
||||
{/* Sun Icon for Light Mode */}
|
||||
<svg
|
||||
className={`h-5 w-5 transition-all ${theme === 'dark' ? 'rotate-90 scale-0' : 'rotate-0 scale-100'}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<circle cx="12" cy="12" r="5" />
|
||||
<path d="M12 1v2m0 18v2M4.22 4.22l1.42 1.42m12.72 12.72l1.42 1.42M1 12h2m18 0h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" />
|
||||
</svg>
|
||||
|
||||
{/* Moon Icon for Dark Mode */}
|
||||
<svg
|
||||
className={`absolute top-2 left-2 h-5 w-5 transition-all ${theme === 'dark' ? 'rotate-0 scale-100' : 'rotate-90 scale-0'}`}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
||||
</svg>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeToggle;
|
||||
@@ -1,239 +0,0 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
|
||||
const WebSocketTerminal = ({ code, language, onClose }) => {
|
||||
const [connected, setConnected] = useState(false);
|
||||
const [output, setOutput] = useState([]);
|
||||
const [input, setInput] = useState('');
|
||||
const [submissionId, setSubmissionId] = useState(null);
|
||||
const wsRef = useRef(null);
|
||||
const outputRef = useRef(null);
|
||||
|
||||
// Auto-scroll to bottom of output
|
||||
useEffect(() => {
|
||||
if (outputRef.current) {
|
||||
outputRef.current.scrollTop = outputRef.current.scrollHeight;
|
||||
}
|
||||
}, [output]);
|
||||
|
||||
// Connect to WebSocket on component mount
|
||||
useEffect(() => {
|
||||
// Use API URL from environment variable
|
||||
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
|
||||
const wsUrl = apiUrl.replace('http://', 'ws://').replace('https://', 'wss://');
|
||||
|
||||
// Create WebSocket connection
|
||||
wsRef.current = new WebSocket(`${wsUrl}/ws`);
|
||||
|
||||
// Connection opened
|
||||
wsRef.current.addEventListener('open', () => {
|
||||
setConnected(true);
|
||||
setOutput(prev => [...prev, { type: 'system', content: 'Connected to server' }]);
|
||||
|
||||
// Send the code submission
|
||||
const submission = {
|
||||
language,
|
||||
code
|
||||
};
|
||||
wsRef.current.send(JSON.stringify(submission));
|
||||
});
|
||||
|
||||
// Listen for messages
|
||||
wsRef.current.addEventListener('message', (event) => {
|
||||
const message = event.data;
|
||||
|
||||
// Check if this is a submission ID message
|
||||
if (message.startsWith('Submission ID: ')) {
|
||||
const id = message.substring('Submission ID: '.length);
|
||||
setSubmissionId(id);
|
||||
setOutput(prev => [...prev, { type: 'system', content: `Execution started with ID: ${id}` }]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Regular output
|
||||
setOutput(prev => [...prev, { type: 'output', content: message }]);
|
||||
});
|
||||
|
||||
// Connection closed
|
||||
wsRef.current.addEventListener('close', () => {
|
||||
setConnected(false);
|
||||
setOutput(prev => [...prev, { type: 'system', content: 'Disconnected from server' }]);
|
||||
});
|
||||
|
||||
// Connection error
|
||||
wsRef.current.addEventListener('error', (error) => {
|
||||
console.error('WebSocket error:', error);
|
||||
setOutput(prev => [...prev, { type: 'error', content: 'Connection error' }]);
|
||||
});
|
||||
|
||||
// Clean up on unmount
|
||||
return () => {
|
||||
if (wsRef.current) {
|
||||
wsRef.current.close();
|
||||
}
|
||||
};
|
||||
}, [code, language]);
|
||||
|
||||
// Handle input submission
|
||||
const handleInputSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (!input.trim() || !connected) return;
|
||||
|
||||
// Send input to server
|
||||
wsRef.current.send(input);
|
||||
|
||||
// Add input to output display
|
||||
setOutput(prev => [...prev, { type: 'input', content: input }]);
|
||||
|
||||
// Clear input field
|
||||
setInput('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="websocket-terminal">
|
||||
<div className="terminal-header">
|
||||
<div className="terminal-title">
|
||||
{connected ? 'Connected' : 'Disconnected'}
|
||||
{submissionId && ` - Execution ID: ${submissionId}`}
|
||||
</div>
|
||||
<button className="terminal-close" onClick={onClose}>×</button>
|
||||
</div>
|
||||
|
||||
<div className="terminal-output" ref={outputRef}>
|
||||
{output.map((line, index) => (
|
||||
<div key={index} className={`terminal-line ${line.type}`}>
|
||||
{line.type === 'input' && <span className="input-prefix">> </span>}
|
||||
{line.content}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form className="terminal-input-form" onSubmit={handleInputSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
placeholder="Enter input..."
|
||||
disabled={!connected}
|
||||
className="terminal-input-field"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!connected}
|
||||
className="terminal-input-submit"
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<style jsx>{`
|
||||
.websocket-terminal {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
font-family: 'Consolas', monospace;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background-color: #252526;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.terminal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #d4d4d4;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.terminal-output {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
overflow-y: auto;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.terminal-line {
|
||||
margin-bottom: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.terminal-line.system {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.terminal-line.error {
|
||||
color: #f44747;
|
||||
}
|
||||
|
||||
.terminal-line.input {
|
||||
color: #ce9178;
|
||||
}
|
||||
|
||||
.input-prefix {
|
||||
color: #569cd6;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.terminal-input-form {
|
||||
display: flex;
|
||||
padding: 8px;
|
||||
background-color: #252526;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
|
||||
.terminal-input-field {
|
||||
flex: 1;
|
||||
background-color: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
border: 1px solid #3c3c3c;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
font-family: 'Consolas', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.terminal-input-field:focus {
|
||||
outline: none;
|
||||
border-color: #007acc;
|
||||
}
|
||||
|
||||
.terminal-input-submit {
|
||||
margin-left: 8px;
|
||||
background-color: #0e639c;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.terminal-input-submit:hover {
|
||||
background-color: #1177bb;
|
||||
}
|
||||
|
||||
.terminal-input-submit:disabled {
|
||||
background-color: #3c3c3c;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WebSocketTerminal;
|
||||
121
Frontend/src/contexts/AuthContext.jsx
Normal file
121
Frontend/src/contexts/AuthContext.jsx
Normal file
@@ -0,0 +1,121 @@
|
||||
import { createContext, useContext, useState, useEffect } from 'react';
|
||||
|
||||
const AuthContext = createContext();
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [token, setToken] = useState(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Check for existing session on app load
|
||||
useEffect(() => {
|
||||
const checkAuthStatus = () => {
|
||||
try {
|
||||
const savedUser = localStorage.getItem('monaco_user');
|
||||
const savedToken = localStorage.getItem('monaco_token');
|
||||
if (savedUser && savedToken) {
|
||||
setUser(JSON.parse(savedUser));
|
||||
setToken(savedToken);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking auth status:', error);
|
||||
localStorage.removeItem('monaco_user');
|
||||
localStorage.removeItem('monaco_token');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAuthStatus();
|
||||
}, []);
|
||||
|
||||
const login = async (email, googleToken, userInfo = null) => {
|
||||
// For Google OAuth login
|
||||
if (googleToken && userInfo) {
|
||||
// Exchange Google token for our JWT
|
||||
const API_URL = import.meta.env.VITE_FACULTY_API_URL || 'http://localhost:5000/api';
|
||||
const response = await fetch(`${API_URL}/students/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${googleToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: email,
|
||||
name: userInfo.name || email.split('@')[0]
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server error: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success) {
|
||||
throw new Error(data.message || 'Login failed');
|
||||
}
|
||||
|
||||
const userData = {
|
||||
id: userInfo.sub || Date.now(),
|
||||
email: email,
|
||||
name: userInfo.name || email.split('@')[0],
|
||||
picture: userInfo.picture || null,
|
||||
loginTime: new Date().toISOString(),
|
||||
token: data.token // Store the JWT instead of Google token
|
||||
};
|
||||
|
||||
setUser(userData);
|
||||
localStorage.setItem('monaco_user', JSON.stringify(userData));
|
||||
localStorage.setItem('monaco_token', userData.token);
|
||||
setToken(userData.token);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Fallback for demo purposes (though we're moving to Google-only)
|
||||
if (email && email.includes('@')) {
|
||||
const userData = {
|
||||
id: Date.now(),
|
||||
email: email,
|
||||
name: email.split('@')[0],
|
||||
loginTime: new Date().toISOString()
|
||||
};
|
||||
|
||||
setUser(userData);
|
||||
localStorage.setItem('monaco_user', JSON.stringify(userData));
|
||||
localStorage.setItem('monaco_token', 'demo_token');
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
localStorage.removeItem('monaco_user');
|
||||
localStorage.removeItem('monaco_token');
|
||||
};
|
||||
|
||||
const value = {
|
||||
user,
|
||||
token,
|
||||
login,
|
||||
logout,
|
||||
isAuthenticated: !!user,
|
||||
isLoading
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
51
Frontend/src/utils/studentApi.js
Normal file
51
Frontend/src/utils/studentApi.js
Normal file
@@ -0,0 +1,51 @@
|
||||
const API_URL = import.meta.env.VITE_FACULTY_API_URL || 'http://localhost:5000/api';
|
||||
|
||||
export const studentApi = {
|
||||
async getTests() {
|
||||
const response = await fetch(`${API_URL}/students/tests`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('monaco_token')}`
|
||||
}
|
||||
});
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
async getTestQuestions(testId) {
|
||||
const response = await fetch(`${API_URL}/students/tests/${testId}/questions`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('monaco_token')}`
|
||||
}
|
||||
});
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
async verifyTestPassword(testId, password) {
|
||||
const response = await fetch(`${API_URL}/students/tests/${testId}/verify-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('monaco_token')}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
return await response.json();
|
||||
},
|
||||
|
||||
async submitAnswer(testId, questionId, code) {
|
||||
const response = await fetch(`${API_URL}/students/submissions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('monaco_token')}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
testId,
|
||||
answers: [{
|
||||
questionId,
|
||||
submittedAnswer: code
|
||||
}]
|
||||
})
|
||||
});
|
||||
return await response.json();
|
||||
}
|
||||
};
|
||||
51
Frontend/start-tunnel.sh
Normal file
51
Frontend/start-tunnel.sh
Normal file
@@ -0,0 +1,51 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Colors for output
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
echo -e "${GREEN}================================${NC}"
|
||||
echo -e "${GREEN}Monaco Frontend Tunnel Setup${NC}"
|
||||
echo -e "${GREEN}================================${NC}"
|
||||
echo ""
|
||||
|
||||
# Check if credentials.json exists
|
||||
if [ ! -f "credentials.json" ]; then
|
||||
echo -e "${RED}Error: credentials.json not found!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if config.json exists
|
||||
if [ ! -f "config.json" ]; then
|
||||
echo -e "${RED}Error: config.json not found!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${YELLOW}Building Docker image...${NC}"
|
||||
docker-compose -f docker-compose.tunnel.yml build
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${GREEN}Build successful!${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Starting services...${NC}"
|
||||
docker-compose -f docker-compose.tunnel.yml up -d
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
echo -e "${GREEN}Services started successfully!${NC}"
|
||||
echo ""
|
||||
echo -e "${GREEN}Frontend is now accessible at:${NC}"
|
||||
echo -e " ${YELLOW}Local:${NC} http://localhost:8001"
|
||||
echo -e " ${YELLOW}Tunnel:${NC} https://monaco.ishikabhoyar.tech"
|
||||
echo ""
|
||||
echo -e "${YELLOW}To view logs:${NC} docker-compose -f docker-compose.tunnel.yml logs -f"
|
||||
echo -e "${YELLOW}To stop:${NC} docker-compose -f docker-compose.tunnel.yml down"
|
||||
else
|
||||
echo -e "${RED}Failed to start services!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}Build failed!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
317
Readme.md
317
Readme.md
@@ -1,125 +1,264 @@
|
||||
# Monaco Online Code Compiler
|
||||
# Monaco Code Execution Engine
|
||||
|
||||
A full-featured online code compiler with a VS Code-like interface. This project allows users to write, edit, and execute code in multiple programming languages directly in the browser.
|
||||
Test 1
|
||||
Monaco is a secure, containerized code execution engine that allows you to run code in multiple programming languages through a simple REST API and WebSocket connections for real-time terminal interaction.
|
||||
|
||||
## Features
|
||||
|
||||
- **VS Code-like Interface**: Familiar editor experience with syntax highlighting, tabs, and file explorer
|
||||
- **Multi-language Support**: Run code in Python, JavaScript, Go, Java, C, and C++
|
||||
- **Input/Output Handling**: Enter input for your programs and see the output in real-time
|
||||
- **Secure Execution**: Code runs in isolated Docker containers on the backend
|
||||
- **File Management**: Create, edit, and organize files and folders
|
||||
- **Multi-language support**: Run code in Python, Java, C, and C++
|
||||
- **Secure execution**: All code runs in isolated Docker containers
|
||||
- **Resource limits**: Memory, CPU, and file descriptor limits to prevent abuse
|
||||
- **Concurrent processing**: Efficient job queue for handling multiple requests
|
||||
- **Simple REST API**: Easy to integrate with any frontend
|
||||
- **Interactive terminal**: Real-time code execution with input/output via WebSockets
|
||||
- **VS Code-like interface**: Modern editor with syntax highlighting and file management
|
||||
|
||||
## Project Structure
|
||||
## Architecture
|
||||
|
||||
- **Frontend**: React-based UI with Monaco Editor
|
||||
- **Backend**: Go-based code execution service with Docker integration
|
||||
- HTTP Handlers (internal/api/handlers): Processes API requests
|
||||
- Execution Service (internal/executor): Manages code execution in containers
|
||||
- Job Queue (internal/queue): Handles concurrent execution of code submissions
|
||||
- Submission Model (internal/models): Defines the data structure for code submissions
|
||||
Monaco consists of several components:
|
||||
|
||||
## Getting Started
|
||||
### Backend Components
|
||||
|
||||
### Prerequisites
|
||||
- **HTTP Handlers** (`handler/handler.go`): Processes API requests and WebSocket connections
|
||||
- **Execution Service** (`service/execution.go`): Manages code execution in containers
|
||||
- **Job Queue** (`queue/queue.go`): Handles concurrent execution of code submissions
|
||||
- **Submission Model** (`model/submission.go`): Defines the data structure for code submissions
|
||||
|
||||
- Node.js 18+ for the frontend
|
||||
- Go 1.22+ for the backend
|
||||
- Docker for code execution
|
||||
### Frontend Components
|
||||
|
||||
### Running the Frontend
|
||||
- **Editor Area** (`EditorArea.jsx`): Main code editor with Monaco editor integration
|
||||
- **Terminal Panel** (`Panel.jsx`): Interactive terminal for code execution and input
|
||||
- **Sidebar** (`Sidebar.jsx`): File explorer and project structure navigation
|
||||
- **Status Bar** (`StatusBar.jsx`): Information display and quick actions
|
||||
|
||||
### Communication Flow
|
||||
|
||||
1. Frontend submits code to backend via REST API
|
||||
2. Backend assigns a unique ID and queues the execution
|
||||
3. Frontend connects to WebSocket endpoint with the execution ID
|
||||
4. Backend sends real-time execution output through WebSocket
|
||||
5. Frontend can send user input back through WebSocket
|
||||
6. Results are stored and retrievable via REST endpoints
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Backend**:
|
||||
- Go 1.22.3 or higher
|
||||
- Docker
|
||||
- Network connectivity for container image pulling
|
||||
- **Frontend**:
|
||||
- Node.js and npm/yarn
|
||||
- Modern web browser
|
||||
|
||||
## Installation
|
||||
|
||||
### Backend Setup
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
cd Frontend
|
||||
npm install
|
||||
npm run dev
|
||||
git clone https://github.com/arnab-afk/monaco.git
|
||||
cd monaco/backend
|
||||
```
|
||||
|
||||
The frontend will be available at http://localhost:5173
|
||||
|
||||
### Running the Backend
|
||||
2.Install Go dependencies:
|
||||
|
||||
```bash
|
||||
go mod download
|
||||
```
|
||||
|
||||
3.Build the application:
|
||||
|
||||
```bash
|
||||
go build -o monaco
|
||||
```
|
||||
|
||||
4.Run the service
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
go build -o monaco ./cmd/server
|
||||
./monaco
|
||||
```
|
||||
|
||||
The backend API will be available at http://localhost:8080
|
||||
The backend service will start on port 8080 by default.
|
||||
|
||||
## Using the Online Compiler
|
||||
### Frontend Setup
|
||||
|
||||
1. **Create a File**: Click the "+" button in the editor tabs or use the file explorer
|
||||
2. **Write Code**: Use the Monaco editor to write your code
|
||||
3. **Run Code**: Click the "Play" button in the top right corner
|
||||
4. **Enter Input**: If your program requires input, enter it in the terminal panel
|
||||
5. **View Output**: See the execution results in the terminal panel
|
||||
1. Navigate to the Frontend directory:
|
||||
|
||||
## Supported Languages
|
||||
```bash
|
||||
cd Frontend
|
||||
```
|
||||
|
||||
- **Python** (.py)
|
||||
- **JavaScript** (.js)
|
||||
- **Go** (.go)
|
||||
- **Java** (.java)
|
||||
- **C** (.c)
|
||||
- **C++** (.cpp)
|
||||
2. Install dependencies:
|
||||
|
||||
## Examples
|
||||
```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
|
||||
|
||||
```python
|
||||
name = input("Enter your name: ")
|
||||
print(f"Hello, {name}!")
|
||||
for i in range(5):
|
||||
print(f"Count: {i}")
|
||||
```
|
||||
- **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
|
||||
|
||||
### JavaScript
|
||||
### Java
|
||||
|
||||
```javascript
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
- **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
|
||||
|
||||
rl.question('Enter your name: ', (name) => {
|
||||
console.log(`Hello, ${name}!`);
|
||||
for (let i = 0; i < 5; i++) {
|
||||
console.log(`Count: ${i}`);
|
||||
}
|
||||
rl.close();
|
||||
});
|
||||
```
|
||||
### C++
|
||||
|
||||
### Go
|
||||
- **Version**: Latest G++
|
||||
- **Standard**: C++17
|
||||
- **Resource Limits**: 100MB memory, 10% CPU
|
||||
|
||||
```go
|
||||
package main
|
||||
### Security Considerations
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
All code execution happens within isolated Docker containers with:
|
||||
|
||||
func main() {
|
||||
fmt.Print("Enter your name: ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
name, _ := reader.ReadString('\n')
|
||||
name = strings.TrimSpace(name)
|
||||
fmt.Printf("Hello, %s!\n", name)
|
||||
for i := 0; i < 5; i++ {
|
||||
fmt.Printf("Count: %d\n", i)
|
||||
}
|
||||
}
|
||||
```
|
||||
- No network access (`--network=none`)
|
||||
- Limited CPU and memory resources
|
||||
- Limited file system access
|
||||
- No persistent storage
|
||||
- Execution time limits (10-15 seconds)
|
||||
|
||||
## Security Considerations
|
||||
### Debugging
|
||||
|
||||
- All code is executed in isolated Docker containers
|
||||
- Network access is disabled
|
||||
- Memory and CPU limits are enforced
|
||||
- Execution timeouts prevent infinite loops
|
||||
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.
|
||||
Contributions are welcome! Please feel free to submit a Pull Request :).
|
||||
|
||||
147
UI_MODERNIZATION_SUMMARY.md
Normal file
147
UI_MODERNIZATION_SUMMARY.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# UI Modernization Summary
|
||||
|
||||
## Overview
|
||||
Successfully modernized the entire UI to match a clean, professional exam interface design while preserving all existing functionalities.
|
||||
|
||||
## Key Changes
|
||||
|
||||
### 🎨 Code Challenge Page (Editor Interface)
|
||||
|
||||
#### 1. **Modern Header**
|
||||
- Added left section with emoji icon (📝) and exam title
|
||||
- Created sophisticated timer display with:
|
||||
- Time Remaining label
|
||||
- Separate blocks for Hours : Minutes : Seconds
|
||||
- Color-coded with soft red background (#fee2e2)
|
||||
- Labels below timer blocks
|
||||
- Added user profile avatar with gradient background
|
||||
- Clean white background with subtle shadow
|
||||
|
||||
#### 2. **Question Palette (Left Sidebar)**
|
||||
- Transformed from vertical tabs to a modern grid layout
|
||||
- Added "Question Palette" header
|
||||
- 4-column grid of question buttons (20 questions)
|
||||
- Current question highlighted in blue (#2563eb)
|
||||
- Added legend at bottom:
|
||||
- Current (blue dot)
|
||||
- Answered (green dot)
|
||||
- Not Visited (gray dot)
|
||||
- Clean spacing and rounded corners
|
||||
|
||||
#### 3. **Problem Content Area**
|
||||
- Added question header showing "Question X of Y | 10 Points"
|
||||
- Modern test case cards with:
|
||||
- Gray header labels (Example 1, Example 2)
|
||||
- Bordered code blocks for input/output
|
||||
- Clean, readable typography
|
||||
- Added action buttons at bottom:
|
||||
- "Clear Response" with reset icon
|
||||
- "Mark for Review" with checkmark icon
|
||||
- Better spacing and visual hierarchy
|
||||
|
||||
#### 4. **Editor Section**
|
||||
- Modernized language selector dropdown
|
||||
- Updated Run and Submit buttons:
|
||||
- Run: Green (#10b981) with play icon
|
||||
- Submit: Blue (#2563eb) with send icon
|
||||
- Smooth hover effects with shadows
|
||||
- Better padding and spacing
|
||||
|
||||
#### 5. **Terminal/Console Section**
|
||||
- Added tab navigation (Console / Testcases)
|
||||
- Active tab highlighted in blue
|
||||
- Empty state placeholder: "Console output will appear here..."
|
||||
- Modern terminal prompt with white background box
|
||||
- Improved readability with better colors
|
||||
|
||||
#### 6. **Footer Action Bar**
|
||||
- New fixed footer with:
|
||||
- "Run Code" button (outline style)
|
||||
- "Save & Next" button (primary blue)
|
||||
- "Submit Test" button (success green)
|
||||
- "All changes saved" indicator with checkmark
|
||||
- Professional shadow and spacing
|
||||
|
||||
### 🎯 Test List Page
|
||||
Already had modern styling with:
|
||||
- Gradient backgrounds
|
||||
- Card-based layout
|
||||
- Status badges
|
||||
- Smooth animations
|
||||
- Clean modals
|
||||
|
||||
## Color Scheme
|
||||
|
||||
### Primary Colors
|
||||
- **Blue**: #2563eb (Primary actions, active states)
|
||||
- **Green**: #10b981 (Success, Run button)
|
||||
- **Red**: #dc2626 (Timer, errors)
|
||||
|
||||
### Neutral Colors
|
||||
- **Background**: #ffffff (White)
|
||||
- **Secondary BG**: #f9fafb (Light gray)
|
||||
- **Borders**: #e5e7eb (Light gray borders)
|
||||
- **Text Primary**: #111827 (Dark gray)
|
||||
- **Text Secondary**: #6b7280 (Medium gray)
|
||||
- **Text Muted**: #9ca3af (Light gray)
|
||||
|
||||
### Accent Colors
|
||||
- Timer blocks: #fee2e2 (Light red)
|
||||
- User avatar: Linear gradient (#667eea to #764ba2)
|
||||
- Status badges: Various semantic colors
|
||||
|
||||
## Typography
|
||||
- **Font Family**: System fonts (-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto)
|
||||
- **Code Font**: 'Consolas', 'Monaco', monospace
|
||||
- **Font Sizes**:
|
||||
- Headers: 18-20px
|
||||
- Body: 14px
|
||||
- Small text: 12-13px
|
||||
- **Font Weights**: 400 (normal), 500 (medium), 600 (semi-bold), 700 (bold)
|
||||
|
||||
## Spacing & Layout
|
||||
- Consistent padding: 12px, 16px, 24px
|
||||
- Gap spacing: 6px, 8px, 12px
|
||||
- Border radius: 6px, 8px
|
||||
- Box shadows for depth
|
||||
|
||||
## Interactive Elements
|
||||
- Smooth transitions (0.2s ease)
|
||||
- Hover states with background changes
|
||||
- Active states clearly distinguished
|
||||
- Disabled states with reduced opacity
|
||||
- Button hover effects with subtle shadows
|
||||
|
||||
## Responsive Behavior
|
||||
- Flexbox layouts for adaptability
|
||||
- Grid systems for question palette
|
||||
- Scrollable content areas
|
||||
- Fixed header and footer
|
||||
|
||||
## Accessibility Features
|
||||
- Proper color contrast
|
||||
- Clear visual feedback
|
||||
- Semantic HTML structure
|
||||
- Keyboard navigable elements
|
||||
- Focus states preserved
|
||||
|
||||
## All Functionalities Preserved
|
||||
✅ Timer countdown
|
||||
✅ Question navigation
|
||||
✅ Code execution
|
||||
✅ WebSocket terminal communication
|
||||
✅ Code submission
|
||||
✅ Language selection
|
||||
✅ Test case display
|
||||
✅ User authentication flow
|
||||
✅ Test list filtering
|
||||
✅ Password-protected tests
|
||||
✅ Auto-save submissions
|
||||
|
||||
## Files Modified
|
||||
1. `/Frontend/src/index.css` - Complete UI styling overhaul
|
||||
2. `/Frontend/src/components/CodeChallenge.jsx` - Updated JSX structure for new components
|
||||
3. All existing functionality remains intact
|
||||
|
||||
## Result
|
||||
A clean, modern, and professional examination interface that matches industry standards while maintaining all existing features and functionality.
|
||||
@@ -1,500 +0,0 @@
|
||||
# Monaco Backend - Code Execution Service
|
||||
|
||||
## Table of Contents
|
||||
|
||||
1. Introduction
|
||||
2. Architecture
|
||||
3. Installation
|
||||
4. API Reference
|
||||
5. Code Execution
|
||||
6. Job Queue System
|
||||
7. Language Support
|
||||
8. Security Considerations
|
||||
9. Configuration
|
||||
10. Testing
|
||||
11. Performance Tuning
|
||||
12. Troubleshooting
|
||||
|
||||
## Introduction
|
||||
|
||||
Monaco is a secure, containerized code execution backend service designed to run user-submitted code in multiple programming languages. It features a job queue system to manage execution resources, containerized execution environments for security, and a RESTful API for submission and monitoring.
|
||||
|
||||
**Key Features:**
|
||||
- Multi-language support (Python, JavaScript, Go, Java, C, C++)
|
||||
- Secure containerized execution using Docker
|
||||
- Resource limiting to prevent abuse
|
||||
- Job queuing for managing concurrent executions
|
||||
- Detailed execution statistics and monitoring
|
||||
- Support for user input via stdin
|
||||
- CORS support for browser-based clients
|
||||
|
||||
## Architecture
|
||||
|
||||
### Component Overview
|
||||
|
||||
Monaco follows a layered architecture with the following key components:
|
||||
|
||||
1. **HTTP Handlers** (internal/api/handlers) - Processes incoming HTTP requests
|
||||
2. **Execution Service** (internal/executor) - Manages code execution in containers
|
||||
3. **Job Queue** (internal/queue) - Controls concurrent execution
|
||||
4. **Data Models** (internal/models) - Defines data structures
|
||||
|
||||
### Request Flow
|
||||
|
||||
1. Client submits code via `/submit` endpoint
|
||||
2. Request is validated and assigned a unique ID
|
||||
3. Submission is added to the job queue
|
||||
4. Worker picks job from queue when available
|
||||
5. Code is executed in appropriate Docker container
|
||||
6. Results are stored and available via `/result` endpoint
|
||||
|
||||
### Dependency Diagram
|
||||
|
||||
```
|
||||
Client Request → HTTP Handlers → Execution Service → Job Queue → Docker Containers
|
||||
↑
|
||||
Data Models
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.22+
|
||||
- Docker Engine
|
||||
- Docker images for supported languages:
|
||||
- `python:3.9`
|
||||
- `node:18-alpine`
|
||||
- `golang:1.22-alpine`
|
||||
- `eclipse-temurin:11-jdk-alpine`
|
||||
- `gcc:latest`
|
||||
|
||||
### Setup Instructions
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/arnab-afk/monaco.git
|
||||
cd monaco/backend
|
||||
```
|
||||
|
||||
2. Install Go dependencies:
|
||||
```bash
|
||||
go mod download
|
||||
```
|
||||
|
||||
3. Build the application:
|
||||
```bash
|
||||
go build -o monaco ./cmd/server
|
||||
```
|
||||
|
||||
4. Run the service:
|
||||
```bash
|
||||
./monaco
|
||||
```
|
||||
|
||||
The service will start on port 8080 by default.
|
||||
|
||||
## API Reference
|
||||
|
||||
### Endpoints
|
||||
|
||||
#### `POST /submit`
|
||||
|
||||
Submits code for execution.
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"language": "python", // Required: "python", "javascript", "go", "java", "c", or "cpp"
|
||||
"code": "print('Hello, World!')", // Required: source code to execute
|
||||
"input": "optional input string" // Optional: input to stdin
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1" // Unique ID for this submission
|
||||
}
|
||||
```
|
||||
|
||||
**Status Codes:**
|
||||
- 202 Accepted - Code accepted for execution
|
||||
- 400 Bad Request - Invalid request (e.g., unsupported language)
|
||||
|
||||
#### `GET /status?id={submissionId}`
|
||||
|
||||
Checks the status of a submission.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1",
|
||||
"status": "completed", // "pending", "queued", "running", "completed", "failed"
|
||||
"queuedAt": "2025-03-25T14:30:00Z",
|
||||
"startedAt": "2025-03-25T14:30:01Z", // Only present if status is "running", "completed", or "failed"
|
||||
"completedAt": "2025-03-25T14:30:02Z", // Only present if status is "completed" or "failed"
|
||||
"executionTime": 1000 // Execution time in milliseconds (only if completed)
|
||||
}
|
||||
```
|
||||
|
||||
**Status Codes:**
|
||||
- 200 OK - Status retrieved successfully
|
||||
- 400 Bad Request - Missing ID parameter
|
||||
- 404 Not Found - Submission with given ID not found
|
||||
|
||||
#### `GET /result?id={submissionId}`
|
||||
|
||||
Gets the execution result of a submission.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1",
|
||||
"status": "completed",
|
||||
"language": "python",
|
||||
"output": "Hello, World!",
|
||||
"queuedAt": "2025-03-25T14:30:00Z",
|
||||
"startedAt": "2025-03-25T14:30:01Z",
|
||||
"completedAt": "2025-03-25T14:30:02Z",
|
||||
"executionTime": 1000,
|
||||
"executionTimeFormatted": "1.0s",
|
||||
"totalTime": 2000,
|
||||
"totalTimeFormatted": "2.0s"
|
||||
}
|
||||
```
|
||||
|
||||
**Status Codes:**
|
||||
- 200 OK - Result retrieved successfully
|
||||
- 400 Bad Request - Missing ID parameter
|
||||
- 404 Not Found - Submission with given ID not found
|
||||
|
||||
#### `GET /queue-stats`
|
||||
|
||||
Gets statistics about the job queue.
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"queue_stats": {
|
||||
"queue_length": 5,
|
||||
"max_workers": 3,
|
||||
"running_jobs": 3
|
||||
},
|
||||
"submissions": 42
|
||||
}
|
||||
```
|
||||
|
||||
## Code Execution
|
||||
|
||||
### Execution Process
|
||||
|
||||
1. **Language Detection**: The system identifies the programming language of the submission.
|
||||
2. **Environment Setup**: A temporary directory is created for compiled languages.
|
||||
3. **Container Setup**: Docker containers are configured with resource limits.
|
||||
4. **Compilation**: For compiled languages (Java, C, C++), code is compiled first.
|
||||
5. **Execution**: The program is executed with the provided input.
|
||||
6. **Resource Monitoring**: Memory and CPU usage are limited during execution.
|
||||
7. **Result Collection**: Output and errors are captured and stored.
|
||||
|
||||
### Language-Specific Processing
|
||||
|
||||
#### Python
|
||||
- Directly executes Python code using the `-c` flag
|
||||
- Uses `python:3.9` Docker image
|
||||
- Resource limits: 100MB memory, 10% CPU
|
||||
|
||||
#### Java
|
||||
- Detects class name using regex pattern matching
|
||||
- Compiles with `javac` and runs with optimized JVM settings
|
||||
- Uses `eclipse-temurin:11-jdk-alpine` Docker image
|
||||
- Resource limits: 400MB memory, 50% CPU
|
||||
- JVM flags: `-XX:+TieredCompilation`, `-XX:TieredStopAtLevel=1`, `-Xverify:none`
|
||||
|
||||
#### C/C++
|
||||
- Saves code to a file in a temporary directory
|
||||
- Compiles with `gcc`/`g++` and runs the executable
|
||||
- Uses `gcc:latest` Docker image
|
||||
- Resource limits: 100MB memory, 10% CPU
|
||||
|
||||
### Timeout Handling
|
||||
|
||||
All executions have a timeout limit:
|
||||
- Python: 10 seconds
|
||||
- Java: 15 seconds
|
||||
- C/C++: 10 seconds
|
||||
|
||||
If execution exceeds this limit, the process is killed and an error is returned.
|
||||
|
||||
## Job Queue System
|
||||
|
||||
### Worker Pool
|
||||
|
||||
Monaco uses a worker pool to manage concurrent code executions:
|
||||
|
||||
- Default pool size: 20 workers (configurable)
|
||||
- Maximum queue capacity: 100 jobs
|
||||
- FIFO (First-In-First-Out) processing order
|
||||
|
||||
### Job Lifecycle
|
||||
|
||||
1. **Creation**: Job created when code is submitted
|
||||
2. **Queuing**: Job added to queue with `queued` status
|
||||
3. **Execution**: Worker picks job from queue and changes status to `running`
|
||||
4. **Completion**: Job finishes with either `completed` or `failed` status
|
||||
|
||||
### Performance Metrics
|
||||
|
||||
The queue tracks and reports:
|
||||
- Current queue length
|
||||
- Number of running jobs
|
||||
- Maximum worker count
|
||||
- Total number of submissions
|
||||
|
||||
## Language Support
|
||||
|
||||
### Python
|
||||
- **Version**: Python 3.9
|
||||
- **Input Handling**: Direct stdin piping
|
||||
- **Limitations**: No file I/O, no package imports outside standard library
|
||||
|
||||
### JavaScript
|
||||
- **Version**: Node.js 18 (Alpine)
|
||||
- **Input Handling**: File-based input redirection
|
||||
- **Limitations**: No file I/O, no package imports outside standard library
|
||||
|
||||
### Go
|
||||
- **Version**: Go 1.22 (Alpine)
|
||||
- **Compilation**: Standard Go build process
|
||||
- **Input Handling**: Direct stdin piping
|
||||
- **Limitations**: No file I/O, no external dependencies
|
||||
|
||||
### Java
|
||||
- **Version**: Java 11 (Eclipse Temurin)
|
||||
- **Class Detection**: Extracts class name from code using regex
|
||||
- **Memory Settings**: 64MB min heap, 256MB max heap
|
||||
- **Best Practices**: Use `public class` with the main method
|
||||
|
||||
### C
|
||||
- **Version**: Latest GCC
|
||||
- **Compilation Flags**: Default GCC settings
|
||||
- **Execution**: Compiled binary
|
||||
|
||||
### C++
|
||||
- **Version**: Latest G++
|
||||
- **Standard**: C++17
|
||||
- **Execution**: Compiled binary
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Containerization
|
||||
|
||||
All code execution happens within isolated Docker containers with:
|
||||
- No network access (`--network=none`)
|
||||
- Limited CPU and memory resources
|
||||
- Limited file system access
|
||||
- No persistent storage
|
||||
|
||||
### Resource Limiting
|
||||
|
||||
- **Memory Limits**: 100-400MB depending on language
|
||||
- **CPU Limits**: 10-50% of CPU depending on language
|
||||
- **File Descriptors**: Limited to 64 for Python
|
||||
- **Execution Time**: Enforced timeouts (10-15 seconds)
|
||||
|
||||
### Known Limitations
|
||||
|
||||
- Container escape vulnerabilities
|
||||
- Docker daemon security depends on host configuration
|
||||
- Resource limits can be circumvented with certain techniques
|
||||
|
||||
## Configuration
|
||||
|
||||
The service can be configured through environment variables:
|
||||
|
||||
- `PORT`: HTTP port (default: 8080)
|
||||
- `MAX_WORKERS`: Maximum concurrent executions (default: 3)
|
||||
- `QUEUE_SIZE`: Maximum queue size (default: 100)
|
||||
- `DEFAULT_LANGUAGE`: Default language if none specified (default: "python")
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Run unit tests with:
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
# Monaco Backend Test Plan
|
||||
|
||||
## Overview
|
||||
This test plan outlines the testing approach for the Monaco code execution backend service.
|
||||
|
||||
## Test Environment
|
||||
- Development: Local workstations with Docker and Go
|
||||
- Testing: Dedicated test server with Docker
|
||||
- Production-like: Staging environment with similar resources to production
|
||||
|
||||
## Test Types
|
||||
|
||||
### Unit Tests
|
||||
- **Purpose**: Verify individual components work as expected
|
||||
- **Components to Test**:
|
||||
- Handler package
|
||||
- Queue package
|
||||
- Execution service
|
||||
- Models
|
||||
- **Tools**: Go testing framework
|
||||
|
||||
### Integration Tests
|
||||
- **Purpose**: Verify components work together correctly
|
||||
- **Focus Areas**:
|
||||
- API endpoints
|
||||
- End-to-end code execution flow
|
||||
- Error handling
|
||||
- **Tools**: Go testing framework, HTTP test utilities
|
||||
|
||||
### Load Tests
|
||||
- **Purpose**: Verify system performance under load
|
||||
- **Scenarios**:
|
||||
- Concurrent submissions
|
||||
- Mixed language workloads
|
||||
- Queue saturation
|
||||
- **Metrics**:
|
||||
- Request throughput
|
||||
- Response times
|
||||
- Success rates
|
||||
- Resource utilization
|
||||
- **Tools**: Custom Python test scripts
|
||||
|
||||
## Test Data
|
||||
- Simple programs in each language
|
||||
- Programs with input requirements
|
||||
- Programs with compile errors
|
||||
- Programs with runtime errors
|
||||
- Programs with timeouts
|
||||
|
||||
## Test Execution
|
||||
1. Run unit tests on every code change
|
||||
2. Run integration tests before merging to main branch
|
||||
3. Run load tests weekly and before major releases
|
||||
|
||||
## Success Criteria
|
||||
- All unit tests pass
|
||||
- Integration tests complete successfully
|
||||
- Load tests show acceptable performance metrics:
|
||||
- 95% of requests complete successfully
|
||||
- 95th percentile response time < 5 seconds
|
||||
- System can handle 20 concurrent users
|
||||
|
||||
## Reporting
|
||||
- Test results stored in CI/CD pipeline
|
||||
- Performance metrics graphed over time
|
||||
- Issues logged in GitHub issues
|
||||
|
||||
### Load Testing
|
||||
|
||||
A Python script (`test.py`) is included for load testing:
|
||||
```bash
|
||||
python test.py
|
||||
```
|
||||
|
||||
This script sends 500 requests concurrently and reports performance metrics.
|
||||
|
||||
### Manual Testing with Curl
|
||||
|
||||
#### Python Example
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/submit \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"language": "python",
|
||||
"code": "print(\"Hello, World!\")\nfor i in range(5):\n print(f\"Number: {i}\")",
|
||||
"input": ""
|
||||
}'
|
||||
```
|
||||
|
||||
#### Java Example
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/submit \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"language": "java",
|
||||
"code": "public class Solution {\n public static void main(String[] args) {\n System.out.println(\"Hello, World!\");\n for (int i = 0; i < 5; i++) {\n System.out.println(\"Number: \" + i);\n }\n }\n}",
|
||||
"input": ""
|
||||
}'
|
||||
```
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
### Optimizing Worker Count
|
||||
|
||||
The optimal worker count depends on:
|
||||
- CPU cores available
|
||||
- Memory available
|
||||
- Docker container startup time
|
||||
|
||||
For most single-server deployments, 3-5 workers is optimal.
|
||||
|
||||
### Memory Considerations
|
||||
|
||||
Each language has different memory requirements:
|
||||
- Python: ~50-100MB per instance
|
||||
- Java: ~200-400MB per instance
|
||||
- C/C++: ~50-100MB per instance
|
||||
|
||||
Calculate total memory needs as: `(Python instances × 100MB) + (Java instances × 400MB) + (C/C++ instances × 100MB)`
|
||||
|
||||
### Disk Space Management
|
||||
|
||||
Temporary files are cleaned up after execution, but with high request volumes, ensure adequate disk space for concurrent operations (approximately 1-5MB per request for compiled languages).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### Docker Connection Errors
|
||||
```
|
||||
Error: Cannot connect to the Docker daemon
|
||||
```
|
||||
**Solution**: Ensure Docker daemon is running with `systemctl start docker` or `docker --version`
|
||||
|
||||
#### Permissions Issues
|
||||
```
|
||||
Error: Permission denied while trying to connect to the Docker daemon socket
|
||||
```
|
||||
**Solution**: Add user to docker group: `sudo usermod -aG docker $USER`
|
||||
|
||||
#### Container Resource Limits
|
||||
```
|
||||
Error: Container killed due to memory limit
|
||||
```
|
||||
**Solution**: Increase memory limits in execution service or optimize submitted code
|
||||
|
||||
#### File Not Found Errors
|
||||
```
|
||||
Error: Failed to write Java file
|
||||
```
|
||||
**Solution**: Check temporary directory permissions and disk space
|
||||
|
||||
### Logs
|
||||
|
||||
The service provides structured logs with prefixes for easier filtering:
|
||||
- `[HTTP]` - API requests
|
||||
- `[QUEUE]` - Queue operations
|
||||
- `[WORKER-n]` - Worker activities
|
||||
- `[EXEC-id]` - Execution details
|
||||
- `[PYTHON/JAVA/C/CPP-id]` - Language-specific logs
|
||||
- `[TIMEOUT-id]` - Timeout events
|
||||
- `[RESULT-id]` - Execution results
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
@@ -1,38 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/api"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Configure logging
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
log.Println("Starting Monaco code execution backend...")
|
||||
|
||||
// Initialize router with all routes
|
||||
router := api.SetupRoutes()
|
||||
|
||||
// Start the server
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: router,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
}
|
||||
|
||||
log.Printf("Server started at :%s", port)
|
||||
log.Fatal(server.ListenAndServe())
|
||||
}
|
||||
@@ -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,155 +0,0 @@
|
||||
# Monaco Code Execution Examples
|
||||
|
||||
This document provides examples of code submissions for each supported language.
|
||||
|
||||
## Python
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "python",
|
||||
"code": "name = input('Enter your name: ')\nprint(f'Hello, {name}!')\nfor i in range(5):\n print(f'Count: {i}')",
|
||||
"input": "World"
|
||||
}
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Enter your name: Hello, World!
|
||||
Count: 0
|
||||
Count: 1
|
||||
Count: 2
|
||||
Count: 3
|
||||
Count: 4
|
||||
```
|
||||
|
||||
## JavaScript
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "javascript",
|
||||
"code": "const readline = require('readline');\nconst rl = readline.createInterface({\n input: process.stdin,\n output: process.stdout\n});\n\nrl.question('Enter your name: ', (name) => {\n console.log(`Hello, ${name}!`);\n for (let i = 0; i < 5; i++) {\n console.log(`Count: ${i}`);\n }\n rl.close();\n});",
|
||||
"input": "World"
|
||||
}
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Enter your name: Hello, World!
|
||||
Count: 0
|
||||
Count: 1
|
||||
Count: 2
|
||||
Count: 3
|
||||
Count: 4
|
||||
```
|
||||
|
||||
## Go
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "go",
|
||||
"code": "package main\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\nfunc main() {\n\tfmt.Print(\"Enter your name: \")\n\treader := bufio.NewReader(os.Stdin)\n\tname, _ := reader.ReadString('\\n')\n\tname = strings.TrimSpace(name)\n\tfmt.Printf(\"Hello, %s!\\n\", name)\n\tfor i := 0; i < 5; i++ {\n\t\tfmt.Printf(\"Count: %d\\n\", i)\n\t}\n}",
|
||||
"input": "World"
|
||||
}
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Enter your name: Hello, World!
|
||||
Count: 0
|
||||
Count: 1
|
||||
Count: 2
|
||||
Count: 3
|
||||
Count: 4
|
||||
```
|
||||
|
||||
## Java
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "java",
|
||||
"code": "import java.util.Scanner;\n\npublic class Main {\n public static void main(String[] args) {\n Scanner scanner = new Scanner(System.in);\n System.out.print(\"Enter your name: \");\n String name = scanner.nextLine();\n System.out.println(\"Hello, \" + name + \"!\");\n for (int i = 0; i < 5; i++) {\n System.out.println(\"Count: \" + i);\n }\n scanner.close();\n }\n}",
|
||||
"input": "World"
|
||||
}
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Enter your name: Hello, World!
|
||||
Count: 0
|
||||
Count: 1
|
||||
Count: 2
|
||||
Count: 3
|
||||
Count: 4
|
||||
```
|
||||
|
||||
## C
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "c",
|
||||
"code": "#include <stdio.h>\n\nint main() {\n char name[100];\n printf(\"Enter your name: \");\n scanf(\"%s\", name);\n printf(\"Hello, %s!\\n\", name);\n for (int i = 0; i < 5; i++) {\n printf(\"Count: %d\\n\", i);\n }\n return 0;\n}",
|
||||
"input": "World"
|
||||
}
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Enter your name: Hello, World!
|
||||
Count: 0
|
||||
Count: 1
|
||||
Count: 2
|
||||
Count: 3
|
||||
Count: 4
|
||||
```
|
||||
|
||||
## C++
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "cpp",
|
||||
"code": "#include <iostream>\n#include <string>\n\nint main() {\n std::string name;\n std::cout << \"Enter your name: \";\n std::cin >> name;\n std::cout << \"Hello, \" << name << \"!\" << std::endl;\n for (int i = 0; i < 5; i++) {\n std::cout << \"Count: \" << i << std::endl;\n }\n return 0;\n}",
|
||||
"input": "World"
|
||||
}
|
||||
```
|
||||
|
||||
Expected output:
|
||||
```
|
||||
Enter your name: Hello, World!
|
||||
Count: 0
|
||||
Count: 1
|
||||
Count: 2
|
||||
Count: 3
|
||||
Count: 4
|
||||
```
|
||||
|
||||
## Testing with cURL
|
||||
|
||||
You can test these examples using cURL:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/submit \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"language": "python",
|
||||
"code": "name = input(\"Enter your name: \")\nprint(f\"Hello, {name}!\")\nfor i in range(5):\n print(f\"Count: {i}\")",
|
||||
"input": "World"
|
||||
}'
|
||||
```
|
||||
|
||||
This will return a submission ID:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "6423259c-ee14-c5aa-1c90-d5e989f92aa1"
|
||||
}
|
||||
```
|
||||
|
||||
You can then check the status and result:
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/status?id=6423259c-ee14-c5aa-1c90-d5e989f92aa1
|
||||
```
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/result?id=6423259c-ee14-c5aa-1c90-d5e989f92aa1
|
||||
```
|
||||
@@ -1,15 +0,0 @@
|
||||
module github.com/arnab-afk/monaco
|
||||
|
||||
go 1.22.3
|
||||
|
||||
require github.com/stretchr/testify v1.9.0
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.12.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -1,25 +0,0 @@
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -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,85 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/arnab-afk/monaco/model"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
// Allow all origins for development
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
// WebSocketHandler handles WebSocket connections for code execution
|
||||
func (h *Handler) WebSocketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Upgrade the HTTP connection to a WebSocket connection
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to upgrade connection: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Read the initial message containing the code submission
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("Failed to read message: %v", err)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the message as a code submission
|
||||
var submission model.CodeSubmission
|
||||
if err := json.Unmarshal(message, &submission); err != nil {
|
||||
log.Printf("Failed to parse message: %v", err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Invalid submission format"))
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the submission
|
||||
if submission.Code == "" {
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Code is required"))
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Set default language if not provided
|
||||
if submission.Language == "" {
|
||||
submission.Language = "python" // Default to Python
|
||||
}
|
||||
|
||||
// Validate language
|
||||
supportedLanguages := map[string]bool{
|
||||
"python": true,
|
||||
"java": true,
|
||||
"c": true,
|
||||
"cpp": true,
|
||||
}
|
||||
|
||||
if !supportedLanguages[submission.Language] {
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Unsupported language: "+submission.Language))
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a unique ID for the submission
|
||||
submission.ID = h.generateID()
|
||||
submission.Status = "pending"
|
||||
|
||||
// Store the submission
|
||||
h.mu.Lock()
|
||||
h.submissions[submission.ID] = &submission
|
||||
h.mu.Unlock()
|
||||
|
||||
// Send the submission ID to the client
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Submission ID: "+submission.ID))
|
||||
|
||||
// Execute the code with WebSocket communication
|
||||
h.executionService.HandleWebSocket(conn, &submission)
|
||||
}
|
||||
@@ -1,260 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/executor"
|
||||
"github.com/arnab-afk/monaco/internal/models"
|
||||
)
|
||||
|
||||
// Handler manages HTTP requests for code submissions
|
||||
type Handler struct {
|
||||
executionService *executor.ExecutionService
|
||||
mu sync.Mutex
|
||||
submissions map[string]*models.CodeSubmission
|
||||
}
|
||||
|
||||
// NewHandler creates a new handler instance
|
||||
func NewHandler() *Handler {
|
||||
return &Handler{
|
||||
executionService: executor.NewExecutionService(),
|
||||
submissions: make(map[string]*models.CodeSubmission),
|
||||
}
|
||||
}
|
||||
|
||||
// SubmitHandler handles code submission requests
|
||||
func (h *Handler) SubmitHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow POST method
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the request body
|
||||
var submission models.CodeSubmission
|
||||
if err := json.NewDecoder(r.Body).Decode(&submission); err != nil {
|
||||
http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the submission
|
||||
if submission.Code == "" {
|
||||
http.Error(w, "Code is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if submission.Language == "" {
|
||||
http.Error(w, "Language is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a unique ID for the submission
|
||||
h.mu.Lock()
|
||||
submission.ID = executor.GenerateUUID()
|
||||
submission.Status = "pending"
|
||||
h.submissions[submission.ID] = &submission
|
||||
h.mu.Unlock()
|
||||
|
||||
// Execute the code in a goroutine
|
||||
go h.executionService.ExecuteCode(&submission)
|
||||
|
||||
// Return the submission ID
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
json.NewEncoder(w).Encode(map[string]string{"id": submission.ID})
|
||||
}
|
||||
|
||||
// StatusHandler handles status check requests
|
||||
func (h *Handler) StatusHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow GET method
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the submission ID from the query parameters
|
||||
id := r.URL.Query().Get("id")
|
||||
if id == "" {
|
||||
http.Error(w, "ID is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the submission from the map
|
||||
h.mu.Lock()
|
||||
submission, exists := h.submissions[id]
|
||||
h.mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
http.Error(w, "Submission not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Return the submission status
|
||||
response := map[string]interface{}{
|
||||
"id": submission.ID,
|
||||
"status": submission.Status,
|
||||
}
|
||||
|
||||
// Add time information based on status
|
||||
if !submission.QueuedAt.IsZero() {
|
||||
response["queuedAt"] = submission.QueuedAt.Format(time.RFC3339)
|
||||
}
|
||||
if !submission.StartedAt.IsZero() {
|
||||
response["startedAt"] = submission.StartedAt.Format(time.RFC3339)
|
||||
}
|
||||
if !submission.CompletedAt.IsZero() {
|
||||
response["completedAt"] = submission.CompletedAt.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// ResultHandler handles result requests
|
||||
func (h *Handler) ResultHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow GET method
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the submission ID from the query parameters
|
||||
id := r.URL.Query().Get("id")
|
||||
if id == "" {
|
||||
http.Error(w, "ID is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the submission from the map
|
||||
h.mu.Lock()
|
||||
submission, exists := h.submissions[id]
|
||||
h.mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
http.Error(w, "Submission not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Return the submission result
|
||||
response := map[string]interface{}{
|
||||
"id": submission.ID,
|
||||
"status": submission.Status,
|
||||
"language": submission.Language,
|
||||
"output": submission.Output,
|
||||
"input": submission.Input,
|
||||
}
|
||||
|
||||
// Add error information if available
|
||||
if submission.Error != "" {
|
||||
response["error"] = submission.Error
|
||||
}
|
||||
|
||||
// Add time information
|
||||
if !submission.QueuedAt.IsZero() {
|
||||
response["queuedAt"] = submission.QueuedAt.Format(time.RFC3339)
|
||||
}
|
||||
if !submission.StartedAt.IsZero() {
|
||||
response["startedAt"] = submission.StartedAt.Format(time.RFC3339)
|
||||
}
|
||||
if !submission.CompletedAt.IsZero() {
|
||||
response["completedAt"] = submission.CompletedAt.Format(time.RFC3339)
|
||||
if !submission.StartedAt.IsZero() {
|
||||
response["executionTime"] = submission.CompletedAt.Sub(submission.StartedAt).Milliseconds()
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// QueueStatsHandler provides information about the job queue
|
||||
func (h *Handler) QueueStatsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow GET method
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the queue statistics
|
||||
stats := h.executionService.GetQueueStats()
|
||||
|
||||
// Return the queue statistics
|
||||
response := map[string]interface{}{
|
||||
"queue_stats": stats,
|
||||
"submissions": len(h.submissions),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// SubmitInputHandler handles interactive input submission
|
||||
func (h *Handler) SubmitInputHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow POST method
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the request body
|
||||
var inputRequest struct {
|
||||
ID string `json:"id"`
|
||||
Input string `json:"input"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&inputRequest); err != nil {
|
||||
http.Error(w, "Invalid request body: "+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the request
|
||||
if inputRequest.ID == "" {
|
||||
http.Error(w, "ID is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the submission from the map
|
||||
h.mu.Lock()
|
||||
submission, exists := h.submissions[inputRequest.ID]
|
||||
h.mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
http.Error(w, "Submission not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if the submission is waiting for input or running
|
||||
// We're more lenient here to handle race conditions
|
||||
if submission.Status != "waiting_for_input" && submission.Status != "running" {
|
||||
http.Error(w, fmt.Sprintf("Submission is not waiting for input (status: %s)", submission.Status), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Send the input to the execution service
|
||||
h.executionService.SubmitInput(submission, inputRequest.Input)
|
||||
|
||||
// Return success response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "input_submitted"})
|
||||
}
|
||||
|
||||
// HealthCheckHandler handles health check requests
|
||||
func (h *Handler) HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Only allow GET method
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// Return a simple health check response
|
||||
response := map[string]interface{}{
|
||||
"status": "ok",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestSubmitHandler(t *testing.T) {
|
||||
h := NewHandler()
|
||||
|
||||
// Create a test request
|
||||
reqBody := map[string]string{
|
||||
"language": "python",
|
||||
"code": "print('Hello, World!')",
|
||||
"input": "",
|
||||
}
|
||||
reqJSON, _ := json.Marshal(reqBody)
|
||||
req, err := http.NewRequest("POST", "/submit", bytes.NewBuffer(reqJSON))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Create a response recorder
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// Call the handler
|
||||
h.SubmitHandler(rr, req)
|
||||
|
||||
// Check the status code
|
||||
assert.Equal(t, http.StatusAccepted, rr.Code)
|
||||
|
||||
// Check the response body
|
||||
var response map[string]string
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Contains(t, response, "id")
|
||||
assert.NotEmpty(t, response["id"])
|
||||
}
|
||||
|
||||
func TestHealthCheckHandler(t *testing.T) {
|
||||
h := NewHandler()
|
||||
|
||||
// Create a test request
|
||||
req, err := http.NewRequest("GET", "/health", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a response recorder
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
// Call the handler
|
||||
h.HealthCheckHandler(rr, req)
|
||||
|
||||
// Check the status code
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
|
||||
// Check the response body
|
||||
var response map[string]interface{}
|
||||
err = json.Unmarshal(rr.Body.Bytes(), &response)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ok", response["status"])
|
||||
assert.Contains(t, response, "timestamp")
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LoggingMiddleware logs HTTP requests
|
||||
func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
log.Printf("[HTTP] %s %s %s", r.Method, r.URL.Path, r.RemoteAddr)
|
||||
next.ServeHTTP(w, r)
|
||||
log.Printf("[HTTP] %s %s completed in %v", r.Method, r.URL.Path, time.Since(startTime))
|
||||
})
|
||||
}
|
||||
|
||||
// CORSMiddleware adds CORS headers to responses
|
||||
func CORSMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Set CORS headers
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
|
||||
// Handle preflight requests
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the next handler
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// RecoveryMiddleware recovers from panics
|
||||
func RecoveryMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
log.Printf("[PANIC] %v", err)
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/executor"
|
||||
"github.com/arnab-afk/monaco/internal/models"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// WebSocketTerminal represents a terminal session over WebSocket
|
||||
type WebSocketTerminal struct {
|
||||
ID string
|
||||
Conn *websocket.Conn
|
||||
InputChan chan string
|
||||
OutputChan chan string
|
||||
Done chan struct{}
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
var (
|
||||
// Configure the upgrader
|
||||
upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
// Allow all origins for development
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
// Active terminal sessions
|
||||
terminals = make(map[string]*WebSocketTerminal)
|
||||
terminalsMu sync.Mutex
|
||||
)
|
||||
|
||||
// TerminalHandler handles WebSocket connections for terminal sessions
|
||||
func (h *Handler) TerminalHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Upgrade the HTTP connection to a WebSocket connection
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to upgrade connection: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a unique ID for this terminal session
|
||||
terminalID := executor.GenerateUUID()
|
||||
|
||||
// Create channels for communication
|
||||
inputChan := make(chan string)
|
||||
outputChan := make(chan string)
|
||||
done := make(chan struct{})
|
||||
|
||||
// Create a new terminal session
|
||||
terminal := &WebSocketTerminal{
|
||||
ID: terminalID,
|
||||
Conn: conn,
|
||||
InputChan: inputChan,
|
||||
OutputChan: outputChan,
|
||||
Done: done,
|
||||
}
|
||||
|
||||
// Store the terminal session
|
||||
terminalsMu.Lock()
|
||||
terminals[terminalID] = terminal
|
||||
terminalsMu.Unlock()
|
||||
|
||||
// Send the terminal ID to the client
|
||||
if err := conn.WriteJSON(map[string]string{"type": "terminal_id", "id": terminalID}); err != nil {
|
||||
log.Printf("Failed to send terminal ID: %v", err)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle incoming messages (input from the client)
|
||||
go func() {
|
||||
defer func() {
|
||||
close(done)
|
||||
conn.Close()
|
||||
|
||||
// Remove the terminal from the map
|
||||
terminalsMu.Lock()
|
||||
delete(terminals, terminalID)
|
||||
terminalsMu.Unlock()
|
||||
|
||||
log.Printf("Terminal session %s closed", terminalID)
|
||||
}()
|
||||
|
||||
for {
|
||||
// Read message from the WebSocket
|
||||
messageType, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
log.Printf("WebSocket error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Handle different message types
|
||||
if messageType == websocket.TextMessage {
|
||||
// Parse the message
|
||||
input := string(message)
|
||||
|
||||
// Send the input to the execution service
|
||||
select {
|
||||
case inputChan <- input:
|
||||
// Input sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle outgoing messages (output to the client)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case output := <-outputChan:
|
||||
// Send the output to the client
|
||||
err := conn.WriteMessage(websocket.TextMessage, []byte(output))
|
||||
if err != nil {
|
||||
log.Printf("Failed to write message: %v", err)
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Keep the connection alive with ping/pong
|
||||
go func() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ExecuteCodeWebSocket executes code and streams the output over WebSocket
|
||||
func (h *Handler) ExecuteCodeWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
// Upgrade the HTTP connection to a WebSocket connection
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("Failed to upgrade connection: %v", err)
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
// Read the initial message containing the code to execute
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("Failed to read message: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the message into a code submission
|
||||
var submission models.CodeSubmission
|
||||
if err := submission.UnmarshalJSON(message); err != nil {
|
||||
log.Printf("Failed to parse submission: %v", err)
|
||||
conn.WriteJSON(map[string]string{"error": "Invalid submission format"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate a unique ID for the submission
|
||||
submission.ID = executor.GenerateUUID()
|
||||
submission.Status = "pending"
|
||||
|
||||
// Store the submission
|
||||
h.mu.Lock()
|
||||
h.submissions[submission.ID] = &submission
|
||||
h.mu.Unlock()
|
||||
|
||||
// Create channels for communication
|
||||
inputChan := make(chan string)
|
||||
outputChan := make(chan string)
|
||||
done := make(chan struct{})
|
||||
|
||||
// Set up the execution service to use these channels
|
||||
h.executionService.SetupWebSocketChannels(&submission, inputChan, outputChan)
|
||||
|
||||
// Send the submission ID to the client
|
||||
if err := conn.WriteJSON(map[string]string{"type": "submission_id", "id": submission.ID}); err != nil {
|
||||
log.Printf("Failed to send submission ID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Execute the code in a goroutine
|
||||
go func() {
|
||||
h.executionService.ExecuteCodeWebSocket(&submission)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Handle incoming messages (input from the client)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
// Read message from the WebSocket
|
||||
_, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
log.Printf("WebSocket error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Send the input to the execution service
|
||||
select {
|
||||
case inputChan <- string(message):
|
||||
// Input sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle outgoing messages (output to the client)
|
||||
for {
|
||||
select {
|
||||
case output := <-outputChan:
|
||||
// Send the output to the client
|
||||
err := conn.WriteMessage(websocket.TextMessage, []byte(output))
|
||||
if err != nil {
|
||||
log.Printf("Failed to write message: %v", err)
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
// Execution completed
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetTerminal returns a terminal session by ID
|
||||
func GetTerminal(id string) (*WebSocketTerminal, error) {
|
||||
terminalsMu.Lock()
|
||||
defer terminalsMu.Unlock()
|
||||
|
||||
terminal, exists := terminals[id]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("terminal not found: %s", id)
|
||||
}
|
||||
|
||||
return terminal, nil
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/api/handlers"
|
||||
)
|
||||
|
||||
// SetupRoutes configures all API routes
|
||||
func SetupRoutes() http.Handler {
|
||||
// Create a new handler
|
||||
h := handlers.NewHandler()
|
||||
|
||||
// Create a new router
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Apply middleware to all routes
|
||||
var handler http.Handler = mux
|
||||
handler = handlers.RecoveryMiddleware(handler)
|
||||
handler = handlers.LoggingMiddleware(handler)
|
||||
handler = handlers.CORSMiddleware(handler)
|
||||
|
||||
// Register routes
|
||||
mux.HandleFunc("/submit", h.SubmitHandler)
|
||||
mux.HandleFunc("/status", h.StatusHandler)
|
||||
mux.HandleFunc("/result", h.ResultHandler)
|
||||
mux.HandleFunc("/submit-input", h.SubmitInputHandler)
|
||||
mux.HandleFunc("/queue-stats", h.QueueStatsHandler)
|
||||
mux.HandleFunc("/health", h.HealthCheckHandler)
|
||||
|
||||
return handler
|
||||
}
|
||||
@@ -1,683 +0,0 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/models"
|
||||
"github.com/arnab-afk/monaco/internal/queue"
|
||||
)
|
||||
|
||||
// ExecutionService manages code execution
|
||||
type ExecutionService struct {
|
||||
queue *queue.JobQueue
|
||||
mu sync.Mutex
|
||||
// Map of submission ID to input channel for interactive programs
|
||||
inputChannels map[string]chan string
|
||||
// WebSocket channels for real-time communication
|
||||
wsInputChannels map[string]chan string
|
||||
wsOutputChannels map[string]chan string
|
||||
}
|
||||
|
||||
// CodeExecutionJob represents a code execution job
|
||||
type CodeExecutionJob struct {
|
||||
service *ExecutionService
|
||||
submission *models.CodeSubmission
|
||||
}
|
||||
|
||||
// NewExecutionService creates a new execution service
|
||||
func NewExecutionService() *ExecutionService {
|
||||
return &ExecutionService{
|
||||
queue: queue.NewJobQueue(5), // 5 concurrent workers
|
||||
inputChannels: make(map[string]chan string),
|
||||
wsInputChannels: make(map[string]chan string),
|
||||
wsOutputChannels: make(map[string]chan string),
|
||||
}
|
||||
}
|
||||
|
||||
// NewCodeExecutionJob creates a new code execution job
|
||||
func NewCodeExecutionJob(service *ExecutionService, submission *models.CodeSubmission) *CodeExecutionJob {
|
||||
return &CodeExecutionJob{
|
||||
service: service,
|
||||
submission: submission,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute runs the code execution job
|
||||
func (j *CodeExecutionJob) Execute() {
|
||||
submission := j.submission
|
||||
submission.Status = "running"
|
||||
submission.StartedAt = time.Now()
|
||||
|
||||
log.Printf("[JOB-%s] Starting execution for language: %s", submission.ID, submission.Language)
|
||||
|
||||
j.service.executeLanguageSpecific(submission)
|
||||
|
||||
submission.CompletedAt = time.Now()
|
||||
log.Printf("[JOB-%s] Execution completed in %v", submission.ID, submission.CompletedAt.Sub(submission.StartedAt))
|
||||
}
|
||||
|
||||
// ExecuteCode adds the submission to the execution queue
|
||||
func (s *ExecutionService) ExecuteCode(submission *models.CodeSubmission) {
|
||||
submission.Status = "queued"
|
||||
submission.QueuedAt = time.Now()
|
||||
|
||||
log.Printf("[SUBMISSION-%s] Code submission queued for language: %s", submission.ID, submission.Language)
|
||||
|
||||
// Create and add the job to the queue
|
||||
job := NewCodeExecutionJob(s, submission)
|
||||
s.queue.AddJob(job)
|
||||
}
|
||||
|
||||
// executeLanguageSpecific executes code based on the language
|
||||
func (s *ExecutionService) executeLanguageSpecific(submission *models.CodeSubmission) {
|
||||
switch strings.ToLower(submission.Language) {
|
||||
case "python":
|
||||
s.executePython(submission)
|
||||
case "javascript", "js":
|
||||
s.executeJavaScript(submission)
|
||||
case "go", "golang":
|
||||
s.executeGo(submission)
|
||||
case "java":
|
||||
s.executeJava(submission)
|
||||
case "c":
|
||||
s.executeC(submission)
|
||||
case "cpp", "c++":
|
||||
s.executeCpp(submission)
|
||||
default:
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Unsupported language: %s", submission.Language)
|
||||
log.Printf("[EXEC-%s] ERROR: Unsupported language: %s", submission.ID, submission.Language)
|
||||
}
|
||||
}
|
||||
|
||||
// executePython runs Python code in a container
|
||||
func (s *ExecutionService) executePython(submission *models.CodeSubmission) {
|
||||
log.Printf("[PYTHON-%s] Preparing Python execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Create a temporary file for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-python-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "code.py")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we should use interactive mode
|
||||
if strings.Contains(submission.Code, "input(") {
|
||||
// This code likely requires interactive input
|
||||
submission.IsInteractive = true
|
||||
s.executePythonInteractive(submission, tempDir)
|
||||
return
|
||||
}
|
||||
|
||||
// Non-interactive mode
|
||||
// Create a file for input if provided
|
||||
inputPath := ""
|
||||
if submission.Input != "" {
|
||||
inputPath = filepath.Join(tempDir, "input.txt")
|
||||
if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write input file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if inputPath != "" {
|
||||
cmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"--ulimit", "nofile=64:64", // File descriptor limits
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"python:3.9",
|
||||
"sh", "-c", "cat /code/input.txt | python /code/code.py")
|
||||
} else {
|
||||
cmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"--ulimit", "nofile=64:64", // File descriptor limits
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"python:3.9",
|
||||
"python", "/code/code.py")
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[PYTHON-%s] Python execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err, ctx.Err() != nil)
|
||||
}
|
||||
|
||||
// executeJavaScript runs JavaScript code in a container
|
||||
func (s *ExecutionService) executeJavaScript(submission *models.CodeSubmission) {
|
||||
log.Printf("[JS-%s] Preparing JavaScript execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Create a temporary file for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-js-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "code.js")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if we should use interactive mode
|
||||
if strings.Contains(submission.Code, "readline") && strings.Contains(submission.Code, "question") {
|
||||
// This code likely requires interactive input
|
||||
submission.IsInteractive = true
|
||||
s.executeJavaScriptInteractive(submission, tempDir)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a file for input if provided
|
||||
inputPath := ""
|
||||
if submission.Input != "" {
|
||||
inputPath = filepath.Join(tempDir, "input.txt")
|
||||
if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write input file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var cmd *exec.Cmd
|
||||
if inputPath != "" {
|
||||
// Create a wrapper script to handle input
|
||||
wrapperPath := filepath.Join(tempDir, "wrapper.js")
|
||||
wrapperCode := `
|
||||
const fs = require('fs');
|
||||
const input = fs.readFileSync('/code/input.txt', 'utf8');
|
||||
// Redirect input to stdin
|
||||
process.stdin.push(input);
|
||||
process.stdin.push(null);
|
||||
// Load and run the user code
|
||||
require('./code.js');
|
||||
`
|
||||
if err := os.WriteFile(wrapperPath, []byte(wrapperCode), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write wrapper file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
cmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"node:18-alpine",
|
||||
"node", "/code/wrapper.js")
|
||||
} else {
|
||||
cmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"node:18-alpine",
|
||||
"node", "/code/code.js")
|
||||
}
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[JS-%s] JavaScript execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err, ctx.Err() != nil)
|
||||
}
|
||||
|
||||
// executeGo runs Go code in a container
|
||||
func (s *ExecutionService) executeGo(submission *models.CodeSubmission) {
|
||||
log.Printf("[GO-%s] Preparing Go execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Create a temporary file for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-go-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "main.go")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a file for input if provided
|
||||
inputPath := ""
|
||||
if submission.Input != "" {
|
||||
inputPath = filepath.Join(tempDir, "input.txt")
|
||||
if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write input file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// First compile the Go code
|
||||
compileCmd := exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"golang:1.22-alpine",
|
||||
"go", "build", "-o", "/code/app", "/code/main.go")
|
||||
|
||||
compileOutput, compileErr := compileCmd.CombinedOutput()
|
||||
if compileErr != nil {
|
||||
log.Printf("[GO-%s] Compilation failed: %v", submission.ID, compileErr)
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Compilation error: %s", compileOutput)
|
||||
return
|
||||
}
|
||||
|
||||
// Then run the compiled binary
|
||||
var runCmd *exec.Cmd
|
||||
if inputPath != "" {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"golang:1.22-alpine",
|
||||
"sh", "-c", "cat /code/input.txt | /code/app")
|
||||
} else {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"golang:1.22-alpine",
|
||||
"/code/app")
|
||||
}
|
||||
|
||||
output, err := runCmd.CombinedOutput()
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[GO-%s] Go execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err, ctx.Err() != nil)
|
||||
}
|
||||
|
||||
// executeJava runs Java code in a container
|
||||
func (s *ExecutionService) executeJava(submission *models.CodeSubmission) {
|
||||
log.Printf("[JAVA-%s] Preparing Java execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Create a temporary file for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-java-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Extract class name from the code
|
||||
className := extractJavaClassName(submission.Code)
|
||||
if className == "" {
|
||||
className = "Main" // Default class name
|
||||
}
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, className+".java")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a file for input if provided
|
||||
inputPath := ""
|
||||
if submission.Input != "" {
|
||||
inputPath = filepath.Join(tempDir, "input.txt")
|
||||
if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write input file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// First compile the Java code
|
||||
compileCmd := exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"eclipse-temurin:11-jdk-alpine",
|
||||
"javac", "/code/"+className+".java")
|
||||
|
||||
compileOutput, compileErr := compileCmd.CombinedOutput()
|
||||
if compileErr != nil {
|
||||
log.Printf("[JAVA-%s] Compilation failed: %v", submission.ID, compileErr)
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Compilation error: %s", compileOutput)
|
||||
return
|
||||
}
|
||||
|
||||
// Then run the compiled class
|
||||
var runCmd *exec.Cmd
|
||||
if inputPath != "" {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=400m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=50000", // 50% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"eclipse-temurin:11-jdk-alpine",
|
||||
"sh", "-c", "cd /code && cat input.txt | java -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -Xverify:none -Xms64m -Xmx256m "+className)
|
||||
} else {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=400m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=50000", // 50% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"eclipse-temurin:11-jdk-alpine",
|
||||
"java", "-XX:+TieredCompilation", "-XX:TieredStopAtLevel=1", "-Xverify:none", "-Xms64m", "-Xmx256m", "-cp", "/code", className)
|
||||
}
|
||||
|
||||
output, err := runCmd.CombinedOutput()
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[JAVA-%s] Java execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err, ctx.Err() != nil)
|
||||
}
|
||||
|
||||
// executeC runs C code in a container
|
||||
func (s *ExecutionService) executeC(submission *models.CodeSubmission) {
|
||||
log.Printf("[C-%s] Preparing C execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Create a temporary file for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-c-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "code.c")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a file for input if provided
|
||||
inputPath := ""
|
||||
if submission.Input != "" {
|
||||
inputPath = filepath.Join(tempDir, "input.txt")
|
||||
if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write input file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// First compile the C code
|
||||
compileCmd := exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest",
|
||||
"gcc", "-o", "/code/app", "/code/code.c")
|
||||
|
||||
compileOutput, compileErr := compileCmd.CombinedOutput()
|
||||
if compileErr != nil {
|
||||
log.Printf("[C-%s] Compilation failed: %v", submission.ID, compileErr)
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Compilation error: %s", compileOutput)
|
||||
return
|
||||
}
|
||||
|
||||
// Then run the compiled binary
|
||||
var runCmd *exec.Cmd
|
||||
if inputPath != "" {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest",
|
||||
"sh", "-c", "cat /code/input.txt | /code/app")
|
||||
} else {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest",
|
||||
"/code/app")
|
||||
}
|
||||
|
||||
output, err := runCmd.CombinedOutput()
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[C-%s] C execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err, ctx.Err() != nil)
|
||||
}
|
||||
|
||||
// executeCpp runs C++ code in a container
|
||||
func (s *ExecutionService) executeCpp(submission *models.CodeSubmission) {
|
||||
log.Printf("[CPP-%s] Preparing C++ execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Create a temporary file for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-cpp-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "code.cpp")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a file for input if provided
|
||||
inputPath := ""
|
||||
if submission.Input != "" {
|
||||
inputPath = filepath.Join(tempDir, "input.txt")
|
||||
if err := os.WriteFile(inputPath, []byte(submission.Input), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write input file: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// First compile the C++ code
|
||||
compileCmd := exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest",
|
||||
"g++", "-o", "/code/app", "/code/code.cpp")
|
||||
|
||||
compileOutput, compileErr := compileCmd.CombinedOutput()
|
||||
if compileErr != nil {
|
||||
log.Printf("[CPP-%s] Compilation failed: %v", submission.ID, compileErr)
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Compilation error: %s", compileOutput)
|
||||
return
|
||||
}
|
||||
|
||||
// Then run the compiled binary
|
||||
var runCmd *exec.Cmd
|
||||
if inputPath != "" {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest",
|
||||
"sh", "-c", "cat /code/input.txt | /code/app")
|
||||
} else {
|
||||
runCmd = exec.CommandContext(ctx, "docker", "run", "--rm",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest",
|
||||
"/code/app")
|
||||
}
|
||||
|
||||
output, err := runCmd.CombinedOutput()
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[CPP-%s] C++ execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err, ctx.Err() != nil)
|
||||
}
|
||||
|
||||
// updateSubmissionResult updates the submission with the execution result
|
||||
func (s *ExecutionService) updateSubmissionResult(submission *models.CodeSubmission, output []byte, err error, timedOut bool) {
|
||||
// Format the output to include the input if provided
|
||||
formattedOutput := ""
|
||||
if submission.Input != "" {
|
||||
// Only add input lines that were actually used
|
||||
inputLines := strings.Split(submission.Input, "\n")
|
||||
for _, line := range inputLines {
|
||||
if line != "" {
|
||||
// Don't add the input marker for empty lines
|
||||
formattedOutput += "[Input] " + line + "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the actual output
|
||||
rawOutput := string(output)
|
||||
|
||||
if timedOut {
|
||||
submission.Status = "failed"
|
||||
submission.Error = "Execution timed out"
|
||||
submission.Output = formattedOutput + rawOutput
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = err.Error()
|
||||
submission.Output = formattedOutput + rawOutput
|
||||
return
|
||||
}
|
||||
|
||||
submission.Status = "completed"
|
||||
submission.Output = formattedOutput + rawOutput
|
||||
}
|
||||
|
||||
// SubmitInput submits input to a running interactive program
|
||||
func (s *ExecutionService) SubmitInput(submission *models.CodeSubmission, input string) {
|
||||
s.mu.Lock()
|
||||
inputChan, exists := s.inputChannels[submission.ID]
|
||||
s.mu.Unlock()
|
||||
|
||||
if !exists {
|
||||
log.Printf("[ERROR] No input channel found for submission %s", submission.ID)
|
||||
return
|
||||
}
|
||||
|
||||
// Send the input to the channel
|
||||
inputChan <- input
|
||||
|
||||
// Update the submission status
|
||||
submission.Status = "running"
|
||||
submission.Output += "[Input] " + input + "\n"
|
||||
}
|
||||
|
||||
// GetQueueStats returns statistics about the job queue
|
||||
func (s *ExecutionService) GetQueueStats() models.QueueStats {
|
||||
return s.queue.GetStats()
|
||||
}
|
||||
|
||||
// GenerateUUID generates a unique ID for submissions
|
||||
func GenerateUUID() string {
|
||||
b := make([]byte, 16)
|
||||
_, err := rand.Read(b)
|
||||
if err != nil {
|
||||
return fmt.Sprintf("%d", time.Now().UnixNano())
|
||||
}
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
|
||||
// extractJavaClassName extracts the class name from Java code
|
||||
func extractJavaClassName(code string) string {
|
||||
// Simple regex-like extraction
|
||||
lines := strings.Split(code, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "public class ") {
|
||||
parts := strings.Split(line, " ")
|
||||
if len(parts) > 2 {
|
||||
className := parts[2]
|
||||
// Remove any { or implements/extends
|
||||
className = strings.Split(className, "{")[0]
|
||||
className = strings.Split(className, " ")[0]
|
||||
return strings.TrimSpace(className)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,366 +0,0 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/models"
|
||||
)
|
||||
|
||||
// executePythonInteractive runs Python code in interactive mode
|
||||
func (s *ExecutionService) executePythonInteractive(submission *models.CodeSubmission, tempDir string) {
|
||||
log.Printf("[PYTHON-%s] Running Python in interactive mode", submission.ID)
|
||||
|
||||
// Create an input channel for this submission
|
||||
inputChan := make(chan string)
|
||||
s.mu.Lock()
|
||||
s.inputChannels[submission.ID] = inputChan
|
||||
s.mu.Unlock()
|
||||
|
||||
// Clean up when done
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
delete(s.inputChannels, submission.ID)
|
||||
close(inputChan)
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
|
||||
// Create a wrapper script that handles interactive input
|
||||
wrapperPath := filepath.Join(tempDir, "wrapper.py")
|
||||
wrapperCode := `
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
|
||||
# Load the user's code
|
||||
with open('/code/code.py', 'r') as f:
|
||||
user_code = f.read()
|
||||
|
||||
# Replace the built-in input function
|
||||
original_input = input
|
||||
|
||||
def custom_input(prompt=''):
|
||||
# Print the prompt without newline
|
||||
sys.stdout.write(prompt)
|
||||
sys.stdout.flush()
|
||||
|
||||
# Signal that we're waiting for input
|
||||
sys.stdout.write('\n[WAITING_FOR_INPUT]\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
# Wait for input from the parent process
|
||||
# Use a blocking read that won't raise EOFError
|
||||
line = ''
|
||||
while True:
|
||||
try:
|
||||
char = sys.stdin.read(1)
|
||||
if char == '\n':
|
||||
break
|
||||
if char:
|
||||
line += char
|
||||
except:
|
||||
# If any error occurs, wait a bit and try again
|
||||
time.sleep(0.1)
|
||||
continue
|
||||
|
||||
# Echo the input as if the user typed it
|
||||
sys.stdout.write(line + '\n')
|
||||
sys.stdout.flush()
|
||||
|
||||
return line
|
||||
|
||||
# Replace the built-in input function
|
||||
input = custom_input
|
||||
|
||||
# Execute the user's code
|
||||
try:
|
||||
# Use globals and locals to ensure proper variable scope
|
||||
exec(user_code, globals(), globals())
|
||||
except Exception as e:
|
||||
# Print detailed error information
|
||||
sys.stdout.write(f'\nError: {str(e)}\n')
|
||||
traceback.print_exc(file=sys.stdout)
|
||||
sys.stdout.flush()
|
||||
`
|
||||
|
||||
if err := os.WriteFile(wrapperPath, []byte(wrapperCode), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write wrapper file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) // Longer timeout for interactive
|
||||
defer cancel()
|
||||
|
||||
// Start the container
|
||||
cmd := exec.CommandContext(ctx, "docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"--ulimit", "nofile=64:64", // File descriptor limits
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"python:3.9",
|
||||
"python", "/code/wrapper.py")
|
||||
|
||||
// Get pipes for stdin and stdout
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdin pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdout pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to start command: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Set status to running
|
||||
submission.Status = "running"
|
||||
|
||||
// Read output in a goroutine
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Check if the program is waiting for input
|
||||
if line == "[WAITING_FOR_INPUT]" {
|
||||
// Update status to waiting for input
|
||||
submission.Status = "waiting_for_input"
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the output to the submission
|
||||
submission.Output += line + "\n"
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle input in a goroutine
|
||||
go func() {
|
||||
for input := range inputChan {
|
||||
// Write the input to stdin
|
||||
_, err := stdin.Write([]byte(input + "\n"))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to write to stdin: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the command to complete
|
||||
err = cmd.Wait()
|
||||
|
||||
// Update the submission status
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = "Execution timed out"
|
||||
} else {
|
||||
submission.Status = "failed"
|
||||
submission.Error = err.Error()
|
||||
}
|
||||
} else {
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
submission.CompletedAt = time.Now()
|
||||
log.Printf("[PYTHON-%s] Interactive execution completed", submission.ID)
|
||||
}
|
||||
|
||||
// executeJavaScriptInteractive runs JavaScript code in interactive mode
|
||||
func (s *ExecutionService) executeJavaScriptInteractive(submission *models.CodeSubmission, tempDir string) {
|
||||
log.Printf("[JS-%s] Running JavaScript in interactive mode", submission.ID)
|
||||
|
||||
// Create an input channel for this submission
|
||||
inputChan := make(chan string)
|
||||
s.mu.Lock()
|
||||
s.inputChannels[submission.ID] = inputChan
|
||||
s.mu.Unlock()
|
||||
|
||||
// Clean up when done
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
delete(s.inputChannels, submission.ID)
|
||||
close(inputChan)
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
|
||||
// Create a wrapper script that handles interactive input
|
||||
wrapperPath := filepath.Join(tempDir, "wrapper.js")
|
||||
wrapperCode := `
|
||||
const fs = require('fs');
|
||||
const readline = require('readline');
|
||||
|
||||
// Load the user's code
|
||||
const userCode = fs.readFileSync('/code/code.js', 'utf8');
|
||||
|
||||
// Create a custom readline interface
|
||||
const originalReadline = readline.createInterface;
|
||||
readline.createInterface = function(options) {
|
||||
// Create a custom interface that intercepts input
|
||||
const rl = originalReadline({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
terminal: false
|
||||
});
|
||||
|
||||
// Override the question method
|
||||
const originalQuestion = rl.question;
|
||||
rl.question = function(query, callback) {
|
||||
// Print the prompt
|
||||
process.stdout.write(query);
|
||||
|
||||
// Signal that we're waiting for input
|
||||
process.stdout.write('\n[WAITING_FOR_INPUT]\n');
|
||||
process.stdout.flush();
|
||||
|
||||
// Set up a more robust input handler
|
||||
const onLine = (answer) => {
|
||||
// Echo the input as if the user typed it
|
||||
process.stdout.write(answer + '\n');
|
||||
process.stdout.flush();
|
||||
callback(answer);
|
||||
};
|
||||
|
||||
// Handle input with error recovery
|
||||
rl.once('line', onLine);
|
||||
|
||||
// Add error handler
|
||||
rl.once('error', (err) => {
|
||||
console.error('Input error:', err.message);
|
||||
// Provide a default answer in case of error
|
||||
callback('');
|
||||
});
|
||||
};
|
||||
|
||||
return rl;
|
||||
};
|
||||
|
||||
// Capture uncaught exceptions
|
||||
process.on('uncaughtException', (err) => {
|
||||
console.error('Uncaught Exception:', err.message);
|
||||
console.error(err.stack);
|
||||
});
|
||||
|
||||
// Execute the user's code
|
||||
try {
|
||||
eval(userCode);
|
||||
} catch (e) {
|
||||
console.error('Error:', e.message);
|
||||
console.error(e.stack);
|
||||
}
|
||||
`
|
||||
|
||||
if err := os.WriteFile(wrapperPath, []byte(wrapperCode), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write wrapper file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Run the code in a Docker container
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) // Longer timeout for interactive
|
||||
defer cancel()
|
||||
|
||||
// Start the container
|
||||
cmd := exec.CommandContext(ctx, "docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"node:18-alpine",
|
||||
"node", "/code/wrapper.js")
|
||||
|
||||
// Get pipes for stdin and stdout
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdin pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdout pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to start command: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Set status to running
|
||||
submission.Status = "running"
|
||||
|
||||
// Read output in a goroutine
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// Check if the program is waiting for input
|
||||
if line == "[WAITING_FOR_INPUT]" {
|
||||
// Update status to waiting for input
|
||||
submission.Status = "waiting_for_input"
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the output to the submission
|
||||
submission.Output += line + "\n"
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle input in a goroutine
|
||||
go func() {
|
||||
for input := range inputChan {
|
||||
// Write the input to stdin
|
||||
_, err := stdin.Write([]byte(input + "\n"))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to write to stdin: %v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the command to complete
|
||||
err = cmd.Wait()
|
||||
|
||||
// Update the submission status
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = "Execution timed out"
|
||||
} else {
|
||||
submission.Status = "failed"
|
||||
submission.Error = err.Error()
|
||||
}
|
||||
} else {
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
submission.CompletedAt = time.Now()
|
||||
log.Printf("[JS-%s] Interactive execution completed", submission.ID)
|
||||
}
|
||||
@@ -1,376 +0,0 @@
|
||||
package executor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/models"
|
||||
)
|
||||
|
||||
// WebSocketSession represents a WebSocket execution session
|
||||
type WebSocketSession struct {
|
||||
Submission *models.CodeSubmission
|
||||
InputChan chan string
|
||||
OutputChan chan string
|
||||
Done chan struct{}
|
||||
}
|
||||
|
||||
// SetupWebSocketChannels sets up the channels for WebSocket communication
|
||||
func (s *ExecutionService) SetupWebSocketChannels(submission *models.CodeSubmission, inputChan chan string, outputChan chan string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// Store the channels in the service
|
||||
s.wsInputChannels[submission.ID] = inputChan
|
||||
s.wsOutputChannels[submission.ID] = outputChan
|
||||
}
|
||||
|
||||
// ExecuteCodeWebSocket executes code and streams the output over WebSocket
|
||||
func (s *ExecutionService) ExecuteCodeWebSocket(submission *models.CodeSubmission) {
|
||||
log.Printf("[WS-%s] Starting WebSocket execution for %s code", submission.ID, submission.Language)
|
||||
|
||||
// Update submission status
|
||||
submission.Status = "running"
|
||||
submission.StartedAt = time.Now()
|
||||
|
||||
// Execute the code based on the language
|
||||
switch strings.ToLower(submission.Language) {
|
||||
case "python":
|
||||
s.executePythonWebSocket(submission)
|
||||
case "javascript":
|
||||
s.executeJavaScriptWebSocket(submission)
|
||||
case "go":
|
||||
s.executeGoWebSocket(submission)
|
||||
case "java":
|
||||
s.executeJavaWebSocket(submission)
|
||||
case "c":
|
||||
s.executeCWebSocket(submission)
|
||||
case "cpp":
|
||||
s.executeCppWebSocket(submission)
|
||||
default:
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Unsupported language: %s", submission.Language)
|
||||
submission.CompletedAt = time.Now()
|
||||
}
|
||||
|
||||
log.Printf("[WS-%s] Execution completed with status: %s", submission.ID, submission.Status)
|
||||
}
|
||||
|
||||
// executePythonWebSocket executes Python code with WebSocket communication
|
||||
func (s *ExecutionService) executePythonWebSocket(submission *models.CodeSubmission) {
|
||||
log.Printf("[WS-PYTHON-%s] Preparing Python WebSocket execution", submission.ID)
|
||||
|
||||
// Create a temporary directory for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-ws-python-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "code.py")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the input and output channels
|
||||
s.mu.Lock()
|
||||
inputChan := s.wsInputChannels[submission.ID]
|
||||
outputChan := s.wsOutputChannels[submission.ID]
|
||||
s.mu.Unlock()
|
||||
|
||||
// Create a context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Run the code in a Docker container
|
||||
cmd := exec.CommandContext(ctx, "docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"--ulimit", "nofile=64:64", // File descriptor limits
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"python:3.9",
|
||||
"python", "/code/code.py")
|
||||
|
||||
// Get pipes for stdin and stdout
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdin pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdout pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stderr pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to start command: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a done channel to signal when the command is complete
|
||||
done := make(chan struct{})
|
||||
|
||||
// Read from stdout and send to the output channel
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
select {
|
||||
case outputChan <- line + "\n":
|
||||
// Output sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from stderr and send to the output channel
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
select {
|
||||
case outputChan <- "ERROR: " + line + "\n":
|
||||
// Error sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from the input channel and write to stdin
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case input := <-inputChan:
|
||||
// Write the input to stdin
|
||||
_, err := io.WriteString(stdin, input+"\n")
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to write to stdin: %v", submission.ID, err)
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the command to complete
|
||||
err = cmd.Wait()
|
||||
close(done)
|
||||
|
||||
// Update the submission status
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = "Execution timed out"
|
||||
} else {
|
||||
submission.Status = "failed"
|
||||
submission.Error = err.Error()
|
||||
}
|
||||
} else {
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
submission.CompletedAt = time.Now()
|
||||
log.Printf("[WS-PYTHON-%s] WebSocket execution completed", submission.ID)
|
||||
}
|
||||
|
||||
// executeJavaScriptWebSocket executes JavaScript code with WebSocket communication
|
||||
func (s *ExecutionService) executeJavaScriptWebSocket(submission *models.CodeSubmission) {
|
||||
log.Printf("[WS-JS-%s] Preparing JavaScript WebSocket execution", submission.ID)
|
||||
|
||||
// Create a temporary directory for the code
|
||||
tempDir, err := os.MkdirTemp("", "monaco-ws-js-*")
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to create temp directory: %v", err)
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Write the code to a file
|
||||
codePath := filepath.Join(tempDir, "code.js")
|
||||
if err := os.WriteFile(codePath, []byte(submission.Code), 0644); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to write code file: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the input and output channels
|
||||
s.mu.Lock()
|
||||
inputChan := s.wsInputChannels[submission.ID]
|
||||
outputChan := s.wsOutputChannels[submission.ID]
|
||||
s.mu.Unlock()
|
||||
|
||||
// Create a context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Run the code in a Docker container
|
||||
cmd := exec.CommandContext(ctx, "docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"node:18-alpine",
|
||||
"node", "/code/code.js")
|
||||
|
||||
// Get pipes for stdin and stdout
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdin pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stdout pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to get stderr pipe: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = fmt.Sprintf("Failed to start command: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Create a done channel to signal when the command is complete
|
||||
done := make(chan struct{})
|
||||
|
||||
// Read from stdout and send to the output channel
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
select {
|
||||
case outputChan <- line + "\n":
|
||||
// Output sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from stderr and send to the output channel
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stderr)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
select {
|
||||
case outputChan <- "ERROR: " + line + "\n":
|
||||
// Error sent successfully
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Read from the input channel and write to stdin
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case input := <-inputChan:
|
||||
// Write the input to stdin
|
||||
_, err := io.WriteString(stdin, input+"\n")
|
||||
if err != nil {
|
||||
log.Printf("[WS-JS-%s] Failed to write to stdin: %v", submission.ID, err)
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the command to complete
|
||||
err = cmd.Wait()
|
||||
close(done)
|
||||
|
||||
// Update the submission status
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Error = "Execution timed out"
|
||||
} else {
|
||||
submission.Status = "failed"
|
||||
submission.Error = err.Error()
|
||||
}
|
||||
} else {
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
submission.CompletedAt = time.Now()
|
||||
log.Printf("[WS-JS-%s] WebSocket execution completed", submission.ID)
|
||||
}
|
||||
|
||||
// executeGoWebSocket executes Go code with WebSocket communication
|
||||
func (s *ExecutionService) executeGoWebSocket(submission *models.CodeSubmission) {
|
||||
// Implementation similar to executePythonWebSocket but for Go
|
||||
// For brevity, this is left as a placeholder
|
||||
submission.Status = "failed"
|
||||
submission.Error = "WebSocket execution for Go not implemented yet"
|
||||
}
|
||||
|
||||
// executeJavaWebSocket executes Java code with WebSocket communication
|
||||
func (s *ExecutionService) executeJavaWebSocket(submission *models.CodeSubmission) {
|
||||
// Implementation similar to executePythonWebSocket but for Java
|
||||
// For brevity, this is left as a placeholder
|
||||
submission.Status = "failed"
|
||||
submission.Error = "WebSocket execution for Java not implemented yet"
|
||||
}
|
||||
|
||||
// executeCWebSocket executes C code with WebSocket communication
|
||||
func (s *ExecutionService) executeCWebSocket(submission *models.CodeSubmission) {
|
||||
// Implementation similar to executePythonWebSocket but for C
|
||||
// For brevity, this is left as a placeholder
|
||||
submission.Status = "failed"
|
||||
submission.Error = "WebSocket execution for C not implemented yet"
|
||||
}
|
||||
|
||||
// executeCppWebSocket executes C++ code with WebSocket communication
|
||||
func (s *ExecutionService) executeCppWebSocket(submission *models.CodeSubmission) {
|
||||
// Implementation similar to executePythonWebSocket but for C++
|
||||
// For brevity, this is left as a placeholder
|
||||
submission.Status = "failed"
|
||||
submission.Error = "WebSocket execution for C++ not implemented yet"
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package models
|
||||
|
||||
import "time"
|
||||
|
||||
// CodeSubmission represents a code submission for execution
|
||||
type CodeSubmission struct {
|
||||
ID string `json:"id"`
|
||||
Code string `json:"code"`
|
||||
Language string `json:"language"`
|
||||
Input string `json:"input"`
|
||||
Status string `json:"status"` // "pending", "queued", "running", "waiting_for_input", "completed", "failed"
|
||||
QueuedAt time.Time `json:"queuedAt,omitempty"`
|
||||
StartedAt time.Time `json:"startedAt,omitempty"`
|
||||
CompletedAt time.Time `json:"completedAt,omitempty"`
|
||||
Output string `json:"output,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
IsInteractive bool `json:"isInteractive,omitempty"` // Whether the program requires interactive input
|
||||
CurrentPrompt string `json:"currentPrompt,omitempty"` // Current input prompt if waiting for input
|
||||
}
|
||||
|
||||
// ExecutionResult represents the result of code execution
|
||||
type ExecutionResult struct {
|
||||
Output string `json:"output"`
|
||||
Error string `json:"error"`
|
||||
ExitCode int `json:"exitCode"`
|
||||
ExecutionMS int64 `json:"executionMs"`
|
||||
}
|
||||
|
||||
// QueueStats represents statistics about the job queue
|
||||
type QueueStats struct {
|
||||
QueueLength int `json:"queueLength"`
|
||||
RunningJobs int `json:"runningJobs"`
|
||||
CompletedJobs int `json:"completedJobs"`
|
||||
FailedJobs int `json:"failedJobs"`
|
||||
TotalProcessed int `json:"totalProcessed"`
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package queue
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/internal/models"
|
||||
)
|
||||
|
||||
// Job represents a job to be executed
|
||||
type Job interface {
|
||||
Execute()
|
||||
}
|
||||
|
||||
// JobQueue manages the execution of jobs
|
||||
type JobQueue struct {
|
||||
queue chan Job
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
runningJobs int
|
||||
completedJobs int
|
||||
failedJobs int
|
||||
totalProcessed int
|
||||
workerCount int
|
||||
}
|
||||
|
||||
// NewJobQueue creates a new job queue with the specified number of workers
|
||||
func NewJobQueue(workerCount int) *JobQueue {
|
||||
q := &JobQueue{
|
||||
queue: make(chan Job, 100), // Buffer size of 100 jobs
|
||||
workerCount: workerCount,
|
||||
}
|
||||
|
||||
// Start workers
|
||||
for i := 0; i < workerCount; i++ {
|
||||
q.wg.Add(1)
|
||||
go q.worker(i)
|
||||
}
|
||||
|
||||
return q
|
||||
}
|
||||
|
||||
// worker processes jobs from the queue
|
||||
func (q *JobQueue) worker(id int) {
|
||||
defer q.wg.Done()
|
||||
|
||||
log.Printf("[WORKER-%d] Started", id)
|
||||
|
||||
for job := range q.queue {
|
||||
// Update stats
|
||||
q.mu.Lock()
|
||||
q.runningJobs++
|
||||
q.mu.Unlock()
|
||||
|
||||
// Execute the job
|
||||
startTime := time.Now()
|
||||
log.Printf("[WORKER-%d] Processing job", id)
|
||||
|
||||
// Execute the job and handle panics
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[WORKER-%d] Panic in job execution: %v", id, r)
|
||||
q.mu.Lock()
|
||||
q.failedJobs++
|
||||
q.runningJobs--
|
||||
q.totalProcessed++
|
||||
q.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
job.Execute()
|
||||
}()
|
||||
|
||||
// Update stats if no panic occurred
|
||||
q.mu.Lock()
|
||||
q.completedJobs++
|
||||
q.runningJobs--
|
||||
q.totalProcessed++
|
||||
q.mu.Unlock()
|
||||
|
||||
log.Printf("[WORKER-%d] Job completed in %v", id, time.Since(startTime))
|
||||
}
|
||||
|
||||
log.Printf("[WORKER-%d] Stopped", id)
|
||||
}
|
||||
|
||||
// AddJob adds a job to the queue
|
||||
func (q *JobQueue) AddJob(job Job) {
|
||||
q.queue <- job
|
||||
}
|
||||
|
||||
// GetStats returns statistics about the job queue
|
||||
func (q *JobQueue) GetStats() models.QueueStats {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
return models.QueueStats{
|
||||
QueueLength: len(q.queue),
|
||||
RunningJobs: q.runningJobs,
|
||||
CompletedJobs: q.completedJobs,
|
||||
FailedJobs: q.failedJobs,
|
||||
TotalProcessed: q.totalProcessed,
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown stops the job queue
|
||||
func (q *JobQueue) Shutdown() {
|
||||
close(q.queue)
|
||||
q.wg.Wait()
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/handler"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Configure logging with timestamps and file locations
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
log.Println("Starting Monaco code execution backend...")
|
||||
|
||||
h := handler.NewHandler()
|
||||
|
||||
// Create a middleware for request logging
|
||||
loggingMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
startTime := time.Now()
|
||||
log.Printf("[HTTP] %s %s %s", r.Method, r.URL.Path, r.RemoteAddr)
|
||||
next(w, r)
|
||||
log.Printf("[HTTP] %s %s completed in %v", r.Method, r.URL.Path, time.Since(startTime))
|
||||
}
|
||||
}
|
||||
|
||||
// Create a middleware for CORS - allow all origins
|
||||
corsMiddleware := func(next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Set CORS headers to allow any origin
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
|
||||
w.Header().Set("Access-Control-Max-Age", "3600")
|
||||
|
||||
// Handle preflight OPTIONS requests
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
next(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Register handlers with logging and CORS middleware
|
||||
http.HandleFunc("/submit", corsMiddleware(loggingMiddleware(h.SubmitHandler)))
|
||||
http.HandleFunc("/status", corsMiddleware(loggingMiddleware(h.StatusHandler)))
|
||||
http.HandleFunc("/result", corsMiddleware(loggingMiddleware(h.ResultHandler)))
|
||||
http.HandleFunc("/queue-stats", corsMiddleware(loggingMiddleware(h.QueueStatsHandler)))
|
||||
http.HandleFunc("/ws", corsMiddleware(h.WebSocketHandler)) // WebSocket doesn't need logging middleware
|
||||
|
||||
port := ":8080"
|
||||
log.Printf("Server started at %s", port)
|
||||
log.Fatal(http.ListenAndServe(port, nil))
|
||||
}
|
||||
@@ -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"`)
|
||||
}
|
||||
@@ -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,712 +0,0 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/arnab-afk/monaco/model"
|
||||
"github.com/arnab-afk/monaco/queue"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
// ExecutionService handles code execution for multiple languages
|
||||
type ExecutionService struct {
|
||||
mu sync.Mutex
|
||||
queue *queue.JobQueue
|
||||
wsConnections map[string]*websocket.Conn // Map of submission ID to WebSocket connection
|
||||
wsInputChannels map[string]chan string // Map of submission ID to input channel
|
||||
}
|
||||
|
||||
// NewExecutionService creates a new execution service
|
||||
func NewExecutionService() *ExecutionService {
|
||||
log.Println("Initializing execution service with 3 concurrent workers")
|
||||
return &ExecutionService{
|
||||
queue: queue.NewJobQueue(3), // 3 concurrent executions max
|
||||
wsConnections: make(map[string]*websocket.Conn),
|
||||
wsInputChannels: make(map[string]chan string),
|
||||
}
|
||||
}
|
||||
|
||||
// CodeExecutionJob represents a job to execute code
|
||||
type CodeExecutionJob struct {
|
||||
service *ExecutionService
|
||||
submission *model.CodeSubmission
|
||||
}
|
||||
|
||||
// NewCodeExecutionJob creates a new code execution job
|
||||
func NewCodeExecutionJob(service *ExecutionService, submission *model.CodeSubmission) *CodeExecutionJob {
|
||||
return &CodeExecutionJob{
|
||||
service: service,
|
||||
submission: submission,
|
||||
}
|
||||
}
|
||||
|
||||
// Execute runs the code execution job
|
||||
func (j *CodeExecutionJob) Execute() {
|
||||
submission := j.submission
|
||||
submission.Status = "running"
|
||||
submission.StartedAt = time.Now()
|
||||
|
||||
log.Printf("[JOB-%s] Starting execution for language: %s",
|
||||
submission.ID, submission.Language)
|
||||
|
||||
j.service.executeLanguageSpecific(submission)
|
||||
}
|
||||
|
||||
// ExecuteCode adds the submission to the execution queue
|
||||
func (s *ExecutionService) ExecuteCode(submission *model.CodeSubmission) {
|
||||
submission.Status = "queued"
|
||||
submission.QueuedAt = time.Now()
|
||||
|
||||
log.Printf("[SUBMISSION-%s] Code submission queued for language: %s (Queue length: %d)",
|
||||
submission.ID, submission.Language, s.queue.QueueStats()["queue_length"])
|
||||
|
||||
// Log if input is provided
|
||||
if len(submission.Input) > 0 {
|
||||
inputLen := len(submission.Input)
|
||||
previewLen := 30
|
||||
if inputLen > previewLen {
|
||||
log.Printf("[INPUT-%s] Input provided (%d bytes): %s...",
|
||||
submission.ID, inputLen, submission.Input[:previewLen])
|
||||
} else {
|
||||
log.Printf("[INPUT-%s] Input provided (%d bytes): %s",
|
||||
submission.ID, inputLen, submission.Input)
|
||||
}
|
||||
}
|
||||
|
||||
job := NewCodeExecutionJob(s, submission)
|
||||
s.queue.Enqueue(job)
|
||||
}
|
||||
|
||||
// executeLanguageSpecific runs code in the appropriate language container
|
||||
func (s *ExecutionService) executeLanguageSpecific(submission *model.CodeSubmission) {
|
||||
log.Printf("[EXEC-%s] Selecting execution environment for language: %s",
|
||||
submission.ID, submission.Language)
|
||||
|
||||
switch submission.Language {
|
||||
case "python":
|
||||
log.Printf("[EXEC-%s] Executing Python code", submission.ID)
|
||||
s.executePython(submission)
|
||||
case "java":
|
||||
log.Printf("[EXEC-%s] Executing Java code", submission.ID)
|
||||
s.executeJava(submission)
|
||||
case "c":
|
||||
log.Printf("[EXEC-%s] Executing C code", submission.ID)
|
||||
s.executeC(submission)
|
||||
case "cpp":
|
||||
log.Printf("[EXEC-%s] Executing C++ code", submission.ID)
|
||||
s.executeCpp(submission)
|
||||
default:
|
||||
log.Printf("[EXEC-%s] ERROR: Unsupported language: %s", submission.ID, submission.Language)
|
||||
submission.Status = "failed"
|
||||
submission.Output = "Unsupported language: " + submission.Language
|
||||
}
|
||||
}
|
||||
|
||||
// executeWithInput runs a command with a timeout and provides input
|
||||
func (s *ExecutionService) executeWithInput(cmd *exec.Cmd, input string, timeout time.Duration, submissionID string) ([]byte, error) {
|
||||
log.Printf("[TIMEOUT-%s] Setting execution timeout: %v", submissionID, timeout)
|
||||
|
||||
// Set up input pipe if input is provided
|
||||
if input != "" {
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR-%s] Failed to create stdin pipe: %v", submissionID, err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Write input in a goroutine to avoid blocking
|
||||
go func() {
|
||||
defer stdin.Close()
|
||||
io.WriteString(stdin, input)
|
||||
}()
|
||||
|
||||
log.Printf("[INPUT-%s] Providing input to process", submissionID)
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
var output []byte
|
||||
var err error
|
||||
|
||||
go func() {
|
||||
log.Printf("[EXEC-%s] Starting command execution: %v", submissionID, cmd.Args)
|
||||
output, err = cmd.CombinedOutput()
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
log.Printf("[TIMEOUT-%s] Execution timed out after %v seconds", submissionID, timeout.Seconds())
|
||||
if err := cmd.Process.Kill(); err != nil {
|
||||
log.Printf("[TIMEOUT-%s] Failed to kill process: %v", submissionID, err)
|
||||
return nil, fmt.Errorf("timeout reached but failed to kill process: %v", err)
|
||||
}
|
||||
return nil, fmt.Errorf("execution timed out after %v seconds", timeout.Seconds())
|
||||
case <-done:
|
||||
if err != nil {
|
||||
log.Printf("[EXEC-%s] Command execution failed: %v", submissionID, err)
|
||||
} else {
|
||||
log.Printf("[EXEC-%s] Command execution completed successfully", submissionID)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
}
|
||||
|
||||
// executePython runs Python code in a container
|
||||
func (s *ExecutionService) executePython(submission *model.CodeSubmission) {
|
||||
log.Printf("[PYTHON-%s] Preparing Python execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
cmd := exec.Command("docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"--ulimit", "nofile=64:64", // File descriptor limits
|
||||
"python:3.9", "python", "-c", submission.Code)
|
||||
|
||||
log.Printf("[PYTHON-%s] Executing Python code with timeout: 10s", submission.ID)
|
||||
var output []byte
|
||||
var err error
|
||||
|
||||
if submission.Input != "" {
|
||||
cmd.Stdin = strings.NewReader(submission.Input)
|
||||
output, err = cmd.CombinedOutput()
|
||||
} else {
|
||||
output, err = s.executeWithTimeout(cmd, 10*time.Second, submission.ID)
|
||||
}
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[PYTHON-%s] Python execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err)
|
||||
}
|
||||
|
||||
// extractClassName extracts the Java class name from code
|
||||
func extractClassName(code string) string {
|
||||
// Default class name as fallback
|
||||
defaultClass := "Solution"
|
||||
|
||||
// Look for public class
|
||||
re := regexp.MustCompile(`public\s+class\s+(\w+)`)
|
||||
matches := re.FindStringSubmatch(code)
|
||||
if len(matches) > 1 {
|
||||
return matches[1]
|
||||
}
|
||||
|
||||
// Look for any class if no public class
|
||||
re = regexp.MustCompile(`class\s+(\w+)`)
|
||||
matches = re.FindStringSubmatch(code)
|
||||
if len(matches) > 1 {
|
||||
return matches[1]
|
||||
}
|
||||
|
||||
return defaultClass
|
||||
}
|
||||
|
||||
// executeJava runs Java code in a container
|
||||
func (s *ExecutionService) executeJava(submission *model.CodeSubmission) {
|
||||
log.Printf("[JAVA-%s] Preparing Java execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Extract class name from code
|
||||
className := extractClassName(submission.Code)
|
||||
log.Printf("[JAVA-%s] Detected class name: %s", submission.ID, className)
|
||||
|
||||
// Create temp directory for Java files
|
||||
tempDir, err := os.MkdirTemp("", "java-execution-"+submission.ID)
|
||||
if err != nil {
|
||||
log.Printf("[JAVA-%s] Failed to create temp directory: %v", submission.ID, err)
|
||||
submission.Status = "failed"
|
||||
submission.Output = "Failed to create temp directory: " + err.Error()
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
log.Printf("[JAVA-%s] Created temp directory: %s", submission.ID, tempDir)
|
||||
|
||||
// Write Java code to file with detected class name
|
||||
javaFilePath := filepath.Join(tempDir, className+".java")
|
||||
if err := os.WriteFile(javaFilePath, []byte(submission.Code), 0644); err != nil {
|
||||
log.Printf("[JAVA-%s] Failed to write Java file: %v", submission.ID, err)
|
||||
submission.Status = "failed"
|
||||
submission.Output = "Failed to write Java file: " + err.Error()
|
||||
return
|
||||
}
|
||||
log.Printf("[JAVA-%s] Wrote code to file: %s", submission.ID, javaFilePath)
|
||||
|
||||
// First compile without running
|
||||
compileCmd := exec.Command("docker", "run", "--rm",
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"eclipse-temurin:11-jdk-alpine",
|
||||
"javac", "/code/"+className+".java")
|
||||
|
||||
log.Printf("[JAVA-%s] Compiling Java code", submission.ID)
|
||||
compileOutput, compileErr := compileCmd.CombinedOutput()
|
||||
|
||||
if compileErr != nil {
|
||||
log.Printf("[JAVA-%s] Compilation failed: %v", submission.ID, compileErr)
|
||||
submission.Status = "failed"
|
||||
submission.Output = "Compilation error:\n" + string(compileOutput)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[JAVA-%s] Compilation successful", submission.ID)
|
||||
|
||||
// Now run the compiled class
|
||||
runCmd := exec.Command("docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=400m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=50000", // 50% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"eclipse-temurin:11-jdk-alpine",
|
||||
"java", "-XX:+TieredCompilation", "-XX:TieredStopAtLevel=1",
|
||||
"-Xverify:none", "-Xms64m", "-Xmx256m",
|
||||
"-cp", "/code", className)
|
||||
|
||||
// Add input if provided
|
||||
var output []byte
|
||||
|
||||
if submission.Input != "" {
|
||||
log.Printf("[JAVA-%s] Executing Java code with input", submission.ID)
|
||||
runCmd.Stdin = strings.NewReader(submission.Input)
|
||||
output, err = runCmd.CombinedOutput()
|
||||
} else {
|
||||
log.Printf("[JAVA-%s] Executing Java code without input", submission.ID)
|
||||
output, err = s.executeWithTimeout(runCmd, 15*time.Second, submission.ID)
|
||||
}
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[JAVA-%s] Java execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err)
|
||||
}
|
||||
|
||||
// executeC runs C code in a container with improved file handling
|
||||
func (s *ExecutionService) executeC(submission *model.CodeSubmission) {
|
||||
log.Printf("[C-%s] Preparing C execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Create unique temp directory for C files
|
||||
tempDir, err := os.MkdirTemp("", "c-execution-"+submission.ID)
|
||||
if err != nil {
|
||||
log.Printf("[C-%s] Failed to create temp directory: %v", submission.ID, err)
|
||||
submission.Status = "failed"
|
||||
submission.Output = "Failed to create temp directory: " + err.Error()
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
log.Printf("[C-%s] Created temp directory: %s", submission.ID, tempDir)
|
||||
|
||||
// Write C code to file
|
||||
cFilePath := filepath.Join(tempDir, "solution.c")
|
||||
if err := os.WriteFile(cFilePath, []byte(submission.Code), 0644); err != nil {
|
||||
log.Printf("[C-%s] Failed to write C file: %v", submission.ID, err)
|
||||
submission.Status = "failed"
|
||||
submission.Output = "Failed to write C file: " + err.Error()
|
||||
return
|
||||
}
|
||||
log.Printf("[C-%s] Wrote code to file: %s", submission.ID, cFilePath)
|
||||
|
||||
// Compile C code first
|
||||
compileCmd := exec.Command("docker", "run", "--rm",
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest", "gcc", "-o", "/code/solution", "/code/solution.c")
|
||||
|
||||
compileOutput, compileErr := compileCmd.CombinedOutput()
|
||||
|
||||
if compileErr != nil {
|
||||
log.Printf("[C-%s] Compilation failed: %v", submission.ID, compileErr)
|
||||
submission.Status = "failed"
|
||||
submission.Output = "Compilation error:\n" + string(compileOutput)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[C-%s] Compilation successful", submission.ID)
|
||||
|
||||
// Run C executable
|
||||
runCmd := exec.Command("docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest", "/code/solution")
|
||||
|
||||
// Add input if provided
|
||||
var output []byte
|
||||
// Don't redeclare err here - use the existing variable
|
||||
if submission.Input != "" {
|
||||
log.Printf("[C-%s] Executing C code with input", submission.ID)
|
||||
runCmd.Stdin = strings.NewReader(submission.Input)
|
||||
output, err = runCmd.CombinedOutput() // Use the existing err variable
|
||||
} else {
|
||||
log.Printf("[C-%s] Executing C code without input", submission.ID)
|
||||
output, err = s.executeWithTimeout(runCmd, 10*time.Second, submission.ID) // Use the existing err variable
|
||||
}
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[C-%s] C execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err)
|
||||
}
|
||||
|
||||
// executeCpp runs C++ code in a container with improved file handling
|
||||
func (s *ExecutionService) executeCpp(submission *model.CodeSubmission) {
|
||||
log.Printf("[CPP-%s] Preparing C++ execution environment", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Create unique temp directory for C++ files
|
||||
tempDir, err := os.MkdirTemp("", "cpp-execution-"+submission.ID)
|
||||
if err != nil {
|
||||
log.Printf("[CPP-%s] Failed to create temp directory: %v", submission.ID, err)
|
||||
submission.Status = "failed"
|
||||
submission.Output = "Failed to create temp directory: " + err.Error()
|
||||
return
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
log.Printf("[CPP-%s] Created temp directory: %s", submission.ID, tempDir)
|
||||
|
||||
// Write C++ code to file
|
||||
cppFilePath := filepath.Join(tempDir, "solution.cpp")
|
||||
if err := os.WriteFile(cppFilePath, []byte(submission.Code), 0644); err != nil {
|
||||
log.Printf("[CPP-%s] Failed to write C++ file: %v", submission.ID, err)
|
||||
submission.Status = "failed"
|
||||
submission.Output = "Failed to write C++ file: " + err.Error()
|
||||
return
|
||||
}
|
||||
log.Printf("[CPP-%s] Wrote code to file: %s", submission.ID, cppFilePath)
|
||||
|
||||
// Compile C++ code first
|
||||
compileCmd := exec.Command("docker", "run", "--rm",
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest", "g++", "-o", "/code/solution", "/code/solution.cpp")
|
||||
|
||||
compileOutput, compileErr := compileCmd.CombinedOutput()
|
||||
|
||||
if compileErr != nil {
|
||||
log.Printf("[CPP-%s] Compilation failed: %v", submission.ID, compileErr)
|
||||
submission.Status = "failed"
|
||||
submission.Output = "Compilation error:\n" + string(compileOutput)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[CPP-%s] Compilation successful", submission.ID)
|
||||
|
||||
// Run C++ executable
|
||||
runCmd := exec.Command("docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"-v", tempDir+":/code", // Mount code directory
|
||||
"gcc:latest", "/code/solution")
|
||||
|
||||
// Add input if provided
|
||||
var output []byte
|
||||
if submission.Input != "" {
|
||||
log.Printf("[CPP-%s] Executing C++ code with input", submission.ID)
|
||||
runCmd.Stdin = strings.NewReader(submission.Input)
|
||||
output, err = runCmd.CombinedOutput()
|
||||
} else {
|
||||
log.Printf("[CPP-%s] Executing C++ code without input", submission.ID)
|
||||
output, err = s.executeWithTimeout(runCmd, 10*time.Second, submission.ID)
|
||||
}
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[CPP-%s] C++ execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
s.updateSubmissionResult(submission, output, err)
|
||||
}
|
||||
|
||||
// executeWithTimeout runs a command with a timeout
|
||||
func (s *ExecutionService) executeWithTimeout(cmd *exec.Cmd, timeout time.Duration, submissionID string) ([]byte, error) {
|
||||
log.Printf("[TIMEOUT-%s] Setting execution timeout: %v", submissionID, timeout)
|
||||
|
||||
done := make(chan error, 1)
|
||||
var output []byte
|
||||
var err error
|
||||
|
||||
go func() {
|
||||
log.Printf("[EXEC-%s] Starting command execution: %v", submissionID, cmd.Args)
|
||||
output, err = cmd.CombinedOutput()
|
||||
done <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
log.Printf("[TIMEOUT-%s] Execution timed out after %v seconds", submissionID, timeout.Seconds())
|
||||
if err := cmd.Process.Kill(); err != nil {
|
||||
log.Printf("[TIMEOUT-%s] Failed to kill process: %v", submissionID, err)
|
||||
return nil, fmt.Errorf("timeout reached but failed to kill process: %v", err)
|
||||
}
|
||||
return nil, fmt.Errorf("execution timed out after %v seconds", timeout.Seconds())
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
log.Printf("[EXEC-%s] Command execution failed: %v", submissionID, err)
|
||||
} else {
|
||||
log.Printf("[EXEC-%s] Command execution completed successfully", submissionID)
|
||||
}
|
||||
return output, err
|
||||
}
|
||||
}
|
||||
|
||||
// updateSubmissionResult updates the submission with execution results
|
||||
func (s *ExecutionService) updateSubmissionResult(submission *model.CodeSubmission, output []byte, err error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
submission.CompletedAt = time.Now()
|
||||
executionTime := submission.CompletedAt.Sub(submission.StartedAt)
|
||||
totalTime := submission.CompletedAt.Sub(submission.QueuedAt)
|
||||
|
||||
if err != nil {
|
||||
submission.Status = "failed"
|
||||
submission.Output = string(output) + "\n" + err.Error()
|
||||
log.Printf("[RESULT-%s] Execution FAILED in %v (total time: %v, including queue: %v)",
|
||||
submission.ID, executionTime, totalTime, totalTime-executionTime)
|
||||
} else {
|
||||
submission.Status = "completed"
|
||||
submission.Output = string(output)
|
||||
log.Printf("[RESULT-%s] Execution COMPLETED in %v (total time: %v, including queue: %v)",
|
||||
submission.ID, executionTime, totalTime, totalTime-executionTime)
|
||||
}
|
||||
}
|
||||
|
||||
// GetQueueStats returns statistics about the job queue
|
||||
func (s *ExecutionService) GetQueueStats() map[string]int {
|
||||
stats := s.queue.QueueStats()
|
||||
log.Printf("[QUEUE] Stats - Jobs in queue: %d, Running jobs: %d, Max workers: %d",
|
||||
stats["queue_length"], stats["running_jobs"], stats["max_workers"])
|
||||
return stats
|
||||
}
|
||||
|
||||
// HandleWebSocket handles a WebSocket connection for a code submission
|
||||
func (s *ExecutionService) HandleWebSocket(conn *websocket.Conn, submission *model.CodeSubmission) {
|
||||
// Store the WebSocket connection
|
||||
s.mu.Lock()
|
||||
s.wsConnections[submission.ID] = conn
|
||||
|
||||
// Create an input channel for this submission
|
||||
inputChan := make(chan string, 10) // Buffer size of 10
|
||||
s.wsInputChannels[submission.ID] = inputChan
|
||||
s.mu.Unlock()
|
||||
|
||||
// Clean up when done
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
delete(s.wsConnections, submission.ID)
|
||||
delete(s.wsInputChannels, submission.ID)
|
||||
s.mu.Unlock()
|
||||
conn.Close()
|
||||
}()
|
||||
|
||||
// Start a goroutine to read input from the WebSocket
|
||||
go func() {
|
||||
for {
|
||||
// Read message from WebSocket
|
||||
messageType, message, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
log.Printf("[WS-%s] Error reading message: %v", submission.ID, err)
|
||||
break
|
||||
}
|
||||
|
||||
// Only process text messages
|
||||
if messageType == websocket.TextMessage {
|
||||
// Send the input to the input channel
|
||||
inputChan <- string(message)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Execute the code
|
||||
submission.Status = "running"
|
||||
submission.StartedAt = time.Now()
|
||||
|
||||
log.Printf("[WS-JOB-%s] Starting WebSocket execution for language: %s",
|
||||
submission.ID, submission.Language)
|
||||
|
||||
// Execute the code based on the language
|
||||
s.executeLanguageSpecificWithWebSocket(submission, inputChan, conn)
|
||||
}
|
||||
|
||||
// executeLanguageSpecificWithWebSocket runs code in the appropriate language with WebSocket I/O
|
||||
func (s *ExecutionService) executeLanguageSpecificWithWebSocket(submission *model.CodeSubmission, inputChan chan string, conn *websocket.Conn) {
|
||||
log.Printf("[WS-EXEC-%s] Selecting execution environment for language: %s",
|
||||
submission.ID, submission.Language)
|
||||
|
||||
switch submission.Language {
|
||||
case "python":
|
||||
log.Printf("[WS-EXEC-%s] Executing Python code", submission.ID)
|
||||
s.executePythonWithWebSocket(submission, inputChan, conn)
|
||||
case "java":
|
||||
log.Printf("[WS-EXEC-%s] Executing Java code", submission.ID)
|
||||
s.executeJavaWithWebSocket(submission, inputChan, conn)
|
||||
case "c":
|
||||
log.Printf("[WS-EXEC-%s] Executing C code", submission.ID)
|
||||
s.executeCWithWebSocket(submission, inputChan, conn)
|
||||
case "cpp":
|
||||
log.Printf("[WS-EXEC-%s] Executing C++ code", submission.ID)
|
||||
s.executeCppWithWebSocket(submission, inputChan, conn)
|
||||
default:
|
||||
log.Printf("[WS-EXEC-%s] ERROR: Unsupported language: %s", submission.ID, submission.Language)
|
||||
submission.Status = "failed"
|
||||
output := "Unsupported language: " + submission.Language
|
||||
submission.Output = output
|
||||
|
||||
// Send error message to WebSocket
|
||||
conn.WriteMessage(websocket.TextMessage, []byte(output))
|
||||
}
|
||||
|
||||
// Update submission status
|
||||
submission.CompletedAt = time.Now()
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
// executePythonWithWebSocket runs Python code with WebSocket for I/O
|
||||
func (s *ExecutionService) executePythonWithWebSocket(submission *model.CodeSubmission, inputChan chan string, conn *websocket.Conn) {
|
||||
log.Printf("[WS-PYTHON-%s] Preparing Python WebSocket execution", submission.ID)
|
||||
startTime := time.Now()
|
||||
|
||||
// Send initial message to client
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Starting Python execution...\n"))
|
||||
|
||||
// Create a command to run Python in a Docker container
|
||||
cmd := exec.Command("docker", "run", "--rm", "-i",
|
||||
"--network=none", // No network access
|
||||
"--memory=100m", // Memory limit
|
||||
"--cpu-period=100000", // CPU quota period
|
||||
"--cpu-quota=10000", // 10% CPU
|
||||
"--ulimit", "nofile=64:64", // File descriptor limits
|
||||
"python:3.9", "python", "-c", submission.Code)
|
||||
|
||||
// Get stdin pipe
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to create stdin pipe: %v", submission.ID, err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Failed to create stdin pipe\n"))
|
||||
return
|
||||
}
|
||||
|
||||
// Get stdout and stderr pipes
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to create stdout pipe: %v", submission.ID, err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Failed to create stdout pipe\n"))
|
||||
return
|
||||
}
|
||||
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to create stderr pipe: %v", submission.ID, err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Error: Failed to create stderr pipe\n"))
|
||||
return
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to start command: %v", submission.ID, err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("Error: Failed to start command: %v\n", err)))
|
||||
return
|
||||
}
|
||||
|
||||
// Create a context with timeout
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Create a channel to signal when the command is done
|
||||
done := make(chan struct{})
|
||||
|
||||
// Start a goroutine to handle command completion
|
||||
go func() {
|
||||
err := cmd.Wait()
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Command failed: %v", submission.ID, err)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("\nExecution failed: %v\n", err)))
|
||||
} else {
|
||||
log.Printf("[WS-PYTHON-%s] Command completed successfully", submission.ID)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("\nExecution completed successfully\n"))
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
// Start a goroutine to read from stdout and stderr
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(io.MultiReader(stdout, stderr))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
log.Printf("[WS-PYTHON-%s] Output: %s", submission.ID, line)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte(line+"\n"))
|
||||
}
|
||||
}()
|
||||
|
||||
// Handle input from the WebSocket
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case input := <-inputChan:
|
||||
log.Printf("[WS-PYTHON-%s] Received input: %s", submission.ID, input)
|
||||
// Write the input to stdin
|
||||
_, err := io.WriteString(stdin, input+"\n")
|
||||
if err != nil {
|
||||
log.Printf("[WS-PYTHON-%s] Failed to write to stdin: %v", submission.ID, err)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for the command to complete or timeout
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Printf("[WS-PYTHON-%s] Execution timed out after 30 seconds", submission.ID)
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("\nExecution timed out after 30 seconds\n"))
|
||||
cmd.Process.Kill()
|
||||
case <-done:
|
||||
// Command completed
|
||||
}
|
||||
|
||||
elapsed := time.Since(startTime)
|
||||
log.Printf("[WS-PYTHON-%s] Python execution completed in %v", submission.ID, elapsed)
|
||||
|
||||
// Update submission result
|
||||
submission.CompletedAt = time.Now()
|
||||
submission.Status = "completed"
|
||||
}
|
||||
|
||||
// executeJavaWithWebSocket runs Java code with WebSocket for I/O
|
||||
func (s *ExecutionService) executeJavaWithWebSocket(submission *model.CodeSubmission, inputChan chan string, conn *websocket.Conn) {
|
||||
// For now, just send a message that this is not implemented
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("Java WebSocket execution not yet implemented\n"))
|
||||
submission.Status = "failed"
|
||||
submission.Output = "Java WebSocket execution not yet implemented"
|
||||
}
|
||||
|
||||
// executeCWithWebSocket runs C code with WebSocket for I/O
|
||||
func (s *ExecutionService) executeCWithWebSocket(submission *model.CodeSubmission, inputChan chan string, conn *websocket.Conn) {
|
||||
// For now, just send a message that this is not implemented
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("C WebSocket execution not yet implemented\n"))
|
||||
submission.Status = "failed"
|
||||
submission.Output = "C WebSocket execution not yet implemented"
|
||||
}
|
||||
|
||||
// executeCppWithWebSocket runs C++ code with WebSocket for I/O
|
||||
func (s *ExecutionService) executeCppWithWebSocket(submission *model.CodeSubmission, inputChan chan string, conn *websocket.Conn) {
|
||||
// For now, just send a message that this is not implemented
|
||||
conn.WriteMessage(websocket.TextMessage, []byte("C++ WebSocket execution not yet implemented\n"))
|
||||
submission.Status = "failed"
|
||||
submission.Output = "C++ WebSocket execution not yet implemented"
|
||||
}
|
||||
@@ -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 1exit status 1exit status 1exit status 1
|
||||
@@ -1,3 +0,0 @@
|
||||
# Very basic input example
|
||||
name = input("What is your name? ")
|
||||
print(f"Hello, {name}!")
|
||||
@@ -1,41 +0,0 @@
|
||||
// Interactive Calculator Example
|
||||
// This demonstrates how the interactive input/output works
|
||||
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
function calculator() {
|
||||
console.log("Welcome to the Interactive Calculator!");
|
||||
console.log("Enter 'q' to quit at any time.");
|
||||
|
||||
function promptUser() {
|
||||
rl.question("Enter an expression (e.g., 2 + 3): ", (expression) => {
|
||||
if (expression.toLowerCase() === 'q') {
|
||||
console.log("Thank you for using the Interactive Calculator!");
|
||||
rl.close();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Safely evaluate the expression
|
||||
const result = eval(expression);
|
||||
console.log(`Result: ${result}`);
|
||||
} catch (e) {
|
||||
console.log(`Error: ${e.message}`);
|
||||
console.log("Please try again with a valid expression.");
|
||||
}
|
||||
|
||||
// Continue prompting
|
||||
promptUser();
|
||||
});
|
||||
}
|
||||
|
||||
// Start the prompt loop
|
||||
promptUser();
|
||||
}
|
||||
|
||||
// Run the calculator
|
||||
calculator();
|
||||
@@ -1,24 +0,0 @@
|
||||
# Interactive Calculator Example
|
||||
# This demonstrates how the interactive input/output works
|
||||
|
||||
def calculator():
|
||||
print("Welcome to the Interactive Calculator!")
|
||||
print("Enter 'q' to quit at any time.")
|
||||
|
||||
while True:
|
||||
expression = input("Enter an expression (e.g., 2 + 3): ")
|
||||
|
||||
if expression.lower() == 'q':
|
||||
print("Thank you for using the Interactive Calculator!")
|
||||
break
|
||||
|
||||
try:
|
||||
# Safely evaluate the expression
|
||||
result = eval(expression)
|
||||
print(f"Result: {result}")
|
||||
except Exception as e:
|
||||
print(f"Error: {str(e)}")
|
||||
print("Please try again with a valid expression.")
|
||||
|
||||
# Run the calculator
|
||||
calculator()
|
||||
@@ -1,22 +0,0 @@
|
||||
// Interactive JavaScript Example
|
||||
// This example demonstrates interactive input/output
|
||||
|
||||
const readline = require('readline');
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
rl.question('Enter your name: ', (name) => {
|
||||
console.log(`Hello, ${name}!`);
|
||||
|
||||
rl.question('Enter your age: ', (age) => {
|
||||
console.log(`You are ${age} years old.`);
|
||||
|
||||
rl.question('What is your favorite color? ', (color) => {
|
||||
console.log(`Your favorite color is ${color}.`);
|
||||
console.log('Thank you for using the interactive example!');
|
||||
rl.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
# Interactive Python Example
|
||||
# This example demonstrates interactive input/output
|
||||
|
||||
name = input("Enter your name: ")
|
||||
print(f"Hello, {name}!")
|
||||
|
||||
age = input("Enter your age: ")
|
||||
print(f"You are {age} years old.")
|
||||
|
||||
favorite_color = input("What is your favorite color? ")
|
||||
print(f"Your favorite color is {favorite_color}.")
|
||||
|
||||
print("Thank you for using the interactive example!")
|
||||
@@ -1,5 +0,0 @@
|
||||
# Simple input example
|
||||
name = input("Enter your name: ")
|
||||
print(f"Hello, {name}!")
|
||||
for i in range(5):
|
||||
print(f"Count: {i}")
|
||||
37
new-backend/Dockerfile
Normal file
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"]
|
||||
24
new-backend/Dockerfile.tunnel-only
Normal file
24
new-backend/Dockerfile.tunnel-only
Normal file
@@ -0,0 +1,24 @@
|
||||
# Tunnel-only Dockerfile - Backend runs outside Docker on port 9090
|
||||
FROM alpine:latest
|
||||
|
||||
# Install wget to download cloudflared
|
||||
RUN apk update && apk add --no-cache wget ca-certificates
|
||||
|
||||
# Get cloudflared directly from GitHub
|
||||
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.tunnel-only.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"
|
||||
|
||||
# Run cloudflared tunnel
|
||||
CMD ["cloudflared", "tunnel", "--config", "/etc/cloudflared/config.json", "run"]
|
||||
24
new-backend/Dockerfile.tunnel-only-v2
Normal file
24
new-backend/Dockerfile.tunnel-only-v2
Normal file
@@ -0,0 +1,24 @@
|
||||
# Tunnel-only Dockerfile V2 - Uses localhost with host network mode
|
||||
FROM alpine:latest
|
||||
|
||||
# Install wget to download cloudflared
|
||||
RUN apk update && apk add --no-cache wget ca-certificates
|
||||
|
||||
# Get cloudflared directly from GitHub
|
||||
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.tunnel-only-v2.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"
|
||||
|
||||
# Run cloudflared tunnel
|
||||
CMD ["cloudflared", "tunnel", "--config", "/etc/cloudflared/config.json", "run"]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user