Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add a possibility to connect to a running session #1813

Merged
merged 4 commits into from
Dec 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
}
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