Bug 1231981 - Part 2: A websocket-to-process bridge script that can be used by JS to launch an ICE server for testing. r=ahal draft
authorByron Campen [:bwc] <docfaraday@gmail.com>
Tue, 15 Mar 2016 17:02:00 -0500
changeset 357057 31da75e2a941d55dfafba73260254515f1ca9f0e
parent 357056 9a8def9299b1923a4422bca85852530a21caa3cc
child 357058 650ec67caaf0d6d9b2a8a3cae3ec93006de708d4
child 357877 34f4b174e630e21c429c173adfc44b4fbf54cd43
push id16679
push userbcampen@mozilla.com
push dateWed, 27 Apr 2016 21:23:57 +0000
reviewersahal
bugs1231981
milestone49.0a1
Bug 1231981 - Part 2: A websocket-to-process bridge script that can be used by JS to launch an ICE server for testing. r=ahal MozReview-Commit-ID: FbfNzyw9SZp
testing/mochitest/moz.build
testing/mochitest/runtests.py
testing/tools/websocketprocessbridge/websocketprocessbridge.py
--- a/testing/mochitest/moz.build
+++ b/testing/mochitest/moz.build
@@ -148,8 +148,17 @@ TEST_HARNESS_FILES.testing.mochitest.tes
     'tests/MochiKit-1.4.2/MochiKit/Position.js',
     'tests/MochiKit-1.4.2/MochiKit/Selector.js',
     'tests/MochiKit-1.4.2/MochiKit/Signal.js',
     'tests/MochiKit-1.4.2/MochiKit/Sortable.js',
     'tests/MochiKit-1.4.2/MochiKit/Style.js',
     'tests/MochiKit-1.4.2/MochiKit/Test.js',
     'tests/MochiKit-1.4.2/MochiKit/Visual.js',
 ]
+
+TEST_HARNESS_FILES.testing.mochitest.iceserver += [
+    '/testing/tools/iceserver/iceserver.py',
+]
+
+TEST_HARNESS_FILES.testing.mochitest.websocketprocessbridge += [
+    '/testing/tools/websocketprocessbridge/websocketprocessbridge.py',
+]
+
--- a/testing/mochitest/runtests.py
+++ b/testing/mochitest/runtests.py
@@ -21,16 +21,17 @@ import mozdebug
 import mozinfo
 import mozprocess
 import mozrunner
 import numbers
 import platform
 import re
 import shutil
 import signal
+import socket
 import subprocess
 import sys
 import tempfile
 import time
 import traceback
 import urllib2
 import uuid
 import zipfile
@@ -523,16 +524,17 @@ class MochitestBase(object):
     CHROME_PATH = "redirect.html"
     urlOpts = []
     log = None
 
     def __init__(self, logger_options):
         self.update_mozinfo()
         self.server = None
         self.wsserver = None
+        self.websocketProcessBridge = None
         self.sslTunnel = None
         self._active_tests = None
         self._locations = None
 
         self.marionette = None
         self.start_script = None
         self.mozLogs = None
         self.start_script_args = []
@@ -800,16 +802,44 @@ class MochitestBase(object):
 
         self.server = MochitestServer(options, self.log)
         self.server.start()
 
         if options.pidFile != "":
             with open(options.pidFile + ".xpcshell.pid", 'w') as f:
                 f.write("%s" % self.server._process.pid)
 
+    def startWebsocketProcessBridge(self, options):
+        """Create a websocket server that can launch various processes that
+        JS needs (eg; ICE server for webrtc testing)
+        """
+
+        command = [sys.executable,
+                   os.path.join("websocketprocessbridge",
+                                "websocketprocessbridge.py")]
+        self.websocketProcessBridge = mozprocess.ProcessHandler(command,
+                                                                cwd=SCRIPT_DIR)
+        self.websocketProcessBridge.run()
+        self.log.info("runtests.py | websocket/process bridge pid: %d"
+                      % self.websocketProcessBridge.pid)
+
+        # ensure the server is up, wait for at most ten seconds
+        for i in range(1,100):
+            try:
+                sock = socket.create_connection(("127.0.0.1", 8191))
+                sock.close()
+                break
+            except:
+                time.sleep(0.1)
+        else:
+            self.log.error("runtests.py | Timed out while waiting for "
+                           "websocket/process bridge startup.")
+            self.stopServers()
+            sys.exit(1)
+
     def startServers(self, options, debuggerInfo, ignoreSSLTunnelExts=False):
         # 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
 
@@ -817,16 +847,17 @@ class MochitestBase(object):
         # 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)
+        self.startWebsocketProcessBridge(options)
 
         # start SSL pipe
         self.sslTunnel = SSLTunnel(
             options,
             logger=self.log,
             ignoreSSLTunnelExts=ignoreSSLTunnelExts)
         self.sslTunnel.buildConfig(self.locations)
         self.sslTunnel.start()
@@ -857,16 +888,23 @@ class MochitestBase(object):
 
         if self.sslTunnel is not None:
             try:
                 self.log.info('Stopping ssltunnel')
                 self.sslTunnel.stop()
             except Exception:
                 self.log.critical('Exception stopping ssltunnel')
 
+        if self.websocketProcessBridge is not None:
+            try:
+                self.log.info('Stopping websocketProcessBridge')
+                self.websocketProcessBridge.kill()
+            except Exception:
+                self.log.critical('Exception stopping websocketProcessBridge')
+
     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):
                 dest = os.path.join(
new file mode 100644
--- /dev/null
+++ b/testing/tools/websocketprocessbridge/websocketprocessbridge.py
@@ -0,0 +1,100 @@
+# vim: set ts=4 et sw=4 tw=80
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from twisted.internet import protocol, reactor
+from twisted.internet.task import LoopingCall
+import txws
+import psutil
+
+import sys
+import os
+
+# maps a command issued via websocket to running an executable with args
+commands = {
+    'iceserver' : [sys.executable,
+                   "-u",
+                   os.path.join("iceserver", "iceserver.py")]
+}
+
+class ProcessSide(protocol.ProcessProtocol):
+    """Handles the spawned process (I/O, process termination)"""
+
+    def __init__(self, socketSide):
+        self.socketSide = socketSide
+
+    def outReceived(self, data):
+        if self.socketSide:
+            lines = data.splitlines()
+            for line in lines:
+                self.socketSide.transport.write(line)
+
+    def errReceived(self, data):
+        self.outReceived(data)
+
+    def processEnded(self, reason):
+        if self.socketSide:
+            self.outReceived(str(reason))
+            self.socketSide.processGone()
+
+    def socketGone(self):
+        self.socketSide = None
+        self.transport.loseConnection()
+        self.transport.signalProcess("KILL")
+
+
+class SocketSide(protocol.Protocol):
+    """
+    Handles the websocket (I/O, closed connection), and spawning the process
+    """
+
+    def __init__(self):
+        self.processSide = None
+
+    def dataReceived(self, data):
+        if not self.processSide:
+            self.processSide = ProcessSide(self)
+            # We deliberately crash if |data| isn't on the "menu",
+            # or there is some problem spawning.
+            reactor.spawnProcess(self.processSide,
+                                 commands[data][0],
+                                 commands[data],
+                                 env=os.environ)
+
+    def connectionLost(self, reason):
+        if self.processSide:
+            self.processSide.socketGone()
+
+    def processGone(self):
+        self.processSide = None
+        self.transport.loseConnection()
+
+
+class ProcessSocketBridgeFactory(protocol.Factory):
+    """Builds sockets that can launch/bridge to a process"""
+
+    def buildProtocol(self, addr):
+        return SocketSide()
+
+# Parent process could have already exited, so this is slightly racy. Only
+# alternative is to set up a pipe between parent and child, but that requires
+# special cooperation from the parent.
+parent_process = psutil.Process(os.getpid()).parent()
+
+def check_parent():
+    """ Checks if parent process is still alive, and exits if not """
+    if not parent_process.is_running():
+        print("websocket/process bridge exiting because parent process is gone")
+        reactor.stop()
+
+if __name__ == "__main__":
+    parent_checker = LoopingCall(check_parent)
+    parent_checker.start(1)
+
+    bridgeFactory = ProcessSocketBridgeFactory()
+    reactor.listenTCP(8191, txws.WebSocketFactory(bridgeFactory))
+    print("websocket/process bridge listening on port 8191")
+    reactor.run()
+
+