1//go:build xserver
2
3package xturbo_test
4
5import (
6 "bytes"
7 "crypto/rand"
8 "encoding/binary"
9 "errors"
10 "os"
11 "os/exec"
12 "path/filepath"
13 "strconv"
14 "strings"
15 "testing"
16 "time"
17
18 "bnema.dev/xturbo"
19 "bnema.dev/xturbo/xproto"
20 "golang.org/x/sys/unix"
21)
22
23// startXvfb runs Xvfb on a free display with -nolisten tcp, requiring the
24// MIT-MAGIC-COOKIE-1 in authFile. It returns the DISPLAY string.
25func startXvfb(t *testing.T, authFile string) string {
26 t.Helper()
27 if _, err := exec.LookPath("Xvfb"); err != nil {
28 t.Skip("Xvfb not installed")
29 }
30 r, w, err := os.Pipe()
31 if err != nil {
32 t.Fatal(err)
33 }
34 // Xvfb may write to -displayfd again (e.g. after a server reset); closing
35 // the read end early makes that write fail and kills the server.
36 t.Cleanup(func() { _ = r.Close() })
37 cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-auth", authFile)
38 cmd.ExtraFiles = []*os.File{w}
39 var log bytes.Buffer
40 cmd.Stderr = &log
41 if err := cmd.Start(); err != nil {
42 w.Close()
43 t.Fatal(err)
44 }
45 w.Close()
46 t.Cleanup(func() { _ = cmd.Process.Kill(); _ = cmd.Wait() })
47 _ = r.SetReadDeadline(time.Now().Add(10 * time.Second))
48 var buf [32]byte
49 n, err := r.Read(buf[:])
50 if err != nil {
51 t.Fatalf("Xvfb: %v: %s", err, log.String())
52 }
53 num := strings.TrimSpace(string(buf[:n]))
54 if _, err := strconv.Atoi(num); err != nil {
55 t.Fatalf("Xvfb display %q", num)
56 }
57 return ":" + num
58}
59
60// writeXauthority writes one FamilyLocal entry for this host, any display.
61func writeXauthority(t *testing.T, path string, cookie []byte) {
62 t.Helper()
63 host, err := os.Hostname()
64 if err != nil {
65 t.Fatal(err)
66 }
67 var b []byte
68 field := func(s []byte) {
69 b = binary.BigEndian.AppendUint16(b, uint16(len(s)))
70 b = append(b, s...)
71 }
72 b = binary.BigEndian.AppendUint16(b, 256) // FamilyLocal
73 field([]byte(host))
74 field(nil) // any display number
75 field([]byte("MIT-MAGIC-COOKIE-1"))
76 field(cookie)
77 if err := os.WriteFile(path, b, 0o600); err != nil {
78 t.Fatal(err)
79 }
80}
81
82func TestXserverDial(t *testing.T) {
83 dir := t.TempDir()
84 auth := filepath.Join(dir, "Xauthority")
85 cookie := make([]byte, 16)
86 _, _ = rand.Read(cookie)
87 writeXauthority(t, auth, cookie)
88 display := startXvfb(t, auth)
89
90 // Without a cookie the server refuses the setup: some builds answer Failed,
91 // others close the connection without a reply.
92 t.Setenv("XAUTHORITY", filepath.Join(dir, "missing"))
93 if _, err := xturbo.Dial(display); !errors.Is(err, xturbo.ErrSetupFailed) && !errors.Is(err, xturbo.ErrClosed) {
94 t.Fatalf("unauthenticated Dial: %v", err)
95 }
96
97 t.Setenv("XAUTHORITY", auth)
98 c, err := xturbo.Dial(display + ".0")
99 if err != nil {
100 t.Fatal(err)
101 }
102 defer c.Close()
103
104 // Compare root's own parsing against the generated Setup view instead of
105 // server-assigned constants (they differ between X server builds).
106 setup := c.SetupBytes()
107 if c.Root() == 0 || len(setup) < xproto.SetupSize || len(setup) != 8+4*int(binary.LittleEndian.Uint16(setup[6:])) {
108 t.Fatalf("root %#x, setup %d bytes", c.Root(), len(setup))
109 }
110 it := xproto.NewSetupIter(setup, 1)
111 if !it.Next() {
112 t.Fatal("setup does not parse")
113 }
114 sv := it.Value()
115 if sv.Size() != len(setup) || sv.Status() != 1 || sv.ProtocolMajorVersion() != 11 {
116 t.Fatalf("setup size %d status %d", sv.Size(), sv.Status())
117 }
118 screens := sv.Roots()
119 if !screens.Next() || uint32(screens.Value().Root()) != c.Root() {
120 t.Fatalf("Root %#x, setup view says otherwise", c.Root())
121 }
122
123 // Round trips through Dispatch: GetInputFocus's reply, then the error
124 // of GetGeometry on window None, both attributed through Expect.
125 const kindFocus, kindGeom = 16, 17
126 seq := xproto.GetInputFocus(c.Buf())
127 bad := xproto.GetGeometry(c.Buf(), xproto.Drawable(xproto.WindowNone))
128 if seq == 0 || bad != seq+1 {
129 t.Fatalf("seqs %d %d", seq, bad)
130 }
131 if err := c.Expect(seq, kindFocus, 1, 2, 3, 4); err != nil {
132 t.Fatal(err)
133 }
134 if err := c.Expect(bad, kindGeom, 5, 0, 0, 0); err != nil {
135 t.Fatal(err)
136 }
137 if err := c.Flush(); err != nil {
138 t.Fatal(err)
139 }
140 var reply xturbo.Reply
141 var xerr xturbo.XError
142 h := xturbo.Handler{
143 OnReply: func(r *xturbo.Reply) { reply = *r; reply.Raw = nil },
144 OnError: func(e *xturbo.XError) { xerr = *e },
145 }
146 dispatchUntil(t, c, &h, func() bool { return xerr.Seq != 0 })
147 if reply.Seq != seq || reply.Kind != kindFocus || reply.Args != [4]uint32{1, 2, 3, 4} {
148 t.Fatalf("reply %+v", reply)
149 }
150 if xerr.Seq != bad || xerr.Kind != kindGeom || xerr.Args[0] != 5 || xerr.Code != xproto.BadDrawable ||
151 xerr.Major != 14 || xerr.Ext != xturbo.ExtCore || xerr.Bad != 0 {
152 t.Fatalf("error %+v", xerr)
153 }
154 if st := c.Stats(); st.DroppedReplies != 0 || st.UnhandledErrors != 0 {
155 t.Fatalf("%+v", st)
156 }
157}
158
159// dispatchUntil polls the connection and dispatches until done reports true.
160func dispatchUntil(t *testing.T, c *xturbo.Conn, h *xturbo.Handler, done func() bool) {
161 t.Helper()
162 deadline := time.Now().Add(5 * time.Second)
163 for !done() {
164 if time.Now().After(deadline) {
165 t.Fatal("timeout waiting for the server")
166 }
167 fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: unix.POLLIN}}
168 if _, err := unix.Poll(fds, 1000); err != nil && err != unix.EINTR {
169 t.Fatal(err)
170 }
171 if err := c.Dispatch(h); err != nil {
172 t.Fatal(err)
173 }
174 }
175}