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;"'
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" href="/public/favicon.ico" type="image/x-icon" />
|
||||||
<title>VSCode</title>
|
<title>VSCode</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
BIN
Frontend/public/favicon.ico
Normal file
BIN
Frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
@@ -1 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,10 +1,18 @@
|
|||||||
import VSCodeUI from "./components/VSCodeUI.jsx"
|
import CodeChallenge from "./components/CodeChallenge.jsx"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
<VSCodeUI />
|
<CodeChallenge />
|
||||||
|
<footer className="footer-bar fixed bottom-0 left-0 right-0 border-t border-slate-200/40 dark:border-gray-800/20 bg-black">
|
||||||
|
<div className="flex items-center justify-center h-7">
|
||||||
|
<span className="text-xs text-slate-400 dark:text-gray-400 flex items-center">
|
||||||
|
Copyright © 2025. Made with
|
||||||
|
♡ by Ishika and Arnab.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
750
Frontend/src/components/CodeChallenge.jsx
Normal file
750
Frontend/src/components/CodeChallenge.jsx
Normal file
@@ -0,0 +1,750 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import Editor from "@monaco-editor/react";
|
||||||
|
import { Play, Send } from 'lucide-react';
|
||||||
|
|
||||||
|
const CodeChallenge = () => {
|
||||||
|
const [activeQuestion, setActiveQuestion] = useState("Q.1");
|
||||||
|
const [language, setLanguage] = useState("JavaScript");
|
||||||
|
const [code, setCode] = useState("");
|
||||||
|
const [isRunning, setIsRunning] = useState(false);
|
||||||
|
const [terminalOutput, setTerminalOutput] = useState([]);
|
||||||
|
const [autoSelected, setAutoSelected] = useState(true);
|
||||||
|
const [activeSocket, setActiveSocket] = useState(null);
|
||||||
|
const [submissionId, setSubmissionId] = useState(null);
|
||||||
|
const socketRef = useRef(null);
|
||||||
|
|
||||||
|
// Map frontend language names to backend language identifiers
|
||||||
|
const getLanguageIdentifier = (uiLanguage) => {
|
||||||
|
const languageMap = {
|
||||||
|
'javascript': 'javascript',
|
||||||
|
'python': 'python',
|
||||||
|
'java': 'java',
|
||||||
|
'c++': 'cpp',
|
||||||
|
'c': 'c'
|
||||||
|
};
|
||||||
|
// Important: make sure we convert to lowercase to match the backend's expected format
|
||||||
|
return languageMap[uiLanguage.toLowerCase()] || uiLanguage.toLowerCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset execution state to allow rerunning
|
||||||
|
const resetExecutionState = () => {
|
||||||
|
setIsRunning(false);
|
||||||
|
|
||||||
|
// Properly close the socket if it exists and is open
|
||||||
|
if (socketRef.current) {
|
||||||
|
if (socketRef.current.readyState === WebSocket.OPEN) {
|
||||||
|
socketRef.current.close();
|
||||||
|
}
|
||||||
|
socketRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure activeSocket is also nullified
|
||||||
|
setActiveSocket(null);
|
||||||
|
|
||||||
|
console.log('Execution state reset, buttons should be enabled');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Example problem data
|
||||||
|
const problems = {
|
||||||
|
"Q.1": {
|
||||||
|
id: "two-sum",
|
||||||
|
title: "Two Sum",
|
||||||
|
description: "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.",
|
||||||
|
constraints: "You may assume that each input would have exactly one solution, and you may not use the same element twice.",
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
input: "nums = [2,7,11,15], target = 9",
|
||||||
|
output: "[0,1]",
|
||||||
|
explanation: "Because nums[0] + nums[1] == 9, we return [0, 1]."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "nums = [3,2,4], target = 6",
|
||||||
|
output: "[1,2]"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "nums = [3,3], target = 6",
|
||||||
|
output: "[0,1]"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
starterCode: `/**
|
||||||
|
* @param {number[]} nums
|
||||||
|
* @param {number} target
|
||||||
|
* @return {number[]}
|
||||||
|
*/
|
||||||
|
var twoSum = function(nums, target) {
|
||||||
|
// Write your solution here
|
||||||
|
|
||||||
|
};`
|
||||||
|
},
|
||||||
|
"Q.2": {
|
||||||
|
id: "palindrome-number",
|
||||||
|
title: "Palindrome Number",
|
||||||
|
description: "Given an integer x, return true if x is a palindrome, and false otherwise.",
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
input: "x = 121",
|
||||||
|
output: "true"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: "x = -121",
|
||||||
|
output: "false",
|
||||||
|
explanation: "From left to right, it reads -121. From right to left, it reads 121-. Therefore it is not a palindrome."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
starterCode: `/**
|
||||||
|
* @param {number} x
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
var isPalindrome = function(x) {
|
||||||
|
// Write your solution here
|
||||||
|
|
||||||
|
};`
|
||||||
|
},
|
||||||
|
"Q.3": {
|
||||||
|
id: "valid-parentheses",
|
||||||
|
title: "Valid Parentheses",
|
||||||
|
description: "Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.",
|
||||||
|
constraints: "An input string is valid if: Open brackets must be closed by the same type of brackets. Open brackets must be closed in the correct order.",
|
||||||
|
examples: [
|
||||||
|
{
|
||||||
|
input: 's = "()"',
|
||||||
|
output: "true"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 's = "()[]{}"',
|
||||||
|
output: "true"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 's = "(]"',
|
||||||
|
output: "false"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
starterCode: `/**
|
||||||
|
* @param {string} s
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
var isValid = function(s) {
|
||||||
|
// Write your solution here
|
||||||
|
|
||||||
|
};`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get appropriate starter code based on language
|
||||||
|
const getStarterCode = (problem, lang) => {
|
||||||
|
// Language-specific starter code templates
|
||||||
|
const templates = {
|
||||||
|
'JavaScript': problem.starterCode,
|
||||||
|
'C': `#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
// ${problem.title} solution
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// Write your solution here
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}`,
|
||||||
|
'Python': `# ${problem.title}
|
||||||
|
def solution():
|
||||||
|
# Write your solution here
|
||||||
|
# Use input() for user input in Python
|
||||||
|
# Example: name = input("Enter your name: ")
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
solution()`,
|
||||||
|
'Java': `public class Solution {
|
||||||
|
// ${problem.title}
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// Write your solution here
|
||||||
|
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
'C++': `#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
using namespace std;
|
||||||
|
|
||||||
|
// ${problem.title} solution
|
||||||
|
int main() {
|
||||||
|
// Write your solution here
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}`
|
||||||
|
};
|
||||||
|
|
||||||
|
return templates[lang] || problem.starterCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set initial code based on active problem
|
||||||
|
useEffect(() => {
|
||||||
|
if (problems[activeQuestion]) {
|
||||||
|
setCode(getStarterCode(problems[activeQuestion], language));
|
||||||
|
}
|
||||||
|
}, [activeQuestion, language]);
|
||||||
|
|
||||||
|
// Cleanup WebSocket connection on unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (socketRef.current) {
|
||||||
|
socketRef.current.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Set a safety timeout to ensure buttons are re-enabled if execution hangs
|
||||||
|
useEffect(() => {
|
||||||
|
let safetyTimer = null;
|
||||||
|
|
||||||
|
if (isRunning) {
|
||||||
|
// If execution is running for more than 30 seconds, reset state
|
||||||
|
safetyTimer = setTimeout(() => {
|
||||||
|
console.log('Safety timeout reached, re-enabling buttons');
|
||||||
|
resetExecutionState();
|
||||||
|
}, 30000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (safetyTimer) clearTimeout(safetyTimer);
|
||||||
|
};
|
||||||
|
}, [isRunning]);
|
||||||
|
|
||||||
|
// Connect to WebSocket
|
||||||
|
const connectToWebSocket = (id) => {
|
||||||
|
console.log('Connecting to WebSocket with ID:', id);
|
||||||
|
|
||||||
|
// Force close any existing connections
|
||||||
|
if (socketRef.current) {
|
||||||
|
console.log('Closing existing socket, state:', socketRef.current.readyState);
|
||||||
|
socketRef.current.onclose = null; // Remove existing handler to avoid conflicts
|
||||||
|
socketRef.current.onerror = null;
|
||||||
|
socketRef.current.onmessage = null;
|
||||||
|
|
||||||
|
if (socketRef.current.readyState !== WebSocket.CLOSED) {
|
||||||
|
socketRef.current.close();
|
||||||
|
}
|
||||||
|
socketRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeSocket) {
|
||||||
|
console.log('Clearing active socket reference');
|
||||||
|
setActiveSocket(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Creating new WebSocket connection');
|
||||||
|
const wsUrl = `ws://localhost:8080/api/ws/terminal/${id}`;
|
||||||
|
const socket = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
socket.onopen = () => {
|
||||||
|
console.log('WebSocket connection established');
|
||||||
|
setActiveSocket(socket);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const message = JSON.parse(event.data);
|
||||||
|
console.log('WebSocket message received:', message, 'Current isRunning state:', isRunning);
|
||||||
|
|
||||||
|
switch (message.type) {
|
||||||
|
case 'output':
|
||||||
|
// Handle output message based on the format seen in the screenshot
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
type: message.content.isError ? 'error' : 'output',
|
||||||
|
content: message.content.text
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'input_prompt':
|
||||||
|
// Handle input prompt message (e.g., "Enter your name:")
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{ type: 'output', content: message.content }
|
||||||
|
]);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'status':
|
||||||
|
let statusText = '';
|
||||||
|
let statusValue = '';
|
||||||
|
|
||||||
|
if (typeof message.content === 'object') {
|
||||||
|
statusText = `Status: ${message.content.status}`;
|
||||||
|
statusValue = message.content.status;
|
||||||
|
} else {
|
||||||
|
statusText = `Status: ${message.content}`;
|
||||||
|
statusValue = message.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{ type: 'system', content: statusText }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// If status contains "completed" or "failed", stop running
|
||||||
|
if (statusValue.includes('completed') || statusValue.includes('failed')) {
|
||||||
|
console.log('Execution completed or failed, stopping');
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsRunning(false);
|
||||||
|
}, 500); // Small delay to ensure UI updates properly
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'error':
|
||||||
|
let errorContent = '';
|
||||||
|
if (typeof message.content === 'object' && message.content.message) {
|
||||||
|
errorContent = message.content.message;
|
||||||
|
} else {
|
||||||
|
errorContent = String(message.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{ type: 'error', content: errorContent }
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('Error received, enabling buttons');
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsRunning(false);
|
||||||
|
}, 500); // Small delay to ensure UI updates properly
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'system':
|
||||||
|
const systemContent = String(message.content);
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{ type: 'system', content: systemContent }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check for connection closing message which indicates execution is complete
|
||||||
|
if (systemContent.includes('Connection will close') ||
|
||||||
|
systemContent.includes('completed successfully') ||
|
||||||
|
systemContent.includes('Execution completed')) {
|
||||||
|
console.log('System message indicates completion, enabling buttons');
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsRunning(false);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Handle any other message types or direct string content
|
||||||
|
console.log('Unknown message type:', message);
|
||||||
|
if (typeof message === 'object') {
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{ type: 'output', content: JSON.stringify(message) }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error parsing WebSocket message:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = (error) => {
|
||||||
|
console.error('WebSocket error:', error);
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{ type: 'error', content: 'WebSocket connection error' }
|
||||||
|
]);
|
||||||
|
|
||||||
|
console.log('WebSocket error, enabling buttons');
|
||||||
|
setTimeout(() => {
|
||||||
|
setIsRunning(false);
|
||||||
|
}, 500); // Small delay to ensure UI updates properly
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = () => {
|
||||||
|
console.log('WebSocket connection closed');
|
||||||
|
setActiveSocket(null);
|
||||||
|
|
||||||
|
// Ensure buttons are re-enabled when the connection closes
|
||||||
|
setTimeout(() => {
|
||||||
|
resetExecutionState();
|
||||||
|
}, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Set the socket reference early to ensure we can clean it up if needed
|
||||||
|
socketRef.current = socket;
|
||||||
|
return socket;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle code execution
|
||||||
|
const runCode = async () => {
|
||||||
|
console.log('Run button clicked, current state:', {
|
||||||
|
isRunning,
|
||||||
|
socketState: activeSocket ? activeSocket.readyState : 'no socket',
|
||||||
|
socketRefState: socketRef.current ? socketRef.current.readyState : 'no socket ref'
|
||||||
|
});
|
||||||
|
|
||||||
|
// First make sure previous connections are fully closed
|
||||||
|
resetExecutionState();
|
||||||
|
|
||||||
|
// Increase the delay to ensure clean state before starting new execution
|
||||||
|
setTimeout(async () => {
|
||||||
|
// Double-check socket state before proceeding
|
||||||
|
if (activeSocket || socketRef.current) {
|
||||||
|
console.warn('Socket still exists after reset, forcing cleanup');
|
||||||
|
if (activeSocket && activeSocket.readyState !== WebSocket.CLOSED) {
|
||||||
|
activeSocket.close();
|
||||||
|
}
|
||||||
|
if (socketRef.current && socketRef.current.readyState !== WebSocket.CLOSED) {
|
||||||
|
socketRef.current.close();
|
||||||
|
}
|
||||||
|
socketRef.current = null;
|
||||||
|
setActiveSocket(null);
|
||||||
|
|
||||||
|
// Extra delay to ensure socket is fully closed
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsRunning(true);
|
||||||
|
setTerminalOutput([
|
||||||
|
{ type: 'system', content: `Running ${problems[activeQuestion].id}...` }
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Submit code to the backend
|
||||||
|
const response = await fetch('http://localhost:8080/api/submit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
code: code,
|
||||||
|
language: getLanguageIdentifier(language),
|
||||||
|
input: '',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setSubmissionId(data.id);
|
||||||
|
|
||||||
|
// Connect to WebSocket for real-time updates
|
||||||
|
connectToWebSocket(data.id);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error submitting code:', error);
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{ type: 'error', content: `Error: ${error.message}` }
|
||||||
|
]);
|
||||||
|
|
||||||
|
resetExecutionState();
|
||||||
|
}
|
||||||
|
}, 200); // Increased delay to ensure clean state
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle code submission
|
||||||
|
const submitCode = async () => {
|
||||||
|
setIsRunning(true);
|
||||||
|
setTerminalOutput([
|
||||||
|
{ type: 'system', content: `Submitting solution for ${problems[activeQuestion].id}...` }
|
||||||
|
]);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Submit code to the backend
|
||||||
|
const response = await fetch('http://localhost:8080/api/submit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
code: code,
|
||||||
|
language: getLanguageIdentifier(language),
|
||||||
|
input: '',
|
||||||
|
problemId: problems[activeQuestion].id
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Error: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
setSubmissionId(data.id);
|
||||||
|
|
||||||
|
// Connect to WebSocket for real-time updates
|
||||||
|
connectToWebSocket(data.id);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error submitting solution:', error);
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{ type: 'error', content: `Error: ${error.message}` }
|
||||||
|
]);
|
||||||
|
setIsRunning(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Render the current problem
|
||||||
|
const renderProblem = () => {
|
||||||
|
const problem = problems[activeQuestion];
|
||||||
|
if (!problem) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="problem-container">
|
||||||
|
<h1>{problem.title}</h1>
|
||||||
|
|
||||||
|
<div className="problem-description">
|
||||||
|
<p>{problem.description}</p>
|
||||||
|
{problem.constraints && <p>{problem.constraints}</p>}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Test cases section removed */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add this useEffect to monitor socket state
|
||||||
|
useEffect(() => {
|
||||||
|
// If we have an active socket but aren't running, we should clean up
|
||||||
|
if (activeSocket && !isRunning) {
|
||||||
|
console.log('Cleaning up inactive socket');
|
||||||
|
if (activeSocket.readyState === WebSocket.OPEN) {
|
||||||
|
activeSocket.close();
|
||||||
|
}
|
||||||
|
setActiveSocket(null);
|
||||||
|
}
|
||||||
|
}, [activeSocket, isRunning]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="code-challenge-container">
|
||||||
|
<header className="code-challenge-header">
|
||||||
|
<h1>OnScreen Test</h1>
|
||||||
|
<button className="sign-in-btn">Sign In</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* <div className="code-challenge-problem-nav">
|
||||||
|
<h3 className="problem-number">1. {problems["Q.1"].title}</h3>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
<div className="code-challenge-main">
|
||||||
|
<div className="problem-tabs">
|
||||||
|
<button
|
||||||
|
className={activeQuestion === "Q.1" ? "tab-active" : ""}
|
||||||
|
onClick={() => setActiveQuestion("Q.1")}
|
||||||
|
>
|
||||||
|
Q.1
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={activeQuestion === "Q.2" ? "tab-active" : ""}
|
||||||
|
onClick={() => setActiveQuestion("Q.2")}
|
||||||
|
>
|
||||||
|
Q.2
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={activeQuestion === "Q.3" ? "tab-active" : ""}
|
||||||
|
onClick={() => setActiveQuestion("Q.3")}
|
||||||
|
>
|
||||||
|
Q.3
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="problem-content">
|
||||||
|
{renderProblem()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="editor-section">
|
||||||
|
<div className="editor-header">
|
||||||
|
<div className="editor-controls">
|
||||||
|
<select
|
||||||
|
value={language}
|
||||||
|
onChange={(e) => setLanguage(e.target.value)}
|
||||||
|
className="language-selector"
|
||||||
|
>
|
||||||
|
<option value="JavaScript">JavaScript</option>
|
||||||
|
<option value="Python">Python</option>
|
||||||
|
<option value="Java">Java</option>
|
||||||
|
<option value="C++">C++</option>
|
||||||
|
<option value="C">C</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="editor-actions">
|
||||||
|
<button
|
||||||
|
className="run-btn"
|
||||||
|
onClick={runCode}
|
||||||
|
disabled={isRunning}
|
||||||
|
title={isRunning ? "Code execution in progress..." : "Run code"}
|
||||||
|
>
|
||||||
|
{isRunning ? (
|
||||||
|
<>
|
||||||
|
<span className="loading-spinner"></span> Running...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Play size={16} /> Run
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
className="submit-btn"
|
||||||
|
onClick={submitCode}
|
||||||
|
disabled={isRunning}
|
||||||
|
title={isRunning ? "Code execution in progress..." : "Submit solution"}
|
||||||
|
>
|
||||||
|
{isRunning ? (
|
||||||
|
<>
|
||||||
|
<span className="loading-spinner"></span> Submitting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Send size={16} /> Submit
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="editor-container">
|
||||||
|
<Editor
|
||||||
|
height="100%"
|
||||||
|
defaultLanguage="python"
|
||||||
|
language={language.toLowerCase() === 'c++' ? 'cpp' : language.toLowerCase()}
|
||||||
|
value={code}
|
||||||
|
onChange={(value) => setCode(value)}
|
||||||
|
theme="hc-black"
|
||||||
|
options={{
|
||||||
|
fontSize: 14,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
automaticLayout: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="terminal-section">
|
||||||
|
<div className="terminal-header">
|
||||||
|
<span>Terminal</span>
|
||||||
|
{/* <div className="terminal-controls">
|
||||||
|
<button className="terminal-btn">⊞</button>
|
||||||
|
<button className="terminal-btn">□</button>
|
||||||
|
<button className="terminal-btn">✕</button>
|
||||||
|
</div> */}
|
||||||
|
</div>
|
||||||
|
<div className="terminal-content">
|
||||||
|
{terminalOutput.map((line, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`terminal-line ${line.type}`}
|
||||||
|
>
|
||||||
|
{line.content}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="terminal-prompt">
|
||||||
|
<span className="prompt-symbol">$</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="terminal-input"
|
||||||
|
placeholder="Type here..."
|
||||||
|
disabled={!isRunning}
|
||||||
|
// Update the ref callback
|
||||||
|
ref={(inputEl) => {
|
||||||
|
// Auto-focus input when isRunning changes to true
|
||||||
|
if (inputEl && isRunning) {
|
||||||
|
inputEl.focus();
|
||||||
|
// Clear any previous input
|
||||||
|
inputEl.value = '';
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => { // Change from onKeyPress to onKeyDown for better cross-browser support
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault(); // Prevent default to avoid form submissions
|
||||||
|
const input = e.target.value.trim();
|
||||||
|
|
||||||
|
if (!input) return; // Skip empty input
|
||||||
|
|
||||||
|
if (activeSocket && activeSocket.readyState === WebSocket.OPEN) {
|
||||||
|
try {
|
||||||
|
// Send input to server
|
||||||
|
activeSocket.send(JSON.stringify({
|
||||||
|
"type": "input",
|
||||||
|
"content": input
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Add input to terminal output
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{ type: 'system', content: `$ ${input}` }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Clear the input field
|
||||||
|
e.target.value = '';
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending input:", error);
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{ type: 'error', content: `Failed to send input: ${error.message}` }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Better error message with socket state information
|
||||||
|
const socketState = activeSocket ?
|
||||||
|
['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'][activeSocket.readyState] :
|
||||||
|
'NO_SOCKET';
|
||||||
|
|
||||||
|
console.log(`Cannot send input: Socket state is ${socketState}`);
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{ type: 'error', content: `Cannot send input: connection not available (${socketState})` }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.key === 'Enter' && activeSocket && activeSocket.readyState === WebSocket.OPEN) {
|
||||||
|
const input = e.target.value;
|
||||||
|
// Send input to WebSocket with the correct format
|
||||||
|
try {
|
||||||
|
activeSocket.send(JSON.stringify({
|
||||||
|
"type": "input",
|
||||||
|
"content": input
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Add input to terminal output
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{ type: 'system', content: `$ ${input}` }
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Clear the input field
|
||||||
|
e.target.value = '';
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error sending input:", error);
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{ type: 'error', content: `Failed to send input: ${error.message}` }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
// Inform user if socket isn't available
|
||||||
|
if (!activeSocket || activeSocket.readyState !== WebSocket.OPEN) {
|
||||||
|
setTerminalOutput(prev => [
|
||||||
|
...prev,
|
||||||
|
{ type: 'error', content: `Cannot send input: connection closed` }
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CodeChallenge;
|
||||||
@@ -136,11 +136,6 @@ const EditorArea = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem("vscode-clone-files", JSON.stringify(files));
|
localStorage.setItem("vscode-clone-files", JSON.stringify(files));
|
||||||
}, [files]);
|
}, [files]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem("vscode-clone-structure", JSON.stringify(fileStructure));
|
|
||||||
}, [fileStructure]);
|
|
||||||
|
|
||||||
// Add this effect to handle editor resize when sidebar changes
|
// Add this effect to handle editor resize when sidebar changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Force editor to readjust layout when sidebar visibility changes
|
// Force editor to readjust layout when sidebar visibility changes
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
:root {
|
:root {
|
||||||
--vscode-background: #1e1e1e;
|
--vscode-background: #000000;
|
||||||
--vscode-foreground: #d4d4d4;
|
--vscode-foreground: #d4d4d4;
|
||||||
--vscode-activityBar-background: #333333;
|
--vscode-activityBar-background: #333333;
|
||||||
--vscode-activityBar-foreground: #ffffff;
|
--vscode-activityBar-foreground: #ffffff;
|
||||||
--vscode-activityBar-inactiveForeground: #ffffff80;
|
--vscode-activityBar-inactiveForeground: #ffffff80;
|
||||||
--vscode-sideBar-background: #252526;
|
--vscode-sideBar-background: #252526;
|
||||||
--vscode-sideBar-foreground: #cccccc;
|
--vscode-sideBar-foreground: #cccccc;
|
||||||
--vscode-editor-background: #1e1e1e;
|
--vscode-editor-background: #000000;
|
||||||
--vscode-statusBar-background: #007acc;
|
--vscode-statusBar-background: #007acc;
|
||||||
--vscode-statusBar-foreground: #ffffff;
|
--vscode-statusBar-foreground: #ffffff;
|
||||||
--vscode-panel-background: #1e1e1e;
|
--vscode-panel-background: #000000;
|
||||||
--vscode-panel-border: #80808059;
|
--vscode-panel-border: #80808059;
|
||||||
--vscode-tab-activeBackground: #1e1e1e;
|
--vscode-tab-activeBackground: #000000;
|
||||||
--vscode-tab-inactiveBackground: #2d2d2d;
|
--vscode-tab-inactiveBackground: #2d2d2d;
|
||||||
--vscode-tab-activeForeground: #ffffff;
|
--vscode-tab-activeForeground: #ffffff;
|
||||||
--vscode-tab-inactiveForeground: #ffffff80;
|
--vscode-tab-inactiveForeground: #ffffff80;
|
||||||
@@ -422,15 +422,36 @@ body {
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
height: calc(100% - 36px); /* Adjust based on your header height */
|
height: calc(100% - 36px); /* Adjust based on your header height */
|
||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
outline: none; /* Remove focus outline */
|
outline: none; /* Remove focus outline */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #555555 #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-terminal::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-terminal::-webkit-scrollbar-track {
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-terminal::-webkit-scrollbar-thumb {
|
||||||
|
background: #555555;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-terminal::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #666666;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-terminal .terminal-line {
|
.panel-terminal .terminal-line {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
|
word-wrap: break-word; /* Ensure long words don't cause horizontal overflow */
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-line {
|
.terminal-line {
|
||||||
@@ -1026,3 +1047,408 @@ body {
|
|||||||
.panel-close-btn:hover {
|
.panel-close-btn:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Code Challenge Component Styles */
|
||||||
|
.code-challenge-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: var(--vscode-background);
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-challenge-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background-color: var(--vscode-background);
|
||||||
|
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-challenge-header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 25px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-in-btn {
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
border: 1px solid rgba(128, 128, 128, 0.5);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-in-btn:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-challenge-problem-nav {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-number {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-challenge-main {
|
||||||
|
display: flex;
|
||||||
|
height: 60vh;
|
||||||
|
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 80px;
|
||||||
|
border-right: 1px solid rgba(128, 128, 128, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-tabs button {
|
||||||
|
padding: 16px;
|
||||||
|
background-color: transparent;
|
||||||
|
color: var(--vscode-foreground);
|
||||||
|
border: none;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid rgba(128, 128, 128, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-tabs button.tab-active {
|
||||||
|
background-color: var(--vscode-background);
|
||||||
|
font-weight: 500;
|
||||||
|
border-left: 2px solid #007acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-tabs button:hover:not(.tab-active) {
|
||||||
|
background-color: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16px;
|
||||||
|
border-right: 1px solid rgba(128, 128, 128, 0.35);
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-container h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 22px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-description {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.problem-examples h2 {
|
||||||
|
font-size: 16px;
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-box {
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-section {
|
||||||
|
width: 50%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background-color: #252526;
|
||||||
|
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.language-selector {
|
||||||
|
background-color: #3c3c3c;
|
||||||
|
color: #d4d4d4;
|
||||||
|
border: 1px solid #3c3c3c;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-btn {
|
||||||
|
background-color: #3c3c3c;
|
||||||
|
color: #d4d4d4;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.auto-selected {
|
||||||
|
background-color: #4d4d4d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background-color: #3c3c3c;
|
||||||
|
color: #d4d4d4;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
background-color: #0e639c;
|
||||||
|
color: #ffffff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.run-btn:hover {
|
||||||
|
background-color: #4d4d4d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submit-btn:hover {
|
||||||
|
background-color: #1177bb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-container {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-section {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--vscode-panel-background);
|
||||||
|
padding-bottom: 30px; /* Reduced padding to keep buttons just above footer */
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px 12px;
|
||||||
|
background-color: #252526;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-btn {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #d4d4d4;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
max-height: calc(100% - 10px); /* Ensure content doesn't expand beyond container */
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: #555555 #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-content::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-content::-webkit-scrollbar-track {
|
||||||
|
background: #1e1e1e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-content::-webkit-scrollbar-thumb {
|
||||||
|
background: #555555;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-line {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-line.system {
|
||||||
|
color: #569cd6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-line.error {
|
||||||
|
color: #f48771;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-prompt {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-symbol {
|
||||||
|
color: #569cd6;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.terminal-input {
|
||||||
|
background-color: transparent;
|
||||||
|
color: #d4d4d4;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
flex: 1;
|
||||||
|
font-family: 'Consolas', 'Monaco', monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer Styles */
|
||||||
|
.fixed {
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-0 {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-0 {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-0 {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-t {
|
||||||
|
border-top-width: 1px;
|
||||||
|
border-top-style: solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-slate-200\/40 {
|
||||||
|
border-color: rgba(226, 232, 240, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark\:border-gray-800\/20 {
|
||||||
|
border-color: rgba(31, 41, 55, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-white {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark\:bg-\[\#070c1f\] {
|
||||||
|
background-color: #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bar {
|
||||||
|
background-color: #000000;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.items-center {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.justify-center {
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h-7 {
|
||||||
|
height: 1.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-slate-500 {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark\:text-gray-500 {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-slate-400 {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark\:text-gray-400 {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-red-400 {
|
||||||
|
color: #f87171;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark\:text-red-500 {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mx-0\.5 {
|
||||||
|
margin-left: 0.125rem;
|
||||||
|
margin-right: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make sure the footer appears on top of other elements */
|
||||||
|
footer {
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
74
new-backend/Dockerfile.tunnel
Normal file
74
new-backend/Dockerfile.tunnel
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
FROM golang:1.19-alpine AS builder
|
||||||
|
|
||||||
|
# Install git and required dependencies
|
||||||
|
RUN apk update && apk add --no-cache git
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy go mod and sum files
|
||||||
|
COPY go.mod go.sum* ./
|
||||||
|
|
||||||
|
# Download dependencies
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application with optimizations
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o monaco-backend .
|
||||||
|
|
||||||
|
# Final stage
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Install Docker client and supervisor
|
||||||
|
RUN apk update && apk add --no-cache docker-cli supervisor wget
|
||||||
|
|
||||||
|
# Get cloudflared directly from GitHub (more reliable than the tarball)
|
||||||
|
RUN wget -O cloudflared https://github.com/cloudflare/cloudflared/releases/download/2023.5.0/cloudflared-linux-amd64 && \
|
||||||
|
chmod +x cloudflared && \
|
||||||
|
mv cloudflared /usr/local/bin/
|
||||||
|
|
||||||
|
# Create directories for cloudflared
|
||||||
|
RUN mkdir -p /etc/cloudflared
|
||||||
|
|
||||||
|
# Copy the certificate file and config
|
||||||
|
COPY cert.pem /etc/cloudflared/cert.pem
|
||||||
|
COPY credentials.json /etc/cloudflared/credentials.json
|
||||||
|
COPY config.json /etc/cloudflared/config.json
|
||||||
|
|
||||||
|
# Setup DNS routing for the tunnel (only needs to be done once)
|
||||||
|
RUN cloudflared tunnel route dns 5d2682ef-0b5b-47e5-b0fa-ad48968ce016 api.ishikabhoyar.tech || echo "DNS routing already set up or failed - continuing anyway"
|
||||||
|
|
||||||
|
# Copy the binary from builder
|
||||||
|
COPY --from=builder /app/monaco-backend /monaco-backend
|
||||||
|
|
||||||
|
# Create supervisord config
|
||||||
|
RUN mkdir -p /etc/supervisor/conf.d/
|
||||||
|
RUN echo "[supervisord]" > /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "nodaemon=true" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "user=root" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "[program:backend]" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "command=/monaco-backend" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "autostart=true" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "autorestart=true" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "stdout_logfile=/dev/stdout" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "stdout_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "stderr_logfile=/dev/stderr" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "stderr_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "[program:cloudflared]" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "command=cloudflared tunnel --config /etc/cloudflared/config.json run" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "autostart=true" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "autorestart=true" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "stdout_logfile=/dev/stdout" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "stdout_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "stderr_logfile=/dev/stderr" >> /etc/supervisor/conf.d/supervisord.conf && \
|
||||||
|
echo "stderr_logfile_maxbytes=0" >> /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
|
||||||
|
# Expose port for local access
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Use supervisord to manage processes
|
||||||
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||||
66
new-backend/Dockerfile.tunnel.new
Normal file
66
new-backend/Dockerfile.tunnel.new
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
FROM golang:1.19-alpine AS builder
|
||||||
|
|
||||||
|
# Install git and required dependencies
|
||||||
|
RUN apk update && apk add --no-cache git
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy go mod and sum files
|
||||||
|
COPY go.mod go.sum* ./
|
||||||
|
|
||||||
|
# Download dependencies
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application with optimizations
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o monaco-backend .
|
||||||
|
|
||||||
|
# Final stage
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# Install Docker client and tools
|
||||||
|
RUN apk update && apk add --no-cache docker-cli bash wget
|
||||||
|
|
||||||
|
# Get cloudflared directly
|
||||||
|
RUN wget -O cloudflared https://github.com/cloudflare/cloudflared/releases/download/2023.7.3/cloudflared-linux-amd64 && \
|
||||||
|
chmod +x cloudflared && \
|
||||||
|
mv cloudflared /usr/local/bin/
|
||||||
|
|
||||||
|
# Create directories for cloudflared
|
||||||
|
RUN mkdir -p /etc/cloudflared
|
||||||
|
|
||||||
|
# Copy the certificate file and config
|
||||||
|
COPY cert.pem /etc/cloudflared/cert.pem
|
||||||
|
COPY config.json /etc/cloudflared/config.json
|
||||||
|
|
||||||
|
# Copy the binary from builder
|
||||||
|
COPY --from=builder /app/monaco-backend /monaco-backend
|
||||||
|
|
||||||
|
# Create a simple bash script to run both services
|
||||||
|
RUN echo '#!/bin/bash\n\
|
||||||
|
# Start the backend in the background\n\
|
||||||
|
/monaco-backend &\n\
|
||||||
|
BACKEND_PID=$!\n\
|
||||||
|
echo "Backend started with PID: $BACKEND_PID"\n\
|
||||||
|
\n\
|
||||||
|
# Wait for backend to start\n\
|
||||||
|
echo "Waiting for backend to initialize..."\n\
|
||||||
|
sleep 5\n\
|
||||||
|
\n\
|
||||||
|
# Start cloudflared with proper arguments\n\
|
||||||
|
echo "Starting Cloudflare tunnel to api.ishikabhoyar.tech..."\n\
|
||||||
|
# Use the specific tunnel name from config.json\n\
|
||||||
|
cloudflared tunnel run monaco-backend-tunnel\n\
|
||||||
|
\n\
|
||||||
|
# If cloudflared exits, kill the backend\n\
|
||||||
|
kill $BACKEND_PID\n\
|
||||||
|
' > /start.sh && chmod +x /start.sh
|
||||||
|
|
||||||
|
# Expose port for local access
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Run the startup script
|
||||||
|
CMD ["/start.sh"]
|
||||||
21
new-backend/Makefile
Normal file
21
new-backend/Makefile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
.PHONY: build run run-detached stop logs
|
||||||
|
|
||||||
|
# Build the image
|
||||||
|
build:
|
||||||
|
docker-compose -f docker-compose.tunnel.yml build
|
||||||
|
|
||||||
|
# Run the container
|
||||||
|
run: build
|
||||||
|
docker-compose -f docker-compose.tunnel.yml up
|
||||||
|
|
||||||
|
# Run in detached mode
|
||||||
|
run-detached: build
|
||||||
|
docker-compose -f docker-compose.tunnel.yml up -d
|
||||||
|
|
||||||
|
# Stop the container
|
||||||
|
stop:
|
||||||
|
docker-compose -f docker-compose.tunnel.yml down
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
logs:
|
||||||
|
docker-compose -f docker-compose.tunnel.yml logs -f
|
||||||
46
new-backend/README.tunnel.md
Normal file
46
new-backend/README.tunnel.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Backend with Cloudflare Tunnel
|
||||||
|
|
||||||
|
This setup runs the Monaco backend service and establishes a Cloudflare tunnel, exposing the service to the internet securely via api.ishikabhoyar.tech.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker and Docker Compose installed
|
||||||
|
- The Cloudflare tunnel certificate (cert.pem) in the same directory as the Dockerfile.tunnel
|
||||||
|
|
||||||
|
## Files
|
||||||
|
|
||||||
|
- `Dockerfile.tunnel`: Dockerfile that builds the backend and sets up Cloudflare tunnel
|
||||||
|
- `cert.pem`: Cloudflare tunnel certificate
|
||||||
|
- `config.json`: Cloudflare tunnel configuration that routes traffic to api.ishikabhoyar.tech
|
||||||
|
- `docker-compose.tunnel.yml`: Docker Compose configuration for easy deployment
|
||||||
|
|
||||||
|
## How to Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build and start the container
|
||||||
|
docker-compose -f docker-compose.tunnel.yml up -d
|
||||||
|
|
||||||
|
# Check logs
|
||||||
|
docker-compose -f docker-compose.tunnel.yml logs -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## How it Works
|
||||||
|
|
||||||
|
1. The Dockerfile builds the Go backend application
|
||||||
|
2. It installs the Cloudflare tunnel client (cloudflared)
|
||||||
|
3. On container start:
|
||||||
|
- The backend server starts on port 8080
|
||||||
|
- The Cloudflare tunnel connects to Cloudflare's edge network using the config.json
|
||||||
|
- External traffic to api.ishikabhoyar.tech is routed through the tunnel to the backend
|
||||||
|
- The cloudflared runs entirely within the container, isolated from any host cloudflared instance
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
You can customize the behavior by modifying the environment variables in the docker-compose.tunnel.yml file:
|
||||||
|
|
||||||
|
- `PORT`: The port the backend server listens on (default: 8080)
|
||||||
|
- `CONCURRENT_EXECUTIONS`: Number of concurrent code executions (default: 5)
|
||||||
|
- `QUEUE_CAPACITY`: Maximum queue capacity for code executions (default: 100)
|
||||||
|
- `DEFAULT_TIMEOUT`: Default timeout for code execution in seconds (default: 30)
|
||||||
|
- `SANDBOX_NETWORK_DISABLED`: Whether to disable network in sandbox containers (default: true)
|
||||||
|
- `SANDBOX_PIDS_LIMIT`: Process ID limit for sandbox containers (default: 50)
|
||||||
15
new-backend/config.json
Normal file
15
new-backend/config.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"tunnel": "5d2682ef-0b5b-47e5-b0fa-ad48968ce016",
|
||||||
|
"credentials-file": "/etc/cloudflared/credentials.json",
|
||||||
|
"ingress": [
|
||||||
|
{
|
||||||
|
"hostname": "api.ishikabhoyar.tech",
|
||||||
|
"service": "http://localhost:8080"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http_status:404"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"protocol": "http2",
|
||||||
|
"loglevel": "info"
|
||||||
|
}
|
||||||
13
new-backend/config.json.new
Normal file
13
new-backend/config.json.new
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"tunnel": "monaco-backend-tunnel",
|
||||||
|
"credentials-file": "/etc/cloudflared/cert.pem",
|
||||||
|
"ingress": [
|
||||||
|
{
|
||||||
|
"hostname": "api.ishikabhoyar.tech",
|
||||||
|
"service": "http://localhost:8080"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"service": "http_status:404"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -8,10 +8,10 @@ import (
|
|||||||
|
|
||||||
// Config holds all configuration for the application
|
// Config holds all configuration for the application
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server ServerConfig
|
Server ServerConfig
|
||||||
Executor ExecutorConfig
|
Executor ExecutorConfig
|
||||||
Languages map[string]LanguageConfig
|
Languages map[string]LanguageConfig
|
||||||
Sandbox SandboxConfig
|
Sandbox SandboxConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConfig holds server-related configurations
|
// ServerConfig holds server-related configurations
|
||||||
@@ -31,15 +31,15 @@ type ExecutorConfig struct {
|
|||||||
|
|
||||||
// LanguageConfig holds language-specific configurations
|
// LanguageConfig holds language-specific configurations
|
||||||
type LanguageConfig struct {
|
type LanguageConfig struct {
|
||||||
Name string
|
Name string
|
||||||
Image string
|
Image string
|
||||||
MemoryLimit string
|
MemoryLimit string
|
||||||
CPULimit string
|
CPULimit string
|
||||||
TimeoutSec int
|
TimeoutSec int
|
||||||
CompileCmd []string
|
CompileCmd []string
|
||||||
RunCmd []string
|
RunCmd []string
|
||||||
FileExt string
|
FileExt string
|
||||||
VersionCmd []string
|
VersionCmd []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// SandboxConfig holds sandbox-related configurations
|
// SandboxConfig holds sandbox-related configurations
|
||||||
@@ -56,11 +56,11 @@ func GetConfig() *Config {
|
|||||||
Port: getEnv("PORT", "8080"),
|
Port: getEnv("PORT", "8080"),
|
||||||
ReadTimeout: time.Duration(getEnvAsInt("READ_TIMEOUT", 15)) * time.Second,
|
ReadTimeout: time.Duration(getEnvAsInt("READ_TIMEOUT", 15)) * time.Second,
|
||||||
WriteTimeout: time.Duration(getEnvAsInt("WRITE_TIMEOUT", 15)) * time.Second,
|
WriteTimeout: time.Duration(getEnvAsInt("WRITE_TIMEOUT", 15)) * time.Second,
|
||||||
IdleTimeout: time.Duration(getEnvAsInt("IDLE_TIMEOUT", 60)) * time.Second,
|
IdleTimeout: time.Duration(getEnvAsInt("IDLE_TIMEOUT", 90)) * time.Second,
|
||||||
},
|
},
|
||||||
Executor: ExecutorConfig{
|
Executor: ExecutorConfig{
|
||||||
ConcurrentExecutions: getEnvAsInt("CONCURRENT_EXECUTIONS", 5),
|
ConcurrentExecutions: getEnvAsInt("CONCURRENT_EXECUTIONS", 100),
|
||||||
QueueCapacity: getEnvAsInt("QUEUE_CAPACITY", 100),
|
QueueCapacity: getEnvAsInt("QUEUE_CAPACITY", 1000),
|
||||||
DefaultTimeout: time.Duration(getEnvAsInt("DEFAULT_TIMEOUT", 30)) * time.Second,
|
DefaultTimeout: time.Duration(getEnvAsInt("DEFAULT_TIMEOUT", 30)) * time.Second,
|
||||||
},
|
},
|
||||||
Languages: getLanguageConfigs(),
|
Languages: getLanguageConfigs(),
|
||||||
@@ -80,7 +80,7 @@ func getLanguageConfigs() map[string]LanguageConfig {
|
|||||||
Image: "python:3.9-slim",
|
Image: "python:3.9-slim",
|
||||||
MemoryLimit: "100m",
|
MemoryLimit: "100m",
|
||||||
CPULimit: "0.1",
|
CPULimit: "0.1",
|
||||||
TimeoutSec: 30,
|
TimeoutSec: 90,
|
||||||
RunCmd: []string{"python", "-c"},
|
RunCmd: []string{"python", "-c"},
|
||||||
FileExt: ".py",
|
FileExt: ".py",
|
||||||
VersionCmd: []string{"python", "--version"},
|
VersionCmd: []string{"python", "--version"},
|
||||||
@@ -90,7 +90,7 @@ func getLanguageConfigs() map[string]LanguageConfig {
|
|||||||
Image: "eclipse-temurin:11-jdk",
|
Image: "eclipse-temurin:11-jdk",
|
||||||
MemoryLimit: "400m",
|
MemoryLimit: "400m",
|
||||||
CPULimit: "0.5",
|
CPULimit: "0.5",
|
||||||
TimeoutSec: 60,
|
TimeoutSec: 100,
|
||||||
CompileCmd: []string{"javac"},
|
CompileCmd: []string{"javac"},
|
||||||
RunCmd: []string{"java"},
|
RunCmd: []string{"java"},
|
||||||
FileExt: ".java",
|
FileExt: ".java",
|
||||||
@@ -101,7 +101,7 @@ func getLanguageConfigs() map[string]LanguageConfig {
|
|||||||
Image: "gcc:latest",
|
Image: "gcc:latest",
|
||||||
MemoryLimit: "100m",
|
MemoryLimit: "100m",
|
||||||
CPULimit: "0.1",
|
CPULimit: "0.1",
|
||||||
TimeoutSec: 30,
|
TimeoutSec: 90,
|
||||||
CompileCmd: []string{"gcc", "-o", "program"},
|
CompileCmd: []string{"gcc", "-o", "program"},
|
||||||
RunCmd: []string{"./program"},
|
RunCmd: []string{"./program"},
|
||||||
FileExt: ".c",
|
FileExt: ".c",
|
||||||
@@ -112,7 +112,7 @@ func getLanguageConfigs() map[string]LanguageConfig {
|
|||||||
Image: "gcc:latest",
|
Image: "gcc:latest",
|
||||||
MemoryLimit: "100m",
|
MemoryLimit: "100m",
|
||||||
CPULimit: "0.1",
|
CPULimit: "0.1",
|
||||||
TimeoutSec: 30,
|
TimeoutSec: 90,
|
||||||
CompileCmd: []string{"g++", "-o", "program"},
|
CompileCmd: []string{"g++", "-o", "program"},
|
||||||
RunCmd: []string{"./program"},
|
RunCmd: []string{"./program"},
|
||||||
FileExt: ".cpp",
|
FileExt: ".cpp",
|
||||||
@@ -123,7 +123,7 @@ func getLanguageConfigs() map[string]LanguageConfig {
|
|||||||
Image: "node:16-alpine",
|
Image: "node:16-alpine",
|
||||||
MemoryLimit: "100m",
|
MemoryLimit: "100m",
|
||||||
CPULimit: "0.1",
|
CPULimit: "0.1",
|
||||||
TimeoutSec: 30,
|
TimeoutSec: 90,
|
||||||
RunCmd: []string{"node", "-e"},
|
RunCmd: []string{"node", "-e"},
|
||||||
FileExt: ".js",
|
FileExt: ".js",
|
||||||
VersionCmd: []string{"node", "--version"},
|
VersionCmd: []string{"node", "--version"},
|
||||||
@@ -133,7 +133,7 @@ func getLanguageConfigs() map[string]LanguageConfig {
|
|||||||
Image: "golang:1.19-alpine",
|
Image: "golang:1.19-alpine",
|
||||||
MemoryLimit: "100m",
|
MemoryLimit: "100m",
|
||||||
CPULimit: "0.1",
|
CPULimit: "0.1",
|
||||||
TimeoutSec: 30,
|
TimeoutSec: 90,
|
||||||
CompileCmd: []string{"go", "build", "-o", "program"},
|
CompileCmd: []string{"go", "build", "-o", "program"},
|
||||||
RunCmd: []string{"./program"},
|
RunCmd: []string{"./program"},
|
||||||
FileExt: ".go",
|
FileExt: ".go",
|
||||||
|
|||||||
1
new-backend/credentials.json
Normal file
1
new-backend/credentials.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"AccountTag":"453afb9373a00a55876e6127cf0efd97","TunnelSecret":"02VClcBt+1nxxu8ioUzw/UizXtKKh4X7UUpneVbfB/Y=","TunnelID":"5d2682ef-0b5b-47e5-b0fa-ad48968ce016"}
|
||||||
28
new-backend/docker-compose.tunnel.yml
Normal file
28
new-backend/docker-compose.tunnel.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
services:
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.tunnel
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- //var/run/docker.sock:/var/run/docker.sock
|
||||||
|
# Port is only exposed locally, traffic comes through the tunnel
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8080:8080"
|
||||||
|
environment:
|
||||||
|
- PORT=8080
|
||||||
|
- CONCURRENT_EXECUTIONS=5
|
||||||
|
- QUEUE_CAPACITY=100
|
||||||
|
- DEFAULT_TIMEOUT=30
|
||||||
|
- SANDBOX_NETWORK_DISABLED=true
|
||||||
|
- SANDBOX_PIDS_LIMIT=50
|
||||||
|
# Define cloudflared environment variables
|
||||||
|
- TUNNEL_ORIGIN_CERT=/etc/cloudflared/cert.pem
|
||||||
|
- NO_AUTOUPDATE=true
|
||||||
|
# Isolated network to prevent conflicts with host cloudflared
|
||||||
|
networks:
|
||||||
|
- monaco-backend-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
monaco-backend-network:
|
||||||
|
driver: bridge
|
||||||
16
new-backend/start.sh
Normal file
16
new-backend/start.sh
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Start the backend
|
||||||
|
/monaco-backend &
|
||||||
|
BACKEND_PID=$!
|
||||||
|
echo "Backend started with PID: $BACKEND_PID"
|
||||||
|
|
||||||
|
# Wait for backend to start
|
||||||
|
echo "Waiting for backend to initialize..."
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Start cloudflared tunnel using config file
|
||||||
|
echo "Starting Cloudflare tunnel to api.ishikabhoyar.tech..."
|
||||||
|
cloudflared tunnel --no-autoupdate run --config /etc/cloudflared/config.json
|
||||||
|
|
||||||
|
# If cloudflared exits, kill the backend too
|
||||||
|
kill $BACKEND_PID
|
||||||
21
new-backend/supervisord.conf
Normal file
21
new-backend/supervisord.conf
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
user=root
|
||||||
|
|
||||||
|
[program:backend]
|
||||||
|
command=/monaco-backend
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
|
||||||
|
[program:cloudflared]
|
||||||
|
command=cloudflared tunnel --no-autoupdate run --config /etc/cloudflared/config.json
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
29
nginx.conf
Normal file
29
nginx.conf
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
events {}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types; # <-- ADD THIS LINE
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# Serve the static frontend files
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy API requests to the Go backend
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://localhost:8081;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
nginx/nginx.conf
Normal file
26
nginx/nginx.conf
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
events {}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types; # <-- ADD THIS LINE
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name localhost;
|
||||||
|
|
||||||
|
# Serve the static frontend files
|
||||||
|
location / {
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html index.htm;
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy API requests to the Go backend
|
||||||
|
location /api/ {
|
||||||
|
proxy_pass http://localhost:8080;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user