1#!/usr/bin/env python 2# Copyright (C) 2010 Google Inc. All rights reserved. 3# 4# Redistribution and use in source and binary forms, with or without 5# modification, are permitted provided that the following conditions are 6# met: 7# 8# * Redistributions of source code must retain the above copyright 9# notice, this list of conditions and the following disclaimer. 10# * Redistributions in binary form must reproduce the above 11# copyright notice, this list of conditions and the following disclaimer 12# in the documentation and/or other materials provided with the 13# distribution. 14# * Neither the Google name nor the names of its 15# contributors may be used to endorse or promote products derived from 16# this software without specific prior written permission. 17# 18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 30"""Package that implements the ServerProcess wrapper class""" 31 32import logging 33import os 34import select 35import signal 36import subprocess 37import sys 38import time 39if sys.platform != 'win32': 40 import fcntl 41 42from webkitpy.common.system.executive import Executive 43 44_log = logging.getLogger("webkitpy.layout_tests.port.server_process") 45 46 47class ServerProcess: 48 """This class provides a wrapper around a subprocess that 49 implements a simple request/response usage model. The primary benefit 50 is that reading responses takes a timeout, so that we don't ever block 51 indefinitely. The class also handles transparently restarting processes 52 as necessary to keep issuing commands.""" 53 54 def __init__(self, port_obj, name, cmd, env=None, executive=Executive()): 55 self._port = port_obj 56 self._name = name 57 self._cmd = cmd 58 self._env = env 59 self._reset() 60 self._executive = executive 61 62 def _reset(self): 63 self._proc = None 64 self._output = '' 65 self.crashed = False 66 self.timed_out = False 67 self.error = '' 68 69 def _start(self): 70 if self._proc: 71 raise ValueError("%s already running" % self._name) 72 self._reset() 73 # close_fds is a workaround for http://bugs.python.org/issue2320 74 close_fds = sys.platform not in ('win32', 'cygwin') 75 self._proc = subprocess.Popen(self._cmd, stdin=subprocess.PIPE, 76 stdout=subprocess.PIPE, 77 stderr=subprocess.PIPE, 78 close_fds=close_fds, 79 env=self._env) 80 fd = self._proc.stdout.fileno() 81 fl = fcntl.fcntl(fd, fcntl.F_GETFL) 82 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) 83 fd = self._proc.stderr.fileno() 84 fl = fcntl.fcntl(fd, fcntl.F_GETFL) 85 fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) 86 87 def handle_interrupt(self): 88 """This routine checks to see if the process crashed or exited 89 because of a keyboard interrupt and raises KeyboardInterrupt 90 accordingly.""" 91 if self.crashed: 92 # This is hex code 0xc000001d, which is used for abrupt 93 # termination. This happens if we hit ctrl+c from the prompt 94 # and we happen to be waiting on the DumpRenderTree. 95 # sdoyon: Not sure for which OS and in what circumstances the 96 # above code is valid. What works for me under Linux to detect 97 # ctrl+c is for the subprocess returncode to be negative 98 # SIGINT. And that agrees with the subprocess documentation. 99 if (-1073741510 == self._proc.returncode or 100 - signal.SIGINT == self._proc.returncode): 101 raise KeyboardInterrupt 102 return 103 104 def poll(self): 105 """Check to see if the underlying process is running; returns None 106 if it still is (wrapper around subprocess.poll).""" 107 if self._proc: 108 # poll() is not threadsafe and can throw OSError due to: 109 # http://bugs.python.org/issue1731717 110 return self._proc.poll() 111 return None 112 113 def write(self, input): 114 """Write a request to the subprocess. The subprocess is (re-)start()'ed 115 if is not already running.""" 116 if not self._proc: 117 self._start() 118 try: 119 self._proc.stdin.write(input) 120 except IOError, e: 121 self.stop() 122 self.crashed = True 123 124 def read_line(self, timeout): 125 """Read a single line from the subprocess, waiting until the deadline. 126 If the deadline passes, the call times out. Note that even if the 127 subprocess has crashed or the deadline has passed, if there is output 128 pending, it will be returned. 129 130 Args: 131 timeout: floating-point number of seconds the call is allowed 132 to block for. A zero or negative number will attempt to read 133 any existing data, but will not block. There is no way to 134 block indefinitely. 135 Returns: 136 output: data returned, if any. If no data is available and the 137 call times out or crashes, an empty string is returned. Note 138 that the returned string includes the newline ('\n').""" 139 return self._read(timeout, size=0) 140 141 def read(self, timeout, size): 142 """Attempts to read size characters from the subprocess, waiting until 143 the deadline passes. If the deadline passes, any available data will be 144 returned. Note that even if the deadline has passed or if the 145 subprocess has crashed, any available data will still be returned. 146 147 Args: 148 timeout: floating-point number of seconds the call is allowed 149 to block for. A zero or negative number will attempt to read 150 any existing data, but will not block. There is no way to 151 block indefinitely. 152 size: amount of data to read. Must be a postive integer. 153 Returns: 154 output: data returned, if any. If no data is available, an empty 155 string is returned. 156 """ 157 if size <= 0: 158 raise ValueError('ServerProcess.read() called with a ' 159 'non-positive size: %d ' % size) 160 return self._read(timeout, size) 161 162 def _read(self, timeout, size): 163 """Internal routine that actually does the read.""" 164 index = -1 165 out_fd = self._proc.stdout.fileno() 166 err_fd = self._proc.stderr.fileno() 167 select_fds = (out_fd, err_fd) 168 deadline = time.time() + timeout 169 while not self.timed_out and not self.crashed: 170 # poll() is not threadsafe and can throw OSError due to: 171 # http://bugs.python.org/issue1731717 172 if self._proc.poll() != None: 173 self.crashed = True 174 self.handle_interrupt() 175 176 now = time.time() 177 if now > deadline: 178 self.timed_out = True 179 180 # Check to see if we have any output we can return. 181 if size and len(self._output) >= size: 182 index = size 183 elif size == 0: 184 index = self._output.find('\n') + 1 185 186 if index > 0 or self.crashed or self.timed_out: 187 output = self._output[0:index] 188 self._output = self._output[index:] 189 return output 190 191 # Nope - wait for more data. 192 (read_fds, write_fds, err_fds) = select.select(select_fds, [], 193 select_fds, 194 deadline - now) 195 try: 196 if out_fd in read_fds: 197 self._output += self._proc.stdout.read() 198 if err_fd in read_fds: 199 self.error += self._proc.stderr.read() 200 except IOError, e: 201 pass 202 203 def stop(self): 204 """Stop (shut down) the subprocess), if it is running.""" 205 pid = self._proc.pid 206 self._proc.stdin.close() 207 self._proc.stdout.close() 208 if self._proc.stderr: 209 self._proc.stderr.close() 210 if sys.platform not in ('win32', 'cygwin'): 211 # Closing stdin/stdout/stderr hangs sometimes on OS X, 212 # (see restart(), above), and anyway we don't want to hang 213 # the harness if DumpRenderTree is buggy, so we wait a couple 214 # seconds to give DumpRenderTree a chance to clean up, but then 215 # force-kill the process if necessary. 216 KILL_TIMEOUT = 3.0 217 timeout = time.time() + KILL_TIMEOUT 218 # poll() is not threadsafe and can throw OSError due to: 219 # http://bugs.python.org/issue1731717 220 while self._proc.poll() is None and time.time() < timeout: 221 time.sleep(0.1) 222 # poll() is not threadsafe and can throw OSError due to: 223 # http://bugs.python.org/issue1731717 224 if self._proc.poll() is None: 225 _log.warning('stopping %s timed out, killing it' % 226 self._name) 227 self._executive.kill_process(self._proc.pid) 228 _log.warning('killed') 229 self._reset() 230