detached ad1316e

#6 feat(runtime): bootstrap extensions, Sync, InternAtoms and XID allocation

state
merged · ad1316e · fast-forward
branch
feat/runtime-bootstrap → main
opened
bnema ·
updated
CommitSubjectAuthorDateSignatureStatus
ad1316efix(runtime): bound blocking calls, validate XID ranges and prefetch XIDs without blockingbriceGPG Verified
57cbbe3feat(runtime): bootstrap extensions at connect with Sync, InternAtoms, NewXID and auto-resyncbriceGPG Verified

Checks 4

15 files · +2087 -61
4/4
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 @@
11 package xturbo
22
33 import (
4+ "fmt"
45 "os"
6+ "time"
57
68 "golang.org/x/sys/unix"
79 )
@@ -15,7 +17,23 @@ type Conn struct {
1517 setup []byte
1618 root uint32
1719
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
1937
2038 // Read side, owned by Dispatch: rb[rpos:rend] is received, unconsumed.
2139 rb []byte
@@ -38,8 +56,12 @@ type Conn struct {
3856
3957 // NewConn takes ownership of fd, a connected stream socket to an X server,
4058 // 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.
4365 func NewConn(fd int) (*Conn, error) {
4466 return newConn(fd, "", nil)
4567 }
@@ -54,17 +76,19 @@ func newConn(fd int, authName string, authData []byte) (*Conn, error) {
5476 }
5577
5678 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 {
5881 return nil, os.NewSyscallError("fcntl", err)
5982 }
60- setup, info, err := handshake(fd, authName, authData)
83+ setup, info, err := handshake(fd, authName, authData, deadline)
6184 if err != nil {
6285 return nil, err
6386 }
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)
6690 }
67- return newConnState(fd, setup, info), nil
91+ return c, nil
6892 }
6993
7094 // newConnState builds a Conn over an fd whose setup already completed.
@@ -78,8 +102,10 @@ func newConnState(fd int, setup []byte, info setupInfo) *Conn {
78102 xidMask: info.xidMask,
79103 rb: make([]byte, readBufSize),
80104 maxPacket: MaxPacket,
105+ timeout: syncTimeout,
81106 }
82107 c.buf.Ext = &c.ext
108+ c.initXIDs()
83109 return c
84110 }
85111
@@ -114,12 +140,35 @@ func (c *Conn) SetupBytes() []byte { return c.setup }
114140 // Root returns the root window of the first screen.
115141 func (c *Conn) Root() uint32 { return c.root }
116142
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+
117148 // Flush writes queued requests without blocking. It returns ErrWouldBlock when
118149 // 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.
120153 func (c *Conn) Flush() error {
121154 if c.fd < 0 {
122155 return ErrClosed
123156 }
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
125174 }
@@ -1,7 +1,9 @@
1 package xturbo1 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 []byte17 setup []byte
16 root uint3218 root uint32
17 19
18- xidBase, xidMask uint32 // resource-id allocation, used by NewXID20+ 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 []byte39 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 an58 // and runs the connection setup without authorization (e.g. the -wm fd of an
41-// XWayland the caller spawned). The handshake blocks; afterwards fd is59+// 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, err85 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), nil91+ 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.ext107 c.buf.Ext = &c.ext
108+ c.initXIDs()
83 return c109 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 when148 // Flush writes queued requests without blocking. It returns ErrWouldBlock when
118 // the socket is full (arm writable interest and call again) and the sticky149 // 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 ErrClosed155 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 {
2424
2525 // scriptedServer runs the server side of a socketpair: it reads one setup
2626 // 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{}).
2829 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) {
2937 t.Helper()
3038 fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
3139 if err != nil {
3240 t.Fatal(err)
3341 }
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 {
3644 t.Fatal(err)
3745 }
3846 server := os.NewFile(uintptr(fds[1]), "server")
47+ srv := &scriptConn{f: server, sc: sc}
3948 ch := make(chan []byte, 1)
4049 done := make(chan struct{})
4150 t.Cleanup(func() { <-done; server.Close() })
@@ -65,9 +74,11 @@ func scriptedServer(t *testing.T, reply []byte, chunk int, hangup bool) (client
6574 }
6675 if hangup {
6776 server.Close()
77+ return
6878 }
79+ srv.serve()
6980 }()
70- return fds[0], ch
81+ return fds[0], srv, ch
7182 }
7283
7384 func TestNewConnSuccess(t *testing.T) {
@@ -91,8 +102,9 @@ func TestNewConnSuccess(t *testing.T) {
91102 if c.Root() != 0x39f || c.xidBase != 0x00200000 || c.xidMask != 0x001fffff {
92103 t.Fatalf("root %#x base %#x mask %#x", c.Root(), c.xidBase, c.xidMask)
93104 }
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())
96108 }
97109 if c.Ext().Major(ExtXFixes) != 0 || !c.Ext().Info(ExtCore).Present {
98110 t.Fatal("ExtTable not empty")
@@ -219,32 +231,18 @@ func truncateSetup(setup []byte, n int) []byte {
219231 }
220232
221233 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()
238236 p, seq := c.Buf().Request(43, 0, 0) // GetInputFocus
239- if p == nil || seq != 1 {
237+ if p == nil || seq != first {
240238 t.Fatal("request")
241239 }
242240 if err := c.Flush(); err != nil {
243241 t.Fatal(err)
244242 }
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)
248246 }
249247 if err := c.Close(); err != nil {
250248 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 setup25 // scriptedServer runs the server side of a socketpair: it reads one setup
26 // request, reports it on the returned channel, writes reply in chunks of26 // 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], ch81+ 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) // GetInputFocus236 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]byte243+ 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) {
125125 const kindFocus, kindGeom = 16, 17
126126 seq := xproto.GetInputFocus(c.Buf())
127127 bad := xproto.GetGeometry(c.Buf(), xproto.Drawable(xproto.WindowNone))
128- if seq != 1 || bad != 2 {
128+ if seq == 0 || bad != seq+1 {
129129 t.Fatalf("seqs %d %d", seq, bad)
130130 }
131131 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, 17125 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
1717 // ErrPacketTooLarge reports a server packet longer than MaxPacket.
1818 var ErrPacketTooLarge = errors.New("xturbo: packet too large")
1919
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
2122 // callback. It is unexported: it reports a programming error, not a condition
2223 // callers handle. The outer Dispatch is unaffected.
2324 var errReentrantDispatch = errors.New("xturbo: Dispatch called from a Handler callback")
@@ -34,8 +35,8 @@ const (
3435 // once at init; the pointers passed to callbacks are owned by the Conn and are
3536 // reused for the next packet, so callbacks must not retain them, nor the
3637 // 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.
3940 type Handler struct {
4041 OnEvent func(ev *Event)
4142 OnReply func(r *Reply)
@@ -61,7 +62,10 @@ var noHandler Handler
6162 // still readable, and Flush requests queued by callbacks in between. It
6263 // returns io.EOF when the server closed the connection and ErrPacketTooLarge
6364 // 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.
6569 func (c *Conn) Dispatch(h *Handler) error {
6670 if c.dispatching {
6771 return errReentrantDispatch
@@ -76,8 +80,12 @@ func (c *Conn) Dispatch(h *Handler) error {
7680 }
7781 if h == nil {
7882 h = &noHandler
83+ } else {
84+ c.h = h
7985 }
8086 budget := len(c.rb)
87+ c.rdIdle = false
88+ read := false
8189 for {
8290 if err := c.drain(h); err != nil {
8391 return c.failRead(err)
@@ -93,6 +101,7 @@ func (c *Conn) Dispatch(h *Handler) error {
93101 case err == unix.EINTR:
94102 continue
95103 case err == unix.EAGAIN:
104+ c.rdIdle = !read
96105 return nil
97106 case err == unix.ECONNRESET:
98107 return c.failRead(io.EOF)
@@ -103,6 +112,7 @@ func (c *Conn) Dispatch(h *Handler) error {
103112 }
104113 c.rend += n
105114 budget -= n
115+ read = true
106116 }
107117 }
108118
@@ -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 Handler20+// 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 condition22 // 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 are35 // 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 the36 // 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 and37 // 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 error38+// 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. It62 // still readable, and Flush requests queued by callbacks in between. It
62 // returns io.EOF when the server closed the connection and ErrPacketTooLarge63 // 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 errReentrantDispatch71 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 = &noHandler82 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 continue102 continue
95 case err == unix.EAGAIN:103 case err == unix.EAGAIN:
104+ c.rdIdle = !read
96 return nil105 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 += n113 c.rend += n
105 budget -= n114 budget -= n
115+ read = true
106 }116 }
107 }117 }
108 118
M export_test.go +31 -0
@@ -1,4 +1,35 @@
11 package xturbo
22
3+import "time"
4+
35 // SetMaxReqWords exposes the Conn-owned BIG-REQUESTS limit setter to external tests.
46 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 xturbo1 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).setMaxReqWords6 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 @@
11 package xturbo
22
3-import "errors"
3+import (
4+ "encoding/binary"
5+ "errors"
6+)
47
58 // ErrRingFull reports more than ringSize registered requests outstanding:
69 // 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) {
119122 return kind, args, true
120123 }
121124
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-
129125 // 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 xturbo1 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 not9 // 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, true122 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. kindDiscard125 // 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 consumers127+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 (
55 "fmt"
66 "io"
77 "os"
8+ "time"
89
910 "golang.org/x/sys/unix"
1011 )
@@ -46,22 +47,24 @@ func encodeSetupRequest(authName string, authData []byte) []byte {
4647
4748 func pad4(n int) int { return (n + 3) &^ 3 }
4849
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) {
5255 if len(authName) > 0xffff || len(authData) > 0xffff {
5356 return nil, setupInfo{}, ErrBadLength
5457 }
55- if err := writeFull(fd, encodeSetupRequest(authName, authData)); err != nil {
58+ if err := writeFull(fd, encodeSetupRequest(authName, authData), deadline); err != nil {
5659 return nil, setupInfo{}, err
5760 }
5861 var hdr [setupHeaderSize]byte
59- if err := readFull(fd, hdr[:]); err != nil {
62+ if err := readFull(fd, hdr[:], deadline); err != nil {
6063 return nil, setupInfo{}, err
6164 }
6265 reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:])))
6366 copy(reply, hdr[:])
64- if err := readFull(fd, reply[setupHeaderSize:]); err != nil {
67+ if err := readFull(fd, reply[setupHeaderSize:], deadline); err != nil {
6568 return nil, setupInfo{}, err
6669 }
6770 info, err := parseSetup(reply)
@@ -141,12 +144,26 @@ func parseSetup(p []byte) (setupInfo, error) {
141144 return info, nil
142145 }
143146
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 {
145157 for len(p) > 0 {
146158 n, err := unix.Write(fd, p)
147159 switch {
148160 case err == unix.EINTR:
149161 continue
162+ case err == unix.EAGAIN:
163+ if err := waitFd(fd, unix.POLLOUT, deadline); err != nil {
164+ return err
165+ }
166+ continue
150167 case err == unix.EPIPE || err == unix.ECONNRESET:
151168 return ErrClosed
152169 case err != nil:
@@ -159,12 +176,17 @@ func writeFull(fd int, p []byte) error {
159176 return nil
160177 }
161178
162-func readFull(fd int, p []byte) error {
179+func readFull(fd int, p []byte, deadline time.Time) error {
163180 for len(p) > 0 {
164181 n, err := unix.Read(fd, p)
165182 switch {
166183 case err == unix.EINTR:
167184 continue
185+ case err == unix.EAGAIN:
186+ if err := waitFd(fd, unix.POLLIN, deadline); err != nil {
187+ return err
188+ }
189+ continue
168190 case err == unix.ECONNRESET:
169191 return ErrClosed
170192 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 setup50+// 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{}, ErrBadLength56 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{}, err59 return nil, setupInfo{}, err
57 }60 }
58 var hdr [setupHeaderSize]byte61 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{}, err63 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{}, err68 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, nil144 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 continue161 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 ErrClosed168 return ErrClosed
152 case err != nil:169 case err != nil:
@@ -159,12 +176,17 @@ func writeFull(fd int, p []byte) error {
159 return nil176 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 continue184 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 ErrClosed191 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.

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
l
merge request: the list of files (j k move, enter opens, esc back)
M
merge request: the merge menu; there 1–3 pick the mode, D deletes the branch, enter merges
R
merge request: mark the head read
esc
close a dialog, leave a field

Branches 1

Tags 1

↑↓ moveenter openesc close