All checks were successful
release / build-release (push) Successful in 1m42s
- tools/shots/headless.go: startar en egen headless Wayfire (wlroots headless-backend + Xwayland) och kör spelet där, så att inga fönster syns eller tar fokus från det man gör på datorn; snäll avstängning och städning av sockets; METRO_HEADLESS=0 använder skärmen - skriptläget ritar scenen synkront vid shot så att bilden alltid speglar exakt den uppdateringen oavsett FPS (två körningar ger identiska PNG:er) - README/roadmap: hur den osynliga skärmen fungerar och används manuellt Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
129 lines
3.4 KiB
Go
129 lines
3.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
)
|
|
|
|
// Headless skärm för skärmdumparna (Linux): en egen wlroots-kompositor
|
|
// (Wayfire) med headless-backend och Xwayland, så att spelfönstren varken
|
|
// syns eller tar fokus från det Björn gör på datorn. Finns inte wayfire,
|
|
// eller sätts METRO_HEADLESS=0, används den vanliga skärmen.
|
|
|
|
type headless struct {
|
|
cmd *exec.Cmd
|
|
wayland string // t.ex. wayland-1
|
|
x11 string // t.ex. :1
|
|
}
|
|
|
|
var (
|
|
reSocket = regexp.MustCompile(`Using socket name (wayland-\d+)`)
|
|
reXwayland = regexp.MustCompile(`Starting Xwayland on (:\d+)`)
|
|
)
|
|
|
|
// startHeadless startar kompositorn och väntar på socket + Xwayland.
|
|
// Returnerar nil (utan fel) när headless inte är tillgängligt.
|
|
func startHeadless(buildDir string) (*headless, error) {
|
|
if runtime.GOOS != "linux" || os.Getenv("METRO_HEADLESS") == "0" {
|
|
return nil, nil
|
|
}
|
|
bin, err := exec.LookPath("wayfire")
|
|
if err != nil {
|
|
return nil, nil
|
|
}
|
|
ini := filepath.Join(buildDir, "wayfire-headless.ini")
|
|
if err := os.WriteFile(ini, []byte("[core]\nplugins = \nxwayland = true\npreferred_decoration_mode = server\nvwidth = 1\nvheight = 1\n"), 0o644); err != nil {
|
|
return nil, err
|
|
}
|
|
cmd := exec.Command(bin, "-c", ini)
|
|
cmd.Env = append(os.Environ(), "WLR_BACKENDS=headless", "WLR_LIBINPUT_NO_DEVICES=1", "WLR_RENDERER=pixman")
|
|
logf, err := os.Create(filepath.Join(buildDir, "wayfire-headless.log"))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pr, pw, err := os.Pipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
cmd.Stdout, cmd.Stderr = pw, pw
|
|
if err := cmd.Start(); err != nil {
|
|
return nil, err
|
|
}
|
|
pw.Close()
|
|
h := &headless{cmd: cmd}
|
|
done := make(chan struct{})
|
|
go func() {
|
|
sc := bufio.NewScanner(pr)
|
|
for sc.Scan() {
|
|
line := sc.Text()
|
|
fmt.Fprintln(logf, line)
|
|
if m := reSocket.FindStringSubmatch(line); m != nil {
|
|
h.wayland = m[1]
|
|
}
|
|
if m := reXwayland.FindStringSubmatch(line); m != nil {
|
|
h.x11 = m[1]
|
|
}
|
|
if h.wayland != "" && h.x11 != "" {
|
|
select {
|
|
case <-done:
|
|
default:
|
|
close(done)
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(10 * time.Second):
|
|
h.stop()
|
|
return nil, fmt.Errorf("headless-kompositorn startade inte inom 10 s (se %s)", logf.Name())
|
|
}
|
|
time.Sleep(500 * time.Millisecond) // låt Xwayland bli redo
|
|
return h, nil
|
|
}
|
|
|
|
// env ger miljön som pekar spelet mot den osynliga skärmen.
|
|
func (h *headless) env() []string {
|
|
var out []string
|
|
for _, e := range os.Environ() {
|
|
if strings.HasPrefix(e, "DISPLAY=") || strings.HasPrefix(e, "WAYLAND_DISPLAY=") {
|
|
continue
|
|
}
|
|
out = append(out, e)
|
|
}
|
|
return append(out, "DISPLAY="+h.x11, "WAYLAND_DISPLAY="+h.wayland)
|
|
}
|
|
|
|
// stop stänger kompositorn snällt (SIGTERM, så att den tar bort sina
|
|
// sockets) och städar det som ändå blir kvar.
|
|
func (h *headless) stop() {
|
|
if h == nil || h.cmd == nil || h.cmd.Process == nil {
|
|
return
|
|
}
|
|
_ = h.cmd.Process.Signal(syscall.SIGTERM)
|
|
done := make(chan struct{})
|
|
go func() { _, _ = h.cmd.Process.Wait(); close(done) }()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(3 * time.Second):
|
|
_ = h.cmd.Process.Kill()
|
|
<-done
|
|
}
|
|
if rt := os.Getenv("XDG_RUNTIME_DIR"); rt != "" && h.wayland != "" {
|
|
os.Remove(filepath.Join(rt, h.wayland))
|
|
os.Remove(filepath.Join(rt, h.wayland+".lock"))
|
|
}
|
|
if h.x11 != "" {
|
|
os.Remove(filepath.Join("/tmp/.X11-unix", "X"+strings.TrimPrefix(h.x11, ":")))
|
|
}
|
|
h.cmd = nil
|
|
}
|