a geicko-2 based round robin ranking system designed to test c++ battleship submissions battleship.dunkirk.sh

feat: improve error handling

dunkirk.sh 96e63d83 b64871ca

verified
+61 -1
+60 -1
internal/runner/runner.go
··· 31 31 ctx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSec)*time.Second) 32 32 defer cancel() 33 33 34 + // Check if systemd-run is available (not on macOS/local dev) 35 + _, err := exec.LookPath("systemd-run") 36 + if err != nil { 37 + // Fallback: run directly without sandbox (development only) 38 + log.Printf("systemd-run not available, running without sandbox: %v", args) 39 + cmd := exec.CommandContext(ctx, args[0], args[1:]...) 40 + output, err := cmd.CombinedOutput() 41 + 42 + if ctx.Err() == context.DeadlineExceeded { 43 + return output, fmt.Errorf("command timed out after %d seconds", timeoutSec) 44 + } 45 + 46 + return output, err 47 + } 48 + 34 49 // Build systemd-run command with security properties 35 50 // Using service unit (not scope) to get access to network/filesystem isolation 36 51 systemdArgs := []string{ ··· 64 79 // Check for timeout 65 80 if ctx.Err() == context.DeadlineExceeded { 66 81 return output, fmt.Errorf("command timed out after %d seconds", timeoutSec) 82 + } 83 + 84 + // Check if process was killed by a signal 85 + if err != nil { 86 + if exitErr, ok := err.(*exec.ExitError); ok { 87 + if status, ok := exitErr.Sys().(syscall.WaitStatus); ok { 88 + // Direct execution: check if signaled 89 + if status.Signaled() { 90 + sig := status.Signal() 91 + return output, fmt.Errorf("killed by signal: %s", sig.String()) 92 + } 93 + // systemd-run execution: exit code 128+N means killed by signal N 94 + exitCode := status.ExitStatus() 95 + if exitCode >= 128 && exitCode <= 192 { 96 + sigNum := exitCode - 128 97 + sigName := "unknown" 98 + switch sigNum { 99 + case 1: sigName = "SIGHUP" 100 + case 2: sigName = "SIGINT" 101 + case 3: sigName = "SIGQUIT" 102 + case 4: sigName = "SIGILL" 103 + case 5: sigName = "SIGTRAP" 104 + case 6: sigName = "SIGABRT" 105 + case 7: sigName = "SIGBUS" 106 + case 8: sigName = "SIGFPE" 107 + case 9: sigName = "SIGKILL" 108 + case 10: sigName = "SIGUSR1" 109 + case 11: sigName = "SIGSEGV" 110 + case 12: sigName = "SIGUSR2" 111 + case 13: sigName = "SIGPIPE" 112 + case 14: sigName = "SIGALRM" 113 + case 15: sigName = "SIGTERM" 114 + default: sigName = fmt.Sprintf("signal %d", sigNum) 115 + } 116 + return output, fmt.Errorf("killed by %s (exit code %d)", sigName, exitCode) 117 + } 118 + } 119 + } 67 120 } 68 121 69 122 return output, err ··· 231 284 output, err = runSandboxed(context.Background(), "run-match", runArgs, 300) 232 285 if err != nil { 233 286 log.Printf("Match execution failed: %v\n%s", err, output) 234 - return 0, 0, 0, fmt.Sprintf("Runtime error: %s (possible crash, timeout, or infinite loop)", strings.TrimSpace(string(output))) 287 + errMsg := strings.TrimSpace(string(output)) 288 + if errMsg != "" { 289 + // If there's output, show it along with the exit status 290 + return 0, 0, 0, fmt.Sprintf("Runtime error: %s (%s)", errMsg, err.Error()) 291 + } 292 + // If no output, just show the error 293 + return 0, 0, 0, fmt.Sprintf("Runtime error: %s", err.Error()) 235 294 } 236 295 237 296 p1, p2, moves := parseMatchOutput(string(output))
+1
internal/server/web.go
··· 527 527 528 528 .collapsible-section { 529 529 margin-top: 2rem; 530 + margin-bottom: 2rem; 530 531 background: #1e293b; 531 532 border: 1px solid #334155; 532 533 border-radius: 0.75rem;