/* * [Exclusive.java] * * Summary: First cut to 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.example; import javax.swing.JOptionPane; import java.awt.EventQueue; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.net.DatagramPacket; import java.net.InetAddress; import java.net.MulticastSocket; import java.nio.charset.StandardCharsets; import static java.lang.System.*; /** * First cut to arrange for single instance using UDP. * * @author Roedy Green, Canadian Mind Products * @version 1.0 2013-01-08 initial version * @since 2013-01-08 */ public class Exclusive implements Runnable { private final InetAddress group; private final MulticastSocket socket; private final int port; private final long myTime; private volatile Thread thread; private volatile boolean runFlag; /** * constructor * * @param ipStr multicast IP * @param portStr port * * @throws IOException */ public Exclusive( String ipStr, String portStr ) throws IOException { myTime = System.currentTimeMillis(); group = InetAddress.getByName( ipStr ); port = Integer.parseInt( portStr ); // listen to everything said by the group of processes multicasting on port:group, including myself. socket = new MulticastSocket( port ); socket.joinGroup( group ); } /** * abort. Another instance is already running */ private void stop() { if ( thread != null && thread.isAlive() ) { runFlag = false; if ( socket != null ) { socket.close(); } } // signal the waitFor() method to stop waiting synchronized ( this ) { notify(); } } /** * main method * * @param args not used */ public static void main( String[] args ) { try { // ip is randomly selected 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 1024 .. 65535 we hope will be unused for any other purpose. Exclusive e = new Exclusive( "227.228.229.230", "23222" ); e.start(); if ( e.waitFor() ) { out.println( "no other copy running!" ); } else { out.println( "another copy is running" ); } } catch ( IOException | InterruptedException ex ) { // probably don't want to start if you get an exception either ex.printStackTrace(); } } @Override /** implements Runnable */ public void run() { while ( runFlag ) { try { // receive their time byte[] buf = new byte[ 64 ]; DatagramPacket dp = new DatagramPacket( buf, buf.length ); socket.receive( dp ); String timeStr = new String( dp.getData(), dp.getOffset(), dp.getLength(), StandardCharsets.US_ASCII ); long theirTime = Long.parseLong( timeStr ); // if we are seeing our own packet, do nothing if ( theirTime == myTime ) { // if their time is before my time, we need to shut down. They started first. } else if ( theirTime < myTime ) { stop(); shutdownHook(); } else if ( theirTime > myTime ) { // if their time is after my time, send out my time // They probably missed my earlier broadcast. Tell 'em again I got here first. String str = Long.toString( myTime ); buf = str.getBytes( StandardCharsets.US_ASCII ); dp = new DatagramPacket( buf, buf.length, group, port ); socket.send( dp ); } } catch ( IOException | NumberFormatException ex ) { ex.printStackTrace(); stop(); } } } /** * schedule dialog on shutdown */ public void shutdownHook() { // can't use invokeLater() try { EventQueue.invokeAndWait( new Runnable() { public void run() { JOptionPane.showMessageDialog( null, "Another copy of this program is already running", "Start Inhibited", JOptionPane.ERROR_MESSAGE ); } } ); } catch ( InterruptedException | InvocationTargetException ex ) { ex.printStackTrace(); } } /** * probe to see if other instance already running */ public void start() throws IOException { if ( thread == null || !thread.isAlive() ) { runFlag = true; thread = new Thread( this ); thread.start(); // send out my time String str = Long.toString( myTime ); byte[] buf = str.getBytes( StandardCharsets.US_ASCII ); DatagramPacket dp = new DatagramPacket( buf, buf.length, group, port ); socket.send( dp ); } } /** * wait for two seconds to delay program startup until we have * time to determine if there is another copy running. * returns true if no other copy is running. */ public synchronized boolean waitFor() throws InterruptedException { wait( 2000 ); return runFlag; } } /* 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. */