1package xturbo
2
3import (
4 "fmt"
5 "os"
6 "time"
7
8 "golang.org/x/sys/unix"
9)
10
11// Conn is a client connection to an X server over a stream socket. It is not
12// safe for concurrent use: one goroutine owns it.
13type Conn struct {
14 fd int
15 buf *Buf
16 ext ExtTable
17 setup []byte
18 root uint32
19
20 xidBase, xidMask uint32 // resource-id allocation, used by NewXID
21 xids, xidPend xidRange // current range; prefetched range (left 0: none)
22 xidFree [xidFreeCap]uint32
23 xidFreeN int
24 xidReq Seq // last GetXIDRange sent
25 xidNone bool // the last GetXIDRange gave no usable range
26 xidFailed bool // ... or an X error, xidErr
27 xidErr XError
28
29 // Blocking calls (Sync, InternAtoms, NewXID refill).
30 h *Handler // last non-nil Handler given to Dispatch or Sync
31 rdIdle bool // the last Dispatch read nothing: the socket was empty
32 atoms []uint32 // InternAtoms destination, indexed by slot Args[0]
33 syncErr XError // first error on a reserved-kind request
34 syncFailed bool // syncErr is set
35 resync Seq // last GetInputFocus appended by Flush
36 timeout time.Duration // bound of one Sync, InternAtoms or XID refill
37
38 // Read side, owned by Dispatch: rb[rpos:rend] is received, unconsumed.
39 rb []byte
40 rpos, rend int
41 maxPacket int // MaxPacket; lowered by tests
42 rerr error // sticky read failure
43 dispatching bool // inside Dispatch: rejects nested calls
44 seq Seq // last widened sequence number seen
45
46 // Reply ring: Expect registrations indexed by seq&ringMask.
47 ring [ringSize]slot
48 live int // registered slots
49 stats Stats
50
51 // Callback arguments, reused per packet so nothing escapes.
52 ev Event
53 reply Reply
54 xerr XError
55}
56
57// NewConn takes ownership of fd, a connected stream socket to an X server,
58// and runs the connection setup without authorization (e.g. the -wm fd of an
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.
65func NewConn(fd int) (*Conn, error) {
66 return newConn(fd, "", nil)
67}
68
69func newConn(fd int, authName string, authData []byte) (*Conn, error) {
70 c, err := setupConn(fd, authName, authData)
71 if err != nil {
72 _ = unix.Close(fd)
73 return nil, err
74 }
75 return c, nil
76}
77
78func setupConn(fd int, authName string, authData []byte) (*Conn, error) {
79 deadline := time.Now().Add(connectTimeout)
80 if err := unix.SetNonblock(fd, true); err != nil {
81 return nil, os.NewSyscallError("fcntl", err)
82 }
83 setup, info, err := handshake(fd, authName, authData, deadline)
84 if err != nil {
85 return nil, err
86 }
87 c := newConnState(fd, setup, info)
88 if err := c.bootstrap(deadline); err != nil {
89 return nil, fmt.Errorf("xturbo: extension bootstrap: %w", err)
90 }
91 return c, nil
92}
93
94// newConnState builds a Conn over an fd whose setup already completed.
95func newConnState(fd int, setup []byte, info setupInfo) *Conn {
96 c := &Conn{
97 fd: fd,
98 buf: NewBuf(0),
99 setup: setup,
100 root: info.root,
101 xidBase: info.xidBase,
102 xidMask: info.xidMask,
103 rb: make([]byte, readBufSize),
104 maxPacket: MaxPacket,
105 timeout: syncTimeout,
106 }
107 c.buf.Ext = &c.ext
108 c.initXIDs()
109 return c
110}
111
112// Close closes the socket. Later calls, and Flush after Close, return
113// ErrClosed.
114func (c *Conn) Close() error {
115 if c.fd < 0 {
116 return ErrClosed
117 }
118 fd := c.fd
119 c.fd = -1
120 c.buf.Fail(ErrClosed)
121 if err := unix.Close(fd); err != nil {
122 return os.NewSyscallError("close", err)
123 }
124 return nil
125}
126
127// Fd returns the socket, for the caller's poll loop; -1 after Close.
128func (c *Conn) Fd() int { return c.fd }
129
130// Buf returns the request encoder bound to this connection.
131func (c *Conn) Buf() *Buf { return c.buf }
132
133// Ext returns the connection's extension table.
134func (c *Conn) Ext() *ExtTable { return &c.ext }
135
136// SetupBytes returns the full setup reply. It is retained for the life of the
137// Conn; parse it with the xproto Setup view. Do not modify it.
138func (c *Conn) SetupBytes() []byte { return c.setup }
139
140// Root returns the root window of the first screen.
141func (c *Conn) Root() uint32 { return c.root }
142
143// resyncAt is the number of unacknowledged requests after which Flush appends
144// a GetInputFocus, so a long stream of requests without replies or events
145// still brings back a sequence number before the 16-bit wire value wraps.
146const resyncAt = 0x8000
147
148// Flush writes queued requests without blocking. It returns ErrWouldBlock when
149// the socket is full (arm writable interest and call again) and the sticky
150// Buf error once the connection is unusable. With resyncAt or more requests
151// unacknowledged it first appends a GetInputFocus whose reply is discarded
152// (Stats.Resyncs), at most one in flight.
153func (c *Conn) Flush() error {
154 if c.fd < 0 {
155 return ErrClosed
156 }
157 b := c.buf
158 if b.err == nil && b.next-b.acked >= resyncAt && c.resync <= b.acked {
159 if seq := c.fence(); seq != 0 {
160 c.resync = seq
161 c.stats.Resyncs++
162 }
163 }
164 return b.flush(c.fd)
165}
166
167// fence queues a GetInputFocus whose reply is consumed and dropped. Its
168// reply proves every earlier request complete.
169func (c *Conn) fence() Seq {
170 seq := encodeGetInputFocus(c.buf)
171 // On ErrRingFull the reply is merely counted as dropped.
172 _ = c.expect(seq, kindDiscard, [4]uint32{})
173 return seq
174}