package edu.washington.cs.oneswarm.test.integration.oop;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;
import org.gudy.azureus2.core3.util.Constants;
import org.junit.Assert;
import com.google.common.io.Files;
import edu.washington.cs.oneswarm.test.util.ConditionWaiter;
import edu.washington.cs.oneswarm.test.util.ProcessLogConsumer;
import edu.washington.cs.oneswarm.test.util.TestUtils;
/**
* Encapsulates a locally running testing instance of OneSwarm. Each instance of
* this class corresponds to a separate process running on the local machine.
*
* This class should only be used by integration tests.
*/
public class LocalOneSwarm {
private static Logger logger = Logger.getLogger(LocalOneSwarm.class.getName());
// Used to automatically choose instance labels.
private static int instanceCount = 0;
/** The set of listeners. */
List<LocalOneSwarmListener> listeners = Collections
.synchronizedList(new ArrayList<LocalOneSwarmListener>());
/** The configuration of this instance. */
LocalOneSwarm.Config config = new LocalOneSwarm.Config();
/** The running OneSwarm process. */
Process process = null;
/** The root path to the OneSwarm. Paths are constructed relative to this. */
String rootPath = null;
/** The experimental coordinator for this instance. */
LocalOneSwarmCoordinator coordinator = null;
/** The current state of this instance. */
State state = State.CONFIGURING;
/** Possible states of a LocalOneSwarm instance. */
public enum State {
CONFIGURING, STARTING, RUNNING,
};
/** Configuration parameters for the local instance. */
public class Config {
/** The label for this instance. */
String label = "LocalOneSwarm";
/** The path to system java. */
String javaPath = "java";
/** The path to the GWT war output directory. */
String warRootPath;
/** The port on which the local webserver listens for GUI connections. */
int webUiPort = 29615;
/**
* The port to use for the StartServer (used to pass params on Windows
* and detect multiple concurrent invocations on other platforms.
*/
int startServerPort = 6885;
/** Classpath elements for the invocation. */
List<String> classPathElements = new ArrayList<String>();
/** System properties for the instance. */
Map<String, String> systemProperties = new HashMap<String, String>();
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public String getWarRootPath() {
return warRootPath;
}
public void setWarRootPath(String path) {
warRootPath = path;
}
public List<String> getClassPathElements() {
return classPathElements;
}
public void addClassPathElement(String path) {
classPathElements.add(path);
}
public int getWebUiPort() {
return webUiPort;
}
public void setWebUiPort(int port) {
webUiPort = port;
}
public int getStartServerPort() {
return startServerPort;
}
public void setStartServerPort(int port) {
startServerPort = port;
}
public void addSystemProperty(String key, String value) {
systemProperties.put(key, value);
}
}
/** Forcibly shuts down the OneSwarm instance associated with this object. */
Thread cancelThread = new Thread() {
@Override
public void run() {
if (process == null) {
return;
}
logger.info("Attemting to kill: " + process);
process.destroy();
}
};
public LocalOneSwarm(boolean experimentalSupport) throws IOException {
rootPath = new File(".").getAbsolutePath();
config.setLabel("LocalOneSwarm-" + instanceCount);
/*
* 2 * instanceCount since the client uses the local port + 1 for
* SSL/remote access, and we specify 3*instanceCount+2 as the start
* server port for the instance.
*/
config.setWebUiPort(3000 + (3 * instanceCount));
config.setStartServerPort(3000 + (3 * instanceCount + 2));
if (experimentalSupport) {
config.addSystemProperty("oneswarm.experimental.config.file", "dummy");
}
// The coordinator listens for connections from running clients and
// sends commands
coordinator = new LocalOneSwarmCoordinator(this);
coordinator.start();
instanceCount++;
config.setWarRootPath("gwt-bin/war");
if (System.getProperty("oneswarm.test.local.classpath") == null) {
System.err.println("********************************************************\n"
+ "* Need to specify oneswarm.test.local.classpath *\n"
+ "* *\n"
+ "* To support both IDE auto builds and ant builds, *\n"
+ "* LocalOneSwarm requires you to manually set the *\n"
+ "* OneSwarm-specific classpath entries. See the ant *\n"
+ "* build.xml run-tests target for an example of this *\n"
+ "* value. *\n"
+ "* (If you're building in eclipse, perhaps add: *\n"
+ "* -Doneswarm.test.local.classpath= *\n"
+ "* oneswarm_gwt_ui/war/WEB-INF/classes *\n"
+ "* to your run configuration JVM parameters. *\n"
+ "********************************************************");
Assert.fail();
}
String[] entries = System.getProperty("oneswarm.test.local.classpath").split(":");
for (String entry : entries) {
config.addClassPathElement(entry);
System.out.println("Added " + entry + " to cp");
}
/* SWT */
String swt = "build/swt/";
if (Constants.isOSX) {
swt += "swt-osx-cocoa-x86_64.jar";
} else if (Constants.isLinux) {
if (System.getProperty("sun.arch.data.model").equals("32")) {
swt += "swt-gtk-linux-x86.jar";
} else {
swt += "swt-gtk-linux-x86_64.jar";
}
} else if (Constants.isWindows) {
swt += "swt-win32-x86.jar";
}
config.addClassPathElement(swt);
/* Other dependencies */
final String COMMONS = "build/gwt-libs/commons-http/";
final String GWT = "build/gwt-libs/";
final String F2F = "build/f2f-libs/";
final String CORE = "build/core-libs/";
String[] deps = {
// Apache commons
COMMONS + "jcip-annotations.jar", COMMONS + "httpmime-4.0.jar",
COMMONS + "httpcore-4.0.1.jar", COMMONS + "httpclient-4.0.jar",
COMMONS + "commons-logging-1.1.1.jar", COMMONS + "commons-io-1.3.2.jar",
COMMONS + "commons-fileupload-1.2.1.jar",
COMMONS + "commons-codec-1.4.jar",
COMMONS + "apache-mime4j-0.6.jar",
// GWT Core
GWT + "gwt/gwt-user.jar",
GWT + "gwt/gwt-servlet.jar",
GWT + "gwt/gwt-dev.jar",
// GWT Libs
GWT + "gwt-dnd/gwt-dnd-2.6.5.jar",
// TODO(piatek): Move this?
GWT + "jaudiotagger.jar",
// Jetty
GWT + "jetty/jetty.jar", GWT + "jetty/jetty-util.jar",
GWT + "jetty/jetty-servlet-api.jar", GWT + "jetty/jetty-management.jar",
// F2F Libs
F2F + "smack.jar", F2F + "publickey-client.jar", F2F + "ecs-1.4.2.jar",
// Core libs
CORE + "derby.jar", CORE + "log4j.jar", CORE + "junit.jar",
CORE + "commons-cli.jar", CORE + "guava.jar", };
for (String dep : deps) {
config.addClassPathElement(dep);
}
}
/** Adds {@code listener} to the set of listeners. */
public void addListener(LocalOneSwarmListener listener) {
listeners.add(listener);
}
/** Removes {@code listener} from the set of listeners. */
public void removeListener(LocalOneSwarmListener listener) {
listeners.remove(listener);
}
/** Returns the current state of the instance. */
public State getState() {
return state;
}
/**
* Called by our OneSwarmCoordinator when receiving heartbeats from clients.
*/
void coordinatorReceivedHeartbeat() {
if (state == State.STARTING) {
state = State.RUNNING;
// Broadcast the start event to listeners
for (LocalOneSwarmListener l : listeners.toArray(new LocalOneSwarmListener[0])) {
l.instanceStarted(this);
}
}
}
/** Asynchronously starts the process associated with this instance. */
public void start() throws IOException {
state = State.STARTING;
StringBuilder propertiesString = new StringBuilder();
for (String property : config.systemProperties.keySet()) {
propertiesString.append(" -D" + property + "=" + config.systemProperties.get(property));
}
// Construct a ProcessBuilder with common options
ProcessBuilder pb = new ProcessBuilder(config.javaPath, "-Xmx256m", "-Ddebug.war="
+ new File(rootPath, config.warRootPath), "-Dazureus.security.manager.install=0",
"-DMULTI_INSTANCE=true" + propertiesString);
List<String> command = pb.command();
// Add platform-specific options
if (Constants.isOSX) {
command.add("-XstartOnFirstThread");
}
// Add classpath
StringBuilder cpString = new StringBuilder();
for (String path : config.classPathElements) {
File entry = new File(rootPath, path);
if (entry.exists() == false) {
logger.warning("Classpath entry not found: " + entry.getAbsolutePath());
}
cpString.append(entry.getAbsolutePath());
cpString.append(File.pathSeparator);
}
command.add("-cp");
// -1 because of the spurious ':' at the end
command.add(cpString.substring(0, cpString.length() - 1));
// Configure system properties for test instances
Map<String, String> scratchPaths = TestUtils.createScratchLocationsForTest(config.label);
logger.info(config.getLabel() + " paths: " + scratchPaths);
/*
* Create the experimental config file that will register this client
* with our locally running coordination server.
*/
PrintStream experimentalConfig = new PrintStream(new FileOutputStream(
scratchPaths.get("experimentalConfig")));
experimentalConfig
.println("inject edu.washington.cs.oneswarm.test.integration.oop.LocalOneSwarmExperiment");
experimentalConfig.println("name " + config.getLabel());
experimentalConfig.println("register http://127.0.0.1:" + coordinator.getServerPort()
+ "/s");
// Disable lan-, cht-, dht-friend connect
// experimentalConfig.println("booleanSetting OSF2F.Use@DHT@Proxy false");
// experimentalConfig.println("booleanSetting OSF2F.LanFriendFinder false");
// experimentalConfig.println("booleanSetting dht.enabled false");
// Make it communicate regularly for shorter test timeouts.
experimentalConfig.println("setprop oneswarm.test.coordinator.poll 1");
experimentalConfig.close();
// Add the appropriate config properties
command.add("-Doneswarm.integration.test=1");
command.add("-Doneswarm.integration.user.data=" + scratchPaths.get("userData"));
command.add("-Dazureus.config.path=" + scratchPaths.get("userData"));
command.add("-Doneswarm.integration.web.ui.port=" + config.getWebUiPort());
command.add("-Doneswarm.integration.start.server.port=" + config.getStartServerPort());
command.add("-Doneswarm.experimental.config.file=" + scratchPaths.get("experimentalConfig"));
command.add("-Dnolaunch_startup=1");
command.add("-Doneswarm.test.coordinator.poll=2");
if (Constants.isWindows) {
command.add("-Djava.library.path="
+ (new File(rootPath, "build/core-libs/dll").getAbsolutePath()));
}
// Main class
command.add("com.aelitis.azureus.ui.Main");
// Kick-off: merge stderr and stdout, set the working directory, and
// start.
pb.redirectErrorStream(true);
File workingDirFile = new File(scratchPaths.get("workingDir"));
pb.directory(workingDirFile);
// If there's a local logging.properties here, copy it to the
// workingDir.
File localLoggingProperties = new File("logging.properties");
if (localLoggingProperties.exists()) {
Files.copy(localLoggingProperties, new File(workingDirFile, "logging.properties"));
logger.info("Copied local logging.properties to OOP instace.");
}
process = pb.start();
logger.info("Forked OneSwarm instance: " + config.label);
// Consume the unified log.
new ProcessLogConsumer(config.label, process).start();
// Make sure this process gets torn down when if the test is killed
Runtime.getRuntime().addShutdownHook(cancelThread);
}
/** Asynchronously stops the process associated with this instance. */
public void stop() {
cancelThread.start();
coordinator.setDone();
Runtime.getRuntime().removeShutdownHook(cancelThread);
}
/** Blocks until {@count} friends are online. */
public void waitForOnlineFriends(final int count) {
new ConditionWaiter(new ConditionWaiter.Predicate() {
@Override
public boolean satisfied() {
return getCoordinator().onlineFriendCount >= count;
}
}, 40 * 1000).awaitFail();
}
public void waitForClean() {
new ConditionWaiter(new ConditionWaiter.Predicate() {
@Override
public boolean satisfied() {
return getCoordinator().getPendingCommands().size() == 0;
}
}, 40 * 1000).awaitFail();
}
/** Blocks until the instance's public key is available and returns it. */
public String getPublicKey() {
new ConditionWaiter(new ConditionWaiter.Predicate() {
@Override
public boolean satisfied() {
return getCoordinator().encodedPublicKey != null;
}
}, 20 * 1000).awaitFail();
return getCoordinator().encodedPublicKey;
}
/** Returns the root path of the OneSwarm build folder. */
public String getRootPath() {
return rootPath;
}
public LocalOneSwarmCoordinator getCoordinator() {
return coordinator;
}
@Override
public String toString() {
return config.label;
}
public static void main(String[] args) throws Exception {
new LocalOneSwarm(false).start();
while (true) {
Thread.sleep(100);
}
}
/** Returns the label of this instance. */
public String getLabel() {
return config.label;
}
}