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
--- 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()
+
+