//go:build xserver package xturbo_test import ( "bytes" "crypto/rand" "encoding/binary" "errors" "os" "os/exec" "path/filepath" "strconv" "strings" "testing" "time" "bnema.dev/xturbo" "bnema.dev/xturbo/xproto" "golang.org/x/sys/unix" ) // startXvfb runs Xvfb on a free display with -nolisten tcp, requiring the // MIT-MAGIC-COOKIE-1 in authFile. It returns the DISPLAY string. func startXvfb(t *testing.T, authFile string) string { t.Helper() if _, err := exec.LookPath("Xvfb"); err != nil { t.Skip("Xvfb not installed") } r, w, err := os.Pipe() if err != nil { t.Fatal(err) } // Xvfb may write to -displayfd again (e.g. after a server reset); closing // the read end early makes that write fail and kills the server. t.Cleanup(func() { _ = r.Close() }) cmd := exec.Command("Xvfb", "-displayfd", "3", "-screen", "0", "800x600x24", "-nolisten", "tcp", "-auth", authFile) cmd.ExtraFiles = []*os.File{w} var log bytes.Buffer cmd.Stderr = &log if err := cmd.Start(); err != nil { w.Close() t.Fatal(err) } w.Close() t.Cleanup(func() { _ = cmd.Process.Kill(); _ = cmd.Wait() }) _ = r.SetReadDeadline(time.Now().Add(10 * time.Second)) var buf [32]byte n, err := r.Read(buf[:]) if err != nil { t.Fatalf("Xvfb: %v: %s", err, log.String()) } num := strings.TrimSpace(string(buf[:n])) if _, err := strconv.Atoi(num); err != nil { t.Fatalf("Xvfb display %q", num) } return ":" + num } // writeXauthority writes one FamilyLocal entry for this host, any display. func writeXauthority(t *testing.T, path string, cookie []byte) { t.Helper() host, err := os.Hostname() if err != nil { t.Fatal(err) } var b []byte field := func(s []byte) { b = binary.BigEndian.AppendUint16(b, uint16(len(s))) b = append(b, s...) } b = binary.BigEndian.AppendUint16(b, 256) // FamilyLocal field([]byte(host)) field(nil) // any display number field([]byte("MIT-MAGIC-COOKIE-1")) field(cookie) if err := os.WriteFile(path, b, 0o600); err != nil { t.Fatal(err) } } func TestXserverDial(t *testing.T) { dir := t.TempDir() auth := filepath.Join(dir, "Xauthority") cookie := make([]byte, 16) _, _ = rand.Read(cookie) writeXauthority(t, auth, cookie) display := startXvfb(t, auth) // Without a cookie the server refuses the setup: some builds answer Failed, // others close the connection without a reply. t.Setenv("XAUTHORITY", filepath.Join(dir, "missing")) if _, err := xturbo.Dial(display); !errors.Is(err, xturbo.ErrSetupFailed) && !errors.Is(err, xturbo.ErrClosed) { t.Fatalf("unauthenticated Dial: %v", err) } t.Setenv("XAUTHORITY", auth) c, err := xturbo.Dial(display + ".0") if err != nil { t.Fatal(err) } defer c.Close() // Compare root's own parsing against the generated Setup view instead of // server-assigned constants (they differ between X server builds). setup := c.SetupBytes() if c.Root() == 0 || len(setup) < xproto.SetupSize || len(setup) != 8+4*int(binary.LittleEndian.Uint16(setup[6:])) { t.Fatalf("root %#x, setup %d bytes", c.Root(), len(setup)) } it := xproto.NewSetupIter(setup, 1) if !it.Next() { t.Fatal("setup does not parse") } sv := it.Value() if sv.Size() != len(setup) || sv.Status() != 1 || sv.ProtocolMajorVersion() != 11 { t.Fatalf("setup size %d status %d", sv.Size(), sv.Status()) } screens := sv.Roots() if !screens.Next() || uint32(screens.Value().Root()) != c.Root() { t.Fatalf("Root %#x, setup view says otherwise", c.Root()) } // Round trips through Dispatch: GetInputFocus's reply, then the error // of GetGeometry on window None, both attributed through Expect. const kindFocus, kindGeom = 16, 17 seq := xproto.GetInputFocus(c.Buf()) bad := xproto.GetGeometry(c.Buf(), xproto.Drawable(xproto.WindowNone)) if seq == 0 || bad != seq+1 { t.Fatalf("seqs %d %d", seq, bad) } if err := c.Expect(seq, kindFocus, 1, 2, 3, 4); err != nil { t.Fatal(err) } if err := c.Expect(bad, kindGeom, 5, 0, 0, 0); err != nil { t.Fatal(err) } if err := c.Flush(); err != nil { t.Fatal(err) } var reply xturbo.Reply var xerr xturbo.XError h := xturbo.Handler{ OnReply: func(r *xturbo.Reply) { reply = *r; reply.Raw = nil }, OnError: func(e *xturbo.XError) { xerr = *e }, } dispatchUntil(t, c, &h, func() bool { return xerr.Seq != 0 }) if reply.Seq != seq || reply.Kind != kindFocus || reply.Args != [4]uint32{1, 2, 3, 4} { t.Fatalf("reply %+v", reply) } if xerr.Seq != bad || xerr.Kind != kindGeom || xerr.Args[0] != 5 || xerr.Code != xproto.BadDrawable || xerr.Major != 14 || xerr.Ext != xturbo.ExtCore || xerr.Bad != 0 { t.Fatalf("error %+v", xerr) } if st := c.Stats(); st.DroppedReplies != 0 || st.UnhandledErrors != 0 { t.Fatalf("%+v", st) } } // dispatchUntil polls the connection and dispatches until done reports true. func dispatchUntil(t *testing.T, c *xturbo.Conn, h *xturbo.Handler, done func() bool) { t.Helper() deadline := time.Now().Add(5 * time.Second) for !done() { if time.Now().After(deadline) { t.Fatal("timeout waiting for the server") } fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: unix.POLLIN}} if _, err := unix.Poll(fds, 1000); err != nil && err != unix.EINTR { t.Fatal(err) } if err := c.Dispatch(h); err != nil { t.Fatal(err) } } }