/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.java.sip.communicator.util.launchutils;
import java.io.*;
import java.lang.reflect.*;
import java.net.*;
import java.util.*;
import net.java.sip.communicator.launcher.*;
import net.java.sip.communicator.util.*;
/**
* This class is used to prevent from running multiple instances of Jitsi. The
* class binds a socket somewhere on the localhost domain and records its socket
* address in the Jitsi configuration directory.
*
* All following instances of Jitsi (and hence this class) will look for this
* record in the configuration directory and try to connect to the original
* instance through the socket address in there.
*
* @author Emil Ivov
*/
public class SipCommunicatorLock extends Thread
{
private static final Logger logger
= Logger.getLogger(SipCommunicatorLock.class);
/**
* Indicates that something went wrong. More information will probably be
* available in the console ... if anyone cares at all.
*/
public static final int LOCK_ERROR = 300;
/**
* Returned by the soft start method to indicate that we have successfully
* started and locked the configuration directory.
*/
public static final int SUCCESS = 0;
/**
* Returned by the soft start method to indicate that an instance of Jitsi
* has been already started and we should exit. This return code also
* indicates that all arguments were passed to that new instance.
*/
public static final int ALREADY_STARTED = 301;
/**
* The name of the file that we use to store the address and port that this
* lock is bound on.
*/
private static final String LOCK_FILE_NAME = ".lock";
/**
* The name of the property that we use to store the address that we bind on
* in this class.
*/
private static final String PNAME_LOCK_ADDRESS = "lockAddress";
/**
* The name of the property that we use to store the address that we bind on
* in this class.
*/
private static final String PNAME_LOCK_PORT = "lockPort";
/**
* The header preceding each of the arguments that we toss around between
* instances of Jitsi
*/
private static final String ARGUMENT = "Argument";
/**
* The name of the header that contains the number of arguments that we send
* from one instance to another.
*/
private static final String ARG_COUNT = "Arg-Count";
/**
* The name of the header that contains any error messages resulting from
* remote argument handling.
*/
private static final String ERROR_ARG = "ERROR";
/**
* The carriage return, line feed sequence (\r\n).
*/
private static final String CRLF = "\r\n";
/**
* The number of milliseconds that we should wait for a remote SC instance
* to come back to us.
*/
private long LOCK_COMMUNICATION_DELAY = 1000;
/**
* The socket that we use for cross instance lock and communication.
*/
private ServerSocket instanceServerSocket = null;
/**
* Retry times reading the lock file.
*/
private static final int LOCK_FILE_READ_RETRY = 8;
/**
* Time between retires reading lock file in milliseconds.
*/
private static final long LOCK_FILE_READ_WAIT = 500;
/**
* An address that is reported not local on macosx and is assigned as
* default on loopback interface.
*/
private static final String WEIRD_MACOSX_LOOPBACK_ADDRESS
= "fe80:0:0:0:0:0:0:1";
/**
* Tries to lock the configuration directory. If lock-ing is not possible
* because a previous instance is already running, then it transmits the
* list of args to that running instance.
*
* There are three possible outcomes of this method. 1. We lock
* successfully; 2. We fail to lock because another instance of Jitsi is
* already running; 3. We fail to lock for some unknown error. Each of these
* cases is represented by an error code returned as a result.
*
* @param args the array of arguments that we are to submit in case an
* instance of Jitsi has already been started.
* @return an error or success code indicating the outcome of the lock
* operation.
*/
public int tryLock(String[] args)
{
// first check whether we have a file.
File lockFile = getLockFile();
if (lockFile.exists())
{
InetSocketAddress lockAddress = readLockFileRetrying(lockFile);
if (lockAddress != null)
{
// we have a valid lockAddress and hence possibly an already
// running instance of SC. Try to communicate with it.
if (interInstanceConnect(lockAddress, args) == SUCCESS)
{
return ALREADY_STARTED;
}
}
// our lockFile is probably stale and left from a previous instance.
// or an instance that is still running but is not responding.
lockFile.delete();
}
// if we get here then this means that we should go for a real lock
// initialization
return lock(lockFile);
}
/**
* Locks the configuration directory by binding our lock socket and
* recording the lock file into the configuration directory. Returns SUCCESS
* if everything goes well and ERROR if something fails. This method does
* not return the ALREADY_RUNNING code as it is assumed that this has
* already been checked before calling this method.
*
* @param lockFile the file that we should use to lock the configuration
* directory.
* @return the SUCCESS or ERROR codes defined by this class.
*/
private int lock(File lockFile)
{
InetAddress lockAddress = getRandomBindAddress();
if (lockAddress == null)
{
return LOCK_ERROR;
}
// create a new socket
// seven time retry binding to port
int retries = 7;
int port = getRandomPortNumber();
InetSocketAddress serverSocketAddress;
while(startLockServer(
serverSocketAddress = new InetSocketAddress(lockAddress, port))
!= SUCCESS
&& retries > 0)
{
// port possibly taken, change it
port = getRandomPortNumber();
retries--;
}
// right the bind address in the file
try
{
lockFile.getParentFile().mkdirs();
lockFile.createNewFile();
}
catch (IOException e)
{
logger.error("Failed to create lock file" + lockFile, e);
}
lockFile.deleteOnExit();
DeleteOnHaltHook.add(lockFile.getAbsolutePath());
writeLockFile(lockFile, serverSocketAddress);
return SUCCESS;
}
/**
* Creates and binds a socket on lockAddress and then starts a
* LockServer instance so that we would start interacting with
* other instances of Jitsi that are trying to start.
*
* @return the ERROR code if something goes wrong and
* SUCCESS otherwise.
*/
private int startLockServer(InetSocketAddress localAddress)
{
try
{
// check config directory
instanceServerSocket = new ServerSocket();
}
catch (IOException exc)
{
// Just checked the impl and this doesn't seem to ever be thrown
// .... ignore ...
logger.error("Couldn't create server socket", exc);
return LOCK_ERROR;
}
try
{
instanceServerSocket.bind(localAddress, 16);// Why 16? 'cos I say
// so.
}
catch (IOException exc)
{
logger.error("Couldn't create server socket", exc);
return LOCK_ERROR;
}
LockServer lockServ = new LockServer(instanceServerSocket);
lockServ.start();
return SUCCESS;
}
/**
* Returns a randomly chosen socket address using a loopback interface (or
* another one in case the loopback is not available) that we should bind
* on.
*
* @return an InetAddress (most probably a loopback) that we can use to bind
* our semaphore socket on.
*/
private InetAddress getRandomBindAddress()
{
NetworkInterface loopback;
try
{
// find a loopback interface
Enumeration interfaces;
try
{
interfaces = NetworkInterface.getNetworkInterfaces();
}
catch (SocketException exc)
{
// I don't quite understand why this would happen ...
logger.error(
"Failed to obtain a list of the local interfaces.",
exc);
return null;
}
loopback = null;
while (interfaces.hasMoreElements())
{
NetworkInterface iface = interfaces.nextElement();
if (isLoopbackInterface(iface))
{
loopback = iface;
break;
}
}
// if we didn't find a loopback (unlikely but possible)
// return the first available interface on this machine
if (loopback == null)
{
loopback = NetworkInterface.getNetworkInterfaces()
.nextElement();
}
}
catch (SocketException exc)
{
// I don't quite understand what could possibly cause this ...
logger.error("Could not find the loopback interface", exc);
return null;
}
// get the first address on the loopback.
InetAddress addr = loopback.getInetAddresses().nextElement();
return addr;
}
/**
* Returns a random port number that we can use to bind a socket on.
*
* @return a random port number that we can use to bind a socket on.
*/
private int getRandomPortNumber()
{
return (int) (Math.random() * 64509) + 1025;
}
/**
* Calls reading of the lock file, retrying if there is nothing written in
* it.
*
* @param lockFile the file that we are to parse.
* @return the SocketAddress that we should use to communicate with
* a possibly already running version of Jitsi.
*/
private InetSocketAddress readLockFileRetrying(File lockFile)
{
int retries = LOCK_FILE_READ_RETRY;
InetSocketAddress res = null;
while(res == null && retries > 0)
{
res = readLockFile(lockFile);
if(res == null)
{
try
{
Thread.sleep(LOCK_FILE_READ_WAIT);
}
catch(InterruptedException e){}
}
retries--;
}
return res;
}
/**
* Parses the lockFile into a standard Properties Object and
* verifies it for completeness. The method also tries to validate the
* contents of lockFile and asserts presence of all properties
* mandated by this version.
*
* @param lockFile the file that we are to parse.
* @return the SocketAddress that we should use to communicate with
* a possibly already running version of Jitsi.
*/
private InetSocketAddress readLockFile(File lockFile)
{
Properties lockProperties = new Properties();
try
{
lockProperties.load(new FileInputStream(lockFile));
}
catch (Exception exc)
{
logger.error("Failed to read lock properties.", exc);
return null;
}
String lockAddressStr = lockProperties.getProperty(PNAME_LOCK_ADDRESS);
if (lockAddressStr == null)
{
logger.error("Lock file contains no lock address.");
return null;
}
String lockPort = lockProperties.getProperty(PNAME_LOCK_PORT);
if (lockPort == null)
{
logger.error("Lock file contains no lock port.");
return null;
}
InetAddress lockAddress = findLocalAddress(lockAddressStr);
if (lockAddress == null)
{
logger.error(lockAddressStr + " is not a valid local address.");
return null;
}
int port;
try
{
port = Integer.parseInt(lockPort);
}
catch (NumberFormatException exc)
{
logger.error(lockPort + " is not a valid port number.", exc);
return null;
}
InetSocketAddress lockSocketAddress = new InetSocketAddress(
lockAddress, port);
return lockSocketAddress;
}
/**
* Records our lockAddress into lockFile using the
* standard properties format.
*
* @param lockFile the file that we should store the address in.
* @param lockAddress the address that we have to record.
* @return SUCCESS upon success and ERROR if we fail to
* store the file.
*/
private int writeLockFile(File lockFile, InetSocketAddress lockAddress)
{
Properties lockProperties = new Properties();
lockProperties.setProperty(
PNAME_LOCK_ADDRESS,
lockAddress.getAddress().getHostAddress());
lockProperties.setProperty(
PNAME_LOCK_PORT,
Integer.toString(lockAddress.getPort()));
try
{
lockProperties.store(
new FileOutputStream(lockFile),
"Jitsi lock file. This file will be automatically removed"
+ " when execution of Jitsi terminates.");
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
catch (IOException e)
{
logger.error("Failed to create lock file.", e);
return LOCK_ERROR;
}
return SUCCESS;
}
/**
* Returns a reference to the file that we should be using to lock Jitsi's
* home directory, whether it exists or not.
*
* @return a reference to the file that we should be using to lock Jitsi's
* home directory.
*/
private File getLockFile()
{
String homeDirLocation =
System
.getProperty(SIPCommunicator.PNAME_SC_CACHE_DIR_LOCATION);
String homeDirName = System
.getProperty(SIPCommunicator.PNAME_SC_HOME_DIR_NAME);
return new File(new File(homeDirLocation, homeDirName), LOCK_FILE_NAME);
}
/**
* Returns an InetAddress instance corresponding to
* addressStr or null if no such address exists on the
* local interfaces.
*
* @param addressStr the address string that we are trying to resolve into
* an InetAddress
* @return an InetAddress instance corresponding to
* addressStr or null if none of the local interfaces has
* such an address.
*/
private InetAddress findLocalAddress(String addressStr)
{
Enumeration ifaces;
try
{
ifaces = NetworkInterface.getNetworkInterfaces();
}
catch (SocketException exc)
{
logger.error(
"Could not extract the list of local intefcaces.",
exc);
return null;
}
// loop through local interfaces
while (ifaces.hasMoreElements())
{
NetworkInterface iface = ifaces.nextElement();
Enumeration addreses = iface.getInetAddresses();
// loop iface addresses
while (addreses.hasMoreElements())
{
InetAddress addr = addreses.nextElement();
if (addr.getHostAddress().equals(addressStr))
return addr;
}
}
return null;
}
/**
* Initializes a client TCP socket, connects if to sockAddr and
* sends all args to it.
*
* @param sockAddr the address that we are to connect to.
* @param args the args that we need to send to sockAddr.
* @return SUCCESS upond success and ERROR if anything
* goes wrong.
*/
private int interInstanceConnect(InetSocketAddress sockAddr, String[] args)
{
try
{
Socket interInstanceSocket = new Socket(sockAddr.getAddress(),
sockAddr.getPort());
LockClient lockClient = new LockClient(interInstanceSocket);
lockClient.start();
PrintStream printStream = new PrintStream(interInstanceSocket
.getOutputStream());
printStream.print(ARG_COUNT + "=" + args.length + CRLF);
for (int i = 0; i < args.length; i++)
{
printStream.print(ARGUMENT + "=" + args[i] + CRLF);
}
lockClient.waitForReply(LOCK_COMMUNICATION_DELAY);
//NPEs are handled in catch so no need to check whether or not we
//actually have a reply.
String serverReadArgCountStr = lockClient.message
.substring((ARG_COUNT + "=").length());
int serverReadArgCount = Integer.parseInt(serverReadArgCountStr);
if (logger.isDebugEnabled())
logger.debug("Server read " + serverReadArgCount + " args.");
if(serverReadArgCount != args.length)
return LOCK_ERROR;
printStream.flush();
printStream.close();
interInstanceSocket.close();
}
//catch IOExceptions, NPEs and NumberFormatExceptions here.
catch (Exception e)
{
if (logger.isDebugEnabled())
logger.debug("Failed to connect to a running sc instance.");
return LOCK_ERROR;
}
return SUCCESS;
}
/**
* We use this thread to communicate with an already running instance of
* Jitsi. This thread will listen for a reply to a message that we've sent
* to the other instance. We will wait for this message for a maximum of
* runDuration milliseconds and then consider the remote instance
* dead.
*/
private static class LockClient extends Thread
{
/**
* The String that we've read from the socketInputStream
*/
public String message = null;
/**
* The socket that this LockClient is created to read from.
*/
private final Socket interInstanceSocket;
/**
* Creates a LockClient that should read whatever data we
* receive on sockInputStream.
*
* @param commSocket the socket that this client should be reading from.
*/
public LockClient(Socket commSocket)
{
super(LockClient.class.getName());
setDaemon(true);
this.interInstanceSocket = commSocket;
}
/**
* Blocks until a reply has been received or until runDuration
* milliseconds had passed.
*
* @param runDuration the number of seconds to wait for a reply from the
* remote instance
*/
public void waitForReply(long runDuration)
{
try
{
synchronized(this)
{
//return if we have already received a message.
if(message != null)
return;
wait(runDuration);
}
if (logger.isDebugEnabled())
logger.debug("Done waiting. Will close socket");
interInstanceSocket.close();
}
catch (Exception exception)
{
logger.error("Failed to close our inter instance input stream",
exception);
}
}
/**
* Simply collects everything that we read from the InputStream that
* this InterInstanceCommunicationClient was created with.
*/
@Override
public void run()
{
try
{
BufferedReader lineReader = new BufferedReader(
new InputStreamReader(interInstanceSocket
.getInputStream()));
//we only need to read a single line and then bail out.
message = lineReader.readLine();
if (logger.isDebugEnabled())
logger.debug("Message is " + message);
synchronized(this)
{
notifyAll();
}
}
catch (IOException exc)
{
// does not necessarily mean something is wrong. Could be
// that we got tired of waiting and want to quit.
if (logger.isInfoEnabled())
{
logger.info(
"An IOException is thrown while reading sock",
exc);
}
}
}
}
/**
* We start this thread when running Jitsi as a means of notifying others
* that this is
*/
private static class LockServer extends Thread
{
private boolean keepAccepting = true;
/**
* The socket that we use for cross instance lock and communication.
*/
private final ServerSocket lockSocket;
/**
* Creates an instance of this LockServer wrapping the
* specified serverSocket. It is expected that the serverSocket
* will be already bound and ready to accept.
*
* @param serverSocket the serverSocket that we should use for inter
* instance communication.
*/
public LockServer(ServerSocket serverSocket)
{
super(LockServer.class.getName());
setDaemon(true);
this.lockSocket = serverSocket;
}
@Override
public void run()
{
try
{
while (keepAccepting)
{
Socket instanceSocket = lockSocket.accept();
new LockServerConnectionProcessor(instanceSocket).start();
}
}
catch (Exception exc)
{
logger.warn("Someone tried ", exc);
}
}
}
/**
* We use this thread to handle individual messages in server side inter
* instance communication.
*/
private static class LockServerConnectionProcessor extends Thread
{
/**
* The socket that we will be using to communicate with the fellow Jitsi
* instance.
*/
private final Socket connectionSocket;
/**
* Creates an instance of LockServerConnectionProcessor that
* would handle parameters received through the
* connectionSocket.
*
* @param connectionSocket the socket that we will be using to read
* arguments from the remote Jitsi instance.
*/
public LockServerConnectionProcessor(Socket connectionSocket)
{
this.connectionSocket = connectionSocket;
}
/**
* Starts reading messages arriving through the connection socket.
*/
@Override
public void run()
{
InputStream is;
PrintWriter printer;
try
{
is = connectionSocket.getInputStream();
printer = new PrintWriter(connectionSocket
.getOutputStream());
}
catch (IOException exc)
{
logger.warn("Failed to read arguments from another SC instance",
exc);
return;
}
ArrayList argsList = new ArrayList();
if (logger.isDebugEnabled())
logger.debug("Handling incoming connection");
int argCount = 1024;
try
{
BufferedReader lineReader =
new BufferedReader(new InputStreamReader(is));
while (true)
{
String line = lineReader.readLine();
if (logger.isDebugEnabled())
logger.debug(line);
if (line.startsWith(ARG_COUNT))
{
argCount = Integer.parseInt(line
.substring((ARG_COUNT + "=").length()));
}
else if (line.startsWith(ARGUMENT))
{
String arg = line.substring((ARGUMENT + "=").length());
argsList.add(arg);
}
else
{
// ignore unknown headers.
}
if (argCount <= argsList.size())
break;
}
// first tell the remote application that everything went OK
// and end the connection so that it could exit
printer.print(ARG_COUNT + "=" + argCount + CRLF);
printer.close();
connectionSocket.close();
// now let's handle what we've got
String[] args = new String[argsList.size()];
LaunchArgHandler.getInstance()
.handleConcurrentInvocationRequestArgs(
argsList.toArray(args));
}
catch (IOException exc)
{
if (logger.isInfoEnabled())
logger.info("An IOException is thrown while "
+ "processing remote args", exc);
printer.print(ERROR_ARG + "=" + exc.getMessage());
}
}
}
/**
* Determines whether or not the iface interface is a loopback
* interface. We use this method as a replacement to the
* NetworkInterface.isLoopback() method that only comes with Java
* 1.6.
*
* @param iface the inteface that we'd like to determine as loopback or not.
* @return true if iface contains at least one loopback address and
* false otherwise.
*/
private boolean isLoopbackInterface(NetworkInterface iface)
{
try
{
Method method = iface.getClass().getMethod("isLoopback");
return ((Boolean)method.invoke(iface, new Object[]{}))
.booleanValue();
}
catch(Throwable t)
{
//apparently we are not running in a JVM that supports the
//is Loopback method. we'll try another approach.
}
Enumeration addresses = iface.getInetAddresses();
if(addresses.hasMoreElements())
{
InetAddress address = addresses.nextElement();
if(address.isLoopbackAddress()
|| address.getHostAddress().startsWith(
WEIRD_MACOSX_LOOPBACK_ADDRESS))
return true;
}
return false;
}
}