1 # Copyright (C) 2011 Google Inc. All rights reserved. 2 # 3 # Redistribution and use in source and binary forms, with or without 4 # modification, are permitted provided that the following conditions are 5 # met: 6 # 7 # * Redistributions of source code must retain the above copyright 8 # notice, this list of conditions and the following disclaimer. 9 # * Redistributions in binary form must reproduce the above 10 # copyright notice, this list of conditions and the following disclaimer 11 # in the documentation and/or other materials provided with the 12 # distribution. 13 # * Neither the name of Google Inc. nor the names of its 14 # contributors may be used to endorse or promote products derived from 15 # this software without specific prior written permission. 16 # 17 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 # OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 29 """Base class used to start servers used by the layout tests.""" 30 31 import errno 32 import logging 33 import socket 34 import tempfile 35 import time 36 37 38 _log = logging.getLogger(__name__) 39 40 41 class ServerError(Exception): 42 pass 43 44 45 class ServerBase(object): 46 """A skeleton class for starting and stopping servers used by the layout tests.""" 47 48 def __init__(self, port_obj, output_dir): 49 self._port_obj = port_obj 50 self._executive = port_obj._executive 51 self._filesystem = port_obj._filesystem 52 self._platform = port_obj.host.platform 53 self._output_dir = output_dir 54 55 # We need a non-checkout-dependent place to put lock files, etc. We 56 # don't use the Python default on the Mac because it defaults to a 57 # randomly-generated directory under /var/folders and no one would ever 58 # look there. 59 tmpdir = tempfile.gettempdir() 60 if self._platform.is_mac(): 61 tmpdir = '/tmp' 62 63 self._runtime_path = self._filesystem.join(tmpdir, "WebKit") 64 self._filesystem.maybe_make_directory(self._runtime_path) 65 66 # Subclasses must override these fields. 67 self._name = '<virtual>' 68 self._log_prefixes = tuple() 69 self._mappings = {} 70 self._pid_file = None 71 self._start_cmd = None 72 73 # Subclasses may override these fields. 74 self._env = None 75 self._stdout = self._executive.PIPE 76 self._stderr = self._executive.PIPE 77 self._process = None 78 self._pid = None 79 self._error_log_path = None 80 81 def start(self): 82 """Starts the server. It is an error to start an already started server. 83 84 This method also stops any stale servers started by a previous instance.""" 85 assert not self._pid, '%s server is already running' % self._name 86 87 # Stop any stale servers left over from previous instances. 88 if self._filesystem.exists(self._pid_file): 89 try: 90 self._pid = int(self._filesystem.read_text_file(self._pid_file)) 91 _log.debug('stale %s pid file, pid %d' % (self._name, self._pid)) 92 self._stop_running_server() 93 except (ValueError, UnicodeDecodeError): 94 # These could be raised if the pid file is corrupt. 95 self._remove_pid_file() 96 self._pid = None 97 98 self._remove_stale_logs() 99 self._prepare_config() 100 self._check_that_all_ports_are_available() 101 102 self._pid = self._spawn_process() 103 104 if self._wait_for_action(self._is_server_running_on_all_ports): 105 _log.debug("%s successfully started (pid = %d)" % (self._name, self._pid)) 106 else: 107 self._log_errors_from_subprocess() 108 self._stop_running_server() 109 raise ServerError('Failed to start %s server' % self._name) 110 111 def stop(self): 112 """Stops the server. Stopping a server that isn't started is harmless.""" 113 actual_pid = None 114 try: 115 if self._filesystem.exists(self._pid_file): 116 try: 117 actual_pid = int(self._filesystem.read_text_file(self._pid_file)) 118 except (ValueError, UnicodeDecodeError): 119 # These could be raised if the pid file is corrupt. 120 pass 121 if not self._pid: 122 self._pid = actual_pid 123 124 if not self._pid: 125 return 126 127 if not actual_pid: 128 _log.warning('Failed to stop %s: pid file is missing' % self._name) 129 return 130 if self._pid != actual_pid: 131 _log.warning('Failed to stop %s: pid file contains %d, not %d' % 132 (self._name, actual_pid, self._pid)) 133 # Try to kill the existing pid, anyway, in case it got orphaned. 134 self._executive.kill_process(self._pid) 135 self._pid = None 136 return 137 138 _log.debug("Attempting to shut down %s server at pid %d" % (self._name, self._pid)) 139 self._stop_running_server() 140 _log.debug("%s server at pid %d stopped" % (self._name, self._pid)) 141 self._pid = None 142 finally: 143 # Make sure we delete the pid file no matter what happens. 144 self._remove_pid_file() 145 146 def _prepare_config(self): 147 """This routine can be overridden by subclasses to do any sort 148 of initialization required prior to starting the server that may fail.""" 149 pass 150 151 def _remove_stale_logs(self): 152 """This routine can be overridden by subclasses to try and remove logs 153 left over from a prior run. This routine should log warnings if the 154 files cannot be deleted, but should not fail unless failure to 155 delete the logs will actually cause start() to fail.""" 156 # Sometimes logs are open in other processes but they should clear eventually. 157 for log_prefix in self._log_prefixes: 158 try: 159 self._remove_log_files(self._output_dir, log_prefix) 160 except OSError, e: 161 _log.warning('Failed to remove old %s %s files' % (self._name, log_prefix)) 162 163 def _spawn_process(self): 164 _log.debug('Starting %s server, cmd="%s"' % (self._name, self._start_cmd)) 165 process = self._executive.popen(self._start_cmd, env=self._env, stdout=self._stdout, stderr=self._stderr) 166 pid = process.pid 167 self._filesystem.write_text_file(self._pid_file, str(pid)) 168 return pid 169 170 def _stop_running_server(self): 171 self._wait_for_action(self._check_and_kill) 172 if self._filesystem.exists(self._pid_file): 173 self._filesystem.remove(self._pid_file) 174 175 def _check_and_kill(self): 176 if self._executive.check_running_pid(self._pid): 177 _log.debug('pid %d is running, killing it' % self._pid) 178 host = self._port_obj.host 179 self._executive.kill_process(self._pid) 180 return False 181 else: 182 _log.debug('pid %d is not running' % self._pid) 183 184 return True 185 186 def _remove_pid_file(self): 187 if self._filesystem.exists(self._pid_file): 188 self._filesystem.remove(self._pid_file) 189 190 def _remove_log_files(self, folder, starts_with): 191 files = self._filesystem.listdir(folder) 192 for file in files: 193 if file.startswith(starts_with): 194 full_path = self._filesystem.join(folder, file) 195 self._filesystem.remove(full_path) 196 197 def _log_errors_from_subprocess(self): 198 _log.error('logging %s errors, if any' % self._name) 199 if self._process: 200 _log.error('%s returncode %s' % (self._name, str(self._process.returncode))) 201 if self._process.stderr: 202 stderr_text = self._process.stderr.read() 203 if stderr_text: 204 _log.error('%s stderr:' % self._name) 205 for line in stderr_text.splitlines(): 206 _log.error(' %s' % line) 207 else: 208 _log.error('%s no stderr' % self._name) 209 else: 210 _log.error('%s no stderr handle' % self._name) 211 else: 212 _log.error('%s no process' % self._name) 213 if self._error_log_path and self._filesystem.exists(self._error_log_path): 214 error_log_text = self._filesystem.read_text_file(self._error_log_path) 215 if error_log_text: 216 _log.error('%s error log (%s) contents:' % (self._name, self._error_log_path)) 217 for line in error_log_text.splitlines(): 218 _log.error(' %s' % line) 219 else: 220 _log.error('%s error log empty' % self._name) 221 _log.error('') 222 else: 223 _log.error('%s no error log' % self._name) 224 225 def _wait_for_action(self, action, wait_secs=20.0, sleep_secs=1.0): 226 """Repeat the action for wait_sec or until it succeeds, sleeping for sleep_secs 227 in between each attempt. Returns whether it succeeded.""" 228 start_time = time.time() 229 while time.time() - start_time < wait_secs: 230 if action(): 231 return True 232 _log.debug("Waiting for action: %s" % action) 233 time.sleep(sleep_secs) 234 235 return False 236 237 def _is_server_running_on_all_ports(self): 238 """Returns whether the server is running on all the desired ports.""" 239 240 # TODO(dpranke): crbug/378444 maybe pid is unreliable on win? 241 if not self._platform.is_win() and not self._executive.check_running_pid(self._pid): 242 _log.debug("Server isn't running at all") 243 self._log_errors_from_subprocess() 244 raise ServerError("Server exited") 245 246 for mapping in self._mappings: 247 s = socket.socket() 248 port = mapping['port'] 249 try: 250 s.connect(('localhost', port)) 251 _log.debug("Server running on %d" % port) 252 except IOError, e: 253 if e.errno not in (errno.ECONNREFUSED, errno.ECONNRESET): 254 raise 255 _log.debug("Server NOT running on %d: %s" % (port, e)) 256 return False 257 finally: 258 s.close() 259 return True 260 261 def _check_that_all_ports_are_available(self): 262 for mapping in self._mappings: 263 s = socket.socket() 264 if not self._platform.is_win(): 265 s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 266 port = mapping['port'] 267 try: 268 s.bind(('localhost', port)) 269 except IOError, e: 270 if e.errno in (errno.EALREADY, errno.EADDRINUSE): 271 raise ServerError('Port %d is already in use.' % port) 272 elif self._platform.is_win() and e.errno in (errno.WSAEACCES,): # pylint: disable=E1101 273 raise ServerError('Port %d is already in use.' % port) 274 else: 275 raise 276 finally: 277 s.close() 278 _log.debug('all ports are available') 279