diff --git a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java index 4657ab946..6d612e2bb 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java +++ b/apps/i2psnark/java/src/org/klomp/snark/SnarkManager.java @@ -22,9 +22,7 @@ import java.util.Map; import java.util.Properties; import java.util.Set; import java.util.StringTokenizer; -import java.util.Queue; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.LinkedBlockingQueue; import net.i2p.I2PAppContext; import net.i2p.app.ClientApp; @@ -46,6 +44,7 @@ import net.i2p.util.SimpleTimer; import net.i2p.util.SimpleTimer2; import net.i2p.util.SystemVersion; import net.i2p.util.Translate; +import net.i2p.util.UIMessages; import org.klomp.snark.comments.Comment; import org.klomp.snark.comments.CommentSet; @@ -75,7 +74,7 @@ public class SnarkManager implements CompleteListener, ClientApp { private final String _contextPath; private final String _contextName; private final Log _log; - private final Queue _messages; + private final UIMessages _messages; private final I2PSnarkUtil _util; private PeerCoordinatorSet _peerCoordinatorSet; private ConnectionAcceptor _connectionAcceptor; @@ -156,6 +155,7 @@ public class SnarkManager implements CompleteListener, ClientApp { public static final String CONFIG_DIR_SUFFIX = ".d"; private static final String SUBDIR_PREFIX = "s"; private static final String B64 = Base64.ALPHABET_I2P; + private static final int MAX_MESSAGES = 100; /** * "name", "announceURL=websiteURL" pairs @@ -246,7 +246,7 @@ public class SnarkManager implements CompleteListener, ClientApp { _contextPath = ctxPath; _contextName = ctxName; _log = _context.logManager().getLog(SnarkManager.class); - _messages = new LinkedBlockingQueue(); + _messages = new UIMessages(MAX_MESSAGES); _util = new I2PSnarkUtil(_context, ctxName); String cfile = ctxName + CONFIG_FILE_SUFFIX; File configFile = new File(cfile); @@ -397,8 +397,6 @@ public class SnarkManager implements CompleteListener, ClientApp { /** hook to I2PSnarkUtil for the servlet */ public I2PSnarkUtil util() { return _util; } - private static final int MAX_MESSAGES = 100; - /** * Use if it does not include a link. * Escapes '<' and '>' before queueing @@ -413,19 +411,14 @@ public class SnarkManager implements CompleteListener, ClientApp { * @since 0.9.14.1 */ public void addMessageNoEscape(String message) { - _messages.offer(message); - while (_messages.size() > MAX_MESSAGES) { - _messages.poll(); - } + _messages.addMessageNoEscape(message); if (_log.shouldLog(Log.INFO)) _log.info("MSG: " + message); } /** newest last */ - public List getMessages() { - if (_messages.isEmpty()) - return Collections.emptyList(); - return new ArrayList(_messages); + public List getMessages() { + return _messages.getMessages(); } /** @since 0.9 */ @@ -433,6 +426,14 @@ public class SnarkManager implements CompleteListener, ClientApp { _messages.clear(); } + /** + * Clear through this id + * @since 0.9.33 + */ + public void clearMessages(int id) { + _messages.clearThrough(id); + } + /** * @return default false * @since 0.8.9 @@ -2363,11 +2364,10 @@ public class SnarkManager implements CompleteListener, ClientApp { // don't bother delaying if auto start is false long delay = (60L * 1000) * getStartupDelayMinutes(); if (delay > 0 && shouldAutoStart()) { - addMessageNoEscape(_t("Adding torrents in {0}", DataHelper.formatDuration2(delay))); + int id = _messages.addMessageNoEscape(_t("Adding torrents in {0}", DataHelper.formatDuration2(delay))); try { Thread.sleep(delay); } catch (InterruptedException ie) {} // Remove that first message - if (_messages.size() == 1) - _messages.poll(); + _messages.clearThrough(id); } // here because we need to delay until I2CP is up diff --git a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java index 55da25007..1863a6c17 100644 --- a/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java +++ b/apps/i2psnark/java/src/org/klomp/snark/web/I2PSnarkServlet.java @@ -38,6 +38,7 @@ import net.i2p.util.Log; import net.i2p.util.SecureFile; import net.i2p.util.SystemVersion; import net.i2p.util.Translate; +import net.i2p.util.UIMessages; import org.klomp.snark.I2PSnarkUtil; import org.klomp.snark.MagnetURI; @@ -406,7 +407,7 @@ public class I2PSnarkServlet extends BasicServlet { } private void writeMessages(PrintWriter out, boolean isConfigure, String peerString) throws IOException { - List msgs = _manager.getMessages(); + List msgs = _manager.getMessages(); if (!msgs.isEmpty()) { out.write("\n
"); out.write(""); + int lastID = msgs.get(msgs.size() - 1).id; + out.write("action=Clear&id=" + lastID + "&nonce=" + _nonce + "\">"); String tx = _t("clear messages"); out.write(toThemeImg("delete", tx, tx)); out.write("" + "\n
    \n"); - out.write(""); + // FIXME translate, only show once + //out.write(""); for (int i = msgs.size()-1; i >= 0; i--) { - String msg = msgs.get(i); + String msg = msgs.get(i).message; out.write("
  • " + msg + "
  • \n"); } out.write("
\n
"); @@ -1340,7 +1343,13 @@ public class I2PSnarkServlet extends BasicServlet { } else if ("StartAll".equals(action)) { _manager.startAllTorrents(); } else if ("Clear".equals(action)) { - _manager.clearMessages(); + String sid = req.getParameter("id"); + if (sid != null) { + try { + int id = Integer.parseInt(sid); + _manager.clearMessages(id); + } catch (NumberFormatException nfe) {} + } } else { _manager.addMessage("Unknown POST action: \"" + action + '\"'); } diff --git a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java index e35f4342a..a4e980e50 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/TunnelControllerGroup.java @@ -330,6 +330,7 @@ public class TunnelControllerGroup implements ClientApp { /** * 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. + * This does not return or clear the controller messages. * * @throws IllegalArgumentException if unable to reload config file */ @@ -380,7 +381,8 @@ public class TunnelControllerGroup implements ClientApp { } /** - * Stop and remove the given tunnel + * Stop and remove the given tunnel. + * Side effect - clears all messages the controller. * * @return list of messages from the controller as it is stopped */ @@ -400,6 +402,7 @@ public class TunnelControllerGroup implements ClientApp { /** * Stop all tunnels. May be restarted. + * Side effect - clears all messages from all controllers. * * @return list of messages the tunnels generate when stopped */ @@ -436,7 +439,8 @@ public class TunnelControllerGroup implements ClientApp { } /** - * Start all tunnels + * Start all tunnels. + * Side effect - clears all messages from all controllers. * * @return list of messages the tunnels generate when started */ @@ -459,7 +463,8 @@ public class TunnelControllerGroup implements ClientApp { } /** - * Restart all tunnels + * Restart all tunnels. + * Side effect - clears all messages from all controllers. * * @return list of messages the tunnels generate when restarted */ @@ -481,7 +486,7 @@ public class TunnelControllerGroup implements ClientApp { } /** - * Fetch all outstanding messages from any of the known tunnels + * Fetch and clear all outstanding messages from any of the known tunnels. * * @return list of messages the tunnels have generated */ 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 93b2550cf..30507cb1a 100644 --- a/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java +++ b/apps/i2ptunnel/java/src/net/i2p/i2ptunnel/web/IndexBean.java @@ -34,6 +34,7 @@ import net.i2p.i2ptunnel.ui.GeneralHelper; import net.i2p.i2ptunnel.ui.TunnelConfig; import net.i2p.util.Addresses; import net.i2p.util.Log; +import net.i2p.util.UIMessages; /** * Simple accessor for exposing tunnel info, but also an ugly form handler @@ -54,6 +55,7 @@ public class IndexBean { //private long _prevNonce2; private String _curNonce; //private long _nextNonce; + private int _msgID = -1; private final TunnelConfig _config; private boolean _removeConfirmed; @@ -72,6 +74,7 @@ public class IndexBean { private static final int MAX_NONCES = 8; /** store nonces in a static FIFO instead of in System Properties @since 0.8.1 */ private static final List _nonces = new ArrayList(MAX_NONCES + 1); + private static final UIMessages _messages = new UIMessages(100); public static final String PROP_THEME_NAME = "routerconsole.theme"; public static final String DEFAULT_THEME = "light"; @@ -150,7 +153,18 @@ public class IndexBean { _tunnel = -1; } } + + /** @since 0.9.33 */ + public void setMsgid(String id) { + if (id == null) return; + try { + _msgID = Integer.parseInt(id); + } catch (NumberFormatException nfe) { + _msgID = -1; + } + } + /** @return non-null */ private String processAction() { if ( (_action == null) || (_action.trim().length() <= 0) || ("Cancel".equals(_action))) return ""; @@ -162,32 +176,43 @@ public class IndexBean { return _t("Invalid form submission, probably because you used the 'back' or 'reload' button on your browser. Please resubmit.") + ' ' + _t("If the problem persists, verify that you have cookies enabled in your browser."); - if ("Stop all".equals(_action)) - return stopAll(); - else if ("Start all".equals(_action)) - return startAll(); - else if ("Restart all".equals(_action)) - return restartAll(); - else if ("Reload configuration".equals(_action)) + // for any of these that call getMessage(msgs), + // we return "", as getMessage() will add them to the returned string. + if ("Stop all".equals(_action)) { + stopAll(); + return ""; + } else if ("Start all".equals(_action)) { + startAll(); + return ""; + } else if ("Restart all".equals(_action)) { + restartAll(); + return ""; + } else if ("Reload configuration".equals(_action)) { return reloadConfig(); - else if ("stop".equals(_action)) + } else if ("stop".equals(_action)) { return stop(); - else if ("start".equals(_action)) + } else if ("start".equals(_action)) { return start(); - else if ("Save changes".equals(_action) || // IE workaround: - (_action.toLowerCase(Locale.US).indexOf("save") >= 0)) - return saveChanges(); - else if ("Delete this proxy".equals(_action) || // IE workaround: - (_action.toLowerCase(Locale.US).indexOf("delete") >= 0)) - return deleteTunnel(); - else if ("Estimate".equals(_action)) + } else if ("Save changes".equals(_action) || // IE workaround: + (_action.toLowerCase(Locale.US).indexOf("save") >= 0)) { + saveChanges(); + return ""; + } else if ("Delete this proxy".equals(_action) || // IE workaround: + (_action.toLowerCase(Locale.US).indexOf("delete") >= 0)) { + deleteTunnel(); + return ""; + } else if ("Estimate".equals(_action)) { return PrivateKeyFile.estimateHashCashTime(_hashCashValue); - else if ("Modify".equals(_action)) + } else if ("Modify".equals(_action)) { return modifyDestination(); - else if ("Generate".equals(_action)) + } else if ("Generate".equals(_action)) { return generateNewEncryptionKey(); - else + } else if ("Clear".equals(_action)) { + _messages.clearThrough(_msgID); + return ""; + } else { return "Action " + _action + " unknown"; + } } private String stopAll() { @@ -258,7 +283,7 @@ public class IndexBean { * Executes any action requested (start/stop/etc) and dump out the * messages. * - * @return HTML escaped + * @return HTML escaped or "" if empty */ public String getMessages() { if (_group == null) @@ -267,16 +292,33 @@ public class IndexBean { StringBuilder buf = new StringBuilder(512); if (_action != null) { try { - buf.append(processAction()).append('\n'); + String result = processAction(); + if (result.length() > 0) + buf.append(processAction()).append('\n'); } catch (RuntimeException e) { _log.log(Log.CRIT, "Error processing " + _action, e); buf.append("Error: ").append(e.toString()).append('\n'); } } + List msgs = _messages.getMessages(); + if (!msgs.isEmpty()) { + for (UIMessages.Message msg : msgs) { + buf.append(msg.message).append('\n'); + } + } getMessages(_group.clearAllMessages(), buf); return DataHelper.escapeHTML(buf.toString()); } + /** + * The last stored message ID + * + * @since 0.9.33 + */ + public int getLastMessageID() { + return _messages.getLastMessageID(); + } + //// // The remaining methods are simple bean props for the jsp to query //// @@ -1151,7 +1193,9 @@ public class IndexBean { private static void getMessages(List msgs, StringBuilder buf) { if (msgs == null) return; for (int i = 0; i < msgs.size(); i++) { - buf.append(msgs.get(i)).append("\n"); + String msg = msgs.get(i); + _messages.addMessageNoEscape(msg); + buf.append(msg).append("\n"); } } diff --git a/apps/i2ptunnel/jsp/index.jsp b/apps/i2ptunnel/jsp/index.jsp index 240194773..eddee6702 100644 --- a/apps/i2ptunnel/jsp/index.jsp +++ b/apps/i2ptunnel/jsp/index.jsp @@ -35,6 +35,15 @@ +<% + if (indexBean.isInitialized()) { + String nextNonce = net.i2p.i2ptunnel.web.IndexBean.getNextNonce(); + + // not synced, oh well + int lastID = indexBean.getLastMessageID(); + String msgs = indexBean.getMessages(); + if (msgs.length() > 0) { +%>

<%=intl._t("Status Messages")%>

@@ -43,23 +52,17 @@ - -
<%=intl._t("Refresh")%> + <%=intl._t("Clear")%>
- <% - - if (indexBean.isInitialized()) { - String nextNonce = net.i2p.i2ptunnel.web.IndexBean.getNextNonce(); - + } // !msgs.isEmpty() %> -

<%=intl._t("Global Tunnel Control")%>

diff --git a/core/java/src/net/i2p/util/UIMessages.java b/core/java/src/net/i2p/util/UIMessages.java new file mode 100644 index 000000000..214e50371 --- /dev/null +++ b/core/java/src/net/i2p/util/UIMessages.java @@ -0,0 +1,114 @@ +package net.i2p.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * A queue of messages, where each has an ID number. + * Provide the ID back to the clear call, so you don't + * erase messages you haven't seen yet. + * + * Thread-safe. + * + * @since 0.9.33 adapted from SnarkManager + */ +public class UIMessages { + + private final int _maxSize; + private int _count; + private final LinkedList _messages; + + /** + * @param maxSize + */ + public UIMessages(int maxSize) { + if (maxSize < 1) + throw new IllegalArgumentException(); + _maxSize = maxSize; + _messages = new LinkedList(); + } + + /** + * Will remove an old message if over the max size. + * Use if it does not include a link. + * Escapes '<' and '>' before queueing + * + * @return the message id + */ + public int addMessage(String message) { + return addMessageNoEscape(message.replace("&", "&").replace("<", "<").replace(">", ">")); + } + + /** + * Use if it includes a link. + * Does not escape '<' and '>' before queueing + * + * @return the message id + */ + public synchronized int addMessageNoEscape(String message) { + _messages.offer(new Message(_count++, message)); + while (_messages.size() > _maxSize) { + _messages.poll(); + } + return _count; + } + + /** + * The ID of the last message added, or -1 if never. + */ + public synchronized int getLastMessageID() { + return _count - 1; + } + + /** + * Newest last, or empty list. + * Provide id of last one back to clearThrough(). + * @return a copy + */ + public synchronized List getMessages() { + if (_messages.isEmpty()) + return Collections.emptyList(); + return new ArrayList(_messages); + } + + /** clear all */ + public synchronized void clear() { + _messages.clear(); + } + + /** clear all up to and including this id */ + public synchronized void clearThrough(int id) { + Message m = _messages.peekLast(); + if (m == null) { + // nothing to do + } else if (m.id <= id) { + // easy way + _messages.clear(); + } else { + for (Iterator iter = _messages.iterator(); iter.hasNext(); ) { + Message msg = iter.next(); + if (msg.id > id) + break; + iter.remove(); + } + } + } + + public static class Message { + public final int id; + public final String message; + + private Message(int i, String msg) { + id = i; + message = msg; + } + + @Override + public String toString() { + return message; + } + } +} diff --git a/history.txt b/history.txt index a04b158dc..e1f15bb16 100644 --- a/history.txt +++ b/history.txt @@ -1,3 +1,9 @@ +2017-12-03 zzz + * i2ptunnel: + - Don't lose messages on refresh (ticket #2107) + - New clear messages button + - Hide message box if none + 2017-12-02 zzz * i2ptunnel: Propagate resets from streaming to Socket and vice versa (ticket #2071) diff --git a/router/java/src/net/i2p/router/RouterVersion.java b/router/java/src/net/i2p/router/RouterVersion.java index 5a58b19bc..e36cd3b14 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 = 11; + public final static long BUILD = 12; /** for example "-test" */ public final static String EXTRA = "";