2004-09-07 jrandom

* Write the native libraries to the current directory when they are loaded
      from a resource, and load them from that file on subsequent runs (in
      turn, we no longer *cough* delete the running libraries...)
    * Added support for a graceful restart.
    * Added new pseudo-shutdown hook specific to the router, allowing
      applications to request tasks to be run when the router shuts down.  We
      use this for integration with the service manager, since otherwise a
      graceful shutdown would cause a timeout, followed by a forced hard
      shutdown.
    * Handle a bug in the SimpleTimer with requeued tasks.
    * Made the capacity calculator a bit more dynamic by not outright ignoring
      the otherwise valid capacity data for a period with a single rejected
      tunnel (except for the 10 minute period).  In addition, peers with an
      equal capacity are ordered by speed rather than by their hashes.
    * Cleaned up the SimpleTimer, addressing some threading and synchronization
      issues.
    * When an I2PTunnel client or httpclient is explicitly closed, destroy the
      associated session (unless there are other clients using it), and deal
      with a closed session when starting a new I2PTunnel instance.
    * Refactoring and logging.
This commit is contained in:
jrandom
2004-09-07 07:17:02 +00:00
committed by zzz
parent e57aa68854
commit 6151d63eac
18 changed files with 611 additions and 394 deletions

View File

@ -17,6 +17,8 @@ import java.util.List;
import java.util.Properties;
import net.i2p.I2PException;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
import net.i2p.client.streaming.I2PSocket;
import net.i2p.client.streaming.I2PSocketManager;
import net.i2p.client.streaming.I2PSocketManagerFactory;
@ -118,7 +120,16 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
return getSocketManager(getTunnel());
}
protected static synchronized I2PSocketManager getSocketManager(I2PTunnel tunnel) {
if (socketManager == null) {
if (socketManager != null) {
I2PSession s = socketManager.getSession();
if ( (s == null) || (s.isClosed()) ) {
_log.info("Building a new socket manager since the old one closed [s=" + s + "]");
socketManager = buildSocketManager(tunnel);
} else {
_log.info("Not building a new socket manager since the old one is open [s=" + s + "]");
}
} else {
_log.info("Building a new socket manager since there is no other one");
socketManager = buildSocketManager(tunnel);
}
return socketManager;
@ -277,7 +288,10 @@ public abstract class I2PTunnelClientBase extends I2PTunnelTask implements Runna
}
return false;
}
getTunnel().removeSession(sockMgr.getSession());
I2PSession session = sockMgr.getSession();
if (session != null) {
getTunnel().removeSession(session);
}
l.log("Closing client " + toString());
try {
if (ss != null) ss.close();

View File

@ -28,6 +28,7 @@ public class TunnelController implements Logging {
private Properties _config;
private I2PTunnel _tunnel;
private List _messages;
private List _sessions;
private boolean _running;
/**
@ -144,9 +145,42 @@ public class TunnelController implements Logging {
_tunnel.runHttpClient(new String[] { listenPort }, this);
else
_tunnel.runHttpClient(new String[] { listenPort, proxyList }, this);
acquire();
_running = true;
}
/**
* Note the fact that we are using some sessions, so that they dont get
* closed by some other tunnels
*/
private void acquire() {
List sessions = _tunnel.getSessions();
if (sessions != null) {
for (int i = 0; i < sessions.size(); i++) {
I2PSession session = (I2PSession)sessions.get(i);
TunnelControllerGroup.getInstance().acquire(this, session);
}
_sessions = sessions;
} else {
_log.error("No sessions to acquire?");
}
}
/**
* Note the fact that we are no longer using some sessions, and if
* no other tunnels are using them, close them.
*/
private void release() {
if (_sessions != null) {
for (int i = 0; i < _sessions.size(); i++) {
I2PSession s = (I2PSession)_sessions.get(i);
TunnelControllerGroup.getInstance().release(this, s);
}
} else {
_log.error("No sessions to release?");
}
}
private void startClient() {
setI2CPOptions();
setSessionOptions();
@ -154,6 +188,7 @@ public class TunnelController implements Logging {
String listenPort = getListenPort();
String dest = getTargetDestination();
_tunnel.runClient(new String[] { listenPort, dest }, this);
acquire();
_running = true;
}
@ -164,6 +199,7 @@ public class TunnelController implements Logging {
String targetPort = getTargetPort();
String privKeyFile = getPrivKeyFile();
_tunnel.runServer(new String[] { targetHost, targetPort, privKeyFile }, this);
acquire();
_running = true;
}
@ -201,6 +237,7 @@ public class TunnelController implements Logging {
public void stopTunnel() {
_tunnel.runClose(new String[] { "forced", "all" }, this);
release();
_running = false;
}

View File

@ -8,13 +8,18 @@ import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import net.i2p.I2PAppContext;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
import net.i2p.util.Log;
/**
@ -30,23 +35,33 @@ public class TunnelControllerGroup {
private List _controllers;
private String _configFile = DEFAULT_CONFIG_FILE;
/**
* Map of I2PSession to a Set of TunnelController objects
* using the session (to prevent closing the session until
* no more tunnels are using it)
*
*/
private Map _sessions;
public static TunnelControllerGroup getInstance() { return _instance; }
private TunnelControllerGroup(String configFile) {
_log = I2PAppContext.getGlobalContext().logManager().getLog(TunnelControllerGroup.class);
_instance = this;
_controllers = new ArrayList();
_configFile = configFile;
_sessions = new HashMap(4);
loadControllers(_configFile);
}
public static void main(String args[]) {
if ( (args == null) || (args.length <= 0) ) {
_instance = new TunnelControllerGroup(DEFAULT_CONFIG_FILE);
new TunnelControllerGroup(DEFAULT_CONFIG_FILE);
} else if (args.length == 1) {
if (DEFAULT_CONFIG_FILE.equals(args[0]))
_instance = new TunnelControllerGroup(DEFAULT_CONFIG_FILE);
new TunnelControllerGroup(DEFAULT_CONFIG_FILE);
else
_instance = new TunnelControllerGroup(args[0]);
new TunnelControllerGroup(args[0]);
} else {
System.err.println("Usage: TunnelControllerGroup [filename]");
return;
@ -281,4 +296,61 @@ public class TunnelControllerGroup {
*/
public List getControllers() { return _controllers; }
/**
* Note the fact that the controller is using the session so that
* it isn't destroyed prematurely.
*
*/
void acquire(TunnelController controller, I2PSession session) {
synchronized (_sessions) {
Set owners = (Set)_sessions.get(session);
if (owners == null) {
owners = new HashSet(1);
_sessions.put(session, owners);
}
owners.add(controller);
}
if (_log.shouldLog(Log.INFO))
_log.info("Acquiring session " + session + " for " + controller);
}
/**
* Note the fact that the controller is no longer using the session, and if
* no other controllers are using it, destroy the session.
*
*/
void release(TunnelController controller, I2PSession session) {
boolean shouldClose = false;
synchronized (_sessions) {
Set owners = (Set)_sessions.get(session);
if (owners != null) {
owners.remove(controller);
if (owners.size() <= 0) {
if (_log.shouldLog(Log.INFO))
_log.info("After releasing session " + session + " by " + controller + ", no more owners remain");
shouldClose = true;
_sessions.remove(session);
} else {
if (_log.shouldLog(Log.INFO))
_log.info("After releasing session " + session + " by " + controller + ", " + owners.size() + " owners remain");
shouldClose = false;
}
} else {
if (_log.shouldLog(Log.WARN))
_log.warn("After releasing session " + session + " by " + controller + ", no owners were even known?!");
shouldClose = true;
}
}
if (shouldClose) {
try {
session.destroySession();
if (_log.shouldLog(Log.INFO))
_log.info("Session destroyed: " + session);
} catch (I2PSessionException ise) {
_log.error("Error closing the client session", ise);
}
}
}
}

View File

@ -15,32 +15,40 @@ import org.tanukisoftware.wrapper.WrapperManager;
public class ConfigServiceHandler extends FormHandler {
public void ConfigNetHandler() {}
private class UpdateWrapperManagerTask implements Runnable {
private int _exitCode;
public UpdateWrapperManagerTask(int exitCode) {
_exitCode = exitCode;
}
public void run() {
try {
WrapperManager.signalStopped(_exitCode);
} catch (Throwable t) {
t.printStackTrace();
}
}
}
protected void processForm() {
if (_action == null) return;
if ("Shutdown gracefully".equals(_action)) {
try {
WrapperManager.signalStopped(Router.EXIT_GRACEFUL);
} catch (Throwable t) {
addFormError("Warning: unable to contact the service manager - " + t.getMessage());
}
_context.router().addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_GRACEFUL));
_context.router().shutdownGracefully();
addFormNotice("Graceful shutdown initiated");
} else if ("Shutdown immediately".equals(_action)) {
try {
WrapperManager.signalStopped(Router.EXIT_HARD);
} catch (Throwable t) {
addFormError("Warning: unable to contact the service manager - " + t.getMessage());
}
_context.router().addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_HARD));
_context.router().shutdown(Router.EXIT_HARD);
addFormNotice("Shutdown immediately! boom bye bye bad bwoy");
} else if ("Cancel graceful shutdown".equals(_action)) {
_context.router().cancelGracefulShutdown();
addFormNotice("Graceful shutdown cancelled");
} else if ("Graceful restart".equals(_action)) {
_context.router().addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_GRACEFUL_RESTART));
_context.router().shutdownGracefully(Router.EXIT_GRACEFUL_RESTART);
addFormNotice("Graceful restart requested");
} else if ("Hard restart".equals(_action)) {
_context.router().addShutdownTask(new UpdateWrapperManagerTask(Router.EXIT_HARD_RESTART));
_context.router().shutdown(Router.EXIT_HARD_RESTART);
addFormNotice("Hard restart requested");
} else if ("Run I2P on startup".equals(_action)) {

View File

@ -417,17 +417,17 @@ public class CPUID {
String wantedProp = System.getProperty("jcpuid.enable", "true");
boolean wantNative = "true".equalsIgnoreCase(wantedProp);
if (wantNative) {
boolean loaded = loadFromResource();
boolean loaded = loadGeneric();
if (loaded) {
_nativeOk = true;
if (_doLog)
System.err.println("INFO: Native CPUID library '"+getResourceName()+"' loaded from resource");
System.err.println("INFO: Native CPUID library '"+getLibraryMiddlePart()+"' loaded from somewhere in the path");
} else {
loaded = loadGeneric();
loaded = loadFromResource();
if (loaded) {
_nativeOk = true;
if (_doLog)
System.err.println("INFO: Native CPUID library '"+getLibraryMiddlePart()+"' loaded from somewhere in the path");
System.err.println("INFO: Native CPUID library '"+getResourceName()+"' loaded from resource");
} else {
_nativeOk = false;
if (_doLog)
@ -451,6 +451,12 @@ public class CPUID {
*
*/
private static final boolean loadGeneric() {
try {
System.loadLibrary("jcpuid");
return true;
} catch (UnsatisfiedLinkError ule) {
// fallthrough, try the OS-specific filename
}
try {
System.loadLibrary(getLibraryMiddlePart());
return true;
@ -486,7 +492,7 @@ public class CPUID {
FileOutputStream fos = null;
try {
InputStream libStream = resource.openStream();
outFile = File.createTempFile(libPrefix + "jcpuid", "lib.tmp" + libSuffix);
outFile = new File(libPrefix + "jcpuid" + libSuffix);
fos = new FileOutputStream(outFile);
byte buf[] = new byte[4096*1024];
while (true) {
@ -515,10 +521,6 @@ public class CPUID {
if (fos != null) {
try { fos.close(); } catch (IOException ioe) {}
}
if (outFile != null) {
if (!outFile.delete())
outFile.deleteOnExit();
}
}
}

View File

@ -101,6 +101,13 @@ public interface I2PSession {
*/
public void connect() throws I2PSessionException;
/**
* Have we closed the session?
*
* @return true if the session is closed
*/
public boolean isClosed();
/**
* Retrieve the Destination this session serves as the endpoint for.
* Returns null if no destination is available.

View File

@ -354,7 +354,12 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
_pendingSizes = new ArrayList(2);
}
public void stopNotifying() { _alive = false; }
public void stopNotifying() {
_alive = false;
synchronized (AvailabilityNotifier.this) {
AvailabilityNotifier.this.notifyAll();
}
}
public void available(int msgId, int size) {
synchronized (AvailabilityNotifier.this) {
@ -499,7 +504,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
public void destroySession(boolean sendDisconnect) {
if (_closed) return;
if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Destroy the session", new Exception("DestroySession()"));
if (_log.shouldLog(Log.INFO)) _log.info(getPrefix() + "Destroy the session", new Exception("DestroySession()"));
if (sendDisconnect) {
try {
_producer.disconnect(this);
@ -518,7 +523,7 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
*
*/
private void closeSocket() {
if (_log.shouldLog(Log.DEBUG)) _log.debug(getPrefix() + "Closing the socket", new Exception("closeSocket"));
if (_log.shouldLog(Log.INFO)) _log.info(getPrefix() + "Closing the socket", new Exception("closeSocket"));
_closed = true;
if (_reader != null) _reader.stopReading();
_reader = null;

View File

@ -67,9 +67,13 @@ class RequestLeaseSetMessageHandler extends HandlerImpl {
synchronized (_existingLeaseSets) {
_existingLeaseSets.put(session.getMyDestination(), li);
}
_log.debug("Creating new leaseInfo keys", new Exception("new leaseInfo keys"));
if (_log.shouldLog(Log.DEBUG))
_log.debug("Creating new leaseInfo keys for "
+ session.getMyDestination().calculateHash().toBase64());
} else {
_log.debug("Caching the old leaseInfo keys", new Exception("cached! w00t"));
if (_log.shouldLog(Log.DEBUG))
_log.debug("Caching the old leaseInfo keys for "
+ session.getMyDestination().calculateHash().toBase64());
}
leaseSet.setEncryptionKey(li.getPublicKey());

View File

@ -485,7 +485,7 @@ public class NativeBigInteger extends BigInteger {
FileOutputStream fos = null;
try {
InputStream libStream = resource.openStream();
outFile = File.createTempFile(_libPrefix + "jbigi", "lib.tmp" + _libSuffix);
outFile = new File(_libPrefix + "jbigi" + _libSuffix);
fos = new FileOutputStream(outFile);
byte buf[] = new byte[4096*1024];
while (true) {
@ -514,10 +514,6 @@ public class NativeBigInteger extends BigInteger {
if (fos != null) {
try { fos.close(); } catch (IOException ioe) {}
}
if (outFile != null) {
if (!outFile.delete())
outFile.deleteOnExit();
}
}
}

View File

@ -64,6 +64,8 @@ public class SimpleTimer {
private class SimpleTimerRunner implements Runnable {
public void run() {
List eventsToFire = new ArrayList(1);
List timesToRemove = new ArrayList(1);
while (true) {
try {
synchronized (_events) {
@ -71,34 +73,48 @@ public class SimpleTimer {
_events.wait();
long now = System.currentTimeMillis();
long nextEventDelay = -1;
List removed = null;
for (Iterator iter = _events.keySet().iterator(); iter.hasNext(); ) {
Long when = (Long)iter.next();
if (when.longValue() <= now) {
TimedEvent evt = (TimedEvent)_events.get(when);
try {
evt.timeReached();
} catch (Throwable t) {
log("wtf, event borked: " + evt, t);
}
if (removed == null)
removed = new ArrayList(1);
removed.add(when);
eventsToFire.add(evt);
timesToRemove.add(when);
} else {
nextEventDelay = when.longValue() - now;
break;
}
}
if (removed != null) {
for (int i = 0; i < removed.size(); i++)
_events.remove(removed.get(i));
if (timesToRemove.size() > 0) {
for (int i = 0; i < timesToRemove.size(); i++)
_events.remove(timesToRemove.get(i));
} else {
if (nextEventDelay != -1)
_events.wait(nextEventDelay);
else
_events.wait();
}
if (nextEventDelay != -1)
_events.wait(nextEventDelay);
else
_events.wait();
}
} catch (InterruptedException ie) {}
} catch (InterruptedException ie) {
// ignore
} catch (Throwable t) {
if (_log != null) {
_log.log(Log.CRIT, "Uncaught exception in the SimpleTimer!", t);
} else {
System.err.println("Uncaught exception in SimpleTimer");
t.printStackTrace();
}
}
for (int i = 0; i < eventsToFire.size(); i++) {
TimedEvent evt = (TimedEvent)eventsToFire.get(i);
try {
evt.timeReached();
} catch (Throwable t) {
log("wtf, event borked: " + evt, t);
}
}
eventsToFire.clear();
timesToRemove.clear();
}
}
}

View File

@ -1,4 +1,26 @@
$Id: history.txt,v 1.7 2004/09/04 16:54:09 jrandom Exp $
$Id: history.txt,v 1.8 2004/09/06 00:20:42 jrandom Exp $
2004-09-07 jrandom
* Write the native libraries to the current directory when they are loaded
from a resource, and load them from that file on subsequent runs (in
turn, we no longer *cough* delete the running libraries...)
* Added support for a graceful restart.
* Added new pseudo-shutdown hook specific to the router, allowing
applications to request tasks to be run when the router shuts down. We
use this for integration with the service manager, since otherwise a
graceful shutdown would cause a timeout, followed by a forced hard
shutdown.
* Handle a bug in the SimpleTimer with requeued tasks.
* Made the capacity calculator a bit more dynamic by not outright ignoring
the otherwise valid capacity data for a period with a single rejected
tunnel (except for the 10 minute period). In addition, peers with an
equal capacity are ordered by speed rather than by their hashes.
* Cleaned up the SimpleTimer, addressing some threading and synchronization
issues.
* When an I2PTunnel client or httpclient is explicitly closed, destroy the
associated session (unless there are other clients using it), and deal
with a closed session when starting a new I2PTunnel instance.
* Refactoring and logging.
2004-09-06 jrandom
* Address a race condition in the key management code that would manifest

View File

@ -1,145 +1,147 @@
#********************************************************************
# Wrapper Properties
#********************************************************************
# Java Application
wrapper.java.command=java
# Java Main class. This class must implement the WrapperListener interface
# or guarantee that the WrapperManager class is initialized. Helper
# classes are provided to do this for you. See the Integration section
# of the documentation for details.
wrapper.java.mainclass=org.tanukisoftware.wrapper.WrapperSimpleApp
# Java Classpath (include wrapper.jar) Add class path elements as
# needed starting from 1
wrapper.java.classpath.1=lib/ant.jar
wrapper.java.classpath.2=lib/heartbeat.jar
wrapper.java.classpath.3=lib/i2p.jar
wrapper.java.classpath.4=lib/i2ptunnel.jar
wrapper.java.classpath.5=lib/jasper-compiler.jar
wrapper.java.classpath.6=lib/jasper-runtime.jar
wrapper.java.classpath.7=lib/javax.servlet.jar
wrapper.java.classpath.8=lib/jnet.jar
wrapper.java.classpath.9=lib/mstreaming.jar
wrapper.java.classpath.10=lib/netmonitor.jar
wrapper.java.classpath.11=lib/org.mortbay.jetty.jar
wrapper.java.classpath.12=lib/router.jar
wrapper.java.classpath.13=lib/routerconsole.jar
wrapper.java.classpath.14=lib/sam.jar
wrapper.java.classpath.15=lib/wrapper.jar
wrapper.java.classpath.16=lib/xercesImpl.jar
wrapper.java.classpath.17=lib/xml-apis.jar
wrapper.java.classpath.18=lib/jbigi.jar
wrapper.java.classpath.19=lib/systray.jar
wrapper.java.classpath.20=lib/systray4j.jar
# Java Library Path (location of Wrapper.DLL or libwrapper.so)
wrapper.java.library.path.1=.
wrapper.java.library.path.2=lib
# Java Additional Parameters
wrapper.java.additional.1=-DloggerFilenameOverride=logs/log-router-@.txt
# Initial Java Heap Size (in MB)
#wrapper.java.initmemory=4
# Maximum Java Heap Size (in MB)
#wrapper.java.maxmemory=32
# Application parameters. Add parameters as needed starting from 1
wrapper.app.parameter.1=net.i2p.router.Router
#********************************************************************
# Wrapper Logging Properties
#********************************************************************
# Format of output for the console. (See docs for formats)
wrapper.console.format=PM
# Log Level for console output. (See docs for log levels)
wrapper.console.loglevel=INFO
# Log file to use for wrapper output logging.
wrapper.logfile=wrapper.log
# Format of output for the log file. (See docs for formats)
wrapper.logfile.format=LPTM
# Log Level for log file output. (See docs for log levels)
wrapper.logfile.loglevel=INFO
# Maximum size that the log file will be allowed to grow to before
# the log is rolled. Size is specified in bytes. The default value
# of 0, disables log rolling. May abbreviate with the 'k' (kb) or
# 'm' (mb) suffix. For example: 10m = 10 megabytes.
wrapper.logfile.maxsize=1m
# Maximum number of rolled log files which will be allowed before old
# files are deleted. The default value of 0 implies no limit.
wrapper.logfile.maxfiles=2
# Log Level for sys/event log output. (See docs for log levels)
wrapper.syslog.loglevel=NONE
# choose what to do if the JVM kills itself based on the exit code
wrapper.on_exit.default=SHUTDOWN
wrapper.on_exit.0=SHUTDOWN
wrapper.on_exit.1=SHUTDOWN
# OOM
wrapper.on_exit.10=RESTART
# graceful shutdown
wrapper.on_exit.2=SHUTDOWN
# hard shutdown
wrapper.on_exit.3=SHUTDOWN
# hard restart
wrapper.on_exit.4=RESTART
# the router may take a few seconds to save state, etc
wrapper.jvm_exit.timeout=60
# give the OS 60s to clear all the old sockets / etc before restarting
wrapper.restart.delay=60
# use the wrapper's internal timer thread. otherwise this would
# force a restart of the router during daylight savings time as well
# as any time that the OS clock changes
wrapper.use_system_time=false
# pid file for the JVM
wrapper.java.pidfile=routerjvm.pid
# pid file for the service monitoring the JVM
#
# From i2prouter:
#
# PIDDIR="."
# APP_NAME="i2p"
# PIDFILE="$PIDDIR/$APP_NAME.pid"
#
# This means i2prouter looks for './i2p.pid'.
wrapper.pidfile=i2p.pid
#********************************************************************
# Wrapper NT Service Properties
#********************************************************************
# WARNING - Do not modify any of these properties when an application
# using this configuration file has been installed as a service.
# Please uninstall the service before modifying this section. The
# service can then be reinstalled.
# Name of the service
wrapper.ntservice.name=i2p
# Display name of the service
wrapper.ntservice.displayname=I2P Service
# Description of the service
wrapper.ntservice.description=The I2P router service
# Service dependencies. Add dependencies as needed starting from 1
wrapper.ntservice.dependency.1=
# Mode in which the service is installed. AUTO_START or DEMAND_START
wrapper.ntservice.starttype=AUTO_START
# Allow the service to interact with the desktop.
wrapper.ntservice.interactive=true
#********************************************************************
# Wrapper Properties
#********************************************************************
# Java Application
wrapper.java.command=java
# Java Main class. This class must implement the WrapperListener interface
# or guarantee that the WrapperManager class is initialized. Helper
# classes are provided to do this for you. See the Integration section
# of the documentation for details.
wrapper.java.mainclass=org.tanukisoftware.wrapper.WrapperSimpleApp
# Java Classpath (include wrapper.jar) Add class path elements as
# needed starting from 1
wrapper.java.classpath.1=lib/ant.jar
wrapper.java.classpath.2=lib/heartbeat.jar
wrapper.java.classpath.3=lib/i2p.jar
wrapper.java.classpath.4=lib/i2ptunnel.jar
wrapper.java.classpath.5=lib/jasper-compiler.jar
wrapper.java.classpath.6=lib/jasper-runtime.jar
wrapper.java.classpath.7=lib/javax.servlet.jar
wrapper.java.classpath.8=lib/jnet.jar
wrapper.java.classpath.9=lib/mstreaming.jar
wrapper.java.classpath.10=lib/netmonitor.jar
wrapper.java.classpath.11=lib/org.mortbay.jetty.jar
wrapper.java.classpath.12=lib/router.jar
wrapper.java.classpath.13=lib/routerconsole.jar
wrapper.java.classpath.14=lib/sam.jar
wrapper.java.classpath.15=lib/wrapper.jar
wrapper.java.classpath.16=lib/xercesImpl.jar
wrapper.java.classpath.17=lib/xml-apis.jar
wrapper.java.classpath.18=lib/jbigi.jar
wrapper.java.classpath.19=lib/systray.jar
wrapper.java.classpath.20=lib/systray4j.jar
# Java Library Path (location of Wrapper.DLL or libwrapper.so)
wrapper.java.library.path.1=.
wrapper.java.library.path.2=lib
# Java Additional Parameters
wrapper.java.additional.1=-DloggerFilenameOverride=logs/log-router-@.txt
# Initial Java Heap Size (in MB)
#wrapper.java.initmemory=4
# Maximum Java Heap Size (in MB)
#wrapper.java.maxmemory=32
# Application parameters. Add parameters as needed starting from 1
wrapper.app.parameter.1=net.i2p.router.Router
#********************************************************************
# Wrapper Logging Properties
#********************************************************************
# Format of output for the console. (See docs for formats)
wrapper.console.format=PM
# Log Level for console output. (See docs for log levels)
wrapper.console.loglevel=INFO
# Log file to use for wrapper output logging.
wrapper.logfile=wrapper.log
# Format of output for the log file. (See docs for formats)
wrapper.logfile.format=LPTM
# Log Level for log file output. (See docs for log levels)
wrapper.logfile.loglevel=INFO
# Maximum size that the log file will be allowed to grow to before
# the log is rolled. Size is specified in bytes. The default value
# of 0, disables log rolling. May abbreviate with the 'k' (kb) or
# 'm' (mb) suffix. For example: 10m = 10 megabytes.
wrapper.logfile.maxsize=1m
# Maximum number of rolled log files which will be allowed before old
# files are deleted. The default value of 0 implies no limit.
wrapper.logfile.maxfiles=2
# Log Level for sys/event log output. (See docs for log levels)
wrapper.syslog.loglevel=NONE
# choose what to do if the JVM kills itself based on the exit code
wrapper.on_exit.default=SHUTDOWN
wrapper.on_exit.0=SHUTDOWN
wrapper.on_exit.1=SHUTDOWN
# OOM
wrapper.on_exit.10=RESTART
# graceful shutdown
wrapper.on_exit.2=SHUTDOWN
# hard shutdown
wrapper.on_exit.3=SHUTDOWN
# hard restart
wrapper.on_exit.4=RESTART
# hard restart
wrapper.on_exit.5=RESTART
# the router may take a few seconds to save state, etc
wrapper.jvm_exit.timeout=10
# give the OS 60s to clear all the old sockets / etc before restarting
wrapper.restart.delay=60
# use the wrapper's internal timer thread. otherwise this would
# force a restart of the router during daylight savings time as well
# as any time that the OS clock changes
wrapper.use_system_time=false
# pid file for the JVM
wrapper.java.pidfile=routerjvm.pid
# pid file for the service monitoring the JVM
#
# From i2prouter:
#
# PIDDIR="."
# APP_NAME="i2p"
# PIDFILE="$PIDDIR/$APP_NAME.pid"
#
# This means i2prouter looks for './i2p.pid'.
wrapper.pidfile=i2p.pid
#********************************************************************
# Wrapper NT Service Properties
#********************************************************************
# WARNING - Do not modify any of these properties when an application
# using this configuration file has been installed as a service.
# Please uninstall the service before modifying this section. The
# service can then be reinstalled.
# Name of the service
wrapper.ntservice.name=i2p
# Display name of the service
wrapper.ntservice.displayname=I2P Service
# Description of the service
wrapper.ntservice.description=The I2P router service
# Service dependencies. Add dependencies as needed starting from 1
wrapper.ntservice.dependency.1=
# Mode in which the service is installed. AUTO_START or DEMAND_START
wrapper.ntservice.starttype=AUTO_START
# Allow the service to interact with the desktop.
wrapper.ntservice.interactive=true

View File

@ -59,6 +59,7 @@ public class Router {
private I2PThread.OOMEventListener _oomListener;
private ShutdownHook _shutdownHook;
private I2PThread _gracefulShutdownDetector;
private Set _shutdownTasks;
public final static String PROP_CONFIG_FILE = "router.configLocation";
@ -123,6 +124,8 @@ public class Router {
_gracefulShutdownDetector.setDaemon(true);
_gracefulShutdownDetector.setName("Graceful shutdown hook");
_gracefulShutdownDetector.start();
_shutdownTasks = new HashSet(0);
}
/**
@ -542,6 +545,12 @@ public class Router {
buf.setLength(0);
}
public void addShutdownTask(Runnable task) {
synchronized (_shutdownTasks) {
_shutdownTasks.add(task);
}
}
public static final int EXIT_GRACEFUL = 2;
public static final int EXIT_HARD = 3;
public static final int EXIT_OOM = 10;
@ -564,6 +573,14 @@ public class Router {
try { _sessionKeyPersistenceHelper.shutdown(); } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting down the session key manager", t); }
_context.listContexts().remove(_context);
dumpStats();
try {
for (Iterator iter = _shutdownTasks.iterator(); iter.hasNext(); ) {
Runnable task = (Runnable)iter.next();
task.run();
}
} catch (Throwable t) {
_log.log(Log.CRIT, "Error running shutdown task", t);
}
_log.log(Log.CRIT, "Shutdown(" + exitCode + ") complete", new Exception("Shutdown"));
try { _context.logManager().shutdown(); } catch (Throwable t) { }
File f = new File(getPingFile());

View File

@ -15,9 +15,9 @@ import net.i2p.CoreVersion;
*
*/
public class RouterVersion {
public final static String ID = "$Revision: 1.23 $ $Date: 2004/09/04 16:54:09 $";
public final static String ID = "$Revision: 1.24 $ $Date: 2004/09/06 00:21:26 $";
public final static String VERSION = "0.4";
public final static long BUILD = 6;
public final static long BUILD = 7;
public static void main(String args[]) {
System.out.println("I2P Router version: " + VERSION);
System.out.println("Router ID: " + RouterVersion.ID);

View File

@ -78,8 +78,6 @@ public class CapacityCalculator extends Calculator {
Rate curAccepted = acceptStat.getRate(period);
Rate curRejected = rejectStat.getRate(period);
Rate curFailed = failedStat.getRate(period);
if (curRejected.getCurrentEventCount() + curRejected.getLastEventCount() > 0)
return 0.0d;
long eventCount = 0;
if (curAccepted != null)
@ -91,6 +89,12 @@ public class CapacityCalculator extends Calculator {
failed = curFailed.getCurrentEventCount() + curFailed.getLastEventCount();
if (failed > 0)
val -= failed * stretch;
if ( (period == 10*60*1000) && (curRejected.getCurrentEventCount() + curRejected.getLastEventCount() > 0) )
return 0.0d;
else
val -= stretch * (curRejected.getCurrentEventCount() + curRejected.getLastEventCount());
if (val >= 0) {
return (val + GROWTH_FACTOR) * periodWeight(period);
} else {

View File

@ -0,0 +1,54 @@
package net.i2p.router.peermanager;
import java.util.Comparator;
import net.i2p.data.DataHelper;
/**
* Order profiles by their capacity, but backwards (highest capacity / value first).
*
*/
class InverseCapacityComparator implements Comparator {
/**
* Compare the two objects backwards. The standard comparator returns
* -1 if lhs is less than rhs, 1 if lhs is greater than rhs, or 0 if they're
* equal. To keep a strict ordering, we measure peers with equal capacity
* values according to their speed
*
* @return -1 if the right hand side is smaller, 1 if the left hand side is
* smaller, or 0 if they are the same peer (Comparator.compare() inverted)
*/
public int compare(Object lhs, Object rhs) {
if ( (lhs == null) || (rhs == null) || (!(lhs instanceof PeerProfile)) || (!(rhs instanceof PeerProfile)) )
throw new ClassCastException("Only profiles can be compared - lhs = " + lhs + " rhs = " + rhs);
PeerProfile left = (PeerProfile)lhs;
PeerProfile right= (PeerProfile)rhs;
double rval = right.getCapacityValue();
double lval = left.getCapacityValue();
if (lval == rval) {
rval = right.getSpeedValue();
lval = left.getSpeedValue();
if (lval == rval) {
// note the following call inverts right and left (see: classname)
return DataHelper.compareTo(right.getPeer().getData(), left.getPeer().getData());
} else {
// ok, fall through and compare based on speed, since the capacity is equal
}
}
boolean rightBigger = rval > lval;
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("The capacity of " + right.getPeer().toBase64()
// + " and " + left.getPeer().toBase64() + " marks " + (rightBigger ? "right" : "left")
// + " as larger: r=" + right.getCapacityValue()
// + " l="
// + left.getCapacityValue());
if (rightBigger)
return 1;
else
return -1;
}
}

View File

@ -99,49 +99,12 @@ public class ProfileOrganizer {
_persistenceHelper = new ProfilePersistenceHelper(_context);
}
/**
* Order profiles by their capacity, but backwards (highest capacity / value first).
*
*/
private final class InverseCapacityComparator implements Comparator {
/**
* Compare the two objects backwards. The standard comparator returns
* -1 if lhs is less than rhs, 1 if lhs is greater than rhs, or 0 if they're
* equal. To keep a strict ordering, we measure peers with equal capacity
* values according to their hashes
*
* @return -1 if the right hand side is smaller, 1 if the left hand side is
* smaller, or 0 if they are the same peer (Comparator.compare() inverted)
*/
public int compare(Object lhs, Object rhs) {
if ( (lhs == null) || (rhs == null) || (!(lhs instanceof PeerProfile)) || (!(rhs instanceof PeerProfile)) )
throw new ClassCastException("Only profiles can be compared - lhs = " + lhs + " rhs = " + rhs);
PeerProfile left = (PeerProfile)lhs;
PeerProfile right= (PeerProfile)rhs;
double rval = right.getCapacityValue();
double lval = left.getCapacityValue();
if (lval == rval) // note the following call inverts right and left (see: classname)
return DataHelper.compareTo(right.getPeer().getData(), left.getPeer().getData());
boolean rightBigger = rval > lval;
//if (_log.shouldLog(Log.DEBUG))
// _log.debug("The capacity of " + right.getPeer().toBase64()
// + " and " + left.getPeer().toBase64() + " marks " + (rightBigger ? "right" : "left")
// + " as larger: r=" + right.getCapacityValue()
// + " l="
// + left.getCapacityValue());
if (rightBigger)
return 1;
else
return -1;
}
}
public void setUs(Hash us) { _us = us; }
Hash getUs() { return _us; }
public double getSpeedThreshold() { return _thresholdSpeedValue; }
public double getCapacityThreshold() { return _thresholdCapacityValue; }
public double getIntegrationThreshold() { return _thresholdIntegrationValue; }
/**
* Retrieve the profile for the given peer, if one exists (else null)
@ -207,7 +170,6 @@ public class ProfileOrganizer {
public boolean isHighCapacity(Hash peer) { synchronized (_reorganizeLock) { return _highCapacityPeers.containsKey(peer); } }
public boolean isWellIntegrated(Hash peer) { synchronized (_reorganizeLock) { return _wellIntegratedPeers.containsKey(peer); } }
public boolean isFailing(Hash peer) { synchronized (_reorganizeLock) { return _failingPeers.containsKey(peer); } }
/**
* if a peer sends us more than 5 replies in a searchReply that we cannot
@ -234,8 +196,17 @@ public class ProfileOrganizer {
}
return false;
}
public void exportProfile(Hash profile, OutputStream out) throws IOException {
PeerProfile prof = getProfile(profile);
if (prof != null)
_persistenceHelper.writeProfile(prof, out);
}
public void renderStatusHTML(OutputStream out) throws IOException {
ProfileOrganizerRenderer rend = new ProfileOrganizerRenderer(this, _context);
rend.renderStatusHTML(out);
}
/**
* Return a set of Hashes for peers that are both fast and reliable. If an insufficient
@ -422,12 +393,7 @@ public class ProfileOrganizer {
allPeers.addAll(_notFailingPeers.values());
allPeers.addAll(_highCapacityPeers.values());
allPeers.addAll(_fastPeers.values());
_failingPeers.clear();
_notFailingPeers.clear();
_highCapacityPeers.clear();
_fastPeers.clear();
Set reordered = new TreeSet(_comp);
for (Iterator iter = _strictCapacityOrder.iterator(); iter.hasNext(); ) {
PeerProfile prof = (PeerProfile)iter.next();
@ -534,9 +500,6 @@ public class ProfileOrganizer {
}
}
public double getSpeedThreshold() { return _thresholdSpeedValue; }
public double getCapacityThreshold() { return _thresholdCapacityValue; }
////////
// no more public stuff below
////////
@ -578,7 +541,6 @@ public class ProfileOrganizer {
_thresholdIntegrationValue = 1.0d * avg(totalIntegration, reordered.size());
}
/**
* Update the _thresholdCapacityValue by using a few simple formulas run
* against the specified peers. Ideally, we set the threshold capacity to
@ -775,116 +737,6 @@ public class ProfileOrganizer {
*/
private boolean shouldDrop(PeerProfile profile) { return false; }
public void exportProfile(Hash profile, OutputStream out) throws IOException {
PeerProfile prof = getProfile(profile);
if (prof != null)
_persistenceHelper.writeProfile(prof, out);
}
public void renderStatusHTML(OutputStream out) throws IOException {
Set peers = selectAllPeers();
long hideBefore = _context.clock().now() - 6*60*60*1000;
TreeMap order = new TreeMap();
for (Iterator iter = peers.iterator(); iter.hasNext();) {
Hash peer = (Hash)iter.next();
if (_us.equals(peer)) continue;
PeerProfile prof = getProfile(peer);
if (prof.getLastSendSuccessful() <= hideBefore) continue;
order.put(peer.toBase64(), prof);
}
int fast = 0;
int reliable = 0;
int integrated = 0;
int failing = 0;
StringBuffer buf = new StringBuffer(16*1024);
buf.append("<h2>Peer Profiles</h2>\n");
buf.append("<table border=\"1\">");
buf.append("<tr>");
buf.append("<td><b>Peer</b> (").append(order.size()).append(", hiding ").append(peers.size()-order.size()).append(")</td>");
buf.append("<td><b>Groups</b></td>");
buf.append("<td><b>Speed</b></td>");
buf.append("<td><b>Capacity</b></td>");
buf.append("<td><b>Integration</b></td>");
buf.append("<td><b>Failing?</b></td>");
buf.append("<td>&nbsp;</td>");
buf.append("</tr>");
for (Iterator iter = order.keySet().iterator(); iter.hasNext();) {
String name = (String)iter.next();
PeerProfile prof = (PeerProfile)order.get(name);
Hash peer = prof.getPeer();
buf.append("<tr>");
buf.append("<td><code>");
if (prof.getIsFailing()) {
buf.append("<font color=\"red\">--").append(peer.toBase64().substring(0,6)).append("</font>");
} else {
if (prof.getIsActive()) {
buf.append("<font color=\"blue\">++").append(peer.toBase64().substring(0,6)).append("</font>");
} else {
buf.append("__").append(peer.toBase64().substring(0,6));
}
}
buf.append("</code></td>");
buf.append("<td>");
int tier = 0;
boolean isIntegrated = false;
synchronized (_reorganizeLock) {
if (_fastPeers.containsKey(peer)) {
tier = 1;
fast++;
reliable++;
} else if (_highCapacityPeers.containsKey(peer)) {
tier = 2;
reliable++;
} else if (_notFailingPeers.containsKey(peer)) {
tier = 3;
} else {
failing++;
}
if (_wellIntegratedPeers.containsKey(peer)) {
isIntegrated = true;
integrated++;
}
}
switch (tier) {
case 1: buf.append("Fast"); break;
case 2: buf.append("High Capacity"); break;
case 3: buf.append("Not Failing"); break;
default: buf.append("Failing"); break;
}
if (isIntegrated) buf.append(", Integrated");
buf.append("<td align=\"right\">").append(num(prof.getSpeedValue())).append("</td>");
buf.append("<td align=\"right\">").append(num(prof.getCapacityValue())).append("</td>");
buf.append("<td align=\"right\">").append(num(prof.getIntegrationValue())).append("</td>");
buf.append("<td align=\"right\">").append(prof.getIsFailing()).append("</td>");
//buf.append("<td><a href=\"/profile/").append(prof.getPeer().toBase64().substring(0, 32)).append("\">profile.txt</a> ");
//buf.append(" <a href=\"#").append(prof.getPeer().toBase64().substring(0, 32)).append("\">netDb</a></td>");
buf.append("<td><a href=\"netdb.jsp#").append(peer.toBase64().substring(0,6)).append("\">netDb</a></td>\n");
buf.append("</tr>");
}
buf.append("</table>");
buf.append("<i>Definitions:<ul>");
buf.append("<li><b>speed</b>: how many round trip messages can we pump through the peer per minute?</li>");
buf.append("<li><b>capacity</b>: how many tunnels can we ask them to join in an hour?</li>");
buf.append("<li><b>integration</b>: how many new peers have they told us about lately?</li>");
buf.append("<li><b>failing?</b>: is the peer currently swamped (and if possible we should avoid nagging them)?</li>");
buf.append("</ul></i>");
buf.append("Red peers prefixed with '--' means the peer is failing, and blue peers prefixed ");
buf.append("with '++' means we've sent or received a message from them ");
buf.append("in the last five minutes</i><br />");
buf.append("<b>Thresholds:</b><br />");
buf.append("<b>Speed:</b> ").append(num(_thresholdSpeedValue)).append(" (").append(fast).append(" fast peers)<br />");
buf.append("<b>Capacity:</b> ").append(num(_thresholdCapacityValue)).append(" (").append(reliable).append(" high capacity peers)<br />");
buf.append("<b>Integration:</b> ").append(num(_thresholdIntegrationValue)).append(" (").append(integrated).append(" well integrated peers)<br />");
out.write(buf.toString().getBytes());
}
/**
* Defines the minimum number of 'fast' peers that the organizer should select. If
* the profile calculators derive a threshold that does not select at least this many peers,
@ -895,20 +747,6 @@ public class ProfileOrganizer {
* @return minimum number of peers to be placed in the 'fast' group
*/
protected int getMinimumFastPeers() {
if (_context.router() != null) {
String val = _context.router().getConfigSetting(PROP_MINIMUM_FAST_PEERS);
if (val != null) {
try {
int rv = Integer.parseInt(val);
if (_log.shouldLog(Log.DEBUG))
_log.debug("router config said " + PROP_MINIMUM_FAST_PEERS + '=' + val);
return rv;
} catch (NumberFormatException nfe) {
if (_log.shouldLog(Log.WARN))
_log.warn("Minimum fast peers improperly set in the router config [" + val + "]", nfe);
}
}
}
String val = _context.getProperty(PROP_MINIMUM_FAST_PEERS, ""+DEFAULT_MINIMUM_FAST_PEERS);
if (val != null) {
try {
@ -938,20 +776,6 @@ public class ProfileOrganizer {
* @return minimum number of peers to be placed in the 'fast' group
*/
protected int getMinimumHighCapacityPeers() {
if (_context.router() != null) {
String val = _context.router().getConfigSetting(PROP_MINIMUM_HIGH_CAPACITY_PEERS);
if (val != null) {
try {
int rv = Integer.parseInt(val);
if (_log.shouldLog(Log.DEBUG))
_log.debug("router config said " + PROP_MINIMUM_HIGH_CAPACITY_PEERS + '=' + val);
return rv;
} catch (NumberFormatException nfe) {
if (_log.shouldLog(Log.WARN))
_log.warn("Minimum high capacity peers improperly set in the router config [" + val + "]", nfe);
}
}
}
String val = _context.getProperty(PROP_MINIMUM_HIGH_CAPACITY_PEERS, ""+DEFAULT_MINIMUM_HIGH_CAPACITY_PEERS);
if (val != null) {
try {
@ -970,7 +794,6 @@ public class ProfileOrganizer {
return DEFAULT_MINIMUM_HIGH_CAPACITY_PEERS;
}
private final static DecimalFormat _fmt = new DecimalFormat("###,##0.00", new DecimalFormatSymbols(Locale.UK));
private final static String num(double num) { synchronized (_fmt) { return _fmt.format(num); } }

View File

@ -0,0 +1,134 @@
package net.i2p.router.peermanager;
import java.io.IOException;
import java.io.OutputStream;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import net.i2p.data.Hash;
import net.i2p.router.RouterContext;
/**
* Helper class to refactor the HTML rendering from out of the ProfileOrganizer
*
*/
class ProfileOrganizerRenderer {
private RouterContext _context;
private ProfileOrganizer _organizer;
public ProfileOrganizerRenderer(ProfileOrganizer organizer, RouterContext context) {
_context = context;
_organizer = organizer;
}
public void renderStatusHTML(OutputStream out) throws IOException {
Set peers = _organizer.selectAllPeers();
long hideBefore = _context.clock().now() - 3*60*60*1000;
TreeMap order = new TreeMap();
for (Iterator iter = peers.iterator(); iter.hasNext();) {
Hash peer = (Hash)iter.next();
if (_organizer.getUs().equals(peer)) continue;
PeerProfile prof = _organizer.getProfile(peer);
if (prof.getLastSendSuccessful() <= hideBefore) continue;
order.put(peer.toBase64(), prof);
}
int fast = 0;
int reliable = 0;
int integrated = 0;
int failing = 0;
StringBuffer buf = new StringBuffer(16*1024);
buf.append("<h2>Peer Profiles</h2>\n");
buf.append("<table border=\"1\">");
buf.append("<tr>");
buf.append("<td><b>Peer</b> (").append(order.size()).append(", hiding ").append(peers.size()-order.size()).append(")</td>");
buf.append("<td><b>Groups</b></td>");
buf.append("<td><b>Speed</b></td>");
buf.append("<td><b>Capacity</b></td>");
buf.append("<td><b>Integration</b></td>");
buf.append("<td><b>Failing?</b></td>");
buf.append("<td>&nbsp;</td>");
buf.append("</tr>");
for (Iterator iter = order.keySet().iterator(); iter.hasNext();) {
String name = (String)iter.next();
PeerProfile prof = (PeerProfile)order.get(name);
Hash peer = prof.getPeer();
buf.append("<tr>");
buf.append("<td><code>");
if (prof.getIsFailing()) {
buf.append("<font color=\"red\">--").append(peer.toBase64().substring(0,6)).append("</font>");
} else {
if (prof.getIsActive()) {
buf.append("<font color=\"blue\">++").append(peer.toBase64().substring(0,6)).append("</font>");
} else {
buf.append("__").append(peer.toBase64().substring(0,6));
}
}
buf.append("</code></td>");
buf.append("<td>");
int tier = 0;
boolean isIntegrated = false;
if (_organizer.isFast(peer)) {
tier = 1;
fast++;
reliable++;
} else if (_organizer.isHighCapacity(peer)) {
tier = 2;
reliable++;
} else if (_organizer.isFailing(peer)) {
failing++;
} else {
tier = 3;
}
if (_organizer.isWellIntegrated(peer)) {
isIntegrated = true;
integrated++;
}
switch (tier) {
case 1: buf.append("Fast"); break;
case 2: buf.append("High Capacity"); break;
case 3: buf.append("Not Failing"); break;
default: buf.append("Failing"); break;
}
if (isIntegrated) buf.append(", Integrated");
buf.append("<td align=\"right\">").append(num(prof.getSpeedValue())).append("</td>");
buf.append("<td align=\"right\">").append(num(prof.getCapacityValue())).append("</td>");
buf.append("<td align=\"right\">").append(num(prof.getIntegrationValue())).append("</td>");
buf.append("<td align=\"right\">").append(prof.getIsFailing()).append("</td>");
//buf.append("<td><a href=\"/profile/").append(prof.getPeer().toBase64().substring(0, 32)).append("\">profile.txt</a> ");
//buf.append(" <a href=\"#").append(prof.getPeer().toBase64().substring(0, 32)).append("\">netDb</a></td>");
buf.append("<td><a href=\"netdb.jsp#").append(peer.toBase64().substring(0,6)).append("\">netDb</a></td>\n");
buf.append("</tr>");
}
buf.append("</table>");
buf.append("<i>Definitions:<ul>");
buf.append("<li><b>speed</b>: how many round trip messages can we pump through the peer per minute?</li>");
buf.append("<li><b>capacity</b>: how many tunnels can we ask them to join in an hour?</li>");
buf.append("<li><b>integration</b>: how many new peers have they told us about lately?</li>");
buf.append("<li><b>failing?</b>: is the peer currently swamped (and if possible we should avoid nagging them)?</li>");
buf.append("</ul></i>");
buf.append("Red peers prefixed with '--' means the peer is failing, and blue peers prefixed ");
buf.append("with '++' means we've sent or received a message from them ");
buf.append("in the last five minutes</i><br />");
buf.append("<b>Thresholds:</b><br />");
buf.append("<b>Speed:</b> ").append(num(_organizer.getSpeedThreshold())).append(" (").append(fast).append(" fast peers)<br />");
buf.append("<b>Capacity:</b> ").append(num(_organizer.getCapacityThreshold())).append(" (").append(reliable).append(" high capacity peers)<br />");
buf.append("<b>Integration:</b> ").append(num(_organizer.getIntegrationThreshold())).append(" (").append(integrated).append(" well integrated peers)<br />");
out.write(buf.toString().getBytes());
}
private final static DecimalFormat _fmt = new DecimalFormat("###,##0.00", new DecimalFormatSymbols(Locale.UK));
private final static String num(double num) { synchronized (_fmt) { return _fmt.format(num); } }
}