package net.gnehzr.tnoodle.server;
import static net.gnehzr.tnoodle.utils.GwtSafeUtils.azzert;
import tray.SystemTrayProvider;
import tray.SystemTrayAdapter;
import tray.TrayIconAdapter;
import tray.java.JavaIconAdapter;
import java.awt.Desktop;
import java.awt.Image;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.PopupMenu;
import java.awt.MenuItem;
import java.awt.SystemTray;
import java.awt.TrayIcon;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.BindException;
import java.net.MalformedURLException;
import java.net.ServerSocket;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLConnection;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.naming.NamingException;
import javax.swing.ImageIcon;
import joptsimple.OptionParser;
import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import joptsimple.util.KeyValuePair;
import net.gnehzr.tnoodle.utils.Launcher;
import net.gnehzr.tnoodle.utils.TNoodleLogging;
import net.gnehzr.tnoodle.utils.Utils;
import net.gnehzr.tnoodle.utils.GwtSafeUtils;
import winstone.TNoodleWinstoneLauncher;
public class TNoodleServer {
private static final Logger l = Logger.getLogger(TNoodleServer.class.getName());
public static String NAME = Utils.getProjectName();
public static String VERSION = Utils.getVersion();
private static final int TNOODLE_PORT = 2014;
private static final int MIN_HEAP_SIZE_MEGS = 512;
private static final String ICONS_FOLDER = "icons";
private static final String ICON_WRAPPER = "tnoodle_logo_1024_gray.png";
private static final String ICON_WORKER = "tnoodle_logo_1024.png";
private static final String DB_NAME = "tnoodledb";
private static final String DB_USERNAME = "root";
private static final String DB_PASSWORD = "password";
private int httpPort;
public TNoodleServer(int httpPort, boolean bindAggressively, boolean browse) throws IOException, ClassNotFoundException, NamingException {
this.httpPort = httpPort;
// at startup
Map<String, String> serverArgs = new HashMap<String, String>();
serverArgs.put("webappsDir", Utils.getWebappsDir().getAbsolutePath());
serverArgs.put("httpPort", "" + httpPort);
serverArgs.put("ajp13Port", "-1");
String dbDriver = "org.h2.Driver";
boolean initializeDb;
try {
Class.forName(dbDriver);
initializeDb = true;
} catch(ClassNotFoundException e) {
initializeDb = false;
l.info("Could not find class " + dbDriver + ", so we're not creating an entry in JNDI for a db.");
}
if(initializeDb) {
File db = new File(Utils.getResourceDirectory(), DB_NAME);
serverArgs.put("useJNDI", "true");
serverArgs.put("jndi.resource.jdbc/connPool", "javax.sql.DataSource");
serverArgs.put("jndi.param.jdbc/connPool.url", String.format("jdbc:h2:%s;MODE=MySQL;USER=%s;PASSWORD=%s;MVCC=TRUE", db.getAbsoluteFile(), DB_USERNAME, DB_PASSWORD));
serverArgs.put("jndi.param.jdbc/connPool.driverClassName", dbDriver);
serverArgs.put("jndi.param.jdbc/connPool.username", DB_USERNAME);
serverArgs.put("jndi.param.jdbc/connPool.password", DB_PASSWORD);
}
// By default, winstone looks in ./lib, which I don't like, as it means
// we'll behave differently when run from different directories.
serverArgs.put("commonLibFolder", "");
ServerSocket ss = aggressivelyBindSocket(httpPort, bindAggressively);
azzert(ss != null);
final Logger winstoneLogger = Logger.getLogger(winstone.Logger.class.getName());
winstone.Logger.init(winstone.Logger.MAX, new OutputStream() {
private StringBuilder msg = new StringBuilder();
@Override
public void write(int b) throws IOException {
char ch = (char) b;
if(ch == '\n') {
winstoneLogger.log(Level.FINER, msg.toString());
msg.setLength(0);
} else {
msg.append(ch);
}
}
}, false);
TNoodleWinstoneLauncher.create(serverArgs, ss);
System.out.println(NAME + "-" + VERSION + " started");
String url = openTabInBrowser(browse);
System.out.println("Visit " + url + " for a readme and demo.");
}
public String openTabInBrowser(boolean browse) {
String url = "http://localhost" + ":" + this.httpPort;
if(browse) {
if(Desktop.isDesktopSupported()) {
Desktop d = Desktop.getDesktop();
if(d.isSupported(Desktop.Action.BROWSE)) {
try {
URI uri = new URI(url);
l.info("Attempting to open " + uri + " in browser.");
d.browse(uri);
} catch(URISyntaxException e) {
l.log(Level.WARNING, "Could not convert " + url + " to URI", e);
} catch(IOException e) {
l.log(Level.WARNING, "Error opening tab in browser", e);
}
} else {
l.warning("Sorry, it appears the Desktop api is supported on your platform, but the BROWSE action is not.");
}
} else {
l.warning("Sorry, it appears the Desktop api is not supported on your platform.");
}
}
return url;
}
private ServerSocket aggressivelyBindSocket(int port, boolean bindAggressively) throws IOException {
ServerSocket server = null;
final int MAX_TRIES = 10;
for(int i = 0; i < MAX_TRIES && server == null; i++) {
if(i > 0) {
System.out.println("Attempt " + (i+1) + "/" + MAX_TRIES + " to bind to port " + port);
}
try {
server = new ServerSocket(port, AggressiveHttpListener.getBacklogCount());
} catch(BindException e) {
// If this port is in use, we assume it's an instance of
// TNoodleServer, and ask it to commit honorable suicide.
// After that, we can start up. If it was a TNoodleServer,
// it hopefully will have freed up the port we want.
System.out.println("Detected server running on port " + port + ", maybe it's an old " + NAME + "?");
if(!bindAggressively) {
System.out.println("noupgrade option set. You'll have to free up port " + port + " manually, or clear this option.");
break;
}
try {
URL url = new URL("http://localhost:" + port + "/kill/now");
System.out.println("Sending request to " + url + " to hopefully kill it.");
URLConnection conn = url.openConnection();
InputStream in = conn.getInputStream();
in.close();
Thread.sleep(1000);
} catch(MalformedURLException ee) {
ee.printStackTrace();
} catch(IOException ee) {
ee.printStackTrace();
} catch(InterruptedException ee) {
ee.printStackTrace();
}
}
}
if(server == null) {
System.out.println("Failed to bind to port " + port + ". Giving up");
System.exit(1);
}
return server;
}
// Preferred way to detect OSX according to https://developer.apple.com/library/mac/#technotes/tn2002/tn2110.html
public static boolean isOSX() {
String osName = System.getProperty("os.name");
return osName.contains("OS X");
}
/*
* Sets the dock icon in OSX. Could be made to have uses in other operating systems.
*/
private static void setApplicationIcon() {
// Find out which icon to use.
final Launcher.PROCESS_TYPE processType = Launcher.getProcessType();
final String iconFileName;
switch (processType) {
case WORKER:
iconFileName = ICON_WORKER;
break;
default:
iconFileName = ICON_WRAPPER;
break;
}
// Get the file name of the icon.
final String fullFileName = Utils.getResourceDirectory() + "/" + ICONS_FOLDER + "/" + iconFileName;
final Image image = new ImageIcon(fullFileName).getImage();
// OSX-specific code to set the dock icon.
if(isOSX()) {
try {
final com.apple.eawt.Application application = com.apple.eawt.Application.getApplication();
application.setDockIconImage(image);
} catch(Exception e) {
l.log(Level.WARNING, "Error setting OSX dock icon", e);
}
} else {
if(iconFileName != ICON_WORKER) {
// Only want to create one tray icon.
return;
}
if(!SystemTray.isSupported()) {
l.warning("SystemTray is not supported");
return;
}
URL imageUrl;
try {
imageUrl = new File(fullFileName).toURI().toURL();
} catch(MalformedURLException e) {
l.log(Level.WARNING, "Could not convert " + fullFileName + " to a URL", e);
return;
}
SystemTrayAdapter trayAdapter = new SystemTrayProvider().getSystemTray();
final PopupMenu popup = new PopupMenu();
MenuItem openItem = new MenuItem("Open");
popup.add(openItem);
MenuItem exitItem = new MenuItem("Exit");
popup.add(exitItem);
openItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
if(tnoodleServer != null) {
tnoodleServer.openTabInBrowser(true);
}
}
});
exitItem.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
l.info("Exit initiated from tray icon");
System.exit(0);
}
});
TrayIconAdapter trayIconAdapter = trayAdapter.createAndAddTrayIcon(
imageUrl,
NAME + " v" + VERSION,
popup);
if(trayIconAdapter instanceof JavaIconAdapter) {
// Unfortunately, java internally uses some shitty resizing
// algorithm for this, so we have to do this ourselves.
//trayIconAdapter.setImageAutoSize(true);
SystemTray st = SystemTray.getSystemTray();
JavaIconAdapter jia = (JavaIconAdapter) trayIconAdapter;
TrayIcon ti = jia.getTrayIcon();
ti.setImage(image.getScaledInstance(st.getTrayIconSize().width, -1, Image.SCALE_SMOOTH));
}
}
}
private static TNoodleServer tnoodleServer = null;
public static void main(String[] args) throws IOException {
Utils.doFirstRunStuff();
TNoodleLogging.initializeLogging();
OptionParser parser = new OptionParser();
OptionSpec<Integer> httpPortOpt = parser.acceptsAll(Arrays.asList("p", "http"),
"The port to run the http server on")
.withRequiredArg().ofType(Integer.class).defaultsTo(TNOODLE_PORT);
OptionSpec<?> noBrowserOpt = parser.acceptsAll(Arrays.asList("n", "nobrowser"),
"Don't open the browser when starting the server");
OptionSpec<?> noUpgradeOpt = parser.acceptsAll(Arrays.asList("u", "noupgrade"),
"If an instance of " + NAME + " is running on the desired port(s), " +
"do not attempt to kill it and start up");
OptionSpec<?> noReexecOpt = parser.acceptsAll(Arrays.asList(Launcher.NO_REEXEC_OPT),
"Do not reexec. This is sometimes done to rename java.exe on Windows, or to get a larger heap size.");
OptionSpec<File> injectJsOpt = parser.acceptsAll(Arrays.asList("i", "inject"),
"File containing code to inject into the bottom of the " +
"<head>...</head> section of all html served")
.withRequiredArg().ofType(File.class);
OptionSpec<KeyValuePair> jsEnvOpt = parser.accepts("jsenv",
"Add entry to global js object TNOODLE_ENV in /env.js. " +
"Treated as strings, so FOO=42 will create the entry TNOODLE_ENV['FOO'] = '42';")
.withOptionalArg().ofType(KeyValuePair.class);
OptionSpec<?> help = parser.acceptsAll(Arrays.asList("h", "help", "?"), "Show this help");
String levels = GwtSafeUtils.join(TNoodleLogging.getLevels(), ",");
OptionSpec<String> consoleLogLevel = parser.
acceptsAll(Arrays.asList("cl", "consoleLevel"),
"The minimum level a log must be to be " +
"printed to the console. Options: " + levels).
withRequiredArg().
ofType(String.class).
defaultsTo(Level.WARNING.getName());
OptionSpec<String> fileLogLevel = parser.
acceptsAll(Arrays.asList("fl", "fileLevel"),
"The minimum level a log must be to be printed to " +
TNoodleLogging.getLogFile() + ". Options: " + levels).
withRequiredArg().
ofType(String.class).
defaultsTo(Level.INFO.getName());
try {
OptionSet options = parser.parse(args);
if(!options.has(help)) {
boolean bindAggressively = !options.has(noUpgradeOpt);
Level cl = Level.parse(options.valueOf(consoleLogLevel));
TNoodleLogging.setConsoleLogLevel(cl);
Level fl = Level.parse(options.valueOf(fileLogLevel));
TNoodleLogging.setFileLogLevel(fl);
// Note that we set the log level *before* we do any of this.
// These two calls to setApplicationIcon() are intentional.
// We want different icons for the parent process and the child
// process.
setApplicationIcon();
Launcher.wrapMain(args, MIN_HEAP_SIZE_MEGS);
setApplicationIcon();
if(options.has(injectJsOpt)) {
File injectCodeFile = options.valueOf(injectJsOpt);
if(!injectCodeFile.exists() || !injectCodeFile.canRead()) {
System.err.println("Cannot find or read " + injectCodeFile);
System.exit(1);
}
HtmlInjectFilter.setHeadInjectFile(injectCodeFile);
}
if(options.has(jsEnvOpt)) {
List<KeyValuePair> jsEnv = options.valuesOf(jsEnvOpt);
for(KeyValuePair key_value : jsEnv) {
JsEnvServlet.putJsEnv(key_value.key, key_value.value);
}
}
int httpPort = options.valueOf(httpPortOpt);
boolean openBrowser = !options.has(noBrowserOpt);
tnoodleServer = new TNoodleServer(httpPort, bindAggressively, openBrowser);
return;
}
} catch(Exception e) {
e.printStackTrace();
}
parser.printHelpOn(System.out);
System.exit(1); // non zero exit status
}
}