feat: add web playground with Docker sandbox
- 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
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user