/
PhantomJSDriverService.java
448 lines (398 loc) · 19.3 KB
/
PhantomJSDriverService.java
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
443
444
445
446
447
448
/*
This file is part of the GhostDriver by Ivan De Marino <http://ivandemarino.me>.
Copyright (c) 2012, Ivan De Marino <http://ivandemarino.me>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.openqa.selenium.phantomjs;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import org.openqa.selenium.Capabilities;
import org.openqa.selenium.Proxy;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.net.PortProber;
import org.openqa.selenium.os.CommandLine;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.service.DriverService;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import static com.google.common.base.Preconditions.*;
/**
* Service that controls the life-cycle of a PhantomJS in Remote WebDriver mode.
* The Remote WebDriver is implemented via GhostDriver.
* <p/>
* NOTE: Yes, the design of this class is heavily inspired by {@link org.openqa.selenium.chrome.ChromeDriverService}.
*
* @author Ivan De Marino <http://ivandemarino.me>
*/
public class PhantomJSDriverService extends DriverService {
/**
* System property/capability that defines the location of the PhantomJS executable.
*/
public static final String PHANTOMJS_EXECUTABLE_PATH_PROPERTY = "phantomjs.binary.path";
/**
* Optional System property/capability that defines the location of the
* GhostDriver JavaScript launch file (i.e. <code>"src/main.js"</code>).
*/
public static final String PHANTOMJS_GHOSTDRIVER_PATH_PROPERTY = "phantomjs.ghostdriver.path";
/**
* <p>Optional capability that allows to add custom command line arguments
* to the spawned phantomjs process.</p>
*
* <p>Set this capability with a list of of argument strings to add, e.g.
* <code>new String[] { "--ignore-ssl-errors=yes", "--load-images=no" }
* </code>.</p>
*/
public static final String PHANTOMJS_CLI_ARGS_CAPABILITY = "phantomjs.cli.args";
/**
* Set capabilities with this prefix to apply it to the PhantomJS <code>page.settings.*</code> object.
* Every PhantomJS WebPage Setting can be used.
* See <a href="https://github.com/ariya/phantomjs/wiki/API-Reference#wiki-webpage-settings">PhantomJS docs/a>.
*/
public static final String PHANTOMJS_PAGE_SETTINGS_PREFIX = "phantomjs.page.settings.";
private static final String PHANTOMJS_DEFAULT_EXECUTABLE = "phantomjs";
private static final String PHANTOMJS_DOC_LINK = "https://github.com/ariya/phantomjs/wiki";
private static final String PHANTOMJS_DOWNLOAD_LINK = "http://phantomjs.org/download.html";
private static final String GHOSTDRIVER_DOC_LINK = "https://github.com/detro/ghostdriver/blob/master/README.md";
private static final String GHOSTDRIVER_DOWNLOAD_LINK = "https://github.com/detro/ghostdriver/downloads";
/**
* Constructor
*
* @param executable File pointing at the PhantomJS executable.
* @param port Which port to start the PhantomJS/GhostDriver on.
* @param args The arguments to the launched server.
* @param environment The environment for the launched server.
* @throws java.io.IOException If an I/O error occurs.
*/
private PhantomJSDriverService(File executable,
int port,
ImmutableList<String> args,
ImmutableMap<String, String> environment) throws IOException {
super(executable, port, args, environment);
}
/**
* Configures and returns a new {@link PhantomJSDriverService} using the default configuration.
* <p/>
* In this configuration, the service will use the PhantomJS executable identified by the the
* following capability, system property or PATH environment variables:
* <ul>
* <li>{@link PhantomJSDriverService#PHANTOMJS_EXECUTABLE_PATH_PROPERTY}</li>
* <li>
* {@link PhantomJSDriverService#PHANTOMJS_GHOSTDRIVER_PATH_PROPERTY}
* (Optional - without will use GhostDriver internal to PhantomJS)
* </li>
* </ul>
* <p/>
* Each service created by this method will be configured to find and use a free port on the current system.
*
* @return A new ChromeDriverService using the default configuration.
*/
public static PhantomJSDriverService createDefaultService(Capabilities desiredCapabilities) {
// Look for Proxy configuration within the Capabilities
Proxy proxy = null;
if (desiredCapabilities != null) {
proxy = (Proxy) desiredCapabilities.getCapability(CapabilityType.PROXY);
}
// Find PhantomJS executable
File phantomjsfile = findPhantomJS(desiredCapabilities, PHANTOMJS_DOC_LINK, PHANTOMJS_DOWNLOAD_LINK);
// Find GhostDriver main JavaScript file
File ghostDriverfile = findGhostDriver(desiredCapabilities, GHOSTDRIVER_DOC_LINK, GHOSTDRIVER_DOWNLOAD_LINK);
// Find command line arguments to add
String[] commandLineArguments = findCommandLineArguments(desiredCapabilities);
// Build & return service
return new Builder().usingPhantomJSExecutable(phantomjsfile)
.usingGhostDriver(ghostDriverfile)
.usingAnyFreePort()
.withProxy(proxy)
.usingCommandLineArguments(commandLineArguments)
.build();
}
/**
* Same as {@link PhantomJSDriverService#createDefaultService(org.openqa.selenium.Capabilities)}.
* <p/>
* In this case PhantomJS or GhostDriver can't be searched within the Capabilities, only System
* Properties.
*
* @return A new ChromeDriverService using the default configuration.
*/
public static PhantomJSDriverService createDefaultService() {
return createDefaultService(null);
}
/**
* Looks into the Capabilities, the current $PATH and the System Properties for
* {@link PhantomJSDriverService#PHANTOMJS_EXECUTABLE_PATH_PROPERTY}.
* <p/>
* NOTE: If the Capability, the $PATH and the System Property are set, the Capability takes
* priority over the System Property, that in turn takes priority over the $PATH.
*
* @param desiredCapabilities Capabilities in which we will look for the path to PhantomJS
* @param docsLink The link to the PhantomJS documentation page
* @param downloadLink The link to the PhantomJS download page
* @return The driver executable as a {@link File} object
* @throws IllegalStateException If the executable not found or cannot be executed
*/
protected static File findPhantomJS(Capabilities desiredCapabilities, String docsLink,
String downloadLink) {
String phantomjspath = null;
if (desiredCapabilities != null &&
desiredCapabilities.getCapability(PHANTOMJS_EXECUTABLE_PATH_PROPERTY) != null) {
phantomjspath = (String) desiredCapabilities.getCapability(PHANTOMJS_EXECUTABLE_PATH_PROPERTY);
} else {
phantomjspath = CommandLine.find(PHANTOMJS_DEFAULT_EXECUTABLE);
phantomjspath = System.getProperty(PHANTOMJS_EXECUTABLE_PATH_PROPERTY, phantomjspath);
}
checkState(phantomjspath != null,
"The path to the driver executable must be set by the %s capability/system property/PATH variable;"
+ " for more information, see %s. "
+ "The latest version can be downloaded from %s",
PHANTOMJS_EXECUTABLE_PATH_PROPERTY,
docsLink,
downloadLink);
File phantomjs = new File(phantomjspath);
checkExecutable(phantomjs);
return phantomjs;
}
/**
* Find the GhostDriver main file (i.e. <code>"main.js"</code>).
* <p/>
* Looks into the Capabilities and the System Properties for
* {@link PhantomJSDriverService#PHANTOMJS_GHOSTDRIVER_PATH_PROPERTY}.
* <p/>
* NOTE: If both the Capability and the System Property are set, the Capability takes priority.
*
* @param desiredCapabilities Capabilities in which we will look for the path to GhostDriver
* @param docsLink The link to the GhostDriver documentation page
* @param downloadLink The link to the GhostDriver download page
* @return The driver executable as a {@link File} object
* @throws IllegalStateException If the executable not found or cannot be executed
*/
protected static File findGhostDriver(Capabilities desiredCapabilities, String docsLink, String downloadLink) {
// Recover path to GhostDriver from the System Properties or the Capabilities
String ghostdriverpath = null;
if (desiredCapabilities != null &&
(String) desiredCapabilities.getCapability(PHANTOMJS_GHOSTDRIVER_PATH_PROPERTY) != null) {
ghostdriverpath = (String) desiredCapabilities.getCapability(PHANTOMJS_GHOSTDRIVER_PATH_PROPERTY);
} else {
ghostdriverpath = System.getProperty(PHANTOMJS_GHOSTDRIVER_PATH_PROPERTY, ghostdriverpath);
}
if (ghostdriverpath != null) {
checkState(ghostdriverpath != null,
"The path to the driver executable must be set by the '%s' capability/system property;"
+ " for more information, see %s. "
+ "The latest version can be downloaded from %s",
PHANTOMJS_GHOSTDRIVER_PATH_PROPERTY,
docsLink,
downloadLink);
// Check few things on the file before returning it
File ghostdriver = new File(ghostdriverpath);
checkState(ghostdriver.exists(),
"The GhostDriver does not exist: %s",
ghostdriver.getAbsolutePath());
checkState(ghostdriver.isFile(),
"The GhostDriver is a directory: %s",
ghostdriver.getAbsolutePath());
checkState(ghostdriver.canRead(),
"The GhostDriver is not a readable file: %s",
ghostdriver.getAbsolutePath());
return ghostdriver;
}
// This means that no GhostDriver System Property nor Capability was set
return null;
}
private static String[] findCommandLineArguments(Capabilities desiredCapabilities) {
String[] args = new String[]{};
if (desiredCapabilities != null) {
Object capability = desiredCapabilities.getCapability(PHANTOMJS_CLI_ARGS_CAPABILITY);
if (capability != null) {
args = (String[]) capability;
}
}
return args;
}
/**
* Builder used to configure new {@link PhantomJSDriverService} instances.
*/
public static class Builder {
private int port = 0;
private File phantomjs = null;
private File ghostdriver = null;
private ImmutableMap<String, String> environment = ImmutableMap.of();
private File logFile;
private Proxy proxy = null;
private String[] commandLineArguments = null;
/**
* Sets which PhantomJS executable the builder will use.
*
* @param file The executable to use.
* @return A self reference.
*/
public Builder usingPhantomJSExecutable(File file) {
checkNotNull(file);
checkExecutable(file);
this.phantomjs = file;
return this;
}
/**
* Sets which GhostDriver the builder will use.
*
* @param file The GhostDriver's <code>main.js</code> to use.
* @return A self reference.
*/
public Builder usingGhostDriver(File file) {
this.ghostdriver = file;
return this;
}
/**
* Sets which port the service should listen on. A value of 0 indicates that any free port
* may be used.
*
* @param port The port to use; must be non-negative.
* @return A self reference.
*/
public Builder usingPort(int port) {
checkArgument(port >= 0, "Invalid port number: %d", port);
this.port = port;
return this;
}
/**
* Configures the service to listen on any available port.
*
* @return A self reference.
*/
public Builder usingAnyFreePort() {
this.port = 0;
return this;
}
/**
* Defines the environment for the service. These settings will be inherited by every
* browser session launched by the service.
*
* @param environment A map of the environment variables to launch the service with.
* @return A self reference.
*/
public Builder withEnvironment(Map<String, String> environment) {
this.environment = ImmutableMap.copyOf(environment);
return this;
}
/**
* Configures the service to write log to the given file.
*
* @param logFile A file to write log to.
* @return A self reference.
*/
public Builder withLogFile(File logFile) {
this.logFile = logFile;
return this;
}
/**
* Configures the service to use a specific Proxy configuration.
* <p/>
* NOTE: Usually the proxy configuration is passed to the Remote WebDriver via WireProtocol
* Capabilities. PhantomJS doesn't yet support protocol configuration at runtime: it
* requires it to be defined on launch.
*
* @param proxy The {@link Proxy} configuration from the {@link org.openqa.selenium.remote.DesiredCapabilities}
* @return A self reference.
*/
public Builder withProxy(Proxy proxy) {
this.proxy = proxy;
return this;
}
/**
* Configures the service to pass additional command line arguments to the PhantomJS executable.
* @param commandLineArguments list of command line arguments, e.g. "--ignore-ssl-errors=yes"
* @return A self reference.
*/
public Builder usingCommandLineArguments(String[] commandLineArguments) {
this.commandLineArguments = commandLineArguments;
return this;
}
/**
* Creates a new service. Before creating a new service, the builder will find a port for
* the server to listen to.
*
* @return The new service object.
*/
public PhantomJSDriverService build() {
// Find a port to listen on, if not already decided
port = port == 0 ? PortProber.findFreePort() : port;
// Few final checks
checkState(phantomjs != null, "Path to PhantomJS executable not specified");
try {
// Build a list of command line arguments for the executable
ImmutableList.Builder<String> argsBuilder = ImmutableList.builder();
// Add command line proxy configuration for PhantomJS
if (proxy != null) {
switch (proxy.getProxyType()) {
case MANUAL:
if (!proxy.getHttpProxy().isEmpty()) { //< HTTP proxy
argsBuilder.add("--proxy-type=http");
argsBuilder.add(String.format("--proxy=%s", proxy.getHttpProxy()));
} else if (!proxy.getSocksProxy().isEmpty()) { //< SOCKS5 proxy
argsBuilder.add("--proxy-type=socks5");
argsBuilder.add(String.format("--proxy=%s", proxy.getSocksProxy()));
argsBuilder.add(String.format("--proxy-auth=%s:%s", proxy.getSocksUsername(),
proxy.getSocksPassword()));
} else {
// TODO Not supported yet by PhantomJS
checkArgument(true, "PhantomJS supports only HTTP and Socks5 Proxy currently");
}
break;
case PAC:
// TODO Not supported yet by PhantomJS
checkArgument(true, "PhantomJS doesn't support Proxy PAC files");
break;
case SYSTEM:
argsBuilder.add("--proxy-type=system");
break;
case AUTODETECT:
// TODO Not supported yet by PhantomJS
checkArgument(true, "PhantomJS doesn't support Proxy Auto-configuration");
break;
case DIRECT:
default:
argsBuilder.add("--proxy-type=none");
break;
}
}
// Add additional command line arguments provided via the
// PHANTOMJS_CLI_ARGS_CAPABILITY (if any).
if (this.commandLineArguments != null) {
argsBuilder.add(this.commandLineArguments);
}
if (ghostdriver != null) {
// Path to GhostDriver provided: use it simply as a PhantomJS script
// Add the canonical path to GhostDriver
argsBuilder.add(ghostdriver.getCanonicalPath());
// Add the port to listed on
argsBuilder.add(String.format("%d", port));
} else {
// Path to GhostDriver not provided: use PhantomJS's internal GhostDriver (default behaviour)
// Append required parameters
argsBuilder.add(String.format("--webdriver=%d", port));
}
// Create a new service
return new PhantomJSDriverService(phantomjs, port, argsBuilder.build(), environment);
} catch (IOException e) {
throw new WebDriverException(e);
}
}
}
}