diff options
Diffstat (limited to 'testing/mozbase/mozserve')
-rw-r--r-- | testing/mozbase/mozserve/mozserve/__init__.py | 12 | ||||
-rw-r--r-- | testing/mozbase/mozserve/mozserve/servers.py | 289 | ||||
-rw-r--r-- | testing/mozbase/mozserve/setup.py | 17 |
3 files changed, 318 insertions, 0 deletions
diff --git a/testing/mozbase/mozserve/mozserve/__init__.py b/testing/mozbase/mozserve/mozserve/__init__.py new file mode 100644 index 0000000000..9e884916a1 --- /dev/null +++ b/testing/mozbase/mozserve/mozserve/__init__.py @@ -0,0 +1,12 @@ +# 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/. + +""" +mozserve is a simple script that is used to launch test servers, and +is designed for use in mochitest and xpcshelltest. +""" + +from .servers import DoHServer, Http2Server, Http3Server + +__all__ = ["Http3Server", "Http2Server", "DoHServer"] diff --git a/testing/mozbase/mozserve/mozserve/servers.py b/testing/mozbase/mozserve/mozserve/servers.py new file mode 100644 index 0000000000..fe75a12496 --- /dev/null +++ b/testing/mozbase/mozserve/mozserve/servers.py @@ -0,0 +1,289 @@ +# 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/. + +import copy +import os +import re +import subprocess +import sys +import time +from argparse import Namespace +from contextlib import contextmanager +from subprocess import PIPE, Popen +from threading import Thread + + +@contextmanager +def popenCleanupHack(isWin): + """ + Hack to work around https://bugs.python.org/issue37380 + The basic idea is that on old versions of Python on Windows, + we need to clear subprocess._cleanup before we call Popen(), + then restore it afterwards. + """ + savedCleanup = None + if isWin and sys.version_info[0] == 3 and sys.version_info < (3, 7, 5): + savedCleanup = subprocess._cleanup + subprocess._cleanup = lambda: None + try: + yield + finally: + if savedCleanup: + subprocess._cleanup = savedCleanup + + +class Http3Server(object): + """ + Class which encapsulates the Http3 server + """ + + def __init__(self, options, env, logger): + if isinstance(options, Namespace): + options = vars(options) + self._log = logger + self._profileDir = options["profilePath"] + self._env = copy.deepcopy(env) + self._ports = {} + self._echConfig = "" + self._isMochitest = options["isMochitest"] + self._http3ServerPath = options["http3ServerPath"] + self._isWin = options["isWin"] + self._http3ServerProc = {} + self._proxyPort = -1 + if options.get("proxyPort"): + self._proxyPort = options["proxyPort"] + + def ports(self): + return self._ports + + def echConfig(self): + return self._echConfig + + def read_streams(self, name, proc, pipe): + output = "stdout" if pipe == proc.stdout else "stderr" + for line in iter(pipe.readline, ""): + self._log.info("server: %s [%s] %s" % (name, output, line)) + + def start(self): + if not os.path.exists(self._http3ServerPath): + raise Exception("Http3 server not found at %s" % self._http3ServerPath) + + self._log.info("mozserve | Found Http3Server path: %s" % self._http3ServerPath) + + dbPath = os.path.join(self._profileDir, "cert9.db") + if not os.path.exists(dbPath): + raise Exception("cert db not found at %s" % dbPath) + + dbPath = self._profileDir + self._log.info("mozserve | cert db path: %s" % dbPath) + + try: + if self._isMochitest: + self._env["MOZ_HTTP3_MOCHITEST"] = "1" + if self._proxyPort != -1: + self._env["MOZ_HTTP3_PROXY_PORT"] = str(self._proxyPort) + with popenCleanupHack(self._isWin): + process = Popen( + [self._http3ServerPath, dbPath], + stdin=PIPE, + stdout=PIPE, + stderr=PIPE, + env=self._env, + cwd=os.getcwd(), + universal_newlines=True, + ) + self._http3ServerProc["http3Server"] = process + + # Check to make sure the server starts properly by waiting for it to + # tell us it's started + msg = process.stdout.readline() + self._log.info("mozserve | http3 server msg: %s" % msg) + name = "http3server" + t1 = Thread( + target=self.read_streams, + args=(name, process, process.stdout), + daemon=True, + ) + t1.start() + t2 = Thread( + target=self.read_streams, + args=(name, process, process.stderr), + daemon=True, + ) + t2.start() + if "server listening" in msg: + searchObj = re.search( + r"HTTP3 server listening on ports ([0-9]+), ([0-9]+), ([0-9]+), ([0-9]+) and ([0-9]+)." + " EchConfig is @([\x00-\x7F]+)@", + msg, + 0, + ) + if searchObj: + self._ports["MOZHTTP3_PORT"] = searchObj.group(1) + self._ports["MOZHTTP3_PORT_FAILED"] = searchObj.group(2) + self._ports["MOZHTTP3_PORT_ECH"] = searchObj.group(3) + self._ports["MOZHTTP3_PORT_PROXY"] = searchObj.group(4) + self._ports["MOZHTTP3_PORT_NO_RESPONSE"] = searchObj.group(5) + self._echConfig = searchObj.group(6) + else: + self._log.error("http3server failed to start?") + except OSError as e: + # This occurs if the subprocess couldn't be started + self._log.error("Could not run the http3 server: %s" % (str(e))) + + def stop(self): + """ + Shutdown our http3Server process, if it exists + """ + for name, proc in self._http3ServerProc.items(): + self._log.info("%s server shutting down ..." % name) + if proc.poll() is not None: + self._log.info("Http3 server %s already dead %s" % (name, proc.poll())) + else: + proc.terminate() + retries = 0 + while proc.poll() is None: + time.sleep(0.1) + retries += 1 + if retries > 40: + self._log.info("Killing proc") + proc.kill() + break + self._http3ServerProc = {} + + +class NodeHttp2Server(object): + """ + Class which encapsulates a Node Http/2 server + """ + + def __init__(self, name, options, env, logger): + if isinstance(options, Namespace): + options = vars(options) + self._name = name + self._log = logger + self._port = options["port"] + self._env = copy.deepcopy(env) + self._nodeBin = options["nodeBin"] + self._serverPath = options["serverPath"] + self._dstServerPort = options["dstServerPort"] + self._isWin = options["isWin"] + self._nodeProc = None + self._searchStr = options["searchStr"] + self._alpn = options["alpn"] + + def port(self): + return self._port + + def start(self): + if not os.path.exists(self._serverPath): + raise Exception( + "%s server not found at %s" % (self._name, self._serverPath) + ) + + self._log.info( + "mozserve | Found %s server path: %s" % (self._name, self._serverPath) + ) + + if not os.path.exists(self._nodeBin) or not os.path.isfile(self._nodeBin): + raise Exception("node not found at path %s" % (self._nodeBin)) + + self._log.info("Found node at %s" % (self._nodeBin)) + + try: + # We pipe stdin to node because the server will exit when its + # stdin reaches EOF + with popenCleanupHack(self._isWin): + process = Popen( + [ + self._nodeBin, + self._serverPath, + "serverPort={}".format(self._dstServerPort), + "listeningPort={}".format(self._port), + "alpn={}".format(self._alpn), + ], + stdin=PIPE, + stdout=PIPE, + stderr=PIPE, + env=self._env, + cwd=os.getcwd(), + universal_newlines=True, + ) + self._nodeProc = process + + msg = process.stdout.readline() + self._log.info("runtests.py | %s server msg: %s" % (self._name, msg)) + if "server listening" in msg: + searchObj = re.search(self._searchStr, msg, 0) + if searchObj: + self._port = int(searchObj.group(1)) + self._log.info( + "%s server started at port: %d" % (self._name, self._port) + ) + except OSError as e: + # This occurs if the subprocess couldn't be started + self._log.error("Could not run %s server: %s" % (self._name, str(e))) + + def stop(self): + """ + Shut down our node process, if it exists + """ + if self._nodeProc is not None: + if self._nodeProc.poll() is not None: + self._log.info("Node server already dead %s" % (self._nodeProc.poll())) + else: + self._nodeProc.terminate() + + def dumpOutput(fd, label): + firstTime = True + for msg in fd: + if firstTime: + firstTime = False + self._log.info("Process %s" % label) + self._log.info(msg) + + dumpOutput(self._nodeProc.stdout, "stdout") + dumpOutput(self._nodeProc.stderr, "stderr") + + self._nodeProc = None + + +class DoHServer(object): + """ + Class which encapsulates the DoH server + """ + + def __init__(self, options, env, logger): + options["searchStr"] = r"DoH server listening on ports ([0-9]+)" + self._server = NodeHttp2Server("DoH", options, env, logger) + + def port(self): + return self._server.port() + + def start(self): + self._server.start() + + def stop(self): + self._server.stop() + + +class Http2Server(object): + """ + Class which encapsulates the Http2 server + """ + + def __init__(self, options, env, logger): + options["searchStr"] = r"Http2 server listening on ports ([0-9]+)" + options["dstServerPort"] = -1 + options["alpn"] = "" + self._server = NodeHttp2Server("Http/2", options, env, logger) + + def port(self): + return self._server.port() + + def start(self): + self._server.start() + + def stop(self): + self._server.stop() diff --git a/testing/mozbase/mozserve/setup.py b/testing/mozbase/mozserve/setup.py new file mode 100644 index 0000000000..c00000bb0c --- /dev/null +++ b/testing/mozbase/mozserve/setup.py @@ -0,0 +1,17 @@ +# 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 setuptools import setup + +PACKAGE_VERSION = "0.1" + +setup( + name="mozserve", + version=PACKAGE_VERSION, + description="Python test server launcher intended for use with Mozilla testing", + long_description="see https://firefox-source-docs.mozilla.org/mozbase/index.html", + license="MPL", + packages=["mozserve"], + zip_safe=False, +) |