mirror of
https://github.com/Qortal/qortal.git
synced 2025-07-22 20:26:50 +00:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
99
src/main/java/org/qora/utils/NTP.java
Normal file
99
src/main/java/org/qora/utils/NTP.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user