diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..fe5494b --- /dev/null +++ b/Dockerfile @@ -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;"' \ No newline at end of file diff --git a/Frontend/index.html b/Frontend/index.html index 238f04b..174b698 100644 --- a/Frontend/index.html +++ b/Frontend/index.html @@ -3,6 +3,7 @@ + VSCode diff --git a/Frontend/public/favicon.ico b/Frontend/public/favicon.ico new file mode 100644 index 0000000..b02b952 Binary files /dev/null and b/Frontend/public/favicon.ico differ diff --git a/Frontend/public/vite.svg b/Frontend/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/Frontend/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/Frontend/src/App.jsx b/Frontend/src/App.jsx index 8f8699c..8002bc3 100644 --- a/Frontend/src/App.jsx +++ b/Frontend/src/App.jsx @@ -1,10 +1,18 @@ -import VSCodeUI from "./components/VSCodeUI.jsx" +import CodeChallenge from "./components/CodeChallenge.jsx" import "./index.css" function App() { return (
- + +
) } diff --git a/Frontend/src/components/CodeChallenge.jsx b/Frontend/src/components/CodeChallenge.jsx new file mode 100644 index 0000000..d56bed5 --- /dev/null +++ b/Frontend/src/components/CodeChallenge.jsx @@ -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 +#include +#include + +// ${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 +#include +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 ( +
+

{problem.title}

+ +
+

{problem.description}

+ {problem.constraints &&

{problem.constraints}

} +
+ + {/* Test cases section removed */} +
+ ); + }; + + // 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 ( +
+
+

OnScreen Test

+ +
+ + {/*
+

1. {problems["Q.1"].title}

+
*/} + +
+
+ + + +
+ +
+ {renderProblem()} +
+ +
+
+
+ + + +
+ +
+ + + +
+
+ +
+ setCode(value)} + theme="hc-black" + options={{ + fontSize: 14, + minimap: { enabled: false }, + scrollBeyondLastLine: false, + automaticLayout: true, + }} + /> +
+
+
+ +
+
+ Terminal + {/*
+ + + +
*/} +
+
+ {terminalOutput.map((line, index) => ( +
+ {line.content} +
+ ))} +
+ $ + { + // 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` } + ]); + } + } + }} + /> +
+
+
+
+ ); +}; + +export default CodeChallenge; diff --git a/Frontend/src/components/EditorArea.jsx b/Frontend/src/components/EditorArea.jsx index cc88414..e0c7a50 100644 --- a/Frontend/src/components/EditorArea.jsx +++ b/Frontend/src/components/EditorArea.jsx @@ -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 diff --git a/Frontend/src/index.css b/Frontend/src/index.css index c762840..edf7378 100644 --- a/Frontend/src/index.css +++ b/Frontend/src/index.css @@ -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; } \ No newline at end of file diff --git a/new-backend/Dockerfile.tunnel b/new-backend/Dockerfile.tunnel new file mode 100644 index 0000000..fabbb01 --- /dev/null +++ b/new-backend/Dockerfile.tunnel @@ -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"] diff --git a/new-backend/Dockerfile.tunnel.new b/new-backend/Dockerfile.tunnel.new new file mode 100644 index 0000000..f9a7aee --- /dev/null +++ b/new-backend/Dockerfile.tunnel.new @@ -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"] diff --git a/new-backend/Makefile b/new-backend/Makefile new file mode 100644 index 0000000..b39dec5 --- /dev/null +++ b/new-backend/Makefile @@ -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 diff --git a/new-backend/README.tunnel.md b/new-backend/README.tunnel.md new file mode 100644 index 0000000..2a12762 --- /dev/null +++ b/new-backend/README.tunnel.md @@ -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) diff --git a/new-backend/config.json b/new-backend/config.json new file mode 100644 index 0000000..814bcd6 --- /dev/null +++ b/new-backend/config.json @@ -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" +} diff --git a/new-backend/config.json.new b/new-backend/config.json.new new file mode 100644 index 0000000..7846252 --- /dev/null +++ b/new-backend/config.json.new @@ -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" + } + ] +} diff --git a/new-backend/config/config.go b/new-backend/config/config.go index a893c6b..ae46dbe 100644 --- a/new-backend/config/config.go +++ b/new-backend/config/config.go @@ -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", diff --git a/new-backend/credentials.json b/new-backend/credentials.json new file mode 100644 index 0000000..922f3fd --- /dev/null +++ b/new-backend/credentials.json @@ -0,0 +1 @@ +{"AccountTag":"453afb9373a00a55876e6127cf0efd97","TunnelSecret":"02VClcBt+1nxxu8ioUzw/UizXtKKh4X7UUpneVbfB/Y=","TunnelID":"5d2682ef-0b5b-47e5-b0fa-ad48968ce016"} diff --git a/new-backend/docker-compose.tunnel.yml b/new-backend/docker-compose.tunnel.yml new file mode 100644 index 0000000..45a180c --- /dev/null +++ b/new-backend/docker-compose.tunnel.yml @@ -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 diff --git a/new-backend/start.sh b/new-backend/start.sh new file mode 100644 index 0000000..6090f69 --- /dev/null +++ b/new-backend/start.sh @@ -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 diff --git a/new-backend/supervisord.conf b/new-backend/supervisord.conf new file mode 100644 index 0000000..434c549 --- /dev/null +++ b/new-backend/supervisord.conf @@ -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 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..e30e44b --- /dev/null +++ b/nginx.conf @@ -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; + } + } +} \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..b0a7d44 --- /dev/null +++ b/nginx/nginx.conf @@ -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; + } + } +} \ No newline at end of file