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>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" href="/public/favicon.ico" type="image/x-icon" />
|
||||
<title>VSCode</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
BIN
Frontend/public/favicon.ico
Normal file
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"
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<VSCodeUI />
|
||||
<CodeChallenge />
|
||||
<footer className="footer-bar fixed bottom-0 left-0 right-0 border-t border-slate-200/40 dark:border-gray-800/20 bg-black">
|
||||
<div className="flex items-center justify-center h-7">
|
||||
<span className="text-xs text-slate-400 dark:text-gray-400 flex items-center">
|
||||
Copyright © 2025. Made with
|
||||
♡ by Ishika and Arnab.
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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(() => {
|
||||
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
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
:root {
|
||||
--vscode-background: #1e1e1e;
|
||||
--vscode-background: #000000;
|
||||
--vscode-foreground: #d4d4d4;
|
||||
--vscode-activityBar-background: #333333;
|
||||
--vscode-activityBar-foreground: #ffffff;
|
||||
--vscode-activityBar-inactiveForeground: #ffffff80;
|
||||
--vscode-sideBar-background: #252526;
|
||||
--vscode-sideBar-foreground: #cccccc;
|
||||
--vscode-editor-background: #1e1e1e;
|
||||
--vscode-editor-background: #000000;
|
||||
--vscode-statusBar-background: #007acc;
|
||||
--vscode-statusBar-foreground: #ffffff;
|
||||
--vscode-panel-background: #1e1e1e;
|
||||
--vscode-panel-background: #000000;
|
||||
--vscode-panel-border: #80808059;
|
||||
--vscode-tab-activeBackground: #1e1e1e;
|
||||
--vscode-tab-activeBackground: #000000;
|
||||
--vscode-tab-inactiveBackground: #2d2d2d;
|
||||
--vscode-tab-activeForeground: #ffffff;
|
||||
--vscode-tab-inactiveForeground: #ffffff80;
|
||||
@@ -422,15 +422,36 @@ body {
|
||||
padding: 8px;
|
||||
font-family: monospace;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: calc(100% - 36px); /* Adjust based on your header height */
|
||||
background-color: #1e1e1e;
|
||||
color: #ddd;
|
||||
outline: none; /* Remove focus outline */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #555555 #1e1e1e;
|
||||
}
|
||||
|
||||
.panel-terminal::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.panel-terminal::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.panel-terminal::-webkit-scrollbar-thumb {
|
||||
background: #555555;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.panel-terminal::-webkit-scrollbar-thumb:hover {
|
||||
background: #666666;
|
||||
}
|
||||
|
||||
.panel-terminal .terminal-line {
|
||||
white-space: pre-wrap;
|
||||
margin-bottom: 3px;
|
||||
word-wrap: break-word; /* Ensure long words don't cause horizontal overflow */
|
||||
}
|
||||
|
||||
.terminal-line {
|
||||
@@ -1025,4 +1046,409 @@ body {
|
||||
|
||||
.panel-close-btn:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Code Challenge Component Styles */
|
||||
.code-challenge-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: var(--vscode-background);
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.code-challenge-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background-color: var(--vscode-background);
|
||||
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
|
||||
|
||||
}
|
||||
|
||||
.code-challenge-header h1 {
|
||||
margin: 0;
|
||||
font-size: 25px;
|
||||
font-weight: 400;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sign-in-btn {
|
||||
background-color: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
border: 1px solid rgba(128, 128, 128, 0.5);
|
||||
padding: 4px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sign-in-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.code-challenge-problem-nav {
|
||||
padding: 8px 16px;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
|
||||
}
|
||||
|
||||
.problem-number {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.code-challenge-main {
|
||||
display: flex;
|
||||
height: 60vh;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
|
||||
}
|
||||
|
||||
.problem-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 80px;
|
||||
border-right: 1px solid rgba(128, 128, 128, 0.35);
|
||||
}
|
||||
|
||||
.problem-tabs button {
|
||||
padding: 16px;
|
||||
background-color: transparent;
|
||||
color: var(--vscode-foreground);
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
.problem-tabs button.tab-active {
|
||||
background-color: var(--vscode-background);
|
||||
font-weight: 500;
|
||||
border-left: 2px solid #007acc;
|
||||
}
|
||||
|
||||
.problem-tabs button:hover:not(.tab-active) {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.problem-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
border-right: 1px solid rgba(128, 128, 128, 0.35);
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.problem-container h1 {
|
||||
margin-top: 0;
|
||||
font-size: 22px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.problem-description {
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.problem-examples h2 {
|
||||
font-size: 16px;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.example-box {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 12px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.editor-section {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
background-color: #252526;
|
||||
border-bottom: 1px solid rgba(128, 128, 128, 0.35);
|
||||
}
|
||||
|
||||
.editor-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.language-selector {
|
||||
background-color: #3c3c3c;
|
||||
color: #d4d4d4;
|
||||
border: 1px solid #3c3c3c;
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 13px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.auto-btn {
|
||||
background-color: #3c3c3c;
|
||||
color: #d4d4d4;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auto-selected {
|
||||
background-color: #4d4d4d;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.run-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background-color: #3c3c3c;
|
||||
color: #d4d4d4;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background-color: #0e639c;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 4px 12px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.run-btn:hover {
|
||||
background-color: #4d4d4d;
|
||||
}
|
||||
|
||||
.submit-btn:hover {
|
||||
background-color: #1177bb;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.terminal-section {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--vscode-panel-background);
|
||||
padding-bottom: 30px; /* Reduced padding to keep buttons just above footer */
|
||||
}
|
||||
|
||||
.terminal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 12px;
|
||||
background-color: #252526;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.terminal-controls {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.terminal-btn {
|
||||
background-color: transparent;
|
||||
color: #d4d4d4;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.terminal-content {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 14px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
white-space: pre-wrap;
|
||||
max-height: calc(100% - 10px); /* Ensure content doesn't expand beyond container */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #555555 #1e1e1e;
|
||||
}
|
||||
|
||||
.terminal-content::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.terminal-content::-webkit-scrollbar-track {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.terminal-content::-webkit-scrollbar-thumb {
|
||||
background: #555555;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.terminal-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #666666;
|
||||
}
|
||||
|
||||
.terminal-line {
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.terminal-line.system {
|
||||
color: #569cd6;
|
||||
}
|
||||
|
||||
.terminal-line.error {
|
||||
color: #f48771;
|
||||
}
|
||||
|
||||
.terminal-prompt {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.prompt-symbol {
|
||||
color: #569cd6;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.terminal-input {
|
||||
background-color: transparent;
|
||||
color: #d4d4d4;
|
||||
border: none;
|
||||
outline: none;
|
||||
flex: 1;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Footer Styles */
|
||||
.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.bottom-0 {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.left-0 {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.right-0 {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.border-t {
|
||||
border-top-width: 1px;
|
||||
border-top-style: solid;
|
||||
}
|
||||
|
||||
.border-slate-200\/40 {
|
||||
border-color: rgba(226, 232, 240, 0.4);
|
||||
}
|
||||
|
||||
.dark\:border-gray-800\/20 {
|
||||
border-color: rgba(31, 41, 55, 0.2);
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.dark\:bg-\[\#070c1f\] {
|
||||
background-color: #000000;
|
||||
}
|
||||
|
||||
.footer-bar {
|
||||
background-color: #000000;
|
||||
z-index: 1000;
|
||||
box-shadow: 0 -2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.h-7 {
|
||||
height: 1.75rem;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1rem;
|
||||
}
|
||||
|
||||
.text-slate-500 {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.dark\:text-gray-500 {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.text-slate-400 {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.dark\:text-gray-400 {
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.text-red-400 {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.dark\:text-red-500 {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.mx-0\.5 {
|
||||
margin-left: 0.125rem;
|
||||
margin-right: 0.125rem;
|
||||
}
|
||||
|
||||
/* Make sure the footer appears on top of other elements */
|
||||
footer {
|
||||
z-index: 1000;
|
||||
}
|
||||
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
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Executor ExecutorConfig
|
||||
Languages map[string]LanguageConfig
|
||||
Sandbox SandboxConfig
|
||||
Server ServerConfig
|
||||
Executor ExecutorConfig
|
||||
Languages map[string]LanguageConfig
|
||||
Sandbox SandboxConfig
|
||||
}
|
||||
|
||||
// ServerConfig holds server-related configurations
|
||||
@@ -31,15 +31,15 @@ type ExecutorConfig struct {
|
||||
|
||||
// LanguageConfig holds language-specific configurations
|
||||
type LanguageConfig struct {
|
||||
Name string
|
||||
Image string
|
||||
MemoryLimit string
|
||||
CPULimit string
|
||||
TimeoutSec int
|
||||
CompileCmd []string
|
||||
RunCmd []string
|
||||
FileExt string
|
||||
VersionCmd []string
|
||||
Name string
|
||||
Image string
|
||||
MemoryLimit string
|
||||
CPULimit string
|
||||
TimeoutSec int
|
||||
CompileCmd []string
|
||||
RunCmd []string
|
||||
FileExt string
|
||||
VersionCmd []string
|
||||
}
|
||||
|
||||
// SandboxConfig holds sandbox-related configurations
|
||||
@@ -56,11 +56,11 @@ func GetConfig() *Config {
|
||||
Port: getEnv("PORT", "8080"),
|
||||
ReadTimeout: time.Duration(getEnvAsInt("READ_TIMEOUT", 15)) * time.Second,
|
||||
WriteTimeout: time.Duration(getEnvAsInt("WRITE_TIMEOUT", 15)) * time.Second,
|
||||
IdleTimeout: time.Duration(getEnvAsInt("IDLE_TIMEOUT", 60)) * time.Second,
|
||||
IdleTimeout: time.Duration(getEnvAsInt("IDLE_TIMEOUT", 90)) * time.Second,
|
||||
},
|
||||
Executor: ExecutorConfig{
|
||||
ConcurrentExecutions: getEnvAsInt("CONCURRENT_EXECUTIONS", 5),
|
||||
QueueCapacity: getEnvAsInt("QUEUE_CAPACITY", 100),
|
||||
ConcurrentExecutions: getEnvAsInt("CONCURRENT_EXECUTIONS", 100),
|
||||
QueueCapacity: getEnvAsInt("QUEUE_CAPACITY", 1000),
|
||||
DefaultTimeout: time.Duration(getEnvAsInt("DEFAULT_TIMEOUT", 30)) * time.Second,
|
||||
},
|
||||
Languages: getLanguageConfigs(),
|
||||
@@ -80,7 +80,7 @@ func getLanguageConfigs() map[string]LanguageConfig {
|
||||
Image: "python:3.9-slim",
|
||||
MemoryLimit: "100m",
|
||||
CPULimit: "0.1",
|
||||
TimeoutSec: 30,
|
||||
TimeoutSec: 90,
|
||||
RunCmd: []string{"python", "-c"},
|
||||
FileExt: ".py",
|
||||
VersionCmd: []string{"python", "--version"},
|
||||
@@ -90,7 +90,7 @@ func getLanguageConfigs() map[string]LanguageConfig {
|
||||
Image: "eclipse-temurin:11-jdk",
|
||||
MemoryLimit: "400m",
|
||||
CPULimit: "0.5",
|
||||
TimeoutSec: 60,
|
||||
TimeoutSec: 100,
|
||||
CompileCmd: []string{"javac"},
|
||||
RunCmd: []string{"java"},
|
||||
FileExt: ".java",
|
||||
@@ -101,7 +101,7 @@ func getLanguageConfigs() map[string]LanguageConfig {
|
||||
Image: "gcc:latest",
|
||||
MemoryLimit: "100m",
|
||||
CPULimit: "0.1",
|
||||
TimeoutSec: 30,
|
||||
TimeoutSec: 90,
|
||||
CompileCmd: []string{"gcc", "-o", "program"},
|
||||
RunCmd: []string{"./program"},
|
||||
FileExt: ".c",
|
||||
@@ -112,7 +112,7 @@ func getLanguageConfigs() map[string]LanguageConfig {
|
||||
Image: "gcc:latest",
|
||||
MemoryLimit: "100m",
|
||||
CPULimit: "0.1",
|
||||
TimeoutSec: 30,
|
||||
TimeoutSec: 90,
|
||||
CompileCmd: []string{"g++", "-o", "program"},
|
||||
RunCmd: []string{"./program"},
|
||||
FileExt: ".cpp",
|
||||
@@ -123,7 +123,7 @@ func getLanguageConfigs() map[string]LanguageConfig {
|
||||
Image: "node:16-alpine",
|
||||
MemoryLimit: "100m",
|
||||
CPULimit: "0.1",
|
||||
TimeoutSec: 30,
|
||||
TimeoutSec: 90,
|
||||
RunCmd: []string{"node", "-e"},
|
||||
FileExt: ".js",
|
||||
VersionCmd: []string{"node", "--version"},
|
||||
@@ -133,7 +133,7 @@ func getLanguageConfigs() map[string]LanguageConfig {
|
||||
Image: "golang:1.19-alpine",
|
||||
MemoryLimit: "100m",
|
||||
CPULimit: "0.1",
|
||||
TimeoutSec: 30,
|
||||
TimeoutSec: 90,
|
||||
CompileCmd: []string{"go", "build", "-o", "program"},
|
||||
RunCmd: []string{"./program"},
|
||||
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