detached 602baa7

#7 chore(release): prepare v0.1.0 (README, example, fuzz CI)

state
merged · 602baa7 · fast-forward
branch
chore/release-v0.1.0 → main
opened
bnema ·
updated

README with usage example and rules, compile-checked example_test.go, make fuzz target and scheduled fuzz workflow.

CommitSubjectAuthorDateSignatureStatus
602baa7test(runtime): unblock the flood writer by closing the client in TestSyncDeadlineFloodbriceGPG Verified
c65691cdocs(readme): document handshake blocking and widen fuzz timeoutbriceGPG Verified
1bfe747ci(fuzz): give generated targets a shorter fuzz time so the workflow fits its timeoutbriceGPG Verified
32dc121docs(readme): add README with a compiling example and benchmark tablebriceGPG Verified
bb30cc0ci(fuzz): add make fuzz target and a fuzz workflow on mainbriceGPG Verified

Checks 5

5 files · +294 -2
5/5
A .github/workflows/fuzz.yml +17 -0
@@ -0,0 +1,17 @@
1+name: fuzz
2+on:
3+ push:
4+ branches: [main]
5+ workflow_dispatch:
6+permissions:
7+ contents: read
8+jobs:
9+ fuzz:
10+ runs-on: ubuntu-latest
11+ timeout-minutes: 60
12+ steps:
13+ - uses: actions/checkout@v4
14+ - uses: actions/setup-go@v5
15+ with:
16+ go-version-file: go.mod
17+ - run: make fuzz FUZZTIME=60s GENFUZZTIME=20s
@@ -0,0 +1,17 @@
1+name: fuzz
2+on:
3+ push:
4+ branches: [main]
5+ workflow_dispatch:
6+permissions:
7+ contents: read
8+jobs:
9+ fuzz:
10+ runs-on: ubuntu-latest
11+ timeout-minutes: 60
12+ steps:
13+ - uses: actions/checkout@v4
14+ - uses: actions/setup-go@v5
15+ with:
16+ go-version-file: go.mod
17+ - run: make fuzz FUZZTIME=60s GENFUZZTIME=20s
M Makefile +13 -1
@@ -1,7 +1,9 @@
11 export CGO_ENABLED := 0
22 STATICCHECK ?= staticcheck
3+FUZZTIME ?= 60s
4+GENFUZZTIME ?= 10s
35
4-.PHONY: gen gen-check test race alloc poison bench xserver staticcheck vet check
6+.PHONY: gen gen-check test race alloc poison bench fuzz xserver staticcheck vet check
57
68 gen:
79 go generate ./...
@@ -18,6 +20,16 @@ poison:
1820 go test -tags xturbo_poison ./...
1921 bench:
2022 go test -bench . -benchmem ./...
23+# Root targets (dispatch, event decode, reply ring) get FUZZTIME; generated
24+# packages and the generator get GENFUZZTIME.
25+fuzz:
26+ @set -e; root=$$(go list .); for pkg in $$(go list ./...); do \
27+ time=$(GENFUZZTIME); [ "$$pkg" = "$$root" ] && time=$(FUZZTIME); \
28+ for t in $$(go test -list 'Fuzz.*' $$pkg | grep '^Fuzz'); do \
29+ echo "== $$pkg $$t ($$time)"; \
30+ go test -run '^$$' -fuzz "^$$t$$" -fuzztime $$time $$pkg; \
31+ done; \
32+ done
2133 xserver:
2234 PATH="$$PATH:/usr/sbin" go test -tags xserver ./...
2335 staticcheck:
@@ -1,7 +1,9 @@
1 export CGO_ENABLED := 01 export CGO_ENABLED := 0
2 STATICCHECK ?= staticcheck2 STATICCHECK ?= staticcheck
3+FUZZTIME ?= 60s
4+GENFUZZTIME ?= 10s
3 5
4-.PHONY: gen gen-check test race alloc poison bench xserver staticcheck vet check6+.PHONY: gen gen-check test race alloc poison bench fuzz xserver staticcheck vet check
5 7
6 gen:8 gen:
7 go generate ./...9 go generate ./...
@@ -18,6 +20,16 @@ poison:
18 go test -tags xturbo_poison ./...20 go test -tags xturbo_poison ./...
19 bench:21 bench:
20 go test -bench . -benchmem ./...22 go test -bench . -benchmem ./...
23+# Root targets (dispatch, event decode, reply ring) get FUZZTIME; generated
24+# packages and the generator get GENFUZZTIME.
25+fuzz:
26+ @set -e; root=$$(go list .); for pkg in $$(go list ./...); do \
27+ time=$(GENFUZZTIME); [ "$$pkg" = "$$root" ] && time=$(FUZZTIME); \
28+ for t in $$(go test -list 'Fuzz.*' $$pkg | grep '^Fuzz'); do \
29+ echo "== $$pkg $$t ($$time)"; \
30+ go test -run '^$$' -fuzz "^$$t$$" -fuzztime $$time $$pkg; \
31+ done; \
32+ done
21 xserver:33 xserver:
22 PATH="$$PATH:/usr/sbin" go test -tags xserver ./...34 PATH="$$PATH:/usr/sbin" go test -tags xserver ./...
23 staticcheck:35 staticcheck:
A README.md +176 -0
@@ -0,0 +1,176 @@
1+# xturbo
2+
3+A pure-Go X11 client runtime with generated bindings. The runtime speaks the
4+X11 wire protocol over a non-blocking Unix socket; the bindings come from
5+xcb-proto 1.17.0. It depends only on the standard library and
6+`golang.org/x/sys`, builds with `CGO_ENABLED=0`, and keeps the steady-state
7+request, reply, event and error paths free of allocations.
8+
9+Module: `bnema.dev/xturbo`. Source: <https://git.bnema.dev/xturbo>.
10+
11+## Packages
12+
13+| Package | Contents |
14+| --- | --- |
15+| `bnema.dev/xturbo` | `Conn`, `Dial`, `Buf`, `Dispatch`, `Expect`, `Sync`, `InternAtoms`, XID allocation |
16+| `xproto` | core protocol |
17+| `bigreq`, `ge`, `xcmisc` | BIG-REQUESTS, Generic Event, XC-MISC |
18+| `composite`, `xfixes`, `render`, `shape`, `res` | Composite, XFIXES, RENDER, SHAPE, X-Resource |
19+
20+The root package never imports the generated packages.
21+
22+## Install
23+
24+```sh
25+go get bnema.dev/xturbo
26+```
27+
28+## Example
29+
30+`Dial` (or `NewConn(fd)` for an already-connected socket) performs the
31+handshake and negotiates the supported extensions. Requests are encoded into
32+the connection's `Buf` and return a `Seq` (0 on failure). `Flush` never
33+blocks, and `Dispatch` reads whatever the server sent and calls the `Handler`.
34+
35+```go
36+package xturbo_test
37+
38+import (
39+ "errors"
40+ "log"
41+
42+ "golang.org/x/sys/unix"
43+
44+ "bnema.dev/xturbo"
45+ "bnema.dev/xturbo/xproto"
46+)
47+
48+// Replies registered with Expect carry this kind (16 and above are free).
49+const kindGeometry uint16 = 16
50+
51+func Example() {
52+ c, err := xturbo.Dial("") // "" means $DISPLAY
53+ if err != nil {
54+ log.Fatal(err)
55+ }
56+ defer c.Close()
57+
58+ // Startup: blocking calls, bounded by a 5 s deadline.
59+ var atoms [1]uint32
60+ if err := c.InternAtoms([]string{"WM_DELETE_WINDOW"}, false, atoms[:]); err != nil {
61+ log.Fatal(err)
62+ }
63+
64+ // Queue requests; they return a Seq (0 on failure, see c.Buf().Err()).
65+ b := c.Buf()
66+ win, err := c.NewXID()
67+ if err != nil {
68+ log.Fatal(err)
69+ }
70+ var v xproto.CreateWindowValues
71+ v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify)
72+ xproto.CreateWindow(b, 0, xproto.Window(win), xproto.Window(c.Root()),
73+ 0, 0, 640, 480, 0, xproto.WindowClassInputOutput, 0, &v)
74+ xproto.MapWindow(b, xproto.Window(win))
75+ if seq := xproto.GetGeometry(b, xproto.Drawable(win)); seq != 0 {
76+ if err := c.Expect(seq, kindGeometry, win, 0, 0, 0); err != nil {
77+ log.Fatal(err)
78+ }
79+ }
80+
81+ // Callbacks run inside Dispatch. Events, replies and errors are borrowed:
82+ // copy what you keep.
83+ h := &xturbo.Handler{
84+ OnEvent: func(ev *xturbo.Event) {
85+ switch ev.Kind {
86+ case xproto.KindExpose:
87+ e := xproto.ExposeOf(ev)
88+ log.Printf("expose %dx%d", e.Width(), e.Height())
89+ }
90+ },
91+ OnReply: func(r *xturbo.Reply) {
92+ if r.Kind == kindGeometry {
93+ if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok {
94+ log.Printf("window %#x is %dx%d", r.Args[0], g.Width(), g.Height())
95+ }
96+ }
97+ },
98+ OnError: func(e *xturbo.XError) { log.Print(e) },
99+ }
100+
101+ for {
102+ // Flush never blocks. On ErrWouldBlock, also wait for POLLOUT.
103+ events := int16(unix.POLLIN)
104+ if err := c.Flush(); errors.Is(err, xturbo.ErrWouldBlock) {
105+ events |= unix.POLLOUT
106+ } else if err != nil {
107+ log.Fatal(err)
108+ }
109+ fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}}
110+ if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR {
111+ log.Fatal(err)
112+ }
113+ if err := c.Dispatch(h); err != nil { // io.EOF: server closed
114+ log.Fatal(err)
115+ }
116+ // Keep the XID pool topped up without blocking later.
117+ if c.XIDsLow() {
118+ _ = c.PrefetchXIDs()
119+ }
120+ }
121+}
122+```
123+
124+## Rules
125+
126+- **Lifetimes.** A request's body slice is valid only until the next request is
127+ encoded. Reply views (`Reply.Raw` and the parsed views) and event `Tail` are
128+ valid only during the callback; `Event.Raw` is a copy. Copy what you keep.
129+- **Callbacks.** They may queue requests, call `Expect` and `PrefetchXIDs`.
130+ They must not call `Dispatch`, `Sync`, `InternAtoms`, or `NewXID` when it
131+ needs a refill.
132+- **Blocking.** `Dial` and `NewConn` block for at most 10 s during the
133+ handshake. Only `Sync`, `InternAtoms` and a `NewXID` refill block after
134+ that, for at most 5 s, and are meant for startup. Use `XIDsLow` and `PrefetchXIDs` to
135+ keep the event loop non-blocking.
136+- **Errors.** A failed `Buf` is sticky: later requests return 0 and `Flush`
137+ returns the error.
138+
139+## Performance
140+
141+Measured with `make bench` on an AMD Ryzen 9 7900X3D (Go 1.27, `CGO_ENABLED=0`).
142+Every benchmark reports 0 allocations per operation.
143+
144+| Benchmark | Result |
145+| --- | --- |
146+| Encode a request (`MapWindow`) | 6.2 ns |
147+| Encode and flush 64 requests over a socketpair | 1.55 µs (about 24 ns per request) |
148+| Decode 10k events | 124 µs (about 12 ns per event) |
149+| Dispatch 10k mixed packets | 251 µs (about 25 ns per packet) |
150+| Parse a `GetProperty` reply view | 1.9 ns |
151+| Parse a `QueryTree` reply view | 3.2 ns |
152+
153+## Regenerating bindings
154+
155+Generated code lives in the extension packages and `xproto`; do not edit it.
156+The inputs are the pinned xcb-proto 1.17.0 files in `spec/`, listed with
157+checksums in `spec/manifest.json`.
158+
159+```sh
160+make gen # go generate ./... (cmd/xtgen)
161+make gen-check # regenerate and fail on any difference
162+go run ./cmd/xtsync # refresh spec/ from upstream and update the manifest
163+```
164+
165+## Tests
166+
167+```sh
168+make check # vet, staticcheck, tests, race, allocation, poison, gen-check
169+make xserver # tests against a real Xvfb (needs xvfb and xtrace)
170+make fuzz # root targets for FUZZTIME (60s), generated ones for GENFUZZTIME (10s)
171+make bench # benchmarks
172+```
173+
174+## License
175+
176+MIT, see `LICENSE`. The xcb-proto XML and its license are under `LICENSES/`.
@@ -0,0 +1,176 @@
1+# xturbo
2+
3+A pure-Go X11 client runtime with generated bindings. The runtime speaks the
4+X11 wire protocol over a non-blocking Unix socket; the bindings come from
5+xcb-proto 1.17.0. It depends only on the standard library and
6+`golang.org/x/sys`, builds with `CGO_ENABLED=0`, and keeps the steady-state
7+request, reply, event and error paths free of allocations.
8+
9+Module: `bnema.dev/xturbo`. Source: <https://git.bnema.dev/xturbo>.
10+
11+## Packages
12+
13+| Package | Contents |
14+| --- | --- |
15+| `bnema.dev/xturbo` | `Conn`, `Dial`, `Buf`, `Dispatch`, `Expect`, `Sync`, `InternAtoms`, XID allocation |
16+| `xproto` | core protocol |
17+| `bigreq`, `ge`, `xcmisc` | BIG-REQUESTS, Generic Event, XC-MISC |
18+| `composite`, `xfixes`, `render`, `shape`, `res` | Composite, XFIXES, RENDER, SHAPE, X-Resource |
19+
20+The root package never imports the generated packages.
21+
22+## Install
23+
24+```sh
25+go get bnema.dev/xturbo
26+```
27+
28+## Example
29+
30+`Dial` (or `NewConn(fd)` for an already-connected socket) performs the
31+handshake and negotiates the supported extensions. Requests are encoded into
32+the connection's `Buf` and return a `Seq` (0 on failure). `Flush` never
33+blocks, and `Dispatch` reads whatever the server sent and calls the `Handler`.
34+
35+```go
36+package xturbo_test
37+
38+import (
39+ "errors"
40+ "log"
41+
42+ "golang.org/x/sys/unix"
43+
44+ "bnema.dev/xturbo"
45+ "bnema.dev/xturbo/xproto"
46+)
47+
48+// Replies registered with Expect carry this kind (16 and above are free).
49+const kindGeometry uint16 = 16
50+
51+func Example() {
52+ c, err := xturbo.Dial("") // "" means $DISPLAY
53+ if err != nil {
54+ log.Fatal(err)
55+ }
56+ defer c.Close()
57+
58+ // Startup: blocking calls, bounded by a 5 s deadline.
59+ var atoms [1]uint32
60+ if err := c.InternAtoms([]string{"WM_DELETE_WINDOW"}, false, atoms[:]); err != nil {
61+ log.Fatal(err)
62+ }
63+
64+ // Queue requests; they return a Seq (0 on failure, see c.Buf().Err()).
65+ b := c.Buf()
66+ win, err := c.NewXID()
67+ if err != nil {
68+ log.Fatal(err)
69+ }
70+ var v xproto.CreateWindowValues
71+ v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify)
72+ xproto.CreateWindow(b, 0, xproto.Window(win), xproto.Window(c.Root()),
73+ 0, 0, 640, 480, 0, xproto.WindowClassInputOutput, 0, &v)
74+ xproto.MapWindow(b, xproto.Window(win))
75+ if seq := xproto.GetGeometry(b, xproto.Drawable(win)); seq != 0 {
76+ if err := c.Expect(seq, kindGeometry, win, 0, 0, 0); err != nil {
77+ log.Fatal(err)
78+ }
79+ }
80+
81+ // Callbacks run inside Dispatch. Events, replies and errors are borrowed:
82+ // copy what you keep.
83+ h := &xturbo.Handler{
84+ OnEvent: func(ev *xturbo.Event) {
85+ switch ev.Kind {
86+ case xproto.KindExpose:
87+ e := xproto.ExposeOf(ev)
88+ log.Printf("expose %dx%d", e.Width(), e.Height())
89+ }
90+ },
91+ OnReply: func(r *xturbo.Reply) {
92+ if r.Kind == kindGeometry {
93+ if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok {
94+ log.Printf("window %#x is %dx%d", r.Args[0], g.Width(), g.Height())
95+ }
96+ }
97+ },
98+ OnError: func(e *xturbo.XError) { log.Print(e) },
99+ }
100+
101+ for {
102+ // Flush never blocks. On ErrWouldBlock, also wait for POLLOUT.
103+ events := int16(unix.POLLIN)
104+ if err := c.Flush(); errors.Is(err, xturbo.ErrWouldBlock) {
105+ events |= unix.POLLOUT
106+ } else if err != nil {
107+ log.Fatal(err)
108+ }
109+ fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}}
110+ if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR {
111+ log.Fatal(err)
112+ }
113+ if err := c.Dispatch(h); err != nil { // io.EOF: server closed
114+ log.Fatal(err)
115+ }
116+ // Keep the XID pool topped up without blocking later.
117+ if c.XIDsLow() {
118+ _ = c.PrefetchXIDs()
119+ }
120+ }
121+}
122+```
123+
124+## Rules
125+
126+- **Lifetimes.** A request's body slice is valid only until the next request is
127+ encoded. Reply views (`Reply.Raw` and the parsed views) and event `Tail` are
128+ valid only during the callback; `Event.Raw` is a copy. Copy what you keep.
129+- **Callbacks.** They may queue requests, call `Expect` and `PrefetchXIDs`.
130+ They must not call `Dispatch`, `Sync`, `InternAtoms`, or `NewXID` when it
131+ needs a refill.
132+- **Blocking.** `Dial` and `NewConn` block for at most 10 s during the
133+ handshake. Only `Sync`, `InternAtoms` and a `NewXID` refill block after
134+ that, for at most 5 s, and are meant for startup. Use `XIDsLow` and `PrefetchXIDs` to
135+ keep the event loop non-blocking.
136+- **Errors.** A failed `Buf` is sticky: later requests return 0 and `Flush`
137+ returns the error.
138+
139+## Performance
140+
141+Measured with `make bench` on an AMD Ryzen 9 7900X3D (Go 1.27, `CGO_ENABLED=0`).
142+Every benchmark reports 0 allocations per operation.
143+
144+| Benchmark | Result |
145+| --- | --- |
146+| Encode a request (`MapWindow`) | 6.2 ns |
147+| Encode and flush 64 requests over a socketpair | 1.55 µs (about 24 ns per request) |
148+| Decode 10k events | 124 µs (about 12 ns per event) |
149+| Dispatch 10k mixed packets | 251 µs (about 25 ns per packet) |
150+| Parse a `GetProperty` reply view | 1.9 ns |
151+| Parse a `QueryTree` reply view | 3.2 ns |
152+
153+## Regenerating bindings
154+
155+Generated code lives in the extension packages and `xproto`; do not edit it.
156+The inputs are the pinned xcb-proto 1.17.0 files in `spec/`, listed with
157+checksums in `spec/manifest.json`.
158+
159+```sh
160+make gen # go generate ./... (cmd/xtgen)
161+make gen-check # regenerate and fail on any difference
162+go run ./cmd/xtsync # refresh spec/ from upstream and update the manifest
163+```
164+
165+## Tests
166+
167+```sh
168+make check # vet, staticcheck, tests, race, allocation, poison, gen-check
169+make xserver # tests against a real Xvfb (needs xvfb and xtrace)
170+make fuzz # root targets for FUZZTIME (60s), generated ones for GENFUZZTIME (10s)
171+make bench # benchmarks
172+```
173+
174+## License
175+
176+MIT, see `LICENSE`. The xcb-proto XML and its license are under `LICENSES/`.
A example_test.go +86 -0
@@ -0,0 +1,86 @@
1+package xturbo_test
2+
3+import (
4+ "errors"
5+ "log"
6+
7+ "golang.org/x/sys/unix"
8+
9+ "bnema.dev/xturbo"
10+ "bnema.dev/xturbo/xproto"
11+)
12+
13+// Replies registered with Expect carry this kind (16 and above are free).
14+const kindGeometry uint16 = 16
15+
16+func Example() {
17+ c, err := xturbo.Dial("") // "" means $DISPLAY
18+ if err != nil {
19+ log.Fatal(err)
20+ }
21+ defer c.Close()
22+
23+ // Startup: blocking calls, bounded by a 5 s deadline.
24+ var atoms [1]uint32
25+ if err := c.InternAtoms([]string{"WM_DELETE_WINDOW"}, false, atoms[:]); err != nil {
26+ log.Fatal(err)
27+ }
28+
29+ // Queue requests; they return a Seq (0 on failure, see c.Buf().Err()).
30+ b := c.Buf()
31+ win, err := c.NewXID()
32+ if err != nil {
33+ log.Fatal(err)
34+ }
35+ var v xproto.CreateWindowValues
36+ v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify)
37+ xproto.CreateWindow(b, 0, xproto.Window(win), xproto.Window(c.Root()),
38+ 0, 0, 640, 480, 0, xproto.WindowClassInputOutput, 0, &v)
39+ xproto.MapWindow(b, xproto.Window(win))
40+ if seq := xproto.GetGeometry(b, xproto.Drawable(win)); seq != 0 {
41+ if err := c.Expect(seq, kindGeometry, win, 0, 0, 0); err != nil {
42+ log.Fatal(err)
43+ }
44+ }
45+
46+ // Callbacks run inside Dispatch. Events, replies and errors are borrowed:
47+ // copy what you keep.
48+ h := &xturbo.Handler{
49+ OnEvent: func(ev *xturbo.Event) {
50+ switch ev.Kind {
51+ case xproto.KindExpose:
52+ e := xproto.ExposeOf(ev)
53+ log.Printf("expose %dx%d", e.Width(), e.Height())
54+ }
55+ },
56+ OnReply: func(r *xturbo.Reply) {
57+ if r.Kind == kindGeometry {
58+ if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok {
59+ log.Printf("window %#x is %dx%d", r.Args[0], g.Width(), g.Height())
60+ }
61+ }
62+ },
63+ OnError: func(e *xturbo.XError) { log.Print(e) },
64+ }
65+
66+ for {
67+ // Flush never blocks. On ErrWouldBlock, also wait for POLLOUT.
68+ events := int16(unix.POLLIN)
69+ if err := c.Flush(); errors.Is(err, xturbo.ErrWouldBlock) {
70+ events |= unix.POLLOUT
71+ } else if err != nil {
72+ log.Fatal(err)
73+ }
74+ fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}}
75+ if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR {
76+ log.Fatal(err)
77+ }
78+ if err := c.Dispatch(h); err != nil { // io.EOF: server closed
79+ log.Fatal(err)
80+ }
81+ // Keep the XID pool topped up without blocking later.
82+ if c.XIDsLow() {
83+ _ = c.PrefetchXIDs()
84+ }
85+ }
86+}
@@ -0,0 +1,86 @@
1+package xturbo_test
2+
3+import (
4+ "errors"
5+ "log"
6+
7+ "golang.org/x/sys/unix"
8+
9+ "bnema.dev/xturbo"
10+ "bnema.dev/xturbo/xproto"
11+)
12+
13+// Replies registered with Expect carry this kind (16 and above are free).
14+const kindGeometry uint16 = 16
15+
16+func Example() {
17+ c, err := xturbo.Dial("") // "" means $DISPLAY
18+ if err != nil {
19+ log.Fatal(err)
20+ }
21+ defer c.Close()
22+
23+ // Startup: blocking calls, bounded by a 5 s deadline.
24+ var atoms [1]uint32
25+ if err := c.InternAtoms([]string{"WM_DELETE_WINDOW"}, false, atoms[:]); err != nil {
26+ log.Fatal(err)
27+ }
28+
29+ // Queue requests; they return a Seq (0 on failure, see c.Buf().Err()).
30+ b := c.Buf()
31+ win, err := c.NewXID()
32+ if err != nil {
33+ log.Fatal(err)
34+ }
35+ var v xproto.CreateWindowValues
36+ v.SetEventMask(xproto.EventMaskExposure | xproto.EventMaskStructureNotify)
37+ xproto.CreateWindow(b, 0, xproto.Window(win), xproto.Window(c.Root()),
38+ 0, 0, 640, 480, 0, xproto.WindowClassInputOutput, 0, &v)
39+ xproto.MapWindow(b, xproto.Window(win))
40+ if seq := xproto.GetGeometry(b, xproto.Drawable(win)); seq != 0 {
41+ if err := c.Expect(seq, kindGeometry, win, 0, 0, 0); err != nil {
42+ log.Fatal(err)
43+ }
44+ }
45+
46+ // Callbacks run inside Dispatch. Events, replies and errors are borrowed:
47+ // copy what you keep.
48+ h := &xturbo.Handler{
49+ OnEvent: func(ev *xturbo.Event) {
50+ switch ev.Kind {
51+ case xproto.KindExpose:
52+ e := xproto.ExposeOf(ev)
53+ log.Printf("expose %dx%d", e.Width(), e.Height())
54+ }
55+ },
56+ OnReply: func(r *xturbo.Reply) {
57+ if r.Kind == kindGeometry {
58+ if g, ok := xproto.ParseGetGeometryReply(r.Raw); ok {
59+ log.Printf("window %#x is %dx%d", r.Args[0], g.Width(), g.Height())
60+ }
61+ }
62+ },
63+ OnError: func(e *xturbo.XError) { log.Print(e) },
64+ }
65+
66+ for {
67+ // Flush never blocks. On ErrWouldBlock, also wait for POLLOUT.
68+ events := int16(unix.POLLIN)
69+ if err := c.Flush(); errors.Is(err, xturbo.ErrWouldBlock) {
70+ events |= unix.POLLOUT
71+ } else if err != nil {
72+ log.Fatal(err)
73+ }
74+ fds := []unix.PollFd{{Fd: int32(c.Fd()), Events: events}}
75+ if _, err := unix.Poll(fds, -1); err != nil && err != unix.EINTR {
76+ log.Fatal(err)
77+ }
78+ if err := c.Dispatch(h); err != nil { // io.EOF: server closed
79+ log.Fatal(err)
80+ }
81+ // Keep the XID pool topped up without blocking later.
82+ if c.XIDsLow() {
83+ _ = c.PrefetchXIDs()
84+ }
85+ }
86+}
M hardening_test.go +2 -1
@@ -91,7 +91,8 @@ func TestSyncDeadlineFlood(t *testing.T) {
9191 }
9292 }
9393 }()
94- defer func() { close(stop); <-done }()
94+ // Closing the client unblocks a writer stuck on a full socket buffer.
95+ defer func() { close(stop); _ = c.Close(); <-done }()
9596 within(t, shortDeadline, func() error { return c.Sync(nil) })
9697 }
9798
@@ -91,7 +91,8 @@ func TestSyncDeadlineFlood(t *testing.T) {
91 }91 }
92 }92 }
93 }()93 }()
94- defer func() { close(stop); <-done }()94+ // Closing the client unblocks a writer stuck on a full socket buffer.
95+ defer func() { close(stop); _ = c.Close(); <-done }()
95 within(t, shortDeadline, func() error { return c.Sync(nil) })96 within(t, shortDeadline, func() error { return c.Sync(nil) })
96 }97 }
97 98

Comments0 open / 0

no comments yet.

Keyboard shortcuts

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

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

Branches 1

Tags 1

↑↓ moveenter openesc close