From c9dd42ce1f2f9d0693b236616e4bf7c402399632 Mon Sep 17 00:00:00 2001 From: Ganesh Date: Fri, 30 Oct 2026 09:15:46 +0000 Subject: [PATCH] fix: default logcat broadcast to the connected Appium server startLogcatBroadcast() hardcoded 127.0.0.1:4723, so a remote server or another port never reached the logcat socket. The no-arg method now uses the command executor URL. https becomes wss, and an HTTP base path such as /wd/hub is not added because Appium serves the socket at the server root. The host and host/port overloads stay compatible. --- .../android/ListensToLogcatMessages.java | 65 +++++++-- .../android/ListensToLogcatMessagesTest.java | 133 ++++++++++++++++++ 2 files changed, 188 insertions(+), 10 deletions(-) create mode 100644 src/test/java/io/appium/java_client/android/ListensToLogcatMessagesTest.java diff --git a/src/main/java/io/appium/java_client/android/ListensToLogcatMessages.java b/src/main/java/io/appium/java_client/android/ListensToLogcatMessages.java index d3fcb5c7c..c51ac44d5 100644 --- a/src/main/java/io/appium/java_client/android/ListensToLogcatMessages.java +++ b/src/main/java/io/appium/java_client/android/ListensToLogcatMessages.java @@ -20,14 +20,15 @@ import io.appium.java_client.ExecutesMethod; import io.appium.java_client.remote.AppiumCommandExecutor; import io.appium.java_client.remote.AppiumRemoteWebDriver; -import io.appium.java_client.remote.SessionId; import io.appium.java_client.ws.StringWebSocketClient; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.util.function.Consumer; import static io.appium.java_client.service.local.AppiumServiceBuilder.DEFAULT_APPIUM_PORT; +import static java.util.Objects.requireNonNull; /** Provides listening to logcat messages broadcast via web socket. */ public interface ListensToLogcatMessages extends ExecutesMethod { @@ -40,16 +41,22 @@ public interface ListensToLogcatMessages extends ExecutesMethod { /** * Start logcat messages broadcast via web socket. - * This method assumes that Appium server is running on localhost and - * is assigned to the default port (4723). + * The host, port and scheme are taken from the Appium server this driver is connected to. + * {@code https} and {@code wss} become {@code wss}; any other scheme becomes {@code ws}. + * A port omitted from the server URL is omitted from the socket URI. + * Appium serves this socket at the server root + * ({@code /ws/session/.../appium/device/logcat}), so an HTTP base path such as + * {@code /wd/hub} is not added. */ default void startLogcatBroadcast() { - startLogcatBroadcast("127.0.0.1"); + URL serverUrl = remoteServerUrl(); + startLogcatBroadcast(serverUrl.getHost(), serverUrl.getPort()); } /** * Start logcat messages broadcast via web socket. * This method assumes that Appium server is assigned to the default port (4723). + * The scheme still follows the server this driver is connected to. * * @param host the name of the host where Appium server is running */ @@ -59,18 +66,56 @@ default void startLogcatBroadcast(String host) { /** * Start logcat messages broadcast via web socket. + * The scheme follows the server this driver is connected to. + * The socket path is the server-root logcat route; an HTTP base path is not added. * * @param host the name of the host where Appium server is running - * @param port the port of the host where Appium server is running + * @param port the port of the host where Appium server is running. + * A negative port means the scheme default and is omitted from the URI. */ default void startLogcatBroadcast(String host, int port) { var remoteWebDriver = (AppiumRemoteWebDriver) this; - URL serverUrl = ((AppiumCommandExecutor) remoteWebDriver.getCommandExecutor()).getAddressOfRemoteServer(); - var scheme = "https".equals(serverUrl.getProtocol()) ? "wss" : "ws"; + URI endpoint = logcatEndpoint(remoteServerUrl(), host, port, + String.valueOf(remoteWebDriver.getSessionId())); CommandExecutionHelper.executeScript(this, "mobile: startLogsBroadcast"); - SessionId sessionId = remoteWebDriver.getSessionId(); - var endpoint = String.format("%s://%s:%s/ws/session/%s/appium/device/logcat", scheme, host, port, sessionId); - getLogcatClient().connect(URI.create(endpoint)); + getLogcatClient().connect(endpoint); + } + + /** + * Returns the Appium server URL commands are sent to. + * + * @return the remote server URL + */ + private URL remoteServerUrl() { + var remoteWebDriver = (AppiumRemoteWebDriver) this; + URL serverUrl = ((AppiumCommandExecutor) remoteWebDriver.getCommandExecutor()).getAddressOfRemoteServer(); + if (serverUrl == null) { + throw new IllegalArgumentException("The Appium server address is not set"); + } + return serverUrl; + } + + /** + * Builds the logcat web socket URI. + * The socket is registered at the server root, so {@code serverUrl}'s HTTP base path is not included. + * + * @param serverUrl the Appium server the driver is connected to. Only its scheme is used. + * @param host the socket host + * @param port the socket port, or a negative value to omit the port + * @param sessionId the current session id + * @return the absolute web socket URI + */ + private static URI logcatEndpoint(URL serverUrl, String host, int port, String sessionId) { + try { + return new URI(websocketScheme(serverUrl.getProtocol()), null, requireNonNull(host, "Host"), port, + "/ws/session/" + sessionId + "/appium/device/logcat", null, null); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Cannot build the logcat web socket endpoint", e); + } + } + + private static String websocketScheme(String protocol) { + return "https".equalsIgnoreCase(protocol) || "wss".equalsIgnoreCase(protocol) ? "wss" : "ws"; } /** diff --git a/src/test/java/io/appium/java_client/android/ListensToLogcatMessagesTest.java b/src/test/java/io/appium/java_client/android/ListensToLogcatMessagesTest.java new file mode 100644 index 000000000..d7fec811f --- /dev/null +++ b/src/test/java/io/appium/java_client/android/ListensToLogcatMessagesTest.java @@ -0,0 +1,133 @@ +/* + * 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.android; + +import io.appium.java_client.android.options.UiAutomator2Options; +import io.appium.java_client.http.Contents; +import io.appium.java_client.http.HttpClient; +import io.appium.java_client.http.HttpRequest; +import io.appium.java_client.http.HttpResponse; +import io.appium.java_client.http.Message; +import io.appium.java_client.http.WebSocket; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.net.URI; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Checks the logcat socket URL without a device. The HTTP client is scripted. + */ +class ListensToLogcatMessagesTest { + private static final String SESSION_RESPONSE = "{\"value\":{\"sessionId\":\"s1\",\"capabilities\":" + + "{\"platformName\":\"Android\",\"appium:automationName\":\"UiAutomator2\"}}}"; + + @ParameterizedTest(name = "[{index}] {0}") + @CsvSource({ + "http://127.0.0.1:4723, ws://127.0.0.1:4723/ws/session/s1/appium/device/logcat", + "http://10.0.0.8:5000, ws://10.0.0.8:5000/ws/session/s1/appium/device/logcat", + "http://10.0.0.8:5000/, ws://10.0.0.8:5000/ws/session/s1/appium/device/logcat", + "http://10.0.0.8:5000/wd/hub, ws://10.0.0.8:5000/ws/session/s1/appium/device/logcat", + "http://127.0.0.1:4723/wd/hub/, ws://127.0.0.1:4723/ws/session/s1/appium/device/logcat", + "https://appium.example.com/wd/hub, wss://appium.example.com/ws/session/s1/appium/device/logcat", + "https://appium.example.com:8443/custom/, wss://appium.example.com:8443/ws/session/s1/appium/device/logcat", + "http://[::1]:4723/wd/hub, ws://[::1]:4723/ws/session/s1/appium/device/logcat" + }) + void noArgUsesTheConnectedServer(String serverUrl, String expectedSocket) throws Exception { + List events = broadcast(serverUrl, AndroidDriver::startLogcatBroadcast); + assertEquals(List.of( + "POST /session/s1/execute/sync", + "WS " + expectedSocket + ), events); + } + + @Test + void explicitHostKeepsDefaultPortAndServerScheme() throws Exception { + List events = broadcast("https://appium.example.com:8443/wd/hub", + driver -> driver.startLogcatBroadcast("logs.internal")); + assertEquals(List.of( + "POST /session/s1/execute/sync", + "WS wss://logs.internal:4723/ws/session/s1/appium/device/logcat" + ), events); + } + + @Test + void explicitHostAndPortStayOnTheGivenEndpoint() throws Exception { + List events = broadcast("http://10.0.0.8:5000/wd/hub/", + driver -> driver.startLogcatBroadcast("logs.internal", 9000)); + assertEquals(List.of( + "POST /session/s1/execute/sync", + "WS ws://logs.internal:9000/ws/session/s1/appium/device/logcat" + ), events); + } + + private static List broadcast(String serverUrl, Consumer action) throws Exception { + ScriptedServer server = new ScriptedServer(); + URL remoteAddress = URI.create(serverUrl).toURL(); + AndroidDriver driver = new AndroidDriver(remoteAddress, server, new UiAutomator2Options().setDeviceName("emu")); + try { + server.events.clear(); + action.accept(driver); + return List.copyOf(server.events); + } finally { + driver.quit(); + } + } + + /** Records HTTP and web socket calls and answers the session handshake. */ + private static final class ScriptedServer implements HttpClient.Factory { + private final List events = new ArrayList<>(); + + @Override + public HttpClient createClient(io.appium.java_client.http.ClientConfig config) { + return new HttpClient() { + @Override + public HttpResponse execute(HttpRequest request) { + events.add(request.getMethod() + " " + request.getUri()); + String body = "POST".equals(request.getMethod().name()) && "/session".equals(request.getUri()) + ? SESSION_RESPONSE : "{\"value\":null}"; + return new HttpResponse().setStatus(200) + .setHeader("Content-Type", "application/json; charset=utf-8") + .setContent(Contents.utf8String(body)); + } + + @Override + public WebSocket openSocket(HttpRequest request, WebSocket.Listener listener) { + events.add("WS " + request.getUri()); + return new NoopSocket(); + } + }; + } + } + + private static final class NoopSocket implements WebSocket { + @Override + public WebSocket send(Message message) { + return this; + } + + @Override + public void close() { + } + } +}