detached 57cbbe3

feat(runtime): bootstrap extensions at connect with Sync, InternAtoms, NewXID and auto-resync

NewConn pipelines QueryExtension for every ExtID and BIG-REQUESTS Enable
(queued when its QueryExtension reply arrives) in one blocking Sync, fills
the ExtTable and raises the Buf request limit.

- Sync(h): blocking fence (GetInputFocus) + flush + bounded Dispatch steps
  until the fence is answered and no Expect slot is live; poll for
  writability on ErrWouldBlock; user packets still reach h; refused from a
  Handler callback.
- InternAtoms: pipelined, one round trip, atoms written by index from the
  reserved-kind slot args; first X error returned as *XError.
- NewXID/FreeXID: setup range, fixed 256-entry free list, XC-MISC
  GetXIDRange refill in one blocking round trip; error when exhausted.
- Flush appends a discarded GetInputFocus when Next-acked >= 0x8000, one in
  flight, counted in Stats.Resyncs.
- Errors on reserved-kind requests go through consumeError, never OnError.
- Root encodes its own QueryExtension, InternAtom, GetInputFocus, bigreq
  Enable and XC-MISC GetXIDRange; tests compare them with the generated
  encoders and the InternAtom golden.

Tests: scripted X server over a socketpair (extensions present/absent,
BIG-REQUESTS limit, InternAtoms order/errors/ring wrap, Sync with
interleaved events and chained callbacks, XID refill and exhaustion,
auto-resync), alloc tests for Flush with resync and NewXID/FreeXID, and
Xvfb integration under the xserver tag.
commit
57cbbe37f975e61910075a789a7f684f4a25fb0b
author
brice · · 2026-10-22 09:19 UTC
committer
brice · · 2026-10-22 09:43 UTC
parent
1aebbe1
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
13 files · +1502 -45
A atoms.go +44 -0
@@ -0,0 +1,44 @@
1+package xturbo
2+
3+// InternAtoms interns every name in one pipelined batch and blocks for one
4+// round trip (Sync), writing the atom of names[i] into dst[i] (0 for a name
5+// that does not exist when onlyIfExists is set). len(dst) must equal
6+// len(names) (ErrBadLength). The first X error on one of these requests is
7+// returned as *XError, its Args[0] the index of the name; the other atoms
8+// are still filled. Packets read meanwhile go to the last Handler given to
9+// Dispatch. Meant for startup; it must not be called from a Handler callback.
10+func (c *Conn) InternAtoms(names []string, onlyIfExists bool, dst []uint32) error {
11+ if len(names) != len(dst) {
12+ return ErrBadLength
13+ }
14+ if c.dispatching {
15+ return errReentrantDispatch
16+ }
17+ if c.fd < 0 {
18+ return ErrClosed
19+ }
20+ clear(dst)
21+ c.atoms = dst
22+ defer func() { c.atoms = nil }()
23+ c.syncFailed = false
24+ for i, name := range names {
25+ if err := c.waitSlot(); err != nil {
26+ return err
27+ }
28+ seq := encodeInternAtom(c.buf, onlyIfExists, name)
29+ if seq == 0 {
30+ return c.buf.Err()
31+ }
32+ if err := c.expect(seq, kindInternAtom, [4]uint32{uint32(i)}); err != nil {
33+ return err
34+ }
35+ }
36+ if err := c.wait(c.h); err != nil {
37+ return err
38+ }
39+ if c.syncFailed {
40+ e := c.syncErr
41+ return &e
42+ }
43+ return nil
44+}
@@ -0,0 +1,44 @@
1+package xturbo
2+
3+// InternAtoms interns every name in one pipelined batch and blocks for one
4+// round trip (Sync), writing the atom of names[i] into dst[i] (0 for a name
5+// that does not exist when onlyIfExists is set). len(dst) must equal
6+// len(names) (ErrBadLength). The first X error on one of these requests is
7+// returned as *XError, its Args[0] the index of the name; the other atoms
8+// are still filled. Packets read meanwhile go to the last Handler given to
9+// Dispatch. Meant for startup; it must not be called from a Handler callback.
10+func (c *Conn) InternAtoms(names []string, onlyIfExists bool, dst []uint32) error {
11+ if len(names) != len(dst) {
12+ return ErrBadLength
13+ }
14+ if c.dispatching {
15+ return errReentrantDispatch
16+ }
17+ if c.fd < 0 {
18+ return ErrClosed
19+ }
20+ clear(dst)
21+ c.atoms = dst
22+ defer func() { c.atoms = nil }()
23+ c.syncFailed = false
24+ for i, name := range names {
25+ if err := c.waitSlot(); err != nil {
26+ return err
27+ }
28+ seq := encodeInternAtom(c.buf, onlyIfExists, name)
29+ if seq == 0 {
30+ return c.buf.Err()
31+ }
32+ if err := c.expect(seq, kindInternAtom, [4]uint32{uint32(i)}); err != nil {
33+ return err
34+ }
35+ }
36+ if err := c.wait(c.h); err != nil {
37+ return err
38+ }
39+ if c.syncFailed {
40+ e := c.syncErr
41+ return &e
42+ }
43+ return nil
44+}
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.xidLeft, c.xidFreeN = 0, 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.xidLeft = 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.xidLeft = 0
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.xidLeft = 0
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.xidLeft, c.xidFreeN = 0, 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.xidLeft = 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.xidLeft = 0
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.xidLeft = 0
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 +236 -0
@@ -0,0 +1,236 @@
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+}
211+
212+// A long silent stream gets fences; the connection keeps working.
213+func TestXserverAutoResync(t *testing.T) {
214+ c := dialXvfb(t)
215+ b := c.Buf()
216+ for range 3 * 0x8000 {
217+ xproto.MapWindow(b, xproto.Window(c.Root())) // no reply, no event
218+ if b.Len() > 32<<10 {
219+ if err := c.Flush(); err != nil && !errors.Is(err, xturbo.ErrWouldBlock) {
220+ t.Fatal(err)
221+ }
222+ if err := c.Dispatch(nil); err != nil {
223+ t.Fatal(err)
224+ }
225+ }
226+ }
227+ if b.Err() != nil {
228+ t.Fatal(b.Err())
229+ }
230+ if err := c.Sync(nil); err != nil {
231+ t.Fatal(err)
232+ }
233+ if st := c.Stats(); st.Resyncs == 0 || st.DroppedReplies != 0 {
234+ t.Fatalf("%+v", st)
235+ }
236+}
@@ -0,0 +1,236 @@
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+}
211+
212+// A long silent stream gets fences; the connection keeps working.
213+func TestXserverAutoResync(t *testing.T) {
214+ c := dialXvfb(t)
215+ b := c.Buf()
216+ for range 3 * 0x8000 {
217+ xproto.MapWindow(b, xproto.Window(c.Root())) // no reply, no event
218+ if b.Len() > 32<<10 {
219+ if err := c.Flush(); err != nil && !errors.Is(err, xturbo.ErrWouldBlock) {
220+ t.Fatal(err)
221+ }
222+ if err := c.Dispatch(nil); err != nil {
223+ t.Fatal(err)
224+ }
225+ }
226+ }
227+ if b.Err() != nil {
228+ t.Fatal(b.Err())
229+ }
230+ if err := c.Sync(nil); err != nil {
231+ t.Fatal(err)
232+ }
233+ if st := c.Stats(); st.Resyncs == 0 || st.DroppedReplies != 0 {
234+ t.Fatalf("%+v", st)
235+ }
236+}
M conn.go +48 -4
@@ -16,6 +16,20 @@ type Conn struct {
1616 root uint32
1717
1818 xidBase, xidMask uint32 // resource-id allocation, used by NewXID
19+ xidNext, xidLeft uint32 // current range: next id, ids left in it
20+ xidFree [xidFreeCap]uint32
21+ xidFreeN int
22+ // Last XC-MISC GetXIDRange reply, filled by consumeReply.
23+ rangeNext, rangeLeft uint32
24+ rangeGot bool
25+
26+ // Blocking calls (Sync, InternAtoms, NewXID refill).
27+ h *Handler // last non-nil Handler given to Dispatch or Sync
28+ rdIdle bool // the last Dispatch read nothing: the socket was empty
29+ atoms []uint32 // InternAtoms destination, indexed by slot Args[0]
30+ syncErr XError // first error on a reserved-kind request
31+ syncFailed bool // syncErr is set
32+ resync Seq // last GetInputFocus appended by Flush
1933
2034 // Read side, owned by Dispatch: rb[rpos:rend] is received, unconsumed.
2135 rb []byte
@@ -39,7 +53,9 @@ type Conn struct {
3953 // NewConn takes ownership of fd, a connected stream socket to an X server,
4054 // and runs the connection setup without authorization (e.g. the -wm fd of an
4155 // XWayland the caller spawned). The handshake blocks; afterwards fd is
42-// switched to O_NONBLOCK. On failure fd is closed.
56+// switched to O_NONBLOCK and one blocking round trip negotiates every known
57+// extension (QueryExtension each, BIG-REQUESTS Enable when present) into
58+// Ext() and the Buf request limit. On failure fd is closed.
4359 func NewConn(fd int) (*Conn, error) {
4460 return newConn(fd, "", nil)
4561 }
@@ -64,7 +80,11 @@ func setupConn(fd int, authName string, authData []byte) (*Conn, error) {
6480 if err := unix.SetNonblock(fd, true); err != nil {
6581 return nil, os.NewSyscallError("fcntl", err)
6682 }
67- return newConnState(fd, setup, info), nil
83+ c := newConnState(fd, setup, info)
84+ if err := c.bootstrap(); err != nil {
85+ return nil, err
86+ }
87+ return c, nil
6888 }
6989
7090 // newConnState builds a Conn over an fd whose setup already completed.
@@ -80,6 +100,7 @@ func newConnState(fd int, setup []byte, info setupInfo) *Conn {
80100 maxPacket: MaxPacket,
81101 }
82102 c.buf.Ext = &c.ext
103+ c.initXIDs()
83104 return c
84105 }
85106
@@ -114,12 +135,35 @@ func (c *Conn) SetupBytes() []byte { return c.setup }
114135 // Root returns the root window of the first screen.
115136 func (c *Conn) Root() uint32 { return c.root }
116137
138+// resyncAt is the number of unacknowledged requests after which Flush appends
139+// a GetInputFocus, so a long stream of requests without replies or events
140+// still brings back a sequence number before the 16-bit wire value wraps.
141+const resyncAt = 0x8000
142+
117143 // Flush writes queued requests without blocking. It returns ErrWouldBlock when
118144 // the socket is full (arm writable interest and call again) and the sticky
119-// Buf error once the connection is unusable.
145+// Buf error once the connection is unusable. With resyncAt or more requests
146+// unacknowledged it first appends a GetInputFocus whose reply is discarded
147+// (Stats.Resyncs), at most one in flight.
120148 func (c *Conn) Flush() error {
121149 if c.fd < 0 {
122150 return ErrClosed
123151 }
124- return c.buf.flush(c.fd)
152+ b := c.buf
153+ if b.err == nil && b.next-b.acked >= resyncAt && c.resync <= b.acked {
154+ if seq := c.fence(); seq != 0 {
155+ c.resync = seq
156+ c.stats.Resyncs++
157+ }
158+ }
159+ return b.flush(c.fd)
160+}
161+
162+// fence queues a GetInputFocus whose reply is consumed and dropped. Its
163+// reply proves every earlier request complete.
164+func (c *Conn) fence() Seq {
165+ seq := encodeGetInputFocus(c.buf)
166+ // On ErrRingFull the reply is merely counted as dropped.
167+ _ = c.expect(seq, kindDiscard, [4]uint32{})
168+ return seq
125169 }
@@ -16,6 +16,20 @@ type Conn struct {
16 root uint3216 root uint32
17 17
18 xidBase, xidMask uint32 // resource-id allocation, used by NewXID18 xidBase, xidMask uint32 // resource-id allocation, used by NewXID
19+ xidNext, xidLeft uint32 // current range: next id, ids left in it
20+ xidFree [xidFreeCap]uint32
21+ xidFreeN int
22+ // Last XC-MISC GetXIDRange reply, filled by consumeReply.
23+ rangeNext, rangeLeft uint32
24+ rangeGot bool
25+
26+ // Blocking calls (Sync, InternAtoms, NewXID refill).
27+ h *Handler // last non-nil Handler given to Dispatch or Sync
28+ rdIdle bool // the last Dispatch read nothing: the socket was empty
29+ atoms []uint32 // InternAtoms destination, indexed by slot Args[0]
30+ syncErr XError // first error on a reserved-kind request
31+ syncFailed bool // syncErr is set
32+ resync Seq // last GetInputFocus appended by Flush
19 33
20 // Read side, owned by Dispatch: rb[rpos:rend] is received, unconsumed.34 // Read side, owned by Dispatch: rb[rpos:rend] is received, unconsumed.
21 rb []byte35 rb []byte
@@ -39,7 +53,9 @@ type Conn struct {
39 // NewConn takes ownership of fd, a connected stream socket to an X server,53 // 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 an54 // and runs the connection setup without authorization (e.g. the -wm fd of an
41 // XWayland the caller spawned). The handshake blocks; afterwards fd is55 // XWayland the caller spawned). The handshake blocks; afterwards fd is
42-// switched to O_NONBLOCK. On failure fd is closed.56+// switched to O_NONBLOCK and one blocking round trip negotiates every known
57+// extension (QueryExtension each, BIG-REQUESTS Enable when present) into
58+// Ext() and the Buf request limit. On failure fd is closed.
43 func NewConn(fd int) (*Conn, error) {59 func NewConn(fd int) (*Conn, error) {
44 return newConn(fd, "", nil)60 return newConn(fd, "", nil)
45 }61 }
@@ -64,7 +80,11 @@ func setupConn(fd int, authName string, authData []byte) (*Conn, error) {
64 if err := unix.SetNonblock(fd, true); err != nil {80 if err := unix.SetNonblock(fd, true); err != nil {
65 return nil, os.NewSyscallError("fcntl", err)81 return nil, os.NewSyscallError("fcntl", err)
66 }82 }
67- return newConnState(fd, setup, info), nil83+ c := newConnState(fd, setup, info)
84+ if err := c.bootstrap(); err != nil {
85+ return nil, err
86+ }
87+ return c, nil
68 }88 }
69 89
70 // newConnState builds a Conn over an fd whose setup already completed.90 // newConnState builds a Conn over an fd whose setup already completed.
@@ -80,6 +100,7 @@ func newConnState(fd int, setup []byte, info setupInfo) *Conn {
80 maxPacket: MaxPacket,100 maxPacket: MaxPacket,
81 }101 }
82 c.buf.Ext = &c.ext102 c.buf.Ext = &c.ext
103+ c.initXIDs()
83 return c104 return c
84 }105 }
85 106
@@ -114,12 +135,35 @@ func (c *Conn) SetupBytes() []byte { return c.setup }
114 // Root returns the root window of the first screen.135 // Root returns the root window of the first screen.
115 func (c *Conn) Root() uint32 { return c.root }136 func (c *Conn) Root() uint32 { return c.root }
116 137
138+// resyncAt is the number of unacknowledged requests after which Flush appends
139+// a GetInputFocus, so a long stream of requests without replies or events
140+// still brings back a sequence number before the 16-bit wire value wraps.
141+const resyncAt = 0x8000
142+
117 // Flush writes queued requests without blocking. It returns ErrWouldBlock when143 // Flush writes queued requests without blocking. It returns ErrWouldBlock when
118 // the socket is full (arm writable interest and call again) and the sticky144 // the socket is full (arm writable interest and call again) and the sticky
119-// Buf error once the connection is unusable.145+// Buf error once the connection is unusable. With resyncAt or more requests
146+// unacknowledged it first appends a GetInputFocus whose reply is discarded
147+// (Stats.Resyncs), at most one in flight.
120 func (c *Conn) Flush() error {148 func (c *Conn) Flush() error {
121 if c.fd < 0 {149 if c.fd < 0 {
122 return ErrClosed150 return ErrClosed
123 }151 }
124- return c.buf.flush(c.fd)152+ b := c.buf
153+ if b.err == nil && b.next-b.acked >= resyncAt && c.resync <= b.acked {
154+ if seq := c.fence(); seq != 0 {
155+ c.resync = seq
156+ c.stats.Resyncs++
157+ }
158+ }
159+ return b.flush(c.fd)
160+}
161+
162+// fence queues a GetInputFocus whose reply is consumed and dropped. Its
163+// reply proves every earlier request complete.
164+func (c *Conn) fence() Seq {
165+ seq := encodeGetInputFocus(c.buf)
166+ // On ErrRingFull the reply is merely counted as dropped.
167+ _ = c.expect(seq, kindDiscard, [4]uint32{})
168+ return seq
125 }169 }
M conn_test.go +22 -24
@@ -24,8 +24,16 @@ 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 {
@@ -36,6 +44,7 @@ func scriptedServer(t *testing.T, reply []byte, chunk int, hangup bool) (client
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,8 +24,16 @@ 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 {
@@ -36,6 +44,7 @@ func scriptedServer(t *testing.T, reply []byte, chunk int, hangup bool) (client
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 +14 -0
@@ -2,3 +2,17 @@ package xturbo
22
33 // SetMaxReqWords exposes the Conn-owned BIG-REQUESTS limit setter to external tests.
44 var SetMaxReqWords = (*Buf).setMaxReqWords
5+
6+// ExhaustXIDs empties the setup range and the free list so the next NewXID
7+// refills through XC-MISC.
8+func ExhaustXIDs(c *Conn) { c.xidLeft, c.xidFreeN = 0, 0 }
9+
10+// Root's own encoders of the requests it issues, compared with the generated
11+// ones and the goldens by external tests.
12+var (
13+ EncodeQueryExtension = encodeQueryExtension
14+ EncodeInternAtom = encodeInternAtom
15+ EncodeGetInputFocus = encodeGetInputFocus
16+ EncodeBigreqEnable = func(b *Buf) Seq { return encodeExtRequest(b, ExtBigRequests, 0) }
17+ EncodeGetXIDRange = func(b *Buf) Seq { return encodeExtRequest(b, ExtXCMisc, minorGetXIDRange) }
18+)
@@ -2,3 +2,17 @@ package xturbo
2 2
3 // SetMaxReqWords exposes the Conn-owned BIG-REQUESTS limit setter to external tests.3 // SetMaxReqWords exposes the Conn-owned BIG-REQUESTS limit setter to external tests.
4 var SetMaxReqWords = (*Buf).setMaxReqWords4 var SetMaxReqWords = (*Buf).setMaxReqWords
5+
6+// ExhaustXIDs empties the setup range and the free list so the next NewXID
7+// refills through XC-MISC.
8+func ExhaustXIDs(c *Conn) { c.xidLeft, c.xidFreeN = 0, 0 }
9+
10+// Root's own encoders of the requests it issues, compared with the generated
11+// ones and the goldens by external tests.
12+var (
13+ EncodeQueryExtension = encodeQueryExtension
14+ EncodeInternAtom = encodeInternAtom
15+ EncodeGetInputFocus = encodeGetInputFocus
16+ EncodeBigreqEnable = func(b *Buf) Seq { return encodeExtRequest(b, ExtBigRequests, 0) }
17+ EncodeGetXIDRange = func(b *Buf) Seq { return encodeExtRequest(b, ExtXCMisc, minorGetXIDRange) }
18+)
M ring.go +51 -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,51 @@ 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.rangeNext = binary.LittleEndian.Uint32(r.Raw[8:])
156+ c.rangeLeft = binary.LittleEndian.Uint32(r.Raw[12:])
157+ c.rangeGot = true
158+ }
159+}
160+
161+// consumeError handles an error whose request was registered with a reserved
162+// kind (1..15) and reports whether it consumed it; an unconsumed error is
163+// delivered to OnError. Every reserved kind is root's own request, so all are
164+// consumed: the first is kept for the blocking call (Sync, InternAtoms,
165+// NewXID) that issued the request, which returns it as *XError.
166+func (c *Conn) consumeError(e *XError) bool {
167+ if !c.syncFailed {
168+ c.syncFailed = true
169+ c.syncErr = *e
170+ }
171+ return true
172+}
@@ -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,51 @@ 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.rangeNext = binary.LittleEndian.Uint32(r.Raw[8:])
156+ c.rangeLeft = binary.LittleEndian.Uint32(r.Raw[12:])
157+ c.rangeGot = true
158+ }
159+}
160+
161+// consumeError handles an error whose request was registered with a reserved
162+// kind (1..15) and reports whether it consumed it; an unconsumed error is
163+// delivered to OnError. Every reserved kind is root's own request, so all are
164+// consumed: the first is kept for the blocking call (Sync, InternAtoms,
165+// NewXID) that issued the request, which returns it as *XError.
166+func (c *Conn) consumeError(e *XError) bool {
167+ if !c.syncFailed {
168+ c.syncFailed = true
169+ c.syncErr = *e
170+ }
171+ return true
172+}
A sync.go +158 -0
@@ -0,0 +1,158 @@
1+package xturbo
2+
3+import (
4+ "encoding/binary"
5+ "os"
6+
7+ "golang.org/x/sys/unix"
8+)
9+
10+// Core and extension requests root issues itself. Root does not import the
11+// generated packages, so these few are encoded here; tests compare them with
12+// the generated encoders and the request goldens.
13+const (
14+ opInternAtom = 16
15+ opGetInputFocus = 43
16+ opQueryExtension = 98
17+ minorGetXIDRange = 1 // XC-MISC
18+)
19+
20+func encodeGetInputFocus(b *Buf) Seq {
21+ _, seq := b.Request(opGetInputFocus, 0, 0)
22+ return seq
23+}
24+
25+// encodeNamed encodes QueryExtension and InternAtom: CARD16 name_len, 2
26+// pad bytes, then the name padded to 4.
27+func encodeNamed(b *Buf, major, data uint8, name string) Seq {
28+ if len(name) > 0xffff {
29+ b.Fail(ErrBadLength)
30+ return 0
31+ }
32+ p, seq := b.Request(major, data, pad4(4+len(name)))
33+ if p == nil {
34+ return 0
35+ }
36+ binary.LittleEndian.PutUint16(p, uint16(len(name)))
37+ copy(p[4:], name)
38+ return seq
39+}
40+
41+func encodeQueryExtension(b *Buf, name string) Seq {
42+ return encodeNamed(b, opQueryExtension, 0, name)
43+}
44+
45+func encodeInternAtom(b *Buf, onlyIfExists bool, name string) Seq {
46+ var data uint8
47+ if onlyIfExists {
48+ data = 1
49+ }
50+ return encodeNamed(b, opInternAtom, data, name)
51+}
52+
53+// encodeExtRequest encodes a body-less extension request (BIG-REQUESTS
54+// Enable, XC-MISC GetXIDRange). It fails with ErrExtMissing when id is absent.
55+func encodeExtRequest(b *Buf, id ExtID, minor uint8) Seq {
56+ major := b.Ext.Major(id)
57+ if major == 0 {
58+ b.Fail(ErrExtMissing)
59+ return 0
60+ }
61+ _, seq := b.Request(major, minor, 0)
62+ return seq
63+}
64+
65+// bootstrap negotiates every extension root knows (one pipelined
66+// QueryExtension each, then BIG-REQUESTS Enable when present) in one Sync.
67+func (c *Conn) bootstrap() error {
68+ for id := ExtCore + 1; id < extCount; id++ {
69+ seq := encodeQueryExtension(c.buf, ExtXName[id])
70+ if seq == 0 {
71+ return c.buf.Err()
72+ }
73+ if err := c.expect(seq, kindQueryExtension, [4]uint32{uint32(id)}); err != nil {
74+ return err
75+ }
76+ }
77+ // consumeReply queues bigreq Enable when BIG-REQUESTS is present; Sync
78+ // waits for it too.
79+ return c.Sync(nil)
80+}
81+
82+// Sync flushes every queued request and blocks until the server has
83+// processed them and every request registered with Expect has completed. It
84+// appends a GetInputFocus as a fence, so even with nothing registered it is a
85+// full round trip: errors of earlier requests have been delivered when it
86+// returns. Packets read meanwhile go to h as with Dispatch (nil h: the last
87+// Handler given to Dispatch, or none). An error on a request root issued
88+// itself is returned as *XError. Meant for startup; it must not be called
89+// from a Handler callback.
90+func (c *Conn) Sync(h *Handler) error {
91+ if c.dispatching {
92+ return errReentrantDispatch
93+ }
94+ if h == nil {
95+ h = c.h
96+ }
97+ c.syncFailed = false
98+ if err := c.wait(h); err != nil {
99+ return err
100+ }
101+ if c.syncFailed {
102+ e := c.syncErr
103+ return &e
104+ }
105+ return nil
106+}
107+
108+// wait fences, flushes and dispatches, one bounded Dispatch at a time, until
109+// the fence has been answered and no registered slot remains. A fence is
110+// re-queued whenever callbacks registered requests after the last one.
111+func (c *Conn) wait(h *Handler) error {
112+ if c.fd < 0 {
113+ return ErrClosed
114+ }
115+ var fence Seq
116+ for {
117+ if fence != 0 && c.seq >= fence && c.live == 0 {
118+ return nil
119+ }
120+ if fence == 0 || fence < c.buf.next-1 {
121+ // Requests queued after the fence (or none yet): fence them.
122+ if fence = c.fence(); fence == 0 {
123+ return c.buf.Err()
124+ }
125+ }
126+ if err := c.pollOnce(h); err != nil {
127+ return err
128+ }
129+ }
130+}
131+
132+// pollOnce makes one step of progress: flush, dispatch what is readable and,
133+// only when Dispatch read nothing at all, block until the socket is readable
134+// (or writable while requests remain queued). Dispatch is bounded per call,
135+// so it may return with input left; the caller loops and re-checks.
136+func (c *Conn) pollOnce(h *Handler) error {
137+ events := int16(unix.POLLIN)
138+ switch err := c.Flush(); err {
139+ case nil:
140+ case ErrWouldBlock:
141+ events |= unix.POLLOUT
142+ default:
143+ return err
144+ }
145+ if err := c.Dispatch(h); err != nil {
146+ return err
147+ }
148+ // Packets handled may have queued requests (callbacks, the bootstrap):
149+ // loop to flush them, and to fence them, before blocking.
150+ if !c.rdIdle || (events&unix.POLLOUT == 0 && c.buf.Len() > 0) {
151+ return nil
152+ }
153+ fds := []unix.PollFd{{Fd: int32(c.fd), Events: events}}
154+ if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR {
155+ return os.NewSyscallError("poll", err)
156+ }
157+ return nil
158+}
@@ -0,0 +1,158 @@
1+package xturbo
2+
3+import (
4+ "encoding/binary"
5+ "os"
6+
7+ "golang.org/x/sys/unix"
8+)
9+
10+// Core and extension requests root issues itself. Root does not import the
11+// generated packages, so these few are encoded here; tests compare them with
12+// the generated encoders and the request goldens.
13+const (
14+ opInternAtom = 16
15+ opGetInputFocus = 43
16+ opQueryExtension = 98
17+ minorGetXIDRange = 1 // XC-MISC
18+)
19+
20+func encodeGetInputFocus(b *Buf) Seq {
21+ _, seq := b.Request(opGetInputFocus, 0, 0)
22+ return seq
23+}
24+
25+// encodeNamed encodes QueryExtension and InternAtom: CARD16 name_len, 2
26+// pad bytes, then the name padded to 4.
27+func encodeNamed(b *Buf, major, data uint8, name string) Seq {
28+ if len(name) > 0xffff {
29+ b.Fail(ErrBadLength)
30+ return 0
31+ }
32+ p, seq := b.Request(major, data, pad4(4+len(name)))
33+ if p == nil {
34+ return 0
35+ }
36+ binary.LittleEndian.PutUint16(p, uint16(len(name)))
37+ copy(p[4:], name)
38+ return seq
39+}
40+
41+func encodeQueryExtension(b *Buf, name string) Seq {
42+ return encodeNamed(b, opQueryExtension, 0, name)
43+}
44+
45+func encodeInternAtom(b *Buf, onlyIfExists bool, name string) Seq {
46+ var data uint8
47+ if onlyIfExists {
48+ data = 1
49+ }
50+ return encodeNamed(b, opInternAtom, data, name)
51+}
52+
53+// encodeExtRequest encodes a body-less extension request (BIG-REQUESTS
54+// Enable, XC-MISC GetXIDRange). It fails with ErrExtMissing when id is absent.
55+func encodeExtRequest(b *Buf, id ExtID, minor uint8) Seq {
56+ major := b.Ext.Major(id)
57+ if major == 0 {
58+ b.Fail(ErrExtMissing)
59+ return 0
60+ }
61+ _, seq := b.Request(major, minor, 0)
62+ return seq
63+}
64+
65+// bootstrap negotiates every extension root knows (one pipelined
66+// QueryExtension each, then BIG-REQUESTS Enable when present) in one Sync.
67+func (c *Conn) bootstrap() error {
68+ for id := ExtCore + 1; id < extCount; id++ {
69+ seq := encodeQueryExtension(c.buf, ExtXName[id])
70+ if seq == 0 {
71+ return c.buf.Err()
72+ }
73+ if err := c.expect(seq, kindQueryExtension, [4]uint32{uint32(id)}); err != nil {
74+ return err
75+ }
76+ }
77+ // consumeReply queues bigreq Enable when BIG-REQUESTS is present; Sync
78+ // waits for it too.
79+ return c.Sync(nil)
80+}
81+
82+// Sync flushes every queued request and blocks until the server has
83+// processed them and every request registered with Expect has completed. It
84+// appends a GetInputFocus as a fence, so even with nothing registered it is a
85+// full round trip: errors of earlier requests have been delivered when it
86+// returns. Packets read meanwhile go to h as with Dispatch (nil h: the last
87+// Handler given to Dispatch, or none). An error on a request root issued
88+// itself is returned as *XError. Meant for startup; it must not be called
89+// from a Handler callback.
90+func (c *Conn) Sync(h *Handler) error {
91+ if c.dispatching {
92+ return errReentrantDispatch
93+ }
94+ if h == nil {
95+ h = c.h
96+ }
97+ c.syncFailed = false
98+ if err := c.wait(h); err != nil {
99+ return err
100+ }
101+ if c.syncFailed {
102+ e := c.syncErr
103+ return &e
104+ }
105+ return nil
106+}
107+
108+// wait fences, flushes and dispatches, one bounded Dispatch at a time, until
109+// the fence has been answered and no registered slot remains. A fence is
110+// re-queued whenever callbacks registered requests after the last one.
111+func (c *Conn) wait(h *Handler) error {
112+ if c.fd < 0 {
113+ return ErrClosed
114+ }
115+ var fence Seq
116+ for {
117+ if fence != 0 && c.seq >= fence && c.live == 0 {
118+ return nil
119+ }
120+ if fence == 0 || fence < c.buf.next-1 {
121+ // Requests queued after the fence (or none yet): fence them.
122+ if fence = c.fence(); fence == 0 {
123+ return c.buf.Err()
124+ }
125+ }
126+ if err := c.pollOnce(h); err != nil {
127+ return err
128+ }
129+ }
130+}
131+
132+// pollOnce makes one step of progress: flush, dispatch what is readable and,
133+// only when Dispatch read nothing at all, block until the socket is readable
134+// (or writable while requests remain queued). Dispatch is bounded per call,
135+// so it may return with input left; the caller loops and re-checks.
136+func (c *Conn) pollOnce(h *Handler) error {
137+ events := int16(unix.POLLIN)
138+ switch err := c.Flush(); err {
139+ case nil:
140+ case ErrWouldBlock:
141+ events |= unix.POLLOUT
142+ default:
143+ return err
144+ }
145+ if err := c.Dispatch(h); err != nil {
146+ return err
147+ }
148+ // Packets handled may have queued requests (callbacks, the bootstrap):
149+ // loop to flush them, and to fence them, before blocking.
150+ if !c.rdIdle || (events&unix.POLLOUT == 0 && c.buf.Len() > 0) {
151+ return nil
152+ }
153+ fds := []unix.PollFd{{Fd: int32(c.fd), Events: events}}
154+ if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR {
155+ return os.NewSyscallError("poll", err)
156+ }
157+ return nil
158+}
A xid.go +105 -0
@@ -0,0 +1,105 @@
1+package xturbo
2+
3+import "errors"
4+
5+// errXIDsExhausted reports that the server has no resource IDs left for this
6+// client (XC-MISC GetXIDRange returned an empty range).
7+var errXIDsExhausted = errors.New("xturbo: server has no resource IDs left")
8+
9+// xidFreeCap bounds the FreeXID list; IDs freed beyond it are not reused.
10+const xidFreeCap = 256
11+
12+// initXIDs starts allocation over the setup range: base|0, base|inc, …
13+// up to base|mask, in libxcb's order.
14+func (c *Conn) initXIDs() {
15+ inc := c.xidMask & -c.xidMask
16+ if inc == 0 {
17+ return
18+ }
19+ c.xidNext = 0
20+ c.xidLeft = c.xidMask/inc + 1
21+}
22+
23+// NewXID returns a fresh resource ID for this client. IDs released with
24+// FreeXID are reused first; then the setup range is used, and when it is
25+// exhausted one blocking XC-MISC GetXIDRange round trip refills it (packets
26+// read meanwhile go to the last Handler given to Dispatch). It fails when
27+// XC-MISC is absent, the server has no IDs left, or it is called from a
28+// Handler callback while a refill is needed.
29+func (c *Conn) NewXID() (uint32, error) {
30+ if c.xidFreeN > 0 {
31+ c.xidFreeN--
32+ return c.xidFree[c.xidFreeN], nil
33+ }
34+ if c.xidLeft == 0 {
35+ if err := c.refillXIDs(); err != nil {
36+ return 0, err
37+ }
38+ }
39+ id := c.xidBase | c.xidNext
40+ c.xidNext += c.xidMask & -c.xidMask
41+ c.xidLeft--
42+ return id, nil
43+}
44+
45+// FreeXID returns id, whose resource the caller has already destroyed (the
46+// destroy request may still be queued), for reuse by NewXID. IDs outside this
47+// client's range, and IDs beyond the free list capacity, are ignored.
48+func (c *Conn) FreeXID(id uint32) {
49+ if id == 0 || id&^c.xidMask != c.xidBase || c.xidFreeN == xidFreeCap {
50+ return
51+ }
52+ c.xidFree[c.xidFreeN] = id
53+ c.xidFreeN++
54+}
55+
56+func (c *Conn) refillXIDs() error {
57+ if c.dispatching {
58+ return errReentrantDispatch
59+ }
60+ if c.fd < 0 {
61+ return ErrClosed
62+ }
63+ if !c.ext.Info(ExtXCMisc).Present {
64+ return ErrExtMissing
65+ }
66+ if err := c.waitSlot(); err != nil {
67+ return err
68+ }
69+ seq := encodeExtRequest(c.buf, ExtXCMisc, minorGetXIDRange)
70+ if seq == 0 {
71+ return c.buf.Err()
72+ }
73+ if err := c.expect(seq, kindXIDRange, [4]uint32{}); err != nil {
74+ return err
75+ }
76+ c.rangeGot = false
77+ c.syncFailed = false
78+ if err := c.wait(c.h); err != nil {
79+ return err
80+ }
81+ if c.syncFailed {
82+ e := c.syncErr
83+ return &e
84+ }
85+ // The server answers start 0, count 1 (or count 0) when it has none.
86+ inc := c.xidMask & -c.xidMask
87+ if !c.rangeGot || c.rangeLeft == 0 || c.rangeNext == 0 || inc == 0 ||
88+ c.rangeNext&^c.xidMask != c.xidBase {
89+ return errXIDsExhausted
90+ }
91+ c.xidNext = c.rangeNext & c.xidMask
92+ c.xidLeft = c.rangeLeft
93+ return nil
94+}
95+
96+// waitSlot makes sure the ring slot of the next request is free, so a
97+// reserved registration can always be made for it: when an earlier request
98+// still holds it, it waits for every outstanding request (fenced, so even a
99+// registered request without reply completes).
100+func (c *Conn) waitSlot() error {
101+ if s := c.ring[c.buf.next&ringMask].seq; s == 0 || s < c.seq {
102+ return nil
103+ }
104+ return c.wait(c.h)
105+}
@@ -0,0 +1,105 @@
1+package xturbo
2+
3+import "errors"
4+
5+// errXIDsExhausted reports that the server has no resource IDs left for this
6+// client (XC-MISC GetXIDRange returned an empty range).
7+var errXIDsExhausted = errors.New("xturbo: server has no resource IDs left")
8+
9+// xidFreeCap bounds the FreeXID list; IDs freed beyond it are not reused.
10+const xidFreeCap = 256
11+
12+// initXIDs starts allocation over the setup range: base|0, base|inc, …
13+// up to base|mask, in libxcb's order.
14+func (c *Conn) initXIDs() {
15+ inc := c.xidMask & -c.xidMask
16+ if inc == 0 {
17+ return
18+ }
19+ c.xidNext = 0
20+ c.xidLeft = c.xidMask/inc + 1
21+}
22+
23+// NewXID returns a fresh resource ID for this client. IDs released with
24+// FreeXID are reused first; then the setup range is used, and when it is
25+// exhausted one blocking XC-MISC GetXIDRange round trip refills it (packets
26+// read meanwhile go to the last Handler given to Dispatch). It fails when
27+// XC-MISC is absent, the server has no IDs left, or it is called from a
28+// Handler callback while a refill is needed.
29+func (c *Conn) NewXID() (uint32, error) {
30+ if c.xidFreeN > 0 {
31+ c.xidFreeN--
32+ return c.xidFree[c.xidFreeN], nil
33+ }
34+ if c.xidLeft == 0 {
35+ if err := c.refillXIDs(); err != nil {
36+ return 0, err
37+ }
38+ }
39+ id := c.xidBase | c.xidNext
40+ c.xidNext += c.xidMask & -c.xidMask
41+ c.xidLeft--
42+ return id, nil
43+}
44+
45+// FreeXID returns id, whose resource the caller has already destroyed (the
46+// destroy request may still be queued), for reuse by NewXID. IDs outside this
47+// client's range, and IDs beyond the free list capacity, are ignored.
48+func (c *Conn) FreeXID(id uint32) {
49+ if id == 0 || id&^c.xidMask != c.xidBase || c.xidFreeN == xidFreeCap {
50+ return
51+ }
52+ c.xidFree[c.xidFreeN] = id
53+ c.xidFreeN++
54+}
55+
56+func (c *Conn) refillXIDs() error {
57+ if c.dispatching {
58+ return errReentrantDispatch
59+ }
60+ if c.fd < 0 {
61+ return ErrClosed
62+ }
63+ if !c.ext.Info(ExtXCMisc).Present {
64+ return ErrExtMissing
65+ }
66+ if err := c.waitSlot(); err != nil {
67+ return err
68+ }
69+ seq := encodeExtRequest(c.buf, ExtXCMisc, minorGetXIDRange)
70+ if seq == 0 {
71+ return c.buf.Err()
72+ }
73+ if err := c.expect(seq, kindXIDRange, [4]uint32{}); err != nil {
74+ return err
75+ }
76+ c.rangeGot = false
77+ c.syncFailed = false
78+ if err := c.wait(c.h); err != nil {
79+ return err
80+ }
81+ if c.syncFailed {
82+ e := c.syncErr
83+ return &e
84+ }
85+ // The server answers start 0, count 1 (or count 0) when it has none.
86+ inc := c.xidMask & -c.xidMask
87+ if !c.rangeGot || c.rangeLeft == 0 || c.rangeNext == 0 || inc == 0 ||
88+ c.rangeNext&^c.xidMask != c.xidBase {
89+ return errXIDsExhausted
90+ }
91+ c.xidNext = c.rangeNext & c.xidMask
92+ c.xidLeft = c.rangeLeft
93+ return nil
94+}
95+
96+// waitSlot makes sure the ring slot of the next request is free, so a
97+// reserved registration can always be made for it: when an earlier request
98+// still holds it, it waits for every outstanding request (fenced, so even a
99+// registered request without reply completes).
100+func (c *Conn) waitSlot() error {
101+ if s := c.ring[c.buf.next&ringMask].seq; s == 0 || s < c.seq {
102+ return nil
103+ }
104+ return c.wait(c.h)
105+}
A xserver_script_test.go +212 -0
@@ -0,0 +1,212 @@
1+package xturbo
2+
3+import (
4+ "encoding/binary"
5+ "io"
6+ "os"
7+ "sync"
8+ "testing"
9+)
10+
11+// scriptExt is an extension the scripted server advertises.
12+type scriptExt struct{ major, firstEvent, firstError uint8 }
13+
14+// script is the behaviour of the scripted X server behind a socketpair. It
15+// answers QueryExtension, InternAtom, GetInputFocus, BIG-REQUESTS Enable and
16+// XC-MISC GetXIDRange like an X server; other requests get no reply unless
17+// hook handles them.
18+type script struct {
19+ exts map[string]scriptExt
20+ bigreqMax uint32 // Enable reply maximum_request_length
21+ atoms map[string]uint32 // known atoms; new names get nextAtom++
22+ nextAtom uint32
23+ badAtoms map[string]bool // InternAtom answers BadAlloc
24+ xidRanges [][2]uint32 // successive GetXIDRange replies {start, count}
25+ // hook sees every request first; returning true skips the default.
26+ hook func(s *scriptConn, major, minor uint8, seq uint16, body []byte) bool
27+}
28+
29+// scriptConn is the server side of one scripted connection.
30+type scriptConn struct {
31+ f *os.File
32+ sc *script
33+ seq uint16
34+
35+ mu sync.Mutex
36+ reqs []scriptReq // every request after setup, in order
37+}
38+
39+type scriptReq struct {
40+ seq uint16
41+ major uint8
42+ minor uint8
43+ raw []byte // whole request, header included
44+}
45+
46+func (s *scriptConn) requests() []scriptReq {
47+ s.mu.Lock()
48+ defer s.mu.Unlock()
49+ return append([]scriptReq(nil), s.reqs...)
50+}
51+
52+func (s *scriptConn) write(p []byte) { _, _ = s.f.Write(p) }
53+
54+func (s *scriptConn) reply(data uint8, body []byte) {
55+ words := (max(len(body), 24) - 24 + 3) / 4
56+ p := make([]byte, 32+4*words)
57+ p[0], p[1] = 1, data
58+ binary.LittleEndian.PutUint16(p[2:], s.seq)
59+ binary.LittleEndian.PutUint32(p[4:], uint32(words))
60+ copy(p[8:], body)
61+ s.write(p)
62+}
63+
64+func (s *scriptConn) xerror(code, major uint8, bad uint32) {
65+ p := make([]byte, 32)
66+ p[1] = code
67+ binary.LittleEndian.PutUint16(p[2:], s.seq)
68+ binary.LittleEndian.PutUint32(p[4:], bad)
69+ p[10] = major
70+ s.write(p)
71+}
72+
73+// event writes a 32-byte core event carrying the current sequence number.
74+func (s *scriptConn) event(code uint8, fill byte) {
75+ p := make([]byte, 32)
76+ for i := range p {
77+ p[i] = fill
78+ }
79+ p[0] = code
80+ binary.LittleEndian.PutUint16(p[2:], s.seq)
81+ s.write(p)
82+}
83+
84+func (sc *script) extByMajor(major uint8) string {
85+ for name, e := range sc.exts {
86+ if e.major == major {
87+ return name
88+ }
89+ }
90+ return ""
91+}
92+
93+// serve reads requests until EOF and answers them.
94+func (s *scriptConn) serve() {
95+ sc := s.sc
96+ for {
97+ var hdr [4]byte
98+ if _, err := io.ReadFull(s.f, hdr[:]); err != nil {
99+ return
100+ }
101+ raw := hdr[:]
102+ n := int(binary.LittleEndian.Uint16(hdr[2:])) * 4
103+ if n == 0 { // BIG-REQUESTS header
104+ var l [4]byte
105+ if _, err := io.ReadFull(s.f, l[:]); err != nil {
106+ return
107+ }
108+ raw = append(raw, l[:]...)
109+ n = int(binary.LittleEndian.Uint32(l[:])) * 4
110+ }
111+ rest := make([]byte, n-len(raw))
112+ if _, err := io.ReadFull(s.f, rest); err != nil {
113+ return
114+ }
115+ raw = append(append([]byte(nil), raw...), rest...)
116+ body := raw[len(raw)-len(rest):]
117+ s.seq++
118+ major, minor := hdr[0], hdr[1]
119+ s.mu.Lock()
120+ s.reqs = append(s.reqs, scriptReq{s.seq, major, minor, raw})
121+ s.mu.Unlock()
122+ if sc.hook != nil && sc.hook(s, major, minor, s.seq, body) {
123+ continue
124+ }
125+ s.answer(major, minor, body)
126+ }
127+}
128+
129+// answer runs the default behaviour for one request, holding s.mu so tests
130+// can read sc (atom) concurrently.
131+func (s *scriptConn) answer(major, minor uint8, body []byte) {
132+ sc := s.sc
133+ s.mu.Lock()
134+ defer s.mu.Unlock()
135+ switch {
136+ case major == opQueryExtension:
137+ e, ok := sc.exts[string(body[4:4+binary.LittleEndian.Uint16(body)])]
138+ var present uint8
139+ if ok {
140+ present = 1
141+ }
142+ s.reply(0, []byte{present, e.major, e.firstEvent, e.firstError})
143+ case major == opInternAtom:
144+ name := string(body[4 : 4+binary.LittleEndian.Uint16(body)])
145+ if sc.badAtoms[name] {
146+ s.xerror(11, opInternAtom, 0) // BadAlloc
147+ return
148+ }
149+ a, ok := sc.atoms[name]
150+ if !ok && minor == 0 {
151+ if sc.atoms == nil {
152+ sc.atoms = map[string]uint32{}
153+ }
154+ sc.nextAtom++
155+ a = sc.nextAtom + 1000
156+ sc.atoms[name] = a
157+ }
158+ s.reply(0, binary.LittleEndian.AppendUint32(nil, a))
159+ case major == opGetInputFocus:
160+ s.reply(1, binary.LittleEndian.AppendUint32(nil, 0x1234))
161+ case major >= 128 && sc.extByMajor(major) == "BIG-REQUESTS" && minor == 0:
162+ s.reply(0, binary.LittleEndian.AppendUint32(nil, sc.bigreqMax))
163+ case major >= 128 && sc.extByMajor(major) == "XC-MISC" && minor == minorGetXIDRange:
164+ r := [2]uint32{0, 1}
165+ if len(sc.xidRanges) > 0 {
166+ r, sc.xidRanges = sc.xidRanges[0], sc.xidRanges[1:]
167+ }
168+ s.reply(0, binary.LittleEndian.AppendUint32(binary.LittleEndian.AppendUint32(nil, r[0]), r[1]))
169+ }
170+}
171+
172+// fullExts is what a typical Xvfb advertises, at its usual opcodes.
173+func fullExts() map[string]scriptExt {
174+ return map[string]scriptExt{
175+ "BIG-REQUESTS": {133, 0, 0},
176+ "Generic Event Extension": {128, 0, 0},
177+ "XC-MISC": {136, 0, 0},
178+ "Composite": {142, 0, 0},
179+ "XFIXES": {138, 87, 140},
180+ "X-Resource": {144, 0, 0},
181+ "RENDER": {139, 0, 141},
182+ "SHAPE": {129, 64, 0},
183+ }
184+}
185+
186+// startScript serves sc behind a socketpair after answering the setup with
187+// the recorded Success reply, and returns the client fd (not yet set up) and
188+// the server connection.
189+func startScript(t *testing.T, sc *script) (int, *scriptConn) {
190+ t.Helper()
191+ fd, srv, _ := serveSetup(t, loadSetup(t, "Success"), 0, false, sc)
192+ return fd, srv
193+}
194+
195+// dialScript runs NewConn against sc.
196+func dialScript(t *testing.T, sc *script) (*Conn, *scriptConn) {
197+ t.Helper()
198+ fd, srv := startScript(t, sc)
199+ c, err := NewConn(fd)
200+ if err != nil {
201+ t.Fatal(err)
202+ }
203+ t.Cleanup(func() { _ = c.Close() })
204+ return c, srv
205+}
206+
207+// atom returns the atom the server assigned to name.
208+func (s *scriptConn) atom(name string) uint32 {
209+ s.mu.Lock()
210+ defer s.mu.Unlock()
211+ return s.sc.atoms[name]
212+}
@@ -0,0 +1,212 @@
1+package xturbo
2+
3+import (
4+ "encoding/binary"
5+ "io"
6+ "os"
7+ "sync"
8+ "testing"
9+)
10+
11+// scriptExt is an extension the scripted server advertises.
12+type scriptExt struct{ major, firstEvent, firstError uint8 }
13+
14+// script is the behaviour of the scripted X server behind a socketpair. It
15+// answers QueryExtension, InternAtom, GetInputFocus, BIG-REQUESTS Enable and
16+// XC-MISC GetXIDRange like an X server; other requests get no reply unless
17+// hook handles them.
18+type script struct {
19+ exts map[string]scriptExt
20+ bigreqMax uint32 // Enable reply maximum_request_length
21+ atoms map[string]uint32 // known atoms; new names get nextAtom++
22+ nextAtom uint32
23+ badAtoms map[string]bool // InternAtom answers BadAlloc
24+ xidRanges [][2]uint32 // successive GetXIDRange replies {start, count}
25+ // hook sees every request first; returning true skips the default.
26+ hook func(s *scriptConn, major, minor uint8, seq uint16, body []byte) bool
27+}
28+
29+// scriptConn is the server side of one scripted connection.
30+type scriptConn struct {
31+ f *os.File
32+ sc *script
33+ seq uint16
34+
35+ mu sync.Mutex
36+ reqs []scriptReq // every request after setup, in order
37+}
38+
39+type scriptReq struct {
40+ seq uint16
41+ major uint8
42+ minor uint8
43+ raw []byte // whole request, header included
44+}
45+
46+func (s *scriptConn) requests() []scriptReq {
47+ s.mu.Lock()
48+ defer s.mu.Unlock()
49+ return append([]scriptReq(nil), s.reqs...)
50+}
51+
52+func (s *scriptConn) write(p []byte) { _, _ = s.f.Write(p) }
53+
54+func (s *scriptConn) reply(data uint8, body []byte) {
55+ words := (max(len(body), 24) - 24 + 3) / 4
56+ p := make([]byte, 32+4*words)
57+ p[0], p[1] = 1, data
58+ binary.LittleEndian.PutUint16(p[2:], s.seq)
59+ binary.LittleEndian.PutUint32(p[4:], uint32(words))
60+ copy(p[8:], body)
61+ s.write(p)
62+}
63+
64+func (s *scriptConn) xerror(code, major uint8, bad uint32) {
65+ p := make([]byte, 32)
66+ p[1] = code
67+ binary.LittleEndian.PutUint16(p[2:], s.seq)
68+ binary.LittleEndian.PutUint32(p[4:], bad)
69+ p[10] = major
70+ s.write(p)
71+}
72+
73+// event writes a 32-byte core event carrying the current sequence number.
74+func (s *scriptConn) event(code uint8, fill byte) {
75+ p := make([]byte, 32)
76+ for i := range p {
77+ p[i] = fill
78+ }
79+ p[0] = code
80+ binary.LittleEndian.PutUint16(p[2:], s.seq)
81+ s.write(p)
82+}
83+
84+func (sc *script) extByMajor(major uint8) string {
85+ for name, e := range sc.exts {
86+ if e.major == major {
87+ return name
88+ }
89+ }
90+ return ""
91+}
92+
93+// serve reads requests until EOF and answers them.
94+func (s *scriptConn) serve() {
95+ sc := s.sc
96+ for {
97+ var hdr [4]byte
98+ if _, err := io.ReadFull(s.f, hdr[:]); err != nil {
99+ return
100+ }
101+ raw := hdr[:]
102+ n := int(binary.LittleEndian.Uint16(hdr[2:])) * 4
103+ if n == 0 { // BIG-REQUESTS header
104+ var l [4]byte
105+ if _, err := io.ReadFull(s.f, l[:]); err != nil {
106+ return
107+ }
108+ raw = append(raw, l[:]...)
109+ n = int(binary.LittleEndian.Uint32(l[:])) * 4
110+ }
111+ rest := make([]byte, n-len(raw))
112+ if _, err := io.ReadFull(s.f, rest); err != nil {
113+ return
114+ }
115+ raw = append(append([]byte(nil), raw...), rest...)
116+ body := raw[len(raw)-len(rest):]
117+ s.seq++
118+ major, minor := hdr[0], hdr[1]
119+ s.mu.Lock()
120+ s.reqs = append(s.reqs, scriptReq{s.seq, major, minor, raw})
121+ s.mu.Unlock()
122+ if sc.hook != nil && sc.hook(s, major, minor, s.seq, body) {
123+ continue
124+ }
125+ s.answer(major, minor, body)
126+ }
127+}
128+
129+// answer runs the default behaviour for one request, holding s.mu so tests
130+// can read sc (atom) concurrently.
131+func (s *scriptConn) answer(major, minor uint8, body []byte) {
132+ sc := s.sc
133+ s.mu.Lock()
134+ defer s.mu.Unlock()
135+ switch {
136+ case major == opQueryExtension:
137+ e, ok := sc.exts[string(body[4:4+binary.LittleEndian.Uint16(body)])]
138+ var present uint8
139+ if ok {
140+ present = 1
141+ }
142+ s.reply(0, []byte{present, e.major, e.firstEvent, e.firstError})
143+ case major == opInternAtom:
144+ name := string(body[4 : 4+binary.LittleEndian.Uint16(body)])
145+ if sc.badAtoms[name] {
146+ s.xerror(11, opInternAtom, 0) // BadAlloc
147+ return
148+ }
149+ a, ok := sc.atoms[name]
150+ if !ok && minor == 0 {
151+ if sc.atoms == nil {
152+ sc.atoms = map[string]uint32{}
153+ }
154+ sc.nextAtom++
155+ a = sc.nextAtom + 1000
156+ sc.atoms[name] = a
157+ }
158+ s.reply(0, binary.LittleEndian.AppendUint32(nil, a))
159+ case major == opGetInputFocus:
160+ s.reply(1, binary.LittleEndian.AppendUint32(nil, 0x1234))
161+ case major >= 128 && sc.extByMajor(major) == "BIG-REQUESTS" && minor == 0:
162+ s.reply(0, binary.LittleEndian.AppendUint32(nil, sc.bigreqMax))
163+ case major >= 128 && sc.extByMajor(major) == "XC-MISC" && minor == minorGetXIDRange:
164+ r := [2]uint32{0, 1}
165+ if len(sc.xidRanges) > 0 {
166+ r, sc.xidRanges = sc.xidRanges[0], sc.xidRanges[1:]
167+ }
168+ s.reply(0, binary.LittleEndian.AppendUint32(binary.LittleEndian.AppendUint32(nil, r[0]), r[1]))
169+ }
170+}
171+
172+// fullExts is what a typical Xvfb advertises, at its usual opcodes.
173+func fullExts() map[string]scriptExt {
174+ return map[string]scriptExt{
175+ "BIG-REQUESTS": {133, 0, 0},
176+ "Generic Event Extension": {128, 0, 0},
177+ "XC-MISC": {136, 0, 0},
178+ "Composite": {142, 0, 0},
179+ "XFIXES": {138, 87, 140},
180+ "X-Resource": {144, 0, 0},
181+ "RENDER": {139, 0, 141},
182+ "SHAPE": {129, 64, 0},
183+ }
184+}
185+
186+// startScript serves sc behind a socketpair after answering the setup with
187+// the recorded Success reply, and returns the client fd (not yet set up) and
188+// the server connection.
189+func startScript(t *testing.T, sc *script) (int, *scriptConn) {
190+ t.Helper()
191+ fd, srv, _ := serveSetup(t, loadSetup(t, "Success"), 0, false, sc)
192+ return fd, srv
193+}
194+
195+// dialScript runs NewConn against sc.
196+func dialScript(t *testing.T, sc *script) (*Conn, *scriptConn) {
197+ t.Helper()
198+ fd, srv := startScript(t, sc)
199+ c, err := NewConn(fd)
200+ if err != nil {
201+ t.Fatal(err)
202+ }
203+ t.Cleanup(func() { _ = c.Close() })
204+ return c, srv
205+}
206+
207+// atom returns the atom the server assigned to name.
208+func (s *scriptConn) atom(name string) uint32 {
209+ s.mu.Lock()
210+ defer s.mu.Unlock()
211+ return s.sc.atoms[name]
212+}

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
esc
close a dialog, leave a field

Branches 1

Tags 1

↑↓ moveenter openesc close