Skip to content

Commit

Permalink
feat: Add a possibility to connect to a running session (#1813)
Browse files Browse the repository at this point in the history
  • Loading branch information
mykola-mokhnach authored Dec 5, 2022
1 parent 7a98b69 commit f575ee4
Show file tree
Hide file tree
Showing 13 changed files with 292 additions and 51 deletions.
52 changes: 44 additions & 8 deletions src/main/java/io/appium/java_client/AppiumDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@

package io.appium.java_client;

import com.google.common.collect.ImmutableMap;
import io.appium.java_client.internal.CapabilityHelpers;
import io.appium.java_client.internal.ReflectionHelpers;
import io.appium.java_client.internal.SessionHelpers;
import io.appium.java_client.remote.AppiumCommandExecutor;
import io.appium.java_client.remote.AppiumNewSessionCommandPayload;
import io.appium.java_client.remote.AppiumW3CHttpCommandCodec;
import io.appium.java_client.remote.MobileCapabilityType;
import io.appium.java_client.remote.options.BaseOptions;
import io.appium.java_client.service.local.AppiumDriverLocalService;
Expand All @@ -36,11 +40,11 @@
import org.openqa.selenium.remote.HttpCommandExecutor;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.remote.Response;
import org.openqa.selenium.remote.codec.w3c.W3CHttpResponseCodec;
import org.openqa.selenium.remote.html5.RemoteLocationContext;
import org.openqa.selenium.remote.http.HttpClient;
import org.openqa.selenium.remote.http.HttpMethod;

import java.lang.reflect.Field;
import java.net.URL;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -128,6 +132,42 @@ public AppiumDriver(Capabilities capabilities) {
this(AppiumDriverLocalService.buildDefaultService(), capabilities);
}

/**
* This is a special constructor used to connect to a running driver instance.
* It does not do any necessary verifications, but rather assumes the given
* driver session is already running at `remoteSessionAddress`.
* The maintenance of driver state(s) is the caller's responsibility.
* !!! This API is supposed to be used for **debugging purposes only**.
*
* @param remoteSessionAddress The address of the **running** session including the session identifier.
* @param platformName The name of the target platform.
* @param automationName The name of the target automation.
*/
public AppiumDriver(URL remoteSessionAddress, String platformName, String automationName) {
super();
ReflectionHelpers.setPrivateFieldValue(
RemoteWebDriver.class, this, "capabilities", new ImmutableCapabilities(
ImmutableMap.of(
PLATFORM_NAME, platformName,
APPIUM_PREFIX + AUTOMATION_NAME, automationName
)
)
);
SessionHelpers.SessionAddress sessionAddress = SessionHelpers.parseSessionAddress(remoteSessionAddress);
AppiumCommandExecutor executor = new AppiumCommandExecutor(
MobileCommand.commandRepository, sessionAddress.getServerUrl()
);
executor.setCommandCodec(new AppiumW3CHttpCommandCodec());
executor.setResponseCodec(new W3CHttpResponseCodec());
setCommandExecutor(executor);
this.executeMethod = new AppiumExecutionMethod(this);
locationContext = new RemoteLocationContext(executeMethod);
super.setErrorHandler(errorHandler);
this.remoteAddress = executor.getAddressOfRemoteServer();

setSessionId(sessionAddress.getId());
}

/**
* Changes platform name if it is not set and returns merged capabilities.
*
Expand Down Expand Up @@ -252,13 +292,9 @@ && isBlank((String) rawCapabilities.get(CapabilityType.BROWSER_NAME))) {
rawCapabilities.remove(CapabilityType.BROWSER_NAME);
}
MutableCapabilities returnedCapabilities = new BaseOptions<>(rawCapabilities);
try {
Field capsField = RemoteWebDriver.class.getDeclaredField("capabilities");
capsField.setAccessible(true);
capsField.set(this, returnedCapabilities);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new WebDriverException(e);
}
ReflectionHelpers.setPrivateFieldValue(
RemoteWebDriver.class, this, "capabilities", returnedCapabilities
);
setSessionId(response.getSessionId());
}

Expand Down
18 changes: 3 additions & 15 deletions src/main/java/io/appium/java_client/AppiumFluentWait.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@
package io.appium.java_client;

import com.google.common.base.Throwables;
import io.appium.java_client.internal.ReflectionHelpers;
import lombok.AccessLevel;
import lombok.Getter;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.support.ui.FluentWait;
import org.openqa.selenium.support.ui.Sleeper;

import java.lang.reflect.Field;
import java.time.Clock;
import java.time.Duration;
import java.time.Instant;
Expand Down Expand Up @@ -99,23 +99,11 @@ public AppiumFluentWait(T input, Clock clock, Sleeper sleeper) {
}

private <B> B getPrivateFieldValue(String fieldName, Class<B> fieldType) {
try {
final Field f = getClass().getSuperclass().getDeclaredField(fieldName);
f.setAccessible(true);
return fieldType.cast(f.get(this));
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new WebDriverException(e);
}
return ReflectionHelpers.getPrivateFieldValue(FluentWait.class, this, fieldName, fieldType);
}

private Object getPrivateFieldValue(String fieldName) {
try {
final Field f = getClass().getSuperclass().getDeclaredField(fieldName);
f.setAccessible(true);
return f.get(this);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new WebDriverException(e);
}
return getPrivateFieldValue(fieldName, Object.class);
}

protected Clock getClock() {
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/io/appium/java_client/android/AndroidDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,20 @@ public AndroidDriver(Capabilities capabilities) {
super(ensurePlatformName(capabilities, ANDROID_PLATFORM));
}

/**
* This is a special constructor used to connect to a running driver instance.
* It does not do any necessary verifications, but rather assumes the given
* driver session is already running at `remoteSessionAddress`.
* The maintenance of driver state(s) is the caller's responsibility.
* !!! This API is supposed to be used for **debugging purposes only**.
*
* @param remoteSessionAddress The address of the **running** session including the session identifier.
* @param automationName The name of the target automation.
*/
public AndroidDriver(URL remoteSessionAddress, String automationName) {
super(remoteSessionAddress, ANDROID_PLATFORM, automationName);
}

/**
* Get test-coverage data.
*
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/io/appium/java_client/gecko/GeckoDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ public GeckoDriver(HttpClient.Factory httpClientFactory, Capabilities capabiliti
super(httpClientFactory, ensureAutomationName(capabilities, AUTOMATION_NAME));
}

/**
* This is a special constructor used to connect to a running driver instance.
* It does not do any necessary verifications, but rather assumes the given
* driver session is already running at `remoteSessionAddress`.
* The maintenance of driver state(s) is the caller's responsibility.
* !!! This API is supposed to be used for **debugging purposes only**.
*
* @param remoteSessionAddress The address of the **running** session including the session identifier.
* @param platformName The name of the target platform.
*/
public GeckoDriver(URL remoteSessionAddress, String platformName) {
super(remoteSessionAddress, platformName, AUTOMATION_NAME);
}

/**
* Creates a new instance based on the given ClientConfig and {@code capabilities}.
* The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.appium.java_client.internal;

import org.openqa.selenium.WebDriverException;

import java.lang.reflect.Field;

public class ReflectionHelpers {

/**
* Sets the given value to a private instance field.
*
* @param cls The target class or a superclass.
* @param target Target instance.
* @param fieldName Target field name.
* @param newValue The value to be set.
* @return The same instance for chaining.
*/
public static <T> T setPrivateFieldValue(Class<?> cls, T target, String fieldName, Object newValue) {
try {
final Field f = cls.getDeclaredField(fieldName);
f.setAccessible(true);
f.set(target, newValue);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new WebDriverException(e);
}
return target;
}

/**
* Fetches the value of a private instance field.
*
* @param cls The target class or a superclass.
* @param target Target instance.
* @param fieldName Target field name.
* @param fieldType Field type.
* @return The retrieved field value.
*/
public static <T> T getPrivateFieldValue(Class<?> cls, Object target, String fieldName, Class<T> fieldType) {
try {
final Field f = cls.getDeclaredField(fieldName);
f.setAccessible(true);
return fieldType.cast(f.get(target));
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new WebDriverException(e);
}
}
}
59 changes: 59 additions & 0 deletions src/main/java/io/appium/java_client/internal/SessionHelpers.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.appium.java_client.internal;

import lombok.Data;
import org.openqa.selenium.InvalidArgumentException;
import org.openqa.selenium.WebDriverException;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SessionHelpers {
private static final Pattern SESSION = Pattern.compile("/session/([^/]+)");

@Data public static class SessionAddress {
private final URL serverUrl;
private final String id;
}

/**
* Parses the address of a running remote session.
*
* @param address The address string containing /session/id suffix.
* @return Parsed address object.
* @throws InvalidArgumentException If no session identifier could be parsed.
*/
public static SessionAddress parseSessionAddress(URL address) {
String addressString = address.toString();
Matcher matcher = SESSION.matcher(addressString);
if (!matcher.find()) {
throw new InvalidArgumentException(
String.format("The server URL '%s' must include /session/<id> suffix", addressString)
);
}
try {
return new SessionAddress(
new URL(addressString.replace(matcher.group(), "")), matcher.group(1)
);
} catch (MalformedURLException e) {
throw new WebDriverException(e);
}
}
}
13 changes: 13 additions & 0 deletions src/main/java/io/appium/java_client/ios/IOSDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import io.appium.java_client.PushesFiles;
import io.appium.java_client.SupportsLegacyAppManagement;
import io.appium.java_client.battery.HasBattery;
import io.appium.java_client.remote.AutomationName;
import io.appium.java_client.remote.SupportsContextSwitching;
import io.appium.java_client.remote.SupportsLocation;
import io.appium.java_client.remote.SupportsRotation;
Expand Down Expand Up @@ -220,6 +221,18 @@ public IOSDriver(AppiumClientConfig appiumClientConfig, Capabilities capabilitie
super(appiumClientConfig, ensurePlatformName(capabilities, PLATFORM_NAME));
}

/**
* This is a special constructor used to connect to a running driver instance.
* It does not do any necessary verifications, but rather assumes the given
* driver session is already running at `remoteSessionAddress`.
* The maintenance of driver state(s) is the caller's responsibility.
* !!! This API is supposed to be used for **debugging purposes only**.
*
* @param remoteSessionAddress The address of the **running** session including the session identifier.
*/
public IOSDriver(URL remoteSessionAddress) {
super(remoteSessionAddress, PLATFORM_NAME, AutomationName.IOS_XCUI_TEST);
}

/**
* Creates a new instance based on {@code capabilities}.
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/io/appium/java_client/mac/Mac2Driver.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ public Mac2Driver(HttpClient.Factory httpClientFactory, Capabilities capabilitie
capabilities, PLATFORM_NAME, AUTOMATION_NAME));
}

/**
* This is a special constructor used to connect to a running driver instance.
* It does not do any necessary verifications, but rather assumes the given
* driver session is already running at `remoteSessionAddress`.
* The maintenance of driver state(s) is the caller's responsibility.
* !!! This API is supposed to be used for **debugging purposes only**.
*
* @param remoteSessionAddress The address of the **running** session including the session identifier.
*/
public Mac2Driver(URL remoteSessionAddress) {
super(remoteSessionAddress, PLATFORM_NAME, AUTOMATION_NAME);
}

/**
* Creates a new instance based on the given ClientConfig and {@code capabilities}.
* The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.google.common.net.HttpHeaders;
import io.appium.java_client.AppiumClientConfig;
import io.appium.java_client.AppiumUserAgentFilter;
import io.appium.java_client.internal.ReflectionHelpers;
import org.openqa.selenium.SessionNotCreatedException;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.remote.Command;
Expand All @@ -42,7 +43,6 @@
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.lang.reflect.Field;
import java.net.ConnectException;
import java.net.MalformedURLException;
import java.net.URL;
Expand Down Expand Up @@ -128,25 +128,13 @@ public AppiumCommandExecutor(Map<String, CommandInfo> additionalCommands,
@SuppressWarnings("SameParameterValue")
protected <B> B getPrivateFieldValue(
Class<? extends CommandExecutor> cls, String fieldName, Class<B> fieldType) {
try {
final Field f = cls.getDeclaredField(fieldName);
f.setAccessible(true);
return fieldType.cast(f.get(this));
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new WebDriverException(e);
}
return ReflectionHelpers.getPrivateFieldValue(cls, this, fieldName, fieldType);
}

@SuppressWarnings("SameParameterValue")
protected void setPrivateFieldValue(
Class<? extends CommandExecutor> cls, String fieldName, Object newValue) {
try {
final Field f = cls.getDeclaredField(fieldName);
f.setAccessible(true);
f.set(this, newValue);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new WebDriverException(e);
}
ReflectionHelpers.setPrivateFieldValue(cls, this, fieldName, newValue);
}

protected Map<String, CommandInfo> getAdditionalCommands() {
Expand All @@ -159,11 +147,11 @@ protected CommandCodec<HttpRequest> getCommandCodec() {
return getPrivateFieldValue(HttpCommandExecutor.class, "commandCodec", CommandCodec.class);
}

protected void setCommandCodec(CommandCodec<HttpRequest> newCodec) {
public void setCommandCodec(CommandCodec<HttpRequest> newCodec) {
setPrivateFieldValue(HttpCommandExecutor.class, "commandCodec", newCodec);
}

protected void setResponseCodec(ResponseCodec<HttpResponse> codec) {
public void setResponseCodec(ResponseCodec<HttpResponse> codec) {
setPrivateFieldValue(HttpCommandExecutor.class, "responseCodec", codec);
}

Expand Down
Loading

0 comments on commit f575ee4

Please sign in to comment.