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.
12 from telemetry.core import forwarders
15 NamedPort = collections.namedtuple('NamedPort', ['name', 'port'])
18 class LocalServerBackend(object):
22 def StartAndGetNamedPorts(self, args):
23 """Starts the actual server and obtains any sockets on which it
26 Returns a list of NamedPort on which this backend is listening.
28 raise NotImplementedError()
30 def ServeForever(self):
31 raise NotImplementedError()
34 class LocalServer(object):
35 def __init__(self, server_backend_class):
36 assert LocalServerBackend in server_backend_class.__bases__
37 server_module_name = server_backend_class.__module__
38 assert server_module_name in sys.modules, \
39 'The server class\' module must be findable via sys.modules'
40 assert getattr(sys.modules[server_module_name],
41 server_backend_class.__name__), \
42 'The server class must getattrable from its __module__ by its __name__'
44 self._server_backend_class = server_backend_class
45 self._subprocess = None
47 self._local_server_controller = None
51 def Start(self, local_server_controller):
52 assert self._subprocess == None
53 self._local_server_controller = local_server_controller
55 self.host_ip = local_server_controller.host_ip
57 server_args = self.GetBackendStartupArgs()
58 server_args_as_json = json.dumps(server_args)
59 server_module_name = self._server_backend_class.__module__
61 self._devnull = open(os.devnull, 'w')
62 cmd = [sys.executable, '-m', __name__]
63 cmd.extend(["run_backend"])
64 cmd.extend([server_module_name, self._server_backend_class.__name__,
67 env = os.environ.copy()
68 env['PYTHONPATH'] = os.pathsep.join(sys.path)
70 cwd = os.path.abspath(
71 os.path.join(os.path.dirname(__file__), '..', '..'))
73 self._subprocess = subprocess.Popen(
74 cmd, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=sys.stderr)
76 named_ports = self._GetNamedPortsFromBackend()
77 named_port_pair_map = {'http': None, 'https': None, 'dns': None}
78 for name, port in named_ports:
79 assert name in named_port_pair_map, '%s forwarding is unsupported' % name
80 named_port_pair_map[name] = (
81 forwarders.PortPair(port,
82 local_server_controller.GetRemotePort(port)))
83 self.forwarder = local_server_controller.CreateForwarder(
84 forwarders.PortPairs(**named_port_pair_map))
86 def _GetNamedPortsFromBackend(self):
87 named_ports_json = None
88 named_ports_re = re.compile('LocalServerBackend started: (?P<port>.+)')
89 while self._subprocess.poll() == None:
90 m = named_ports_re.match(self._subprocess.stdout.readline())
92 named_ports_json = m.group('port')
95 if not named_ports_json:
96 raise Exception('Server process died prematurely ' +
97 'without giving us port pairs.')
98 return [NamedPort(**pair) for pair in json.loads(named_ports_json.lower())]
101 def is_running(self):
102 return self._subprocess != None
107 def __exit__(self, *args):
115 self.forwarder.Close()
116 self.forwarder = None
118 # TODO(tonyg): Should this block until it goes away?
119 self._subprocess.kill()
120 self._subprocess = None
122 self._devnull.close()
124 if self._local_server_controller:
125 self._local_server_controller.ServerDidClose(self)
126 self._local_server_controller = None
128 def GetBackendStartupArgs(self):
129 """Returns whatever arguments are required to start up the backend"""
130 raise NotImplementedError()
133 class LocalServerController():
134 """Manages the list of running servers
136 This class manages the running servers, but also provides an isolation layer
137 to prevent LocalServer subclasses from accessing the browser backend directly.
140 def __init__(self, browser_backend):
141 self._browser_backend = browser_backend
142 self._local_servers_by_class = {}
143 self.host_ip = self._browser_backend.forwarder_factory.host_ip
145 def StartServer(self, server):
146 assert not server.is_running, 'Server already started'
147 assert isinstance(server, LocalServer)
148 if server.__class__ in self._local_servers_by_class:
150 'Canont have two servers of the same class running at once. ' +
151 'Locate the existing one and use it, or call Close() on it.')
154 self._local_servers_by_class[server.__class__] = server
156 def GetRunningServer(self, server_class, default_value):
157 return self._local_servers_by_class.get(server_class, default_value)
160 def local_servers(self):
161 return self._local_servers_by_class.values()
164 while len(self._local_servers_by_class):
165 server = self._local_servers_by_class.itervalues().next()
170 traceback.print_exc()
172 def CreateForwarder(self, port_pairs):
173 return self._browser_backend.forwarder_factory.Create(port_pairs)
175 def GetRemotePort(self, port):
176 return self._browser_backend.GetRemotePort(port)
178 def ServerDidClose(self, server):
179 del self._local_servers_by_class[server.__class__]
182 def _LocalServerBackendMain(args):
183 assert len(args) == 4
184 (cmd, server_module_name,
185 server_backend_class_name, server_args_as_json) = args[:4]
186 assert cmd == 'run_backend'
187 server_module = __import__(server_module_name, fromlist=[True])
188 server_backend_class = getattr(server_module, server_backend_class_name)
189 server = server_backend_class()
191 server_args = json.loads(server_args_as_json)
193 named_ports = server.StartAndGetNamedPorts(server_args)
194 assert isinstance(named_ports, list)
195 for named_port in named_ports:
196 assert isinstance(named_port, NamedPort)
198 # Note: This message is scraped by the parent process'
199 # _GetNamedPortsFromBackend(). Do **not** change it.
200 print 'LocalServerBackend started: %s' % json.dumps(
201 [pair._asdict() for pair in named_ports]) # pylint: disable=W0212
204 return server.ServeForever()
207 if __name__ == '__main__':
208 # This trick is needed because local_server.NamedPort is not the
209 # same as sys.modules['__main__'].NamedPort. The module itself is loaded
211 from telemetry.core import local_server # pylint: disable=W0406
212 sys.exit(local_server._LocalServerBackendMain( # pylint: disable=W0212