package com.limegroup.gnutella;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
import org.limewire.core.settings.ConnectionSettings;
import org.limewire.core.settings.FilterSettings;
import org.limewire.core.settings.NetworkSettings;
import org.limewire.core.settings.SearchSettings;
import org.limewire.core.settings.UltrapeerSettings;
import org.limewire.service.ErrorCallback;
import org.limewire.service.ErrorService;
import org.limewire.util.TestUtils;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Stage;
import com.limegroup.gnutella.library.FileManager;
import com.limegroup.gnutella.stubs.ActivityCallbackStub;
/**
* Utility class that constructs a LimeWire backend for testing
* purposes. This creates a backend with a true <tt>FileManager</tt>,
* creating a temporary shared directory that files are copied into.
* The only component of this backend that is a stub is
* <tt>ActivityCallbackStub</tt>. Otherwise, all classes are
* constructed as they normally would be in the client.
*/
/**
* This isn't really a JUNIT test class, but subclassing BastTestCase gives us
* access to a lot of neat support routines and does no harm. Side benefit, as
* soon as the constructor is called the internal save and user preference
* directories will be switched to harmless test directories, saving us from
* having to save and restore important files.
*/
@SuppressWarnings("all")
public class Backend extends com.limegroup.gnutella.util.LimeTestCase {
/** Extensions of files that the backend automatically shares */
public static final String SHARED_EXTENSION = "tmp";
/** Port that normal backend will listen on */
public static final int BACKEND_PORT = 6300;
/** Port that the reject backend will listen on */
public static final int REJECT_PORT = 6301;
/** Port used by the leaf */
private static final int LEAF_PORT = 6302;
/** Port that will shutdown the normal backend process */
private static final int SHUTDOWN_PORT = 6310;
/** Port that will shutdown the reject backend process */
private static final int SHUTDOWN_REJECT_PORT = 6311;
/** Port that will shutdown the leaf backend process */
private static final int SHUTDOWN_LEAF_PORT = 6312;
/** Port used to pass error reports to reporting JVM */
private static final int ERROR_PORT = 6399;
/**
* The <tt>RouterService</tt> instance the constructs the backend.
*/
// private RouterService ROUTER_SERVICE;
/**
* The thread which is catching the error reports from the various backend
* servers.
*/
private static ErrorMonitor errorMonitor = null;
/**
* Return the linstening port number
*/
public static int getPort() {
return BACKEND_PORT;
}
/**
* erturn the reject server listening port number
*/
public static int getRejectPort() {
return REJECT_PORT;
}
/**
* Launches a standard backend on port 6300.
*/
public synchronized static boolean launch() throws IOException {
return launch(BACKEND_PORT);
}
/**
* Launch normal backend process if it isn't already running
*
* @return true if we have launched a new backend process false if one was
* already running
* @throws IOException if backend launch was unsucessful
*/
public synchronized static boolean launchReject() throws IOException {
return launch(REJECT_PORT);
}
/**
* Creates a new leaf that connects to the "primary" backend, running on
* 6300.
*/
public synchronized static boolean launchLeaf() throws IOException {
return launch(LEAF_PORT);
}
/**
* Launch backend process if it isn't already running
*
* @param reject true to launch the reject backend server, false to launch
* the regular backend server
* @return true if we have launched a new backend process false if one was
* already running
* @throws IOException if backend launch was unsucessful
*/
public synchronized static boolean launch(int port) throws IOException {
// Try to open a connection to our listening port. If it works, we
// will assume the backend is up and running
if (isPortInUse(port))
return false;
String[] args = new String[5];
args[0] = "java";
args[1] = "-classpath";
args[2] = System.getProperty("java.class.path", ".");
args[3] = Backend.class.getName();
args[4] = Integer.toString(port);
Process proc = Runtime.getRuntime().exec(args);
new CopyThread(proc.getErrorStream(), System.err);
new CopyThread(proc.getInputStream(), System.out);
try {
Thread.sleep(15000);
} catch (InterruptedException ex) {
}
if (!isPortInUse(port)) {
proc.destroy();
throw new IOException("Backend process failed to open port");
}
return true;
}
/** Simple thread to copy backend stdout and stderr */
private static class CopyThread extends Thread {
private InputStream is;
private PrintStream os;
public CopyThread(InputStream is, PrintStream os) {
this.is = is;
this.os = os;
start();
}
public void run() {
try {
while (true) {
int chr = is.read();
if (chr < 0)
break;
os.print((char) chr);
}
} catch (IOException ex) {
}
}
}
/**
* Determine if specfied port is in use
*
* @param port the port number
* @return true if port is in use
*/
private static boolean isPortInUse(int port) {
try {
Socket sock = new Socket("127.0.0.1", port);
try {
sock.close();
} catch (IOException ex) {
}
return true;
} catch (IOException ex) {
return false;
}
}
/**
* Sets the <tt>ErrorCallback</tt> class to which errors detected backend
* the backend servers will be reported. Only one of these an be active at
* any time.
*
* @param callback Callback interface to be called to report errors, or null
* to release the interface.
*/
public static void setErrorCallback(ErrorCallback callback) {
// A null callback means we want to stop listening.
if (callback == null) {
if (errorMonitor != null) {
errorMonitor.stopRunning();
}
errorMonitor = null;
// If errorMonitor is null, we need to make one.
} else if (errorMonitor == null) {
ServerSocket errorServer = null;
try {
errorServer = new ServerSocket(ERROR_PORT);
} catch (IOException e) {
callback.error(e);
return;
}
errorMonitor = new ErrorMonitor(callback, errorServer);
Thread errorThread = new Thread(errorMonitor, "ErrorMonitorThread");
errorThread.setDaemon(true);
errorThread.start();
Thread.yield(); // let it start up.
// The errorMonitor is already running,
// just redirect the error callback.
} else {
errorMonitor.setErrorCallback(callback);
}
}
/**
* Inner class that listens for errors reported from the backends and
* redirects them to the correct error callback.
*/
public static class ErrorMonitor implements Runnable {
private volatile ErrorCallback callback;
private volatile boolean isStopped = true;
private ServerSocket listenSocket = null;
ErrorMonitor(ErrorCallback cb, ServerSocket listen) {
callback = cb;
listenSocket = listen;
}
public void setErrorCallback(ErrorCallback cb) {
callback = cb;
}
public void stopRunning() {
isStopped = true;
try {
listenSocket.close();
} catch (IOException ignored) {
}
listenSocket = null;
}
public void run() {
try {
isStopped = false;
while (!isStopped) {
Socket sock = listenSocket.accept();
try {
sock.setSoTimeout(1000);
ObjectInputStream ois = new ObjectInputStream(sock.getInputStream());
Throwable error = (Throwable) ois.readObject();
callback.error(error);
sock.close();
} catch (Exception ex) {
callback.error(ex);
}
}
} catch (IOException ex) {
if (!isStopped) {
callback.error(ex);
try {
listenSocket.close();
} catch (IOException ignored) {
}
}
isStopped = true;
}
}
}
/**
* Shutdown normal backend process
*/
public static void shutdown() {
shutdown(false);
}
/**
* Shutdown backend process
*
* @param reject true to shut down the reject backend process false to shut
* down the normal reject process
*/
public static void shutdown(boolean reject) {
/*
* This might be called from an entirely separate JVM than the one
* running the backend, so we can't trust any of the mutable data
* members.
*
* Just opening a cnonection to the shutdown port should do it
*/
isPortInUse(reject ? SHUTDOWN_REJECT_PORT : SHUTDOWN_PORT);
}
/**
* Main method is necessary to run a stand-alone server that tests can be
* run off of.
*/
public static void main(String[] args) throws IOException {
boolean shutdown = false;
int port = Integer.parseInt(args[args.length - 1]);
if (shutdown) {
shutdown(port == REJECT_PORT);
} else {
new Backend(port);
}
}
@Inject private ConnectionServices connectionServices;
@Inject private FileManager fileManager;
@Inject private LifecycleManager lifecycleManager;
@Inject private NetworkManager networkManager;
/**
* Constructs and launches a new <tt>Backend</tt>.
*
* @param reject true to launch a reject server
*/
// private Backend(boolean reject) throws IOException {
private Backend(int port) throws IOException {
super("Backend");
System.getProperties().setProperty("org.apache.commons.logging.Log", "org.apache.commons.logging.impl.NoOpLog");
int shutdownPort = (port == BACKEND_PORT ? SHUTDOWN_PORT
: port == REJECT_PORT ? SHUTDOWN_REJECT_PORT
: port == LEAF_PORT ? SHUTDOWN_LEAF_PORT : -1);
ServerSocket shutdownSocket = null;
boolean reject = (port == REJECT_PORT);
try {
shutdownSocket = new ServerSocket(shutdownPort);
preSetUp();
setStandardSettings(port);
Guice.createInjector(Stage.PRODUCTION, new LimeWireCoreModule(ActivityCallbackStub.class), new AbstractModule() {
@Override
protected void configure() {
requestInjection(Backend.this);
}
});
lifecycleManager.start();
populateSharedDirectory(fileManager);
if (!reject)
connectionServices.connect();
try {
// sleep to let the file manager initialize
Thread.sleep(2000);
} catch (InterruptedException e) {
}
if (networkManager.getPort() != port) {
throw new IOException("Opened wrong port (wanted: " + port + ", was: "
+ networkManager.getPort() + ")");
}
waitForShutdown(shutdownSocket);
doShutdown(lifecycleManager, reject, "");
} catch (Exception ex) {
ErrorService.error(ex);
doShutdown(lifecycleManager, reject, "Exception thrown by backend shutdown monitor");
} finally {
try {
if (shutdownSocket != null)
shutdownSocket.close();
} catch (IOException ex2) {
}
try {
postTearDown();
} finally {
cleanFiles(_baseDir, true); // get rid of tmp dirs.
}
}
}
private void waitForShutdown(ServerSocket shutdownSocket) throws IOException {
Socket sock = null;
try {
while (true) {
sock = shutdownSocket.accept();
String host = sock.getInetAddress().getHostAddress();
sock.close();
if (host.equals("127.0.0.1"))
break;
}
} catch (IOException ex) {
try {
if (sock != null)
sock.close();
} catch (IOException ex2) {
}
throw ex;
}
}
/**
* Creates a temporary shared directory for testing purposes.
*/
private void populateSharedDirectory(FileManager fileManager) {
File coreDir;
coreDir = TestUtils.getResourceFile("com/limegroup/gnutella");
File[] files = coreDir.listFiles();
if (files != null) {
for (int i = 0; i < files.length; i++) {
if (!files[i].isFile())
continue;
fileManager.getGnutellaFileList().add(files[i]);
}
}
}
/**
* Sets the standard settings for a test backend, such as the ports, the
* number of connections to maintain, etc.
*/
private void setStandardSettings(int port) {
SearchSettings.GUESS_ENABLED.setValue(true);
UltrapeerSettings.DISABLE_ULTRAPEER_MODE.setValue(false);
UltrapeerSettings.EVER_ULTRAPEER_CAPABLE.setValue(true);
UltrapeerSettings.FORCE_ULTRAPEER_MODE.setValue(true);
NetworkSettings.PORT.setValue(port);
ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(true);
ConnectionSettings.CONNECT_ON_STARTUP.setValue(false);
ConnectionSettings.LOCAL_IS_PRIVATE.setValue(false);
ConnectionSettings.ACCEPT_DEFLATE.setValue(true);
ConnectionSettings.ENCODE_DEFLATE.setValue(true);
FilterSettings.BLACK_LISTED_IP_ADDRESSES.setValue(new String[] { "*.*.*.*" });
try {
FilterSettings.WHITE_LISTED_IP_ADDRESSES.setValue(new String[] { "127.*.*.*",
InetAddress.getLocalHost().getHostAddress() });
} catch (UnknownHostException bad) {
fail(bad);
}
}
/**
* Notifies <tt>RouterService</tt> that the backend should be shut down.
*/
private void doShutdown(LifecycleManager lifecycleManager, boolean reject, String msg) {
if(lifecycleManager != null)
lifecycleManager.shutdown();
System.exit(0);
}
/** Handles throwable error report from the backend */
public void error(Throwable ex) {
// First try to serialize the exception to the error port
try {
Socket sock = new Socket("127.0.0.1", ERROR_PORT);
ObjectOutputStream oos = new ObjectOutputStream(sock.getOutputStream());
oos.writeObject(ex);
oos.flush();
sock.close();
return;
} catch (IOException ex2) {
}
// If that didn't work, print a stack tracke and throw a runtime
// exception
ex.printStackTrace();
throw new RuntimeException(ex);
}
}