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 }