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

@@ -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,46 +422,50 @@ 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;
final boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
Boolean isNtpActive = null;
if (isWindows) {
// Detecting Windows Time service
// 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;
}
String[] detectCmd = new String[] { "net", "start" };
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() : "";
boolean isRunning = output.contains("Windows Time");
if (isRunning)
return;
String[] detectCmd = new String[] { "net", "start" };
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("Windows Time");
}
} catch (IOException e) {
// Not important
}
} 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
}
} catch (IOException e) {
// Not important
return;
}
// 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;
}
}