/* ******************************************************************************
* Copyright (c) 2006-2012 XMind Ltd. and others.
*
* This file is a part of XMind 3. XMind releases 3 and
* above are dual-licensed under the Eclipse Public License (EPL),
* which is available at http://www.eclipse.org/legal/epl-v10.html
* and the GNU Lesser General Public License (LGPL),
* which is available at http://www.gnu.org/licenses/lgpl.html
* See http://www.xmind.net/license.html for details.
*
* Contributors:
* XMind Ltd. - initial API and implementation
*******************************************************************************/
package org.xmind.core.command.remote.socket;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.xmind.core.command.remote.ICommandServer;
import org.xmind.core.command.remote.ICommandServiceDomain;
import org.xmind.core.command.remote.ICommandServiceInfo;
import org.xmind.core.internal.command.remote.Messages;
import org.xmind.core.internal.command.remote.RemoteCommandPlugin;
/**
* This class provides basic implementations of a command server that relies on
* a server socket.
*
* @author Frank Shaka
*/
public class SocketCommandServer implements ICommandServer {
private ICommandServiceDomain domain;
private int defaultPort;
private int defaultBacklog;
private boolean triesEphemeralPort;
private ServerSocket server = null;
private Thread thread = null;
private SocketCommandServiceInfo info = null;
private Object lock = new Object();
private Runnable serverRunner = new Runnable() {
/**
* Runs the server handling loop. Accepts all socket connections and
* fork them into separate handling job.
*/
public void run() {
ServerSocket theServer = server;
if (theServer == null)
return;
try {
while (true) {
Socket socket = theServer.accept();
socketPool.addSocket(socket);
runIncomingCommandHandler(socket);
}
} catch (IOException e) {
if (thread != null || server != null) {
RemoteCommandPlugin
.log("Error occurred while handling local command server.", //$NON-NLS-1$
e);
}
} finally {
try {
theServer.close();
} catch (IOException e) {
}
}
}
};
private SocketPool socketPool = new SocketPool();
/**
* Constructs a new instance using default configurations, i.e. an ephemeral
* port as the default port, a backlog valued <code>50</code>.
*/
public SocketCommandServer() {
this(0, 50, false);
}
/**
* Constructs a new instance with specified configurations.
*
* @param defaultPort
* the default port number
* @param defaultBacklog
* the default connection waiting queue size
* @param triesEphemeralPort
* <code>true</code> to indicate that an ephemeral port will be
* tried if the default port fails to be bound to, which makes
* the best effort to ensure the command server started up, or
* <code>false</code> if no other port number than the default
* one should be tried to make the command server either fix on
* the given port number or just fail
*/
public SocketCommandServer(int defaultPort, int defaultBacklog,
boolean triesEphemeralPort) {
if (defaultPort < 0)
defaultPort = 0;
this.defaultPort = defaultPort;
this.defaultBacklog = defaultBacklog;
this.triesEphemeralPort = triesEphemeralPort;
}
public void init(ICommandServiceDomain domain) {
this.domain = domain;
}
/**
* @return the domain
*/
public ICommandServiceDomain getDomain() {
return domain;
}
/**
* @return the defaultBacklog
*/
public int getDefaultBacklog() {
return defaultBacklog;
}
/**
* @return the defaultPort
*/
public int getDefaultPort() {
return defaultPort;
}
/**
* @return the server
*/
public ServerSocket getServer() {
return server;
}
/**
* Returns the thread that handles the server socket's events. It is
* referred to as <i>'the loop thread'</i> in this class.
*
* @return the loop thread, or <code>null</code> if the command server is
* not deployed or has been undeployed
*/
public Thread getThread() {
return thread;
}
/**
* Deploys this command server using a server socket.
*
* <p>
* This method will first try to bind the server socket to the default port.
* If that fails, it will use an ephemeral port provided by the system.
* </p>
*
* <p>
* A daemon thread will be started once the server socket is ready. The
* thread will start a loop of accepting new socket connections on the
* server socket and starting an {@link org.eclipse.core.runtime.jobs.Job}
* to handle the command coming in through the accepted socket connection.
* </p>
*
* @param monitor
* the progress monitor
* @return the status of the deployment
*/
public IStatus deploy(IProgressMonitor monitor) {
monitor.beginTask(null, 100);
monitor.subTask(Messages.SocketCommandServer_OperationLock);
synchronized (lock) {
monitor.subTask(Messages.SocketCommandServer_OpenCommandServerSocket);
if (server == null || server.isClosed()) {
try {
server = new ServerSocket();
} catch (IOException e) {
return new Status(IStatus.ERROR,
RemoteCommandPlugin.PLUGIN_ID, null, e);
}
if (monitor.isCanceled())
return Status.CANCEL_STATUS;
monitor.worked(20);
InetAddress localAddress = null;
int port = defaultPort;
try {
server.bind(new InetSocketAddress(localAddress, port),
defaultBacklog);
} catch (IOException e) {
if (monitor.isCanceled())
return Status.CANCEL_STATUS;
if (port != 0 && triesEphemeralPort) {
try {
server.bind(new InetSocketAddress(localAddress, 0),
defaultBacklog);
} catch (IOException e1) {
if (monitor.isCanceled())
return Status.CANCEL_STATUS;
return new Status(IStatus.ERROR,
RemoteCommandPlugin.PLUGIN_ID, null, e);
}
} else {
return new Status(IStatus.ERROR,
RemoteCommandPlugin.PLUGIN_ID, null, e);
}
}
if (monitor.isCanceled())
return Status.CANCEL_STATUS;
monitor.worked(60);
} else {
monitor.worked(70);
}
if (info == null) {
info = new SocketCommandServiceInfo();
info.setAddress(new SocketAddress(server.getInetAddress()
.getHostName(), server.getLocalPort()));
info.setName(System.getProperty("user.name")); //$NON-NLS-1$
}
if (monitor.isCanceled())
return Status.CANCEL_STATUS;
monitor.worked(10);
if (thread == null) {
thread = new Thread(serverRunner, getLoopThreadName());
thread.setDaemon(true);
thread.setPriority(Thread.MIN_PRIORITY);
thread.start();
}
if (monitor.isCanceled())
return Status.CANCEL_STATUS;
monitor.worked(10);
monitor.done();
return Status.OK_STATUS;
}
}
/**
* Undeploys this command server. First, the daemon looping thread will be
* interrupted. Then the server socket opened by
* {@link #deploy(IProgressMonitor)} will be closed.
*
* @param monitor
* the progress monitor
* @return the status of the undeployment
*/
public IStatus undeploy(IProgressMonitor monitor) {
monitor.beginTask(null, 100);
monitor.subTask(Messages.SocketCommandServer_OperationLock);
synchronized (lock) {
monitor.subTask(Messages.SocketCommandServer_CloseCommandServerSocket);
Thread oldThread = thread;
ServerSocket oldServer = server;
thread = null;
server = null;
info = null;
if (monitor.isCanceled())
return Status.CANCEL_STATUS;
monitor.worked(10);
if (oldThread != null) {
oldThread.interrupt();
}
if (oldServer != null) {
try {
oldServer.close();
} catch (IOException e) {
RemoteCommandPlugin.log(
"Could not stop local command server socket.", e); //$NON-NLS-1$
}
}
if (monitor.isCanceled())
return Status.CANCEL_STATUS;
monitor.worked(90);
socketPool.clear();
monitor.done();
return Status.OK_STATUS;
}
}
public ICommandServiceInfo getRegisteringInfo() {
return info;
}
/**
* Returns the socket pool owned by this command server. A socket pool
* maintains a list of all active sockets and provides a method to close all
* active sockets.
*
* @return the socket pool
*/
public SocketPool getSocketPool() {
return socketPool;
}
/**
* Handles the incoming accepted socket. The default implementation simply
* delegates the handling process to an {@link IncomingSocketCommandHandler}
* . Subclasses may extend this method to provide their own implementation.
*
* @param socket
* the accepted socket to handle, never <code>null</code>
*/
protected void runIncomingCommandHandler(Socket socket) {
createIncomingCommandHandler(socket).schedule();
}
/**
* Creates a socket command handler for the given accepted socket. The
* default implementation returns an {@link IncomingSocketCommandHandler}.
* Subclasses may extend this method to provide their own implementation.
*
* @param socket
* the accepted socket to handle, never <code>null</code>
* @return a socket command handler, never <code>null</code>
*/
protected Job createIncomingCommandHandler(Socket socket) {
IncomingSocketCommandHandler handler = new IncomingSocketCommandHandler(
socket);
handler.setSocketPool(getSocketPool());
return handler;
}
/**
* Returns the name of the command server loop thread. Subclasses may extend
* this method to provide their own name for the loop thread.
*
* @return a name, never <code>null</code>
*/
protected String getLoopThreadName() {
return "SocketCommandServerLoop"; //$NON-NLS-1$
}
@SuppressWarnings({ "rawtypes", "unchecked" })
public Object getAdapter(Class adapter) {
if (adapter == SocketPool.class)
return getSocketPool();
return null;
}
}