forked from I2P_Developers/i2p.i2p
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:
@ -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 '<' 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<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
|
||||
|
@ -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 + "&");
|
||||
else
|
||||
out.write("?");
|
||||
out.write("action=Clear&nonce=" + _nonce + "\">");
|
||||
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("</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 + '\"');
|
||||
}
|
||||
|
@ -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
|
||||
*/
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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&msgid=<%=lastID%>&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>
|
||||
|
114
core/java/src/net/i2p/util/UIMessages.java
Normal file
114
core/java/src/net/i2p/util/UIMessages.java
Normal 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 '<' 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<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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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 = "";
|
||||
|
Reference in New Issue
Block a user