detached ad1316e

fix(runtime): bound blocking calls, validate XID ranges and prefetch XIDs without blocking

- Deadlines: NewConn (handshake + extension bootstrap) gives up after 10 s,
  Sync, InternAtoms and a blocking XID refill after 5 s each, with an error
  wrapping os.ErrDeadlineExceeded. The handshake now runs on the
  non-blocking fd and polls with the remaining time; pollOnce polls with
  the remaining time and also fails once the deadline passes while a server
  floods events without answering.
- XC-MISC GetXIDRange answers are accepted only when every ID of the range
  lies in this client's base and mask (start aligned to the mask's lowest
  bit, start + (count-1)*inc <= mask); anything else counts as exhausted.
- XIDsLow and PrefetchXIDs: a non-blocking GetXIDRange (reserved kind)
  whose reply becomes a pending range; NewXID switches to it when the
  current range runs out, and only blocks (outside callbacks) when none is
  pending. A blocking refill waits for a prefetch in flight instead of
  sending a second request. GetXIDRange errors are kept apart from Sync's
  error and returned by NewXID.
- FreeXID documents its contract (IDs from NewXID, freed once, after the
  destroy).
- InternAtoms checks name lengths first and returns ErrBadLength without
  failing the Buf.

Decisions:
- Timeouts are package vars (connectTimeout 10 s, syncTimeout 5 s) copied
  into an unexported Conn field; tests shorten them. No public setter.
- One deadline per call, covering waitSlot and the final wait.
- XIDsLow is false while a range is pending, a prefetch is in flight, the
  last prefetch reported the server out of IDs, or XC-MISC is absent.
- PrefetchXIDs returns ErrRingFull rather than waiting when the next
  request's Expect slot is taken (it may run in a callback).

Tests: hung server during setup, bootstrap, Sync, InternAtoms and refill;
flooding server; range bounds incl. {0x00200000, 0xffffffff}; prefetch from
a callback across ranges; refill waiting for a prefetch; prefetch error;
over-long atom name; 0xffff requests without Flush -> ErrSeqOverflow while
periodic Flushes resync past it; Xvfb prefetch. Scripted tests run with
3 s bounds so they fail fast.
commit
ad1316e01a9f213cd5ef3cc8fd2766a7ef1bf431
author
brice · · 2026-10-22 09:43 UTC
committer
brice · · 2026-10-22 09:43 UTC
parent
57cbbe3
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
checks
4/4
12 files · +677 -108
M atoms.go +16 -6
@@ -1,16 +1,25 @@
11 package xturbo
22
3+import "time"
4+
35 // InternAtoms interns every name in one pipelined batch and blocks for one
46 // round trip (Sync), writing the atom of names[i] into dst[i] (0 for a name
57 // 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.
8+// len(names) and every name be at most 65535 bytes (ErrBadLength, nothing
9+// sent, the connection unaffected). The first X error on one of these
10+// requests is returned as *XError, its Args[0] the index of the name; the
11+// other atoms are still filled. Packets read meanwhile go to the last Handler
12+// given to Dispatch. Like Sync it gives up after 5 s (os.ErrDeadlineExceeded).
13+// Meant for startup; it must not be called from a Handler callback.
1014 func (c *Conn) InternAtoms(names []string, onlyIfExists bool, dst []uint32) error {
1115 if len(names) != len(dst) {
1216 return ErrBadLength
1317 }
18+ for _, name := range names {
19+ if len(name) > 0xffff {
20+ return ErrBadLength // a caller bug: fail before queuing anything
21+ }
22+ }
1423 if c.dispatching {
1524 return errReentrantDispatch
1625 }
@@ -20,9 +29,10 @@ func (c *Conn) InternAtoms(names []string, onlyIfExists bool, dst []uint32) erro
2029 clear(dst)
2130 c.atoms = dst
2231 defer func() { c.atoms = nil }()
32+ deadline := time.Now().Add(c.timeout)
2333 c.syncFailed = false
2434 for i, name := range names {
25- if err := c.waitSlot(); err != nil {
35+ if err := c.waitSlot(deadline); err != nil {
2636 return err
2737 }
2838 seq := encodeInternAtom(c.buf, onlyIfExists, name)
@@ -33,7 +43,7 @@ func (c *Conn) InternAtoms(names []string, onlyIfExists bool, dst []uint32) erro
3343 return err
3444 }
3545 }
36- if err := c.wait(c.h); err != nil {
46+ if err := c.waitUntil(c.h, deadline); err != nil {
3747 return err
3848 }
3949 if c.syncFailed {
@@ -1,16 +1,25 @@
1 package xturbo1 package xturbo
2 2
3+import "time"
4+
3 // InternAtoms interns every name in one pipelined batch and blocks for one5 // 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 name6 // 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 equal7 // 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 is8+// len(names) and every name be at most 65535 bytes (ErrBadLength, nothing
7-// returned as *XError, its Args[0] the index of the name; the other atoms9+// sent, the connection unaffected). The first X error on one of these
8-// are still filled. Packets read meanwhile go to the last Handler given to10+// requests is returned as *XError, its Args[0] the index of the name; the
9-// Dispatch. Meant for startup; it must not be called from a Handler callback.11+// other atoms are still filled. Packets read meanwhile go to the last Handler
12+// given to Dispatch. Like Sync it gives up after 5 s (os.ErrDeadlineExceeded).
13+// Meant for startup; it must not be called from a Handler callback.
10 func (c *Conn) InternAtoms(names []string, onlyIfExists bool, dst []uint32) error {14 func (c *Conn) InternAtoms(names []string, onlyIfExists bool, dst []uint32) error {
11 if len(names) != len(dst) {15 if len(names) != len(dst) {
12 return ErrBadLength16 return ErrBadLength
13 }17 }
18+ for _, name := range names {
19+ if len(name) > 0xffff {
20+ return ErrBadLength // a caller bug: fail before queuing anything
21+ }
22+ }
14 if c.dispatching {23 if c.dispatching {
15 return errReentrantDispatch24 return errReentrantDispatch
16 }25 }
@@ -20,9 +29,10 @@ func (c *Conn) InternAtoms(names []string, onlyIfExists bool, dst []uint32) erro
20 clear(dst)29 clear(dst)
21 c.atoms = dst30 c.atoms = dst
22 defer func() { c.atoms = nil }()31 defer func() { c.atoms = nil }()
32+ deadline := time.Now().Add(c.timeout)
23 c.syncFailed = false33 c.syncFailed = false
24 for i, name := range names {34 for i, name := range names {
25- if err := c.waitSlot(); err != nil {35+ if err := c.waitSlot(deadline); err != nil {
26 return err36 return err
27 }37 }
28 seq := encodeInternAtom(c.buf, onlyIfExists, name)38 seq := encodeInternAtom(c.buf, onlyIfExists, name)
@@ -33,7 +43,7 @@ func (c *Conn) InternAtoms(names []string, onlyIfExists bool, dst []uint32) erro
33 return err43 return err
34 }44 }
35 }45 }
36- if err := c.wait(c.h); err != nil {46+ if err := c.waitUntil(c.h, deadline); err != nil {
37 return err47 return err
38 }48 }
39 if c.syncFailed {49 if c.syncFailed {
M bootstrap_test.go +4 -4
@@ -273,7 +273,7 @@ func TestSyncFromCallback(t *testing.T) {
273273 h := Handler{OnEvent: func(*Event) {
274274 errSync = c.Sync(nil)
275275 errAtoms = c.InternAtoms([]string{"X"}, false, make([]uint32, 1))
276- c.xidLeft, c.xidFreeN = 0, 0
276+ c.xids, c.xidFreeN = xidRange{}, 0
277277 _, errXID = c.NewXID()
278278 }}
279279 c.Buf().Request(8, 0, 4)
@@ -333,7 +333,7 @@ func TestNewXIDRefill(t *testing.T) {
333333 sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200040, 3}, {0, 1}}}
334334 c, srv := dialScript(t, sc)
335335 // Pretend only two ids of the setup range are left.
336- c.xidLeft = 2
336+ c.xids.left = 2
337337 var ids []uint32
338338 for range 5 {
339339 id, err := c.NewXID()
@@ -367,7 +367,7 @@ func TestNewXIDRefill(t *testing.T) {
367367
368368 func TestNewXIDNoXCMisc(t *testing.T) {
369369 c, _ := dialScript(t, &script{})
370- c.xidLeft = 0
370+ c.xids = xidRange{}
371371 if _, err := c.NewXID(); !errors.Is(err, ErrExtMissing) {
372372 t.Fatal(err)
373373 }
@@ -383,7 +383,7 @@ func TestNewXIDRefillError(t *testing.T) {
383383 return false
384384 }
385385 c, _ := dialScript(t, sc)
386- c.xidLeft = 0
386+ c.xids = xidRange{}
387387 var xe *XError
388388 if _, err := c.NewXID(); !errors.As(err, &xe) || xe.Kind != kindXIDRange {
389389 t.Fatal(err)
@@ -273,7 +273,7 @@ func TestSyncFromCallback(t *testing.T) {
273 h := Handler{OnEvent: func(*Event) {273 h := Handler{OnEvent: func(*Event) {
274 errSync = c.Sync(nil)274 errSync = c.Sync(nil)
275 errAtoms = c.InternAtoms([]string{"X"}, false, make([]uint32, 1))275 errAtoms = c.InternAtoms([]string{"X"}, false, make([]uint32, 1))
276- c.xidLeft, c.xidFreeN = 0, 0276+ c.xids, c.xidFreeN = xidRange{}, 0
277 _, errXID = c.NewXID()277 _, errXID = c.NewXID()
278 }}278 }}
279 c.Buf().Request(8, 0, 4)279 c.Buf().Request(8, 0, 4)
@@ -333,7 +333,7 @@ func TestNewXIDRefill(t *testing.T) {
333 sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200040, 3}, {0, 1}}}333 sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200040, 3}, {0, 1}}}
334 c, srv := dialScript(t, sc)334 c, srv := dialScript(t, sc)
335 // Pretend only two ids of the setup range are left.335 // Pretend only two ids of the setup range are left.
336- c.xidLeft = 2336+ c.xids.left = 2
337 var ids []uint32337 var ids []uint32
338 for range 5 {338 for range 5 {
339 id, err := c.NewXID()339 id, err := c.NewXID()
@@ -367,7 +367,7 @@ func TestNewXIDRefill(t *testing.T) {
367 367
368 func TestNewXIDNoXCMisc(t *testing.T) {368 func TestNewXIDNoXCMisc(t *testing.T) {
369 c, _ := dialScript(t, &script{})369 c, _ := dialScript(t, &script{})
370- c.xidLeft = 0370+ c.xids = xidRange{}
371 if _, err := c.NewXID(); !errors.Is(err, ErrExtMissing) {371 if _, err := c.NewXID(); !errors.Is(err, ErrExtMissing) {
372 t.Fatal(err)372 t.Fatal(err)
373 }373 }
@@ -383,7 +383,7 @@ func TestNewXIDRefillError(t *testing.T) {
383 return false383 return false
384 }384 }
385 c, _ := dialScript(t, sc)385 c, _ := dialScript(t, sc)
386- c.xidLeft = 0386+ c.xids = xidRange{}
387 var xe *XError387 var xe *XError
388 if _, err := c.NewXID(); !errors.As(err, &xe) || xe.Kind != kindXIDRange {388 if _, err := c.NewXID(); !errors.As(err, &xe) || xe.Kind != kindXIDRange {
389 t.Fatal(err)389 t.Fatal(err)
M bootstrap_xserver_test.go +23 -0
@@ -207,6 +207,29 @@ func TestXserverXIDRefill(t *testing.T) {
207207 if err := c.Sync(&h); err != nil || xerr != nil {
208208 t.Fatal(err, xerr)
209209 }
210+ // Non-blocking prefetch: queued from the loop, used after the next
211+ // exhaustion without a blocking round trip.
212+ xturbo.ExhaustXIDs(c)
213+ if !c.XIDsLow() {
214+ t.Fatal("not low after exhaustion")
215+ }
216+ if err := c.PrefetchXIDs(); err != nil {
217+ t.Fatal(err)
218+ }
219+ if err := c.Sync(&h); err != nil || c.XIDsLow() {
220+ t.Fatal(err, c.XIDsLow())
221+ }
222+ // The server hands out its lowest free range, which may hold the id just
223+ // destroyed: check the id is usable, not that it is new.
224+ id2, err := c.NewXID()
225+ if err != nil {
226+ t.Fatal(err)
227+ }
228+ xproto.CreateWindow(c.Buf(), 0, xproto.Window(id2), xproto.Window(c.Root()), 0, 0, 1, 1, 0, xproto.WindowClassInputOutput, 0, &v)
229+ xproto.DestroyWindow(c.Buf(), xproto.Window(id2))
230+ if err := c.Sync(&h); err != nil || xerr != nil {
231+ t.Fatal(err, xerr)
232+ }
210233 }
211234
212235 // A long silent stream gets fences; the connection keeps working.
@@ -207,6 +207,29 @@ func TestXserverXIDRefill(t *testing.T) {
207 if err := c.Sync(&h); err != nil || xerr != nil {207 if err := c.Sync(&h); err != nil || xerr != nil {
208 t.Fatal(err, xerr)208 t.Fatal(err, xerr)
209 }209 }
210+ // Non-blocking prefetch: queued from the loop, used after the next
211+ // exhaustion without a blocking round trip.
212+ xturbo.ExhaustXIDs(c)
213+ if !c.XIDsLow() {
214+ t.Fatal("not low after exhaustion")
215+ }
216+ if err := c.PrefetchXIDs(); err != nil {
217+ t.Fatal(err)
218+ }
219+ if err := c.Sync(&h); err != nil || c.XIDsLow() {
220+ t.Fatal(err, c.XIDsLow())
221+ }
222+ // The server hands out its lowest free range, which may hold the id just
223+ // destroyed: check the id is usable, not that it is new.
224+ id2, err := c.NewXID()
225+ if err != nil {
226+ t.Fatal(err)
227+ }
228+ xproto.CreateWindow(c.Buf(), 0, xproto.Window(id2), xproto.Window(c.Root()), 0, 0, 1, 1, 0, xproto.WindowClassInputOutput, 0, &v)
229+ xproto.DestroyWindow(c.Buf(), xproto.Window(id2))
230+ if err := c.Sync(&h); err != nil || xerr != nil {
231+ t.Fatal(err, xerr)
232+ }
210 }233 }
211 234
212 // A long silent stream gets fences; the connection keeps working.235 // A long silent stream gets fences; the connection keeps working.
M conn.go +27 -22
@@ -1,7 +1,9 @@
11 package xturbo
22
33 import (
4+ "fmt"
45 "os"
6+ "time"
57
68 "golang.org/x/sys/unix"
79 )
@@ -15,21 +17,23 @@ type Conn struct {
1517 setup []byte
1618 root uint32
1719
18- xidBase, xidMask uint32 // resource-id allocation, used by NewXID
19- xidNext, xidLeft uint32 // current range: next id, ids left in it
20+ xidBase, xidMask uint32 // resource-id allocation, used by NewXID
21+ xids, xidPend xidRange // current range; prefetched range (left 0: none)
2022 xidFree [xidFreeCap]uint32
2123 xidFreeN int
22- // Last XC-MISC GetXIDRange reply, filled by consumeReply.
23- rangeNext, rangeLeft uint32
24- rangeGot bool
24+ xidReq Seq // last GetXIDRange sent
25+ xidNone bool // the last GetXIDRange gave no usable range
26+ xidFailed bool // ... or an X error, xidErr
27+ xidErr XError
2528
2629 // 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
30+ h *Handler // last non-nil Handler given to Dispatch or Sync
31+ rdIdle bool // the last Dispatch read nothing: the socket was empty
32+ atoms []uint32 // InternAtoms destination, indexed by slot Args[0]
33+ syncErr XError // first error on a reserved-kind request
34+ syncFailed bool // syncErr is set
35+ resync Seq // last GetInputFocus appended by Flush
36+ timeout time.Duration // bound of one Sync, InternAtoms or XID refill
3337
3438 // Read side, owned by Dispatch: rb[rpos:rend] is received, unconsumed.
3539 rb []byte
@@ -52,10 +56,12 @@ type Conn struct {
5256
5357 // NewConn takes ownership of fd, a connected stream socket to an X server,
5458 // and runs the connection setup without authorization (e.g. the -wm fd of an
55-// XWayland the caller spawned). The handshake blocks; afterwards fd is
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.
59+// XWayland the caller spawned). It blocks for the handshake, then one round
60+// trip negotiates every known extension (QueryExtension each, BIG-REQUESTS
61+// Enable when present) into Ext() and the Buf request limit. fd is left
62+// O_NONBLOCK. The whole call gives up after 10 s with an error wrapping
63+// os.ErrDeadlineExceeded (an alive but silent server). On failure fd is
64+// closed.
5965 func NewConn(fd int) (*Conn, error) {
6066 return newConn(fd, "", nil)
6167 }
@@ -70,19 +76,17 @@ func newConn(fd int, authName string, authData []byte) (*Conn, error) {
7076 }
7177
7278 func setupConn(fd int, authName string, authData []byte) (*Conn, error) {
73- if err := unix.SetNonblock(fd, false); err != nil {
79+ deadline := time.Now().Add(connectTimeout)
80+ if err := unix.SetNonblock(fd, true); err != nil {
7481 return nil, os.NewSyscallError("fcntl", err)
7582 }
76- setup, info, err := handshake(fd, authName, authData)
83+ setup, info, err := handshake(fd, authName, authData, deadline)
7784 if err != nil {
7885 return nil, err
7986 }
80- if err := unix.SetNonblock(fd, true); err != nil {
81- return nil, os.NewSyscallError("fcntl", err)
82- }
8387 c := newConnState(fd, setup, info)
84- if err := c.bootstrap(); err != nil {
85- return nil, err
88+ if err := c.bootstrap(deadline); err != nil {
89+ return nil, fmt.Errorf("xturbo: extension bootstrap: %w", err)
8690 }
8791 return c, nil
8892 }
@@ -98,6 +102,7 @@ func newConnState(fd int, setup []byte, info setupInfo) *Conn {
98102 xidMask: info.xidMask,
99103 rb: make([]byte, readBufSize),
100104 maxPacket: MaxPacket,
105+ timeout: syncTimeout,
101106 }
102107 c.buf.Ext = &c.ext
103108 c.initXIDs()
@@ -1,7 +1,9 @@
1 package xturbo1 package xturbo
2 2
3 import (3 import (
4+ "fmt"
4 "os"5 "os"
6+ "time"
5 7
6 "golang.org/x/sys/unix"8 "golang.org/x/sys/unix"
7 )9 )
@@ -15,21 +17,23 @@ type Conn struct {
15 setup []byte17 setup []byte
16 root uint3218 root uint32
17 19
18- xidBase, xidMask uint32 // resource-id allocation, used by NewXID20+ xidBase, xidMask uint32 // resource-id allocation, used by NewXID
19- xidNext, xidLeft uint32 // current range: next id, ids left in it21+ xids, xidPend xidRange // current range; prefetched range (left 0: none)
20 xidFree [xidFreeCap]uint3222 xidFree [xidFreeCap]uint32
21 xidFreeN int23 xidFreeN int
22- // Last XC-MISC GetXIDRange reply, filled by consumeReply.24+ xidReq Seq // last GetXIDRange sent
23- rangeNext, rangeLeft uint3225+ xidNone bool // the last GetXIDRange gave no usable range
24- rangeGot bool26+ xidFailed bool // ... or an X error, xidErr
27+ xidErr XError
25 28
26 // Blocking calls (Sync, InternAtoms, NewXID refill).29 // Blocking calls (Sync, InternAtoms, NewXID refill).
27- h *Handler // last non-nil Handler given to Dispatch or Sync30+ h *Handler // last non-nil Handler given to Dispatch or Sync
28- rdIdle bool // the last Dispatch read nothing: the socket was empty31+ rdIdle bool // the last Dispatch read nothing: the socket was empty
29- atoms []uint32 // InternAtoms destination, indexed by slot Args[0]32+ atoms []uint32 // InternAtoms destination, indexed by slot Args[0]
30- syncErr XError // first error on a reserved-kind request33+ syncErr XError // first error on a reserved-kind request
31- syncFailed bool // syncErr is set34+ syncFailed bool // syncErr is set
32- resync Seq // last GetInputFocus appended by Flush35+ resync Seq // last GetInputFocus appended by Flush
36+ timeout time.Duration // bound of one Sync, InternAtoms or XID refill
33 37
34 // Read side, owned by Dispatch: rb[rpos:rend] is received, unconsumed.38 // Read side, owned by Dispatch: rb[rpos:rend] is received, unconsumed.
35 rb []byte39 rb []byte
@@ -52,10 +56,12 @@ type Conn struct {
52 56
53 // NewConn takes ownership of fd, a connected stream socket to an X server,57 // NewConn takes ownership of fd, a connected stream socket to an X server,
54 // and runs the connection setup without authorization (e.g. the -wm fd of an58 // and runs the connection setup without authorization (e.g. the -wm fd of an
55-// XWayland the caller spawned). The handshake blocks; afterwards fd is59+// XWayland the caller spawned). It blocks for the handshake, then one round
56-// switched to O_NONBLOCK and one blocking round trip negotiates every known60+// trip negotiates every known extension (QueryExtension each, BIG-REQUESTS
57-// extension (QueryExtension each, BIG-REQUESTS Enable when present) into61+// Enable when present) into Ext() and the Buf request limit. fd is left
58-// Ext() and the Buf request limit. On failure fd is closed.62+// O_NONBLOCK. The whole call gives up after 10 s with an error wrapping
63+// os.ErrDeadlineExceeded (an alive but silent server). On failure fd is
64+// closed.
59 func NewConn(fd int) (*Conn, error) {65 func NewConn(fd int) (*Conn, error) {
60 return newConn(fd, "", nil)66 return newConn(fd, "", nil)
61 }67 }
@@ -70,19 +76,17 @@ func newConn(fd int, authName string, authData []byte) (*Conn, error) {
70 }76 }
71 77
72 func setupConn(fd int, authName string, authData []byte) (*Conn, error) {78 func setupConn(fd int, authName string, authData []byte) (*Conn, error) {
73- if err := unix.SetNonblock(fd, false); err != nil {79+ deadline := time.Now().Add(connectTimeout)
80+ if err := unix.SetNonblock(fd, true); err != nil {
74 return nil, os.NewSyscallError("fcntl", err)81 return nil, os.NewSyscallError("fcntl", err)
75 }82 }
76- setup, info, err := handshake(fd, authName, authData)83+ setup, info, err := handshake(fd, authName, authData, deadline)
77 if err != nil {84 if err != nil {
78 return nil, err85 return nil, err
79 }86 }
80- if err := unix.SetNonblock(fd, true); err != nil {
81- return nil, os.NewSyscallError("fcntl", err)
82- }
83 c := newConnState(fd, setup, info)87 c := newConnState(fd, setup, info)
84- if err := c.bootstrap(); err != nil {88+ if err := c.bootstrap(deadline); err != nil {
85- return nil, err89+ return nil, fmt.Errorf("xturbo: extension bootstrap: %w", err)
86 }90 }
87 return c, nil91 return c, nil
88 }92 }
@@ -98,6 +102,7 @@ func newConnState(fd int, setup []byte, info setupInfo) *Conn {
98 xidMask: info.xidMask,102 xidMask: info.xidMask,
99 rb: make([]byte, readBufSize),103 rb: make([]byte, readBufSize),
100 maxPacket: MaxPacket,104 maxPacket: MaxPacket,
105+ timeout: syncTimeout,
101 }106 }
102 c.buf.Ext = &c.ext107 c.buf.Ext = &c.ext
103 c.initXIDs()108 c.initXIDs()
M conn_test.go +2 -2
@@ -39,8 +39,8 @@ func serveSetup(t *testing.T, reply []byte, chunk int, hangup bool, sc *script)
3939 if err != nil {
4040 t.Fatal(err)
4141 }
42- // Exercise NewConn switching a non-blocking fd back to blocking.
43- if err := unix.SetNonblock(fds[0], true); err != nil {
42+ // A blocking fd: NewConn switches it to O_NONBLOCK itself.
43+ if err := unix.SetNonblock(fds[0], false); err != nil {
4444 t.Fatal(err)
4545 }
4646 server := os.NewFile(uintptr(fds[1]), "server")
@@ -39,8 +39,8 @@ func serveSetup(t *testing.T, reply []byte, chunk int, hangup bool, sc *script)
39 if err != nil {39 if err != nil {
40 t.Fatal(err)40 t.Fatal(err)
41 }41 }
42- // Exercise NewConn switching a non-blocking fd back to blocking.42+ // A blocking fd: NewConn switches it to O_NONBLOCK itself.
43- if err := unix.SetNonblock(fds[0], true); err != nil {43+ if err := unix.SetNonblock(fds[0], false); err != nil {
44 t.Fatal(err)44 t.Fatal(err)
45 }45 }
46 server := os.NewFile(uintptr(fds[1]), "server")46 server := os.NewFile(uintptr(fds[1]), "server")
M export_test.go +18 -1
@@ -1,11 +1,28 @@
11 package xturbo
22
3+import "time"
4+
35 // SetMaxReqWords exposes the Conn-owned BIG-REQUESTS limit setter to external tests.
46 var SetMaxReqWords = (*Buf).setMaxReqWords
57
8+// testTimeouts bounds every blocking call in tests so a broken script fails
9+// fast instead of hanging until the go test timeout; tests of the deadlines
10+// shorten them further with shortTimeouts.
11+func init() {
12+ connectTimeout = 3 * time.Second
13+ syncTimeout = 3 * time.Second
14+}
15+
16+// shortTimeouts sets d as both bounds for the test's duration.
17+func shortTimeouts(t interface{ Cleanup(func()) }, d time.Duration) {
18+ c, s := connectTimeout, syncTimeout
19+ connectTimeout, syncTimeout = d, d
20+ t.Cleanup(func() { connectTimeout, syncTimeout = c, s })
21+}
22+
623 // ExhaustXIDs empties the setup range and the free list so the next NewXID
724 // refills through XC-MISC.
8-func ExhaustXIDs(c *Conn) { c.xidLeft, c.xidFreeN = 0, 0 }
25+func ExhaustXIDs(c *Conn) { c.xids, c.xidFreeN = xidRange{}, 0 }
926
1027 // Root's own encoders of the requests it issues, compared with the generated
1128 // ones and the goldens by external tests.
@@ -1,11 +1,28 @@
1 package xturbo1 package xturbo
2 2
3+import "time"
4+
3 // SetMaxReqWords exposes the Conn-owned BIG-REQUESTS limit setter to external tests.5 // SetMaxReqWords exposes the Conn-owned BIG-REQUESTS limit setter to external tests.
4 var SetMaxReqWords = (*Buf).setMaxReqWords6 var SetMaxReqWords = (*Buf).setMaxReqWords
5 7
8+// testTimeouts bounds every blocking call in tests so a broken script fails
9+// fast instead of hanging until the go test timeout; tests of the deadlines
10+// shorten them further with shortTimeouts.
11+func init() {
12+ connectTimeout = 3 * time.Second
13+ syncTimeout = 3 * time.Second
14+}
15+
16+// shortTimeouts sets d as both bounds for the test's duration.
17+func shortTimeouts(t interface{ Cleanup(func()) }, d time.Duration) {
18+ c, s := connectTimeout, syncTimeout
19+ connectTimeout, syncTimeout = d, d
20+ t.Cleanup(func() { connectTimeout, syncTimeout = c, s })
21+}
22+
6 // ExhaustXIDs empties the setup range and the free list so the next NewXID23 // ExhaustXIDs empties the setup range and the free list so the next NewXID
7 // refills through XC-MISC.24 // refills through XC-MISC.
8-func ExhaustXIDs(c *Conn) { c.xidLeft, c.xidFreeN = 0, 0 }25+func ExhaustXIDs(c *Conn) { c.xids, c.xidFreeN = xidRange{}, 0 }
9 26
10 // Root's own encoders of the requests it issues, compared with the generated27 // Root's own encoders of the requests it issues, compared with the generated
11 // ones and the goldens by external tests.28 // ones and the goldens by external tests.
A hardening_test.go +358 -0
@@ -0,0 +1,358 @@
1+package xturbo
2+
3+import (
4+ "errors"
5+ "os"
6+ "strings"
7+ "testing"
8+ "time"
9+
10+ "golang.org/x/sys/unix"
11+)
12+
13+const shortDeadline = 150 * time.Millisecond
14+
15+// within runs f and fails unless it returns an error wrapping
16+// os.ErrDeadlineExceeded after about d (not much later).
17+func within(t *testing.T, d time.Duration, f func() error) {
18+ t.Helper()
19+ start := time.Now()
20+ err := f()
21+ took := time.Since(start)
22+ if !errors.Is(err, os.ErrDeadlineExceeded) {
23+ t.Fatalf("err %v, want a deadline error", err)
24+ }
25+ if took < d || took > d+2*time.Second {
26+ t.Fatalf("returned after %v, deadline %v", took, d)
27+ }
28+}
29+
30+// A server that accepts the connection but never answers the setup request.
31+func TestNewConnSetupDeadline(t *testing.T) {
32+ shortTimeouts(t, shortDeadline)
33+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
34+ if err != nil {
35+ t.Fatal(err)
36+ }
37+ defer unix.Close(fds[1])
38+ within(t, shortDeadline, func() error {
39+ c, err := NewConn(fds[0])
40+ if c != nil {
41+ t.Fatal("conn")
42+ }
43+ return err
44+ })
45+ if _, err := unix.FcntlInt(uintptr(fds[0]), unix.F_GETFD, 0); err != unix.EBADF {
46+ t.Fatal("fd not closed")
47+ }
48+}
49+
50+// A server that completes the setup, then hangs during the bootstrap.
51+func TestNewConnBootstrapDeadline(t *testing.T) {
52+ shortTimeouts(t, shortDeadline)
53+ sc := &script{}
54+ sc.silent.Store(true)
55+ fd, _ := startScript(t, sc)
56+ within(t, shortDeadline, func() error { _, err := NewConn(fd); return err })
57+}
58+
59+func TestSyncDeadline(t *testing.T) {
60+ sc := &script{exts: fullExts()}
61+ c, _ := dialScript(t, sc)
62+ sc.silent.Store(true)
63+ c.timeout = shortDeadline
64+ within(t, shortDeadline, func() error { return c.Sync(nil) })
65+ within(t, shortDeadline, func() error { return c.InternAtoms([]string{"A"}, false, make([]uint32, 1)) })
66+ c.xids = xidRange{}
67+ within(t, shortDeadline, func() error { _, err := c.NewXID(); return err })
68+}
69+
70+// A server flooding events but never answering the fence still times out.
71+func TestSyncDeadlineFlood(t *testing.T) {
72+ sc := &script{}
73+ c, srv := dialScript(t, sc)
74+ c.timeout = shortDeadline
75+ sc.silent.Store(true)
76+ // Events carry the last request the server processed: the bootstrap's.
77+ ev := pktEvent(19, uint16(c.seq), 0)
78+ stop := make(chan struct{})
79+ done := make(chan struct{})
80+ go func() {
81+ defer close(done)
82+ for {
83+ select {
84+ case <-stop:
85+ return
86+ default:
87+ // Written raw: the silent server goroutine owns srv.seq.
88+ if _, err := srv.f.Write(ev); err != nil {
89+ return
90+ }
91+ }
92+ }
93+ }()
94+ defer func() { close(stop); <-done }()
95+ within(t, shortDeadline, func() error { return c.Sync(nil) })
96+}
97+
98+// A GetXIDRange answer whose last ID overflows the mask (into other clients'
99+// IDs) is rejected.
100+func TestXIDRangeBounds(t *testing.T) {
101+ c, _ := dialScript(t, &script{})
102+ // Recorded setup: base 0x00200000, mask 0x001fffff (inc 1).
103+ for _, tc := range []struct {
104+ start, count uint32
105+ ok bool
106+ }{
107+ {0x00200000, 0xffffffff, false},
108+ {0x00200000, 0x00200001, false},
109+ {0x00200000, 0x00200000, true},
110+ {0x003fffff, 1, true},
111+ {0x003fffff, 2, false},
112+ {0x00400000, 1, false}, // other client's base
113+ {0, 1, false},
114+ {0x00200040, 0, false},
115+ } {
116+ if got := c.validXIDRange(tc.start, tc.count); got != tc.ok {
117+ t.Errorf("{%#x, %#x}: %v", tc.start, tc.count, got)
118+ }
119+ }
120+ // A mask with inc > 1: the start must be aligned.
121+ c.xidMask = 0x001ffff0
122+ if c.validXIDRange(0x00200008, 1) || !c.validXIDRange(0x00200010, 2) || c.validXIDRange(0x001ffff0|0x00200000, 2) {
123+ t.Fatal("inc 16")
124+ }
125+}
126+
127+func TestNewXIDRejectsOverflowingRange(t *testing.T) {
128+ sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200000, 0xffffffff}}}
129+ c, _ := dialScript(t, sc)
130+ c.xids = xidRange{}
131+ if _, err := c.NewXID(); !errors.Is(err, errXIDsExhausted) {
132+ t.Fatal(err)
133+ }
134+}
135+
136+// XIDsLow + PrefetchXIDs let a callback allocate past the end of the current
137+// range without blocking.
138+func TestPrefetchXIDsFromCallback(t *testing.T) {
139+ sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200100, 3}}}
140+ sc.hook = func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool {
141+ if major == 8 {
142+ s.event(19, 0)
143+ return true
144+ }
145+ return false
146+ }
147+ c, srv := dialScript(t, sc)
148+ c.xids = xidRange{next: 0x10, left: 2}
149+ if !c.XIDsLow() {
150+ t.Fatal("not low")
151+ }
152+ var got []uint32
153+ var errs []error
154+ h := Handler{OnEvent: func(*Event) {
155+ // Inside a callback: the prefetch is queued, no wait.
156+ if c.XIDsLow() {
157+ if err := c.PrefetchXIDs(); err != nil {
158+ errs = append(errs, err)
159+ }
160+ }
161+ }}
162+ c.Buf().Request(8, 0, 4)
163+ if err := c.Sync(&h); err != nil {
164+ t.Fatal(err)
165+ }
166+ if len(errs) != 0 || c.xidPend.left != 3 || c.XIDsLow() {
167+ t.Fatal(errs, c.xidPend, c.XIDsLow())
168+ }
169+ // Second event: allocate 5 ids inside the callback, crossing ranges.
170+ h.OnEvent = func(*Event) {
171+ for range 5 {
172+ id, err := c.NewXID()
173+ if err != nil {
174+ errs = append(errs, err)
175+ return
176+ }
177+ got = append(got, id)
178+ }
179+ // Now everything is used; NewXID cannot block here.
180+ if _, err := c.NewXID(); !errors.Is(err, errReentrantDispatch) {
181+ errs = append(errs, err)
182+ }
183+ }
184+ c.Buf().Request(8, 0, 4)
185+ if err := c.Sync(&h); err != nil {
186+ t.Fatal(err)
187+ }
188+ want := []uint32{0x00200010, 0x00200011, 0x00200100, 0x00200101, 0x00200102}
189+ if len(errs) != 0 || len(got) != 5 {
190+ t.Fatal(errs, got)
191+ }
192+ for i := range want {
193+ if got[i] != want[i] {
194+ t.Fatalf("%#x", got)
195+ }
196+ }
197+ var ranges int
198+ for _, r := range srv.requests() {
199+ if r.major == 136 && r.minor == minorGetXIDRange {
200+ ranges++
201+ }
202+ }
203+ if ranges != 1 {
204+ t.Fatal("GetXIDRange requests", ranges)
205+ }
206+}
207+
208+func TestPrefetchXIDsStates(t *testing.T) {
209+ sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0, 1}}}
210+ c, srv := dialScript(t, sc)
211+ if c.XIDsLow() {
212+ t.Fatal("fresh setup range reported low")
213+ }
214+ c.xids = xidRange{next: 0x10, left: 1}
215+ before := len(srv.requests())
216+ // Twice: one request only while in flight.
217+ first, second := c.PrefetchXIDs(), c.PrefetchXIDs()
218+ if first != nil || second != nil || c.XIDsLow() {
219+ t.Fatal("prefetch", first, second)
220+ }
221+ if err := c.Sync(nil); err != nil {
222+ t.Fatal(err)
223+ }
224+ if n := len(srv.requests()) - before; n != 2 { // GetXIDRange + fence
225+ t.Fatal(n)
226+ }
227+ // The server had none: not low (nothing to gain), current id still works,
228+ // then NewXID's blocking refill reports exhaustion.
229+ if c.XIDsLow() {
230+ t.Fatal("low after a refused prefetch")
231+ }
232+ if _, err := c.NewXID(); err != nil {
233+ t.Fatal(err)
234+ }
235+ if _, err := c.NewXID(); !errors.Is(err, errXIDsExhausted) {
236+ t.Fatal(err)
237+ }
238+ // Without XC-MISC.
239+ c2, _ := dialScript(t, &script{})
240+ c2.xids = xidRange{}
241+ if c2.XIDsLow() || !errors.Is(c2.PrefetchXIDs(), ErrExtMissing) {
242+ t.Fatal("no XC-MISC")
243+ }
244+}
245+
246+// A blocking refill waits for a prefetch already in flight instead of sending
247+// a second request; an X error on it is returned by NewXID, not OnError.
248+func TestRefillWaitsForPrefetch(t *testing.T) {
249+ sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200200, 4}}}
250+ c, srv := dialScript(t, sc)
251+ c.xids = xidRange{}
252+ if err := c.PrefetchXIDs(); err != nil {
253+ t.Fatal(err)
254+ }
255+ id, err := c.NewXID()
256+ if err != nil || id != 0x00200200 {
257+ t.Fatalf("%#x %v", id, err)
258+ }
259+ var ranges int
260+ for _, r := range srv.requests() {
261+ if r.major == 136 && r.minor == minorGetXIDRange {
262+ ranges++
263+ }
264+ }
265+ if ranges != 1 {
266+ t.Fatal(ranges)
267+ }
268+
269+ esc := &script{exts: fullExts()}
270+ esc.hook = func(s *scriptConn, major, minor uint8, _ uint16, _ []byte) bool {
271+ if major == 136 && minor == minorGetXIDRange {
272+ s.xerror(11, major, 0)
273+ return true
274+ }
275+ return false
276+ }
277+ ec, _ := dialScript(t, esc)
278+ var userErrs int
279+ h := Handler{OnError: func(*XError) { userErrs++ }}
280+ ec.xids = xidRange{}
281+ if err := ec.PrefetchXIDs(); err != nil {
282+ t.Fatal(err)
283+ }
284+ if err := ec.Sync(&h); err != nil {
285+ t.Fatal("a prefetch error leaked into Sync:", err)
286+ }
287+ var xe *XError
288+ if _, err := ec.NewXID(); !errors.As(err, &xe) || xe.Kind != kindXIDRange || userErrs != 0 {
289+ t.Fatal(err, userErrs)
290+ }
291+}
292+
293+// Steady state: XIDsLow does not allocate.
294+func TestAllocXIDsLow(t *testing.T) {
295+ c, _ := dialScript(t, &script{exts: fullExts()})
296+ if n := testing.AllocsPerRun(1000, func() { _ = c.XIDsLow() }); n != 0 {
297+ t.Fatal(n)
298+ }
299+}
300+
301+// An over-long name is a caller bug: ErrBadLength, nothing queued, the Buf
302+// still usable.
303+func TestInternAtomsNameTooLong(t *testing.T) {
304+ c, _ := dialScript(t, &script{})
305+ next := c.Buf().Next()
306+ err := c.InternAtoms([]string{"OK", strings.Repeat("x", 0x10000)}, false, make([]uint32, 2))
307+ if !errors.Is(err, ErrBadLength) || c.Buf().Err() != nil || c.Buf().Next() != next {
308+ t.Fatal(err, c.Buf().Err())
309+ }
310+ dst := make([]uint32, 1)
311+ if err := c.InternAtoms([]string{"OK"}, false, dst); err != nil || dst[0] == 0 {
312+ t.Fatal(err)
313+ }
314+}
315+
316+// 0xffff requests queued without a Flush reach ErrSeqOverflow (the auto-resync
317+// fence needs a Flush to be appended); with a Flush every so often the fence
318+// keeps the stream going far past 0xffff.
319+func TestAutoResyncVsSeqOverflow(t *testing.T) {
320+ c, _ := dialScript(t, &script{})
321+ b := c.Buf()
322+ for b.Err() == nil {
323+ b.Request(8, 0, 4)
324+ }
325+ if !errors.Is(b.Err(), ErrSeqOverflow) || b.Next()-b.acked != maxSeqAhead {
326+ t.Fatal(b.Err(), b.Next()-b.acked)
327+ }
328+ if c.Stats().Resyncs != 0 {
329+ t.Fatal(c.Stats())
330+ }
331+
332+ c2, _ := dialScript(t, &script{})
333+ b2 := c2.Buf()
334+ for i := range 3 * 0x10000 {
335+ b2.Request(8, 0, 4)
336+ if i%4096 == 0 {
337+ if err := flushAll(c2); err != nil {
338+ t.Fatal(err)
339+ }
340+ if err := c2.Dispatch(nil); err != nil {
341+ t.Fatal(err)
342+ }
343+ // Give the fence's reply time to come back before overflow.
344+ if b2.Next()-b2.acked > 0xe000 {
345+ dispatchFor(t, c2, func() bool { return b2.Next()-b2.acked < 0x9000 })
346+ }
347+ }
348+ }
349+ if b2.Err() != nil {
350+ t.Fatal(b2.Err())
351+ }
352+ if err := c2.Sync(nil); err != nil {
353+ t.Fatal(err)
354+ }
355+ if st := c2.Stats(); st.Resyncs < 3 || st.DroppedReplies != 0 {
356+ t.Fatalf("%+v", st)
357+ }
358+}
@@ -0,0 +1,358 @@
1+package xturbo
2+
3+import (
4+ "errors"
5+ "os"
6+ "strings"
7+ "testing"
8+ "time"
9+
10+ "golang.org/x/sys/unix"
11+)
12+
13+const shortDeadline = 150 * time.Millisecond
14+
15+// within runs f and fails unless it returns an error wrapping
16+// os.ErrDeadlineExceeded after about d (not much later).
17+func within(t *testing.T, d time.Duration, f func() error) {
18+ t.Helper()
19+ start := time.Now()
20+ err := f()
21+ took := time.Since(start)
22+ if !errors.Is(err, os.ErrDeadlineExceeded) {
23+ t.Fatalf("err %v, want a deadline error", err)
24+ }
25+ if took < d || took > d+2*time.Second {
26+ t.Fatalf("returned after %v, deadline %v", took, d)
27+ }
28+}
29+
30+// A server that accepts the connection but never answers the setup request.
31+func TestNewConnSetupDeadline(t *testing.T) {
32+ shortTimeouts(t, shortDeadline)
33+ fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
34+ if err != nil {
35+ t.Fatal(err)
36+ }
37+ defer unix.Close(fds[1])
38+ within(t, shortDeadline, func() error {
39+ c, err := NewConn(fds[0])
40+ if c != nil {
41+ t.Fatal("conn")
42+ }
43+ return err
44+ })
45+ if _, err := unix.FcntlInt(uintptr(fds[0]), unix.F_GETFD, 0); err != unix.EBADF {
46+ t.Fatal("fd not closed")
47+ }
48+}
49+
50+// A server that completes the setup, then hangs during the bootstrap.
51+func TestNewConnBootstrapDeadline(t *testing.T) {
52+ shortTimeouts(t, shortDeadline)
53+ sc := &script{}
54+ sc.silent.Store(true)
55+ fd, _ := startScript(t, sc)
56+ within(t, shortDeadline, func() error { _, err := NewConn(fd); return err })
57+}
58+
59+func TestSyncDeadline(t *testing.T) {
60+ sc := &script{exts: fullExts()}
61+ c, _ := dialScript(t, sc)
62+ sc.silent.Store(true)
63+ c.timeout = shortDeadline
64+ within(t, shortDeadline, func() error { return c.Sync(nil) })
65+ within(t, shortDeadline, func() error { return c.InternAtoms([]string{"A"}, false, make([]uint32, 1)) })
66+ c.xids = xidRange{}
67+ within(t, shortDeadline, func() error { _, err := c.NewXID(); return err })
68+}
69+
70+// A server flooding events but never answering the fence still times out.
71+func TestSyncDeadlineFlood(t *testing.T) {
72+ sc := &script{}
73+ c, srv := dialScript(t, sc)
74+ c.timeout = shortDeadline
75+ sc.silent.Store(true)
76+ // Events carry the last request the server processed: the bootstrap's.
77+ ev := pktEvent(19, uint16(c.seq), 0)
78+ stop := make(chan struct{})
79+ done := make(chan struct{})
80+ go func() {
81+ defer close(done)
82+ for {
83+ select {
84+ case <-stop:
85+ return
86+ default:
87+ // Written raw: the silent server goroutine owns srv.seq.
88+ if _, err := srv.f.Write(ev); err != nil {
89+ return
90+ }
91+ }
92+ }
93+ }()
94+ defer func() { close(stop); <-done }()
95+ within(t, shortDeadline, func() error { return c.Sync(nil) })
96+}
97+
98+// A GetXIDRange answer whose last ID overflows the mask (into other clients'
99+// IDs) is rejected.
100+func TestXIDRangeBounds(t *testing.T) {
101+ c, _ := dialScript(t, &script{})
102+ // Recorded setup: base 0x00200000, mask 0x001fffff (inc 1).
103+ for _, tc := range []struct {
104+ start, count uint32
105+ ok bool
106+ }{
107+ {0x00200000, 0xffffffff, false},
108+ {0x00200000, 0x00200001, false},
109+ {0x00200000, 0x00200000, true},
110+ {0x003fffff, 1, true},
111+ {0x003fffff, 2, false},
112+ {0x00400000, 1, false}, // other client's base
113+ {0, 1, false},
114+ {0x00200040, 0, false},
115+ } {
116+ if got := c.validXIDRange(tc.start, tc.count); got != tc.ok {
117+ t.Errorf("{%#x, %#x}: %v", tc.start, tc.count, got)
118+ }
119+ }
120+ // A mask with inc > 1: the start must be aligned.
121+ c.xidMask = 0x001ffff0
122+ if c.validXIDRange(0x00200008, 1) || !c.validXIDRange(0x00200010, 2) || c.validXIDRange(0x001ffff0|0x00200000, 2) {
123+ t.Fatal("inc 16")
124+ }
125+}
126+
127+func TestNewXIDRejectsOverflowingRange(t *testing.T) {
128+ sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200000, 0xffffffff}}}
129+ c, _ := dialScript(t, sc)
130+ c.xids = xidRange{}
131+ if _, err := c.NewXID(); !errors.Is(err, errXIDsExhausted) {
132+ t.Fatal(err)
133+ }
134+}
135+
136+// XIDsLow + PrefetchXIDs let a callback allocate past the end of the current
137+// range without blocking.
138+func TestPrefetchXIDsFromCallback(t *testing.T) {
139+ sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200100, 3}}}
140+ sc.hook = func(s *scriptConn, major, _ uint8, _ uint16, _ []byte) bool {
141+ if major == 8 {
142+ s.event(19, 0)
143+ return true
144+ }
145+ return false
146+ }
147+ c, srv := dialScript(t, sc)
148+ c.xids = xidRange{next: 0x10, left: 2}
149+ if !c.XIDsLow() {
150+ t.Fatal("not low")
151+ }
152+ var got []uint32
153+ var errs []error
154+ h := Handler{OnEvent: func(*Event) {
155+ // Inside a callback: the prefetch is queued, no wait.
156+ if c.XIDsLow() {
157+ if err := c.PrefetchXIDs(); err != nil {
158+ errs = append(errs, err)
159+ }
160+ }
161+ }}
162+ c.Buf().Request(8, 0, 4)
163+ if err := c.Sync(&h); err != nil {
164+ t.Fatal(err)
165+ }
166+ if len(errs) != 0 || c.xidPend.left != 3 || c.XIDsLow() {
167+ t.Fatal(errs, c.xidPend, c.XIDsLow())
168+ }
169+ // Second event: allocate 5 ids inside the callback, crossing ranges.
170+ h.OnEvent = func(*Event) {
171+ for range 5 {
172+ id, err := c.NewXID()
173+ if err != nil {
174+ errs = append(errs, err)
175+ return
176+ }
177+ got = append(got, id)
178+ }
179+ // Now everything is used; NewXID cannot block here.
180+ if _, err := c.NewXID(); !errors.Is(err, errReentrantDispatch) {
181+ errs = append(errs, err)
182+ }
183+ }
184+ c.Buf().Request(8, 0, 4)
185+ if err := c.Sync(&h); err != nil {
186+ t.Fatal(err)
187+ }
188+ want := []uint32{0x00200010, 0x00200011, 0x00200100, 0x00200101, 0x00200102}
189+ if len(errs) != 0 || len(got) != 5 {
190+ t.Fatal(errs, got)
191+ }
192+ for i := range want {
193+ if got[i] != want[i] {
194+ t.Fatalf("%#x", got)
195+ }
196+ }
197+ var ranges int
198+ for _, r := range srv.requests() {
199+ if r.major == 136 && r.minor == minorGetXIDRange {
200+ ranges++
201+ }
202+ }
203+ if ranges != 1 {
204+ t.Fatal("GetXIDRange requests", ranges)
205+ }
206+}
207+
208+func TestPrefetchXIDsStates(t *testing.T) {
209+ sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0, 1}}}
210+ c, srv := dialScript(t, sc)
211+ if c.XIDsLow() {
212+ t.Fatal("fresh setup range reported low")
213+ }
214+ c.xids = xidRange{next: 0x10, left: 1}
215+ before := len(srv.requests())
216+ // Twice: one request only while in flight.
217+ first, second := c.PrefetchXIDs(), c.PrefetchXIDs()
218+ if first != nil || second != nil || c.XIDsLow() {
219+ t.Fatal("prefetch", first, second)
220+ }
221+ if err := c.Sync(nil); err != nil {
222+ t.Fatal(err)
223+ }
224+ if n := len(srv.requests()) - before; n != 2 { // GetXIDRange + fence
225+ t.Fatal(n)
226+ }
227+ // The server had none: not low (nothing to gain), current id still works,
228+ // then NewXID's blocking refill reports exhaustion.
229+ if c.XIDsLow() {
230+ t.Fatal("low after a refused prefetch")
231+ }
232+ if _, err := c.NewXID(); err != nil {
233+ t.Fatal(err)
234+ }
235+ if _, err := c.NewXID(); !errors.Is(err, errXIDsExhausted) {
236+ t.Fatal(err)
237+ }
238+ // Without XC-MISC.
239+ c2, _ := dialScript(t, &script{})
240+ c2.xids = xidRange{}
241+ if c2.XIDsLow() || !errors.Is(c2.PrefetchXIDs(), ErrExtMissing) {
242+ t.Fatal("no XC-MISC")
243+ }
244+}
245+
246+// A blocking refill waits for a prefetch already in flight instead of sending
247+// a second request; an X error on it is returned by NewXID, not OnError.
248+func TestRefillWaitsForPrefetch(t *testing.T) {
249+ sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200200, 4}}}
250+ c, srv := dialScript(t, sc)
251+ c.xids = xidRange{}
252+ if err := c.PrefetchXIDs(); err != nil {
253+ t.Fatal(err)
254+ }
255+ id, err := c.NewXID()
256+ if err != nil || id != 0x00200200 {
257+ t.Fatalf("%#x %v", id, err)
258+ }
259+ var ranges int
260+ for _, r := range srv.requests() {
261+ if r.major == 136 && r.minor == minorGetXIDRange {
262+ ranges++
263+ }
264+ }
265+ if ranges != 1 {
266+ t.Fatal(ranges)
267+ }
268+
269+ esc := &script{exts: fullExts()}
270+ esc.hook = func(s *scriptConn, major, minor uint8, _ uint16, _ []byte) bool {
271+ if major == 136 && minor == minorGetXIDRange {
272+ s.xerror(11, major, 0)
273+ return true
274+ }
275+ return false
276+ }
277+ ec, _ := dialScript(t, esc)
278+ var userErrs int
279+ h := Handler{OnError: func(*XError) { userErrs++ }}
280+ ec.xids = xidRange{}
281+ if err := ec.PrefetchXIDs(); err != nil {
282+ t.Fatal(err)
283+ }
284+ if err := ec.Sync(&h); err != nil {
285+ t.Fatal("a prefetch error leaked into Sync:", err)
286+ }
287+ var xe *XError
288+ if _, err := ec.NewXID(); !errors.As(err, &xe) || xe.Kind != kindXIDRange || userErrs != 0 {
289+ t.Fatal(err, userErrs)
290+ }
291+}
292+
293+// Steady state: XIDsLow does not allocate.
294+func TestAllocXIDsLow(t *testing.T) {
295+ c, _ := dialScript(t, &script{exts: fullExts()})
296+ if n := testing.AllocsPerRun(1000, func() { _ = c.XIDsLow() }); n != 0 {
297+ t.Fatal(n)
298+ }
299+}
300+
301+// An over-long name is a caller bug: ErrBadLength, nothing queued, the Buf
302+// still usable.
303+func TestInternAtomsNameTooLong(t *testing.T) {
304+ c, _ := dialScript(t, &script{})
305+ next := c.Buf().Next()
306+ err := c.InternAtoms([]string{"OK", strings.Repeat("x", 0x10000)}, false, make([]uint32, 2))
307+ if !errors.Is(err, ErrBadLength) || c.Buf().Err() != nil || c.Buf().Next() != next {
308+ t.Fatal(err, c.Buf().Err())
309+ }
310+ dst := make([]uint32, 1)
311+ if err := c.InternAtoms([]string{"OK"}, false, dst); err != nil || dst[0] == 0 {
312+ t.Fatal(err)
313+ }
314+}
315+
316+// 0xffff requests queued without a Flush reach ErrSeqOverflow (the auto-resync
317+// fence needs a Flush to be appended); with a Flush every so often the fence
318+// keeps the stream going far past 0xffff.
319+func TestAutoResyncVsSeqOverflow(t *testing.T) {
320+ c, _ := dialScript(t, &script{})
321+ b := c.Buf()
322+ for b.Err() == nil {
323+ b.Request(8, 0, 4)
324+ }
325+ if !errors.Is(b.Err(), ErrSeqOverflow) || b.Next()-b.acked != maxSeqAhead {
326+ t.Fatal(b.Err(), b.Next()-b.acked)
327+ }
328+ if c.Stats().Resyncs != 0 {
329+ t.Fatal(c.Stats())
330+ }
331+
332+ c2, _ := dialScript(t, &script{})
333+ b2 := c2.Buf()
334+ for i := range 3 * 0x10000 {
335+ b2.Request(8, 0, 4)
336+ if i%4096 == 0 {
337+ if err := flushAll(c2); err != nil {
338+ t.Fatal(err)
339+ }
340+ if err := c2.Dispatch(nil); err != nil {
341+ t.Fatal(err)
342+ }
343+ // Give the fence's reply time to come back before overflow.
344+ if b2.Next()-b2.acked > 0xe000 {
345+ dispatchFor(t, c2, func() bool { return b2.Next()-b2.acked < 0x9000 })
346+ }
347+ }
348+ }
349+ if b2.Err() != nil {
350+ t.Fatal(b2.Err())
351+ }
352+ if err := c2.Sync(nil); err != nil {
353+ t.Fatal(err)
354+ }
355+ if st := c2.Stats(); st.Resyncs < 3 || st.DroppedReplies != 0 {
356+ t.Fatalf("%+v", st)
357+ }
358+}
M ring.go +9 -5
@@ -152,18 +152,22 @@ func (c *Conn) consumeReply(r *Reply) {
152152 c.buf.setMaxReqWords(words)
153153 }
154154 case kindXIDRange:
155- c.rangeNext = binary.LittleEndian.Uint32(r.Raw[8:])
156- c.rangeLeft = binary.LittleEndian.Uint32(r.Raw[12:])
157- c.rangeGot = true
155+ c.gotXIDRange(binary.LittleEndian.Uint32(r.Raw[8:]), binary.LittleEndian.Uint32(r.Raw[12:]))
158156 }
159157 }
160158
161159 // consumeError handles an error whose request was registered with a reserved
162160 // kind (1..15) and reports whether it consumed it; an unconsumed error is
163161 // 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.
162+// consumed: the first is kept for the blocking call (Sync, InternAtoms) that
163+// issued the request, which returns it as *XError; a GetXIDRange error (maybe
164+// from PrefetchXIDs) is kept for NewXID.
166165 func (c *Conn) consumeError(e *XError) bool {
166+ if e.Kind == kindXIDRange {
167+ // Possibly a prefetch: kept apart from the error of a Sync.
168+ c.xidNone, c.xidFailed, c.xidErr = true, true, *e
169+ return true
170+ }
167171 if !c.syncFailed {
168172 c.syncFailed = true
169173 c.syncErr = *e
@@ -152,18 +152,22 @@ func (c *Conn) consumeReply(r *Reply) {
152 c.buf.setMaxReqWords(words)152 c.buf.setMaxReqWords(words)
153 }153 }
154 case kindXIDRange:154 case kindXIDRange:
155- c.rangeNext = binary.LittleEndian.Uint32(r.Raw[8:])155+ c.gotXIDRange(binary.LittleEndian.Uint32(r.Raw[8:]), binary.LittleEndian.Uint32(r.Raw[12:]))
156- c.rangeLeft = binary.LittleEndian.Uint32(r.Raw[12:])
157- c.rangeGot = true
158 }156 }
159 }157 }
160 158
161 // consumeError handles an error whose request was registered with a reserved159 // consumeError handles an error whose request was registered with a reserved
162 // kind (1..15) and reports whether it consumed it; an unconsumed error is160 // 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 are161 // 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,162+// consumed: the first is kept for the blocking call (Sync, InternAtoms) that
165-// NewXID) that issued the request, which returns it as *XError.163+// issued the request, which returns it as *XError; a GetXIDRange error (maybe
164+// from PrefetchXIDs) is kept for NewXID.
166 func (c *Conn) consumeError(e *XError) bool {165 func (c *Conn) consumeError(e *XError) bool {
166+ if e.Kind == kindXIDRange {
167+ // Possibly a prefetch: kept apart from the error of a Sync.
168+ c.xidNone, c.xidFailed, c.xidErr = true, true, *e
169+ return true
170+ }
167 if !c.syncFailed {171 if !c.syncFailed {
168 c.syncFailed = true172 c.syncFailed = true
169 c.syncErr = *e173 c.syncErr = *e
M setup.go +30 -8
@@ -5,6 +5,7 @@ import (
55 "fmt"
66 "io"
77 "os"
8+ "time"
89
910 "golang.org/x/sys/unix"
1011 )
@@ -46,22 +47,24 @@ func encodeSetupRequest(authName string, authData []byte) []byte {
4647
4748 func pad4(n int) int { return (n + 3) &^ 3 }
4849
49-// handshake runs the blocking setup exchange on fd and returns the full setup
50-// reply. A refusal is a *SetupError; a broken reply wraps ErrMalformed.
51-func handshake(fd int, authName string, authData []byte) ([]byte, setupInfo, error) {
50+// handshake runs the setup exchange on the non-blocking fd, waiting with poll
51+// until deadline, and returns the full setup reply. A refusal is a
52+// *SetupError; a broken reply wraps ErrMalformed; a silent server an error
53+// wrapping os.ErrDeadlineExceeded.
54+func handshake(fd int, authName string, authData []byte, deadline time.Time) ([]byte, setupInfo, error) {
5255 if len(authName) > 0xffff || len(authData) > 0xffff {
5356 return nil, setupInfo{}, ErrBadLength
5457 }
55- if err := writeFull(fd, encodeSetupRequest(authName, authData)); err != nil {
58+ if err := writeFull(fd, encodeSetupRequest(authName, authData), deadline); err != nil {
5659 return nil, setupInfo{}, err
5760 }
5861 var hdr [setupHeaderSize]byte
59- if err := readFull(fd, hdr[:]); err != nil {
62+ if err := readFull(fd, hdr[:], deadline); err != nil {
6063 return nil, setupInfo{}, err
6164 }
6265 reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:])))
6366 copy(reply, hdr[:])
64- if err := readFull(fd, reply[setupHeaderSize:]); err != nil {
67+ if err := readFull(fd, reply[setupHeaderSize:], deadline); err != nil {
6568 return nil, setupInfo{}, err
6669 }
6770 info, err := parseSetup(reply)
@@ -141,12 +144,26 @@ func parseSetup(p []byte) (setupInfo, error) {
141144 return info, nil
142145 }
143146
144-func writeFull(fd int, p []byte) error {
147+// waitFd polls fd for events until deadline.
148+func waitFd(fd int, events int16, deadline time.Time) error {
149+ left := time.Until(deadline)
150+ if left <= 0 {
151+ return deadlineError("connection setup")
152+ }
153+ return pollFd(fd, events, left)
154+}
155+
156+func writeFull(fd int, p []byte, deadline time.Time) error {
145157 for len(p) > 0 {
146158 n, err := unix.Write(fd, p)
147159 switch {
148160 case err == unix.EINTR:
149161 continue
162+ case err == unix.EAGAIN:
163+ if err := waitFd(fd, unix.POLLOUT, deadline); err != nil {
164+ return err
165+ }
166+ continue
150167 case err == unix.EPIPE || err == unix.ECONNRESET:
151168 return ErrClosed
152169 case err != nil:
@@ -159,12 +176,17 @@ func writeFull(fd int, p []byte) error {
159176 return nil
160177 }
161178
162-func readFull(fd int, p []byte) error {
179+func readFull(fd int, p []byte, deadline time.Time) error {
163180 for len(p) > 0 {
164181 n, err := unix.Read(fd, p)
165182 switch {
166183 case err == unix.EINTR:
167184 continue
185+ case err == unix.EAGAIN:
186+ if err := waitFd(fd, unix.POLLIN, deadline); err != nil {
187+ return err
188+ }
189+ continue
168190 case err == unix.ECONNRESET:
169191 return ErrClosed
170192 case err != nil:
@@ -5,6 +5,7 @@ import (
5 "fmt"5 "fmt"
6 "io"6 "io"
7 "os"7 "os"
8+ "time"
8 9
9 "golang.org/x/sys/unix"10 "golang.org/x/sys/unix"
10 )11 )
@@ -46,22 +47,24 @@ func encodeSetupRequest(authName string, authData []byte) []byte {
46 47
47 func pad4(n int) int { return (n + 3) &^ 3 }48 func pad4(n int) int { return (n + 3) &^ 3 }
48 49
49-// handshake runs the blocking setup exchange on fd and returns the full setup50+// handshake runs the setup exchange on the non-blocking fd, waiting with poll
50-// reply. A refusal is a *SetupError; a broken reply wraps ErrMalformed.51+// until deadline, and returns the full setup reply. A refusal is a
51-func handshake(fd int, authName string, authData []byte) ([]byte, setupInfo, error) {52+// *SetupError; a broken reply wraps ErrMalformed; a silent server an error
53+// wrapping os.ErrDeadlineExceeded.
54+func handshake(fd int, authName string, authData []byte, deadline time.Time) ([]byte, setupInfo, error) {
52 if len(authName) > 0xffff || len(authData) > 0xffff {55 if len(authName) > 0xffff || len(authData) > 0xffff {
53 return nil, setupInfo{}, ErrBadLength56 return nil, setupInfo{}, ErrBadLength
54 }57 }
55- if err := writeFull(fd, encodeSetupRequest(authName, authData)); err != nil {58+ if err := writeFull(fd, encodeSetupRequest(authName, authData), deadline); err != nil {
56 return nil, setupInfo{}, err59 return nil, setupInfo{}, err
57 }60 }
58 var hdr [setupHeaderSize]byte61 var hdr [setupHeaderSize]byte
59- if err := readFull(fd, hdr[:]); err != nil {62+ if err := readFull(fd, hdr[:], deadline); err != nil {
60 return nil, setupInfo{}, err63 return nil, setupInfo{}, err
61 }64 }
62 reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:])))65 reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:])))
63 copy(reply, hdr[:])66 copy(reply, hdr[:])
64- if err := readFull(fd, reply[setupHeaderSize:]); err != nil {67+ if err := readFull(fd, reply[setupHeaderSize:], deadline); err != nil {
65 return nil, setupInfo{}, err68 return nil, setupInfo{}, err
66 }69 }
67 info, err := parseSetup(reply)70 info, err := parseSetup(reply)
@@ -141,12 +144,26 @@ func parseSetup(p []byte) (setupInfo, error) {
141 return info, nil144 return info, nil
142 }145 }
143 146
144-func writeFull(fd int, p []byte) error {147+// waitFd polls fd for events until deadline.
148+func waitFd(fd int, events int16, deadline time.Time) error {
149+ left := time.Until(deadline)
150+ if left <= 0 {
151+ return deadlineError("connection setup")
152+ }
153+ return pollFd(fd, events, left)
154+}
155+
156+func writeFull(fd int, p []byte, deadline time.Time) error {
145 for len(p) > 0 {157 for len(p) > 0 {
146 n, err := unix.Write(fd, p)158 n, err := unix.Write(fd, p)
147 switch {159 switch {
148 case err == unix.EINTR:160 case err == unix.EINTR:
149 continue161 continue
162+ case err == unix.EAGAIN:
163+ if err := waitFd(fd, unix.POLLOUT, deadline); err != nil {
164+ return err
165+ }
166+ continue
150 case err == unix.EPIPE || err == unix.ECONNRESET:167 case err == unix.EPIPE || err == unix.ECONNRESET:
151 return ErrClosed168 return ErrClosed
152 case err != nil:169 case err != nil:
@@ -159,12 +176,17 @@ func writeFull(fd int, p []byte) error {
159 return nil176 return nil
160 }177 }
161 178
162-func readFull(fd int, p []byte) error {179+func readFull(fd int, p []byte, deadline time.Time) error {
163 for len(p) > 0 {180 for len(p) > 0 {
164 n, err := unix.Read(fd, p)181 n, err := unix.Read(fd, p)
165 switch {182 switch {
166 case err == unix.EINTR:183 case err == unix.EINTR:
167 continue184 continue
185+ case err == unix.EAGAIN:
186+ if err := waitFd(fd, unix.POLLIN, deadline); err != nil {
187+ return err
188+ }
189+ continue
168 case err == unix.ECONNRESET:190 case err == unix.ECONNRESET:
169 return ErrClosed191 return ErrClosed
170 case err != nil:192 case err != nil:
M sync.go +55 -18
@@ -2,7 +2,9 @@ package xturbo
22
33 import (
44 "encoding/binary"
5+ "fmt"
56 "os"
7+ "time"
68
79 "golang.org/x/sys/unix"
810 )
@@ -62,9 +64,24 @@ func encodeExtRequest(b *Buf, id ExtID, minor uint8) Seq {
6264 return seq
6365 }
6466
67+// Bounds of the blocking calls. Vars so tests can shorten them.
68+var (
69+ // connectTimeout bounds NewConn: setup handshake plus extension bootstrap.
70+ connectTimeout = 10 * time.Second
71+ // syncTimeout bounds one Sync, InternAtoms or blocking XID refill.
72+ syncTimeout = 5 * time.Second
73+)
74+
75+// deadlineError wraps os.ErrDeadlineExceeded for a blocking call that timed
76+// out waiting for the server.
77+func deadlineError(what string) error {
78+ return fmt.Errorf("xturbo: %s: no answer from the X server: %w", what, os.ErrDeadlineExceeded)
79+}
80+
6581 // 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 {
82+// QueryExtension each, then BIG-REQUESTS Enable when present) in one Sync,
83+// before deadline.
84+func (c *Conn) bootstrap(deadline time.Time) error {
6885 for id := ExtCore + 1; id < extCount; id++ {
6986 seq := encodeQueryExtension(c.buf, ExtXName[id])
7087 if seq == 0 {
@@ -74,9 +91,9 @@ func (c *Conn) bootstrap() error {
7491 return err
7592 }
7693 }
77- // consumeReply queues bigreq Enable when BIG-REQUESTS is present; Sync
78- // waits for it too.
79- return c.Sync(nil)
94+ // consumeReply queues bigreq Enable when BIG-REQUESTS is present; the
95+ // wait covers it too.
96+ return c.sync(nil, deadline)
8097 }
8198
8299 // Sync flushes every queued request and blocks until the server has
@@ -85,9 +102,15 @@ func (c *Conn) bootstrap() error {
85102 // full round trip: errors of earlier requests have been delivered when it
86103 // returns. Packets read meanwhile go to h as with Dispatch (nil h: the last
87104 // 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.
105+// itself is returned as *XError. Sync gives up after 5 s with an error
106+// wrapping os.ErrDeadlineExceeded (a hung server); the connection is then
107+// still usable but its state unknown, treat it as dead. Meant for startup;
108+// called from a Handler callback it returns an error at once.
90109 func (c *Conn) Sync(h *Handler) error {
110+ return c.sync(h, time.Now().Add(c.timeout))
111+}
112+
113+func (c *Conn) sync(h *Handler, deadline time.Time) error {
91114 if c.dispatching {
92115 return errReentrantDispatch
93116 }
@@ -95,7 +118,7 @@ func (c *Conn) Sync(h *Handler) error {
95118 h = c.h
96119 }
97120 c.syncFailed = false
98- if err := c.wait(h); err != nil {
121+ if err := c.waitUntil(h, deadline); err != nil {
99122 return err
100123 }
101124 if c.syncFailed {
@@ -105,10 +128,11 @@ func (c *Conn) Sync(h *Handler) error {
105128 return nil
106129 }
107130
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 {
131+// waitUntil fences, flushes and dispatches, one bounded Dispatch at a time,
132+// until the fence has been answered and no registered slot remains, or
133+// deadline passes. A fence is re-queued whenever callbacks registered
134+// requests after the last one.
135+func (c *Conn) waitUntil(h *Handler, deadline time.Time) error {
112136 if c.fd < 0 {
113137 return ErrClosed
114138 }
@@ -123,7 +147,7 @@ func (c *Conn) wait(h *Handler) error {
123147 return c.buf.Err()
124148 }
125149 }
126- if err := c.pollOnce(h); err != nil {
150+ if err := c.pollOnce(h, deadline); err != nil {
127151 return err
128152 }
129153 }
@@ -131,9 +155,15 @@ func (c *Conn) wait(h *Handler) error {
131155
132156 // pollOnce makes one step of progress: flush, dispatch what is readable and,
133157 // 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 {
158+// (or writable while requests remain queued) or deadline passes. Dispatch is
159+// bounded per call, so it may return with input left; the caller loops and
160+// re-checks. A deadline already past fails even while packets keep coming
161+// (a server flooding events but never answering).
162+func (c *Conn) pollOnce(h *Handler, deadline time.Time) error {
163+ left := time.Until(deadline)
164+ if left <= 0 {
165+ return deadlineError("blocking round trip")
166+ }
137167 events := int16(unix.POLLIN)
138168 switch err := c.Flush(); err {
139169 case nil:
@@ -150,8 +180,15 @@ func (c *Conn) pollOnce(h *Handler) error {
150180 if !c.rdIdle || (events&unix.POLLOUT == 0 && c.buf.Len() > 0) {
151181 return nil
152182 }
153- fds := []unix.PollFd{{Fd: int32(c.fd), Events: events}}
154- if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR {
183+ return pollFd(c.fd, events, left)
184+}
185+
186+// pollFd blocks until fd has one of events or timeout passes (returning nil;
187+// callers re-check their deadline). EINTR is a wake-up.
188+func pollFd(fd int, events int16, timeout time.Duration) error {
189+ ms := int((timeout + time.Millisecond - 1) / time.Millisecond)
190+ fds := []unix.PollFd{{Fd: int32(fd), Events: events}}
191+ if _, err := unix.Poll(fds, ms); err != nil && err != unix.EINTR {
155192 return os.NewSyscallError("poll", err)
156193 }
157194 return nil
@@ -2,7 +2,9 @@ package xturbo
2 2
3 import (3 import (
4 "encoding/binary"4 "encoding/binary"
5+ "fmt"
5 "os"6 "os"
7+ "time"
6 8
7 "golang.org/x/sys/unix"9 "golang.org/x/sys/unix"
8 )10 )
@@ -62,9 +64,24 @@ func encodeExtRequest(b *Buf, id ExtID, minor uint8) Seq {
62 return seq64 return seq
63 }65 }
64 66
67+// Bounds of the blocking calls. Vars so tests can shorten them.
68+var (
69+ // connectTimeout bounds NewConn: setup handshake plus extension bootstrap.
70+ connectTimeout = 10 * time.Second
71+ // syncTimeout bounds one Sync, InternAtoms or blocking XID refill.
72+ syncTimeout = 5 * time.Second
73+)
74+
75+// deadlineError wraps os.ErrDeadlineExceeded for a blocking call that timed
76+// out waiting for the server.
77+func deadlineError(what string) error {
78+ return fmt.Errorf("xturbo: %s: no answer from the X server: %w", what, os.ErrDeadlineExceeded)
79+}
80+
65 // bootstrap negotiates every extension root knows (one pipelined81 // bootstrap negotiates every extension root knows (one pipelined
66-// QueryExtension each, then BIG-REQUESTS Enable when present) in one Sync.82+// QueryExtension each, then BIG-REQUESTS Enable when present) in one Sync,
67-func (c *Conn) bootstrap() error {83+// before deadline.
84+func (c *Conn) bootstrap(deadline time.Time) error {
68 for id := ExtCore + 1; id < extCount; id++ {85 for id := ExtCore + 1; id < extCount; id++ {
69 seq := encodeQueryExtension(c.buf, ExtXName[id])86 seq := encodeQueryExtension(c.buf, ExtXName[id])
70 if seq == 0 {87 if seq == 0 {
@@ -74,9 +91,9 @@ func (c *Conn) bootstrap() error {
74 return err91 return err
75 }92 }
76 }93 }
77- // consumeReply queues bigreq Enable when BIG-REQUESTS is present; Sync94+ // consumeReply queues bigreq Enable when BIG-REQUESTS is present; the
78- // waits for it too.95+ // wait covers it too.
79- return c.Sync(nil)96+ return c.sync(nil, deadline)
80 }97 }
81 98
82 // Sync flushes every queued request and blocks until the server has99 // Sync flushes every queued request and blocks until the server has
@@ -85,9 +102,15 @@ func (c *Conn) bootstrap() error {
85 // full round trip: errors of earlier requests have been delivered when it102 // 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 last103 // 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 issued104 // 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 called105+// itself is returned as *XError. Sync gives up after 5 s with an error
89-// from a Handler callback.106+// wrapping os.ErrDeadlineExceeded (a hung server); the connection is then
107+// still usable but its state unknown, treat it as dead. Meant for startup;
108+// called from a Handler callback it returns an error at once.
90 func (c *Conn) Sync(h *Handler) error {109 func (c *Conn) Sync(h *Handler) error {
110+ return c.sync(h, time.Now().Add(c.timeout))
111+}
112+
113+func (c *Conn) sync(h *Handler, deadline time.Time) error {
91 if c.dispatching {114 if c.dispatching {
92 return errReentrantDispatch115 return errReentrantDispatch
93 }116 }
@@ -95,7 +118,7 @@ func (c *Conn) Sync(h *Handler) error {
95 h = c.h118 h = c.h
96 }119 }
97 c.syncFailed = false120 c.syncFailed = false
98- if err := c.wait(h); err != nil {121+ if err := c.waitUntil(h, deadline); err != nil {
99 return err122 return err
100 }123 }
101 if c.syncFailed {124 if c.syncFailed {
@@ -105,10 +128,11 @@ func (c *Conn) Sync(h *Handler) error {
105 return nil128 return nil
106 }129 }
107 130
108-// wait fences, flushes and dispatches, one bounded Dispatch at a time, until131+// waitUntil fences, flushes and dispatches, one bounded Dispatch at a time,
109-// the fence has been answered and no registered slot remains. A fence is132+// until the fence has been answered and no registered slot remains, or
110-// re-queued whenever callbacks registered requests after the last one.133+// deadline passes. A fence is re-queued whenever callbacks registered
111-func (c *Conn) wait(h *Handler) error {134+// requests after the last one.
135+func (c *Conn) waitUntil(h *Handler, deadline time.Time) error {
112 if c.fd < 0 {136 if c.fd < 0 {
113 return ErrClosed137 return ErrClosed
114 }138 }
@@ -123,7 +147,7 @@ func (c *Conn) wait(h *Handler) error {
123 return c.buf.Err()147 return c.buf.Err()
124 }148 }
125 }149 }
126- if err := c.pollOnce(h); err != nil {150+ if err := c.pollOnce(h, deadline); err != nil {
127 return err151 return err
128 }152 }
129 }153 }
@@ -131,9 +155,15 @@ func (c *Conn) wait(h *Handler) error {
131 155
132 // pollOnce makes one step of progress: flush, dispatch what is readable and,156 // 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 readable157 // 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,158+// (or writable while requests remain queued) or deadline passes. Dispatch is
135-// so it may return with input left; the caller loops and re-checks.159+// bounded per call, so it may return with input left; the caller loops and
136-func (c *Conn) pollOnce(h *Handler) error {160+// re-checks. A deadline already past fails even while packets keep coming
161+// (a server flooding events but never answering).
162+func (c *Conn) pollOnce(h *Handler, deadline time.Time) error {
163+ left := time.Until(deadline)
164+ if left <= 0 {
165+ return deadlineError("blocking round trip")
166+ }
137 events := int16(unix.POLLIN)167 events := int16(unix.POLLIN)
138 switch err := c.Flush(); err {168 switch err := c.Flush(); err {
139 case nil:169 case nil:
@@ -150,8 +180,15 @@ func (c *Conn) pollOnce(h *Handler) error {
150 if !c.rdIdle || (events&unix.POLLOUT == 0 && c.buf.Len() > 0) {180 if !c.rdIdle || (events&unix.POLLOUT == 0 && c.buf.Len() > 0) {
151 return nil181 return nil
152 }182 }
153- fds := []unix.PollFd{{Fd: int32(c.fd), Events: events}}183+ return pollFd(c.fd, events, left)
154- if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR {184+}
185+
186+// pollFd blocks until fd has one of events or timeout passes (returning nil;
187+// callers re-check their deadline). EINTR is a wake-up.
188+func pollFd(fd int, events int16, timeout time.Duration) error {
189+ ms := int((timeout + time.Millisecond - 1) / time.Millisecond)
190+ fds := []unix.PollFd{{Fd: int32(fd), Events: events}}
191+ if _, err := unix.Poll(fds, ms); err != nil && err != unix.EINTR {
155 return os.NewSyscallError("poll", err)192 return os.NewSyscallError("poll", err)
156 }193 }
157 return nil194 return nil
M xid.go +130 -41
@@ -1,13 +1,27 @@
11 package xturbo
22
3-import "errors"
3+import (
4+ "errors"
5+ "time"
6+)
47
58 // errXIDsExhausted reports that the server has no resource IDs left for this
6-// client (XC-MISC GetXIDRange returned an empty range).
9+// client: XC-MISC GetXIDRange returned an empty range, or one outside this
10+// client's base and mask.
711 var errXIDsExhausted = errors.New("xturbo: server has no resource IDs left")
812
9-// xidFreeCap bounds the FreeXID list; IDs freed beyond it are not reused.
10-const xidFreeCap = 256
13+const (
14+ // xidFreeCap bounds the FreeXID list; IDs freed beyond it are not reused.
15+ xidFreeCap = 256
16+ // xidLowMark is the remaining-ID count below which XIDsLow reports true.
17+ xidLowMark = 4096
18+)
19+
20+// xidRange is a run of count IDs base|next, base|next+inc, … (inc = lowest
21+// bit of the mask).
22+type xidRange struct {
23+ next, left uint32
24+}
1125
1226 // initXIDs starts allocation over the setup range: base|0, base|inc, …
1327 // up to base|mask, in libxcb's order.
@@ -16,55 +30,86 @@ func (c *Conn) initXIDs() {
1630 if inc == 0 {
1731 return
1832 }
19- c.xidNext = 0
20- c.xidLeft = c.xidMask/inc + 1
33+ c.xids = xidRange{0, c.xidMask/inc + 1}
2134 }
2235
2336 // 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.
37+// FreeXID are reused first, then the current range, then a range fetched
38+// ahead by PrefetchXIDs. When all are empty, outside a Handler callback, one
39+// blocking XC-MISC GetXIDRange round trip (bounded like Sync) refills it;
40+// packets read meanwhile go to the last Handler given to Dispatch. Inside a
41+// callback that refill is impossible and NewXID fails instead, so an event
42+// loop allocating from callbacks calls PrefetchXIDs whenever XIDsLow reports
43+// true (the XWM does it once per loop iteration). It also fails when XC-MISC
44+// is absent or the server has no IDs left.
2945 func (c *Conn) NewXID() (uint32, error) {
3046 if c.xidFreeN > 0 {
3147 c.xidFreeN--
3248 return c.xidFree[c.xidFreeN], nil
3349 }
34- if c.xidLeft == 0 {
35- if err := c.refillXIDs(); err != nil {
36- return 0, err
50+ if c.xids.left == 0 {
51+ if c.xidPend.left == 0 {
52+ if err := c.refillXIDs(); err != nil {
53+ return 0, err
54+ }
3755 }
56+ c.xids, c.xidPend = c.xidPend, xidRange{}
3857 }
39- id := c.xidBase | c.xidNext
40- c.xidNext += c.xidMask & -c.xidMask
41- c.xidLeft--
58+ id := c.xidBase | c.xids.next
59+ c.xids.next += c.xidMask & -c.xidMask
60+ c.xids.left--
4261 return id, nil
4362 }
4463
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.
64+// FreeXID gives back id for reuse by a later NewXID. id must come from
65+// NewXID on this Conn and be freed once, after the caller destroyed its
66+// resource (the destroy request may still be queued, it precedes any reuse
67+// on the wire). IDs outside this client's range, 0, and IDs beyond the free
68+// list capacity are ignored; freeing an ID twice hands it out twice.
4869 func (c *Conn) FreeXID(id uint32) {
49- if id == 0 || id&^c.xidMask != c.xidBase || c.xidFreeN == xidFreeCap {
70+ if id&^c.xidMask != c.xidBase || c.xidFreeN == xidFreeCap {
5071 return
5172 }
5273 c.xidFree[c.xidFreeN] = id
5374 c.xidFreeN++
5475 }
5576
56-func (c *Conn) refillXIDs() error {
57- if c.dispatching {
58- return errReentrantDispatch
77+// XIDsLow reports that fewer than 4096 IDs remain in the current range with
78+// an empty free list, and that PrefetchXIDs would help: XC-MISC is present,
79+// no prefetched range is waiting, no prefetch is in flight and the last one
80+// did not report the server out of IDs (after that only NewXID's blocking
81+// refill retries).
82+func (c *Conn) XIDsLow() bool {
83+ if c.xidFreeN > 0 || c.xidPend.left > 0 || c.xidNone || c.xidInFlight() || !c.ext.Info(ExtXCMisc).Present {
84+ return false
5985 }
86+ return c.xids.left < xidLowMark
87+}
88+
89+// PrefetchXIDs queues an XC-MISC GetXIDRange without waiting; its reply,
90+// read by a later Dispatch, becomes the range NewXID switches to when the
91+// current one runs out. Callers flush as usual. It does nothing while a
92+// prefetched range is unused or a prefetch is in flight, and may be called
93+// from a Handler callback. It fails with ErrExtMissing without XC-MISC,
94+// ErrRingFull when the next request's Expect slot is taken, and with the
95+// Buf's sticky error.
96+func (c *Conn) PrefetchXIDs() error {
6097 if c.fd < 0 {
6198 return ErrClosed
6299 }
63100 if !c.ext.Info(ExtXCMisc).Present {
64101 return ErrExtMissing
65102 }
66- if err := c.waitSlot(); err != nil {
67- return err
103+ if c.xidPend.left > 0 || c.xidInFlight() {
104+ return nil
105+ }
106+ return c.queueXIDRange()
107+}
108+
109+// queueXIDRange queues a GetXIDRange registered with kindXIDRange.
110+func (c *Conn) queueXIDRange() error {
111+ if c.ring[c.buf.next&ringMask].seq != 0 {
112+ return ErrRingFull
68113 }
69114 seq := encodeExtRequest(c.buf, ExtXCMisc, minorGetXIDRange)
70115 if seq == 0 {
@@ -73,23 +118,67 @@ func (c *Conn) refillXIDs() error {
73118 if err := c.expect(seq, kindXIDRange, [4]uint32{}); err != nil {
74119 return err
75120 }
76- c.rangeGot = false
77- c.syncFailed = false
78- if err := c.wait(c.h); err != nil {
121+ c.xidReq = seq
122+ c.xidNone, c.xidFailed = false, false
123+ return nil
124+}
125+
126+// xidInFlight reports a GetXIDRange whose reply or error has not arrived.
127+func (c *Conn) xidInFlight() bool {
128+ return c.xidReq != 0 && c.ring[c.xidReq&ringMask].seq == c.xidReq
129+}
130+
131+// gotXIDRange records a GetXIDRange reply (from consumeReply).
132+func (c *Conn) gotXIDRange(start, count uint32) {
133+ if !c.validXIDRange(start, count) {
134+ c.xidNone = true
135+ return
136+ }
137+ c.xidPend = xidRange{start & c.xidMask, count}
138+}
139+
140+// validXIDRange accepts a GetXIDRange answer only when every ID of it lies in
141+// this client's base and mask. The server answers start 0 (count 0 or 1)
142+// when it has none.
143+func (c *Conn) validXIDRange(start, count uint32) bool {
144+ inc := uint64(c.xidMask & -c.xidMask)
145+ off := uint64(start & c.xidMask)
146+ return inc != 0 && count != 0 && start != 0 &&
147+ start&^c.xidMask == c.xidBase && off%inc == 0 &&
148+ off+uint64(count-1)*inc <= uint64(c.xidMask)
149+}
150+
151+// refillXIDs blocks until a prefetched range is available: it waits for the
152+// prefetch in flight, or queues one.
153+func (c *Conn) refillXIDs() error {
154+ if c.dispatching {
155+ return errReentrantDispatch
156+ }
157+ if c.fd < 0 {
158+ return ErrClosed
159+ }
160+ if !c.ext.Info(ExtXCMisc).Present {
161+ return ErrExtMissing
162+ }
163+ deadline := time.Now().Add(c.timeout)
164+ if !c.xidInFlight() {
165+ if err := c.waitSlot(deadline); err != nil {
166+ return err
167+ }
168+ if err := c.queueXIDRange(); err != nil {
169+ return err
170+ }
171+ }
172+ if err := c.waitUntil(c.h, deadline); err != nil {
79173 return err
80174 }
81- if c.syncFailed {
82- e := c.syncErr
175+ switch {
176+ case c.xidFailed:
177+ e := c.xidErr
83178 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 {
179+ case c.xidPend.left == 0:
89180 return errXIDsExhausted
90181 }
91- c.xidNext = c.rangeNext & c.xidMask
92- c.xidLeft = c.rangeLeft
93182 return nil
94183 }
95184
@@ -97,9 +186,9 @@ func (c *Conn) refillXIDs() error {
97186 // reserved registration can always be made for it: when an earlier request
98187 // still holds it, it waits for every outstanding request (fenced, so even a
99188 // 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 {
189+func (c *Conn) waitSlot(deadline time.Time) error {
190+ if c.ring[c.buf.next&ringMask].seq == 0 {
102191 return nil
103192 }
104- return c.wait(c.h)
193+ return c.waitUntil(c.h, deadline)
105194 }
@@ -1,13 +1,27 @@
1 package xturbo1 package xturbo
2 2
3-import "errors"3+import (
4+ "errors"
5+ "time"
6+)
4 7
5 // errXIDsExhausted reports that the server has no resource IDs left for this8 // errXIDsExhausted reports that the server has no resource IDs left for this
6-// client (XC-MISC GetXIDRange returned an empty range).9+// client: XC-MISC GetXIDRange returned an empty range, or one outside this
10+// client's base and mask.
7 var errXIDsExhausted = errors.New("xturbo: server has no resource IDs left")11 var errXIDsExhausted = errors.New("xturbo: server has no resource IDs left")
8 12
9-// xidFreeCap bounds the FreeXID list; IDs freed beyond it are not reused.13+const (
10-const xidFreeCap = 25614+ // xidFreeCap bounds the FreeXID list; IDs freed beyond it are not reused.
15+ xidFreeCap = 256
16+ // xidLowMark is the remaining-ID count below which XIDsLow reports true.
17+ xidLowMark = 4096
18+)
19+
20+// xidRange is a run of count IDs base|next, base|next+inc, … (inc = lowest
21+// bit of the mask).
22+type xidRange struct {
23+ next, left uint32
24+}
11 25
12 // initXIDs starts allocation over the setup range: base|0, base|inc, …26 // initXIDs starts allocation over the setup range: base|0, base|inc, …
13 // up to base|mask, in libxcb's order.27 // up to base|mask, in libxcb's order.
@@ -16,55 +30,86 @@ func (c *Conn) initXIDs() {
16 if inc == 0 {30 if inc == 0 {
17 return31 return
18 }32 }
19- c.xidNext = 033+ c.xids = xidRange{0, c.xidMask/inc + 1}
20- c.xidLeft = c.xidMask/inc + 1
21 }34 }
22 35
23 // NewXID returns a fresh resource ID for this client. IDs released with36 // 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 is37+// FreeXID are reused first, then the current range, then a range fetched
25-// exhausted one blocking XC-MISC GetXIDRange round trip refills it (packets38+// ahead by PrefetchXIDs. When all are empty, outside a Handler callback, one
26-// read meanwhile go to the last Handler given to Dispatch). It fails when39+// blocking XC-MISC GetXIDRange round trip (bounded like Sync) refills it;
27-// XC-MISC is absent, the server has no IDs left, or it is called from a40+// packets read meanwhile go to the last Handler given to Dispatch. Inside a
28-// Handler callback while a refill is needed.41+// callback that refill is impossible and NewXID fails instead, so an event
42+// loop allocating from callbacks calls PrefetchXIDs whenever XIDsLow reports
43+// true (the XWM does it once per loop iteration). It also fails when XC-MISC
44+// is absent or the server has no IDs left.
29 func (c *Conn) NewXID() (uint32, error) {45 func (c *Conn) NewXID() (uint32, error) {
30 if c.xidFreeN > 0 {46 if c.xidFreeN > 0 {
31 c.xidFreeN--47 c.xidFreeN--
32 return c.xidFree[c.xidFreeN], nil48 return c.xidFree[c.xidFreeN], nil
33 }49 }
34- if c.xidLeft == 0 {50+ if c.xids.left == 0 {
35- if err := c.refillXIDs(); err != nil {51+ if c.xidPend.left == 0 {
36- return 0, err52+ if err := c.refillXIDs(); err != nil {
53+ return 0, err
54+ }
37 }55 }
56+ c.xids, c.xidPend = c.xidPend, xidRange{}
38 }57 }
39- id := c.xidBase | c.xidNext58+ id := c.xidBase | c.xids.next
40- c.xidNext += c.xidMask & -c.xidMask59+ c.xids.next += c.xidMask & -c.xidMask
41- c.xidLeft--60+ c.xids.left--
42 return id, nil61 return id, nil
43 }62 }
44 63
45-// FreeXID returns id, whose resource the caller has already destroyed (the64+// FreeXID gives back id for reuse by a later NewXID. id must come from
46-// destroy request may still be queued), for reuse by NewXID. IDs outside this65+// NewXID on this Conn and be freed once, after the caller destroyed its
47-// client's range, and IDs beyond the free list capacity, are ignored.66+// resource (the destroy request may still be queued, it precedes any reuse
67+// on the wire). IDs outside this client's range, 0, and IDs beyond the free
68+// list capacity are ignored; freeing an ID twice hands it out twice.
48 func (c *Conn) FreeXID(id uint32) {69 func (c *Conn) FreeXID(id uint32) {
49- if id == 0 || id&^c.xidMask != c.xidBase || c.xidFreeN == xidFreeCap {70+ if id&^c.xidMask != c.xidBase || c.xidFreeN == xidFreeCap {
50 return71 return
51 }72 }
52 c.xidFree[c.xidFreeN] = id73 c.xidFree[c.xidFreeN] = id
53 c.xidFreeN++74 c.xidFreeN++
54 }75 }
55 76
56-func (c *Conn) refillXIDs() error {77+// XIDsLow reports that fewer than 4096 IDs remain in the current range with
57- if c.dispatching {78+// an empty free list, and that PrefetchXIDs would help: XC-MISC is present,
58- return errReentrantDispatch79+// no prefetched range is waiting, no prefetch is in flight and the last one
80+// did not report the server out of IDs (after that only NewXID's blocking
81+// refill retries).
82+func (c *Conn) XIDsLow() bool {
83+ if c.xidFreeN > 0 || c.xidPend.left > 0 || c.xidNone || c.xidInFlight() || !c.ext.Info(ExtXCMisc).Present {
84+ return false
59 }85 }
86+ return c.xids.left < xidLowMark
87+}
88+
89+// PrefetchXIDs queues an XC-MISC GetXIDRange without waiting; its reply,
90+// read by a later Dispatch, becomes the range NewXID switches to when the
91+// current one runs out. Callers flush as usual. It does nothing while a
92+// prefetched range is unused or a prefetch is in flight, and may be called
93+// from a Handler callback. It fails with ErrExtMissing without XC-MISC,
94+// ErrRingFull when the next request's Expect slot is taken, and with the
95+// Buf's sticky error.
96+func (c *Conn) PrefetchXIDs() error {
60 if c.fd < 0 {97 if c.fd < 0 {
61 return ErrClosed98 return ErrClosed
62 }99 }
63 if !c.ext.Info(ExtXCMisc).Present {100 if !c.ext.Info(ExtXCMisc).Present {
64 return ErrExtMissing101 return ErrExtMissing
65 }102 }
66- if err := c.waitSlot(); err != nil {103+ if c.xidPend.left > 0 || c.xidInFlight() {
67- return err104+ return nil
105+ }
106+ return c.queueXIDRange()
107+}
108+
109+// queueXIDRange queues a GetXIDRange registered with kindXIDRange.
110+func (c *Conn) queueXIDRange() error {
111+ if c.ring[c.buf.next&ringMask].seq != 0 {
112+ return ErrRingFull
68 }113 }
69 seq := encodeExtRequest(c.buf, ExtXCMisc, minorGetXIDRange)114 seq := encodeExtRequest(c.buf, ExtXCMisc, minorGetXIDRange)
70 if seq == 0 {115 if seq == 0 {
@@ -73,23 +118,67 @@ func (c *Conn) refillXIDs() error {
73 if err := c.expect(seq, kindXIDRange, [4]uint32{}); err != nil {118 if err := c.expect(seq, kindXIDRange, [4]uint32{}); err != nil {
74 return err119 return err
75 }120 }
76- c.rangeGot = false121+ c.xidReq = seq
77- c.syncFailed = false122+ c.xidNone, c.xidFailed = false, false
78- if err := c.wait(c.h); err != nil {123+ return nil
124+}
125+
126+// xidInFlight reports a GetXIDRange whose reply or error has not arrived.
127+func (c *Conn) xidInFlight() bool {
128+ return c.xidReq != 0 && c.ring[c.xidReq&ringMask].seq == c.xidReq
129+}
130+
131+// gotXIDRange records a GetXIDRange reply (from consumeReply).
132+func (c *Conn) gotXIDRange(start, count uint32) {
133+ if !c.validXIDRange(start, count) {
134+ c.xidNone = true
135+ return
136+ }
137+ c.xidPend = xidRange{start & c.xidMask, count}
138+}
139+
140+// validXIDRange accepts a GetXIDRange answer only when every ID of it lies in
141+// this client's base and mask. The server answers start 0 (count 0 or 1)
142+// when it has none.
143+func (c *Conn) validXIDRange(start, count uint32) bool {
144+ inc := uint64(c.xidMask & -c.xidMask)
145+ off := uint64(start & c.xidMask)
146+ return inc != 0 && count != 0 && start != 0 &&
147+ start&^c.xidMask == c.xidBase && off%inc == 0 &&
148+ off+uint64(count-1)*inc <= uint64(c.xidMask)
149+}
150+
151+// refillXIDs blocks until a prefetched range is available: it waits for the
152+// prefetch in flight, or queues one.
153+func (c *Conn) refillXIDs() error {
154+ if c.dispatching {
155+ return errReentrantDispatch
156+ }
157+ if c.fd < 0 {
158+ return ErrClosed
159+ }
160+ if !c.ext.Info(ExtXCMisc).Present {
161+ return ErrExtMissing
162+ }
163+ deadline := time.Now().Add(c.timeout)
164+ if !c.xidInFlight() {
165+ if err := c.waitSlot(deadline); err != nil {
166+ return err
167+ }
168+ if err := c.queueXIDRange(); err != nil {
169+ return err
170+ }
171+ }
172+ if err := c.waitUntil(c.h, deadline); err != nil {
79 return err173 return err
80 }174 }
81- if c.syncFailed {175+ switch {
82- e := c.syncErr176+ case c.xidFailed:
177+ e := c.xidErr
83 return &e178 return &e
84- }179+ case c.xidPend.left == 0:
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 errXIDsExhausted180 return errXIDsExhausted
90 }181 }
91- c.xidNext = c.rangeNext & c.xidMask
92- c.xidLeft = c.rangeLeft
93 return nil182 return nil
94 }183 }
95 184
@@ -97,9 +186,9 @@ func (c *Conn) refillXIDs() error {
97 // reserved registration can always be made for it: when an earlier request186 // 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 a187 // still holds it, it waits for every outstanding request (fenced, so even a
99 // registered request without reply completes).188 // registered request without reply completes).
100-func (c *Conn) waitSlot() error {189+func (c *Conn) waitSlot(deadline time.Time) error {
101- if s := c.ring[c.buf.next&ringMask].seq; s == 0 || s < c.seq {190+ if c.ring[c.buf.next&ringMask].seq == 0 {
102 return nil191 return nil
103 }192 }
104- return c.wait(c.h)193+ return c.waitUntil(c.h, deadline)
105 }194 }
M xserver_script_test.go +5 -1
@@ -5,6 +5,7 @@ import (
55 "io"
66 "os"
77 "sync"
8+ "sync/atomic"
89 "testing"
910 )
1011
@@ -24,6 +25,9 @@ type script struct {
2425 xidRanges [][2]uint32 // successive GetXIDRange replies {start, count}
2526 // hook sees every request first; returning true skips the default.
2627 hook func(s *scriptConn, major, minor uint8, seq uint16, body []byte) bool
28+ // silent, once set, makes the server read requests and never answer
29+ // (alive but hung).
30+ silent atomic.Bool
2731 }
2832
2933 // scriptConn is the server side of one scripted connection.
@@ -119,7 +123,7 @@ func (s *scriptConn) serve() {
119123 s.mu.Lock()
120124 s.reqs = append(s.reqs, scriptReq{s.seq, major, minor, raw})
121125 s.mu.Unlock()
122- if sc.hook != nil && sc.hook(s, major, minor, s.seq, body) {
126+ if sc.silent.Load() || sc.hook != nil && sc.hook(s, major, minor, s.seq, body) {
123127 continue
124128 }
125129 s.answer(major, minor, body)
@@ -5,6 +5,7 @@ import (
5 "io"5 "io"
6 "os"6 "os"
7 "sync"7 "sync"
8+ "sync/atomic"
8 "testing"9 "testing"
9 )10 )
10 11
@@ -24,6 +25,9 @@ type script struct {
24 xidRanges [][2]uint32 // successive GetXIDRange replies {start, count}25 xidRanges [][2]uint32 // successive GetXIDRange replies {start, count}
25 // hook sees every request first; returning true skips the default.26 // hook sees every request first; returning true skips the default.
26 hook func(s *scriptConn, major, minor uint8, seq uint16, body []byte) bool27 hook func(s *scriptConn, major, minor uint8, seq uint16, body []byte) bool
28+ // silent, once set, makes the server read requests and never answer
29+ // (alive but hung).
30+ silent atomic.Bool
27 }31 }
28 32
29 // scriptConn is the server side of one scripted connection.33 // scriptConn is the server side of one scripted connection.
@@ -119,7 +123,7 @@ func (s *scriptConn) serve() {
119 s.mu.Lock()123 s.mu.Lock()
120 s.reqs = append(s.reqs, scriptReq{s.seq, major, minor, raw})124 s.reqs = append(s.reqs, scriptReq{s.seq, major, minor, raw})
121 s.mu.Unlock()125 s.mu.Unlock()
122- if sc.hook != nil && sc.hook(s, major, minor, s.seq, body) {126+ if sc.silent.Load() || sc.hook != nil && sc.hook(s, major, minor, s.seq, body) {
123 continue127 continue
124 }128 }
125 s.answer(major, minor, body)129 s.answer(major, minor, body)

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