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.12 files · +677 -108
M atoms.go +16 -6
| @@ -1,16 +1,25 @@ | |||
| 1 | 1 | package xturbo | |
| 2 | 2 | ||
| 3 | + | import "time" | |
| 4 | + | ||
| 3 | 5 | // InternAtoms interns every name in one pipelined batch and blocks for one | |
| 4 | 6 | // round trip (Sync), writing the atom of names[i] into dst[i] (0 for a name | |
| 5 | 7 | // 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. | |
| 10 | 14 | func (c *Conn) InternAtoms(names []string, onlyIfExists bool, dst []uint32) error { | |
| 11 | 15 | if len(names) != len(dst) { | |
| 12 | 16 | 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 | 23 | if c.dispatching { | |
| 15 | 24 | return errReentrantDispatch | |
| 16 | 25 | } | |
| @@ -20,9 +29,10 @@ func (c *Conn) InternAtoms(names []string, onlyIfExists bool, dst []uint32) erro | |||
| 20 | 29 | clear(dst) | |
| 21 | 30 | c.atoms = dst | |
| 22 | 31 | defer func() { c.atoms = nil }() | |
| 32 | + | deadline := time.Now().Add(c.timeout) | |
| 23 | 33 | c.syncFailed = false | |
| 24 | 34 | for i, name := range names { | |
| 25 | - | if err := c.waitSlot(); err != nil { | |
| 35 | + | if err := c.waitSlot(deadline); err != nil { | |
| 26 | 36 | return err | |
| 27 | 37 | } | |
| 28 | 38 | seq := encodeInternAtom(c.buf, onlyIfExists, name) | |
| @@ -33,7 +43,7 @@ func (c *Conn) InternAtoms(names []string, onlyIfExists bool, dst []uint32) erro | |||
| 33 | 43 | 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 | 47 | return err | |
| 38 | 48 | } | |
| 39 | 49 | if c.syncFailed { | |
| @@ -1,16 +1,25 @@ | |||||
| 1 | package xturbo | 1 | package xturbo | ||
| 2 | 2 | ||||
| 3 | + | import "time" | |||
| 4 | + | ||||
| 3 | // InternAtoms interns every name in one pipelined batch and blocks for one | 5 | // InternAtoms interns every name in one pipelined batch and blocks for one | ||
| 4 | // round trip (Sync), writing the atom of names[i] into dst[i] (0 for a name | 6 | // round trip (Sync), writing the atom of names[i] into dst[i] (0 for a name | ||
| 5 | // that does not exist when onlyIfExists is set). len(dst) must equal | 7 | // 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 | 8 | + | // 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 atoms | 9 | + | // 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 to | 10 | + | // 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 ErrBadLength | 16 | 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 errReentrantDispatch | 24 | 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 = dst | 30 | 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 = false | 33 | 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 err | 36 | 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 err | 43 | 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 err | 47 | 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) { | |||
| 273 | 273 | h := Handler{OnEvent: func(*Event) { | |
| 274 | 274 | errSync = c.Sync(nil) | |
| 275 | 275 | errAtoms = c.InternAtoms([]string{"X"}, false, make([]uint32, 1)) | |
| 276 | - | c.xidLeft, c.xidFreeN = 0, 0 | |
| 276 | + | c.xids, c.xidFreeN = xidRange{}, 0 | |
| 277 | 277 | _, errXID = c.NewXID() | |
| 278 | 278 | }} | |
| 279 | 279 | c.Buf().Request(8, 0, 4) | |
| @@ -333,7 +333,7 @@ func TestNewXIDRefill(t *testing.T) { | |||
| 333 | 333 | sc := &script{exts: fullExts(), xidRanges: [][2]uint32{{0x00200040, 3}, {0, 1}}} | |
| 334 | 334 | c, srv := dialScript(t, sc) | |
| 335 | 335 | // Pretend only two ids of the setup range are left. | |
| 336 | - | c.xidLeft = 2 | |
| 336 | + | c.xids.left = 2 | |
| 337 | 337 | var ids []uint32 | |
| 338 | 338 | for range 5 { | |
| 339 | 339 | id, err := c.NewXID() | |
| @@ -367,7 +367,7 @@ func TestNewXIDRefill(t *testing.T) { | |||
| 367 | 367 | ||
| 368 | 368 | func TestNewXIDNoXCMisc(t *testing.T) { | |
| 369 | 369 | c, _ := dialScript(t, &script{}) | |
| 370 | - | c.xidLeft = 0 | |
| 370 | + | c.xids = xidRange{} | |
| 371 | 371 | if _, err := c.NewXID(); !errors.Is(err, ErrExtMissing) { | |
| 372 | 372 | t.Fatal(err) | |
| 373 | 373 | } | |
| @@ -383,7 +383,7 @@ func TestNewXIDRefillError(t *testing.T) { | |||
| 383 | 383 | return false | |
| 384 | 384 | } | |
| 385 | 385 | c, _ := dialScript(t, sc) | |
| 386 | - | c.xidLeft = 0 | |
| 386 | + | c.xids = xidRange{} | |
| 387 | 387 | var xe *XError | |
| 388 | 388 | if _, err := c.NewXID(); !errors.As(err, &xe) || xe.Kind != kindXIDRange { | |
| 389 | 389 | 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, 0 | 276 | + | 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 = 2 | 336 | + | c.xids.left = 2 |
| 337 | var ids []uint32 | 337 | 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 = 0 | 370 | + | 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 false | 383 | return false | ||
| 384 | } | 384 | } | ||
| 385 | c, _ := dialScript(t, sc) | 385 | c, _ := dialScript(t, sc) | ||
| 386 | - | c.xidLeft = 0 | 386 | + | c.xids = xidRange{} |
| 387 | var xe *XError | 387 | 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) { | |||
| 207 | 207 | if err := c.Sync(&h); err != nil || xerr != nil { | |
| 208 | 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 | 235 | // 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 @@ | |||
| 1 | 1 | package xturbo | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | + | "fmt" | |
| 4 | 5 | "os" | |
| 6 | + | "time" | |
| 5 | 7 | ||
| 6 | 8 | "golang.org/x/sys/unix" | |
| 7 | 9 | ) | |
| @@ -15,21 +17,23 @@ type Conn struct { | |||
| 15 | 17 | setup []byte | |
| 16 | 18 | root uint32 | |
| 17 | 19 | ||
| 18 | - | xidBase, xidMask uint32 // resource-id allocation, used by NewXID | |
| 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) | |
| 20 | 22 | xidFree [xidFreeCap]uint32 | |
| 21 | 23 | 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 | |
| 25 | 28 | ||
| 26 | 29 | // 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 | |
| 33 | 37 | ||
| 34 | 38 | // Read side, owned by Dispatch: rb[rpos:rend] is received, unconsumed. | |
| 35 | 39 | rb []byte | |
| @@ -52,10 +56,12 @@ type Conn struct { | |||
| 52 | 56 | ||
| 53 | 57 | // NewConn takes ownership of fd, a connected stream socket to an X server, | |
| 54 | 58 | // 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. | |
| 59 | 65 | func NewConn(fd int) (*Conn, error) { | |
| 60 | 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 | 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 | 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 | 84 | if err != nil { | |
| 78 | 85 | return nil, err | |
| 79 | 86 | } | |
| 80 | - | if err := unix.SetNonblock(fd, true); err != nil { | |
| 81 | - | return nil, os.NewSyscallError("fcntl", err) | |
| 82 | - | } | |
| 83 | 87 | 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) | |
| 86 | 90 | } | |
| 87 | 91 | return c, nil | |
| 88 | 92 | } | |
| @@ -98,6 +102,7 @@ func newConnState(fd int, setup []byte, info setupInfo) *Conn { | |||
| 98 | 102 | xidMask: info.xidMask, | |
| 99 | 103 | rb: make([]byte, readBufSize), | |
| 100 | 104 | maxPacket: MaxPacket, | |
| 105 | + | timeout: syncTimeout, | |
| 101 | 106 | } | |
| 102 | 107 | c.buf.Ext = &c.ext | |
| 103 | 108 | c.initXIDs() | |
| @@ -1,7 +1,9 @@ | |||||
| 1 | package xturbo | 1 | package xturbo | ||
| 2 | 2 | ||||
| 3 | import ( | 3 | import ( | ||
| 4 | + | "fmt" | |||
| 4 | "os" | 5 | "os" | ||
| 6 | + | "time" | |||
| 5 | 7 | ||||
| 6 | "golang.org/x/sys/unix" | 8 | "golang.org/x/sys/unix" | ||
| 7 | ) | 9 | ) | ||
| @@ -15,21 +17,23 @@ type Conn struct { | |||||
| 15 | setup []byte | 17 | setup []byte | ||
| 16 | root uint32 | 18 | root uint32 | ||
| 17 | 19 | ||||
| 18 | - | xidBase, xidMask uint32 // resource-id allocation, used by NewXID | 20 | + | xidBase, xidMask uint32 // resource-id allocation, used by NewXID |
| 19 | - | xidNext, xidLeft uint32 // current range: next id, ids left in it | 21 | + | xids, xidPend xidRange // current range; prefetched range (left 0: none) |
| 20 | xidFree [xidFreeCap]uint32 | 22 | xidFree [xidFreeCap]uint32 | ||
| 21 | xidFreeN int | 23 | xidFreeN int | ||
| 22 | - | // Last XC-MISC GetXIDRange reply, filled by consumeReply. | 24 | + | xidReq Seq // last GetXIDRange sent |
| 23 | - | rangeNext, rangeLeft uint32 | 25 | + | xidNone bool // the last GetXIDRange gave no usable range |
| 24 | - | rangeGot bool | 26 | + | 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 Sync | 30 | + | h *Handler // last non-nil Handler given to Dispatch or Sync |
| 28 | - | rdIdle bool // the last Dispatch read nothing: the socket was empty | 31 | + | 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 request | 33 | + | syncErr XError // first error on a reserved-kind request |
| 31 | - | syncFailed bool // syncErr is set | 34 | + | syncFailed bool // syncErr is set |
| 32 | - | resync Seq // last GetInputFocus appended by Flush | 35 | + | 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 []byte | 39 | 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 an | 58 | // 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 | 59 | + | // XWayland the caller spawned). It blocks for the handshake, then one round |
| 56 | - | // switched to O_NONBLOCK and one blocking round trip negotiates every known | 60 | + | // trip negotiates every known extension (QueryExtension each, BIG-REQUESTS |
| 57 | - | // extension (QueryExtension each, BIG-REQUESTS Enable when present) into | 61 | + | // 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, err | 85 | 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, err | 89 | + | return nil, fmt.Errorf("xturbo: extension bootstrap: %w", err) |
| 86 | } | 90 | } | ||
| 87 | return c, nil | 91 | 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.ext | 107 | 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) | |||
| 39 | 39 | if err != nil { | |
| 40 | 40 | t.Fatal(err) | |
| 41 | 41 | } | |
| 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 { | |
| 44 | 44 | t.Fatal(err) | |
| 45 | 45 | } | |
| 46 | 46 | 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 @@ | |||
| 1 | 1 | package xturbo | |
| 2 | 2 | ||
| 3 | + | import "time" | |
| 4 | + | ||
| 3 | 5 | // SetMaxReqWords exposes the Conn-owned BIG-REQUESTS limit setter to external tests. | |
| 4 | 6 | var SetMaxReqWords = (*Buf).setMaxReqWords | |
| 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 | 23 | // ExhaustXIDs empties the setup range and the free list so the next NewXID | |
| 7 | 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 | 27 | // Root's own encoders of the requests it issues, compared with the generated | |
| 11 | 28 | // ones and the goldens by external tests. | |
| @@ -1,11 +1,28 @@ | |||||
| 1 | package xturbo | 1 | package xturbo | ||
| 2 | 2 | ||||
| 3 | + | import "time" | |||
| 4 | + | ||||
| 3 | // SetMaxReqWords exposes the Conn-owned BIG-REQUESTS limit setter to external tests. | 5 | // SetMaxReqWords exposes the Conn-owned BIG-REQUESTS limit setter to external tests. | ||
| 4 | var SetMaxReqWords = (*Buf).setMaxReqWords | 6 | var SetMaxReqWords = (*Buf).setMaxReqWords | ||
| 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 NewXID | 23 | // 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 generated | 27 | // 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) { | |||
| 152 | 152 | c.buf.setMaxReqWords(words) | |
| 153 | 153 | } | |
| 154 | 154 | case kindXIDRange: | |
| 155 | - | c.rangeNext = binary.LittleEndian.Uint32(r.Raw[8:]) | |
| 156 | - | c.rangeLeft = binary.LittleEndian.Uint32(r.Raw[12:]) | |
| 157 | - | c.rangeGot = true | |
| 155 | + | c.gotXIDRange(binary.LittleEndian.Uint32(r.Raw[8:]), binary.LittleEndian.Uint32(r.Raw[12:])) | |
| 158 | 156 | } | |
| 159 | 157 | } | |
| 160 | 158 | ||
| 161 | 159 | // consumeError handles an error whose request was registered with a reserved | |
| 162 | 160 | // kind (1..15) and reports whether it consumed it; an unconsumed error is | |
| 163 | 161 | // 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. | |
| 166 | 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 | 171 | if !c.syncFailed { | |
| 168 | 172 | c.syncFailed = true | |
| 169 | 173 | 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 reserved | 159 | // consumeError handles an error whose request was registered with a reserved | ||
| 162 | // kind (1..15) and reports whether it consumed it; an unconsumed error is | 160 | // kind (1..15) and reports whether it consumed it; an unconsumed error is | ||
| 163 | // delivered to OnError. Every reserved kind is root's own request, so all are | 161 | // 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 = true | 172 | c.syncFailed = true | ||
| 169 | c.syncErr = *e | 173 | c.syncErr = *e | ||
M setup.go +30 -8
| @@ -5,6 +5,7 @@ import ( | |||
| 5 | 5 | "fmt" | |
| 6 | 6 | "io" | |
| 7 | 7 | "os" | |
| 8 | + | "time" | |
| 8 | 9 | ||
| 9 | 10 | "golang.org/x/sys/unix" | |
| 10 | 11 | ) | |
| @@ -46,22 +47,24 @@ func encodeSetupRequest(authName string, authData []byte) []byte { | |||
| 46 | 47 | ||
| 47 | 48 | func pad4(n int) int { return (n + 3) &^ 3 } | |
| 48 | 49 | ||
| 49 | - | // handshake runs the blocking setup exchange on fd and returns the full setup | |
| 50 | - | // reply. A refusal is a *SetupError; a broken reply wraps ErrMalformed. | |
| 51 | - | func handshake(fd int, authName string, authData []byte) ([]byte, setupInfo, error) { | |
| 50 | + | // handshake runs the setup exchange on the non-blocking fd, waiting with poll | |
| 51 | + | // until deadline, and returns the full setup reply. A refusal is a | |
| 52 | + | // *SetupError; a broken reply wraps ErrMalformed; a silent server an error | |
| 53 | + | // wrapping os.ErrDeadlineExceeded. | |
| 54 | + | func handshake(fd int, authName string, authData []byte, deadline time.Time) ([]byte, setupInfo, error) { | |
| 52 | 55 | if len(authName) > 0xffff || len(authData) > 0xffff { | |
| 53 | 56 | return nil, setupInfo{}, ErrBadLength | |
| 54 | 57 | } | |
| 55 | - | if err := writeFull(fd, encodeSetupRequest(authName, authData)); err != nil { | |
| 58 | + | if err := writeFull(fd, encodeSetupRequest(authName, authData), deadline); err != nil { | |
| 56 | 59 | return nil, setupInfo{}, err | |
| 57 | 60 | } | |
| 58 | 61 | var hdr [setupHeaderSize]byte | |
| 59 | - | if err := readFull(fd, hdr[:]); err != nil { | |
| 62 | + | if err := readFull(fd, hdr[:], deadline); err != nil { | |
| 60 | 63 | return nil, setupInfo{}, err | |
| 61 | 64 | } | |
| 62 | 65 | reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:]))) | |
| 63 | 66 | copy(reply, hdr[:]) | |
| 64 | - | if err := readFull(fd, reply[setupHeaderSize:]); err != nil { | |
| 67 | + | if err := readFull(fd, reply[setupHeaderSize:], deadline); err != nil { | |
| 65 | 68 | return nil, setupInfo{}, err | |
| 66 | 69 | } | |
| 67 | 70 | info, err := parseSetup(reply) | |
| @@ -141,12 +144,26 @@ func parseSetup(p []byte) (setupInfo, error) { | |||
| 141 | 144 | return info, nil | |
| 142 | 145 | } | |
| 143 | 146 | ||
| 144 | - | func writeFull(fd int, p []byte) error { | |
| 147 | + | // waitFd polls fd for events until deadline. | |
| 148 | + | func waitFd(fd int, events int16, deadline time.Time) error { | |
| 149 | + | left := time.Until(deadline) | |
| 150 | + | if left <= 0 { | |
| 151 | + | return deadlineError("connection setup") | |
| 152 | + | } | |
| 153 | + | return pollFd(fd, events, left) | |
| 154 | + | } | |
| 155 | + | ||
| 156 | + | func writeFull(fd int, p []byte, deadline time.Time) error { | |
| 145 | 157 | for len(p) > 0 { | |
| 146 | 158 | n, err := unix.Write(fd, p) | |
| 147 | 159 | switch { | |
| 148 | 160 | case err == unix.EINTR: | |
| 149 | 161 | continue | |
| 162 | + | case err == unix.EAGAIN: | |
| 163 | + | if err := waitFd(fd, unix.POLLOUT, deadline); err != nil { | |
| 164 | + | return err | |
| 165 | + | } | |
| 166 | + | continue | |
| 150 | 167 | case err == unix.EPIPE || err == unix.ECONNRESET: | |
| 151 | 168 | return ErrClosed | |
| 152 | 169 | case err != nil: | |
| @@ -159,12 +176,17 @@ func writeFull(fd int, p []byte) error { | |||
| 159 | 176 | return nil | |
| 160 | 177 | } | |
| 161 | 178 | ||
| 162 | - | func readFull(fd int, p []byte) error { | |
| 179 | + | func readFull(fd int, p []byte, deadline time.Time) error { | |
| 163 | 180 | for len(p) > 0 { | |
| 164 | 181 | n, err := unix.Read(fd, p) | |
| 165 | 182 | switch { | |
| 166 | 183 | case err == unix.EINTR: | |
| 167 | 184 | continue | |
| 185 | + | case err == unix.EAGAIN: | |
| 186 | + | if err := waitFd(fd, unix.POLLIN, deadline); err != nil { | |
| 187 | + | return err | |
| 188 | + | } | |
| 189 | + | continue | |
| 168 | 190 | case err == unix.ECONNRESET: | |
| 169 | 191 | return ErrClosed | |
| 170 | 192 | case err != nil: | |
| @@ -5,6 +5,7 @@ import ( | |||||
| 5 | "fmt" | 5 | "fmt" | ||
| 6 | "io" | 6 | "io" | ||
| 7 | "os" | 7 | "os" | ||
| 8 | + | "time" | |||
| 8 | 9 | ||||
| 9 | "golang.org/x/sys/unix" | 10 | "golang.org/x/sys/unix" | ||
| 10 | ) | 11 | ) | ||
| @@ -46,22 +47,24 @@ func encodeSetupRequest(authName string, authData []byte) []byte { | |||||
| 46 | 47 | ||||
| 47 | func pad4(n int) int { return (n + 3) &^ 3 } | 48 | func pad4(n int) int { return (n + 3) &^ 3 } | ||
| 48 | 49 | ||||
| 49 | - | // handshake runs the blocking setup exchange on fd and returns the full setup | 50 | + | // handshake runs the setup exchange on the non-blocking fd, waiting with poll |
| 50 | - | // reply. A refusal is a *SetupError; a broken reply wraps ErrMalformed. | 51 | + | // until deadline, and returns the full setup reply. A refusal is a |
| 51 | - | func handshake(fd int, authName string, authData []byte) ([]byte, setupInfo, error) { | 52 | + | // *SetupError; a broken reply wraps ErrMalformed; a silent server an error |
| 53 | + | // wrapping os.ErrDeadlineExceeded. | |||
| 54 | + | func handshake(fd int, authName string, authData []byte, deadline time.Time) ([]byte, setupInfo, error) { | |||
| 52 | if len(authName) > 0xffff || len(authData) > 0xffff { | 55 | if len(authName) > 0xffff || len(authData) > 0xffff { | ||
| 53 | return nil, setupInfo{}, ErrBadLength | 56 | return nil, setupInfo{}, ErrBadLength | ||
| 54 | } | 57 | } | ||
| 55 | - | if err := writeFull(fd, encodeSetupRequest(authName, authData)); err != nil { | 58 | + | if err := writeFull(fd, encodeSetupRequest(authName, authData), deadline); err != nil { |
| 56 | return nil, setupInfo{}, err | 59 | return nil, setupInfo{}, err | ||
| 57 | } | 60 | } | ||
| 58 | var hdr [setupHeaderSize]byte | 61 | var hdr [setupHeaderSize]byte | ||
| 59 | - | if err := readFull(fd, hdr[:]); err != nil { | 62 | + | if err := readFull(fd, hdr[:], deadline); err != nil { |
| 60 | return nil, setupInfo{}, err | 63 | return nil, setupInfo{}, err | ||
| 61 | } | 64 | } | ||
| 62 | reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:]))) | 65 | reply := make([]byte, setupHeaderSize+4*int(binary.LittleEndian.Uint16(hdr[6:]))) | ||
| 63 | copy(reply, hdr[:]) | 66 | copy(reply, hdr[:]) | ||
| 64 | - | if err := readFull(fd, reply[setupHeaderSize:]); err != nil { | 67 | + | if err := readFull(fd, reply[setupHeaderSize:], deadline); err != nil { |
| 65 | return nil, setupInfo{}, err | 68 | return nil, setupInfo{}, err | ||
| 66 | } | 69 | } | ||
| 67 | info, err := parseSetup(reply) | 70 | info, err := parseSetup(reply) | ||
| @@ -141,12 +144,26 @@ func parseSetup(p []byte) (setupInfo, error) { | |||||
| 141 | return info, nil | 144 | return info, nil | ||
| 142 | } | 145 | } | ||
| 143 | 146 | ||||
| 144 | - | func writeFull(fd int, p []byte) error { | 147 | + | // waitFd polls fd for events until deadline. |
| 148 | + | func waitFd(fd int, events int16, deadline time.Time) error { | |||
| 149 | + | left := time.Until(deadline) | |||
| 150 | + | if left <= 0 { | |||
| 151 | + | return deadlineError("connection setup") | |||
| 152 | + | } | |||
| 153 | + | return pollFd(fd, events, left) | |||
| 154 | + | } | |||
| 155 | + | ||||
| 156 | + | func writeFull(fd int, p []byte, deadline time.Time) error { | |||
| 145 | for len(p) > 0 { | 157 | for len(p) > 0 { | ||
| 146 | n, err := unix.Write(fd, p) | 158 | n, err := unix.Write(fd, p) | ||
| 147 | switch { | 159 | switch { | ||
| 148 | case err == unix.EINTR: | 160 | case err == unix.EINTR: | ||
| 149 | continue | 161 | continue | ||
| 162 | + | case err == unix.EAGAIN: | |||
| 163 | + | if err := waitFd(fd, unix.POLLOUT, deadline); err != nil { | |||
| 164 | + | return err | |||
| 165 | + | } | |||
| 166 | + | continue | |||
| 150 | case err == unix.EPIPE || err == unix.ECONNRESET: | 167 | case err == unix.EPIPE || err == unix.ECONNRESET: | ||
| 151 | return ErrClosed | 168 | return ErrClosed | ||
| 152 | case err != nil: | 169 | case err != nil: | ||
| @@ -159,12 +176,17 @@ func writeFull(fd int, p []byte) error { | |||||
| 159 | return nil | 176 | return nil | ||
| 160 | } | 177 | } | ||
| 161 | 178 | ||||
| 162 | - | func readFull(fd int, p []byte) error { | 179 | + | func readFull(fd int, p []byte, deadline time.Time) error { |
| 163 | for len(p) > 0 { | 180 | for len(p) > 0 { | ||
| 164 | n, err := unix.Read(fd, p) | 181 | n, err := unix.Read(fd, p) | ||
| 165 | switch { | 182 | switch { | ||
| 166 | case err == unix.EINTR: | 183 | case err == unix.EINTR: | ||
| 167 | continue | 184 | continue | ||
| 185 | + | case err == unix.EAGAIN: | |||
| 186 | + | if err := waitFd(fd, unix.POLLIN, deadline); err != nil { | |||
| 187 | + | return err | |||
| 188 | + | } | |||
| 189 | + | continue | |||
| 168 | case err == unix.ECONNRESET: | 190 | case err == unix.ECONNRESET: | ||
| 169 | return ErrClosed | 191 | return ErrClosed | ||
| 170 | case err != nil: | 192 | case err != nil: | ||
M sync.go +55 -18
| @@ -2,7 +2,9 @@ package xturbo | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "encoding/binary" | |
| 5 | + | "fmt" | |
| 5 | 6 | "os" | |
| 7 | + | "time" | |
| 6 | 8 | ||
| 7 | 9 | "golang.org/x/sys/unix" | |
| 8 | 10 | ) | |
| @@ -62,9 +64,24 @@ func encodeExtRequest(b *Buf, id ExtID, minor uint8) Seq { | |||
| 62 | 64 | 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 | 81 | // 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 { | |
| 68 | 85 | for id := ExtCore + 1; id < extCount; id++ { | |
| 69 | 86 | seq := encodeQueryExtension(c.buf, ExtXName[id]) | |
| 70 | 87 | if seq == 0 { | |
| @@ -74,9 +91,9 @@ func (c *Conn) bootstrap() error { | |||
| 74 | 91 | return err | |
| 75 | 92 | } | |
| 76 | 93 | } | |
| 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) | |
| 80 | 97 | } | |
| 81 | 98 | ||
| 82 | 99 | // Sync flushes every queued request and blocks until the server has | |
| @@ -85,9 +102,15 @@ func (c *Conn) bootstrap() error { | |||
| 85 | 102 | // full round trip: errors of earlier requests have been delivered when it | |
| 86 | 103 | // returns. Packets read meanwhile go to h as with Dispatch (nil h: the last | |
| 87 | 104 | // 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. | |
| 90 | 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 | 114 | if c.dispatching { | |
| 92 | 115 | return errReentrantDispatch | |
| 93 | 116 | } | |
| @@ -95,7 +118,7 @@ func (c *Conn) Sync(h *Handler) error { | |||
| 95 | 118 | h = c.h | |
| 96 | 119 | } | |
| 97 | 120 | c.syncFailed = false | |
| 98 | - | if err := c.wait(h); err != nil { | |
| 121 | + | if err := c.waitUntil(h, deadline); err != nil { | |
| 99 | 122 | return err | |
| 100 | 123 | } | |
| 101 | 124 | if c.syncFailed { | |
| @@ -105,10 +128,11 @@ func (c *Conn) Sync(h *Handler) error { | |||
| 105 | 128 | return nil | |
| 106 | 129 | } | |
| 107 | 130 | ||
| 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 { | |
| 112 | 136 | if c.fd < 0 { | |
| 113 | 137 | return ErrClosed | |
| 114 | 138 | } | |
| @@ -123,7 +147,7 @@ func (c *Conn) wait(h *Handler) error { | |||
| 123 | 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 | 151 | return err | |
| 128 | 152 | } | |
| 129 | 153 | } | |
| @@ -131,9 +155,15 @@ func (c *Conn) wait(h *Handler) error { | |||
| 131 | 155 | ||
| 132 | 156 | // pollOnce makes one step of progress: flush, dispatch what is readable and, | |
| 133 | 157 | // 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 | + | } | |
| 137 | 167 | events := int16(unix.POLLIN) | |
| 138 | 168 | switch err := c.Flush(); err { | |
| 139 | 169 | case nil: | |
| @@ -150,8 +180,15 @@ func (c *Conn) pollOnce(h *Handler) error { | |||
| 150 | 180 | if !c.rdIdle || (events&unix.POLLOUT == 0 && c.buf.Len() > 0) { | |
| 151 | 181 | return nil | |
| 152 | 182 | } | |
| 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 { | |
| 155 | 192 | return os.NewSyscallError("poll", err) | |
| 156 | 193 | } | |
| 157 | 194 | 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 seq | 64 | 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 pipelined | 81 | // 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 err | 91 | return err | ||
| 75 | } | 92 | } | ||
| 76 | } | 93 | } | ||
| 77 | - | // consumeReply queues bigreq Enable when BIG-REQUESTS is present; Sync | 94 | + | // 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 has | 99 | // 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 it | 102 | // full round trip: errors of earlier requests have been delivered when it | ||
| 86 | // returns. Packets read meanwhile go to h as with Dispatch (nil h: the last | 103 | // returns. Packets read meanwhile go to h as with Dispatch (nil h: the last | ||
| 87 | // Handler given to Dispatch, or none). An error on a request root issued | 104 | // 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 | 105 | + | // 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 errReentrantDispatch | 115 | return errReentrantDispatch | ||
| 93 | } | 116 | } | ||
| @@ -95,7 +118,7 @@ func (c *Conn) Sync(h *Handler) error { | |||||
| 95 | h = c.h | 118 | h = c.h | ||
| 96 | } | 119 | } | ||
| 97 | c.syncFailed = false | 120 | c.syncFailed = false | ||
| 98 | - | if err := c.wait(h); err != nil { | 121 | + | if err := c.waitUntil(h, deadline); err != nil { |
| 99 | return err | 122 | 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 nil | 128 | return nil | ||
| 106 | } | 129 | } | ||
| 107 | 130 | ||||
| 108 | - | // wait fences, flushes and dispatches, one bounded Dispatch at a time, until | 131 | + | // waitUntil fences, flushes and dispatches, one bounded Dispatch at a time, |
| 109 | - | // the fence has been answered and no registered slot remains. A fence is | 132 | + | // 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 ErrClosed | 137 | 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 err | 151 | 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 readable | 157 | // 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 nil | 181 | 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 nil | 194 | return nil | ||
M xid.go +130 -41
| @@ -1,13 +1,27 @@ | |||
| 1 | 1 | package xturbo | |
| 2 | 2 | ||
| 3 | - | import "errors" | |
| 3 | + | import ( | |
| 4 | + | "errors" | |
| 5 | + | "time" | |
| 6 | + | ) | |
| 4 | 7 | ||
| 5 | 8 | // 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 | 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. | |
| 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 | + | } | |
| 11 | 25 | ||
| 12 | 26 | // initXIDs starts allocation over the setup range: base|0, base|inc, … | |
| 13 | 27 | // up to base|mask, in libxcb's order. | |
| @@ -16,55 +30,86 @@ func (c *Conn) initXIDs() { | |||
| 16 | 30 | if inc == 0 { | |
| 17 | 31 | return | |
| 18 | 32 | } | |
| 19 | - | c.xidNext = 0 | |
| 20 | - | c.xidLeft = c.xidMask/inc + 1 | |
| 33 | + | c.xids = xidRange{0, c.xidMask/inc + 1} | |
| 21 | 34 | } | |
| 22 | 35 | ||
| 23 | 36 | // 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. | |
| 29 | 45 | func (c *Conn) NewXID() (uint32, error) { | |
| 30 | 46 | if c.xidFreeN > 0 { | |
| 31 | 47 | c.xidFreeN-- | |
| 32 | 48 | return c.xidFree[c.xidFreeN], nil | |
| 33 | 49 | } | |
| 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 | + | } | |
| 37 | 55 | } | |
| 56 | + | c.xids, c.xidPend = c.xidPend, xidRange{} | |
| 38 | 57 | } | |
| 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-- | |
| 42 | 61 | return id, nil | |
| 43 | 62 | } | |
| 44 | 63 | ||
| 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. | |
| 48 | 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 | 71 | return | |
| 51 | 72 | } | |
| 52 | 73 | c.xidFree[c.xidFreeN] = id | |
| 53 | 74 | c.xidFreeN++ | |
| 54 | 75 | } | |
| 55 | 76 | ||
| 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 | |
| 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 | 97 | if c.fd < 0 { | |
| 61 | 98 | return ErrClosed | |
| 62 | 99 | } | |
| 63 | 100 | if !c.ext.Info(ExtXCMisc).Present { | |
| 64 | 101 | return ErrExtMissing | |
| 65 | 102 | } | |
| 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 | |
| 68 | 113 | } | |
| 69 | 114 | seq := encodeExtRequest(c.buf, ExtXCMisc, minorGetXIDRange) | |
| 70 | 115 | if seq == 0 { | |
| @@ -73,23 +118,67 @@ func (c *Conn) refillXIDs() error { | |||
| 73 | 118 | if err := c.expect(seq, kindXIDRange, [4]uint32{}); err != nil { | |
| 74 | 119 | return err | |
| 75 | 120 | } | |
| 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 { | |
| 79 | 173 | return err | |
| 80 | 174 | } | |
| 81 | - | if c.syncFailed { | |
| 82 | - | e := c.syncErr | |
| 175 | + | switch { | |
| 176 | + | case c.xidFailed: | |
| 177 | + | e := c.xidErr | |
| 83 | 178 | 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: | |
| 89 | 180 | return errXIDsExhausted | |
| 90 | 181 | } | |
| 91 | - | c.xidNext = c.rangeNext & c.xidMask | |
| 92 | - | c.xidLeft = c.rangeLeft | |
| 93 | 182 | return nil | |
| 94 | 183 | } | |
| 95 | 184 | ||
| @@ -97,9 +186,9 @@ func (c *Conn) refillXIDs() error { | |||
| 97 | 186 | // reserved registration can always be made for it: when an earlier request | |
| 98 | 187 | // still holds it, it waits for every outstanding request (fenced, so even a | |
| 99 | 188 | // 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 { | |
| 102 | 191 | return nil | |
| 103 | 192 | } | |
| 104 | - | return c.wait(c.h) | |
| 193 | + | return c.waitUntil(c.h, deadline) | |
| 105 | 194 | } | |
| @@ -1,13 +1,27 @@ | |||||
| 1 | package xturbo | 1 | 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 this | 8 | // 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 = 256 | 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 | + | } | |||
| 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 | return | 31 | return | ||
| 18 | } | 32 | } | ||
| 19 | - | c.xidNext = 0 | 33 | + | 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 with | 36 | // 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 | 37 | + | // FreeXID are reused first, then the current range, then a range fetched |
| 25 | - | // exhausted one blocking XC-MISC GetXIDRange round trip refills it (packets | 38 | + | // 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 when | 39 | + | // 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 a | 40 | + | // 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], nil | 48 | 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, err | 52 | + | 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.xidNext | 58 | + | id := c.xidBase | c.xids.next |
| 40 | - | c.xidNext += c.xidMask & -c.xidMask | 59 | + | c.xids.next += c.xidMask & -c.xidMask |
| 41 | - | c.xidLeft-- | 60 | + | c.xids.left-- |
| 42 | return id, nil | 61 | return id, nil | ||
| 43 | } | 62 | } | ||
| 44 | 63 | ||||
| 45 | - | // FreeXID returns id, whose resource the caller has already destroyed (the | 64 | + | // 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 this | 65 | + | // 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 | return | 71 | return | ||
| 51 | } | 72 | } | ||
| 52 | c.xidFree[c.xidFreeN] = id | 73 | 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 errReentrantDispatch | 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 | |||
| 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 ErrClosed | 98 | return ErrClosed | ||
| 62 | } | 99 | } | ||
| 63 | if !c.ext.Info(ExtXCMisc).Present { | 100 | if !c.ext.Info(ExtXCMisc).Present { | ||
| 64 | return ErrExtMissing | 101 | return ErrExtMissing | ||
| 65 | } | 102 | } | ||
| 66 | - | if err := c.waitSlot(); err != nil { | 103 | + | if c.xidPend.left > 0 || c.xidInFlight() { |
| 67 | - | return err | 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 | |||
| 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 err | 119 | return err | ||
| 75 | } | 120 | } | ||
| 76 | - | c.rangeGot = false | 121 | + | c.xidReq = seq |
| 77 | - | c.syncFailed = false | 122 | + | 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 err | 173 | return err | ||
| 80 | } | 174 | } | ||
| 81 | - | if c.syncFailed { | 175 | + | switch { |
| 82 | - | e := c.syncErr | 176 | + | case c.xidFailed: |
| 177 | + | e := c.xidErr | |||
| 83 | return &e | 178 | 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 errXIDsExhausted | 180 | return errXIDsExhausted | ||
| 90 | } | 181 | } | ||
| 91 | - | c.xidNext = c.rangeNext & c.xidMask | |||
| 92 | - | c.xidLeft = c.rangeLeft | |||
| 93 | return nil | 182 | 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 request | 186 | // reserved registration can always be made for it: when an earlier request | ||
| 98 | // still holds it, it waits for every outstanding request (fenced, so even a | 187 | // 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 nil | 191 | 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 ( | |||
| 5 | 5 | "io" | |
| 6 | 6 | "os" | |
| 7 | 7 | "sync" | |
| 8 | + | "sync/atomic" | |
| 8 | 9 | "testing" | |
| 9 | 10 | ) | |
| 10 | 11 | ||
| @@ -24,6 +25,9 @@ type script struct { | |||
| 24 | 25 | xidRanges [][2]uint32 // successive GetXIDRange replies {start, count} | |
| 25 | 26 | // hook sees every request first; returning true skips the default. | |
| 26 | 27 | hook func(s *scriptConn, major, minor uint8, seq uint16, body []byte) bool | |
| 28 | + | // silent, once set, makes the server read requests and never answer | |
| 29 | + | // (alive but hung). | |
| 30 | + | silent atomic.Bool | |
| 27 | 31 | } | |
| 28 | 32 | ||
| 29 | 33 | // scriptConn is the server side of one scripted connection. | |
| @@ -119,7 +123,7 @@ func (s *scriptConn) serve() { | |||
| 119 | 123 | s.mu.Lock() | |
| 120 | 124 | s.reqs = append(s.reqs, scriptReq{s.seq, major, minor, raw}) | |
| 121 | 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 | 127 | continue | |
| 124 | 128 | } | |
| 125 | 129 | 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) bool | 27 | hook func(s *scriptConn, major, minor uint8, seq uint16, body []byte) bool | ||
| 28 | + | // silent, once set, makes the server read requests and never answer | |||
| 29 | + | // (alive but hung). | |||
| 30 | + | silent atomic.Bool | |||
| 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 | continue | 127 | continue | ||
| 124 | } | 128 | } | ||
| 125 | s.answer(major, minor, body) | 129 | s.answer(major, minor, body) | ||