small pixel drawing of a pufferfish gore

main.go

package main

import (
	"fmt"
	"html/template"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"j3s.sh/gore/internal/module"
)

var (
	buildTimestamp = "uninitialized"
	httpPassword   string // or empty to only permit unix socket access
	hostname       string
)

func WaitForClock() {
	epochPlus1Year := time.Unix(60*60*24*365, 0)
	for {
		if time.Now().After(epochPlus1Year) {
			return
		}
		// Sleeps for 1 real second, regardless of wall-clock time.
		// See https://github.com/golang/proposal/blob/master/design/12914-monotonic.md
		time.Sleep(1 * time.Second)
	}
}

type LogEntry struct {
	IsStdErr bool
	Message  string
}

func main() {
	http.HandleFunc("GET /", indexHandler)
	http.HandleFunc("POST /submit", submitHandler)
	http.HandleFunc("GET /logs", fetchLogs)

	log.Println("Starting Gore server on :8080...")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
	tmpl := `
<!DOCTYPE html>
<html>
<head>
	<title>Gore</title>
	<style>
		body { font-family: monospace; background-color: #000; color: #fff; }
		.log-stdout { color: #0f0; }
		.log-stderr { color: #f00; }
	</style>
	<script>
		function toggleLogs(module) {
			const logContainer = document.getElementById("logs-" + module);
			if (logContainer.style.display === "none") {
				fetch("/logs?module=" + module)
					.then(response => response.json())
					.then(data => {
						logContainer.innerHTML = "";
						data.forEach(entry => {
							const logLine = document.createElement("div");
							logLine.textContent = entry.message;
							logLine.className = entry.isStdErr ? "log-stderr" : "log-stdout";
							logContainer.appendChild(logLine);
						});
						logContainer.style.display = "block";
					})
					.catch(err => console.error("Error fetching logs:", err));
			} else {
				logContainer.style.display = "none";
			}
		}
	</script>
</head>
<body>
	<h1>Gore</h1>
	<p>Submit a Go module for execution:</p>
	<form action="/submit" method="POST">
		<input type="text" name="module" placeholder="Go module URL" required style="width: 300px;">
		<button type="submit">Submit</button>
	</form>
	<h2>Services</h2>
	<table>
		<tr><th>Module</th><th>Status</th><th>Logs</th></tr>
		{{range $module, $status := .Modules}}
		<tr>
			<td>{{$module}}</td>
			<td>{{$status.Status}}</td>
			<td><button onclick="toggleLogs('{{ $module }}')">View Logs</button></td>
		</tr>
		<tr>
			<td colspan="3">
				<div id="logs-{{ $module }}" style="display: none; background-color: #111; padding: 10px;"></div>
			</td>
		</tr>
		{{end}}
	</table>
</body>
</html>
`
	services.Lock()
	defer services.Unlock()

	data := struct {
		Modules map[string]*services.S
	}{
		Modules: services.S,
	}

	t := template.Must(template.New("index").Parse(tmpl))
	t.Execute(w, data)
}

func submitHandler(w http.ResponseWriter, r *http.Request) {
	formInput := r.FormValue("module")
	if formInput == "" {
		http.Error(w, "Module URL is required (e.g. git.j3s.sh/vore@latest)", http.StatusBadRequest)
		return
	}

	m := module.New()
	before, after, found := strings.Cut(formInput, "@")
	if found {
		m.Path = before
		m.Version = after
	} else {
		// assume latest if not specified
		m.Path = formInput
		m.Version = "latest"
	}

	supervisedMu.Lock()
	if _, exists := moduleStatuses[module]; exists {
		supervisedMu.Unlock()
		http.Error(w, "Module already submitted", http.StatusBadRequest)
		return
	}
	moduleStatuses[module] = &ModuleStatus{Status: "Building", Logs: []LogEntry{}}
	supervisedMu.Unlock()

	go func() {
		if err := processModule(module); err != nil {
			log.Printf("Error processing module %s: %v", module, err)
			supervisedMu.Lock()
			moduleStatuses[module].Status = "Failed"
			supervisedMu.Unlock()
		} else {
			supervisedMu.Lock()
			moduleStatuses[module].Status = "Running"
			supervisedMu.Unlock()
		}
	}()

	http.Redirect(w, r, "/", http.StatusSeeOther)
}

func fetchLogs(w http.ResponseWriter, r *http.Request) {
	module := r.URL.Query().Get("module")
	if module == "" {
		http.Error(w, "Module URL is required", http.StatusBadRequest)
		return
	}

	supervisedMu.Lock()
	status, exists := moduleStatuses[module]
	supervisedMu.Unlock()

	if !exists {
		http.Error(w, "No logs found for the specified module", http.StatusNotFound)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	fmt.Fprintf(w, "[")
	for i, log := range status.Logs {
		fmt.Fprintf(w, `{"isStdErr":%t,"message":%q}`, log.IsStdErr, log.Message)
		if i < len(status.Logs)-1 {
			fmt.Fprintf(w, ",")
		}
	}
	fmt.Fprintf(w, "]")
}

func processModule(module string) error {
	mu.Lock()
	defer mu.Unlock()

	log.Printf("Processing module: %s", module)

	tempDir, err := os.MkdirTemp("", "gore-*")
	if err != nil {
		return fmt.Errorf("failed to create temp dir: %v", err)
	}
	defer os.RemoveAll(tempDir)

	cmd := exec.Command("go", "get", module)
	cmd.Dir = tempDir
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("failed to get module: %v", err)
	}

	outputBinary := filepath.Join(tempDir, "module-binary")
	cmd = exec.Command("go", "build", "-o", outputBinary, module)
	cmd.Dir = tempDir
	if err := cmd.Run(); err != nil {
		return fmt.Errorf("failed to build module: %v", err)
	}

	supervisedMu.Lock()
	moduleStatuses[module].Logs = append(moduleStatuses[module].Logs, LogEntry{IsStdErr: false, Message: "Module built successfully."})
	supervisedMu.Unlock()

	return nil
}