555 lines
18 KiB
Go
555 lines
18 KiB
Go
|
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
|
||
|
}
|
||
|
}
|