diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java
index 600464f87302e0ef7acc1ba38a8276b3017c43c4..5592b5b878334b52b7bb0ffcec18db340573363f 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java
@@ -12,6 +12,8 @@ import java.util.Properties;
 import java.util.Set;
 
 import net.i2p.I2PAppContext;
+import net.i2p.app.*;
+import static net.i2p.app.ClientAppState.*;
 import net.i2p.client.I2PSession;
 import net.i2p.client.I2PSessionException;
 import net.i2p.data.DataHelper;
@@ -23,16 +25,21 @@ import net.i2p.util.OrderedProperties;
  * Coordinate a set of tunnels within the JVM, loading and storing their config
  * to disk, and building new ones as requested.
  *
- * Warning - this is a singleton. Todo: fix
+ * This is the entry point from clients.config.
  */
-public class TunnelControllerGroup {
-    private Log _log;
-    private static TunnelControllerGroup _instance;
+public class TunnelControllerGroup implements ClientApp {
+    private final Log _log;
+    private volatile ClientAppState _state;
+    private final I2PAppContext _context;
+    private final ClientAppManager _mgr;
+    private static volatile TunnelControllerGroup _instance;
     static final String DEFAULT_CONFIG_FILE = "i2ptunnel.config";
     
     private final List<TunnelController> _controllers;
-    private String _configFile = DEFAULT_CONFIG_FILE;
+    private final String _configFile;
     
+    private static final String REGISTERED_NAME = "i2ptunnel";
+
     /** 
      * Map of I2PSession to a Set of TunnelController objects 
      * using the session (to prevent closing the session until
@@ -41,48 +48,143 @@ public class TunnelControllerGroup {
      */
     private final Map<I2PSession, Set<TunnelController>> _sessions;
     
+    /**
+     *  In I2PAppContext will instantiate if necessary and always return non-null.
+     *  As of 0.9.4, when in RouterContext, will return null
+     *  if the TCG has not yet been started by the router.
+     *
+     *  @throws IllegalArgumentException if unable to load from i2ptunnel.config
+     */
     public static TunnelControllerGroup getInstance() { 
         synchronized (TunnelControllerGroup.class) {
-            if (_instance == null)
-                _instance = new TunnelControllerGroup(DEFAULT_CONFIG_FILE);
+            if (_instance == null) {
+                I2PAppContext ctx = I2PAppContext.getGlobalContext();
+                if (!ctx.isRouterContext()) {
+                    _instance = new TunnelControllerGroup(ctx, null, null);
+                    _instance.startup();
+                } // else wait for the router to start it
+            }
             return _instance; 
         }
     }
 
-    private TunnelControllerGroup(String configFile) {
-        _log = I2PAppContext.getGlobalContext().logManager().getLog(TunnelControllerGroup.class);
-        _controllers = Collections.synchronizedList(new ArrayList());
-        _configFile = configFile;
+    /**
+     *  Instantiation only. Caller must call startup().
+     *  Config file problems will not throw exception until startup().
+     *
+     *  @param mgr may be null
+     *  @param args one arg, the config file, if not absolute will be relative to the context's config dir,
+     *              if empty or null, the default is i2ptunnel.config
+     *  @since 0.9.4
+     */
+    public TunnelControllerGroup(I2PAppContext context, ClientAppManager mgr, String[] args) {
+        _state = UNINITIALIZED;
+        _context = context;
+        _mgr = mgr;
+        _log = _context.logManager().getLog(TunnelControllerGroup.class);
+        _controllers = new ArrayList();
+        if (args == null || args.length <= 0)
+            _configFile = DEFAULT_CONFIG_FILE;
+        else if (args.length == 1)
+            _configFile = args[0];
+        else
+            throw new IllegalArgumentException("Usage: TunnelControllerGroup [filename]");
         _sessions = new HashMap(4);
-        loadControllers(_configFile);
-        I2PAppContext.getGlobalContext().addShutdownTask(new Shutdown());
+        synchronized (TunnelControllerGroup.class) {
+            if (_instance == null)
+                _instance = this;
+        }
+        if (_instance != this) {
+            _log.logAlways(Log.WARN, "New TunnelControllerGroup, now you have two");
+            if (_log.shouldLog(Log.WARN))
+                _log.warn("I did it", new Exception());
+        }
+        _state = INITIALIZED;
     }
 
+    /**
+     *  @param args one arg, the config file, if not absolute will be relative to the context's config dir,
+     *              if no args, the default is i2ptunnel.config
+     *  @throws IllegalArgumentException if unable to load from config from file
+     */
     public static void main(String args[]) {
         synchronized (TunnelControllerGroup.class) {
             if (_instance != null) return; // already loaded through the web
-            
-            if ( (args == null) || (args.length <= 0) ) {
-                _instance = new TunnelControllerGroup(DEFAULT_CONFIG_FILE);
-            } else if (args.length == 1) {
-                _instance = new TunnelControllerGroup(args[0]);
-            } else {
-                System.err.println("Usage: TunnelControllerGroup [filename]");
-                return;
-            }
+            _instance = new TunnelControllerGroup(I2PAppContext.getGlobalContext(), null, args);
+            _instance.startup();
         }
     }
 
+    /**
+     *  ClientApp interface
+     *  @throws IllegalArgumentException if unable to load config from file
+     *  @since 0.9.4
+     */
+    public void startup() {
+        loadControllers(_configFile);
+        if (_mgr != null)
+            _mgr.register(this);
+        _context.addShutdownTask(new Shutdown());
+    }
+
+    /**
+     *  ClientApp interface
+     *  @since 0.9.4
+     */
+    public ClientAppState getState() {
+        return _state;
+    }
+
+    /**
+     *  ClientApp interface
+     *  @since 0.9.4
+     */
+    public String getName() {
+        return REGISTERED_NAME;
+    }
+
+    /**
+     *  ClientApp interface
+     *  @since 0.9.4
+     */
+    public String getDisplayName() {
+        return REGISTERED_NAME;
+    }
+
+    /**
+     *  @since 0.9.4
+     */
+    private void changeState(ClientAppState state) {
+        changeState(state, null);
+    }
+
+    /**
+     *  @since 0.9.4
+     */
+    private synchronized void changeState(ClientAppState state, Exception e) {
+        _state = state;
+        if (_mgr != null)
+            _mgr.notify(this, state, null, e);
+    }
+
     /**
      *  Warning - destroys the singleton!
      *  @since 0.8.8
      */
-    private static class Shutdown implements Runnable {
+    private class Shutdown implements Runnable {
         public void run() {
             shutdown();
         }
     }
 
+    /**
+     *  ClientApp interface
+     *  @since 0.9.4
+     */
+    public void shutdown(String[] args) {
+        shutdown();
+    }
+
     /**
      *  Warning - destroys the singleton!
      *  Caller must root a new context before calling instance() or main() again.
@@ -91,28 +193,31 @@ public class TunnelControllerGroup {
      *
      *  @since 0.8.8
      */
-    public static void shutdown() {
+    public void shutdown() {
+        changeState(STOPPING);
+        if (_mgr != null)
+            _mgr.unregister(this);
+        unloadControllers();
         synchronized (TunnelControllerGroup.class) {
-            if (_instance == null) return;
-            _instance.unloadControllers();
-            _instance._log = null;
-            _instance = null;
+            if (_instance == this)
+                _instance = null;
         }
+/// fixme static
         I2PTunnelClientBase.killClientExecutor();
+        changeState(STOPPED);
     }
     
     /**
      * Load up all of the tunnels configured in the given file (but do not start
      * them)
      *
+     * DEPRECATED for use outside this class. Use startup() or getInstance().
+     *
+     * @throws IllegalArgumentException if unable to load from file
      */
-    public void loadControllers(String configFile) {
+    public synchronized void loadControllers(String configFile) {
+        changeState(STARTING);
         Properties cfg = loadConfig(configFile);
-        if (cfg == null) {
-            if (_log.shouldLog(Log.WARN))
-                _log.warn("Unable to load the config from " + configFile);
-            return;
-        }
         int i = 0; 
         while (true) {
             String type = cfg.getProperty("tunnel." + i + ".type");
@@ -127,20 +232,28 @@ public class TunnelControllerGroup {
         
         if (_log.shouldLog(Log.INFO))
             _log.info(i + " controllers loaded from " + configFile);
+        changeState(RUNNING);
     }
     
     private class StartControllers implements Runnable {
         public void run() {
-            for (int i = 0; i < _controllers.size(); i++) {
-                TunnelController controller = _controllers.get(i);
-                if (controller.getStartOnLoad())
-                    controller.startTunnel();
+            synchronized(TunnelControllerGroup.this) {
+                for (int i = 0; i < _controllers.size(); i++) {
+                    TunnelController controller = _controllers.get(i);
+                    if (controller.getStartOnLoad())
+                        controller.startTunnel();
+                }
             }
         }
     }
     
-    
-    public void reloadControllers() {
+    /**
+     * Stop all tunnels, reload config, and restart those configured to do so.
+     * WARNING - Does NOT simply reload the configuration!!! This is probably not what you want.
+     *
+     * @throws IllegalArgumentException if unable to reload config file
+     */
+    public synchronized void reloadControllers() {
         unloadControllers();
         loadControllers(_configFile);
     }
@@ -150,7 +263,7 @@ public class TunnelControllerGroup {
      * file or do other silly things)
      *
      */
-    public void unloadControllers() {
+    public synchronized void unloadControllers() {
         stopAllControllers();
         _controllers.clear();
         if (_log.shouldLog(Log.INFO))
@@ -162,14 +275,14 @@ public class TunnelControllerGroup {
      * a config file or start it or anything)
      *
      */
-    public void addController(TunnelController controller) { _controllers.add(controller); }
+    public synchronized void addController(TunnelController controller) { _controllers.add(controller); }
     
     /**
      * Stop and remove the given tunnel
      *
      * @return list of messages from the controller as it is stopped
      */
-    public List<String> removeController(TunnelController controller) {
+    public synchronized List<String> removeController(TunnelController controller) {
         if (controller == null) return new ArrayList();
         controller.stopTunnel();
         List<String> msgs = controller.clearMessages();
@@ -183,7 +296,7 @@ public class TunnelControllerGroup {
      *
      * @return list of messages the tunnels generate when stopped
      */
-    public List<String> stopAllControllers() {
+    public synchronized List<String> stopAllControllers() {
         List<String> msgs = new ArrayList();
         for (int i = 0; i < _controllers.size(); i++) {
             TunnelController controller = _controllers.get(i);
@@ -200,7 +313,7 @@ public class TunnelControllerGroup {
      *
      * @return list of messages the tunnels generate when started
      */
-    public List<String> startAllControllers() {
+    public synchronized List<String> startAllControllers() {
         List<String> msgs = new ArrayList();
         for (int i = 0; i < _controllers.size(); i++) {
             TunnelController controller = _controllers.get(i);
@@ -218,7 +331,7 @@ public class TunnelControllerGroup {
      *
      * @return list of messages the tunnels generate when restarted
      */
-    public List<String> restartAllControllers() {
+    public synchronized List<String> restartAllControllers() {
         List<String> msgs = new ArrayList();
         for (int i = 0; i < _controllers.size(); i++) {
             TunnelController controller = _controllers.get(i);
@@ -235,7 +348,7 @@ public class TunnelControllerGroup {
      *
      * @return list of messages the tunnels have generated
      */
-    public List<String> clearAllMessages() {
+    public synchronized List<String> clearAllMessages() {
         List<String> msgs = new ArrayList();
         for (int i = 0; i < _controllers.size(); i++) {
             TunnelController controller = _controllers.get(i);
@@ -257,8 +370,7 @@ public class TunnelControllerGroup {
      * Save the configuration of all known tunnels to the given file
      *
      */
-    public void saveConfig(String configFile) throws IOException {
-        _configFile = configFile;
+    public synchronized void saveConfig(String configFile) throws IOException {
         File cfgFile = new File(configFile);
         if (!cfgFile.isAbsolute())
             cfgFile = new File(I2PAppContext.getGlobalContext().getConfigDir(), configFile);
@@ -279,16 +391,17 @@ public class TunnelControllerGroup {
     /**
      * Load up the config data from the file
      *
-     * @return properties loaded or null if there was an error
+     * @return properties loaded
+     * @throws IllegalArgumentException if unable to load from file
      */
-    private Properties loadConfig(String configFile) {
+    private synchronized Properties loadConfig(String configFile) {
         File cfgFile = new File(configFile);
         if (!cfgFile.isAbsolute())
             cfgFile = new File(I2PAppContext.getGlobalContext().getConfigDir(), configFile);
         if (!cfgFile.exists()) {
             if (_log.shouldLog(Log.ERROR))
                 _log.error("Unable to load the controllers from " + cfgFile.getAbsolutePath());
-            return null;
+            throw new IllegalArgumentException("Unable to load the controllers from " + cfgFile.getAbsolutePath());
         }
         
         Properties props = new Properties();
@@ -298,7 +411,7 @@ public class TunnelControllerGroup {
         } catch (IOException ioe) {
             if (_log.shouldLog(Log.ERROR))
                 _log.error("Error reading the controllers from " + cfgFile.getAbsolutePath(), ioe);
-            return null;
+            throw new IllegalArgumentException("Error reading the controllers from " + cfgFile.getAbsolutePath(), ioe);
         }
     }
     
@@ -307,7 +420,9 @@ public class TunnelControllerGroup {
      *
      * @return list of TunnelController objects
      */
-    public List<TunnelController> getControllers() { return _controllers; }
+    public synchronized List<TunnelController> getControllers() {
+        return new ArrayList(_controllers);
+    }
     
     
     /** 
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
index 33e4f3fffde088f1a0e2b418fad7729d20c239f9..1413360703a71e49666a7a336f7982f96a7d3314 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/EditBean.java
@@ -29,8 +29,8 @@ import net.i2p.util.Addresses;
 /**
  * Ugly little accessor for the edit page
  *
- * Warning - This class is not part of the i2ptunnel API, and at some point
- * it will be moved from the jar to the war.
+ * Warning - This class is not part of the i2ptunnel API,
+ * it has been moved from the jar to the war.
  * Usage by classes outside of i2ptunnel.war is deprecated.
  */
 public class EditBean extends IndexBean {
@@ -38,6 +38,8 @@ public class EditBean extends IndexBean {
     
     public static boolean staticIsClient(int tunnel) {
         TunnelControllerGroup group = TunnelControllerGroup.getInstance();
+        if (group == null)
+            return false;
         List controllers = group.getControllers();
         if (controllers.size() > tunnel) {
             TunnelController cur = (TunnelController)controllers.get(tunnel);
@@ -55,6 +57,7 @@ public class EditBean extends IndexBean {
         else
             return "127.0.0.1";
     }
+
     public String getTargetPort(int tunnel) {
         TunnelController tun = getController(tunnel);
         if (tun != null && tun.getTargetPort() != null)
@@ -62,6 +65,7 @@ public class EditBean extends IndexBean {
         else
             return "";
     }
+
     public String getSpoofedHost(int tunnel) {
         TunnelController tun = getController(tunnel);
         if (tun != null && tun.getSpoofedHost() != null)
@@ -69,12 +73,13 @@ public class EditBean extends IndexBean {
         else
             return "";
     }
+
     public String getPrivateKeyFile(int tunnel) {
         TunnelController tun = getController(tunnel);
         if (tun != null && tun.getPrivKeyFile() != null)
             return tun.getPrivKeyFile();
         if (tunnel < 0)
-            tunnel = _group.getControllers().size();
+            tunnel = _group == null ? 1 : _group.getControllers().size() + 1;
         return "i2ptunnel" + tunnel + "-privKeys.dat";
     }
     
diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
index e850d273b179f3ff53a70868bd493f8cf9ea956f..838377624e680a5c0f72641b798f1b655eddae56 100644
--- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
+++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java
@@ -42,14 +42,15 @@ import net.i2p.util.PasswordManager;
 /**
  * Simple accessor for exposing tunnel info, but also an ugly form handler
  *
- * Warning - This class is not part of the i2ptunnel API, and at some point
- * it will be moved from the jar to the war.
+ * Warning - This class is not part of the i2ptunnel API,
+ * it has been moved from the jar to the war.
  * Usage by classes outside of i2ptunnel.war is deprecated.
  */
 public class IndexBean {
     protected final I2PAppContext _context;
     protected final Log _log;
     protected final TunnelControllerGroup _group;
+    private final String _fatalError;
     private String _action;
     private int _tunnel;
     //private long _prevNonce;
@@ -110,7 +111,18 @@ public class IndexBean {
     public IndexBean() {
         _context = I2PAppContext.getGlobalContext();
         _log = _context.logManager().getLog(IndexBean.class);
-        _group = TunnelControllerGroup.getInstance();
+        TunnelControllerGroup tcg;
+        String error;
+        try {
+            tcg = TunnelControllerGroup.getInstance();
+            error = tcg == null ? _("Tunnels are not initialized yet, please reload in two minutes.")
+                                : null;
+        } catch (IllegalArgumentException iae) {
+            tcg = null;
+            error = iae.toString();
+        }
+        _group = tcg;
+        _fatalError = error;
         _tunnel = -1;
         _curNonce = "-1";
         addNonce();
@@ -118,6 +130,13 @@ public class IndexBean {
         _otherOptions = new ConcurrentHashMap(4);
     }
     
+    /**
+     *  @since 0.9.4
+     */
+    public boolean isInitialized() {
+        return _group != null;
+    }
+
     public static String getNextNonce() {
         synchronized (_nonces) {
             return _nonces.get(0);
@@ -164,6 +183,8 @@ public class IndexBean {
     private String processAction() {
         if ( (_action == null) || (_action.trim().length() <= 0) || ("Cancel".equals(_action)))
             return "";
+        if (_group == null)
+            return "Error - tunnels are not initialized yet";
         // If passwords are turned on, all is assumed good
         if (!_context.getBooleanProperty(PROP_PW_ENABLE) &&
             !haveNonce(_curNonce))
@@ -197,27 +218,27 @@ public class IndexBean {
         else
             return "Action " + _action + " unknown";
     }
+
     private String stopAll() {
-        if (_group == null) return "";
         List<String> msgs = _group.stopAllControllers();
         return getMessages(msgs);
     }
+
     private String startAll() {
-        if (_group == null) return "";
         List<String> msgs = _group.startAllControllers();
         return getMessages(msgs);
     }
+
     private String restartAll() {
-        if (_group == null) return "";
         List<String> msgs = _group.restartAllControllers();
         return getMessages(msgs);
     }
+
     private String reloadConfig() {
-        if (_group == null) return "";
-        
         _group.reloadControllers();
         return _("Configuration reloaded for all tunnels");
     }
+
     private String start() {
         if (_tunnel < 0) return "Invalid tunnel";
         
@@ -372,7 +393,7 @@ public class IndexBean {
      */
     public String getMessages() {
         if (_group == null)
-            return "";
+            return _fatalError;
         
         StringBuilder buf = new StringBuilder(512);
         if (_action != null) {
diff --git a/apps/i2ptunnel/jsp/editClient.jsp b/apps/i2ptunnel/jsp/editClient.jsp
index bd147880be9117b996ca625bca54e60ac0272967..92d2c1fd9229d7eec9457bf0d5f874551439c359 100644
--- a/apps/i2ptunnel/jsp/editClient.jsp
+++ b/apps/i2ptunnel/jsp/editClient.jsp
@@ -31,7 +31,11 @@
 <body id="tunnelEditPage">
     <div id="pageHeader">
     </div>
+<%
 
+  if (editBean.isInitialized()) {
+
+%>
     <form method="post" action="list">
 
         <div id="tunnelEditPanel" class="panel">
@@ -508,5 +512,12 @@
     </form>
     <div id="pageFooter">
         </div>
+<%
+
+  } else {
+     %>Tunnels are not initialized yet, please reload in two minutes.<%
+  }  // isInitialized()
+
+%>
     </body>
 </html>
diff --git a/apps/i2ptunnel/jsp/editServer.jsp b/apps/i2ptunnel/jsp/editServer.jsp
index 1931182cce3d84df048895f9ff10c24e53be2108..9447fc36873025f04ffe74a285927afd3a9e3266 100644
--- a/apps/i2ptunnel/jsp/editServer.jsp
+++ b/apps/i2ptunnel/jsp/editServer.jsp
@@ -31,7 +31,11 @@
 <body id="tunnelEditPage">
     <div id="pageHeader">
     </div>
+<%
 
+  if (editBean.isInitialized()) {
+
+%>
     <form method="post" action="list">
 
         <div id="tunnelEditPanel" class="panel">
@@ -518,5 +522,12 @@
     </form>
     <div id="pageFooter">
     </div>
+<%
+
+  } else {
+     %>Tunnels are not initialized yet, please reload in two minutes.<%
+  }  // isInitialized()
+
+%>
 </body>
 </html>
diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp
index 6494430e4ea742d637ca89226677d3b2902edbc3..00dbc8c92fb06ad9c0f97d35fc3b3dde5dd9d751 100644
--- a/apps/i2ptunnel/jsp/index.jsp
+++ b/apps/i2ptunnel/jsp/index.jsp
@@ -55,12 +55,23 @@
             </div>
         </div>    
     </div>
+<%
+
+  if (indexBean.isInitialized()) {
 
+%>
     <div id="globalOperationsPanel" class="panel">
         <div class="header"></div>
         <div class="footer">
             <div class="toolbox">
-                <a class="control" href="wizard"><%=intl._("Tunnel Wizard")%></a> <a class="control" href="list?nonce=<%=indexBean.getNextNonce()%>&amp;action=Stop%20all"><%=intl._("Stop All")%></a> <a class="control" href="list?nonce=<%=indexBean.getNextNonce()%>&amp;action=Start%20all"><%=intl._("Start All")%></a> <a class="control" href="list?nonce=<%=indexBean.getNextNonce()%>&amp;action=Restart%20all"><%=intl._("Restart All")%></a> <a class="control" href="list?nonce=<%=indexBean.getNextNonce()%>&amp;action=Reload%20configuration"><%=intl._("Reload Config")%></a>
+                <a class="control" href="wizard"><%=intl._("Tunnel Wizard")%></a>
+                <a class="control" href="list?nonce=<%=indexBean.getNextNonce()%>&amp;action=Stop%20all"><%=intl._("Stop All")%></a>
+                <a class="control" href="list?nonce=<%=indexBean.getNextNonce()%>&amp;action=Start%20all"><%=intl._("Start All")%></a>
+                <a class="control" href="list?nonce=<%=indexBean.getNextNonce()%>&amp;action=Restart%20all"><%=intl._("Restart All")%></a>
+<%--
+                //this is really bad because it stops and restarts all tunnels, which is probably not what you want
+                <a class="control" href="list?nonce=<%=indexBean.getNextNonce()%>&amp;action=Reload%20configuration"><%=intl._("Reload Config")%></a>
+--%>
             </div>
         </div> 
     </div>
@@ -327,6 +338,11 @@
             </form>
         </div>
     </div>
+<%
+
+  }  // isInitialized()
+
+%>
     <div id="pageFooter">
     </div>
 </body>