package com.limegroup.gnutella; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; 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 com.limegroup.gnutella.settings.ConnectionSettings; import com.limegroup.gnutella.settings.FilterSettings; import com.limegroup.gnutella.settings.SearchSettings; import com.limegroup.gnutella.settings.SharingSettings; import com.limegroup.gnutella.settings.UltrapeerSettings; import com.limegroup.gnutella.stubs.ActivityCallbackStub; import com.limegroup.gnutella.util.CommonUtils; /** * 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. */ public class Backend extends com.limegroup.gnutella.util.BaseTestCase { /** 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(10000); } 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(); } // 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); } } /** * 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.out.println(port == REJECT_PORT ? "STARTING REJECT BACKEND" : port == LEAF_PORT ? "STARTING LEAF BACKEND" : port == BACKEND_PORT ? "STARTING NORMAL BACKEND" : "STARTING UNKNOWN BACKEND"); 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); populateSharedDirectory(); ROUTER_SERVICE = new RouterService(new ActivityCallbackStub()); ROUTER_SERVICE.start(); if (!reject) RouterService.connect(); try { // sleep to let the file manager initialize Thread.sleep(2000); } catch(InterruptedException e) {} if (RouterService.getPort() != port) { throw new IOException("Opened wrong port (wanted: " + port + ", was: " + RouterService.getPort() +")"); } waitForShutdown(shutdownSocket); doShutdown(reject, ""); } catch (Exception ex) { ErrorService.error(ex); doShutdown(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; System.out.println("Ignoring shutdown request from" + host); } } 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() { File coreDir; coreDir = CommonUtils.getResourceFile("com/limegroup/gnutella"); File[] files = coreDir.listFiles(); if ( files != null ) { for(int i=0; i<files.length; i++) { if(!files[i].isFile()) continue; copyResourceFile(files[i], files[i].getName() + "."+ SHARED_EXTENSION); } } } /** * Sets the standard settings for a test backend, such as the ports, the * number of connections to maintain, etc. */ private void setStandardSettings(int port) { SharingSettings.EXTENSIONS_TO_SHARE.setValue(SHARED_EXTENSION); SearchSettings.GUESS_ENABLED.setValue(true); UltrapeerSettings.DISABLE_ULTRAPEER_MODE.setValue(false); UltrapeerSettings.EVER_ULTRAPEER_CAPABLE.setValue(true); UltrapeerSettings.FORCE_ULTRAPEER_MODE.setValue(true); ConnectionSettings.PORT.setValue(port); ConnectionSettings.EVER_ACCEPTED_INCOMING.setValue(true); ConnectionSettings.CONNECT_ON_STARTUP.setValue(false); ConnectionSettings.LOCAL_IS_PRIVATE.setValue(false); ConnectionSettings.USE_GWEBCACHE.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(boolean reject, String msg) { String message = (reject ? "REJECT BACKEND SHUTDOWN" : "NORMAL BACKEND SHUTDOWN"); if (msg != null) message = message + ": " + msg; System.out.println(message); RouterService.shutdown(); System.exit(0); } /** * Copies the specified resource file into the current directory from * the jar file. If the file already exists, no copy is performed. * * @param fileName the name of the file to copy * @param newName the new name for the target */ private final void copyResourceFile(final File fileToCopy, String newName) { File file = new File(getSharedDirectory(), newName); // return quickly if the file is already there, no copy necessary if(file.exists() ) return; BufferedInputStream bis = null; BufferedOutputStream bos = null; try { InputStream is = new FileInputStream(fileToCopy); //buffer the streams to improve I/O performance final int bufferSize = 2048; bis = new BufferedInputStream(is, bufferSize); file.deleteOnExit(); bos = new BufferedOutputStream(new FileOutputStream(file), bufferSize); byte[] buffer = new byte[bufferSize]; int c = 0; do { //read and write in chunks of buffer size until EOF reached c = bis.read(buffer, 0, bufferSize); bos.write(buffer, 0, c); } while (c == bufferSize); //(# of bytes read)c will = bufferSize until EOF } catch(Exception e) { //if there is any error, delete any portion of file that did write file.delete(); } finally { try { if(bis != null) bis.close(); if(bos != null) bos.close(); } catch(IOException ioe) {} // all we can do is try to close the streams } } /** 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); } }