diff options
Diffstat (limited to 'nexgb/xgb.go')
-rw-r--r-- | nexgb/xgb.go | 554 |
1 files changed, 554 insertions, 0 deletions
diff --git a/nexgb/xgb.go b/nexgb/xgb.go new file mode 100644 index 0000000..3d2c61f --- /dev/null +++ b/nexgb/xgb.go @@ -0,0 +1,554 @@ +package xgb + +import ( + "errors" + "io" + "log" + "net" + "os" + "sync" +) + +var ( + // Where to log error-messages. Defaults to stderr. + // To disable logging, just set this to log.New(ioutil.Discard, "", 0) + Logger = log.New(os.Stderr, "XGB: ", log.Lshortfile) +) + +const ( + // cookieBuffer represents the queue size of cookies existing at any + // point in time. The size of the buffer is really only important when + // there are many requests without replies made in sequence. Once the + // buffer fills, a round trip request is made to clear the buffer. + cookieBuffer = 1000 + + // xidBuffer represents the queue size of the xid channel. + // I don't think this value matters much, since xid generation is not + // that expensive. + xidBuffer = 5 + + // seqBuffer represents the queue size of the sequence number channel. + // I don't think this value matters much, since sequence number generation + // is not that expensive. + seqBuffer = 5 + + // reqBuffer represents the queue size of the number of requests that + // can be made until new ones block. This value seems OK. + reqBuffer = 100 + + // eventBuffer represents the queue size of the number of events or errors + // that can be loaded off the wire and not grabbed with WaitForEvent + // until reading an event blocks. This value should be big enough to handle + // bursts of events. + eventBuffer = 5000 +) + +// A Conn represents a connection to an X server. +type Conn struct { + host string + conn net.Conn + display string + DisplayNumber int + DefaultScreen int + SetupBytes []byte + + setupResourceIdBase uint32 + setupResourceIdMask uint32 + + eventChan chan eventOrError + cookieChan chan *Cookie + xidChan chan xid + seqChan chan uint16 + reqChan chan *request + closing chan chan struct{} + + // ExtLock is a lock used whenever new extensions are initialized. + // It should not be used. It is exported for use in the extension + // sub-packages. + ExtLock sync.RWMutex + + // Extensions is a map from extension name to major opcode. It should + // not be used. It is exported for use in the extension sub-packages. + Extensions map[string]byte +} + +// NewConn creates a new connection instance. It initializes locks, data +// structures, and performs the initial handshake. (The code for the handshake +// has been relegated to conn.go.) +func NewConn() (*Conn, error) { + return NewConnDisplay("") +} + +// NewConnDisplay is just like NewConn, but allows a specific DISPLAY +// string to be used. +// If 'display' is empty it will be taken from os.Getenv("DISPLAY"). +// +// Examples: +// NewConn(":1") -> net.Dial("unix", "", "/tmp/.X11-unix/X1") +// NewConn("/tmp/launch-12/:0") -> net.Dial("unix", "", "/tmp/launch-12/:0") +// NewConn("hostname:2.1") -> net.Dial("tcp", "", "hostname:6002") +// NewConn("tcp/hostname:1.0") -> net.Dial("tcp", "", "hostname:6001") +func NewConnDisplay(display string) (*Conn, error) { + conn := &Conn{} + + // First connect. This reads authority, checks DISPLAY environment + // variable, and loads the initial Setup info. + err := conn.connect(display) + if err != nil { + return nil, err + } + + return postNewConn(conn) +} + +// NewConnDisplay is just like NewConn, but allows a specific net.Conn +// to be used. +func NewConnNet(netConn net.Conn) (*Conn, error) { + conn := &Conn{} + + // First connect. This reads authority, checks DISPLAY environment + // variable, and loads the initial Setup info. + err := conn.connectNet(netConn) + + if err != nil { + return nil, err + } + + return postNewConn(conn) +} + +func postNewConn(conn *Conn) (*Conn, error) { + conn.Extensions = make(map[string]byte) + + conn.cookieChan = make(chan *Cookie, cookieBuffer) + conn.xidChan = make(chan xid, xidBuffer) + conn.seqChan = make(chan uint16, seqBuffer) + conn.reqChan = make(chan *request, reqBuffer) + conn.eventChan = make(chan eventOrError, eventBuffer) + conn.closing = make(chan chan struct{}, 1) + + go conn.generateXIds() + go conn.generateSeqIds() + go conn.sendRequests() + go conn.readResponses() + + return conn, nil +} + +// Close gracefully closes the connection to the X server. +func (c *Conn) Close() { + close(c.reqChan) +} + +// Event is an interface that can contain any of the events returned by the +// server. Use a type assertion switch to extract the Event structs. +type Event interface { + Bytes() []byte + String() string +} + +// NewEventFun is the type of function use to construct events from raw bytes. +// It should not be used. It is exported for use in the extension sub-packages. +type NewEventFun func(buf []byte) Event + +// NewEventFuncs is a map from event numbers to functions that create +// the corresponding event. It should not be used. It is exported for use +// in the extension sub-packages. +var NewEventFuncs = make(map[int]NewEventFun) + +// NewExtEventFuncs is a temporary map that stores event constructor functions +// for each extension. When an extension is initialized, each event for that +// extension is added to the 'NewEventFuncs' map. It should not be used. It is +// exported for use in the extension sub-packages. +var NewExtEventFuncs = make(map[string]map[int]NewEventFun) + +// Error is an interface that can contain any of the errors returned by +// the server. Use a type assertion switch to extract the Error structs. +type Error interface { + SequenceId() uint16 + BadId() uint32 + Error() string +} + +// NewErrorFun is the type of function use to construct errors from raw bytes. +// It should not be used. It is exported for use in the extension sub-packages. +type NewErrorFun func(buf []byte) Error + +// NewErrorFuncs is a map from error numbers to functions that create +// the corresponding error. It should not be used. It is exported for use in +// the extension sub-packages. +var NewErrorFuncs = make(map[int]NewErrorFun) + +// NewExtErrorFuncs is a temporary map that stores error constructor functions +// for each extension. When an extension is initialized, each error for that +// extension is added to the 'NewErrorFuncs' map. It should not be used. It is +// exported for use in the extension sub-packages. +var NewExtErrorFuncs = make(map[string]map[int]NewErrorFun) + +// eventOrError corresponds to values that can be either an event or an +// error. +type eventOrError interface{} + +// NewId generates a new unused ID for use with requests like CreateWindow. +// If no new ids can be generated, the id returned is 0 and error is non-nil. +// This shouldn't be used directly, and is exported for use in the extension +// sub-packages. +// If you need identifiers, use the appropriate constructor. +// e.g., For a window id, use xproto.NewWindowId. For +// a new pixmap id, use xproto.NewPixmapId. And so on. +func (c *Conn) NewId() (uint32, error) { + xid := <-c.xidChan + if xid.err != nil { + return 0, xid.err + } + return xid.id, nil +} + +// xid encapsulates a resource identifier being sent over the Conn.xidChan +// channel. If no new resource id can be generated, id is set to 0 and a +// non-nil error is set in xid.err. +type xid struct { + id uint32 + err error +} + +// generateXids sends new Ids down the channel for NewId to use. +// generateXids should be run in its own goroutine. +// This needs to be updated to use the XC Misc extension once we run out of +// new ids. +// Thanks to libxcb/src/xcb_xid.c. This code is greatly inspired by it. +func (conn *Conn) generateXIds() { + defer close(conn.xidChan) + + // This requires some explanation. From the horse's mouth: + // "The resource-id-mask contains a single contiguous set of bits (at least + // 18). The client allocates resource IDs for types WINDOW, PIXMAP, + // CURSOR, FONT, GCONTEXT, and COLORMAP by choosing a value with only some + // subset of these bits set and ORing it with resource-id-base. Only values + // constructed in this way can be used to name newly created resources over + // this connection." + // So for example (using 8 bit integers), the mask might look like: + // 00111000 + // So that valid values would be 00101000, 00110000, 00001000, and so on. + // Thus, the idea is to increment it by the place of the last least + // significant '1'. In this case, that value would be 00001000. To get + // that value, we can AND the original mask with its two's complement: + // 00111000 & 11001000 = 00001000. + // And we use that value to increment the last resource id to get a new one. + // (And then, of course, we OR it with resource-id-base.) + inc := conn.setupResourceIdMask & -conn.setupResourceIdMask + max := conn.setupResourceIdMask + last := uint32(0) + for { + // TODO: Use the XC Misc extension to look for released ids. + if last > 0 && last >= max-inc+1 { + conn.xidChan <- xid{ + id: 0, + err: errors.New("There are no more available resource" + + "identifiers."), + } + } + + last += inc + conn.xidChan <- xid{ + id: last | conn.setupResourceIdBase, + err: nil, + } + } +} + +// newSeqId fetches the next sequence id from the Conn.seqChan channel. +func (c *Conn) newSequenceId() uint16 { + return <-c.seqChan +} + +// generateSeqIds returns new sequence ids. It is meant to be run in its +// own goroutine. +// A sequence id is generated for *every* request. It's the identifier used +// to match up replies with requests. +// Since sequence ids can only be 16 bit integers we start over at zero when it +// comes time to wrap. +// N.B. As long as the cookie buffer is less than 2^16, there are no limitations +// on the number (or kind) of requests made in sequence. +func (c *Conn) generateSeqIds() { + defer close(c.seqChan) + + seqid := uint16(1) + for { + c.seqChan <- seqid + if seqid == uint16((1<<16)-1) { + seqid = 0 + } else { + seqid++ + } + } +} + +// request encapsulates a buffer of raw bytes (containing the request data) +// and a cookie, which when combined represents a single request. +// The cookie is used to match up the reply/error. +type request struct { + buf []byte + cookie *Cookie + + // seq is closed when the request (cookie) has been sequenced by the Conn. + seq chan struct{} +} + +// NewRequest takes the bytes and a cookie of a particular request, constructs +// a request type, and sends it over the Conn.reqChan channel. +// Note that the sequence number is added to the cookie after it is sent +// over the request channel, but before it is sent to X. +// +// Note that you may safely use NewRequest to send arbitrary byte requests +// to X. The resulting cookie can be used just like any normal cookie and +// abides by the same rules, except that for replies, you'll get back the +// raw byte data. This may be useful for performance critical sections where +// every allocation counts, since all X requests in XGB allocate a new byte +// slice. In contrast, NewRequest allocates one small request struct and +// nothing else. (Except when the cookie buffer is full and has to be flushed.) +// +// If you're using NewRequest manually, you'll need to use NewCookie to create +// a new cookie. +// +// In all likelihood, you should be able to copy and paste with some minor +// edits the generated code for the request you want to issue. +func (c *Conn) NewRequest(buf []byte, cookie *Cookie) { + seq := make(chan struct{}) + c.reqChan <- &request{buf: buf, cookie: cookie, seq: seq} + <-seq +} + +// sendRequests is run as a single goroutine that takes requests and writes +// the bytes to the wire and adds the cookie to the cookie queue. +// It is meant to be run as its own goroutine. +func (c *Conn) sendRequests() { + defer close(c.cookieChan) + + for req := range c.reqChan { + // ho there! if the cookie channel is nearly full, force a round + // trip to clear out the cookie buffer. + // Note that we circumvent the request channel, because we're *in* + // the request channel. + if len(c.cookieChan) == cookieBuffer-1 { + if err := c.noop(); err != nil { + // Shut everything down. + break + } + } + req.cookie.Sequence = c.newSequenceId() + c.cookieChan <- req.cookie + c.writeBuffer(req.buf) + close(req.seq) + } + response := make(chan struct{}) + c.closing <- response + c.noop() // Flush the response reading goroutine, ignore error. + <-response + c.conn.Close() +} + +// noop circumvents the usual request sending goroutines and forces a round +// trip request manually. +func (c *Conn) noop() error { + cookie := c.NewCookie(true, true) + cookie.Sequence = c.newSequenceId() + c.cookieChan <- cookie + if err := c.writeBuffer(c.getInputFocusRequest()); err != nil { + return err + } + cookie.Reply() // wait for the buffer to clear + return nil +} + +// writeBuffer is a convenience function for writing a byte slice to the wire. +func (c *Conn) writeBuffer(buf []byte) error { + if _, err := c.conn.Write(buf); err != nil { + Logger.Printf("A write error is unrecoverable: %s", err) + return err + } else { + return nil + } +} + +// readResponses is a goroutine that reads events, errors and +// replies off the wire. +// When an event is read, it is always added to the event channel. +// When an error is read, if it corresponds to an existing checked cookie, +// it is sent to that cookie's error channel. Otherwise it is added to the +// event channel. +// When a reply is read, it is added to the corresponding cookie's reply +// channel. (It is an error if no such cookie exists in this case.) +// Finally, cookies that came "before" this reply are always cleaned up. +func (c *Conn) readResponses() { + defer close(c.eventChan) + + var ( + err Error + seq uint16 + replyBytes []byte + ) + + for { + select { + case respond := <-c.closing: + respond <- struct{}{} + return + default: + } + + buf := make([]byte, 32) + err, seq = nil, 0 + if _, err := io.ReadFull(c.conn, buf); err != nil { + Logger.Printf("A read error is unrecoverable: %s", err) + c.eventChan <- err + c.Close() + continue + } + switch buf[0] { + case 0: // This is an error + // Use the constructor function for this error (that is auto + // generated) by looking it up by the error number. + newErrFun, ok := NewErrorFuncs[int(buf[1])] + if !ok { + Logger.Printf("BUG: Could not find error constructor function "+ + "for error with number %d.", buf[1]) + continue + } + err = newErrFun(buf) + seq = err.SequenceId() + + // This error is either sent to the event channel or a specific + // cookie's error channel below. + case 1: // This is a reply + seq = Get16(buf[2:]) + + // check to see if this reply has more bytes to be read + size := Get32(buf[4:]) + if size > 0 { + byteCount := 32 + size*4 + biggerBuf := make([]byte, byteCount) + copy(biggerBuf[:32], buf) + if _, err := io.ReadFull(c.conn, biggerBuf[32:]); err != nil { + Logger.Printf("A read error is unrecoverable: %s", err) + c.eventChan <- err + c.Close() + continue + } + replyBytes = biggerBuf + } else { + replyBytes = buf + } + + // This reply is sent to its corresponding cookie below. + default: // This is an event + // Use the constructor function for this event (like for errors, + // and is also auto generated) by looking it up by the event number. + // Note that we AND the event number with 127 so that we ignore + // the most significant bit (which is set when it was sent from + // a SendEvent request). + evNum := int(buf[0] & 127) + newEventFun, ok := NewEventFuncs[evNum] + if !ok { + Logger.Printf("BUG: Could not find event construct function "+ + "for event with number %d.", evNum) + continue + } + c.eventChan <- newEventFun(buf) + continue + } + + // At this point, we have a sequence number and we're either + // processing an error or a reply, which are both responses to + // requests. So all we have to do is find the cookie corresponding + // to this error/reply, and send the appropriate data to it. + // In doing so, we make sure that any cookies that came before it + // are marked as successful if they are void and checked. + // If there's a cookie that requires a reply that is before this + // reply, then something is wrong. + for cookie := range c.cookieChan { + // This is the cookie we're looking for. Process and break. + if cookie.Sequence == seq { + if err != nil { // this is an error to a request + // synchronous processing + if cookie.errorChan != nil { + cookie.errorChan <- err + } else { // asynchronous processing + c.eventChan <- err + // if this is an unchecked reply, ping the cookie too + if cookie.pingChan != nil { + cookie.pingChan <- true + } + } + } else { // this is a reply + if cookie.replyChan == nil { + Logger.Printf("Reply with sequence id %d does not "+ + "have a cookie with a valid reply channel.", seq) + continue + } else { + cookie.replyChan <- replyBytes + } + } + break + } + + switch { + // Checked requests with replies + case cookie.replyChan != nil && cookie.errorChan != nil: + Logger.Printf("Found cookie with sequence id %d that is "+ + "expecting a reply but will never get it. Currently "+ + "on sequence number %d", cookie.Sequence, seq) + // Unchecked requests with replies + case cookie.replyChan != nil && cookie.pingChan != nil: + Logger.Printf("Found cookie with sequence id %d that is "+ + "expecting a reply (and not an error) but will never "+ + "get it. Currently on sequence number %d", + cookie.Sequence, seq) + // Checked requests without replies + case cookie.pingChan != nil && cookie.errorChan != nil: + cookie.pingChan <- true + // Unchecked requests without replies don't have any channels, + // so we can't do anything with them except let them pass by. + } + } + } +} + +// processEventOrError takes an eventOrError, type switches on it, +// and returns it in Go idiomatic style. +func processEventOrError(everr eventOrError) (Event, Error) { + switch ee := everr.(type) { + case Event: + return ee, nil + case Error: + return nil, ee + default: + Logger.Printf("Invalid event/error type: %T", everr) + return nil, nil + } +} + +// WaitForEvent returns the next event from the server. +// It will block until an event is available. +// WaitForEvent returns either an Event or an Error. (Returning both +// is a bug.) Note than an Error here is an X error and not an XGB error. That +// is, X errors are sometimes completely expected (and you may want to ignore +// them in some cases). +// +// If both the event and error are nil, then the connection has been closed. +func (c *Conn) WaitForEvent() (Event, Error) { + return processEventOrError(<-c.eventChan) +} + +// PollForEvent returns the next event from the server if one is available in +// the internal queue without blocking. Note that unlike WaitForEvent, both +// Event and Error could be nil. Indeed, they are both nil when the event queue +// is empty. +func (c *Conn) PollForEvent() (Event, Error) { + select { + case everr := <-c.eventChan: + return processEventOrError(everr) + default: + return nil, nil + } +} |