/
client.go
163 lines (138 loc) · 2.92 KB
/
client.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package jack
import (
"bufio"
"code.google.com/p/go-uuid/uuid"
"encoding/json"
"errors"
"golang.org/x/net/context"
"io"
"os/exec"
"sync"
)
var (
ErrStopped = errors.New("client stopped before response received")
)
type Client struct {
// goroutine state
cancel func()
context context.Context
// req/rep state
lock sync.Mutex
messages chan *Request
waiting map[string]chan *Response
// command state
path string
cmd *exec.Cmd
in io.WriteCloser
out io.ReadCloser
}
func NewClient(path string) *Client {
context, cancel := context.WithCancel(context.Background())
return &Client{
context: context,
cancel: cancel,
lock: sync.Mutex{},
path: path,
messages: make(chan *Request, 1),
waiting: make(map[string]chan *Response),
}
}
func (c *Client) Start() error {
// start command
c.cmd = exec.Command(c.path)
inStream, err := c.cmd.StdinPipe()
if err != nil {
return err
}
c.in = inStream
outStream, err := c.cmd.StdoutPipe()
if err != nil {
return err
}
c.out = outStream
err = c.cmd.Start()
if err != nil {
return err
}
// start internal state goroutines
go c.acceptRequests()
go c.dispatchResponses()
return nil
}
func (c *Client) acceptRequests() {
newline := []byte("\n")
for {
select {
case message := <-c.messages:
blob, err := json.Marshal(message)
if err != nil {
panic(err) // TODO: more graceful failure
}
c.in.Write(blob)
c.in.Write(newline)
case <-c.context.Done():
c.in.Close()
return
}
}
}
func (c *Client) dispatchResponses() {
messages := make(chan *Response, 1)
go func(out chan *Response, src io.Reader) {
scanner := bufio.NewScanner(c.out)
for scanner.Scan() {
response := new(Response)
err := json.Unmarshal(scanner.Bytes(), response)
if err != nil {
panic(err) // TODO: more graceful failure
}
out <- response
}
// we can get a deadlock pretty easily if we stop before everything is
// finished, so we need to stop the rest here when we exit. `Stop` should be
// OK with being called multiple times.
c.Stop()
}(messages, c.out)
for {
select {
case message := <-messages:
c.lock.Lock()
client, ok := c.waiting[message.ID]
if ok {
delete(c.waiting, message.ID)
client <- message
}
c.lock.Unlock()
case <-c.context.Done():
c.out.Close()
return
}
}
}
func (c *Client) Stop() {
c.cancel()
c.cmd.Wait()
}
func (c *Client) Call(method string, args ...interface{}) (interface{}, error) {
message := &Request{
ID: uuid.NewRandom().String(),
Method: method,
Args: args,
}
c.messages <- message
// put our listener on the queue and listen for it
c.lock.Lock()
results := make(chan *Response, 1)
c.waiting[message.ID] = results
c.lock.Unlock()
select {
case result := <-results:
var err error
if result.Error != "" {
err = errors.New(result.Error)
}
return result.Payload, err
case <-c.context.Done():
return nil, ErrStopped
}
}