/*
 * [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.
*/