OLD | NEW |
| (Empty) |
1 #!/usr/bin/env python | |
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
3 # Use of this source code is governed by a BSD-style license that can be | |
4 # found in the LICENSE file. | |
5 | |
6 """Start and stop Web Page Replay. | |
7 | |
8 Of the public module names, the following one is key: | |
9 ReplayServer: a class to start/stop Web Page Replay. | |
10 """ | |
11 | |
12 import logging | |
13 import os | |
14 import re | |
15 import signal | |
16 import subprocess | |
17 import sys | |
18 import time | |
19 import urllib | |
20 | |
21 | |
22 _CHROME_SRC_DIR = os.path.abspath(os.path.join( | |
23 os.path.dirname(__file__), os.pardir, os.pardir, os.pardir)) | |
24 REPLAY_DIR = os.path.join( | |
25 _CHROME_SRC_DIR, 'third_party', 'webpagereplay') | |
26 LOG_PATH = os.path.join( | |
27 _CHROME_SRC_DIR, 'webpagereplay_logs', 'logs.txt') | |
28 | |
29 | |
30 # Chrome options to make it work with Web Page Replay. | |
31 def GetChromeFlags(replay_host, http_port, https_port): | |
32 assert replay_host and http_port and https_port, 'All arguments required' | |
33 return [ | |
34 '--host-resolver-rules=MAP * %s,EXCLUDE localhost' % replay_host, | |
35 '--testing-fixed-http-port=%s' % http_port, | |
36 '--testing-fixed-https-port=%s' % https_port, | |
37 '--ignore-certificate-errors', | |
38 ] | |
39 | |
40 | |
41 # Signal masks on Linux are inherited from parent processes. If anything | |
42 # invoking us accidentally masks SIGINT (e.g. by putting a process in the | |
43 # background from a shell script), sending a SIGINT to the child will fail | |
44 # to terminate it. Running this signal handler before execing should fix that | |
45 # problem. | |
46 def ResetInterruptHandler(): | |
47 signal.signal(signal.SIGINT, signal.SIG_DFL) | |
48 | |
49 | |
50 class ReplayError(Exception): | |
51 """Catch-all exception for the module.""" | |
52 pass | |
53 | |
54 | |
55 class ReplayNotFoundError(ReplayError): | |
56 def __init__(self, label, path): | |
57 self.args = (label, path) | |
58 | |
59 def __str__(self): | |
60 label, path = self.args | |
61 return 'Path does not exist for %s: %s' % (label, path) | |
62 | |
63 | |
64 class ReplayNotStartedError(ReplayError): | |
65 pass | |
66 | |
67 | |
68 class ReplayServer(object): | |
69 """Start and Stop Web Page Replay. | |
70 | |
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 | |
75 bandwidth throttling. | |
76 | |
77 Example: | |
78 with ReplayServer(archive_path): | |
79 self.NavigateToURL(start_url) | |
80 self.WaitUntil(...) | |
81 | |
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. | |
86 """ | |
87 | |
88 def __init__(self, archive_path, replay_host, dns_port, http_port, https_port, | |
89 replay_options=None, replay_dir=None, | |
90 log_path=None): | |
91 """Initialize ReplayServer. | |
92 | |
93 Args: | |
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 | |
98 disabled. | |
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. | |
106 """ | |
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 | |
115 | |
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() | |
120 | |
121 self.replay_py = os.path.join(self.replay_dir, 'replay.py') | |
122 | |
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) | |
128 | |
129 self.log_fh = None | |
130 self.replay_process = None | |
131 | |
132 def _AddDefaultReplayOptions(self): | |
133 """Set WPR command-line options. Can be overridden if needed.""" | |
134 self.replay_options = [ | |
135 '--host', str(self._replay_host), | |
136 '--port', str(self.http_port), | |
137 '--ssl_port', str(self.https_port), | |
138 '--use_closest_match', | |
139 '--no-dns_forwarding', | |
140 '--log_level', 'warning' | |
141 ] + self.replay_options | |
142 if self.dns_port is not None: | |
143 self.replay_options.extend(['--dns_port', str(self.dns_port)]) | |
144 | |
145 def _CheckPath(self, label, path): | |
146 if not os.path.exists(path): | |
147 raise ReplayNotFoundError(label, path) | |
148 | |
149 def _OpenLogFile(self): | |
150 log_dir = os.path.dirname(self.log_path) | |
151 if not os.path.exists(log_dir): | |
152 os.makedirs(log_dir) | |
153 return open(self.log_path, 'w') | |
154 | |
155 def WaitForStart(self, timeout): | |
156 """Checks to see if the server is up and running.""" | |
157 port_re = re.compile( | |
158 '.*?(?P<protocol>[A-Z]+) server started on (?P<host>.*):(?P<port>\d+)') | |
159 | |
160 start_time = time.time() | |
161 elapsed_time = 0 | |
162 while elapsed_time < timeout: | |
163 if self.replay_process.poll() is not None: | |
164 break # The process has exited. | |
165 | |
166 # Read the ports from the WPR log. | |
167 if not self.http_port or not self.https_port or not self.dns_port: | |
168 for line in open(self.log_path).readlines(): | |
169 m = port_re.match(line.strip()) | |
170 if m: | |
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')) | |
177 | |
178 # Try to connect to the WPR ports. | |
179 if self.http_port and self.https_port: | |
180 try: | |
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()): | |
186 return True | |
187 except IOError: | |
188 pass | |
189 | |
190 poll_interval = min(max(elapsed_time / 10., .1), 5) | |
191 time.sleep(poll_interval) | |
192 elapsed_time = time.time() - start_time | |
193 | |
194 return False | |
195 | |
196 def StartServer(self): | |
197 """Start Web Page Replay and verify that it started. | |
198 | |
199 Raises: | |
200 ReplayNotStartedError: if Replay start-up fails. | |
201 """ | |
202 cmd_line = [sys.executable, self.replay_py] | |
203 cmd_line.extend(self.replay_options) | |
204 cmd_line.append(self.archive_path) | |
205 self.log_fh = self._OpenLogFile() | |
206 logging.debug('Starting Web-Page-Replay: %s', cmd_line) | |
207 kwargs = {'stdout': self.log_fh, 'stderr': subprocess.STDOUT} | |
208 if sys.platform.startswith('linux') or sys.platform == 'darwin': | |
209 kwargs['preexec_fn'] = ResetInterruptHandler | |
210 self.replay_process = subprocess.Popen(cmd_line, **kwargs) | |
211 if not self.WaitForStart(30): | |
212 log = open(self.log_path).read() | |
213 raise ReplayNotStartedError( | |
214 'Web Page Replay failed to start. Log output:\n%s' % log) | |
215 | |
216 def StopServer(self): | |
217 """Stop Web Page Replay.""" | |
218 if self.replay_process: | |
219 logging.debug('Trying to stop Web-Page-Replay gracefully') | |
220 try: | |
221 url = 'http://localhost:%s/web-page-replay-command-exit' | |
222 urllib.urlopen(url % self.http_port, None, {}) | |
223 except IOError: | |
224 # IOError is possible because the server might exit without response. | |
225 pass | |
226 | |
227 start_time = time.time() | |
228 while time.time() - start_time < 10: # Timeout after 10 seconds. | |
229 if self.replay_process.poll() is not None: | |
230 break | |
231 time.sleep(1) | |
232 else: | |
233 try: | |
234 # Use a SIGINT so that it can do graceful cleanup. | |
235 self.replay_process.send_signal(signal.SIGINT) | |
236 except: # pylint: disable=W0702 | |
237 # On Windows, we are left with no other option than terminate(). | |
238 if 'no-dns_forwarding' not in self.replay_options: | |
239 logging.warning('DNS configuration might not be restored!') | |
240 try: | |
241 self.replay_process.terminate() | |
242 except: # pylint: disable=W0702 | |
243 pass | |
244 self.replay_process.wait() | |
245 if self.log_fh: | |
246 self.log_fh.close() | |
247 | |
248 def __enter__(self): | |
249 """Add support for with-statement.""" | |
250 self.StartServer() | |
251 return self | |
252 | |
253 def __exit__(self, unused_exc_type, unused_exc_val, unused_exc_tb): | |
254 """Add support for with-statement.""" | |
255 self.StopServer() | |
OLD | NEW |