From a5d4ad6c9d763b3d3f797075038023756c38bb28 Mon Sep 17 00:00:00 2001 From: "Andrew Gallant (Ocelot)" Date: Thu, 3 May 2012 22:47:50 -0400 Subject: [PATCH] reworking xgb. cleaned up connection stuff a little. making new xid generation cleaner and use goroutines for it. --- nexgb/auth.go | 6 +- nexgb/conn.go | 161 +++++++++++++++++++++++ nexgb/examples/atom.go | 27 ++++ nexgb/examples/property.go | 39 ++++++ nexgb/xgb.go | 257 ++++++++++++------------------------- 5 files changed, 316 insertions(+), 174 deletions(-) create mode 100644 nexgb/conn.go create mode 100644 nexgb/examples/atom.go create mode 100644 nexgb/examples/property.go diff --git a/nexgb/auth.go b/nexgb/auth.go index 355afeb..ac43e07 100644 --- a/nexgb/auth.go +++ b/nexgb/auth.go @@ -44,8 +44,10 @@ func getString(r io.Reader, b []byte) (string, error) { // readAuthority reads the X authority file for the DISPLAY. // If hostname == "" or hostname == "localhost", -// readAuthority uses the system's hostname (as returned by os.Hostname) instead. -func readAuthority(hostname, display string) (name string, data []byte, err error) { +// then use the system's hostname (as returned by os.Hostname) instead. +func readAuthority(hostname, display string) ( + name string, data []byte, err error) { + // b is a scratch buffer to use and should be at least 256 bytes long // (i.e. it should be able to hold a hostname). var b [256]byte diff --git a/nexgb/conn.go b/nexgb/conn.go new file mode 100644 index 0000000..235402d --- /dev/null +++ b/nexgb/conn.go @@ -0,0 +1,161 @@ +package xgb + +import ( + "errors" + "fmt" + "io" + "net" + "os" + "strconv" + "strings" +) + +// connect connects to the X server given in the 'display' string. +// If 'display' is empty it will be taken from os.Getenv("DISPLAY"). +// Note that you should read and understand the "Connection Setup" of the +// X Protocol Reference Manual before changing this function: +// http://goo.gl/4zGQg +func (c *Conn) connect(display string) error { + err := c.dial(display) + if err != nil { + return err + } + + // Get authentication data + authName, authData, err := readAuthority(c.host, c.display) + noauth := false + if err != nil { + fmt.Fprintf(os.Stderr, "Could not get authority info: %v\n", err) + fmt.Fprintf(os.Stderr, "Trying connection without authority info...\n") + authName = "" + authData = []byte{} + noauth = true + } + + // Assume that the authentication protocol is "MIT-MAGIC-COOKIE-1". + if !noauth && (authName != "MIT-MAGIC-COOKIE-1" || len(authData) != 16) { + return errors.New("unsupported auth protocol " + authName) + } + + buf := make([]byte, 12+pad(len(authName))+pad(len(authData))) + buf[0] = 0x6c + buf[1] = 0 + Put16(buf[2:], 11) + Put16(buf[4:], 0) + Put16(buf[6:], uint16(len(authName))) + Put16(buf[8:], uint16(len(authData))) + Put16(buf[10:], 0) + copy(buf[12:], []byte(authName)) + copy(buf[12+pad(len(authName)):], authData) + if _, err = c.conn.Write(buf); err != nil { + return err + } + + head := make([]byte, 8) + if _, err = io.ReadFull(c.conn, head[0:8]); err != nil { + return err + } + code := head[0] + reasonLen := head[1] + major := Get16(head[2:]) + minor := Get16(head[4:]) + dataLen := Get16(head[6:]) + + if major != 11 || minor != 0 { + return errors.New(fmt.Sprintf("x protocol version mismatch: %d.%d", + major, minor)) + } + + buf = make([]byte, int(dataLen)*4+8, int(dataLen)*4+8) + copy(buf, head) + if _, err = io.ReadFull(c.conn, buf[8:]); err != nil { + return err + } + + if code == 0 { + reason := buf[8 : 8+reasonLen] + return errors.New(fmt.Sprintf("x protocol authentication refused: %s", + string(reason))) + } + + ReadSetupInfo(buf, &c.Setup) + + if c.defaultScreen >= len(c.Setup.Roots) { + c.defaultScreen = 0 + } + + return nil +} + +func (c *Conn) dial(display string) error { + if len(display) == 0 { + display = os.Getenv("DISPLAY") + } + + display0 := display + if len(display) == 0 { + return errors.New("empty display string") + } + + colonIdx := strings.LastIndex(display, ":") + if colonIdx < 0 { + return errors.New("bad display string: " + display0) + } + + var protocol, socket string + + if display[0] == '/' { + socket = display[0:colonIdx] + } else { + slashIdx := strings.LastIndex(display, "/") + if slashIdx >= 0 { + protocol = display[0:slashIdx] + c.host = display[slashIdx+1 : colonIdx] + } else { + c.host = display[0:colonIdx] + } + } + + display = display[colonIdx+1 : len(display)] + if len(display) == 0 { + return errors.New("bad display string: " + display0) + } + + var scr string + dotIdx := strings.LastIndex(display, ".") + if dotIdx < 0 { + c.display = display[0:] + } else { + c.display = display[0:dotIdx] + scr = display[dotIdx+1:] + } + + dispnum, err := strconv.Atoi(c.display) + if err != nil || dispnum < 0 { + return errors.New("bad display string: " + display0) + } + + if len(scr) != 0 { + c.defaultScreen, err = strconv.Atoi(scr) + if err != nil { + return errors.New("bad display string: " + display0) + } + } + + // Connect to server + if len(socket) != 0 { + c.conn, err = net.Dial("unix", socket+":"+c.display) + } else if len(c.host) != 0 { + if protocol == "" { + protocol = "tcp" + } + c.conn, err = net.Dial(protocol, c.host+":"+strconv.Itoa(6000+dispnum)) + } else { + c.conn, err = net.Dial("unix", "/tmp/.X11-unix/X"+c.display) + } + + if err != nil { + return errors.New("cannot connect to " + display0 + ": " + err.Error()) + } + return nil +} diff --git a/nexgb/examples/atom.go b/nexgb/examples/atom.go new file mode 100644 index 0000000..c64acee --- /dev/null +++ b/nexgb/examples/atom.go @@ -0,0 +1,27 @@ +package main + +import ( + // "fmt" + "log" + + "github.com/BurntSushi/xgb" +) + +func init() { + log.SetFlags(0) +} + +func main() { + X, err := xgb.NewConn() + if err != nil { + log.Fatal(err) + } + + aname := "_NET_ACTIVE_WINDOW" + atom, err := X.InternAtom(true, uint16(len(aname)), aname) + if err != nil { + log.Fatal(err) + } + log.Printf("%d", atom.Atom) +} + diff --git a/nexgb/examples/property.go b/nexgb/examples/property.go new file mode 100644 index 0000000..2477df4 --- /dev/null +++ b/nexgb/examples/property.go @@ -0,0 +1,39 @@ +package main + +import ( + "log" + + "github.com/BurntSushi/xgb" +) + +func init() { + log.SetFlags(0) +} + +func get32(buf []byte) uint32 { + v := uint32(buf[0]) + v |= uint32(buf[1]) << 8 + v |= uint32(buf[2]) << 16 + v |= uint32(buf[3]) << 24 + return v +} + +func main() { + X, err := xgb.NewConn() + if err != nil { + log.Fatal(err) + } + + root := X.DefaultScreen().Root + + aname := "_NET_ACTIVE_WINDOW" + atom, err := X.InternAtom(true, uint16(len(aname)), aname) + if err != nil { + log.Fatal(err) + } + + reply, err := X.GetProperty(false, root, atom.Atom, xgb.GetPropertyTypeAny, + 0, (1<<32)-1) + log.Printf("%X", get32(reply.Value)) +} + diff --git a/nexgb/xgb.go b/nexgb/xgb.go index 0dd8163..1a4ada1 100644 --- a/nexgb/xgb.go +++ b/nexgb/xgb.go @@ -12,7 +12,6 @@ import ( "io" "net" "os" - "strconv" "strings" "sync" ) @@ -23,11 +22,9 @@ const ( ) // A Conn represents a connection to an X server. -// Only one goroutine should use a Conn's methods at a time. type Conn struct { host string conn net.Conn - nextId Id nextCookie uint16 cookies map[uint16]*Cookie events queue @@ -44,6 +41,7 @@ type Conn struct { eventChan chan bool errorChan chan bool + xidChan chan xid newIdLock sync.Mutex writeLock sync.Mutex dequeueLock sync.Mutex @@ -51,6 +49,51 @@ type Conn struct { extLock sync.Mutex } +// 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-123/:0") -> net.Dial("unix", "", "/tmp/launch-123/: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 + } + + conn.xidChan = make(chan xid, 5) + go conn.generateXids() + + conn.nextCookie = 1 + conn.cookies = make(map[uint16]*Cookie) + conn.events = queue{make([][]byte, 100), 0, 0} + conn.extensions = make(map[string]byte) + + conn.newReadChannels() + conn.newRequestChannels() + + return conn, nil +} + +// Close closes the connection to the X server. +func (c *Conn) Close() { + c.conn.Close() +} + // Id is used for all X identifiers, such as windows, pixmaps, and GCs. type Id uint32 @@ -111,14 +154,46 @@ type Error interface { var newErrorFuncs = map[int]func(buf []byte) Error{} // NewID generates a new unused ID for use with requests like CreateWindow. -func (c *Conn) NewId() Id { - c.newIdLock.Lock() - defer c.newIdLock.Unlock() +// If no new ids can be generated, the id returned is 0 and error is non-nil. +func (c *Conn) NewId() (Id, error) { + xid := <-c.xidChan + if xid.err != nil { + return 0, xid.err + } + return xid.id, nil +} - id := c.nextId - // TODO: handle ID overflow - c.nextId++ - return id +// xid encapsulates a resource identifier being sent over the Conn.xidChan +// channel. If no new resource id can be generated, id is set to -1 and a +// non-nil error is set in xid.err. +type xid struct { + id Id + err error +} + +// generateXids sends new Ids down the channel for NewId to use. +// This needs to be updated to use the XC Misc extension once we run out of +// new ids. +func (conn *Conn) generateXids() { + inc := conn.Setup.ResourceIdMask & -conn.Setup.ResourceIdMask + max := conn.Setup.ResourceIdMask + 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: Id(0), + err: errors.New("There are no more available resource" + + "identifiers."), + } + } + + last += inc + conn.xidChan <- xid{ + id: Id(last | conn.Setup.ResourceIdBase), + err: nil, + } + } } // RegisterExtension adds the respective extension's major op code to @@ -328,165 +403,3 @@ func (c *Conn) PollForEvent() (Event, error) { return nil, nil } -// Dial connects to the X server given in the 'display' string. -// If 'display' is empty it will be taken from os.Getenv("DISPLAY"). -// -// Examples: -// Dial(":1") // connect to net.Dial("unix", "", "/tmp/.X11-unix/X1") -// Dial("/tmp/launch-123/:0") // connect to net.Dial("unix", "", "/tmp/launch-123/:0") -// Dial("hostname:2.1") // connect to net.Dial("tcp", "", "hostname:6002") -// Dial("tcp/hostname:1.0") // connect to net.Dial("tcp", "", "hostname:6001") -func Dial(display string) (*Conn, error) { - c, err := connect(display) - if err != nil { - return nil, err - } - - // Get authentication data - authName, authData, err := readAuthority(c.host, c.display) - noauth := false - if err != nil { - fmt.Fprintf(os.Stderr, "Could not get authority info: %v\n", err) - fmt.Fprintf(os.Stderr, "Trying connection without authority info...\n") - authName = "" - authData = []byte{} - noauth = true - } - - // Assume that the authentication protocol is "MIT-MAGIC-COOKIE-1". - if !noauth && (authName != "MIT-MAGIC-COOKIE-1" || len(authData) != 16) { - return nil, errors.New("unsupported auth protocol " + authName) - } - - buf := make([]byte, 12+pad(len(authName))+pad(len(authData))) - buf[0] = 0x6c - buf[1] = 0 - Put16(buf[2:], 11) - Put16(buf[4:], 0) - Put16(buf[6:], uint16(len(authName))) - Put16(buf[8:], uint16(len(authData))) - Put16(buf[10:], 0) - copy(buf[12:], []byte(authName)) - copy(buf[12+pad(len(authName)):], authData) - if _, err = c.conn.Write(buf); err != nil { - return nil, err - } - - head := make([]byte, 8) - if _, err = io.ReadFull(c.conn, head[0:8]); err != nil { - return nil, err - } - code := head[0] - reasonLen := head[1] - major := Get16(head[2:]) - minor := Get16(head[4:]) - dataLen := Get16(head[6:]) - - if major != 11 || minor != 0 { - return nil, errors.New(fmt.Sprintf("x protocol version mismatch: %d.%d", major, minor)) - } - - buf = make([]byte, int(dataLen)*4+8, int(dataLen)*4+8) - copy(buf, head) - if _, err = io.ReadFull(c.conn, buf[8:]); err != nil { - return nil, err - } - - if code == 0 { - reason := buf[8 : 8+reasonLen] - return nil, errors.New(fmt.Sprintf("x protocol authentication refused: %s", string(reason))) - } - - ReadSetupInfo(buf, &c.Setup) - - if c.defaultScreen >= len(c.Setup.Roots) { - c.defaultScreen = 0 - } - - c.nextId = Id(c.Setup.ResourceIdBase) - c.nextCookie = 1 - c.cookies = make(map[uint16]*Cookie) - c.events = queue{make([][]byte, 100), 0, 0} - c.extensions = make(map[string]byte) - - c.newReadChannels() - c.newRequestChannels() - return c, nil -} - -// Close closes the connection to the X server. -func (c *Conn) Close() { c.conn.Close() } - -func connect(display string) (*Conn, error) { - if len(display) == 0 { - display = os.Getenv("DISPLAY") - } - - display0 := display - if len(display) == 0 { - return nil, errors.New("empty display string") - } - - colonIdx := strings.LastIndex(display, ":") - if colonIdx < 0 { - return nil, errors.New("bad display string: " + display0) - } - - var protocol, socket string - c := new(Conn) - - if display[0] == '/' { - socket = display[0:colonIdx] - } else { - slashIdx := strings.LastIndex(display, "/") - if slashIdx >= 0 { - protocol = display[0:slashIdx] - c.host = display[slashIdx+1 : colonIdx] - } else { - c.host = display[0:colonIdx] - } - } - - display = display[colonIdx+1 : len(display)] - if len(display) == 0 { - return nil, errors.New("bad display string: " + display0) - } - - var scr string - dotIdx := strings.LastIndex(display, ".") - if dotIdx < 0 { - c.display = display[0:] - } else { - c.display = display[0:dotIdx] - scr = display[dotIdx+1:] - } - - dispnum, err := strconv.Atoi(c.display) - if err != nil || dispnum < 0 { - return nil, errors.New("bad display string: " + display0) - } - - if len(scr) != 0 { - c.defaultScreen, err = strconv.Atoi(scr) - if err != nil { - return nil, errors.New("bad display string: " + display0) - } - } - - // Connect to server - if len(socket) != 0 { - c.conn, err = net.Dial("unix", socket+":"+c.display) - } else if len(c.host) != 0 { - if protocol == "" { - protocol = "tcp" - } - c.conn, err = net.Dial(protocol, c.host+":"+strconv.Itoa(6000+dispnum)) - } else { - c.conn, err = net.Dial("unix", "/tmp/.X11-unix/X"+c.display) - } - - if err != nil { - return nil, errors.New("cannot connect to " + display0 + ": " + err.Error()) - } - return c, nil -}