ef4aa42b0b
- playground/backend/main.go — Go HTTP server with /compile endpoint - playground/frontend/index.html — Monaco Editor with 8 code examples - playground/sandbox/Dockerfile — isolated Docker container with buxc2 - playground/docker-compose.yml — orchestrates backend + sandbox - playground/Makefile — build and run targets - playground/README.md — deployment guide for VPS with nginx + SSL
241 lines
5.4 KiB
Go
241 lines
5.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
compileTimeout = 10 * time.Second
|
|
runTimeout = 5 * time.Second
|
|
maxOutputSize = 64 * 1024 // 64KB
|
|
maxCodeSize = 64 * 1024 // 64KB
|
|
)
|
|
|
|
type CompileRequest struct {
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
type CompileResponse struct {
|
|
Output string `json:"output"`
|
|
IsError bool `json:"isError"`
|
|
}
|
|
|
|
func main() {
|
|
port := os.Getenv("PORT")
|
|
if port == "" {
|
|
port = "8080"
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/compile", handleCompile)
|
|
mux.HandleFunc("/health", handleHealth)
|
|
mux.Handle("/", http.FileServer(http.Dir("./frontend")))
|
|
|
|
log.Printf("Bux Playground server starting on :%s", port)
|
|
log.Fatal(http.ListenAndServe(":"+port, cors(mux)))
|
|
}
|
|
|
|
func cors(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Access-Control-Allow-Origin", "*")
|
|
w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS")
|
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
|
if r.Method == "OPTIONS" {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func handleHealth(w http.ResponseWriter, r *http.Request) {
|
|
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
|
}
|
|
|
|
func handleCompile(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
|
|
// Read code from body
|
|
body, err := io.ReadAll(io.LimitReader(r.Body, maxCodeSize+1))
|
|
if err != nil {
|
|
writeError(w, "Failed to read request body")
|
|
return
|
|
}
|
|
if len(body) > maxCodeSize {
|
|
writeError(w, "Code exceeds maximum size (64KB)")
|
|
return
|
|
}
|
|
|
|
code := strings.TrimSpace(string(body))
|
|
if code == "" {
|
|
writeError(w, "Empty code")
|
|
return
|
|
}
|
|
|
|
// Create temp project directory
|
|
tmpDir, err := os.MkdirTemp("", "bux-playground-*")
|
|
if err != nil {
|
|
writeError(w, "Failed to create temp directory")
|
|
return
|
|
}
|
|
defer os.RemoveAll(tmpDir)
|
|
|
|
// Write code to src/Main.bux
|
|
srcDir := filepath.Join(tmpDir, "src")
|
|
if err := os.MkdirAll(srcDir, 0755); err != nil {
|
|
writeError(w, "Failed to create src directory")
|
|
return
|
|
}
|
|
|
|
mainFile := filepath.Join(srcDir, "Main.bux")
|
|
if err := os.WriteFile(mainFile, []byte(code), 0644); err != nil {
|
|
writeError(w, "Failed to write source file")
|
|
return
|
|
}
|
|
|
|
// Write bux.toml
|
|
buxToml := `[Package]
|
|
Name = "playground"
|
|
Version = "0.1.0"
|
|
Type = "bin"
|
|
|
|
[Build]
|
|
Output = "Bin"
|
|
`
|
|
if err := os.WriteFile(filepath.Join(tmpDir, "bux.toml"), []byte(buxToml), 0644); err != nil {
|
|
writeError(w, "Failed to write bux.toml")
|
|
return
|
|
}
|
|
|
|
// Run buxc2 build inside Docker sandbox
|
|
output, isError := runInSandbox(tmpDir)
|
|
|
|
// Truncate output if too large
|
|
if len(output) > maxOutputSize {
|
|
output = output[:maxOutputSize] + "\n... (output truncated)"
|
|
}
|
|
|
|
resp := CompileResponse{
|
|
Output: output,
|
|
IsError: isError,
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|
|
|
|
func runInSandbox(projectDir string) (string, bool) {
|
|
// Check if running inside Docker (sandbox container available)
|
|
sandboxImage := os.Getenv("SANDBOX_IMAGE")
|
|
if sandboxImage == "" {
|
|
sandboxImage = "bux-playground-sandbox"
|
|
}
|
|
|
|
// Determine if we use Docker or local buxc2
|
|
useDocker := os.Getenv("USE_DOCKER") != "0"
|
|
|
|
var cmd *exec.Cmd
|
|
var ctx context.Context
|
|
var cancel context.CancelFunc
|
|
|
|
if useDocker {
|
|
// Run compilation inside Docker container
|
|
ctx, cancel = context.WithTimeout(context.Background(), compileTimeout)
|
|
defer cancel()
|
|
|
|
cmd = exec.CommandContext(ctx, "docker", "run",
|
|
"--rm",
|
|
"--network=none",
|
|
"--memory=128m",
|
|
"--memory-swap=128m",
|
|
"--cpus=1.0",
|
|
"--pids-limit=64",
|
|
"--read-only",
|
|
"--tmpfs", "/tmp:noexec,nosuid,size=50m",
|
|
"-v", projectDir+":/project:ro",
|
|
"-w", "/project",
|
|
sandboxImage,
|
|
"sh", "-c",
|
|
"buxc2 build 2>&1 && timeout 5s ./build/playground 2>&1 || true",
|
|
)
|
|
} else {
|
|
// Local mode (for development)
|
|
ctx, cancel = context.WithTimeout(context.Background(), compileTimeout)
|
|
defer cancel()
|
|
|
|
buxc2Path := os.Getenv("BUXC2_PATH")
|
|
if buxc2Path == "" {
|
|
buxc2Path = "buxc2"
|
|
}
|
|
|
|
// First compile
|
|
compileCmd := exec.CommandContext(ctx, buxc2Path, "build")
|
|
compileCmd.Dir = projectDir
|
|
compileCmd.Env = append(os.Environ(),
|
|
"HOME=/tmp",
|
|
)
|
|
|
|
compileOut, compileErr := compileCmd.CombinedOutput()
|
|
output := string(compileOut)
|
|
if compileErr != nil {
|
|
return output, true
|
|
}
|
|
|
|
// Then run the binary
|
|
runCtx, runCancel := context.WithTimeout(context.Background(), runTimeout)
|
|
defer runCancel()
|
|
|
|
binPath := filepath.Join(projectDir, "build", "playground")
|
|
runCmd := exec.CommandContext(runCtx, binPath)
|
|
runCmd.Dir = projectDir
|
|
runOut, runErr := runCmd.CombinedOutput()
|
|
output += string(runOut)
|
|
if runErr != nil && runCtx.Err() != context.DeadlineExceeded {
|
|
output += "\nRuntime error: " + runErr.Error()
|
|
}
|
|
|
|
return output, false
|
|
}
|
|
|
|
// Run Docker command
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
cmd.Stderr = &out
|
|
|
|
err := cmd.Run()
|
|
output := out.String()
|
|
|
|
if ctx.Err() == context.DeadlineExceeded {
|
|
output += "\nTimeout: compilation or execution took too long"
|
|
return output, true
|
|
}
|
|
|
|
if err != nil {
|
|
return output, true
|
|
}
|
|
|
|
return output, false
|
|
}
|
|
|
|
func writeError(w http.ResponseWriter, msg string) {
|
|
resp := CompileResponse{
|
|
Output: msg,
|
|
IsError: true,
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(resp)
|
|
}
|