From 6462e2a2922f44558ddbae7f6ebca51d0a3a9ddf Mon Sep 17 00:00:00 2001
From: zzz <zzz@mail.i2p>
Date: Wed, 14 Nov 2018 14:48:10 +0000
Subject: [PATCH] MLab/NDT: Prep for connecting to wizard, fixes, cleanups

---
 .../src/com/vuze/plugins/mlab/MLabRunner.java |  84 ++++++-----
 .../i2p/router/web/helpers/WizardHandler.java | 133 ++++++++++++++++++
 .../i2p/router/web/helpers/WizardHelper.java  |   4 +-
 apps/routerconsole/jsp/index.jsp              |   7 +-
 apps/routerconsole/jsp/welcome.jsp            |  12 +-
 5 files changed, 200 insertions(+), 40 deletions(-)

diff --git a/apps/routerconsole/java/src/com/vuze/plugins/mlab/MLabRunner.java b/apps/routerconsole/java/src/com/vuze/plugins/mlab/MLabRunner.java
index fab96920e3..ea28c46f8a 100644
--- a/apps/routerconsole/java/src/com/vuze/plugins/mlab/MLabRunner.java
+++ b/apps/routerconsole/java/src/com/vuze/plugins/mlab/MLabRunner.java
@@ -28,6 +28,7 @@ import java.util.HashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import com.vuze.plugins.mlab.tools.ndt.swingemu.Tcpbw100UIWrapper;
 import com.vuze.plugins.mlab.tools.ndt.swingemu.Tcpbw100UIWrapperListener;
@@ -40,7 +41,6 @@ import net.minidev.json.parser.ParseException;
 
 import net.i2p.I2PAppContext;
 import net.i2p.data.DataHelper;
-import net.i2p.router.RouterContext;
 import net.i2p.util.EepGet;
 import net.i2p.util.I2PAppThread;
 import net.i2p.util.Log;
@@ -51,13 +51,14 @@ import net.i2p.util.Log;
  * @since 0.9.38
  */
 public class MLabRunner {
+    // ns.measurementlab.net does not support https
+    // use ndt_ssl for test over ssl? but Tcpbw100 doesn't support it
     private static final String NS_URL = "http://ns.measurementlab.net/ndt?format=json";
     private static final long NS_TIMEOUT = 20*1000;
     private boolean test_active;
     private final I2PAppContext _context;
-    // null for testing
-    private final RouterContext _rcontext;
     private final Log _log;
+    private final AtomicBoolean _running = new AtomicBoolean();
     private static MLabRunner _instance;
     
     public static MLabRunner getInstance(I2PAppContext ctx) {
@@ -70,13 +71,26 @@ public class MLabRunner {
 
     private MLabRunner(I2PAppContext ctx) {
         _context = ctx;
-        _rcontext = ctx.isRouterContext() ? (RouterContext) ctx : null;
         _log = ctx.logManager().getLog(MLabRunner.class);
     }
+
+    public boolean isRunning() {
+       return _running.get();
+    }
     
+    /**
+     * Non-blocking, spawns a thread and returns immediately.
+     *
+     * @param listener use to detect completion and get results
+     * @return a ToolRun object which may be used to cancel the test,
+     *         or null if there was already a test in progress.
+     */
     public ToolRun runNDT(final ToolListener listener) {
+        if (!_running.compareAndSet(false, true)) {
+            _log.warn("Test already running");
+            return null;
+        }
         final ToolRun run = new ToolRunImpl();
-        //final AESemaphore    sem = new AESemaphore( "waiter" );
         
         runTool(
             new Runnable()
@@ -85,7 +99,6 @@ public class MLabRunner {
                     boolean completed = false;
                     try{
                         _log.warn("Starting NDT Test");
-                        _log.warn("-----------------");
                         
                         new Tcpbw100UIWrapper(
                             new Tcpbw100UIWrapperListener()
@@ -141,7 +154,6 @@ public class MLabRunner {
                             // public EepGet(I2PAppContext ctx, boolean shouldProxy, String proxyHost, int proxyPort,
                             //               int numRetries, long minSize, long maxSize, String outputFile, OutputStream outputStream,
                             //               String url, boolean allowCaching, String etag, String postData) {
-                            // TODO why no HTTPS?
                             EepGet eepget = new EepGet(_context, false, null, 0,
                                                        0, 2, 1024, null, baos,
                                                        NS_URL, false, null, null);
@@ -160,6 +172,7 @@ public class MLabRunner {
                             if (_log.shouldWarn())
                                 _log.warn("Got response: " + DataHelper.getUTF8(b));
                             // TODO use IP instead to avoid another lookup?
+                            // or use "fqdn" in response instead of "url"
                             URL url = new URL((String)map.get( "url" ));
                             if (url == null) {
                                 throw new IOException("no url");
@@ -174,7 +187,7 @@ public class MLabRunner {
                         }
                         
                         if (server_host == null) {
-                                // fallback to old, discouraged approach
+                            // fallback to old, discouraged approach
                             server_host = "ndt.iupui.donar.measurement-lab.org";
                             if (_log.shouldWarn())
                                 _log.warn("Failed to select server, falling back to donar method");
@@ -191,8 +204,6 @@ public class MLabRunner {
                                 }
                             });
                         
-                        //sem.release();
-                        
                         test.runIt();
                         
                         try { Thread.sleep(2000); } catch (InterruptedException ie) { return; }
@@ -202,11 +213,13 @@ public class MLabRunner {
                             try { Thread.sleep(1000); } catch (InterruptedException ie) { break; }
                         }
 
+                        // in integer bytes per second
                         long up_bps = 0;
                         try {
                             up_bps = (long)(Double.parseDouble(test.get_c2sspd())*1000000)/8;
                         } catch(Throwable e) {}
                         
+                        // in integer bytes per second
                         long down_bps = 0;
                         try {
                             down_bps = (long)(Double.parseDouble(test.get_s2cspd())*1000000)/8;
@@ -236,54 +249,44 @@ public class MLabRunner {
                             _log.warn("Test complete in " + DataHelper.formatDuration(end - start));
                         }
                     } finally {
-                        //sem.release();
                         if (!completed && listener != null) {
                             listener.complete( new HashMap<String, Object>());
                         }
+                        _running.set(false);
                     }
                 }
             });
         
-        //sem.reserve();
         return run;
     }
     
-    protected void runTool(final Runnable target) {
-        // need something like this
-        //ap.setEnabled( false );
-        new I2PAppThread("toolRunner")
+    /**
+     * Non-blocking, spawns a thread and returns immediately.
+     */
+    private void runTool(final Runnable target) {
+        new I2PAppThread("MLabRunner")
         {
             @Override
             public void run() {
                 try{
                     target.run();
                 }finally{
-                    //ap.setEnabled( true );
                 }
             }
         }.start();
     }
     
-    public void runTest(
-        final Map<String,Object> args,
-        //final IPCInterface        callback,
-        final boolean autoApply)
-    
-        throws Exception
-    {
-        synchronized( this ){
-            if (test_active) {
-                throw new Exception("Test already active");
-            }
-            test_active = true;
-        }
-    }
-    
+    /**
+     * Returned from runNDT
+     */
     public interface ToolRun {
         public void cancel();
         public void addListener(ToolRunListener    l);
     }
     
+    /**
+     * Returned from runNDT
+     */
     private class ToolRunImpl implements ToolRun {
         private List<ToolRunListener> listeners = new ArrayList<ToolRunListener>();
         private boolean cancelled;
@@ -319,10 +322,12 @@ public class MLabRunner {
         }
     }
     
+    /** The listener for ToolRun */
     public interface ToolRunListener {
         public void cancelled();
     }
     
+    /** The parameter for runNDT() */
     public interface ToolListener {
         public void reportSummary(String str);
         public void reportDetail(String str);
@@ -331,6 +336,8 @@ public class MLabRunner {
 
     /** standalone test */
     private static class TestListener implements ToolListener {
+        private final AtomicBoolean _complete = new AtomicBoolean();
+
         public void reportSummary(String str) {
             System.out.println(str);
         }
@@ -341,6 +348,11 @@ public class MLabRunner {
 
         public void complete(Map<String, Object> results) {
             System.out.println("**************** Results: " + DataHelper.toString(results) + "***********************");
+            _complete.set(true);
+        }
+
+        public boolean isComplete() {
+            return _complete.get();
         }
     }
 
@@ -348,7 +360,13 @@ public class MLabRunner {
     public static void main(String[] args) {
         I2PAppContext ctx = I2PAppContext.getGlobalContext();
         MLabRunner mlab = MLabRunner.getInstance(ctx);
-        ToolListener lsnr = new TestListener();
+        TestListener lsnr = new TestListener();
         mlab.runNDT(lsnr);
+        try { Thread.sleep(2000); } catch (InterruptedException ie) { return; }
+        for (int i = 0; i < 180; i++) {
+            if (lsnr.isComplete())
+                break;
+            try { Thread.sleep(1000); } catch (InterruptedException ie) { break; }
+        }
     }
 }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/WizardHandler.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/WizardHandler.java
index c5c3d60941..7bf50fbddc 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/WizardHandler.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/WizardHandler.java
@@ -2,6 +2,9 @@ package net.i2p.router.web.helpers;
 
 import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import com.vuze.plugins.mlab.MLabRunner;
 
 import net.i2p.router.Router;
 import net.i2p.router.transport.FIFOBandwidthRefiller;
@@ -9,10 +12,23 @@ import net.i2p.router.web.FormHandler;
 
 /**
  *  The new user wizard.
+ *  This bean has SESSION scope so the results may be retrieved.
+ *  All necessary methods are synchronized.
  *
  *  @since 0.9.38
  */
 public class WizardHandler extends FormHandler {
+
+    // session scope, but it's an underlying singleton
+    private MLabRunner _mlab;
+    // session scope
+    private TestListener _listener;
+
+    @Override
+    public void setContextId(String contextId) {
+        super.setContextId(contextId);
+        _mlab = MLabRunner.getInstance(_context);
+    }
     
     @Override
     protected void processForm() {
@@ -99,4 +115,121 @@ public class WizardHandler extends FormHandler {
         }
         return updated; 
     }
+
+    public synchronized boolean isNDTComplete() {
+        return _listener != null && _listener.isComplete();
+    }
+
+    public synchronized boolean isNDTRunning() {
+        return _listener != null && !_listener.isComplete();
+    }
+
+    /**
+     * @return status string or null
+     */
+    public synchronized String getCompletionStatus() {
+        return _listener != null ? _listener.getSummary() : null;
+    }
+
+    /**
+     * @return status string or null
+     */
+    public synchronized String getDetailStatus() {
+        return _listener != null ? _listener.getDetail() : null;
+    }
+
+    /**
+     * @return bytes per second or 0
+     */
+    public long getUpBandwidth() {
+        return getLongResult("up");
+    }
+
+    /**
+     * @return bytes per second or 0
+     */
+    public long getDownBandwidth() {
+        return getLongResult("down");
+    }
+
+    public synchronized long getLongResult(String key) {
+        if (_listener != null) {
+            Map<String, Object> results = _listener.getResults();
+            if (results != null) {
+                Long v = (Long) results.get(key);
+                if (v != null)
+                    return v.longValue();
+            }
+        }
+        return 0;
+    }
+
+    /** start the test */
+    public synchronized void startNDT() {
+        if (_mlab.isRunning() || _listener != null && !_listener.isComplete()) {
+            addFormError(_t("Bandwidth test is already running"));
+            return;
+        }
+        _listener = new TestListener();
+        MLabRunner.ToolRun runner = _mlab.runNDT(_listener);
+        if (runner != null) {
+            addFormNotice(_t("Started bandwidth test"));
+        } else {
+            Map<String, Object> map = new HashMap<String, Object>(2);
+            _listener.complete(map);
+            addFormError(_t("Bandwidth test is already running"));
+        }
+    }
+
+    /** cancel the test */
+    public synchronized void cancelNDT() {
+        synchronized(WizardHandler.class) {
+            if (!_mlab.isRunning()) {
+                addFormError(_t("Bandwidth test was not running"));
+                return;
+            }
+/****
+TODO
+            if (runner != null)
+                addFormNotice(_t("Started bandwidth test"));
+            else
+                addFormError(_t("Bandwidth test is already running"));
+****/
+        }
+    }
+
+    /** test results */
+    private static class TestListener implements MLabRunner.ToolListener {
+        private String _summary, _detail;
+        private Map<String, Object> _results;
+
+        public synchronized void reportSummary(String str) {
+            _summary = str;
+        }
+
+        public synchronized void reportDetail(String str) {
+            _detail = str;
+        }
+
+        public synchronized void complete(Map<String, Object> results) {
+            _results = results;
+        }
+
+        public synchronized boolean isComplete() {
+            return _results != null;
+        }
+
+        public synchronized String getSummary() {
+            return _summary;
+        }
+
+        public synchronized String getDetail() {
+            return _detail;
+        }
+
+        public synchronized Map<String, Object> getResults() {
+            return _results;
+        }
+    }
+
 }
diff --git a/apps/routerconsole/java/src/net/i2p/router/web/helpers/WizardHelper.java b/apps/routerconsole/java/src/net/i2p/router/web/helpers/WizardHelper.java
index 8420e139d7..c3a326d006 100644
--- a/apps/routerconsole/java/src/net/i2p/router/web/helpers/WizardHelper.java
+++ b/apps/routerconsole/java/src/net/i2p/router/web/helpers/WizardHelper.java
@@ -9,7 +9,9 @@ import net.i2p.router.web.HelperBase;
  */
 public class WizardHelper extends HelperBase {
 
+    public static final String PROP_COMPLETE = "routerconsole.welcomeWizardComplete";
+
     public void complete() {
-        _context.router().saveConfig("routerconsole.welcomeWizardComplete", "true");
+        _context.router().saveConfig(PROP_COMPLETE, "true");
     }
 }
diff --git a/apps/routerconsole/jsp/index.jsp b/apps/routerconsole/jsp/index.jsp
index 10010d1819..bc2939e6cc 100644
--- a/apps/routerconsole/jsp/index.jsp
+++ b/apps/routerconsole/jsp/index.jsp
@@ -12,6 +12,7 @@
     //  while preserving any query parameters
     //
     response.setStatus(307);
+    response.setHeader("Cache-Control","no-cache");
     String req = request.getRequestURL().toString();
     StringBuilder buf = new StringBuilder(128);
     if (req.endsWith("index"))
@@ -23,13 +24,13 @@
         buf.append('/');
     net.i2p.I2PAppContext ctx = net.i2p.I2PAppContext.getGlobalContext();
     boolean oldHome = ctx.getBooleanProperty("routerconsole.oldHomePage");
-    boolean wizRun = ctx.getBooleanProperty("routerconsole.welcomeWizardComplete");
+    boolean wizRun = ctx.getBooleanProperty(net.i2p.router.web.helpers.WizardHelper.PROP_COMPLETE);
     String firstVersion = ctx.getProperty("router.firstVersion");
     String tgt;
     final boolean ENABLE_WIZARD_ON_FIRST_RUN = false;
     if (oldHome) {
         tgt = "console";
-    } else if (ENABLE_WIZARD_ON_FIRST_RUN && (wizRun || firstVersion == null)) {
+    } else if (!ENABLE_WIZARD_ON_FIRST_RUN || wizRun || firstVersion == null) {
         // wizard already run
         tgt = "home";
     } else {
@@ -52,4 +53,4 @@
     response.setHeader("Location", buf.toString());
     // force commitment
     response.getOutputStream().close();
-%>
\ No newline at end of file
+%>
diff --git a/apps/routerconsole/jsp/welcome.jsp b/apps/routerconsole/jsp/welcome.jsp
index 5056807248..906040c6b8 100644
--- a/apps/routerconsole/jsp/welcome.jsp
+++ b/apps/routerconsole/jsp/welcome.jsp
@@ -42,6 +42,7 @@
 
         // redirect to /home
         response.setStatus(307);
+        response.setHeader("Cache-Control","no-cache");
         String req = request.getRequestURL().toString();
         int slash = req.indexOf("/welcome");
         if (slash >= 0)
@@ -63,8 +64,8 @@
 <script src="/js/ajax.js" type="text/javascript"></script>
 <script type="text/javascript">
   var failMessage = "<hr><b><%=intl._t("Router is down")%><\/b>";
-  function requestAjax1() { ajax("/welcomexhr.jsp?requestURI=<%=request.getRequestURI()%>", "xhr", "1000"); }
-  function initAjax() { setTimeout(requestAjax1, <%=intl.getRefresh()%>000);  }
+  function requestAjax1() { ajax("/welcomexhr.jsp", "xhr", "1000"); }
+  function initAjax() { setTimeout(requestAjax1, "1000");  }
 </script>
 <%
     }
@@ -83,7 +84,12 @@
 %>
 <h2><%=intl._t("New Install Wizard")%> <%=ipg%>/<%=LAST_PAGE%></h2>
 <div id="wizard">
-<jsp:useBean class="net.i2p.router.web.helpers.WizardHandler" id="formhandler" scope="request" />
+<%--
+    // note that for the handler we use a session scope, not a page scope,
+    // so that we can access the NDT test results.
+    // The MLabHelper singleton will prevent multiple simultaneous tests, even across sessions.
+--%>
+<jsp:useBean class="net.i2p.router.web.helpers.WizardHandler" id="formhandler" scope="session" />
 <%@include file="formhandler.jsi" %>
 <form action="" method="POST">
 <input type="hidden" name="nonce" value="<%=pageNonce%>">
-- 
GitLab