diff --git a/build.gradle b/build.gradle index aeffacd..d72797f 100644 --- a/build.gradle +++ b/build.gradle @@ -5,6 +5,15 @@ plugins { group = 'com.zebrunner' version = "${version != 'unspecified' ? version : '1.7.0'}" +sourceSets.all { + configurations.getByName(runtimeClasspathConfigurationName) { + attributes.attribute(Attribute.of("org.gradle.jvm.environment", String), "standard-jvm") + } + configurations.getByName(compileClasspathConfigurationName) { + attributes.attribute(Attribute.of("org.gradle.jvm.environment", String), "standard-jvm") + } +} + repositories { mavenCentral() } @@ -13,8 +22,8 @@ dependencies { compileOnly('com.konghq:unirest-java:3.13.10') implementation('org.yaml:snakeyaml:1.30') - implementation("net.bytebuddy:byte-buddy:1.12.18") - compileOnly('io.appium:java-client:8.3.0') + implementation("net.bytebuddy:byte-buddy:1.14.11") + compileOnly('org.seleniumhq.selenium:selenium-remote-driver:4.17.0') implementation('org.slf4j:slf4j-api:1.7.36') compileOnly("log4j:log4j:1.2.17") diff --git a/src/main/java/com/zebrunner/agent/core/webdriver/DriverSessionsAgent.java b/src/main/java/com/zebrunner/agent/core/webdriver/DriverSessionsAgent.java index c05cf24..ae5a203 100644 --- a/src/main/java/com/zebrunner/agent/core/webdriver/DriverSessionsAgent.java +++ b/src/main/java/com/zebrunner/agent/core/webdriver/DriverSessionsAgent.java @@ -2,23 +2,36 @@ import lombok.extern.slf4j.Slf4j; import net.bytebuddy.agent.builder.AgentBuilder; +import net.bytebuddy.asm.Advice; import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.implementation.MethodDelegation; +import net.bytebuddy.implementation.bind.annotation.FieldProxy; +import net.bytebuddy.implementation.bind.annotation.Morph; import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.NameMatcher; import net.bytebuddy.pool.TypePool; +import org.openqa.selenium.remote.CommandInfo; +import org.openqa.selenium.remote.HttpCommandExecutor; +import org.openqa.selenium.remote.http.ClientConfig; +import org.openqa.selenium.remote.http.HttpClient; import java.lang.instrument.Instrumentation; +import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.HashSet; +import java.util.Map; import java.util.Set; import static net.bytebuddy.implementation.MethodDelegation.to; +import static net.bytebuddy.matcher.ElementMatchers.any; +import static net.bytebuddy.matcher.ElementMatchers.isConstructor; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.isStatic; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; @Slf4j public class DriverSessionsAgent { @@ -54,6 +67,13 @@ public static void premain(String args, Instrumentation instrumentation) { .transform((builder, type, classloader, module, protectionDomain) -> builder.method(named(START_SESSION_METHOD_MAME)) .intercept(to(startSessionInterceptor()))) + .type(named("org.openqa.selenium.remote.HttpCommandExecutor")) + .transform((builder, typeDescription, classLoader, module, protectionDomain) -> builder. + defineConstructor(Modifier.PUBLIC) + .withParameters(Map.class, ClientConfig.class, HttpClient.Factory.class) + .intercept(MethodDelegation.withDefaultConfiguration().withBinders( + Morph.Binder.install(HttpCommandExecutor.class) + ).to(HttpCommandExecutorInterceptor.class))) .installOn(instrumentation); } catch (Exception e) { log.error("Could not add interceptors for RemoteWebDriver", e); @@ -68,29 +88,29 @@ public static ElementMatcher isPublicMethodToIntercep private static DynamicType.Builder addInterceptors(DynamicType.Builder builder) { return builder.method(isPublicMethodToIntercept()) - .intercept(to(publicMethodsInterceptor())) - .method(named(START_SESSION_METHOD_MAME)) - .intercept(to(startSessionInterceptor())) - .method(named(QUIT_METHOD_MAME)) - .intercept(to(quitSessionInterceptor())); + .intercept(to(publicMethodsInterceptor())) + .method(named(START_SESSION_METHOD_MAME)) + .intercept(to(startSessionInterceptor())) + .method(named(QUIT_METHOD_MAME)) + .intercept(to(quitSessionInterceptor())); } private static TypeDescription publicMethodsInterceptor() { return TypePool.Default.ofSystemLoader() - .describe(PublicMethodInvocationInterceptor.class.getName()) - .resolve(); + .describe(PublicMethodInvocationInterceptor.class.getName()) + .resolve(); } private static TypeDescription startSessionInterceptor() { return TypePool.Default.ofSystemLoader() - .describe(StartSessionInterceptor.class.getName()) - .resolve(); + .describe(StartSessionInterceptor.class.getName()) + .resolve(); } private static TypeDescription quitSessionInterceptor() { return TypePool.Default.ofSystemLoader() - .describe(QuitSessionInterceptor.class.getName()) - .resolve(); + .describe(QuitSessionInterceptor.class.getName()) + .resolve(); } } diff --git a/src/main/java/com/zebrunner/agent/core/webdriver/HttpCommandExecutorInterceptor.java b/src/main/java/com/zebrunner/agent/core/webdriver/HttpCommandExecutorInterceptor.java new file mode 100644 index 0000000..d49007c --- /dev/null +++ b/src/main/java/com/zebrunner/agent/core/webdriver/HttpCommandExecutorInterceptor.java @@ -0,0 +1,50 @@ +package com.zebrunner.agent.core.webdriver; + +import com.zebrunner.agent.core.config.ConfigurationHolder; +import lombok.extern.slf4j.Slf4j; +import net.bytebuddy.implementation.bind.annotation.AllArguments; +import net.bytebuddy.implementation.bind.annotation.Morph; +import net.bytebuddy.implementation.bind.annotation.Origin; +import net.bytebuddy.implementation.bind.annotation.RuntimeType; +import net.bytebuddy.implementation.bind.annotation.This; +import org.openqa.selenium.UsernameAndPassword; +import org.openqa.selenium.remote.http.ClientConfig; + +import java.lang.reflect.Constructor; +import java.net.URL; +import java.util.concurrent.Callable; + +@Slf4j +public class HttpCommandExecutorInterceptor { + + @RuntimeType + public static Object constructor(@This Object inst, @Morph Callable callable, + @AllArguments Object[] allArguments) throws Exception { + if (ConfigurationHolder.shouldSubstituteRemoteWebDrivers()) { + log.debug("Selenium Hub URL will be substituted by the value provided from Zebrunner."); + int index = -1; + for (int i = 0; i < allArguments.length; i++) { + if (allArguments[i] instanceof ClientConfig) { + index = i; + break; + } + } + if (index >= 0) { + ClientConfig config = (ClientConfig) allArguments[index]; + URL seleniumHubUrl = RemoteWebDriverFactory.getSeleniumHubUrl(); + String userInfo = seleniumHubUrl.getUserInfo(); + if (userInfo != null && !userInfo.isEmpty()) { + String[] credentials = userInfo.split(":", 2); + String username = credentials[0]; + String password = credentials.length > 1 ? credentials[1] : ""; + config = config.authenticateAs(new UsernameAndPassword(username, password)); + } + config = config.baseUri(seleniumHubUrl.toURI()); + allArguments[index] = config; + } else { + log.warn("Could not find ClientConfig parameter in HttpCommandExecutor class."); + } + } + return callable.call(); + } +} diff --git a/src/main/java/com/zebrunner/agent/core/webdriver/StartSessionInterceptor.java b/src/main/java/com/zebrunner/agent/core/webdriver/StartSessionInterceptor.java index 89c4a66..ec1e7d9 100644 --- a/src/main/java/com/zebrunner/agent/core/webdriver/StartSessionInterceptor.java +++ b/src/main/java/com/zebrunner/agent/core/webdriver/StartSessionInterceptor.java @@ -9,17 +9,11 @@ import net.bytebuddy.implementation.bind.annotation.SuperCall; import net.bytebuddy.implementation.bind.annotation.This; import org.openqa.selenium.Capabilities; -import org.openqa.selenium.UsernameAndPassword; -import org.openqa.selenium.remote.HttpCommandExecutor; import org.openqa.selenium.remote.RemoteWebDriver; -import org.openqa.selenium.remote.http.ClientConfig; -import org.openqa.selenium.remote.http.netty.NettyClient; import java.io.PrintWriter; import java.io.StringWriter; import java.lang.reflect.Field; -import java.net.URISyntaxException; -import java.net.URL; import java.util.Arrays; @Slf4j @@ -33,7 +27,6 @@ public static void onSessionStart(@This RemoteWebDriver driver, @SuperCall Runnable methodInvocationProxy, @Argument(0) Capabilities capabilities) throws Exception { if (ConfigurationHolder.shouldSubstituteRemoteWebDrivers()) { - substituteSeleniumHub(driver); capabilities = customizeCapabilities(methodInvocationProxy, capabilities); } @@ -72,60 +65,6 @@ public static void onSessionStart(@This RemoteWebDriver driver, } } - private static void substituteSeleniumHub(RemoteWebDriver driver) throws NoSuchFieldException, IllegalAccessException, URISyntaxException { - URL seleniumHubUrl = RemoteWebDriverFactory.getSeleniumHubUrl(); - if (driver.getCommandExecutor() instanceof HttpCommandExecutor && seleniumHubUrl != null) { - log.debug("Selenium Hub URL will be substituted by the value provided from Zebrunner."); - - HttpCommandExecutor commandExecutor = (HttpCommandExecutor) driver.getCommandExecutor(); - setFieldValue(commandExecutor, "remoteServer", seleniumHubUrl); - - Object clientObject = getFieldValue(commandExecutor, "client"); - if (clientObject instanceof NettyClient) { - String userInfo = seleniumHubUrl.getUserInfo(); - if (userInfo != null && !userInfo.isEmpty()) { - String[] credentials = userInfo.split(":", 2); - String username = credentials[0]; - String password = credentials.length > 1 ? credentials[1] : ""; - - ClientConfig clientConfig = ((ClientConfig) getFieldValue(clientObject, "config")) - .baseUri(seleniumHubUrl.toURI()) - .authenticateAs(new UsernameAndPassword(username, password)); - setFieldValue(clientObject, "config", clientConfig); - } - } else { - log.debug("Could not substitute address of remote selenium hub because of unknown http client."); - } - } - } - - private static Object getFieldValue(Object targetObject, String fieldName) throws NoSuchFieldException, IllegalAccessException { - Field remoteServer = findField(targetObject.getClass(), fieldName); - remoteServer.setAccessible(true); - Object value = remoteServer.get(targetObject); - remoteServer.setAccessible(false); - return value; - } - - private static void setFieldValue(Object targetObject, String fieldName, Object value) throws NoSuchFieldException, IllegalAccessException { - Field remoteServer = findField(targetObject.getClass(), fieldName); - remoteServer.setAccessible(true); - remoteServer.set(targetObject, value); - remoteServer.setAccessible(false); - } - - private static Field findField(Class targetClass, String fieldName) throws NoSuchFieldException { - try { - return targetClass.getDeclaredField(fieldName); - } catch (NoSuchFieldException e) { - Class superclass = targetClass.getSuperclass(); - if (superclass != Object.class) { - return findField(superclass, fieldName); - } - throw e; - } - } - private static Capabilities customizeCapabilities(Runnable methodInvocationProxy, Capabilities capabilities) { Class methodInvocationProxyClass = methodInvocationProxy.getClass(); log.debug("Class of the #startSession() invocation proxy is {}", methodInvocationProxyClass.getName()); @@ -156,5 +95,4 @@ private static Capabilities customizeCapabilities(Runnable methodInvocationProxy return capabilities; } - }