[tor-commits] [tor-launcher/master] Add "Copy Tor Log To Clipboard" button to network settings dialog.

brade at torproject.org brade at torproject.org
Wed Apr 17 15:12:05 UTC 2013


commit d91c02620cd8960d11a073f4fb3f0f0ce031bb53
Author: Kathy Brade <brade at torproject.org>
Date:   Wed Apr 17 10:48:17 2013 -0400

    Add "Copy Tor Log To Clipboard" button to network settings dialog.
    
    Use SETEVENTS with a non-blocking control connection to capture log messages.
    Add logic to wait until tor control port is ready before reading settings, etc.
    Close progress dialog if tor exits while dialog is open.
---
 src/chrome/content/network-settings.js      |  106 +++++++++-
 src/chrome/content/network-settings.xul     |  219 ++++++++++---------
 src/chrome/content/progress.js              |   18 ++-
 src/chrome/locale/en/network-settings.dtd   |    3 +
 src/chrome/locale/en/torlauncher.properties |    1 +
 src/chrome/skin/network-settings.css        |   11 +-
 src/components/tl-process.js                |   95 ++++++++-
 src/components/tl-protocol.js               |  305 +++++++++++++++++++++++----
 8 files changed, 584 insertions(+), 174 deletions(-)

diff --git a/src/chrome/content/network-settings.js b/src/chrome/content/network-settings.js
index 85f7422..069b8d1 100644
--- a/src/chrome/content/network-settings.js
+++ b/src/chrome/content/network-settings.js
@@ -15,6 +15,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "TorLauncherUtil",
 XPCOMUtils.defineLazyModuleGetter(this, "TorLauncherLogger",
                           "resource://torlauncher/modules/tl-logger.jsm");
 
+const kTorProcessReadyTopic = "TorProcessIsReady";
+const kTorProcessExitedTopic = "TorProcessExited";
+const kTorProcessDidNotStartTopic = "TorProcessDidNotStart";
+
 const kUseProxyCheckbox = "useProxy";
 const kProxyTypeMenulist = "proxyType";
 const kProxyAddr = "proxyAddr";
@@ -39,13 +43,13 @@ const kTorConfKeyBridgeList = "Bridge";
 
 var gProtocolSvc = null;
 var gTorProcessService = null;
+var gObsService = null;
 var gIsInitialBootstrap = false;
 var gIsBootstrapComplete = false;
 
 
 function initDialog()
 {
-  var okBtn = document.documentElement.getButton("accept");
   gIsInitialBootstrap = window.arguments[0];
   if (gIsInitialBootstrap)
   {
@@ -55,11 +59,10 @@ function initDialog()
     var quitKey = (TorLauncherUtil.isWindows) ? "quit_win" : "quit";
     cancelBtn.label = TorLauncherUtil.getLocalizedString(quitKey);
 
+    var okBtn = document.documentElement.getButton("accept");
     okBtn.label = TorLauncherUtil.getLocalizedString("connect");
   }
 
-  okBtn.focus();
-
   try
   {
     var svc = Cc["@torproject.org/torlauncher-protocol-service;1"]
@@ -76,8 +79,50 @@ function initDialog()
   }
   catch (e) { dump(e + "\n"); }
 
-  TorLauncherLogger.log(2, "initDialog retrieving tor settings " +
-                             "----------------------------------------------");
+  gObsService = Cc["@mozilla.org/observer-service;1"]
+                  .getService(Ci.nsIObserverService);
+
+  if (gTorProcessService.TorIsProcessReady)
+  {
+    showOrHideSettings(true, false);
+    readTorSettings();
+  }
+  else
+  { 
+    showOrHideSettings(false, true);
+    gObsService.addObserver(gObserver, kTorProcessReadyTopic, false);
+    gObsService.addObserver(gObserver, kTorProcessExitedTopic, false);
+    gObsService.addObserver(gObserver, kTorProcessDidNotStartTopic, false);
+  }
+
+  TorLauncherLogger.log(2, "initDialog done");
+}
+
+
+var gObserver = {
+  observe: function(aSubject, aTopic, aData)
+  {
+    gObsService.removeObserver(gObserver, kTorProcessReadyTopic);
+    gObsService.removeObserver(gObserver, kTorProcessExitedTopic);
+    gObsService.removeObserver(gObserver, kTorProcessDidNotStartTopic);
+
+    if (kTorProcessReadyTopic == aTopic)
+    {
+      showOrHideSettings(true, false);
+      readTorSettings();
+    }
+    else if (kTorProcessDidNotStartTopic == aTopic)
+      showOrHideSettings(false, false);
+    else
+      close();
+  }
+};
+
+
+function readTorSettings()
+{
+  TorLauncherLogger.log(2, "readTorSettings " +
+                            "----------------------------------------------");
 
   var didSucceed = false;
   try
@@ -86,14 +131,12 @@ function initDialog()
     didSucceed = initProxySettings() && initFirewallSettings() &&
                  initBridgeSettings();
   }
-  catch (e) { TorLauncherLogger.safelog(4, "Error in initDialog: ", e); }
+  catch (e) { TorLauncherLogger.safelog(4, "Error in readTorSettings: ", e); }
 
   if (!didSucceed)
   {
     // Unable to communicate with tor.  Hide settings and display an error.
-    setElemValue(kUseProxyCheckbox, false);
-    setElemValue(kUseFirewallPortsCheckbox, false);
-    setElemValue(kUseBridgesCheckbox, false);
+    showOrHideSettings(false, false);
 
     setTimeout(function()
         {
@@ -105,11 +148,44 @@ function initDialog()
           close();
         }, 0);
   }
+  TorLauncherLogger.log(2, "readTorSettings done");
+}
+
+
+function showOrHideSettings(aShow, aIsStartingTor)
+{
+  showOrHideElem("settings", aShow);
+  showOrHideElem("startingTor", !aShow && aIsStartingTor);
 
-  TorLauncherLogger.log(2, "initDialog done\n\n\n");
+  var okBtn = document.documentElement.getButton("accept");
+  if (okBtn)
+  {
+    if (aShow)
+    {
+      okBtn.removeAttribute("hidden");
+      okBtn.focus();
+    }
+    else
+      okBtn.setAttribute("hidden", true);
+  }
 }
 
 
+function showOrHideElem(aID, aShow)
+{
+  if (!aID)
+    return;
+
+  var e = document.getElementById(aID);
+  if (e)
+  {
+    if (aShow)
+      e.removeAttribute("hidden");
+    else
+      e.setAttribute("hidden", true);
+  }
+}
+
 function onCancel()
 {
   if (gIsInitialBootstrap) try
@@ -123,6 +199,14 @@ function onCancel()
 }
 
 
+function onCopyLog()
+{
+  var chSvc = Cc["@mozilla.org/widget/clipboardhelper;1"]
+                             .getService(Ci.nsIClipboardHelper);
+  chSvc.copyString(gProtocolSvc.TorGetLog());
+}
+
+
 // Returns true if successful.
 function initProxySettings()
 {
@@ -281,7 +365,7 @@ function applySettings()
       close();
   }
 
-  TorLauncherLogger.log(2, "applySettings done\n\n\n");
+  TorLauncherLogger.log(2, "applySettings done");
 
   return false;
 }
diff --git a/src/chrome/content/network-settings.xul b/src/chrome/content/network-settings.xul
index 5daabac..06e817b 100644
--- a/src/chrome/content/network-settings.xul
+++ b/src/chrome/content/network-settings.xul
@@ -15,118 +15,131 @@
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
         title="&torsettings.dialog.title;"
         persist="screenX screenY"
-        buttons="accept,cancel"
+        buttons="accept,cancel,extra1"
+        buttonlabelextra1="&torsettings.copyLog;"
         ondialogaccept="return applySettings();"
         ondialogcancel="return onCancel();"
+        ondialogextra1="onCopyLog();"
         onload="initDialog();">
 
-    <script type="application/x-javascript"
-            src="chrome://torlauncher/content/network-settings.js"/>
+  <script type="application/x-javascript"
+          src="chrome://torlauncher/content/network-settings.js"/>
 
-  <vbox>
-  <vbox flex="1">
-    <hbox id="tbb-header">
-      <vbox>
-        <spacer flex="1" />
-        <image id="tbb-logo" />
-        <spacer flex="1" />
-      </vbox>
-      <separator orient="vertical" />
-      <groupbox>
-        <description>&torsettings.prompt1;</description>
+  <stack>
+    <vbox id="settings">
+      <vbox flex="1">
+        <hbox id="tbb-header">
+          <vbox>
+            <spacer flex="1" />
+            <image id="tbb-logo" />
+            <spacer flex="1" />
+          </vbox>
+          <separator orient="vertical" />
+          <groupbox>
+            <description>&torsettings.prompt1;</description>
+            <separator orient="horizontal" class="thin" />
+            <description>&torsettings.prompt2;</description>
+            <description>&torsettings.prompt3;</description>
+            <description>&torsettings.prompt4;</description>
+          </groupbox>
+        </hbox>
         <separator orient="horizontal" class="thin" />
-        <description>&torsettings.prompt2;</description>
-        <description>&torsettings.prompt3;</description>
-        <description>&torsettings.prompt4;</description>
-      </groupbox>
-    </hbox>
-    <separator orient="horizontal" class="thin" />
-    <checkbox id="useProxy" groupboxID="proxySpecificSettings"
-              label="&torsettings.useProxy.checkbox;"
-              oncommand="toggleElemUI(this)"/>
-    <groupbox id="proxySpecificSettings">
-      <grid flex="1">
-        <columns>
-          <column/>
-          <column/>
-        </columns>
-        <rows>
-          <row align="center">
-            <label value="&torsettings.useProxy.address;" control="proxyAddr"
-                   style="text-align:right" />
-            <hbox align="center">
-              <textbox id="proxyAddr" size="30" flex="1" />
-              <label value="&torsettings.useProxy.port;" control="proxyPort"/>
-              <textbox id="proxyPort" size="5" />
-            </hbox>
-          </row>
-          <row align="center">
-            <label value="&torsettings.useProxy.username;"
-                   control="proxyUsername" style="text-align:right" />
-            <hbox align="center">
-              <textbox id="proxyUsername" flex="1" />
-              <label value="&torsettings.useProxy.password;"
-                     control="proxyPassword"/>
-              <textbox id="proxyPassword" type="password" />
-            </hbox>
-          </row>
-          <row align="center">
-            <label value="&torsettings.useProxy.type;" control="proxyType"
-                   style="text-align:right" />
-            <hbox align="center">
-              <menulist id="proxyType">
-                <menupopup id="proxyType_menuPopup">
-                  <menuitem label="&torsettings.useProxy.type.socks4;"
-                            value="SOCKS4"/>
-                  <menuitem label="&torsettings.useProxy.type.socks5;"
-                            value="SOCKS5"/>
-                  <menuitem label="&torsettings.useProxy.type.http;"
-                            value="HTTP"/>
-                </menupopup>
-              </menulist>
-            </hbox>
-          </row>
-        </rows>
-      </grid>
-    </groupbox>
-  </vbox>
+        <checkbox id="useProxy" groupboxID="proxySpecificSettings"
+                  label="&torsettings.useProxy.checkbox;"
+                  oncommand="toggleElemUI(this)"/>
+        <groupbox id="proxySpecificSettings">
+          <grid flex="1">
+            <columns>
+              <column/>
+              <column/>
+            </columns>
+            <rows>
+              <row align="center">
+                <label value="&torsettings.useProxy.address;" control="proxyAddr"
+                       style="text-align:right" />
+                <hbox align="center">
+                  <textbox id="proxyAddr" size="30" flex="1" />
+                  <label value="&torsettings.useProxy.port;" control="proxyPort"/>
+                  <textbox id="proxyPort" size="5" />
+                </hbox>
+              </row>
+              <row align="center">
+                <label value="&torsettings.useProxy.username;"
+                       control="proxyUsername" style="text-align:right" />
+                <hbox align="center">
+                  <textbox id="proxyUsername" flex="1" />
+                  <label value="&torsettings.useProxy.password;"
+                         control="proxyPassword"/>
+                  <textbox id="proxyPassword" type="password" />
+                </hbox>
+              </row>
+              <row align="center">
+                <label value="&torsettings.useProxy.type;" control="proxyType"
+                       style="text-align:right" />
+                <hbox align="center">
+                  <menulist id="proxyType">
+                    <menupopup id="proxyType_menuPopup">
+                      <menuitem label="&torsettings.useProxy.type.socks4;"
+                                value="SOCKS4"/>
+                      <menuitem label="&torsettings.useProxy.type.socks5;"
+                                value="SOCKS5"/>
+                      <menuitem label="&torsettings.useProxy.type.http;"
+                                value="HTTP"/>
+                    </menupopup>
+                  </menulist>
+                </hbox>
+              </row>
+            </rows>
+          </grid>
+        </groupbox>
+      </vbox>
 
-  <vbox flex="1">
-    <checkbox id="useFirewallPorts" groupboxID="firewallSpecificSettings"
-              label="&torsettings.firewall.checkbox;"
-              oncommand="toggleElemUI(this)"/>
-    <groupbox id="firewallSpecificSettings">
-      <hbox align="center">
-        <label value="&torsettings.firewall.allowedPorts;"
-               control="firewallAllowedPorts"/>
-        <textbox id="firewallAllowedPorts" value="80,443" />
-      </hbox>
-    </groupbox>
-  </vbox>
+      <vbox flex="1">
+        <checkbox id="useFirewallPorts" groupboxID="firewallSpecificSettings"
+                  label="&torsettings.firewall.checkbox;"
+                  oncommand="toggleElemUI(this)"/>
+        <groupbox id="firewallSpecificSettings">
+          <hbox align="center">
+            <label value="&torsettings.firewall.allowedPorts;"
+                   control="firewallAllowedPorts"/>
+            <textbox id="firewallAllowedPorts" value="80,443" />
+          </hbox>
+        </groupbox>
+      </vbox>
 
-  <vbox flex="1">
-    <checkbox id="useBridges" groupboxID="bridgeSpecificSettings"
-              label="&torsettings.useBridges.checkbox;"
-              oncommand="toggleElemUI(this);" />
-    <groupbox id="bridgeSpecificSettings">
+      <vbox flex="1">
+        <checkbox id="useBridges" groupboxID="bridgeSpecificSettings"
+                  label="&torsettings.useBridges.checkbox;"
+                  oncommand="toggleElemUI(this);" />
+        <groupbox id="bridgeSpecificSettings">
+          <hbox>
+            <vbox flex="1">
+              <label value="&torsettings.useBridges.label;" control="bridgeList"/>
+              <textbox id="bridgeList" multiline="true" rows="4" />
+            </vbox>
+    <!--
+            <vbox>
+              <button id="copyBridgeList" label="Copy TODO" style="width: 20px"
+                      oncommand="copyBridgeList();"/>
+              <button id="pasteBridgeList" label="Paste TODO" style="width: 20px"
+                      oncommand="pasteBridgeList();"/>
+              <button id="howToFindBridges" label="How can I find bridges? TODO" style="width: 20px"
+                        oncommand="findBridgeList();"/>
+            </vbox>
+    -->
+          </hbox>
+        </groupbox>
+      </vbox>
+    </vbox>
+    <vbox id="startingTor" flex="1">
+      <spring flex="1" />
       <hbox>
-        <vbox flex="1">
-          <label value="&torsettings.useBridges.label;" control="bridgeList"/>
-          <textbox id="bridgeList" multiline="true" rows="4" />
-        </vbox>
-<!--
-        <vbox>
-          <button id="copyBridgeList" label="Copy TODO" style="width: 20px"
-                  oncommand="copyBridgeList();"/>
-          <button id="pasteBridgeList" label="Paste TODO" style="width: 20px"
-                  oncommand="pasteBridgeList();"/>
-          <button id="howToFindBridges" label="How can I find bridges? TODO" style="width: 20px"
-                    oncommand="findBridgeList();"/>
-        </vbox>
--->
+        <spring flex="1" />
+        <label value="&torsettings.startingTor;"/>
+        <spring flex="1" />
       </hbox>
-    </groupbox>
-  </vbox>
-  </vbox>
+      <spring flex="1" />
+    </vbox>
+  </stack>
   <spring />
 </dialog>
diff --git a/src/chrome/content/progress.js b/src/chrome/content/progress.js
index 0f21d4c..865ddec 100644
--- a/src/chrome/content/progress.js
+++ b/src/chrome/content/progress.js
@@ -7,6 +7,7 @@ const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
+const kTorProcessExitedTopic = "TorProcessExited";
 const kBootstrapStatusTopic = "TorBootstrapStatus";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@@ -23,7 +24,8 @@ function initDialog()
   {
     gObsSvc = Cc["@mozilla.org/observer-service;1"]
                   .getService(Ci.nsIObserverService);
-    gObsSvc.addObserver(BootstrapStatusObserver, kBootstrapStatusTopic, false);
+    gObsSvc.addObserver(gObserver, kTorProcessExitedTopic, false);
+    gObsSvc.addObserver(gObserver, kBootstrapStatusTopic, false);
   }
   catch (e) {}
 
@@ -57,7 +59,10 @@ function initDialog()
 function cleanup()
 {
   if (gObsSvc)
-    gObsSvc.removeObserver(BootstrapStatusObserver, kBootstrapStatusTopic);
+  {
+    gObsSvc.removeObserver(gObserver, kTorProcessExitedTopic);
+    gObsSvc.removeObserver(gObserver, kBootstrapStatusTopic);
+  }
 }
 
 
@@ -92,11 +97,16 @@ function onCancel()
 }
 
 
-var BootstrapStatusObserver = {
+var gObserver = {
   // nsIObserver implementation.
   observe: function(aSubject, aTopic, aParam)
   {
-    if (kBootstrapStatusTopic == aTopic)
+    if (kTorProcessExitedTopic == aTopic)
+    {
+      onCancel();
+      window.close();
+    }
+    else if (kBootstrapStatusTopic == aTopic)
     {
       var statusObj = aSubject.wrappedJSObject;
       var labelText = (statusObj.SUMMARY) ? statusObj.SUMMARY : "";
diff --git a/src/chrome/locale/en/network-settings.dtd b/src/chrome/locale/en/network-settings.dtd
index 5d5fa99..3382b17 100644
--- a/src/chrome/locale/en/network-settings.dtd
+++ b/src/chrome/locale/en/network-settings.dtd
@@ -5,6 +5,8 @@
 <!ENTITY torsettings.prompt3 "If so, choose those options for further configuration.">
 <!ENTITY torsettings.prompt4 "If not, just click Connect.">
 
+<!ENTITY torsettings.startingTor "Waiting for Tor to start…">
+
 <!ENTITY torsettings.useProxy.checkbox "I use a proxy to access the Internet">
 <!ENTITY torsettings.useProxy.address "Address:">
 <!ENTITY torsettings.useProxy.port "Port:">
@@ -18,3 +20,4 @@
 <!ENTITY torsettings.firewall.allowedPorts "Allowed Ports:">
 <!ENTITY torsettings.useBridges.checkbox "My ISP blocks connections to the Tor network">
 <!ENTITY torsettings.useBridges.label "Enter one or more bridge relays in the form address:port.">
+<!ENTITY torsettings.copyLog "Copy Tor Log To Clipboard">
diff --git a/src/chrome/locale/en/torlauncher.properties b/src/chrome/locale/en/torlauncher.properties
index 9331ecf..0453faa 100644
--- a/src/chrome/locale/en/torlauncher.properties
+++ b/src/chrome/locale/en/torlauncher.properties
@@ -4,6 +4,7 @@
 torlauncher.error_title=Tor Launcher
 
 torlauncher.tor_exited=Tor unexpectedly exited.
+torlauncher.tor_controlconn_failed=Could not connect to Tor control port.
 torlauncher.tor_failed_to_start=Tor failed to start.
 torlauncher.tor_bootstrap_failed=Tor failed to establish a Tor network connection.\n\n%S
 
diff --git a/src/chrome/skin/network-settings.css b/src/chrome/skin/network-settings.css
index 3297ffc..e165bca 100644
--- a/src/chrome/skin/network-settings.css
+++ b/src/chrome/skin/network-settings.css
@@ -6,7 +6,7 @@
  */
 
 dialog {
-  width: 592px;
+  width: 635px;
   height: 440px;
   font: -moz-dialog;
 }
@@ -32,3 +32,12 @@ dialog.initialBootstrap #tbb-header {
   width: 120px;
   height: 100px;
 }
+
+#startingTor {
+  min-height: 300px;
+}
+
+#startingTor label {
+  font-size: 120%;
+  font-weight: bold;
+}
diff --git a/src/components/tl-process.js b/src/components/tl-process.js
index 740c7cf..0148529 100644
--- a/src/components/tl-process.js
+++ b/src/components/tl-process.js
@@ -31,6 +31,9 @@ TorProcessService.prototype =
   kClassID: Components.ID("{FE7B4CAF-BCF4-4848-8BFF-EFA66C9AFDA1}"),
 
   kPrefPromptAtStartup: "extensions.torlauncher.prompt_at_startup",
+  kInitialControlConnDelayMS: 25,
+  kMaxControlConnRetryMS: 500,
+  kControlConnTimeoutMS: 30000, // Wait at most 30 seconds for tor to start.
   kInitialMonitorDelayMS: 1000, // TODO: how can we avoid this delay?
   kMonitorDelayMS: 200,
 
@@ -103,9 +106,17 @@ TorProcessService.prototype =
     }
     else if (("process-failed" == aTopic) || ("process-finished" == aTopic))
     {
+      if (this.mControlConnTimer)
+      {
+        this.mControlConnTimer.cancel();
+        this.mControlConnTimer = null;
+      }
+
       this.TorMonitorBootstrap(false);
       this.mTorProcess = null;
 
+      this.mObsSvc.notifyObservers(null, "TorProcessExited", null);
+
       if (!this.mIsQuitting)
       {
         var s = TorLauncherUtil.getLocalizedString("tor_exited");
@@ -115,9 +126,50 @@ TorProcessService.prototype =
     }
     else if ("timer-callback" == aTopic)
     {
-      this._checkBootStrapStatus();
-      if (this.mTimer)
-        this.mTimer.init(this, this.kMonitorDelayMS, this.mTimer.TYPE_ONE_SHOT);
+      if (aSubject == this.mControlConnTimer)
+      {
+        var haveConnection = this.mProtocolSvc.TorHaveControlConnection();
+        if (haveConnection)
+        {
+          this.mControlConnTimer = null;
+          this.mIsTorProcessReady = true;
+          this.mProtocolSvc.TorStartEventMonitor();
+
+          var isInitialBootstrap =
+                     TorLauncherUtil.getBoolPref(this.kPrefPromptAtStartup);
+          if (!isInitialBootstrap)
+            this.TorMonitorBootstrap(true);
+
+          this.mObsSvc.notifyObservers(null, "TorProcessIsReady", null);
+        }
+        else if ((Date.now() - this.mTorProcessStartTime)
+                 > this.kControlConnTimeoutMS)
+        {
+          this.mObsSvc.notifyObservers(null, "TorProcessDidNotStart", null);
+          var s = TorLauncherUtil.getLocalizedString("tor_controlconn_failed");
+          TorLauncherUtil.showAlert(null, s);
+          TorLauncherLogger.log(4, s);
+        }
+        else
+        {
+          this.mControlConnDelayMS *= 2;
+          if (this.mControlConnDelayMS > this.kMaxControlConnRetryMS)
+            this.mControlConnDelayMS = this.kMaxControlConnRetryMS;
+          this.mControlConnTimer = Cc["@mozilla.org/timer;1"]
+                                  .createInstance(Ci.nsITimer);
+          this.mControlConnTimer.init(this, this.mControlConnDelayMS,
+                                      this.mControlConnTimer .TYPE_ONE_SHOT);
+        }
+      }
+      else if (aSubject == this.mTimer)
+      {
+        this._checkBootStrapStatus();
+        if (this.mTimer)
+        {
+          this.mTimer.init(this, this.kMonitorDelayMS,
+                           this.mTimer.TYPE_ONE_SHOT);
+        }
+      }
     }
     else if (kOpenNetworkSettingsTopic == aTopic)
       this._openNetworkSettings(false);
@@ -157,7 +209,12 @@ TorProcessService.prototype =
 
   lockFactory: function (aDoLock) {},
 
-  // Public Constants and Methods ////////////////////////////////////////////
+  // Public Properties and Methods ///////////////////////////////////////////
+  get TorIsProcessReady()
+  {
+    return (this.mTorProcess) ? this.mIsTorProcessReady : false;
+  },
+
   TorMonitorBootstrap: function(aEnable)
   {
     if (!this.mProtocolSvc)
@@ -177,24 +234,32 @@ TorProcessService.prototype =
     if (this.mTimer)
       return;
 
-    this.mTimer = Components.classes["@mozilla.org/timer;1"]
-                            .createInstance(Components.interfaces.nsITimer);
+    this.mTimer = Cc["@mozilla.org/timer;1"]
+                            .createInstance(Ci.nsITimer);
     this.mTimer.init(this, this.kInitialMonitorDelayMS,
                      this.mTimer.TYPE_ONE_SHOT);
   },
 
+
   // Private Member Variables ////////////////////////////////////////////////
+  mIsTorProcessReady: false,
   mIsQuitting: false,
   mObsSvc: null,
   mProtocolSvc: null,
   mTorProcess: null,    // nsIProcess
+  mTorProcessStartTime: null, // JS Date.now()
   mTBBTopDir: null,     // nsIFile for top of TBB installation (cached)
+  mControlConnTimer: null,
+  mControlConnDelayMS: 0,
   mTimer: null,
   mQuitSoon: false,     // Quit was requested by the user; do so soon.
 
+
   // Private Methods /////////////////////////////////////////////////////////
   _startTor: function()
   {
+    this.mIsTorProcessReady = false;
+
     var isInitialBootstrap =
                      TorLauncherUtil.getBoolPref(this.kPrefPromptAtStartup);
 
@@ -249,8 +314,7 @@ TorProcessService.prototype =
         args.push("1");
       }
 
-      var p = Components.classes["@mozilla.org/process/util;1"]
-                        .createInstance(Components.interfaces.nsIProcess);
+      var p = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
       p.init(exeFile);
 
       TorLauncherLogger.log(2, "Starting " + exeFile.path);
@@ -259,6 +323,9 @@ TorProcessService.prototype =
 
       p.runAsync(args, args.length, this, false);
       this.mTorProcess = p;
+      this.mTorProcessStartTime = Date.now();
+
+      this._waitForTorProcessStartup();
 
       if (isInitialBootstrap)
       {
@@ -273,10 +340,7 @@ TorProcessService.prototype =
           this._openNetworkSettings(true); // Blocks until dialog is closed.
       }
       else
-      {
-        this.TorMonitorBootstrap(true);
         this._openProgressDialog();
-      }
 
       // If the user pressed "Quit" within settings/progress, exit.
       if (this.mQuitSoon) try
@@ -300,6 +364,15 @@ TorProcessService.prototype =
     }
   }, // _startTor()
 
+  _waitForTorProcessStartup: function()
+  {
+    this.mControlConnDelayMS = this.kInitialControlConnDelayMS;
+    this.mControlConnTimer = Cc["@mozilla.org/timer;1"]
+                               .createInstance(Ci.nsITimer);
+    this.mControlConnTimer.init(this, this.mControlConnDelayMS,
+                                this.mControlConnTimer.TYPE_ONE_SHOT);
+  },
+
   _checkBootStrapStatus: function()
   {
     var statusObj = this.mProtocolSvc.TorGetBootStrapStatus();
diff --git a/src/components/tl-protocol.js b/src/components/tl-protocol.js
index 2dee37f..22df4a6 100644
--- a/src/components/tl-protocol.js
+++ b/src/components/tl-protocol.js
@@ -130,6 +130,7 @@ TorProtocolService.prototype =
 
   // Public Constants and Methods ////////////////////////////////////////////
   kCmdStatusOK: 250,
+  kCmdStatusEventNotification: 650,
 
   // Returns Tor password string or null if an error occurs.
   TorGetPassword: function(aPleaseHash)
@@ -352,25 +353,96 @@ TorProtocolService.prototype =
   // TorCleanupConnection() is called during browser shutdown.
   TorCleanupConnection: function()
   {
-      this._closeConnection();
+    this._closeConnection();
+    this._closeConnection(this.mEventMonitorConnection);
+    this.mEventMonitorConnection = null;
   },
 
+  TorStartEventMonitor: function()
+  {
+    if (this.mEventMonitorConnection)
+      return;
+
+    var conn = this._openAuthenticatedConnection(true);
+    // TODO: optionally monitor INFO and DEBUG log messages.
+    var events = "STATUS_CLIENT NOTICE WARN ERR";
+    var reply = this._sendCommand(conn, "SETEVENTS", events);
+    if (!this.TorCommandSucceeded(reply))
+    {
+      TorLauncherLogger.log(4, "SETEVENTS failed");
+      this._closeConnection(conn);
+      return;
+    }
+
+    this.mEventMonitorConnection = conn;
+    this._waitForEventData();
+  },
+
+  // Returns captured log message as a text string (one message per line).
+  TorGetLog: function()
+  {
+    let s = "";
+    if (this.mTorLog)
+    {
+      let dateFmtSvc = Cc["@mozilla.org/intl/scriptabledateformat;1"]
+                      .getService(Ci.nsIScriptableDateFormat);
+      let dateFormat = dateFmtSvc.dateFormatShort;
+      let timeFormat = dateFmtSvc.timeFormatSecondsForce24Hour;
+      for (let i = 0; i < this.mTorLog.length; ++i)
+      {
+        let logObj = this.mTorLog[i];
+        let secs = logObj.date.getSeconds();
+        let timeStr = dateFmtSvc.FormatDateTime("", dateFormat, timeFormat,
+                         logObj.date.getFullYear(), logObj.date.getMonth() + 1,
+                             logObj.date.getDate(), logObj.date.getHours(),
+                             logObj.date.getMinutes(), secs);
+        if (' ' == timeStr.substr(-1))
+          timeStr = timeStr.substr(0, timeStr.length - 1);
+        let fracSecsStr = "" + logObj.date.getMilliseconds();
+        while (fracSecsStr.length < 3)
+          fracSecsStr += "0";
+        timeStr += '.' + fracSecsStr;
+
+        s += timeStr + " [" + logObj.type + "] " + logObj.msg + "\n";
+      }
+    }
+
+    return s;
+  },
+
+
+  // Return true if a control connection is established (will create a
+  // connection if necessary).
+  TorHaveControlConnection: function()
+  {
+    var conn = this._getConnection();
+    this._returnConnection(conn);
+    return (conn != null);
+  },
+
+
   // Private Member Variables ////////////////////////////////////////////////
   mObsSvc: null,
   mControlPort: null,
   mControlHost: null,
   mControlPassword: null,     // JS string that contains hex-encoded password.
   mControlConnection: null,   // This is cached and reused.
+  mEventMonitorConnection: null,
+  mEventMonitorBuffer: null,
+  mEventMonitorInProgressReply: null,
+  mTorLog: null,      // Array of objects with date, type, and msg properties.
+
   mCheckPasswordHash: false,  // set to true to perform a unit test
 
   // Private Methods /////////////////////////////////////////////////////////
 
   // Returns a JS object that contains these fields:
-  //   inUse      // Boolean
-  //   useCount   // Integer
-  //   socket     // nsISocketTransport
-  //   inStream   // nsIBinaryInputStream
-  //   outStream  // nsIBinaryOutputStream
+  //   inUse        // Boolean
+  //   useCount     // Integer
+  //   socket       // nsISocketTransport
+  //   inStream     // nsIInputStream
+  //   binInStream  // nsIBinaryInputStream
+  //   binOutStream // nsIBinaryOutputStream
   _getConnection: function()
   {
     if (this.mControlConnection)
@@ -382,8 +454,7 @@ TorProtocolService.prototype =
       }
     }
     else
-      this.mControlConnection = this._openAuthenticatedConnection();
-
+      this.mControlConnection = this._openAuthenticatedConnection(false);
 
     if (this.mControlConnection)
       this.mControlConnection.inUse = true;
@@ -397,7 +468,7 @@ TorProtocolService.prototype =
       this.mControlConnection.inUse = false;
   },
 
-  _openAuthenticatedConnection: function()
+  _openAuthenticatedConnection: function(aIsEventConnection)
   {
     var conn;
     try
@@ -408,18 +479,19 @@ TorProtocolService.prototype =
                                  this.mControlHost + ":" + this.mControlPort);
       var socket = sts.createTransport(null, 0, this.mControlHost,
                                        this.mControlPort, null);
-      const kFlags = socket.OPEN_BLOCKING | socket.OPEN_UNBUFFERED;
-      var input = socket.openInputStream(kFlags, 0, 0);
-      var output = socket.openOutputStream(kFlags, 0, 0);
+      var flags = (aIsEventConnection) ? 0
+                              : socket.OPEN_BLOCKING | socket.OPEN_UNBUFFERED;
+      var inStream = socket.openInputStream(flags, 0, 0);
+      var outStream = socket.openOutputStream(flags, 0, 0);
 
-      var inputStream  = Cc["@mozilla.org/binaryinputstream;1"]
+      var binInStream  = Cc["@mozilla.org/binaryinputstream;1"]
                            .createInstance(Ci.nsIBinaryInputStream);
-      var outputStream = Cc["@mozilla.org/binaryoutputstream;1"]
+      var binOutStream = Cc["@mozilla.org/binaryoutputstream;1"]
                            .createInstance(Ci.nsIBinaryOutputStream);
-      inputStream.setInputStream(input);
-      outputStream.setOutputStream(output);
-      conn = { useCount: 0, socket: socket,
-               inStream: inputStream, outStream: outputStream };
+      binInStream.setInputStream(inStream);
+      binOutStream.setOutputStream(outStream);
+      conn = { useCount: 0, socket: socket, inStream: inStream,
+               binInStream: binInStream, binOutStream: binOutStream };
 
       // AUTHENTICATE
       var pwdArg = this._strEscape(this.mControlPassword);
@@ -437,7 +509,7 @@ TorProtocolService.prototype =
         return null;
       }
 
-      if (TorLauncherUtil.shouldStartAndOwnTor)
+      if (!aIsEventConnection && TorLauncherUtil.shouldStartAndOwnTor)
       {
         // Try to become the primary controller (TAKEOWNERSHIP).
         reply = this._sendCommand(conn, "TAKEOWNERSHIP", null);
@@ -487,48 +559,48 @@ TorProtocolService.prototype =
       cmd += "\r\n";
 
       ++aConn.useCount;
-      aConn.outStream.writeBytes(cmd, cmd.length);
-      reply = this._torReadReply(aConn.inStream);
+      aConn.binOutStream.writeBytes(cmd, cmd.length);
+      reply = this._torReadReply(aConn.binInStream);
     }
 
     return reply;
   },
 
-  // Returns a reply object.
+  // Returns a reply object.  Blocks until entire reply has been received.
   _torReadReply: function(aInput)
   {
     var replyObj = {};
-    replyObj.statusCode = 0;
-    replyObj.lineArray = [];
-
-    var sepChar = '';
     do
     {
-      // TODO: handle + separators (data)
       var line = this._torReadLine(aInput);
       TorLauncherLogger.safelog(2, "Command response: ", line);
-      if (line.length < 4)
-      {
-        TorLauncherLogger.safelog(4, "Unexpected response: ", line);
-        return null;
-      }
-
-      replyObj.statusCode = parseInt(line.substr(0, 3), 10);
-      sepChar = line.charAt(3);
-      var s = (line.length < 5) ? "" : line.substr(4);
-      if (s != "OK")  // Include all lines except simple "250 OK" ones.
-        replyObj.lineArray.push(s);
-    } while (sepChar != ' ');
+    } while (!this._parseOneReplyLine(line, replyObj));
 
-    return replyObj;
+    return (replyObj._parseError) ? null : replyObj;
   },
 
+  // Returns a string.  Blocks until a line has been received.
   _torReadLine: function(aInput)
   {
     var str = "";
-    var bytes;
-    while ((bytes = aInput.readBytes(1)) != "\n")
-      str += bytes;
+    while(true)
+    {
+      try
+      {
+// TODO: readBytes() will sometimes hang if the control connection is opened
+// immediately after tor opens its listener socket.  Why?
+        let bytes = aInput.readBytes(1);
+        if ('\n' == bytes)
+          break;
+
+        str += bytes;
+      }
+      catch (e)
+      {
+        if (e.result != Cr.NS_BASE_STREAM_WOULD_BLOCK)
+          throw e;
+      }
+    }
 
     var len = str.length;
     if ((len > 0) && ('\r' == str.substr(len - 1)))
@@ -536,6 +608,38 @@ TorProtocolService.prototype =
     return str;
   },
 
+  // Returns false if more lines are needed.  The first time, callers
+  // should pass an empty aReplyObj.
+  // Parsing errors are indicated by aReplyObj._parseError = true.
+  _parseOneReplyLine: function(aLine, aReplyObj)
+  {
+    if (!aLine || !aReplyObj)
+      return false;
+
+    if (!("_parseError" in aReplyObj))
+    {
+      aReplyObj.statusCode = 0;
+      aReplyObj.lineArray = [];
+      aReplyObj._parseError = false;
+    }
+
+    if (aLine.length < 4)
+    {
+      TorLauncherLogger.safelog(4, "Unexpected response: ", aLine);
+      aReplyObj._parseError = true;
+      return true;
+    }
+
+    // TODO: handle + separators (data)
+    aReplyObj.statusCode = parseInt(aLine.substr(0, 3), 10);
+    var s = (aLine.length < 5) ? "" : aLine.substr(4);
+     // Include all lines except simple "250 OK" ones.
+    if ((aReplyObj.statusCode != this.kCmdStatusOK) || (s != "OK"))
+      aReplyObj.lineArray.push(s);
+
+    return (aLine.charAt(3) == ' ');
+  },
+
   // _parseReply() understands simple GETCONF and GETINFO replies.
   _parseReply: function(aCmd, aKey, aReply)
   {
@@ -948,6 +1052,110 @@ TorProtocolService.prototype =
     return this.mRNGService;
   },
 
+  _waitForEventData: function()
+  {
+    if (!this.mEventMonitorConnection)
+      return;
+
+    var _this = this;
+    var eventReader = // An implementation of nsIInputStreamCallback.
+    {
+      onInputStreamReady: function(aInStream)
+      {
+        if (_this.mEventMonitorConnection.inStream != aInStream)
+          return;
+
+        var binStream = _this.mEventMonitorConnection.binInStream;
+        var bytes = binStream.readBytes(binStream.available());
+        if (!_this.mEventMonitorBuffer)
+          _this.mEventMonitorBuffer = bytes;
+        else
+          _this.mEventMonitorBuffer += bytes;
+        _this._processEventData();
+
+        _this._waitForEventData();
+      }
+    };
+
+    var curThread = Cc["@mozilla.org/thread-manager;1"].getService()
+                      .currentThread;
+    var asyncInStream = this.mEventMonitorConnection.inStream
+                            .QueryInterface(Ci.nsIAsyncInputStream);
+    asyncInStream.asyncWait(eventReader, 0, 0, curThread);
+  },
+
+  _processEventData: function()
+  {
+    var replyData = this.mEventMonitorBuffer;
+    if (!replyData)
+      return;
+
+    var idx = -1;
+    do
+    {
+      idx = replyData.indexOf('\n');
+      if (idx >= 0)
+      {
+        let line = replyData.substring(0, idx);
+        replyData = replyData.substring(idx + 1);
+        let len = line.length;
+        if ((len > 0) && ('\r' == line.substr(len - 1)))
+          line = line.substr(0, len - 1);
+
+        TorLauncherLogger.safelog(2, "Event response: ", line);
+        if (!this.mEventMonitorInProgressReply)
+          this.mEventMonitorInProgressReply = {};
+        var replyObj = this.mEventMonitorInProgressReply;
+        var isComplete = this._parseOneReplyLine(line, replyObj);
+        if (isComplete)
+        {
+          this._processEventReply(replyObj);
+          this.mEventMonitorInProgressReply = null;
+        }
+      }
+    } while ((idx >= 0) && replyData)
+
+    this.mEventMonitorBuffer = replyData;
+  },
+
+  _processEventReply: function(aReply)
+  {
+    if (aReply._parseError || (0 == aReply.lineArray.length))
+      return;
+
+    if (aReply.statusCode != this.kCmdStatusEventNotification)
+    {
+      TorLauncherLogger.log(4, "Unexpected event status code: "
+                               + aReply.statusCode);
+      return;
+    }
+
+    // TODO: do we need to handle multiple lines?
+    let s = aReply.lineArray[0];
+    let idx = s.indexOf(' ');
+    if ((idx > 0))
+    {
+      let eventType = s.substring(0, idx);
+      switch (eventType)
+      {
+        case "DEBUG":
+        case "INFO":
+        case "NOTICE":
+        case "WARN":
+        case "ERR":
+          var now = new Date();
+          let logObj = { date: now, type: eventType, msg: s.substr(idx) };
+          if (!this.mTorLog)
+            this.mTorLog = [];
+          this.mTorLog.push(logObj);
+          break;
+        case "STATUS_CLIENT":
+          /*FALLTHRU*/
+        default:
+          this._dumpObj(eventType + "_event", aReply);
+      }
+    }
+  },
 
   // Debugging Methods ///////////////////////////////////////////////////////
   _dumpObj: function(aObjDesc, aObj)
@@ -962,7 +1170,16 @@ TorProtocolService.prototype =
     }
 
     for (var prop in aObj)
-      dump(aObjDesc + "." + prop + ": " + aObj[prop] + "\n");
+    {
+      let val = aObj[prop];
+      if (Array.isArray(val))
+      {
+        for (let i = 0; i < val.length; ++i)
+          dump(aObjDesc + "." + prop + "[" + i + "]: " + val + "\n");
+      }
+      else
+        dump(aObjDesc + "." + prop + ": " + val + "\n");
+    }
   },
 
   endOfObject: true





More information about the tor-commits mailing list