package net.i2p.router.time;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.StringTokenizer;
import java.util.concurrent.CopyOnWriteArrayList;
import net.i2p.I2PAppContext;
import net.i2p.time.Timestamper;
import net.i2p.util.Addresses;
import net.i2p.util.I2PThread;
import net.i2p.util.Log;
/**
* Periodically query a series of NTP servers and update any associated
* listeners. It tries the NTP servers in order, contacting them using
* SNTP (UDP port 123).
*
* @since 0.9.1 moved from net.i2p.time
*/
public class RouterTimestamper extends Timestamper {
private final I2PAppContext _context;
// warning, may be null
private Log _log;
private final List<String> _servers;
private List<List<String>> _priorityServers;
private final List<UpdateListener> _listeners;
private int _queryFrequency;
private int _concurringServers;
private int _consecutiveFails;
private volatile boolean _disabled;
private final boolean _daemon;
private volatile boolean _initialized;
private boolean _wellSynced;
private volatile boolean _isRunning;
private Thread _timestamperThread;
private final Zones _zones;
private static final int MIN_QUERY_FREQUENCY = 5*60*1000;
private static final int DEFAULT_QUERY_FREQUENCY = 11*60*1000;
private static final String DEFAULT_SERVER_LIST = "0.pool.ntp.org,1.pool.ntp.org,2.pool.ntp.org";
private static final boolean DEFAULT_DISABLED = true;
/** how many times do we have to query if we are changing the clock? */
private static final int DEFAULT_CONCURRING_SERVERS = 3;
private static final int MAX_CONSECUTIVE_FAILS = 10;
private static final int DEFAULT_TIMEOUT = 10*1000;
private static final int SHORT_TIMEOUT = 5*1000;
private static final long MAX_WAIT_INITIALIZATION = 45*1000;
public static final String PROP_QUERY_FREQUENCY = "time.queryFrequencyMs";
public static final String PROP_SERVER_LIST = "time.sntpServerList";
public static final String PROP_DISABLED = "time.disabled";
public static final String PROP_CONCURRING_SERVERS = "time.concurringServers";
public static final String PROP_IP_COUNTRY = "i2np.lastCountry";
/** if different SNTP servers differ by more than 10s, someone is b0rked */
private static final int MAX_VARIANCE = 10*1000;
/**
* Does not start. Caller MUST call startTimestamper()
*/
public RouterTimestamper(I2PAppContext ctx) {
this(ctx, null, true);
}
/**
* Does not start. Caller MUST call startTimestamper()
*/
public RouterTimestamper(I2PAppContext ctx, UpdateListener lsnr) {
this(ctx, lsnr, true);
}
/**
* Does not start. Caller MUST call startTimestamper()
*/
public RouterTimestamper(I2PAppContext ctx, UpdateListener lsnr, boolean daemon) {
super();
// moved here to prevent problems with synchronized statements.
_servers = new ArrayList<String>(3);
_listeners = new CopyOnWriteArrayList<UpdateListener>();
_context = ctx;
_daemon = daemon;
// DO NOT initialize _log here, stack overflow via LogManager init loop
// Don't bother starting a thread if we are disabled.
// This means we no longer check every 5 minutes to see if we got enabled,
// so the property must be set at startup.
// We still need to be instantiated since the router calls clock().getTimestamper().waitForInitialization()
_disabled = ctx.getProperty(PROP_DISABLED, DEFAULT_DISABLED);
if (_disabled) {
_initialized = true;
_zones = null;
System.out.println("Warning: NTP is disabled");
return;
}
if (lsnr != null)
_listeners.add(lsnr);
_zones = new Zones(ctx);
updateConfig();
}
public int getServerCount() {
synchronized (_servers) {
return _servers.size();
}
}
public String getServer(int index) {
synchronized (_servers) {
return _servers.get(index);
}
}
public int getQueryFrequencyMs() { return _queryFrequency; }
public boolean getIsDisabled() { return _disabled; }
public void addListener(UpdateListener lsnr) {
_listeners.add(lsnr);
}
public void removeListener(UpdateListener lsnr) {
_listeners.remove(lsnr);
}
public int getListenerCount() {
return _listeners.size();
}
public UpdateListener getListener(int index) {
return _listeners.get(index);
}
public void startTimestamper() {
if (_disabled || _initialized)
return;
_timestamperThread = new I2PThread(this, "Timestamper", _daemon);
_timestamperThread.setPriority(I2PThread.MIN_PRIORITY);
_isRunning = true;
_timestamperThread.start();
_context.addShutdownTask(new Shutdown());
}
@Override
public void waitForInitialization() {
try {
synchronized (this) {
if (!_initialized)
wait(MAX_WAIT_INITIALIZATION);
}
} catch (InterruptedException ie) {}
}
/**
* Update the time immediately.
* @since 0.8.8
*/
@Override
public void timestampNow() {
if (_initialized && _isRunning && (!_disabled) && _timestamperThread != null)
_timestamperThread.interrupt();
}
/** @since 0.8.8 */
private class Shutdown implements Runnable {
public void run() {
_isRunning = false;
if (_timestamperThread != null)
_timestamperThread.interrupt();
}
}
@Override
public void run() {
boolean lastFailed = false;
try {
while (_isRunning) {
// NOTE: _log is null the first time through, to prevent problems and stack overflows
updateConfig();
boolean preferIPv6 = Addresses.isConnectedIPv6();
if (!_disabled) {
// first the servers for our country and continent, we know what country we're in...
if (_priorityServers != null) {
for (List<String> servers : _priorityServers) {
if (_log != null && _log.shouldDebug())
_log.debug("Querying servers " + servers);
try {
lastFailed = !queryTime(servers.toArray(new String[servers.size()]), SHORT_TIMEOUT, preferIPv6);
} catch (IllegalArgumentException iae) {
if (!lastFailed && _log != null && _log.shouldWarn())
_log.warn("Unable to reach any regional NTP servers: " + servers);
lastFailed = true;
}
if (!lastFailed)
break;
}
}
// ... and then the global list, if that failed
if (_priorityServers == null || lastFailed) {
if (_log != null && _log.shouldDebug())
_log.debug("Querying servers " + _servers);
try {
// If we failed, maybe it's because IPv6 is blocked, so try IPv4 only
// also first time through, and randomly
boolean prefIPv6 = preferIPv6 && !lastFailed && _log != null && _context.random().nextInt(4) != 0;
lastFailed = !queryTime(_servers.toArray(new String[_servers.size()]), DEFAULT_TIMEOUT, prefIPv6);
} catch (IllegalArgumentException iae) {
lastFailed = true;
}
}
}
boolean wasInitialized;
synchronized (this) {
wasInitialized = _initialized;
if (!wasInitialized)
_initialized = true;
notifyAll();
}
if (!wasInitialized) {
// let the log manager get initialized
try { Thread.sleep(10*1000); } catch (InterruptedException ie) {}
// NOW we set up logging
_log = _context.logManager().getLog(RouterTimestamper.class);
if (lastFailed) {
List<String> all = new ArrayList<String>(9);
if (_priorityServers != null) {
for (List<String> servers : _priorityServers) {
all.addAll(servers);
}
}
all.addAll(_servers);
String msg = "Unable to reach any of the NTP servers " + all +
" - network disconnected? Or set time.sntpServerList=myserver1.com,myserver2.com in advanced configuration.";
_log.logAlways(Log.WARN, msg);
System.out.println("Warning: " + msg);
} else if (_log.shouldDebug()) {
_log.debug("NTP initialization successful");
int i = 1;
if (_priorityServers != null) {
for (List<String> servers : _priorityServers) {
_log.debug("NTP Server list " + (i++) + ": " + servers);
}
}
_log.debug("NTP Server list " + i + ": " + _servers);
}
}
long sleepTime;
if (lastFailed) {
if (++_consecutiveFails >= MAX_CONSECUTIVE_FAILS)
sleepTime = 30*60*1000;
else
sleepTime = 30*1000;
} else {
_consecutiveFails = 0;
sleepTime = _context.random().nextInt(_queryFrequency / 2) + _queryFrequency;
if (_wellSynced)
sleepTime *= 3;
}
try { Thread.sleep(sleepTime); } catch (InterruptedException ie) {}
}
} catch (Throwable t) {
synchronized (this) { notifyAll(); }
if (_log != null)
_log.log(Log.CRIT, "Timestamper died!", t);
t.printStackTrace();
}
}
/**
* True if the time was queried successfully, false if it couldn't be
*/
private boolean queryTime(String serverList[], int perServerTimeout, boolean preferIPv6) throws IllegalArgumentException {
long found[] = new long[_concurringServers];
long now = -1;
int stratum = -1;
long expectedDelta = 0;
_wellSynced = false;
for (int i = 0; i < _concurringServers; i++) {
//if (i > 0) {
// // this delays startup when net is disconnected or the timeserver list is bad, don't make it too long
// try { Thread.sleep(2*1000); } catch (InterruptedException ie) {}
//}
long[] timeAndStratum = NtpClient.currentTimeAndStratum(serverList, perServerTimeout, preferIPv6, _log);
now = timeAndStratum[0];
stratum = (int) timeAndStratum[1];
long delta = now - _context.clock().now();
found[i] = delta;
if (i == 0) {
if (Math.abs(delta) < MAX_VARIANCE) {
if (_log != null && _log.shouldInfo())
_log.info("a single SNTP query was within the tolerance (" + delta + "ms)");
// If less than a half second on the first try, we're in good shape
_wellSynced = Math.abs(delta) < 500;
break;
} else {
// outside the tolerance, lets iterate across the concurring queries
expectedDelta = delta;
}
} else {
if (Math.abs(delta - expectedDelta) > MAX_VARIANCE) {
if (_log != null && _log.shouldError()) {
StringBuilder err = new StringBuilder(96);
err.append("SNTP client variance exceeded at query ").append(i);
err.append(". expected = ");
err.append(expectedDelta);
err.append(", found = ");
err.append(delta);
err.append(" all deltas: ");
for (int j = 0; j < found.length; j++)
err.append(found[j]).append(' ');
_log.error(err.toString());
}
return false;
}
}
}
stampTime(now, stratum);
if (_log != null && _log.shouldDebug()) {
StringBuilder buf = new StringBuilder(64);
buf.append("Deltas: ");
for (int i = 0; i < found.length; i++)
buf.append(found[i]).append(' ');
_log.debug(buf.toString());
}
return true;
}
/**
* Notify the listeners
*
* @since stratum param added in 0.7.12
*/
private void stampTime(long now, int stratum) {
long before = _context.clock().now();
for (UpdateListener lsnr : _listeners) {
lsnr.setNow(now, stratum);
}
if (_log != null && _log.shouldDebug())
_log.debug("Stamped the time as " + now + " (delta=" + (now-before) + ")");
}
/**
* Reload all the config elements from the appContext.
* No logging allowed here
*/
private void updateConfig() {
String serverList = _context.getProperty(PROP_SERVER_LIST);
if ( (serverList == null) || (serverList.trim().length() <= 0) ) {
serverList = DEFAULT_SERVER_LIST;
String country = _context.getProperty(PROP_IP_COUNTRY);
if (country == null) {
country = Locale.getDefault().getCountry();
if (country != null)
country = country.toLowerCase(Locale.US);
}
if (country != null && country.length() > 0 &&
!country.equals("a1") && !country.equals("a2")) {
_priorityServers = new ArrayList<List<String>>(2);
List<String> p1 = new ArrayList<String>(3);
for (int i = 0; i < 3; i++) {
p1.add(i + "." + country + ".pool.ntp.org");
}
_priorityServers.add(p1);
String zone = _zones.getZone(country);
if (zone != null) {
List<String> p2 = new ArrayList<String>(3);
for (int i = 0; i < 3; i++) {
p2.add(i + "." + zone + ".pool.ntp.org");
}
_priorityServers.add(p2);
}
} else {
_priorityServers = null;
}
} else {
_priorityServers = null;
}
_servers.clear();
StringTokenizer tok = new StringTokenizer(serverList, ", ");
while (tok.hasMoreTokens()) {
String val = tok.nextToken();
val = val.trim();
if (val.length() > 0)
_servers.add(val);
}
_queryFrequency = Math.max(MIN_QUERY_FREQUENCY,
_context.getProperty(PROP_QUERY_FREQUENCY, DEFAULT_QUERY_FREQUENCY));
_disabled = _context.getProperty(PROP_DISABLED, DEFAULT_DISABLED);
_concurringServers = Math.min(4, Math.max(1,
_context.getProperty(PROP_CONCURRING_SERVERS, DEFAULT_CONCURRING_SERVERS)));
}
/****
public static void main(String args[]) {
System.setProperty(PROP_DISABLED, "false");
System.setProperty(PROP_QUERY_FREQUENCY, "30000");
I2PAppContext.getGlobalContext();
for (int i = 0; i < 5*60*1000; i += 61*1000) {
try { Thread.sleep(61*1000); } catch (InterruptedException ie) {}
}
}
****/
}