1# Copyright 2014 The Chromium Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5# TODO(aiolos): this should be moved to catapult/base after the repo move. 6# It is used by tracing in tvcm/browser_controller. 7import collections 8import json 9import os 10import re 11import subprocess 12import sys 13 14from telemetry.core import util 15 16NamedPort = collections.namedtuple('NamedPort', ['name', 'port']) 17 18 19class LocalServerBackend(object): 20 21 def __init__(self): 22 pass 23 24 def StartAndGetNamedPorts(self, args): 25 """Starts the actual server and obtains any sockets on which it 26 should listen. 27 28 Returns a list of NamedPort on which this backend is listening. 29 """ 30 raise NotImplementedError() 31 32 def ServeForever(self): 33 raise NotImplementedError() 34 35 36class LocalServer(object): 37 38 def __init__(self, server_backend_class): 39 assert LocalServerBackend in server_backend_class.__bases__ 40 server_module_name = server_backend_class.__module__ 41 assert server_module_name in sys.modules, \ 42 'The server class\' module must be findable via sys.modules' 43 assert getattr(sys.modules[server_module_name], 44 server_backend_class.__name__), \ 45 'The server class must getattrable from its __module__ by its __name__' 46 47 self._server_backend_class = server_backend_class 48 self._subprocess = None 49 self._devnull = None 50 self._local_server_controller = None 51 self.host_ip = None 52 self.port = None 53 54 def Start(self, local_server_controller): 55 assert self._subprocess == None 56 self._local_server_controller = local_server_controller 57 58 self.host_ip = local_server_controller.host_ip 59 60 server_args = self.GetBackendStartupArgs() 61 server_args_as_json = json.dumps(server_args) 62 server_module_name = self._server_backend_class.__module__ 63 64 self._devnull = open(os.devnull, 'w') 65 cmd = [ 66 sys.executable, 67 '-m', 68 __name__, 69 'run_backend', 70 server_module_name, 71 self._server_backend_class.__name__, 72 server_args_as_json, 73 ] 74 75 env = os.environ.copy() 76 env['PYTHONPATH'] = os.pathsep.join(sys.path) 77 78 self._subprocess = subprocess.Popen(cmd, 79 cwd=util.GetTelemetryDir(), 80 env=env, 81 stdout=subprocess.PIPE) 82 83 named_ports = self._GetNamedPortsFromBackend() 84 http_port = None 85 for p in named_ports: 86 if p.name == 'http': 87 http_port = p.port 88 assert http_port and len(named_ports) == 1, ( 89 'Only http port is supported: %s' % named_ports) 90 self.port = http_port 91 92 def _GetNamedPortsFromBackend(self): 93 named_ports_json = None 94 named_ports_re = re.compile('LocalServerBackend started: (?P<port>.+)') 95 # TODO: This will hang if the subprocess doesn't print the correct output. 96 while self._subprocess.poll() == None: 97 m = named_ports_re.match(self._subprocess.stdout.readline()) 98 if m: 99 named_ports_json = m.group('port') 100 break 101 102 if not named_ports_json: 103 raise Exception('Server process died prematurely ' + 104 'without giving us port pairs.') 105 return [NamedPort(**pair) for pair in json.loads(named_ports_json.lower())] 106 107 @property 108 def is_running(self): 109 return self._subprocess != None 110 111 def __enter__(self): 112 return self 113 114 def __exit__(self, *args): 115 self.Close() 116 117 def __del__(self): 118 self.Close() 119 120 def Close(self): 121 if self._subprocess: 122 # TODO(tonyg): Should this block until it goes away? 123 self._subprocess.kill() 124 self._subprocess = None 125 if self._devnull: 126 self._devnull.close() 127 self._devnull = None 128 if self._local_server_controller: 129 self._local_server_controller.ServerDidClose(self) 130 self._local_server_controller = None 131 132 def GetBackendStartupArgs(self): 133 """Returns whatever arguments are required to start up the backend""" 134 raise NotImplementedError() 135 136 137class LocalServerController(object): 138 """Manages the list of running servers 139 140 This class manages the running servers, but also provides an isolation layer 141 to prevent LocalServer subclasses from accessing the browser backend directly. 142 143 """ 144 145 def __init__(self, platform_backend): 146 self._platform_backend = platform_backend 147 self._local_servers_by_class = {} 148 self.host_ip = self._platform_backend.forwarder_factory.host_ip 149 150 def StartServer(self, server): 151 assert not server.is_running, 'Server already started' 152 assert self._platform_backend.network_controller_backend.is_initialized 153 assert isinstance(server, LocalServer) 154 if server.__class__ in self._local_servers_by_class: 155 raise Exception( 156 'Cannot have two servers of the same class running at once. ' + 157 'Locate the existing one and use it, or call Close() on it.') 158 159 server.Start(self) 160 self._local_servers_by_class[server.__class__] = server 161 162 def GetRunningServer(self, server_class, default_value): 163 return self._local_servers_by_class.get(server_class, default_value) 164 165 @property 166 def local_servers(self): 167 return self._local_servers_by_class.values() 168 169 def Close(self): 170 while len(self._local_servers_by_class): 171 server = self._local_servers_by_class.itervalues().next() 172 try: 173 server.Close() 174 except Exception: 175 import traceback 176 traceback.print_exc() 177 178 def GetRemotePort(self, port): 179 return self._platform_backend.GetRemotePort(port) 180 181 def ServerDidClose(self, server): 182 del self._local_servers_by_class[server.__class__] 183 184 185def _LocalServerBackendMain(args): 186 assert len(args) == 4 187 (cmd, server_module_name, server_backend_class_name, 188 server_args_as_json) = args[:4] 189 assert cmd == 'run_backend' 190 server_module = __import__(server_module_name, fromlist=[True]) 191 server_backend_class = getattr(server_module, server_backend_class_name) 192 server = server_backend_class() 193 194 server_args = json.loads(server_args_as_json) 195 196 named_ports = server.StartAndGetNamedPorts(server_args) 197 assert isinstance(named_ports, list) 198 for named_port in named_ports: 199 assert isinstance(named_port, NamedPort) 200 201 # Note: This message is scraped by the parent process' 202 # _GetNamedPortsFromBackend(). Do **not** change it. 203 # pylint: disable=protected-access 204 print 'LocalServerBackend started: %s' % json.dumps([pair._asdict() 205 for pair in named_ports]) 206 sys.stdout.flush() 207 208 return server.ServeForever() 209 210 211if __name__ == '__main__': 212 # This trick is needed because local_server.NamedPort is not the 213 # same as sys.modules['__main__'].NamedPort. The module itself is loaded 214 # twice, basically. 215 from telemetry.core import local_server # pylint: disable=import-self 216 sys.exit( 217 local_server._LocalServerBackendMain( # pylint: disable=protected-access 218 sys.argv[1:])) 219