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