forked from tebeka/selenium
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathservice.go
277 lines (250 loc) · 7.34 KB
/
service.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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
package selenium
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"strconv"
"strings"
"time"
)
// ServiceOption configures a Service instance.
type ServiceOption func(*Service) error
// Display specifies the value to which set the DISPLAY environment variable,
// as well as the path to the Xauthority file containing credentials needed to
// write to that X server.
func Display(d, xauthPath string) ServiceOption {
return func(s *Service) error {
if s.display != "" {
return fmt.Errorf("service display already set: %v", s.display)
}
if s.xauthPath != "" {
return fmt.Errorf("service xauth path already set: %v", s.xauthPath)
}
if _, err := strconv.Atoi(d); err != nil {
return fmt.Errorf("supplied display %q must be an integer as a string", d)
}
s.display = d
s.xauthPath = xauthPath
return nil
}
}
// StartFrameBuffer causes an X virtual frame buffer to start before the
// WebDriver service. The frame buffer process will be terminated when the
// service itself is stopped.
func StartFrameBuffer() ServiceOption {
return func(s *Service) error {
if s.display != "" {
return fmt.Errorf("service display already set: %v", s.display)
}
if s.xauthPath != "" {
return fmt.Errorf("service xauth path already set: %v", s.xauthPath)
}
if s.xvfb != nil {
return fmt.Errorf("service Xvfb instance already running")
}
fb, err := NewFrameBuffer()
if err != nil {
return fmt.Errorf("error starting frame buffer: %v", err)
}
s.xvfb = fb
return Display(fb.Display, fb.AuthPath)(s)
}
}
// Output specifies that the WebDriver service should log to the provided
// writer.
func Output(w io.Writer) ServiceOption {
return func(s *Service) error {
s.output = w
return nil
}
}
// GeckoDriver sets the path to the geckodriver binary. Unlike other drivers,
// Selenium Server does not support specifying the geckodriver path. This
// ServiceOption is only useful when calling NewSeleniumService.
func GeckoDriver(path string) ServiceOption {
return func(s *Service) error {
s.geckoDriverPath = path
return nil
}
}
// Service controls a locally-running Selenium subprocess.
type Service struct {
addr string
cmd *exec.Cmd
shutdownURLPath string
display, xauthPath string
xvfb *FrameBuffer
geckoDriverPath string
output io.Writer
}
// NewSeleniumService starts a Selenium instance in the background.
func NewSeleniumService(jarPath string, port int, opts ...ServiceOption) (*Service, error) {
cmd := exec.Command("java", "-jar", jarPath, "-port", strconv.Itoa(port))
s, err := newService(cmd, port, opts...)
if err != nil {
return nil, err
}
return s, nil
}
// NewChromeDriverService starts a ChromeDriver instance in the background.
func NewChromeDriverService(path string, port int, opts ...ServiceOption) (*Service, error) {
cmd := exec.Command(path, "--port="+strconv.Itoa(port), "--url-base=wd/hub", "--verbose")
s, err := newService(cmd, port, opts...)
if err != nil {
return nil, err
}
s.shutdownURLPath = "/wd/hub/shutdown"
return s, nil
}
func newService(cmd *exec.Cmd, port int, opts ...ServiceOption) (*Service, error) {
s := &Service{addr: fmt.Sprintf("http://localhost:%d", port)}
for _, opt := range opts {
if err := opt(s); err != nil {
return nil, err
}
}
cmd.Stderr = s.output
cmd.Stdout = s.output
cmd.Env = os.Environ()
// TODO(minusnine): Pdeathsig is only supported on Linux. Somehow, make sure
// process cleanup happens as gracefully as possible.
if s.display != "" {
cmd.Env = append(cmd.Env, "DISPLAY=:"+s.display)
}
if s.xauthPath != "" {
cmd.Env = append(cmd.Env, "XAUTHORITY="+s.xauthPath)
}
if s.geckoDriverPath != "" {
cmd.Args = append([]string{"java", "-Dwebdriver.gecko.driver=" + s.geckoDriverPath}, cmd.Args[1:]...)
}
if err := cmd.Start(); err != nil {
return nil, err
}
for i := 0; i < 30; i++ {
time.Sleep(time.Second)
resp, err := http.Get(s.addr)
if err == nil {
resp.Body.Close()
switch resp.StatusCode {
// Selenium <3 returned Forbidden and BadRequest. ChromeDriver and
// Selenium 3 return OK.
case http.StatusForbidden, http.StatusBadRequest, http.StatusOK:
s.cmd = cmd
return s, nil
}
}
}
return nil, fmt.Errorf("server did not respond on port %d", port)
}
// Stop shuts down the WebDriver service, and the X virtual frame buffer
// if one was started.
func (s *Service) Stop() error {
// Selenium 3 stopped supporting the shutdown URL by default.
// https://github.com/SeleniumHQ/selenium/issues/2852
if s.shutdownURLPath == "" {
if err := s.cmd.Process.Kill(); err != nil {
return err
}
} else {
resp, err := http.Get(s.addr + s.shutdownURLPath)
if err != nil {
return err
}
resp.Body.Close()
}
if err := s.cmd.Wait(); err != nil && err.Error() != "signal: killed" {
return err
}
if s.xvfb != nil {
return s.xvfb.Stop()
}
return nil
}
// FrameBuffer controls an X virtual frame buffer running as a background
// process.
type FrameBuffer struct {
// Display is the X11 display number that the Xvfb process is hosting
// (without the preceding colon).
Display string
// AuthPath is the path to the X11 authorization file that permits X clients
// to use the X server. This is typically provided to the client via the
// XAUTHORITY environment variable.
AuthPath string
cmd *exec.Cmd
}
// NewFrameBuffer starts an X virtual frame buffer running in the background.
func NewFrameBuffer() (*FrameBuffer, error) {
r, w, err := os.Pipe()
if err != nil {
return nil, err
}
defer r.Close()
auth, err := ioutil.TempFile("", "selenium-xvfb")
if err != nil {
return nil, err
}
authPath := auth.Name()
if err := auth.Close(); err != nil {
return nil, err
}
// Xvfb will print the display on which it is listening to file descriptor 3,
// for which we provide a pipe.
xvfb := exec.Command("Xvfb", "-displayfd", "3", "-nolisten", "tcp")
xvfb.ExtraFiles = []*os.File{w}
// TODO(minusnine): plumb a way to set xvfb.Std{err,out} conditionally.
// TODO(minusnine): Pdeathsig is only supported on Linux. Somehow, make sure
// process cleanup happens as gracefully as possible.
xvfb.Env = []string{"XAUTHORITY=" + authPath}
if err := xvfb.Start(); err != nil {
return nil, err
}
w.Close()
type resp struct {
display string
err error
}
ch := make(chan resp)
go func() {
bufr := bufio.NewReader(r)
s, err := bufr.ReadString('\n')
ch <- resp{s, err}
}()
var display string
select {
case resp := <-ch:
if resp.err != nil {
return nil, resp.err
}
display = strings.TrimSpace(resp.display)
if _, err := strconv.Atoi(display); err != nil {
return nil, errors.New("Xvfb did not print the display number")
}
case <-time.After(3 * time.Second):
return nil, errors.New("timeout waiting for Xvfb")
}
xauth := exec.Command("xauth", "generate", ":"+display, ".", "trusted")
xauth.Stderr = os.Stderr
xauth.Stdout = os.Stdout
xauth.Env = []string{"XAUTHORITY=" + authPath}
if err := xauth.Run(); err != nil {
return nil, err
}
return &FrameBuffer{display, authPath, xvfb}, nil
}
// Stop kills the background frame buffer process and removes the X
// authorization file.
func (f FrameBuffer) Stop() error {
if err := f.cmd.Process.Kill(); err != nil {
return err
}
os.Remove(f.AuthPath) // best effort removal; ignore error
if err := f.cmd.Wait(); err != nil && err.Error() != "signal: killed" {
return err
}
return nil
}