-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathfirefox.go
185 lines (163 loc) · 3.96 KB
/
firefox.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
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
// Copyright (c) 2020 Dean Jackson <[email protected]>
// MIT Licence applies http://opensource.org/licenses/MIT
package main
import (
"encoding/binary"
"encoding/json"
"fmt"
"io"
"log"
"os"
"time"
)
const timeout = time.Second * 5
// errTimeout is returned by firefox.call if execution time exceeds timeout.
type errTimeout struct {
ID string // command ID
}
func (err errTimeout) Error() string {
return fmt.Sprintf("timeout: %q", err.ID)
}
// command is a command sent to the extension.
type command struct {
ID string `json:"id"`
Name string `json:"command"`
Params interface{} `json:"params"`
ch chan response
}
func (c command) String() string {
return fmt.Sprintf("command #%s - %q", c.ID, c.Name)
}
// encode command into extension STDIO format.
func (c command) encode() ([]byte, error) {
js, err := json.Marshal(c)
if err != nil {
return nil, err
}
b := make([]byte, 4)
binary.LittleEndian.PutUint32(b, uint32(len(js)))
b = append(b, js...)
return b, nil
}
// response is a generic response from the browser extension.
// The full JSON response from the extension is contained in data to
// be unmarshalled by the receiver.
type response struct {
ID string `json:"id"` // ID of the command this is a response to
Err string `json:"error"` // error message returned by extension
data []byte // full JSON response
err error // error encountered decoding response
}
func (r response) String() string {
return fmt.Sprintf("response #%s - %d bytes", r.ID, len(r.data))
}
// firefox communicates with the browser extension via STDIN/STOUT.
type firefox struct {
commands chan command
responses chan response
done chan struct{}
handlers map[string]chan response
}
func newFirefox() *firefox {
return &firefox{
commands: make(chan command, 1),
responses: make(chan response, 1),
done: make(chan struct{}),
handlers: map[string]chan response{},
}
}
// run the read/write loop to send & receive messages from the extension.
func (f *firefox) run() {
go func() {
b := make([]byte, 4)
for {
// read payload size
_, err := os.Stdin.Read(b)
if err == io.EOF {
return
}
if err != nil {
log.Printf("[ERROR] read from stdin: %v", err)
return
}
// read payload
data := make([]byte, binary.LittleEndian.Uint32(b))
_, err = io.ReadFull(os.Stdin, data)
if err == io.EOF {
return
}
if err != nil {
log.Printf("[ERROR] read from stdin: %v", err)
return
}
// log.Printf("received %s", data)
var r response
if err := json.Unmarshal(data, &r); err != nil {
r.err = err
}
r.data = data
log.Printf("received %v", r)
f.responses <- r
}
}()
for {
select {
case <-f.done:
return
case cmd := <-f.commands:
var (
data []byte
// n int
err error
)
if data, err = cmd.encode(); err != nil {
cmd.ch <- response{err: err}
break
}
if _, err = os.Stdout.Write(data); err != nil {
cmd.ch <- response{err: err}
break
}
log.Printf("sent %v", cmd)
f.handlers[cmd.ID] = cmd.ch
case r := <-f.responses:
ch, ok := f.handlers[r.ID]
if ok {
ch <- r
delete(f.handlers, r.ID)
} else {
log.Printf("[ERROR] no handler for message %q", r.ID)
}
}
}
}
// call passes a command to the extension and unmarshals the response into pointer v.
// It returns an error if the command fails, the response isn't understood or
// the respones takes too long.
func (f *firefox) call(cmd string, params, v interface{}) error {
c := command{
ID: newID(),
Name: cmd,
Params: params,
ch: make(chan response),
}
f.commands <- c
t := time.Tick(timeout)
select {
case r := <-c.ch:
if r.err != nil {
return r.err
}
return json.Unmarshal(r.data, v)
case <-t:
return errTimeout{c.ID}
}
}
// exist the run loop.
func (f *firefox) stop() { close(f.done) }
var lastUID = 0
// create a new command ID.
func newID() string {
lastUID++
return fmt.Sprintf("%d.%d", time.Now().Unix(), lastUID)
}