blob: fa5adf0e5a929e110e0d17681d2a3241909e44d8 [file] [log] [blame]
/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. 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 com.google.gwt.dev.shell;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.TreeLogger.HelpInfo;
import com.google.gwt.dev.shell.BrowserChannel.SessionHandler.ExceptionOrReturnValue;
import com.google.gwt.dev.shell.JsValue.DispatchObject;
import com.google.gwt.dev.util.log.dashboard.DashboardNotifier;
import com.google.gwt.dev.util.log.dashboard.DashboardNotifierFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/**
* Server-side of the browser channel protocol.
*/
public class BrowserChannelServer extends BrowserChannel
implements Runnable {
/**
* Hook interface for responding to messages from the client.
*/
public abstract static class SessionHandlerServer extends SessionHandler<BrowserChannelServer> {
public abstract ExceptionOrReturnValue getProperty(BrowserChannelServer channel,
int refId, int dispId);
public abstract ExceptionOrReturnValue invoke(BrowserChannelServer channel,
Value thisObj, int dispId, Value[] args);
/**
* Load a new instance of a module.
*
* @param channel
* @param moduleName
* @param userAgent
* @param url top-level URL of the main page, null if using an old plugin
* @param tabKey opaque key of the tab, may be empty if the plugin can't
* distinguish tabs or null if using an old plugin
* @param sessionKey opaque key for this session, null if using an old plugin
* @param userAgentIcon byte array containing an icon (which fits within
* 24x24) representing the user agent or null if unavailable
* @return a TreeLogger to use for the module's logs, or null if the module
* load failed
*/
public abstract TreeLogger loadModule(BrowserChannelServer channel,
String moduleName, String userAgent, String url, String tabKey,
String sessionKey, byte[] userAgentIcon);
public abstract ExceptionOrReturnValue setProperty(BrowserChannelServer channel,
int refId, int dispId, Value newValue);
public abstract void unloadModule(BrowserChannelServer channel, String moduleName);
}
private static class ServerObjectRefFactory implements ObjectRefFactory {
private final RemoteObjectTable<JsObjectRef> remoteObjectTable;
public ServerObjectRefFactory() {
remoteObjectTable = new RemoteObjectTable<JsObjectRef>();
}
public JavaObjectRef getJavaObjectRef(int refId) {
return new JavaObjectRef(refId);
}
public JsObjectRef getJsObjectRef(int refId) {
JsObjectRef objectRef = remoteObjectTable.getRemoteObjectRef(refId);
if (objectRef == null) {
objectRef = new JsObjectRef(refId);
remoteObjectTable.putRemoteObjectRef(refId, objectRef);
}
return objectRef;
}
public Set<Integer> getRefIdsForCleanup() {
return remoteObjectTable.getRefIdsForCleanup();
}
}
/**
* Full qualified class name of JavaScriptObject. This must be a string
* because this class is in a different class loader.
*/
public static final String JSO_CLASS = "com.google.gwt.core.client.JavaScriptObject";
private static Map<String, byte[]> iconCache = new HashMap<String, byte[]>();
private static final Object cacheLock = new Object();
private DevModeSession devModeSession;
private final SessionHandlerServer handler;
private final boolean ignoreRemoteDeath;
private final ServerObjectsTable javaObjectsInBrowser = new ServerObjectsTable();
private TreeLogger logger;
private String moduleName;
private String userAgent;
private int protocolVersion = -1;
/**
* Create a code server for the supplied socket.
*
* @param initialLogger
* @param socket
* @param handler
* @param ignoreRemoteDeath
* @throws IOException
*/
public BrowserChannelServer(TreeLogger initialLogger, Socket socket,
SessionHandlerServer handler, boolean ignoreRemoteDeath) throws IOException {
super(socket, new ServerObjectRefFactory());
this.handler = handler;
this.ignoreRemoteDeath = ignoreRemoteDeath;
init(initialLogger);
}
// @VisibleForTesting
BrowserChannelServer(TreeLogger initialLogger, InputStream inputStream,
OutputStream outputStream, SessionHandlerServer handler,
boolean ignoreRemoteDeath) {
super(inputStream, outputStream, new ServerObjectRefFactory());
this.handler = handler;
this.ignoreRemoteDeath = ignoreRemoteDeath;
init(initialLogger);
}
/**
* Indicate that Java no longer has references to the supplied JS objects.
*
* @param ids array of JS object IDs that have been freeded
*/
public void freeJsValue(int[] ids) {
try {
new FreeMessage(this, ids).send();
} catch (IOException e) {
// TODO(jat): error handling?
e.printStackTrace();
throw new HostedModeException("I/O error communicating with client");
}
}
/**
* Returns the {@code DevModeSession} representing this browser connection.
*/
public DevModeSession getDevModeSession() {
return devModeSession;
}
/**
* @return the table of Java objects which have been sent to the browser.
*/
public ServerObjectsTable getJavaObjectsExposedInBrowser() {
return javaObjectsInBrowser;
}
/**
* @return the negotiated protocol version, or -1 if not yet negotiated.
*/
public int getProtocolVersion() {
return protocolVersion;
}
public ReturnMessage invoke(String methodName, Value vthis, Value[] vargs,
SessionHandlerServer handler) throws IOException, BrowserChannelException {
new InvokeOnClientMessage(this, methodName, vthis, vargs).send();
return reactToMessagesWhileWaitingForReturn(handler);
}
/**
* @param ccl
* @param jsthis
* @param methodName
* @param args
* @param returnJsValue
* @throws Throwable
*/
public void invokeJavascript(CompilingClassLoader ccl, JsValueOOPHM jsthis,
String methodName, JsValueOOPHM[] args, JsValueOOPHM returnJsValue)
throws Throwable {
final ServerObjectsTable remoteObjects = getJavaObjectsExposedInBrowser();
Value vthis = convertFromJsValue(remoteObjects, jsthis);
Value[] vargs = new Value[args.length];
for (int i = 0; i < args.length; ++i) {
vargs[i] = convertFromJsValue(remoteObjects, args[i]);
}
try {
InvokeOnClientMessage invokeMessage = new InvokeOnClientMessage(this,
methodName, vthis, vargs);
invokeMessage.send();
final ReturnMessage msg = reactToMessagesWhileWaitingForReturn(handler);
Value returnValue = msg.getReturnValue();
convertToJsValue(ccl, remoteObjects, returnValue, returnJsValue);
if (msg.isException()) {
Object exceptionValue;
if (returnValue.isNull() || returnValue.isUndefined()) {
exceptionValue = null;
} else if (returnValue.isString()) {
exceptionValue = returnValue.getString();
} else if (returnValue.isJsObject()) {
exceptionValue = JsValueGlue.createJavaScriptObject(returnJsValue,
ccl);
} else if (returnValue.isJavaObject()) {
Object object = remoteObjects.get(returnValue.getJavaObject().getRefid());
Object target = ((JsValueOOPHM.DispatchObjectOOPHM) object).getTarget();
if (target instanceof Throwable) {
throw (Throwable) (target);
} else {
// JS throwing random Java Objects, which we'll wrap in JSException
exceptionValue = target;
}
} else {
// JS throwing random primitives, which we'll wrap as a string in
// JSException
exceptionValue = returnValue.getValue().toString();
}
RuntimeException exception = ModuleSpace.createJavaScriptException(
ccl, exceptionValue);
// reset the stack trace to here to minimize GWT infrastructure in
// the stack trace
exception.fillInStackTrace();
throw exception;
}
} catch (IOException e) {
throw new RemoteDeathError(e);
} catch (BrowserChannelException e) {
throw new RemoteDeathError(e);
}
}
/**
* Load the supplied JSNI code into the browser.
*
* @param jsni JSNI source to load into the browser
*/
public void loadJsni(String jsni) {
try {
LoadJsniMessage jsniMessage = new LoadJsniMessage(this, jsni);
jsniMessage.send();
// we do not wait for a return value
} catch (IOException e) {
throw new RemoteDeathError(e);
}
}
/**
* React to messages from the other side, where no return value is expected.
*
* @param handler
* @throws RemoteDeathError
*/
public void reactToMessages(SessionHandlerServer handler) {
do {
try {
getStreamToOtherSide().flush();
MessageType messageType = Message.readMessageType(
getStreamFromOtherSide());
switch (messageType) {
case FREE_VALUE:
final FreeMessage freeMsg = FreeMessage.receive(this);
handler.freeValue(this, freeMsg.getIds());
break;
case INVOKE:
InvokeOnServerMessage imsg = InvokeOnServerMessage.receive(this);
ExceptionOrReturnValue result = handler.invoke(this, imsg.getThis(),
imsg.getMethodDispatchId(), imsg.getArgs());
sendFreedValues();
ReturnMessage.send(this, result);
break;
case INVOKE_SPECIAL:
handleInvokeSpecial(handler);
break;
case QUIT:
return;
default:
throw new RemoteDeathError(new BrowserChannelException(
"Invalid message type " + messageType));
}
} catch (IOException e) {
throw new RemoteDeathError(e);
} catch (BrowserChannelException e) {
throw new RemoteDeathError(e);
}
} while (true);
}
/**
* React to messages from the other side, where a return value is expected.
*
* @param handler
* @throws BrowserChannelException
* @throws RemoteDeathError
*/
public ReturnMessage reactToMessagesWhileWaitingForReturn(
SessionHandlerServer handler) throws BrowserChannelException, RemoteDeathError {
do {
try {
getStreamToOtherSide().flush();
MessageType messageType = Message.readMessageType(
getStreamFromOtherSide());
switch (messageType) {
case FREE_VALUE:
final FreeMessage freeMsg = FreeMessage.receive(this);
handler.freeValue(this, freeMsg.getIds());
break;
case RETURN:
return ReturnMessage.receive(this);
case INVOKE:
InvokeOnServerMessage imsg = InvokeOnServerMessage.receive(this);
ExceptionOrReturnValue result = handler.invoke(this, imsg.getThis(),
imsg.getMethodDispatchId(), imsg.getArgs());
sendFreedValues();
ReturnMessage.send(this, result);
break;
case INVOKE_SPECIAL:
handleInvokeSpecial(handler);
break;
case QUIT:
// if we got an unexpected QUIT here, the remote plugin probably
// realized it was dying and had time to close the socket properly.
throw new RemoteDeathError(null);
default:
throw new BrowserChannelException("Invalid message type "
+ messageType + " received waiting for return.");
}
} catch (IOException e) {
throw new RemoteDeathError(e);
} catch (BrowserChannelException e) {
throw new RemoteDeathError(e);
}
} while (true);
}
public void run() {
try {
processConnection();
} catch (IOException e) {
logger.log(TreeLogger.WARN, "Client connection lost", e);
} catch (BrowserChannelException e) {
logger.log(TreeLogger.ERROR,
"Unrecognized command for client; closing connection", e);
} finally {
try {
shutdown();
} catch (IOException ignored) {
}
endSession();
}
}
/**
* Close the connection to the browser.
*
* @throws IOException
*/
public void shutdown() throws IOException {
getDashboardNotifier().devModeSessionEnded(devModeSession);
QuitMessage.send(this);
}
// @VisibleForTesting
protected void processConnection() throws IOException, BrowserChannelException {
MessageType type = Message.readMessageType(getStreamFromOtherSide());
// TODO(jat): add support for getting the a shim plugin downloading the
// real plugin via a GetRealPlugin message before CheckVersions
String url = null;
String tabKey = null;
String sessionKey = null;
byte[] iconBytes = null;
switch (type) {
case OLD_LOAD_MODULE:
// v1 client
OldLoadModuleMessage oldLoadModule = OldLoadModuleMessage.receive(this);
if (oldLoadModule.getProtoVersion() != 1) {
// This message type was only used in v1, so something is really
// broken here.
throw new BrowserChannelException(
"Old LoadModule message used, but not v1 protocol");
}
moduleName = oldLoadModule.getModuleName();
userAgent = oldLoadModule.getUserAgent();
protocolVersion = 1;
HelpInfo helpInfo = new HelpInfo() {
@Override
public String getAnchorText() {
return "UsingOOPHM wiki page";
}
@Override
public URL getURL() {
try {
// TODO(jat): better landing page for more info
return new URL(
"http://code.google.com/p/google-web-toolkit/wiki/UsingOOPHM");
} catch (MalformedURLException e) {
// can't happen
return null;
}
}
};
logger.log(TreeLogger.WARN, "Connection from old browser plugin -- "
+ "please upgrade to a later version for full functionality", null,
helpInfo);
break;
case CHECK_VERSIONS:
String connectError = null;
CheckVersionsMessage hello = CheckVersionsMessage.receive(this);
int minVersion = hello.getMinVersion();
int maxVersion = hello.getMaxVersion();
String hostedHtmlVersion = hello.getHostedHtmlVersion();
if (minVersion > PROTOCOL_VERSION_CURRENT
|| maxVersion < PROTOCOL_VERSION_OLDEST) {
connectError = "Client supported protocol version range "
+ minVersion + " - " + maxVersion + "; server "
+ PROTOCOL_VERSION_OLDEST + " - " + PROTOCOL_VERSION_CURRENT;
} else {
if (!HostedHtmlVersion.validHostedHtmlVersion(logger,
hostedHtmlVersion)) {
new FatalErrorMessage(this,
"Invalid hosted.html version - check log window").send();
return;
}
}
if (connectError != null) {
logger.log(TreeLogger.ERROR, "Connection error: " + connectError,
null);
new FatalErrorMessage(this, connectError).send();
return;
}
protocolVersion = Math.min(PROTOCOL_VERSION_CURRENT, maxVersion);
new ProtocolVersionMessage(this, protocolVersion).send();
type = Message.readMessageType(getStreamFromOtherSide());
// Optionally allow client to request switch of transports. Inband is
// always supported, so a return of an empty transport string requires
// the client to stay in this channel.
if (type == MessageType.CHOOSE_TRANSPORT) {
ChooseTransportMessage chooseTransport = ChooseTransportMessage.receive(this);
String transport = selectTransport(chooseTransport.getTransports());
String transportArgs = null;
if (transport != null) {
transportArgs = createTransport(transport);
}
new SwitchTransportMessage(this, transport, transportArgs).send();
type = Message.readMessageType(getStreamFromOtherSide());
}
// Now we expect a LoadModule message to load a GWT module.
if (type != MessageType.LOAD_MODULE) {
logger.log(TreeLogger.ERROR, "Unexpected message type " + type
+ "; expecting LoadModule");
return;
}
LoadModuleMessage loadModule = LoadModuleMessage.receive(this);
url = loadModule.getUrl();
tabKey = loadModule.getTabKey();
sessionKey = loadModule.getSessionKey();
moduleName = loadModule.getModuleName();
userAgent = loadModule.getUserAgent();
break;
case REQUEST_PLUGIN:
logger.log(TreeLogger.ERROR, "Plugin download not supported yet");
// We can't clear the socket since we don't know how to interpret this
// message yet -- it is only here now so we can give a better error
// message with mixed versions once it is supported.
new FatalErrorMessage(this, "Plugin download not supported").send();
return;
default:
logger.log(TreeLogger.ERROR, "Unexpected message type " + type
+ "; expecting CheckVersions");
return;
}
if (protocolVersion >= PROTOCOL_VERSION_GET_ICON) {
synchronized (cacheLock) {
if (iconCache.containsKey(userAgent)) {
iconBytes = iconCache.get(userAgent);
} else {
RequestIconMessage.send(this);
type = Message.readMessageType(getStreamFromOtherSide());
if (type != MessageType.USER_AGENT_ICON) {
logger.log(TreeLogger.ERROR, "Unexpected message type " + type
+ "; expecting UserAgentIcon");
return;
}
UserAgentIconMessage uaIconMessage = UserAgentIconMessage.receive(
this);
iconBytes = uaIconMessage.getIconBytes();
iconCache.put(userAgent, iconBytes);
}
}
}
Thread.currentThread().setName(
"Code server for " + moduleName + " from " + userAgent + " on " + url
+ " @ " + sessionKey);
createDevModeSession();
logger = handler.loadModule(this, moduleName, userAgent, url,
tabKey, sessionKey, iconBytes);
if (logger == null) {
// got an error
try {
Value errMsg = new Value();
errMsg.setString("An error occurred loading the GWT module "
+ moduleName);
ReturnMessage.send(this, true, errMsg);
return;
} catch (IOException e) {
throw new RemoteDeathError(e);
}
}
try {
// send LoadModule response
try {
ReturnMessage.send(this, false, new Value());
} catch (IOException e) {
throw new RemoteDeathError(e);
}
reactToMessages(handler);
} catch (RemoteDeathError e) {
if (!ignoreRemoteDeath) {
logger.log(TreeLogger.ERROR, e.getMessage(), e);
}
} finally {
handler.unloadModule(this, moduleName);
}
}
/**
* Convert a JsValue into a BrowserChannel Value.
*
* @param localObjects lookup table for local objects -- may be null if jsval
* is known to be a primitive (including String).
* @param jsval value to convert
* @return jsval as a Value object.
*/
Value convertFromJsValue(ServerObjectsTable localObjects, JsValueOOPHM jsval) {
Value value = new Value();
if (jsval.isNull()) {
value.setNull();
} else if (jsval.isUndefined()) {
value.setUndefined();
} else if (jsval.isBoolean()) {
value.setBoolean(jsval.getBoolean());
} else if (jsval.isInt()) {
value.setInt(jsval.getInt());
} else if (jsval.isNumber()) {
value.setDouble(jsval.getNumber());
} else if (jsval.isString()) {
value.setString(jsval.getString());
} else if (jsval.isJavaScriptObject()) {
value.setJsObject(jsval.getJavascriptObject());
} else if (jsval.isWrappedJavaObject()) {
assert localObjects != null;
DispatchObject javaObj = jsval.getJavaObjectWrapper();
value.setJavaObject(new JavaObjectRef(localObjects.add(javaObj)));
} else if (jsval.isWrappedJavaFunction()) {
assert localObjects != null;
value.setJavaObject(new JavaObjectRef(
localObjects.add(jsval.getWrappedJavaFunction())));
} else {
throw new RuntimeException("Unknown JsValue type " + jsval);
}
return value;
}
/**
* Convert a BrowserChannel Value into a JsValue.
*
* @param ccl Compiling class loader, may be null if val is known to not be a
* Java object or exception.
* @param localObjects table of Java objects, may be null as above.
* @param val Value to convert
* @param jsval JsValue object to receive converted value.
*/
void convertToJsValue(CompilingClassLoader ccl, ServerObjectsTable localObjects,
Value val, JsValueOOPHM jsval) {
switch (val.getType()) {
case NULL:
jsval.setNull();
break;
case BOOLEAN:
jsval.setBoolean(val.getBoolean());
break;
case BYTE:
jsval.setByte(val.getByte());
break;
case CHAR:
jsval.setChar(val.getChar());
break;
case DOUBLE:
jsval.setDouble(val.getDouble());
break;
case FLOAT:
jsval.setDouble(val.getFloat());
break;
case INT:
jsval.setInt(val.getInt());
break;
case LONG:
jsval.setDouble(val.getLong());
break;
case SHORT:
jsval.setShort(val.getShort());
break;
case STRING:
jsval.setString(val.getString());
break;
case UNDEFINED:
jsval.setUndefined();
break;
case JS_OBJECT:
jsval.setJavascriptObject(val.getJsObject());
break;
case JAVA_OBJECT:
assert ccl != null && localObjects != null;
jsval.setWrappedJavaObject(ccl,
localObjects.get(val.getJavaObject().getRefid()));
break;
}
}
/**
* Returns the {@code DashboardNotifier} used to send notices to a dashboard
* service.
*/
// @VisibleForTesting
DashboardNotifier getDashboardNotifier() {
return DashboardNotifierFactory.getNotifier();
}
/**
* Creates the {@code DevModeSession} that represents the current browser
* connection, sets it as the "default" session for the current thread, and
* notifies a GWT Dashboard.
*/
private void createDevModeSession() {
devModeSession = new DevModeSession(moduleName, userAgent);
DevModeSession.setSessionForCurrentThread(devModeSession);
getDashboardNotifier().devModeSession(devModeSession);
}
/**
* Create the requested transport and return the appropriate information so
* the client can connect to the same transport.
*
* @param transport transport name to create
* @return transport-specific arguments for the client to use in attaching
* to this transport
*/
private String createTransport(String transport) {
// TODO(jat): implement support for additional transports
throw new UnsupportedOperationException(
"No alternate transports supported");
}
private void handleInvokeSpecial(SessionHandlerServer handler) throws IOException,
BrowserChannelException {
final InvokeSpecialMessage ismsg = InvokeSpecialMessage.receive(this);
Value[] args = ismsg.getArgs();
ExceptionOrReturnValue retExc = null;
switch (ismsg.getDispatchId()) {
case GetProperty:
assert args.length == 2;
retExc = handler.getProperty(this, args[0].getInt(), args[1].getInt());
break;
case SetProperty:
assert args.length == 3;
retExc = handler.setProperty(this, args[0].getInt(), args[1].getInt(),
args[2]);
break;
default:
throw new HostedModeException("Unexpected InvokeSpecial method "
+ ismsg.getDispatchId());
}
ReturnMessage.send(this, retExc);
}
private void init(TreeLogger initialLogger) {
this.logger = initialLogger;
Thread thread = new Thread(this);
thread.setDaemon(true);
thread.setName("Code server (initializing)");
thread.start();
}
/**
* Select a transport from those provided by the client.
*
* @param transports array of supported transports
* @return null to continue in-band, or a transport type
*/
private String selectTransport(String[] transports) {
// TODO(jat): add support for shared memory, others
return null;
}
}