2010-04-21 Eric Seidel <eric@webkit.org>
[WebKit-https.git] / WebKitTools / Scripts / webkitpy / layout_tests / port / server_process.py
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
32 import fcntl
33 import logging
34 import os
35 import select
36 import signal
37 import subprocess
38 import sys
39 import time
40
41 _log = logging.getLogger("webkitpy.layout_tests.port.server_process")
42
43
44 class ServerProcess:
45     """This class provides a wrapper around a subprocess that
46     implements a simple request/response usage model. The primary benefit
47     is that reading responses takes a timeout, so that we don't ever block
48     indefinitely. The class also handles transparently restarting processes
49     as necessary to keep issuing commands."""
50
51     def __init__(self, port_obj, name, cmd, env=None):
52         self._port = port_obj
53         self._name = name
54         self._cmd = cmd
55         self._env = env
56         self._reset()
57
58     def _reset(self):
59         self._proc = None
60         self._output = ''
61         self.crashed = False
62         self.timed_out = False
63         self.error = ''
64
65     def _start(self):
66         if self._proc:
67             raise ValueError("%s already running" % self._name)
68         self._reset()
69         close_fds = sys.platform not in ('win32', 'cygwin')
70         self._proc = subprocess.Popen(self._cmd, stdin=subprocess.PIPE,
71                                       stdout=subprocess.PIPE,
72                                       stderr=subprocess.PIPE,
73                                       close_fds=close_fds,
74                                       env=self._env)
75         fd = self._proc.stdout.fileno()
76         fl = fcntl.fcntl(fd, fcntl.F_GETFL)
77         fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
78         fd = self._proc.stderr.fileno()
79         fl = fcntl.fcntl(fd, fcntl.F_GETFL)
80         fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK)
81
82     def handle_interrupt(self):
83         """This routine checks to see if the process crashed or exited
84         because of a keyboard interrupt and raises KeyboardInterrupt
85         accordingly."""
86         if self.crashed:
87             # This is hex code 0xc000001d, which is used for abrupt
88             # termination. This happens if we hit ctrl+c from the prompt
89             # and we happen to be waiting on the DumpRenderTree.
90             # sdoyon: Not sure for which OS and in what circumstances the
91             # above code is valid. What works for me under Linux to detect
92             # ctrl+c is for the subprocess returncode to be negative
93             # SIGINT. And that agrees with the subprocess documentation.
94             if (-1073741510 == self._proc.returncode or
95                 - signal.SIGINT == self._proc.returncode):
96                 raise KeyboardInterrupt
97             return
98
99     def poll(self):
100         """Check to see if the underlying process is running; returns None
101         if it still is (wrapper around subprocess.poll)."""
102         if self._proc:
103             return self._proc.poll()
104         return None
105
106     def returncode(self):
107         """Returns the exit code from the subprcoess; returns None if the
108         process hasn't exited (this is a wrapper around subprocess.returncode).
109         """
110         if self._proc:
111             return self._proc.returncode
112         return None
113
114     def write(self, input):
115         """Write a request to the subprocess. The subprocess is (re-)start()'ed
116         if is not already running."""
117         if not self._proc:
118             self._start()
119         self._proc.stdin.write(input)
120
121     def read_line(self, timeout):
122         """Read a single line from the subprocess, waiting until the deadline.
123         If the deadline passes, the call times out. Note that even if the
124         subprocess has crashed or the deadline has passed, if there is output
125         pending, it will be returned.
126
127         Args:
128             timeout: floating-point number of seconds the call is allowed
129                 to block for. A zero or negative number will attempt to read
130                 any existing data, but will not block. There is no way to
131                 block indefinitely.
132         Returns:
133             output: data returned, if any. If no data is available and the
134                 call times out or crashes, an empty string is returned. Note
135                 that the returned string includes the newline ('\n')."""
136         return self._read(timeout, size=0)
137
138     def read(self, timeout, size):
139         """Attempts to read size characters from the subprocess, waiting until
140         the deadline passes. If the deadline passes, any available data will be
141         returned. Note that even if the deadline has passed or if the
142         subprocess has crashed, any available data will still be returned.
143
144         Args:
145             timeout: floating-point number of seconds the call is allowed
146                 to block for. A zero or negative number will attempt to read
147                 any existing data, but will not block. There is no way to
148                 block indefinitely.
149             size: amount of data to read. Must be a postive integer.
150         Returns:
151             output: data returned, if any. If no data is available, an empty
152                 string is returned.
153         """
154         if size <= 0:
155             raise ValueError('ServerProcess.read() called with a '
156                              'non-positive size: %d ' % size)
157         return self._read(timeout, size)
158
159     def _read(self, timeout, size):
160         """Internal routine that actually does the read."""
161         index = -1
162         out_fd = self._proc.stdout.fileno()
163         err_fd = self._proc.stderr.fileno()
164         select_fds = (out_fd, err_fd)
165         deadline = time.time() + timeout
166         while not self.timed_out and not self.crashed:
167             if self._proc.poll() != None:
168                 self.crashed = True
169                 self.handle_interrupt()
170
171             now = time.time()
172             if now > deadline:
173                 self.timed_out = True
174
175             # Check to see if we have any output we can return.
176             if size and len(self._output) >= size:
177                 index = size
178             elif size == 0:
179                 index = self._output.find('\n') + 1
180
181             if index or self.crashed or self.timed_out:
182                 output = self._output[0:index]
183                 self._output = self._output[index:]
184                 return output
185
186             # Nope - wait for more data.
187             (read_fds, write_fds, err_fds) = select.select(select_fds, [],
188                                                            select_fds,
189                                                            deadline - now)
190             try:
191                 if out_fd in read_fds:
192                     self._output += self._proc.stdout.read()
193                 if err_fd in read_fds:
194                     self.error += self._proc.stderr.read()
195             except IOError, e:
196                 pass
197
198     def stop(self):
199         """Stop (shut down) the subprocess), if it is running."""
200         pid = self._proc.pid
201         self._proc.stdin.close()
202         self._proc.stdout.close()
203         if self._proc.stderr:
204             self._proc.stderr.close()
205         if sys.platform not in ('win32', 'cygwin'):
206             # Closing stdin/stdout/stderr hangs sometimes on OS X,
207             # (see restart(), above), and anyway we don't want to hang
208             # the harness if DumpRenderTree is buggy, so we wait a couple
209             # seconds to give DumpRenderTree a chance to clean up, but then
210             # force-kill the process if necessary.
211             KILL_TIMEOUT = 3.0
212             timeout = time.time() + KILL_TIMEOUT
213             while self._proc.poll() is None and time.time() < timeout:
214                 time.sleep(0.1)
215             if self._proc.poll() is None:
216                 _log.warning('stopping %s timed out, killing it' %
217                              self._name)
218                 # FIXME: This should use Executive.
219                 null = open(os.devnull, "w")
220                 subprocess.Popen(["kill", "-9",
221                                   str(self._proc.pid)], stderr=null)
222                 null.close()
223                 _log.warning('killed')
224         self._reset()