de.ailis.oneinstance.OneInstance.java Source code

Java tutorial

Introduction

Here is the source code for de.ailis.oneinstance.OneInstance.java

Source

/*
 * Copyright (C) 2012 Klaus Reimer <k@ailis.de>
 * See LICENSE.txt for licensing information.
 */

package de.ailis.oneinstance;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectOutputStream;
import java.io.RandomAccessFile;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Random;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * The One-Instance manager. This is a singleton so you must use the
 * {@link OneInstance#getInstance()} method to obtain it. The application
 * must call the {@link OneInstance#register(Class, String[])} method at
 * the beginning of its main method. If this method returns false
 * then the application must exit immediately.
 * 
 * The application must also provide an implementation of the
 * {@link OneInstanceListener} interface which must be registered with
 * the {@link OneInstance#addListener(OneInstanceListener)} method. THis
 * listener is called when additional instances of the application are
 * started. The listener can then decide what to do with the new instance
 * and with its command-line arguments.
 * 
 * When the application exits then it should call
 * {@link OneInstance#unregister(Class)} but this is not a requirement.
 * This method just closes the server socket (Which will be closed anyway when
 * the application exits) and it removes the port number from the preferences so
 * the next started application does not need to check if this port is still
 * valid. When the application instance was not the first instance then this
 * method does nothing at all.
 * 
 * @author Klaus Reimer (k@ailis.de)
 */
public final class OneInstance {
    /** The logger. */
    private static final Log LOG = LogFactory.getLog(OneInstance.class);

    /** The singleton instance. */
    private static final OneInstance instance = new OneInstance();

    /** The registered listeners. */
    private List<OneInstanceListener> listeners = Collections
            .synchronizedList(new ArrayList<OneInstanceListener>());

    /** The key name used in the preferences to remember the server port. */
    public static final String PORT_KEY = "oneInstanceServerPort";

    /** The minimum port address. */
    public static final int MIN_PORT = 49152;

    /** The maximum port address. */
    public static final int MAX_PORT = 65535;

    /** The random number generator used to find a free port number. */
    private final Random random = new Random();

    /** The server socket. */
    private OneInstanceServer server;

    /**
     * Private constructor to prevent instantiation of singleton from outside.
     */
    private OneInstance() {
        // Empty
    }

    /**
     * Returns the singleton instance.
     * 
     * @return The singleton instance.
     */
    public static OneInstance getInstance() {
        return instance;
    }

    /**
     * Adds a new listener. This listener is informed about a new application
     * instance which is about to be started. The listener gets the
     * command-line arguments and can decide what to do with the new instance
     * by returning true or false.
     * 
     * @param listener
     *            The listener to add. Must not be null.
     */
    public void addListener(OneInstanceListener listener) {
        if (listener == null)
            throw new IllegalArgumentException("listener must be set");
        this.listeners.add(listener);
    }

    /**
     * Removes a listener.
     * 
     * @param listener
     *            The listener to remove. Must not be null.
     */
    public void removeListener(OneInstanceListener listener) {
        if (listener == null)
            throw new IllegalArgumentException("listener must be set");
        this.listeners.remove(listener);
    }

    /**
     * Creates and returns the lock file for the specified class name.
     * 
     * @param className
     *            The name of the main class.
     * @return The lock file.
     */
    private File getLockFile(final String className) {
        return new File(System.getProperty("java.io.tmpdir"), "oneinstance-" + className + ".lock");
    }

    /**
     * Locks the specified lock file.
     * 
     * @param lockFile
     *            The lock file.
     * @return The lock or null if no locking could be performed.
     */
    private FileLock lock(final File lockFile) {
        try {
            FileChannel channel = new RandomAccessFile(lockFile, "rw").getChannel();
            return channel.lock();
        } catch (IOException e) {
            LOG.warn("Unable to lock the lock file: " + e + ". Trying to run without a lock.", e);
            return null;
        }
    }

    /**
     * Releases the specified lock.
     * 
     * @param fileLock
     *            The file lock to release. If null then nothing is done.
     */
    private void release(final FileLock fileLock) {
        if (fileLock == null)
            return;
        try {
            fileLock.release();
        } catch (IOException e) {
            LOG.warn("Unable to release lock file: " + e, e);
        }
    }

    /**
     * Registers this instance of the application. Returns true if the
     * application is allowed to run or false when the application must
     * exit immediately.
     * 
     * @param mainClass
     *            The main class of the application. Must not be null.
     *            This is used for determining the application ID and as
     *            the user node key for the preferences.
     * @param args
     *            The command line arguments. They are passed to an already
     *            running instance if found. Must not be null.
     * @return True if instance is allowed to start, false if not.
     */
    public boolean register(Class<?> mainClass, final String[] args) {
        if (mainClass == null)
            throw new IllegalArgumentException("mainClass must be set");
        if (args == null)
            throw new IllegalArgumentException("args must be set");

        // Determine application ID from class name.
        String appId = mainClass.getName();

        try {
            // Acquire a lock
            File lockFile = getLockFile(appId);
            FileLock lock = lock(lockFile);

            try {
                // Get the port which is currently recorded as active.
                Integer port = getActivePort(mainClass);

                // If port is found then we have to validate it
                if (port != null) {
                    // Try to connect to the first instance.
                    Socket socket = openClientSocket(appId, port);

                    // If connection is successful then run as a client
                    // (non-first instance)
                    if (socket != null) {
                        try {
                            // Run the client and return the result from the
                            // server
                            return runClient(socket, args);
                        } finally {
                            socket.close();
                        }
                    }
                }

                // Run the server 
                runServer(mainClass);

                // Mark the lock file to be deleted when this instance exits.
                lockFile.deleteOnExit();

                // Allow this first instance to run.
                return true;
            } finally {
                release(lock);
            }
        } catch (IOException e) {
            // When something went wrong log the error as a warning and then
            // let instance start.
            LOG.warn(e.toString(), e);
            return true;
        }
    }

    /**
     * Unregisters this instance of the application. If this is the first
     * instance then the server is closed and the port is removed from
     * the preferences. If this is not the first instance then this method
     * does nothing.
     * 
     * This method should be called when the application exits. But it is
     * not a requirement. When you don't do this then the port number will
     * stay in the preferences so on next start of the application this port
     * number must be validated. So by calling this method on application exit
     * you just save the time for this port validation.
     * 
     * @param mainClass
     *            The main class of the application. Must not be null.
     *            This is used as the user node key for the preferences.
     */
    public void unregister(Class<?> mainClass) {
        if (mainClass == null)
            throw new IllegalArgumentException("mainClass must be set");

        // Nothing to do when no server socket is present
        if (this.server == null)
            return;

        // Close the server socket
        this.server.stop();
        this.server = null;

        // Remove the port from the preferences
        Preferences prefs = Preferences.userNodeForPackage(mainClass);
        prefs.remove(PORT_KEY);
        try {
            prefs.flush();
        } catch (BackingStoreException e) {
            LOG.error(e.toString(), e);
        }
    }

    /**
     * Returns the port which was last recorded as active.
     * 
     * @param mainClass
     *            The main class of the application.
     * @return The active port address or null if not found.
     */
    private Integer getActivePort(Class<?> mainClass) {
        Preferences prefs = Preferences.userNodeForPackage(mainClass);
        int port = prefs.getInt(PORT_KEY, -1);
        return port >= MIN_PORT && port <= MAX_PORT ? port : null;
    }

    /**
     * Remembers an active port number in the preferences.
     * 
     * @param mainClass
     *            The main class of the application.
     * @param port
     *            The port number.
     */
    private void setActivePort(Class<?> mainClass, int port) {
        Preferences prefs = Preferences.userNodeForPackage(mainClass);
        prefs.putInt(PORT_KEY, port);
        try {
            prefs.flush();
        } catch (BackingStoreException e) {
            LOG.error(e.toString(), e);
        }

    }

    /**
     * Returns a random port number.
     * 
     * @return A random port number.
     */
    private int getRandomPort() {
        return this.random.nextInt(MAX_PORT - MIN_PORT) + MIN_PORT;
    }

    /**
     * Opens a client socket to the specified port. If this is successful
     * then the port is returned, otherwise it is closed and null is
     * returned.
     * 
     * @param appId
     *            The application ID.
     * @param port
     *            The port number to connect to.
     * @return The client socket or null if no connection to
     *         the server was possible or the server is not the same
     *         application.
     */
    private Socket openClientSocket(String appId, int port) {
        try {
            Socket socket = new Socket(InetAddress.getByName(null), port);
            try {
                // Open communication channels
                BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));

                // Read the appId from the server. Use a one second timeout for
                // this just in case some unresponsive application listens on
                // this port.
                socket.setSoTimeout(1000);
                String serverAppId = in.readLine();
                socket.setSoTimeout(0);

                // Abort if server app ID doesn't match (Or there was none at
                // all)
                if (serverAppId == null || !serverAppId.equals(appId)) {
                    socket.close();
                    socket = null;
                }

                return socket;
            } catch (IOException e) {
                socket.close();
                return null;
            }
        } catch (IOException e) {
            return null;
        }
    }

    /**
     * Runs the client.
     * 
     * @param socket
     *            The client socket.
     * @param args
     *            The command-line arguments.
     * @return True if server accepted the new instance, false if not.
     * @throws IOException
     *             When communication with the server fails.
     */
    private boolean runClient(Socket socket, String[] args) throws IOException {
        // Send serialized command-line argument list to the server.
        ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
        out.writeObject(new File(".").getCanonicalFile());
        out.writeObject(args);
        out.flush();

        // Read response from server
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), "UTF-8"));
        String response = in.readLine();

        // If response is "exit" then don't start new instance. Any other
        // reply will allow the new instance.
        return response == null || !response.equals("exit");
    }

    /**
     * Runs the server in a new thread and then returns.
     * 
     * @param mainClass
     *            The main class.
     */
    private void runServer(Class<?> mainClass) {
        while (true) {
            String appId = mainClass.getName();
            int port = getRandomPort();
            try {
                this.server = new OneInstanceServer(appId, port);
                setActivePort(mainClass, port);
                this.server.start();
            } catch (PortAlreadyInUseException e) {
                // Ignored, trying next port.
            }
            return;
        }
    }

    /**
     * Fires the newInstance event. When no listeners are registered then
     * this method always returns false. If at least one listener accepts
     * the new instance then true is returned.
     * 
     * @param workingDir
     *            The current working directory of the client. Needed
     *            if relative pathnames are specified on the command line
     *            because the server may currently be in a different 
     *            directory than the client.
     * @param args
     *            The command line arguments of the new instance.
     * @return True if the new instance is allowed to start, false if it must
     *         exit.
     */
    boolean fireNewInstance(final File workingDir, final String[] args) {
        boolean start = false;
        for (OneInstanceListener listener : this.listeners)
            start |= listener.newInstanceCreated(workingDir, args);
        return start;
    }
}