/* * [SingleInstance.java] * * Summary: Arrange for single instance using UDP. * * Copyright: (c) 2013-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 1.0 2013-01-08 initial version */ package com.mindprod.singleinstance; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.MulticastSocket; import java.util.UUID; import java.util.concurrent.TimeUnit; import static java.lang.System.*; /** * Arrange for single instance using UDP. *

* Based no the work of Knut Johnson and Petter Dunhio. * * @author Roedy Green, Canadian Mind Products * @version 1.0 2013-01-08 initial version * @since 2013-01-08 */ public class SingleInstance implements Runnable { /** * multicast IP used to rendezvous all apps that want to be a single instance */ private final InetAddress group; /** * multicast socket uset to distributed messages to all apps that want to be a single instance */ private final MulticastSocket socket; /** * which apps to avoid. avoids[0] is the name of this app */ private final String[] avoids; /** * multicast port used to rendezvous all apps that want to be a single instance */ private final int port; /** * how long to wait in ms for messages from other instances before deciding we are alone. */ private final long delay; /** * low bits of 128 bit unique identifier for this instance */ private final long uuidLsb; /** * high bits of 128 bit unique identifier for this instance */ private final long uuidMsb; /** * timestamp when this app started execution */ private final long whenIStarted; /** * datagram packat for outgoing DataPacket */ private DatagramPacket sendDP; /** * thread to listen to other instances */ private volatile Thread thread; /** * true if we should keep going. No other instances, or other instances came after us. */ private volatile boolean keepListening; /** * buffer for outgoing DataPacket */ private byte[] sendBuf; /** * constructor * * @param multicastIP value in range 224.x.x.x .. 239.x.x.x we hope is unused for any other purpose. * @param port port 16 bit port number 1024 .. 65535 we hope will be unused for any other purpose. * @param delay how long to wait in ms for messages from other instances before deciding we are alone. * @param avoids names of other apps to avoid. First one is name of this app. Might combine appname with * current dir if other instances in other dirs are ok e.g. "ENCRYPT-C:\sam" * * @throws java.io.IOException */ public SingleInstance( String multicastIP, int port, long delay, String... avoids ) throws IOException { this.whenIStarted = System.currentTimeMillis(); this.group = InetAddress.getByName( multicastIP ); this.port = port; this.delay = delay; this.avoids = avoids; // listen to everything said by the group of processes multicasting on port:group, including myself. this.socket = new MulticastSocket( port ); this.socket.joinGroup( group ); final UUID uuid = UUID.randomUUID(); this.uuidMsb = uuid.getMostSignificantBits(); this.uuidLsb = uuid.getLeastSignificantBits(); } /** * let other instances know when I started */ private void broadcastWhenIStarted() throws IOException { // send out time when I started. if ( sendBuf == null ) { composeBroadcast(); } socket.send( sendDP ); } /** * compose the datagram we send out into the world to announce our presence, like a frog croaking. * We only need to do it once. */ private void composeBroadcast() throws IOException { // O P E N final ByteArrayOutputStream baos = new ByteArrayOutputStream( 8 + 8 + 8 + avoids[ 0 ].length() * 2 + 2 ); final DataOutputStream dos = new DataOutputStream( baos ); // W R I T E dos.writeLong( this.whenIStarted ); dos.writeLong( this.uuidMsb ); dos.writeLong( this.uuidLsb ); dos.writeUTF( this.avoids[ 0 ] ); // C L O S E dos.close(); sendBuf = baos.toByteArray(); sendDP = new DatagramPacket( sendBuf, sendBuf.length, group, port ); } /** * Defer to another instance that is already running */ private void defer() { assert thread != null && thread.isAlive(); keepListening = false; if ( socket != null ) { socket.close(); } // signal the waitFor() method to stop waiting synchronized ( this ) { notify(); } } /** * delay program startup until we have * time to determine if there is another copy running. * returns true if no other copy is running. * * @see #defer() */ synchronized boolean heardEnoughSilence() throws InterruptedException { wait( this.delay ); // true, we keep listening to fend off other instances. Our wait terminated because of the delay. // false, stop, defer to some other existing instance. Our wait terminated prematurely because of notify. return keepListening; } /** * probe to see if other instance already running */ void start() throws IOException { assert thread == null || !thread.isAlive(); keepListening = true; thread = new Thread( this ); thread.start(); broadcastWhenIStarted(); } /** * main method. Your main method in your App will look something like this: * * @param args not used */ public static void main( String[] args ) { try { // ip is randomly selected multicast IP value in range 224.x.x.x .. 239.x.x.x // We hope is unused for any other purpose. // 23222 is randomly chosen 16 bit port number in range 1024 .. 65535 // We hope will be unused for any other purpose. // We wait 2000 ms, 2 sec for messages to exchange before deciding if this instance should proceed. // "MYAPP" in the name of this app. I could have added names of other apps to avoid too. // e.g. "ENCRYPT","DECRYPT" or include a dir name so two apps can run, so long as on different dirs, // e.g. "ENCRYPT-C:\plain" if ( new SingleInstance( "227.228.229.230", 23222, TimeUnit.SECONDS.toMillis( 2 ), "MYAPP" ).isSingle() ) { out.println( "No other instance running! We can proceed." ); // start up the real app here. } else { out.println( "Another instance is already running. We must exit." ); System.exit( 1 ); } } catch ( IOException e ) { e.printStackTrace(); System.exit( 2 ); } catch ( InterruptedException e ) { e.printStackTrace(); System.exit( 2 ); } } /** * compare our time with time from other +ve if we came after. * * @param whenIstarted time our instance started. * @param uuidMsb our instance Uuid * @param uuidLsb our instance Uuid * @param whenTheyStarted time other instance started. * @param theirUuidMsb our instance Uuid * @param theirUuidLsb our instance Uuid * * @return +ve if we came later, 0 if same, -ve if we came first. */ public final long compare( long whenIstarted, long uuidMsb, long uuidLsb, long whenTheyStarted, long theirUuidMsb, long theirUuidLsb ) { long diff = whenIstarted - whenTheyStarted; if ( diff != 0 ) { return diff; } long diff1 = uuidMsb - theirUuidMsb; if ( diff1 != 0 ) { return diff1; } return uuidLsb - theirUuidLsb; } /** * is this app a single instance, no other conflicting instances running? * * @return true if I am alone, false if another app started first */ public boolean isSingle() throws IOException, InterruptedException { start(); return heardEnoughSilence(); } /** * implements Runnable */ @Override public void run() { // This thread keeps running the entire time the prevailing app is running. while ( keepListening ) { try { // Receive the time of other instances running, that may have started before or after us. // We will also receive our multicast of our time. final byte[] listenBuf = new byte[ 1024 ]; /* big enough to receive multiple packets */ DatagramPacket listenDP = new DatagramPacket( listenBuf, listenBuf.length ); // This will block waiting for an in incoming packet until some other instance says something. // We are not in a tight loop. this.socket.receive( listenDP ); // take the binary packet apart // O P E N final ByteArrayInputStream bais = new ByteArrayInputStream( listenDP.getData(), listenDP.getOffset(), listenDP.getLength() ); final DataInputStream dis = new DataInputStream( bais ); // R E A D final long whenTheyStarted = dis.readLong(); final long theirUuidMsb = dis.readLong(); final long theirUuidLsb = dis.readLong(); final String toAvoid = dis.readUTF(); // C L O S E dis.close(); long diff = compare( whenTheyStarted, uuidMsb, uuidMsb, whenTheyStarted, theirUuidMsb, theirUuidLsb ); if ( diff > 0 ) { // if their time is before my time, we need to shut down. They started first. // We need look no further. // Check that this in one of the apps ne need to avoid before we panic. for ( String avoid : avoids ) { if ( toAvoid.equals( avoid ) ) { // this is one of the apps we must avoid, defer to it. defer(); // ??? } } // false alarm. Was some other app, one we need not worry about. } else if ( diff < 0 ) { // if their time is after my time, send out my time // Tell 'em, possibly again I got here first. // They might have started 30 minutes after I did. I have not been // broadcasting anything for the last 30 minutes. My app may be running. // I am not going to shut it down now. Am listinging broadcasting just // to fend off new instances. We keep telling them till they shut up. broadcastWhenIStarted(); } // else, exact match. We are definitely seeing our own packet, do nothing // Even our uuid matches. } catch ( IOException e ) { err.println( "SingleInstance failure: " + e.getMessage() ); e.printStackTrace(); defer(); } catch ( NumberFormatException e ) { err.println( "SingleInstance garbled message: " + e.getMessage() ); e.printStackTrace(); defer(); } } } /* Commentary by Peter Dunhio With that technique, there shouldn't be a need to have a unique port. Multiple clients can listen on the same port/group without conflict. It looks like a good start. I will point out that there's no guarantee that the "myTime" value for each process will be unique, so one process could misidentify the other process's message as its own. You need some form of tie-breaker, or a better unique ID that can be used (e.g. GUID). That conflict could be related to the failures you noted when testing the code. The code itself has places that seem like could be improved. For example, duplicated code to send the local "myTime" value ought to be in a method called by the two places where that operation is done. I also don't understand the check for "thread != null" and "thread.isAlive()" in the "stop()" method, but maybe I'm just overlooking something (as near as I can tell, "stop()" is called only when those conditions are assured of being true). In a real implementation, each program would have a way to include a unique identifier (i.e. similar to the name one would use for a named mutex on Windows) in the transmitted message, to insure against two completely different programs that are using the same "exclusive" implementation from conflicting with each other. It is important to keep in mind that even this approach is not 100% reliable. UDP messages are not guaranteed delivery, so the earlier instance could fail to receive a request from a later instance, or the later instance could fail to receive the reply. I think that between processes on the same machine, the chances of such a failure are extremely slim. UDP datagrams are usually dropped due to congestion, lack of process buffer space, that sort of thing, none of which should be an issue in this scenario. But nothing about the UDP protocol or socket API actually guarantees delivery. */ }