diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java index 761694fb1..a95850850 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHandler.java @@ -11,9 +11,12 @@ import java.util.Map; import java.util.Properties; import java.util.Set; +import net.i2p.app.ClientApp; +import net.i2p.app.ClientAppState; import net.i2p.router.client.ClientManagerFacadeImpl; import net.i2p.router.startup.ClientAppConfig; import net.i2p.router.startup.LoadClientAppsJob; +import net.i2p.router.startup.RouterAppManager; import net.i2p.router.update.ConsoleUpdateManager; import static net.i2p.update.UpdateType.*; @@ -214,10 +217,12 @@ public class ConfigClientsHandler extends FormHandler { ClientAppConfig.writeClientAppConfig(_context, clients); addFormNotice(_("Client configuration saved successfully")); - addFormNotice(_("Restart required to take effect")); + //addFormNotice(_("Restart required to take effect")); } - // STUB for stopClient, not completed yet. + /** + * @since Implemented in 0.9.6 using ClientAppManager + */ private void stopClient(int i) { List clients = ClientAppConfig.getClientApps(_context); if (i >= clients.size()) { @@ -225,10 +230,23 @@ public class ConfigClientsHandler extends FormHandler { return; } ClientAppConfig ca = clients.get(i); - // - // What do we do here? - // - addFormNotice(_("Client") + ' ' + _(ca.clientName) + ' ' + _("stopped") + '.'); + ClientApp clientApp = _context.clientAppManager().getClientApp(ca.className, LoadClientAppsJob.parseArgs(ca.args)); + if (clientApp != null && clientApp.getState() == ClientAppState.RUNNING) { + try { + // todo parseArgs(ca.stopArgs) ? + clientApp.shutdown(null); + addFormNotice(_("Client {0} stopped", ca.clientName)); + // Give a chance for status to update + try { + Thread.sleep(1000); + } catch (InterruptedException ie) {} + } catch (Throwable t) { + addFormError("Cannot stop client " + ca.className + ": " + t); + _log.error("Error stopping client " + ca.className, t); + } + } else { + addFormError("Cannot stop client " + i + ": " + ca.className); + } } private void startClient(int i) { @@ -239,7 +257,11 @@ public class ConfigClientsHandler extends FormHandler { } ClientAppConfig ca = clients.get(i); LoadClientAppsJob.runClient(ca.className, ca.clientName, LoadClientAppsJob.parseArgs(ca.args), _context, _log); - addFormNotice(_("Client") + ' ' + _(ca.clientName) + ' ' + _("started") + '.'); + addFormNotice(_("Client {0} started", ca.clientName)); + // Give a chance for status to update + try { + Thread.sleep(1000); + } catch (InterruptedException ie) {} } private void deleteClient(int i) { @@ -250,7 +272,7 @@ public class ConfigClientsHandler extends FormHandler { } ClientAppConfig ca = clients.remove(i); ClientAppConfig.writeClientAppConfig(_context, clients); - addFormNotice(_("Client") + ' ' + _(ca.clientName) + ' ' + _("deleted") + '.'); + addFormNotice(_("Client {0} deleted", ca.clientName)); } private void saveWebAppChanges() { diff --git a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java index f3f1dd456..8e366b716 100644 --- a/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java +++ b/apps/routerconsole/java/src/net/i2p/router/web/ConfigClientsHelper.java @@ -9,9 +9,13 @@ import java.util.Properties; import java.util.Set; import java.util.TreeSet; +import net.i2p.app.ClientApp; +import net.i2p.app.ClientAppState; import net.i2p.data.DataHelper; import net.i2p.router.client.ClientManagerFacadeImpl; import net.i2p.router.startup.ClientAppConfig; +import net.i2p.router.startup.LoadClientAppsJob; +import net.i2p.router.startup.RouterAppManager; import net.i2p.util.Addresses; public class ConfigClientsHelper extends HelperBase { @@ -94,17 +98,33 @@ public class ConfigClientsHelper extends HelperBase { List clients = ClientAppConfig.getClientApps(_context); for (int cur = 0; cur < clients.size(); cur++) { ClientAppConfig ca = clients.get(cur); - renderForm(buf, ""+cur, ca.clientName, false, !ca.disabled, + boolean isConsole = ca.className.equals("net.i2p.router.web.RouterConsoleRunner"); + boolean showStart; + boolean showStop; + if (isConsole) { + showStart = false; + showStop = false; + } else { + ClientApp clientApp = _context.clientAppManager().getClientApp(ca.className, LoadClientAppsJob.parseArgs(ca.args)); + showStart = clientApp == null; + showStop = clientApp != null && clientApp.getState() == ClientAppState.RUNNING; + } + renderForm(buf, ""+cur, ca.clientName, + // urlify, enabled + false, !ca.disabled, + // read only // dangerous, but allow editing the console args too //"webConsole".equals(ca.clientName) || "Web console".equals(ca.clientName), false, + // description, edit ca.className + ((ca.args != null) ? " " + ca.args : ""), (""+cur).equals(_edit), - true, false, - // Enable this one and comment out the false below once the stub is filled in. - //!ca.disabled && !("webConsole".equals(ca.clientName) || "Web console".equals(ca.clientName)), - false, - - true, ca.disabled); + // show edit button, show update button + // Don't allow edit if it's running, or else we would lose the "handle" to the ClientApp to stop it. + !showStop, false, + // show stop button + showStop, + // show delete button, show start button + true, showStart); } if ("new".equals(_edit)) @@ -258,10 +278,10 @@ public class ConfigClientsHelper extends HelperBase { if (showStartButton && (!ro) && !edit) { buf.append(""); } - if (showEditButton && (!edit) && !ro) - buf.append(""); if (showStopButton && (!edit)) buf.append(""); + if (showEditButton && (!edit) && !ro) + buf.append(""); if (showUpdateButton && (!edit) && !ro) { buf.append(""); buf.append(""); diff --git a/core/java/src/net/i2p/app/ClientApp.java b/core/java/src/net/i2p/app/ClientApp.java index 8900630bc..545659917 100644 --- a/core/java/src/net/i2p/app/ClientApp.java +++ b/core/java/src/net/i2p/app/ClientApp.java @@ -36,23 +36,28 @@ public interface ClientApp { * If previously running, client must call ClientAppManager.notify() at least once within this * method to change the state to STOPPING or STOPPED. * May be called multiple times on the same object, in any state. + * + * @param args generally null but could be stopArgs from clients.config */ public void shutdown(String[] args) throws Throwable; /** * The current state of the ClientApp. + * @return non-null */ public ClientAppState getState(); /** * The generic name of the ClientApp, used for registration, * e.g. "console". Do not translate. + * @return non-null */ public String getName(); /** * The dislplay name of the ClientApp, used in user interfaces. * The app must translate. + * @return non-null */ public String getDisplayName(); } diff --git a/history.txt b/history.txt index 7996dec3d..8b4cc571e 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,17 @@ +2013-04-16 zzz + * ClientAppManager: Add method to look up clients by class and args + * Console: Implement stopping of clients using the ClientApp interface + (ticket #347) + +2013-04-15 zzz + * Console: Move from deprecated Jetty SSL methods to SslContextFactory + * i2psnark: + - Add data directory configuration to GUI (ticket #768) + - Add page size configuration to GUI + - Multiple instance DHT file cleanup + - Mime type fixes + - Remove web classes from jar + 2013-04-14 zzz * i2psnark: - Set unique tunnel nickname for additional instances diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index f6e6df3a5..4e28a2c8d 100644 --- a/router/java/src/net/i2p/router/RouterVersion.java +++ b/router/java/src/net/i2p/router/RouterVersion.java @@ -18,7 +18,7 @@ public class RouterVersion { /** deprecated */ public final static String ID = "Monotone"; public final static String VERSION = CoreVersion.VERSION; - public final static long BUILD = 4; + public final static long BUILD = 5; /** for example "-test" */ public final static String EXTRA = ""; diff --git a/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java b/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java index 9f0ce8c82..92e47a5f9 100644 --- a/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java +++ b/router/java/src/net/i2p/router/startup/LoadClientAppsJob.java @@ -87,8 +87,18 @@ public class LoadClientAppsJob extends JobImpl { } } + /** + * Parse arg string into an array of args. + * Spaces or tabs separate args. + * Args may be single- or double-quoted if they contain spaces or tabs. + * There is no provision for escaping quotes. + * A quoted string may not contain a quote of any kind. + * + * @param args may be null + * @return non-null, 0-length if args is null + */ public static String[] parseArgs(String args) { - List argList = new ArrayList(4); + List argList = new ArrayList(4); if (args != null) { char data[] = args.toCharArray(); StringBuilder buf = new StringBuilder(32); @@ -130,8 +140,9 @@ public class LoadClientAppsJob extends JobImpl { } } String rv[] = new String[argList.size()]; - for (int i = 0; i < argList.size(); i++) - rv[i] = (String)argList.get(i); + for (int i = 0; i < argList.size(); i++) { + rv[i] = argList.get(i); + } return rv; } @@ -151,6 +162,7 @@ public class LoadClientAppsJob extends JobImpl { /** * Run client in this thread. + * Used for plugin sub-clients only. Does not register with the ClientAppManager. * * @param clientName can be null * @param args can be null @@ -163,6 +175,7 @@ public class LoadClientAppsJob extends JobImpl { /** * Run client in this thread. + * Used for plugin sub-clients only. Does not register with the ClientAppManager. * * @param clientName can be null * @param args can be null @@ -249,13 +262,13 @@ public class LoadClientAppsJob extends JobImpl { RouterAppManager mgr = _ctx.clientAppManager(); Object[] conArgs = new Object[] {_ctx, _ctx.clientAppManager(), _args}; RouterApp app = (RouterApp) con.newInstance(conArgs); - mgr.addAndStart(app); + mgr.addAndStart(app, _args); } else if (isClientApp(cls)) { Constructor con = cls.getConstructor(I2PAppContext.class, ClientAppManager.class, String[].class); RouterAppManager mgr = _ctx.clientAppManager(); Object[] conArgs = new Object[] {_ctx, _ctx.clientAppManager(), _args}; ClientApp app = (ClientApp) con.newInstance(conArgs); - mgr.addAndStart(app); + mgr.addAndStart(app, _args); } else { Method method = cls.getMethod("main", new Class[] { String[].class }); method.invoke(cls, new Object[] { _args }); diff --git a/router/java/src/net/i2p/router/startup/RouterAppManager.java b/router/java/src/net/i2p/router/startup/RouterAppManager.java index a58087a3c..2956b7f9b 100644 --- a/router/java/src/net/i2p/router/startup/RouterAppManager.java +++ b/router/java/src/net/i2p/router/startup/RouterAppManager.java @@ -1,12 +1,12 @@ package net.i2p.router.startup; -import java.util.Set; +import java.util.Arrays; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import net.i2p.app.*; import static net.i2p.app.ClientAppState.*; import net.i2p.router.RouterContext; -import net.i2p.util.ConcurrentHashSet; import net.i2p.util.Log; /** @@ -19,26 +19,59 @@ public class RouterAppManager implements ClientAppManager { private final RouterContext _context; private final Log _log; - private final Set _clients; + // client to args + // this assumes clients do not override equals() + private final ConcurrentHashMap _clients; + // registered name to client private final ConcurrentHashMap _registered; public RouterAppManager(RouterContext ctx) { _context = ctx; _log = ctx.logManager().getLog(RouterAppManager.class); - _clients = new ConcurrentHashSet(16); + _clients = new ConcurrentHashMap(16); _registered = new ConcurrentHashMap(8); } - public void addAndStart(ClientApp app) { - _clients.add(app); + /** + * @param args the args that were used to instantiate the app, non-null, may be zero-length + * @return success + * @throws IllegalArgumentException if already added + */ + public boolean addAndStart(ClientApp app, String[] args) { + if (_log.shouldLog(Log.INFO)) + _log.info("Adding and starting " + app + " with class " + app.getClass().getName() + " and args " + Arrays.toString(args)); + String[] old = _clients.put(app, args); + if (old != null) + throw new IllegalArgumentException("already added"); try { app.startup(); + return true; } catch (Throwable t) { _clients.remove(app); _log.error("Client " + app + " failed to start", t); + return false; } } + /** + * Get the first known ClientApp with this class name and exact arguments. + * Caller may then retrieve or control the state of the returned client. + * A client will generally be found only if it is running or transitioning; + * after it is stopped it will not be tracked by the manager. + * + * @param args non-null, may be zero-length + * @return client app or null + * @since 0.9.6 + */ + public ClientApp getClientApp(String className, String[] args) { + for (Map.Entry e : _clients.entrySet()) { + if (e.getKey().getClass().getName().equals(className) && + Arrays.equals(e.getValue(), args)) + return e.getKey(); + } + return null; + } + // ClientAppManager methods /** @@ -97,7 +130,7 @@ public class RouterAppManager implements ClientAppManager { * @return true if successful, false if duplicate name */ public boolean register(ClientApp app) { - if (!_clients.contains(app)) + if (!_clients.containsKey(app)) return false; // TODO if old app in there is not running and != this app, allow replacement return _registered.putIfAbsent(app.getName(), app) == null;