From a641c3c23d471ad8a5ad6f230bc39ca24a3e8d0d Mon Sep 17 00:00:00 2001 From: Andrzej Kapolka Date: Mon, 12 Sep 2011 05:29:53 +0000 Subject: [PATCH] Provide a means of exchanging messages between the JavaScript context of the page hosting the applet and the launched application through a simple server socket bound to the loopback address on any available port (where the port is passed to the application as a system property). Not tested yet, but it shouldn't affect any deployments that don't specifically enable it. --- build.xml | 13 +- .../threerings/getdown/data/Properties.java | 4 + .../getdown/launcher/GetdownApplet.java | 136 ++++++++++++++++++ .../getdown/launcher/GetdownAppletConfig.java | 11 +- 4 files changed, 159 insertions(+), 5 deletions(-) diff --git a/build.xml b/build.xml index 7b7957f..af66c19 100644 --- a/build.xml +++ b/build.xml @@ -15,7 +15,7 @@ - @@ -23,12 +23,16 @@ - + + + + + @@ -124,13 +128,14 @@ - + @@ -149,7 +154,7 @@ - diff --git a/src/main/java/com/threerings/getdown/data/Properties.java b/src/main/java/com/threerings/getdown/data/Properties.java index f33a033..f5d2d25 100644 --- a/src/main/java/com/threerings/getdown/data/Properties.java +++ b/src/main/java/com/threerings/getdown/data/Properties.java @@ -32,4 +32,8 @@ public class Properties { /** This property will be set to "true" on the application when it is being run by getdown. */ public static final String GETDOWN = "com.threerings.getdown"; + + /** If accepting connections from the launched application, this property + * will be set to the connection server port. */ + public static final String CONNECT_PORT = "com.threerings.getdown.connectPort"; } diff --git a/src/main/java/com/threerings/getdown/launcher/GetdownApplet.java b/src/main/java/com/threerings/getdown/launcher/GetdownApplet.java index 7ab498c..3711f35 100644 --- a/src/main/java/com/threerings/getdown/launcher/GetdownApplet.java +++ b/src/main/java/com/threerings/getdown/launcher/GetdownApplet.java @@ -27,12 +27,17 @@ package com.threerings.getdown.launcher; import java.awt.Container; import java.awt.Image; +import java.io.DataInputStream; +import java.io.DataOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintStream; +import java.net.InetAddress; import java.net.MalformedURLException; +import java.net.ServerSocket; +import java.net.Socket; import java.net.URL; import java.util.ArrayList; import java.util.List; @@ -44,6 +49,11 @@ import java.security.cert.CertificateFactory; import javax.swing.JApplet; import javax.swing.JPanel; +import netscape.javascript.JSObject; + +import com.threerings.getdown.data.Application; +import com.threerings.getdown.data.Properties; + import static com.threerings.getdown.Log.log; /** @@ -52,6 +62,35 @@ import static com.threerings.getdown.Log.log; public class GetdownApplet extends JApplet implements ImageLoader { + /** + * Sets the JavaScript callback to invoke when a message is received from the launched app. + * The callback should be a function that accepts a single string parameter (the received + * message). + */ + public synchronized void setMessageCallback (JSObject callback) + { + _messageCallback = callback; + } + + /** + * Attempts to send a message to the launched app. + * + * @return true if we succeeded in sending the message, false if the launched app has not (yet) + * established a connection to Getdown, or the send failed. + */ + public synchronized boolean sendMessage (String message) + { + if (_connectOut != null) { + try { + _connectOut.writeUTF(message); + return true; + } catch (IOException e) { + log.warning("Error sending message to app.", "message", message, e); + } + } + return false; + } + @Override // documentation inherited public void init () { @@ -108,6 +147,19 @@ public class GetdownApplet extends JApplet return GetdownApplet.this; } @Override + protected void launch () { + // if so configured, create a server socket to listen + // for a connection from the app + if (_config.allowConnect) { + try { + startConnectServer(); + } catch (IOException e) { + log.warning("Failed to start connect server.", e); + } + } + super.launch(); + } + @Override protected void exit (int exitCode) { _app.releaseLock(); _config.redirect(); @@ -128,6 +180,62 @@ public class GetdownApplet extends JApplet } } + /** + * Attempts to start the server that will accept a connection from the launched app, allowing + * it to exchange messages with the JavaScript context. + */ + protected void startConnectServer () + throws IOException + { + // bind and set a property with the local port that will be passed through to the app + _serverSocket = new ServerSocket(0, 0, InetAddress.getByName(null)); + System.setProperty(Application.PROP_PASSTHROUGH_PREFIX + Properties.CONNECT_PORT, + String.valueOf(_serverSocket.getLocalPort())); + Thread thread = new Thread("ConnectServer") { + @Override + public void run () { + while (true) { + try { + acceptConnection(); + } catch (IOException e) { + if (!_serverSocket.isClosed()) { + log.warning("Error accepting connection.", e); + } + break; + } + } + } + protected void acceptConnection () throws IOException { + Socket socket = _serverSocket.accept(); + DataInputStream connectIn = new DataInputStream(socket.getInputStream()); + synchronized (GetdownApplet.this) { + _connectOut = new DataOutputStream(socket.getOutputStream()); + } + while (true) { + try { + String message = connectIn.readUTF(); + synchronized (GetdownApplet.this) { + if (_messageCallback != null) { + _messageCallback.call("call", + new Object[] { _messageCallback, message }); + } + } + } catch (IOException e) { + if (!socket.isClosed()) { + log.warning("Error reading message.", e); + } + break; + } + } + synchronized (GetdownApplet.this) { + _connectOut = null; + } + } + }; + thread.setDaemon(true); + thread.start(); + } + // implemented from ImageLoader public Image loadImage (String path) { @@ -163,6 +271,25 @@ public class GetdownApplet extends JApplet _getdown._app.releaseLock(); } + @Override // documentation inherited + public synchronized void destroy () + { + if (_serverSocket != null) { + try { + _serverSocket.close(); + } catch (IOException e) { + log.warning("Error closing server socket.", e); + } + } + if (_connectOut != null) { + try { + _connectOut.close(); + } catch (IOException e) { + log.warning("Error closing connect socket/output stream.", e); + } + } + } + /** * Creates the specified file and writes the supplied contents to it. */ @@ -207,4 +334,13 @@ public class GetdownApplet extends JApplet /** An error encountered during initialization. */ protected String _errmsg; + + /** The message callback registered by JavaScript on the containing page, if any. */ + protected JSObject _messageCallback; + + /** The server socket on which we listen for connections, if any. */ + protected ServerSocket _serverSocket; + + /** The output stream to the launched app, if a connection has been established. */ + protected DataOutputStream _connectOut; } diff --git a/src/main/java/com/threerings/getdown/launcher/GetdownAppletConfig.java b/src/main/java/com/threerings/getdown/launcher/GetdownAppletConfig.java index 9f5d73b..a0fa155 100644 --- a/src/main/java/com/threerings/getdown/launcher/GetdownAppletConfig.java +++ b/src/main/java/com/threerings/getdown/launcher/GetdownAppletConfig.java @@ -57,6 +57,7 @@ public class GetdownAppletConfig public static final String ERRORBGIMAGE = "errorbgimage"; public static final String PARAM_DELIMITER = "."; public static final String DIRECT = "direct"; + public static final String CONNECT = "connect"; public static final String REDIRECT_ON_FINISH = "redirect_on_finish"; public static final String REDIRECT_ON_FINISH_TARGET = "redirect_on_finish_target"; @@ -77,7 +78,7 @@ public class GetdownAppletConfig * } * When a number is reached for which no value exists, we stop looking. */ public static final String APPARG_PREFIX = "appargs"; - + public String appbase; public String appname; @@ -107,6 +108,11 @@ public class GetdownAppletConfig * separate java process (false). */ public boolean invokeDirect; + /** Indicates whether Getdown should allow the launched app to connect to it through a server + * socket bound to localhost on any available port in order to allow interaction with + * JavaScript code on the page containing the applet. */ + public boolean allowConnect; + /** Optional default bounds for the status panel. */ public Rectangle statusBounds; @@ -183,6 +189,9 @@ public class GetdownAppletConfig String direct = getParameter(DIRECT, "false"); invokeDirect = Boolean.valueOf(direct); + String connect = getParameter(CONNECT, "false"); + allowConnect = Boolean.valueOf(connect); + String redirectURL = getParameter(REDIRECT_ON_FINISH); if (redirectURL != null) { try {