Commit a25a26c5 authored by Sergey Ryazanov's avatar Sergey Ryazanov

Implementing RTC debugging session objects (client and server parts).

Pair of ClientSession and ServerSession holds WebRTC connection which tunnels DevTools UNIX socket.

Once cloud based signaling channel doesn't exist at the moment connections work on a single android device. For manual testing the testing APK has launch activity which lets to start service which tunnels Chrome Shell socket to another local socket (only works when Chrome Shell signed with the same certificate).

BUG=383418
TEST=Automatic: LocalSessionBridgeTest, SessionControlMessagesTest. See description for manual testing.
R=mnaganov@chromium.org

Review URL: https://codereview.chromium.org/537253003

Cr-Commit-Position: refs/heads/master@{#299871}
parent 243100c4
...@@ -806,7 +806,6 @@ ...@@ -806,7 +806,6 @@
'../chrome/chrome.gyp:chrome_shell_uiautomator_tests', '../chrome/chrome.gyp:chrome_shell_uiautomator_tests',
'../chrome/chrome.gyp:unit_tests_apk', '../chrome/chrome.gyp:unit_tests_apk',
'../components/components_tests.gyp:components_unittests_apk', '../components/components_tests.gyp:components_unittests_apk',
'../components/devtools_bridge.gyp:devtools_bridge_tests_apk',
'../content/content_shell_and_tests.gyp:content_browsertests_apk', '../content/content_shell_and_tests.gyp:content_browsertests_apk',
'../content/content_shell_and_tests.gyp:content_gl_tests_apk', '../content/content_shell_and_tests.gyp:content_gl_tests_apk',
'../content/content_shell_and_tests.gyp:content_unittests_apk', '../content/content_shell_and_tests.gyp:content_unittests_apk',
...@@ -825,6 +824,13 @@ ...@@ -825,6 +824,13 @@
'../ui/events/events.gyp:events_unittests_apk', '../ui/events/events.gyp:events_unittests_apk',
'../ui/gfx/gfx_tests.gyp:gfx_unittests_apk', '../ui/gfx/gfx_tests.gyp:gfx_unittests_apk',
], ],
'conditions': [
['"<(libpeer_target_type)"=="static_library"', {
'dependencies': [
'../components/devtools_bridge.gyp:devtools_bridge_tests_apk',
],
}],
],
}, },
{ {
# WebRTC Chromium tests to run on Android. # WebRTC Chromium tests to run on Android.
...@@ -838,13 +844,6 @@ ...@@ -838,13 +844,6 @@
# Unit test bundles packaged as an apk. # Unit test bundles packaged as an apk.
'../content/content_shell_and_tests.gyp:content_browsertests_apk', '../content/content_shell_and_tests.gyp:content_browsertests_apk',
], ],
'conditions': [
['"<(libpeer_target_type)"=="static_library"', {
'dependencies': [
'../third_party/libjingle/libjingle.gyp:libjingle_peerconnection_javalib',
],
}],
],
}, # target_name: android_builder_chromium_webrtc }, # target_name: android_builder_chromium_webrtc
], # targets ], # targets
}], # OS="android" }], # OS="android"
......
...@@ -22,3 +22,5 @@ M V EI: org.chromium.chrome.browser.ChromeBrowserProvider$BookmarkNode.thumbnail ...@@ -22,3 +22,5 @@ M V EI: org.chromium.chrome.browser.ChromeBrowserProvider$BookmarkNode.thumbnail
M M LI: Incorrect lazy initialization of static field org.chromium.chrome.browser.sync.ProfileSyncService.sSyncSetupManager in org.chromium.chrome.browser.sync.ProfileSyncService.get(Context) At ProfileSyncService.java M M LI: Incorrect lazy initialization of static field org.chromium.chrome.browser.sync.ProfileSyncService.sSyncSetupManager in org.chromium.chrome.browser.sync.ProfileSyncService.get(Context) At ProfileSyncService.java
M V EI2: org.chromium.content_public.browser.LoadUrlParams.setPostData(byte[]) may expose internal representation by storing an externally mutable object into LoadUrlParams.mPostData At LoadUrlParams.java M V EI2: org.chromium.content_public.browser.LoadUrlParams.setPostData(byte[]) may expose internal representation by storing an externally mutable object into LoadUrlParams.mPostData At LoadUrlParams.java
M V EI: org.chromium.content_public.browser.LoadUrlParams.getPostData() may expose internal representation by returning LoadUrlParams.mPostData At LoadUrlParams.java M V EI: org.chromium.content_public.browser.LoadUrlParams.getPostData() may expose internal representation by returning LoadUrlParams.mPostData At LoadUrlParams.java
M D NP: Read of unwritten public or protected field data in org.chromium.components.devtools_bridge.SessionDependencyFactory$DataChannelObserverAdapter.onMessage(DataChannel$Buffer) At SessionDependencyFactory.java
M D NP: Read of unwritten public or protected field mandatory in org.chromium.components.devtools_bridge.SessionDependencyFactory.createPeerConnection(RTCConfiguration, AbstractPeerConnection$Observer) At SessionDependencyFactory.java
...@@ -11,6 +11,9 @@ ...@@ -11,6 +11,9 @@
'java_in_dir': 'devtools_bridge/android/java', 'java_in_dir': 'devtools_bridge/android/java',
}, },
'includes': [ '../build/java.gypi' ], 'includes': [ '../build/java.gypi' ],
'dependencies': [
'../third_party/libjingle/libjingle.gyp:libjingle_peerconnection_javalib',
],
}, },
{ {
'target_name': 'devtools_bridge_testutils', 'target_name': 'devtools_bridge_testutils',
...@@ -20,6 +23,7 @@ ...@@ -20,6 +23,7 @@
}, },
'includes': [ '../build/java.gypi' ], 'includes': [ '../build/java.gypi' ],
'dependencies': [ 'dependencies': [
'../third_party/libjingle/libjingle.gyp:libjingle_peerconnection_javalib',
'devtools_bridge_javalib', 'devtools_bridge_javalib',
], ],
}, },
...@@ -34,6 +38,7 @@ ...@@ -34,6 +38,7 @@
'apk_name': 'DevToolsBridgeTest', 'apk_name': 'DevToolsBridgeTest',
'test_suite_name': 'devtools_bridge_tests', 'test_suite_name': 'devtools_bridge_tests',
'java_in_dir': 'devtools_bridge/android/javatests', 'java_in_dir': 'devtools_bridge/android/javatests',
'native_lib_target': 'libjingle_peerconnection_so',
'is_test_apk': 1, 'is_test_apk': 1,
}, },
'includes': [ '../build/java_apk.gypi' ], 'includes': [ '../build/java_apk.gypi' ],
......
include_rules = [
"-chrome",
"-content",
]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge;
/**
* Limited view on org.webrtc.PeerConnection. Abstraction layer helps with:
* 1. Allows both native and Java API implementation.
* 2. Hides unused features.
* Should be accessed on a single thread.
*/
public abstract class AbstractPeerConnection {
/**
* All methods are callen on WebRTC signaling thread.
*/
public interface Observer {
/**
* Called when createAndSetLocalDescription or setRemoteDescription failed.
*/
void onFailure(String description);
/**
* Called when createAndSetLocalDescription succeeded.
*/
void onLocalDescriptionCreatedAndSet(SessionDescriptionType type, String description);
/**
* Called when setRemoteDescription succeeded.
*/
void onRemoteDescriptionSet();
/**
* New ICE candidate available. String representation defined in the IceCandidate class.
* To be sent to the remote peer connection.
*/
void onIceCandidate(String iceCandidate);
/**
* Called when connected or disconnected. In disconnected state recovery procedure
* should only rely on signaling channel.
*/
void onIceConnectionChange(boolean connected);
}
/**
* Type of session description.
*/
public enum SessionDescriptionType {
OFFER, ANSWER
}
/**
* The result of this method will be invocation onLocalDescriptionCreatedAndSet
* or onFailure on the observer. Should not be called when waiting result of
* setRemoteDescription.
*/
public abstract void createAndSetLocalDescription(SessionDescriptionType type);
/**
* Result of this method will be invocation onRemoteDescriptionSet or onFailure on the observer.
*/
public abstract void setRemoteDescription(SessionDescriptionType type, String description);
/**
* Adds a remote ICE candidate.
*/
public abstract void addIceCandidate(String candidate);
/**
* Destroys native objects. Synchronized with the signaling thread
* (no observer method called when the connection disposed)
*/
public abstract void dispose();
/**
* Creates prenegotiated SCTP data channel.
*/
public abstract AbstractDataChannel createDataChannel(int channelId);
/**
* Helper class which enforces string representation of an ICE candidate.
*/
static class IceCandidate {
public final String sdpMid;
public final int sdpMLineIndex;
public final String sdp;
public IceCandidate(String sdpMid, int sdpMLineIndex, String sdp) {
this.sdpMid = sdpMid;
this.sdpMLineIndex = sdpMLineIndex;
this.sdp = sdp;
}
public String toString() {
return sdpMid + ":" + sdpMLineIndex + ":" + sdp;
}
public static IceCandidate fromString(String candidate) throws IllegalArgumentException {
String[] parts = candidate.split(":", 3);
if (parts.length != 3)
throw new IllegalArgumentException("Expected column separated list.");
return new IceCandidate(parts[0], Integer.parseInt(parts[1]), parts[2]);
}
}
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Represents RTCConfiguration (http://www.w3.org/TR/webrtc/#rtcconfiguration-type).
* Replacement for List<PeerConnection.IceServer> in Java WebRTC API.
* Transferable through signaling channel.
* Immutable.
*/
public class RTCConfiguration {
/**
* Single ICE server description.
*/
public static class IceServer {
public final String uri;
public final String username;
public final String credential;
public IceServer(String uri, String username, String credential) {
this.uri = uri;
this.username = username;
this.credential = credential;
}
}
public final List<IceServer> iceServers;
private RTCConfiguration(List<IceServer> iceServers) {
this.iceServers = Collections.unmodifiableList(new ArrayList<IceServer>(iceServers));
}
public RTCConfiguration() {
this(Collections.<IceServer>emptyList());
}
/**
* Builder for RTCConfiguration.
*/
public static class Builder {
private final List<IceServer> mIceServers = new ArrayList<IceServer>();
public RTCConfiguration build() {
return new RTCConfiguration(mIceServers);
}
public Builder addIceServer(String uri, String username, String credential) {
mIceServers.add(new IceServer(uri, username, credential));
return this;
}
public Builder addIceServer(String uri) {
return addIceServer(uri, "", "");
}
}
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge;
import java.util.List;
/**
* DevTools Bridge server session. Handles connection with a ClientSession.
* See SessionBase description for more detais.
*/
public class ServerSession extends SessionBase implements SessionBase.ServerSessionInterface {
private NegotiationCallback mNegotiationCallback;
private IceExchangeCallback mIceExchangeCallback;
private boolean mIceEchangeRequested = false;
protected int mGatheringDelayMs = 200;
public ServerSession(SessionDependencyFactory factory,
Executor executor,
String defaultSocketName) {
super(factory, executor, new SocketTunnelServer(defaultSocketName));
}
@Override
public void stop() {
super.stop();
if (mNegotiationCallback != null) {
mNegotiationCallback.onFailure("Session stopped");
mNegotiationCallback = null;
}
if (mIceExchangeCallback != null) {
mIceExchangeCallback.onFailure("Session stopped");
mIceExchangeCallback = null;
}
}
@Override
public void startSession(RTCConfiguration config,
String offer,
NegotiationCallback callback) {
checkCalledOnSessionThread();
if (isStarted()) {
callback.onFailure("Session already started");
return;
}
ClientMessageHandler handler = new ClientMessageHandler();
start(config, handler);
negotiate(offer, callback);
}
@Override
public void renegotiate(String offer, NegotiationCallback callback) {
checkCalledOnSessionThread();
if (!isStarted()) {
callback.onFailure("Session is not started");
return;
}
callback.onFailure("Not implemented");
}
private void negotiate(String offer, NegotiationCallback callback) {
if (mNegotiationCallback != null) {
callback.onFailure("Negotiation already in progress");
return;
}
mNegotiationCallback = callback;
// If success will call onRemoteDescriptionSet.
connection().setRemoteDescription(
AbstractPeerConnection.SessionDescriptionType.OFFER, offer);
}
protected void onRemoteDescriptionSet() {
// If success will call onLocalDescriptionCreatedAndSet.
connection().createAndSetLocalDescription(
AbstractPeerConnection.SessionDescriptionType.ANSWER);
}
@Override
protected void onLocalDescriptionCreatedAndSet(
AbstractPeerConnection.SessionDescriptionType type, String description) {
assert type == AbstractPeerConnection.SessionDescriptionType.ANSWER;
mNegotiationCallback.onSuccess(description);
mNegotiationCallback = null;
onSessionNegotiated();
}
protected void onSessionNegotiated() {
if (!isControlChannelOpened())
startAutoCloseTimer();
}
@Override
public void iceExchange(List<String> clientCandidates,
IceExchangeCallback callback) {
checkCalledOnSessionThread();
if (!isStarted()) {
callback.onFailure("Session disposed");
return;
}
if (mNegotiationCallback != null || mIceExchangeCallback != null) {
callback.onFailure("Concurrent requests detected");
return;
}
mIceExchangeCallback = callback;
addIceCandidates(clientCandidates);
// Give libjingle some time for gathering ice candidates.
postOnSessionThread(mGatheringDelayMs, new Runnable() {
@Override
public void run() {
if (isStarted())
sendIceCandidatesBack();
}
});
}
private void sendIceCandidatesBack() {
mIceExchangeCallback.onSuccess(takeIceCandidates());
mIceExchangeCallback = null;
mIceEchangeRequested = false;
}
@Override
protected void onControlChannelOpened() {
stopAutoCloseTimer();
}
@Override
protected void onFailure(String message) {
if (mNegotiationCallback != null) {
mNegotiationCallback.onFailure(message);
mNegotiationCallback = null;
}
super.onFailure(message);
}
@Override
protected void onIceCandidate(String candidate) {
super.onIceCandidate(candidate);
if (isControlChannelOpened() && !mIceEchangeRequested) {
// New ICE candidate may improve connection even if control channel operable.
// If control channel closed client will exchange candidates anyway.
sendControlMessage(new SessionControlMessages.IceExchangeMessage());
mIceEchangeRequested = true;
}
}
protected SocketTunnelServer createSocketTunnelServer(String serverSocketName) {
return new SocketTunnelServer(serverSocketName);
}
private final class ClientMessageHandler extends SessionControlMessages.ClientMessageHandler {
@Override
protected void onMessage(SessionControlMessages.ClientMessage message) {
switch (message.type) {
case UNKNOWN_REQUEST:
sendControlMessage(((SessionControlMessages.UnknownRequestMessage) message)
.createResponse());
break;
}
}
}
@Override
protected void sendControlMessage(SessionControlMessages.Message<?> message) {
assert message instanceof SessionControlMessages.ServerMessage;
super.sendControlMessage(message);
}
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge;
import android.util.JsonReader;
import android.util.JsonWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
/**
* Defines protocol of control channel of SessionBase. Messages are JSON serializable
* and transferred through AbstractDataChannel.
*/
public final class SessionControlMessages {
private SessionControlMessages() {
throw new RuntimeException("Class not intended to instantiate");
}
/**
* Types of messages that client sends to server.
*/
enum ClientMessageType {
UNKNOWN_REQUEST
}
/**
* Types of messages that servers sends to client.
*/
enum ServerMessageType {
ICE_EXCHANGE,
UNKNOWN_RESPONSE
}
/**
* Base class for all messages.
*/
public abstract static class Message<T extends Enum> {
public final T type;
protected Message(T type) {
this.type = type;
}
public void write(JsonWriter writer) throws IOException {
writer.name("type");
writer.value(type.toString());
}
}
/**
* Base calss for messages that client sends to server.
*/
public abstract static class ClientMessage extends Message<ClientMessageType> {
protected ClientMessage(ClientMessageType type) {
super(type);
}
}
/**
* Base class for messages that server sends to client.
*/
public abstract static class ServerMessage extends Message<ServerMessageType> {
protected ServerMessage(ServerMessageType type) {
super(type);
}
}
/**
* Server sends this message when it has ICE candidates to exchange. Client initiates
* ICE exchange over signaling channel.
*/
public static final class IceExchangeMessage extends ServerMessage {
public IceExchangeMessage() {
super(ServerMessageType.ICE_EXCHANGE);
}
}
/**
* Server response on unrecognized client message.
*/
public static final class UnknownResponseMessage extends ServerMessage {
public final String rawRequestType;
public UnknownResponseMessage(String rawRequestType) {
super(ServerMessageType.UNKNOWN_RESPONSE);
this.rawRequestType = rawRequestType;
}
public void write(JsonWriter writer) throws IOException {
super.write(writer);
writer.name("rawRequestType");
writer.value(rawRequestType.toString());
}
}
/**
* Helper class to represent message of unknown type. Should not be sent.
*/
public static final class UnknownRequestMessage extends ClientMessage {
public final String rawType;
public UnknownRequestMessage(String rawType) {
super(ClientMessageType.UNKNOWN_REQUEST);
this.rawType = rawType;
}
@Override
public void write(JsonWriter writer) throws IOException {
throw new RuntimeException("Should not be serialized");
}
public UnknownResponseMessage createResponse() {
return new UnknownResponseMessage(rawType);
}
}
private static <T extends Enum<T>> T getMessageType(
Class<T> enumType, String rawType, T defaultType) throws IOException {
try {
return Enum.valueOf(enumType, rawType);
} catch (IllegalArgumentException e) {
if (defaultType != null)
return defaultType;
throw new IOException("Invalid message type " + rawType);
}
}
public static void write(JsonWriter writer, Message<?> message) throws IOException {
writer.beginObject();
message.write(writer);
writer.endObject();
}
public static ClientMessage readClientMessage(JsonReader reader) throws IOException {
String rawType = "";
boolean success = false;
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
if ("type".equals(name)) {
rawType = reader.nextString();
}
}
reader.endObject();
switch (getMessageType(ClientMessageType.class,
rawType,
ClientMessageType.UNKNOWN_REQUEST)) {
case UNKNOWN_REQUEST:
return new UnknownRequestMessage(rawType);
}
throw new IOException("Invalid message");
}
public static ServerMessage readServerMessage(JsonReader reader) throws IOException {
String rawType = "";
String rawRequestType = null;
reader.beginObject();
while (reader.hasNext()) {
String name = reader.nextName();
if ("type".equals(name)) {
rawType = reader.nextString();
} else if ("rawRequestType".equals(name)) {
rawRequestType = reader.nextString();
}
}
reader.endObject();
switch (getMessageType(ServerMessageType.class, rawType, null)) {
case ICE_EXCHANGE:
return new IceExchangeMessage();
case UNKNOWN_RESPONSE:
return new UnknownResponseMessage(rawRequestType);
}
throw new IOException("Invalid message");
}
/**
* Base class for client and server message handlers.
*/
public abstract static class MessageHandler {
protected abstract void readMessage(JsonReader reader) throws IOException;
public boolean readMessage(byte[] bytes) throws InvalidFormatException {
try {
readMessage(new JsonReader(new InputStreamReader(new ByteArrayInputStream(bytes))));
return true;
} catch (IOException e) {
throw new InvalidFormatException(e);
}
}
}
/**
* Exception when parsing or handling message.
*/
public static class InvalidFormatException extends IOException {
public InvalidFormatException(IOException e) {
super(e);
}
public InvalidFormatException(String message) {
super(message);
}
}
/**
* Base class for handler of server messages (to be created on client).
*/
public abstract static class ServerMessageHandler extends MessageHandler {
@Override
protected void readMessage(JsonReader reader) throws IOException {
onMessage(readServerMessage(reader));
}
protected abstract void onMessage(ServerMessage message);
}
/**
* Base class for handler of client messages (to be created on server).
*/
public abstract static class ClientMessageHandler extends MessageHandler {
@Override
public void readMessage(JsonReader reader) throws IOException {
onMessage(readClientMessage(reader));
}
protected abstract void onMessage(ClientMessage message);
}
public static byte[] toByteArray(Message<?> message) {
ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
JsonWriter writer = new JsonWriter(new OutputStreamWriter(byteStream));
try {
write(writer, message);
writer.close();
return byteStream.toByteArray();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
...@@ -40,7 +40,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock; ...@@ -40,7 +40,7 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
* ID is safe to be reused. * ID is safe to be reused.
*/ */
public abstract class SocketTunnelBase { public abstract class SocketTunnelBase {
// Data channel is threadsafe but access to the reference needs synchromization. // Data channel is threadsafe but access to the reference needs synchronization.
private final ReadWriteLock mDataChanneliReferenceLock = new ReentrantReadWriteLock(); private final ReadWriteLock mDataChanneliReferenceLock = new ReentrantReadWriteLock();
private volatile AbstractDataChannel mDataChannel; private volatile AbstractDataChannel mDataChannel;
......
...@@ -27,6 +27,7 @@ ...@@ -27,6 +27,7 @@
<uses-permission android:name="android.permission.RUN_INSTRUMENTATION" /> <uses-permission android:name="android.permission.RUN_INSTRUMENTATION" />
<uses-permission android:name="android.permission.INJECT_EVENTS" /> <uses-permission android:name="android.permission.INJECT_EVENTS" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- For manual testing with Chrome Shell --> <!-- For manual testing with Chrome Shell -->
<uses-permission android:name="org.chromium.chrome.shell.permission.DEBUG" /> <uses-permission android:name="org.chromium.chrome.shell.permission.DEBUG" />
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge;
import android.net.LocalServerSocket;
import android.net.LocalSocket;
import android.test.InstrumentationTestCase;
import android.test.suitebuilder.annotation.MediumTest;
import junit.framework.Assert;
import java.util.concurrent.Future;
/**
* Tests for both client and server sessions bound with {@link LocalSessionBridge}.
*/
public class LocalSessionBridgeTest extends InstrumentationTestCase {
private static final String SERVER_SOCKET_NAME =
"org.chromium.components.devtools_bridge.LocalSessionBridgeTest.SERVER_SOCKET";
private static final String CLIENT_SOCKET_NAME =
"org.chromium.components.devtools_bridge.LocalSessionBridgeTest.CLIENT_SOCKET";
private static final String REQUEST = "Request";
private static final String RESPONSE = "Response";
private LocalSessionBridge mBridge;
@Override
public void setUp() throws Exception {
super.setUp();
mBridge = new LocalSessionBridge(SERVER_SOCKET_NAME, CLIENT_SOCKET_NAME);
}
@Override
public void tearDown() throws Exception {
mBridge.dispose();
super.tearDown();
}
@MediumTest
public void testDisposeAfeterStart() {
mBridge.start();
}
@MediumTest
public void testNegotiating() throws InterruptedException {
mBridge.start();
mBridge.awaitNegotiated();
}
@MediumTest
public void testOpenControlChannel() throws InterruptedException {
mBridge.start();
mBridge.awaitControlChannelOpened();
}
@MediumTest
public void testClientAutocloseTimeout() throws InterruptedException {
mBridge.setMessageDeliveryDelayMs(1000);
mBridge.setClientAutoCloseTimeoutMs(100);
mBridge.start();
mBridge.awaitClientAutoClosed();
}
@MediumTest
public void testServerAutocloseTimeout() throws InterruptedException {
mBridge.setMessageDeliveryDelayMs(1000);
mBridge.setServerAutoCloseTimeoutMs(100);
mBridge.start();
mBridge.awaitServerAutoClosed();
}
@MediumTest
public void testRequestResponse() throws Exception {
mBridge.start();
LocalServerSocket serverListeningSocket = new LocalServerSocket(SERVER_SOCKET_NAME);
Future<String> response = TestUtils.asyncRequest(CLIENT_SOCKET_NAME, REQUEST);
LocalSocket serverSocket = serverListeningSocket.accept();
String request = TestUtils.readAll(serverSocket);
Assert.assertEquals(REQUEST, request);
TestUtils.writeAndShutdown(serverSocket, RESPONSE);
Assert.assertEquals(RESPONSE, response.get());
}
}
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge;
import android.test.InstrumentationTestCase;
import android.test.suitebuilder.annotation.SmallTest;
import junit.framework.Assert;
import org.chromium.components.devtools_bridge.SessionControlMessages.ClientMessage;
import org.chromium.components.devtools_bridge.SessionControlMessages.ClientMessageHandler;
import org.chromium.components.devtools_bridge.SessionControlMessages.IceExchangeMessage;
import org.chromium.components.devtools_bridge.SessionControlMessages.InvalidFormatException;
import org.chromium.components.devtools_bridge.SessionControlMessages.Message;
import org.chromium.components.devtools_bridge.SessionControlMessages.MessageHandler;
import org.chromium.components.devtools_bridge.SessionControlMessages.ServerMessage;
import org.chromium.components.devtools_bridge.SessionControlMessages.ServerMessageHandler;
import org.chromium.components.devtools_bridge.SessionControlMessages.UnknownRequestMessage;
import org.chromium.components.devtools_bridge.SessionControlMessages.UnknownResponseMessage;
/**
* Tests for {@link SessionControlMessages}
*/
public class SessionControlMessagesTest extends InstrumentationTestCase {
private static final String UNKNOWN_REQUEST_TYPE = "@unknown request@";
private static final String UNKNOWN_REQUEST = "{\"type\": \"" + UNKNOWN_REQUEST_TYPE + "\"}";
@SmallTest
public void testIceExchangeMessage() {
recode(new IceExchangeMessage());
}
@SmallTest
public void testUnknownRequest() throws InvalidFormatException {
UnknownRequestMessage request =
(UnknownRequestMessage) ClientMessageReader.readMessage(UNKNOWN_REQUEST);
UnknownResponseMessage response = ServerMessageReader.recode(request.createResponse());
Assert.assertEquals(UNKNOWN_REQUEST_TYPE, response.rawRequestType);
}
private <T extends ServerMessage> T recode(T message) {
assert message != null;
return ServerMessageReader.recode(message);
}
@SuppressWarnings("unchecked")
private static <T> T cast(T prototype, Object object) {
Assert.assertNotNull(object);
if (prototype.getClass() == object.getClass())
return (T) object;
else
throw new ClassCastException();
}
private static void checkedRead(MessageHandler handler, Message<?> message) {
try {
handler.readMessage(SessionControlMessages.toByteArray(message));
} catch (InvalidFormatException e) {
Assert.fail(e.toString());
}
}
private static class ServerMessageReader extends ServerMessageHandler {
private ServerMessage mLastMessage;
@Override
protected void onMessage(ServerMessage message) {
mLastMessage = message;
}
public static <T extends ServerMessage> T recode(T message) {
ServerMessageReader handler = new ServerMessageReader();
checkedRead(handler, message);
return cast(message, handler.mLastMessage);
}
}
private static class ClientMessageReader extends ClientMessageHandler {
private ClientMessage mLastMessage;
@Override
protected void onMessage(ClientMessage message) {
mLastMessage = message;
}
public static ClientMessage readMessage(String json) throws InvalidFormatException {
ClientMessageReader reader = new ClientMessageReader();
reader.readMessage(json.getBytes());
return reader.mLastMessage;
}
}
}
...@@ -38,21 +38,22 @@ public class DebugActivity extends Activity { ...@@ -38,21 +38,22 @@ public class DebugActivity extends Activity {
textView.setText(intro); textView.setText(intro);
mLayout.addView(textView); mLayout.addView(textView);
Button startButton = new Button(this); addActionButton("Start LocalTunnelBridge", DebugService.START_TUNNEL_BRIDGE_ACTION);
startButton.setText("Start LocalTunnelBridge"); addActionButton("Start LocalSessionBridge", DebugService.START_SESSION_BRIDGE_ACTION);
startButton.setOnClickListener(new SendActionOnClickListener(DebugService.START_ACTION)); addActionButton("Stop", DebugService.STOP_ACTION);
mLayout.addView(startButton);
Button stopButton = new Button(this);
stopButton.setText("Stop");
stopButton.setOnClickListener(new SendActionOnClickListener(DebugService.STOP_ACTION));
mLayout.addView(stopButton);
LayoutParams layoutParam = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams layoutParam = new LayoutParams(LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT); LayoutParams.MATCH_PARENT);
setContentView(mLayout, layoutParam); setContentView(mLayout, layoutParam);
} }
private void addActionButton(String text, String action) {
Button button = new Button(this);
button.setText(text);
button.setOnClickListener(new SendActionOnClickListener(action));
mLayout.addView(button);
}
private class SendActionOnClickListener implements View.OnClickListener { private class SendActionOnClickListener implements View.OnClickListener {
private final String mAction; private final String mAction;
......
...@@ -13,6 +13,7 @@ import android.os.IBinder; ...@@ -13,6 +13,7 @@ import android.os.IBinder;
import android.os.Process; import android.os.Process;
import android.widget.Toast; import android.widget.Toast;
import org.chromium.components.devtools_bridge.LocalSessionBridge;
import org.chromium.components.devtools_bridge.LocalTunnelBridge; import org.chromium.components.devtools_bridge.LocalTunnelBridge;
import java.io.IOException; import java.io.IOException;
...@@ -22,15 +23,86 @@ import java.io.IOException; ...@@ -22,15 +23,86 @@ import java.io.IOException;
*/ */
public class DebugService extends Service { public class DebugService extends Service {
private static final String PACKAGE = "org.chromium.components.devtools_bridge.tests"; private static final String PACKAGE = "org.chromium.components.devtools_bridge.tests";
public static final String START_ACTION = PACKAGE + ".START_ACTION"; public static final String START_TUNNEL_BRIDGE_ACTION =
PACKAGE + ".START_TUNNEL_BRIDGE_ACTION";
public static final String START_SESSION_BRIDGE_ACTION =
PACKAGE + ".START_SESSION_BRIDGE_ACTION";
public static final String STOP_ACTION = PACKAGE + ".STOP_ACTION"; public static final String STOP_ACTION = PACKAGE + ".STOP_ACTION";
private static final int NOTIFICATION_ID = 1; private static final int NOTIFICATION_ID = 1;
private LocalTunnelBridge mBridge; private Controller mRunningController;
private LocalTunnelBridge createBridge() throws IOException { private interface Controller {
String exposingSocketName = "webview_devtools_remote_" + Integer.valueOf(Process.myPid()); void create() throws IOException;
return new LocalTunnelBridge("chrome_shell_devtools_remote", exposingSocketName); void start() throws Exception;
void stop();
void dispose();
}
private String replicatingSocketName() {
return "chrome_shell_devtools_remote";
}
private String exposingSocketName() {
return "webview_devtools_remote_" + Integer.valueOf(Process.myPid());
}
private class LocalTunnelBridgeController implements Controller {
private LocalTunnelBridge mBridge;
@Override
public void create() throws IOException {
mBridge = new LocalTunnelBridge(replicatingSocketName(), exposingSocketName());
}
@Override
public void start() throws Exception {
mBridge.start();
}
@Override
public void stop() {
mBridge.stop();
}
@Override
public void dispose() {
mBridge.dispose();
}
@Override
public String toString() {
return "LocalTunnelBridge";
}
}
private class LocalSessionBridgeController implements Controller {
private LocalSessionBridge mBridge;
@Override
public void create() throws IOException {
mBridge = new LocalSessionBridge(replicatingSocketName(), exposingSocketName());
}
@Override
public void start() {
mBridge.start();
}
@Override
public void stop() {
mBridge.stop();
}
@Override
public void dispose() {
mBridge.dispose();
}
@Override
public String toString() {
return "LocalSessionBridge";
}
} }
@Override @Override
...@@ -38,42 +110,48 @@ public class DebugService extends Service { ...@@ -38,42 +110,48 @@ public class DebugService extends Service {
if (intent == null) return START_NOT_STICKY; if (intent == null) return START_NOT_STICKY;
String action = intent.getAction(); String action = intent.getAction();
if (START_ACTION.equals(action)) { if (START_TUNNEL_BRIDGE_ACTION.equals(action)) {
return start(); return start(new LocalTunnelBridgeController());
} else if (START_SESSION_BRIDGE_ACTION.equals(action)) {
return start(new LocalSessionBridgeController());
} else if (STOP_ACTION.equals(action)) { } else if (STOP_ACTION.equals(action)) {
return stop(); return stop();
} }
return START_NOT_STICKY; return START_NOT_STICKY;
} }
private int start() { private int start(Controller controller) {
if (mBridge != null) if (mRunningController != null) {
Toast.makeText(this, "Already started", Toast.LENGTH_SHORT).show();
return START_NOT_STICKY; return START_NOT_STICKY;
}
try { try {
mBridge = createBridge(); controller.create();
mBridge.start(); controller.start();
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "Failed to start", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Failed to start", Toast.LENGTH_SHORT).show();
mBridge.dispose(); controller.dispose();
mBridge = null;
return START_NOT_STICKY; return START_NOT_STICKY;
} }
mRunningController = controller;
startForeground(NOTIFICATION_ID, makeForegroundServiceNotification()); startForeground(NOTIFICATION_ID, makeForegroundServiceNotification());
Toast.makeText(this, "Service started", Toast.LENGTH_SHORT).show(); Toast.makeText(this, controller.toString() + " started", Toast.LENGTH_SHORT).show();
return START_STICKY; return START_STICKY;
} }
private int stop() { private int stop() {
if (mBridge == null) if (mRunningController == null)
return START_NOT_STICKY; return START_NOT_STICKY;
mBridge.stop(); String name = mRunningController.toString();
mBridge.dispose(); mRunningController.stop();
mBridge = null; mRunningController.dispose();
mRunningController = null;
stopSelf(); stopSelf();
Toast.makeText(this, "Service stopped", Toast.LENGTH_SHORT).show(); Toast.makeText(this, name + " stopped", Toast.LENGTH_SHORT).show();
return START_NOT_STICKY; return START_NOT_STICKY;
} }
......
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.devtools_bridge;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Client session. Creates client socket tunnel for clientSocketName as a default tunnel.
* See SessionBase for details.
*/
public class ClientSession extends SessionBase {
private final ServerSessionInterface mServer;
private RTCConfiguration mConfig;
private Cancellable mIceExchangeTask;
private boolean mIceExchangeRequested = false;
private IceExchangeHandler mPendingIceExchangeRequest;
private int mExchangeDelayMs = -1;
protected int mInitialIceExchangeDelayMs = 200;
protected int mMaxIceExchangeDelayMs = 5000;
private final Map<Integer, SocketTunnelClient> mPendingTunnel =
new HashMap<Integer, SocketTunnelClient>();
public ClientSession(SessionDependencyFactory factory,
Executor executor,
ServerSessionInterface server,
String clientSocketName) throws IOException {
super(factory, executor, new SocketTunnelClient(clientSocketName));
mServer = server;
}
public void start(RTCConfiguration config) {
checkCalledOnSessionThread();
super.start(config, new ServerMessageHandler());
mConfig = config;
for (Map.Entry<Integer, SocketTunnelClient> entry : mPendingTunnel.entrySet()) {
int channelId = entry.getKey();
entry.getValue().bind(connection().createDataChannel(channelId));
}
connection().createAndSetLocalDescription(
AbstractPeerConnection.SessionDescriptionType.OFFER);
}
@Override
public void stop() {
for (SocketTunnelClient tunnel : mPendingTunnel.values())
tunnel.unbind().dispose();
if (mIceExchangeTask != null)
mIceExchangeTask.cancel();
super.stop();
}
@Override
protected void onLocalDescriptionCreatedAndSet(
AbstractPeerConnection.SessionDescriptionType type, String offer) {
assert type == AbstractPeerConnection.SessionDescriptionType.OFFER;
mServer.startSession(mConfig, offer, new CreateSessionHandler());
mConfig = null;
}
private void onAnswerReceived(String answer) {
connection().setRemoteDescription(
AbstractPeerConnection.SessionDescriptionType.ANSWER, answer);
}
@Override
protected void onRemoteDescriptionSet() {
onSessionNegotiated();
}
protected void onSessionNegotiated() {
assert !isIceExchangeStarted();
updateIceExchangeStatus();
assert isIceExchangeStarted();
}
@Override
protected void onControlChannelOpened() {
assert isIceExchangeStarted();
updateIceExchangeStatus();
}
@Override
protected void onIceConnectionChange() {
super.onIceConnectionChange();
updateIceExchangeStatus();
}
private void updateIceExchangeStatus() {
boolean needed = !isConnected() || !isControlChannelOpened();
if (needed == isIceExchangeStarted())
return;
if (needed)
startIceExchange();
else
stopIceExchange();
}
private boolean isIceExchangeStarted() {
return mExchangeDelayMs >= 0;
}
private void startIceExchange() {
assert !isIceExchangeStarted();
mExchangeDelayMs = mInitialIceExchangeDelayMs;
startAutoCloseTimer();
if (!isIceExchangeScheduledOrPending()) {
scheduleIceExchange(mExchangeDelayMs);
}
assert isIceExchangeScheduledOrPending();
assert isIceExchangeStarted();
}
private void stopIceExchange() {
assert isIceExchangeStarted();
mExchangeDelayMs = -1;
stopAutoCloseTimer();
// Last exchange will happen, not more will be scheduled (unless mIceExchangeRequested).
assert isIceExchangeScheduledOrPending();
assert !isIceExchangeStarted();
}
private void scheduleIceExchange(int delay) {
assert mIceExchangeTask == null;
mIceExchangeTask = postOnSessionThread(delay, new Runnable() {
@Override
public void run() {
mIceExchangeTask = null;
mServer.iceExchange(takeIceCandidates(), new IceExchangeHandler());
mIceExchangeRequested = false;
}
});
}
private boolean isIceExchangeScheduledOrPending() {
return mIceExchangeTask != null || mPendingIceExchangeRequest != null;
}
private void onServerCandidates(List<String> serverCandidates) {
addIceCandidates(serverCandidates);
if (isIceExchangeStarted()) {
mExchangeDelayMs *= 2;
if (mExchangeDelayMs > mMaxIceExchangeDelayMs) {
mExchangeDelayMs = mMaxIceExchangeDelayMs;
}
scheduleIceExchange(mExchangeDelayMs);
} else if (mIceExchangeRequested) {
scheduleIceExchange(mInitialIceExchangeDelayMs);
}
}
/**
* Queries single ICE eqchange cycle regardless of ICE exchange process.
*/
private void queryIceExchange() {
mIceExchangeRequested = true;
if (mIceExchangeTask == null && mPendingIceExchangeRequest != null) {
assert !isIceExchangeStarted();
scheduleIceExchange(mInitialIceExchangeDelayMs);
}
}
private final class CreateSessionHandler implements NegotiationCallback {
@Override
public void onSuccess(String answer) {
checkCalledOnSessionThread();
onAnswerReceived(answer);
}
@Override
public void onFailure(String message) {
checkCalledOnSessionThread();
ClientSession.this.onFailure(message);
}
}
private final class IceExchangeHandler implements IceExchangeCallback {
public IceExchangeHandler() {
assert mPendingIceExchangeRequest == null;
mPendingIceExchangeRequest = this;
}
@Override
public void onSuccess(List<String> serverCandidates) {
checkCalledOnSessionThread();
mPendingIceExchangeRequest = null;
if (isStarted()) {
onServerCandidates(serverCandidates);
}
}
@Override
public void onFailure(String message) {
checkCalledOnSessionThread();
mPendingIceExchangeRequest = null;
if (isStarted()) {
ClientSession.this.onFailure(message);
}
}
}
private final class ServerMessageHandler extends SessionControlMessages.ServerMessageHandler {
@Override
protected void onMessage(SessionControlMessages.ServerMessage message) {
switch (message.type) {
case ICE_EXCHANGE:
queryIceExchange();
break;
case UNKNOWN_RESPONSE:
onUnknownResponse((SessionControlMessages.UnknownResponseMessage) message);
break;
}
}
}
private void onUnknownResponse(SessionControlMessages.UnknownResponseMessage message) {
// TODO(serya): Handle server version incompatibility.
}
@Override
protected void sendControlMessage(SessionControlMessages.Message<?> message) {
assert message instanceof SessionControlMessages.ClientMessage;
super.sendControlMessage(message);
}
}
...@@ -17,7 +17,7 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -17,7 +17,7 @@ import java.util.concurrent.atomic.AtomicInteger;
/** /**
* Convinience class for tests. Like WebRTC threads supports posts * Convinience class for tests. Like WebRTC threads supports posts
* and synchromous invokes. * and synchronous invokes.
*/ */
class SignalingThreadMock { class SignalingThreadMock {
// TODO: use scaleTimeout when natives for org.chromium.base get available. // TODO: use scaleTimeout when natives for org.chromium.base get available.
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment