feat: enhance terminal input handling and improve WebSocket connection safety

This commit is contained in:
ishikabhoyar
2025-11-09 13:52:48 +05:30
parent ebaef13845
commit 35d40da727
4 changed files with 38 additions and 75 deletions

View File

@@ -175,21 +175,22 @@ const CodeChallenge = () => {
const handleTerminalInput = (e) => { const handleTerminalInput = (e) => {
e.preventDefault(); e.preventDefault();
if (!terminalInput.trim()) { // Allow empty input to be sent (user might press Enter on purpose)
return; // But display it properly
} const inputValue = terminalInput;
// Display the input in terminal // Display the input in terminal
setTerminalOutput(prev => [ setTerminalOutput(prev => [
...prev, ...prev,
{ type: 'input', content: terminalInput } { type: 'input', content: inputValue || '(empty)' }
]); ]);
// If socket is available, send input to backend // If socket is available, send input to backend
// Note: backend will add the newline, so we don't add it here
if (activeSocket && activeSocket.readyState === WebSocket.OPEN) { if (activeSocket && activeSocket.readyState === WebSocket.OPEN) {
activeSocket.send(JSON.stringify({ activeSocket.send(JSON.stringify({
type: 'input', type: 'input',
content: terminalInput + '\n' content: inputValue
})); }));
} }
@@ -471,17 +472,13 @@ const CodeChallenge = () => {
// Submit code to the backend // Submit code to the backend
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080'; const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:8080';
// Ensure code has actual newlines, not escaped \n strings
// This handles cases where code might have literal "\n" instead of newlines
const processedCode = code.replace(/\\n/g, '\n').replace(/\\t/g, '\t');
const response = await fetch(`${apiUrl}/api/submit`, { const response = await fetch(`${apiUrl}/api/submit`, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
code: processedCode, code: code,
language: getLanguageIdentifier(language), language: getLanguageIdentifier(language),
input: '', input: '',
}), }),
@@ -971,6 +968,13 @@ const CodeChallenge = () => {
minimap: { enabled: false }, minimap: { enabled: false },
scrollBeyondLastLine: false, scrollBeyondLastLine: false,
automaticLayout: true, automaticLayout: true,
wordWrap: 'off',
wrappingIndent: 'none',
formatOnPaste: false,
formatOnType: false,
acceptSuggestionOnEnter: 'off',
quickSuggestions: false,
suggestOnTriggerCharacters: false,
}} }}
/> />
</div> </div>

View File

@@ -21,13 +21,26 @@ import (
"github.com/ishikabhoyar/monaco/new-backend/models" "github.com/ishikabhoyar/monaco/new-backend/models"
) )
// SafeWebSocketConn wraps a WebSocket connection with a mutex for thread-safe writes
type SafeWebSocketConn struct {
conn *websocket.Conn
mutex sync.Mutex
}
// WriteJSON writes JSON to the WebSocket connection in a thread-safe manner
func (s *SafeWebSocketConn) WriteJSON(v interface{}) error {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.conn.WriteJSON(v)
}
// CodeExecutor handles code execution for all languages // CodeExecutor handles code execution for all languages
type CodeExecutor struct { type CodeExecutor struct {
config *config.Config config *config.Config
execQueue chan *models.CodeSubmission execQueue chan *models.CodeSubmission
submissions map[string]*models.CodeSubmission submissions map[string]*models.CodeSubmission
submissionsMutex sync.RWMutex submissionsMutex sync.RWMutex
terminalConnections map[string][]*websocket.Conn terminalConnections map[string][]*SafeWebSocketConn
terminalMutex sync.RWMutex terminalMutex sync.RWMutex
inputChannels map[string]chan string inputChannels map[string]chan string
inputMutex sync.RWMutex inputMutex sync.RWMutex
@@ -39,7 +52,7 @@ func NewCodeExecutor(cfg *config.Config) *CodeExecutor {
config: cfg, config: cfg,
execQueue: make(chan *models.CodeSubmission, cfg.Executor.QueueCapacity), execQueue: make(chan *models.CodeSubmission, cfg.Executor.QueueCapacity),
submissions: make(map[string]*models.CodeSubmission), submissions: make(map[string]*models.CodeSubmission),
terminalConnections: make(map[string][]*websocket.Conn), terminalConnections: make(map[string][]*SafeWebSocketConn),
inputChannels: make(map[string]chan string), inputChannels: make(map[string]chan string),
} }
@@ -87,7 +100,8 @@ func (e *CodeExecutor) RegisterTerminalConnection(submissionID string, conn *web
e.terminalMutex.Lock() e.terminalMutex.Lock()
defer e.terminalMutex.Unlock() defer e.terminalMutex.Unlock()
e.terminalConnections[submissionID] = append(e.terminalConnections[submissionID], conn) safeConn := &SafeWebSocketConn{conn: conn}
e.terminalConnections[submissionID] = append(e.terminalConnections[submissionID], safeConn)
log.Printf("WebSocket connection registered for submission %s (total: %d)", log.Printf("WebSocket connection registered for submission %s (total: %d)",
submissionID, len(e.terminalConnections[submissionID])) submissionID, len(e.terminalConnections[submissionID]))
@@ -103,13 +117,14 @@ func (e *CodeExecutor) UnregisterTerminalConnection(submissionID string, conn *w
connections := e.terminalConnections[submissionID] connections := e.terminalConnections[submissionID]
for i, c := range connections { for i, c := range connections {
if c == conn { if c.conn == conn {
// Remove the connection // Remove the connection
e.terminalConnections[submissionID] = append(connections[:i], connections[i+1:]...) e.terminalConnections[submissionID] = append(connections[:i], connections[i+1:]...)
break break
} }
} }
// Clean up if no more connections // Clean up if no more connections
if len(e.terminalConnections[submissionID]) == 0 { if len(e.terminalConnections[submissionID]) == 0 {
delete(e.terminalConnections, submissionID) delete(e.terminalConnections, submissionID)
@@ -326,7 +341,7 @@ func (e *CodeExecutor) executeJava(submission *models.CodeSubmission, tempDir st
// executeC executes C code // executeC executes C code
func (e *CodeExecutor) executeC(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) { func (e *CodeExecutor) executeC(submission *models.CodeSubmission, tempDir string, langConfig config.LanguageConfig) {
// Write code to file // Write code to file exactly as submitted by user
codeFile := filepath.Join(tempDir, "code"+langConfig.FileExt) codeFile := filepath.Join(tempDir, "code"+langConfig.FileExt)
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil { if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
submission.Status = "failed" submission.Status = "failed"
@@ -334,62 +349,6 @@ func (e *CodeExecutor) executeC(submission *models.CodeSubmission, tempDir strin
return return
} }
// Create a wrapper script that will include setbuf to disable buffering
wrapperCode := `#include <stdio.h>
// Forward declaration of user's main function
int user_main();
int main() {
// Disable buffering completely for stdout
setbuf(stdout, NULL);
// Call the user's code
return user_main();
}
// User's code begins here
`
// Modify the user's code to be a function called from our wrapper
modifiedCode := submission.Code
// Replace main function with our wrapper
mainRegex := regexp.MustCompile(`int\s+main\s*\([^)]*\)\s*{`)
if mainRegex.MatchString(modifiedCode) {
// Rename user's main to user_main
modifiedCode = mainRegex.ReplaceAllString(modifiedCode, "int user_main() {")
// Combine wrapper with modified user code
finalCode := wrapperCode + modifiedCode
// Write the final code with wrapper to file
if err := os.WriteFile(codeFile, []byte(finalCode), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
} else {
// If no main function found, create a minimal program that includes the user code
finalCode := `#include <stdio.h>
int main() {
// Disable buffering completely for stdout
setbuf(stdout, NULL);
// Execute the user's code
` + submission.Code + `
return 0;
}
`
// Write the final code to file
if err := os.WriteFile(codeFile, []byte(finalCode), 0644); err != nil {
submission.Status = "failed"
submission.Output = "Failed to write code file: " + err.Error()
return
}
}
// Compile C code // Compile C code
compileCmd := exec.Command( compileCmd := exec.Command(
"docker", "run", "--rm", "docker", "run", "--rm",
@@ -406,7 +365,7 @@ int main() {
return return
} }
// Setup Docker run command // Setup Docker run command with stdbuf to force line buffering
cmd := exec.Command( cmd := exec.Command(
"docker", "run", "--rm", "-i", "docker", "run", "--rm", "-i",
"--network=none", "--network=none",
@@ -415,7 +374,7 @@ int main() {
"--pids-limit=20", "--pids-limit=20",
"-v", tempDir+":/code", "-v", tempDir+":/code",
langConfig.Image, langConfig.Image,
"/code/program", "stdbuf", "-oL", "-eL", "/code/program",
) )
// Execute the code with input handling // Execute the code with input handling
@@ -448,7 +407,7 @@ func (e *CodeExecutor) executeCpp(submission *models.CodeSubmission, tempDir str
return return
} }
// Setup Docker run command // Setup Docker run command with stdbuf to force line buffering
cmd := exec.Command( cmd := exec.Command(
"docker", "run", "--rm", "-i", "docker", "run", "--rm", "-i",
"--network=none", "--network=none",
@@ -457,7 +416,7 @@ func (e *CodeExecutor) executeCpp(submission *models.CodeSubmission, tempDir str
"--pids-limit=20", "--pids-limit=20",
"-v", tempDir+":/code", "-v", tempDir+":/code",
langConfig.Image, langConfig.Image,
"/code/program", "stdbuf", "-oL", "-eL", "/code/program",
) )
// Execute the code with input handling // Execute the code with input handling

Binary file not shown.

BIN
new-backend/new-backend Executable file

Binary file not shown.