From 343d2f55d9711d4959c447eabe35ac606549fa54 Mon Sep 17 00:00:00 2001
From: Benoit Lavenier <benoit.lavenier@e-is.pro>
Date: Wed, 11 Mar 2020 15:05:49 +0100
Subject: [PATCH] [fix] BMA, ES http: avoid to restart service to often, if not
 need (e.g. when changing node from Settings) [fix] BMA: make sure to clean
 cache request [fix] ES Http: make sure to clean cache request [fix] Network::
 avoid to create a new BMA instance, is same as default BMA node [enh] ES
 http: add latest URL management

---
 www/js/api/app.js                             |   2 +-
 www/js/controllers/blockchain-controllers.js  |   6 +-
 www/js/controllers/network-controllers.js     |  10 +-
 www/js/controllers/settings-controllers.js    |  24 ++---
 www/js/platform.js                            |   2 +-
 www/js/services/bma-services.js               | 101 +++++++++++-------
 .../es/js/controllers/network-controllers.js  |   2 +-
 www/plugins/es/js/services/http-services.js   |  75 ++++++++++---
 .../es/js/services/settings-services.js       |   1 +
 www/templates/menu.html                       |   6 +-
 10 files changed, 151 insertions(+), 78 deletions(-)

diff --git a/www/js/api/app.js b/www/js/api/app.js
index c595aa43..a5d1fa6a 100644
--- a/www/js/api/app.js
+++ b/www/js/api/app.js
@@ -340,7 +340,7 @@ angular.module('cesium-api', ['ionic', 'ionic-material', 'ngMessages', 'pascalpr
       $scope.loading = true;
 
       // Set BMA node
-      if (!$scope.error && $scope.node && !BMA.node.same($scope.node.host, $scope.node.port)) {
+      if (!$scope.error && $scope.node && !BMA.node.same($scope.node)) {
         console.debug("[api] Using preferred node: {0}:{1}".format($scope.node.host, $scope.node.port));
         BMA.stop();
         BMA.copy($scope.node);
diff --git a/www/js/controllers/blockchain-controllers.js b/www/js/controllers/blockchain-controllers.js
index 7db59569..e9dc0a54 100644
--- a/www/js/controllers/blockchain-controllers.js
+++ b/www/js/controllers/blockchain-controllers.js
@@ -140,7 +140,7 @@ function BlockLookupController($scope, $timeout, $focus, $filter, $state, $ancho
           node.port = serverParts[1];
         }
 
-        if (BMA.node.same(node.host, node.port)) {
+        if (BMA.node.same(node)) {
           $scope.node = BMA;
         }
         else {
@@ -454,7 +454,7 @@ function BlockLookupController($scope, $timeout, $focus, $filter, $state, $ancho
         $anchorScroll('block-' + block.number);
       }, 900);
     }
-    else if (BMA.node.same($scope.node.host, $scope.node.port)) {
+    else if (BMA.node.same($scope.node)) {
       $state.go('app.view_block_hash', {number: block.number, hash: block.hash});
     }
     else {
@@ -534,7 +534,7 @@ function BlockViewController($scope, $ionicPopover, $state, UIUtils, BMA, csCurr
           node.port = serverParts[1];
         }
 
-        if (BMA.node.same(node.host, node.port)) {
+        if (BMA.node.same(node)) {
           $scope.node = BMA;
         }
         else {
diff --git a/www/js/controllers/network-controllers.js b/www/js/controllers/network-controllers.js
index 35ca9fda..7a718b93 100644
--- a/www/js/controllers/network-controllers.js
+++ b/www/js/controllers/network-controllers.js
@@ -92,8 +92,9 @@ function NetworkLookupController($scope,  $state, $location, $ionicPopover, $win
     csCurrency.get()
       .then(function (currency) {
         if (currency) {
-          $scope.node = !BMA.node.same(currency.node.host, currency.node.port) ?
-            BMA.instance(currency.node.host, currency.node.port) : BMA;
+          var isDefaultNode = BMA.node.same(currency.node);
+          $scope.node = isDefaultNode ? BMA :
+            BMA.instance(currency.node.host, currency.node.port);
           if (state && state.stateParams) {
             if (state.stateParams.type && ['mirror', 'member', 'offline'].indexOf(state.stateParams.type) != -1) {
               $scope.search.type = state.stateParams.type;
@@ -116,6 +117,10 @@ function NetworkLookupController($scope,  $state, $location, $ionicPopover, $win
    * Leave the view
    */
   $scope.leave = function() {
+    // Close node, if not the default BMA
+    if ($scope.node !== BMA) {
+      $scope.node.close();
+    }
     if (!$scope.networkStarted) return;
     $scope.removeListeners();
     csNetwork.close();
@@ -452,6 +457,7 @@ function PeerInfoPopoverController($scope, $q, csSettings, csCurrency, csHttp, B
 
   $scope.load = function() {
 
+    console.debug("[peer-popover] Loading peer info...");
     $scope.loading = true;
     $scope.formData = {};
 
diff --git a/www/js/controllers/settings-controllers.js b/www/js/controllers/settings-controllers.js
index 4981f8e6..0239a70f 100644
--- a/www/js/controllers/settings-controllers.js
+++ b/www/js/controllers/settings-controllers.js
@@ -84,24 +84,24 @@ function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $ti
     $q.all([
       csSettings.ready(),
       csCurrency.parameters()
-        .then(function(parameters) {
-          return parameters && parameters.avgGenTime;
-        })
-        // Make sure to continue even if node is down - Fix #788
         .catch(function(err) {
-          console.error("[settings] Could not not currency parameters. Using default 'avgGenTime' (300)", err);
-          return {avgGenTime: 300};
+          // Continue (will use default value)
+          // Make sure to continue even if node is down - Fix #788
         })
         .then(function(parameters) {
+          var avgGenTime = parameters && parameters.avgGenTime;
+          if (!avgGenTime || avgGenTime < 0) {
+            console.warn("[settings] Could not not currency parameters. Using default G1 'avgGenTime' (300s)");
+            avgGenTime = 300; /* = G1 value = 5min */
+          }
           _.each($scope.blockValidityWindows, function(blockCount) {
             if (blockCount > 0) {
-              $scope.blockValidityWindowLabels[blockCount].labelParams.time= parameters.avgGenTime * blockCount;
+              $scope.blockValidityWindowLabels[blockCount].labelParams.time = avgGenTime * blockCount;
             }
           });
         })
     ])
-      .then($scope.load)
-    ;
+    .then($scope.load);
   });
 
   $scope.setPopupForm = function(popupForm) {
@@ -167,8 +167,7 @@ function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $ti
       }
       UIUtils.loading.show();
 
-      var nodeBMA = BMA.instance(newNode.host, newNode.port, newNode.useSsl, true /*cache*/);
-      nodeBMA.isAlive()
+      BMA.isAlive(newNode)
         .then(function(alive) {
           if (!alive) {
             UIUtils.loading.hide();
@@ -180,7 +179,8 @@ function SettingsController($scope, $q, $window, $ionicHistory, $ionicPopup, $ti
           UIUtils.loading.hide();
           angular.merge($scope.formData.node, newNode);
           delete $scope.formData.node.temporary;
-          BMA.copy(nodeBMA);
+          BMA.stop();
+          BMA.copy(newNode);
           $scope.bma = BMA;
 
           // Restart platform (or start if not already started)
diff --git a/www/js/platform.js b/www/js/platform.js
index f2e30f8e..6bbb1afb 100644
--- a/www/js/platform.js
+++ b/www/js/platform.js
@@ -147,7 +147,7 @@ angular.module('cesium.platform', ['ngIdle', 'cesium.config', 'cesium.services']
       var newServer = fallbackNode.host + ((!fallbackNode.port && fallbackNode.port != 80 && fallbackNode.port != 443) ? (':' + fallbackNode.port) : '');
 
       // Skip is same as actual node
-      if (BMA.node.same(fallbackNode.host, fallbackNode.port)) {
+      if (BMA.node.same(fallbackNode)) {
         console.debug('[platform] Skipping fallback node [{0}]: same as actual node'.format(newServer));
         return checkBmaNodeAlive(); // loop (= go to next node)
       }
diff --git a/www/js/services/bma-services.js b/www/js/services/bma-services.js
index 230ed12a..b23484cd 100644
--- a/www/js/services/bma-services.js
+++ b/www/js/services/bma-services.js
@@ -7,7 +7,9 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
 
   function BMA(host, port, useSsl, useCache) {
 
-    var cachePrefix = "BMA-",
+    var
+      id = (!host ? 'default' : '{0}:{1}'.format(host, (port || (useSsl ? '443' : '80')))), // Unique id of this instance
+      cachePrefix = "BMA-",
       pubkey = "[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{43,44}",
       // TX output conditions
       SIG = "SIG\\((" + pubkey + ")\\)",
@@ -76,7 +78,7 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
       postByPath: {},
       wsByPath: {}
     };
-    that.api = new Api(this, 'BMA-' + that.server);
+    that.api = new Api(this, 'BMA-' + id);
     that.started = false;
     that.init = init;
 
@@ -121,7 +123,7 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
     }
 
     function closeWs() {
-      if (!that.cache) return;
+      if (!that.raw) return;
 
       console.warn('[BMA] Closing all websockets...');
       _.keys(that.raw.wsByPath||{}).forEach(function(key) {
@@ -135,12 +137,10 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
      console.debug("[BMA] Cleaning cache {prefix: '{0}'}...".format(cachePrefix));
      csCache.clear(cachePrefix);
 
-     // Clean raw requests cache
-     angular.merge(that.raw, {
-        getByPath: {},
-        postByPath: {},
-        wsByPath: {}
-     });
+     // Clean raw requests by path cache
+     that.raw.getByPath = {};
+     that.raw.postByPath = {};
+     that.raw.wsByPath = {};
    }
 
    function get(path, cacheTime) {
@@ -234,9 +234,13 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
       };
     }
 
-    that.isAlive = function() {
-      // Warn: cannot use previous get() function, because node may not be started yet
-      return csHttp.get(that.host, that.port, '/node/summary', that.useSsl)()
+    that.isAlive = function(node) {
+      node = node || that;
+      // WARN:
+      //  - Cannot use previous get() function, because
+      //    node can be !=that, or not be started yet
+      //  - Do NOT use cache here
+      return csHttp.get(node.host, node.port, '/node/summary', node.useSsl)()
         .then(function(json) {
           var software = json && json.duniter && json.duniter.software;
           var isCompatible = true;
@@ -259,6 +263,17 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
         });
     };
 
+    function isSameNode(node2) {
+      node2 = node2 || {};
+      node2.useSsl = angular.isDefined(node2.useSsl) ? node2.useSsl : (node2.port && node2.port == 443);
+      // Same host
+      return that.host === node2.host &&
+          // Same port
+          ((!that.port && !node2.port2) || (that.port == node2.port2||80)) &&
+          // Same useSsl
+          (that.useSsl === node2.useSsl);
+    }
+
     function removeListeners() {
       _.forEach(listeners, function(remove){
         remove();
@@ -274,13 +289,10 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
     }
 
     function onSettingsChanged(settings) {
-
-      var server = csHttp.getUrl(settings.node.host, settings.node.port, ''/*path*/, settings.node.useSsl);
-      var hasChanged = (server !== that.url);
-      if (hasChanged) {
-        init(settings.node.host, settings.node.port, settings.node.useSsl, that.useCache);
-        that.restart();
-      }
+      // Wait 1s (because settings controller can have restart the service), then copy the settings node
+      $timeout(function() {
+        exports.copy(settings.node);
+      }, 1000);
     }
 
     that.isStarted = function() {
@@ -308,12 +320,7 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
           });
       }
 
-      if (that.useSsl) {
-        console.debug('[BMA] Starting [{0}] (SSL on)...'.format(that.server));
-      }
-      else {
-        console.debug('[BMA] Starting [{0}]...'.format(that.server));
-      }
+      console.debug("[BMA] Starting {0} {ssl: {1})...".format(that.server, that.useSsl));
 
       var now = Date.now();
 
@@ -324,14 +331,14 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
         .then(function(res) {
           that.alive = res[1];
           if (!that.alive) {
-            console.error('[BMA] Could not start [{0}]: node unreachable'.format(that.server));
+            console.error("[BMA] Could not start {0} : unreachable".format(that.server));
             that.started = true;
             delete that._startPromise;
             return false;
           }
 
           // Add listeners
-          if (!listeners || listeners.length === 0) {
+          if (!listeners || !listeners.length) {
             addListeners();
           }
           console.debug('[BMA] Started in '+(Date.now()-now)+'ms');
@@ -345,14 +352,24 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
     };
 
     that.stop = function() {
+      if (!that.started && !that._startPromise) return $q.when(); // Skip multiple call
+
       console.debug('[BMA] Stopping...');
+
       removeListeners();
-      closeWs();
-      cleanCache();
-      that.alive = false;
-      that.started = false;
       delete that._startPromise;
-      that.api.node.raise.stop();
+
+      if (that.alive) {
+        closeWs();
+        cleanCache();
+        that.alive = false;
+        that.started = false;
+        that.api.node.raise.stop();
+      }
+      else {
+        that.started = false;
+      }
+      return $q.when();
     };
 
     that.restart = function() {
@@ -397,9 +414,7 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
       },
       node: {
         summary: get('/node/summary', csCache.constants.LONG),
-        same: function(host2, port2) {
-          return host2 === that.host && ((!that.port && !port2) || (that.port == port2||80)) && (that.useSsl == (port2 && port2 === 443));
-        },
+        same: isSameNode,
         forceUseSsl: that.forceUseSsl
       },
       network: {
@@ -680,14 +695,24 @@ angular.module('cesium.bma.services', ['ngApi', 'cesium.http.services', 'cesium.
     };
 
     exports.copy = function(otherNode) {
-      var wasStarted = that.started;
 
       var server = csHttp.getUrl(otherNode.host, otherNode.port, ''/*path*/, otherNode.useSsl);
       var hasChanged = (server !== that.url);
       if (hasChanged) {
+        var wasStarted = that.started;
+        if (wasStarted) that.stop();
         that.init(otherNode.host, otherNode.port, otherNode.useSsl, that.useCache/*keep original value*/);
-        // Restart (only if was already started)
-        return wasStarted ? that.restart() : $q.when();
+        if (wasStarted) {
+          return $timeout(function () {
+            return that.start()
+              .then(function (alive) {
+                if (alive) {
+                  that.api.node.raise.restart();
+                }
+                return alive;
+              });
+          }, 200); // Wait stop finished
+        }
       }
     };
 
diff --git a/www/plugins/es/js/controllers/network-controllers.js b/www/plugins/es/js/controllers/network-controllers.js
index c0648292..949bb2cb 100644
--- a/www/plugins/es/js/controllers/network-controllers.js
+++ b/www/plugins/es/js/controllers/network-controllers.js
@@ -431,7 +431,7 @@ function ESPeerInfoPopoverController($scope, $q, csSettings, csCurrency, csHttp,
 
     return $q.all([
       // get current block
-      csCurrency.blockchain.current()
+      esHttp.blockchain.current()
         .then(function(block) {
           $scope.formData.number = block.number;
           $scope.formData.medianTime = block.medianTime;
diff --git a/www/plugins/es/js/services/http-services.js b/www/plugins/es/js/services/http-services.js
index 36e4cfcb..2513603e 100644
--- a/www/plugins/es/js/services/http-services.js
+++ b/www/plugins/es/js/services/http-services.js
@@ -42,9 +42,7 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic
     that.data = {
       isFallback: false
     };
-    that.cache = {
-      enable: angular.isDefined(enableCache) ? enableCache : false, // need here because used in get() function
-    };
+    that.useCache = angular.isDefined(enableCache) ? enableCache : false; // need here because used in get() function
     that.raw = {
       getByPath: {},
       postByPath: {},
@@ -124,19 +122,25 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic
       return deferred.promise;
     }
 
-    that.clearAllCache = function() {
-      console.debug("[ES] [http] Cleaning cache {prefix: '{0}'}...".format(cachePrefix));
-      _.keys(that.raw.wsByPath).forEach(function(key) {
+    that.closeWs = function() {
+
+      if (!that.raw) return;
+
+      console.warn('[ES] [http] Closing all websockets...');
+      _.keys(that.raw.wsByPath||{}).forEach(function(key) {
         var sock = that.raw.wsByPath[key];
         sock.close();
       });
+      that.raw.wsByPath = {};
+    };
 
-      angular.merge(that.raw, {
-        getByPath: {},
-        postByPath: {},
-        wsByPath: {}
-      });
+    that.cleanCache = function() {
+      console.debug("[ES] [http] Cleaning cache {prefix: '{0}'}...".format(cachePrefix));
       csCache.clear(cachePrefix);
+
+      that.raw.getByPath = {};
+      that.raw.postByPath = {};
+      that.raw.wsByPath = {};
     };
 
     that.copy = function(otherNode) {
@@ -160,7 +164,7 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic
 
     that.get = function (path, cacheTime) {
 
-      cacheTime = that.cache.enable && cacheTime;
+      cacheTime = that.useCache && cacheTime;
       var requestKey = path + (cacheTime ? ('#'+cacheTime) : '');
 
       var getRequestFn = function(params) {
@@ -346,7 +350,6 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic
               console.debug('[ES] [http] Started in '+(Date.now()-now)+'ms');
               that.api.node.raise.start();
 
-
               that.started = true;
               delete that._startPromise;
               fallbackNodeIndex = 0; // reset the fallback node counter
@@ -359,6 +362,8 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic
     };
 
     that.stop = function() {
+      if (!that.started && !that._startPromise) return $q.when(); // Skip multiple call
+
       console.debug('[ES] [http] Stopping...');
 
       removeListeners();
@@ -366,7 +371,8 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic
       setIsFallbackNode(false); // will be re-computed during start phase
       delete that._startPromise;
       if (that.alive) {
-        that.clearAllCache();
+        that.closeWs();
+        that.cleanCache();
         that.alive = false;
         that.started = false;
         that.api.node.raise.stop();
@@ -729,6 +735,38 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic
       };
     }
 
+    // Get latest release, of Cesium+ pod
+    function getLatestVersion() {
+      var getRequest = that.raw.getLatestRelease;
+      if (!getRequest) {
+        var url = csHttp.uri.parse(csSettings.data.plugins.es.latestReleaseUrl);
+        var useSsl = (url.port == 443 || url.protocol === 'https:' || forceUseSsl);
+        getRequest = csHttp.getWithCache(url.host, url.port, "/" + url.pathname, useSsl, csCache.constants.LONG);
+        that.raw.getLatestRelease = getRequest
+      }
+
+      return getRequest()
+        .then(function (json) {
+          if (!json) return;
+          if (json.name && json.html_url) {
+            return {
+              version: json.name,
+              url: json.html_url
+            };
+          }
+          if (json.tag_name && json.html_url) {
+            return {
+              version: json.tag_name.substring(1),
+              url: json.html_url
+            };
+          }
+        })
+        .catch(function(err) {
+          // silent (just log it)
+          console.error('[BMA] Failed to get Duniter latest version', err);
+        });
+    }
+
     function addListeners() {
       // Watch some service events
       listeners = [
@@ -758,6 +796,9 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic
         sameAsSettings: isSameNodeAsSettings,
         isFallback: isFallbackNode
       },
+      version: {
+        latest: getLatestVersion
+      },
       websocket: {
         changes: that.wsChanges,
         block: that.ws('/ws/block'),
@@ -774,6 +815,9 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic
         },
         peers: that.get('/network/peers')
       },
+      blockchain: {
+        current: that.get('/blockchain/current?_source=number,hash,medianTime')
+      },
       record: {
         post: postRecord,
         remove: removeRecord,
@@ -791,9 +835,6 @@ angular.module('cesium.es.http.services', ['ngResource', 'ngApi', 'cesium.servic
         parseAsHtml: parseAsHtml,
         findObjectInTree: findObjectInTree
       },
-      cache: {
-        clearAll: that.clearAllCache
-      },
       constants: constants
     };
     exports.constants.regexp = regexp;
diff --git a/www/plugins/es/js/services/settings-services.js b/www/plugins/es/js/services/settings-services.js
index 90f888ae..b50438fa 100644
--- a/www/plugins/es/js/services/settings-services.js
+++ b/www/plugins/es/js/services/settings-services.js
@@ -41,6 +41,7 @@ angular.module('cesium.es.settings.services', ['cesium.services', 'cesium.es.htt
           es: {
             askEnable: false,
             useRemoteStorage: true,
+            latestReleaseUrl: "https://api.github.com/repos/duniter/cesium-plus-pod/releases/latest",
             notifications: {
               txSent: true,
               txReceived: true,
diff --git a/www/templates/menu.html b/www/templates/menu.html
index 18dc349f..284b767b 100644
--- a/www/templates/menu.html
+++ b/www/templates/menu.html
@@ -1,5 +1,5 @@
 <ion-side-menus enable-menu-with-back-views="true"
-                bind-notifier="{locale:$root.settings.locale.id}">
+                bind-notifier="{locale: $root.settings.locale.id, peer: $root.currency.node.url}">
   <!-- HEADER -->
   <ion-side-menu-content>
     <ion-nav-bar class="bar-dark" title-align="left">
@@ -17,8 +17,8 @@
                 ng-if="$root.settings.expertMode"
                 style="max-width: 450px !important;"
                 ng-click="showPeerInfoPopover($event)">
-          <small class="ion-locked" ng-if="$root.currency.node.useSsl">&nbsp;</small>
-          {{$root.currency.node.host}}{{$root.currency.node.port != 80 && $root.currency.node.port != 443 ? ':'+$root.currency.node.port : ''}}
+          <small class="ion-locked" ng-if=":peer:$root.currency.node.useSsl">&nbsp;</small>
+          {{:peer:$root.currency.node.host}}{{:peer:$root.currency.node.port != 80 && $root.currency.node.port != 443 ? ':'+$root.currency.node.port : ''}}
           <small>&nbsp;</small>
           <small class="ion-arrow-down-b"></small>
         </button>
-- 
GitLab