import errno import io import os import random import re import shutil import subprocess import sys import tempfile class BaseIntegrationServer(object): protocols = ('TCP', 'HTTP') protocol_re = re.compile(' '.join([ r'(?P<protocol>[A-Z]+):', r'listening on', r'(?P<address>(?:[0-9]{1,3}\.){3}[0-9]{1,3}):(?P<port>[0-9]+)', ])) version_re = re.compile(' '.join([ r'(?P<name>[a-z]+)', r'v(?P<version>[0-9]+\.[0-9]+\.[0-9]+)(?:-[a-z]+)?', r'\(built w\/(?P<go_version>[a-z0-9.-]+)\)' ])) def __init__(self, address='127.0.0.1'): self.address = address self.protocol_ports = {} self.data_path = tempfile.mkdtemp() self.name, self.version, self.go_version = self._parse_version() def _parse_version(self): output = subprocess.check_output([self.executable, '--version']) match = self.version_re.match(output.decode('utf-8')) name = match.group('name') version = tuple(int(v) for v in match.group('version').split('.')) go_version = match.group('go_version') return name, version, go_version @property def tcp_port(self): return self.protocol_ports['TCP'] @property def tcp_address(self): return '%s:%d' % (self.address, self.tcp_port) @property def http_port(self): return self.protocol_ports['HTTP'] @property def http_address(self): return '%s:%d' % (self.address, self.http_port) def _random_port(self): if self.version < (0, 3, 5): return random.randint(10000, 65535) return 0 def _random_address(self): return '%s:%d' % (self.address, self._random_port()) def _parse_protocol_ports(self): while len(self.protocol_ports) < len(self.protocols): line = self.child.stderr.readline() sys.stderr.write(line) if not line: raise Exception('server exited prematurely') if 'listening on' not in line: continue match = self.protocol_re.search(line) if not match: raise Exception('unexpected line: %r' % line) protocol = match.group('protocol') if protocol not in self.protocols: continue port = int(match.group('port'), 10) self.protocol_ports[protocol] = port def __enter__(self): sys.stderr.write('running: %s\n' % ' '.join(self.cmd)) self.child = subprocess.Popen(self.cmd, stderr=subprocess.PIPE) if sys.version_info[0] == 3: self.child.stderr = io.TextIOWrapper(self.child.stderr, 'utf-8') self._parse_protocol_ports() return self def __exit__(self, exc_type, exc_value, traceback): try: self.child.terminate() except OSError as error: if error.errno == errno.ESRCH: return raise while True: line = self.child.stderr.readline() if not line: break sys.stderr.write(line) self.child.wait() shutil.rmtree(self.data_path) class NsqdIntegrationServer(BaseIntegrationServer): executable = 'nsqd' tls_cert = os.path.join(os.path.dirname(__file__), 'cert.pem') tls_key = os.path.join(os.path.dirname(__file__), 'key.pem') def __init__(self, lookupd=None, **kwargs): super(NsqdIntegrationServer, self).__init__(**kwargs) if self.has_https(): self.protocols = ('TCP', 'HTTP', 'HTTPS') self.lookupd = lookupd def has_https(self): return self.version >= (0, 2, 28) @property def https_port(self): return self.protocol_ports['HTTPS'] @property def https_address(self): return '%s:%d' % (self.address, self.https_port) @property def cmd(self): cmd = [ self.executable, '--broadcast-address', self.address, '--tcp-address', self._random_address(), '--http-address', self._random_address(), '--data-path', self.data_path, '--tls-cert', self.tls_cert, '--tls-key', self.tls_key, ] if self.has_https(): cmd.extend(['--https-address', self._random_address()]) if self.lookupd: cmd.extend(['--lookupd-tcp-address', self.lookupd]) return cmd class LookupdIntegrationServer(BaseIntegrationServer): executable = 'nsqlookupd' @property def cmd(self): return [ self.executable, '--broadcast-address', self.address, '--tcp-address', self._random_address(), '--http-address', self._random_address(), ]