feat: enhance terminal input handling and improve WebSocket connection safety
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
BIN
new-backend/main
BIN
new-backend/main
Binary file not shown.
BIN
new-backend/new-backend
Executable file
BIN
new-backend/new-backend
Executable file
Binary file not shown.
Reference in New Issue
Block a user