| 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 |