Improved detection of inaccurate system clock & nagging.

Now uses several NTP servers to determine mean offset from
system clock to internet time.

If abs(offset) > 500ms or NTP service not running then
user is 'nagged' via system tray pop-up notification
with instructions on how to fix.

Also improved system tray translations!
This commit is contained in:
catbref 2019-07-25 11:08:43 +01:00
parent 0c17f9cff6
commit 73e53120a9
7 changed files with 263 additions and 39 deletions

View File

@ -335,13 +335,12 @@
<artifactId>commons-text</artifactId>
<version>1.4</version>
</dependency>
<!-- No longer needed? Was used by NTP
<!-- For NTP -->
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.3</version>
</dependency>
-->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>

View File

@ -70,6 +70,7 @@ import org.qora.transaction.Transaction.TransactionType;
import org.qora.transaction.Transaction.ValidationResult;
import org.qora.ui.UiService;
import org.qora.utils.Base58;
import org.qora.utils.NTP;
import org.qora.utils.Triple;
public class Controller extends Thread {
@ -91,6 +92,7 @@ public class Controller extends Thread {
private static final long ARBITRARY_REQUEST_TIMEOUT = 5 * 1000; // ms
private static final long REPOSITORY_BACKUP_PERIOD = 123 * 60 * 1000; // ms
private static final long NTP_NAG_PERIOD = 5 * 60 * 1000; // ms
private static final long MAX_NTP_OFFSET = 500; // ms
private static volatile boolean isStopping = false;
private static BlockGenerator blockGenerator = null;
@ -420,26 +422,15 @@ public class Controller extends Thread {
}
}
/** Nag Windows users that don't have many/any peers and not using Windows' auto time sync. */
/** Nag if we detect system clock is too far from internet time. */
private void ntpNag() {
// Only for Windows users
if (!System.getProperty("os.name").toLowerCase().contains("win"))
return;
// Fetch mean offset from internet time (ms).
Long meanOffset = NTP.getOffset();
// Suffering from lack of peers?
final int numberOfPeers = Network.getInstance().getUniqueHandshakedPeers().size();
if (numberOfPeers > 0)
return;
// Do we actually know any peers to connect to?
try (final Repository repository = RepositoryManager.getRepository()) {
final int numberOfKnownPeers = repository.getNetworkRepository().getAllPeers().size();
if (numberOfKnownPeers == 0)
return;
} catch (DataException e) {
// Not important
return;
}
final boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
Boolean isNtpActive = null;
if (isWindows) {
// Detecting Windows Time service
String[] detectCmd = new String[] { "net", "start" };
try {
@ -447,19 +438,34 @@ public class Controller extends Thread {
try (InputStream in = process.getInputStream(); Scanner scanner = new Scanner(in, "UTF8")) {
scanner.useDelimiter("\\A");
String output = scanner.hasNext() ? scanner.next() : "";
boolean isRunning = output.contains("Windows Time");
if (isRunning)
return;
isNtpActive = output.contains("Windows Time");
}
} catch (IOException e) {
// Not important
return;
}
} else {
// Very basic unix-based attempt to check for ntpd
String[] detectCmd = new String[] { "ps", "-agx" };
try {
Process process = new ProcessBuilder(Arrays.asList(detectCmd)).start();
try (InputStream in = process.getInputStream(); Scanner scanner = new Scanner(in, "UTF8")) {
scanner.useDelimiter("\\A");
String output = scanner.hasNext() ? scanner.next() : "";
isNtpActive = output.contains("ntpd");
}
} catch (IOException e) {
// Not important
}
}
// If offset is good and ntp is active then we're good
if (Math.abs(meanOffset) < MAX_NTP_OFFSET && isNtpActive == true)
return;
// Time to nag
String caption = Translator.INSTANCE.translate("SysTray", "NTP_NAG_CAPTION");
String text = Translator.INSTANCE.translate("SysTray", "NTP_NAG_TEXT");
SysTray.getInstance().showMessage(caption, text, MessageType.INFO);
String text = Translator.INSTANCE.translate("SysTray", isWindows ? "NTP_NAG_TEXT_WINDOWS" : "NTP_NAG_TEXT_UNIX");
SysTray.getInstance().showMessage(caption, text, MessageType.WARNING);
}
public void updateSysTray() {
@ -467,7 +473,9 @@ public class Controller extends Thread {
final int height = getChainHeight();
String tooltip = String.format("qora-core - %d peer%s - height %d", numberOfPeers, (numberOfPeers != 1 ? "s" : ""), height);
String connectionsText = Translator.INSTANCE.translate("SysTray", numberOfPeers != 1 ? "CONNECTIONS" : "CONNECTION");
String heightText = Translator.INSTANCE.translate("SysTray", "BLOCK_HEIGHT");
String tooltip = String.format("qora-core - %d %s - %s %d", numberOfPeers, connectionsText, heightText, height);
SysTray.getInstance().setToolTipText(tooltip);
}

View File

@ -95,6 +95,20 @@ public class Settings {
"https://raw.githubusercontent.com@151.101.16.133/catbref/qora-core/%s/qora-core.jar"
};
// NTP sources
private String[] ntpServers = new String[] {
"pool.ntp.org",
"0.pool.ntp.org",
"1.pool.ntp.org",
"2.pool.ntp.org",
"3.pool.ntp.org",
"asia.pool.ntp.org",
"0.asia.pool.ntp.org",
"1.asia.pool.ntp.org",
"2.asia.pool.ntp.org",
"3.asia.pool.ntp.org"
};
// Constructors
private Settings() {
@ -308,4 +322,8 @@ public class Settings {
return this.autoUpdateRepos;
}
public String[] getNtpServers() {
return this.ntpServers;
}
}

View File

@ -0,0 +1,99 @@
package org.qora.utils;
import java.io.IOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.net.ntp.NTPUDPClient;
import org.apache.commons.net.ntp.TimeInfo;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.qora.settings.Settings;
public class NTP {
private static final Logger LOGGER = LogManager.getLogger(NTP.class);
private static final double MAX_STDDEV = 25; // ms
/**
* Returns aggregated internet time.
*
* @return internet time (ms), or null if unsuccessful.
*/
public static Long getTime() {
Long meanOffset = getOffset();
if (meanOffset == null)
return null;
return System.currentTimeMillis() + meanOffset;
}
/**
* Returns mean offset from internet time.
*
* Positive offset means local clock is behind internet time.
*
* @return offset (ms), or null if unsuccessful.
*/
public static Long getOffset() {
String[] ntpServers = Settings.getInstance().getNtpServers();
NTPUDPClient client = new NTPUDPClient();
client.setDefaultTimeout(2000);
List<Double> offsets = new ArrayList<>();
for (String server : ntpServers) {
try {
TimeInfo timeInfo = client.getTime(InetAddress.getByName(server));
timeInfo.computeDetails();
LOGGER.debug(() -> String.format("%c%16.16s %16.16s %2d %c %4d %4d %3o %6dms % 5dms % 5dms",
' ',
server,
timeInfo.getMessage().getReferenceIdString(),
timeInfo.getMessage().getStratum(),
'u',
0,
1 << timeInfo.getMessage().getPoll(),
1,
timeInfo.getDelay(),
timeInfo.getOffset(),
0
));
offsets.add((double) timeInfo.getOffset());
} catch (IOException e) {
// Try next server...
}
}
if (offsets.size() < ntpServers.length / 2) {
LOGGER.debug("Not enough replies");
return null;
}
// sₙ represents sum of offsetⁿ
double s0 = 0;
double s1 = 0;
double s2 = 0;
for (Double offset : offsets) {
s0 += 1;
s1 += offset;
s2 += offset * offset;
}
double mean = s1 / s0;
double stddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
// If stddev is excessive then we're not very sure so give up
if (stddev > MAX_STDDEV)
return null;
return (long) mean;
}
}

View File

@ -1,14 +1,22 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# SysTray pop-up menu
BLOCK_HEIGHT = height
CHECK_TIME_ACCURACY = Check time accuracy
CONNECTION = block
CONNECTIONS = connections
EXIT = Exit
# Nagging about lack of NTP time sync
NTP_NAG_CAPTION = No connections?
NTP_NAG_CAPTION = Computer's clock is inaccurate!
NTP_NAG_TEXT = Please enable Windows automatic time synchronization
NTP_NAG_TEXT_UNIX = Install NTP service to get an accurate clock.
NTP_NAG_TEXT_WINDOWS = Select "Synchronize clock" from menu to fix.
OPEN_NODE_UI = Open Node UI

View File

@ -1,14 +1,20 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# SysTray pop-up menu
BLOCK_HEIGHT = \u5757\u9AD8\u5EA6
CHECK_TIME_ACCURACY = \u68C0\u67E5\u65F6\u95F4\u51C6\u786E\u6027
CONNECTIONS = \u4E2A\u8FDE\u63A5
EXIT = \u9000\u51FA\u8F6F\u4EF6
# Nagging about lack of NTP time sync
NTP_NAG_CAPTION = \u6CA1\u6709\u8FDE\u63A5\u4E0A\u8282\u70B9\uFF1F
NTP_NAG_CAPTION = \u7535\u8111\u7684\u65F6\u949F\u4E0D\u51C6\u786E\uFF01
NTP_NAG_TEXT = \u8BF7\u542F\u7528Windows\u81EA\u52A8\u65F6\u95F4\u540C\u6B65\u3002
NTP_NAG_TEXT_UNIX = \u5B89\u88C5NTP\u670D\u52A1\u4EE5\u83B7\u5F97\u51C6\u786E\u7684\u65F6\u949F\u3002
NTP_NAG_TEXT_WINDOWS = \u4ECE\u83DC\u5355\u4E2D\u9009\u62E9\u201C\u540C\u6B65\u65F6\u949F\u201D\u8FDB\u884C\u4FEE\u590D\u3002
OPEN_NODE_UI = \u5F00\u542F\u754C\u9762

View File

@ -0,0 +1,86 @@
package org.qora.test;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.net.ntp.NTPUDPClient;
import org.apache.commons.net.ntp.NtpV3Packet;
import org.apache.commons.net.ntp.TimeInfo;
public class NTPTests {
private static final List<String> CC_TLDS = Arrays.asList("oceania", "europe", "lat", "asia", "africa");
public static void main(String[] args) throws UnknownHostException, IOException {
NTPUDPClient client = new NTPUDPClient();
client.setDefaultTimeout(2000);
System.out.println(String.format("%c%16s %16s %2s %c %4s %4s %3s %7s %7s %7s",
' ', "remote", "refid", "st", 't', "when", "poll", "reach", "delay", "offset", "jitter"
));
List<Double> offsets = new ArrayList<>();
List<String> ntpServers = new ArrayList<>();
for (String ccTld : CC_TLDS) {
ntpServers.add(ccTld + ".pool.ntp.org");
for (int subpool = 0; subpool <=3; ++subpool)
ntpServers.add(subpool + "." + ccTld + ".pool.ntp.org");
}
for (String server : ntpServers) {
try {
TimeInfo timeInfo = client.getTime(InetAddress.getByName(server));
timeInfo.computeDetails();
NtpV3Packet ntpMessage = timeInfo.getMessage();
System.out.println(String.format("%c%16.16s %16.16s %2d %c %4d %4d %3o %6dms % 5dms % 5dms",
' ',
server,
ntpMessage.getReferenceIdString(),
ntpMessage.getStratum(),
'u',
0,
1 << ntpMessage.getPoll(),
1,
timeInfo.getDelay(),
timeInfo.getOffset(),
0
));
offsets.add((double) timeInfo.getOffset());
} catch (IOException e) {
// Try next server...
}
}
if (offsets.size() < ntpServers.size() / 2) {
System.err.println("Not enough replies");
System.exit(1);
}
double s0 = 0;
double s1 = 0;
double s2 = 0;
for (Double offset : offsets) {
// Exclude nearby results for more extreme testing
if (offset < 100.0)
continue;
s0 += 1;
s1 += offset;
s2 += offset * offset;
}
double mean = s1 / s0;
double stddev = Math.sqrt(((s0 * s2) - (s1 * s1)) / (s0 * (s0 - 1)));
System.out.println(String.format("mean: %7.3f, stddev: %7.3f", mean, stddev));
}
}