1package harness
2
3import (
4 "context"
5 "fmt"
6 "net"
7 "os"
8 "path/filepath"
9 "regexp"
10 "strings"
11)
12
13// sizePattern accepts one headless output size, or several comma-separated
14// ones (HEADLESS-1, HEADLESS-2, ... left to right).
15var sizePattern = regexp.MustCompile(`^[1-9][0-9]*x[1-9][0-9]*(,[1-9][0-9]*x[1-9][0-9]*)*$`)
16var layoutPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
17var socketPattern = regexp.MustCompile(`^wayland-[0-9]+$`)
18
19func Config(size, scale, layout, background string) (string, error) {
20 if !sizePattern.MatchString(size) || !layoutPattern.MatchString(layout) {
21 return "", fmt.Errorf("invalid size or layout")
22 }
23 if scale != "1" && scale != "1.25" && scale != "1.5" && scale != "2" {
24 return "", fmt.Errorf("unsupported scale %q", scale)
25 }
26 if _, err := ParseColor(background); err != nil {
27 return "", err
28 }
29 // Wayland debug logs record each client cursor shape change, which
30 // systemtests read from the compositor run log.
31 var b strings.Builder
32 for i, s := range strings.Split(size, ",") {
33 fmt.Fprintf(&b, "output.HEADLESS-%d = %s\noutput.HEADLESS-%d.scale = %s\n", i+1, s, i+1, scale)
34 }
35 fmt.Fprintf(&b, "keyboard.layout = %s\nbackground = %s\nlog.debug = wayland\n", layout, background)
36 return b.String(), nil
37}
38
39// FramesDir is where NeferWL writes the screenshots of the first output: the
40// frames directory itself, or its HEADLESS-1 subdirectory with several outputs.
41func FramesDir(frames, size string) string {
42 if strings.Contains(size, ",") {
43 return filepath.Join(frames, "HEADLESS-1")
44 }
45 return frames
46}
47
48// DiscoverSocket only accepts a real Unix socket paired with NeferWL's state file.
49// A fresh private runtime directory prevents picking up a foreign session.
50func DiscoverSocket(ctx context.Context, runtime string) (string, error) {
51 entries, err := os.ReadDir(filepath.Join(runtime, "neferwl"))
52 if os.IsNotExist(err) {
53 return "", nil
54 }
55 if err != nil {
56 return "", err
57 }
58 var names []string
59 for _, e := range entries {
60 if err := ctx.Err(); err != nil {
61 return "", err
62 }
63 name, ok := strings.CutSuffix(e.Name(), ".json")
64 if !ok || !socketPattern.MatchString(name) {
65 continue
66 }
67 fi, err := os.Lstat(filepath.Join(runtime, name))
68 if err == nil && fi.Mode()&os.ModeSocket != 0 {
69 c, err := (&net.Dialer{}).DialContext(ctx, "unix", filepath.Join(runtime, name))
70 if err == nil {
71 _ = c.Close()
72 names = append(names, name)
73 }
74 }
75 }
76 if len(names) > 1 {
77 return "", fmt.Errorf("multiple compositor sockets: %v", names)
78 }
79 if len(names) == 1 {
80 return names[0], nil
81 }
82 return "", nil
83}