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) => {
e.preventDefault();
if (!terminalInput.trim()) {
return;
}
// Allow empty input to be sent (user might press Enter on purpose)
// But display it properly
const inputValue = terminalInput;
// Display the input in terminal
setTerminalOutput(prev => [
...prev,
{ type: 'input', content: terminalInput }
{ type: 'input', content: inputValue || '(empty)' }
]);
// 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) {
activeSocket.send(JSON.stringify({
type: 'input',
content: terminalInput + '\n'
content: inputValue
}));
}
@@ -471,17 +472,13 @@ const CodeChallenge = () => {
// Submit code to the backend
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`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
code: processedCode,
code: code,
language: getLanguageIdentifier(language),
input: '',
}),
@@ -971,6 +968,13 @@ const CodeChallenge = () => {
minimap: { enabled: false },
scrollBeyondLastLine: false,
automaticLayout: true,
wordWrap: 'off',
wrappingIndent: 'none',
formatOnPaste: false,
formatOnType: false,
acceptSuggestionOnEnter: 'off',
quickSuggestions: false,
suggestOnTriggerCharacters: false,
}}
/>
</div>

View File

@@ -21,13 +21,26 @@ import (
"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
type CodeExecutor struct {
config *config.Config
execQueue chan *models.CodeSubmission
submissions map[string]*models.CodeSubmission
submissionsMutex sync.RWMutex
terminalConnections map[string][]*websocket.Conn
terminalConnections map[string][]*SafeWebSocketConn
terminalMutex sync.RWMutex
inputChannels map[string]chan string
inputMutex sync.RWMutex
@@ -39,7 +52,7 @@ func NewCodeExecutor(cfg *config.Config) *CodeExecutor {
config: cfg,
execQueue: make(chan *models.CodeSubmission, cfg.Executor.QueueCapacity),
submissions: make(map[string]*models.CodeSubmission),
terminalConnections: make(map[string][]*websocket.Conn),
terminalConnections: make(map[string][]*SafeWebSocketConn),
inputChannels: make(map[string]chan string),
}
@@ -87,7 +100,8 @@ func (e *CodeExecutor) RegisterTerminalConnection(submissionID string, conn *web
e.terminalMutex.Lock()
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)",
submissionID, len(e.terminalConnections[submissionID]))
@@ -103,13 +117,14 @@ func (e *CodeExecutor) UnregisterTerminalConnection(submissionID string, conn *w
connections := e.terminalConnections[submissionID]
for i, c := range connections {
if c == conn {
if c.conn == conn {
// Remove the connection
e.terminalConnections[submissionID] = append(connections[:i], connections[i+1:]...)
break
}
}
// Clean up if no more connections
if len(e.terminalConnections[submissionID]) == 0 {
delete(e.terminalConnections, submissionID)
@@ -326,7 +341,7 @@ func (e *CodeExecutor) executeJava(submission *models.CodeSubmission, tempDir st
// executeC executes C code
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)
if err := os.WriteFile(codeFile, []byte(submission.Code), 0644); err != nil {
submission.Status = "failed"
@@ -334,62 +349,6 @@ func (e *CodeExecutor) executeC(submission *models.CodeSubmission, tempDir strin
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
compileCmd := exec.Command(
"docker", "run", "--rm",
@@ -406,7 +365,7 @@ int main() {
return
}
// Setup Docker run command
// Setup Docker run command with stdbuf to force line buffering
cmd := exec.Command(
"docker", "run", "--rm", "-i",
"--network=none",
@@ -415,7 +374,7 @@ int main() {
"--pids-limit=20",
"-v", tempDir+":/code",
langConfig.Image,
"/code/program",
"stdbuf", "-oL", "-eL", "/code/program",
)
// Execute the code with input handling
@@ -448,7 +407,7 @@ func (e *CodeExecutor) executeCpp(submission *models.CodeSubmission, tempDir str
return
}
// Setup Docker run command
// Setup Docker run command with stdbuf to force line buffering
cmd := exec.Command(
"docker", "run", "--rm", "-i",
"--network=none",
@@ -457,7 +416,7 @@ func (e *CodeExecutor) executeCpp(submission *models.CodeSubmission, tempDir str
"--pids-limit=20",
"-v", tempDir+":/code",
langConfig.Image,
"/code/program",
"stdbuf", "-oL", "-eL", "/code/program",
)
// Execute the code with input handling

Binary file not shown.

BIN
new-backend/new-backend Executable file

Binary file not shown.