• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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