Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
*/
Expand All @@ -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";
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> events = broadcast(serverUrl, AndroidDriver::startLogcatBroadcast);
assertEquals(List.of(
"POST /session/s1/execute/sync",
"WS " + expectedSocket
), events);
}

@Test
void explicitHostKeepsDefaultPortAndServerScheme() throws Exception {
List<String> 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<String> 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<String> broadcast(String serverUrl, Consumer<AndroidDriver> 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<String> 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() {
}
}
}