Bug 907770 - Part 1: Making ssltunnel run for android and b2g mochitests. r=jmaher
authorMartin Thomson <martin.thomson@gmail.com>
Fri, 14 Mar 2014 11:25:41 -0700
changeset 180043 4c6a0f776c320699597fd82c556a8742cfcda9e5
parent 180042 c8f78e4ede49f777f302ad9faac26a8e3415ace6
child 180044 b9582a409beb6e831537b4d42accbfe16b14376c
push id272
push userpvanderbeken@mozilla.com
push dateMon, 05 May 2014 16:31:18 +0000
reviewersjmaher
bugs907770
milestone31.0a1
Bug 907770 - Part 1: Making ssltunnel run for android and b2g mochitests. r=jmaher
testing/mochitest/mochitest_options.py
testing/mochitest/runtests.py
testing/mochitest/runtestsb2g.py
testing/mochitest/runtestsremote.py
--- a/testing/mochitest/mochitest_options.py
+++ b/testing/mochitest/mochitest_options.py
@@ -404,16 +404,23 @@ class MochitestOptions(optparse.OptionPa
            "help": "Take screenshots on all test failures. Set $MOZ_UPLOAD_DIR to a directory for storing the screenshots."
         }],
         [["--quiet"],
          { "action": "store_true",
            "default": False,
            "dest": "quiet",
            "help": "Do not print test log lines unless a failure occurs."
          }],
+        [["--pidfile"],
+        { "action": "store",
+          "type": "string",
+          "dest": "pidFile",
+          "help": "name of the pidfile to generate",
+          "default": "",
+        }],
     ]
 
     def __init__(self, **kwargs):
 
         optparse.OptionParser.__init__(self, **kwargs)
         for option, value in self.mochitest_options:
             self.add_option(*option, **value)
         addCommonOptions(self)
@@ -664,23 +671,16 @@ class B2GOptions(MochitestOptions):
         }],
         [["--ssl-port"],
         { "action": "store",
           "type": "string",
           "dest": "sslPort",
           "help": "ip address where the remote web server is hosted at",
           "default": None,
         }],
-        [["--pidfile"],
-        { "action": "store",
-          "type": "string",
-          "dest": "pidFile",
-          "help": "name of the pidfile to generate",
-          "default": "",
-        }],
         [["--gecko-path"],
         { "action": "store",
           "type": "string",
           "dest": "geckoPath",
           "help": "the path to a gecko distribution that should \
                    be installed on the emulator prior to test",
           "default": None,
         }],
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -257,16 +257,20 @@ class MochitestUtilsMixin(object):
 
   # Path to the test script on the server
   TEST_PATH = "tests"
   CHROME_PATH = "redirect.html"
   urlOpts = []
 
   def __init__(self):
     self.update_mozinfo()
+    self.server = None
+    self.wsserver = None
+    self.sslTunnel = None
+    self._locations = None
 
   def update_mozinfo(self):
     """walk up directories to find mozinfo.json update the info"""
     # TODO: This should go in a more generic place, e.g. mozinfo
 
     path = SCRIPT_DIR
     dirs = set()
     while path != os.path.expanduser('~'):
@@ -282,16 +286,24 @@ class MochitestUtilsMixin(object):
     return os.path.normpath(os.path.join(self.oldcwd, os.path.expanduser(path)))
 
   def getLogFilePath(self, logFile):
     """ return the log file path relative to the device we are testing on, in most cases
         it will be the full path on the local system
     """
     return self.getFullPath(logFile)
 
+  @property
+  def locations(self):
+    if self._locations is not None:
+      return self._locations
+    locations_file = os.path.join(SCRIPT_DIR, 'server-locations.txt')
+    self._locations = ServerLocations(locations_file)
+    return self._locations
+
   def buildURLOptions(self, options, env):
     """ Add test control options from the command line to the url
 
         URL parameters to test URL:
 
         autorun -- kick off tests automatically
         closeWhenDone -- closes the browser after the tests
         hideResultsTable -- hides the table of individual test results
@@ -502,53 +514,81 @@ class MochitestUtilsMixin(object):
       with open(os.path.join(testdir, 'tests.json'), 'w') as manifestFile:
         manifestFile.write(json.dumps({'tests': paths}))
       options.manifestFile = 'tests.json'
 
     return self.buildTestURL(options)
 
   def startWebSocketServer(self, options, debuggerInfo):
     """ Launch the websocket server """
-    if options.webServer != '127.0.0.1':
-      return
-
     self.wsserver = WebSocketServer(options, SCRIPT_DIR, debuggerInfo)
     self.wsserver.start()
 
-  def stopWebSocketServer(self, options):
-    if options.webServer != '127.0.0.1':
-      return
-
-    log.info('Stopping web socket server')
-    self.wsserver.stop()
-
   def startWebServer(self, options):
     """Create the webserver and start it up"""
 
-    if options.webServer != '127.0.0.1':
-      return
-
     self.server = MochitestServer(options)
     self.server.start()
 
+    if options.pidFile != "":
+      with open(options.pidFile + ".xpcshell.pid", 'w') as f:
+        f.write("%s" % self.server._process.pid)
+
+  def startServers(self, options, debuggerInfo):
+    # start servers and set ports
+    # TODO: pass these values, don't set on `self`
+    self.webServer = options.webServer
+    self.httpPort = options.httpPort
+    self.sslPort = options.sslPort
+    self.webSocketPort = options.webSocketPort
+
+    # httpd-path is specified by standard makefile targets and may be specified
+    # on the command line to select a particular version of httpd.js. If not
+    # specified, try to select the one from hostutils.zip, as required in bug 882932.
+    if not options.httpdPath:
+      options.httpdPath = os.path.join(options.utilityPath, "components")
+
+    self.startWebServer(options)
+    self.startWebSocketServer(options, debuggerInfo)
+
+    # start SSL pipe
+    self.sslTunnel = SSLTunnel(options)
+    self.sslTunnel.buildConfig(self.locations)
+    self.sslTunnel.start()
+
     # If we're lucky, the server has fully started by now, and all paths are
     # ready, etc.  However, xpcshell cold start times suck, at least for debug
     # builds.  We'll try to connect to the server for awhile, and if we fail,
     # we'll try to kill the server and exit with an error.
-    self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
+    if self.server is not None:
+      self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
+
+  def stopServers(self):
+    """Servers are no longer needed, and perhaps more importantly, anything they
+        might spew to console might confuse things."""
+    if self.server is not None:
+      try:
+        log.info('Stopping web server')
+        self.server.stop()
+      except Exception:
+        log.exception('Exception when stopping web server')
 
-  def stopWebServer(self, options):
-    """ Server's no longer needed, and perhaps more importantly, anything it might
-        spew to console shouldn't disrupt the leak information table we print next.
-    """
-    if options.webServer != '127.0.0.1':
-      return
+    if self.wsserver is not None:
+      try:
+        log.info('Stopping web socket server')
+        self.wsserver.stop()
+      except Exception:
+        log.exception('Exception when stopping web socket server');
 
-    log.info('Stopping web server')
-    self.server.stop()
+    if self.sslTunnel is not None:
+      try:
+        log.info('Stopping ssltunnel')
+        self.sslTunnel.stop()
+      except Exception:
+        log.exception('Exception stopping ssltunnel');
 
   def copyExtraFilesToProfile(self, options):
     "Copy extra files or dirs specified on the command line to the testing profile."
     for f in options.extraProfileFiles:
       abspath = self.getFullPath(f)
       if os.path.isfile(abspath):
         shutil.copy2(abspath, options.profilePath)
       elif os.path.isdir(abspath):
@@ -637,19 +677,91 @@ overlay chrome://webapprt/content/webapp
             path = os.path.join(extensionDir, dirEntry)
             if os.path.isdir(path) or (os.path.isfile(path) and path.endswith(".xpi")):
               extensions.append(path)
 
     # append mochikit
     extensions.append(os.path.join(SCRIPT_DIR, self.jarDir))
     return extensions
 
+class SSLTunnel:
+  def __init__(self, options):
+    self.process = None
+    self.utilityPath = options.utilityPath
+    self.xrePath = options.xrePath
+    self.certPath = options.certPath
+    self.sslPort = options.sslPort
+    self.httpPort = options.httpPort
+    self.webServer = options.webServer
+    self.webSocketPort = options.webSocketPort
+
+    self.customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
+    self.clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
+    self.redirRE      = re.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)")
+
+  def writeLocation(self, config, loc):
+    for option in loc.options:
+      match = self.customCertRE.match(option)
+      if match:
+        customcert = match.group("nickname");
+        config.write("listen:%s:%s:%s:%s\n" %
+                     (loc.host, loc.port, self.sslPort, customcert))
+
+      match = self.clientAuthRE.match(option)
+      if match:
+        clientauth = match.group("clientauth");
+        config.write("clientauth:%s:%s:%s:%s\n" %
+                     (loc.host, loc.port, self.sslPort, clientauth))
+
+      match = self.redirRE.match(option)
+      if match:
+        redirhost = match.group("redirhost")
+        config.write("redirhost:%s:%s:%s:%s\n" %
+                     (loc.host, loc.port, self.sslPort, redirhost))
+
+  def buildConfig(self, locations):
+    """Create the ssltunnel configuration file"""
+    configFd, self.configFile = tempfile.mkstemp(prefix="ssltunnel", suffix=".cfg")
+    with os.fdopen(configFd, "w") as config:
+      config.write("httpproxy:1\n")
+      config.write("certdbdir:%s\n" % self.certPath)
+      config.write("forward:127.0.0.1:%s\n" % self.httpPort)
+      config.write("websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort))
+      config.write("listen:*:%s:pgo server certificate\n" % self.sslPort)
+
+      for loc in locations:
+        if loc.scheme == "https" and "nocert" not in loc.options:
+          self.writeLocation(config, loc)
+
+  def start(self):
+    """ Starts the SSL Tunnel """
+
+    # start ssltunnel to provide https:// URLs capability
+    bin_suffix = mozinfo.info.get('bin_suffix', '')
+    ssltunnel = os.path.join(self.utilityPath, "ssltunnel" + bin_suffix)
+    if not os.path.exists(ssltunnel):
+      log.error("INFO | runtests.py | expected to find ssltunnel at %s", ssltunnel)
+      exit(1)
+
+    env = environment(xrePath=self.xrePath)
+    self.process = mozprocess.ProcessHandler([ssltunnel, self.configFile],
+                                               env=env)
+    self.process.run()
+    log.info("INFO | runtests.py | SSL tunnel pid: %d", self.process.pid)
+
+  def stop(self):
+    """ Stops the SSL Tunnel and cleans up """
+    if self.process is not None:
+      self.process.kill()
+    if os.path.exists(self.configFile):
+      os.remove(self.configFile)
 
 class Mochitest(MochitestUtilsMixin):
-  runSSLTunnel = True
+  certdbNew = False
+  sslTunnel = None
   vmwareHelper = None
   DEFAULT_TIMEOUT = 60.0
 
   # XXX use automation.py for test name to avoid breaking legacy
   # TODO: replace this with 'runtests.py' or 'mochitest' or the like
   test_name = 'automation.py'
 
   def __init__(self):
@@ -661,30 +773,72 @@ class Mochitest(MochitestUtilsMixin):
     # Max time in seconds to wait for server startup before tests will fail -- if
     # this seems big, it's mostly for debug machines where cold startup
     # (particularly after a build) takes forever.
     self.SERVER_STARTUP_TIMEOUT = 180 if mozinfo.info.get('debug') else 90
 
     # metro browser sub process id
     self.browserProcessId = None
 
-    # cached server locations
-    self._locations = {}
 
     self.haveDumpedScreen = False
 
   def extraPrefs(self, extraPrefs):
     """interpolate extra preferences from option strings"""
 
     try:
       return dict(parseKeyValue(extraPrefs, context='--setpref='))
     except KeyValueParseError, e:
       print str(e)
       sys.exit(1)
 
+  def fillCertificateDB(self, options):
+    # TODO: move -> mozprofile:
+    # https://bugzilla.mozilla.org/show_bug.cgi?id=746243#c35
+
+    pwfilePath = os.path.join(options.profilePath, ".crtdbpw")
+    with open(pwfilePath, "w") as pwfile:
+      pwfile.write("\n")
+
+    # Pre-create the certification database for the profile
+    env = self.environment(xrePath=options.xrePath)
+    bin_suffix = mozinfo.info.get('bin_suffix', '')
+    certutil = os.path.join(options.utilityPath, "certutil" + bin_suffix)
+    pk12util = os.path.join(options.utilityPath, "pk12util" + bin_suffix)
+
+    if self.certdbNew:
+      # android and b2g use the new DB formats exclusively
+      certdbPath = "sql:" + options.profilePath
+    else:
+      # desktop seems to use the old
+      certdbPath = options.profilePath
+
+    status = call([certutil, "-N", "-d", certdbPath, "-f", pwfilePath], env=env)
+    if status:
+      return status
+
+    # Walk the cert directory and add custom CAs and client certs
+    files = os.listdir(options.certPath)
+    for item in files:
+      root, ext = os.path.splitext(item)
+      if ext == ".ca":
+        trustBits = "CT,,"
+        if root.endswith("-object"):
+          trustBits = "CT,,CT"
+        call([certutil, "-A", "-i", os.path.join(options.certPath, item),
+              "-d", certdbPath, "-f", pwfilePath, "-n", root, "-t", trustBits],
+              env=env)
+      elif ext == ".client":
+        call([pk12util, "-i", os.path.join(options.certPath, item),
+              "-w", pwfilePath, "-d", certdbPath],
+              env=env)
+
+    os.unlink(pwfilePath)
+    return 0
+
   def buildProfile(self, options):
     """ create the profile and add optional chrome bits and files if requested """
     if options.browserChrome and options.timeout:
       options.extraPrefs.append("testing.browserTestHarness.timeout=%d" % options.timeout)
     options.extraPrefs.append("browser.tabs.remote=%s" % ('true' if options.e10s else 'false'))
     options.extraPrefs.append("browser.tabs.remote.autostart=%s" % ('true' if options.e10s else 'false'))
 
     # get extensions to install
@@ -693,20 +847,16 @@ class Mochitest(MochitestUtilsMixin):
     # web apps
     appsPath = os.path.join(SCRIPT_DIR, 'profile_data', 'webapps_mochitest.json')
     if os.path.exists(appsPath):
       with open(appsPath) as apps_file:
         apps = json.load(apps_file)
     else:
       apps = None
 
-    # locations
-    locations_file = os.path.join(SCRIPT_DIR, 'server-locations.txt')
-    locations = ServerLocations(locations_file)
-
     # preferences
     prefsPath = os.path.join(SCRIPT_DIR, 'profile_data', 'prefs_general.js')
     prefs = dict(Preferences.read_prefs(prefsPath))
     prefs.update(self.extraPrefs(options.extraPrefs))
 
     # interpolate preferences
     interpolation = {"server": "%s:%s" % (options.webServer, options.httpPort)}
     prefs = json.loads(json.dumps(prefs) % interpolation)
@@ -722,30 +872,39 @@ class Mochitest(MochitestUtilsMixin):
     # use SSL port for legacy compatibility; see
     # - https://bugzilla.mozilla.org/show_bug.cgi?id=688667#c66
     # - https://bugzilla.mozilla.org/show_bug.cgi?id=899221
     # - https://github.com/mozilla/mozbase/commit/43f9510e3d58bfed32790c82a57edac5f928474d
     #             'ws': str(self.webSocketPort)
              'ws': options.sslPort
              }
 
+
     # create a profile
     self.profile = Profile(profile=options.profilePath,
                            addons=extensions,
-                           locations=locations,
+                           locations=self.locations,
                            preferences=prefs,
                            apps=apps,
                            proxy=proxy
                            )
 
     # Fix options.profilePath for legacy consumers.
     options.profilePath = self.profile.profile
 
     manifest = self.addChromeToProfile(options)
     self.copyExtraFilesToProfile(options)
+
+    # create certificate database for the profile
+    # TODO: this should really be upstreamed somewhere, maybe mozprofile
+    certificateStatus = self.fillCertificateDB(options)
+    if certificateStatus:
+      log.info("TEST-UNEXPECTED-FAIL | runtests.py | Certificate integration failed")
+      return None
+
     return manifest
 
   def buildBrowserEnv(self, options, debugger=False):
     """build the environment variables for the specific test and operating system"""
     browserEnv = self.environment(xrePath=options.xrePath, debugger=debugger,
                                   dmdPath=options.dmdPath)
 
     # These variables are necessary for correct application startup; change
@@ -777,16 +936,23 @@ class Mochitest(MochitestUtilsMixin):
       browserEnv["JS_DISABLE_SLOW_SCRIPT_SIGNALS"] = "1"
 
     return browserEnv
 
   def cleanup(self, manifest, options):
     """ remove temporary files and profile """
     os.remove(manifest)
     del self.profile
+    if options.pidFile != "":
+      try:
+        os.remove(options.pidFile)
+        if os.path.exists(options.pidFile + ".xpcshell.pid"):
+          os.remove(options.pidFile + ".xpcshell.pid")
+      except:
+        log.warn("cleaning up pidfile '%s' was unsuccessful from the test harness", options.pidFile)
 
   def dumpScreen(self, utilityPath):
     if self.haveDumpedScreen:
       log.info("Not taking screenshot here: see the one that was previously logged")
       return
     self.haveDumpedScreen = True
     dumpScreen(utilityPath)
 
@@ -863,35 +1029,35 @@ class Mochitest(MochitestUtilsMixin):
       self.vmwareHelper.StartRecording()
     except Exception, e:
       log.warning("runtests.py | Failed to start "
                   "VMware recording: (%s)" % str(e))
       self.vmwareHelper = None
 
   def stopVMwareRecording(self):
     """ stops recording inside VMware VM using the recording helper dll """
-    assert mozinfo.isWin
-    if self.vmwareHelper is not None:
-      log.info("runtests.py | Stopping VMware recording.")
-      try:
+    try:
+      assert mozinfo.isWin
+      if self.vmwareHelper is not None:
+        log.info("runtests.py | Stopping VMware recording.")
         self.vmwareHelper.StopRecording()
-      except Exception, e:
-        log.warning("runtests.py | Failed to stop "
-                    "VMware recording: (%s)" % str(e))
-      self.vmwareHelper = None
+    except Exception, e:
+      log.warning("runtests.py | Failed to stop "
+                  "VMware recording: (%s)" % str(e))
+      log.exception('Error stopping VMWare recording')
+
+    self.vmwareHelper = None
 
   def runApp(self,
              testUrl,
              env,
              app,
              profile,
              extraArgs,
              utilityPath,
-             xrePath,
-             certPath,
              debuggerInfo=None,
              symbolsPath=None,
              timeout=-1,
              onLaunch=None,
              webapprtChrome=False,
              hide_subtests=False,
              screenshotOnFail=False):
     """
@@ -901,26 +1067,16 @@ class Mochitest(MochitestUtilsMixin):
 
     # debugger information
     interactive = False
     debug_args = None
     if debuggerInfo:
         interactive = debuggerInfo['interactive']
         debug_args = [debuggerInfo['path']] + debuggerInfo['args']
 
-    # ensure existence of required paths
-    required_paths = ('utilityPath', 'xrePath', 'certPath')
-    missing = [(path, locals()[path])
-               for path in required_paths
-               if not os.path.exists(locals()[path])]
-    if missing:
-      log.error("runtests.py | runApp called with missing paths: %s" % (
-        ', '.join([("%s->%s" % (key, value)) for key, value in missing])))
-      return 1
-
     # fix default timeout
     if timeout == -1:
       timeout = self.DEFAULT_TIMEOUT
 
     # build parameters
     is_test_build = mozinfo.info.get('tests_enabled', True)
     bin_suffix = mozinfo.info.get('bin_suffix', '')
 
@@ -929,38 +1085,16 @@ class Mochitest(MochitestUtilsMixin):
 
     # make sure we clean up after ourselves.
     try:
       # set process log environment variable
       tmpfd, processLog = tempfile.mkstemp(suffix='pidlog')
       os.close(tmpfd)
       env["MOZ_PROCESS_LOG"] = processLog
 
-      if self.runSSLTunnel:
-
-        # create certificate database for the profile
-        # TODO: this should really be upstreamed somewhere, maybe mozprofile
-        certificateStatus = self.fillCertificateDB(self.profile.profile,
-                                                   certPath,
-                                                   utilityPath,
-                                                   xrePath)
-        if certificateStatus:
-          log.info("TEST-UNEXPECTED-FAIL | runtests.py | Certificate integration failed")
-          return certificateStatus
-
-        # start ssltunnel to provide https:// URLs capability
-        ssltunnel = os.path.join(utilityPath, "ssltunnel" + bin_suffix)
-        ssltunnel_cfg = os.path.join(self.profile.profile, "ssltunnel.cfg")
-        ssltunnelProcess = mozprocess.ProcessHandler([ssltunnel, ssltunnel_cfg], cwd=SCRIPT_DIR,
-                                                      env=environment(xrePath=xrePath))
-        ssltunnelProcess.run()
-        log.info("INFO | runtests.py | SSL tunnel pid: %d", ssltunnelProcess.pid)
-      else:
-        ssltunnelProcess = None
-
       if interactive:
         # If an interactive debugger is attached,
         # don't use timeouts, and don't capture ctrl-c.
         timeout = None
         signal.signal(signal.SIGINT, lambda sigid, frame: None)
 
       # build command line
       cmd = os.path.abspath(app)
@@ -1069,18 +1203,16 @@ class Mochitest(MochitestUtilsMixin):
 
       if crashed or zombieProcesses:
         status = 1
 
     finally:
       # cleanup
       if os.path.exists(processLog):
         os.remove(processLog)
-      if ssltunnelProcess:
-        ssltunnelProcess.kill()
 
     return status
 
   def runTests(self, options, onLaunch=None):
     """ Prepare, configure, run tests and cleanup """
 
     # get debugger info, a dict of:
     # {'path': path to the debugger (string),
@@ -1101,114 +1233,92 @@ class Mochitest(MochitestUtilsMixin):
 
     # buildProfile sets self.profile .
     # This relies on sideeffects and isn't very stateful:
     # https://bugzilla.mozilla.org/show_bug.cgi?id=919300
     manifest = self.buildProfile(options)
     if manifest is None:
       return 1
 
-    # start servers and set ports
-    # TODO: pass these values, don't set on `self`
-    self.webServer = options.webServer
-    self.httpPort = options.httpPort
-    self.sslPort = options.sslPort
-    self.webSocketPort = options.webSocketPort
+    try:
+      self.startServers(options, debuggerInfo)
 
-    try:
-        self.startWebServer(options)
-        self.startWebSocketServer(options, debuggerInfo)
+      testURL = self.buildTestPath(options)
+      self.buildURLOptions(options, browserEnv)
+      if self.urlOpts:
+        testURL += "?" + "&".join(self.urlOpts)
 
-        testURL = self.buildTestPath(options)
-        self.buildURLOptions(options, browserEnv)
-        if self.urlOpts:
-          testURL += "?" + "&".join(self.urlOpts)
+      if options.webapprtContent:
+        options.browserArgs.extend(('-test-mode', testURL))
+        testURL = None
 
-        if options.webapprtContent:
-          options.browserArgs.extend(('-test-mode', testURL))
-          testURL = None
-
-        if options.immersiveMode:
-          options.browserArgs.extend(('-firefoxpath', options.app))
-          options.app = self.immersiveHelperPath
+      if options.immersiveMode:
+        options.browserArgs.extend(('-firefoxpath', options.app))
+        options.app = self.immersiveHelperPath
 
-        if options.jsdebugger:
-          options.browserArgs.extend(['-jsdebugger'])
+      if options.jsdebugger:
+        options.browserArgs.extend(['-jsdebugger'])
 
-        # Remove the leak detection file so it can't "leak" to the tests run.
-        # The file is not there if leak logging was not enabled in the application build.
-        if os.path.exists(self.leak_report_file):
-          os.remove(self.leak_report_file)
+      # Remove the leak detection file so it can't "leak" to the tests run.
+      # The file is not there if leak logging was not enabled in the application build.
+      if os.path.exists(self.leak_report_file):
+        os.remove(self.leak_report_file)
 
-        # then again to actually run mochitest
-        if options.timeout:
-          timeout = options.timeout + 30
-        elif options.debugger or not options.autorun:
-          timeout = None
-        else:
-          timeout = 330.0 # default JS harness timeout is 300 seconds
+      # then again to actually run mochitest
+      if options.timeout:
+        timeout = options.timeout + 30
+      elif options.debugger or not options.autorun:
+        timeout = None
+      else:
+        timeout = 330.0 # default JS harness timeout is 300 seconds
 
-        if options.vmwareRecording:
-          self.startVMwareRecording(options);
+      if options.vmwareRecording:
+        self.startVMwareRecording(options);
 
-        log.info("runtests.py | Running tests: start.\n")
-        try:
-          status = self.runApp(testURL,
-                               browserEnv,
-                               options.app,
-                               profile=self.profile,
-                               extraArgs=options.browserArgs,
-                               utilityPath=options.utilityPath,
-                               xrePath=options.xrePath,
-                               certPath=options.certPath,
-                               debuggerInfo=debuggerInfo,
-                               symbolsPath=options.symbolsPath,
-                               timeout=timeout,
-                               onLaunch=onLaunch,
-                               webapprtChrome=options.webapprtChrome,
-                               hide_subtests=options.hide_subtests,
-                               screenshotOnFail=options.screenshotOnFail
-                               )
-        except KeyboardInterrupt:
-          log.info("runtests.py | Received keyboard interrupt.\n");
-          status = -1
-        except:
-          traceback.print_exc()
-          log.error("Automation Error: Received unexpected exception while running application\n")
-          status = 1
+      log.info("runtests.py | Running tests: start.\n")
+      try:
+        status = self.runApp(testURL,
+                             browserEnv,
+                             options.app,
+                             profile=self.profile,
+                             extraArgs=options.browserArgs,
+                             utilityPath=options.utilityPath,
+                             debuggerInfo=debuggerInfo,
+                             symbolsPath=options.symbolsPath,
+                             timeout=timeout,
+                             onLaunch=onLaunch,
+                             webapprtChrome=options.webapprtChrome,
+                             hide_subtests=options.hide_subtests,
+                             screenshotOnFail=options.screenshotOnFail
+        )
+      except KeyboardInterrupt:
+        log.info("runtests.py | Received keyboard interrupt.\n");
+        status = -1
+      except:
+        traceback.print_exc()
+        log.error("Automation Error: Received unexpected exception while running application\n")
+        status = 1
 
     finally:
-        if options.vmwareRecording:
-            try:
-              self.stopVMwareRecording();
-            except Exception:
-                log.exception('Error stopping VMWare recording')
+      if options.vmwareRecording:
+        self.stopVMwareRecording();
+      self.stopServers()
 
-        try:
-            self.stopWebServer(options)
-        except Exception:
-            log.exception('Exception when stopping web server')
+    processLeakLog(self.leak_report_file, options.leakThreshold)
 
-        try:
-            self.stopWebSocketServer(options)
-        except Exception:
-            log.exception('Exception when stopping websocket server')
-
-        processLeakLog(self.leak_report_file, options.leakThreshold)
+    if self.nsprLogs:
+      with zipfile.ZipFile("%s/nsprlog.zip" % browserEnv["MOZ_UPLOAD_DIR"], "w", zipfile.ZIP_DEFLATED) as logzip:
+        for logfile in glob.glob("%s/nspr*.log*" % tempfile.gettempdir()):
+          logzip.write(logfile)
+          os.remove(logfile)
 
-        if self.nsprLogs:
-            with zipfile.ZipFile("%s/nsprlog.zip" % browserEnv["MOZ_UPLOAD_DIR"], "w", zipfile.ZIP_DEFLATED) as logzip:
-                for logfile in glob.glob("%s/nspr*.log*" % tempfile.gettempdir()):
-                    logzip.write(logfile)
-                    os.remove(logfile)
+    log.info("runtests.py | Running tests: end.")
 
-        log.info("runtests.py | Running tests: end.")
-
-        if manifest is not None:
-            self.cleanup(manifest, options)
+    if manifest is not None:
+      self.cleanup(manifest, options)
 
     return status
 
   def handleTimeout(self, timeout, proc, utilityPath, debuggerInfo, browserProcessId):
     """handle process output timeout"""
     # TODO: bug 913975 : _processOutput should call self.processOutputLine one more time one timeout (I think)
     log.info("TEST-UNEXPECTED-FAIL | %s | application timed out after %d seconds with no output", self.lastTestSeen, int(timeout))
     browserProcessId = browserProcessId or proc.pid
@@ -1423,103 +1533,16 @@ class Mochitest(MochitestUtilsMixin):
 
     addons.install_from_path(path)
 
   def installExtensionsToProfile(self, options):
     "Install special testing extensions, application distributed extensions, and specified on the command line ones to testing profile."
     for path in self.getExtensionsToInstall(options):
       self.installExtensionFromPath(options, path)
 
-  def readLocations(self, locations_file):
-    """
-    Reads the locations at which the Mochitest HTTP server is available from
-    `locations_file`.
-    """
-    path = os.path.realpath(locations_file)
-    return self._locations.setdefault(path, ServerLocations(path))
-
-  def fillCertificateDB(self, profileDir, certPath, utilityPath, xrePath):
-    # TODO: move -> mozprofile:
-    # https://bugzilla.mozilla.org/show_bug.cgi?id=746243#c35
-
-    pwfilePath = os.path.join(profileDir, ".crtdbpw")
-    with open(pwfilePath, "w") as pwfile:
-      pwfile.write("\n")
-
-    # Create head of the ssltunnel configuration file
-    sslTunnelConfigPath = os.path.join(profileDir, "ssltunnel.cfg")
-    sslTunnelConfig = open(sslTunnelConfigPath, "w")
-    sslTunnelConfig.write("httpproxy:1\n")
-    sslTunnelConfig.write("certdbdir:%s\n" % certPath)
-    sslTunnelConfig.write("forward:127.0.0.1:%s\n" % self.httpPort)
-    sslTunnelConfig.write("websocketserver:%s:%s\n" % (self.webServer, self.webSocketPort))
-    sslTunnelConfig.write("listen:*:%s:pgo server certificate\n" % self.sslPort)
-
-    # Configure automatic certificate and bind custom certificates, client authentication
-    locations = self.readLocations(os.path.join(SCRIPT_DIR, 'server-locations.txt'))
-
-    for loc in locations:
-
-      if loc.scheme == "https" and "nocert" not in loc.options:
-        customCertRE = re.compile("^cert=(?P<nickname>[0-9a-zA-Z_ ]+)")
-        clientAuthRE = re.compile("^clientauth=(?P<clientauth>[a-z]+)")
-        redirRE      = re.compile("^redir=(?P<redirhost>[0-9a-zA-Z_ .]+)")
-        for option in loc.options:
-          match = customCertRE.match(option)
-          if match:
-            customcert = match.group("nickname");
-            sslTunnelConfig.write("listen:%s:%s:%s:%s\n" %
-                      (loc.host, loc.port, self.sslPort, customcert))
-
-          match = clientAuthRE.match(option)
-          if match:
-            clientauth = match.group("clientauth");
-            sslTunnelConfig.write("clientauth:%s:%s:%s:%s\n" %
-                      (loc.host, loc.port, self.sslPort, clientauth))
-
-          match = redirRE.match(option)
-          if match:
-            redirhost = match.group("redirhost")
-            sslTunnelConfig.write("redirhost:%s:%s:%s:%s\n" %
-                      (loc.host, loc.port, self.sslPort, redirhost))
-
-    sslTunnelConfig.close()
-
-    # Pre-create the certification database for the profile
-    env = self.environment(xrePath=xrePath)
-    bin_suffix = mozinfo.info.get('bin_suffix', '')
-    certutil = os.path.join(utilityPath, "certutil" + bin_suffix)
-    pk12util = os.path.join(utilityPath, "pk12util" + bin_suffix)
-
-    status = call([certutil, "-N", "-d", profileDir, "-f", pwfilePath], env=env)
-    printstatus(status, "certutil")
-    if status:
-      return status
-
-    # Walk the cert directory and add custom CAs and client certs
-    files = os.listdir(certPath)
-    for item in files:
-      root, ext = os.path.splitext(item)
-      if ext == ".ca":
-        trustBits = "CT,,"
-        if root.endswith("-object"):
-          trustBits = "CT,,CT"
-        status = call([certutil, "-A", "-i", os.path.join(certPath, item),
-              "-d", profileDir, "-f", pwfilePath, "-n", root, "-t", trustBits],
-              env=env)
-        printstatus(status, "certutil")
-      elif ext == ".client":
-        status = call([pk12util, "-i", os.path.join(certPath, item), "-w",
-              pwfilePath, "-d", profileDir],
-              env=env)
-        printstatus(status, "pk2util")
-
-    os.unlink(pwfilePath)
-    return 0
-
 
 def main():
 
   # parse command line options
   mochitest = Mochitest()
   parser = MochitestOptions()
   options, args = parser.parse_args()
   options = parser.verifyOptions(options, mochitest)
--- a/testing/mochitest/runtestsb2g.py
+++ b/testing/mochitest/runtestsb2g.py
@@ -35,17 +35,17 @@ log = mozlog.getLogger('Mochitest')
 class B2GMochitest(MochitestUtilsMixin):
     def __init__(self, marionette,
                        out_of_process=True,
                        profile_data_dir=None,
                        locations=os.path.join(here, 'server-locations.txt')):
         super(B2GMochitest, self).__init__()
         self.marionette = marionette
         self.out_of_process = out_of_process
-        self.locations = locations
+        self.locations_file = locations
         self.preferences = []
         self.webapps = None
         self.test_script = os.path.join(here, 'b2g_start_script.js')
         self.test_script_args = [self.out_of_process]
         self.product = 'b2g'
 
         if profile_data_dir:
             self.preferences = [os.path.join(profile_data_dir, f)
@@ -87,17 +87,17 @@ class B2GMochitest(MochitestUtilsMixin):
                           "OOP": "true" if self.out_of_process else "false" }
         prefs = json.loads(json.dumps(prefs) % interpolation)
         for pref in prefs:
             prefs[pref] = Preferences.cast(prefs[pref])
 
         kwargs = {
             'addons': self.getExtensionsToInstall(options),
             'apps': self.webapps,
-            'locations': self.locations,
+            'locations': self.locations_file,
             'preferences': prefs,
             'proxy': {"remote": options.webServer}
         }
 
         if options.profile:
             self.profile = Profile.clone(options.profile, **kwargs)
         else:
             self.profile = Profile(**kwargs)
@@ -109,18 +109,17 @@ class B2GMochitest(MochitestUtilsMixin):
         return manifest
 
     def run_tests(self, options):
         """ Prepare, configure, run tests and cleanup """
 
         self.leak_report_file = os.path.join(options.profilePath, "runtests_leaks.log")
         manifest = self.build_profile(options)
 
-        self.startWebServer(options)
-        self.startWebSocketServer(options, None)
+        self.startServers(options, None)
         self.buildURLOptions(options, {'MOZ_HIDE_RESULTS_TABLE': '1'})
         self.test_script_args.append(not options.emulator)
         self.test_script_args.append(options.wifi)
 
         if options.debugger or not options.autorun:
             timeout = None
         else:
             if not options.timeout:
@@ -150,18 +149,17 @@ class B2GMochitest(MochitestUtilsMixin):
             log.info("runtests.py | Received keyboard interrupt.\n");
             status = -1
         except:
             traceback.print_exc()
             log.error("Automation Error: Received unexpected exception while running application\n")
             self.runner.check_for_crashes()
             status = 1
 
-        self.stopWebServer(options)
-        self.stopWebSocketServer(options)
+        self.stopServers()
 
         log.info("runtestsb2g.py | Running tests: end.")
 
         if manifest is not None:
             self.cleanup(manifest, options)
         return status
 
 
@@ -194,36 +192,30 @@ class B2GDeviceMochitest(B2GMochitest):
             except:
                 print "Warning: cleaning up pidfile '%s' was unsuccessful from the test harness" % options.pidFile
 
         # stop and clean up the runner
         if getattr(self, 'runner', False):
             self.runner.cleanup()
             self.runner = None
 
-    def startWebServer(self, options):
-        """ Create the webserver on the host and start it up """
-        d = vars(options).copy()
-        d['xrePath'] = self.local_binary_dir
-        d['utilityPath'] = self.local_binary_dir
-        d['profilePath'] = tempfile.mkdtemp()
-        if d.get('httpdPath') is None:
-            d['httpdPath'] = os.path.abspath(os.path.join(self.local_binary_dir, 'components'))
-        self.server = MochitestServer(d)
-        self.server.start()
+    def startServers(self, options, debuggerInfo):
+        """ Create the servers on the host and start them up """
+        savedXre = options.xrePath
+        savedUtility = options.utilityPath
+        savedProfie = options.profilePath
+        options.xrePath = self.local_binary_dir
+        options.utilityPath = self.local_binary_dir
+        options.profilePath = tempfile.mkdtemp()
 
-        if (options.pidFile != ""):
-            f = open(options.pidFile + ".xpcshell.pid", 'w')
-            f.write("%s" % self.server._process.pid)
-            f.close()
-        self.server.ensureReady(90)
+        MochitestUtilsMixin.startServers(self, options, debuggerInfo)
 
-    def stopWebServer(self, options):
-        if hasattr(self, 'server'):
-            self.server.stop()
+        options.xrePath = savedXre
+        options.utilityPath = savedUtility
+        options.profilePath = savedProfie
 
     def buildURLOptions(self, options, env):
         self.local_log = options.logFile
         options.logFile = self.remote_log
         options.profilePath = self.profile.profile
         retVal = super(B2GDeviceMochitest, self).buildURLOptions(options, env)
 
         self.setup_common_options(options)
@@ -233,16 +225,17 @@ class B2GDeviceMochitest(B2GMochitest):
         return retVal
 
 
 class B2GDesktopMochitest(B2GMochitest, Mochitest):
 
     def __init__(self, marionette, profile_data_dir):
         B2GMochitest.__init__(self, marionette, out_of_process=False, profile_data_dir=profile_data_dir)
         Mochitest.__init__(self)
+        self.certdbNew = True
 
     def runMarionetteScript(self, marionette, test_script, test_script_args):
         assert(marionette.wait_for_port())
         marionette.start_session()
         marionette.set_context(marionette.CONTEXT_CHROME)
 
         if os.path.isfile(test_script):
             f = open(test_script, 'r')
@@ -335,18 +328,17 @@ def run_remote_mochitests(parser, option
 
     retVal = 1
     try:
         mochitest.cleanup(None, options)
         retVal = mochitest.run_tests(options)
     except:
         print "Automation Error: Exception caught while running tests"
         traceback.print_exc()
-        mochitest.stopWebServer(options)
-        mochitest.stopWebSocketServer(options)
+        mochitest.stopServers()
         try:
             mochitest.cleanup(None, options)
         except:
             pass
         retVal = 1
 
     sys.exit(retVal)
 
--- a/testing/mochitest/runtestsremote.py
+++ b/testing/mochitest/runtestsremote.py
@@ -77,21 +77,16 @@ class RemoteOptions(MochitestOptions):
                     help = "http port of the remote web server")
         defaults["httpPort"] = automation.DEFAULT_HTTP_PORT
 
         self.add_option("--ssl-port", action = "store",
                     type = "string", dest = "sslPort",
                     help = "ssl port of the remote web server")
         defaults["sslPort"] = automation.DEFAULT_SSL_PORT
 
-        self.add_option("--pidfile", action = "store",
-                    type = "string", dest = "pidFile",
-                    help = "name of the pidfile to generate")
-        defaults["pidFile"] = ""
-
         self.add_option("--robocop-ini", action = "store",
                     type = "string", dest = "robocopIni",
                     help = "name of the .ini file containing the list of tests to run")
         defaults["robocopIni"] = ""
 
         self.add_option("--robocop", action = "store",
                     type = "string", dest = "robocop",
                     help = "name of the .ini file containing the list of tests to run. [DEPRECATED- please use --robocop-ini")
@@ -218,134 +213,137 @@ class RemoteOptions(MochitestOptions):
         # like it's not set for verification purposes.
         options.dumpOutputDirectory = None
         options = MochitestOptions.verifyOptions(self, options, mochitest)
         options.webServer = tempIP
         options.app = temp
         options.sslPort = tempSSL
         options.httpPort = tempPort
 
-        return options 
+        return options
 
 class MochiRemote(Mochitest):
 
     _automation = None
     _dm = None
     localProfile = None
     logLines = []
 
     def __init__(self, automation, devmgr, options):
         self._automation = automation
         Mochitest.__init__(self)
         self._dm = devmgr
-        self.runSSLTunnel = False
         self.environment = self._automation.environment
         self.remoteProfile = options.remoteTestRoot + "/profile"
         self._automation.setRemoteProfile(self.remoteProfile)
         self.remoteLog = options.remoteLogFile
         self.localLog = options.logFile
         self._automation.deleteANRs()
+        self.certdbNew = True
 
     def cleanup(self, manifest, options):
         if self._dm.fileExists(self.remoteLog):
             self._dm.getFile(self.remoteLog, self.localLog)
             self._dm.removeFile(self.remoteLog)
         else:
             log.warn("Unable to retrieve log file (%s) from remote device",
                 self.remoteLog)
         self._dm.removeDir(self.remoteProfile)
-
-        if (options.pidFile != ""):
-            try:
-                os.remove(options.pidFile)
-                os.remove(options.pidFile + ".xpcshell.pid")
-            except:
-                log.warn("cleaning up pidfile '%s' was unsuccessful from the test harness", options.pidFile)
+        Mochitest.cleanup(self, manifest, options)
 
     def findPath(self, paths, filename = None):
         for path in paths:
             p = path
             if filename:
                 p = os.path.join(p, filename)
             if os.path.exists(self.getFullPath(p)):
                 return path
         return None
 
-    def startWebServer(self, options):
-        """ Create the webserver on the host and start it up """
-        remoteXrePath = options.xrePath
-        remoteProfilePath = options.profilePath
-        remoteUtilityPath = options.utilityPath
+    def makeLocalAutomation(self):
         localAutomation = Automation()
         localAutomation.IS_WIN32 = False
         localAutomation.IS_LINUX = False
         localAutomation.IS_MAC = False
         localAutomation.UNIXISH = False
         hostos = sys.platform
         if (hostos == 'mac' or  hostos == 'darwin'):
-          localAutomation.IS_MAC = True
+            localAutomation.IS_MAC = True
         elif (hostos == 'linux' or hostos == 'linux2'):
-          localAutomation.IS_LINUX = True
-          localAutomation.UNIXISH = True
+            localAutomation.IS_LINUX = True
+            localAutomation.UNIXISH = True
         elif (hostos == 'win32' or hostos == 'win64'):
-          localAutomation.BIN_SUFFIX = ".exe"
-          localAutomation.IS_WIN32 = True
+            localAutomation.BIN_SUFFIX = ".exe"
+            localAutomation.IS_WIN32 = True
+        return localAutomation
 
-        paths = [options.xrePath, localAutomation.DIST_BIN, self._automation._product, os.path.join('..', self._automation._product)]
+    # This seems kludgy, but this class uses paths from the remote host in the
+    # options, except when calling up to the base class, which doesn't
+    # understand the distinction.  This switches out the remote values for local
+    # ones that the base class understands.  This is necessary for the web
+    # server, SSL tunnel and profile building functions.
+    def switchToLocalPaths(self, options):
+        """ Set local paths in the options, return a function that will restore remote values """
+        remoteXrePath = options.xrePath
+        remoteProfilePath = options.profilePath
+        remoteUtilityPath = options.utilityPath
+
+        localAutomation = self.makeLocalAutomation()
+        paths = [
+            options.xrePath,
+            localAutomation.DIST_BIN,
+            self._automation._product,
+            os.path.join('..', self._automation._product)
+        ]
         options.xrePath = self.findPath(paths)
         if options.xrePath == None:
             log.error("unable to find xulrunner path for %s, please specify with --xre-path", os.name)
             sys.exit(1)
 
         xpcshell = "xpcshell"
         if (os.name == "nt"):
             xpcshell += ".exe"
-      
+
         if options.utilityPath:
             paths = [options.utilityPath, options.xrePath]
         else:
             paths = [options.xrePath]
         options.utilityPath = self.findPath(paths, xpcshell)
+
         if options.utilityPath == None:
             log.error("unable to find utility path for %s, please specify with --utility-path", os.name)
             sys.exit(1)
-        # httpd-path is specified by standard makefile targets and may be specified
-        # on the command line to select a particular version of httpd.js. If not
-        # specified, try to select the one from hostutils.zip, as required in bug 882932.
-        if not options.httpdPath:
-            options.httpdPath = os.path.join(options.utilityPath, "components")
 
         xpcshell_path = os.path.join(options.utilityPath, xpcshell)
         if localAutomation.elf_arm(xpcshell_path):
             log.error('xpcshell at %s is an ARM binary; please use '
                       'the --utility-path argument to specify the path '
                       'to a desktop version.' % xpcshell_path)
             sys.exit(1)
 
-        options.profilePath = tempfile.mkdtemp()
-        self.server = MochitestServer(options)
-        self.server.start()
-
-        if (options.pidFile != ""):
-            f = open(options.pidFile + ".xpcshell.pid", 'w')
-            f.write("%s" % self.server._process.pid)
-            f.close()
-        self.server.ensureReady(self.SERVER_STARTUP_TIMEOUT)
-
-        options.xrePath = remoteXrePath
-        options.utilityPath = remoteUtilityPath
-        options.profilePath = remoteProfilePath
-         
-    def stopWebServer(self, options):
-        if hasattr(self, 'server'):
-            self.server.stop()
-        
-    def buildProfile(self, options):
         if self.localProfile:
             options.profilePath = self.localProfile
+        else:
+            options.profilePath = tempfile.mkdtemp()
+
+        def fixup():
+            options.xrePath = remoteXrePath
+            options.utilityPath = remoteUtilityPath
+            options.profilePath = remoteProfilePath
+
+        return fixup
+
+    def startServers(self, options, debuggerInfo):
+        """ Create the servers on the host and start them up """
+        restoreRemotePaths = self.switchToLocalPaths(options)
+        Mochitest.startServers(self, options, debuggerInfo)
+        restoreRemotePaths()
+
+    def buildProfile(self, options):
+        restoreRemotePaths = self.switchToLocalPaths(options)
         manifest = Mochitest.buildProfile(self, options)
         self.localProfile = options.profilePath
         self._dm.removeDir(self.remoteProfile)
 
         # we do not need this for robotium based tests, lets save a LOT of time
         if options.robocopIni:
             shutil.rmtree(os.path.join(options.profilePath, 'webapps'))
             shutil.rmtree(os.path.join(options.profilePath, 'extensions', 'staged', 'mochikit@mozilla.org'))
@@ -354,19 +352,20 @@ class MochiRemote(Mochitest):
             os.remove(os.path.join(options.profilePath, 'userChrome.css'))
 
         try:
             self._dm.pushDir(options.profilePath, self.remoteProfile)
         except devicemanager.DMError:
             log.error("Automation Error: Unable to copy profile to device.")
             raise
 
+        restoreRemotePaths()
         options.profilePath = self.remoteProfile
         return manifest
-    
+
     def buildURLOptions(self, options, env):
         self.localLog = options.logFile
         options.logFile = self.remoteLog
         options.profilePath = self.localProfile
         env["MOZ_HIDE_RESULTS_TABLE"] = "1"
         retVal = Mochitest.buildURLOptions(self, options, env)
 
         if not options.robocopIni:
@@ -398,17 +397,17 @@ class MochiRemote(Mochitest):
         try:
             self._dm.pushFile(filename, manifest)
         except devicemanager.DMError:
             log.error("Automation Error: Unable to install Chrome files on device.")
             raise
 
         return manifest
 
-    def getLogFilePath(self, logFile):             
+    def getLogFilePath(self, logFile):
         return logFile
 
     # In the future we could use LogParser: http://hg.mozilla.org/automation/logparser/
     def addLogData(self):
         with open(self.localLog) as currentLog:
             data = currentLog.readlines()
 
         restart = re.compile('0 INFO SimpleTest START.*')
@@ -439,17 +438,17 @@ class MochiRemote(Mochitest):
             result = 1
         return result
 
     def printLog(self):
         passed = 0
         failed = 0
         todo = 0
         incr = 1
-        logFile = [] 
+        logFile = []
         logFile.append("0 INFO SimpleTest START")
         for line in self.logLines:
             if line.startswith("INFO TEST-PASS"):
                 passed += 1
             elif line.startswith("INFO TEST-UNEXPECTED"):
                 failed += 1
             elif line.startswith("INFO TEST-KNOWN"):
                 todo += 1
@@ -543,16 +542,20 @@ class MochiRemote(Mochitest):
     def runApp(self, *args, **kwargs):
         """front-end automation.py's `runApp` functionality until FennecRunner is written"""
 
         # automation.py/remoteautomation `runApp` takes the profile path,
         # whereas runtest.py's `runApp` takes a mozprofile object.
         if 'profileDir' not in kwargs and 'profile' in kwargs:
             kwargs['profileDir'] = kwargs.pop('profile').profile
 
+        # We're handling ssltunnel, so we should lie to automation.py to avoid
+        # it trying to set up ssltunnel as well
+        kwargs['runSSLTunnel'] = False
+
         return self._automation.runApp(*args, **kwargs)
 
 def main():
     auto = RemoteAutomation(None, "fennec")
     parser = RemoteOptions(auto)
     options, args = parser.parse_args()
 
     if (options.dm_trans == "adb"):
@@ -575,17 +578,17 @@ def main():
         auto.setProduct(options.remoteProductName)
     auto.setAppName(options.remoteappname)
 
     mochitest = MochiRemote(auto, dm, options)
 
     options = parser.verifyOptions(options, mochitest)
     if (options == None):
         sys.exit(1)
-    
+
     logParent = os.path.dirname(options.remoteLogFile)
     dm.mkDir(logParent);
     auto.setRemoteLog(options.remoteLogFile)
     auto.setServerInfo(options.webServer, options.httpPort, options.sslPort)
 
     mochitest.printDeviceInfo()
 
     # Add Android version (SDK level) to mozinfo so that manifest entries
@@ -663,17 +666,17 @@ def main():
 
             options.app = "am"
             options.browserArgs = ["instrument", "-w", "-e", "deviceroot", deviceRoot, "-e", "class"]
             options.browserArgs.append("org.mozilla.gecko.tests.%s" % test['name'])
             options.browserArgs.append("org.mozilla.roboexample.test/org.mozilla.gecko.FennecInstrumentationTestRunner")
 
             # If the test is for checking the import from bookmarks then make sure there is data to import
             if test['name'] == "testImportFromAndroid":
-                
+
                 # Get the OS so we can run the insert in the apropriate database and following the correct table schema
                 osInfo = dm.getInfo("os")
                 devOS = " ".join(osInfo['os'])
 
                 if ("pandaboard" in devOS):
                     delete = ['execsu', 'sqlite3', "/data/data/com.android.browser/databases/browser2.db \'delete from bookmarks where _id > 14;\'"]
                 else:
                     delete = ['execsu', 'sqlite3', "/data/data/com.android.browser/databases/browser.db \'delete from bookmarks where _id > 14;\'"]
@@ -701,18 +704,17 @@ def main():
                     mochitest.printDeviceInfo(printLogcat=True)
                     mochitest.printScreenshots(screenShotDir)
                 # Ensure earlier failures aren't overwritten by success on this run
                 if retVal is None or retVal == 0:
                     retVal = result
             except:
                 log.error("Automation Error: Exception caught while running tests")
                 traceback.print_exc()
-                mochitest.stopWebServer(options)
-                mochitest.stopWebSocketServer(options)
+                mochitest.stopServers()
                 try:
                     mochitest.cleanup(None, options)
                 except devicemanager.DMError:
                     # device error cleaning up... oh well!
                     pass
                 retVal = 1
                 break
             finally:
@@ -737,18 +739,17 @@ def main():
                 retVal = overallResult
     else:
         try:
             dm.recordLogcat()
             retVal = mochitest.runTests(options)
         except:
             log.error("Automation Error: Exception caught while running tests")
             traceback.print_exc()
-            mochitest.stopWebServer(options)
-            mochitest.stopWebSocketServer(options)
+            mochitest.stopServers()
             try:
                 mochitest.cleanup(None, options)
             except devicemanager.DMError:
                 # device error cleaning up... oh well!
                 pass
             retVal = 1
 
     mochitest.printDeviceInfo(printLogcat=True)