clone

an offline map explorer where each world, from a game map to a city's open data, ships as one .atlas file

parity: sweep without a window

Dami committed 2668bb9
The harness drove the app through a browser from the start; the
desktop window was along only because the dev build always opened
one, and it popped over whatever the reader was doing every time a
sweep ran. ATLAS_HEADLESS now serves the same application over plain
HTTP from the same embedded assets, writes the listener's address
into inspector.url exactly where the framework would, and opens
nothing. The harness sets the variable itself; release builds do not
carry the mode.
commit
2668bb94325d79fb9f2659904d79f0c991da4c1c
parent
bce427985f20899a91af8eeadc5686c8790e2d43
author
Dami <dami.etoile@gmail.com>
kind
feat risk score 1.7 of 3, Jev's estimate
4 files changed, +85 -5
Mfrontend/parity/run.mjs10+++-
Mmain.go5++
Mparity_dev.go65++++++++++++++++++++++++++++++
Mparity_release.go10++++-
M frontend/parity/run.mjs +7 -3
M main.go +5 -0
M parity_dev.go +65 -0
@@ -5,12 +5,77 @@ package main
55 import (66 "fmt"77 "io"8+ "io/fs"9+ "log/slog"10+ "net"811 "net/http"912 "os"1013 "path/filepath"1114 "time"15+16+ "github.com/FelineStateMachine/atlas/internal/bundle"1217 )1318 19+// runHeadless serves the whole application over plain HTTP and never opens a20+// window, when ATLAS_HEADLESS asks for it. The parity harness drives the app21+// through a browser either way; a desktop window popping over the reader's22+// work was the one part of the sweep that was never needed. The listener's23+// address lands in inspector.url exactly where the framework would put it,24+// so the harness finds a headless run and a windowed one the same way.25+func runHeadless(assets fs.FS) bool {26+ if os.Getenv("ATLAS_HEADLESS") == "" {27+ return false28+ }29+ dir := os.Getenv("ATLAS_BUNDLES_DIR")30+ if dir == "" {31+ base, err := os.UserConfigDir()32+ if err != nil {33+ fmt.Fprintln(os.Stderr, "atlas: headless without a config dir:", err)34+ os.Exit(1)35+ }36+ dir = filepath.Join(base, "dev.felinestatemachine.atlas", "bundles")37+ }38+ registry := bundle.NewRegistry(dir)39+ if err := registry.Rescan(); err != nil {40+ slog.Warn("atlas: scanning bundles", "error", err)41+ }42+ handler := routes(assets, registry)43+ // The framework serves the shell's own files under /static; headless44+ // answers for them from the same embedded tree.45+ mux := http.NewServeMux()46+ mux.Handle("/", handler)47+ for name, kind := range map[string]string{"app.css": "text/css", "app.js": "text/javascript"} {48+ mux.HandleFunc("GET /static/"+name, func(w http.ResponseWriter, r *http.Request) {49+ data, err := fs.ReadFile(assets, "assets/"+filepath.Base(r.URL.Path))50+ if err != nil {51+ http.NotFound(w, r)52+ return53+ }54+ w.Header().Set("Content-Type", kind+"; charset=utf-8")55+ _, _ = w.Write(data)56+ })57+ }58+ listener, err := net.Listen("tcp", "127.0.0.1:0")59+ if err != nil {60+ fmt.Fprintln(os.Stderr, "atlas: headless listener:", err)61+ os.Exit(1)62+ }63+ base, err := os.UserConfigDir()64+ if err == nil {65+ address := "http://" + listener.Addr().String()66+ path := filepath.Join(base, "dev.felinestatemachine.atlas", "inspector.url")67+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err == nil {68+ _ = os.WriteFile(path, []byte(address+"\n"), 0o644)69+ }70+ fmt.Println("atlas: headless at", address)71+ }72+ if err := http.Serve(listener, mux); err != nil {73+ fmt.Fprintln(os.Stderr, "atlas: headless server:", err)74+ os.Exit(1)75+ }76+ return true77+}78+1479 // parityDir returns the directory that receives behavior-parity tour results.1580 // It sits beside the app's own data so the harness can find results without1681 // threading an environment variable through `open`.
M parity_release.go +8 -2