package xturbo import ( "fmt" "os" "time" "golang.org/x/sys/unix" ) // Conn is a client connection to an X server over a stream socket. It is not // safe for concurrent use: one goroutine owns it. type Conn struct { fd int buf *Buf ext ExtTable setup []byte root uint32 xidBase, xidMask uint32 // resource-id allocation, used by NewXID xids, xidPend xidRange // current range; prefetched range (left 0: none) xidFree [xidFreeCap]uint32 xidFreeN int xidReq Seq // last GetXIDRange sent xidNone bool // the last GetXIDRange gave no usable range xidFailed bool // ... or an X error, xidErr xidErr XError // Blocking calls (Sync, InternAtoms, NewXID refill). h *Handler // last non-nil Handler given to Dispatch or Sync rdIdle bool // the last Dispatch read nothing: the socket was empty atoms []uint32 // InternAtoms destination, indexed by slot Args[0] syncErr XError // first error on a reserved-kind request syncFailed bool // syncErr is set resync Seq // last GetInputFocus appended by Flush timeout time.Duration // bound of one Sync, InternAtoms or XID refill // Read side, owned by Dispatch: rb[rpos:rend] is received, unconsumed. rb []byte rpos, rend int maxPacket int // MaxPacket; lowered by tests rerr error // sticky read failure dispatching bool // inside Dispatch: rejects nested calls seq Seq // last widened sequence number seen // Reply ring: Expect registrations indexed by seq&ringMask. ring [ringSize]slot live int // registered slots stats Stats // Callback arguments, reused per packet so nothing escapes. ev Event reply Reply xerr XError } // NewConn takes ownership of fd, a connected stream socket to an X server, // and runs the connection setup without authorization (e.g. the -wm fd of an // XWayland the caller spawned). It blocks for the handshake, then one round // trip negotiates every known extension (QueryExtension each, BIG-REQUESTS // Enable when present) into Ext() and the Buf request limit. fd is left // O_NONBLOCK. The whole call gives up after 10 s with an error wrapping // os.ErrDeadlineExceeded (an alive but silent server). On failure fd is // closed. func NewConn(fd int) (*Conn, error) { return newConn(fd, "", nil) } func newConn(fd int, authName string, authData []byte) (*Conn, error) { c, err := setupConn(fd, authName, authData) if err != nil { _ = unix.Close(fd) return nil, err } return c, nil } func setupConn(fd int, authName string, authData []byte) (*Conn, error) { deadline := time.Now().Add(connectTimeout) if err := unix.SetNonblock(fd, true); err != nil { return nil, os.NewSyscallError("fcntl", err) } setup, info, err := handshake(fd, authName, authData, deadline) if err != nil { return nil, err } c := newConnState(fd, setup, info) if err := c.bootstrap(deadline); err != nil { return nil, fmt.Errorf("xturbo: extension bootstrap: %w", err) } return c, nil } // newConnState builds a Conn over an fd whose setup already completed. func newConnState(fd int, setup []byte, info setupInfo) *Conn { c := &Conn{ fd: fd, buf: NewBuf(0), setup: setup, root: info.root, xidBase: info.xidBase, xidMask: info.xidMask, rb: make([]byte, readBufSize), maxPacket: MaxPacket, timeout: syncTimeout, } c.buf.Ext = &c.ext c.initXIDs() return c } // Close closes the socket. Later calls, and Flush after Close, return // ErrClosed. func (c *Conn) Close() error { if c.fd < 0 { return ErrClosed } fd := c.fd c.fd = -1 c.buf.Fail(ErrClosed) if err := unix.Close(fd); err != nil { return os.NewSyscallError("close", err) } return nil } // Fd returns the socket, for the caller's poll loop; -1 after Close. func (c *Conn) Fd() int { return c.fd } // Buf returns the request encoder bound to this connection. func (c *Conn) Buf() *Buf { return c.buf } // Ext returns the connection's extension table. func (c *Conn) Ext() *ExtTable { return &c.ext } // SetupBytes returns the full setup reply. It is retained for the life of the // Conn; parse it with the xproto Setup view. Do not modify it. func (c *Conn) SetupBytes() []byte { return c.setup } // Root returns the root window of the first screen. func (c *Conn) Root() uint32 { return c.root } // resyncAt is the number of unacknowledged requests after which Flush appends // a GetInputFocus, so a long stream of requests without replies or events // still brings back a sequence number before the 16-bit wire value wraps. const resyncAt = 0x8000 // Flush writes queued requests without blocking. It returns ErrWouldBlock when // the socket is full (arm writable interest and call again) and the sticky // Buf error once the connection is unusable. With resyncAt or more requests // unacknowledged it first appends a GetInputFocus whose reply is discarded // (Stats.Resyncs), at most one in flight. func (c *Conn) Flush() error { if c.fd < 0 { return ErrClosed } b := c.buf if b.err == nil && b.next-b.acked >= resyncAt && c.resync <= b.acked { if seq := c.fence(); seq != 0 { c.resync = seq c.stats.Resyncs++ } } return b.flush(c.fd) } // fence queues a GetInputFocus whose reply is consumed and dropped. Its // reply proves every earlier request complete. func (c *Conn) fence() Seq { seq := encodeGetInputFocus(c.buf) // On ErrRingFull the reply is merely counted as dropped. _ = c.expect(seq, kindDiscard, [4]uint32{}) return seq }