Skip to content

Commit 8264ba5

Browse files
committed
[Grid] Support auto downloads in Grid
Fixes #11656 #11658 Following has been done: * Specify the default base directory into which all downloads at a node will go into via the flag “—-base-dir-downloads”. If this flag does not have a value then we default to user’s home. * Turn ON managing download folders via the flag “-—enable-manage-downloads” * Enabled support for Chrome|Edge|Firefox browsers. * File downloads will be done only in a session aware directory for a given web driver session. After session is killed, the directory gets cleaned up as well.
1 parent 63e8543 commit 8264ba5

File tree

14 files changed

+625
-132
lines changed

14 files changed

+625
-132
lines changed

java/src/org/openqa/selenium/grid/data/DefaultSlotMatcher.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ public boolean matches(Capabilities stereotype, Capabilities capabilities) {
6464
return false;
6565
}
6666

67+
if (!autoDownloadsMatch(stereotype, capabilities)) {
68+
return false;
69+
}
70+
6771
if (!platformVersionMatch(stereotype, capabilities)) {
6872
return false;
6973
}
@@ -106,6 +110,22 @@ private Boolean initialMatch(Capabilities stereotype, Capabilities capabilities)
106110
.orElse(true);
107111
}
108112

113+
private Boolean autoDownloadsMatch(Capabilities stereotype, Capabilities capabilities) {
114+
//First lets check if user wanted auto downloads
115+
Object raw = capabilities.getCapability("se:enableDownloads");
116+
if (raw == null || !Boolean.parseBoolean(raw.toString())) {
117+
//User didn't ask. So lets move on to the next matching criteria
118+
return true;
119+
}
120+
//User wants auto downloads to be done on this browser flavor.
121+
raw = stereotype.getCapability("se:enableDownloads");
122+
if (raw == null || !Boolean.parseBoolean(raw.toString())) {
123+
//User wants it, we don't have it. So no match
124+
return false;
125+
}
126+
return true;
127+
}
128+
109129
private Boolean platformVersionMatch(Capabilities stereotype, Capabilities capabilities) {
110130
/*
111131
This platform version match is not W3C compliant but users can add Appium servers as

java/src/org/openqa/selenium/grid/node/config/NodeFlags.java

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,21 @@ public class NodeFlags implements HasRoles {
230230
private String nodeImplementation = DEFAULT_NODE_IMPLEMENTATION;
231231

232232
@Parameter(
233-
names = {"--downloads-path"},
234-
description = "The default location wherein all browser triggered file downloads would be "
235-
+ "available to be retrieved from. This is usually the directory that you configure in "
236-
+ "your browser as the default location for storing downloaded files.")
237-
@ConfigValue(section = NODE_SECTION, name = "downloads-path", example = "")
238-
private String downloadsPath = "";
233+
names = {"--enable-manage-downloads"},
234+
description = "When enabled, the Grid node will automatically do the following: \n" +
235+
"\t1. Creates a directory named '$HOME/.cache/selenium/downloads/' which will now represent the directory "
236+
+ "into which files downloaded by Chrome/Firefox/Edge browser will be under.\n" +
237+
"\t2. For every new session, a sub-directory will be created/deleted so that all files that were "
238+
+ "downloaded for a given session are stored in.")
239+
@ConfigValue(section = NODE_SECTION, name = "enable-manage-downloads", example = "false")
240+
public Boolean enableManageDownloads;
241+
242+
@Parameter(
243+
names = {"--base-dir-downloads"},
244+
description = "The base directory into which all downloads would be saved. If no value is specified "
245+
+ "then this defaults to the user's home directory.")
246+
@ConfigValue(section = NODE_SECTION, name = "enable-manage-downloads", example = "user.home")
247+
public String baseDirectory;
239248

240249
@Override
241250
public Set<Role> getRoles() {

java/src/org/openqa/selenium/grid/node/config/NodeOptions.java

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.openqa.selenium.json.JsonOutput;
4141
import org.openqa.selenium.net.NetworkUtils;
4242
import org.openqa.selenium.net.Urls;
43+
import org.openqa.selenium.remote.Browser;
4344
import org.openqa.selenium.remote.service.DriverService;
4445

4546
import java.io.File;
@@ -149,8 +150,14 @@ public Optional<URI> getPublicGridUri() {
149150
}
150151
}
151152

152-
public Optional<String> getDownloadsPath() {
153-
return config.get(NODE_SECTION, "downloads-path");
153+
public boolean isAutoManageDownloadsFolder() {
154+
return config.getBool(NODE_SECTION, "enable-manage-downloads")
155+
.orElse(Boolean.FALSE);
156+
}
157+
158+
public String baseDirForDownloads() {
159+
return config.get(NODE_SECTION, "base-dir-downloads")
160+
.orElse(System.getProperty("user.home"));
154161
}
155162

156163
public Node getNode() {
@@ -216,8 +223,9 @@ public Map<Capabilities, Collection<SessionFactory>> getSessionFactories(
216223
addDriverConfigs(factoryFactory, sessionFactories);
217224
addSpecificDrivers(allDrivers, sessionFactories);
218225
addDetectedDrivers(allDrivers, sessionFactories);
226+
ImmutableMultimap<Capabilities, SessionFactory> built = sessionFactories.build();
219227

220-
return sessionFactories.build().asMap();
228+
return built.asMap();
221229
}
222230

223231
public int getMaxSessions() {
@@ -634,6 +642,10 @@ private Capabilities enhanceStereotype(Capabilities capabilities) {
634642
.setCapability("se:vncEnabled", true)
635643
.setCapability("se:noVncPort", noVncPort());
636644
}
645+
if (isAutoManageDownloadsFolder() && Browser.honoursSpecifiedDownloadsDir(capabilities)) {
646+
capabilities = new PersistentCapabilities(capabilities)
647+
.setCapability("se:enableDownloads", true);
648+
}
637649
return capabilities;
638650
}
639651

java/src/org/openqa/selenium/grid/node/local/LocalNode.java

Lines changed: 139 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@
2626
import com.google.common.collect.ImmutableList;
2727
import com.google.common.collect.ImmutableMap;
2828

29+
import java.io.Serializable;
30+
import java.nio.file.Files;
31+
import java.nio.file.Path;
32+
import java.nio.file.Paths;
2933
import java.util.Arrays;
34+
import java.util.Comparator;
35+
import java.util.concurrent.ConcurrentHashMap;
36+
import java.util.stream.Stream;
3037
import org.openqa.selenium.Capabilities;
3138
import org.openqa.selenium.ImmutableCapabilities;
3239
import org.openqa.selenium.MutableCapabilities;
@@ -63,6 +70,7 @@
6370
import org.openqa.selenium.io.TemporaryFilesystem;
6471
import org.openqa.selenium.io.Zip;
6572
import org.openqa.selenium.json.Json;
73+
import org.openqa.selenium.remote.Browser;
6674
import org.openqa.selenium.remote.SessionId;
6775
import org.openqa.selenium.remote.http.HttpMethod;
6876
import org.openqa.selenium.remote.http.HttpRequest;
@@ -116,6 +124,7 @@ public class LocalNode extends Node {
116124

117125
private static final Json JSON = new Json();
118126
private static final Logger LOG = Logger.getLogger(LocalNode.class.getName());
127+
119128
private final EventBus bus;
120129
private final URI externalUri;
121130
private final URI gridUri;
@@ -124,7 +133,7 @@ public class LocalNode extends Node {
124133
private final int maxSessionCount;
125134
private final int configuredSessionCount;
126135
private final boolean cdpEnabled;
127-
private final String downloadsPath;
136+
private final boolean enableManageDownloads;
128137

129138
private final boolean bidiEnabled;
130139
private final AtomicBoolean drainAfterSessions = new AtomicBoolean();
@@ -133,6 +142,10 @@ public class LocalNode extends Node {
133142
private final Cache<SessionId, TemporaryFilesystem> tempFileSystems;
134143
private final AtomicInteger pendingSessions = new AtomicInteger();
135144
private final AtomicInteger sessionCount = new AtomicInteger();
145+
private final Map<SessionId,UUID> downloadFolderToSessionMapping = new ConcurrentHashMap<>();
146+
private final String baseDir;
147+
148+
private File defaultDownloadsDir;
136149

137150
private LocalNode(
138151
Tracer tracer,
@@ -149,7 +162,8 @@ private LocalNode(
149162
Duration heartbeatPeriod,
150163
List<SessionSlot> factories,
151164
Secret registrationSecret,
152-
String downloadsPath) {
165+
String baseDir,
166+
boolean enableManageDownloads) {
153167
super(tracer, new NodeId(UUID.randomUUID()), uri, registrationSecret);
154168

155169
this.bus = Require.nonNull("Event bus", bus);
@@ -166,7 +180,8 @@ private LocalNode(
166180
this.sessionCount.set(drainAfterSessionCount);
167181
this.cdpEnabled = cdpEnabled;
168182
this.bidiEnabled = bidiEnabled;
169-
this.downloadsPath = Optional.ofNullable(downloadsPath).orElse("");
183+
this.enableManageDownloads = enableManageDownloads;
184+
this.baseDir = baseDir;
170185

171186
this.healthCheck = healthCheck == null ?
172187
() -> {
@@ -283,6 +298,11 @@ public int getCurrentSessionCount() {
283298
return Math.toIntExact(currentSessions.size());
284299
}
285300

301+
@VisibleForTesting
302+
public String getDownloadsDirForSession(SessionId sessionId) {
303+
return downloadFolderToSessionMapping.get(sessionId).toString();
304+
}
305+
286306
@ManagedAttribute(name = "MaxSessions")
287307
public int getMaxSessionCount() {
288308
return maxSessionCount;
@@ -381,10 +401,19 @@ public Either<WebDriverException, CreateSessionResponse> newSession(CreateSessio
381401
new RetrySessionRequestException("No slot matched the requested capabilities."));
382402
}
383403

404+
UUID folderIdForPossibleSession = UUID.randomUUID();
405+
Capabilities desiredCapabilities = sessionRequest.getDesiredCapabilities();
406+
Capabilities enriched = enrichWithDownloadsPathInfo(folderIdForPossibleSession, desiredCapabilities);
407+
enriched = desiredCapabilities.merge(enriched);
408+
sessionRequest = new CreateSessionRequest(sessionRequest.getDownstreamDialects(), enriched, sessionRequest.getMetadata());
409+
384410
Either<WebDriverException, ActiveSession> possibleSession = slotToUse.apply(sessionRequest);
385411

386412
if (possibleSession.isRight()) {
387413
ActiveSession session = possibleSession.right();
414+
downloadFolderToSessionMapping.put(session.getId(), folderIdForPossibleSession);
415+
LOG.info("Downloads pertaining to Session Id [" + session.getId().toString() + "] will be " +
416+
"saved to " + defaultDownloadsBaseFolder().getAbsolutePath() + "/" + folderIdForPossibleSession);
388417
currentSessions.put(session.getId(), slotToUse);
389418

390419
checkSessionCount();
@@ -407,7 +436,7 @@ public Either<WebDriverException, CreateSessionResponse> newSession(CreateSessio
407436
externalUri,
408437
slotToUse.isSupportingCdp(),
409438
slotToUse.isSupportingBiDi(),
410-
sessionRequest.getDesiredCapabilities());
439+
desiredCapabilities);
411440

412441
String sessionCreatedMessage = "Session created by the Node";
413442
LOG.info(String.format("%s. Id: %s, Caps: %s", sessionCreatedMessage, sessionId, caps));
@@ -425,6 +454,57 @@ public Either<WebDriverException, CreateSessionResponse> newSession(CreateSessio
425454
}
426455
}
427456

457+
private Capabilities enrichWithDownloadsPathInfo(UUID id, Capabilities caps) {
458+
if (!enableManageDownloads || !Browser.honoursSpecifiedDownloadsDir(caps)) {
459+
// Auto enable of downloads is not turned on. So don't bother fiddling around
460+
// with the capabilities.
461+
return caps;
462+
}
463+
464+
File subDir = new File(defaultDownloadsBaseFolder(), id.toString());
465+
boolean created = subDir.mkdirs();
466+
if (created) {
467+
LOG.fine("Created folder " + subDir.getAbsolutePath() + " for auto downloads");
468+
} else {
469+
//Should we error out here because if the downloads folder can't be created, then
470+
//there's a good chance that the downloads may not work too.
471+
LOG.warning("Could not create folder " + subDir.getAbsolutePath() + " for auto downloads");
472+
}
473+
if (Browser.CHROME.is(caps)) {
474+
ImmutableMap<String, Serializable> map = ImmutableMap.of(
475+
"download.prompt_for_download", false,
476+
"download.default_directory", subDir.getAbsolutePath());
477+
appendPrefs(caps, "goog:chromeOptions", map);
478+
return caps;
479+
}
480+
481+
if (Browser.EDGE.is(caps)) {
482+
ImmutableMap<String, Serializable> map = ImmutableMap.of(
483+
"download.prompt_for_download", false,
484+
"download.default_directory", subDir.getAbsolutePath());
485+
appendPrefs(caps, "ms:edgeOptions", map);
486+
return caps;
487+
}
488+
489+
if (Browser.FIREFOX.is(caps)) {
490+
ImmutableMap<String, Serializable> map = ImmutableMap.of(
491+
"browser.download.folderList", 2,
492+
"browser.download.dir", subDir.getAbsolutePath());
493+
appendPrefs(caps, "moz:firefoxOptions", map);
494+
}
495+
return caps;
496+
}
497+
498+
@SuppressWarnings("unchecked")
499+
private Capabilities appendPrefs(Capabilities caps, String optionsKey, Map<String, Serializable> map) {
500+
Map<String, Object> currentOptions = (Map<String, Object>) Optional.ofNullable(
501+
caps.getCapability(optionsKey))
502+
.orElse(new HashMap<>());
503+
504+
((Map<String, Serializable>)currentOptions.computeIfAbsent("prefs",k -> new HashMap<>())).putAll(map);
505+
return caps;
506+
}
507+
428508
@Override
429509
public boolean isSessionOwner(SessionId id) {
430510
Require.nonNull("Session ID", id);
@@ -484,18 +564,19 @@ public HttpResponse downloadFile(HttpRequest req, SessionId id) {
484564
if (slot != null && slot.getSession() instanceof DockerSession) {
485565
return executeWebDriverCommand(req);
486566
}
487-
if (this.downloadsPath.isEmpty()) {
488-
String msg = "Please specify the path wherein the files downloaded using the browser "
489-
+ "would be available via the command line arg [--downloads-path] and restart the node";
567+
if (!this.enableManageDownloads) {
568+
String msg = "Please enable management of downloads via the command line arg "
569+
+ "[--enable-manage-downloads] and restart the node";
490570
throw new WebDriverException(msg);
491571
}
492-
File dir = new File(this.downloadsPath);
572+
File dir = new File(defaultDownloadsBaseFolder(),
573+
downloadFolderToSessionMapping.get(id).toString());
493574
if (!dir.exists()) {
494575
throw new WebDriverException(
495-
String.format("Cannot locate downloads directory %s.", downloadsPath));
576+
String.format("Cannot locate downloads directory %s.", dir));
496577
}
497578
if (!dir.isDirectory()) {
498-
throw new WebDriverException(String.format("Invalid directory: %s.", downloadsPath));
579+
throw new WebDriverException(String.format("Invalid directory: %s.", dir));
499580
}
500581
if (req.getMethod().equals(HttpMethod.GET)) {
501582
//User wants to list files that can be downloaded
@@ -525,7 +606,7 @@ public HttpResponse downloadFile(HttpRequest req, SessionId id) {
525606
).orElse(new File[]{});
526607
if (allFiles.length == 0) {
527608
throw new WebDriverException(
528-
String.format("Cannot find file [%s] in directory %s.", filename, downloadsPath));
609+
String.format("Cannot find file [%s] in directory %s.", filename, dir.getAbsolutePath()));
529610
}
530611
if (allFiles.length != 1) {
531612
throw new WebDriverException(
@@ -590,6 +671,26 @@ public void stop(SessionId id) throws NoSuchSessionException {
590671
}
591672

592673
currentSessions.invalidate(id);
674+
deleteDownloadsFolderForSession(id);
675+
}
676+
677+
private void deleteDownloadsFolderForSession(SessionId id) {
678+
String childDir = downloadFolderToSessionMapping.get(id).toString();
679+
Path toDelete = Paths.get(defaultDownloadsBaseFolder().getAbsolutePath(), childDir);
680+
if (!toDelete.toFile().exists()) {
681+
return;
682+
}
683+
try (Stream<Path> walk = Files.walk(toDelete)) {
684+
walk
685+
.sorted(Comparator.reverseOrder())
686+
.map(Path::toFile)
687+
.peek(each -> LOG.info("Deleting downloads folder for session " + id))
688+
.forEach(File::delete);
689+
downloadFolderToSessionMapping.remove(id);
690+
} catch (IOException e) {
691+
LOG.warning("Could not delete downloads directory for session " + id + ". Root cause: "
692+
+ e.getMessage());
693+
}
593694
}
594695

595696
private void stopAllSessions() {
@@ -756,6 +857,22 @@ private Map<String, Object> toJson() {
756857
.collect(Collectors.toSet()));
757858
}
758859

860+
private File defaultDownloadsBaseFolder() {
861+
if (defaultDownloadsDir != null) {
862+
return defaultDownloadsDir;
863+
}
864+
String location = baseDir + File.separator + ".cache" +
865+
File.separator + "selenium" + File.separator + "downloads";
866+
defaultDownloadsDir = new File(location);
867+
boolean created = defaultDownloadsDir.mkdirs();
868+
if (created) {
869+
LOG.info("All files downloaded by sessions on this node can be found under [" + location + "].");
870+
} else {
871+
LOG.info(location + " already exists. Using it for managing downloaded files.");
872+
}
873+
return defaultDownloadsDir;
874+
}
875+
759876
public static class Builder {
760877

761878
private final Tracer tracer;
@@ -772,7 +889,8 @@ public static class Builder {
772889
private Duration sessionTimeout = Duration.ofSeconds(NodeOptions.DEFAULT_SESSION_TIMEOUT);
773890
private HealthCheck healthCheck;
774891
private Duration heartbeatPeriod = Duration.ofSeconds(NodeOptions.DEFAULT_HEARTBEAT_PERIOD);
775-
private String downloadsPath = "";
892+
private boolean enableManageDownloads = false;
893+
private String baseDirectory;
776894

777895
private Builder(
778896
Tracer tracer,
@@ -827,8 +945,13 @@ public Builder heartbeatPeriod(Duration heartbeatPeriod) {
827945
return this;
828946
}
829947

830-
public Builder downloadsPath(String path) {
831-
this.downloadsPath = path;
948+
public Builder enableManageDownloads(boolean enable) {
949+
this.enableManageDownloads = enable;
950+
return this;
951+
}
952+
953+
public Builder baseDirectory(String directory) {
954+
this.baseDirectory = directory;
832955
return this;
833956
}
834957

@@ -848,7 +971,8 @@ public LocalNode build() {
848971
heartbeatPeriod,
849972
factories.build(),
850973
registrationSecret,
851-
downloadsPath);
974+
Optional.ofNullable(baseDirectory).orElse(System.getProperty("user.home")),
975+
enableManageDownloads);
852976
}
853977

854978
public Advanced advanced() {

0 commit comments

Comments
 (0)