...3import static com.saucelabs.grid.Common.SAUCE_OPTIONS;4import static com.saucelabs.grid.Common.getSauceCapability;5import static java.util.Optional.ofNullable;6import static org.openqa.selenium.ImmutableCapabilities.copyOf;7import static org.openqa.selenium.docker.ContainerConfig.image;8import static org.openqa.selenium.remote.Dialect.W3C;9import static org.openqa.selenium.remote.http.Contents.string;10import static org.openqa.selenium.remote.http.HttpMethod.GET;11import static org.openqa.selenium.remote.http.HttpMethod.POST;12import static org.openqa.selenium.remote.tracing.Tags.EXCEPTION;13import org.openqa.selenium.Capabilities;14import org.openqa.selenium.Dimension;15import org.openqa.selenium.ImmutableCapabilities;16import org.openqa.selenium.PersistentCapabilities;17import org.openqa.selenium.RetrySessionRequestException;18import org.openqa.selenium.SessionNotCreatedException;19import org.openqa.selenium.TimeoutException;20import org.openqa.selenium.UsernameAndPassword;21import org.openqa.selenium.WebDriverException;22import org.openqa.selenium.docker.Container;23import org.openqa.selenium.docker.ContainerConfig;24import org.openqa.selenium.docker.ContainerInfo;25import org.openqa.selenium.docker.Docker;26import org.openqa.selenium.docker.Image;27import org.openqa.selenium.docker.Port;28import org.openqa.selenium.grid.data.CreateSessionRequest;29import org.openqa.selenium.grid.node.ActiveSession;30import org.openqa.selenium.grid.node.SessionFactory;31import org.openqa.selenium.grid.node.docker.DockerAssetsPath;32import org.openqa.selenium.internal.Either;33import org.openqa.selenium.internal.Require;34import org.openqa.selenium.net.PortProber;35import org.openqa.selenium.remote.Command;36import org.openqa.selenium.remote.Dialect;37import org.openqa.selenium.remote.DriverCommand;38import org.openqa.selenium.remote.ProtocolHandshake;39import org.openqa.selenium.remote.Response;40import org.openqa.selenium.remote.SessionId;41import org.openqa.selenium.remote.http.HttpClient;42import org.openqa.selenium.remote.http.HttpRequest;43import org.openqa.selenium.remote.http.HttpResponse;44import org.openqa.selenium.remote.tracing.AttributeKey;45import org.openqa.selenium.remote.tracing.EventAttribute;46import org.openqa.selenium.remote.tracing.EventAttributeValue;47import org.openqa.selenium.remote.tracing.Span;48import org.openqa.selenium.remote.tracing.Status;49import org.openqa.selenium.remote.tracing.Tracer;50import org.openqa.selenium.support.ui.FluentWait;51import org.openqa.selenium.support.ui.Wait;52import java.io.IOException;53import java.io.UncheckedIOException;54import java.net.MalformedURLException;55import java.net.URI;56import java.net.URL;57import java.nio.charset.Charset;58import java.nio.file.Files;59import java.nio.file.Paths;60import java.time.Duration;61import java.time.Instant;62import java.util.Arrays;63import java.util.Collections;64import java.util.HashMap;65import java.util.Map;66import java.util.Objects;67import java.util.Optional;68import java.util.TimeZone;69import java.util.TreeMap;70import java.util.logging.Level;71import java.util.logging.Logger;72public class SauceDockerSessionFactory implements SessionFactory {73 private static final Logger LOG = Logger.getLogger(SauceDockerSessionFactory.class.getName());74 private final Tracer tracer;75 private final HttpClient.Factory clientFactory;76 private final Docker docker;77 private final URI dockerUri;78 private final Image browserImage;79 private final Capabilities stereotype;80 private final Image videoImage;81 private final Image assetsUploaderImage;82 private final DockerAssetsPath assetsPath;83 private final String networkName;84 private final boolean runningInDocker;85 public SauceDockerSessionFactory(86 Tracer tracer,87 HttpClient.Factory clientFactory,88 Docker docker,89 URI dockerUri,90 Image browserImage,91 Capabilities stereotype,92 Image videoImage,93 Image assetsUploaderImage,94 DockerAssetsPath assetsPath,95 String networkName,96 boolean runningInDocker) {97 this.tracer = Require.nonNull("Tracer", tracer);98 this.clientFactory = Require.nonNull("HTTP client", clientFactory);99 this.docker = Require.nonNull("Docker command", docker);100 this.dockerUri = Require.nonNull("Docker URI", dockerUri);101 this.browserImage = Require.nonNull("Docker browser image", browserImage);102 this.networkName = Require.nonNull("Docker network name", networkName);103 this.stereotype = copyOf(Require.nonNull("Stereotype", stereotype));104 this.videoImage = videoImage;105 this.assetsUploaderImage = assetsUploaderImage;106 this.assetsPath = assetsPath;107 this.runningInDocker = runningInDocker;108 }109 @Override110 public boolean test(Capabilities capabilities) {111 return stereotype.getCapabilityNames().stream()112 .map(113 name ->114 Objects.equals(stereotype.getCapability(name), capabilities.getCapability(name)))115 .reduce(Boolean::logicalAnd)116 .orElse(false);117 }118 @Override119 public Either<WebDriverException, ActiveSession> apply(CreateSessionRequest sessionRequest) {120 Optional<Object> accessKey =121 getSauceCapability(sessionRequest.getDesiredCapabilities(), "accessKey");122 Optional<Object> userName =123 getSauceCapability(sessionRequest.getDesiredCapabilities(), "username");124 if (!accessKey.isPresent() && !userName.isPresent()) {125 String message = String.format("Unable to create session. No Sauce Labs accessKey and "126 + "username were found in the '%s' block.", SAUCE_OPTIONS);127 LOG.log(Level.WARNING, message);128 return Either.left(new SessionNotCreatedException(message));129 }130 @SuppressWarnings("OptionalGetWithoutIsPresent")131 UsernameAndPassword usernameAndPassword =132 new UsernameAndPassword(userName.get().toString(), accessKey.get().toString());133 Optional<Object> dc =134 getSauceCapability(sessionRequest.getDesiredCapabilities(), "dataCenter");135 DataCenter dataCenter = DataCenter.US_WEST;136 if (dc.isPresent()) {137 dataCenter = DataCenter.fromString(String.valueOf(dc.get()));138 }139 Capabilities sessionReqCaps = removeSauceKey(sessionRequest.getDesiredCapabilities());140 LOG.info("Starting session for " + sessionReqCaps);141 int port = runningInDocker ? 4444 : PortProber.findFreePort();142 try (Span span = tracer.getCurrentContext().createSpan("docker_session_factory.apply")) {143 Map<String, EventAttributeValue> attributeMap = new HashMap<>();144 attributeMap.put(AttributeKey.LOGGER_CLASS.getKey(),145 EventAttribute.setValue(this.getClass().getName()));146 String logMessage = runningInDocker ? "Creating container..." :147 "Creating container, mapping container port 4444 to " + port;148 LOG.info(logMessage);149 Container container = createBrowserContainer(port, sessionReqCaps);150 container.start();151 ContainerInfo containerInfo = container.inspect();152 String containerIp = containerInfo.getIp();153 URL remoteAddress = getUrl(port, containerIp);154 HttpClient client = clientFactory.createClient(remoteAddress);155 attributeMap.put("docker.browser.image", EventAttribute.setValue(browserImage.toString()));156 attributeMap.put("container.port", EventAttribute.setValue(port));157 attributeMap.put("container.id", EventAttribute.setValue(container.getId().toString()));158 attributeMap.put("container.ip", EventAttribute.setValue(containerInfo.getIp()));159 attributeMap.put("docker.server.url", EventAttribute.setValue(remoteAddress.toString()));160 LOG.info(161 String.format("Waiting for server to start (container id: %s, url %s)",162 container.getId(),163 remoteAddress));164 try {165 waitForServerToStart(client, Duration.ofMinutes(1));166 } catch (TimeoutException e) {167 span.setAttribute("error", true);168 span.setStatus(Status.CANCELLED);169 EXCEPTION.accept(attributeMap, e);170 attributeMap.put(AttributeKey.EXCEPTION_MESSAGE.getKey(),171 EventAttribute.setValue(172 "Unable to connect to docker server. Stopping container: " +173 e.getMessage()));174 span.addEvent(AttributeKey.EXCEPTION_EVENT.getKey(), attributeMap);175 container.stop(Duration.ofMinutes(1));176 String message = String.format(177 "Unable to connect to docker server (container id: %s)", container.getId());178 LOG.warning(message);179 return Either.left(new RetrySessionRequestException(message));180 }181 LOG.info(String.format("Server is ready (container id: %s)", container.getId()));182 Command command = new Command(183 null,184 DriverCommand.NEW_SESSION(sessionReqCaps));185 ProtocolHandshake.Result result;186 Response response;187 try {188 result = new ProtocolHandshake().createSession(client, command);189 response = result.createResponse();190 attributeMap.put(191 AttributeKey.DRIVER_RESPONSE.getKey(),192 EventAttribute.setValue(response.toString()));193 } catch (IOException | RuntimeException e) {194 span.setAttribute("error", true);195 span.setStatus(Status.CANCELLED);196 EXCEPTION.accept(attributeMap, e);197 attributeMap.put(198 AttributeKey.EXCEPTION_MESSAGE.getKey(),199 EventAttribute200 .setValue("Unable to create session. Stopping and container: " + e.getMessage()));201 span.addEvent(AttributeKey.EXCEPTION_EVENT.getKey(), attributeMap);202 container.stop(Duration.ofMinutes(1));203 String message = "Unable to create session: " + e.getMessage();204 LOG.log(Level.WARNING, message);205 return Either.left(new SessionNotCreatedException(message));206 }207 SessionId id = new SessionId(response.getSessionId());208 Capabilities capabilities = new ImmutableCapabilities((Map<?, ?>) response.getValue());209 Capabilities mergedCapabilities = capabilities.merge(sessionReqCaps);210 Container videoContainer = null;211 Optional<DockerAssetsPath> path = ofNullable(this.assetsPath);212 if (path.isPresent()) {213 // Seems we can store session assets214 String containerPath = path.get().getContainerPath(id);215 saveSessionCapabilities(mergedCapabilities, containerPath);216 String hostPath = path.get().getHostPath(id);217 videoContainer = startVideoContainer(mergedCapabilities, containerInfo.getIp(), hostPath);218 }219 Instant startTime = Instant.now();220 Instant videoStartTime = Instant.now();221 Dialect downstream = sessionRequest.getDownstreamDialects().contains(result.getDialect()) ?222 result.getDialect() :223 W3C;224 attributeMap.put(225 AttributeKey.DOWNSTREAM_DIALECT.getKey(),226 EventAttribute.setValue(downstream.toString()));227 attributeMap.put(228 AttributeKey.DRIVER_RESPONSE.getKey(),229 EventAttribute.setValue(response.toString()));230 SauceCommandInfo commandInfo = new SauceCommandInfo.Builder()231 .setStartTime(startTime.toEpochMilli())232 .setVideoStartTime(videoStartTime.toEpochMilli())233 .setEndTime(Instant.now().toEpochMilli())234 .setRequest(sessionReqCaps)235 .setResult(mergedCapabilities)236 .setPath("/session")237 .setHttpStatus(response.getStatus())238 .setHttpMethod(POST.name())239 .setStatusCode(0)240 .setScreenshotId(-1)241 .build();242 span.addEvent("Docker driver service created session", attributeMap);243 LOG.fine(String.format(244 "Created session: %s - %s (container id: %s)",245 id,246 capabilities,247 container.getId()));248 return Either.right(new SauceDockerSession(249 container,250 videoContainer,251 tracer,252 client,253 id,254 remoteAddress,255 stereotype,256 mergedCapabilities,257 downstream,258 result.getDialect(),259 startTime,260 assetsPath,261 usernameAndPassword,262 dataCenter,263 assetsUploaderImage,264 commandInfo,265 docker));266 }267 }268 private Container createBrowserContainer(int port, Capabilities sessionCapabilities) {269 Map<String, String> browserContainerEnvVars = getBrowserContainerEnvVars(sessionCapabilities);270 long browserContainerShmMemorySize = 2147483648L; //2GB271 ContainerConfig containerConfig = image(browserImage)272 .env(browserContainerEnvVars)273 .shmMemorySize(browserContainerShmMemorySize)274 .network(networkName);275 if (!runningInDocker) {276 containerConfig = containerConfig.map(Port.tcp(4444), Port.tcp(port));277 }278 return docker.create(containerConfig);279 }280 private Map<String, String> getBrowserContainerEnvVars(Capabilities sessionRequestCapabilities) {281 Optional<Dimension> screenResolution =282 ofNullable(getScreenResolution(sessionRequestCapabilities));283 Map<String, String> envVars = new HashMap<>();284 if (screenResolution.isPresent()) {285 envVars.put("SCREEN_WIDTH", String.valueOf(screenResolution.get().getWidth()));286 envVars.put("SCREEN_HEIGHT", String.valueOf(screenResolution.get().getHeight()));287 }288 Optional<TimeZone> timeZone = ofNullable(getTimeZone(sessionRequestCapabilities));289 timeZone.ifPresent(zone -> envVars.put("TZ", zone.getID()));290 return envVars;291 }292 private Container startVideoContainer(Capabilities sessionCapabilities,293 String browserContainerIp, String hostPath) {294 if (!recordVideoForSession(sessionCapabilities)) {295 return null;296 }297 int videoPort = 9000;298 Map<String, String> envVars = getVideoContainerEnvVars(299 sessionCapabilities,300 browserContainerIp);301 Map<String, String> volumeBinds = Collections.singletonMap(hostPath, "/videos");302 ContainerConfig containerConfig = image(videoImage)303 .env(envVars)304 .bind(volumeBinds)305 .network(networkName);306 if (!runningInDocker) {307 videoPort = PortProber.findFreePort();308 containerConfig = containerConfig.map(Port.tcp(9000), Port.tcp(videoPort));309 }310 Container videoContainer = docker.create(containerConfig);311 videoContainer.start();312 String videoContainerIp = runningInDocker ? videoContainer.inspect().getIp() : "localhost";313 try {314 URL videoContainerUrl = new URL(String.format("http://%s:%s", videoContainerIp, videoPort));315 HttpClient videoClient = clientFactory.createClient(videoContainerUrl);316 LOG.fine(String.format("Waiting for video recording... (id: %s)", videoContainer.getId()));...