diff --git a/core/src/main/java/bisq/core/app/BisqHeadlessApp.java b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java index 9fb6abd8285..a6fcca5401c 100644 --- a/core/src/main/java/bisq/core/app/BisqHeadlessApp.java +++ b/core/src/main/java/bisq/core/app/BisqHeadlessApp.java @@ -94,7 +94,6 @@ protected void setupHandlers() { bisqSetup.setShowPopupIfInvalidBtcConfigHandler(() -> log.error("onShowPopupIfInvalidBtcConfigHandler")); bisqSetup.setRevolutAccountsUpdateHandler(revolutAccountList -> log.info("setRevolutAccountsUpdateHandler: revolutAccountList={}", revolutAccountList)); bisqSetup.setQubesOSInfoHandler(() -> log.info("setQubesOSInfoHandler")); - bisqSetup.setFirewallIssueHandler(() -> log.info("setFirewallIssueHandler")); bisqSetup.setDownGradePreventionHandler(lastVersion -> log.info("Downgrade from version {} to version {} is not supported", lastVersion, Version.VERSION)); bisqSetup.setDaoRequiresRestartHandler(() -> { diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index be950bd3e30..48a4d9371e5 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -198,9 +198,6 @@ default void onRequestWalletPassword() { private Runnable qubesOSInfoHandler; @Setter @Nullable - private Runnable firewallIssueHandler; - @Setter - @Nullable private Runnable daoRequiresRestartHandler; @Setter @Nullable @@ -217,7 +214,6 @@ default void onRequestWalletPassword() { @SuppressWarnings("FieldCanBeLocal") private MonadicBinding p2pNetworkAndWalletInitialized; private final List bisqSetupListeners = new ArrayList<>(); - private int failedSelfPings = 0; @Inject public BisqSetup(DomainInitialisation domainInitialisation, @@ -338,7 +334,7 @@ private void step4() { maybeShowLocalhostRunningInfo(); maybeShowAccountSigningStateInfo(); maybeShowTorAddressUpgradeInformation(); - checkTorFirewall(); + checkInboundConnections(); } @@ -662,30 +658,34 @@ private void checkIfRunningOnQubesOS() { } /** + * Check if we have inbound connections. If not, try to ping ourselves. * If Bisq cannot connect to its own onion address through Tor, display * an informative message to let the user know to configure their firewall else * their offers will not be reachable. - * In rare cases a self ping can fail (thanks Tor), so we retry up to 3 times at random intervals in hope of success. + * Repeat this test hourly. */ - private void checkTorFirewall() { + private void checkInboundConnections() { NodeAddress onionAddress = p2PService.getNetworkNode().nodeAddressProperty().get(); if (onionAddress == null || !onionAddress.getFullAddress().contains("onion")) { return; } - privateNotificationManager.sendPing(onionAddress, stringResult -> { - log.info(stringResult); - if (stringResult.contains("failed")) { - // the self-ping failed: after 3 failures notify the user and stop trying - if (++failedSelfPings >= 3) { - if (firewallIssueHandler != null) { - firewallIssueHandler.run(); - } - } else { - // retry self ping after a random delay - UserThread.runAfter(this::checkTorFirewall, new Random().nextInt((int) STARTUP_TIMEOUT_MINUTES), TimeUnit.MINUTES); + + if (p2PService.getNetworkNode().upTime() > TimeUnit.HOURS.toMillis(1) && + p2PService.getNetworkNode().getInboundConnectionCount() == 0) { + // we've been online a while and did not find any inbound connections; lets try the self-ping check + log.info("no recent inbound connections found, starting the self-ping test"); + privateNotificationManager.sendPing(onionAddress, stringResult -> { + log.info(stringResult); + if (stringResult.contains("failed")) { + getP2PNetworkStatusIconId().set("flashing:image-yellow_circle"); } - } - }); + }); + } + + // schedule another inbound connection check for later + int nextCheckInMinutes = 30 + new Random().nextInt(30); + log.debug("next inbound connections check in {} minutes", nextCheckInMinutes); + UserThread.runAfter(this::checkInboundConnections, nextCheckInMinutes, TimeUnit.MINUTES); } private void maybeShowSecurityRecommendation() { @@ -828,6 +828,10 @@ public StringProperty getP2PNetworkIconId() { return p2PNetworkSetup.getP2PNetworkIconId(); } + public StringProperty getP2PNetworkStatusIconId() { + return p2PNetworkSetup.getP2PNetworkStatusIconId(); + } + public BooleanProperty getUpdatedDataReceived() { return p2PNetworkSetup.getUpdatedDataReceived(); } diff --git a/core/src/main/java/bisq/core/app/P2PNetworkSetup.java b/core/src/main/java/bisq/core/app/P2PNetworkSetup.java index 44ff2ece1ce..f947c550439 100644 --- a/core/src/main/java/bisq/core/app/P2PNetworkSetup.java +++ b/core/src/main/java/bisq/core/app/P2PNetworkSetup.java @@ -65,6 +65,8 @@ public class P2PNetworkSetup { @Getter final StringProperty p2PNetworkIconId = new SimpleStringProperty(); @Getter + final StringProperty p2PNetworkStatusIconId = new SimpleStringProperty(); + @Getter final BooleanProperty splashP2PNetworkAnimationVisible = new SimpleBooleanProperty(true); @Getter final StringProperty p2pNetworkLabelId = new SimpleStringProperty("footer-pane"); @@ -127,10 +129,12 @@ BooleanProperty init(Runnable initWalletServiceHandler, p2PService.getNetworkNode().addConnectionListener(new ConnectionListener() { @Override public void onConnection(Connection connection) { + updateNetworkStatusIndicator(); } @Override public void onDisconnect(CloseConnectionReason closeConnectionReason, Connection connection) { + updateNetworkStatusIndicator(); // We only check at seed nodes as they are running the latest version // Other disconnects might be caused by peers running an older version if (connection.getConnectionState().isSeedNode() && @@ -243,4 +247,14 @@ private void addP2PMessageFilter() { !(payload instanceof ProofOfWorkPayload); }); } + + private void updateNetworkStatusIndicator() { + if (p2PService.getNetworkNode().getInboundConnectionCount() > 0) { + p2PNetworkStatusIconId.set("image-green_circle"); + } else if (p2PService.getNetworkNode().getOutboundConnectionCount() > 0) { + p2PNetworkStatusIconId.set("image-yellow_circle"); + } else { + p2PNetworkStatusIconId.set("image-alert-round"); + } + } } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 7b35c16bf23..c66a64b172c 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -293,7 +293,7 @@ mainView.networkWarning.clockWatcher=Your computer was asleep for {0} seconds. \ Standby mode has been known to cause trades to fail. \ In order to operate correctly Bisq requires that standby mode be disabled in your computer's settings. mainView.version.update=(Update available) - +mainView.status.connections=Inbound connections: {0}\nOutbound connections: {1} #################################################################### # MarketView @@ -3126,9 +3126,15 @@ popup.info.shutDownWithTradeInit={0}\n\ This trade has not finished initializing; shutting down now will probably make it corrupted. Please wait a minute and try again. popup.info.qubesOSSetupInfo=It appears you are running Bisq on Qubes OS. \n\n\ Please make sure your Bisq qube is setup according to our Setup Guide at [HYPERLINK:https://bisq.wiki/Running_Bisq_on_Qubes]. -popup.info.firewallSetupInfo=It appears this machine blocks incoming Tor connections. \ - This can happen in VM environments such as Qubes/VirtualBox/Whonix. \n\n\ - Please set up your environment to accept incoming Tor connections, otherwise no-one will be able to take your offers. +popup.info.p2pStatusIndicator.red={0}\n\n\ + Your node has no connection to the P2P network. Bisq cannot operate in this state. \ + See [HYPERLINK:https://bisq.wiki/Network_status_indicator] for more information. +popup.info.p2pStatusIndicator.yellow={0}\n\n\ + Your node has no inbound Tor connections. Bisq will function ok, but if this state persists for several hours it may be an indication of connectivity problems. \ + See [HYPERLINK:https://bisq.wiki/Network_status_indicator] for more information. +popup.info.p2pStatusIndicator.green={0}\n\n\ + Good news, your P2P connection state looks healthy! \ + [HYPERLINK:https://bisq.wiki/Network_status_indicator] popup.warn.downGradePrevention=Downgrade from version {0} to version {1} is not supported. Please use the latest Bisq version. popup.warn.daoRequiresRestart=There was a problem with synchronizing the DAO state. You have to restart the application to fix the issue. diff --git a/desktop/src/main/java/bisq/desktop/images.css b/desktop/src/main/java/bisq/desktop/images.css index 175bd56fc79..49c5ba407ba 100644 --- a/desktop/src/main/java/bisq/desktop/images.css +++ b/desktop/src/main/java/bisq/desktop/images.css @@ -21,6 +21,10 @@ -fx-image: url("../../images/green_circle.png"); } +#image-yellow_circle { + -fx-image: url("../../images/yellow_circle.png"); +} + #image-blue_circle { -fx-image: url("../../images/blue_circle.png"); } diff --git a/desktop/src/main/java/bisq/desktop/main/MainView.java b/desktop/src/main/java/bisq/desktop/main/MainView.java index 9158d35d94d..802e1e5281e 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainView.java +++ b/desktop/src/main/java/bisq/desktop/main/MainView.java @@ -35,6 +35,7 @@ import bisq.desktop.main.offer.BuyOfferView; import bisq.desktop.main.offer.SellOfferView; import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.main.overlays.windows.TorNetworkSettingsWindow; import bisq.desktop.main.portfolio.PortfolioView; import bisq.desktop.main.settings.SettingsView; import bisq.desktop.main.shared.PriceFeedComboBoxItem; @@ -60,6 +61,10 @@ import com.jfoenix.controls.JFXComboBox; import com.jfoenix.controls.JFXProgressBar; +import javafx.animation.Animation; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; + import javafx.scene.control.Button; import javafx.scene.control.ComboBox; import javafx.scene.control.Label; @@ -92,6 +97,8 @@ import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; +import javafx.util.Duration; + import java.text.DecimalFormat; import java.text.NumberFormat; @@ -156,17 +163,20 @@ public static void removeEffect() { private Label btcSplashInfo; private Popup p2PNetworkWarnMsgPopup, btcNetworkWarnMsgPopup; private final DaoStateMonitoringService daoStateMonitoringService; + private final TorNetworkSettingsWindow torNetworkSettingsWindow; @Inject public MainView(MainViewModel model, CachingViewLoader viewLoader, Navigation navigation, Transitions transitions, + TorNetworkSettingsWindow torNetworkSettingsWindow, DaoStateMonitoringService daoStateMonitoringService) { super(model); this.viewLoader = viewLoader; this.navigation = navigation; MainView.transitions = transitions; + this.torNetworkSettingsWindow = torNetworkSettingsWindow; this.daoStateMonitoringService = daoStateMonitoringService; } @@ -631,6 +641,9 @@ private VBox createSplashScreen() { splashP2PNetworkIcon.setVisible(false); splashP2PNetworkIcon.setManaged(false); HBox.setMargin(splashP2PNetworkIcon, new Insets(0, 0, 5, 0)); + splashP2PNetworkIcon.setOnMouseClicked(e -> { + torNetworkSettingsWindow.show(); + }); Timer showTorNetworkSettingsTimer = UserThread.runAfter(() -> { showTorNetworkSettingsButton.setVisible(true); @@ -772,6 +785,40 @@ private AnchorPane createFooter() { p2PNetworkWarnMsgPopup.hide(); } }); + p2PNetworkIcon.setOnMouseClicked(e -> { + torNetworkSettingsWindow.show(); + }); + + ImageView p2PNetworkStatusIcon = new ImageView(); + setRightAnchor(p2PNetworkStatusIcon, 30d); + setBottomAnchor(p2PNetworkStatusIcon, 7d); + Tooltip p2pNetworkStatusToolTip = new Tooltip(); + Tooltip.install(p2PNetworkStatusIcon, p2pNetworkStatusToolTip); + p2PNetworkStatusIcon.setOnMouseEntered(e -> p2pNetworkStatusToolTip.setText(model.getP2pConnectionSummary())); + Timeline flasher = new Timeline( + new KeyFrame(Duration.seconds(0.5), e -> p2PNetworkStatusIcon.setOpacity(0.2)), + new KeyFrame(Duration.seconds(1.0), e -> p2PNetworkStatusIcon.setOpacity(1)) + ); + flasher.setCycleCount(Animation.INDEFINITE); + model.getP2PNetworkStatusIconId().addListener((ov, oldValue, newValue) -> { + if (newValue.equalsIgnoreCase("flashing:image-yellow_circle")) { + p2PNetworkStatusIcon.setId("image-yellow_circle"); + flasher.play(); + } else { + p2PNetworkStatusIcon.setId(newValue); + flasher.stop(); + p2PNetworkStatusIcon.setOpacity(1); + } + }); + p2PNetworkStatusIcon.setOnMouseClicked(e -> { + if (p2PNetworkStatusIcon.getId().equalsIgnoreCase("image-alert-round")) { + new Popup().warning(Res.get("popup.info.p2pStatusIndicator.red", model.getP2pConnectionSummary())).show(); + } else if (p2PNetworkStatusIcon.getId().equalsIgnoreCase("image-yellow_circle")) { + new Popup().information(Res.get("popup.info.p2pStatusIndicator.yellow", model.getP2pConnectionSummary())).show(); + } else { + new Popup().information(Res.get("popup.info.p2pStatusIndicator.green", model.getP2pConnectionSummary())).show(); + } + }); model.getUpdatedDataReceived().addListener((observable, oldValue, newValue) -> { p2PNetworkIcon.setOpacity(1); @@ -785,10 +832,10 @@ private AnchorPane createFooter() { VBox vBox = new VBox(); vBox.setAlignment(Pos.CENTER_RIGHT); vBox.getChildren().addAll(p2PNetworkLabel, p2pNetworkProgressBar); - setRightAnchor(vBox, 33d); + setRightAnchor(vBox, 53d); setBottomAnchor(vBox, 5d); - return new AnchorPane(separator, btcInfoLabel, versionBox, vBox, p2PNetworkIcon) {{ + return new AnchorPane(separator, btcInfoLabel, versionBox, vBox, p2PNetworkStatusIcon, p2PNetworkIcon) {{ setId("footer-pane"); setMinHeight(30); setMaxHeight(30); diff --git a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java index 8f7d0fc4f15..c9d1579f841 100644 --- a/desktop/src/main/java/bisq/desktop/main/MainViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/MainViewModel.java @@ -479,15 +479,6 @@ private void setupHandlers() { .show(); } }); - bisqSetup.setFirewallIssueHandler(() -> { - String key = "firewallSetupInfo"; - if (preferences.showAgain(key)) { - new Popup().information(Res.get("popup.info.firewallSetupInfo")) - .closeButtonText(Res.get("shared.iUnderstand")) - .dontShowAgainId(key) - .show(); - } - }); bisqSetup.setDownGradePreventionHandler(lastVersion -> { new Popup().warning(Res.get("popup.warn.downGradePrevention", lastVersion, Version.VERSION)) @@ -833,6 +824,10 @@ StringProperty getP2PNetworkIconId() { return bisqSetup.getP2PNetworkIconId(); } + StringProperty getP2PNetworkStatusIconId() { + return bisqSetup.getP2PNetworkStatusIconId(); + } + BooleanProperty getUpdatedDataReceived() { return bisqSetup.getUpdatedDataReceived(); } @@ -900,4 +895,10 @@ private void maybeShowPopupsFromQueue() { overlay.show(); } } + + public String getP2pConnectionSummary() { + return Res.get("mainView.status.connections", + p2PService.getNetworkNode().getInboundConnectionCount(), + p2PService.getNetworkNode().getOutboundConnectionCount()); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TorNetworkSettingsWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TorNetworkSettingsWindow.java index 40ef64e4986..e99ebf81927 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/TorNetworkSettingsWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/TorNetworkSettingsWindow.java @@ -123,7 +123,7 @@ public void show() { headLine = Res.get("torNetworkSettingWindow.header"); width = 1068; - + rowIndex = 0; createGridPane(); gridPane.getColumnConstraints().get(0).setHalignment(HPos.LEFT); diff --git a/desktop/src/main/resources/images/yellow_circle.png b/desktop/src/main/resources/images/yellow_circle.png new file mode 100644 index 00000000000..44e5a272fa5 Binary files /dev/null and b/desktop/src/main/resources/images/yellow_circle.png differ diff --git a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java index 9fca7006b5d..35a4edd03f3 100644 --- a/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java +++ b/p2p/src/main/java/bisq/network/p2p/network/NetworkNode.java @@ -45,6 +45,7 @@ import java.io.IOException; +import java.util.Date; import java.util.HashSet; import java.util.Optional; import java.util.Set; @@ -513,4 +514,22 @@ public Optional findPeersCapabilities(NodeAddress nodeAddress) { .map(Connection::getCapabilities) .findAny(); } + + public long upTime() { + // how long Bisq has been running with at least one connection + // uptime is relative to last all connections lost event + long earliestConnection = new Date().getTime(); + for (Connection connection : outBoundConnections) { + earliestConnection = Math.min(earliestConnection, connection.getStatistic().getCreationDate().getTime()); + } + return new Date().getTime() - earliestConnection; + } + + public int getInboundConnectionCount() { + return inBoundConnections.size(); + } + + public int getOutboundConnectionCount() { + return outBoundConnections.size(); + } }