package net.i2p.router;
/*
 * free (adj.): unencumbered; not under the control of others
 * Written by jrandom in 2003 and released into the public domain 
 * with no warranty of any kind, either expressed or implied.  
 * It probably won't make your computer catch on fire, or eat 
 * your children, but it might.  Use at your own risk.
 *
 */

import java.io.File;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import gnu.getopt.Getopt;

import freenet.support.CPUInformation.CPUID;
import freenet.support.CPUInformation.UnknownCPUException;
import net.i2p.client.impl.I2PSessionImpl;
import net.i2p.crypto.SigUtil;
import net.i2p.data.Base64;
import net.i2p.data.Certificate;
import net.i2p.data.DataFormatException;
import net.i2p.data.DataHelper;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.data.PublicKey;
import net.i2p.data.SigningPrivateKey;
import net.i2p.data.SigningPublicKey;
import net.i2p.data.i2np.GarlicMessage;
import net.i2p.data.router.RouterInfo;
import net.i2p.router.CommSystemFacade.Status;
import net.i2p.router.crypto.FamilyKeyCrypto;
import net.i2p.router.message.GarlicMessageHandler;
import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade;
import net.i2p.router.startup.CreateRouterInfoJob;
import net.i2p.router.startup.PortableWorkingDir;
import net.i2p.router.startup.StartupJob;
import net.i2p.router.startup.WorkingDir;
import net.i2p.router.sybil.Analysis;
import net.i2p.router.tasks.*;
import net.i2p.router.transport.FIFOBandwidthLimiter;
import net.i2p.router.transport.UPnPScannerCallback;
import net.i2p.router.transport.ntcp.NTCPTransport;
import net.i2p.router.transport.udp.UDPTransport;
import net.i2p.router.util.EventLog;
import net.i2p.stat.RateStat;
import net.i2p.stat.StatManager;
import net.i2p.util.ByteCache;
import net.i2p.util.FileUtil;
import net.i2p.util.FortunaRandomSource;
import net.i2p.util.I2PAppThread;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
import net.i2p.util.NativeBigInteger;
import net.i2p.util.OrderedProperties;
import net.i2p.util.ReusableGZIPInputStream;
import net.i2p.util.ReusableGZIPOutputStream;
import net.i2p.util.SecureFileOutputStream;
import net.i2p.util.SimpleByteCache;
import net.i2p.util.SystemVersion;
import net.i2p.util.Translate;

/**
 * Main driver for the router.
 *
 * For embedded use, instantiate, call setKillVMOnEnd(false), and then call runRouter().
 *
 */
public class Router implements RouterClock.ClockShiftListener {
    private Log _log;
    private final RouterContext _context;
    private final Map<String, String> _config;
    /** full path */
    private String _configFilename;
    private RouterInfo _routerInfo;
    private final ReentrantReadWriteLock _routerInfoLock = new ReentrantReadWriteLock(false);
    /** not for external use */
    public final Object routerInfoFileLock = new Object();
    private final Object _configFileLock = new Object();
    private long _started;
    private boolean _killVMOnEnd;
    private int _gracefulExitCode;
    private I2PThread.OOMEventListener _oomListener;
    private ShutdownHook _shutdownHook;
    private I2PThread _gracefulShutdownDetector;
    private RouterWatchdog _watchdog;
    private Thread _watchdogThread;
    private final EventLog _eventLog;
    private final Object _stateLock = new Object();
    private State _state = State.UNINITIALIZED;
    private FamilyKeyCrypto _familyKeyCrypto;
    private boolean _familyKeyCryptoFail;
    public final Object _familyKeyLock = new Object();
    private UPnPScannerCallback _upnpScannerCallback;
    private long _downtime = -1;
    
    private static final String BUNDLE_NAME = "net.i2p.router.web.messages";
    public final static String PROP_CONFIG_FILE = "router.configLocation";
    
    /** let clocks be off by 1 minute */
    public final static long CLOCK_FUDGE_FACTOR = 1*60*1000; 

    /** used to differentiate routerInfo files on different networks */
    private static final int DEFAULT_NETWORK_ID = 2;
    private static final String PROP_NETWORK_ID = "router.networkID";
    private final int _networkID;
    
    /** coalesce stats this often - should be a little less than one minute, so the graphs get updated */
    public static final int COALESCE_TIME = 50*1000;

    /** this puts an 'H' in your routerInfo **/
    public final static String PROP_HIDDEN = "router.hiddenMode";
    /** this does not put an 'H' in your routerInfo **/
    public final static String PROP_HIDDEN_HIDDEN = "router.isHidden";
    /** New router keys at every restart. Disabled. */
    public final static String PROP_DYNAMIC_KEYS = "router.dynamicKeys";
    /**
     *  New router keys once only.
     *  @since 0.9.34
     */
    public final static String PROP_REBUILD_KEYS = "router.rebuildKeys";
    /** deprecated, use gracefulShutdownInProgress() */
    private final static String PROP_SHUTDOWN_IN_PROGRESS = "__shutdownInProgress";
    public static final String PROP_IB_RANDOM_KEY = TunnelPoolSettings.PREFIX_INBOUND_EXPLORATORY + TunnelPoolSettings.PROP_RANDOM_KEY;
    public static final String PROP_OB_RANDOM_KEY = TunnelPoolSettings.PREFIX_OUTBOUND_EXPLORATORY + TunnelPoolSettings.PROP_RANDOM_KEY;
    private static final String EVENTLOG = "eventlog.txt";
    private static final String PROP_JBIGI = "jbigi.loadedResource";
    private static final String PROP_JBIGI_PROCESSOR = "jbigi.lastProcessor";
    public static final String UPDATE_FILE = "i2pupdate.zip";
        
    private static final int SHUTDOWN_WAIT_SECS = 60;

    private static final String originalTimeZoneID;
    static {
        //
        // If embedding I2P you may wish to disable one or more of the following
        // via the associated System property. Since 0.9.19.
        //
        if (System.getProperty("I2P_DISABLE_DNS_CACHE_OVERRIDE") == null) {
            // grumble about sun's java caching DNS entries *forever* by default
            // so lets just keep 'em for a short time
            String DNS_CACHE_TIME = Integer.toString(2*60*60);
            String DNS_NEG_CACHE_TIME = Integer.toString(30*60);
            System.setProperty("sun.net.inetaddr.ttl", DNS_CACHE_TIME);
            System.setProperty("sun.net.inetaddr.negative.ttl", DNS_NEG_CACHE_TIME);
            System.setProperty("networkaddress.cache.ttl", DNS_CACHE_TIME);
            System.setProperty("networkaddress.cache.negative.ttl", DNS_NEG_CACHE_TIME);
        }
        // ref: https://docs.oracle.com/javase/8/docs/api/java/net/doc-files/net-properties.html#Proxies
        // This doesn't do what you think, and a bad idea anyway
        //if (System.getProperty("I2P_DISABLE_HTTP_AGENT_OVERRIDE") == null) {
        //    System.setProperty("http.agent", "I2P");
        //}
        if (System.getProperty("I2P_DISABLE_HTTP_KEEPALIVE_OVERRIDE") == null) {
            // (no need for keepalive)
            // Note that doc link above is wrong, it IS capital A Alive
            System.setProperty("http.keepAlive", "false");
        }
        // Save it for LogManager
        originalTimeZoneID = TimeZone.getDefault().getID();
        if (System.getProperty("I2P_DISABLE_TIMEZONE_OVERRIDE") == null) {
            System.setProperty("user.timezone", "GMT");
            // just in case, lets make it explicit...
            TimeZone.setDefault(TimeZone.getTimeZone("GMT"));
        }
        // Note: I2P_DISABLE_OUTPUT_OVERRIDE implemented in startup/WorkingDir.java

        // https://www.kb.cert.org/vuls/id/402580
        // http://docs.codehaus.org/display/JETTY/SystemProperties
        // Fixed in Jetty 5.1.15 but we are running 5.1.12
        // The default is true, unfortunately it was previously
        // set to false in wrapper.config thru 0.7.10 so we must set it back here.
        // Not in Jetty 7
        //System.setProperty("org.mortbay.util.FileResource.checkAliases", "true");
    }
    
    /**
     *  Instantiation only. Starts no threads. Does not install updates.
     *  RouterContext is created but not initialized.
     *  You must call runRouter() after any constructor to start things up.
     *
     *  Config file name is "router.config" unless router.configLocation set in system properties.
     *
     *  See two-arg constructor for more information.
     *
     *  @throws IllegalStateException since 0.9.19 if another router with this config is running
     */
    public Router() { this(null, null); }

    /**
     *  Instantiation only. Starts no threads. Does not install updates.
     *  RouterContext is created but not initialized.
     *  You must call runRouter() after any constructor to start things up.
     *
     *  Config file name is "router.config" unless router.configLocation set in envProps or system properties.
     *
     *  See two-arg constructor for more information.
     *
     *  @param envProps may be null
     *  @throws IllegalStateException since 0.9.19 if another router with this config is running
     */
    public Router(Properties envProps) { this(null, envProps); }

    /**
     *  Instantiation only. Starts no threads. Does not install updates.
     *  RouterContext is created but not initialized.
     *  You must call runRouter() after any constructor to start things up.
     *
     *  See two-arg constructor for more information.
     *
     *  @param configFilename may be null
     *  @throws IllegalStateException since 0.9.19 if another router with this config is running
     */
    public Router(String configFilename) { this(configFilename, null); }

    /**
     *  Instantiation only. Starts no threads. Does not install updates.
     *  RouterContext is created but not initialized.
     *  You must call runRouter() after any constructor to start things up.
     *
     *  If configFilename is non-null, configuration is read in from there.
     *  Else if envProps is non-null, configuration is read in from the
     *  location given in the router.configLocation property.
     *  Else it's read in from the System property router.configLocation.
     *  Else from the file "router.config".
     *
     *  The most important properties are i2p.dir.base (the install directory, may be read-only)
     *  and i2p.dir.config (the user's configuration/data directory).
     *
     *  i2p.dir.base defaults to user.dir (CWD) but should almost always be set.
     *
     *  i2p.dir.config default depends on OS, user name (to detect if running as a service or not),
     *  and auto-detection of whether there appears to be previous data files in the base dir.
     *  See WorkingDir for details.
     *  If the config dir does not exist, it will be created, and files migrated from the base dir,
     *  in this constructor.
     *  If files in an existing config dir indicate that another router is already running
     *  with this directory, the constructor will delay for several seconds to be sure,
     *  and then throw an IllegalStateException.
     *
     *  @param configFilename may be null
     *  @param envProps may be null
     *  @throws IllegalStateException since 0.9.19 if another router with this config is running
     */
    public Router(String configFilename, Properties envProps) {
        _killVMOnEnd = true;
        _gracefulExitCode = -1;
        _config = new ConcurrentHashMap<String, String>();

        if (configFilename == null) {
            if (envProps != null) {
                _configFilename = envProps.getProperty(PROP_CONFIG_FILE);
            }
            if (_configFilename == null)
                _configFilename = System.getProperty(PROP_CONFIG_FILE, "router.config");
        } else {
            _configFilename = configFilename;
        }
                    
        // we need the user directory figured out by now, so figure it out here rather than
        // in the RouterContext() constructor.
        //
        // We have not read the config file yet. Therefore the base and config locations
        // are determined solely by properties (first envProps then System), for the purposes
        // of initializing the user's config directory if it did not exist.
        // If the base dir and/or config dir are set in the config file,
        // they wil be used after the initialization of the (possibly different) dirs
        // determined by WorkingDir.
        // So for now, it doesn't make much sense to set the base or config dirs in the config file -
        // use properties instead. If for some reason, distros need this, we can revisit it.
        //
        // Then add it to envProps (but not _config, we don't want it in the router.config file)
        // where it will then be available to all via _context.dir()
        //
        // This call also migrates all files to the new working directory,
        // including router.config
        //

        // Do we copy all the data files to the new directory? default false
        String migrate = System.getProperty("i2p.dir.migrate");
        boolean migrateFiles = Boolean.parseBoolean(migrate);


        String isPortableStr = System.getProperty("i2p.dir.portableMode");
        boolean isPortable = Boolean.parseBoolean(isPortableStr);
        String userDir = (!isPortable) ? WorkingDir.getWorkingDir(envProps, migrateFiles) : PortableWorkingDir.getWorkingDir(envProps);

        // Use the router.config file specified in the router.configLocation property
        // (default "router.config"),
        // if it is an abolute path, otherwise look in the userDir returned by getWorkingDir
        // replace relative path with absolute
        File cf = new File(_configFilename);
        if (!cf.isAbsolute()) {
            cf = new File(userDir, _configFilename);
            _configFilename = cf.getAbsolutePath();
        }

        readConfig();

        if (envProps == null)
            envProps = new Properties();
        envProps.putAll(_config);

        // This doesn't work, guess it has to be in the static block above?
        // if (Boolean.parseBoolean(envProps.getProperty("router.disableIPv6")))
        //    System.setProperty("java.net.preferIPv4Stack", "true");

        if (envProps.getProperty("i2p.dir.config") == null)
            envProps.setProperty("i2p.dir.config", userDir);
        // Save this in the context for the logger and apps that need it
        envProps.setProperty("i2p.systemTimeZone", originalTimeZoneID);

        // Make darn sure we don't have a leftover I2PAppContext in the same JVM
        // e.g. on Android - see finalShutdown() also
        List<RouterContext> contexts = RouterContext.getContexts();
        if (contexts.isEmpty()) {
            RouterContext.killGlobalContext();
        } else if (SystemVersion.isAndroid()) {
            System.err.println("Warning: Killing " + contexts.size() + " other routers in this JVM");
            contexts.clear();
            RouterContext.killGlobalContext();
        } else {
            System.err.println("Warning: " + contexts.size() + " other routers in this JVM");
        }

        // The important thing that happens here is the directory paths are set and created
        // i2p.dir.router defaults to i2p.dir.config
        // i2p.dir.app defaults to i2p.dir.router
        // i2p.dir.log defaults to i2p.dir.router
        // i2p.dir.pid defaults to i2p.dir.router
        // i2p.dir.base defaults to user.dir == $CWD
        _context = new RouterContext(this, envProps, false);
        RouterContext.setGlobalContext(_context);
        _eventLog = new EventLog(_context, new File(_context.getRouterDir(), EVENTLOG));

        // This is here so that we can get the directory location from the context
        // for the ping file
        // Check for other router but do not start a thread yet so the update doesn't cause
        // a NCDFE
        if (!SystemVersion.isAndroid()) {
            for (int i = 0; i < 14; i++) {
                // Wrapper can start us up too quickly after a crash, the ping file
                // may still be less than LIVELINESS_DELAY (60s) old.
                // So wait at least 60s to be sure.
                if (isOnlyRouterRunning()) {
                    if (i > 0)
                        System.err.println("INFO: No, there wasn't another router already running. Proceeding with startup.");
                    break;
                }
                if (i < 13) {
                    if (i == 0)
                        System.err.println("WARN: There may be another router already running. Waiting a while to be sure...");
                    // yes this is ugly to sleep in the constructor.
                    try { Thread.sleep(5000); } catch (InterruptedException ie) {}
                } else {
                    _eventLog.addEvent(EventLog.ABORTED, "Another router running");
                    System.err.println("ERROR: There appears to be another router already running!");
                    System.err.println("       Please make sure to shut down old instances before starting up");
                    System.err.println("       a new one.  If you are positive that no other instance is running,");
                    System.err.println("       please delete the file " + getPingFile().getAbsolutePath());
                    //System.exit(-1);
                    // throw exception instead, for embedded
                    throw new IllegalStateException(
                                       "ERROR: There appears to be another router already running!" +
                                       " Please make sure to shut down old instances before starting up" +
                                       " a new one.  If you are positive that no other instance is running," +
                                       " please delete the file " + getPingFile().getAbsolutePath());
                }
            }
        }

        if (_config.get("router.firstVersion") == null) {
            // These may be useful someday. First added in 0.8.2
            _config.put("router.firstVersion", RouterVersion.VERSION);
            String now = Long.toString(System.currentTimeMillis());
            _config.put("router.firstInstalled", now);
            _config.put("router.updateLastInstalled", now);
            // First added in 0.8.13
            _config.put("router.previousVersion", RouterVersion.VERSION);
            saveConfig();
        }
        int id = DEFAULT_NETWORK_ID;
        String sid = _config.get(PROP_NETWORK_ID);
        if (sid != null) {
            try {
                id = Integer.parseInt(sid);
                if (id < 2 || id > 254)
                    throw new IllegalArgumentException("Invalid " + PROP_NETWORK_ID);
            } catch (NumberFormatException nfe) {
                throw new IllegalArgumentException("Invalid " + PROP_NETWORK_ID);
            }
        }
        _networkID = id;
        // for testing
        setUPnPScannerCallback(new LoggerCallback());
        changeState(State.INITIALIZED);
        // *********  Start no threads before here ********* //
    }

    /**
     *  Initializes the RouterContext.
     *  Starts some threads. Does not install updates.
     *  All this was in the constructor.
     *
     *  Could block for 10 seconds or forever if waiting for entropy
     *
     *  @since 0.8.12
     */
    private void startupStuff() {
        // *********  Start no threads before here ********* //
        _log = _context.logManager().getLog(Router.class);

        //
        // NOW we can start the ping file thread.
        if (!SystemVersion.isAndroid())
            beginMarkingLiveliness();

        // Apps may use this as an easy way to determine if they are in the router JVM
        // But context.isRouterContext() is even easier...
        // Both of these as of 0.7.9
        // As of 0.9.34, this is FULL_VERSION, not VERSION, which was the same as CoreVersion.VERSION
        // and thus not particularly useful.
        System.setProperty("router.version", RouterVersion.FULL_VERSION);

        // crypto init may block for 10 seconds waiting for entropy
        // we want to do this before context.initAll()
        // which will fire up several things that could block on the PRNG init
        warmupCrypto();

        // NOW we start all the activity
        _context.initAll();

        // Set wrapper.log permissions.
        // Just hope this is the right location, we don't know for sure,
        // but this is the same method used in LogsHelper and we have no complaints.
        // (we could look for the wrapper.config file and parse it I guess...)
        // If we don't have a wrapper, RouterLaunch does this for us.
        if (_context.hasWrapper()) {
            File f = new File(System.getProperty("java.io.tmpdir"), "wrapper.log");
            if (!f.exists())
                f = new File(_context.getBaseDir(), "wrapper.log");
            if (f.exists())
                SecureFileOutputStream.setPerms(f);
        }
        CryptoChecker.warnUnavailableCrypto(_context);

        _routerInfo = null;
        if (_log.shouldLog(Log.INFO))
            _log.info("New router created with config file " + _configFilename);
        _oomListener = new OOMListener(_context);

        _shutdownHook = new ShutdownHook(_context);
        _gracefulShutdownDetector = new I2PAppThread(new GracefulShutdown(_context), "Graceful ShutdownHook", true);
        _gracefulShutdownDetector.setPriority(Thread.NORM_PRIORITY + 1);
        _gracefulShutdownDetector.start();
        
        _watchdog = new RouterWatchdog(_context);
        _watchdogThread = new I2PAppThread(_watchdog, "RouterWatchdog", true);
        _watchdogThread.setPriority(Thread.NORM_PRIORITY + 1);
        _watchdogThread.start();
        
        if (SystemVersion.isWindows())
            BasePerms.fix(_context);
    }
    
    /**
     *  Not for external use.
     *  @since 0.8.8
     */
    public static final void clearCaches() {
        ByteCache.clearAll();
        SimpleByteCache.clearAll();
        Destination.clearCache();
        Translate.clearCache();
        Hash.clearCache();
        PublicKey.clearCache();
        SigningPublicKey.clearCache();
        SigUtil.clearCaches();
        I2PSessionImpl.clearCache();
        ReusableGZIPInputStream.clearCache();
        ReusableGZIPOutputStream.clearCache();
    }

    /**
     * Configure the router to kill the JVM when the router shuts down, as well
     * as whether to explicitly halt the JVM during the hard fail process.
     *
     * Defaults to true. Set to false for embedded before calling runRouter()
     */
    public void setKillVMOnEnd(boolean shouldDie) { _killVMOnEnd = shouldDie; }

    public boolean getKillVMOnEnd() { return _killVMOnEnd; }
    
    /** @return absolute path */
    public String getConfigFilename() { return _configFilename; }

    /** @deprecated unused */
    @Deprecated
    public void setConfigFilename(String filename) { _configFilename = filename; }
    
    public String getConfigSetting(String name) { 
            return _config.get(name); 
    }

    /**
     *  Warning, race between here and saveConfig(),
     *  saveConfig(String name, String value) or saveConfig(Map toAdd, Set toRemove) is recommended.
     *
     *  @since 0.8.13
     *  @deprecated use saveConfig(String name, String value) or saveConfig(Map toAdd, Set toRemove)
     */
    @Deprecated
    public void setConfigSetting(String name, String value) { 
            _config.put(name, value); 
    }

    /**
     *  Warning, race between here and saveConfig(),
     *  saveConfig(String name, String value) or saveConfig(Map toAdd, Set toRemove) is recommended.
     *
     *  @since 0.8.13
     *  @deprecated use saveConfig(String name, String value) or saveConfig(Map toAdd, Set toRemove)
     */
    @Deprecated
    public void removeConfigSetting(String name) { 
            _config.remove(name); 
            // remove the backing default also
            _context.removeProperty(name);
    }

    /**
     *  @return unmodifiable Set, unsorted
     */
    public Set<String> getConfigSettings() { 
        return Collections.unmodifiableSet(_config.keySet()); 
    }

    /**
     *  @return unmodifiable Map, unsorted
     */
    public Map<String, String> getConfigMap() { 
        return Collections.unmodifiableMap(_config); 
    }
    
    /**
     *  Our current router info.
     *  Warning, may be null if called very early.
     *
     *  Warning - risk of deadlock - do not call while holding locks
     *
     *  Note: Due to lock contention, especially during a
     *  rebuild of the router info, this may take a long time.
     *  For determining the current status of the router, use
     *  RouterContext.commSystem().getStatus().
     */
    public RouterInfo getRouterInfo() {
        _routerInfoLock.readLock().lock();
        try {
            return _routerInfo;
        } finally {
            _routerInfoLock.readLock().unlock();
        }
    }

    /**
     *  Caller must ensure info is valid - no validation done here.
     *  Not for external use.
     *
     *  Warning - risk of deadlock - do not call while holding locks
     *
     */
    public void setRouterInfo(RouterInfo info) { 
        _routerInfoLock.writeLock().lock();
        try {
            _routerInfo = info; 
        } finally {
            _routerInfoLock.writeLock().unlock();
        }
        if (_log.shouldLog(Log.INFO))
            _log.info("setRouterInfo() : " + info, new Exception("I did it"));
        if (info != null)
            _context.jobQueue().addJob(new PersistRouterInfoJob(_context));
    }

    /**
     *  Used only by routerconsole.. to be deprecated?
     *  @return System time, NOT context time
     */
    public long getWhenStarted() { return _started; }

    /**
     * Wall clock uptime.
     * This uses System time, NOT context time, so context clock shifts will
     * not affect it. This is important if NTP fails and the
     * clock then shifts from a SSU peer source just after startup.
     */
    public long getUptime() { 
        if (_started <= 0) return 1000; // racing on startup
        return Math.max(1000, System.currentTimeMillis() - _started);
    }

    /**
     *  The network ID. Default 2.
     *  May be changed with the config property router.networkID (restart required).
     *  Change only if running a test network to prevent cross-network contamination.
     *
     *  @return 2 - 254
     *  @since 0.9.25
     */
    public int getNetworkID() { return _networkID; }
    
    /**
     *  Non-null, but take care when accessing context items before runRouter() is called
     *  as the context will not be initialized.
     *
     *  @return non-null
     */
    public RouterContext getContext() { return _context; }
    
    private class LoggerCallback implements UPnPScannerCallback {
        public void beforeScan() { _log.info("SSDP beforeScan()"); }
        public void afterScan() { _log.info("SSDP afterScan()"); }
    }

    /**
     *  For Android only.
     *  MUST be set before runRouter() is called.
     *
     *  @param callback the callback or null to clear it
     *  @since 0.9.41
     */
    public synchronized void setUPnPScannerCallback(UPnPScannerCallback callback) {
        _upnpScannerCallback = callback;
    }

    /**
     *  For Android only.
     *
     *  @return the callback or null if none
     *  @since 0.9.41
     */
    public synchronized UPnPScannerCallback getUPnPScannerCallback() {
        return _upnpScannerCallback;
    }

    /**
     *  This must be called after instantiation.
     *  Starts the threads. Does not install updates.
     *  This is for embedded use.
     *  Standard standalone installation uses main() instead, which
     *  checks for updates and then calls this.
     *
     *  This may take quite a while, especially if NTP fails
     *  or the system lacks entropy
     *
     *  @since public as of 0.9 for Android and other embedded uses
     *  @throws IllegalStateException if called more than once
     */
    public synchronized void runRouter() {
        synchronized(_stateLock) {
            if (_state != State.INITIALIZED)
                throw new IllegalStateException();
            changeState(State.STARTING_1);
        }
        String last = _config.get("router.previousFullVersion");
        if (last != null) {
            _eventLog.addEvent(EventLog.UPDATED, "from " + last + " to " + RouterVersion.FULL_VERSION);
            saveConfig("router.previousFullVersion", null);
        }
        _eventLog.addEvent(EventLog.STARTED, RouterVersion.FULL_VERSION);
        startupStuff();
        changeState(State.STARTING_2);
        _started = System.currentTimeMillis();
        try {
            Runtime.getRuntime().addShutdownHook(_shutdownHook);
        } catch (IllegalStateException ise) {}
        if (!SystemVersion.isAndroid())
            I2PThread.addOOMEventListener(_oomListener);
        
        // message handlers
        _context.inNetMessagePool().registerHandlerJobBuilder(GarlicMessage.MESSAGE_TYPE, new GarlicMessageHandler(_context));

        //if (ALLOW_DYNAMIC_KEYS) {
        //    if ("true".equalsIgnoreCase(_context.getProperty(Router.PROP_HIDDEN, "false")))
        //        killKeys();
        //}
        if (_context.getBooleanProperty(PROP_REBUILD_KEYS))
            killKeys();

        _context.messageValidator().startup();
        _context.tunnelDispatcher().startup();
        _context.inNetMessagePool().startup();
        _context.jobQueue().runQueue(1);
        //_context.jobQueue().addJob(new CoalesceStatsJob(_context));
        _context.simpleTimer2().addPeriodicEvent(new CoalesceStatsEvent(_context), COALESCE_TIME);
        _context.jobQueue().addJob(new UpdateRoutingKeyModifierJob(_context));
        //_context.adminManager().startup();
        _context.blocklist().startup();

        synchronized(_configFileLock) {
            // persistent key for peer ordering since 0.9.17
            // These will be replaced in CreateRouterInfoJob if we rekey
            if (!_config.containsKey(PROP_IB_RANDOM_KEY) ||
                getEstimatedDowntime() > 12*60*60*1000L) {
                byte rk[] = new byte[32];
                _context.random().nextBytes(rk);
                _config.put(PROP_IB_RANDOM_KEY, Base64.encode(rk));
                _context.random().nextBytes(rk);
                _config.put(PROP_OB_RANDOM_KEY, Base64.encode(rk));
                saveConfig();
            }
        }
        
        // let the timestamper get us sync'ed
        // this will block for quite a while on a disconnected machine
        long before = System.currentTimeMillis();
        _context.clock().getTimestamper().waitForInitialization();
        long waited = System.currentTimeMillis() - before;
        if (_log.shouldLog(Log.INFO))
            _log.info("Waited " + waited + "ms to initialize");

        changeState(State.STARTING_3);
        _context.jobQueue().addJob(new StartupJob(_context));
    }
    
    /**
     * This updates the config with all settings found in the file.
     * It does not clear the config first, so settings not found in
     * the file will remain in the config.
     *
     * This is synchronized with saveConfig().
     * Not for external use.
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public void readConfig() {
        synchronized(_configFileLock) {
            String f = getConfigFilename();
            Properties config = getConfig(_context, f);
            // to avoid compiler errror
            Map foo = _config;
            foo.putAll(config);
        }
    }
    
    /**
     *  this does not use ctx.getConfigDir(), must provide a full path in filename
     *  Caller must synchronize
     *
     *  @param ctx will be null at startup when called from constructor
     */
    private static Properties getConfig(RouterContext ctx, String filename) {
        Log log = null;
        if (ctx != null) {
            log = ctx.logManager().getLog(Router.class);
            if (log.shouldLog(Log.DEBUG))
                log.debug("Config file: " + filename, new Exception("location"));
        }
        Properties props = new Properties();
        try {
            File f = new File(filename);
            if (f.canRead()) {
                DataHelper.loadProps(props, f);
                props.remove(PROP_SHUTDOWN_IN_PROGRESS);
            } else {
                if (log != null)
                    log.warn("Configuration file " + filename + " does not exist");
                // normal not to exist at first install
                //else
                //    System.err.println("WARNING: Configuration file " + filename + " does not exist");
            }
        } catch (IOException ioe) {
            if (log != null)
                log.error("Error loading the router configuration from " + filename, ioe);
            else
                System.err.println("Error loading the router configuration from " + filename + ": " + ioe);
        }
        return props;
    }

    ////////// begin state management
    
    /**
     *  Startup / shutdown states
     *
     *  @since 0.9.18
     */
    private enum State {
        UNINITIALIZED,
        /** constructor complete */
        INITIALIZED,
        /** runRouter() called */
        STARTING_1,
        /** startupStuff() complete, most of the time here is NTP */
        STARTING_2,
        /** NTP done, Job queue started, StartupJob queued, runRouter() returned */
        STARTING_3,
        /** RIs loaded. From STARTING_3 */
        NETDB_READY,
        /** Non-zero-hop expl. tunnels built. From STARTING_3 */
        EXPL_TUNNELS_READY,
        /** from NETDB_READY or EXPL_TUNNELS_READY */
        RUNNING,
        /**
         *  A "soft" restart, primarily of the comm system, after
         *  a port change or large step-change in system time.
         *  Does not stop the whole JVM, so it is safe even in the absence
         *  of the wrapper.
         *  This is not a graceful restart - all peer connections are dropped immediately.
         */
        RESTARTING,
        /** cancellable shutdown has begun */
        GRACEFUL_SHUTDOWN,
        /** In shutdown(). Non-cancellable shutdown has begun */
        FINAL_SHUTDOWN_1,
        /** In shutdown2(). Killing everything */
        FINAL_SHUTDOWN_2,
        /** In finalShutdown(). Final cleanup */
        FINAL_SHUTDOWN_3,
        /** all done */
        STOPPED
    }

    /**
     *  For efficiency. EnumSets are bitmasks.
     *  @since 0.9.34
     */
    private static final Set<State> STATES_ALIVE =
        EnumSet.of(State.RUNNING, State.GRACEFUL_SHUTDOWN, State.STARTING_1, State.STARTING_2,
                   State.STARTING_3, State.NETDB_READY, State.EXPL_TUNNELS_READY);

    private static final Set<State> STATES_GRACEFUL =
        EnumSet.of(State.GRACEFUL_SHUTDOWN, State.FINAL_SHUTDOWN_1, State.FINAL_SHUTDOWN_2,
                   State.FINAL_SHUTDOWN_3, State.STOPPED);

    private static final Set<State> STATES_FINAL =
        EnumSet.of(State.FINAL_SHUTDOWN_1, State.FINAL_SHUTDOWN_2, State.FINAL_SHUTDOWN_3, State.STOPPED);
    
    /**
     *  @since 0.9.18
     */
    private void changeState(State state) {
        State oldState;
        synchronized(_stateLock) {
            oldState = _state;
            _state = state;
        }
        if (_log != null && oldState != state && state != State.STOPPED && _log.shouldLog(Log.WARN)) {
            _log.warn("Router state change from " + oldState + " to " + state /* , new Exception() */ );
            //for debugging
            _context.logManager().flush();
        }
    }

    /**
     *  True during the initial start, but false during a soft restart.
     */
    public boolean isAlive() {
        synchronized(_stateLock) {
            return STATES_ALIVE.contains(_state);
        }
    }

    /**
     * @return true if router is RUNNING, i.e NetDB and Expl. tunnels are ready.
     * @since 0.9.39
     */
    public boolean isRunning() {
        synchronized(_stateLock) {
            return _state == State.RUNNING;
        }
    }

    /**
     * @return true if router is RESTARTING (soft restart)
     * @since 0.9.40
     */
    public boolean isRestarting() {
        synchronized(_stateLock) {
            return _state == State.RESTARTING;
        }
    }

    /**
     *  Only for Restarter, after soft restart is complete.
     *  Not for external use.
     *  @since 0.8.12
     */
    public void setIsAlive() {
        changeState(State.RUNNING);
    }

    /**
     *  Only for NetDB, after RIs are loaded.
     *  Not for external use.
     *  @since 0.9.18
     */
    public void setNetDbReady() {
        boolean changed = false;
        synchronized(_stateLock) {
            if (_state == State.STARTING_3) {
                changeState(State.NETDB_READY);
                changed = true;
            } else if (_state == State.EXPL_TUNNELS_READY) {
                changeState(State.RUNNING);
                changed = true;
            }
        }
        if (_context.netDb().isInitialized()) {
            if (_log.shouldWarn())
                _log.warn("NetDB ready, publishing RI");
            // any previous calls to netdb().publish() did not
            // actually publish, because netdb init was not complete
            Republish r = new Republish(_context);
            // this is called from PersistentDataStore.ReadJob,
            // so we probably don't need to throw it to the timer queue,
            // but just to be safe
            _context.simpleTimer2().addEvent(r, 0);
        }
        if (changed) {
            _context.commSystem().initGeoIP();

            if (!SystemVersion.isSlow() &&
                _context.getProperty(Analysis.PROP_FREQUENCY, Analysis.DEFAULT_FREQUENCY) > 0) {
                // registers and starts itself
                Analysis.getInstance(_context);
            }
        }
    }

    /**
     *  Only for Tunnel Building, after we have non-zero-hop expl. tunnels.
     *  Not for external use.
     *  @since 0.9.18
     */
    public void setExplTunnelsReady() {
        synchronized(_stateLock) {
            if (_state == State.STARTING_3)
                changeState(State.EXPL_TUNNELS_READY);
            else if (_state == State.NETDB_READY)
                changeState(State.RUNNING);
            else
                _log.warn("Invalid state " + _state + " for setExplTunnelsReady()");
        }
    }

    /**
     * Is a graceful shutdown in progress? This may be cancelled.
     * Note that this also returns true if an uncancellable final shutdown is in progress.
     */
    public boolean gracefulShutdownInProgress() {
        synchronized(_stateLock) {
            return STATES_GRACEFUL.contains(_state);
        }
    }

    /**
     * Is a final shutdown in progress? This may not be cancelled.
     * @since 0.8.12
     */
    public boolean isFinalShutdownInProgress() {
        synchronized(_stateLock) {
            return STATES_FINAL.contains(_state);
        }
    }

    ////////// end state management

    /**
     * Rebuild and republish our routerInfo since something significant 
     * has changed.
     * This is a non-blocking rebuild.
     *
     * Not for external use.
     *
     * Warning - risk of deadlock - do not call while holding locks
     *
     */
    public void rebuildRouterInfo() { rebuildRouterInfo(false); }

    /**
     * Rebuild and republish our routerInfo since something significant 
     * has changed.
     * Not for external use.
     *
     *  Warning - risk of deadlock - do not call while holding locks
     *
     * @param blockingRebuild ignored, always nonblocking
     */
    public void rebuildRouterInfo(boolean blockingRebuild) {
        if (_log.shouldLog(Log.INFO))
            _log.info("Rebuilding new routerInfo, publish inline? " + blockingRebuild, new Exception("I did it"));
        _routerInfoLock.writeLock().lock();
        try {
            locked_rebuildRouterInfo(blockingRebuild);
        } finally {
            _routerInfoLock.writeLock().unlock();
        }
    }
        

    /**
     * Rebuild and republish our routerInfo since something significant 
     * has changed.
     *
     * @param blockingRebuild ignored, always nonblocking
     */
    private void locked_rebuildRouterInfo(boolean blockingRebuild) {
        RouterInfo ri;
        if (_routerInfo != null)
            ri = new RouterInfo(_routerInfo);
        else
            ri = new RouterInfo();
        
        try {
            ri.setPublished(_context.clock().now());
            Properties stats = _context.statPublisher().publishStatistics();
            
            ri.setOptions(stats);
            // deadlock thru createAddresses() thru SSU REA... move outside lock?
            ri.setAddresses(_context.commSystem().createAddresses());

            SigningPrivateKey key = _context.keyManager().getSigningPrivateKey();
            if (key == null) {
                _log.log(Log.CRIT, "Internal error - signing private key not known? Impossible?");
                return;
            }
            ri.sign(key);
            setRouterInfo(ri);
            if (!ri.isValid())
                throw new DataFormatException("Our RouterInfo has a bad signature");
            Republish r = new Republish(_context);
            // Avoid deadlocks part 1000
            //if (blockingRebuild)
            //    r.timeReached();
            //else
                _context.simpleTimer2().addEvent(r, 0);
        } catch (DataFormatException dfe) {
            _log.log(Log.CRIT, "Internal error - unable to sign our own address?!", dfe);
        }
    }

    /**
     *  Family Key Crypto Signer / Verifier.
     *  Not for external use.
     *  If family key is set, first call Will take a while to generate keys.
     *  Warning - risk of deadlock - do not call while holding locks
     *  (other than routerInfoLock)
     *
     *  @return null on initialization failure
     *  @since 0.9.24
     */
    public FamilyKeyCrypto getFamilyKeyCrypto() {
        synchronized (_familyKeyLock) {
            if (_familyKeyCrypto == null) {
                if (!_familyKeyCryptoFail) {
                    try {
                        _familyKeyCrypto = new FamilyKeyCrypto(_context);
                    } catch (GeneralSecurityException gse) {
                        _log.error("Failed to initialize family key crypto", gse);
                        _familyKeyCryptoFail = true;
                    }
                }
            }
        }
        return _familyKeyCrypto;
    }

    // publicize our ballpark capacity
    public static final char CAPABILITY_BW12 = 'K';
    public static final char CAPABILITY_BW32 = 'L';
    public static final char CAPABILITY_BW64 = 'M';
    public static final char CAPABILITY_BW128 = 'N';
    public static final char CAPABILITY_BW256 = 'O';
    /** @since 0.9.18 */
    public static final char CAPABILITY_BW512 = 'P';
    /** @since 0.9.18 */
    public static final char CAPABILITY_BW_UNLIMITED = 'X';
    /** for testing */
    public static final String PROP_FORCE_BWCLASS = "router.forceBandwidthClass";
    
    public static final char CAPABILITY_REACHABLE = 'R';
    public static final char CAPABILITY_UNREACHABLE = 'U';
    /** for testing */
    public static final String PROP_FORCE_UNREACHABLE = "router.forceUnreachable";

    /** @deprecated unused */
    @Deprecated
    public static final char CAPABILITY_NEW_TUNNEL = 'T';
    
    /** In binary (1024) Kbytes
     *  @since 0.9.33 */
    public static final int MIN_BW_K = 0;
    /** In binary (1024) Kbytes
     *  @since 0.9.33 */
    public static final int MIN_BW_L = 12;
    /** In binary (1024) Kbytes
     *  @since 0.9.33 */
    public static final int MIN_BW_M = 48;
    /** In binary (1024) Kbytes
     *  @since 0.9.33 */
    public static final int MIN_BW_N = 64;
    /** In binary (1024) Kbytes
     *  @since 0.9.33 */
    public static final int MIN_BW_O = 128;
    /** In binary (1024) Kbytes
     *  @since 0.9.33 */
    public static final int MIN_BW_P = 256;
    /** In binary (1024) Kbytes
     *  @since 0.9.33 */
    public static final int MIN_BW_X = 2000;

    /**
     *  The current bandwidth class.
     *  For building our RI. Not for external use.
     *
     *  @return a character to be added to the RI, one of "KLMNOPX"
     *  @since 0.9.31
     */
    public char getBandwidthClass() {
        int bwLim = Math.min(_context.bandwidthLimiter().getInboundKBytesPerSecond(),
                             _context.bandwidthLimiter().getOutboundKBytesPerSecond());
        bwLim = (int)(bwLim * getSharePercentage());
        
        String force = _context.getProperty(PROP_FORCE_BWCLASS);
        if (force != null && force.length() > 0) {
            return force.charAt(0);
        } else if (bwLim < MIN_BW_L) {
            return CAPABILITY_BW12;
        } else if (bwLim <= MIN_BW_M) {
            return CAPABILITY_BW32;
        } else if (bwLim <= MIN_BW_N) {
            return CAPABILITY_BW64;
        } else if (bwLim <= MIN_BW_O) {
            return CAPABILITY_BW128;
        } else if (bwLim <= MIN_BW_P) {
            return CAPABILITY_BW256;
        } else if (bwLim <= MIN_BW_X) {    // TODO adjust threshold
            // 512 supported as of 0.9.18;
            return CAPABILITY_BW512;
        } else {
            // Unlimited supported as of 0.9.18;
            return CAPABILITY_BW_UNLIMITED;
        }
    }
        
    /**
     *  For building our RI. Not for external use.
     *
     *  @return a capabilities string to be added to the RI
     */
    public String getCapabilities() {
        StringBuilder rv = new StringBuilder(4);
        boolean hidden = isHidden();
        char bw = hidden ? CAPABILITY_BW32 : getBandwidthClass();
        rv.append(bw);
        // 512 and unlimited supported as of 0.9.18;
        // Add 256 as well for compatibility
        //if (bw == CAPABILITY_BW512 || bw == CAPABILITY_BW_UNLIMITED)
        //    rv.append(CAPABILITY_BW256);

        // if prop set to true, don't tell people we are ff even if we are
        if (_context.netDb().floodfillEnabled() &&
            !_context.getBooleanProperty("router.hideFloodfillParticipant"))
            rv.append(FloodfillNetworkDatabaseFacade.CAPABILITY_FLOODFILL);
        
        if(_context.getBooleanProperty(PROP_HIDDEN))
            rv.append(RouterInfo.CAPABILITY_HIDDEN);
        
        if (hidden || _context.getBooleanProperty(PROP_FORCE_UNREACHABLE)) {
            rv.append(CAPABILITY_UNREACHABLE);
            return rv.toString();
        }
        switch (_context.commSystem().getStatus()) {
            case OK:
            case IPV4_OK_IPV6_UNKNOWN:
            case IPV4_OK_IPV6_FIREWALLED:
            case IPV4_FIREWALLED_IPV6_OK:
            case IPV4_DISABLED_IPV6_OK:
            case IPV4_UNKNOWN_IPV6_OK:
            case IPV4_SNAT_IPV6_OK:
                rv.append(CAPABILITY_REACHABLE);
                break;

            case DIFFERENT:
            case REJECT_UNSOLICITED:
            case IPV4_DISABLED_IPV6_FIREWALLED:
                rv.append(CAPABILITY_UNREACHABLE);
                break;

            case DISCONNECTED:
            case HOSED:
            case UNKNOWN:
            case IPV4_UNKNOWN_IPV6_FIREWALLED:
            case IPV4_DISABLED_IPV6_UNKNOWN:
            case IPV4_FIREWALLED_IPV6_UNKNOWN:
            case IPV4_SNAT_IPV6_UNKNOWN:
            default:
                // no explicit capability
                break;
        }
        return rv.toString();
    }
    
    /*
     *  This checks the config only. We don't check the current RI
     *  due to deadlocks.
     *
     */
    public boolean isHidden() {
        //RouterInfo ri;
        //synchronized (_routerInfoLock) {
        //    ri = _routerInfo;
        //}
        //if ( (ri != null) && (ri.isHidden()) )
        //    return true;
        if (_context.getBooleanProperty(PROP_HIDDEN))
            return true;
        String h = _context.getProperty(PROP_HIDDEN_HIDDEN);
        if (h != null)
            return Boolean.parseBoolean(h);
        return _context.commSystem().isInStrictCountry();
    }
    
    /**
     *  @since 0.9.3
     */
    public EventLog eventLog() {
        return _eventLog;
    }
    
    /**
     * Ugly list of files that we need to kill if we are building a new identity
     *
     */
    private static final String _rebuildFiles[] = new String[] {
        CreateRouterInfoJob.INFO_FILENAME,
        CreateRouterInfoJob.KEYS_FILENAME,
        CreateRouterInfoJob.KEYS2_FILENAME,
        "netDb/my.info",      // no longer used
        "connectionTag.keys", // never used?
        KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PRIVATE_ENC,
        KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PUBLIC_ENC,
        KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PRIVATE_SIGNING,
        KeyManager.DEFAULT_KEYDIR + '/' + KeyManager.KEYFILE_PUBLIC_SIGNING,
        "sessionKeys.dat"     // no longer used
    };

    /**
     *  Not for external use.
     */
    public void killKeys() {
        //new Exception("Clearing identity files").printStackTrace();
        for (int i = 0; i < _rebuildFiles.length; i++) {
            File f = new File(_context.getRouterDir(),_rebuildFiles[i]);
            if (f.exists()) {
                boolean removed = f.delete();
                if (removed) {
                    System.out.println("INFO:  Removing old identity file: " + _rebuildFiles[i]);
                } else {
                    System.out.println("ERROR: Could not remove old identity file: " + _rebuildFiles[i]);
                }
            }
        }

        // now that we have random ports, keeping the same port would be bad
        synchronized(_configFileLock) {
            removeConfigSetting(UDPTransport.PROP_INTERNAL_PORT);
            removeConfigSetting(UDPTransport.PROP_EXTERNAL_PORT);
            removeConfigSetting(UDPTransport.PROP_INTRO_KEY);
            removeConfigSetting(NTCPTransport.PROP_I2NP_NTCP_PORT);
            removeConfigSetting(NTCPTransport.PROP_NTCP2_SP);
            removeConfigSetting(NTCPTransport.PROP_NTCP2_IV);
            removeConfigSetting(PROP_IB_RANDOM_KEY);
            removeConfigSetting(PROP_OB_RANDOM_KEY);
            removeConfigSetting(PROP_REBUILD_KEYS);
            saveConfig();
        }
    }

    /**
     * Rebuild a new identity the hard way - delete all of our old identity 
     * files, then reboot the router.
     *
     *  Calls exit(), never returns.
     *
     *  Not for external use.
     */
    public synchronized void rebuildNewIdentity() {
        if (_shutdownHook != null) {
            try {
                Runtime.getRuntime().removeShutdownHook(_shutdownHook);
            } catch (IllegalStateException ise) {}
        }
        killKeys();
        for (Runnable task : _context.getShutdownTasks()) {
            if (_log.shouldLog(Log.WARN))
                _log.warn("Running shutdown task " + task.getClass());
            try {
                task.run();
            } catch (Throwable t) {
                _log.log(Log.CRIT, "Error running shutdown task", t);
            }
        }
        _context.removeShutdownTasks();
        // hard and ugly
        if (_context.hasWrapper())
            _log.log(Log.CRIT, "Restarting with new router identity");
        else
            _log.log(Log.CRIT, "Shutting down because old router identity was invalid - restart I2P");
        finalShutdown(EXIT_HARD_RESTART);
    }
    
    /**
     *  Could block for 10 seconds or forever
     */
    private void warmupCrypto() {
        String oldLoaded = _context.getProperty(PROP_JBIGI);
        String oldProcessor = _context.getProperty(PROP_JBIGI_PROCESSOR);
        String processor = null;
        if (SystemVersion.isX86()) {
            // Check to see if processor changed since last time we ran,
            // or if we detected a different processor model due to CPUID code changes,
            // or if we changed 32/64 bit.
            // If so, delete the old jbigi.so file, to protect against a JVM crash.
            // We do this before calling elGamalEngine(), which calls NBI.
            // We take care not to access the NBI class yet.
            try {
                processor = CPUID.getInfo().getCPUModelString();
                if (SystemVersion.is64Bit())
                    processor += "/64";
                if (oldProcessor != null && !oldProcessor.equals(processor)) {
                    // delete old so file
                    boolean isWin = SystemVersion.isWindows();
                    boolean isMac = SystemVersion.isMac();
                    String osName = System.getProperty("os.name").toLowerCase(Locale.US);
                    // only do this on these OSes
                    boolean goodOS = isWin || isMac ||
                                     osName.contains("linux") || osName.contains("freebsd");
                    File jbigiJar = new File(_context.getLibDir(), "jbigi.jar");
                    if (goodOS && jbigiJar.exists() && _context.getBaseDir().canWrite()) {
                        String libPrefix = isWin ? "" : "lib";
                        String libSuffix = isWin ? ".dll" : isMac ? ".jnilib" : ".so";
                        File jbigiLib = new File(_context.getBaseDir(), libPrefix + "jbigi" + libSuffix);
                        if (jbigiLib.canWrite()) {
                            String path = jbigiLib.getAbsolutePath();
                            boolean success = FileUtil.copy(path, path + ".bak", true, true);
                            if (success) {
                                success = jbigiLib.delete();
                                if (success) {
                                    System.out.println("Processor change detected, moved jbigi library to " +
                                                       path + ".bak");
                                    System.out.println("Check logs for successful installation of new library");
                                }
                            }
                        }
                    }
                }
            } catch (UnknownCPUException e) {}
        }
        _context.random().nextBoolean();
        // Instantiate to fire up the YK refiller thread
        _context.elGamalEngine();
        String loaded = NativeBigInteger.getLoadedResourceName();
        Map<String, String> changes = null;
        if (loaded != null && !loaded.equals(oldLoaded)) {
            changes = new HashMap<String, String>(2);
            changes.put(PROP_JBIGI, loaded);
        }
        if (processor != null && !processor.equals(oldProcessor)) {
            if (changes == null)
                changes = new HashMap<String, String>(1);
            changes.put(PROP_JBIGI_PROCESSOR, processor);
        }
        if (changes != null)
            saveConfig(changes, null);
    }
    
    /** shut down after all tunnels are gone */
    public static final int EXIT_GRACEFUL = 2;
    /** shut down immediately */
    public static final int EXIT_HARD = 3;
    /** shut down immediately */
    public static final int EXIT_OOM = 10;
    /** shut down immediately, and tell the wrapper to restart */
    public static final int EXIT_HARD_RESTART = 4;
    /** shut down after all tunnels are gone, and tell the wrapper to restart */
    public static final int EXIT_GRACEFUL_RESTART = 5;
    
    /**
     *  Shutdown with no chance of cancellation.
     *  Blocking, will call exit() and not return unless setKillVMOnExit(false) was previously called,
     *  or a final shutdown is already in progress.
     *  May take several seconds as it runs all the shutdown hooks.
     *
     *  @param exitCode one of the EXIT_* values, non-negative
     *  @throws IllegalArgumentException if exitCode negative
     */
    public synchronized void shutdown(int exitCode) {
        if (exitCode < 0)
            throw new IllegalArgumentException();
        synchronized(_stateLock) {
            if (_state == State.FINAL_SHUTDOWN_1 ||
                _state == State.FINAL_SHUTDOWN_2 ||
                _state == State.FINAL_SHUTDOWN_3 ||
                _state == State.STOPPED)
                return;
            changeState(State.FINAL_SHUTDOWN_1);
        }
        _context.throttle().setShutdownStatus();
        if (_shutdownHook != null) {
            try {
                Runtime.getRuntime().removeShutdownHook(_shutdownHook);
            } catch (IllegalStateException ise) {}
        }
        shutdown2(exitCode);
    }

    /**
     *  Cancel the JVM runtime hook before calling this.
     *  Called by the ShutdownHook.
     *  NOT to be called by others, use shutdown().
     *
     *  @param exitCode one of the EXIT_* values, non-negative
     *  @throws IllegalArgumentException if exitCode negative
     */
    public synchronized void shutdown2(int exitCode) {
        if (exitCode < 0)
            throw new IllegalArgumentException();
        changeState(State.FINAL_SHUTDOWN_2);
        // help us shut down esp. after OOM
        int priority = (exitCode == EXIT_OOM) ? Thread.MAX_PRIORITY - 1 : Thread.NORM_PRIORITY + 2;
        Thread.currentThread().setPriority(priority);
        if (exitCode == EXIT_HARD_RESTART || exitCode == EXIT_GRACEFUL_RESTART)
            _log.log(Log.CRIT, _t("Restart imminent"));
        else
            _log.log(Log.CRIT, _t("Shutdown imminent"));
        // So we can get all the way to the end
        // No, you can't do Thread.currentThread.setDaemon(false)
        if (_killVMOnEnd) {
            try {
                (new Spinner()).start();
            } catch (Throwable t) {}
        }
        ((RouterClock) _context.clock()).removeShiftListener(this);
        _context.random().saveSeed();
        I2PThread.removeOOMEventListener(_oomListener);
        // Run the shutdown hooks first in case they want to send some goodbye messages
        // Maybe we need a delay after this too?
        LinkedList<Thread> tasks = new LinkedList<Thread>();
        for (Runnable task : _context.getShutdownTasks()) {
            //System.err.println("Running shutdown task " + task.getClass());
            if (_log.shouldLog(Log.WARN))
                _log.warn("Running shutdown task " + task.getClass());
            try {
                //task.run();
                Thread t = new I2PAppThread(task, "Shutdown task " + task.getClass().getName());
                t.setDaemon(true);
                t.start();
                tasks.add(t);
            } catch (Throwable t) {
                _log.log(Log.CRIT, "Error running shutdown task", t);
            }
        }
        long waitSecs = SHUTDOWN_WAIT_SECS;
        if (SystemVersion.isARM())
            waitSecs *= 2;
        final long maxWait = System.currentTimeMillis() + (waitSecs *1000);
        Thread th;
        while ((th = tasks.poll()) != null) {
            long toWait = maxWait - System.currentTimeMillis();
            if (toWait <= 0) {
                _log.logAlways(Log.WARN, "Shutdown tasks took more than " + waitSecs + " seconds to run");
                tasks.clear();
                break;
            }
            try {
                th.join(toWait);
            } catch (InterruptedException ie) {}
            if (th.isAlive()) {
                _log.logAlways(Log.WARN, "Shutdown task took more than " + waitSecs + " seconds to run: " + th.getName());
                tasks.clear();
                break;
            } else if (_log.shouldInfo()) {
                _log.info("Shutdown task complete: " + th.getName());
            }
        }

        // Set the last version to the current version, since 0.8.13
        if (!RouterVersion.VERSION.equals(_config.get("router.previousVersion"))) {
            saveConfig("router.previousVersion", RouterVersion.VERSION);
        }

        _context.removeShutdownTasks();

        // All in-JVM clients should be gone by now,
        // unless they are stuck waiting for tunnels.
        // If we have any of those, or external clients,
        // we will wait below for the I2CP disconnect messages to get to them.
        boolean waitForClients = _killVMOnEnd && !_context.clientManager().listClients().isEmpty();
        if (_log.shouldWarn())
            _log.warn("Stopping ClientManager");
        try { _context.clientManager().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the client manager", t); }
        if (waitForClients) {
            // Give time for the disconnect messages to get to them
            // so they can shut down correctly before the JVM goes away
            try { Thread.sleep(1500); } catch (InterruptedException ie) {}
            if (_log.shouldWarn())
                _log.warn("Done waiting for clients to disconnect");
        }

        try { _context.namingService().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the naming service", t); }
        try { _context.jobQueue().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the job queue", t); }
        try { _context.tunnelManager().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the tunnel manager", t); }
        try { _context.tunnelDispatcher().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the tunnel dispatcher", t); }
        try { _context.netDb().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the networkDb", t); }
        try { _context.commSystem().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the comm system", t); }
        try { _context.bandwidthLimiter().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the comm system", t); }
        try { _context.peerManager().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the peer manager", t); }
        try { _context.messageRegistry().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the message registry", t); }
        try { _context.messageValidator().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the message validator", t); }
        try { _context.inNetMessagePool().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the inbound net pool", t); }
        try { _context.clientMessagePool().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the client msg pool", t); }
        try { _context.sessionKeyManager().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the session key manager", t); }
        try { _context.eciesEngine().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the ECIES engine", t); }
        try { _context.messageHistory().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the message history logger", t); }
        // do stat manager last to reduce chance of NPEs in other threads
        try { _context.statManager().shutdown(); } catch (Throwable t) { _log.error("Error shutting down the stats manager", t); }
        _context.deleteTempDir();
        List<RouterContext> contexts = RouterContext.getContexts();
        contexts.remove(_context);

        // shut down I2PAppContext tasks here

        try {
            _context.elGamalEngine().shutdown();
        } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting elGamal", t); }

        if (contexts.isEmpty()) {
            // any thing else to shut down?
        } else {
            _log.logAlways(Log.WARN, "Warning - " + contexts.size() + " routers remaining in this JVM, not releasing all resources");
        }
        try {
            ((FortunaRandomSource)_context.random()).shutdown();
        } catch (Throwable t) { _log.log(Log.CRIT, "Error shutting random()", t); }

        // logManager shut down in finalShutdown()
        _watchdog.shutdown();
        _watchdogThread.interrupt();
        _eventLog.addEvent(EventLog.STOPPED, Integer.toString(exitCode));
        finalShutdown(exitCode);
    }

    /**
     * disable dynamic key functionality for the moment, as it may be harmful and doesn't
     * add meaningful anonymity
     */
    private static final boolean ALLOW_DYNAMIC_KEYS = false;

    /**
     *  Cancel the JVM runtime hook before calling this.
     *
     *  @param exitCode one of the EXIT_* values, non-negative
     */
    private synchronized void finalShutdown(int exitCode) {
        changeState(State.FINAL_SHUTDOWN_3);
        clearCaches();
        _log.log(Log.CRIT, "Shutdown(" + exitCode + ") complete"  /* , new Exception("Shutdown") */ );
        try { _context.logManager().shutdown(); } catch (Throwable t) { }
        if (ALLOW_DYNAMIC_KEYS) {
            if (_context.getBooleanProperty(PROP_DYNAMIC_KEYS))
                killKeys();
        }

        if (!SystemVersion.isAndroid()) {
            File f = getPingFile();
            f.delete();
        }

        // Only do this on Android. On desktop, rogue threads
        // may create a new I2PAppContext before the JVM stops
        // if we delete this one.
        //if (RouterContext.getContexts().isEmpty())
        if (SystemVersion.isAndroid())
            RouterContext.killGlobalContext();

        // Since 0.8.8, for Android and the wrapper
        for (Runnable task : _context.getFinalShutdownTasks()) {
            //System.err.println("Running final shutdown task " + task.getClass());
            try {
                task.run();
            } catch (Throwable t) {
                System.err.println("Running final shutdown task " + t);
            }
        }
        _context.getFinalShutdownTasks().clear();

        if (_killVMOnEnd) {
            try { Thread.sleep(1000); } catch (InterruptedException ie) {}
            //Runtime.getRuntime().halt(exitCode);
            // allow the Runtime shutdown hooks to execute
            Runtime.getRuntime().exit(exitCode);
        } else if (SystemVersion.isAndroid()) {
            Runtime.getRuntime().gc();
        }
        changeState(State.STOPPED);
    }
    
    /**
     * Non-blocking shutdown.
     *
     * Call this if we want the router to kill itself as soon as we aren't 
     * participating in any more tunnels (etc).  This will not block and doesn't
     * guarantee any particular time frame for shutting down.  To shut the 
     * router down immediately, use {@link #shutdown}.  If you want to cancel
     * the graceful shutdown (prior to actual shutdown ;), call 
     * {@link #cancelGracefulShutdown}.
     *
     * Exit code will be EXIT_GRACEFUL.
     *
     * Shutdown delay will be from zero to 11 minutes.
     */
    public void shutdownGracefully() {
        shutdownGracefully(EXIT_GRACEFUL);
    }

    /**
     * Non-blocking shutdown.
     *
     * Call this with EXIT_HARD or EXIT_HARD_RESTART for a non-blocking,
     * hard, non-graceful shutdown with a brief delay to allow a UI response
     *
     * Returns silently if a final shutdown is already in progress.
     *
     * @param exitCode one of the EXIT_* values, non-negative
     * @throws IllegalArgumentException if exitCode negative
     */
    public void shutdownGracefully(int exitCode) {
        if (exitCode < 0)
            throw new IllegalArgumentException();
        synchronized(_stateLock) {
            if (isFinalShutdownInProgress())
                return; // too late
            changeState(State.GRACEFUL_SHUTDOWN);
            _gracefulExitCode = exitCode;
        }
        //_config.put(PROP_SHUTDOWN_IN_PROGRESS, "true");
        _context.throttle().setShutdownStatus();
        synchronized (_gracefulShutdownDetector) {
            _gracefulShutdownDetector.notifyAll();
        }
    }
    
    /**
     * Cancel any prior request to shut the router down gracefully.
     *
     * Returns silently if a final shutdown is already in progress.
     */
    public void cancelGracefulShutdown() {
        synchronized(_stateLock) {
            if (isFinalShutdownInProgress())
                return; // too late
            changeState(State.RUNNING);
            _gracefulExitCode = -1;
        }
        //_config.remove(PROP_SHUTDOWN_IN_PROGRESS);
        _context.throttle().cancelShutdownStatus();
        synchronized (_gracefulShutdownDetector) {
            _gracefulShutdownDetector.notifyAll();
        }        
    }

    /**
     * What exit code do we plan on using when we shut down (or -1, if there isn't a graceful shutdown planned)
     *
     * @return one of the EXIT_* values or -1
     */
    public int scheduledGracefulExitCode() {
        synchronized(_stateLock) {
            return _gracefulExitCode;
        }
    }

    /**
     *  How long until the graceful shutdown will kill us?
     *  @return -1 if no shutdown in progress.
     */
    public long getShutdownTimeRemaining() {
        synchronized(_stateLock) {
            if (_gracefulExitCode <= 0) return -1; // maybe Long.MAX_VALUE would be better?
            if (_gracefulExitCode == EXIT_HARD || _gracefulExitCode == EXIT_HARD_RESTART)
                return 0;
        }
        long exp = _context.tunnelManager().getLastParticipatingExpiration();
        if (exp < 0)
            return 0;
        else
            return Math.max(0, exp + 2*CLOCK_FUDGE_FACTOR - _context.clock().now());
    }
    
    /**
     * Save the current config options (returning true if save was 
     * successful, false otherwise)
     *
     * Synchronized with file read in getConfig()
     */
    public boolean saveConfig() {
        try {
            Properties ordered = new OrderedProperties();
            synchronized(_configFileLock) {
                ordered.putAll(_config);
                DataHelper.storeProps(ordered, new File(_configFilename));
            }
        } catch (IOException ioe) {
                // warning, _log will be null when called from constructor
                if (_log != null)
                    _log.error("Error saving the config to " + _configFilename, ioe);
                else
                    System.err.println("Error saving the config to " + _configFilename + ": " + ioe);
                return false;
        }
        return true;
    }
    
    /**
     * Updates the current config with the given key/value and then saves it.
     * Prevents a race in the interval between setConfigSetting() / removeConfigSetting() and saveConfig(),
     * Synchronized with getConfig() / saveConfig()
     *
     * @param name setting to add/change/remove before saving
     * @param value if non-null, updated value; if null, setting will be removed
     * @return success
     * @since 0.8.13
     */
    public boolean saveConfig(String name, String value) {
        synchronized(_configFileLock) {
            if (value != null)
                _config.put(name, value);
            else
                removeConfigSetting(name);
            return saveConfig();
        }
    }

    /**
     * Updates the current config and then saves it.
     * Prevents a race in the interval between setConfigSetting() / removeConfigSetting() and saveConfig(),
     * Synchronized with getConfig() / saveConfig()
     *
     * @param toAdd settings to add/change before saving, may be null or empty
     * @param toRemove settings to remove before saving, may be null or empty
     * @return success
     * @since 0.8.13
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public boolean saveConfig(Map toAdd, Collection<String> toRemove) {
        synchronized(_configFileLock) {
            if (toAdd != null)
                _config.putAll(toAdd);
            if (toRemove != null) {
                for (String s : toRemove) {
                    removeConfigSetting(s);
                }
            }
            return saveConfig();
        }
    }

    /**
     *  The clock shift listener.
     *  Restart the router if we should.
     *
     *  @since 0.8.8
     */
    public void clockShift(long delta) {
        if (delta > -60*1000 && delta < 60*1000)
            return;
        synchronized(_stateLock) {
            if (gracefulShutdownInProgress() || !isAlive())
                return;
        }
        _eventLog.addEvent(EventLog.CLOCK_SHIFT, Long.toString(delta));
        // update the routing key modifier
        _context.routerKeyGenerator().generateDateBasedModData();
        // Commented because this check makes no sense (#1014)
        // .. and 'active' is relative to our broken time.
        //if (_context.commSystem().countActivePeers() <= 0)
        //    return;
        if (delta > 0)
            _log.error("Restarting after large clock shift forward by " + DataHelper.formatDuration(delta));
        else
            _log.error("Restarting after large clock shift backward by " + DataHelper.formatDuration(0 - delta));
        restart();
    }

    /**
     *  A "soft" restart, primarily of the comm system, after
     *  a port change or large step-change in system time.
     *  Does not stop the whole JVM, so it is safe even in the absence
     *  of the wrapper.
     *  This is not a graceful restart - all peer connections are dropped immediately.
     *
     *  As of 0.8.8, this returns immediately and does the actual restart in a separate thread.
     *  Poll isAlive() if you need to know when the restart is complete.
     *
     *  Not recommended for external use.
     */
    public synchronized void restart() {
        synchronized(_stateLock) {
            if (gracefulShutdownInProgress() || !isAlive())
                return;
            changeState(State.RESTARTING);
        }
        ((RouterClock) _context.clock()).removeShiftListener(this);
        // Let's not stop accepting tunnels, etc
        //_started = _context.clock().now();
        synchronized(_configFileLock) {
            _downtime = 1;
        }
        Thread t = new I2PThread(new Restarter(_context), "Router Restart");
        t.setPriority(Thread.NORM_PRIORITY + 1);
        t.start();
    }    

    /**
     *  Usage: Router [rebuild]
     *  No other options allowed, for now
     *  Instantiates Router(), and either installs updates and exits,
     *  or calls runRouter().
     *
     *  Not recommended for embedded use.
     *  Applications bundling I2P should instantiate a Router and call runRouter().
     *
     *  @param args null ok
     *  @throws IllegalArgumentException
     */
    public static void main(String args[]) {
        boolean rebuild = false;
        if (args != null) {
            boolean error = false;
            Getopt g = new Getopt("router", args, "");
            int c;
            while ((c = g.getopt()) != -1) {
                switch (c) {
                    default:
                        error = true;
                }
            }
            int remaining = args.length - g.getOptind();
            if (remaining > 1) {
                error = true;
            } else if (remaining == 1) {
                rebuild = args[g.getOptind()].equals("rebuild");
                if (!rebuild)
                    error = true;
            }
            if (error)
                throw new IllegalArgumentException();
        }

        System.out.println("Starting I2P " + RouterVersion.FULL_VERSION);
        //verifyWrapperConfig();
        Router r;
        try {
            r = new Router();
        } catch (IllegalStateException ise) {
            System.exit(-1);
            return;
        }
        if (rebuild) {
            r.rebuildNewIdentity();
        } else {
            // This is here so that we can get the directory location from the context
            // for the zip file and the base location to unzip to.
            // If it does an update, it never returns.
            // I guess it's better to have the other-router check above this, we don't want to
            // overwrite an existing running router's jar files. Other than ours.
            InstallUpdate.installUpdates(r);
            // *********  Start no threads before here ********* //
            r.runRouter();
        }
    }

/*******
    private static void verifyWrapperConfig() {
        File cfgUpdated = new File("wrapper.config.updated");
        if (cfgUpdated.exists()) {
            cfgUpdated.delete();
            System.out.println("INFO: Wrapper config updated, but the service wrapper requires you to manually restart");
            System.out.println("INFO: Shutting down the router - please rerun it!");
            System.exit(EXIT_HARD);
        }
    }
*******/
    
/*
    private static String getPingFile(Properties envProps) {
        if (envProps != null) 
            return envProps.getProperty("router.pingFile", "router.ping");
        else
            return "router.ping";
    }
*/
    private File getPingFile() {
        String s = _context.getProperty("router.pingFile", "router.ping");
        File f = new File(s);
        if (!f.isAbsolute())
            f = new File(_context.getPIDDir(), s);
        return f;
    }
    
    private static final long LIVELINESS_DELAY = 60*1000;
    
    /** 
     * Check the file "router.ping", but if 
     * that file already exists and was recently written to, return false as there is
     * another instance running.
     * 
     * @return true if the router is the only one running 
     * @since 0.8.2
     */
    private boolean isOnlyRouterRunning() {
        File f = getPingFile();
        if (f.exists()) {
            long lastWritten = f.lastModified();
            long downtime = System.currentTimeMillis() - lastWritten;
            synchronized(_configFileLock) {
                if (downtime > 0 && _downtime < 0)
                    _downtime = downtime;
            }
            if (downtime > LIVELINESS_DELAY) {
                System.err.println("WARN: Old router was not shut down gracefully, deleting " + f);
                f.delete();
                if (lastWritten > 0)
                    _eventLog.addEvent(EventLog.CRASHED,
                                       Translate.getString("{0} ago", DataHelper.formatDuration2(downtime), _context, BUNDLE_NAME));
            } else {
                return false;
            }
        }
        return true;
    }

    /** 
     * Start a thread that will periodically update the file "router.ping".
     * isOnlyRouterRunning() MUST have been called previously.
     */
    private void beginMarkingLiveliness() {
        File f = getPingFile();
        _context.simpleTimer2().addPeriodicEvent(new MarkLiveliness(this, f), 0, LIVELINESS_DELAY - (5*1000));
    }
    
    /** 
     *  How long this router was down before it started, or 0 if unknown.
     *
     *  This may be used for a determination of whether to regenerate keys, for example.
     *  We use the timestamp of the previous ping file left behind on crash,
     *  as set by isOnlyRouterRunning(), if present.
     *  Otherwise, the last STOPPED entry in the event log.
     *
     *  May take a while to run the first time, if it has to go through the event log.
     *  Once called, the result is cached.
     *
     *  @since 0.9.47
     */
    public long getEstimatedDowntime() {
        synchronized(_configFileLock) {
            if (_downtime >= 0)
                return _downtime;
            long begin = System.currentTimeMillis();
            long stopped = _eventLog.getLastEvent(EventLog.STOPPED, _context.clock().now() - 365*24*60*60*1000L);
            long downtime = stopped > 0 ? _started - stopped : 0;
            if (downtime < 0)
                downtime = 0;
            if (_log.shouldWarn())
                _log.warn("Downtime was " + DataHelper.formatDuration(downtime) +
                          "; calculation took " + DataHelper.formatDuration(System.currentTimeMillis() - begin));
            _downtime = downtime;
            return downtime;
        }
    }
    
    /** 
     *  Only for soft restart. Not for external use.
     *
     *  @since 0.9.47
     */
    public void setEstimatedDowntime(long downtime) {
        if (downtime <= 0)
            downtime = 1;
        synchronized(_configFileLock) {
            _downtime = downtime;
        }
    }

    public static final String PROP_BANDWIDTH_SHARE_PERCENTAGE = "router.sharePercentage";
    public static final int DEFAULT_SHARE_PERCENTAGE = 80;
    
    /** 
     * What fraction of the bandwidth specified in our bandwidth limits should
     * we allow to be consumed by participating tunnels?
     * @return a number less than one, not a percentage!
     *
     */
    public double getSharePercentage() {
        String pct = _context.getProperty(PROP_BANDWIDTH_SHARE_PERCENTAGE);
        if (pct != null) {
            try {
                double d = Double.parseDouble(pct);
                if (d > 1)
                    return d/100d; // *cough* sometimes its 80 instead of .8 (!stab jrandom)
                else
                    return d;
            } catch (NumberFormatException nfe) {
                if (_log.shouldLog(Log.INFO))
                    _log.info("Unable to get the share percentage");
            }
        }
        return DEFAULT_SHARE_PERCENTAGE / 100.0d;
    }

    /**
     *  Max of inbound and outbound rate in bytes per second
     */
    public int get1sRate() { return get1sRate(false); }

    /**
     *  When outboundOnly is false, outbound rate in bytes per second.
     *  When true, max of inbound and outbound rate in bytes per second.
     */
    public int get1sRate(boolean outboundOnly) {
            FIFOBandwidthLimiter bw = _context.bandwidthLimiter();
                int out = (int)bw.getSendBps();
                if (outboundOnly)
                    return out;
                return (int)Math.max(out, bw.getReceiveBps());
    }

    /**
     *  Inbound rate in bytes per second
     */
    public int get1sRateIn() {
            FIFOBandwidthLimiter bw = _context.bandwidthLimiter();
                return (int) bw.getReceiveBps();
    }

    /**
     *  Max of inbound and outbound rate in bytes per second
     */
    public int get15sRate() { return get15sRate(false); }

    /**
     *  When outboundOnly is false, outbound rate in bytes per second.
     *  When true, max of inbound and outbound rate in bytes per second.
     */
    public int get15sRate(boolean outboundOnly) {
            FIFOBandwidthLimiter bw = _context.bandwidthLimiter();
                int out = (int)bw.getSendBps15s();
                if (outboundOnly)
                    return out;
                return (int)Math.max(out, bw.getReceiveBps15s());
    }

    /**
     *  Inbound rate in bytes per second
     */
    public int get15sRateIn() {
            FIFOBandwidthLimiter bw = _context.bandwidthLimiter();
                return (int) bw.getReceiveBps15s();
    }

    /**
     *  Max of inbound and outbound rate in bytes per second
     */
    public int get1mRate() { return get1mRate(false); }

    /**
     *  When outboundOnly is false, outbound rate in bytes per second.
     *  When true, max of inbound and outbound rate in bytes per second.
     */
    public int get1mRate(boolean outboundOnly) {
        int send = 0;
        StatManager mgr = _context.statManager();
        RateStat rs = mgr.getRate("bw.sendRate");
        if (rs != null)
            send = (int)rs.getRate(1*60*1000).getAverageValue();
        if (outboundOnly)
            return send;
        int recv = 0;
        rs = mgr.getRate("bw.recvRate");
        if (rs != null)
            recv = (int)rs.getRate(1*60*1000).getAverageValue();
        return Math.max(send, recv);
    }

    /**
     *  Inbound rate in bytes per second
     */
    public int get1mRateIn() {
        StatManager mgr = _context.statManager();
        RateStat rs = mgr.getRate("bw.recvRate");
        int recv = 0;
        if (rs != null)
            recv = (int)rs.getRate(1*60*1000).getAverageValue();
        return recv;
    }

    /**
     *  Max of inbound and outbound rate in bytes per second
     */
    public int get5mRate() { return get5mRate(false); }

    /**
     *  When outboundOnly is false, outbound rate in bytes per second.
     *  When true, max of inbound and outbound rate in bytes per second.
     */
    public int get5mRate(boolean outboundOnly) {
        int send = 0;
        RateStat rs = _context.statManager().getRate("bw.sendRate");
        if (rs != null)
            send = (int)rs.getRate(5*60*1000).getAverageValue();
        if (outboundOnly)
            return send;
        int recv = 0;
        rs = _context.statManager().getRate("bw.recvRate");
        if (rs != null)
            recv = (int)rs.getRate(5*60*1000).getAverageValue();
        return Math.max(send, recv);
    }

    /**
     *  Translate with console bundle
     *  @since 0.9.53
     */
    private final String _t(String s) {
        return Translate.getString(s, _context, BUNDLE_NAME);
    }
}
