#6 feat(runtime): bootstrap extensions, Sync, InternAtoms and XID allocation
A atoms.go +54 -0
| @@ -0,0 +1,54 @@ | |||
| 1 | + | package xturbo | |
| 2 | + | ||
| 3 | + | import "time" | |
| 4 | + | ||
| 5 | + | // InternAtoms interns every name in one pipelined batch and blocks for one | |
| 6 | + | // round trip (Sync), writing the atom of names[i] into dst[i] (0 for a name | |
| 7 | + | // that does not exist when onlyIfExists is set). len(dst) must equal | |
| 8 | + | // len(names) and every name be at most 65535 bytes (ErrBadLength, nothing | |
| 9 | + | // sent, the connection unaffected). The first X error on one of these | |
| 10 | + | // requests is returned as *XError, its Args[0] the index of the name; the | |
| 11 | + | // other atoms are still filled. Packets read meanwhile go to the last Handler | |
| 12 | + | // given to Dispatch. Like Sync it gives up after 5 s (os.ErrDeadlineExceeded). | |
| 13 | + | // Meant for startup; it must not be called from a Handler callback. | |
| 14 | + | func (c *Conn) InternAtoms(names []string, onlyIfExists bool, dst []uint32) error { | |
| 15 | + | if len(names) != len(dst) { | |
| 16 | + | return ErrBadLength | |
| 17 | + | } | |
| 18 | + | for _, name := range names { | |
| 19 | + | if len(name) > 0xffff { | |
| 20 | + | return ErrBadLength // a caller bug: fail before queuing anything | |
| 21 | + | } | |
| 22 | + | } | |
| 23 | + | if c.dispatching { | |
| 24 | + | return errReentrantDispatch | |
| 25 | + | } | |
| 26 | + | if c.fd < 0 { | |
| 27 | + | return ErrClosed | |
| 28 | + | } | |
| 29 | + | clear(dst) | |
| 30 | + | c.atoms = dst | |
| 31 | + | defer func() { c.atoms = nil }() | |
| 32 | + | deadline := time.Now().Add(c.timeout) | |
| 33 | + | c.syncFailed = false | |
| 34 | + | for i, name := range names { | |
| 35 | + | if err := c.waitSlot(deadline); err != nil { | |
| 36 | + | return err | |
| 37 | + | } | |
| 38 | + | seq := encodeInternAtom(c.buf, onlyIfExists, name) | |
| 39 | + | if seq == 0 { | |
| 40 | + | return c.buf.Err() | |
| 41 | + | } | |
| 42 | + | if err := c.expect(seq, kindInternAtom, [4]uint32{uint32(i)}); err != nil { | |
| 43 | + | return err | |
| 44 | + | } | |
| 45 | + | } | |
| 46 | + | if err := c.waitUntil(c.h, deadline); err != nil { | |
| 47 | + | return err | |
| 48 | + | } | |
| 49 | + | if c.syncFailed { | |
| 50 | + | e := c.syncErr | |
| 51 | + | return &e | |
| 52 | + | } | |
| 53 | + | return nil | |
| 54 | + | } | |
| @@ -0,0 +1,54 @@ | |||||
| 1 | + | package xturbo | |||
| 2 | + | ||||
| 3 | + | import "time" | |||
| 4 | + | ||||
| 5 | + | // InternAtoms interns every name in one pipelined batch and blocks for one | |||
| 6 | + | // round trip (Sync), writing the atom of names[i] into dst[i] (0 for a name | |||
| 7 | + | // that does not exist when onlyIfExists is set). len(dst) must equal | |||
| 8 | + | // len(names) and every name be at most 65535 bytes (ErrBadLength, nothing | |||
| 9 | + | // sent, the connection unaffected). The first X error on one of these | |||
| 10 | + | // requests is returned as *XError, its Args[0] the index of the name; the | |||
| 11 | + | // other atoms are still filled. Packets read meanwhile go to the last Handler | |||
| 12 | + | // given to Dispatch. Like Sync it gives up after 5 s (os.ErrDeadlineExceeded). | |||
| 13 | + | // Meant for startup; it must not be called from a Handler callback. | |||
| 14 | + | func (c *Conn) InternAtoms(names []string, onlyIfExists bool, dst []uint32) error { | |||
| 15 | + | if len(names) != len(dst) { | |||
| 16 | + | return ErrBadLength | |||
| 17 | + | } | |||
| 18 | + | for _, name := range names { | |||
| 19 | + | if len(name) > 0xffff { | |||
| 20 | + | return ErrBadLength // a caller bug: fail before queuing anything | |||
| 21 | + | } | |||
| 22 | + | } | |||
| 23 | + | if c.dispatching { | |||
| 24 | + | return errReentrantDispatch | |||
| 25 | + | } | |||
| 26 | + | if c.fd < 0 { | |||
| 27 | + | return ErrClosed | |||
| 28 | + | } | |||
| 29 | + | clear(dst) | |||
| 30 | + | c.atoms = dst | |||
| 31 | + | defer func() { c.atoms = nil }() | |||
| 32 | + | deadline := time.Now().Add(c.timeout) | |||
| 33 | + | c.syncFailed = false | |||
| 34 | + | for i, name := range names { | |||
| 35 | + | if err := c.waitSlot(deadline); err != nil { | |||
| 36 | + | return err | |||
| 37 | + | } | |||
| 38 | + | seq := encodeInternAtom(c.buf, onlyIfExists, name) | |||
| 39 | + | if seq == 0 { | |||
| 40 | + | return c.buf.Err() | |||
| 41 | + | } | |||
| 42 | + | if err := c.expect(seq, kindInternAtom, [4]uint32{uint32(i)}); err != nil { | |||
| 43 | + | return err | |||
| 44 | + | } | |||
| 45 | + | } | |||
| 46 | + | if err := c.waitUntil(c.h, deadline); err != nil { | |||
| 47 | + | return err | |||
| 48 | + | } | |||
| 49 | + | if c.syncFailed { | |||
| 50 | + | e := c.syncErr | |||
| 51 | + | return &e | |||
| 52 | + | } | |||
| 53 | + | return nil | |||
| 54 | + | } | |||
A bootstrap_requests_test.go +68 -0
| @@ -0,0 +1,68 @@ | |||
| 1 | + | package xturbo_test | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "bytes" | |
| 5 | + | "testing" | |
| 6 | + | ||
| 7 | + | "bnema.dev/xturbo" | |
| 8 | + | "bnema.dev/xturbo/bigreq" | |
| 9 | + | "bnema.dev/xturbo/internal/xtest" | |
| 10 | + | "bnema.dev/xturbo/xcmisc" | |
| 11 | + | "bnema.dev/xturbo/xproto" | |
| 12 | + | ) | |
| 13 | + | ||
| 14 | + | // Root encodes the requests it issues itself; they must match the generated | |
| 15 | + | // encoders byte for byte, and the committed goldens where they exist. | |
| 16 | + | func TestBootstrapRequestsMatchGenerated(t *testing.T) { | |
| 17 | + | newBuf := func() *xturbo.Buf { | |
| 18 | + | b := xturbo.NewBuf(0) | |
| 19 | + | var ext xturbo.ExtTable | |
| 20 | + | ext.Set(xturbo.ExtBigRequests, 133, 0, 0) | |
| 21 | + | ext.Set(xturbo.ExtXCMisc, 136, 0, 0) | |
| 22 | + | b.Ext = &ext | |
| 23 | + | return b | |
| 24 | + | } | |
| 25 | + | for _, tc := range []struct { | |
| 26 | + | name string | |
| 27 | + | root, gen func(*xturbo.Buf) xturbo.Seq | |
| 28 | + | golden string | |
| 29 | + | }{ | |
| 30 | + | {"InternAtom", func(b *xturbo.Buf) xturbo.Seq { return xturbo.EncodeInternAtom(b, false, "XTURBO_") }, | |
| 31 | + | func(b *xturbo.Buf) xturbo.Seq { return xproto.InternAtom(b, false, "XTURBO_") }, "xproto/InternAtom"}, | |
| 32 | + | {"InternAtomOnlyIfExists", func(b *xturbo.Buf) xturbo.Seq { return xturbo.EncodeInternAtom(b, true, "WM_STATE") }, | |
| 33 | + | func(b *xturbo.Buf) xturbo.Seq { return xproto.InternAtom(b, true, "WM_STATE") }, ""}, | |
| 34 | + | {"QueryExtension", func(b *xturbo.Buf) xturbo.Seq { return xturbo.EncodeQueryExtension(b, "Generic Event Extension") }, | |
| 35 | + | func(b *xturbo.Buf) xturbo.Seq { return xproto.QueryExtension(b, "Generic Event Extension") }, ""}, | |
| 36 | + | {"QueryExtensionAligned", func(b *xturbo.Buf) xturbo.Seq { return xturbo.EncodeQueryExtension(b, "XFIXES") }, | |
| 37 | + | func(b *xturbo.Buf) xturbo.Seq { return xproto.QueryExtension(b, "XFIXES") }, ""}, | |
| 38 | + | {"GetInputFocus", xturbo.EncodeGetInputFocus, xproto.GetInputFocus, ""}, | |
| 39 | + | {"BigreqEnable", xturbo.EncodeBigreqEnable, bigreq.Enable, ""}, | |
| 40 | + | {"GetXIDRange", xturbo.EncodeGetXIDRange, xcmisc.GetXIDRange, ""}, | |
| 41 | + | } { | |
| 42 | + | t.Run(tc.name, func(t *testing.T) { | |
| 43 | + | rb, gb := newBuf(), newBuf() | |
| 44 | + | if tc.root(rb) != 1 || tc.gen(gb) != 1 || rb.Err() != nil || gb.Err() != nil { | |
| 45 | + | t.Fatal(rb.Err(), gb.Err()) | |
| 46 | + | } | |
| 47 | + | if !bytes.Equal(rb.Bytes(), gb.Bytes()) { | |
| 48 | + | t.Fatalf("root %x, generated %x", rb.Bytes(), gb.Bytes()) | |
| 49 | + | } | |
| 50 | + | if tc.golden == "" { | |
| 51 | + | return | |
| 52 | + | } | |
| 53 | + | want, err := xtest.LoadHex("testdata/golden/" + tc.golden + ".req.hex") | |
| 54 | + | if err != nil { | |
| 55 | + | t.Fatal(err) | |
| 56 | + | } | |
| 57 | + | if !bytes.Equal(rb.Bytes(), want) { | |
| 58 | + | t.Fatalf("root %x, golden %x", rb.Bytes(), want) | |
| 59 | + | } | |
| 60 | + | }) | |
| 61 | + | } | |
| 62 | + | // Extension requests fail sticky when the extension is absent. | |
| 63 | + | b := xturbo.NewBuf(0) | |
| 64 | + | b.Ext = &xturbo.ExtTable{} | |
| 65 | + | if xturbo.EncodeGetXIDRange(b) != 0 || b.Err() != xturbo.ErrExtMissing { | |
| 66 | + | t.Fatal(b.Err()) | |
| 67 | + | } | |
| 68 | + | } | |
| @@ -0,0 +1,68 @@ | |||||
| 1 | + | package xturbo_test | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "bytes" | |||
| 5 | + | "testing" | |||
| 6 | + | ||||
| 7 | + | "bnema.dev/xturbo" | |||
| 8 | + | "bnema.dev/xturbo/bigreq" | |||
| 9 | + | "bnema.dev/xturbo/internal/xtest" | |||
| 10 | + | "bnema.dev/xturbo/xcmisc" | |||
| 11 | + | "bnema.dev/xturbo/xproto" | |||
| 12 | + | ) | |||
| 13 | + | ||||
| 14 | + | // Root encodes the requests it issues itself; they must match the generated | |||
| 15 | + | // encoders byte for byte, and the committed goldens where they exist. | |||
| 16 | + | func TestBootstrapRequestsMatchGenerated(t *testing.T) { | |||
| 17 | + | newBuf := func() *xturbo.Buf { | |||
| 18 | + | b := xturbo.NewBuf(0) | |||
| 19 | + | var ext xturbo.ExtTable | |||
| 20 | + | ext.Set(xturbo.ExtBigRequests, 133, 0, 0) | |||
| 21 | + | ext.Set(xturbo.ExtXCMisc, 136, 0, 0) | |||
| 22 | + | b.Ext = &ext | |||
| 23 | + | return b | |||
| 24 | + | } | |||
| 25 | + | for _, tc := range []struct { | |||
| 26 | + | name string | |||
| 27 | + | root, gen func(*xturbo.Buf) xturbo.Seq | |||
| 28 | + | golden string | |||
| 29 | + | }{ | |||
| 30 | + | {"InternAtom", func(b *xturbo.Buf) xturbo.Seq { return xturbo.EncodeInternAtom(b, false, "XTURBO_") }, | |||
| 31 | + | func(b *xturbo.Buf) xturbo.Seq { return xproto.InternAtom(b, false, "XTURBO_") }, "xproto/InternAtom"}, | |||
| 32 | + | {"InternAtomOnlyIfExists", func(b *xturbo.Buf) xturbo.Seq { return xturbo.EncodeInternAtom(b, true, "WM_STATE") }, | |||
| 33 | + | func(b *xturbo.Buf) xturbo.Seq { return xproto.InternAtom(b, true, "WM_STATE") }, ""}, | |||
| 34 | + | {"QueryExtension", func(b *xturbo.Buf) xturbo.Seq { return xturbo.EncodeQueryExtension(b, "Generic Event Extension") }, | |||
| 35 | + | func(b *xturbo.Buf) xturbo.Seq { return xproto.QueryExtension(b, "Generic Event Extension") }, ""}, | |||
| 36 | + | {"QueryExtensionAligned", func(b *xturbo.Buf) xturbo.Seq { return xturbo.EncodeQueryExtension(b, "XFIXES") }, | |||
| 37 | + | func(b *xturbo.Buf) xturbo.Seq { return xproto.QueryExtension(b, "XFIXES") }, ""}, | |||
| 38 | + | {"GetInputFocus", xturbo.EncodeGetInputFocus, xproto.GetInputFocus, ""}, | |||
| 39 | + | {"BigreqEnable", xturbo.EncodeBigreqEnable, bigreq.Enable, ""}, | |||
| 40 | + | {"GetXIDRange", xturbo.EncodeGetXIDRange, xcmisc.GetXIDRange, ""}, | |||
| 41 | + | } { | |||
| 42 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 43 | + | rb, gb := newBuf(), newBuf() | |||
| 44 | + | if tc.root(rb) != 1 || tc.gen(gb) != 1 || rb.Err() != nil || gb.Err() != nil { | |||
| 45 | + | t.Fatal(rb.Err(), gb.Err()) | |||
| 46 | + | } | |||
| 47 | + | if !bytes.Equal(rb.Bytes(), gb.Bytes()) { | |||
| 48 | + | t.Fatalf("root %x, generated %x", rb.Bytes(), gb.Bytes()) | |||
| 49 | + | } | |||
| 50 | + | if tc.golden == "" { | |||
| 51 | + | return | |||
| 52 | + | } | |||
| 53 | + | want, err := xtest.LoadHex("testdata/golden/" + tc.golden + ".req.hex") | |||
| 54 | + | if err != nil { | |||
| 55 | + | t.Fatal(err) | |||
| 56 | + | } | |||
| 57 | + | if !bytes.Equal(rb.Bytes(), want) { | |||
| 58 | + | t.Fatalf("root %x, golden %x", rb.Bytes(), want) | |||
| 59 | + | } | |||
| 60 | + | }) | |||
| 61 | + | } | |||
| 62 | + | // Extension requests fail sticky when the extension is absent. | |||
| 63 | + | b := xturbo.NewBuf(0) | |||
| 64 | + | b.Ext = &xturbo.ExtTable{} | |||
| 65 | + | if xturbo.EncodeGetXIDRange(b) != 0 || b.Err() != xturbo.ErrExtMissing { | |||
| 66 | + | t.Fatal(b.Err()) | |||
| 67 | + | } | |||
| 68 | + | } | |||
A bootstrap_test.go +529 -0
| @@ -0,0 +1,529 @@ | |||
| 1 | + | package xturbo | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "errors" | |
| 6 | + | "testing" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "golang.org/x/sys/unix" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // waitRequests waits until the scripted server has read n requests. | |
| 13 | + | func waitRequests(t *testing.T, srv *scriptConn, n int) { | |
| 14 | + | t.Helper() | |
| 15 | + | deadline := time.Now().Add(5 * time.Second) | |
| 16 | + | for len(srv.requests()) < n { | |
| 17 | + | if time.Now().After(deadline) { | |
| 18 | + | t.Fatalf("server saw %d requests, want %d", len(srv.requests()), n) | |
| 19 | + | } | |
| 20 | + | time.Sleep(time.Millisecond) | |
| 21 | + | } | |
| 22 | + | } | |
| 23 | + | ||
| 24 | + | func TestBootstrapAllExtensions(t *testing.T) { | |
| 25 | + | exts := fullExts() | |
| 26 | + | c, srv := dialScript(t, &script{exts: exts, bigreqMax: 0x3fffff}) | |
| 27 | + | for id := ExtCore + 1; id < extCount; id++ { | |
| 28 | + | e := exts[ExtXName[id]] | |
| 29 | + | want := ExtInfo{true, e.major, e.firstEvent, e.firstError} | |
| 30 | + | if got := c.Ext().Info(id); got != want { | |
| 31 | + | t.Errorf("%s: %+v, want %+v", ExtXName[id], got, want) | |
| 32 | + | } | |
| 33 | + | } | |
| 34 | + | if id, rel, ok := c.Ext().ByEvent(88); !ok || id != ExtXFixes || rel != 1 { | |
| 35 | + | t.Fatal("reverse event map", id, rel, ok) | |
| 36 | + | } | |
| 37 | + | if c.buf.maxReq != 0x3fffff*4 { | |
| 38 | + | t.Fatalf("maxReq %d", c.buf.maxReq) | |
| 39 | + | } | |
| 40 | + | // One QueryExtension per extension, then Enable (queued once | |
| 41 | + | // BIG-REQUESTS answered), fences; nothing left registered. | |
| 42 | + | reqs := srv.requests() | |
| 43 | + | for i := range int(extCount) - 1 { | |
| 44 | + | if reqs[i].major != opQueryExtension { | |
| 45 | + | t.Fatalf("request %d major %d", i, reqs[i].major) | |
| 46 | + | } | |
| 47 | + | } | |
| 48 | + | var enables int | |
| 49 | + | for _, r := range reqs { | |
| 50 | + | if r.major == 133 { | |
| 51 | + | enables++ | |
| 52 | + | } | |
| 53 | + | } | |
| 54 | + | if enables != 1 || c.live != 0 || c.Stats().DroppedReplies != 0 { | |
| 55 | + | t.Fatalf("enables %d live %d %+v", enables, c.live, c.Stats()) | |
| 56 | + | } | |
| 57 | + | // A big request now encodes with the 8-byte header. | |
| 58 | + | if p, _ := c.Buf().Request(18, 0, coreMaxReq); p == nil { | |
| 59 | + | t.Fatal(c.Buf().Err()) | |
| 60 | + | } | |
| 61 | + | } | |
| 62 | + | ||
| 63 | + | func TestBootstrapAbsentExtensions(t *testing.T) { | |
| 64 | + | exts := fullExts() | |
| 65 | + | delete(exts, "BIG-REQUESTS") | |
| 66 | + | delete(exts, "Composite") | |
| 67 | + | c, srv := dialScript(t, &script{exts: exts}) | |
| 68 | + | if c.Ext().Info(ExtComposite).Present || c.Ext().Major(ExtBigRequests) != 0 { | |
| 69 | + | t.Fatal("absent extension marked present") | |
| 70 | + | } | |
| 71 | + | if !c.Ext().Info(ExtXFixes).Present { | |
| 72 | + | t.Fatal("XFIXES missing") | |
| 73 | + | } | |
| 74 | + | if c.buf.maxReq != coreMaxReq { | |
| 75 | + | t.Fatalf("maxReq %d without BIG-REQUESTS", c.buf.maxReq) | |
| 76 | + | } | |
| 77 | + | for _, r := range srv.requests() { | |
| 78 | + | if r.major >= 128 { | |
| 79 | + | t.Fatalf("extension request %d sent", r.major) | |
| 80 | + | } | |
| 81 | + | } | |
| 82 | + | if p, _ := c.Buf().Request(18, 0, coreMaxReq); p != nil || !errors.Is(c.Buf().Err(), ErrRequestTooLarge) { | |
| 83 | + | t.Fatal("big request without BIG-REQUESTS") | |
| 84 | + | } | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | func TestBootstrapErrorFailsConnect(t *testing.T) { | |
| 88 | + | fd, _ := startScript(t, &script{hook: func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool { | |
| 89 | + | if major == opQueryExtension { | |
| 90 | + | s.xerror(11, major, 0) | |
| 91 | + | return true | |
| 92 | + | } | |
| 93 | + | return false | |
| 94 | + | }}) | |
| 95 | + | c, err := NewConn(fd) | |
| 96 | + | var xe *XError | |
| 97 | + | if c != nil || !errors.As(err, &xe) || xe.Kind != kindQueryExtension || xe.Code != 11 { | |
| 98 | + | t.Fatalf("conn %v err %v", c != nil, err) | |
| 99 | + | } | |
| 100 | + | } | |
| 101 | + | ||
| 102 | + | func TestInternAtoms(t *testing.T) { | |
| 103 | + | sc := &script{atoms: map[string]uint32{"WM_STATE": 300, "_NET_WM_NAME": 301}} | |
| 104 | + | c, srv := dialScript(t, sc) | |
| 105 | + | before := len(srv.requests()) | |
| 106 | + | names := []string{"_NET_WM_NAME", "NEW_ONE", "WM_STATE", "NEW_TWO"} | |
| 107 | + | dst := make([]uint32, len(names)) | |
| 108 | + | if err := c.InternAtoms(names, false, dst); err != nil { | |
| 109 | + | t.Fatal(err) | |
| 110 | + | } | |
| 111 | + | if dst[0] != 301 || dst[2] != 300 || dst[1] == 0 || dst[3] == 0 || dst[1] == dst[3] { | |
| 112 | + | t.Fatal(dst) | |
| 113 | + | } | |
| 114 | + | if dst[1] != srv.atom("NEW_ONE") || dst[3] != srv.atom("NEW_TWO") { | |
| 115 | + | t.Fatal("atoms out of order", dst) | |
| 116 | + | } | |
| 117 | + | // Pipelined in name order, then one fence. | |
| 118 | + | reqs := srv.requests()[before:] | |
| 119 | + | if len(reqs) != len(names)+1 || reqs[len(names)].major != opGetInputFocus { | |
| 120 | + | t.Fatalf("%d requests", len(reqs)) | |
| 121 | + | } | |
| 122 | + | for i, r := range reqs[:len(names)] { | |
| 123 | + | if r.major != opInternAtom || string(r.raw[8:8+len(names[i])]) != names[i] { | |
| 124 | + | t.Fatalf("request %d: %x", i, r.raw) | |
| 125 | + | } | |
| 126 | + | } | |
| 127 | + | // onlyIfExists: unknown names give 0. | |
| 128 | + | dst2 := make([]uint32, 2) | |
| 129 | + | if err := c.InternAtoms([]string{"WM_STATE", "NOPE"}, true, dst2); err != nil || dst2[0] != 300 || dst2[1] != 0 { | |
| 130 | + | t.Fatal(err, dst2) | |
| 131 | + | } | |
| 132 | + | if err := c.InternAtoms(names, false, dst[:1]); !errors.Is(err, ErrBadLength) { | |
| 133 | + | t.Fatal(err) | |
| 134 | + | } | |
| 135 | + | if err := c.InternAtoms(nil, false, nil); err != nil { | |
| 136 | + | t.Fatal(err) | |
| 137 | + | } | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | func TestInternAtomsError(t *testing.T) { | |
| 141 | + | sc := &script{badAtoms: map[string]bool{"BAD": true}} | |
| 142 | + | c, _ := dialScript(t, sc) | |
| 143 | + | var userErrs int | |
| 144 | + | h := Handler{OnError: func(*XError) { userErrs++ }} | |
| 145 | + | if err := c.Dispatch(&h); err != nil { | |
| 146 | + | t.Fatal(err) | |
| 147 | + | } | |
| 148 | + | dst := make([]uint32, 3) | |
| 149 | + | err := c.InternAtoms([]string{"A", "BAD", "C"}, false, dst) | |
| 150 | + | var xe *XError | |
| 151 | + | if !errors.As(err, &xe) || xe.Code != 11 || xe.Args[0] != 1 || xe.Kind != kindInternAtom || xe.Major != opInternAtom { | |
| 152 | + | t.Fatalf("%v", err) | |
| 153 | + | } | |
| 154 | + | if dst[0] == 0 || dst[1] != 0 || dst[2] == 0 || userErrs != 0 { | |
| 155 | + | t.Fatal(dst, userErrs) | |
| 156 | + | } | |
| 157 | + | // The error does not stick to the next call. | |
| 158 | + | if err := c.InternAtoms([]string{"D"}, false, dst[:1]); err != nil { | |
| 159 | + | t.Fatal(err) | |
| 160 | + | } | |
| 161 | + | } | |
| 162 | + | ||
| 163 | + | func TestInternAtomsManyWrapsRing(t *testing.T) { | |
| 164 | + | c, _ := dialScript(t, &script{}) | |
| 165 | + | names := make([]string, 3*ringSize+7) | |
| 166 | + | for i := range names { | |
| 167 | + | names[i] = "ATOM_" + string(rune('A'+i%26)) + string(rune('a'+i/26%26)) + string(rune('0'+i/676)) | |
| 168 | + | } | |
| 169 | + | dst := make([]uint32, len(names)) | |
| 170 | + | if err := c.InternAtoms(names, false, dst); err != nil { | |
| 171 | + | t.Fatal(err) | |
| 172 | + | } | |
| 173 | + | seen := map[uint32]bool{} | |
| 174 | + | for i, a := range dst { | |
| 175 | + | if a == 0 || seen[a] { | |
| 176 | + | t.Fatalf("atom %d = %d", i, a) | |
| 177 | + | } | |
| 178 | + | seen[a] = true | |
| 179 | + | } | |
| 180 | + | } | |
| 181 | + | ||
| 182 | + | // Sync delivers user packets that arrive meanwhile to h, waits for user | |
| 183 | + | // registrations and keeps errors on reserved requests away from OnError. | |
| 184 | + | func TestSyncInterleavedEvents(t *testing.T) { | |
| 185 | + | const kindProbe = kindUser + 3 | |
| 186 | + | sc := &script{} | |
| 187 | + | sc.hook = func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool { | |
| 188 | + | switch major { | |
| 189 | + | case 8: // MapWindow: emit a MapNotify and a MapRequest | |
| 190 | + | s.event(19, 1) | |
| 191 | + | s.event(20, 2) | |
| 192 | + | return true | |
| 193 | + | case 14: // GetGeometry: answer late, after an event | |
| 194 | + | s.event(28, 3) | |
| 195 | + | s.reply(24, make([]byte, 16)) | |
| 196 | + | return true | |
| 197 | + | case 10: // UnmapWindow: user error | |
| 198 | + | s.xerror(3, major, 0x42) | |
| 199 | + | return true | |
| 200 | + | } | |
| 201 | + | return false | |
| 202 | + | } | |
| 203 | + | c, _ := dialScript(t, sc) | |
| 204 | + | var events []EventKind | |
| 205 | + | var replies []uint16 | |
| 206 | + | var errs []uint8 | |
| 207 | + | h := Handler{ | |
| 208 | + | OnEvent: func(ev *Event) { events = append(events, ev.Kind) }, | |
| 209 | + | OnReply: func(r *Reply) { replies = append(replies, r.Kind) }, | |
| 210 | + | OnError: func(e *XError) { errs = append(errs, e.Code) }, | |
| 211 | + | } | |
| 212 | + | b := c.Buf() | |
| 213 | + | p, _ := b.Request(8, 0, 4) | |
| 214 | + | binary.LittleEndian.PutUint32(p, 7) | |
| 215 | + | _, geom := b.Request(14, 0, 4) | |
| 216 | + | if err := c.Expect(geom, kindProbe, 0, 0, 0, 0); err != nil { | |
| 217 | + | t.Fatal(err) | |
| 218 | + | } | |
| 219 | + | b.Request(10, 0, 4) | |
| 220 | + | if err := c.Sync(&h); err != nil { | |
| 221 | + | t.Fatal(err) | |
| 222 | + | } | |
| 223 | + | if len(events) != 3 || events[0] != 19 || events[1] != 20 || events[2] != 28 { | |
| 224 | + | t.Fatal(events) | |
| 225 | + | } | |
| 226 | + | if len(replies) != 1 || replies[0] != kindProbe || len(errs) != 1 || errs[0] != 3 || c.live != 0 { | |
| 227 | + | t.Fatal(replies, errs, c.live) | |
| 228 | + | } | |
| 229 | + | } | |
| 230 | + | ||
| 231 | + | // A callback registering more requests during Sync gets them waited for too. | |
| 232 | + | func TestSyncFollowsCallbackRequests(t *testing.T) { | |
| 233 | + | const kindChain = kindUser + 4 | |
| 234 | + | sc := &script{hook: func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool { | |
| 235 | + | if major == 14 { | |
| 236 | + | s.reply(0, nil) | |
| 237 | + | return true | |
| 238 | + | } | |
| 239 | + | return false | |
| 240 | + | }} | |
| 241 | + | c, _ := dialScript(t, sc) | |
| 242 | + | chain := 0 | |
| 243 | + | var h Handler | |
| 244 | + | h.OnReply = func(r *Reply) { | |
| 245 | + | chain++ | |
| 246 | + | if chain < 3 { | |
| 247 | + | _, seq := c.Buf().Request(14, 0, 4) | |
| 248 | + | if err := c.Expect(seq, kindChain, 0, 0, 0, 0); err != nil { | |
| 249 | + | t.Error(err) | |
| 250 | + | } | |
| 251 | + | } | |
| 252 | + | } | |
| 253 | + | _, seq := c.Buf().Request(14, 0, 4) | |
| 254 | + | _ = c.Expect(seq, kindChain, 0, 0, 0, 0) | |
| 255 | + | if err := c.Sync(&h); err != nil { | |
| 256 | + | t.Fatal(err) | |
| 257 | + | } | |
| 258 | + | if chain != 3 || c.live != 0 { | |
| 259 | + | t.Fatal(chain, c.live) | |
| 260 | + | } | |
| 261 | + | } | |
| 262 | + | ||
| 263 | + | func TestSyncFromCallback(t *testing.T) { | |
| 264 | + | sc := &script{hook: func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool { | |
| 265 | + | if major == 8 { | |
| 266 | + | s.event(19, 0) | |
| 267 | + | return true | |
| 268 | + | } | |
| 269 | + | return false | |
| 270 | + | }} | |
| 271 | + | c, _ := dialScript(t, sc) | |
| 272 | + | var errSync, errAtoms, errXID error | |
| 273 | + | h := Handler{OnEvent: func(*Event) { | |
| 274 | + | errSync = c.Sync(nil) | |
| 275 | + | errAtoms = c.InternAtoms([]string{"X"}, false, make([]uint32, 1)) | |
| 276 | + | c.xids, c.xidFreeN = xidRange{}, 0 | |
| 277 | + | _, errXID = c.NewXID() | |
| 278 | + | }} | |
| 279 | + | c.Buf().Request(8, 0, 4) | |
| 280 | + | if err := c.Sync(&h); err != nil { | |
| 281 | + | t.Fatal(err) | |
| 282 | + | } | |
| 283 | + | if errSync != errReentrantDispatch || errAtoms != errReentrantDispatch || errXID != errReentrantDispatch { | |
| 284 | + | t.Fatal(errSync, errAtoms, errXID) | |
| 285 | + | } | |
| 286 | + | } | |
| 287 | + | ||
| 288 | + | func TestSyncClosed(t *testing.T) { | |
| 289 | + | c, _ := dialScript(t, &script{}) | |
| 290 | + | _ = c.Close() | |
| 291 | + | if err := c.Sync(nil); !errors.Is(err, ErrClosed) { | |
| 292 | + | t.Fatal(err) | |
| 293 | + | } | |
| 294 | + | if err := c.InternAtoms([]string{"A"}, false, make([]uint32, 1)); !errors.Is(err, ErrClosed) { | |
| 295 | + | t.Fatal(err) | |
| 296 | + | } | |
| 297 | + | } | |
| 298 | + | ||
| 299 | + | func TestNewXIDSetupRange(t *testing.T) { | |
| 300 | + | c, _ := dialScript(t, &script{}) | |
| 301 | + | // Recorded setup: base 0x00200000, mask 0x001fffff. | |
| 302 | + | seen := map[uint32]bool{} | |
| 303 | + | for range 1000 { | |
| 304 | + | id, err := c.NewXID() | |
| 305 | + | if err != nil { | |
| 306 | + | t.Fatal(err) | |
| 307 | + | } | |
| 308 | + | if id&^c.xidMask != c.xidBase || seen[id] || len(seen) == 0 && id != c.xidBase { | |
| 309 | + | t.Fatalf("id %#x", id) | |
| 310 | + | } | |
| 311 | + | seen[id] = true | |
| 312 | + | } | |
| 313 | + | // Freed ids are reused, last freed first; foreign ids are ignored. | |
| 314 | + | c.FreeXID(0x00200005) | |
| 315 | + | c.FreeXID(0x12345678) | |
| 316 | + | c.FreeXID(0) | |
| 317 | + | if id, _ := c.NewXID(); id != 0x00200005 { | |
| 318 | + | t.Fatalf("%#x", id) | |
| 319 | + | } | |
| 320 | + | if id, _ := c.NewXID(); seen[id] { | |
| 321 | + | t.Fatalf("reused %#x without FreeXID", id) | |
| 322 | + | } | |
| 323 | + | // The free list is bounded. | |
| 324 | + | for i := range xidFreeCap + 10 { | |
| 325 | + | c.FreeXID(c.xidBase | uint32(i+1)) | |
| 326 | + | } | |
| 327 | + | if c.xidFreeN != xidFreeCap { | |
| 328 | + | t.Fatal(c.xidFreeN) | |
| 329 | + | } | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | func TestNewXIDRefill(t *testing.T) { | |
| 333 | + | sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200040, 3}, {0, 1}}} | |
| 334 | + | c, srv := dialScript(t, sc) | |
| 335 | + | // Pretend only two ids of the setup range are left. | |
| 336 | + | c.xids.left = 2 | |
| 337 | + | var ids []uint32 | |
| 338 | + | for range 5 { | |
| 339 | + | id, err := c.NewXID() | |
| 340 | + | if err != nil { | |
| 341 | + | t.Fatal(len(ids), err) | |
| 342 | + | } | |
| 343 | + | ids = append(ids, id) | |
| 344 | + | } | |
| 345 | + | if ids[0] != 0x00200000 || ids[1] != 0x00200001 || ids[2] != 0x00200040 || ids[3] != 0x00200041 || ids[4] != 0x00200042 { | |
| 346 | + | t.Fatalf("%#x", ids) | |
| 347 | + | } | |
| 348 | + | var ranges int | |
| 349 | + | for _, r := range srv.requests() { | |
| 350 | + | if r.major == 136 && r.minor == minorGetXIDRange { | |
| 351 | + | ranges++ | |
| 352 | + | } | |
| 353 | + | } | |
| 354 | + | if ranges != 1 { | |
| 355 | + | t.Fatal("GetXIDRange requests", ranges) | |
| 356 | + | } | |
| 357 | + | // The server answers {0, 1}: no ids left. | |
| 358 | + | if _, err := c.NewXID(); !errors.Is(err, errXIDsExhausted) { | |
| 359 | + | t.Fatal(err) | |
| 360 | + | } | |
| 361 | + | // FreeXID still feeds NewXID. | |
| 362 | + | c.FreeXID(ids[0]) | |
| 363 | + | if id, err := c.NewXID(); err != nil || id != ids[0] { | |
| 364 | + | t.Fatal(id, err) | |
| 365 | + | } | |
| 366 | + | } | |
| 367 | + | ||
| 368 | + | func TestNewXIDNoXCMisc(t *testing.T) { | |
| 369 | + | c, _ := dialScript(t, &script{}) | |
| 370 | + | c.xids = xidRange{} | |
| 371 | + | if _, err := c.NewXID(); !errors.Is(err, ErrExtMissing) { | |
| 372 | + | t.Fatal(err) | |
| 373 | + | } | |
| 374 | + | } | |
| 375 | + | ||
| 376 | + | func TestNewXIDRefillError(t *testing.T) { | |
| 377 | + | sc := &script{exts: fullExts()} | |
| 378 | + | sc.hook = func(s *scriptConn, major, minor uint8, _ uint16, _ []byte) bool { | |
| 379 | + | if major == 136 && minor == minorGetXIDRange { | |
| 380 | + | s.xerror(11, major, 0) | |
| 381 | + | return true | |
| 382 | + | } | |
| 383 | + | return false | |
| 384 | + | } | |
| 385 | + | c, _ := dialScript(t, sc) | |
| 386 | + | c.xids = xidRange{} | |
| 387 | + | var xe *XError | |
| 388 | + | if _, err := c.NewXID(); !errors.As(err, &xe) || xe.Kind != kindXIDRange { | |
| 389 | + | t.Fatal(err) | |
| 390 | + | } | |
| 391 | + | } | |
| 392 | + | ||
| 393 | + | func TestAllocXID(t *testing.T) { | |
| 394 | + | c, _ := dialScript(t, &script{}) | |
| 395 | + | if n := testing.AllocsPerRun(1000, func() { | |
| 396 | + | id, err := c.NewXID() | |
| 397 | + | if err != nil { | |
| 398 | + | t.Fatal(err) | |
| 399 | + | } | |
| 400 | + | c.FreeXID(id) | |
| 401 | + | if again, _ := c.NewXID(); again != id { | |
| 402 | + | t.Fatal("free list") | |
| 403 | + | } | |
| 404 | + | }); n != 0 { | |
| 405 | + | t.Fatalf("NewXID/FreeXID allocate %v times", n) | |
| 406 | + | } | |
| 407 | + | } | |
| 408 | + | ||
| 409 | + | // A long silent stream (no replies, no events) gets a GetInputFocus appended | |
| 410 | + | // by Flush once Next-acked reaches resyncAt; its reply is consumed. | |
| 411 | + | func TestFlushAutoResync(t *testing.T) { | |
| 412 | + | c, srv := dialScript(t, &script{}) | |
| 413 | + | base := len(srv.requests()) | |
| 414 | + | start := c.Buf().Next() | |
| 415 | + | b := c.Buf() | |
| 416 | + | if b.acked != start-1 { | |
| 417 | + | t.Fatal("bootstrap not acknowledged", b.acked, start) | |
| 418 | + | } | |
| 419 | + | // k requests after the last acknowledged one: Next-acked = k+1. | |
| 420 | + | for range resyncAt - 2 { | |
| 421 | + | b.Request(8, 0, 4) // MapWindow, no reply | |
| 422 | + | } | |
| 423 | + | if err := flushAll(c); err != nil { | |
| 424 | + | t.Fatal(err) | |
| 425 | + | } | |
| 426 | + | if c.Stats().Resyncs != 0 { | |
| 427 | + | t.Fatal("resync below threshold", b.Next()-b.acked) | |
| 428 | + | } | |
| 429 | + | b.Request(8, 0, 4) | |
| 430 | + | if err := flushAll(c); err != nil { | |
| 431 | + | t.Fatal(err) | |
| 432 | + | } | |
| 433 | + | if c.Stats().Resyncs != 1 { | |
| 434 | + | t.Fatal(c.Stats()) | |
| 435 | + | } | |
| 436 | + | // Further Flushes do not stack fences while one is in flight. | |
| 437 | + | b.Request(8, 0, 4) | |
| 438 | + | if err := flushAll(c); err != nil { | |
| 439 | + | t.Fatal(err) | |
| 440 | + | } | |
| 441 | + | if c.Stats().Resyncs != 1 { | |
| 442 | + | t.Fatal("second fence while one in flight") | |
| 443 | + | } | |
| 444 | + | total := int(b.Next()-start) + base | |
| 445 | + | waitRequests(t, srv, total) | |
| 446 | + | fenceSeq := start + resyncAt - 1 | |
| 447 | + | fence := srv.requests()[base+resyncAt-1] | |
| 448 | + | if fence.major != opGetInputFocus || Seq(fence.seq) != fenceSeq&0xffff { | |
| 449 | + | t.Fatalf("fence %+v", fence) | |
| 450 | + | } | |
| 451 | + | // Its reply acknowledges the stream: no slot left, nothing dropped. | |
| 452 | + | dispatchFor(t, c, func() bool { return b.acked >= fenceSeq }) | |
| 453 | + | if c.live != 0 || c.Stats().DroppedReplies != 0 { | |
| 454 | + | t.Fatal(c.live, c.Stats()) | |
| 455 | + | } | |
| 456 | + | // Past the threshold again (relative to the new ack) a new fence goes out. | |
| 457 | + | for range resyncAt { | |
| 458 | + | b.Request(8, 0, 4) | |
| 459 | + | } | |
| 460 | + | if err := flushAll(c); err != nil { | |
| 461 | + | t.Fatal(err) | |
| 462 | + | } | |
| 463 | + | if c.Stats().Resyncs != 2 { | |
| 464 | + | t.Fatal(c.Stats()) | |
| 465 | + | } | |
| 466 | + | } | |
| 467 | + | ||
| 468 | + | // flushAll flushes, dispatching (to drain replies) while the socket is full. | |
| 469 | + | func flushAll(c *Conn) error { | |
| 470 | + | for { | |
| 471 | + | err := c.Flush() | |
| 472 | + | if err != ErrWouldBlock { | |
| 473 | + | return err | |
| 474 | + | } | |
| 475 | + | if err := c.Dispatch(nil); err != nil { | |
| 476 | + | return err | |
| 477 | + | } | |
| 478 | + | time.Sleep(100 * time.Microsecond) | |
| 479 | + | } | |
| 480 | + | } | |
| 481 | + | ||
| 482 | + | func dispatchFor(t *testing.T, c *Conn, done func() bool) { | |
| 483 | + | t.Helper() | |
| 484 | + | deadline := time.Now().Add(5 * time.Second) | |
| 485 | + | for !done() { | |
| 486 | + | if time.Now().After(deadline) { | |
| 487 | + | t.Fatal("timeout") | |
| 488 | + | } | |
| 489 | + | if err := c.Dispatch(nil); err != nil { | |
| 490 | + | t.Fatal(err) | |
| 491 | + | } | |
| 492 | + | time.Sleep(time.Millisecond) | |
| 493 | + | } | |
| 494 | + | } | |
| 495 | + | ||
| 496 | + | // Steady state: Flush with an auto-resync fence does not allocate. | |
| 497 | + | func TestAllocFlushAutoResync(t *testing.T) { | |
| 498 | + | c, srv := dispatchConn(t) | |
| 499 | + | sink := make([]byte, 64<<10) | |
| 500 | + | b := c.buf | |
| 501 | + | b.next, b.acked = 1, 0 | |
| 502 | + | run := func() { | |
| 503 | + | // Pretend the server acknowledged everything so far, then queue | |
| 504 | + | // resyncAt requests in one go (no reply ever comes back here). | |
| 505 | + | b.acked = b.next - 1 | |
| 506 | + | c.resync = 0 | |
| 507 | + | for range resyncAt { | |
| 508 | + | b.Request(8, 0, 4) | |
| 509 | + | } | |
| 510 | + | for c.Flush() == ErrWouldBlock { | |
| 511 | + | _, _ = unix.Read(srv, sink) | |
| 512 | + | } | |
| 513 | + | for { | |
| 514 | + | if n, _ := unix.Read(srv, sink); n <= 0 { | |
| 515 | + | break | |
| 516 | + | } | |
| 517 | + | } | |
| 518 | + | c.ring[c.resync&ringMask] = slot{} | |
| 519 | + | c.live = 0 | |
| 520 | + | } | |
| 521 | + | run() | |
| 522 | + | before := c.Stats().Resyncs | |
| 523 | + | if n := testing.AllocsPerRun(20, run); n != 0 { | |
| 524 | + | t.Fatalf("Flush with resync allocates %v times", n) | |
| 525 | + | } | |
| 526 | + | if c.Stats().Resyncs-before < 20 { | |
| 527 | + | t.Fatal(c.Stats()) | |
| 528 | + | } | |
| 529 | + | } | |
| @@ -0,0 +1,529 @@ | |||||
| 1 | + | package xturbo | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "errors" | |||
| 6 | + | "testing" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "golang.org/x/sys/unix" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // waitRequests waits until the scripted server has read n requests. | |||
| 13 | + | func waitRequests(t *testing.T, srv *scriptConn, n int) { | |||
| 14 | + | t.Helper() | |||
| 15 | + | deadline := time.Now().Add(5 * time.Second) | |||
| 16 | + | for len(srv.requests()) < n { | |||
| 17 | + | if time.Now().After(deadline) { | |||
| 18 | + | t.Fatalf("server saw %d requests, want %d", len(srv.requests()), n) | |||
| 19 | + | } | |||
| 20 | + | time.Sleep(time.Millisecond) | |||
| 21 | + | } | |||
| 22 | + | } | |||
| 23 | + | ||||
| 24 | + | func TestBootstrapAllExtensions(t *testing.T) { | |||
| 25 | + | exts := fullExts() | |||
| 26 | + | c, srv := dialScript(t, &script{exts: exts, bigreqMax: 0x3fffff}) | |||
| 27 | + | for id := ExtCore + 1; id < extCount; id++ { | |||
| 28 | + | e := exts[ExtXName[id]] | |||
| 29 | + | want := ExtInfo{true, e.major, e.firstEvent, e.firstError} | |||
| 30 | + | if got := c.Ext().Info(id); got != want { | |||
| 31 | + | t.Errorf("%s: %+v, want %+v", ExtXName[id], got, want) | |||
| 32 | + | } | |||
| 33 | + | } | |||
| 34 | + | if id, rel, ok := c.Ext().ByEvent(88); !ok || id != ExtXFixes || rel != 1 { | |||
| 35 | + | t.Fatal("reverse event map", id, rel, ok) | |||
| 36 | + | } | |||
| 37 | + | if c.buf.maxReq != 0x3fffff*4 { | |||
| 38 | + | t.Fatalf("maxReq %d", c.buf.maxReq) | |||
| 39 | + | } | |||
| 40 | + | // One QueryExtension per extension, then Enable (queued once | |||
| 41 | + | // BIG-REQUESTS answered), fences; nothing left registered. | |||
| 42 | + | reqs := srv.requests() | |||
| 43 | + | for i := range int(extCount) - 1 { | |||
| 44 | + | if reqs[i].major != opQueryExtension { | |||
| 45 | + | t.Fatalf("request %d major %d", i, reqs[i].major) | |||
| 46 | + | } | |||
| 47 | + | } | |||
| 48 | + | var enables int | |||
| 49 | + | for _, r := range reqs { | |||
| 50 | + | if r.major == 133 { | |||
| 51 | + | enables++ | |||
| 52 | + | } | |||
| 53 | + | } | |||
| 54 | + | if enables != 1 || c.live != 0 || c.Stats().DroppedReplies != 0 { | |||
| 55 | + | t.Fatalf("enables %d live %d %+v", enables, c.live, c.Stats()) | |||
| 56 | + | } | |||
| 57 | + | // A big request now encodes with the 8-byte header. | |||
| 58 | + | if p, _ := c.Buf().Request(18, 0, coreMaxReq); p == nil { | |||
| 59 | + | t.Fatal(c.Buf().Err()) | |||
| 60 | + | } | |||
| 61 | + | } | |||
| 62 | + | ||||
| 63 | + | func TestBootstrapAbsentExtensions(t *testing.T) { | |||
| 64 | + | exts := fullExts() | |||
| 65 | + | delete(exts, "BIG-REQUESTS") | |||
| 66 | + | delete(exts, "Composite") | |||
| 67 | + | c, srv := dialScript(t, &script{exts: exts}) | |||
| 68 | + | if c.Ext().Info(ExtComposite).Present || c.Ext().Major(ExtBigRequests) != 0 { | |||
| 69 | + | t.Fatal("absent extension marked present") | |||
| 70 | + | } | |||
| 71 | + | if !c.Ext().Info(ExtXFixes).Present { | |||
| 72 | + | t.Fatal("XFIXES missing") | |||
| 73 | + | } | |||
| 74 | + | if c.buf.maxReq != coreMaxReq { | |||
| 75 | + | t.Fatalf("maxReq %d without BIG-REQUESTS", c.buf.maxReq) | |||
| 76 | + | } | |||
| 77 | + | for _, r := range srv.requests() { | |||
| 78 | + | if r.major >= 128 { | |||
| 79 | + | t.Fatalf("extension request %d sent", r.major) | |||
| 80 | + | } | |||
| 81 | + | } | |||
| 82 | + | if p, _ := c.Buf().Request(18, 0, coreMaxReq); p != nil || !errors.Is(c.Buf().Err(), ErrRequestTooLarge) { | |||
| 83 | + | t.Fatal("big request without BIG-REQUESTS") | |||
| 84 | + | } | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | func TestBootstrapErrorFailsConnect(t *testing.T) { | |||
| 88 | + | fd, _ := startScript(t, &script{hook: func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool { | |||
| 89 | + | if major == opQueryExtension { | |||
| 90 | + | s.xerror(11, major, 0) | |||
| 91 | + | return true | |||
| 92 | + | } | |||
| 93 | + | return false | |||
| 94 | + | }}) | |||
| 95 | + | c, err := NewConn(fd) | |||
| 96 | + | var xe *XError | |||
| 97 | + | if c != nil || !errors.As(err, &xe) || xe.Kind != kindQueryExtension || xe.Code != 11 { | |||
| 98 | + | t.Fatalf("conn %v err %v", c != nil, err) | |||
| 99 | + | } | |||
| 100 | + | } | |||
| 101 | + | ||||
| 102 | + | func TestInternAtoms(t *testing.T) { | |||
| 103 | + | sc := &script{atoms: map[string]uint32{"WM_STATE": 300, "_NET_WM_NAME": 301}} | |||
| 104 | + | c, srv := dialScript(t, sc) | |||
| 105 | + | before := len(srv.requests()) | |||
| 106 | + | names := []string{"_NET_WM_NAME", "NEW_ONE", "WM_STATE", "NEW_TWO"} | |||
| 107 | + | dst := make([]uint32, len(names)) | |||
| 108 | + | if err := c.InternAtoms(names, false, dst); err != nil { | |||
| 109 | + | t.Fatal(err) | |||
| 110 | + | } | |||
| 111 | + | if dst[0] != 301 || dst[2] != 300 || dst[1] == 0 || dst[3] == 0 || dst[1] == dst[3] { | |||
| 112 | + | t.Fatal(dst) | |||
| 113 | + | } | |||
| 114 | + | if dst[1] != srv.atom("NEW_ONE") || dst[3] != srv.atom("NEW_TWO") { | |||
| 115 | + | t.Fatal("atoms out of order", dst) | |||
| 116 | + | } | |||
| 117 | + | // Pipelined in name order, then one fence. | |||
| 118 | + | reqs := srv.requests()[before:] | |||
| 119 | + | if len(reqs) != len(names)+1 || reqs[len(names)].major != opGetInputFocus { | |||
| 120 | + | t.Fatalf("%d requests", len(reqs)) | |||
| 121 | + | } | |||
| 122 | + | for i, r := range reqs[:len(names)] { | |||
| 123 | + | if r.major != opInternAtom || string(r.raw[8:8+len(names[i])]) != names[i] { | |||
| 124 | + | t.Fatalf("request %d: %x", i, r.raw) | |||
| 125 | + | } | |||
| 126 | + | } | |||
| 127 | + | // onlyIfExists: unknown names give 0. | |||
| 128 | + | dst2 := make([]uint32, 2) | |||
| 129 | + | if err := c.InternAtoms([]string{"WM_STATE", "NOPE"}, true, dst2); err != nil || dst2[0] != 300 || dst2[1] != 0 { | |||
| 130 | + | t.Fatal(err, dst2) | |||
| 131 | + | } | |||
| 132 | + | if err := c.InternAtoms(names, false, dst[:1]); !errors.Is(err, ErrBadLength) { | |||
| 133 | + | t.Fatal(err) | |||
| 134 | + | } | |||
| 135 | + | if err := c.InternAtoms(nil, false, nil); err != nil { | |||
| 136 | + | t.Fatal(err) | |||
| 137 | + | } | |||
| 138 | + | } | |||
| 139 | + | ||||
| 140 | + | func TestInternAtomsError(t *testing.T) { | |||
| 141 | + | sc := &script{badAtoms: map[string]bool{"BAD": true}} | |||
| 142 | + | c, _ := dialScript(t, sc) | |||
| 143 | + | var userErrs int | |||
| 144 | + | h := Handler{OnError: func(*XError) { userErrs++ }} | |||
| 145 | + | if err := c.Dispatch(&h); err != nil { | |||
| 146 | + | t.Fatal(err) | |||
| 147 | + | } | |||
| 148 | + | dst := make([]uint32, 3) | |||
| 149 | + | err := c.InternAtoms([]string{"A", "BAD", "C"}, false, dst) | |||
| 150 | + | var xe *XError | |||
| 151 | + | if !errors.As(err, &xe) || xe.Code != 11 || xe.Args[0] != 1 || xe.Kind != kindInternAtom || xe.Major != opInternAtom { | |||
| 152 | + | t.Fatalf("%v", err) | |||
| 153 | + | } | |||
| 154 | + | if dst[0] == 0 || dst[1] != 0 || dst[2] == 0 || userErrs != 0 { | |||
| 155 | + | t.Fatal(dst, userErrs) | |||
| 156 | + | } | |||
| 157 | + | // The error does not stick to the next call. | |||
| 158 | + | if err := c.InternAtoms([]string{"D"}, false, dst[:1]); err != nil { | |||
| 159 | + | t.Fatal(err) | |||
| 160 | + | } | |||
| 161 | + | } | |||
| 162 | + | ||||
| 163 | + | func TestInternAtomsManyWrapsRing(t *testing.T) { | |||
| 164 | + | c, _ := dialScript(t, &script{}) | |||
| 165 | + | names := make([]string, 3*ringSize+7) | |||
| 166 | + | for i := range names { | |||
| 167 | + | names[i] = "ATOM_" + string(rune('A'+i%26)) + string(rune('a'+i/26%26)) + string(rune('0'+i/676)) | |||
| 168 | + | } | |||
| 169 | + | dst := make([]uint32, len(names)) | |||
| 170 | + | if err := c.InternAtoms(names, false, dst); err != nil { | |||
| 171 | + | t.Fatal(err) | |||
| 172 | + | } | |||
| 173 | + | seen := map[uint32]bool{} | |||
| 174 | + | for i, a := range dst { | |||
| 175 | + | if a == 0 || seen[a] { | |||
| 176 | + | t.Fatalf("atom %d = %d", i, a) | |||
| 177 | + | } | |||
| 178 | + | seen[a] = true | |||
| 179 | + | } | |||
| 180 | + | } | |||
| 181 | + | ||||
| 182 | + | // Sync delivers user packets that arrive meanwhile to h, waits for user | |||
| 183 | + | // registrations and keeps errors on reserved requests away from OnError. | |||
| 184 | + | func TestSyncInterleavedEvents(t *testing.T) { | |||
| 185 | + | const kindProbe = kindUser + 3 | |||
| 186 | + | sc := &script{} | |||
| 187 | + | sc.hook = func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool { | |||
| 188 | + | switch major { | |||
| 189 | + | case 8: // MapWindow: emit a MapNotify and a MapRequest | |||
| 190 | + | s.event(19, 1) | |||
| 191 | + | s.event(20, 2) | |||
| 192 | + | return true | |||
| 193 | + | case 14: // GetGeometry: answer late, after an event | |||
| 194 | + | s.event(28, 3) | |||
| 195 | + | s.reply(24, make([]byte, 16)) | |||
| 196 | + | return true | |||
| 197 | + | case 10: // UnmapWindow: user error | |||
| 198 | + | s.xerror(3, major, 0x42) | |||
| 199 | + | return true | |||
| 200 | + | } | |||
| 201 | + | return false | |||
| 202 | + | } | |||
| 203 | + | c, _ := dialScript(t, sc) | |||
| 204 | + | var events []EventKind | |||
| 205 | + | var replies []uint16 | |||
| 206 | + | var errs []uint8 | |||
| 207 | + | h := Handler{ | |||
| 208 | + | OnEvent: func(ev *Event) { events = append(events, ev.Kind) }, | |||
| 209 | + | OnReply: func(r *Reply) { replies = append(replies, r.Kind) }, | |||
| 210 | + | OnError: func(e *XError) { errs = append(errs, e.Code) }, | |||
| 211 | + | } | |||
| 212 | + | b := c.Buf() | |||
| 213 | + | p, _ := b.Request(8, 0, 4) | |||
| 214 | + | binary.LittleEndian.PutUint32(p, 7) | |||
| 215 | + | _, geom := b.Request(14, 0, 4) | |||
| 216 | + | if err := c.Expect(geom, kindProbe, 0, 0, 0, 0); err != nil { | |||
| 217 | + | t.Fatal(err) | |||
| 218 | + | } | |||
| 219 | + | b.Request(10, 0, 4) | |||
| 220 | + | if err := c.Sync(&h); err != nil { | |||
| 221 | + | t.Fatal(err) | |||
| 222 | + | } | |||
| 223 | + | if len(events) != 3 || events[0] != 19 || events[1] != 20 || events[2] != 28 { | |||
| 224 | + | t.Fatal(events) | |||
| 225 | + | } | |||
| 226 | + | if len(replies) != 1 || replies[0] != kindProbe || len(errs) != 1 || errs[0] != 3 || c.live != 0 { | |||
| 227 | + | t.Fatal(replies, errs, c.live) | |||
| 228 | + | } | |||
| 229 | + | } | |||
| 230 | + | ||||
| 231 | + | // A callback registering more requests during Sync gets them waited for too. | |||
| 232 | + | func TestSyncFollowsCallbackRequests(t *testing.T) { | |||
| 233 | + | const kindChain = kindUser + 4 | |||
| 234 | + | sc := &script{hook: func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool { | |||
| 235 | + | if major == 14 { | |||
| 236 | + | s.reply(0, nil) | |||
| 237 | + | return true | |||
| 238 | + | } | |||
| 239 | + | return false | |||
| 240 | + | }} | |||
| 241 | + | c, _ := dialScript(t, sc) | |||
| 242 | + | chain := 0 | |||
| 243 | + | var h Handler | |||
| 244 | + | h.OnReply = func(r *Reply) { | |||
| 245 | + | chain++ | |||
| 246 | + | if chain < 3 { | |||
| 247 | + | _, seq := c.Buf().Request(14, 0, 4) | |||
| 248 | + | if err := c.Expect(seq, kindChain, 0, 0, 0, 0); err != nil { | |||
| 249 | + | t.Error(err) | |||
| 250 | + | } | |||
| 251 | + | } | |||
| 252 | + | } | |||
| 253 | + | _, seq := c.Buf().Request(14, 0, 4) | |||
| 254 | + | _ = c.Expect(seq, kindChain, 0, 0, 0, 0) | |||
| 255 | + | if err := c.Sync(&h); err != nil { | |||
| 256 | + | t.Fatal(err) | |||
| 257 | + | } | |||
| 258 | + | if chain != 3 || c.live != 0 { | |||
| 259 | + | t.Fatal(chain, c.live) | |||
| 260 | + | } | |||
| 261 | + | } | |||
| 262 | + | ||||
| 263 | + | func TestSyncFromCallback(t *testing.T) { | |||
| 264 | + | sc := &script{hook: func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool { | |||
| 265 | + | if major == 8 { | |||
| 266 | + | s.event(19, 0) | |||
| 267 | + | return true | |||
| 268 | + | } | |||
| 269 | + | return false | |||
| 270 | + | }} | |||
| 271 | + | c, _ := dialScript(t, sc) | |||
| 272 | + | var errSync, errAtoms, errXID error | |||
| 273 | + | h := Handler{OnEvent: func(*Event) { | |||
| 274 | + | errSync = c.Sync(nil) | |||
| 275 | + | errAtoms = c.InternAtoms([]string{"X"}, false, make([]uint32, 1)) | |||
| 276 | + | c.xids, c.xidFreeN = xidRange{}, 0 | |||
| 277 | + | _, errXID = c.NewXID() | |||
| 278 | + | }} | |||
| 279 | + | c.Buf().Request(8, 0, 4) | |||
| 280 | + | if err := c.Sync(&h); err != nil { | |||
| 281 | + | t.Fatal(err) | |||
| 282 | + | } | |||
| 283 | + | if errSync != errReentrantDispatch || errAtoms != errReentrantDispatch || errXID != errReentrantDispatch { | |||
| 284 | + | t.Fatal(errSync, errAtoms, errXID) | |||
| 285 | + | } | |||
| 286 | + | } | |||
| 287 | + | ||||
| 288 | + | func TestSyncClosed(t *testing.T) { | |||
| 289 | + | c, _ := dialScript(t, &script{}) | |||
| 290 | + | _ = c.Close() | |||
| 291 | + | if err := c.Sync(nil); !errors.Is(err, ErrClosed) { | |||
| 292 | + | t.Fatal(err) | |||
| 293 | + | } | |||
| 294 | + | if err := c.InternAtoms([]string{"A"}, false, make([]uint32, 1)); !errors.Is(err, ErrClosed) { | |||
| 295 | + | t.Fatal(err) | |||
| 296 | + | } | |||
| 297 | + | } | |||
| 298 | + | ||||
| 299 | + | func TestNewXIDSetupRange(t *testing.T) { | |||
| 300 | + | c, _ := dialScript(t, &script{}) | |||
| 301 | + | // Recorded setup: base 0x00200000, mask 0x001fffff. | |||
| 302 | + | seen := map[uint32]bool{} | |||
| 303 | + | for range 1000 { | |||
| 304 | + | id, err := c.NewXID() | |||
| 305 | + | if err != nil { | |||
| 306 | + | t.Fatal(err) | |||
| 307 | + | } | |||
| 308 | + | if id&^c.xidMask != c.xidBase || seen[id] || len(seen) == 0 && id != c.xidBase { | |||
| 309 | + | t.Fatalf("id %#x", id) | |||
| 310 | + | } | |||
| 311 | + | seen[id] = true | |||
| 312 | + | } | |||
| 313 | + | // Freed ids are reused, last freed first; foreign ids are ignored. | |||
| 314 | + | c.FreeXID(0x00200005) | |||
| 315 | + | c.FreeXID(0x12345678) | |||
| 316 | + | c.FreeXID(0) | |||
| 317 | + | if id, _ := c.NewXID(); id != 0x00200005 { | |||
| 318 | + | t.Fatalf("%#x", id) | |||
| 319 | + | } | |||
| 320 | + | if id, _ := c.NewXID(); seen[id] { | |||
| 321 | + | t.Fatalf("reused %#x without FreeXID", id) | |||
| 322 | + | } | |||
| 323 | + | // The free list is bounded. | |||
| 324 | + | for i := range xidFreeCap + 10 { | |||
| 325 | + | c.FreeXID(c.xidBase | uint32(i+1)) | |||
| 326 | + | } | |||
| 327 | + | if c.xidFreeN != xidFreeCap { | |||
| 328 | + | t.Fatal(c.xidFreeN) | |||
| 329 | + | } | |||
| 330 | + | } | |||
| 331 | + | ||||
| 332 | + | func TestNewXIDRefill(t *testing.T) { | |||
| 333 | + | sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200040, 3}, {0, 1}}} | |||
| 334 | + | c, srv := dialScript(t, sc) | |||
| 335 | + | // Pretend only two ids of the setup range are left. | |||
| 336 | + | c.xids.left = 2 | |||
| 337 | + | var ids []uint32 | |||
| 338 | + | for range 5 { | |||
| 339 | + | id, err := c.NewXID() | |||
| 340 | + | if err != nil { | |||
| 341 | + | t.Fatal(len(ids), err) | |||
| 342 | + | } | |||
| 343 | + | ids = append(ids, id) | |||
| 344 | + | } | |||
| 345 | + | if ids[0] != 0x00200000 || ids[1] != 0x00200001 || ids[2] != 0x00200040 || ids[3] != 0x00200041 || ids[4] != 0x00200042 { | |||
| 346 | + | t.Fatalf("%#x", ids) | |||
| 347 | + | } | |||
| 348 | + | var ranges int | |||
| 349 | + | for _, r := range srv.requests() { | |||
| 350 | + | if r.major == 136 && r.minor == minorGetXIDRange { | |||
| 351 | + | ranges++ | |||
| 352 | + | } | |||
| 353 | + | } | |||
| 354 | + | if ranges != 1 { | |||
| 355 | + | t.Fatal("GetXIDRange requests", ranges) | |||
| 356 | + | } | |||
| 357 | + | // The server answers {0, 1}: no ids left. | |||
| 358 | + | if _, err := c.NewXID(); !errors.Is(err, errXIDsExhausted) { | |||
| 359 | + | t.Fatal(err) | |||
| 360 | + | } | |||
| 361 | + | // FreeXID still feeds NewXID. | |||
| 362 | + | c.FreeXID(ids[0]) | |||
| 363 | + | if id, err := c.NewXID(); err != nil || id != ids[0] { | |||
| 364 | + | t.Fatal(id, err) | |||
| 365 | + | } | |||
| 366 | + | } | |||
| 367 | + | ||||
| 368 | + | func TestNewXIDNoXCMisc(t *testing.T) { | |||
| 369 | + | c, _ := dialScript(t, &script{}) | |||
| 370 | + | c.xids = xidRange{} | |||
| 371 | + | if _, err := c.NewXID(); !errors.Is(err, ErrExtMissing) { | |||
| 372 | + | t.Fatal(err) | |||
| 373 | + | } | |||
| 374 | + | } | |||
| 375 | + | ||||
| 376 | + | func TestNewXIDRefillError(t *testing.T) { | |||
| 377 | + | sc := &script{exts: fullExts()} | |||
| 378 | + | sc.hook = func(s *scriptConn, major, minor uint8, _ uint16, _ []byte) bool { | |||
| 379 | + | if major == 136 && minor == minorGetXIDRange { | |||
| 380 | + | s.xerror(11, major, 0) | |||
| 381 | + | return true | |||
| 382 | + | } | |||
| 383 | + | return false | |||
| 384 | + | } | |||
| 385 | + | c, _ := dialScript(t, sc) | |||
| 386 | + | c.xids = xidRange{} | |||
| 387 | + | var xe *XError | |||
| 388 | + | if _, err := c.NewXID(); !errors.As(err, &xe) || xe.Kind != kindXIDRange { | |||
| 389 | + | t.Fatal(err) | |||
| 390 | + | } | |||
| 391 | + | } | |||
| 392 | + | ||||
| 393 | + | func TestAllocXID(t *testing.T) { | |||
| 394 | + | c, _ := dialScript(t, &script{}) | |||
| 395 | + | if n := testing.AllocsPerRun(1000, func() { | |||
| 396 | + | id, err := c.NewXID() | |||
| 397 | + | if err != nil { | |||
| 398 | + | t.Fatal(err) | |||
| 399 | + | } | |||
| 400 | + | c.FreeXID(id) | |||
| 401 | + | if again, _ := c.NewXID(); again != id { | |||
| 402 | + | t.Fatal("free list") | |||
| 403 | + | } | |||
| 404 | + | }); n != 0 { | |||
| 405 | + | t.Fatalf("NewXID/FreeXID allocate %v times", n) | |||
| 406 | + | } | |||
| 407 | + | } | |||
| 408 | + | ||||
| 409 | + | // A long silent stream (no replies, no events) gets a GetInputFocus appended | |||
| 410 | + | // by Flush once Next-acked reaches resyncAt; its reply is consumed. | |||
| 411 | + | func TestFlushAutoResync(t *testing.T) { | |||
| 412 | + | c, srv := dialScript(t, &script{}) | |||
| 413 | + | base := len(srv.requests()) | |||
| 414 | + | start := c.Buf().Next() | |||
| 415 | + | b := c.Buf() | |||
| 416 | + | if b.acked != start-1 { | |||
| 417 | + | t.Fatal("bootstrap not acknowledged", b.acked, start) | |||
| 418 | + | } | |||
| 419 | + | // k requests after the last acknowledged one: Next-acked = k+1. | |||
| 420 | + | for range resyncAt - 2 { | |||
| 421 | + | b.Request(8, 0, 4) // MapWindow, no reply | |||
| 422 | + | } | |||
| 423 | + | if err := flushAll(c); err != nil { | |||
| 424 | + | t.Fatal(err) | |||
| 425 | + | } | |||
| 426 | + | if c.Stats().Resyncs != 0 { | |||
| 427 | + | t.Fatal("resync below threshold", b.Next()-b.acked) | |||
| 428 | + | } | |||
| 429 | + | b.Request(8, 0, 4) | |||
| 430 | + | if err := flushAll(c); err != nil { | |||
| 431 | + | t.Fatal(err) | |||
| 432 | + | } | |||
| 433 | + | if c.Stats().Resyncs != 1 { | |||
| 434 | + | t.Fatal(c.Stats()) | |||
| 435 | + | } | |||
| 436 | + | // Further Flushes do not stack fences while one is in flight. | |||
| 437 | + | b.Request(8, 0, 4) | |||
| 438 | + | if err := flushAll(c); err != nil { | |||
| 439 | + | t.Fatal(err) | |||
| 440 | + | } | |||
| 441 | + | if c.Stats().Resyncs != 1 { | |||
| 442 | + | t.Fatal("second fence while one in flight") | |||
| 443 | + | } | |||
| 444 | + | total := int(b.Next()-start) + base | |||
| 445 | + | waitRequests(t, srv, total) | |||
| 446 | + | fenceSeq := start + resyncAt - 1 | |||
| 447 | + | fence := srv.requests()[base+resyncAt-1] | |||
| 448 | + | if fence.major != opGetInputFocus || Seq(fence.seq) != fenceSeq&0xffff { | |||
| 449 | + | t.Fatalf("fence %+v", fence) | |||
| 450 | + | } | |||
| 451 | + | // Its reply acknowledges the stream: no slot left, nothing dropped. | |||
| 452 | + | dispatchFor(t, c, func() bool { return b.acked >= fenceSeq }) | |||
| 453 | + | if c.live != 0 || c.Stats().DroppedReplies != 0 { | |||
| 454 | + | t.Fatal(c.live, c.Stats()) | |||
| 455 | + | } | |||
| 456 | + | // Past the threshold again (relative to the new ack) a new fence goes out. | |||
| 457 | + | for range resyncAt { | |||
| 458 | + | b.Request(8, 0, 4) | |||
| 459 | + | } | |||
| 460 | + | if err := flushAll(c); err != nil { | |||
| 461 | + | t.Fatal(err) | |||
| 462 | + | } | |||
| 463 | + | if c.Stats().Resyncs != 2 { | |||
| 464 | + | t.Fatal(c.Stats()) | |||
| 465 | + | } | |||
| 466 | + | } | |||
| 467 | + | ||||
| 468 | + | // flushAll flushes, dispatching (to drain replies) while the socket is full. | |||
| 469 | + | func flushAll(c *Conn) error { | |||
| 470 | + | for { | |||
| 471 | + | err := c.Flush() | |||
| 472 | + | if err != ErrWouldBlock { | |||
| 473 | + | return err | |||
| 474 | + | } | |||
| 475 | + | if err := c.Dispatch(nil); err != nil { | |||
| 476 | + | return err | |||
| 477 | + | } | |||
| 478 | + | time.Sleep(100 * time.Microsecond) | |||
| 479 | + | } | |||
| 480 | + | } | |||
| 481 | + | ||||
| 482 | + | func dispatchFor(t *testing.T, c *Conn, done func() bool) { | |||
| 483 | + | t.Helper() | |||
| 484 | + | deadline := time.Now().Add(5 * time.Second) | |||
| 485 | + | for !done() { | |||
| 486 | + | if time.Now().After(deadline) { | |||
| 487 | + | t.Fatal("timeout") | |||
| 488 | + | } | |||
| 489 | + | if err := c.Dispatch(nil); err != nil { | |||
| 490 | + | t.Fatal(err) | |||
| 491 | + | } | |||
| 492 | + | time.Sleep(time.Millisecond) | |||
| 493 | + | } | |||
| 494 | + | } | |||
| 495 | + | ||||
| 496 | + | // Steady state: Flush with an auto-resync fence does not allocate. | |||
| 497 | + | func TestAllocFlushAutoResync(t *testing.T) { | |||
| 498 | + | c, srv := dispatchConn(t) | |||
| 499 | + | sink := make([]byte, 64<<10) | |||
| 500 | + | b := c.buf | |||
| 501 | + | b.next, b.acked = 1, 0 | |||
| 502 | + | run := func() { | |||
| 503 | + | // Pretend the server acknowledged everything so far, then queue | |||
| 504 | + | // resyncAt requests in one go (no reply ever comes back here). | |||
| 505 | + | b.acked = b.next - 1 | |||
| 506 | + | c.resync = 0 | |||
| 507 | + | for range resyncAt { | |||
| 508 | + | b.Request(8, 0, 4) | |||
| 509 | + | } | |||
| 510 | + | for c.Flush() == ErrWouldBlock { | |||
| 511 | + | _, _ = unix.Read(srv, sink) | |||
| 512 | + | } | |||
| 513 | + | for { | |||
| 514 | + | if n, _ := unix.Read(srv, sink); n <= 0 { | |||
| 515 | + | break | |||
| 516 | + | } | |||
| 517 | + | } | |||
| 518 | + | c.ring[c.resync&ringMask] = slot{} | |||
| 519 | + | c.live = 0 | |||
| 520 | + | } | |||
| 521 | + | run() | |||
| 522 | + | before := c.Stats().Resyncs | |||
| 523 | + | if n := testing.AllocsPerRun(20, run); n != 0 { | |||
| 524 | + | t.Fatalf("Flush with resync allocates %v times", n) | |||
| 525 | + | } | |||
| 526 | + | if c.Stats().Resyncs-before < 20 { | |||
| 527 | + | t.Fatal(c.Stats()) | |||
| 528 | + | } | |||
| 529 | + | } | |||
A bootstrap_xserver_test.go +259 -0
| @@ -0,0 +1,259 @@ | |||
| 1 | + | //go:build xserver | |
| 2 | + | ||
| 3 | + | package xturbo_test | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "crypto/rand" | |
| 7 | + | "errors" | |
| 8 | + | "path/filepath" | |
| 9 | + | "testing" | |
| 10 | + | ||
| 11 | + | "bnema.dev/xturbo" | |
| 12 | + | "bnema.dev/xturbo/xproto" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | func dialXvfb(t *testing.T) *xturbo.Conn { | |
| 16 | + | t.Helper() | |
| 17 | + | dir := t.TempDir() | |
| 18 | + | auth := filepath.Join(dir, "Xauthority") | |
| 19 | + | cookie := make([]byte, 16) | |
| 20 | + | _, _ = rand.Read(cookie) | |
| 21 | + | writeXauthority(t, auth, cookie) | |
| 22 | + | display := startXvfb(t, auth) | |
| 23 | + | t.Setenv("XAUTHORITY", auth) | |
| 24 | + | c, err := xturbo.Dial(display) | |
| 25 | + | if err != nil { | |
| 26 | + | t.Fatal(err) | |
| 27 | + | } | |
| 28 | + | t.Cleanup(func() { _ = c.Close() }) | |
| 29 | + | return c | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | // The ExtTable filled at connect matches what independent QueryExtension | |
| 33 | + | // round trips through the generated encoder report. | |
| 34 | + | func TestXserverExtTable(t *testing.T) { | |
| 35 | + | c := dialXvfb(t) | |
| 36 | + | const kindQuery = 16 | |
| 37 | + | type got struct { | |
| 38 | + | present bool | |
| 39 | + | major, firstEv, firstErr uint8 | |
| 40 | + | } | |
| 41 | + | answers := map[uint32]got{} | |
| 42 | + | h := xturbo.Handler{OnReply: func(r *xturbo.Reply) { | |
| 43 | + | q, ok := xproto.ParseQueryExtensionReply(r.Raw) | |
| 44 | + | if !ok { | |
| 45 | + | t.Error("short QueryExtension reply") | |
| 46 | + | return | |
| 47 | + | } | |
| 48 | + | answers[r.Args[0]] = got{q.Present(), q.MajorOpcode(), q.FirstEvent(), q.FirstError()} | |
| 49 | + | }} | |
| 50 | + | for id := xturbo.ExtCore + 1; int(id) < len(xturbo.ExtXName); id++ { | |
| 51 | + | seq := xproto.QueryExtension(c.Buf(), xturbo.ExtXName[id]) | |
| 52 | + | if err := c.Expect(seq, kindQuery, uint32(id), 0, 0, 0); err != nil { | |
| 53 | + | t.Fatal(err) | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | if err := c.Sync(&h); err != nil { | |
| 57 | + | t.Fatal(err) | |
| 58 | + | } | |
| 59 | + | for id := xturbo.ExtCore + 1; int(id) < len(xturbo.ExtXName); id++ { | |
| 60 | + | a, ok := answers[uint32(id)] | |
| 61 | + | info := c.Ext().Info(id) | |
| 62 | + | if !ok { | |
| 63 | + | t.Fatalf("%s: no reply", xturbo.ExtXName[id]) | |
| 64 | + | } | |
| 65 | + | want := xturbo.ExtInfo{} | |
| 66 | + | if a.present { | |
| 67 | + | want = xturbo.ExtInfo{Present: true, Major: a.major, FirstEvent: a.firstEv, FirstError: a.firstErr} | |
| 68 | + | } | |
| 69 | + | if info != want { | |
| 70 | + | t.Errorf("%s: table %+v, server %+v", xturbo.ExtXName[id], info, a) | |
| 71 | + | } | |
| 72 | + | } | |
| 73 | + | // Xvfb builds XFIXES, Composite, RENDER, SHAPE, X-Resource, XC-MISC and | |
| 74 | + | // BIG-REQUESTS in; the XWM depends on them. | |
| 75 | + | for _, id := range []xturbo.ExtID{xturbo.ExtXFixes, xturbo.ExtComposite, xturbo.ExtRender, xturbo.ExtShape, xturbo.ExtRes, xturbo.ExtXCMisc, xturbo.ExtBigRequests} { | |
| 76 | + | if !c.Ext().Info(id).Present { | |
| 77 | + | t.Errorf("%s absent", xturbo.ExtXName[id]) | |
| 78 | + | } | |
| 79 | + | } | |
| 80 | + | if st := c.Stats(); st.DroppedReplies != 0 { | |
| 81 | + | t.Fatal(st) | |
| 82 | + | } | |
| 83 | + | } | |
| 84 | + | ||
| 85 | + | // InternAtoms agrees with independent InternAtom round trips and with the | |
| 86 | + | // predefined atoms; errors come back as *XError. | |
| 87 | + | func TestXserverInternAtoms(t *testing.T) { | |
| 88 | + | c := dialXvfb(t) | |
| 89 | + | names := []string{"WM_NAME", "STRING", "WM_PROTOCOLS", "WM_DELETE_WINDOW", "_NET_WM_NAME", "UTF8_STRING", "_NET_SUPPORTED", "WM_STATE", "XTURBO_TEST_ATOM"} | |
| 90 | + | dst := make([]uint32, len(names)) | |
| 91 | + | if err := c.InternAtoms(names, false, dst); err != nil { | |
| 92 | + | t.Fatal(err) | |
| 93 | + | } | |
| 94 | + | if dst[0] != uint32(xproto.AtomWmName) || dst[1] != uint32(xproto.AtomString) { | |
| 95 | + | t.Fatalf("predefined atoms %v", dst[:2]) | |
| 96 | + | } | |
| 97 | + | const kindAtom = 16 | |
| 98 | + | again := make([]uint32, len(names)) | |
| 99 | + | h := xturbo.Handler{OnReply: func(r *xturbo.Reply) { | |
| 100 | + | a, ok := xproto.ParseInternAtomReply(r.Raw) | |
| 101 | + | if ok { | |
| 102 | + | again[r.Args[0]] = uint32(a.Atom()) | |
| 103 | + | } | |
| 104 | + | }} | |
| 105 | + | for i, name := range names { | |
| 106 | + | seq := xproto.InternAtom(c.Buf(), true, name) | |
| 107 | + | if err := c.Expect(seq, kindAtom, uint32(i), 0, 0, 0); err != nil { | |
| 108 | + | t.Fatal(err) | |
| 109 | + | } | |
| 110 | + | } | |
| 111 | + | if err := c.Sync(&h); err != nil { | |
| 112 | + | t.Fatal(err) | |
| 113 | + | } | |
| 114 | + | seen := map[uint32]bool{} | |
| 115 | + | for i := range names { | |
| 116 | + | if dst[i] == 0 || dst[i] != again[i] || seen[dst[i]] { | |
| 117 | + | t.Fatalf("%s: %d vs %d", names[i], dst[i], again[i]) | |
| 118 | + | } | |
| 119 | + | seen[dst[i]] = true | |
| 120 | + | } | |
| 121 | + | // onlyIfExists on an unknown name gives 0, no error. | |
| 122 | + | none := make([]uint32, 1) | |
| 123 | + | if err := c.InternAtoms([]string{"XTURBO_NEVER_INTERNED_ATOM"}, true, none); err != nil || none[0] != 0 { | |
| 124 | + | t.Fatal(err, none) | |
| 125 | + | } | |
| 126 | + | } | |
| 127 | + | ||
| 128 | + | // NewXID hands out many distinct ids in the client's range; windows created | |
| 129 | + | // with them exist and can be destroyed and the ids reused. | |
| 130 | + | func TestXserverNewXIDWindows(t *testing.T) { | |
| 131 | + | c := dialXvfb(t) | |
| 132 | + | const n = 2000 | |
| 133 | + | ids := make([]uint32, 0, n) | |
| 134 | + | seen := map[uint32]bool{} | |
| 135 | + | for range n { | |
| 136 | + | id, err := c.NewXID() | |
| 137 | + | if err != nil { | |
| 138 | + | t.Fatal(err) | |
| 139 | + | } | |
| 140 | + | if id == 0 || seen[id] { | |
| 141 | + | t.Fatalf("id %#x", id) | |
| 142 | + | } | |
| 143 | + | seen[id] = true | |
| 144 | + | ids = append(ids, id) | |
| 145 | + | } | |
| 146 | + | b := c.Buf() | |
| 147 | + | var v xproto.CreateWindowValues | |
| 148 | + | for _, id := range ids[:64] { | |
| 149 | + | xproto.CreateWindow(b, 0, xproto.Window(id), xproto.Window(c.Root()), 1, 2, 30, 40, 0, xproto.WindowClassInputOutput, 0, &v) | |
| 150 | + | } | |
| 151 | + | var xerrs []xturbo.XError | |
| 152 | + | geoms := 0 | |
| 153 | + | const kindGeom = 16 | |
| 154 | + | h := xturbo.Handler{ | |
| 155 | + | OnReply: func(r *xturbo.Reply) { | |
| 156 | + | if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok && g.Width() == 30 && g.Height() == 40 { | |
| 157 | + | geoms++ | |
| 158 | + | } | |
| 159 | + | }, | |
| 160 | + | OnError: func(e *xturbo.XError) { xerrs = append(xerrs, *e) }, | |
| 161 | + | } | |
| 162 | + | for _, id := range ids[:64] { | |
| 163 | + | seq := xproto.GetGeometry(b, xproto.Drawable(id)) | |
| 164 | + | if err := c.Expect(seq, kindGeom, id, 0, 0, 0); err != nil { | |
| 165 | + | t.Fatal(err) | |
| 166 | + | } | |
| 167 | + | } | |
| 168 | + | if err := c.Sync(&h); err != nil { | |
| 169 | + | t.Fatal(err) | |
| 170 | + | } | |
| 171 | + | if geoms != 64 || len(xerrs) != 0 { | |
| 172 | + | t.Fatalf("geometries %d errors %v", geoms, xerrs) | |
| 173 | + | } | |
| 174 | + | for _, id := range ids[:64] { | |
| 175 | + | xproto.DestroyWindow(b, xproto.Window(id)) | |
| 176 | + | c.FreeXID(id) | |
| 177 | + | } | |
| 178 | + | if err := c.Sync(&h); err != nil || len(xerrs) != 0 { | |
| 179 | + | t.Fatal(err, xerrs) | |
| 180 | + | } | |
| 181 | + | // A reused id can be created again: the destroy completed. | |
| 182 | + | id, err := c.NewXID() | |
| 183 | + | if err != nil || !seen[id] { | |
| 184 | + | t.Fatalf("reuse %#x %v", id, err) | |
| 185 | + | } | |
| 186 | + | xproto.CreateWindow(b, 0, xproto.Window(id), xproto.Window(c.Root()), 0, 0, 1, 1, 0, xproto.WindowClassInputOutput, 0, &v) | |
| 187 | + | xproto.DestroyWindow(b, xproto.Window(id)) | |
| 188 | + | if err := c.Sync(&h); err != nil || len(xerrs) != 0 { | |
| 189 | + | t.Fatal(err, xerrs) | |
| 190 | + | } | |
| 191 | + | } | |
| 192 | + | ||
| 193 | + | // Exhausting the setup range triggers an XC-MISC GetXIDRange refill; the | |
| 194 | + | // refilled ids are usable. | |
| 195 | + | func TestXserverXIDRefill(t *testing.T) { | |
| 196 | + | c := dialXvfb(t) | |
| 197 | + | xturbo.ExhaustXIDs(c) | |
| 198 | + | id, err := c.NewXID() | |
| 199 | + | if err != nil { | |
| 200 | + | t.Fatal(err) | |
| 201 | + | } | |
| 202 | + | var v xproto.CreateWindowValues | |
| 203 | + | xproto.CreateWindow(c.Buf(), 0, xproto.Window(id), xproto.Window(c.Root()), 0, 0, 1, 1, 0, xproto.WindowClassInputOutput, 0, &v) | |
| 204 | + | xproto.DestroyWindow(c.Buf(), xproto.Window(id)) | |
| 205 | + | var xerr *xturbo.XError | |
| 206 | + | h := xturbo.Handler{OnError: func(e *xturbo.XError) { cp := *e; xerr = &cp }} | |
| 207 | + | if err := c.Sync(&h); err != nil || xerr != nil { | |
| 208 | + | t.Fatal(err, xerr) | |
| 209 | + | } | |
| 210 | + | // Non-blocking prefetch: queued from the loop, used after the next | |
| 211 | + | // exhaustion without a blocking round trip. | |
| 212 | + | xturbo.ExhaustXIDs(c) | |
| 213 | + | if !c.XIDsLow() { | |
| 214 | + | t.Fatal("not low after exhaustion") | |
| 215 | + | } | |
| 216 | + | if err := c.PrefetchXIDs(); err != nil { | |
| 217 | + | t.Fatal(err) | |
| 218 | + | } | |
| 219 | + | if err := c.Sync(&h); err != nil || c.XIDsLow() { | |
| 220 | + | t.Fatal(err, c.XIDsLow()) | |
| 221 | + | } | |
| 222 | + | // The server hands out its lowest free range, which may hold the id just | |
| 223 | + | // destroyed: check the id is usable, not that it is new. | |
| 224 | + | id2, err := c.NewXID() | |
| 225 | + | if err != nil { | |
| 226 | + | t.Fatal(err) | |
| 227 | + | } | |
| 228 | + | xproto.CreateWindow(c.Buf(), 0, xproto.Window(id2), xproto.Window(c.Root()), 0, 0, 1, 1, 0, xproto.WindowClassInputOutput, 0, &v) | |
| 229 | + | xproto.DestroyWindow(c.Buf(), xproto.Window(id2)) | |
| 230 | + | if err := c.Sync(&h); err != nil || xerr != nil { | |
| 231 | + | t.Fatal(err, xerr) | |
| 232 | + | } | |
| 233 | + | } | |
| 234 | + | ||
| 235 | + | // A long silent stream gets fences; the connection keeps working. | |
| 236 | + | func TestXserverAutoResync(t *testing.T) { | |
| 237 | + | c := dialXvfb(t) | |
| 238 | + | b := c.Buf() | |
| 239 | + | for range 3 * 0x8000 { | |
| 240 | + | xproto.MapWindow(b, xproto.Window(c.Root())) // no reply, no event | |
| 241 | + | if b.Len() > 32<<10 { | |
| 242 | + | if err := c.Flush(); err != nil && !errors.Is(err, xturbo.ErrWouldBlock) { | |
| 243 | + | t.Fatal(err) | |
| 244 | + | } | |
| 245 | + | if err := c.Dispatch(nil); err != nil { | |
| 246 | + | t.Fatal(err) | |
| 247 | + | } | |
| 248 | + | } | |
| 249 | + | } | |
| 250 | + | if b.Err() != nil { | |
| 251 | + | t.Fatal(b.Err()) | |
| 252 | + | } | |
| 253 | + | if err := c.Sync(nil); err != nil { | |
| 254 | + | t.Fatal(err) | |
| 255 | + | } | |
| 256 | + | if st := c.Stats(); st.Resyncs == 0 || st.DroppedReplies != 0 { | |
| 257 | + | t.Fatalf("%+v", st) | |
| 258 | + | } | |
| 259 | + | } | |
| @@ -0,0 +1,259 @@ | |||||
| 1 | + | //go:build xserver | |||
| 2 | + | ||||
| 3 | + | package xturbo_test | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "crypto/rand" | |||
| 7 | + | "errors" | |||
| 8 | + | "path/filepath" | |||
| 9 | + | "testing" | |||
| 10 | + | ||||
| 11 | + | "bnema.dev/xturbo" | |||
| 12 | + | "bnema.dev/xturbo/xproto" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | func dialXvfb(t *testing.T) *xturbo.Conn { | |||
| 16 | + | t.Helper() | |||
| 17 | + | dir := t.TempDir() | |||
| 18 | + | auth := filepath.Join(dir, "Xauthority") | |||
| 19 | + | cookie := make([]byte, 16) | |||
| 20 | + | _, _ = rand.Read(cookie) | |||
| 21 | + | writeXauthority(t, auth, cookie) | |||
| 22 | + | display := startXvfb(t, auth) | |||
| 23 | + | t.Setenv("XAUTHORITY", auth) | |||
| 24 | + | c, err := xturbo.Dial(display) | |||
| 25 | + | if err != nil { | |||
| 26 | + | t.Fatal(err) | |||
| 27 | + | } | |||
| 28 | + | t.Cleanup(func() { _ = c.Close() }) | |||
| 29 | + | return c | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | // The ExtTable filled at connect matches what independent QueryExtension | |||
| 33 | + | // round trips through the generated encoder report. | |||
| 34 | + | func TestXserverExtTable(t *testing.T) { | |||
| 35 | + | c := dialXvfb(t) | |||
| 36 | + | const kindQuery = 16 | |||
| 37 | + | type got struct { | |||
| 38 | + | present bool | |||
| 39 | + | major, firstEv, firstErr uint8 | |||
| 40 | + | } | |||
| 41 | + | answers := map[uint32]got{} | |||
| 42 | + | h := xturbo.Handler{OnReply: func(r *xturbo.Reply) { | |||
| 43 | + | q, ok := xproto.ParseQueryExtensionReply(r.Raw) | |||
| 44 | + | if !ok { | |||
| 45 | + | t.Error("short QueryExtension reply") | |||
| 46 | + | return | |||
| 47 | + | } | |||
| 48 | + | answers[r.Args[0]] = got{q.Present(), q.MajorOpcode(), q.FirstEvent(), q.FirstError()} | |||
| 49 | + | }} | |||
| 50 | + | for id := xturbo.ExtCore + 1; int(id) < len(xturbo.ExtXName); id++ { | |||
| 51 | + | seq := xproto.QueryExtension(c.Buf(), xturbo.ExtXName[id]) | |||
| 52 | + | if err := c.Expect(seq, kindQuery, uint32(id), 0, 0, 0); err != nil { | |||
| 53 | + | t.Fatal(err) | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | if err := c.Sync(&h); err != nil { | |||
| 57 | + | t.Fatal(err) | |||
| 58 | + | } | |||
| 59 | + | for id := xturbo.ExtCore + 1; int(id) < len(xturbo.ExtXName); id++ { | |||
| 60 | + | a, ok := answers[uint32(id)] | |||
| 61 | + | info := c.Ext().Info(id) | |||
| 62 | + | if !ok { | |||
| 63 | + | t.Fatalf("%s: no reply", xturbo.ExtXName[id]) | |||
| 64 | + | } | |||
| 65 | + | want := xturbo.ExtInfo{} | |||
| 66 | + | if a.present { | |||
| 67 | + | want = xturbo.ExtInfo{Present: true, Major: a.major, FirstEvent: a.firstEv, FirstError: a.firstErr} | |||
| 68 | + | } | |||
| 69 | + | if info != want { | |||
| 70 | + | t.Errorf("%s: table %+v, server %+v", xturbo.ExtXName[id], info, a) | |||
| 71 | + | } | |||
| 72 | + | } | |||
| 73 | + | // Xvfb builds XFIXES, Composite, RENDER, SHAPE, X-Resource, XC-MISC and | |||
| 74 | + | // BIG-REQUESTS in; the XWM depends on them. | |||
| 75 | + | for _, id := range []xturbo.ExtID{xturbo.ExtXFixes, xturbo.ExtComposite, xturbo.ExtRender, xturbo.ExtShape, xturbo.ExtRes, xturbo.ExtXCMisc, xturbo.ExtBigRequests} { | |||
| 76 | + | if !c.Ext().Info(id).Present { | |||
| 77 | + | t.Errorf("%s absent", xturbo.ExtXName[id]) | |||
| 78 | + | } | |||
| 79 | + | } | |||
| 80 | + | if st := c.Stats(); st.DroppedReplies != 0 { | |||
| 81 | + | t.Fatal(st) | |||
| 82 | + | } | |||
| 83 | + | } | |||
| 84 | + | ||||
| 85 | + | // InternAtoms agrees with independent InternAtom round trips and with the | |||
| 86 | + | // predefined atoms; errors come back as *XError. | |||
| 87 | + | func TestXserverInternAtoms(t *testing.T) { | |||
| 88 | + | c := dialXvfb(t) | |||
| 89 | + | names := []string{"WM_NAME", "STRING", "WM_PROTOCOLS", "WM_DELETE_WINDOW", "_NET_WM_NAME", "UTF8_STRING", "_NET_SUPPORTED", "WM_STATE", "XTURBO_TEST_ATOM"} | |||
| 90 | + | dst := make([]uint32, len(names)) | |||
| 91 | + | if err := c.InternAtoms(names, false, dst); err != nil { | |||
| 92 | + | t.Fatal(err) | |||
| 93 | + | } | |||
| 94 | + | if dst[0] != uint32(xproto.AtomWmName) || dst[1] != uint32(xproto.AtomString) { | |||
| 95 | + | t.Fatalf("predefined atoms %v", dst[:2]) | |||
| 96 | + | } | |||
| 97 | + | const kindAtom = 16 | |||
| 98 | + | again := make([]uint32, len(names)) | |||
| 99 | + | h := xturbo.Handler{OnReply: func(r *xturbo.Reply) { | |||
| 100 | + | a, ok := xproto.ParseInternAtomReply(r.Raw) | |||
| 101 | + | if ok { | |||
| 102 | + | again[r.Args[0]] = uint32(a.Atom()) | |||
| 103 | + | } | |||
| 104 | + | }} | |||
| 105 | + | for i, name := range names { | |||
| 106 | + | seq := xproto.InternAtom(c.Buf(), true, name) | |||
| 107 | + | if err := c.Expect(seq, kindAtom, uint32(i), 0, 0, 0); err != nil { | |||
| 108 | + | t.Fatal(err) | |||
| 109 | + | } | |||
| 110 | + | } | |||
| 111 | + | if err := c.Sync(&h); err != nil { | |||
| 112 | + | t.Fatal(err) | |||
| 113 | + | } | |||
| 114 | + | seen := map[uint32]bool{} | |||
| 115 | + | for i := range names { | |||
| 116 | + | if dst[i] == 0 || dst[i] != again[i] || seen[dst[i]] { | |||
| 117 | + | t.Fatalf("%s: %d vs %d", names[i], dst[i], again[i]) | |||
| 118 | + | } | |||
| 119 | + | seen[dst[i]] = true | |||
| 120 | + | } | |||
| 121 | + | // onlyIfExists on an unknown name gives 0, no error. | |||
| 122 | + | none := make([]uint32, 1) | |||
| 123 | + | if err := c.InternAtoms([]string{"XTURBO_NEVER_INTERNED_ATOM"}, true, none); err != nil || none[0] != 0 { | |||
| 124 | + | t.Fatal(err, none) | |||
| 125 | + | } | |||
| 126 | + | } | |||
| 127 | + | ||||
| 128 | + | // NewXID hands out many distinct ids in the client's range; windows created | |||
| 129 | + | // with them exist and can be destroyed and the ids reused. | |||
| 130 | + | func TestXserverNewXIDWindows(t *testing.T) { | |||
| 131 | + | c := dialXvfb(t) | |||
| 132 | + | const n = 2000 | |||
| 133 | + | ids := make([]uint32, 0, n) | |||
| 134 | + | seen := map[uint32]bool{} | |||
| 135 | + | for range n { | |||
| 136 | + | id, err := c.NewXID() | |||
| 137 | + | if err != nil { | |||
| 138 | + | t.Fatal(err) | |||
| 139 | + | } | |||
| 140 | + | if id == 0 || seen[id] { | |||
| 141 | + | t.Fatalf("id %#x", id) | |||
| 142 | + | } | |||
| 143 | + | seen[id] = true | |||
| 144 | + | ids = append(ids, id) | |||
| 145 | + | } | |||
| 146 | + | b := c.Buf() | |||
| 147 | + | var v xproto.CreateWindowValues | |||
| 148 | + | for _, id := range ids[:64] { | |||
| 149 | + | xproto.CreateWindow(b, 0, xproto.Window(id), xproto.Window(c.Root()), 1, 2, 30, 40, 0, xproto.WindowClassInputOutput, 0, &v) | |||
| 150 | + | } | |||
| 151 | + | var xerrs []xturbo.XError | |||
| 152 | + | geoms := 0 | |||
| 153 | + | const kindGeom = 16 | |||
| 154 | + | h := xturbo.Handler{ | |||
| 155 | + | OnReply: func(r *xturbo.Reply) { | |||
| 156 | + | if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok && g.Width() == 30 && g.Height() == 40 { | |||
| 157 | + | geoms++ | |||
| 158 | + | } | |||
| 159 | + | }, | |||
| 160 | + | OnError: func(e *xturbo.XError) { xerrs = append(xerrs, *e) }, | |||
| 161 | + | } | |||
| 162 | + | for _, id := range ids[:64] { | |||
| 163 | + | seq := xproto.GetGeometry(b, xproto.Drawable(id)) | |||
| 164 | + | if err := c.Expect(seq, kindGeom, id, 0, 0, 0); err != nil { | |||
| 165 | + | t.Fatal(err) | |||
| 166 | + | } | |||
| 167 | + | } | |||
| 168 | + | if err := c.Sync(&h); err != nil { | |||
| 169 | + | t.Fatal(err) | |||
| 170 | + | } | |||
| 171 | + | if geoms != 64 || len(xerrs) != 0 { | |||
| 172 | + | t.Fatalf("geometries %d errors %v", geoms, xerrs) | |||
| 173 | + | } | |||
| 174 | + | for _, id := range ids[:64] { | |||
| 175 | + | xproto.DestroyWindow(b, xproto.Window(id)) | |||
| 176 | + | c.FreeXID(id) | |||
| 177 | + | } | |||
| 178 | + | if err := c.Sync(&h); err != nil || len(xerrs) != 0 { | |||
| 179 | + | t.Fatal(err, xerrs) | |||
| 180 | + | } | |||
| 181 | + | // A reused id can be created again: the destroy completed. | |||
| 182 | + | id, err := c.NewXID() | |||
| 183 | + | if err != nil || !seen[id] { | |||
| 184 | + | t.Fatalf("reuse %#x %v", id, err) | |||
| 185 | + | } | |||
| 186 | + | xproto.CreateWindow(b, 0, xproto.Window(id), xproto.Window(c.Root()), 0, 0, 1, 1, 0, xproto.WindowClassInputOutput, 0, &v) | |||
| 187 | + | xproto.DestroyWindow(b, xproto.Window(id)) | |||
| 188 | + | if err := c.Sync(&h); err != nil || len(xerrs) != 0 { | |||
| 189 | + | t.Fatal(err, xerrs) | |||
| 190 | + | } | |||
| 191 | + | } | |||
| 192 | + | ||||
| 193 | + | // Exhausting the setup range triggers an XC-MISC GetXIDRange refill; the | |||
| 194 | + | // refilled ids are usable. | |||
| 195 | + | func TestXserverXIDRefill(t *testing.T) { | |||
| 196 | + | c := dialXvfb(t) | |||
| 197 | + | xturbo.ExhaustXIDs(c) | |||
| 198 | + | id, err := c.NewXID() | |||
| 199 | + | if err != nil { | |||
| 200 | + | t.Fatal(err) | |||
| 201 | + | } | |||
| 202 | + | var v xproto.CreateWindowValues | |||
| 203 | + | xproto.CreateWindow(c.Buf(), 0, xproto.Window(id), xproto.Window(c.Root()), 0, 0, 1, 1, 0, xproto.WindowClassInputOutput, 0, &v) | |||
| 204 | + | xproto.DestroyWindow(c.Buf(), xproto.Window(id)) | |||
| 205 | + | var xerr *xturbo.XError | |||
| 206 | + | h := xturbo.Handler{OnError: func(e *xturbo.XError) { cp := *e; xerr = &cp }} | |||
| 207 | + | if err := c.Sync(&h); err != nil || xerr != nil { | |||
| 208 | + | t.Fatal(err, xerr) | |||
| 209 | + | } | |||
| 210 | + | // Non-blocking prefetch: queued from the loop, used after the next | |||
| 211 | + | // exhaustion without a blocking round trip. | |||
| 212 | + | xturbo.ExhaustXIDs(c) | |||
| 213 | + | if !c.XIDsLow() { | |||
| 214 | + | t.Fatal("not low after exhaustion") | |||
| 215 | + | } | |||
| 216 | + | if err := c.PrefetchXIDs(); err != nil { | |||
| 217 | + | t.Fatal(err) | |||
| 218 | + | } | |||
| 219 | + | if err := c.Sync(&h); err != nil || c.XIDsLow() { | |||
| 220 | + | t.Fatal(err, c.XIDsLow()) | |||
| 221 | + | } | |||
| 222 | + | // The server hands out its lowest free range, which may hold the id just | |||
| 223 | + | // destroyed: check the id is usable, not that it is new. | |||
| 224 | + | id2, err := c.NewXID() | |||
| 225 | + | if err != nil { | |||
| 226 | + | t.Fatal(err) | |||
| 227 | + | } | |||
| 228 | + | xproto.CreateWindow(c.Buf(), 0, xproto.Window(id2), xproto.Window(c.Root()), 0, 0, 1, 1, 0, xproto.WindowClassInputOutput, 0, &v) | |||
| 229 | + | xproto.DestroyWindow(c.Buf(), xproto.Window(id2)) | |||
| 230 | + | if err := c.Sync(&h); err != nil || xerr != nil { | |||
| 231 | + | t.Fatal(err, xerr) | |||
| 232 | + | } | |||
| 233 | + | } | |||
| 234 | + | ||||
| 235 | + | // A long silent stream gets fences; the connection keeps working. | |||
| 236 | + | func TestXserverAutoResync(t *testing.T) { | |||
| 237 | + | c := dialXvfb(t) | |||
| 238 | + | b := c.Buf() | |||
| 239 | + | for range 3 * 0x8000 { | |||
| 240 | + | xproto.MapWindow(b, xproto.Window(c.Root())) // no reply, no event | |||
| 241 | + | if b.Len() > 32<<10 { | |||
| 242 | + | if err := c.Flush(); err != nil && !errors.Is(err, xturbo.ErrWouldBlock) { | |||
| 243 | + | t.Fatal(err) | |||
| 244 | + | } | |||
| 245 | + | if err := c.Dispatch(nil); err != nil { | |||
| 246 | + | t.Fatal(err) | |||
| 247 | + | } | |||
| 248 | + | } | |||
| 249 | + | } | |||
| 250 | + | if b.Err() != nil { | |||
| 251 | + | t.Fatal(b.Err()) | |||
| 252 | + | } | |||
| 253 | + | if err := c.Sync(nil); err != nil { | |||
| 254 | + | t.Fatal(err) | |||
| 255 | + | } | |||
| 256 | + | if st := c.Stats(); st.Resyncs == 0 || st.DroppedReplies != 0 { | |||
| 257 | + | t.Fatalf("%+v", st) | |||
| 258 | + | } | |||
| 259 | + | } | |||
M conn.go +59 -10
| @@ -1,7 +1,9 @@ | |||
| 1 | 1 | package xturbo | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "fmt" | |
| 4 | 5 | "os" | |
| 6 | + | "time" | |
| 5 | 7 | ||
| 6 | 8 | "golang.org/x/sys/unix" | |
| 7 | 9 | ) | |
| @@ -15,7 +17,23 @@ type Conn struct { | |||
| 15 | 17 | setup []byte | |
| 16 | 18 | root uint32 | |
| 17 | 19 | ||
| 18 | - | xidBase, xidMask uint32 // resource-id allocation, used by NewXID | |
| 20 | + | xidBase, xidMask uint32 // resource-id allocation, used by NewXID | |
| 21 | + | xids, xidPend xidRange // current range; prefetched range (left 0: none) | |
| 22 | + | xidFree [xidFreeCap]uint32 | |
| 23 | + | xidFreeN int | |
| 24 | + | xidReq Seq // last GetXIDRange sent | |
| 25 | + | xidNone bool // the last GetXIDRange gave no usable range | |
| 26 | + | xidFailed bool // ... or an X error, xidErr | |
| 27 | + | xidErr XError | |
| 28 | + | ||
| 29 | + | // Blocking calls (Sync, InternAtoms, NewXID refill). | |
| 30 | + | h *Handler // last non-nil Handler given to Dispatch or Sync | |
| 31 | + | rdIdle bool // the last Dispatch read nothing: the socket was empty | |
| 32 | + | atoms []uint32 // InternAtoms destination, indexed by slot Args[0] | |
| 33 | + | syncErr XError // first error on a reserved-kind request | |
| 34 | + | syncFailed bool // syncErr is set | |
| 35 | + | resync Seq // last GetInputFocus appended by Flush | |
| 36 | + | timeout time.Duration // bound of one Sync, InternAtoms or XID refill | |
| 19 | 37 | ||
| 20 | 38 | // Read side, owned by Dispatch: rb[rpos:rend] is received, unconsumed. | |
| 21 | 39 | rb []byte | |
| @@ -38,8 +56,12 @@ type Conn struct { | |||
| 38 | 56 | ||
| 39 | 57 | // NewConn takes ownership of fd, a connected stream socket to an X server, | |
| 40 | 58 | // and runs the connection setup without authorization (e.g. the -wm fd of an | |
| 41 | - | // XWayland the caller spawned). The handshake blocks; afterwards fd is | |
| 42 | - | // switched to O_NONBLOCK. On failure fd is closed. | |
| 59 | + | // XWayland the caller spawned). It blocks for the handshake, then one round | |
| 60 | + | // trip negotiates every known extension (QueryExtension each, BIG-REQUESTS | |
| 61 | + | // Enable when present) into Ext() and the Buf request limit. fd is left | |
| 62 | + | // O_NONBLOCK. The whole call gives up after 10 s with an error wrapping | |
| 63 | + | // os.ErrDeadlineExceeded (an alive but silent server). On failure fd is | |
| 64 | + | // closed. | |
| 43 | 65 | func NewConn(fd int) (*Conn, error) { | |
| 44 | 66 | return newConn(fd, "", nil) | |
| 45 | 67 | } | |
| @@ -54,17 +76,19 @@ func newConn(fd int, authName string, authData []byte) (*Conn, error) { | |||
| 54 | 76 | } | |
| 55 | 77 | ||
| 56 | 78 | func setupConn(fd int, authName string, authData []byte) (*Conn, error) { | |
| 57 | - | if err := unix.SetNonblock(fd, false); err != nil { | |
| 79 | + | deadline := time.Now().Add(connectTimeout) | |
| 80 | + | if err := unix.SetNonblock(fd, true); err != nil { | |
| 58 | 81 | return nil, os.NewSyscallError("fcntl", err) | |
| 59 | 82 | } | |
| 60 | - | setup, info, err := handshake(fd, authName, authData) | |
| 83 | + | setup, info, err := handshake(fd, authName, authData, deadline) | |
| 61 | 84 | if err != nil { | |
| 62 | 85 | return nil, err | |
| 63 | 86 | } | |
| 64 | - | if err := unix.SetNonblock(fd, true); err != nil { | |
| 65 | - | return nil, os.NewSyscallError("fcntl", err) | |
| 87 | + | c := newConnState(fd, setup, info) | |
| 88 | + | if err := c.bootstrap(deadline); err != nil { | |
| 89 | + | return nil, fmt.Errorf("xturbo: extension bootstrap: %w", err) | |
| 66 | 90 | } | |
| 67 | - | return newConnState(fd, setup, info), nil | |
| 91 | + | return c, nil | |
| 68 | 92 | } | |
| 69 | 93 | ||
| 70 | 94 | // newConnState builds a Conn over an fd whose setup already completed. | |
| @@ -78,8 +102,10 @@ func newConnState(fd int, setup []byte, info setupInfo) *Conn { | |||
| 78 | 102 | xidMask: info.xidMask, | |
| 79 | 103 | rb: make([]byte, readBufSize), | |
| 80 | 104 | maxPacket: MaxPacket, | |
| 105 | + | timeout: syncTimeout, | |
| 81 | 106 | } | |
| 82 | 107 | c.buf.Ext = &c.ext | |
| 108 | + | c.initXIDs() | |
| 83 | 109 | return c | |
| 84 | 110 | } | |
| 85 | 111 | ||
| @@ -114,12 +140,35 @@ func (c *Conn) SetupBytes() []byte { return c.setup } | |||
| 114 | 140 | // Root returns the root window of the first screen. | |
| 115 | 141 | func (c *Conn) Root() uint32 { return c.root } | |
| 116 | 142 | ||
| 143 | + | // resyncAt is the number of unacknowledged requests after which Flush appends | |
| 144 | + | // a GetInputFocus, so a long stream of requests without replies or events | |
| 145 | + | // still brings back a sequence number before the 16-bit wire value wraps. | |
| 146 | + | const resyncAt = 0x8000 | |
| 147 | + | ||
| 117 | 148 | // Flush writes queued requests without blocking. It returns ErrWouldBlock when | |
| 118 | 149 | // the socket is full (arm writable interest and call again) and the sticky | |
| 119 | - | // Buf error once the connection is unusable. | |
| 150 | + | // Buf error once the connection is unusable. With resyncAt or more requests | |
| 151 | + | // unacknowledged it first appends a GetInputFocus whose reply is discarded | |
| 152 | + | // (Stats.Resyncs), at most one in flight. | |
| 120 | 153 | func (c *Conn) Flush() error { | |
| 121 | 154 | if c.fd < 0 { | |
| 122 | 155 | return ErrClosed | |
| 123 | 156 | } | |
| 124 | - | return c.buf.flush(c.fd) | |
| 157 | + | b := c.buf | |
| 158 | + | if b.err == nil && b.next-b.acked >= resyncAt && c.resync <= b.acked { | |
| 159 | + | if seq := c.fence(); seq != 0 { | |
| 160 | + | c.resync = seq | |
| 161 | + | c.stats.Resyncs++ | |
| 162 | + | } | |
| 163 | + | } | |
| 164 | + | return b.flush(c.fd) | |
| 165 | + | } | |
| 166 | + | ||
| 167 | + | // fence queues a GetInputFocus whose reply is consumed and dropped. Its | |
| 168 | + | // reply proves every earlier request complete. | |
| 169 | + | func (c *Conn) fence() Seq { | |
| 170 | + | seq := encodeGetInputFocus(c.buf) | |
| 171 | + | // On ErrRingFull the reply is merely counted as dropped. | |
| 172 | + | _ = c.expect(seq, kindDiscard, [4]uint32{}) | |
| 173 | + | return seq | |
| 125 | 174 | } | |
| @@ -1,7 +1,9 @@ | |||||
| 1 | package xturbo | 1 | package xturbo | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "fmt" | |||
| 4 | "os" | 5 | "os" | ||
| 6 | + | "time" | |||
| 5 | 7 | ||||
| 6 | "golang.org/x/sys/unix" | 8 | "golang.org/x/sys/unix" | ||
| 7 | ) | 9 | ) | ||
| @@ -15,7 +17,23 @@ type Conn struct { | |||||
| 15 | setup []byte | 17 | setup []byte | ||
| 16 | root uint32 | 18 | root uint32 | ||
| 17 | 19 | ||||
| 18 | - | xidBase, xidMask uint32 // resource-id allocation, used by NewXID | 20 | + | xidBase, xidMask uint32 // resource-id allocation, used by NewXID |
| 21 | + | xids, xidPend xidRange // current range; prefetched range (left 0: none) | |||
| 22 | + | xidFree [xidFreeCap]uint32 | |||
| 23 | + | xidFreeN int | |||
| 24 | + | xidReq Seq // last GetXIDRange sent | |||
| 25 | + | xidNone bool // the last GetXIDRange gave no usable range | |||
| 26 | + | xidFailed bool // ... or an X error, xidErr | |||
| 27 | + | xidErr XError | |||
| 28 | + | ||||
| 29 | + | // Blocking calls (Sync, InternAtoms, NewXID refill). | |||
| 30 | + | h *Handler // last non-nil Handler given to Dispatch or Sync | |||
| 31 | + | rdIdle bool // the last Dispatch read nothing: the socket was empty | |||
| 32 | + | atoms []uint32 // InternAtoms destination, indexed by slot Args[0] | |||
| 33 | + | syncErr XError // first error on a reserved-kind request | |||
| 34 | + | syncFailed bool // syncErr is set | |||
| 35 | + | resync Seq // last GetInputFocus appended by Flush | |||
| 36 | + | timeout time.Duration // bound of one Sync, InternAtoms or XID refill | |||
| 19 | 37 | ||||
| 20 | // Read side, owned by Dispatch: rb[rpos:rend] is received, unconsumed. | 38 | // Read side, owned by Dispatch: rb[rpos:rend] is received, unconsumed. | ||
| 21 | rb []byte | 39 | rb []byte | ||
| @@ -38,8 +56,12 @@ type Conn struct { | |||||
| 38 | 56 | ||||
| 39 | // NewConn takes ownership of fd, a connected stream socket to an X server, | 57 | // NewConn takes ownership of fd, a connected stream socket to an X server, | ||
| 40 | // and runs the connection setup without authorization (e.g. the -wm fd of an | 58 | // and runs the connection setup without authorization (e.g. the -wm fd of an | ||
| 41 | - | // XWayland the caller spawned). The handshake blocks; afterwards fd is | 59 | + | // XWayland the caller spawned). It blocks for the handshake, then one round |
| 42 | - | // switched to O_NONBLOCK. On failure fd is closed. | 60 | + | // trip negotiates every known extension (QueryExtension each, BIG-REQUESTS |
| 61 | + | // Enable when present) into Ext() and the Buf request limit. fd is left | |||
| 62 | + | // O_NONBLOCK. The whole call gives up after 10 s with an error wrapping | |||
| 63 | + | // os.ErrDeadlineExceeded (an alive but silent server). On failure fd is | |||
| 64 | + | // closed. | |||
| 43 | func NewConn(fd int) (*Conn, error) { | 65 | func NewConn(fd int) (*Conn, error) { | ||
| 44 | return newConn(fd, "", nil) | 66 | return newConn(fd, "", nil) | ||
| 45 | } | 67 | } | ||
| @@ -54,17 +76,19 @@ func newConn(fd int, authName string, authData []byte) (*Conn, error) { | |||||
| 54 | } | 76 | } | ||
| 55 | 77 | ||||
| 56 | func setupConn(fd int, authName string, authData []byte) (*Conn, error) { | 78 | func setupConn(fd int, authName string, authData []byte) (*Conn, error) { | ||
| 57 | - | if err := unix.SetNonblock(fd, false); err != nil { | 79 | + | deadline := time.Now().Add(connectTimeout) |
| 80 | + | if err := unix.SetNonblock(fd, true); err != nil { | |||
| 58 | return nil, os.NewSyscallError("fcntl", err) | 81 | return nil, os.NewSyscallError("fcntl", err) | ||
| 59 | } | 82 | } | ||
| 60 | - | setup, info, err := handshake(fd, authName, authData) | 83 | + | setup, info, err := handshake(fd, authName, authData, deadline) |
| 61 | if err != nil { | 84 | if err != nil { | ||
| 62 | return nil, err | 85 | return nil, err | ||
| 63 | } | 86 | } | ||
| 64 | - | if err := unix.SetNonblock(fd, true); err != nil { | 87 | + | c := newConnState(fd, setup, info) |
| 65 | - | return nil, os.NewSyscallError("fcntl", err) | 88 | + | if err := c.bootstrap(deadline); err != nil { |
| 89 | + | return nil, fmt.Errorf("xturbo: extension bootstrap: %w", err) | |||
| 66 | } | 90 | } | ||
| 67 | - | return newConnState(fd, setup, info), nil | 91 | + | return c, nil |
| 68 | } | 92 | } | ||
| 69 | 93 | ||||
| 70 | // newConnState builds a Conn over an fd whose setup already completed. | 94 | // newConnState builds a Conn over an fd whose setup already completed. | ||
| @@ -78,8 +102,10 @@ func newConnState(fd int, setup []byte, info setupInfo) *Conn { | |||||
| 78 | xidMask: info.xidMask, | 102 | xidMask: info.xidMask, | ||
| 79 | rb: make([]byte, readBufSize), | 103 | rb: make([]byte, readBufSize), | ||
| 80 | maxPacket: MaxPacket, | 104 | maxPacket: MaxPacket, | ||
| 105 | + | timeout: syncTimeout, | |||
| 81 | } | 106 | } | ||
| 82 | c.buf.Ext = &c.ext | 107 | c.buf.Ext = &c.ext | ||
| 108 | + | c.initXIDs() | |||
| 83 | return c | 109 | return c | ||
| 84 | } | 110 | } | ||
| 85 | 111 | ||||
| @@ -114,12 +140,35 @@ func (c *Conn) SetupBytes() []byte { return c.setup } | |||||
| 114 | // Root returns the root window of the first screen. | 140 | // Root returns the root window of the first screen. | ||
| 115 | func (c *Conn) Root() uint32 { return c.root } | 141 | func (c *Conn) Root() uint32 { return c.root } | ||
| 116 | 142 | ||||
| 143 | + | // resyncAt is the number of unacknowledged requests after which Flush appends | |||
| 144 | + | // a GetInputFocus, so a long stream of requests without replies or events | |||
| 145 | + | // still brings back a sequence number before the 16-bit wire value wraps. | |||
| 146 | + | const resyncAt = 0x8000 | |||
| 147 | + | ||||
| 117 | // Flush writes queued requests without blocking. It returns ErrWouldBlock when | 148 | // Flush writes queued requests without blocking. It returns ErrWouldBlock when | ||
| 118 | // the socket is full (arm writable interest and call again) and the sticky | 149 | // the socket is full (arm writable interest and call again) and the sticky | ||
| 119 | - | // Buf error once the connection is unusable. | 150 | + | // Buf error once the connection is unusable. With resyncAt or more requests |
| 151 | + | // unacknowledged it first appends a GetInputFocus whose reply is discarded | |||
| 152 | + | // (Stats.Resyncs), at most one in flight. | |||
| 120 | func (c *Conn) Flush() error { | 153 | func (c *Conn) Flush() error { | ||
| 121 | if c.fd < 0 { | 154 | if c.fd < 0 { | ||
| 122 | return ErrClosed | 155 | return ErrClosed | ||
| 123 | } | 156 | } | ||
| 124 | - | return c.buf.flush(c.fd) | 157 | + | b := c.buf |
| 158 | + | if b.err == nil && b.next-b.acked >= resyncAt && c.resync <= b.acked { | |||
| 159 | + | if seq := c.fence(); seq != 0 { | |||
| 160 | + | c.resync = seq | |||
| 161 | + | c.stats.Resyncs++ | |||
| 162 | + | } | |||
| 163 | + | } | |||
| 164 | + | return b.flush(c.fd) | |||
| 165 | + | } | |||
| 166 | + | ||||
| 167 | + | // fence queues a GetInputFocus whose reply is consumed and dropped. Its | |||
| 168 | + | // reply proves every earlier request complete. | |||
| 169 | + | func (c *Conn) fence() Seq { | |||
| 170 | + | seq := encodeGetInputFocus(c.buf) | |||
| 171 | + | // On ErrRingFull the reply is merely counted as dropped. | |||
| 172 | + | _ = c.expect(seq, kindDiscard, [4]uint32{}) | |||
| 173 | + | return seq | |||
| 125 | } | 174 | } | ||
M conn_test.go +24 -26
| @@ -24,18 +24,27 @@ func loadSetup(t testing.TB, name string) []byte { | |||
| 24 | 24 | ||
| 25 | 25 | // scriptedServer runs the server side of a socketpair: it reads one setup | |
| 26 | 26 | // request, reports it on the returned channel, writes reply in chunks of | |
| 27 | - | // chunk bytes (0 = all at once) and, if hangup, closes its end. | |
| 27 | + | // chunk bytes (0 = all at once) and, if hangup, closes its end. Otherwise it | |
| 28 | + | // then answers requests as a server without extensions (script{}). | |
| 28 | 29 | func scriptedServer(t *testing.T, reply []byte, chunk int, hangup bool) (client int, req <-chan []byte) { | |
| 30 | + | t.Helper() | |
| 31 | + | fd, _, ch := serveSetup(t, reply, chunk, hangup, &script{}) | |
| 32 | + | return fd, ch | |
| 33 | + | } | |
| 34 | + | ||
| 35 | + | // serveSetup is scriptedServer serving sc after the setup. | |
| 36 | + | func serveSetup(t *testing.T, reply []byte, chunk int, hangup bool, sc *script) (int, *scriptConn, <-chan []byte) { | |
| 29 | 37 | t.Helper() | |
| 30 | 38 | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 31 | 39 | if err != nil { | |
| 32 | 40 | t.Fatal(err) | |
| 33 | 41 | } | |
| 34 | - | // Exercise NewConn switching a non-blocking fd back to blocking. | |
| 35 | - | if err := unix.SetNonblock(fds[0], true); err != nil { | |
| 42 | + | // A blocking fd: NewConn switches it to O_NONBLOCK itself. | |
| 43 | + | if err := unix.SetNonblock(fds[0], false); err != nil { | |
| 36 | 44 | t.Fatal(err) | |
| 37 | 45 | } | |
| 38 | 46 | server := os.NewFile(uintptr(fds[1]), "server") | |
| 47 | + | srv := &scriptConn{f: server, sc: sc} | |
| 39 | 48 | ch := make(chan []byte, 1) | |
| 40 | 49 | done := make(chan struct{}) | |
| 41 | 50 | t.Cleanup(func() { <-done; server.Close() }) | |
| @@ -65,9 +74,11 @@ func scriptedServer(t *testing.T, reply []byte, chunk int, hangup bool) (client | |||
| 65 | 74 | } | |
| 66 | 75 | if hangup { | |
| 67 | 76 | server.Close() | |
| 77 | + | return | |
| 68 | 78 | } | |
| 79 | + | srv.serve() | |
| 69 | 80 | }() | |
| 70 | - | return fds[0], ch | |
| 81 | + | return fds[0], srv, ch | |
| 71 | 82 | } | |
| 72 | 83 | ||
| 73 | 84 | func TestNewConnSuccess(t *testing.T) { | |
| @@ -91,8 +102,9 @@ func TestNewConnSuccess(t *testing.T) { | |||
| 91 | 102 | if c.Root() != 0x39f || c.xidBase != 0x00200000 || c.xidMask != 0x001fffff { | |
| 92 | 103 | t.Fatalf("root %#x base %#x mask %#x", c.Root(), c.xidBase, c.xidMask) | |
| 93 | 104 | } | |
| 94 | - | if c.Fd() != fd || c.Buf().Ext != c.Ext() || c.Buf().Next() != 1 { | |
| 95 | - | t.Fatal("accessors") | |
| 105 | + | // The bootstrap issued one QueryExtension per extension and a fence. | |
| 106 | + | if c.Fd() != fd || c.Buf().Ext != c.Ext() || c.Buf().Next() != Seq(extCount)+1 { | |
| 107 | + | t.Fatal("accessors", c.Buf().Next()) | |
| 96 | 108 | } | |
| 97 | 109 | if c.Ext().Major(ExtXFixes) != 0 || !c.Ext().Info(ExtCore).Present { | |
| 98 | 110 | t.Fatal("ExtTable not empty") | |
| @@ -219,32 +231,18 @@ func truncateSetup(setup []byte, n int) []byte { | |||
| 219 | 231 | } | |
| 220 | 232 | ||
| 221 | 233 | func TestConnFlushClose(t *testing.T) { | |
| 222 | - | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 223 | - | if err != nil { | |
| 224 | - | t.Fatal(err) | |
| 225 | - | } | |
| 226 | - | server := os.NewFile(uintptr(fds[1]), "server") | |
| 227 | - | defer server.Close() | |
| 228 | - | setup := loadSetup(t, "Success") | |
| 229 | - | go func() { | |
| 230 | - | var req [12]byte | |
| 231 | - | _, _ = io.ReadFull(server, req[:]) | |
| 232 | - | _, _ = server.Write(setup) | |
| 233 | - | }() | |
| 234 | - | c, err := NewConn(fds[0]) | |
| 235 | - | if err != nil { | |
| 236 | - | t.Fatal(err) | |
| 237 | - | } | |
| 234 | + | c, srv := dialScript(t, &script{}) | |
| 235 | + | first := c.Buf().Next() | |
| 238 | 236 | p, seq := c.Buf().Request(43, 0, 0) // GetInputFocus | |
| 239 | - | if p == nil || seq != 1 { | |
| 237 | + | if p == nil || seq != first { | |
| 240 | 238 | t.Fatal("request") | |
| 241 | 239 | } | |
| 242 | 240 | if err := c.Flush(); err != nil { | |
| 243 | 241 | t.Fatal(err) | |
| 244 | 242 | } | |
| 245 | - | var got [4]byte | |
| 246 | - | if _, err := io.ReadFull(server, got[:]); err != nil || got != [4]byte{43, 0, 1, 0} { | |
| 247 | - | t.Fatalf("%x %v", got, err) | |
| 243 | + | waitRequests(t, srv, int(seq)) | |
| 244 | + | if got := srv.requests()[seq-1].raw; !bytes.Equal(got, []byte{43, 0, 1, 0}) { | |
| 245 | + | t.Fatalf("%x", got) | |
| 248 | 246 | } | |
| 249 | 247 | if err := c.Close(); err != nil { | |
| 250 | 248 | t.Fatal(err) | |
| @@ -24,18 +24,27 @@ func loadSetup(t testing.TB, name string) []byte { | |||||
| 24 | 24 | ||||
| 25 | // scriptedServer runs the server side of a socketpair: it reads one setup | 25 | // scriptedServer runs the server side of a socketpair: it reads one setup | ||
| 26 | // request, reports it on the returned channel, writes reply in chunks of | 26 | // request, reports it on the returned channel, writes reply in chunks of | ||
| 27 | - | // chunk bytes (0 = all at once) and, if hangup, closes its end. | 27 | + | // chunk bytes (0 = all at once) and, if hangup, closes its end. Otherwise it |
| 28 | + | // then answers requests as a server without extensions (script{}). | |||
| 28 | func scriptedServer(t *testing.T, reply []byte, chunk int, hangup bool) (client int, req <-chan []byte) { | 29 | func scriptedServer(t *testing.T, reply []byte, chunk int, hangup bool) (client int, req <-chan []byte) { | ||
| 30 | + | t.Helper() | |||
| 31 | + | fd, _, ch := serveSetup(t, reply, chunk, hangup, &script{}) | |||
| 32 | + | return fd, ch | |||
| 33 | + | } | |||
| 34 | + | ||||
| 35 | + | // serveSetup is scriptedServer serving sc after the setup. | |||
| 36 | + | func serveSetup(t *testing.T, reply []byte, chunk int, hangup bool, sc *script) (int, *scriptConn, <-chan []byte) { | |||
| 29 | t.Helper() | 37 | t.Helper() | ||
| 30 | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | 38 | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | ||
| 31 | if err != nil { | 39 | if err != nil { | ||
| 32 | t.Fatal(err) | 40 | t.Fatal(err) | ||
| 33 | } | 41 | } | ||
| 34 | - | // Exercise NewConn switching a non-blocking fd back to blocking. | 42 | + | // A blocking fd: NewConn switches it to O_NONBLOCK itself. |
| 35 | - | if err := unix.SetNonblock(fds[0], true); err != nil { | 43 | + | if err := unix.SetNonblock(fds[0], false); err != nil { |
| 36 | t.Fatal(err) | 44 | t.Fatal(err) | ||
| 37 | } | 45 | } | ||
| 38 | server := os.NewFile(uintptr(fds[1]), "server") | 46 | server := os.NewFile(uintptr(fds[1]), "server") | ||
| 47 | + | srv := &scriptConn{f: server, sc: sc} | |||
| 39 | ch := make(chan []byte, 1) | 48 | ch := make(chan []byte, 1) | ||
| 40 | done := make(chan struct{}) | 49 | done := make(chan struct{}) | ||
| 41 | t.Cleanup(func() { <-done; server.Close() }) | 50 | t.Cleanup(func() { <-done; server.Close() }) | ||
| @@ -65,9 +74,11 @@ func scriptedServer(t *testing.T, reply []byte, chunk int, hangup bool) (client | |||||
| 65 | } | 74 | } | ||
| 66 | if hangup { | 75 | if hangup { | ||
| 67 | server.Close() | 76 | server.Close() | ||
| 77 | + | return | |||
| 68 | } | 78 | } | ||
| 79 | + | srv.serve() | |||
| 69 | }() | 80 | }() | ||
| 70 | - | return fds[0], ch | 81 | + | return fds[0], srv, ch |
| 71 | } | 82 | } | ||
| 72 | 83 | ||||
| 73 | func TestNewConnSuccess(t *testing.T) { | 84 | func TestNewConnSuccess(t *testing.T) { | ||
| @@ -91,8 +102,9 @@ func TestNewConnSuccess(t *testing.T) { | |||||
| 91 | if c.Root() != 0x39f || c.xidBase != 0x00200000 || c.xidMask != 0x001fffff { | 102 | if c.Root() != 0x39f || c.xidBase != 0x00200000 || c.xidMask != 0x001fffff { | ||
| 92 | t.Fatalf("root %#x base %#x mask %#x", c.Root(), c.xidBase, c.xidMask) | 103 | t.Fatalf("root %#x base %#x mask %#x", c.Root(), c.xidBase, c.xidMask) | ||
| 93 | } | 104 | } | ||
| 94 | - | if c.Fd() != fd || c.Buf().Ext != c.Ext() || c.Buf().Next() != 1 { | 105 | + | // The bootstrap issued one QueryExtension per extension and a fence. |
| 95 | - | t.Fatal("accessors") | 106 | + | if c.Fd() != fd || c.Buf().Ext != c.Ext() || c.Buf().Next() != Seq(extCount)+1 { |
| 107 | + | t.Fatal("accessors", c.Buf().Next()) | |||
| 96 | } | 108 | } | ||
| 97 | if c.Ext().Major(ExtXFixes) != 0 || !c.Ext().Info(ExtCore).Present { | 109 | if c.Ext().Major(ExtXFixes) != 0 || !c.Ext().Info(ExtCore).Present { | ||
| 98 | t.Fatal("ExtTable not empty") | 110 | t.Fatal("ExtTable not empty") | ||
| @@ -219,32 +231,18 @@ func truncateSetup(setup []byte, n int) []byte { | |||||
| 219 | } | 231 | } | ||
| 220 | 232 | ||||
| 221 | func TestConnFlushClose(t *testing.T) { | 233 | func TestConnFlushClose(t *testing.T) { | ||
| 222 | - | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | 234 | + | c, srv := dialScript(t, &script{}) |
| 223 | - | if err != nil { | 235 | + | first := c.Buf().Next() |
| 224 | - | t.Fatal(err) | |||
| 225 | - | } | |||
| 226 | - | server := os.NewFile(uintptr(fds[1]), "server") | |||
| 227 | - | defer server.Close() | |||
| 228 | - | setup := loadSetup(t, "Success") | |||
| 229 | - | go func() { | |||
| 230 | - | var req [12]byte | |||
| 231 | - | _, _ = io.ReadFull(server, req[:]) | |||
| 232 | - | _, _ = server.Write(setup) | |||
| 233 | - | }() | |||
| 234 | - | c, err := NewConn(fds[0]) | |||
| 235 | - | if err != nil { | |||
| 236 | - | t.Fatal(err) | |||
| 237 | - | } | |||
| 238 | p, seq := c.Buf().Request(43, 0, 0) // GetInputFocus | 236 | p, seq := c.Buf().Request(43, 0, 0) // GetInputFocus | ||
| 239 | - | if p == nil || seq != 1 { | 237 | + | if p == nil || seq != first { |
| 240 | t.Fatal("request") | 238 | t.Fatal("request") | ||
| 241 | } | 239 | } | ||
| 242 | if err := c.Flush(); err != nil { | 240 | if err := c.Flush(); err != nil { | ||
| 243 | t.Fatal(err) | 241 | t.Fatal(err) | ||
| 244 | } | 242 | } | ||
| 245 | - | var got [4]byte | 243 | + | waitRequests(t, srv, int(seq)) |
| 246 | - | if _, err := io.ReadFull(server, got[:]); err != nil || got != [4]byte{43, 0, 1, 0} { | 244 | + | if got := srv.requests()[seq-1].raw; !bytes.Equal(got, []byte{43, 0, 1, 0}) { |
| 247 | - | t.Fatalf("%x %v", got, err) | 245 | + | t.Fatalf("%x", got) |
| 248 | } | 246 | } | ||
| 249 | if err := c.Close(); err != nil { | 247 | if err := c.Close(); err != nil { | ||
| 250 | t.Fatal(err) | 248 | t.Fatal(err) | ||
M conn_xserver_test.go +1 -1
| @@ -125,7 +125,7 @@ func TestXserverDial(t *testing.T) { | |||
| 125 | 125 | const kindFocus, kindGeom = 16, 17 | |
| 126 | 126 | seq := xproto.GetInputFocus(c.Buf()) | |
| 127 | 127 | bad := xproto.GetGeometry(c.Buf(), xproto.Drawable(xproto.WindowNone)) | |
| 128 | - | if seq != 1 || bad != 2 { | |
| 128 | + | if seq == 0 || bad != seq+1 { | |
| 129 | 129 | t.Fatalf("seqs %d %d", seq, bad) | |
| 130 | 130 | } | |
| 131 | 131 | if err := c.Expect(seq, kindFocus, 1, 2, 3, 4); err != nil { | |
| @@ -125,7 +125,7 @@ func TestXserverDial(t *testing.T) { | |||||
| 125 | const kindFocus, kindGeom = 16, 17 | 125 | const kindFocus, kindGeom = 16, 17 | ||
| 126 | seq := xproto.GetInputFocus(c.Buf()) | 126 | seq := xproto.GetInputFocus(c.Buf()) | ||
| 127 | bad := xproto.GetGeometry(c.Buf(), xproto.Drawable(xproto.WindowNone)) | 127 | bad := xproto.GetGeometry(c.Buf(), xproto.Drawable(xproto.WindowNone)) | ||
| 128 | - | if seq != 1 || bad != 2 { | 128 | + | if seq == 0 || bad != seq+1 { |
| 129 | t.Fatalf("seqs %d %d", seq, bad) | 129 | t.Fatalf("seqs %d %d", seq, bad) | ||
| 130 | } | 130 | } | ||
| 131 | if err := c.Expect(seq, kindFocus, 1, 2, 3, 4); err != nil { | 131 | if err := c.Expect(seq, kindFocus, 1, 2, 3, 4); err != nil { | ||
M dispatch.go +14 -4
| @@ -17,7 +17,8 @@ const MaxPacket = 16 << 20 | |||
| 17 | 17 | // ErrPacketTooLarge reports a server packet longer than MaxPacket. | |
| 18 | 18 | var ErrPacketTooLarge = errors.New("xturbo: packet too large") | |
| 19 | 19 | ||
| 20 | - | // errReentrantDispatch is returned by a Dispatch called from a Handler | |
| 20 | + | // errReentrantDispatch is returned by Dispatch, and by the blocking calls | |
| 21 | + | // built on it (Sync, InternAtoms, a NewXID refill), when called from a Handler | |
| 21 | 22 | // callback. It is unexported: it reports a programming error, not a condition | |
| 22 | 23 | // callers handle. The outer Dispatch is unaffected. | |
| 23 | 24 | var errReentrantDispatch = errors.New("xturbo: Dispatch called from a Handler callback") | |
| @@ -34,8 +35,8 @@ const ( | |||
| 34 | 35 | // once at init; the pointers passed to callbacks are owned by the Conn and are | |
| 35 | 36 | // reused for the next packet, so callbacks must not retain them, nor the | |
| 36 | 37 | // views they hold (Reply.Raw, Event.Tail). Callbacks may queue requests and | |
| 37 | - | // call Expect but must not call Dispatch: a nested Dispatch returns an error | |
| 38 | - | // and changes nothing. | |
| 38 | + | // call Expect but must not call Dispatch, Sync, InternAtoms, nor NewXID when | |
| 39 | + | // it needs a refill: those return an error there and change nothing. | |
| 39 | 40 | type Handler struct { | |
| 40 | 41 | OnEvent func(ev *Event) | |
| 41 | 42 | OnReply func(r *Reply) | |
| @@ -61,7 +62,10 @@ var noHandler Handler | |||
| 61 | 62 | // still readable, and Flush requests queued by callbacks in between. It | |
| 62 | 63 | // returns io.EOF when the server closed the connection and ErrPacketTooLarge | |
| 63 | 64 | // on a packet beyond MaxPacket. Read errors are sticky and also fail the Buf. | |
| 64 | - | // Dispatch never flushes. A nil h drops every packet. | |
| 65 | + | // Dispatch never flushes. A nil h drops every packet; a non-nil h is | |
| 66 | + | // remembered for the packets Sync(nil), InternAtoms and NewXID read while they | |
| 67 | + | // block. Errors on the requests root issues itself (reserved kinds) are | |
| 68 | + | // handled internally and never reach OnError. | |
| 65 | 69 | func (c *Conn) Dispatch(h *Handler) error { | |
| 66 | 70 | if c.dispatching { | |
| 67 | 71 | return errReentrantDispatch | |
| @@ -76,8 +80,12 @@ func (c *Conn) Dispatch(h *Handler) error { | |||
| 76 | 80 | } | |
| 77 | 81 | if h == nil { | |
| 78 | 82 | h = &noHandler | |
| 83 | + | } else { | |
| 84 | + | c.h = h | |
| 79 | 85 | } | |
| 80 | 86 | budget := len(c.rb) | |
| 87 | + | c.rdIdle = false | |
| 88 | + | read := false | |
| 81 | 89 | for { | |
| 82 | 90 | if err := c.drain(h); err != nil { | |
| 83 | 91 | return c.failRead(err) | |
| @@ -93,6 +101,7 @@ func (c *Conn) Dispatch(h *Handler) error { | |||
| 93 | 101 | case err == unix.EINTR: | |
| 94 | 102 | continue | |
| 95 | 103 | case err == unix.EAGAIN: | |
| 104 | + | c.rdIdle = !read | |
| 96 | 105 | return nil | |
| 97 | 106 | case err == unix.ECONNRESET: | |
| 98 | 107 | return c.failRead(io.EOF) | |
| @@ -103,6 +112,7 @@ func (c *Conn) Dispatch(h *Handler) error { | |||
| 103 | 112 | } | |
| 104 | 113 | c.rend += n | |
| 105 | 114 | budget -= n | |
| 115 | + | read = true | |
| 106 | 116 | } | |
| 107 | 117 | } | |
| 108 | 118 | ||
| @@ -17,7 +17,8 @@ const MaxPacket = 16 << 20 | |||||
| 17 | // ErrPacketTooLarge reports a server packet longer than MaxPacket. | 17 | // ErrPacketTooLarge reports a server packet longer than MaxPacket. | ||
| 18 | var ErrPacketTooLarge = errors.New("xturbo: packet too large") | 18 | var ErrPacketTooLarge = errors.New("xturbo: packet too large") | ||
| 19 | 19 | ||||
| 20 | - | // errReentrantDispatch is returned by a Dispatch called from a Handler | 20 | + | // errReentrantDispatch is returned by Dispatch, and by the blocking calls |
| 21 | + | // built on it (Sync, InternAtoms, a NewXID refill), when called from a Handler | |||
| 21 | // callback. It is unexported: it reports a programming error, not a condition | 22 | // callback. It is unexported: it reports a programming error, not a condition | ||
| 22 | // callers handle. The outer Dispatch is unaffected. | 23 | // callers handle. The outer Dispatch is unaffected. | ||
| 23 | var errReentrantDispatch = errors.New("xturbo: Dispatch called from a Handler callback") | 24 | var errReentrantDispatch = errors.New("xturbo: Dispatch called from a Handler callback") | ||
| @@ -34,8 +35,8 @@ const ( | |||||
| 34 | // once at init; the pointers passed to callbacks are owned by the Conn and are | 35 | // once at init; the pointers passed to callbacks are owned by the Conn and are | ||
| 35 | // reused for the next packet, so callbacks must not retain them, nor the | 36 | // reused for the next packet, so callbacks must not retain them, nor the | ||
| 36 | // views they hold (Reply.Raw, Event.Tail). Callbacks may queue requests and | 37 | // views they hold (Reply.Raw, Event.Tail). Callbacks may queue requests and | ||
| 37 | - | // call Expect but must not call Dispatch: a nested Dispatch returns an error | 38 | + | // call Expect but must not call Dispatch, Sync, InternAtoms, nor NewXID when |
| 38 | - | // and changes nothing. | 39 | + | // it needs a refill: those return an error there and change nothing. |
| 39 | type Handler struct { | 40 | type Handler struct { | ||
| 40 | OnEvent func(ev *Event) | 41 | OnEvent func(ev *Event) | ||
| 41 | OnReply func(r *Reply) | 42 | OnReply func(r *Reply) | ||
| @@ -61,7 +62,10 @@ var noHandler Handler | |||||
| 61 | // still readable, and Flush requests queued by callbacks in between. It | 62 | // still readable, and Flush requests queued by callbacks in between. It | ||
| 62 | // returns io.EOF when the server closed the connection and ErrPacketTooLarge | 63 | // returns io.EOF when the server closed the connection and ErrPacketTooLarge | ||
| 63 | // on a packet beyond MaxPacket. Read errors are sticky and also fail the Buf. | 64 | // on a packet beyond MaxPacket. Read errors are sticky and also fail the Buf. | ||
| 64 | - | // Dispatch never flushes. A nil h drops every packet. | 65 | + | // Dispatch never flushes. A nil h drops every packet; a non-nil h is |
| 66 | + | // remembered for the packets Sync(nil), InternAtoms and NewXID read while they | |||
| 67 | + | // block. Errors on the requests root issues itself (reserved kinds) are | |||
| 68 | + | // handled internally and never reach OnError. | |||
| 65 | func (c *Conn) Dispatch(h *Handler) error { | 69 | func (c *Conn) Dispatch(h *Handler) error { | ||
| 66 | if c.dispatching { | 70 | if c.dispatching { | ||
| 67 | return errReentrantDispatch | 71 | return errReentrantDispatch | ||
| @@ -76,8 +80,12 @@ func (c *Conn) Dispatch(h *Handler) error { | |||||
| 76 | } | 80 | } | ||
| 77 | if h == nil { | 81 | if h == nil { | ||
| 78 | h = &noHandler | 82 | h = &noHandler | ||
| 83 | + | } else { | |||
| 84 | + | c.h = h | |||
| 79 | } | 85 | } | ||
| 80 | budget := len(c.rb) | 86 | budget := len(c.rb) | ||
| 87 | + | c.rdIdle = false | |||
| 88 | + | read := false | |||
| 81 | for { | 89 | for { | ||
| 82 | if err := c.drain(h); err != nil { | 90 | if err := c.drain(h); err != nil { | ||
| 83 | return c.failRead(err) | 91 | return c.failRead(err) | ||
| @@ -93,6 +101,7 @@ func (c *Conn) Dispatch(h *Handler) error { | |||||
| 93 | case err == unix.EINTR: | 101 | case err == unix.EINTR: | ||
| 94 | continue | 102 | continue | ||
| 95 | case err == unix.EAGAIN: | 103 | case err == unix.EAGAIN: | ||
| 104 | + | c.rdIdle = !read | |||
| 96 | return nil | 105 | return nil | ||
| 97 | case err == unix.ECONNRESET: | 106 | case err == unix.ECONNRESET: | ||
| 98 | return c.failRead(io.EOF) | 107 | return c.failRead(io.EOF) | ||
| @@ -103,6 +112,7 @@ func (c *Conn) Dispatch(h *Handler) error { | |||||
| 103 | } | 112 | } | ||
| 104 | c.rend += n | 113 | c.rend += n | ||
| 105 | budget -= n | 114 | budget -= n | ||
| 115 | + | read = true | |||
| 106 | } | 116 | } | ||
| 107 | } | 117 | } | ||
| 108 | 118 | ||||
M export_test.go +31 -0
| @@ -1,4 +1,35 @@ | |||
| 1 | 1 | package xturbo | |
| 2 | 2 | ||
| 3 | + | import "time" | |
| 4 | + | ||
| 3 | 5 | // SetMaxReqWords exposes the Conn-owned BIG-REQUESTS limit setter to external tests. | |
| 4 | 6 | var SetMaxReqWords = (*Buf).setMaxReqWords | |
| 7 | + | ||
| 8 | + | // testTimeouts bounds every blocking call in tests so a broken script fails | |
| 9 | + | // fast instead of hanging until the go test timeout; tests of the deadlines | |
| 10 | + | // shorten them further with shortTimeouts. | |
| 11 | + | func init() { | |
| 12 | + | connectTimeout = 3 * time.Second | |
| 13 | + | syncTimeout = 3 * time.Second | |
| 14 | + | } | |
| 15 | + | ||
| 16 | + | // shortTimeouts sets d as both bounds for the test's duration. | |
| 17 | + | func shortTimeouts(t interface{ Cleanup(func()) }, d time.Duration) { | |
| 18 | + | c, s := connectTimeout, syncTimeout | |
| 19 | + | connectTimeout, syncTimeout = d, d | |
| 20 | + | t.Cleanup(func() { connectTimeout, syncTimeout = c, s }) | |
| 21 | + | } | |
| 22 | + | ||
| 23 | + | // ExhaustXIDs empties the setup range and the free list so the next NewXID | |
| 24 | + | // refills through XC-MISC. | |
| 25 | + | func ExhaustXIDs(c *Conn) { c.xids, c.xidFreeN = xidRange{}, 0 } | |
| 26 | + | ||
| 27 | + | // Root's own encoders of the requests it issues, compared with the generated | |
| 28 | + | // ones and the goldens by external tests. | |
| 29 | + | var ( | |
| 30 | + | EncodeQueryExtension = encodeQueryExtension | |
| 31 | + | EncodeInternAtom = encodeInternAtom | |
| 32 | + | EncodeGetInputFocus = encodeGetInputFocus | |
| 33 | + | EncodeBigreqEnable = func(b *Buf) Seq { return encodeExtRequest(b, ExtBigRequests, 0) } | |
| 34 | + | EncodeGetXIDRange = func(b *Buf) Seq { return encodeExtRequest(b, ExtXCMisc, minorGetXIDRange) } | |
| 35 | + | ) | |
| @@ -1,4 +1,35 @@ | |||||
| 1 | package xturbo | 1 | package xturbo | ||
| 2 | 2 | ||||
| 3 | + | import "time" | |||
| 4 | + | ||||
| 3 | // SetMaxReqWords exposes the Conn-owned BIG-REQUESTS limit setter to external tests. | 5 | // SetMaxReqWords exposes the Conn-owned BIG-REQUESTS limit setter to external tests. | ||
| 4 | var SetMaxReqWords = (*Buf).setMaxReqWords | 6 | var SetMaxReqWords = (*Buf).setMaxReqWords | ||
| 7 | + | ||||
| 8 | + | // testTimeouts bounds every blocking call in tests so a broken script fails | |||
| 9 | + | // fast instead of hanging until the go test timeout; tests of the deadlines | |||
| 10 | + | // shorten them further with shortTimeouts. | |||
| 11 | + | func init() { | |||
| 12 | + | connectTimeout = 3 * time.Second | |||
| 13 | + | syncTimeout = 3 * time.Second | |||
| 14 | + | } | |||
| 15 | + | ||||
| 16 | + | // shortTimeouts sets d as both bounds for the test's duration. | |||
| 17 | + | func shortTimeouts(t interface{ Cleanup(func()) }, d time.Duration) { | |||
| 18 | + | c, s := connectTimeout, syncTimeout | |||
| 19 | + | connectTimeout, syncTimeout = d, d | |||
| 20 | + | t.Cleanup(func() { connectTimeout, syncTimeout = c, s }) | |||
| 21 | + | } | |||
| 22 | + | ||||
| 23 | + | // ExhaustXIDs empties the setup range and the free list so the next NewXID | |||
| 24 | + | // refills through XC-MISC. | |||
| 25 | + | func ExhaustXIDs(c *Conn) { c.xids, c.xidFreeN = xidRange{}, 0 } | |||
| 26 | + | ||||
| 27 | + | // Root's own encoders of the requests it issues, compared with the generated | |||
| 28 | + | // ones and the goldens by external tests. | |||
| 29 | + | var ( | |||
| 30 | + | EncodeQueryExtension = encodeQueryExtension | |||
| 31 | + | EncodeInternAtom = encodeInternAtom | |||
| 32 | + | EncodeGetInputFocus = encodeGetInputFocus | |||
| 33 | + | EncodeBigreqEnable = func(b *Buf) Seq { return encodeExtRequest(b, ExtBigRequests, 0) } | |||
| 34 | + | EncodeGetXIDRange = func(b *Buf) Seq { return encodeExtRequest(b, ExtXCMisc, minorGetXIDRange) } | |||
| 35 | + | ) | |||
A hardening_test.go +358 -0
| @@ -0,0 +1,358 @@ | |||
| 1 | + | package xturbo | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "os" | |
| 6 | + | "strings" | |
| 7 | + | "testing" | |
| 8 | + | "time" | |
| 9 | + | ||
| 10 | + | "golang.org/x/sys/unix" | |
| 11 | + | ) | |
| 12 | + | ||
| 13 | + | const shortDeadline = 150 * time.Millisecond | |
| 14 | + | ||
| 15 | + | // within runs f and fails unless it returns an error wrapping | |
| 16 | + | // os.ErrDeadlineExceeded after about d (not much later). | |
| 17 | + | func within(t *testing.T, d time.Duration, f func() error) { | |
| 18 | + | t.Helper() | |
| 19 | + | start := time.Now() | |
| 20 | + | err := f() | |
| 21 | + | took := time.Since(start) | |
| 22 | + | if !errors.Is(err, os.ErrDeadlineExceeded) { | |
| 23 | + | t.Fatalf("err %v, want a deadline error", err) | |
| 24 | + | } | |
| 25 | + | if took < d || took > d+2*time.Second { | |
| 26 | + | t.Fatalf("returned after %v, deadline %v", took, d) | |
| 27 | + | } | |
| 28 | + | } | |
| 29 | + | ||
| 30 | + | // A server that accepts the connection but never answers the setup request. | |
| 31 | + | func TestNewConnSetupDeadline(t *testing.T) { | |
| 32 | + | shortTimeouts(t, shortDeadline) | |
| 33 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 34 | + | if err != nil { | |
| 35 | + | t.Fatal(err) | |
| 36 | + | } | |
| 37 | + | defer unix.Close(fds[1]) | |
| 38 | + | within(t, shortDeadline, func() error { | |
| 39 | + | c, err := NewConn(fds[0]) | |
| 40 | + | if c != nil { | |
| 41 | + | t.Fatal("conn") | |
| 42 | + | } | |
| 43 | + | return err | |
| 44 | + | }) | |
| 45 | + | if _, err := unix.FcntlInt(uintptr(fds[0]), unix.F_GETFD, 0); err != unix.EBADF { | |
| 46 | + | t.Fatal("fd not closed") | |
| 47 | + | } | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | // A server that completes the setup, then hangs during the bootstrap. | |
| 51 | + | func TestNewConnBootstrapDeadline(t *testing.T) { | |
| 52 | + | shortTimeouts(t, shortDeadline) | |
| 53 | + | sc := &script{} | |
| 54 | + | sc.silent.Store(true) | |
| 55 | + | fd, _ := startScript(t, sc) | |
| 56 | + | within(t, shortDeadline, func() error { _, err := NewConn(fd); return err }) | |
| 57 | + | } | |
| 58 | + | ||
| 59 | + | func TestSyncDeadline(t *testing.T) { | |
| 60 | + | sc := &script{exts: fullExts()} | |
| 61 | + | c, _ := dialScript(t, sc) | |
| 62 | + | sc.silent.Store(true) | |
| 63 | + | c.timeout = shortDeadline | |
| 64 | + | within(t, shortDeadline, func() error { return c.Sync(nil) }) | |
| 65 | + | within(t, shortDeadline, func() error { return c.InternAtoms([]string{"A"}, false, make([]uint32, 1)) }) | |
| 66 | + | c.xids = xidRange{} | |
| 67 | + | within(t, shortDeadline, func() error { _, err := c.NewXID(); return err }) | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | // A server flooding events but never answering the fence still times out. | |
| 71 | + | func TestSyncDeadlineFlood(t *testing.T) { | |
| 72 | + | sc := &script{} | |
| 73 | + | c, srv := dialScript(t, sc) | |
| 74 | + | c.timeout = shortDeadline | |
| 75 | + | sc.silent.Store(true) | |
| 76 | + | // Events carry the last request the server processed: the bootstrap's. | |
| 77 | + | ev := pktEvent(19, uint16(c.seq), 0) | |
| 78 | + | stop := make(chan struct{}) | |
| 79 | + | done := make(chan struct{}) | |
| 80 | + | go func() { | |
| 81 | + | defer close(done) | |
| 82 | + | for { | |
| 83 | + | select { | |
| 84 | + | case <-stop: | |
| 85 | + | return | |
| 86 | + | default: | |
| 87 | + | // Written raw: the silent server goroutine owns srv.seq. | |
| 88 | + | if _, err := srv.f.Write(ev); err != nil { | |
| 89 | + | return | |
| 90 | + | } | |
| 91 | + | } | |
| 92 | + | } | |
| 93 | + | }() | |
| 94 | + | defer func() { close(stop); <-done }() | |
| 95 | + | within(t, shortDeadline, func() error { return c.Sync(nil) }) | |
| 96 | + | } | |
| 97 | + | ||
| 98 | + | // A GetXIDRange answer whose last ID overflows the mask (into other clients' | |
| 99 | + | // IDs) is rejected. | |
| 100 | + | func TestXIDRangeBounds(t *testing.T) { | |
| 101 | + | c, _ := dialScript(t, &script{}) | |
| 102 | + | // Recorded setup: base 0x00200000, mask 0x001fffff (inc 1). | |
| 103 | + | for _, tc := range []struct { | |
| 104 | + | start, count uint32 | |
| 105 | + | ok bool | |
| 106 | + | }{ | |
| 107 | + | {0x00200000, 0xffffffff, false}, | |
| 108 | + | {0x00200000, 0x00200001, false}, | |
| 109 | + | {0x00200000, 0x00200000, true}, | |
| 110 | + | {0x003fffff, 1, true}, | |
| 111 | + | {0x003fffff, 2, false}, | |
| 112 | + | {0x00400000, 1, false}, // other client's base | |
| 113 | + | {0, 1, false}, | |
| 114 | + | {0x00200040, 0, false}, | |
| 115 | + | } { | |
| 116 | + | if got := c.validXIDRange(tc.start, tc.count); got != tc.ok { | |
| 117 | + | t.Errorf("{%#x, %#x}: %v", tc.start, tc.count, got) | |
| 118 | + | } | |
| 119 | + | } | |
| 120 | + | // A mask with inc > 1: the start must be aligned. | |
| 121 | + | c.xidMask = 0x001ffff0 | |
| 122 | + | if c.validXIDRange(0x00200008, 1) || !c.validXIDRange(0x00200010, 2) || c.validXIDRange(0x001ffff0|0x00200000, 2) { | |
| 123 | + | t.Fatal("inc 16") | |
| 124 | + | } | |
| 125 | + | } | |
| 126 | + | ||
| 127 | + | func TestNewXIDRejectsOverflowingRange(t *testing.T) { | |
| 128 | + | sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200000, 0xffffffff}}} | |
| 129 | + | c, _ := dialScript(t, sc) | |
| 130 | + | c.xids = xidRange{} | |
| 131 | + | if _, err := c.NewXID(); !errors.Is(err, errXIDsExhausted) { | |
| 132 | + | t.Fatal(err) | |
| 133 | + | } | |
| 134 | + | } | |
| 135 | + | ||
| 136 | + | // XIDsLow + PrefetchXIDs let a callback allocate past the end of the current | |
| 137 | + | // range without blocking. | |
| 138 | + | func TestPrefetchXIDsFromCallback(t *testing.T) { | |
| 139 | + | sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200100, 3}}} | |
| 140 | + | sc.hook = func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool { | |
| 141 | + | if major == 8 { | |
| 142 | + | s.event(19, 0) | |
| 143 | + | return true | |
| 144 | + | } | |
| 145 | + | return false | |
| 146 | + | } | |
| 147 | + | c, srv := dialScript(t, sc) | |
| 148 | + | c.xids = xidRange{next: 0x10, left: 2} | |
| 149 | + | if !c.XIDsLow() { | |
| 150 | + | t.Fatal("not low") | |
| 151 | + | } | |
| 152 | + | var got []uint32 | |
| 153 | + | var errs []error | |
| 154 | + | h := Handler{OnEvent: func(*Event) { | |
| 155 | + | // Inside a callback: the prefetch is queued, no wait. | |
| 156 | + | if c.XIDsLow() { | |
| 157 | + | if err := c.PrefetchXIDs(); err != nil { | |
| 158 | + | errs = append(errs, err) | |
| 159 | + | } | |
| 160 | + | } | |
| 161 | + | }} | |
| 162 | + | c.Buf().Request(8, 0, 4) | |
| 163 | + | if err := c.Sync(&h); err != nil { | |
| 164 | + | t.Fatal(err) | |
| 165 | + | } | |
| 166 | + | if len(errs) != 0 || c.xidPend.left != 3 || c.XIDsLow() { | |
| 167 | + | t.Fatal(errs, c.xidPend, c.XIDsLow()) | |
| 168 | + | } | |
| 169 | + | // Second event: allocate 5 ids inside the callback, crossing ranges. | |
| 170 | + | h.OnEvent = func(*Event) { | |
| 171 | + | for range 5 { | |
| 172 | + | id, err := c.NewXID() | |
| 173 | + | if err != nil { | |
| 174 | + | errs = append(errs, err) | |
| 175 | + | return | |
| 176 | + | } | |
| 177 | + | got = append(got, id) | |
| 178 | + | } | |
| 179 | + | // Now everything is used; NewXID cannot block here. | |
| 180 | + | if _, err := c.NewXID(); !errors.Is(err, errReentrantDispatch) { | |
| 181 | + | errs = append(errs, err) | |
| 182 | + | } | |
| 183 | + | } | |
| 184 | + | c.Buf().Request(8, 0, 4) | |
| 185 | + | if err := c.Sync(&h); err != nil { | |
| 186 | + | t.Fatal(err) | |
| 187 | + | } | |
| 188 | + | want := []uint32{0x00200010, 0x00200011, 0x00200100, 0x00200101, 0x00200102} | |
| 189 | + | if len(errs) != 0 || len(got) != 5 { | |
| 190 | + | t.Fatal(errs, got) | |
| 191 | + | } | |
| 192 | + | for i := range want { | |
| 193 | + | if got[i] != want[i] { | |
| 194 | + | t.Fatalf("%#x", got) | |
| 195 | + | } | |
| 196 | + | } | |
| 197 | + | var ranges int | |
| 198 | + | for _, r := range srv.requests() { | |
| 199 | + | if r.major == 136 && r.minor == minorGetXIDRange { | |
| 200 | + | ranges++ | |
| 201 | + | } | |
| 202 | + | } | |
| 203 | + | if ranges != 1 { | |
| 204 | + | t.Fatal("GetXIDRange requests", ranges) | |
| 205 | + | } | |
| 206 | + | } | |
| 207 | + | ||
| 208 | + | func TestPrefetchXIDsStates(t *testing.T) { | |
| 209 | + | sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0, 1}}} | |
| 210 | + | c, srv := dialScript(t, sc) | |
| 211 | + | if c.XIDsLow() { | |
| 212 | + | t.Fatal("fresh setup range reported low") | |
| 213 | + | } | |
| 214 | + | c.xids = xidRange{next: 0x10, left: 1} | |
| 215 | + | before := len(srv.requests()) | |
| 216 | + | // Twice: one request only while in flight. | |
| 217 | + | first, second := c.PrefetchXIDs(), c.PrefetchXIDs() | |
| 218 | + | if first != nil || second != nil || c.XIDsLow() { | |
| 219 | + | t.Fatal("prefetch", first, second) | |
| 220 | + | } | |
| 221 | + | if err := c.Sync(nil); err != nil { | |
| 222 | + | t.Fatal(err) | |
| 223 | + | } | |
| 224 | + | if n := len(srv.requests()) - before; n != 2 { // GetXIDRange + fence | |
| 225 | + | t.Fatal(n) | |
| 226 | + | } | |
| 227 | + | // The server had none: not low (nothing to gain), current id still works, | |
| 228 | + | // then NewXID's blocking refill reports exhaustion. | |
| 229 | + | if c.XIDsLow() { | |
| 230 | + | t.Fatal("low after a refused prefetch") | |
| 231 | + | } | |
| 232 | + | if _, err := c.NewXID(); err != nil { | |
| 233 | + | t.Fatal(err) | |
| 234 | + | } | |
| 235 | + | if _, err := c.NewXID(); !errors.Is(err, errXIDsExhausted) { | |
| 236 | + | t.Fatal(err) | |
| 237 | + | } | |
| 238 | + | // Without XC-MISC. | |
| 239 | + | c2, _ := dialScript(t, &script{}) | |
| 240 | + | c2.xids = xidRange{} | |
| 241 | + | if c2.XIDsLow() || !errors.Is(c2.PrefetchXIDs(), ErrExtMissing) { | |
| 242 | + | t.Fatal("no XC-MISC") | |
| 243 | + | } | |
| 244 | + | } | |
| 245 | + | ||
| 246 | + | // A blocking refill waits for a prefetch already in flight instead of sending | |
| 247 | + | // a second request; an X error on it is returned by NewXID, not OnError. | |
| 248 | + | func TestRefillWaitsForPrefetch(t *testing.T) { | |
| 249 | + | sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200200, 4}}} | |
| 250 | + | c, srv := dialScript(t, sc) | |
| 251 | + | c.xids = xidRange{} | |
| 252 | + | if err := c.PrefetchXIDs(); err != nil { | |
| 253 | + | t.Fatal(err) | |
| 254 | + | } | |
| 255 | + | id, err := c.NewXID() | |
| 256 | + | if err != nil || id != 0x00200200 { | |
| 257 | + | t.Fatalf("%#x %v", id, err) | |
| 258 | + | } | |
| 259 | + | var ranges int | |
| 260 | + | for _, r := range srv.requests() { | |
| 261 | + | if r.major == 136 && r.minor == minorGetXIDRange { | |
| 262 | + | ranges++ | |
| 263 | + | } | |
| 264 | + | } | |
| 265 | + | if ranges != 1 { | |
| 266 | + | t.Fatal(ranges) | |
| 267 | + | } | |
| 268 | + | ||
| 269 | + | esc := &script{exts: fullExts()} | |
| 270 | + | esc.hook = func(s *scriptConn, major, minor uint8, _ uint16, _ []byte) bool { | |
| 271 | + | if major == 136 && minor == minorGetXIDRange { | |
| 272 | + | s.xerror(11, major, 0) | |
| 273 | + | return true | |
| 274 | + | } | |
| 275 | + | return false | |
| 276 | + | } | |
| 277 | + | ec, _ := dialScript(t, esc) | |
| 278 | + | var userErrs int | |
| 279 | + | h := Handler{OnError: func(*XError) { userErrs++ }} | |
| 280 | + | ec.xids = xidRange{} | |
| 281 | + | if err := ec.PrefetchXIDs(); err != nil { | |
| 282 | + | t.Fatal(err) | |
| 283 | + | } | |
| 284 | + | if err := ec.Sync(&h); err != nil { | |
| 285 | + | t.Fatal("a prefetch error leaked into Sync:", err) | |
| 286 | + | } | |
| 287 | + | var xe *XError | |
| 288 | + | if _, err := ec.NewXID(); !errors.As(err, &xe) || xe.Kind != kindXIDRange || userErrs != 0 { | |
| 289 | + | t.Fatal(err, userErrs) | |
| 290 | + | } | |
| 291 | + | } | |
| 292 | + | ||
| 293 | + | // Steady state: XIDsLow does not allocate. | |
| 294 | + | func TestAllocXIDsLow(t *testing.T) { | |
| 295 | + | c, _ := dialScript(t, &script{exts: fullExts()}) | |
| 296 | + | if n := testing.AllocsPerRun(1000, func() { _ = c.XIDsLow() }); n != 0 { | |
| 297 | + | t.Fatal(n) | |
| 298 | + | } | |
| 299 | + | } | |
| 300 | + | ||
| 301 | + | // An over-long name is a caller bug: ErrBadLength, nothing queued, the Buf | |
| 302 | + | // still usable. | |
| 303 | + | func TestInternAtomsNameTooLong(t *testing.T) { | |
| 304 | + | c, _ := dialScript(t, &script{}) | |
| 305 | + | next := c.Buf().Next() | |
| 306 | + | err := c.InternAtoms([]string{"OK", strings.Repeat("x", 0x10000)}, false, make([]uint32, 2)) | |
| 307 | + | if !errors.Is(err, ErrBadLength) || c.Buf().Err() != nil || c.Buf().Next() != next { | |
| 308 | + | t.Fatal(err, c.Buf().Err()) | |
| 309 | + | } | |
| 310 | + | dst := make([]uint32, 1) | |
| 311 | + | if err := c.InternAtoms([]string{"OK"}, false, dst); err != nil || dst[0] == 0 { | |
| 312 | + | t.Fatal(err) | |
| 313 | + | } | |
| 314 | + | } | |
| 315 | + | ||
| 316 | + | // 0xffff requests queued without a Flush reach ErrSeqOverflow (the auto-resync | |
| 317 | + | // fence needs a Flush to be appended); with a Flush every so often the fence | |
| 318 | + | // keeps the stream going far past 0xffff. | |
| 319 | + | func TestAutoResyncVsSeqOverflow(t *testing.T) { | |
| 320 | + | c, _ := dialScript(t, &script{}) | |
| 321 | + | b := c.Buf() | |
| 322 | + | for b.Err() == nil { | |
| 323 | + | b.Request(8, 0, 4) | |
| 324 | + | } | |
| 325 | + | if !errors.Is(b.Err(), ErrSeqOverflow) || b.Next()-b.acked != maxSeqAhead { | |
| 326 | + | t.Fatal(b.Err(), b.Next()-b.acked) | |
| 327 | + | } | |
| 328 | + | if c.Stats().Resyncs != 0 { | |
| 329 | + | t.Fatal(c.Stats()) | |
| 330 | + | } | |
| 331 | + | ||
| 332 | + | c2, _ := dialScript(t, &script{}) | |
| 333 | + | b2 := c2.Buf() | |
| 334 | + | for i := range 3 * 0x10000 { | |
| 335 | + | b2.Request(8, 0, 4) | |
| 336 | + | if i%4096 == 0 { | |
| 337 | + | if err := flushAll(c2); err != nil { | |
| 338 | + | t.Fatal(err) | |
| 339 | + | } | |
| 340 | + | if err := c2.Dispatch(nil); err != nil { | |
| 341 | + | t.Fatal(err) | |
| 342 | + | } | |
| 343 | + | // Give the fence's reply time to come back before overflow. | |
| 344 | + | if b2.Next()-b2.acked > 0xe000 { | |
| 345 | + | dispatchFor(t, c2, func() bool { return b2.Next()-b2.acked < 0x9000 }) | |
| 346 | + | } | |
| 347 | + | } | |
| 348 | + | } | |
| 349 | + | if b2.Err() != nil { | |
| 350 | + | t.Fatal(b2.Err()) | |
| 351 | + | } | |
| 352 | + | if err := c2.Sync(nil); err != nil { | |
| 353 | + | t.Fatal(err) | |
| 354 | + | } | |
| 355 | + | if st := c2.Stats(); st.Resyncs < 3 || st.DroppedReplies != 0 { | |
| 356 | + | t.Fatalf("%+v", st) | |
| 357 | + | } | |
| 358 | + | } | |
| @@ -0,0 +1,358 @@ | |||||
| 1 | + | package xturbo | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "os" | |||
| 6 | + | "strings" | |||
| 7 | + | "testing" | |||
| 8 | + | "time" | |||
| 9 | + | ||||
| 10 | + | "golang.org/x/sys/unix" | |||
| 11 | + | ) | |||
| 12 | + | ||||
| 13 | + | const shortDeadline = 150 * time.Millisecond | |||
| 14 | + | ||||
| 15 | + | // within runs f and fails unless it returns an error wrapping | |||
| 16 | + | // os.ErrDeadlineExceeded after about d (not much later). | |||
| 17 | + | func within(t *testing.T, d time.Duration, f func() error) { | |||
| 18 | + | t.Helper() | |||
| 19 | + | start := time.Now() | |||
| 20 | + | err := f() | |||
| 21 | + | took := time.Since(start) | |||
| 22 | + | if !errors.Is(err, os.ErrDeadlineExceeded) { | |||
| 23 | + | t.Fatalf("err %v, want a deadline error", err) | |||
| 24 | + | } | |||
| 25 | + | if took < d || took > d+2*time.Second { | |||
| 26 | + | t.Fatalf("returned after %v, deadline %v", took, d) | |||
| 27 | + | } | |||
| 28 | + | } | |||
| 29 | + | ||||
| 30 | + | // A server that accepts the connection but never answers the setup request. | |||
| 31 | + | func TestNewConnSetupDeadline(t *testing.T) { | |||
| 32 | + | shortTimeouts(t, shortDeadline) | |||
| 33 | + | fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 34 | + | if err != nil { | |||
| 35 | + | t.Fatal(err) | |||
| 36 | + | } | |||
| 37 | + | defer unix.Close(fds[1]) | |||
| 38 | + | within(t, shortDeadline, func() error { | |||
| 39 | + | c, err := NewConn(fds[0]) | |||
| 40 | + | if c != nil { | |||
| 41 | + | t.Fatal("conn") | |||
| 42 | + | } | |||
| 43 | + | return err | |||
| 44 | + | }) | |||
| 45 | + | if _, err := unix.FcntlInt(uintptr(fds[0]), unix.F_GETFD, 0); err != unix.EBADF { | |||
| 46 | + | t.Fatal("fd not closed") | |||
| 47 | + | } | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | // A server that completes the setup, then hangs during the bootstrap. | |||
| 51 | + | func TestNewConnBootstrapDeadline(t *testing.T) { | |||
| 52 | + | shortTimeouts(t, shortDeadline) | |||
| 53 | + | sc := &script{} | |||
| 54 | + | sc.silent.Store(true) | |||
| 55 | + | fd, _ := startScript(t, sc) | |||
| 56 | + | within(t, shortDeadline, func() error { _, err := NewConn(fd); return err }) | |||
| 57 | + | } | |||
| 58 | + | ||||
| 59 | + | func TestSyncDeadline(t *testing.T) { | |||
| 60 | + | sc := &script{exts: fullExts()} | |||
| 61 | + | c, _ := dialScript(t, sc) | |||
| 62 | + | sc.silent.Store(true) | |||
| 63 | + | c.timeout = shortDeadline | |||
| 64 | + | within(t, shortDeadline, func() error { return c.Sync(nil) }) | |||
| 65 | + | within(t, shortDeadline, func() error { return c.InternAtoms([]string{"A"}, false, make([]uint32, 1)) }) | |||
| 66 | + | c.xids = xidRange{} | |||
| 67 | + | within(t, shortDeadline, func() error { _, err := c.NewXID(); return err }) | |||
| 68 | + | } | |||
| 69 | + | ||||
| 70 | + | // A server flooding events but never answering the fence still times out. | |||
| 71 | + | func TestSyncDeadlineFlood(t *testing.T) { | |||
| 72 | + | sc := &script{} | |||
| 73 | + | c, srv := dialScript(t, sc) | |||
| 74 | + | c.timeout = shortDeadline | |||
| 75 | + | sc.silent.Store(true) | |||
| 76 | + | // Events carry the last request the server processed: the bootstrap's. | |||
| 77 | + | ev := pktEvent(19, uint16(c.seq), 0) | |||
| 78 | + | stop := make(chan struct{}) | |||
| 79 | + | done := make(chan struct{}) | |||
| 80 | + | go func() { | |||
| 81 | + | defer close(done) | |||
| 82 | + | for { | |||
| 83 | + | select { | |||
| 84 | + | case <-stop: | |||
| 85 | + | return | |||
| 86 | + | default: | |||
| 87 | + | // Written raw: the silent server goroutine owns srv.seq. | |||
| 88 | + | if _, err := srv.f.Write(ev); err != nil { | |||
| 89 | + | return | |||
| 90 | + | } | |||
| 91 | + | } | |||
| 92 | + | } | |||
| 93 | + | }() | |||
| 94 | + | defer func() { close(stop); <-done }() | |||
| 95 | + | within(t, shortDeadline, func() error { return c.Sync(nil) }) | |||
| 96 | + | } | |||
| 97 | + | ||||
| 98 | + | // A GetXIDRange answer whose last ID overflows the mask (into other clients' | |||
| 99 | + | // IDs) is rejected. | |||
| 100 | + | func TestXIDRangeBounds(t *testing.T) { | |||
| 101 | + | c, _ := dialScript(t, &script{}) | |||
| 102 | + | // Recorded setup: base 0x00200000, mask 0x001fffff (inc 1). | |||
| 103 | + | for _, tc := range []struct { | |||
| 104 | + | start, count uint32 | |||
| 105 | + | ok bool | |||
| 106 | + | }{ | |||
| 107 | + | {0x00200000, 0xffffffff, false}, | |||
| 108 | + | {0x00200000, 0x00200001, false}, | |||
| 109 | + | {0x00200000, 0x00200000, true}, | |||
| 110 | + | {0x003fffff, 1, true}, | |||
| 111 | + | {0x003fffff, 2, false}, | |||
| 112 | + | {0x00400000, 1, false}, // other client's base | |||
| 113 | + | {0, 1, false}, | |||
| 114 | + | {0x00200040, 0, false}, | |||
| 115 | + | } { | |||
| 116 | + | if got := c.validXIDRange(tc.start, tc.count); got != tc.ok { | |||
| 117 | + | t.Errorf("{%#x, %#x}: %v", tc.start, tc.count, got) | |||
| 118 | + | } | |||
| 119 | + | } | |||
| 120 | + | // A mask with inc > 1: the start must be aligned. | |||
| 121 | + | c.xidMask = 0x001ffff0 | |||
| 122 | + | if c.validXIDRange(0x00200008, 1) || !c.validXIDRange(0x00200010, 2) || c.validXIDRange(0x001ffff0|0x00200000, 2) { | |||
| 123 | + | t.Fatal("inc 16") | |||
| 124 | + | } | |||
| 125 | + | } | |||
| 126 | + | ||||
| 127 | + | func TestNewXIDRejectsOverflowingRange(t *testing.T) { | |||
| 128 | + | sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200000, 0xffffffff}}} | |||
| 129 | + | c, _ := dialScript(t, sc) | |||
| 130 | + | c.xids = xidRange{} | |||
| 131 | + | if _, err := c.NewXID(); !errors.Is(err, errXIDsExhausted) { | |||
| 132 | + | t.Fatal(err) | |||
| 133 | + | } | |||
| 134 | + | } | |||
| 135 | + | ||||
| 136 | + | // XIDsLow + PrefetchXIDs let a callback allocate past the end of the current | |||
| 137 | + | // range without blocking. | |||
| 138 | + | func TestPrefetchXIDsFromCallback(t *testing.T) { | |||
| 139 | + | sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200100, 3}}} | |||
| 140 | + | sc.hook = func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool { | |||
| 141 | + | if major == 8 { | |||
| 142 | + | s.event(19, 0) | |||
| 143 | + | return true | |||
| 144 | + | } | |||
| 145 | + | return false | |||
| 146 | + | } | |||
| 147 | + | c, srv := dialScript(t, sc) | |||
| 148 | + | c.xids = xidRange{next: 0x10, left: 2} | |||
| 149 | + | if !c.XIDsLow() { | |||
| 150 | + | t.Fatal("not low") | |||
| 151 | + | } | |||
| 152 | + | var got []uint32 | |||
| 153 | + | var errs []error | |||
| 154 | + | h := Handler{OnEvent: func(*Event) { | |||
| 155 | + | // Inside a callback: the prefetch is queued, no wait. | |||
| 156 | + | if c.XIDsLow() { | |||
| 157 | + | if err := c.PrefetchXIDs(); err != nil { | |||
| 158 | + | errs = append(errs, err) | |||
| 159 | + | } | |||
| 160 | + | } | |||
| 161 | + | }} | |||
| 162 | + | c.Buf().Request(8, 0, 4) | |||
| 163 | + | if err := c.Sync(&h); err != nil { | |||
| 164 | + | t.Fatal(err) | |||
| 165 | + | } | |||
| 166 | + | if len(errs) != 0 || c.xidPend.left != 3 || c.XIDsLow() { | |||
| 167 | + | t.Fatal(errs, c.xidPend, c.XIDsLow()) | |||
| 168 | + | } | |||
| 169 | + | // Second event: allocate 5 ids inside the callback, crossing ranges. | |||
| 170 | + | h.OnEvent = func(*Event) { | |||
| 171 | + | for range 5 { | |||
| 172 | + | id, err := c.NewXID() | |||
| 173 | + | if err != nil { | |||
| 174 | + | errs = append(errs, err) | |||
| 175 | + | return | |||
| 176 | + | } | |||
| 177 | + | got = append(got, id) | |||
| 178 | + | } | |||
| 179 | + | // Now everything is used; NewXID cannot block here. | |||
| 180 | + | if _, err := c.NewXID(); !errors.Is(err, errReentrantDispatch) { | |||
| 181 | + | errs = append(errs, err) | |||
| 182 | + | } | |||
| 183 | + | } | |||
| 184 | + | c.Buf().Request(8, 0, 4) | |||
| 185 | + | if err := c.Sync(&h); err != nil { | |||
| 186 | + | t.Fatal(err) | |||
| 187 | + | } | |||
| 188 | + | want := []uint32{0x00200010, 0x00200011, 0x00200100, 0x00200101, 0x00200102} | |||
| 189 | + | if len(errs) != 0 || len(got) != 5 { | |||
| 190 | + | t.Fatal(errs, got) | |||
| 191 | + | } | |||
| 192 | + | for i := range want { | |||
| 193 | + | if got[i] != want[i] { | |||
| 194 | + | t.Fatalf("%#x", got) | |||
| 195 | + | } | |||
| 196 | + | } | |||
| 197 | + | var ranges int | |||
| 198 | + | for _, r := range srv.requests() { | |||
| 199 | + | if r.major == 136 && r.minor == minorGetXIDRange { | |||
| 200 | + | ranges++ | |||
| 201 | + | } | |||
| 202 | + | } | |||
| 203 | + | if ranges != 1 { | |||
| 204 | + | t.Fatal("GetXIDRange requests", ranges) | |||
| 205 | + | } | |||
| 206 | + | } | |||
| 207 | + | ||||
| 208 | + | func TestPrefetchXIDsStates(t *testing.T) { | |||
| 209 | + | sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0, 1}}} | |||
| 210 | + | c, srv := dialScript(t, sc) | |||
| 211 | + | if c.XIDsLow() { | |||
| 212 | + | t.Fatal("fresh setup range reported low") | |||
| 213 | + | } | |||
| 214 | + | c.xids = xidRange{next: 0x10, left: 1} | |||
| 215 | + | before := len(srv.requests()) | |||
| 216 | + | // Twice: one request only while in flight. | |||
| 217 | + | first, second := c.PrefetchXIDs(), c.PrefetchXIDs() | |||
| 218 | + | if first != nil || second != nil || c.XIDsLow() { | |||
| 219 | + | t.Fatal("prefetch", first, second) | |||
| 220 | + | } | |||
| 221 | + | if err := c.Sync(nil); err != nil { | |||
| 222 | + | t.Fatal(err) | |||
| 223 | + | } | |||
| 224 | + | if n := len(srv.requests()) - before; n != 2 { // GetXIDRange + fence | |||
| 225 | + | t.Fatal(n) | |||
| 226 | + | } | |||
| 227 | + | // The server had none: not low (nothing to gain), current id still works, | |||
| 228 | + | // then NewXID's blocking refill reports exhaustion. | |||
| 229 | + | if c.XIDsLow() { | |||
| 230 | + | t.Fatal("low after a refused prefetch") | |||
| 231 | + | } | |||
| 232 | + | if _, err := c.NewXID(); err != nil { | |||
| 233 | + | t.Fatal(err) | |||
| 234 | + | } | |||
| 235 | + | if _, err := c.NewXID(); !errors.Is(err, errXIDsExhausted) { | |||
| 236 | + | t.Fatal(err) | |||
| 237 | + | } | |||
| 238 | + | // Without XC-MISC. | |||
| 239 | + | c2, _ := dialScript(t, &script{}) | |||
| 240 | + | c2.xids = xidRange{} | |||
| 241 | + | if c2.XIDsLow() || !errors.Is(c2.PrefetchXIDs(), ErrExtMissing) { | |||
| 242 | + | t.Fatal("no XC-MISC") | |||
| 243 | + | } | |||
| 244 | + | } | |||
| 245 | + | ||||
| 246 | + | // A blocking refill waits for a prefetch already in flight instead of sending | |||
| 247 | + | // a second request; an X error on it is returned by NewXID, not OnError. | |||
| 248 | + | func TestRefillWaitsForPrefetch(t *testing.T) { | |||
| 249 | + | sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200200, 4}}} | |||
| 250 | + | c, srv := dialScript(t, sc) | |||
| 251 | + | c.xids = xidRange{} | |||
| 252 | + | if err := c.PrefetchXIDs(); err != nil { | |||
| 253 | + | t.Fatal(err) | |||
| 254 | + | } | |||
| 255 | + | id, err := c.NewXID() | |||
| 256 | + | if err != nil || id != 0x00200200 { | |||
| 257 | + | t.Fatalf("%#x %v", id, err) | |||
| 258 | + | } | |||
| 259 | + | var ranges int | |||
| 260 | + | for _, r := range srv.requests() { | |||
| 261 | + | if r.major == 136 && r.minor == minorGetXIDRange { | |||
| 262 | + | ranges++ | |||
| 263 | + | } | |||
| 264 | + | } | |||
| 265 | + | if ranges != 1 { | |||
| 266 | + | t.Fatal(ranges) | |||
| 267 | + | } | |||
| 268 | + | ||||
| 269 | + | esc := &script{exts: fullExts()} | |||
| 270 | + | esc.hook = func(s *scriptConn, major, minor uint8, _ uint16, _ []byte) bool { | |||
| 271 | + | if major == 136 && minor == minorGetXIDRange { | |||
| 272 | + | s.xerror(11, major, 0) | |||
| 273 | + | return true | |||
| 274 | + | } | |||
| 275 | + | return false | |||
| 276 | + | } | |||
| 277 | + | ec, _ := dialScript(t, esc) | |||
| 278 | + | var userErrs int | |||
| 279 | + | h := Handler{OnError: func(*XError) { userErrs++ }} | |||
| 280 | + | ec.xids = xidRange{} | |||
| 281 | + | if err := ec.PrefetchXIDs(); err != nil { | |||
| 282 | + | t.Fatal(err) | |||
| 283 | + | } | |||
| 284 | + | if err := ec.Sync(&h); err != nil { | |||
| 285 | + | t.Fatal("a prefetch error leaked into Sync:", err) | |||
| 286 | + | } | |||
| 287 | + | var xe *XError | |||
| 288 | + | if _, err := ec.NewXID(); !errors.As(err, &xe) || xe.Kind != kindXIDRange || userErrs != 0 { | |||
| 289 | + | t.Fatal(err, userErrs) | |||
| 290 | + | } | |||
| 291 | + | } | |||
| 292 | + | ||||
| 293 | + | // Steady state: XIDsLow does not allocate. | |||
| 294 | + | func TestAllocXIDsLow(t *testing.T) { | |||
| 295 | + | c, _ := dialScript(t, &script{exts: fullExts()}) | |||
| 296 | + | if n := testing.AllocsPerRun(1000, func() { _ = c.XIDsLow() }); n != 0 { | |||
| 297 | + | t.Fatal(n) | |||
| 298 | + | } | |||
| 299 | + | } | |||
| 300 | + | ||||
| 301 | + | // An over-long name is a caller bug: ErrBadLength, nothing queued, the Buf | |||
| 302 | + | // still usable. | |||
| 303 | + | func TestInternAtomsNameTooLong(t *testing.T) { | |||
| 304 | + | c, _ := dialScript(t, &script{}) | |||
| 305 | + | next := c.Buf().Next() | |||
| 306 | + | err := c.InternAtoms([]string{"OK", strings.Repeat("x", 0x10000)}, false, make([]uint32, 2)) | |||
| 307 | + | if !errors.Is(err, ErrBadLength) || c.Buf().Err() != nil || c.Buf().Next() != next { | |||
| 308 | + | t.Fatal(err, c.Buf().Err()) | |||
| 309 | + | } | |||
| 310 | + | dst := make([]uint32, 1) | |||
| 311 | + | if err := c.InternAtoms([]string{"OK"}, false, dst); err != nil || dst[0] == 0 { | |||
| 312 | + | t.Fatal(err) | |||
| 313 | + | } | |||
| 314 | + | } | |||
| 315 | + | ||||
| 316 | + | // 0xffff requests queued without a Flush reach ErrSeqOverflow (the auto-resync | |||
| 317 | + | // fence needs a Flush to be appended); with a Flush every so often the fence | |||
| 318 | + | // keeps the stream going far past 0xffff. | |||
| 319 | + | func TestAutoResyncVsSeqOverflow(t *testing.T) { | |||
| 320 | + | c, _ := dialScript(t, &script{}) | |||
| 321 | + | b := c.Buf() | |||
| 322 | + | for b.Err() == nil { | |||
| 323 | + | b.Request(8, 0, 4) | |||
| 324 | + | } | |||
| 325 | + | if !errors.Is(b.Err(), ErrSeqOverflow) || b.Next()-b.acked != maxSeqAhead { | |||
| 326 | + | t.Fatal(b.Err(), b.Next()-b.acked) | |||
| 327 | + | } | |||
| 328 | + | if c.Stats().Resyncs != 0 { | |||
| 329 | + | t.Fatal(c.Stats()) | |||
| 330 | + | } | |||
| 331 | + | ||||
| 332 | + | c2, _ := dialScript(t, &script{}) | |||
| 333 | + | b2 := c2.Buf() | |||
| 334 | + | for i := range 3 * 0x10000 { | |||
| 335 | + | b2.Request(8, 0, 4) | |||
| 336 | + | if i%4096 == 0 { | |||
| 337 | + | if err := flushAll(c2); err != nil { | |||
| 338 | + | t.Fatal(err) | |||
| 339 | + | } | |||
| 340 | + | if err := c2.Dispatch(nil); err != nil { | |||
| 341 | + | t.Fatal(err) | |||
| 342 | + | } | |||
| 343 | + | // Give the fence's reply time to come back before overflow. | |||
| 344 | + | if b2.Next()-b2.acked > 0xe000 { | |||
| 345 | + | dispatchFor(t, c2, func() bool { return b2.Next()-b2.acked < 0x9000 }) | |||
| 346 | + | } | |||
| 347 | + | } | |||
| 348 | + | } | |||
| 349 | + | if b2.Err() != nil { | |||
| 350 | + | t.Fatal(b2.Err()) | |||
| 351 | + | } | |||
| 352 | + | if err := c2.Sync(nil); err != nil { | |||
| 353 | + | t.Fatal(err) | |||
| 354 | + | } | |||
| 355 | + | if st := c2.Stats(); st.Resyncs < 3 || st.DroppedReplies != 0 { | |||
| 356 | + | t.Fatalf("%+v", st) | |||
| 357 | + | } | |||
| 358 | + | } | |||
M ring.go +55 -12
| @@ -1,6 +1,9 @@ | |||
| 1 | 1 | package xturbo | |
| 2 | 2 | ||
| 3 | - | import "errors" | |
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "errors" | |
| 6 | + | ) | |
| 4 | 7 | ||
| 5 | 8 | // ErrRingFull reports more than ringSize registered requests outstanding: | |
| 6 | 9 | // the Expect slot of seq still belongs to an earlier request that has not | |
| @@ -119,15 +122,55 @@ func (c *Conn) take(seq Seq) (kind uint16, args [4]uint32, ok bool) { | |||
| 119 | 122 | return kind, args, true | |
| 120 | 123 | } | |
| 121 | 124 | ||
| 122 | - | // consumeError handles an error whose request was registered with a reserved | |
| 123 | - | // kind (1..15). It reports whether it consumed the error; an unconsumed error | |
| 124 | - | // is delivered to OnError. The bootstrap consumers arrive with the connection | |
| 125 | - | // bootstrap; until then every reserved-kind error is consumed and dropped, | |
| 126 | - | // like consumeReply drops its replies. | |
| 127 | - | func (c *Conn) consumeError(*XError) bool { return true } | |
| 128 | - | ||
| 129 | 125 | // consumeReply handles a reply registered with a reserved kind. kindDiscard | |
| 130 | - | // replies are dropped; the bootstrap kinds (kindInternAtom, | |
| 131 | - | // kindQueryExtension, kindXIDRange, kindBigreqEnable) get their consumers | |
| 132 | - | // with the connection bootstrap and are dropped until then. | |
| 133 | - | func (c *Conn) consumeReply(*Reply) {} | |
| 126 | + | // and malformed replies are dropped. | |
| 127 | + | func (c *Conn) consumeReply(r *Reply) { | |
| 128 | + | if len(r.Raw) < packetHeadSize { | |
| 129 | + | return | |
| 130 | + | } | |
| 131 | + | switch r.Kind { | |
| 132 | + | case kindInternAtom: | |
| 133 | + | if i := int(r.Args[0]); i < len(c.atoms) { | |
| 134 | + | c.atoms[i] = binary.LittleEndian.Uint32(r.Raw[8:]) | |
| 135 | + | } | |
| 136 | + | case kindQueryExtension: | |
| 137 | + | id := ExtID(r.Args[0]) | |
| 138 | + | if r.Raw[8] == 0 || id == ExtCore || id >= extCount { | |
| 139 | + | return | |
| 140 | + | } | |
| 141 | + | c.ext.Set(id, r.Raw[9], r.Raw[10], r.Raw[11]) | |
| 142 | + | if id == ExtBigRequests && r.Raw[9] != 0 { | |
| 143 | + | // Enable needs the major opcode: queue it from here so one | |
| 144 | + | // Sync covers the whole bootstrap. | |
| 145 | + | if _, seq := c.buf.Request(r.Raw[9], 0, 0); seq != 0 { | |
| 146 | + | _ = c.expect(seq, kindBigreqEnable, [4]uint32{}) | |
| 147 | + | } | |
| 148 | + | } | |
| 149 | + | case kindBigreqEnable: | |
| 150 | + | // A limit below the core one would be a server bug; keep the core one. | |
| 151 | + | if words := binary.LittleEndian.Uint32(r.Raw[8:]); words > coreMaxReq/4 { | |
| 152 | + | c.buf.setMaxReqWords(words) | |
| 153 | + | } | |
| 154 | + | case kindXIDRange: | |
| 155 | + | c.gotXIDRange(binary.LittleEndian.Uint32(r.Raw[8:]), binary.LittleEndian.Uint32(r.Raw[12:])) | |
| 156 | + | } | |
| 157 | + | } | |
| 158 | + | ||
| 159 | + | // consumeError handles an error whose request was registered with a reserved | |
| 160 | + | // kind (1..15) and reports whether it consumed it; an unconsumed error is | |
| 161 | + | // delivered to OnError. Every reserved kind is root's own request, so all are | |
| 162 | + | // consumed: the first is kept for the blocking call (Sync, InternAtoms) that | |
| 163 | + | // issued the request, which returns it as *XError; a GetXIDRange error (maybe | |
| 164 | + | // from PrefetchXIDs) is kept for NewXID. | |
| 165 | + | func (c *Conn) consumeError(e *XError) bool { | |
| 166 | + | if e.Kind == kindXIDRange { | |
| 167 | + | // Possibly a prefetch: kept apart from the error of a Sync. | |
| 168 | + | c.xidNone, c.xidFailed, c.xidErr = true, true, *e | |
| 169 | + | return true | |
| 170 | + | } | |
| 171 | + | if !c.syncFailed { | |
| 172 | + | c.syncFailed = true | |
| 173 | + | c.syncErr = *e | |
| 174 | + | } | |
| 175 | + | return true | |
| 176 | + | } | |
| @@ -1,6 +1,9 @@ | |||||
| 1 | package xturbo | 1 | package xturbo | ||
| 2 | 2 | ||||
| 3 | - | import "errors" | 3 | + | import ( |
| 4 | + | "encoding/binary" | |||
| 5 | + | "errors" | |||
| 6 | + | ) | |||
| 4 | 7 | ||||
| 5 | // ErrRingFull reports more than ringSize registered requests outstanding: | 8 | // ErrRingFull reports more than ringSize registered requests outstanding: | ||
| 6 | // the Expect slot of seq still belongs to an earlier request that has not | 9 | // the Expect slot of seq still belongs to an earlier request that has not | ||
| @@ -119,15 +122,55 @@ func (c *Conn) take(seq Seq) (kind uint16, args [4]uint32, ok bool) { | |||||
| 119 | return kind, args, true | 122 | return kind, args, true | ||
| 120 | } | 123 | } | ||
| 121 | 124 | ||||
| 122 | - | // consumeError handles an error whose request was registered with a reserved | |||
| 123 | - | // kind (1..15). It reports whether it consumed the error; an unconsumed error | |||
| 124 | - | // is delivered to OnError. The bootstrap consumers arrive with the connection | |||
| 125 | - | // bootstrap; until then every reserved-kind error is consumed and dropped, | |||
| 126 | - | // like consumeReply drops its replies. | |||
| 127 | - | func (c *Conn) consumeError(*XError) bool { return true } | |||
| 128 | - | ||||
| 129 | // consumeReply handles a reply registered with a reserved kind. kindDiscard | 125 | // consumeReply handles a reply registered with a reserved kind. kindDiscard | ||
| 130 | - | // replies are dropped; the bootstrap kinds (kindInternAtom, | 126 | + | // and malformed replies are dropped. |
| 131 | - | // kindQueryExtension, kindXIDRange, kindBigreqEnable) get their consumers | 127 | + | func (c *Conn) consumeReply(r *Reply) { |
| 132 | - | // with the connection bootstrap and are dropped until then. | 128 | + | if len(r.Raw) < packetHeadSize { |
| 133 | - | func (c *Conn) consumeReply(*Reply) {} | 129 | + | return |
| 130 | + | } | |||
| 131 | + | switch r.Kind { | |||
| 132 | + | case kindInternAtom: | |||
| 133 | + | if i := int(r.Args[0]); i < len(c.atoms) { | |||
| 134 | + | c.atoms[i] = binary.LittleEndian.Uint32(r.Raw[8:]) | |||
| 135 | + | } | |||
| 136 | + | case kindQueryExtension: | |||
| 137 | + | id := ExtID(r.Args[0]) | |||
| 138 | + | if r.Raw[8] == 0 || id == ExtCore || id >= extCount { | |||
| 139 | + | return | |||
| 140 | + | } | |||
| 141 | + | c.ext.Set(id, r.Raw[9], r.Raw[10], r.Raw[11]) | |||
| 142 | + | if id == ExtBigRequests && r.Raw[9] != 0 { | |||
| 143 | + | // Enable needs the major opcode: queue it from here so one | |||
| 144 | + | // Sync covers the whole bootstrap. | |||
| 145 | + | if _, seq := c.buf.Request(r.Raw[9], 0, 0); seq != 0 { | |||
| 146 | + | _ = c.expect(seq, kindBigreqEnable, [4]uint32{}) | |||
| 147 | + | } | |||
| 148 | + | } | |||
| 149 | + | case kindBigreqEnable: | |||
| 150 | + | // A limit below the core one would be a server bug; keep the core one. | |||
| 151 | + | if words := binary.LittleEndian.Uint32(r.Raw[8:]); words > coreMaxReq/4 { | |||
| 152 | + | c.buf.setMaxReqWords(words) | |||
| 153 | + | } | |||
| 154 | + | case kindXIDRange: | |||
| 155 | + | c.gotXIDRange(binary.LittleEndian.Uint32(r.Raw[8:]), binary.LittleEndian.Uint32(r.Raw[12:])) | |||
| 156 | + | } | |||
| 157 | + | } | |||
| 158 | + | ||||
| 159 | + | // consumeError handles an error whose request was registered with a reserved | |||
| 160 | + | // kind (1..15) and reports whether it consumed it; an unconsumed error is | |||
| 161 | + | // delivered to OnError. Every reserved kind is root's own request, so all are | |||
| 162 | + | // consumed: the first is kept for the blocking call (Sync, InternAtoms) that | |||
| 163 | + | // issued the request, which returns it as *XError; a GetXIDRange error (maybe | |||
| 164 | + | // from PrefetchXIDs) is kept for NewXID. | |||
| 165 | + | func (c *Conn) consumeError(e *XError) bool { | |||
| 166 | + | if e.Kind == kindXIDRange { | |||
| 167 | + | // Possibly a prefetch: kept apart from the error of a Sync. | |||
| 168 | + | c.xidNone, c.xidFailed, c.xidErr = true, true, *e | |||
| 169 | + | return true | |||
| 170 | + | } | |||
| 171 | + | if !c.syncFailed { | |||
| 172 | + | c.syncFailed = true | |||
| 173 | + | c.syncErr = *e | |||
| 174 | + | } | |||
| 175 | + | return true | |||
| 176 | + | } | |||
M setup.go +30 -8
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "fmt" | |
| 6 | 6 | "io" | |
| 7 | 7 | "os" | |
| 8 | + | "time" | |
| 8 | 9 | ||
| 9 | 10 | "golang.org/x/sys/unix" | |
| 10 | 11 | ) | |
| @@ -46,22 +47,24 @@ func encodeSetupRequest(authName string, authData []byte) []byte { | |||
| 46 | 47 | ||
| 47 | 48 | func pad4(n int) int { return (n + 3) &^ 3 } | |
| 48 | 49 | ||
| 49 | - | // handshake runs the blocking setup exchange on fd and returns the full setup | |
| 50 | - | // reply. A refusal is a *SetupError; a broken reply wraps ErrMalformed. | |
| 51 | - | func handshake(fd int, authName string, authData []byte) ([]byte, setupInfo, error) { | |
| 50 | + | // handshake runs the setup exchange on the non-blocking fd, waiting with poll | |
| 51 | + | // until deadline, and returns the full setup reply. A refusal is a | |
| 52 | + | // *SetupError; a broken reply wraps ErrMalformed; a silent server an error | |
| 53 | + | // wrapping os.ErrDeadlineExceeded. | |
| 54 | + | func handshake(fd int, authName string, authData []byte, deadline time.Time) ([]byte, setupInfo, error) { | |
| 52 | 55 | if len(authName) > 0xffff || len(authData) > 0xffff { | |
| 53 | 56 | return nil, setupInfo{}, ErrBadLength | |
| 54 | 57 | } | |
| 55 | - | if err := writeFull(fd, encodeSetupRequest(authName, authData)); err != nil { | |
| 58 | + | if err := writeFull(fd, encodeSetupRequest(authName, authData), deadline); err != nil { | |
| 56 | 59 | return nil, setupInfo{}, err | |
| 57 | 60 | } | |
| 58 | 61 | var hdr [setupHeaderSize]byte | |
| 59 | - | if err := readFull(fd, hdr[:]); err != nil { | |
| 62 | + | if err := readFull(fd, hdr[:], deadline); err != nil { | |
| 60 | 63 | return nil, setupInfo{}, err | |
| 61 | 64 | } | |
| 62 | 65 | reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:]))) | |
| 63 | 66 | copy(reply, hdr[:]) | |
| 64 | - | if err := readFull(fd, reply[setupHeaderSize:]); err != nil { | |
| 67 | + | if err := readFull(fd, reply[setupHeaderSize:], deadline); err != nil { | |
| 65 | 68 | return nil, setupInfo{}, err | |
| 66 | 69 | } | |
| 67 | 70 | info, err := parseSetup(reply) | |
| @@ -141,12 +144,26 @@ func parseSetup(p []byte) (setupInfo, error) { | |||
| 141 | 144 | return info, nil | |
| 142 | 145 | } | |
| 143 | 146 | ||
| 144 | - | func writeFull(fd int, p []byte) error { | |
| 147 | + | // waitFd polls fd for events until deadline. | |
| 148 | + | func waitFd(fd int, events int16, deadline time.Time) error { | |
| 149 | + | left := time.Until(deadline) | |
| 150 | + | if left <= 0 { | |
| 151 | + | return deadlineError("connection setup") | |
| 152 | + | } | |
| 153 | + | return pollFd(fd, events, left) | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | func writeFull(fd int, p []byte, deadline time.Time) error { | |
| 145 | 157 | for len(p) > 0 { | |
| 146 | 158 | n, err := unix.Write(fd, p) | |
| 147 | 159 | switch { | |
| 148 | 160 | case err == unix.EINTR: | |
| 149 | 161 | continue | |
| 162 | + | case err == unix.EAGAIN: | |
| 163 | + | if err := waitFd(fd, unix.POLLOUT, deadline); err != nil { | |
| 164 | + | return err | |
| 165 | + | } | |
| 166 | + | continue | |
| 150 | 167 | case err == unix.EPIPE || err == unix.ECONNRESET: | |
| 151 | 168 | return ErrClosed | |
| 152 | 169 | case err != nil: | |
| @@ -159,12 +176,17 @@ func writeFull(fd int, p []byte) error { | |||
| 159 | 176 | return nil | |
| 160 | 177 | } | |
| 161 | 178 | ||
| 162 | - | func readFull(fd int, p []byte) error { | |
| 179 | + | func readFull(fd int, p []byte, deadline time.Time) error { | |
| 163 | 180 | for len(p) > 0 { | |
| 164 | 181 | n, err := unix.Read(fd, p) | |
| 165 | 182 | switch { | |
| 166 | 183 | case err == unix.EINTR: | |
| 167 | 184 | continue | |
| 185 | + | case err == unix.EAGAIN: | |
| 186 | + | if err := waitFd(fd, unix.POLLIN, deadline); err != nil { | |
| 187 | + | return err | |
| 188 | + | } | |
| 189 | + | continue | |
| 168 | 190 | case err == unix.ECONNRESET: | |
| 169 | 191 | return ErrClosed | |
| 170 | 192 | case err != nil: | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "io" | 6 | "io" | ||
| 7 | "os" | 7 | "os" | ||
| 8 | + | "time" | |||
| 8 | 9 | ||||
| 9 | "golang.org/x/sys/unix" | 10 | "golang.org/x/sys/unix" | ||
| 10 | ) | 11 | ) | ||
| @@ -46,22 +47,24 @@ func encodeSetupRequest(authName string, authData []byte) []byte { | |||||
| 46 | 47 | ||||
| 47 | func pad4(n int) int { return (n + 3) &^ 3 } | 48 | func pad4(n int) int { return (n + 3) &^ 3 } | ||
| 48 | 49 | ||||
| 49 | - | // handshake runs the blocking setup exchange on fd and returns the full setup | 50 | + | // handshake runs the setup exchange on the non-blocking fd, waiting with poll |
| 50 | - | // reply. A refusal is a *SetupError; a broken reply wraps ErrMalformed. | 51 | + | // until deadline, and returns the full setup reply. A refusal is a |
| 51 | - | func handshake(fd int, authName string, authData []byte) ([]byte, setupInfo, error) { | 52 | + | // *SetupError; a broken reply wraps ErrMalformed; a silent server an error |
| 53 | + | // wrapping os.ErrDeadlineExceeded. | |||
| 54 | + | func handshake(fd int, authName string, authData []byte, deadline time.Time) ([]byte, setupInfo, error) { | |||
| 52 | if len(authName) > 0xffff || len(authData) > 0xffff { | 55 | if len(authName) > 0xffff || len(authData) > 0xffff { | ||
| 53 | return nil, setupInfo{}, ErrBadLength | 56 | return nil, setupInfo{}, ErrBadLength | ||
| 54 | } | 57 | } | ||
| 55 | - | if err := writeFull(fd, encodeSetupRequest(authName, authData)); err != nil { | 58 | + | if err := writeFull(fd, encodeSetupRequest(authName, authData), deadline); err != nil { |
| 56 | return nil, setupInfo{}, err | 59 | return nil, setupInfo{}, err | ||
| 57 | } | 60 | } | ||
| 58 | var hdr [setupHeaderSize]byte | 61 | var hdr [setupHeaderSize]byte | ||
| 59 | - | if err := readFull(fd, hdr[:]); err != nil { | 62 | + | if err := readFull(fd, hdr[:], deadline); err != nil { |
| 60 | return nil, setupInfo{}, err | 63 | return nil, setupInfo{}, err | ||
| 61 | } | 64 | } | ||
| 62 | reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:]))) | 65 | reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:]))) | ||
| 63 | copy(reply, hdr[:]) | 66 | copy(reply, hdr[:]) | ||
| 64 | - | if err := readFull(fd, reply[setupHeaderSize:]); err != nil { | 67 | + | if err := readFull(fd, reply[setupHeaderSize:], deadline); err != nil { |
| 65 | return nil, setupInfo{}, err | 68 | return nil, setupInfo{}, err | ||
| 66 | } | 69 | } | ||
| 67 | info, err := parseSetup(reply) | 70 | info, err := parseSetup(reply) | ||
| @@ -141,12 +144,26 @@ func parseSetup(p []byte) (setupInfo, error) { | |||||
| 141 | return info, nil | 144 | return info, nil | ||
| 142 | } | 145 | } | ||
| 143 | 146 | ||||
| 144 | - | func writeFull(fd int, p []byte) error { | 147 | + | // waitFd polls fd for events until deadline. |
| 148 | + | func waitFd(fd int, events int16, deadline time.Time) error { | |||
| 149 | + | left := time.Until(deadline) | |||
| 150 | + | if left <= 0 { | |||
| 151 | + | return deadlineError("connection setup") | |||
| 152 | + | } | |||
| 153 | + | return pollFd(fd, events, left) | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | func writeFull(fd int, p []byte, deadline time.Time) error { | |||
| 145 | for len(p) > 0 { | 157 | for len(p) > 0 { | ||
| 146 | n, err := unix.Write(fd, p) | 158 | n, err := unix.Write(fd, p) | ||
| 147 | switch { | 159 | switch { | ||
| 148 | case err == unix.EINTR: | 160 | case err == unix.EINTR: | ||
| 149 | continue | 161 | continue | ||
| 162 | + | case err == unix.EAGAIN: | |||
| 163 | + | if err := waitFd(fd, unix.POLLOUT, deadline); err != nil { | |||
| 164 | + | return err | |||
| 165 | + | } | |||
| 166 | + | continue | |||
| 150 | case err == unix.EPIPE || err == unix.ECONNRESET: | 167 | case err == unix.EPIPE || err == unix.ECONNRESET: | ||
| 151 | return ErrClosed | 168 | return ErrClosed | ||
| 152 | case err != nil: | 169 | case err != nil: | ||
| @@ -159,12 +176,17 @@ func writeFull(fd int, p []byte) error { | |||||
| 159 | return nil | 176 | return nil | ||
| 160 | } | 177 | } | ||
| 161 | 178 | ||||
| 162 | - | func readFull(fd int, p []byte) error { | 179 | + | func readFull(fd int, p []byte, deadline time.Time) error { |
| 163 | for len(p) > 0 { | 180 | for len(p) > 0 { | ||
| 164 | n, err := unix.Read(fd, p) | 181 | n, err := unix.Read(fd, p) | ||
| 165 | switch { | 182 | switch { | ||
| 166 | case err == unix.EINTR: | 183 | case err == unix.EINTR: | ||
| 167 | continue | 184 | continue | ||
| 185 | + | case err == unix.EAGAIN: | |||
| 186 | + | if err := waitFd(fd, unix.POLLIN, deadline); err != nil { | |||
| 187 | + | return err | |||
| 188 | + | } | |||
| 189 | + | continue | |||
| 168 | case err == unix.ECONNRESET: | 190 | case err == unix.ECONNRESET: | ||
| 169 | return ErrClosed | 191 | return ErrClosed | ||
| 170 | case err != nil: | 192 | case err != nil: | ||
A sync.go +195 -0
| @@ -0,0 +1,195 @@ | |||
| 1 | + | package xturbo | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "fmt" | |
| 6 | + | "os" | |
| 7 | + | "time" | |
| 8 | + | ||
| 9 | + | "golang.org/x/sys/unix" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // Core and extension requests root issues itself. Root does not import the | |
| 13 | + | // generated packages, so these few are encoded here; tests compare them with | |
| 14 | + | // the generated encoders and the request goldens. | |
| 15 | + | const ( | |
| 16 | + | opInternAtom = 16 | |
| 17 | + | opGetInputFocus = 43 | |
| 18 | + | opQueryExtension = 98 | |
| 19 | + | minorGetXIDRange = 1 // XC-MISC | |
| 20 | + | ) | |
| 21 | + | ||
| 22 | + | func encodeGetInputFocus(b *Buf) Seq { | |
| 23 | + | _, seq := b.Request(opGetInputFocus, 0, 0) | |
| 24 | + | return seq | |
| 25 | + | } | |
| 26 | + | ||
| 27 | + | // encodeNamed encodes QueryExtension and InternAtom: CARD16 name_len, 2 | |
| 28 | + | // pad bytes, then the name padded to 4. | |
| 29 | + | func encodeNamed(b *Buf, major, data uint8, name string) Seq { | |
| 30 | + | if len(name) > 0xffff { | |
| 31 | + | b.Fail(ErrBadLength) | |
| 32 | + | return 0 | |
| 33 | + | } | |
| 34 | + | p, seq := b.Request(major, data, pad4(4+len(name))) | |
| 35 | + | if p == nil { | |
| 36 | + | return 0 | |
| 37 | + | } | |
| 38 | + | binary.LittleEndian.PutUint16(p, uint16(len(name))) | |
| 39 | + | copy(p[4:], name) | |
| 40 | + | return seq | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | func encodeQueryExtension(b *Buf, name string) Seq { | |
| 44 | + | return encodeNamed(b, opQueryExtension, 0, name) | |
| 45 | + | } | |
| 46 | + | ||
| 47 | + | func encodeInternAtom(b *Buf, onlyIfExists bool, name string) Seq { | |
| 48 | + | var data uint8 | |
| 49 | + | if onlyIfExists { | |
| 50 | + | data = 1 | |
| 51 | + | } | |
| 52 | + | return encodeNamed(b, opInternAtom, data, name) | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // encodeExtRequest encodes a body-less extension request (BIG-REQUESTS | |
| 56 | + | // Enable, XC-MISC GetXIDRange). It fails with ErrExtMissing when id is absent. | |
| 57 | + | func encodeExtRequest(b *Buf, id ExtID, minor uint8) Seq { | |
| 58 | + | major := b.Ext.Major(id) | |
| 59 | + | if major == 0 { | |
| 60 | + | b.Fail(ErrExtMissing) | |
| 61 | + | return 0 | |
| 62 | + | } | |
| 63 | + | _, seq := b.Request(major, minor, 0) | |
| 64 | + | return seq | |
| 65 | + | } | |
| 66 | + | ||
| 67 | + | // Bounds of the blocking calls. Vars so tests can shorten them. | |
| 68 | + | var ( | |
| 69 | + | // connectTimeout bounds NewConn: setup handshake plus extension bootstrap. | |
| 70 | + | connectTimeout = 10 * time.Second | |
| 71 | + | // syncTimeout bounds one Sync, InternAtoms or blocking XID refill. | |
| 72 | + | syncTimeout = 5 * time.Second | |
| 73 | + | ) | |
| 74 | + | ||
| 75 | + | // deadlineError wraps os.ErrDeadlineExceeded for a blocking call that timed | |
| 76 | + | // out waiting for the server. | |
| 77 | + | func deadlineError(what string) error { | |
| 78 | + | return fmt.Errorf("xturbo: %s: no answer from the X server: %w", what, os.ErrDeadlineExceeded) | |
| 79 | + | } | |
| 80 | + | ||
| 81 | + | // bootstrap negotiates every extension root knows (one pipelined | |
| 82 | + | // QueryExtension each, then BIG-REQUESTS Enable when present) in one Sync, | |
| 83 | + | // before deadline. | |
| 84 | + | func (c *Conn) bootstrap(deadline time.Time) error { | |
| 85 | + | for id := ExtCore + 1; id < extCount; id++ { | |
| 86 | + | seq := encodeQueryExtension(c.buf, ExtXName[id]) | |
| 87 | + | if seq == 0 { | |
| 88 | + | return c.buf.Err() | |
| 89 | + | } | |
| 90 | + | if err := c.expect(seq, kindQueryExtension, [4]uint32{uint32(id)}); err != nil { | |
| 91 | + | return err | |
| 92 | + | } | |
| 93 | + | } | |
| 94 | + | // consumeReply queues bigreq Enable when BIG-REQUESTS is present; the | |
| 95 | + | // wait covers it too. | |
| 96 | + | return c.sync(nil, deadline) | |
| 97 | + | } | |
| 98 | + | ||
| 99 | + | // Sync flushes every queued request and blocks until the server has | |
| 100 | + | // processed them and every request registered with Expect has completed. It | |
| 101 | + | // appends a GetInputFocus as a fence, so even with nothing registered it is a | |
| 102 | + | // full round trip: errors of earlier requests have been delivered when it | |
| 103 | + | // returns. Packets read meanwhile go to h as with Dispatch (nil h: the last | |
| 104 | + | // Handler given to Dispatch, or none). An error on a request root issued | |
| 105 | + | // itself is returned as *XError. Sync gives up after 5 s with an error | |
| 106 | + | // wrapping os.ErrDeadlineExceeded (a hung server); the connection is then | |
| 107 | + | // still usable but its state unknown, treat it as dead. Meant for startup; | |
| 108 | + | // called from a Handler callback it returns an error at once. | |
| 109 | + | func (c *Conn) Sync(h *Handler) error { | |
| 110 | + | return c.sync(h, time.Now().Add(c.timeout)) | |
| 111 | + | } | |
| 112 | + | ||
| 113 | + | func (c *Conn) sync(h *Handler, deadline time.Time) error { | |
| 114 | + | if c.dispatching { | |
| 115 | + | return errReentrantDispatch | |
| 116 | + | } | |
| 117 | + | if h == nil { | |
| 118 | + | h = c.h | |
| 119 | + | } | |
| 120 | + | c.syncFailed = false | |
| 121 | + | if err := c.waitUntil(h, deadline); err != nil { | |
| 122 | + | return err | |
| 123 | + | } | |
| 124 | + | if c.syncFailed { | |
| 125 | + | e := c.syncErr | |
| 126 | + | return &e | |
| 127 | + | } | |
| 128 | + | return nil | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // waitUntil fences, flushes and dispatches, one bounded Dispatch at a time, | |
| 132 | + | // until the fence has been answered and no registered slot remains, or | |
| 133 | + | // deadline passes. A fence is re-queued whenever callbacks registered | |
| 134 | + | // requests after the last one. | |
| 135 | + | func (c *Conn) waitUntil(h *Handler, deadline time.Time) error { | |
| 136 | + | if c.fd < 0 { | |
| 137 | + | return ErrClosed | |
| 138 | + | } | |
| 139 | + | var fence Seq | |
| 140 | + | for { | |
| 141 | + | if fence != 0 && c.seq >= fence && c.live == 0 { | |
| 142 | + | return nil | |
| 143 | + | } | |
| 144 | + | if fence == 0 || fence < c.buf.next-1 { | |
| 145 | + | // Requests queued after the fence (or none yet): fence them. | |
| 146 | + | if fence = c.fence(); fence == 0 { | |
| 147 | + | return c.buf.Err() | |
| 148 | + | } | |
| 149 | + | } | |
| 150 | + | if err := c.pollOnce(h, deadline); err != nil { | |
| 151 | + | return err | |
| 152 | + | } | |
| 153 | + | } | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | // pollOnce makes one step of progress: flush, dispatch what is readable and, | |
| 157 | + | // only when Dispatch read nothing at all, block until the socket is readable | |
| 158 | + | // (or writable while requests remain queued) or deadline passes. Dispatch is | |
| 159 | + | // bounded per call, so it may return with input left; the caller loops and | |
| 160 | + | // re-checks. A deadline already past fails even while packets keep coming | |
| 161 | + | // (a server flooding events but never answering). | |
| 162 | + | func (c *Conn) pollOnce(h *Handler, deadline time.Time) error { | |
| 163 | + | left := time.Until(deadline) | |
| 164 | + | if left <= 0 { | |
| 165 | + | return deadlineError("blocking round trip") | |
| 166 | + | } | |
| 167 | + | events := int16(unix.POLLIN) | |
| 168 | + | switch err := c.Flush(); err { | |
| 169 | + | case nil: | |
| 170 | + | case ErrWouldBlock: | |
| 171 | + | events |= unix.POLLOUT | |
| 172 | + | default: | |
| 173 | + | return err | |
| 174 | + | } | |
| 175 | + | if err := c.Dispatch(h); err != nil { | |
| 176 | + | return err | |
| 177 | + | } | |
| 178 | + | // Packets handled may have queued requests (callbacks, the bootstrap): | |
| 179 | + | // loop to flush them, and to fence them, before blocking. | |
| 180 | + | if !c.rdIdle || (events&unix.POLLOUT == 0 && c.buf.Len() > 0) { | |
| 181 | + | return nil | |
| 182 | + | } | |
| 183 | + | return pollFd(c.fd, events, left) | |
| 184 | + | } | |
| 185 | + | ||
| 186 | + | // pollFd blocks until fd has one of events or timeout passes (returning nil; | |
| 187 | + | // callers re-check their deadline). EINTR is a wake-up. | |
| 188 | + | func pollFd(fd int, events int16, timeout time.Duration) error { | |
| 189 | + | ms := int((timeout + time.Millisecond - 1) / time.Millisecond) | |
| 190 | + | fds := []unix.PollFd{{Fd: int32(fd), Events: events}} | |
| 191 | + | if _, err := unix.Poll(fds, ms); err != nil && err != unix.EINTR { | |
| 192 | + | return os.NewSyscallError("poll", err) | |
| 193 | + | } | |
| 194 | + | return nil | |
| 195 | + | } | |
| @@ -0,0 +1,195 @@ | |||||
| 1 | + | package xturbo | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "fmt" | |||
| 6 | + | "os" | |||
| 7 | + | "time" | |||
| 8 | + | ||||
| 9 | + | "golang.org/x/sys/unix" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // Core and extension requests root issues itself. Root does not import the | |||
| 13 | + | // generated packages, so these few are encoded here; tests compare them with | |||
| 14 | + | // the generated encoders and the request goldens. | |||
| 15 | + | const ( | |||
| 16 | + | opInternAtom = 16 | |||
| 17 | + | opGetInputFocus = 43 | |||
| 18 | + | opQueryExtension = 98 | |||
| 19 | + | minorGetXIDRange = 1 // XC-MISC | |||
| 20 | + | ) | |||
| 21 | + | ||||
| 22 | + | func encodeGetInputFocus(b *Buf) Seq { | |||
| 23 | + | _, seq := b.Request(opGetInputFocus, 0, 0) | |||
| 24 | + | return seq | |||
| 25 | + | } | |||
| 26 | + | ||||
| 27 | + | // encodeNamed encodes QueryExtension and InternAtom: CARD16 name_len, 2 | |||
| 28 | + | // pad bytes, then the name padded to 4. | |||
| 29 | + | func encodeNamed(b *Buf, major, data uint8, name string) Seq { | |||
| 30 | + | if len(name) > 0xffff { | |||
| 31 | + | b.Fail(ErrBadLength) | |||
| 32 | + | return 0 | |||
| 33 | + | } | |||
| 34 | + | p, seq := b.Request(major, data, pad4(4+len(name))) | |||
| 35 | + | if p == nil { | |||
| 36 | + | return 0 | |||
| 37 | + | } | |||
| 38 | + | binary.LittleEndian.PutUint16(p, uint16(len(name))) | |||
| 39 | + | copy(p[4:], name) | |||
| 40 | + | return seq | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | func encodeQueryExtension(b *Buf, name string) Seq { | |||
| 44 | + | return encodeNamed(b, opQueryExtension, 0, name) | |||
| 45 | + | } | |||
| 46 | + | ||||
| 47 | + | func encodeInternAtom(b *Buf, onlyIfExists bool, name string) Seq { | |||
| 48 | + | var data uint8 | |||
| 49 | + | if onlyIfExists { | |||
| 50 | + | data = 1 | |||
| 51 | + | } | |||
| 52 | + | return encodeNamed(b, opInternAtom, data, name) | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // encodeExtRequest encodes a body-less extension request (BIG-REQUESTS | |||
| 56 | + | // Enable, XC-MISC GetXIDRange). It fails with ErrExtMissing when id is absent. | |||
| 57 | + | func encodeExtRequest(b *Buf, id ExtID, minor uint8) Seq { | |||
| 58 | + | major := b.Ext.Major(id) | |||
| 59 | + | if major == 0 { | |||
| 60 | + | b.Fail(ErrExtMissing) | |||
| 61 | + | return 0 | |||
| 62 | + | } | |||
| 63 | + | _, seq := b.Request(major, minor, 0) | |||
| 64 | + | return seq | |||
| 65 | + | } | |||
| 66 | + | ||||
| 67 | + | // Bounds of the blocking calls. Vars so tests can shorten them. | |||
| 68 | + | var ( | |||
| 69 | + | // connectTimeout bounds NewConn: setup handshake plus extension bootstrap. | |||
| 70 | + | connectTimeout = 10 * time.Second | |||
| 71 | + | // syncTimeout bounds one Sync, InternAtoms or blocking XID refill. | |||
| 72 | + | syncTimeout = 5 * time.Second | |||
| 73 | + | ) | |||
| 74 | + | ||||
| 75 | + | // deadlineError wraps os.ErrDeadlineExceeded for a blocking call that timed | |||
| 76 | + | // out waiting for the server. | |||
| 77 | + | func deadlineError(what string) error { | |||
| 78 | + | return fmt.Errorf("xturbo: %s: no answer from the X server: %w", what, os.ErrDeadlineExceeded) | |||
| 79 | + | } | |||
| 80 | + | ||||
| 81 | + | // bootstrap negotiates every extension root knows (one pipelined | |||
| 82 | + | // QueryExtension each, then BIG-REQUESTS Enable when present) in one Sync, | |||
| 83 | + | // before deadline. | |||
| 84 | + | func (c *Conn) bootstrap(deadline time.Time) error { | |||
| 85 | + | for id := ExtCore + 1; id < extCount; id++ { | |||
| 86 | + | seq := encodeQueryExtension(c.buf, ExtXName[id]) | |||
| 87 | + | if seq == 0 { | |||
| 88 | + | return c.buf.Err() | |||
| 89 | + | } | |||
| 90 | + | if err := c.expect(seq, kindQueryExtension, [4]uint32{uint32(id)}); err != nil { | |||
| 91 | + | return err | |||
| 92 | + | } | |||
| 93 | + | } | |||
| 94 | + | // consumeReply queues bigreq Enable when BIG-REQUESTS is present; the | |||
| 95 | + | // wait covers it too. | |||
| 96 | + | return c.sync(nil, deadline) | |||
| 97 | + | } | |||
| 98 | + | ||||
| 99 | + | // Sync flushes every queued request and blocks until the server has | |||
| 100 | + | // processed them and every request registered with Expect has completed. It | |||
| 101 | + | // appends a GetInputFocus as a fence, so even with nothing registered it is a | |||
| 102 | + | // full round trip: errors of earlier requests have been delivered when it | |||
| 103 | + | // returns. Packets read meanwhile go to h as with Dispatch (nil h: the last | |||
| 104 | + | // Handler given to Dispatch, or none). An error on a request root issued | |||
| 105 | + | // itself is returned as *XError. Sync gives up after 5 s with an error | |||
| 106 | + | // wrapping os.ErrDeadlineExceeded (a hung server); the connection is then | |||
| 107 | + | // still usable but its state unknown, treat it as dead. Meant for startup; | |||
| 108 | + | // called from a Handler callback it returns an error at once. | |||
| 109 | + | func (c *Conn) Sync(h *Handler) error { | |||
| 110 | + | return c.sync(h, time.Now().Add(c.timeout)) | |||
| 111 | + | } | |||
| 112 | + | ||||
| 113 | + | func (c *Conn) sync(h *Handler, deadline time.Time) error { | |||
| 114 | + | if c.dispatching { | |||
| 115 | + | return errReentrantDispatch | |||
| 116 | + | } | |||
| 117 | + | if h == nil { | |||
| 118 | + | h = c.h | |||
| 119 | + | } | |||
| 120 | + | c.syncFailed = false | |||
| 121 | + | if err := c.waitUntil(h, deadline); err != nil { | |||
| 122 | + | return err | |||
| 123 | + | } | |||
| 124 | + | if c.syncFailed { | |||
| 125 | + | e := c.syncErr | |||
| 126 | + | return &e | |||
| 127 | + | } | |||
| 128 | + | return nil | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // waitUntil fences, flushes and dispatches, one bounded Dispatch at a time, | |||
| 132 | + | // until the fence has been answered and no registered slot remains, or | |||
| 133 | + | // deadline passes. A fence is re-queued whenever callbacks registered | |||
| 134 | + | // requests after the last one. | |||
| 135 | + | func (c *Conn) waitUntil(h *Handler, deadline time.Time) error { | |||
| 136 | + | if c.fd < 0 { | |||
| 137 | + | return ErrClosed | |||
| 138 | + | } | |||
| 139 | + | var fence Seq | |||
| 140 | + | for { | |||
| 141 | + | if fence != 0 && c.seq >= fence && c.live == 0 { | |||
| 142 | + | return nil | |||
| 143 | + | } | |||
| 144 | + | if fence == 0 || fence < c.buf.next-1 { | |||
| 145 | + | // Requests queued after the fence (or none yet): fence them. | |||
| 146 | + | if fence = c.fence(); fence == 0 { | |||
| 147 | + | return c.buf.Err() | |||
| 148 | + | } | |||
| 149 | + | } | |||
| 150 | + | if err := c.pollOnce(h, deadline); err != nil { | |||
| 151 | + | return err | |||
| 152 | + | } | |||
| 153 | + | } | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | // pollOnce makes one step of progress: flush, dispatch what is readable and, | |||
| 157 | + | // only when Dispatch read nothing at all, block until the socket is readable | |||
| 158 | + | // (or writable while requests remain queued) or deadline passes. Dispatch is | |||
| 159 | + | // bounded per call, so it may return with input left; the caller loops and | |||
| 160 | + | // re-checks. A deadline already past fails even while packets keep coming | |||
| 161 | + | // (a server flooding events but never answering). | |||
| 162 | + | func (c *Conn) pollOnce(h *Handler, deadline time.Time) error { | |||
| 163 | + | left := time.Until(deadline) | |||
| 164 | + | if left <= 0 { | |||
| 165 | + | return deadlineError("blocking round trip") | |||
| 166 | + | } | |||
| 167 | + | events := int16(unix.POLLIN) | |||
| 168 | + | switch err := c.Flush(); err { | |||
| 169 | + | case nil: | |||
| 170 | + | case ErrWouldBlock: | |||
| 171 | + | events |= unix.POLLOUT | |||
| 172 | + | default: | |||
| 173 | + | return err | |||
| 174 | + | } | |||
| 175 | + | if err := c.Dispatch(h); err != nil { | |||
| 176 | + | return err | |||
| 177 | + | } | |||
| 178 | + | // Packets handled may have queued requests (callbacks, the bootstrap): | |||
| 179 | + | // loop to flush them, and to fence them, before blocking. | |||
| 180 | + | if !c.rdIdle || (events&unix.POLLOUT == 0 && c.buf.Len() > 0) { | |||
| 181 | + | return nil | |||
| 182 | + | } | |||
| 183 | + | return pollFd(c.fd, events, left) | |||
| 184 | + | } | |||
| 185 | + | ||||
| 186 | + | // pollFd blocks until fd has one of events or timeout passes (returning nil; | |||
| 187 | + | // callers re-check their deadline). EINTR is a wake-up. | |||
| 188 | + | func pollFd(fd int, events int16, timeout time.Duration) error { | |||
| 189 | + | ms := int((timeout + time.Millisecond - 1) / time.Millisecond) | |||
| 190 | + | fds := []unix.PollFd{{Fd: int32(fd), Events: events}} | |||
| 191 | + | if _, err := unix.Poll(fds, ms); err != nil && err != unix.EINTR { | |||
| 192 | + | return os.NewSyscallError("poll", err) | |||
| 193 | + | } | |||
| 194 | + | return nil | |||
| 195 | + | } | |||
A xid.go +194 -0
| @@ -0,0 +1,194 @@ | |||
| 1 | + | package xturbo | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "time" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | // errXIDsExhausted reports that the server has no resource IDs left for this | |
| 9 | + | // client: XC-MISC GetXIDRange returned an empty range, or one outside this | |
| 10 | + | // client's base and mask. | |
| 11 | + | var errXIDsExhausted = errors.New("xturbo: server has no resource IDs left") | |
| 12 | + | ||
| 13 | + | const ( | |
| 14 | + | // xidFreeCap bounds the FreeXID list; IDs freed beyond it are not reused. | |
| 15 | + | xidFreeCap = 256 | |
| 16 | + | // xidLowMark is the remaining-ID count below which XIDsLow reports true. | |
| 17 | + | xidLowMark = 4096 | |
| 18 | + | ) | |
| 19 | + | ||
| 20 | + | // xidRange is a run of count IDs base|next, base|next+inc, … (inc = lowest | |
| 21 | + | // bit of the mask). | |
| 22 | + | type xidRange struct { | |
| 23 | + | next, left uint32 | |
| 24 | + | } | |
| 25 | + | ||
| 26 | + | // initXIDs starts allocation over the setup range: base|0, base|inc, … | |
| 27 | + | // up to base|mask, in libxcb's order. | |
| 28 | + | func (c *Conn) initXIDs() { | |
| 29 | + | inc := c.xidMask & -c.xidMask | |
| 30 | + | if inc == 0 { | |
| 31 | + | return | |
| 32 | + | } | |
| 33 | + | c.xids = xidRange{0, c.xidMask/inc + 1} | |
| 34 | + | } | |
| 35 | + | ||
| 36 | + | // NewXID returns a fresh resource ID for this client. IDs released with | |
| 37 | + | // FreeXID are reused first, then the current range, then a range fetched | |
| 38 | + | // ahead by PrefetchXIDs. When all are empty, outside a Handler callback, one | |
| 39 | + | // blocking XC-MISC GetXIDRange round trip (bounded like Sync) refills it; | |
| 40 | + | // packets read meanwhile go to the last Handler given to Dispatch. Inside a | |
| 41 | + | // callback that refill is impossible and NewXID fails instead, so an event | |
| 42 | + | // loop allocating from callbacks calls PrefetchXIDs whenever XIDsLow reports | |
| 43 | + | // true (the XWM does it once per loop iteration). It also fails when XC-MISC | |
| 44 | + | // is absent or the server has no IDs left. | |
| 45 | + | func (c *Conn) NewXID() (uint32, error) { | |
| 46 | + | if c.xidFreeN > 0 { | |
| 47 | + | c.xidFreeN-- | |
| 48 | + | return c.xidFree[c.xidFreeN], nil | |
| 49 | + | } | |
| 50 | + | if c.xids.left == 0 { | |
| 51 | + | if c.xidPend.left == 0 { | |
| 52 | + | if err := c.refillXIDs(); err != nil { | |
| 53 | + | return 0, err | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | c.xids, c.xidPend = c.xidPend, xidRange{} | |
| 57 | + | } | |
| 58 | + | id := c.xidBase | c.xids.next | |
| 59 | + | c.xids.next += c.xidMask & -c.xidMask | |
| 60 | + | c.xids.left-- | |
| 61 | + | return id, nil | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | // FreeXID gives back id for reuse by a later NewXID. id must come from | |
| 65 | + | // NewXID on this Conn and be freed once, after the caller destroyed its | |
| 66 | + | // resource (the destroy request may still be queued, it precedes any reuse | |
| 67 | + | // on the wire). IDs outside this client's range, 0, and IDs beyond the free | |
| 68 | + | // list capacity are ignored; freeing an ID twice hands it out twice. | |
| 69 | + | func (c *Conn) FreeXID(id uint32) { | |
| 70 | + | if id&^c.xidMask != c.xidBase || c.xidFreeN == xidFreeCap { | |
| 71 | + | return | |
| 72 | + | } | |
| 73 | + | c.xidFree[c.xidFreeN] = id | |
| 74 | + | c.xidFreeN++ | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // XIDsLow reports that fewer than 4096 IDs remain in the current range with | |
| 78 | + | // an empty free list, and that PrefetchXIDs would help: XC-MISC is present, | |
| 79 | + | // no prefetched range is waiting, no prefetch is in flight and the last one | |
| 80 | + | // did not report the server out of IDs (after that only NewXID's blocking | |
| 81 | + | // refill retries). | |
| 82 | + | func (c *Conn) XIDsLow() bool { | |
| 83 | + | if c.xidFreeN > 0 || c.xidPend.left > 0 || c.xidNone || c.xidInFlight() || !c.ext.Info(ExtXCMisc).Present { | |
| 84 | + | return false | |
| 85 | + | } | |
| 86 | + | return c.xids.left < xidLowMark | |
| 87 | + | } | |
| 88 | + | ||
| 89 | + | // PrefetchXIDs queues an XC-MISC GetXIDRange without waiting; its reply, | |
| 90 | + | // read by a later Dispatch, becomes the range NewXID switches to when the | |
| 91 | + | // current one runs out. Callers flush as usual. It does nothing while a | |
| 92 | + | // prefetched range is unused or a prefetch is in flight, and may be called | |
| 93 | + | // from a Handler callback. It fails with ErrExtMissing without XC-MISC, | |
| 94 | + | // ErrRingFull when the next request's Expect slot is taken, and with the | |
| 95 | + | // Buf's sticky error. | |
| 96 | + | func (c *Conn) PrefetchXIDs() error { | |
| 97 | + | if c.fd < 0 { | |
| 98 | + | return ErrClosed | |
| 99 | + | } | |
| 100 | + | if !c.ext.Info(ExtXCMisc).Present { | |
| 101 | + | return ErrExtMissing | |
| 102 | + | } | |
| 103 | + | if c.xidPend.left > 0 || c.xidInFlight() { | |
| 104 | + | return nil | |
| 105 | + | } | |
| 106 | + | return c.queueXIDRange() | |
| 107 | + | } | |
| 108 | + | ||
| 109 | + | // queueXIDRange queues a GetXIDRange registered with kindXIDRange. | |
| 110 | + | func (c *Conn) queueXIDRange() error { | |
| 111 | + | if c.ring[c.buf.next&ringMask].seq != 0 { | |
| 112 | + | return ErrRingFull | |
| 113 | + | } | |
| 114 | + | seq := encodeExtRequest(c.buf, ExtXCMisc, minorGetXIDRange) | |
| 115 | + | if seq == 0 { | |
| 116 | + | return c.buf.Err() | |
| 117 | + | } | |
| 118 | + | if err := c.expect(seq, kindXIDRange, [4]uint32{}); err != nil { | |
| 119 | + | return err | |
| 120 | + | } | |
| 121 | + | c.xidReq = seq | |
| 122 | + | c.xidNone, c.xidFailed = false, false | |
| 123 | + | return nil | |
| 124 | + | } | |
| 125 | + | ||
| 126 | + | // xidInFlight reports a GetXIDRange whose reply or error has not arrived. | |
| 127 | + | func (c *Conn) xidInFlight() bool { | |
| 128 | + | return c.xidReq != 0 && c.ring[c.xidReq&ringMask].seq == c.xidReq | |
| 129 | + | } | |
| 130 | + | ||
| 131 | + | // gotXIDRange records a GetXIDRange reply (from consumeReply). | |
| 132 | + | func (c *Conn) gotXIDRange(start, count uint32) { | |
| 133 | + | if !c.validXIDRange(start, count) { | |
| 134 | + | c.xidNone = true | |
| 135 | + | return | |
| 136 | + | } | |
| 137 | + | c.xidPend = xidRange{start & c.xidMask, count} | |
| 138 | + | } | |
| 139 | + | ||
| 140 | + | // validXIDRange accepts a GetXIDRange answer only when every ID of it lies in | |
| 141 | + | // this client's base and mask. The server answers start 0 (count 0 or 1) | |
| 142 | + | // when it has none. | |
| 143 | + | func (c *Conn) validXIDRange(start, count uint32) bool { | |
| 144 | + | inc := uint64(c.xidMask & -c.xidMask) | |
| 145 | + | off := uint64(start & c.xidMask) | |
| 146 | + | return inc != 0 && count != 0 && start != 0 && | |
| 147 | + | start&^c.xidMask == c.xidBase && off%inc == 0 && | |
| 148 | + | off+uint64(count-1)*inc <= uint64(c.xidMask) | |
| 149 | + | } | |
| 150 | + | ||
| 151 | + | // refillXIDs blocks until a prefetched range is available: it waits for the | |
| 152 | + | // prefetch in flight, or queues one. | |
| 153 | + | func (c *Conn) refillXIDs() error { | |
| 154 | + | if c.dispatching { | |
| 155 | + | return errReentrantDispatch | |
| 156 | + | } | |
| 157 | + | if c.fd < 0 { | |
| 158 | + | return ErrClosed | |
| 159 | + | } | |
| 160 | + | if !c.ext.Info(ExtXCMisc).Present { | |
| 161 | + | return ErrExtMissing | |
| 162 | + | } | |
| 163 | + | deadline := time.Now().Add(c.timeout) | |
| 164 | + | if !c.xidInFlight() { | |
| 165 | + | if err := c.waitSlot(deadline); err != nil { | |
| 166 | + | return err | |
| 167 | + | } | |
| 168 | + | if err := c.queueXIDRange(); err != nil { | |
| 169 | + | return err | |
| 170 | + | } | |
| 171 | + | } | |
| 172 | + | if err := c.waitUntil(c.h, deadline); err != nil { | |
| 173 | + | return err | |
| 174 | + | } | |
| 175 | + | switch { | |
| 176 | + | case c.xidFailed: | |
| 177 | + | e := c.xidErr | |
| 178 | + | return &e | |
| 179 | + | case c.xidPend.left == 0: | |
| 180 | + | return errXIDsExhausted | |
| 181 | + | } | |
| 182 | + | return nil | |
| 183 | + | } | |
| 184 | + | ||
| 185 | + | // waitSlot makes sure the ring slot of the next request is free, so a | |
| 186 | + | // reserved registration can always be made for it: when an earlier request | |
| 187 | + | // still holds it, it waits for every outstanding request (fenced, so even a | |
| 188 | + | // registered request without reply completes). | |
| 189 | + | func (c *Conn) waitSlot(deadline time.Time) error { | |
| 190 | + | if c.ring[c.buf.next&ringMask].seq == 0 { | |
| 191 | + | return nil | |
| 192 | + | } | |
| 193 | + | return c.waitUntil(c.h, deadline) | |
| 194 | + | } | |
| @@ -0,0 +1,194 @@ | |||||
| 1 | + | package xturbo | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "errors" | |||
| 5 | + | "time" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | // errXIDsExhausted reports that the server has no resource IDs left for this | |||
| 9 | + | // client: XC-MISC GetXIDRange returned an empty range, or one outside this | |||
| 10 | + | // client's base and mask. | |||
| 11 | + | var errXIDsExhausted = errors.New("xturbo: server has no resource IDs left") | |||
| 12 | + | ||||
| 13 | + | const ( | |||
| 14 | + | // xidFreeCap bounds the FreeXID list; IDs freed beyond it are not reused. | |||
| 15 | + | xidFreeCap = 256 | |||
| 16 | + | // xidLowMark is the remaining-ID count below which XIDsLow reports true. | |||
| 17 | + | xidLowMark = 4096 | |||
| 18 | + | ) | |||
| 19 | + | ||||
| 20 | + | // xidRange is a run of count IDs base|next, base|next+inc, … (inc = lowest | |||
| 21 | + | // bit of the mask). | |||
| 22 | + | type xidRange struct { | |||
| 23 | + | next, left uint32 | |||
| 24 | + | } | |||
| 25 | + | ||||
| 26 | + | // initXIDs starts allocation over the setup range: base|0, base|inc, … | |||
| 27 | + | // up to base|mask, in libxcb's order. | |||
| 28 | + | func (c *Conn) initXIDs() { | |||
| 29 | + | inc := c.xidMask & -c.xidMask | |||
| 30 | + | if inc == 0 { | |||
| 31 | + | return | |||
| 32 | + | } | |||
| 33 | + | c.xids = xidRange{0, c.xidMask/inc + 1} | |||
| 34 | + | } | |||
| 35 | + | ||||
| 36 | + | // NewXID returns a fresh resource ID for this client. IDs released with | |||
| 37 | + | // FreeXID are reused first, then the current range, then a range fetched | |||
| 38 | + | // ahead by PrefetchXIDs. When all are empty, outside a Handler callback, one | |||
| 39 | + | // blocking XC-MISC GetXIDRange round trip (bounded like Sync) refills it; | |||
| 40 | + | // packets read meanwhile go to the last Handler given to Dispatch. Inside a | |||
| 41 | + | // callback that refill is impossible and NewXID fails instead, so an event | |||
| 42 | + | // loop allocating from callbacks calls PrefetchXIDs whenever XIDsLow reports | |||
| 43 | + | // true (the XWM does it once per loop iteration). It also fails when XC-MISC | |||
| 44 | + | // is absent or the server has no IDs left. | |||
| 45 | + | func (c *Conn) NewXID() (uint32, error) { | |||
| 46 | + | if c.xidFreeN > 0 { | |||
| 47 | + | c.xidFreeN-- | |||
| 48 | + | return c.xidFree[c.xidFreeN], nil | |||
| 49 | + | } | |||
| 50 | + | if c.xids.left == 0 { | |||
| 51 | + | if c.xidPend.left == 0 { | |||
| 52 | + | if err := c.refillXIDs(); err != nil { | |||
| 53 | + | return 0, err | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | c.xids, c.xidPend = c.xidPend, xidRange{} | |||
| 57 | + | } | |||
| 58 | + | id := c.xidBase | c.xids.next | |||
| 59 | + | c.xids.next += c.xidMask & -c.xidMask | |||
| 60 | + | c.xids.left-- | |||
| 61 | + | return id, nil | |||
| 62 | + | } | |||
| 63 | + | ||||
| 64 | + | // FreeXID gives back id for reuse by a later NewXID. id must come from | |||
| 65 | + | // NewXID on this Conn and be freed once, after the caller destroyed its | |||
| 66 | + | // resource (the destroy request may still be queued, it precedes any reuse | |||
| 67 | + | // on the wire). IDs outside this client's range, 0, and IDs beyond the free | |||
| 68 | + | // list capacity are ignored; freeing an ID twice hands it out twice. | |||
| 69 | + | func (c *Conn) FreeXID(id uint32) { | |||
| 70 | + | if id&^c.xidMask != c.xidBase || c.xidFreeN == xidFreeCap { | |||
| 71 | + | return | |||
| 72 | + | } | |||
| 73 | + | c.xidFree[c.xidFreeN] = id | |||
| 74 | + | c.xidFreeN++ | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // XIDsLow reports that fewer than 4096 IDs remain in the current range with | |||
| 78 | + | // an empty free list, and that PrefetchXIDs would help: XC-MISC is present, | |||
| 79 | + | // no prefetched range is waiting, no prefetch is in flight and the last one | |||
| 80 | + | // did not report the server out of IDs (after that only NewXID's blocking | |||
| 81 | + | // refill retries). | |||
| 82 | + | func (c *Conn) XIDsLow() bool { | |||
| 83 | + | if c.xidFreeN > 0 || c.xidPend.left > 0 || c.xidNone || c.xidInFlight() || !c.ext.Info(ExtXCMisc).Present { | |||
| 84 | + | return false | |||
| 85 | + | } | |||
| 86 | + | return c.xids.left < xidLowMark | |||
| 87 | + | } | |||
| 88 | + | ||||
| 89 | + | // PrefetchXIDs queues an XC-MISC GetXIDRange without waiting; its reply, | |||
| 90 | + | // read by a later Dispatch, becomes the range NewXID switches to when the | |||
| 91 | + | // current one runs out. Callers flush as usual. It does nothing while a | |||
| 92 | + | // prefetched range is unused or a prefetch is in flight, and may be called | |||
| 93 | + | // from a Handler callback. It fails with ErrExtMissing without XC-MISC, | |||
| 94 | + | // ErrRingFull when the next request's Expect slot is taken, and with the | |||
| 95 | + | // Buf's sticky error. | |||
| 96 | + | func (c *Conn) PrefetchXIDs() error { | |||
| 97 | + | if c.fd < 0 { | |||
| 98 | + | return ErrClosed | |||
| 99 | + | } | |||
| 100 | + | if !c.ext.Info(ExtXCMisc).Present { | |||
| 101 | + | return ErrExtMissing | |||
| 102 | + | } | |||
| 103 | + | if c.xidPend.left > 0 || c.xidInFlight() { | |||
| 104 | + | return nil | |||
| 105 | + | } | |||
| 106 | + | return c.queueXIDRange() | |||
| 107 | + | } | |||
| 108 | + | ||||
| 109 | + | // queueXIDRange queues a GetXIDRange registered with kindXIDRange. | |||
| 110 | + | func (c *Conn) queueXIDRange() error { | |||
| 111 | + | if c.ring[c.buf.next&ringMask].seq != 0 { | |||
| 112 | + | return ErrRingFull | |||
| 113 | + | } | |||
| 114 | + | seq := encodeExtRequest(c.buf, ExtXCMisc, minorGetXIDRange) | |||
| 115 | + | if seq == 0 { | |||
| 116 | + | return c.buf.Err() | |||
| 117 | + | } | |||
| 118 | + | if err := c.expect(seq, kindXIDRange, [4]uint32{}); err != nil { | |||
| 119 | + | return err | |||
| 120 | + | } | |||
| 121 | + | c.xidReq = seq | |||
| 122 | + | c.xidNone, c.xidFailed = false, false | |||
| 123 | + | return nil | |||
| 124 | + | } | |||
| 125 | + | ||||
| 126 | + | // xidInFlight reports a GetXIDRange whose reply or error has not arrived. | |||
| 127 | + | func (c *Conn) xidInFlight() bool { | |||
| 128 | + | return c.xidReq != 0 && c.ring[c.xidReq&ringMask].seq == c.xidReq | |||
| 129 | + | } | |||
| 130 | + | ||||
| 131 | + | // gotXIDRange records a GetXIDRange reply (from consumeReply). | |||
| 132 | + | func (c *Conn) gotXIDRange(start, count uint32) { | |||
| 133 | + | if !c.validXIDRange(start, count) { | |||
| 134 | + | c.xidNone = true | |||
| 135 | + | return | |||
| 136 | + | } | |||
| 137 | + | c.xidPend = xidRange{start & c.xidMask, count} | |||
| 138 | + | } | |||
| 139 | + | ||||
| 140 | + | // validXIDRange accepts a GetXIDRange answer only when every ID of it lies in | |||
| 141 | + | // this client's base and mask. The server answers start 0 (count 0 or 1) | |||
| 142 | + | // when it has none. | |||
| 143 | + | func (c *Conn) validXIDRange(start, count uint32) bool { | |||
| 144 | + | inc := uint64(c.xidMask & -c.xidMask) | |||
| 145 | + | off := uint64(start & c.xidMask) | |||
| 146 | + | return inc != 0 && count != 0 && start != 0 && | |||
| 147 | + | start&^c.xidMask == c.xidBase && off%inc == 0 && | |||
| 148 | + | off+uint64(count-1)*inc <= uint64(c.xidMask) | |||
| 149 | + | } | |||
| 150 | + | ||||
| 151 | + | // refillXIDs blocks until a prefetched range is available: it waits for the | |||
| 152 | + | // prefetch in flight, or queues one. | |||
| 153 | + | func (c *Conn) refillXIDs() error { | |||
| 154 | + | if c.dispatching { | |||
| 155 | + | return errReentrantDispatch | |||
| 156 | + | } | |||
| 157 | + | if c.fd < 0 { | |||
| 158 | + | return ErrClosed | |||
| 159 | + | } | |||
| 160 | + | if !c.ext.Info(ExtXCMisc).Present { | |||
| 161 | + | return ErrExtMissing | |||
| 162 | + | } | |||
| 163 | + | deadline := time.Now().Add(c.timeout) | |||
| 164 | + | if !c.xidInFlight() { | |||
| 165 | + | if err := c.waitSlot(deadline); err != nil { | |||
| 166 | + | return err | |||
| 167 | + | } | |||
| 168 | + | if err := c.queueXIDRange(); err != nil { | |||
| 169 | + | return err | |||
| 170 | + | } | |||
| 171 | + | } | |||
| 172 | + | if err := c.waitUntil(c.h, deadline); err != nil { | |||
| 173 | + | return err | |||
| 174 | + | } | |||
| 175 | + | switch { | |||
| 176 | + | case c.xidFailed: | |||
| 177 | + | e := c.xidErr | |||
| 178 | + | return &e | |||
| 179 | + | case c.xidPend.left == 0: | |||
| 180 | + | return errXIDsExhausted | |||
| 181 | + | } | |||
| 182 | + | return nil | |||
| 183 | + | } | |||
| 184 | + | ||||
| 185 | + | // waitSlot makes sure the ring slot of the next request is free, so a | |||
| 186 | + | // reserved registration can always be made for it: when an earlier request | |||
| 187 | + | // still holds it, it waits for every outstanding request (fenced, so even a | |||
| 188 | + | // registered request without reply completes). | |||
| 189 | + | func (c *Conn) waitSlot(deadline time.Time) error { | |||
| 190 | + | if c.ring[c.buf.next&ringMask].seq == 0 { | |||
| 191 | + | return nil | |||
| 192 | + | } | |||
| 193 | + | return c.waitUntil(c.h, deadline) | |||
| 194 | + | } | |||
A xserver_script_test.go +216 -0
| @@ -0,0 +1,216 @@ | |||
| 1 | + | package xturbo | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "io" | |
| 6 | + | "os" | |
| 7 | + | "sync" | |
| 8 | + | "sync/atomic" | |
| 9 | + | "testing" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // scriptExt is an extension the scripted server advertises. | |
| 13 | + | type scriptExt struct{ major, firstEvent, firstError uint8 } | |
| 14 | + | ||
| 15 | + | // script is the behaviour of the scripted X server behind a socketpair. It | |
| 16 | + | // answers QueryExtension, InternAtom, GetInputFocus, BIG-REQUESTS Enable and | |
| 17 | + | // XC-MISC GetXIDRange like an X server; other requests get no reply unless | |
| 18 | + | // hook handles them. | |
| 19 | + | type script struct { | |
| 20 | + | exts map[string]scriptExt | |
| 21 | + | bigreqMax uint32 // Enable reply maximum_request_length | |
| 22 | + | atoms map[string]uint32 // known atoms; new names get nextAtom++ | |
| 23 | + | nextAtom uint32 | |
| 24 | + | badAtoms map[string]bool // InternAtom answers BadAlloc | |
| 25 | + | xidRanges [][2]uint32 // successive GetXIDRange replies {start, count} | |
| 26 | + | // hook sees every request first; returning true skips the default. | |
| 27 | + | hook func(s *scriptConn, major, minor uint8, seq uint16, body []byte) bool | |
| 28 | + | // silent, once set, makes the server read requests and never answer | |
| 29 | + | // (alive but hung). | |
| 30 | + | silent atomic.Bool | |
| 31 | + | } | |
| 32 | + | ||
| 33 | + | // scriptConn is the server side of one scripted connection. | |
| 34 | + | type scriptConn struct { | |
| 35 | + | f *os.File | |
| 36 | + | sc *script | |
| 37 | + | seq uint16 | |
| 38 | + | ||
| 39 | + | mu sync.Mutex | |
| 40 | + | reqs []scriptReq // every request after setup, in order | |
| 41 | + | } | |
| 42 | + | ||
| 43 | + | type scriptReq struct { | |
| 44 | + | seq uint16 | |
| 45 | + | major uint8 | |
| 46 | + | minor uint8 | |
| 47 | + | raw []byte // whole request, header included | |
| 48 | + | } | |
| 49 | + | ||
| 50 | + | func (s *scriptConn) requests() []scriptReq { | |
| 51 | + | s.mu.Lock() | |
| 52 | + | defer s.mu.Unlock() | |
| 53 | + | return append([]scriptReq(nil), s.reqs...) | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | func (s *scriptConn) write(p []byte) { _, _ = s.f.Write(p) } | |
| 57 | + | ||
| 58 | + | func (s *scriptConn) reply(data uint8, body []byte) { | |
| 59 | + | words := (max(len(body), 24) - 24 + 3) / 4 | |
| 60 | + | p := make([]byte, 32+4*words) | |
| 61 | + | p[0], p[1] = 1, data | |
| 62 | + | binary.LittleEndian.PutUint16(p[2:], s.seq) | |
| 63 | + | binary.LittleEndian.PutUint32(p[4:], uint32(words)) | |
| 64 | + | copy(p[8:], body) | |
| 65 | + | s.write(p) | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | func (s *scriptConn) xerror(code, major uint8, bad uint32) { | |
| 69 | + | p := make([]byte, 32) | |
| 70 | + | p[1] = code | |
| 71 | + | binary.LittleEndian.PutUint16(p[2:], s.seq) | |
| 72 | + | binary.LittleEndian.PutUint32(p[4:], bad) | |
| 73 | + | p[10] = major | |
| 74 | + | s.write(p) | |
| 75 | + | } | |
| 76 | + | ||
| 77 | + | // event writes a 32-byte core event carrying the current sequence number. | |
| 78 | + | func (s *scriptConn) event(code uint8, fill byte) { | |
| 79 | + | p := make([]byte, 32) | |
| 80 | + | for i := range p { | |
| 81 | + | p[i] = fill | |
| 82 | + | } | |
| 83 | + | p[0] = code | |
| 84 | + | binary.LittleEndian.PutUint16(p[2:], s.seq) | |
| 85 | + | s.write(p) | |
| 86 | + | } | |
| 87 | + | ||
| 88 | + | func (sc *script) extByMajor(major uint8) string { | |
| 89 | + | for name, e := range sc.exts { | |
| 90 | + | if e.major == major { | |
| 91 | + | return name | |
| 92 | + | } | |
| 93 | + | } | |
| 94 | + | return "" | |
| 95 | + | } | |
| 96 | + | ||
| 97 | + | // serve reads requests until EOF and answers them. | |
| 98 | + | func (s *scriptConn) serve() { | |
| 99 | + | sc := s.sc | |
| 100 | + | for { | |
| 101 | + | var hdr [4]byte | |
| 102 | + | if _, err := io.ReadFull(s.f, hdr[:]); err != nil { | |
| 103 | + | return | |
| 104 | + | } | |
| 105 | + | raw := hdr[:] | |
| 106 | + | n := int(binary.LittleEndian.Uint16(hdr[2:])) * 4 | |
| 107 | + | if n == 0 { // BIG-REQUESTS header | |
| 108 | + | var l [4]byte | |
| 109 | + | if _, err := io.ReadFull(s.f, l[:]); err != nil { | |
| 110 | + | return | |
| 111 | + | } | |
| 112 | + | raw = append(raw, l[:]...) | |
| 113 | + | n = int(binary.LittleEndian.Uint32(l[:])) * 4 | |
| 114 | + | } | |
| 115 | + | rest := make([]byte, n-len(raw)) | |
| 116 | + | if _, err := io.ReadFull(s.f, rest); err != nil { | |
| 117 | + | return | |
| 118 | + | } | |
| 119 | + | raw = append(append([]byte(nil), raw...), rest...) | |
| 120 | + | body := raw[len(raw)-len(rest):] | |
| 121 | + | s.seq++ | |
| 122 | + | major, minor := hdr[0], hdr[1] | |
| 123 | + | s.mu.Lock() | |
| 124 | + | s.reqs = append(s.reqs, scriptReq{s.seq, major, minor, raw}) | |
| 125 | + | s.mu.Unlock() | |
| 126 | + | if sc.silent.Load() || sc.hook != nil && sc.hook(s, major, minor, s.seq, body) { | |
| 127 | + | continue | |
| 128 | + | } | |
| 129 | + | s.answer(major, minor, body) | |
| 130 | + | } | |
| 131 | + | } | |
| 132 | + | ||
| 133 | + | // answer runs the default behaviour for one request, holding s.mu so tests | |
| 134 | + | // can read sc (atom) concurrently. | |
| 135 | + | func (s *scriptConn) answer(major, minor uint8, body []byte) { | |
| 136 | + | sc := s.sc | |
| 137 | + | s.mu.Lock() | |
| 138 | + | defer s.mu.Unlock() | |
| 139 | + | switch { | |
| 140 | + | case major == opQueryExtension: | |
| 141 | + | e, ok := sc.exts[string(body[4:4+binary.LittleEndian.Uint16(body)])] | |
| 142 | + | var present uint8 | |
| 143 | + | if ok { | |
| 144 | + | present = 1 | |
| 145 | + | } | |
| 146 | + | s.reply(0, []byte{present, e.major, e.firstEvent, e.firstError}) | |
| 147 | + | case major == opInternAtom: | |
| 148 | + | name := string(body[4 : 4+binary.LittleEndian.Uint16(body)]) | |
| 149 | + | if sc.badAtoms[name] { | |
| 150 | + | s.xerror(11, opInternAtom, 0) // BadAlloc | |
| 151 | + | return | |
| 152 | + | } | |
| 153 | + | a, ok := sc.atoms[name] | |
| 154 | + | if !ok && minor == 0 { | |
| 155 | + | if sc.atoms == nil { | |
| 156 | + | sc.atoms = map[string]uint32{} | |
| 157 | + | } | |
| 158 | + | sc.nextAtom++ | |
| 159 | + | a = sc.nextAtom + 1000 | |
| 160 | + | sc.atoms[name] = a | |
| 161 | + | } | |
| 162 | + | s.reply(0, binary.LittleEndian.AppendUint32(nil, a)) | |
| 163 | + | case major == opGetInputFocus: | |
| 164 | + | s.reply(1, binary.LittleEndian.AppendUint32(nil, 0x1234)) | |
| 165 | + | case major >= 128 && sc.extByMajor(major) == "BIG-REQUESTS" && minor == 0: | |
| 166 | + | s.reply(0, binary.LittleEndian.AppendUint32(nil, sc.bigreqMax)) | |
| 167 | + | case major >= 128 && sc.extByMajor(major) == "XC-MISC" && minor == minorGetXIDRange: | |
| 168 | + | r := [2]uint32{0, 1} | |
| 169 | + | if len(sc.xidRanges) > 0 { | |
| 170 | + | r, sc.xidRanges = sc.xidRanges[0], sc.xidRanges[1:] | |
| 171 | + | } | |
| 172 | + | s.reply(0, binary.LittleEndian.AppendUint32(binary.LittleEndian.AppendUint32(nil, r[0]), r[1])) | |
| 173 | + | } | |
| 174 | + | } | |
| 175 | + | ||
| 176 | + | // fullExts is what a typical Xvfb advertises, at its usual opcodes. | |
| 177 | + | func fullExts() map[string]scriptExt { | |
| 178 | + | return map[string]scriptExt{ | |
| 179 | + | "BIG-REQUESTS": {133, 0, 0}, | |
| 180 | + | "Generic Event Extension": {128, 0, 0}, | |
| 181 | + | "XC-MISC": {136, 0, 0}, | |
| 182 | + | "Composite": {142, 0, 0}, | |
| 183 | + | "XFIXES": {138, 87, 140}, | |
| 184 | + | "X-Resource": {144, 0, 0}, | |
| 185 | + | "RENDER": {139, 0, 141}, | |
| 186 | + | "SHAPE": {129, 64, 0}, | |
| 187 | + | } | |
| 188 | + | } | |
| 189 | + | ||
| 190 | + | // startScript serves sc behind a socketpair after answering the setup with | |
| 191 | + | // the recorded Success reply, and returns the client fd (not yet set up) and | |
| 192 | + | // the server connection. | |
| 193 | + | func startScript(t *testing.T, sc *script) (int, *scriptConn) { | |
| 194 | + | t.Helper() | |
| 195 | + | fd, srv, _ := serveSetup(t, loadSetup(t, "Success"), 0, false, sc) | |
| 196 | + | return fd, srv | |
| 197 | + | } | |
| 198 | + | ||
| 199 | + | // dialScript runs NewConn against sc. | |
| 200 | + | func dialScript(t *testing.T, sc *script) (*Conn, *scriptConn) { | |
| 201 | + | t.Helper() | |
| 202 | + | fd, srv := startScript(t, sc) | |
| 203 | + | c, err := NewConn(fd) | |
| 204 | + | if err != nil { | |
| 205 | + | t.Fatal(err) | |
| 206 | + | } | |
| 207 | + | t.Cleanup(func() { _ = c.Close() }) | |
| 208 | + | return c, srv | |
| 209 | + | } | |
| 210 | + | ||
| 211 | + | // atom returns the atom the server assigned to name. | |
| 212 | + | func (s *scriptConn) atom(name string) uint32 { | |
| 213 | + | s.mu.Lock() | |
| 214 | + | defer s.mu.Unlock() | |
| 215 | + | return s.sc.atoms[name] | |
| 216 | + | } | |
| @@ -0,0 +1,216 @@ | |||||
| 1 | + | package xturbo | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "io" | |||
| 6 | + | "os" | |||
| 7 | + | "sync" | |||
| 8 | + | "sync/atomic" | |||
| 9 | + | "testing" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // scriptExt is an extension the scripted server advertises. | |||
| 13 | + | type scriptExt struct{ major, firstEvent, firstError uint8 } | |||
| 14 | + | ||||
| 15 | + | // script is the behaviour of the scripted X server behind a socketpair. It | |||
| 16 | + | // answers QueryExtension, InternAtom, GetInputFocus, BIG-REQUESTS Enable and | |||
| 17 | + | // XC-MISC GetXIDRange like an X server; other requests get no reply unless | |||
| 18 | + | // hook handles them. | |||
| 19 | + | type script struct { | |||
| 20 | + | exts map[string]scriptExt | |||
| 21 | + | bigreqMax uint32 // Enable reply maximum_request_length | |||
| 22 | + | atoms map[string]uint32 // known atoms; new names get nextAtom++ | |||
| 23 | + | nextAtom uint32 | |||
| 24 | + | badAtoms map[string]bool // InternAtom answers BadAlloc | |||
| 25 | + | xidRanges [][2]uint32 // successive GetXIDRange replies {start, count} | |||
| 26 | + | // hook sees every request first; returning true skips the default. | |||
| 27 | + | hook func(s *scriptConn, major, minor uint8, seq uint16, body []byte) bool | |||
| 28 | + | // silent, once set, makes the server read requests and never answer | |||
| 29 | + | // (alive but hung). | |||
| 30 | + | silent atomic.Bool | |||
| 31 | + | } | |||
| 32 | + | ||||
| 33 | + | // scriptConn is the server side of one scripted connection. | |||
| 34 | + | type scriptConn struct { | |||
| 35 | + | f *os.File | |||
| 36 | + | sc *script | |||
| 37 | + | seq uint16 | |||
| 38 | + | ||||
| 39 | + | mu sync.Mutex | |||
| 40 | + | reqs []scriptReq // every request after setup, in order | |||
| 41 | + | } | |||
| 42 | + | ||||
| 43 | + | type scriptReq struct { | |||
| 44 | + | seq uint16 | |||
| 45 | + | major uint8 | |||
| 46 | + | minor uint8 | |||
| 47 | + | raw []byte // whole request, header included | |||
| 48 | + | } | |||
| 49 | + | ||||
| 50 | + | func (s *scriptConn) requests() []scriptReq { | |||
| 51 | + | s.mu.Lock() | |||
| 52 | + | defer s.mu.Unlock() | |||
| 53 | + | return append([]scriptReq(nil), s.reqs...) | |||
| 54 | + | } | |||
| 55 | + | ||||
| 56 | + | func (s *scriptConn) write(p []byte) { _, _ = s.f.Write(p) } | |||
| 57 | + | ||||
| 58 | + | func (s *scriptConn) reply(data uint8, body []byte) { | |||
| 59 | + | words := (max(len(body), 24) - 24 + 3) / 4 | |||
| 60 | + | p := make([]byte, 32+4*words) | |||
| 61 | + | p[0], p[1] = 1, data | |||
| 62 | + | binary.LittleEndian.PutUint16(p[2:], s.seq) | |||
| 63 | + | binary.LittleEndian.PutUint32(p[4:], uint32(words)) | |||
| 64 | + | copy(p[8:], body) | |||
| 65 | + | s.write(p) | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | func (s *scriptConn) xerror(code, major uint8, bad uint32) { | |||
| 69 | + | p := make([]byte, 32) | |||
| 70 | + | p[1] = code | |||
| 71 | + | binary.LittleEndian.PutUint16(p[2:], s.seq) | |||
| 72 | + | binary.LittleEndian.PutUint32(p[4:], bad) | |||
| 73 | + | p[10] = major | |||
| 74 | + | s.write(p) | |||
| 75 | + | } | |||
| 76 | + | ||||
| 77 | + | // event writes a 32-byte core event carrying the current sequence number. | |||
| 78 | + | func (s *scriptConn) event(code uint8, fill byte) { | |||
| 79 | + | p := make([]byte, 32) | |||
| 80 | + | for i := range p { | |||
| 81 | + | p[i] = fill | |||
| 82 | + | } | |||
| 83 | + | p[0] = code | |||
| 84 | + | binary.LittleEndian.PutUint16(p[2:], s.seq) | |||
| 85 | + | s.write(p) | |||
| 86 | + | } | |||
| 87 | + | ||||
| 88 | + | func (sc *script) extByMajor(major uint8) string { | |||
| 89 | + | for name, e := range sc.exts { | |||
| 90 | + | if e.major == major { | |||
| 91 | + | return name | |||
| 92 | + | } | |||
| 93 | + | } | |||
| 94 | + | return "" | |||
| 95 | + | } | |||
| 96 | + | ||||
| 97 | + | // serve reads requests until EOF and answers them. | |||
| 98 | + | func (s *scriptConn) serve() { | |||
| 99 | + | sc := s.sc | |||
| 100 | + | for { | |||
| 101 | + | var hdr [4]byte | |||
| 102 | + | if _, err := io.ReadFull(s.f, hdr[:]); err != nil { | |||
| 103 | + | return | |||
| 104 | + | } | |||
| 105 | + | raw := hdr[:] | |||
| 106 | + | n := int(binary.LittleEndian.Uint16(hdr[2:])) * 4 | |||
| 107 | + | if n == 0 { // BIG-REQUESTS header | |||
| 108 | + | var l [4]byte | |||
| 109 | + | if _, err := io.ReadFull(s.f, l[:]); err != nil { | |||
| 110 | + | return | |||
| 111 | + | } | |||
| 112 | + | raw = append(raw, l[:]...) | |||
| 113 | + | n = int(binary.LittleEndian.Uint32(l[:])) * 4 | |||
| 114 | + | } | |||
| 115 | + | rest := make([]byte, n-len(raw)) | |||
| 116 | + | if _, err := io.ReadFull(s.f, rest); err != nil { | |||
| 117 | + | return | |||
| 118 | + | } | |||
| 119 | + | raw = append(append([]byte(nil), raw...), rest...) | |||
| 120 | + | body := raw[len(raw)-len(rest):] | |||
| 121 | + | s.seq++ | |||
| 122 | + | major, minor := hdr[0], hdr[1] | |||
| 123 | + | s.mu.Lock() | |||
| 124 | + | s.reqs = append(s.reqs, scriptReq{s.seq, major, minor, raw}) | |||
| 125 | + | s.mu.Unlock() | |||
| 126 | + | if sc.silent.Load() || sc.hook != nil && sc.hook(s, major, minor, s.seq, body) { | |||
| 127 | + | continue | |||
| 128 | + | } | |||
| 129 | + | s.answer(major, minor, body) | |||
| 130 | + | } | |||
| 131 | + | } | |||
| 132 | + | ||||
| 133 | + | // answer runs the default behaviour for one request, holding s.mu so tests | |||
| 134 | + | // can read sc (atom) concurrently. | |||
| 135 | + | func (s *scriptConn) answer(major, minor uint8, body []byte) { | |||
| 136 | + | sc := s.sc | |||
| 137 | + | s.mu.Lock() | |||
| 138 | + | defer s.mu.Unlock() | |||
| 139 | + | switch { | |||
| 140 | + | case major == opQueryExtension: | |||
| 141 | + | e, ok := sc.exts[string(body[4:4+binary.LittleEndian.Uint16(body)])] | |||
| 142 | + | var present uint8 | |||
| 143 | + | if ok { | |||
| 144 | + | present = 1 | |||
| 145 | + | } | |||
| 146 | + | s.reply(0, []byte{present, e.major, e.firstEvent, e.firstError}) | |||
| 147 | + | case major == opInternAtom: | |||
| 148 | + | name := string(body[4 : 4+binary.LittleEndian.Uint16(body)]) | |||
| 149 | + | if sc.badAtoms[name] { | |||
| 150 | + | s.xerror(11, opInternAtom, 0) // BadAlloc | |||
| 151 | + | return | |||
| 152 | + | } | |||
| 153 | + | a, ok := sc.atoms[name] | |||
| 154 | + | if !ok && minor == 0 { | |||
| 155 | + | if sc.atoms == nil { | |||
| 156 | + | sc.atoms = map[string]uint32{} | |||
| 157 | + | } | |||
| 158 | + | sc.nextAtom++ | |||
| 159 | + | a = sc.nextAtom + 1000 | |||
| 160 | + | sc.atoms[name] = a | |||
| 161 | + | } | |||
| 162 | + | s.reply(0, binary.LittleEndian.AppendUint32(nil, a)) | |||
| 163 | + | case major == opGetInputFocus: | |||
| 164 | + | s.reply(1, binary.LittleEndian.AppendUint32(nil, 0x1234)) | |||
| 165 | + | case major >= 128 && sc.extByMajor(major) == "BIG-REQUESTS" && minor == 0: | |||
| 166 | + | s.reply(0, binary.LittleEndian.AppendUint32(nil, sc.bigreqMax)) | |||
| 167 | + | case major >= 128 && sc.extByMajor(major) == "XC-MISC" && minor == minorGetXIDRange: | |||
| 168 | + | r := [2]uint32{0, 1} | |||
| 169 | + | if len(sc.xidRanges) > 0 { | |||
| 170 | + | r, sc.xidRanges = sc.xidRanges[0], sc.xidRanges[1:] | |||
| 171 | + | } | |||
| 172 | + | s.reply(0, binary.LittleEndian.AppendUint32(binary.LittleEndian.AppendUint32(nil, r[0]), r[1])) | |||
| 173 | + | } | |||
| 174 | + | } | |||
| 175 | + | ||||
| 176 | + | // fullExts is what a typical Xvfb advertises, at its usual opcodes. | |||
| 177 | + | func fullExts() map[string]scriptExt { | |||
| 178 | + | return map[string]scriptExt{ | |||
| 179 | + | "BIG-REQUESTS": {133, 0, 0}, | |||
| 180 | + | "Generic Event Extension": {128, 0, 0}, | |||
| 181 | + | "XC-MISC": {136, 0, 0}, | |||
| 182 | + | "Composite": {142, 0, 0}, | |||
| 183 | + | "XFIXES": {138, 87, 140}, | |||
| 184 | + | "X-Resource": {144, 0, 0}, | |||
| 185 | + | "RENDER": {139, 0, 141}, | |||
| 186 | + | "SHAPE": {129, 64, 0}, | |||
| 187 | + | } | |||
| 188 | + | } | |||
| 189 | + | ||||
| 190 | + | // startScript serves sc behind a socketpair after answering the setup with | |||
| 191 | + | // the recorded Success reply, and returns the client fd (not yet set up) and | |||
| 192 | + | // the server connection. | |||
| 193 | + | func startScript(t *testing.T, sc *script) (int, *scriptConn) { | |||
| 194 | + | t.Helper() | |||
| 195 | + | fd, srv, _ := serveSetup(t, loadSetup(t, "Success"), 0, false, sc) | |||
| 196 | + | return fd, srv | |||
| 197 | + | } | |||
| 198 | + | ||||
| 199 | + | // dialScript runs NewConn against sc. | |||
| 200 | + | func dialScript(t *testing.T, sc *script) (*Conn, *scriptConn) { | |||
| 201 | + | t.Helper() | |||
| 202 | + | fd, srv := startScript(t, sc) | |||
| 203 | + | c, err := NewConn(fd) | |||
| 204 | + | if err != nil { | |||
| 205 | + | t.Fatal(err) | |||
| 206 | + | } | |||
| 207 | + | t.Cleanup(func() { _ = c.Close() }) | |||
| 208 | + | return c, srv | |||
| 209 | + | } | |||
| 210 | + | ||||
| 211 | + | // atom returns the atom the server assigned to name. | |||
| 212 | + | func (s *scriptConn) atom(name string) uint32 { | |||
| 213 | + | s.mu.Lock() | |||
| 214 | + | defer s.mu.Unlock() | |||
| 215 | + | return s.sc.atoms[name] | |||
| 216 | + | } | |||
Comments0 open / 0
no comments yet.