Add daemon support for adaptors

When used with procrun or jsvc, an adaptor would be able to run as a
Windows service or Unix daemon where it could obtain priviledged ports
before dropping priviledges to a different user.
diff --git a/THIRDPARTYLICENSE.txt b/THIRDPARTYLICENSE.txt
index ec97bc9..34dccfa 100644
--- a/THIRDPARTYLICENSE.txt
+++ b/THIRDPARTYLICENSE.txt
@@ -16,6 +16,11 @@
 
 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
 
+lib/commons-daemon-1.0.15.jar
+Apache2
+
+xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
+
 lib/commons-lang-2.1.jar
 Apache2
 
diff --git a/build.xml b/build.xml
index ddf60c5..7452685 100644
--- a/build.xml
+++ b/build.xml
@@ -40,6 +40,7 @@
       <include name="jsr305.jar"/>
       <include name="gson-2.0.jar"/>
       <include name="velocity-1.5.jar"/>
+      <include name="commons-daemon-1.0.15.jar"/>
     </fileset>
   </path>
 
diff --git a/lib/commons-daemon-1.0.15.jar b/lib/commons-daemon-1.0.15.jar
new file mode 100644
index 0000000..2b6b9c6
--- /dev/null
+++ b/lib/commons-daemon-1.0.15.jar
Binary files differ
diff --git a/src/com/google/enterprise/adaptor/Application.java b/src/com/google/enterprise/adaptor/Application.java
index cb922a8..b980aec 100644
--- a/src/com/google/enterprise/adaptor/Application.java
+++ b/src/com/google/enterprise/adaptor/Application.java
@@ -60,18 +60,46 @@
    */
   public void start() throws IOException, InterruptedException {
     synchronized (this) {
-      if (primaryServer != null) {
-        throw new IllegalStateException("Already started");
-      }
+      daemonInit();
+
+      // The shutdown hook is purposefully not part of the deamon methods,
+      // because it should only be done when running from the command line.
       shutdownHook = new Thread(new ShutdownHook(), "gsacomm-shutdown");
       Runtime.getRuntime().addShutdownHook(shutdownHook);
-
-      primaryServer = createHttpServer();
-      dashboardServer = createDashboardHttpServer();
-      primaryServer.start();
-      dashboardServer.start();
     }
+
+    daemonStart();
+  }
+
+  /**
+   * Reserves resources for later use. This may be run with different
+   * permissions (like as root), to reserve ports or other things that need
+   * elevated privileges.
+   */
+  synchronized void daemonInit() throws IOException {
+    if (primaryServer != null) {
+      throw new IllegalStateException("Already started");
+    }
+    primaryServer = createHttpServer(config);
+    dashboardServer = createDashboardHttpServer(config);
+    // Because once stopped the server can't be reused, we can't reuse its
+    // bind()ed socket if we stop it. So although ideally we would start/stop in
+    // daemonStart/daemonStop, we instead must do it in
+    // daemonInit/daemonDestroy.
+    primaryServer.start();
+    dashboardServer.start();
+  }
+
+  /**
+   * Really start. This must be called after a successful {@link #daemonInit}.
+   */
+  void daemonStart() throws IOException, InterruptedException {
     gsa.start(primaryServer, dashboardServer);
+
+    if (config.isAdaptorPushDocIdsOnStartup()) {
+      log.info("Pushing once at program start");
+      gsa.checkAndScheduleImmediatePushOfDocIds();
+    }
   }
 
   /**
@@ -79,9 +107,8 @@
    * shutdown.
    */
   public synchronized void stop(long time, TimeUnit unit) {
-    if (primaryServer == null) {
-      throw new IllegalStateException("Already stopped");
-    }
+    daemonStop(time, unit);
+    daemonDestroy(time, unit);
     if (shutdownHook != null) {
       try {
         Runtime.getRuntime().removeShutdownHook(shutdownHook);
@@ -90,28 +117,44 @@
       }
       shutdownHook = null;
     }
+  }
 
+  /**
+   * Stop all the services we provide. This is the opposite of {@link
+   * #daemonStart}.
+   */
+  synchronized void daemonStop(long time, TimeUnit unit) {
+    if (primaryServer == null) {
+      throw new IllegalStateException("Already stopped");
+    }
     gsa.stop(time, unit);
-    SleepHandler sleepHandler = new SleepHandler(100 /* millis */);
-    // Workaround Java Bug 7105369.
-    primaryServer.createContext(SLEEP_PATH, sleepHandler);
-    issueSleepGetRequest(config.getServerPort());
+  }
 
-    primaryServer.stop((int) unit.toSeconds(time));
+  /**
+   * Release reserved resources. This is the opposite of {@link
+   * #daemonInit}.
+   */
+  synchronized void daemonDestroy(long time, TimeUnit unit) {
+    httpServerShutdown(primaryServer, time, unit);
     log.finer("Completed primary server stop");
-    ((ExecutorService) primaryServer.getExecutor()).shutdownNow();
     primaryServer = null;
 
-    // Workaround Java Bug 7105369.
-    dashboardServer.createContext(SLEEP_PATH, sleepHandler);
-    issueSleepGetRequest(config.getServerDashboardPort());
-
-    dashboardServer.stop((int) unit.toSeconds(time));
-    ((ExecutorService) dashboardServer.getExecutor()).shutdownNow();
+    httpServerShutdown(dashboardServer, time, unit);
     log.finer("Completed dashboard stop");
     dashboardServer = null;
   }
 
+  private static void httpServerShutdown(HttpServer server, long time,
+      TimeUnit unit) {
+    // Workaround Java Bug 7105369.
+    SleepHandler sleepHandler = new SleepHandler(100 /* millis */);
+    server.createContext(SLEEP_PATH, sleepHandler);
+    issueSleepGetRequest(server.getAddress(), server instanceof HttpsServer);
+
+    server.stop((int) unit.toSeconds(time));
+    ((ExecutorService) server.getExecutor()).shutdownNow();
+  }
+
   /**
    * Issues a GET request to a SleepHandler. This is used to workaround Java
    * Bug 7105369.
@@ -127,11 +170,12 @@
    * before calling stop(). In the event everything goes as planned, the request
    * completes after stop() has been called and allows stop() to exit quickly.
    */
-  private void issueSleepGetRequest(int port) {
+  private static void issueSleepGetRequest(InetSocketAddress address,
+      boolean isHttps) {
     URL url;
     try {
-      url = new URL(config.isServerSecure() ? "https" : "http",
-          config.getServerHostname(), port, SLEEP_PATH);
+      url = new URL(isHttps ? "https" : "http",
+          address.getAddress().getHostAddress(), address.getPort(), SLEEP_PATH);
     } catch (MalformedURLException ex) {
       log.log(Level.WARNING,
           "Unexpected error. Shutting down will be slow.", ex);
@@ -165,7 +209,7 @@
     }).start();
   }
 
-  private HttpServer createHttpServer() throws IOException {
+  private static HttpServer createHttpServer(Config config) throws IOException {
     HttpServer server;
     if (!config.isServerSecure()) {
       server = HttpServer.create();
@@ -206,7 +250,8 @@
     return server;
   }
 
-  private HttpServer createDashboardHttpServer() throws IOException {
+  private static HttpServer createDashboardHttpServer(Config config)
+      throws IOException {
     boolean secure = config.isServerSecure();
     HttpServer server;
     if (!secure) {
@@ -247,6 +292,11 @@
     return gsa;
   }
 
+  /** Returns the {@link Config} used by this instance. */
+  public Config getConfig() {
+    return config;
+  }
+
   /**
    * Main for adaptors to utilize when wanting to act as an application. This
    * method primarily parses arguments and creates an application instance
@@ -255,14 +305,7 @@
    * @return the application instance in use
    */
   public static Application main(Adaptor adaptor, String[] args) {
-    Config config = new Config();
-    adaptor.initConfig(config);
-    config.autoConfig(args);
-
-    if (config.useAdaptorAutoUnzip()) {
-      adaptor = new AutoUnzipAdaptor(adaptor);
-    }
-    Application app = new Application(adaptor, config);
+    Application app = daemonMain(adaptor, args);
 
     // Setup providing content.
     try {
@@ -274,14 +317,24 @@
       throw new RuntimeException("could not start serving", e);
     }
 
-    if (config.isAdaptorPushDocIdsOnStartup()) {
-      log.info("Pushing once at program start");
-      app.getGsaCommunicationHandler().checkAndScheduleImmediatePushOfDocIds();
-    }
-
     return app;
   }
 
+  /**
+   * Performs basic bootstrapping like normal {@link #main}, but does not start
+   * the application.
+   */
+  static Application daemonMain(Adaptor adaptor, String[] args) {
+    Config config = new Config();
+    adaptor.initConfig(config);
+    config.autoConfig(args);
+
+    if (config.useAdaptorAutoUnzip()) {
+      adaptor = new AutoUnzipAdaptor(adaptor);
+    }
+    return new Application(adaptor, config);
+  }
+
   private class ShutdownHook implements Runnable {
     @Override
     public void run() {
diff --git a/src/com/google/enterprise/adaptor/Daemon.java b/src/com/google/enterprise/adaptor/Daemon.java
new file mode 100644
index 0000000..6aa9406
--- /dev/null
+++ b/src/com/google/enterprise/adaptor/Daemon.java
@@ -0,0 +1,107 @@
+// Copyright 2013 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import org.apache.commons.daemon.DaemonContext;
+
+import java.util.Arrays;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Allows running an adaptor as a daemon when used in conjunction with procrun
+ * or jsvc.
+ *
+ * <p>Example execution with jsvc:
+ * <pre>jsvc -pidfile adaptor.pid -cp someadaptor-withlib.jar \
+ *    com.google.enterprise.adaptor.Daemon package.SomeAdaptor</pre>
+ */
+public class Daemon implements org.apache.commons.daemon.Daemon {
+  private Application app;
+  private DaemonContext context;
+
+  @Override
+  public synchronized void init(DaemonContext context) throws Exception {
+    if (this.context != null) {
+      throw new IllegalStateException("Already initialized");
+    }
+    this.context = context;
+    String[] args = context.getArguments();
+    if (args.length < 1) {
+      throw new IllegalArgumentException(
+          "Missing argument: adaptor class name");
+    }
+    Adaptor adaptor
+        = Class.forName(args[0]).asSubclass(Adaptor.class).newInstance();
+    args = Arrays.copyOfRange(args, 1, args.length);
+
+    app = Application.daemonMain(adaptor, args);
+    app.daemonInit();
+  }
+
+  @Override
+  public synchronized void destroy() {
+    if (app != null) {
+      app.daemonDestroy(5, TimeUnit.SECONDS);
+    }
+    context = null;
+    app = null;
+  }
+
+  @Override
+  public void start() throws Exception {
+    final Application savedApp;
+    final DaemonContext savedContext;
+    final CountDownLatch latch = new CountDownLatch(1);
+    // Save values so that there aren't any races with stop/destroy.
+    synchronized (this) {
+      savedApp = this.app;
+      savedContext = this.context;
+    }
+    // Run in a new thread so that stop() can be called before we complete
+    // starting (since starting can take a long time if the Adaptor keeps
+    // throwing an exception). However, we still try to wait for start to
+    // complete normally to ease testing and improve the user experience in the
+    // common case of starting being quick.
+    new Thread(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          savedApp.daemonStart();
+        } catch (InterruptedException ex) {
+          // We must be shutting down.
+          Thread.currentThread().interrupt();
+        } catch (Exception ex) {
+          savedContext.getController().fail(ex);
+        } finally {
+          latch.countDown();
+        }
+      }
+    }).start();
+    latch.await(5, TimeUnit.SECONDS);
+  }
+
+  @Override
+  public synchronized void stop() throws Exception {
+    app.daemonStop(5, TimeUnit.SECONDS);
+  }
+
+  @VisibleForTesting
+  Application getApplication() {
+    return app;
+  }
+}
diff --git a/src/com/google/enterprise/adaptor/GsaCommunicationHandler.java b/src/com/google/enterprise/adaptor/GsaCommunicationHandler.java
index 7ef85c4..09749b8 100644
--- a/src/com/google/enterprise/adaptor/GsaCommunicationHandler.java
+++ b/src/com/google/enterprise/adaptor/GsaCommunicationHandler.java
@@ -539,6 +539,11 @@
     }
   }
 
+  /** The adaptor instance being used. */
+  public Adaptor getAdaptor() {
+    return adaptor;
+  }
+
   HttpContext addFilters(HttpContext context) {
     context.getFilters().add(waiter.filter());
     context.getFilters().addAll(commonFilters);
diff --git a/test/com/google/enterprise/adaptor/DaemonTest.java b/test/com/google/enterprise/adaptor/DaemonTest.java
new file mode 100644
index 0000000..bed6475
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/DaemonTest.java
@@ -0,0 +1,149 @@
+// Copyright 2013 Google Inc. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.enterprise.adaptor;
+
+import static org.junit.Assert.*;
+
+import org.apache.commons.daemon.DaemonContext;
+import org.apache.commons.daemon.DaemonController;
+import org.junit.*;
+import org.junit.rules.ExpectedException;
+
+import java.io.*;
+import java.net.*;
+import java.nio.charset.Charset;
+
+/** Tests for {@link Daemon}. */
+public class DaemonTest {
+  private String[] arguments = new String[] {
+      SingleDocAdaptor.class.getName(), "-Dgsa.hostname=localhost",
+      "-Dserver.port=0", "-Dserver.dashboardPort=0"};
+
+  @Rule
+  public ExpectedException thrown = ExpectedException.none();
+
+  @Test
+  public void testNoArguments() throws Exception {
+    DaemonContext context = new Context(new String[0], null);
+    Daemon daemon = new Daemon();
+    thrown.expect(IllegalArgumentException.class);
+    daemon.init(context);
+  }
+
+  @Test
+  public void testDoubleInit() throws Exception {
+    DaemonContext context = new Context(arguments, null);
+    Daemon daemon = new Daemon();
+    daemon.init(context);
+    try {
+      thrown.expect(IllegalStateException.class);
+      daemon.init(context);
+    } finally {
+      daemon.destroy();
+    }
+  }
+
+  @Test
+  public void testAnyTimeDestroy() {
+    Daemon daemon = new Daemon();
+    // Destroy without init should still work.
+    daemon.destroy();
+  }
+
+  @Test
+  public void testBasicListen() throws Exception {
+    DaemonContext context = new Context(arguments, null);
+    Daemon daemon = new Daemon();
+    SingleDocAdaptor adaptor = null;
+    daemon.init(context);
+    try {
+      daemon.start();
+      try {
+        Adaptor tmpAdaptor = daemon.getApplication()
+            .getGsaCommunicationHandler().getAdaptor();
+        adaptor = (SingleDocAdaptor) tmpAdaptor;
+        assertTrue(adaptor.inited);
+        int port = daemon.getApplication().getConfig().getServerPort();
+        URL url = new URL("http", "localhost", port, "/doc/");
+        URLConnection conn = url.openConnection();
+        InputStream is = conn.getInputStream();
+        try {
+          String out
+              = IOHelper.readInputStreamToString(is, Charset.forName("UTF-8"));
+          assertEquals("success", out);
+        } finally {
+          is.close();
+        }
+      } finally {
+        daemon.stop();
+      }
+    } finally {
+      daemon.destroy();
+    }
+    assertFalse(adaptor.inited);
+  }
+
+  private static class Context implements DaemonContext {
+    private final String[] args;
+    private final DaemonController controller;
+
+    public Context(String[] args, DaemonController controller) {
+      this.args = args;
+      this.controller = controller;
+    }
+
+    @Override
+    public String[] getArguments() {
+      return args;
+    }
+
+    @Override
+    public DaemonController getController() {
+      return controller;
+    }
+  }
+
+  /**
+   * Adaptor with a single document. Marked public so that it can be loaded by
+   * Daemon.
+   */
+  public static class SingleDocAdaptor extends AbstractAdaptor {
+    private volatile boolean inited;
+
+    @Override
+    public void init(AdaptorContext context) {
+      inited = true;
+    }
+
+    @Override
+    public void destroy() {
+      inited = false;
+    }
+
+    @Override
+    public void getDocIds(DocIdPusher pusher) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void getDocContent(Request req, Response resp) throws IOException {
+      if ("".equals(req.getDocId().getUniqueId())) {
+        resp.getOutputStream().write("success".getBytes("UTF-8"));
+      } else {
+        resp.respondNotFound();
+      }
+    }
+  }
+}