Start Stop Embedded Jetty Programmatically


Recently I am trying to package embedded jetty, solr.war, and solr.home in one package, and start and shut the embedded jetty server dynamically.

About how to package embedded jetty, solr.war, and solr.home in one package, and reduce the size, please refer to:
Part 1: Shrink Solr Application Size
Part 2: Use Proguard to Shrink Solr Application Size
Part 3: Use Pack200 to Shrink Solr Application Size
This article would like to talk about how to start and shutdown embedded jetty server dynamically.

There are several things we need consider:
1. How to make sure only one instance running?
Fo each unzipped package, user should only run it once, if user clicks the run.bat again, it should report the application is already running.

I found this article, the basic idea is to create a lock file, and lock it when the application is running. When application ended/closed release the lock and delete the file lock. If not able to create and lock the file, it means the application is already running.
2. Dynamical Port
If the port user gives is not available, maybe already occupied by other application, we will try to find a free port in a range.
How to check whether a port is available?
We can try to create a ServerSocket, bound to that port, if it throws exception, means it is already occupied, if not, means it is a free port.
ServerSocket socket = new ServerSocket(port);
3. How to start and shutdown embedded jetty
The code is like below:
To make sure this jetty exclusively binds to the port, we need create a SelectChannelConnector, and setReuseAddress false.

To make the jetty server shutdown itself on a valid request, add a ShutdownHandler with SHUTDOWN_PASSWORD.Please refer to the article.

Then later, users can call http://host:por/shutdown?token=SHUTDOWN_PASSWORD&_exitJvm=true, _exitJvm=true makes the jvm also exits, the application would end.

Server server = new Server();
// add solr.war
WebAppContext webapp1 = new WebAppContext();
webapp1.setContextPath("/solr");
webapp1.setWar(solrWarPath);

HandlerList handlers = new HandlerList();
handlers.setHandlers(new Handler[] {webapp1,
  new ShutdownHandler(server, SHUTDOWN_PASSWORD)});
server.setHandler(handlers);

SelectChannelConnector connector = new SelectChannelConnector();
connector.setReuseAddress(false);
connector.setPort(port);
server.setConnectors(new Connector[] {connector});

server.start();
Complete Code
The main code to start and shutdown jetty is like below. 
The complete code including scripts to start and shutdown jetty, and configuration file is in Github.
package org.codeexample.jeffery.misc;
public class EmbeddedSolrJettyServer {
  
  private static final String SYS_APP_HOME = "app.home";
  private static final String SYS_SOLR_SOLR_HOME = "solr.solr.home";
  /**
   * The following four parameter can be configured in command line
   */
  private static final String ARG_EXIT_AFTER = "exitAfter";
  private static final String ARG_PORT = "port";
  private static final String ARG_DYNAMIC_PORT = "dynamicPort";
  private static final String ARG_LOCALONLY = "localOnly";
  
  private static final String ARG_SEARCH_PORT_START_RANGE = "searchPortStartRange";
  private static final String ARG_SEARCH_PORT_END_RANGE = "searchPortEndRange";
  
  /**
   * hidden parameter, which will only take effect once, will be removed after
   * that.
   */
  private static final String ARG_DEBUG = "debug";
  
  private static final int DEFAULT_PORT = 8080;
  private static final String CONFILE_FILE = "/etc/config.properties";
  private static final String LOCK_FILE  = "app.lock";
  private static final String RESULT_FILE = "result";
  
  private static final int DEFAULT_SEARCH_PORT_START_RANGE = 5000;
  private static final int DEFAULT_SEARCH_PORT_END_RANGE = 50000;
  
  private static File lockFile;
  private static FileChannel channel;
  private static FileLock lock;
  
  private static String SHUTDOWN_PASSWORD = "shutdown_passwd";
  private String appBaseLocation;
  boolean debug = false;
  public static void main(String[] args) throws Exception {
    
    EmbeddedSolrJettyServer instance = new EmbeddedSolrJettyServer();
    instance.handleRequest(args);
  }
  
  private void handleRequest(String[] args) throws Exception {
    appBaseLocation = getBaseLocation();
    
    if (args.length < 1) {
      exitWithError("No arguments.");
    }
    
    String str = args[0];
    if ("start".equalsIgnoreCase(str)) {
      startServer(args);
    } else if ("shutdown".equalsIgnoreCase(str)) {
      stopServer();
    } else if ("-h".equalsIgnoreCase(str)) {
      printUsage();
    }
  }
  
  private void stopServer() throws FileNotFoundException, IOException {
    Properties properties = readProperties();
    
    String str = properties.getProperty(ARG_PORT);
    if (str != null) {
      shutdown(Integer.valueOf(str), SHUTDOWN_PASSWORD);
    } else {
      System.err.println("Can't read port from properties file.");
    }
  }
  
  private void printUsage() {
    System.err.println("Usage:");
    System.err
        .println("Start Server: java -classpath \"lib\\*;startjetty.jar\" org.codeexample.jeffery.misc.EmbeddedSolrJettyServer start -port port -exitAfter number -dynamicPort");
    System.err
        .println("Shutdown Server: java -classpath \"lib\\*;startjetty.jar\" org.codeexample.jeffery.misc.EmbeddedSolrJettyServer shutdown");
  }
  
  /**
   * First read form the config.file, then use command line arguments to
   * overwrite it, if no exits, set default value.
   */
  private Properties getOptions(String[] args) throws IOException {
    Properties properties = new Properties();
    // put default values
    properties.setProperty(ARG_PORT, String.valueOf(DEFAULT_PORT));
    properties.setProperty(ARG_DYNAMIC_PORT, "false");
    
    properties.setProperty(ARG_SEARCH_PORT_START_RANGE,
        String.valueOf(DEFAULT_SEARCH_PORT_START_RANGE));
    properties.setProperty(ARG_SEARCH_PORT_END_RANGE,
        String.valueOf(DEFAULT_SEARCH_PORT_END_RANGE));
    
    properties.putAll(readProperties());
    // remove exitAfter, as this is a dangerous value, it should take effect
    // when uses set is explicitly, via command line, not from config.properties
    properties.remove(ARG_EXIT_AFTER);
    
    // ignore localOnly from config.proerties
    properties.setProperty(ARG_LOCALONLY, "false");
    // code comes from
    // http://journals.ecs.soton.ac.uk/java/tutorial/java/cmdLineArgs/parsing.html
    
    // the first arg is start
    int i = 1;
    String arg;
    while (i < args.length && args[i].startsWith("-")) {
      arg = args[i++];
      if (arg.equalsIgnoreCase("-" + ARG_DYNAMIC_PORT)) {
        if (i < args.length) {
          arg = args[i++];
          Boolean dynamicPort = Boolean.FALSE;
          try {
            dynamicPort = Boolean.valueOf(arg);
          } catch (Exception e) {
            dynamicPort = Boolean.FALSE;
          }
          properties.setProperty(ARG_DYNAMIC_PORT, dynamicPort.toString());
          
        } else {
          exitWithError("No value is specified for " + ARG_DYNAMIC_PORT);
        }
      } else if (arg.equalsIgnoreCase("-" + ARG_LOCALONLY)) {
        if (i < args.length) {
          arg = args[i++];
          Boolean localOnly = Boolean.FALSE;
          try {
            localOnly = Boolean.valueOf(arg);
          } catch (Exception e) {
            localOnly = Boolean.FALSE;
          }
          properties.setProperty(ARG_LOCALONLY, localOnly.toString());
          
        } else {
          exitWithError("No value is specified for " + ARG_LOCALONLY);
        }
      } else if (arg.equalsIgnoreCase("-" + ARG_PORT)) {
        if (i < args.length) {
          arg = args[i++];
          try {
            int port = Integer.parseInt(arg);
            if (port < 0) {
              exitWithError("Paramter " + ARG_PORT + ":" + arg
                  + " is not valid.");
            }
            properties.setProperty(ARG_PORT, arg);
          } catch (Exception e) {
            exitWithError("Paramter " + ARG_PORT + ":" + arg + " is not valid.");
          }
        } else {
          exitWithError("No value is specified for " + ARG_PORT);
        }
      } else if (arg.equalsIgnoreCase("-" + ARG_EXIT_AFTER)) {
        if (i < args.length) {
          arg = args[i++];
          try {
            long seconds = Long.parseLong(arg);
            if (seconds < 0) {
              exitWithError("Paramter " + ARG_EXIT_AFTER + ":" + arg
                  + " is not valid.");
            }
            properties.setProperty(ARG_EXIT_AFTER, arg);
          } catch (Exception e) {
            exitWithError("Paramter " + ARG_EXIT_AFTER + ":" + arg
                + " is not valid.");
          }
        } else {
          exitWithError("No value is specified for " + ARG_EXIT_AFTER);
        }
      } else if (arg.equalsIgnoreCase("-" + ARG_DEBUG)) {
        if (i < args.length) {
          arg = args[i++];
          debug = false;
          try {
            debug = Boolean.parseBoolean(arg);
          } catch (Exception e) {
            debug = false;
          }
          properties.setProperty(ARG_DEBUG, Boolean.toString(debug));
          
        } else {
          exitWithError("No value is specified for " + ARG_DEBUG);
        }
      }
    }
    return properties;
  }
  
  @SuppressWarnings("resource")
  private void startServer(String[] args) throws Exception {
    Properties properties = getOptions(args);
    // check whether the application is already running
    lockFile = new File(appBaseLocation + LOCK_FILE);
    // Check if the lock exist
    if (lockFile.exists()) {
      // if exist try to delete it
      lockFile.delete();
    }
    // Try to get the lock
    channel = new RandomAccessFile(lockFile, "rw").getChannel();
    lock = channel.tryLock();
    if (lock == null) {
      // File is lock by other application
      channel.close();
      properties = readProperties();
      String portStr = properties.getProperty(ARG_PORT);

      if (portStr != null) {
        printSuccess(portStr, "Application is already running");
        return;
      }
    }
    // Add shutdown hook to release lock when application shutdown
    ShutdownHook shutdownHook = new ShutdownHook();
    Runtime.getRuntime().addShutdownHook(shutdownHook);
    
    try {
      Integer port = Integer.parseInt(properties.getProperty(ARG_PORT));
      boolean dynamicPort = Boolean.valueOf(properties
          .getProperty(ARG_DYNAMIC_PORT));
      boolean localOnly = Boolean
          .valueOf(properties.getProperty(ARG_LOCALONLY));
      
      int searchFrom = Integer.parseInt(properties
          .getProperty(ARG_SEARCH_PORT_START_RANGE));
      int searchTo = Integer.parseInt(properties
          .getProperty(ARG_SEARCH_PORT_END_RANGE));
      
      String sorlHome = appBaseLocation + "solr-home";
      if (!new File(sorlHome).exists()) {
        exitWithError("Solr home " + sorlHome
            + " doesn't exist or is not a folder.");
      }      
      // create logs directory
      File logs = new File(appBaseLocation, "logs");
      if (!logs.isDirectory()) {
        logs.mkdir();
      }
      
      Server server = null;
      if (dynamicPort) {
        // try 10 times
        for (int i = 0; i < 10; i++) {
          if (port == null) {
            port = findUnusedPort(searchFrom, searchTo);
          }
          if (port == null) {
            continue;
          }
          try {
            server = doStartEmbeddedJetty(sorlHome, port, localOnly, properties);
            // break if start the server successfully.
            break;
          } catch (Throwable e) {
            port = null;
            continue;
          }
        }
      } else {
        if (port == null) {
          // should not happen
          exitWithError("In no-dynamicPort mode, a valid port must be specified in command line or config.proprties.");
        }
        server = doStartEmbeddedJetty(sorlHome, port, localOnly, properties);
      }
      if (server != null) {
        properties.setProperty(ARG_PORT, port.toString());
        writeProperties(properties);
        printSuccess(port.toString(), "Server is started at port: " + port);
        server.join();
      } else {
        exitWithError("Unable to find available port.");
      }
    } catch (Throwable e) {
      if (debug) {
        e.printStackTrace();
      }
      exitWithError(e.getMessage());
    }
  }
  
  /**
   * From http://download.eclipse.org/jetty/stable-9/apidocs/org/eclipse/jetty/
   * server/handler/ShutdownHandler.html
   * 
   * @param shutdownCookie
   */
  private static void shutdown(int port, String shutdownCookie) {
    try {
      URL url = new URL("http://localhost:" + port + "/shutdown?token="
          + shutdownCookie + "&_exitJvm=true");
      HttpURLConnection connection = (HttpURLConnection) url.openConnection();
      connection.setRequestMethod("POST");
      connection.getResponseCode();
      System.out.println("Success=True");
      System.out.print("Message=Server (" + port + ") is shutdown");
    } catch (SocketException e) {
      System.out.println("Success=True");
      System.out.print("Message=Server is already not running.");
    } catch (Exception e) {
      System.out.println("Success=False");
      System.out.print("Message=" + e.getMessage());
    }
  }
  
  private Server doStartEmbeddedJetty(String sorlHome, int port, boolean localOnly, Properties properties) throws Throwable {
    System.setProperty(SYS_APP_HOME, appBaseLocation);
    System.setProperty(SYS_SOLR_SOLR_HOME, sorlHome);
    System.setProperty("jetty.port", String.valueOf(port));
    String localhost = "localhost";
    if (localOnly) {
      System.setProperty("jetty.host", localhost);
    } else {
      System.setProperty("jetty.host", "0.0.0.0");
    }
    Server server = null;
    Properties oldProperties = readProperties();
    try {
      // write new properties
      writeProperties(properties);      
      server = new Server();
      
      File jettyXmlFile = new File(appBaseLocation + "/etc/", "jetty.xml");
      XmlConfiguration configuration = new XmlConfiguration(jettyXmlFile
          .toURI().toURL());
      configuration.configure(server);
      
      HandlerList handlers = new HandlerList();
      
      // add solr.war
      // WebAppContext webapp1 = new WebAppContext();
      // webapp1.setContextPath("/solr");
      // webapp1.setWar(solrWar);
      
      File webapps = new File(appBaseLocation + "/webapps/");
      if (webapps.isDirectory()) {
        File[] wars = webapps.listFiles(new FilenameFilter() {
          @Override
          public boolean accept(File dir, String name) {
            return name.endsWith(".war");
          }
        });
        for (File war : wars) {
          String context = war.getName();
          context = context.substring(0, context.length() - 4);
          if (context.length() != 0) {
            WebAppContext webappContext = new WebAppContext();
            webappContext.setContextPath("/" + context);
            webappContext.setWar(war.getAbsolutePath());
            handlers.addHandler(webappContext);
          }
        }
      }
      
      // later add handlers from etc/jetty.xml
      handlers.addHandler(new ShutdownHandler(server, SHUTDOWN_PASSWORD));
      // handlers.setHandlers(new Handler[] {
      // // webapp1,
      // new ShutdownHandler(server, SHUTDOWN_PASSWORD)});
      // handler configured in jetty.xml
      Handler oldHandler = server.getHandler();
      if (oldHandler != null) {
        handlers.addHandler(oldHandler);
      }
      
      server.setHandler(handlers);
      SelectChannelConnector connector = new SelectChannelConnector();
      if (localOnly) {
        connector.setHost(localhost);
      } else {
        connector.setHost(null);
      }
      connector.setReuseAddress(false);
      connector.setPort(port);
      server.setConnectors(new Connector[] {connector});
      
      server.start();
    } catch (Throwable e) {
      if (server != null) {
        server.stop();
      }
      // if failed, restore old properties
      writeProperties(oldProperties);
      if (e instanceof InvocationTargetException) {
        Throwable tmp = e.getCause();
        if (tmp != null) {
          e = tmp;
        }
      }
      throw e;
    }
    return server;
  }
  
  private Integer findUnusedPort(int searchFrom, int searchTo) {
    ServerSocket s = null;
    for (int port = searchFrom; port < searchTo; port++) {
      try {
        s = new ServerSocket(port);
        // Not likely to happen, but if so: s.close throws exception, we will
        // continue and choose another port
        s.close();
        return port;
      } catch (Exception e) {
        continue;
      }
    }
    s.getLocalPort();
    return null;
  }
  
  private String getBaseLocation() throws UnsupportedEncodingException {
    File jarPath = new File(EmbeddedSolrJettyServer.class.getProtectionDomain()
        .getCodeSource().getLocation().getPath());
    // startjetty.jar is under /lib directory
    String baseLocation = jarPath.getParentFile().getParent();
    // handle non-ascii character in path, such as Chinese
    baseLocation = URLDecoder.decode(baseLocation,
        System.getProperty("file.encoding"));
    if (!baseLocation.endsWith(File.separator)) {
      baseLocation = baseLocation + File.separator;
    }
    return baseLocation;
  }
  
  private void unlockFile() {
    // release and delete file lock
    try {
      if (lock != null) {
        lock.release();
        channel.close();
        lockFile.delete();
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
  
  /**
   * Write to a file
   * 
   * @param msg
   */
  private void exitWithError(String msg) throws IOException {
    BufferedWriter bw = null;
    try {
      bw = new BufferedWriter(new FileWriter(appBaseLocation + RESULT_FILE));
      bw.write("Success=False");
      System.err.println("Success=False");
      bw.newLine();
      bw.write("Port=Unkown");
      System.err.println("Port=Unkown");
      bw.newLine();
      bw.write("Message=" + msg);
      System.err.println("Message=" + msg);
      bw.flush();
    } finally {
      if (bw != null) {
        bw.close();
      }
    }
    System.exit(-1);
  }
  
  private void printSuccess(String port, String msg) throws IOException {
    BufferedWriter bw = null;
    try {
      bw = new BufferedWriter(new FileWriter(appBaseLocation + RESULT_FILE));
      bw.write("Success=True");
      System.out.println("Success=True");
      bw.newLine();
      bw.write("Port=" + port);
      System.out.println("Port=" + port);
      bw.newLine();
      bw.write("Message=" + msg);
      System.out.println("Message=" + msg);
      bw.flush();
    } finally {
      if (bw != null) {
        bw.close();
      }
    }
  }
  
  private Properties readProperties() throws FileNotFoundException, IOException {
    String propertyFile = appBaseLocation + CONFILE_FILE;
    InputStream is = null;
    
    Properties properties = new Properties();
    try {
      is = new FileInputStream(propertyFile);
      properties.load(is);
      
    } finally {
      if (is != null) {
        is.close();
      }
    }
    return properties;
  }
  
  private class ShutdownHook extends Thread {
    public void run() {
      unlockFile();
    }
  }
  
  /**
   * Only save properties when it starts the application successfully.
   * 
   * @param properties
   * @throws IOException
   */
  private void writeProperties(Properties properties) throws IOException {
    String propertyFile = appBaseLocation + CONFILE_FILE;
    OutputStream os = null;
    try {
      // remove hidden, one-time only parameter.
      properties.remove(ARG_DEBUG);
      os = new FileOutputStream(propertyFile);
      properties.store(os, "");
    } finally {
      if (os != null) {
        os.close();
      }
    }
  }
  
  static class NullPrintStream extends PrintStream {
    public NullPrintStream() {
      super(new OutputStream() {
        public void write(int b) {
          // DO NOTHING
        }
      });
      
    }
    
    @Override
    public void write(int b) {
      // do nothing
    }
    
  }
}

class ConcatOutputStream extends OutputStream {
  private OutputStream stream1, stream2;
  
  public ConcatOutputStream(OutputStream stream1, OutputStream stream2) {
    super();
    this.stream1 = stream1;
    this.stream2 = stream2;
  }
  
  @Override
  public void write(int b) throws IOException {
    stream1.write(b);
    stream2.write(b);
  } 
}

Labels

adsense (5) Algorithm (69) Algorithm Series (35) Android (7) ANT (6) bat (8) Big Data (7) Blogger (14) Bugs (6) Cache (5) Chrome (19) Code Example (29) Code Quality (7) Coding Skills (5) Database (7) Debug (16) Design (5) Dev Tips (63) Eclipse (32) Git (5) Google (33) Guava (7) How to (9) Http Client (8) IDE (7) Interview (88) J2EE (13) J2SE (49) Java (186) JavaScript (27) JSON (7) Learning code (9) Lesson Learned (6) Linux (26) Lucene-Solr (112) Mac (10) Maven (8) Network (9) Nutch2 (18) Performance (9) PowerShell (11) Problem Solving (11) Programmer Skills (6) regex (5) Scala (6) Security (9) Soft Skills (38) Spring (22) System Design (11) Testing (7) Text Mining (14) Tips (17) Tools (24) Troubleshooting (29) UIMA (9) Web Development (19) Windows (21) xml (5)