package edu.harvard.econcs.turkserver.server;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import java.io.FileNotFoundException;
import java.net.InetAddress;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import javax.swing.UIManager;
import net.andrewmao.misc.Utils;
import org.apache.commons.configuration.Configuration;
import org.apache.commons.configuration.ConfigurationException;
import com.amazonaws.mturk.requester.QualificationRequirement;
import com.google.common.collect.Lists;
import com.google.inject.AbstractModule;
import com.google.inject.Binding;
import com.google.inject.Guice;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import com.google.inject.name.Names;
import com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource;
import edu.harvard.econcs.turkserver.api.Configurator;
import edu.harvard.econcs.turkserver.config.DataModule;
import edu.harvard.econcs.turkserver.config.ServerModule;
import edu.harvard.econcs.turkserver.config.TSConfig;
import edu.harvard.econcs.turkserver.mturk.HITController;
import edu.harvard.econcs.turkserver.mturk.TurkHITController;
import edu.harvard.econcs.turkserver.server.gui.ServerFrame;
import edu.harvard.econcs.turkserver.server.gui.TSTabbedPanel;
import edu.harvard.econcs.turkserver.server.mysql.MySQLDataTracker;
/**
* The main TurkServer class.
*
* Contains static methods for starting experiments.
*
* @author mao
*
*/
public class TurkServer {
static {
// Set GUI LnF
try {
// com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel.class.getCanonicalName();
UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
} catch (Exception e) { e.printStackTrace(); }
}
final DataModule dataModule;
// Parent injector that can still make DB connections and other stuff
final Injector parentInjector;
final ServerFrame gui;
Injector childInjector;
SessionServer sessionServer;
public TurkServer(DataModule data) {
this.dataModule = data;
parentInjector = Guice.createInjector(dataModule);
gui = new ServerFrame(parentInjector.getInstance(TSTabbedPanel.class));
}
public TurkServer(Configuration conf) {
this(new DataModule(conf));
}
public TurkServer(String confFile) throws FileNotFoundException, ConfigurationException {
this(new DataModule(confFile));
}
public void runExperiment(ServerModule serverModule, AbstractModule... otherModules) {
if ( childInjector != null || sessionServer != null )
throw new RuntimeException("TurkServer doesn't support concurrent experiments yet.");
childInjector = parentInjector.createChildInjector(Lists.asList(serverModule, otherModules));
Configuration conf = dataModule.getConfiguration();
String url = conf.getString(TSConfig.MTURK_HIT_EXTERNAL_URL, null);
if( url == null ) {
System.out.println("URL not provided, finding public IP and port...");
InetAddress publicAddr = Utils.getNetworkAddr();
checkNotNull(publicAddr, "Couldn't find public a IP on this server");
int port = conf.getInt(TSConfig.SERVER_HTTPPORT);
url = String.format("http://%s:%d/", publicAddr.getHostAddress(), port);
}
checkExperimentConfiguration(childInjector, conf);
HITController thm = childInjector.getInstance(HITController.class);
// Post HITs if we are actually running on MTurk
if( childInjector.getExistingBinding(Key.get(TurkHITController.class)) != null ) {
// TODO this may not be in conf, but in injector (graph coloring stuff?)
thm.setHITType(
conf.getString(TSConfig.MTURK_HIT_TITLE),
conf.getString(TSConfig.MTURK_HIT_DESCRIPTION),
conf.getString(TSConfig.MTURK_HIT_KEYWORDS),
conf.getDouble(TSConfig.MTURK_HIT_BASE_REWARD),
conf.getInt(TSConfig.MTURK_ASSIGNMENT_DURATION),
conf.getInt(TSConfig.MTURK_AUTO_APPROVAL_DELAY),
childInjector.getInstance(QualificationRequirement[].class));
thm.setExternalParams(url,
conf.getInt(TSConfig.MTURK_HIT_FRAME_HEIGHT),
conf.getInt(TSConfig.MTURK_HIT_LIFETIME));
}
// GUI is automatically created from parent injector now
sessionServer = getSessionServerInstance(childInjector);
sessionServer.start();
// post an adaptive number of HITs based on stuff
int target = conf.getInt(TSConfig.SERVER_HITGOAL);
int min = conf.getInt(TSConfig.HITS_MIN_OVERHEAD);
int max = conf.getInt(TSConfig.HITS_MAX_OVERHEAD);
double pct = conf.getDouble(TSConfig.HITS_OVERHEAD_PERCENT);
int delay = conf.getInt(TSConfig.HITS_MIN_DELAY);
int maxDelay = conf.getInt(TSConfig.HITS_MAX_DELAY);
thm.postBatchHITs(target, min, max, delay, maxDelay, pct);
}
public SessionServer getSessionServer() {
return sessionServer;
}
private static SessionServer getSessionServerInstance(Injector injector) {
if( injector.getExistingBinding(new Key<SimpleExperimentServer>() {}) != null ) {
return injector.getInstance(SimpleExperimentServer.class);
}
else if( injector.getExistingBinding(new Key<GroupServer>() {}) != null ) {
return injector.getInstance(GroupServer.class);
}
else {
throw new RuntimeException("No binding found for session server. " +
"Try bindSingleExperiments() or bindGroupExperiments() in your module.");
}
}
/*
* Last check of sanity before we launch a server
*/
private static void checkExperimentConfiguration(Injector injector, Configuration conf) {
boolean debugMode = conf.getBoolean(TSConfig.SERVER_DEBUGMODE);
// Check MySQL configuration if using
if( injector.getBinding(MySQLDataTracker.class) != null ) {
MysqlConnectionPoolDataSource ds = injector.getInstance(MysqlConnectionPoolDataSource.class);
try( Connection conn = ds.getConnection() ) {
PreparedStatement sql = conn.prepareStatement("SHOW DATABASES");
sql.execute();
}
catch( SQLException e ) {
throw new RuntimeException("Database connection failed. Please check your settings.", e);
}
}
// TODO check AWS config if using. Also could be a good chance to check for cash
// Check bindings
checkNotNull(injector.getBinding(Key.get(String.class, Names.named(TSConfig.EXP_SETID))),
"set not specified");
checkNotNull(injector.getBinding(Key.get(Configurator.class, Names.named(TSConfig.EXP_CONFIGURATOR))),
"experiment configurator not specified");
Binding<QuizFactory> qf = injector.getBinding(Key.get(QuizFactory.class));
Binding<QuizPolicy> qp = injector.getBinding(Key.get(QuizPolicy.class));
checkArgument((qf == null) == (qp == null), "Either both QuizFactory and QuizPolicy must be specified, or neither");
// Check properties
checkNotNull(conf.getProperty(TSConfig.CONCURRENCY_LIMIT), "concurrent limit not specified");
checkNotNull(conf.getProperty(TSConfig.EXP_REPEAT_LIMIT), "set limit not specified");
// Check for Turk settings if real HITs will be created
if( injector.getExistingBinding(Key.get(TurkHITController.class)) != null ) {
checkNotNull(conf.getDouble(TSConfig.MTURK_HIT_BASE_REWARD, null),
"reward not specified");
checkNotNull(conf.getInteger(TSConfig.MTURK_HIT_FRAME_HEIGHT, null),
"frame height not set");
checkNotNull(conf.getInteger(TSConfig.MTURK_HIT_LIFETIME, null),
"hit lifetime not set");
checkNotNull( conf.getInteger(TSConfig.SERVER_HITGOAL, null),
"goal amount not specified");
checkNotNull( conf.getDouble(TSConfig.HITS_OVERHEAD_PERCENT, null),
"HIT overhead percentage not specified ");
checkNotNull( conf.getInteger(TSConfig.HITS_MIN_DELAY, null),
"delay not specified" );
checkNotNull( conf.getInteger(TSConfig.HITS_MIN_OVERHEAD, null),
"HIT min overhead not specified");
checkNotNull( conf.getInteger(TSConfig.HITS_MAX_OVERHEAD, null),
"HIT max overhead not specified");
if( !debugMode ) { // Ignore these settings for local test
checkNotNull(injector.getExistingBinding(Key.get(QualificationRequirement[].class)),
"No qualifications set!");
}
}
// Check that experiment class is proper
Class<?> expClass = injector.getInstance(Key.get(new TypeLiteral<Class<?>>() {}, Names.named(TSConfig.EXP_CLASS)));
checkNotNull(expClass, "experiment class not specified");
EventAnnotationManager.testCallbacks(expClass);
}
/**
* shuts down the experiment,
* waits for threads to finish,
* then disposes the GUI
*/
public void orderlyShutdown() {
stopExperiment();
awaitTermination();
disposeGUI();
}
public void stopExperiment() {
if( sessionServer == null ) return;
sessionServer.shutdown();
}
/**
* Wait for servers to stop, then destroys the GUI.
*/
public void awaitTermination() {
if( sessionServer == null ) return;
try {
sessionServer.join();
} catch (InterruptedException e) {}
sessionServer = null;
}
public void disposeGUI() {
gui.dispose();
// VM should exit here?
}
@Override
protected void finalize() {
// Was causing mysterious window closings
// gui.dispose();
}
public static void main(String[] args) throws Exception {
DataModule dm;
if( args.length > 0 ) {
String file = args[0];
System.out.println("Trying to start TurkServer with " + file);
dm = new DataModule(file);
}
else {
System.out.println("Starting TurkServer with default settings (can't do much)");
dm = new DataModule();
}
TurkServer ts = new TurkServer(dm);
ts.awaitTermination();
}
}