Skip to content
Draft
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
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
org.gradle.daemon=true

selenium.version=4.42.0
selenium.version=4.50.0
# Please increment the value in a release
appiumClient.version=10.1.1
53 changes: 49 additions & 4 deletions src/main/java/io/appium/java_client/AppiumDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.bidi.BiDi;
import org.openqa.selenium.bidi.BiDiException;
import org.openqa.selenium.bidi.Handle;
import org.openqa.selenium.bidi.HasBiDi;
import org.openqa.selenium.remote.CapabilityType;
import org.openqa.selenium.remote.CommandInfo;
Expand Down Expand Up @@ -85,6 +86,8 @@ public class AppiumDriver extends RemoteWebDriver implements
private URI biDiUri;
private BiDi biDi;
private boolean wasBiDiRequested = false;
// Used for log websockets when the command executor is not an AppiumCommandExecutor.
private HttpClient ownHttpClient;

/**
* Creates a new instance based on command {@code executor} and {@code capabilities}.
Expand All @@ -98,7 +101,17 @@ public AppiumDriver(HttpCommandExecutor executor, Capabilities capabilities) {
super(executor, capabilities);
this.executeMethod = new AppiumExecutionMethod(this);
super.setErrorHandler(ERROR_HANDLER);
this.remoteAddress = executor.getAddressOfRemoteServer();
this.remoteAddress = getServerUrl(executor);
}

// Selenium deprecated getAddressOfRemoteServer() without a replacement, so it is only used
// for executors that are not Appium's.
@SuppressWarnings("removal")
private static URL getServerUrl(HttpCommandExecutor executor) {
if (executor instanceof AppiumCommandExecutor) {
return ((AppiumCommandExecutor) executor).getAppiumClientConfig().baseUrl();
}
return executor.getAddressOfRemoteServer();
}

public AppiumDriver(AppiumClientConfig clientConfig, Capabilities capabilities) {
Expand Down Expand Up @@ -174,7 +187,7 @@ public AppiumDriver(URL remoteSessionAddress, String platformName, String automa
setCommandExecutor(executor);
this.executeMethod = new AppiumExecutionMethod(this);
super.setErrorHandler(ERROR_HANDLER);
this.remoteAddress = executor.getAddressOfRemoteServer();
this.remoteAddress = executor.getAppiumClientConfig().baseUrl();

setSessionId(sessionAddress.getId());
}
Expand Down Expand Up @@ -265,11 +278,15 @@ public AppiumDriver markExtensionAbsence(String extName) {
return this;
}

// Selenium deprecated getBiDi() and maybeGetBiDi(), but has no replacement yet for listeners
// scoped to a browsing context (e.g. NATIVE_CONTEXT), so Appium keeps them for its users.
@SuppressWarnings("removal")
@Override
public Optional<BiDi> maybeGetBiDi() {
return Optional.ofNullable(this.biDi);
}

@SuppressWarnings("removal")
@Override
@NonNull
public BiDi getBiDi() {
Expand Down Expand Up @@ -299,8 +316,36 @@ public BiDi getBiDi() {
return this.biDi;
}

protected HttpClient getHttpClient() {
return ((HttpCommandExecutor) getCommandExecutor()).client;
@Override
public Handle getHandle() {
return getBiDi().asHandle();
}

protected synchronized HttpClient getHttpClient() {
var executor = getCommandExecutor();
if (executor instanceof AppiumCommandExecutor) {
return ((AppiumCommandExecutor) executor).getClient();
}
// Selenium does not expose the client of other executors, so create one, as BiDi does.
// Keep a strong reference: StringWebSocketClient only holds the client weakly.
if (ownHttpClient == null) {
ownHttpClient = HttpClient.Factory.createDefault().createClient(remoteAddress);
}
return ownHttpClient;
}

@Override
public void quit() {
try {
super.quit();
} finally {
synchronized (this) {
if (ownHttpClient != null) {
ownHttpClient.close();
ownHttpClient = null;
}
}
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public AndroidDriver(HttpClient.Factory httpClientFactory, Capabilities capabili

/**
* Creates a new instance based on the given ClientConfig and {@code capabilities}.
* The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}.
* The HTTP client is default client generated by {@link HttpClient.Factory#createDefault()}.
* For example:
*
* <pre>
Expand All @@ -200,7 +200,7 @@ public AndroidDriver(ClientConfig clientConfig, Capabilities capabilities) {

/**
* Creates a new instance based on the given ClientConfig and {@code capabilities}.
* The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}.
* The HTTP client is default client generated by {@link HttpClient.Factory#createDefault()}.
* For example:
*
* <pre>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

package io.appium.java_client.android;

import io.appium.java_client.AppiumDriver;
import io.appium.java_client.CommandExecutionHelper;
import io.appium.java_client.ExecutesMethod;
import io.appium.java_client.ws.StringWebSocketClient;
import org.openqa.selenium.remote.HttpCommandExecutor;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.remote.SessionId;

Expand Down Expand Up @@ -59,7 +59,7 @@ default void startLogcatBroadcast(String host) {
*/
default void startLogcatBroadcast(String host, int port) {
var remoteWebDriver = (RemoteWebDriver) this;
URL serverUrl = ((HttpCommandExecutor) remoteWebDriver.getCommandExecutor()).getAddressOfRemoteServer();
URL serverUrl = ((AppiumDriver) this).getRemoteAddress();
var scheme = "https".equals(serverUrl.getProtocol()) ? "wss" : "ws";
CommandExecutionHelper.executeScript(this, "mobile: startLogsBroadcast");
SessionId sessionId = remoteWebDriver.getSessionId();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ public ChromiumDriver(URL remoteSessionAddress, String platformName) {

/**
* Creates a new instance based on the given ClientConfig and {@code capabilities}.
* The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}.
* The HTTP client is default client generated by {@link HttpClient.Factory#createDefault()}.
* For example:
*
* <pre>
Expand All @@ -113,7 +113,7 @@ public ChromiumDriver(ClientConfig clientConfig, Capabilities capabilities) {

/**
* Creates a new instance based on the given ClientConfig and {@code capabilities}.
* The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}.
* The HTTP client is default client generated by {@link HttpClient.Factory#createDefault()}.
* For example:
*
* <pre>
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/io/appium/java_client/gecko/GeckoDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public GeckoDriver(URL remoteSessionAddress, String platformName) {

/**
* Creates a new instance based on the given ClientConfig and {@code capabilities}.
* The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}.
* The HTTP client is default client generated by {@link HttpClient.Factory#createDefault()}.
* For example:
*
* <pre>
Expand All @@ -114,7 +114,7 @@ public GeckoDriver(ClientConfig clientConfig, Capabilities capabilities) {

/**
* Creates a new instance based on the given ClientConfig and {@code capabilities}.
* The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}.
* The HTTP client is default client generated by {@link HttpClient.Factory#createDefault()}.
* For example:
*
* <pre>
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/io/appium/java_client/ios/IOSDriver.java
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ public IOSDriver(HttpClient.Factory httpClientFactory, Capabilities capabilities

/**
* Creates a new instance based on the given ClientConfig and {@code capabilities}.
* The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}.
* The HTTP client is default client generated by {@link HttpClient.Factory#createDefault()}.
* For example:
*
* <pre>
Expand All @@ -192,7 +192,7 @@ public IOSDriver(ClientConfig clientConfig, Capabilities capabilities) {

/**
* Creates a new instance based on the given ClientConfig and {@code capabilities}.
* The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}.
* The HTTP client is default client generated by {@link HttpClient.Factory#createDefault()}.
* For example:
*
* <pre>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

package io.appium.java_client.ios;

import io.appium.java_client.AppiumDriver;
import io.appium.java_client.CommandExecutionHelper;
import io.appium.java_client.ExecutesMethod;
import io.appium.java_client.ws.StringWebSocketClient;
import org.openqa.selenium.remote.HttpCommandExecutor;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.remote.SessionId;

Expand Down Expand Up @@ -60,7 +60,7 @@ default void startSyslogBroadcast(String host) {
*/
default void startSyslogBroadcast(String host, int port) {
var remoteWebDriver = (RemoteWebDriver) this;
URL serverUrl = ((HttpCommandExecutor) remoteWebDriver.getCommandExecutor()).getAddressOfRemoteServer();
URL serverUrl = ((AppiumDriver) this).getRemoteAddress();
var scheme = "https".equals(serverUrl.getProtocol()) ? "wss" : "ws";
CommandExecutionHelper.executeScript(this, "mobile: startLogsBroadcast");
SessionId sessionId = remoteWebDriver.getSessionId();
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/io/appium/java_client/mac/Mac2Driver.java
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ public Mac2Driver(URL remoteSessionAddress) {

/**
* Creates a new instance based on the given ClientConfig and {@code capabilities}.
* The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}.
* The HTTP client is default client generated by {@link HttpClient.Factory#createDefault()}.
* For example:
*
* <pre>
Expand All @@ -125,7 +125,7 @@ public Mac2Driver(ClientConfig clientConfig, Capabilities capabilities) {

/**
* Creates a new instance based on the given ClientConfig and {@code capabilities}.
* The HTTP client is default client generated by {@link HttpCommandExecutor#getDefaultClientFactory}.
* The HTTP client is default client generated by {@link HttpClient.Factory#createDefault()}.
* For example:
*
* <pre>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ public class AppiumCommandExecutor extends HttpCommandExecutor {
private final Optional<DriverService> serviceOptional;
@Getter
private final AppiumClientConfig appiumClientConfig;
// HttpCommandExecutor no longer keeps the factory, but direct connect and BiDi need it.
private final Factory httpClientFactory;
// Lets direct connect point to a new server URL without changing HttpCommandExecutor.client.
private final SwitchableHttpClient switchableClient;

/**
* Create an AppiumCommandExecutor instance.
Expand All @@ -74,13 +78,31 @@ public AppiumCommandExecutor(
@Nullable DriverService service,
@Nullable Factory httpClientFactory,
AppiumClientConfig appiumClientConfig) {
super(additionalCommands,
appiumClientConfig,
ofNullable(httpClientFactory).orElseGet(HttpCommandExecutor::getDefaultClientFactory)
);
this(ofNullable(httpClientFactory).orElseGet(Factory::createDefault),
additionalCommands, service, appiumClientConfig);
}

private AppiumCommandExecutor(
Factory httpClientFactory,
Map<String, CommandInfo> additionalCommands,
@Nullable DriverService service,
AppiumClientConfig appiumClientConfig) {
this(new SwitchableHttpClient(httpClientFactory.createClient(appiumClientConfig)),
httpClientFactory, additionalCommands, service, appiumClientConfig);
}

private AppiumCommandExecutor(
SwitchableHttpClient switchableClient,
Factory httpClientFactory,
Map<String, CommandInfo> additionalCommands,
@Nullable DriverService service,
AppiumClientConfig appiumClientConfig) {
super(switchableClient, additionalCommands, appiumClientConfig.baseUrl());
serviceOptional = ofNullable(service);

this.appiumClientConfig = appiumClientConfig;
this.httpClientFactory = httpClientFactory;
this.switchableClient = switchableClient;
}

public AppiumCommandExecutor(Map<String, CommandInfo> additionalCommands, DriverService service,
Expand Down Expand Up @@ -148,14 +170,14 @@ public void setResponseCodec(ResponseCodec<HttpResponse> codec) {
this.responseCodec = codec;
}

protected HttpClient getClient() {
return this.client;
public HttpClient getClient() {
return this.switchableClient;
}

/**
* Override the http client in the HttpCommandExecutor class with a new http client instance with the given URL.
* It uses the same http client factory and client config for the new http client instance
* if the constructor got them.
* Switch the http client of this executor to a new http client instance with the given URL.
* It uses the same http client factory and client config for the new http client instance.
* The previous http client is closed.
*
* @param serverUrl URL to use for subsequent HTTP requests. Before switching clients, the host is
* resolved and the override is refused if any resolved address is loopback,
Expand All @@ -166,8 +188,7 @@ protected HttpClient getClient() {
*/
protected void overrideServerUrl(URL serverUrl) {
DirectConnectUrlSafety.requireSafeOverrideTarget(serverUrl);
HttpClient newClient = getHttpClientFactory().createClient(appiumClientConfig.baseUrl(serverUrl));
setPrivateFieldValue(HttpCommandExecutor.class, "client", newClient);
switchableClient.switchTo(getHttpClientFactory().createClient(appiumClientConfig.baseUrl(serverUrl)));
}

private Response createSession(Command command) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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.remote;

import org.jspecify.annotations.NullMarked;
import org.openqa.selenium.remote.http.HttpClient;
import org.openqa.selenium.remote.http.HttpRequest;
import org.openqa.selenium.remote.http.HttpResponse;
import org.openqa.selenium.remote.http.WebSocket;

import java.util.concurrent.CompletableFuture;

import static java.util.Objects.requireNonNull;

/**
* An {@link HttpClient} that forwards every call to another client, which can be replaced.
* Direct connect uses it to point the executor to a new server URL without touching the
* client field inside {@link org.openqa.selenium.remote.HttpCommandExecutor}.
*/
@NullMarked
final class SwitchableHttpClient implements HttpClient {

private volatile HttpClient delegate;

SwitchableHttpClient(HttpClient delegate) {
this.delegate = requireNonNull(delegate);
}

/**
* Sends all later calls to the given client and closes the previous one.
*
* @param newDelegate the client to use from now on
*/
void switchTo(HttpClient newDelegate) {
HttpClient previous = this.delegate;
this.delegate = requireNonNull(newDelegate);
previous.close();
}

@Override
public HttpResponse execute(HttpRequest req) {
return delegate.execute(req);
}

@Override
public CompletableFuture<HttpResponse> executeAsync(HttpRequest req) {
return delegate.executeAsync(req);
}

@Override
public WebSocket openSocket(HttpRequest request, WebSocket.Listener listener) {
return delegate.openSocket(request, listener);
}

@Override
public void close() {
delegate.close();
}
}
Loading
Loading