Friday, April 15, 2011

Inter JVM Communication

Ever have an application that should only have one instance of it running?  Well, I do... I have a Swing application, and it integrates with a device via USB.  I want to ensure that there's only one player in the game at a given time.

How can one Java process know if another with the same application is running?  There seems to be a lot of suggestions out there: RMI, Cajo, Terracotta, etc, but since I am using Groovy, I figured there'd be an easier way.  Turns out (for my use case), there is.  Some of the dynamic I/O methods added to java.net.Socket and java.net.ServerSocket make this a snap.

My basic strategy goes something like this:
  • Implement a single method to determine if the process can run.  If it can, fire up a server process, and then store and encrypt the server's port number in a File and return true.  If it can't, return false.
  • On invocation, if there is no file, or an invalid port number (non-Integer), it's safe to say that no other Java process for the given application is running.
  • If there is a valid port, create a socket connection and issue a simple 'ping' following an incredibly simple protocol.  If the server process is not available, or if that process doesn't respond properly to the ping, it's safe to say the application in question is not running.
  • If, however, the ping returns as expected, we know that this application is running already, and we want to return false and then shut down.
  • If it's determined that no other process is running, start up a server socket in another thread, encrypt the port created, and store it in the port file.
The obvious weakness in this strategy is that a user could potentially modify the encrypted file, but for my use case, we can live with that risk.

Also, this solution is certainly doable with just Java, but it's just a lot damn easier with some of the slick dynamic Groovy I/O methods.  Here's the code:


  /**
   * By examining the file, determine if a a socket thread is holding on to the specified port.  If we can 
   * communicate on that port, it's safe to say that another instance of this app is running, and this 
   * method returns false
   *
   * @param portFile file that contains encrypted port.  If null, or invalid, this method will assume 
   *        we can run
   * @return true if this app is launchable
   */
  static boolean isLaunchable(File portFile) {
    boolean ret = true
    if (portFile.exists()) {
      Socket client = null
      try {
        String decrypted = new String(MyUtils.decryptAndDecode(portFile.text))
        if (decrypted.isInteger()) {
          String ping = "ping"
          client = new Socket((String) null, decrypted.toInteger())
          client.setSoTimeout(1000)
          client << ping //send ping to the server
          client.inputStream.withStream {InputStream sis ->
            sis.eachByte(256) {byte[] buff, int read ->
              ret = (ping != new String(buff, 0, read)) //response same? we are not launchable!
            }
          }
        }
      } catch (Exception e) {
        log.debug("Couldn't determine server running. ${e}")
      } finally {
        if (client) client.close()
      }
    }

    if (ret) { //we're launchable, now grab hold of a server connection and encrypt port to file
      Thread.startDaemon("isLaunchableServerSocket") {
        ServerSocket serverSocket = new ServerSocket()
        serverSocket.bind(null) //creates a dynamic port
        portFile.text = MyUtils.encryptAndEncode(serverSocket.localPort.toString().getBytes())
        while (!serverSocket.isClosed()) {
          Socket socketConn = null
          try {
            socketConn = serverSocket.accept() 
            socketConn.inputStream.withStream {InputStream sis ->
              sis.eachByte(256) {byte[] buff, int read ->
                String msg = new String(buff, 0, read)
                if ("close" != msg) {
                  socketConn.outputStream << msg //just ping it right back
                } else {
                  serverSocket.close()
                }
              }
            }
          } catch (java.net.SocketException e) {
            log.debug("Shutting down local socket.")
            portFile.text = "" //wipe out the text of the port file
          } finally {
            if (socketConn && !socketConn.isClosed()) socketConn.close()
          }
        }
      }
    }
    return ret
  }

9 comments:

Kovica said...

You could also use FileChannel for that.
More info at http://kovica.blogspot.com/2011/04/is-my-application-running.html

Roshan said...

Yup, FileChannel is the way to go. The Socket method will probably fail if two or more instances are run almost simultaneously, especially if the computer is underpowered.

Javin Paul said...

This is something really useful can be required in some other scenario as well where not just user but application require not to have two instance running simultaneously.

Thanks
Javin
How Garbage collection works in Java

Brock Heinz said...

@Kovica / @Roshan,

Good call, I knew that, besides the obvious possibility that a user could alter the 'port file', I'd also have to deal with the (small) chance that a second instance of the app could be launched before the first started the server socket thread.

I'm definitely going to look into FileChannel. Assuming it works in OS X, I'll give that ago. Thanks for the feedback :)

Daniel Manzke said...

There is a pretty easier way. ;)

Use JavaWebStart. Java Web Start is able to start an Application only once. If it was already launched, the running instance becomes the Information about the invoke.

I use it for an Application, which process Zip-Files. If an Instance is running, it retrieves the location for the new Zip-File. If not running, a new one will be startet.

Brock Heinz said...

Interesting, Daniel.

Can you provide more information to that? I'm looking at a JNLP guide, and I'm just not seeing what you're describing.

Brock Heinz said...

Daniel, are you speaking of this?

SingleInstanceService

Sandeep said...

Thanks for this great article
Extreme Java

Brock Heinz said...

Here's my FileChannel implementation:

boolean launchable
def raf = new RandomAccessFile(lockFile, "rwd")
try {
def chan = raf.channel
launchable = (chan.tryLock() != null)
if (launchable) chan.write(ByteBuffer.wrap(new Date().time.toString().getBytes()))
} catch (Exception e) {
log.debug("Could not lock file due to ${e}", e)
launchable = false
}
return launchable