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()%>&action=Stop%20all"><%=intl._("Stop All")%></a> <a class="control" href="list?nonce=<%=indexBean.getNextNonce()%>&action=Start%20all"><%=intl._("Start All")%></a> <a class="control" href="list?nonce=<%=indexBean.getNextNonce()%>&action=Restart%20all"><%=intl._("Restart All")%></a> <a class="control" href="list?nonce=<%=indexBean.getNextNonce()%>&action=Reload%20configuration"><%=intl._("Reload Config")%></a> + <a class="control" href="wizard"><%=intl._("Tunnel Wizard")%></a> + <a class="control" href="list?nonce=<%=indexBean.getNextNonce()%>&action=Stop%20all"><%=intl._("Stop All")%></a> + <a class="control" href="list?nonce=<%=indexBean.getNextNonce()%>&action=Start%20all"><%=intl._("Start All")%></a> + <a class="control" href="list?nonce=<%=indexBean.getNextNonce()%>&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()%>&action=Reload%20configuration"><%=intl._("Reload Config")%></a> +--%> </div> </div> </div> @@ -327,6 +338,11 @@ </form> </div> </div> +<% + + } // isInitialized() + +%> <div id="pageFooter"> </div> </body>