forked from tebeka/selenium
-
Notifications
You must be signed in to change notification settings - Fork 1
/
service.go
442 lines (397 loc) · 12.2 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
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
package selenium
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"regexp"
"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 !isDisplay(d) {
return fmt.Errorf("supplied display %q must be of the format 'x' or 'x.y' where x and y are integers", d)
}
s.display = d
s.xauthPath = xauthPath
return nil
}
}
// isDisplay validates that the given disp is in the format "x" or "x.y", where
// x and y are both integers.
func isDisplay(disp string) bool {
ds := strings.Split(disp, ".")
if len(ds) > 2 {
return false
}
for _, d := range ds {
if _, err := strconv.Atoi(d); err != nil {
return false
}
}
return true
}
// 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.
//
// This is equivalent to calling StartFrameBufferWithOptions with an empty
// map.
func StartFrameBuffer() ServiceOption {
return StartFrameBufferWithOptions(FrameBufferOptions{})
}
// FrameBufferOptions describes the options that can be used to create a frame buffer.
type FrameBufferOptions struct {
// ScreenSize is the option for the frame buffer screen size.
// This is of the form "{width}x{height}[x{depth}]". For example: "1024x768x24"
ScreenSize string
}
// StartFrameBufferWithOptions 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 StartFrameBufferWithOptions(options FrameBufferOptions) 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 := NewFrameBufferWithOptions(options)
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 for the Selenium Server.
// Unlike other drivers, Selenium Server does not support specifying the
// geckodriver path at runtime. This ServiceOption is only useful when calling
// NewSeleniumService.
func GeckoDriver(path string) ServiceOption {
return func(s *Service) error {
s.geckoDriverPath = path
return nil
}
}
// ChromeDriver sets the path for Chromedriver for the Selenium Server. This
// ServiceOption is only useful when calling NewSeleniumService.
func ChromeDriver(path string) ServiceOption {
return func(s *Service) error {
s.chromeDriverPath = path
return nil
}
}
// JavaPath specifies the path to the JRE.
func JavaPath(path string) ServiceOption {
return func(s *Service) error {
s.javaPath = path
return nil
}
}
// V4 specifies that selenium server is v4 compliant
func V4() ServiceOption {
return func(s *Service) error {
s.v4 = true
return nil
}
}
// SessionRequestTimeout specifies the timeout it takes to discard a session request
func SessionRequestTimeout(seconds int) ServiceOption {
return func(s *Service) error {
s.sessionRequestTimeout = seconds
return nil
}
}
// HTMLUnit specifies the path to the JAR for the HTMLUnit driver (compiled
// with its dependencies).
//
// https://github.com/SeleniumHQ/htmlunit-driver/releases
func HTMLUnit(path string) ServiceOption {
return func(s *Service) error {
s.htmlUnitPath = path
return nil
}
}
// Service controls a locally-running Selenium subprocess.
type Service struct {
port int
addr string
cmd *exec.Cmd
shutdownURLPath string
display, xauthPath string
xvfb *FrameBuffer
geckoDriverPath, javaPath string
chromeDriverPath string
htmlUnitPath string
output io.Writer
v4 bool
sessionRequestTimeout int
}
// FrameBuffer returns the FrameBuffer if one was started by the service and nil otherwise.
func (s Service) FrameBuffer() *FrameBuffer {
return s.xvfb
}
// NewSeleniumService starts a Selenium instance in the background.
func NewSeleniumService(jarPath string, port int, opts ...ServiceOption) (*Service, error) {
s, err := newService(exec.Command("java"), "/wd/hub", port, opts...)
if err != nil {
return nil, err
}
if s.javaPath != "" {
s.cmd.Path = s.javaPath
}
var classpath []string
if s.htmlUnitPath != "" {
classpath = append(classpath, s.htmlUnitPath)
}
if s.v4 {
if s.geckoDriverPath != "" {
classpath = append(classpath, s.geckoDriverPath)
}
if s.chromeDriverPath != "" {
classpath = append(classpath, s.chromeDriverPath)
}
s.cmd.Args = append(s.cmd.Args, "-jar", jarPath)
if len(classpath) > 0 {
s.cmd.Args = append(s.cmd.Args, "--ext", strings.Join(classpath, ":"))
}
s.cmd.Args = append(s.cmd.Args, "standalone", "--port", strconv.Itoa(port))
if s.sessionRequestTimeout > 0 {
s.cmd.Args = append(s.cmd.Args, "--session-request-timeout", strconv.Itoa(s.sessionRequestTimeout))
}
} else {
if s.geckoDriverPath != "" {
s.cmd.Args = append([]string{"java", "-Dwebdriver.gecko.driver=" + s.geckoDriverPath}, s.cmd.Args[1:]...)
}
if s.chromeDriverPath != "" {
s.cmd.Args = append([]string{"java", "-Dwebdriver.chrome.driver=" + s.chromeDriverPath}, s.cmd.Args[1:]...)
}
classpath = append(classpath, jarPath)
s.cmd.Args = append(s.cmd.Args, "-cp", strings.Join(classpath, ":"))
s.cmd.Args = append(s.cmd.Args, "org.openqa.grid.selenium.GridLauncherV3", "-port", strconv.Itoa(port), "-debug")
}
if err := s.start(port); 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, "/wd/hub", port, opts...)
if err != nil {
return nil, err
}
s.shutdownURLPath = "/shutdown"
if err := s.start(port); err != nil {
return nil, err
}
return s, nil
}
// NewGeckoDriverService starts a GeckoDriver instance in the background.
func NewGeckoDriverService(path string, port int, opts ...ServiceOption) (*Service, error) {
cmd := exec.Command(path, "--port", strconv.Itoa(port))
s, err := newService(cmd, "", port, opts...)
if err != nil {
return nil, err
}
if err := s.start(port); err != nil {
return nil, err
}
return s, nil
}
func newService(cmd *exec.Cmd, urlPrefix string, port int, opts ...ServiceOption) (*Service, error) {
s := &Service{
port: port,
addr: fmt.Sprintf("http://localhost:%d%s", port, urlPrefix),
}
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)
}
s.cmd = cmd
return s, nil
}
func (s *Service) start(port int) error {
if err := s.cmd.Start(); err != nil {
return err
}
for i := 0; i < 30; i++ {
time.Sleep(time.Second)
resp, err := http.Get(s.addr + "/status")
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:
return nil
}
}
}
return 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.
//
// This is equivalent to calling NewFrameBufferWithOptions with an empty NewFrameBufferWithOptions.
func NewFrameBuffer() (*FrameBuffer, error) {
return NewFrameBufferWithOptions(FrameBufferOptions{})
}
// NewFrameBufferWithOptions starts an X virtual frame buffer running in the background.
// FrameBufferOptions may be populated to change the behavior of the frame buffer.
func NewFrameBufferWithOptions(options FrameBufferOptions) (*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.
arguments := []string{"-displayfd", "3", "-nolisten", "tcp"}
if options.ScreenSize != "" {
var screenSizeExpression = regexp.MustCompile(`^\d+x\d+(?:x\d+)$`)
if !screenSizeExpression.MatchString(options.ScreenSize) {
return nil, fmt.Errorf("invalid screen size: expected 'WxH[xD]', got %q", options.ScreenSize)
}
arguments = append(arguments, "-screen", "0", options.ScreenSize)
}
xvfb := exec.Command("Xvfb", arguments...)
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 = append(xvfb.Env, "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 = append(xauth.Env, "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
}