Util: New utility class for UI message queues, for use by i2psnark and i2ptunnel

i2psnark: Use new utility, prevent message loss on clear
i2ptunnel:
- Don't lose messages on refresh (ticket #2107)
- New clear messages button
- Hide message box if none
- javadoc clarifications
This commit is contained in:
zzz
2017-12-03 17:33:20 +00:00
parent 5912f7c259
commit 16282ec5c5
8 changed files with 239 additions and 58 deletions

View File

@ -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<String> _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<String>();
_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 '&lt;' and '&gt;' 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<String> getMessages() {
if (_messages.isEmpty())
return Collections.emptyList();
return new ArrayList<String>(_messages);
public List<UIMessages.Message> 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

View File

@ -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<String> msgs = _manager.getMessages();
List<UIMessages.Message> msgs = _manager.getMessages();
if (!msgs.isEmpty()) {
out.write("\n<div class=\"snarkMessages\" tabindex=\"0\">");
out.write("<a id=\"closeLog\" href=\"" + _contextPath + '/');
@ -416,15 +417,17 @@ public class I2PSnarkServlet extends BasicServlet {
out.write(peerString + "&amp;");
else
out.write("?");
out.write("action=Clear&amp;nonce=" + _nonce + "\">");
int lastID = msgs.get(msgs.size() - 1).id;
out.write("action=Clear&amp;id=" + lastID + "&amp;nonce=" + _nonce + "\">");
String tx = _t("clear messages");
out.write(toThemeImg("delete", tx, tx));
out.write("</a>" +
"\n<ul>\n");
out.write("<noscript><li class=\"noscriptWarning\">Warning! Javascript is disabled in your browser. If <a href=\"configure\">page refresh</a> is enabled, ");
out.write("you will lose any input in the add/create torrent sections when a refresh occurs.</li></noscript>");
// FIXME translate, only show once
//out.write("<noscript><li class=\"noscriptWarning\">Warning! Javascript is disabled in your browser. If <a href=\"configure\">page refresh</a> is enabled, ");
//out.write("you will lose any input in the add/create torrent sections when a refresh occurs.</li></noscript>");
for (int i = msgs.size()-1; i >= 0; i--) {
String msg = msgs.get(i);
String msg = msgs.get(i).message;
out.write("<li>" + msg + "</li>\n");
}
out.write("</ul>\n</div>");
@ -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 + '\"');
}

View File

@ -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
*/

View File

@ -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<String> _nonces = new ArrayList<String>(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";
@ -151,6 +154,17 @@ public class IndexBean {
}
}
/** @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("s</span>ave") >= 0))
return saveChanges();
else if ("Delete this proxy".equals(_action) || // IE workaround:
(_action.toLowerCase(Locale.US).indexOf("d</span>elete") >= 0))
return deleteTunnel();
else if ("Estimate".equals(_action))
} else if ("Save changes".equals(_action) || // IE workaround:
(_action.toLowerCase(Locale.US).indexOf("s</span>ave") >= 0)) {
saveChanges();
return "";
} else if ("Delete this proxy".equals(_action) || // IE workaround:
(_action.toLowerCase(Locale.US).indexOf("d</span>elete") >= 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<UIMessages.Message> 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<String> 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");
}
}

View File

@ -35,6 +35,15 @@
</head>
<body id="tunnelListPage">
<%
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) {
%>
<div class="panel" id="messages">
<h2><%=intl._t("Status Messages")%></h2>
<table id="statusMessagesTable">
@ -43,23 +52,17 @@
<textarea id="statusMessages" rows="4" cols="60" readonly="readonly"><jsp:getProperty name="indexBean" property="messages" /></textarea>
</td>
</tr>
<tr>
<td class="buttons">
<a class="control" href="list"><%=intl._t("Refresh")%></a>
<a class="control" href="list?action=Clear&amp;msgid=<%=lastID%>&amp;nonce=<%=nextNonce%>"><%=intl._t("Clear")%></a>
</td>
</tr>
</table>
</div>
<%
if (indexBean.isInitialized()) {
String nextNonce = net.i2p.i2ptunnel.web.IndexBean.getNextNonce();
} // !msgs.isEmpty()
%>
<div class="panel" id="globalTunnelControl">
<h2><%=intl._t("Global Tunnel Control")%></h2>
<table>

View File

@ -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<Message> _messages;
/**
* @param maxSize
*/
public UIMessages(int maxSize) {
if (maxSize < 1)
throw new IllegalArgumentException();
_maxSize = maxSize;
_messages = new LinkedList<Message>();
}
/**
* Will remove an old message if over the max size.
* Use if it does not include a link.
* Escapes '&lt;' and '&gt;' before queueing
*
* @return the message id
*/
public int addMessage(String message) {
return addMessageNoEscape(message.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;"));
}
/**
* Use if it includes a link.
* Does not escape '&lt;' and '&gt;' 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<Message> getMessages() {
if (_messages.isEmpty())
return Collections.emptyList();
return new ArrayList<Message>(_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<Message> 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;
}
}
}

View File

@ -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)

View File

@ -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 = "";