package xturbo import ( "encoding/binary" "fmt" "os" "time" "golang.org/x/sys/unix" ) // Core and extension requests root issues itself. Root does not import the // generated packages, so these few are encoded here; tests compare them with // the generated encoders and the request goldens. const ( opInternAtom = 16 opGetInputFocus = 43 opQueryExtension = 98 minorGetXIDRange = 1 // XC-MISC ) func encodeGetInputFocus(b *Buf) Seq { _, seq := b.Request(opGetInputFocus, 0, 0) return seq } // encodeNamed encodes QueryExtension and InternAtom: CARD16 name_len, 2 // pad bytes, then the name padded to 4. func encodeNamed(b *Buf, major, data uint8, name string) Seq { if len(name) > 0xffff { b.Fail(ErrBadLength) return 0 } p, seq := b.Request(major, data, pad4(4+len(name))) if p == nil { return 0 } binary.LittleEndian.PutUint16(p, uint16(len(name))) copy(p[4:], name) return seq } func encodeQueryExtension(b *Buf, name string) Seq { return encodeNamed(b, opQueryExtension, 0, name) } func encodeInternAtom(b *Buf, onlyIfExists bool, name string) Seq { var data uint8 if onlyIfExists { data = 1 } return encodeNamed(b, opInternAtom, data, name) } // encodeExtRequest encodes a body-less extension request (BIG-REQUESTS // Enable, XC-MISC GetXIDRange). It fails with ErrExtMissing when id is absent. func encodeExtRequest(b *Buf, id ExtID, minor uint8) Seq { major := b.Ext.Major(id) if major == 0 { b.Fail(ErrExtMissing) return 0 } _, seq := b.Request(major, minor, 0) return seq } // Bounds of the blocking calls. Vars so tests can shorten them. var ( // connectTimeout bounds NewConn: setup handshake plus extension bootstrap. connectTimeout = 10 * time.Second // syncTimeout bounds one Sync, InternAtoms or blocking XID refill. syncTimeout = 5 * time.Second ) // deadlineError wraps os.ErrDeadlineExceeded for a blocking call that timed // out waiting for the server. func deadlineError(what string) error { return fmt.Errorf("xturbo: %s: no answer from the X server: %w", what, os.ErrDeadlineExceeded) } // bootstrap negotiates every extension root knows (one pipelined // QueryExtension each, then BIG-REQUESTS Enable when present) in one Sync, // before deadline. func (c *Conn) bootstrap(deadline time.Time) error { for id := ExtCore + 1; id < extCount; id++ { seq := encodeQueryExtension(c.buf, ExtXName[id]) if seq == 0 { return c.buf.Err() } if err := c.expect(seq, kindQueryExtension, [4]uint32{uint32(id)}); err != nil { return err } } // consumeReply queues bigreq Enable when BIG-REQUESTS is present; the // wait covers it too. return c.sync(nil, deadline) } // Sync flushes every queued request and blocks until the server has // processed them and every request registered with Expect has completed. It // appends a GetInputFocus as a fence, so even with nothing registered it is a // full round trip: errors of earlier requests have been delivered when it // returns. Packets read meanwhile go to h as with Dispatch (nil h: the last // Handler given to Dispatch, or none). An error on a request root issued // itself is returned as *XError. Sync gives up after 5 s with an error // wrapping os.ErrDeadlineExceeded (a hung server); the connection is then // still usable but its state unknown, treat it as dead. Meant for startup; // called from a Handler callback it returns an error at once. func (c *Conn) Sync(h *Handler) error { return c.sync(h, time.Now().Add(c.timeout)) } func (c *Conn) sync(h *Handler, deadline time.Time) error { if c.dispatching { return errReentrantDispatch } if h == nil { h = c.h } c.syncFailed = false if err := c.waitUntil(h, deadline); err != nil { return err } if c.syncFailed { e := c.syncErr return &e } return nil } // waitUntil fences, flushes and dispatches, one bounded Dispatch at a time, // until the fence has been answered and no registered slot remains, or // deadline passes. A fence is re-queued whenever callbacks registered // requests after the last one. func (c *Conn) waitUntil(h *Handler, deadline time.Time) error { if c.fd < 0 { return ErrClosed } var fence Seq for { if fence != 0 && c.seq >= fence && c.live == 0 { return nil } if fence == 0 || fence < c.buf.next-1 { // Requests queued after the fence (or none yet): fence them. if fence = c.fence(); fence == 0 { return c.buf.Err() } } if err := c.pollOnce(h, deadline); err != nil { return err } } } // pollOnce makes one step of progress: flush, dispatch what is readable and, // only when Dispatch read nothing at all, block until the socket is readable // (or writable while requests remain queued) or deadline passes. Dispatch is // bounded per call, so it may return with input left; the caller loops and // re-checks. A deadline already past fails even while packets keep coming // (a server flooding events but never answering). func (c *Conn) pollOnce(h *Handler, deadline time.Time) error { left := time.Until(deadline) if left <= 0 { return deadlineError("blocking round trip") } events := int16(unix.POLLIN) switch err := c.Flush(); err { case nil: case ErrWouldBlock: events |= unix.POLLOUT default: return err } if err := c.Dispatch(h); err != nil { return err } // Packets handled may have queued requests (callbacks, the bootstrap): // loop to flush them, and to fence them, before blocking. if !c.rdIdle || (events&unix.POLLOUT == 0 && c.buf.Len() > 0) { return nil } return pollFd(c.fd, events, left) } // pollFd blocks until fd has one of events or timeout passes (returning nil; // callers re-check their deadline). EINTR is a wake-up. func pollFd(fd int, events int16, timeout time.Duration) error { ms := int((timeout + time.Millisecond - 1) / time.Millisecond) fds := []unix.PollFd{{Fd: int32(fd), Events: events}} if _, err := unix.Poll(fds, ms); err != nil && err != unix.EINTR { return os.NewSyscallError("poll", err) } return nil }