/*
* 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;
}
}