1 //
2 // Copyright (C) 2014 Sebastian 'tokkee' Harl <sh@tokkee.org>
3 // All rights reserved.
4 //
5 // Redistribution and use in source and binary forms, with or without
6 // modification, are permitted provided that the following conditions
7 // are met:
8 // 1. Redistributions of source code must retain the above copyright
9 // notice, this list of conditions and the following disclaimer.
10 // 2. Redistributions in binary form must reproduce the above copyright
11 // notice, this list of conditions and the following disclaimer in the
12 // documentation and/or other materials provided with the distribution.
13 //
14 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15 // ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
16 // TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR
18 // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19 // EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20 // PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
21 // OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
22 // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
23 // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
24 // ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 /*
27 Package client provides a SysDB client implementation.
29 The Connect function connects to a SysDB server as the specified user:
31 c, err := client.Connect("unix:/var/run/sysdbd.sock", "username")
32 if err != nil {
33 // handle error
34 }
35 defer c.Close()
37 The github.com/sysdb/go/proto package provides support for handling requests
38 and responses. Use the Send and Receive functions to communicate with the
39 server:
41 m := &proto.Message{
42 Type: proto.ConnectionQuery,
43 Raw: []byte{"LOOKUP hosts MATCHING attribute.architecture = 'amd64';"},
44 }
45 if err := c.Send(m); err != nil {
46 // handle error
47 }
48 m, err := c.Receive()
49 if err != nil {
50 // handle error
51 }
52 if m.Type == proto.ConnectionError {
53 // handle failed query
54 }
55 // ...
56 */
57 package client
59 import (
60 "encoding/binary"
61 "fmt"
62 "net"
63 "strings"
65 "github.com/sysdb/go/proto"
66 )
68 // A Conn is a connection to a SysDB server instance.
69 //
70 // Multiple goroutines may invoke methods on a Conn simultaneously but since
71 // the SysDB protocol requires a strict ordering of request and response
72 // messages, the communication with the server will usually happen
73 // sequentially.
74 type Conn struct {
75 c net.Conn
76 network, addr, user string
77 }
79 func (c *Conn) connect() (err error) {
80 if c.c, err = net.Dial(c.network, c.addr); err != nil {
81 return err
82 }
83 defer func() {
84 if err != nil {
85 c.Close()
86 }
87 }()
89 m := &proto.Message{
90 Type: proto.ConnectionStartup,
91 Raw: []byte(c.user),
92 }
93 if err := c.Send(m); err != nil {
94 return err
95 }
97 m, err = c.Receive()
98 if err != nil {
99 return err
100 }
101 if m.Type == proto.ConnectionError {
102 return fmt.Errorf("failed to startup session: %s", string(m.Raw))
103 }
104 if m.Type != proto.ConnectionOK {
105 return fmt.Errorf("failed to startup session: unsupported")
106 }
107 return nil
108 }
110 // Connect sets up a client connection to a SysDB server instance at the
111 // specified address using the specified user.
112 //
113 // The address may be a UNIX domain socket, either prefixed with 'unix:' or
114 // specifying an absolute file-system path.
115 func Connect(addr, user string) (*Conn, error) {
116 network := "tcp"
117 if strings.HasPrefix(addr, "unix:") {
118 network = "unix"
119 addr = addr[len("unix:"):]
120 } else if addr[0] == '/' {
121 network = "unix"
122 }
124 c := &Conn{network: network, addr: addr, user: user}
125 if err := c.connect(); err != nil {
126 return nil, err
127 }
128 return c, nil
129 }
131 // Close closes the client connection.
132 //
133 // Any blocked Send or Receive operations will be unblocked and return errors.
134 func (c *Conn) Close() {
135 if c.c == nil {
136 return
137 }
138 c.c.Close()
139 c.c = nil
140 }
142 // Send sends the specified raw message to the server.
143 //
144 // Send operations block until the full message could be written to the
145 // underlying sockets. This ensures that server and client don't get out of
146 // sync.
147 func (c *Conn) Send(m *proto.Message) error {
148 var err error
149 if c.c != nil {
150 err = proto.Write(c.c, m)
151 if err == nil {
152 return nil
153 }
154 c.Close()
155 }
157 // Try to reconnect.
158 if e := c.connect(); e == nil {
159 return proto.Write(c.c, m)
160 } else if err == nil {
161 err = e
162 }
163 return err
164 }
166 // Receive waits for a reply from the server and returns the raw message.
167 //
168 // Receive operations block until a full message could be read from the
169 // underlying socket. This ensures that server and client don't get out of
170 // sync.
171 func (c *Conn) Receive() (*proto.Message, error) {
172 var err error
173 if c.c != nil {
174 var m *proto.Message
175 m, err = proto.Read(c.c)
176 if err == nil {
177 return m, err
178 }
179 c.Close()
180 }
182 // Try to reconnect.
183 if e := c.connect(); e == nil {
184 return proto.Read(c.c)
185 } else if err == nil {
186 err = e
187 }
188 return nil, err
189 }
191 // ServerVersion queries and returns the version of the remote server.
192 func (c *Conn) ServerVersion() (major, minor, patch int, extra string, err error) {
193 m := &proto.Message{Type: proto.ConnectionServerVersion}
194 if err = c.Send(m); err != nil {
195 return 0, 0, 0, "", err
196 }
198 m, err = c.Receive()
199 if err != nil || m.Type != proto.ConnectionOK {
200 if err == nil {
201 err = fmt.Errorf("SERVER_VERSION command failed with status %d", m.Type)
202 }
203 return 0, 0, 0, "", err
204 }
205 if len(m.Raw) < 4 {
206 return 0, 0, 0, "", fmt.Errorf("SERVER_VERSION reply is too short")
207 }
208 version := int(binary.BigEndian.Uint32(m.Raw[:4]))
209 major = version / 10000
210 minor = version/100 - 100*major
211 patch = version - 10000*major - 100*minor
212 if len(m.Raw) > 4 {
213 extra = string(m.Raw[4:])
214 }
215 return major, minor, patch, extra, nil
216 }
218 // vim: set tw=78 sw=4 sw=4 noexpandtab :