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
| M | frontend/ | 10 | +++- |
| M | main.go | 5 | ++ |
| M | parity_dev.go | 65 | ++++++++++++++++++++++++++++++ |
| M | parity_release.go | 10 | ++++- |
@@ -56,11 +56,15 @@ const cli = (...cliArgs) => {
5656 // the file survives from earlier sessions and would otherwise answer for a5757 // server that is no longer there.5858 const launchedAt = Date.now();59+// ATLAS_HEADLESS keeps the sweep out of the reader's way: the dev build60+// serves the same frontend over HTTP and opens no window at all.5961 const app = spawn("go", ["run", "-tags", "dev", "."], {6062 cwd: repoRoot,61- env: flag("--bundles")62- ? { ...process.env, ATLAS_BUNDLES_DIR: resolve(flag("--bundles")) }63- : process.env,63+ env: {64+ ...process.env,65+ ATLAS_HEADLESS: "1",66+ ...(flag("--bundles") ? { ATLAS_BUNDLES_DIR: resolve(flag("--bundles")) } : {}),67+ },6468 stdio: "ignore",6569 detached: true,6670 });@@ -40,6 +40,11 @@ const catalogChangedTopic = "atlas:catalog-changed"
4040 var assets embed.FS4141 4242 func main() {43+ // A development build asked to run headless serves the same application44+ // over plain HTTP and opens no window: the parity sweep's whole ask.45+ if runHeadless(assets) {46+ return47+ }4348 // Created inside Routes, which the framework calls while opening the4449 // application, and used again at startup below; Open completes before the4550 // window exists, so the order holds.@@ -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`.@@ -2,8 +2,14 @@
22 33 package main44 5-import "net/http"5+import (6+ "io/fs"7+ "net/http"8+)69 710 // Release builds carry no parity harness; the frontend's tour hotkey POSTs8-// into a route that does not exist and receives a 404.11+// into a route that does not exist and receives a 404, and the headless12+// server the harness drives does not exist to ask for.913 func registerParityRoutes(*http.ServeMux) {}14+15+func runHeadless(fs.FS) bool { return false }