// Copyright (c) 2009 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.sdk.internal.shellprotocol;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.chromium.sdk.Browser;
import org.chromium.sdk.BrowserTab;
import org.chromium.sdk.TabDebugEventListener;
import org.chromium.sdk.UnsupportedVersionException;
import org.chromium.sdk.Version;
import org.chromium.sdk.internal.CloseableMap;
import org.chromium.sdk.internal.shellprotocol.tools.ToolHandler;
import org.chromium.sdk.internal.shellprotocol.tools.ToolName;
import org.chromium.sdk.internal.shellprotocol.tools.ToolOutput;
import org.chromium.sdk.internal.shellprotocol.tools.devtools.DevToolsServiceHandler;
import org.chromium.sdk.internal.shellprotocol.tools.devtools.DevToolsServiceHandler.TabIdAndUrl;
import org.chromium.sdk.internal.shellprotocol.tools.protocol.output.MessageFactory;
import org.chromium.sdk.internal.transport.Connection;
import org.chromium.sdk.internal.transport.Connection.NetListener;
import org.chromium.sdk.internal.transport.Message;
import org.chromium.sdk.internal.v8native.DebugSession;
import org.chromium.sdk.internal.v8native.JavascriptVmImpl;
import org.chromium.sdk.util.MethodIsBlockingException;
/**
* A thread-safe implementation of the Browser interface.
*/
public class BrowserImpl implements Browser {
private static final Logger LOGGER = Logger.getLogger(BrowserImpl.class.getName());
public static final int OPERATION_TIMEOUT_MS = 3000;
/**
* The protocol version supported by this SDK implementation.
*/
public static final Version PROTOCOL_VERSION = new Version(0, 1);
private final ConnectionSessionManager sessionManager = new ConnectionSessionManager();
/** The browser connection (gets opened in session). */
private final ConnectionFactory connectionFactory;
public BrowserImpl(ConnectionFactory connectionFactory) {
this.connectionFactory = connectionFactory;
}
public TabFetcher createTabFetcher() throws IOException, UnsupportedVersionException {
SessionManager.Ticket<Session> ticket = connectInternal();
return new TabFetcherImpl(ticket);
}
private SessionManager.Ticket<Session> connectInternal() throws IOException,
UnsupportedVersionException {
try {
return sessionManager.connect();
} catch (ExceptionWrapper eWrapper) {
eWrapper.rethrow();
// Not reachable.
throw new RuntimeException();
}
}
/**
* Object that lives during one connection period. Browser should be able to
* reconnect (because we want to support attach-detach-attach sequence). On
* reconnect new session should be created. Each browser tab should be linked
* to a particular session.
*/
public class Session extends SessionManager.SessionBase<Session> {
private final AtomicBoolean alreadyClosingSession = new AtomicBoolean(false);
private final CloseableMap<Integer, ToolHandler> tabId2ToolHandler = CloseableMap.newMap();
// TODO(peter.rybin): get rid of this structure (if we can get rid
// of corresponding notification)
private final Map<Integer, DebugSession> tabId2DebugSession =
new ConcurrentHashMap<Integer, DebugSession>();
/** The DevTools service handler for the browser. */
private volatile DevToolsServiceHandler devToolsHandler;
/** Open connection which is used by the session. */
private final Connection sessionConnection;
Session() throws IOException, UnsupportedVersionException {
super(sessionManager);
devToolsHandler = new DevToolsServiceHandler(devToolsToolOutput);
sessionConnection = connectionFactory.newOpenConnection(netListener);
String serverVersionString;
try {
serverVersionString = devToolsHandler.version(OPERATION_TIMEOUT_MS);
} catch (TimeoutException e) {
throw JavascriptVmImpl.newIOException("Failed to get protocol version from remote", e);
}
if (serverVersionString == null) {
throw new UnsupportedVersionException(BrowserImpl.PROTOCOL_VERSION, null);
}
Version serverVersion = Version.parseString(serverVersionString);
if (serverVersion == null ||
serverVersion.compareTo(BrowserImpl.PROTOCOL_VERSION) < 0) {
throw new UnsupportedVersionException(BrowserImpl.PROTOCOL_VERSION, serverVersion);
}
}
@Override
protected Session getThisAsSession() {
return this;
}
@Override
protected void lastTicketDismissed() {
boolean res = alreadyClosingSession.compareAndSet(false, true);
if (!res) {
// already closing
return;
}
closeSession();
sessionConnection.close();
}
void registerTab(int destinationTabId, ToolHandler toolHandler, DebugSession debugSession)
throws IOException {
try {
tabId2ToolHandler.put(destinationTabId, toolHandler);
} catch (IllegalStateException e) {
throw new IOException("Tab id=" + destinationTabId + " cannot be attached");
}
tabId2DebugSession.put(destinationTabId, debugSession);
}
void unregisterTab(int destinationTabId) {
tabId2DebugSession.remove(destinationTabId);
tabId2ToolHandler.remove(destinationTabId);
}
private DevToolsServiceHandler getDevToolsServiceHandler() {
return devToolsHandler;
}
@Override
protected void checkHealth() {
// We do not actually interrupt here. It's more an assert for now: we throw an exception,
// if connection is unexpectedly closed.
if (sessionConnection.isConnected()) {
// All OK
return;
}
// We should not be here
LOGGER.severe("checkHealth in BrowserImpl found a consistnecy problem; " +
"current session is broken and therefore terminated");
interruptSession();
closeSession();
}
private void checkConnection() {
if (!sessionConnection.isConnected()) {
throw new IllegalStateException("connection is not started");
}
}
private final NetListener netListener = new NetListener() {
public void connectionClosed() {
devToolsHandler.onDebuggerDetached();
// Use a copy to avoid the underlying map modification in #sessionTerminated
// invoked through #onDebuggerDetached
Collection<DebugSession> copy = new ArrayList<DebugSession>(tabId2DebugSession.values());
for (DebugSession session : copy) {
session.onDebuggerDetached();
}
}
public void messageReceived(Message message) {
ToolName toolName = ToolName.forString(message.getTool());
if (toolName == null) {
LOGGER.log(Level.SEVERE, "Bad 'Tool' header received: {0}", message.getTool());
return;
}
ToolHandler handler = null;
switch (toolName) {
case DEVTOOLS_SERVICE:
handler = devToolsHandler;
break;
case V8_DEBUGGER:
handler = tabId2ToolHandler.get(Integer.valueOf(message.getDestination()));
break;
default:
LOGGER.log(Level.SEVERE, "Unregistered handler for tool: {0}", message.getTool());
return;
}
if (handler != null) {
handler.handleMessage(message);
} else {
LOGGER.log(
Level.SEVERE,
"null handler for tool: {0}, destination: {1}",
new Object[] {message.getTool(), message.getDestination()});
}
}
public void eosReceived() {
boolean res = alreadyClosingSession.compareAndSet(false, true);
if (!res) {
// already closing
return;
}
Collection<ToolHandler> allHandlers = tabId2ToolHandler.close().values();
for (ToolHandler handler : allHandlers) {
handler.handleEos();
}
devToolsHandler.handleEos();
Collection<? extends RuntimeException> problems = interruptSession();
for (RuntimeException ex : problems) {
LOGGER.log(Level.SEVERE, "Failure in closing connections", ex);
}
closeSession();
}
};
private final ToolOutput devToolsToolOutput = new ToolOutput() {
public void send(String content) {
Message message =
MessageFactory.createMessage(ToolName.DEVTOOLS_SERVICE.value, null, content);
sessionConnection.send(message);
}
public void runInDispatchThread(Runnable callback) {
sessionConnection.runInDispatchThread(callback);
}
};
public BrowserImpl getBrowser() {
return BrowserImpl.this;
}
}
private class TabFetcherImpl implements TabFetcher {
private final SessionManager.Ticket<Session> ticket;
TabFetcherImpl(SessionManager.Ticket<Session> ticket) {
this.ticket = ticket;
}
public List<? extends TabConnector> getTabs() {
Session session = ticket.getSession();
session.checkConnection();
List<TabIdAndUrl> entries = session.devToolsHandler.listTabs(OPERATION_TIMEOUT_MS);
List<TabConnectorImpl> tabConnectors = new ArrayList<TabConnectorImpl>(entries.size());
for (TabIdAndUrl entry : entries) {
tabConnectors.add(new TabConnectorImpl(entry.id, entry.url, ticket));
}
return tabConnectors;
}
public void dismiss() {
ticket.dismiss();
}
}
private class TabConnectorImpl implements TabConnector {
private final int tabId;
private final String url;
// Ticket that we inherit from TabFetcher.
private final SessionManager.Ticket<Session> ticket;
TabConnectorImpl(int tabId, String url, SessionManager.Ticket<Session> ticket) {
this.tabId = tabId;
this.url = url;
this.ticket = ticket;
}
public String getUrl() {
return url;
}
public boolean isAlreadyAttached() {
return ticket.getSession().tabId2ToolHandler.get(tabId) != null;
}
public BrowserTab attach(TabDebugEventListener listener)
throws IOException, MethodIsBlockingException {
SessionManager.Ticket<Session> ticket;
try {
ticket = connectInternal();
} catch (UnsupportedVersionException e) {
// This exception should have happened on tab fetcher creation.
throw JavascriptVmImpl.newIOException("Unexpected version problem", e);
}
Session session = ticket.getSession();
BrowserTabImpl browserTab = null;
try {
browserTab = new BrowserTabImpl(tabId, session.sessionConnection, ticket);
} finally {
if (browserTab == null) {
ticket.dismiss();
}
}
// From now on browserTab is responsible for the ticket.
browserTab.attach(listener);
browserTab.setUrl(url);
return browserTab;
}
}
/**
* With this session manager we expect all ticket owners to call dismiss in any
* circumstances.
*/
private class ConnectionSessionManager extends
SessionManager<BrowserImpl.Session, ExceptionWrapper> {
@Override
protected Session newSessionObject() throws ExceptionWrapper {
try {
return new Session();
} catch (IOException e) {
throw ExceptionWrapper.wrap(e);
} catch (UnsupportedVersionException e) {
throw ExceptionWrapper.wrap(e);
}
}
}
private static abstract class ExceptionWrapper extends Exception {
abstract void rethrow() throws IOException, UnsupportedVersionException;
static ExceptionWrapper wrap(final IOException e) {
return new ExceptionWrapper() {
@Override
void rethrow() throws IOException {
throw e;
}
};
}
static ExceptionWrapper wrap(final UnsupportedVersionException e) {
return new ExceptionWrapper() {
@Override
void rethrow() throws UnsupportedVersionException {
throw e;
}
};
}
}
public boolean isTabConnectedForTest(int tabId) {
Session session = sessionManager.getCurrentSessionForTest();
if (session == null) {
return false;
}
return session.tabId2ToolHandler.get(tabId) != null;
}
public DevToolsServiceHandler getDevToolsServiceHandlerForTests() {
return sessionManager.getCurrentSessionForTest().getDevToolsServiceHandler();
}
public boolean isConnectedForTests() {
return sessionManager.getCurrentSessionForTest() != null;
}
}