1 # Copyright (c) 2012 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.
5 """Start and stop Web Page Replay.
7 Of the public module names, the following one is key:
8 ReplayServer: a class to start/stop Web Page Replay.
21 _CHROME_SRC_DIR = os.path.abspath(os.path.join(
22 os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, os.pardir))
23 REPLAY_DIR = os.path.join(
24 _CHROME_SRC_DIR, 'third_party', 'webpagereplay')
25 LOG_PATH = os.path.join(
26 _CHROME_SRC_DIR, 'webpagereplay_logs', 'logs.txt')
29 # Chrome options to make it work with Web Page Replay.
30 def GetChromeFlags(replay_host, http_port, https_port):
31 assert replay_host and http_port and https_port, 'All arguments required'
33 '--host-resolver-rules=MAP * %s,EXCLUDE localhost' % replay_host,
34 '--testing-fixed-http-port=%s' % http_port,
35 '--testing-fixed-https-port=%s' % https_port,
36 '--ignore-certificate-errors',
40 # Signal masks on Linux are inherited from parent processes. If anything
41 # invoking us accidentally masks SIGINT (e.g. by putting a process in the
42 # background from a shell script), sending a SIGINT to the child will fail
43 # to terminate it. Running this signal handler before execing should fix that
45 def ResetInterruptHandler():
46 signal.signal(signal.SIGINT, signal.SIG_DFL)
49 class ReplayError(Exception):
50 """Catch-all exception for the module."""
54 class ReplayNotFoundError(ReplayError):
55 def __init__(self, label, path):
56 super(ReplayNotFoundError, self).__init__()
57 self.args = (label, path)
60 label, path = self.args
61 return 'Path does not exist for %s: %s' % (label, path)
64 class ReplayNotStartedError(ReplayError):
68 class ReplayServer(object):
69 """Start and Stop Web Page Replay.
71 Web Page Replay is a proxy that can record and "replay" web pages with
72 simulated network characteristics -- without having to edit the pages
73 by hand. With WPR, tests can use "real" web content, and catch
74 performance issues that may result from introducing network delays and
78 with ReplayServer(archive_path):
79 self.NavigateToURL(start_url)
82 Environment Variables (for development):
83 WPR_ARCHIVE_PATH: path to alternate archive file (e.g. '/tmp/foo.wpr').
84 WPR_RECORD: if set, puts Web Page Replay in record mode instead of replay.
85 WPR_REPLAY_DIR: path to alternate Web Page Replay source.
88 def __init__(self, archive_path, replay_host, dns_port, http_port, https_port,
89 replay_options=None, replay_dir=None,
91 """Initialize ReplayServer.
94 archive_path: a path to a specific WPR archive (required).
95 replay_host: the hostname to serve traffic.
96 dns_port: an integer port on which to serve DNS traffic. May be zero
97 to let the OS choose an available port. If None DNS forwarding is
99 http_port: an integer port on which to serve HTTP traffic. May be zero
100 to let the OS choose an available port.
101 https_port: an integer port on which to serve HTTPS traffic. May be zero
102 to let the OS choose an available port.
103 replay_options: an iterable of options strings to forward to replay.py.
104 replay_dir: directory that has replay.py and related modules.
105 log_path: a path to a log file.
107 self.archive_path = os.environ.get('WPR_ARCHIVE_PATH', archive_path)
108 self.replay_options = list(replay_options or ())
109 self.replay_dir = os.environ.get('WPR_REPLAY_DIR', replay_dir or REPLAY_DIR)
110 self.log_path = log_path or LOG_PATH
111 self.dns_port = dns_port
112 self.http_port = http_port
113 self.https_port = https_port
114 self._replay_host = replay_host
116 if 'WPR_RECORD' in os.environ and '--record' not in self.replay_options:
117 self.replay_options.append('--record')
118 self.is_record_mode = '--record' in self.replay_options
119 self._AddDefaultReplayOptions()
121 self.replay_py = os.path.join(self.replay_dir, 'replay.py')
123 if self.is_record_mode:
124 self._CheckPath('archive directory', os.path.dirname(self.archive_path))
125 elif not os.path.exists(self.archive_path):
126 self._CheckPath('archive file', self.archive_path)
127 self._CheckPath('replay script', self.replay_py)
129 self.replay_process = None
131 def _AddDefaultReplayOptions(self):
132 """Set WPR command-line options. Can be overridden if needed."""
133 self.replay_options = [
134 '--host', str(self._replay_host),
135 '--port', str(self.http_port),
136 '--ssl_port', str(self.https_port),
137 '--use_closest_match',
138 '--no-dns_forwarding',
139 '--log_level', 'warning'
140 ] + self.replay_options
141 if self.dns_port is not None:
142 self.replay_options.extend(['--dns_port', str(self.dns_port)])
144 def _CheckPath(self, label, path):
145 if not os.path.exists(path):
146 raise ReplayNotFoundError(label, path)
148 def _OpenLogFile(self):
149 log_dir = os.path.dirname(self.log_path)
150 if not os.path.exists(log_dir):
152 return open(self.log_path, 'w')
154 def WaitForStart(self, timeout):
155 """Checks to see if the server is up and running."""
156 port_re = re.compile(
157 '.*?(?P<protocol>[A-Z]+) server started on (?P<host>.*):(?P<port>\d+)')
159 start_time = time.time()
161 while elapsed_time < timeout:
162 if self.replay_process.poll() is not None:
163 break # The process has exited.
165 # Read the ports from the WPR log.
166 if not self.http_port or not self.https_port or not self.dns_port:
167 with open(self.log_path) as f:
168 for line in f.readlines():
169 m = port_re.match(line.strip())
171 if not self.http_port and m.group('protocol') == 'HTTP':
172 self.http_port = int(m.group('port'))
173 elif not self.https_port and m.group('protocol') == 'HTTPS':
174 self.https_port = int(m.group('port'))
175 elif not self.dns_port and m.group('protocol') == 'DNS':
176 self.dns_port = int(m.group('port'))
178 # Try to connect to the WPR ports.
179 if self.http_port and self.https_port:
181 up_url = '%s://%s:%s/web-page-replay-generate-200'
182 http_up_url = up_url % ('http', self._replay_host, self.http_port)
183 https_up_url = up_url % ('https', self._replay_host, self.https_port)
184 if (200 == urllib.urlopen(http_up_url, None, {}).getcode() and
185 200 == urllib.urlopen(https_up_url, None, {}).getcode()):
190 poll_interval = min(max(elapsed_time / 10., .1), 5)
191 time.sleep(poll_interval)
192 elapsed_time = time.time() - start_time
196 def StartServer(self):
197 """Start Web Page Replay and verify that it started.
200 ReplayNotStartedError: if Replay start-up fails.
202 cmd_line = [sys.executable, self.replay_py]
203 cmd_line.extend(self.replay_options)
204 cmd_line.append(self.archive_path)
206 logging.debug('Starting Web-Page-Replay: %s', cmd_line)
207 with self._OpenLogFile() as log_fh:
208 kwargs = {'stdout': log_fh, 'stderr': subprocess.STDOUT}
209 if sys.platform.startswith('linux') or sys.platform == 'darwin':
210 kwargs['preexec_fn'] = ResetInterruptHandler
211 self.replay_process = subprocess.Popen(cmd_line, **kwargs)
213 if not self.WaitForStart(30):
214 with open(self.log_path) as f:
216 raise ReplayNotStartedError(
217 'Web Page Replay failed to start. Log output:\n%s' % log)
219 def StopServer(self):
220 """Stop Web Page Replay."""
221 if self.replay_process:
222 logging.debug('Trying to stop Web-Page-Replay gracefully')
224 url = 'http://localhost:%s/web-page-replay-command-exit'
225 urllib.urlopen(url % self.http_port, None, {})
227 # IOError is possible because the server might exit without response.
230 start_time = time.time()
231 while time.time() - start_time < 10: # Timeout after 10 seconds.
232 if self.replay_process.poll() is not None:
237 # Use a SIGINT so that it can do graceful cleanup.
238 self.replay_process.send_signal(signal.SIGINT)
239 except: # pylint: disable=W0702
240 # On Windows, we are left with no other option than terminate().
241 if 'no-dns_forwarding' not in self.replay_options:
242 logging.warning('DNS configuration might not be restored!')
244 self.replay_process.terminate()
245 except: # pylint: disable=W0702
247 self.replay_process.wait()
250 """Add support for with-statement."""
254 def __exit__(self, unused_exc_type, unused_exc_val, unused_exc_tb):
255 """Add support for with-statement."""