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 name of Google Inc. 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"""A class to help start/stop the PyWebSocket server used by layout tests."""
31
32
33from __future__ import with_statement
34
35import codecs
36import logging
37import optparse
38import os
39import subprocess
40import sys
41import tempfile
42import time
43import urllib
44
45import factory
46import http_server
47
48from webkitpy.common.system.executive import Executive
49from webkitpy.thirdparty.autoinstalled.pywebsocket import mod_pywebsocket
50
51
52_log = logging.getLogger("webkitpy.layout_tests.port.websocket_server")
53
54_WS_LOG_PREFIX = 'pywebsocket.ws.log-'
55_WSS_LOG_PREFIX = 'pywebsocket.wss.log-'
56
57_DEFAULT_WS_PORT = 8880
58_DEFAULT_WSS_PORT = 9323
59
60
61def url_is_alive(url):
62    """Checks to see if we get an http response from |url|.
63    We poll the url 20 times with a 0.5 second delay.  If we don't
64    get a reply in that time, we give up and assume the httpd
65    didn't start properly.
66
67    Args:
68      url: The URL to check.
69    Return:
70      True if the url is alive.
71    """
72    sleep_time = 0.5
73    wait_time = 10
74    while wait_time > 0:
75        try:
76            response = urllib.urlopen(url, proxies={})
77            # Server is up and responding.
78            return True
79        except IOError:
80            pass
81        # Wait for sleep_time before trying again.
82        wait_time -= sleep_time
83        time.sleep(sleep_time)
84
85    return False
86
87
88class PyWebSocketNotStarted(Exception):
89    pass
90
91
92class PyWebSocketNotFound(Exception):
93    pass
94
95
96class PyWebSocket(http_server.Lighttpd):
97
98    def __init__(self, port_obj, output_dir, port=_DEFAULT_WS_PORT,
99                 root=None, use_tls=False,
100                 pidfile=None):
101        """Args:
102          output_dir: the absolute path to the layout test result directory
103        """
104        http_server.Lighttpd.__init__(self, port_obj, output_dir,
105                                      port=_DEFAULT_WS_PORT,
106                                      root=root)
107        self._output_dir = output_dir
108        self._process = None
109        self._port = port
110        self._root = root
111        self._use_tls = use_tls
112        self._private_key = self._pem_file
113        self._certificate = self._pem_file
114        if self._port:
115            self._port = int(self._port)
116        if self._use_tls:
117            self._server_name = 'PyWebSocket(Secure)'
118        else:
119            self._server_name = 'PyWebSocket'
120        self._pidfile = pidfile
121        self._wsout = None
122
123        # Webkit tests
124        if self._root:
125            self._layout_tests = os.path.abspath(self._root)
126            self._web_socket_tests = os.path.abspath(
127                os.path.join(self._root, 'http', 'tests',
128                             'websocket', 'tests'))
129        else:
130            try:
131                self._layout_tests = self._port_obj.layout_tests_dir()
132                self._web_socket_tests = os.path.join(self._layout_tests,
133                     'http', 'tests', 'websocket', 'tests')
134            except:
135                self._web_socket_tests = None
136
137    def start(self):
138        if not self._web_socket_tests:
139            _log.info('No need to start %s server.' % self._server_name)
140            return
141        if self.is_running():
142            raise PyWebSocketNotStarted('%s is already running.' %
143                                        self._server_name)
144
145        time_str = time.strftime('%d%b%Y-%H%M%S')
146        if self._use_tls:
147            log_prefix = _WSS_LOG_PREFIX
148        else:
149            log_prefix = _WS_LOG_PREFIX
150        log_file_name = log_prefix + time_str
151
152        # Remove old log files. We only need to keep the last ones.
153        self.remove_log_files(self._output_dir, log_prefix)
154
155        error_log = os.path.join(self._output_dir, log_file_name + "-err.txt")
156
157        output_log = os.path.join(self._output_dir, log_file_name + "-out.txt")
158        self._wsout = codecs.open(output_log, "w", "utf-8")
159
160        python_interp = sys.executable
161        pywebsocket_base = os.path.join(
162            os.path.dirname(os.path.dirname(os.path.dirname(
163            os.path.abspath(__file__)))), 'thirdparty',
164            'autoinstalled', 'pywebsocket')
165        pywebsocket_script = os.path.join(pywebsocket_base, 'mod_pywebsocket',
166            'standalone.py')
167        start_cmd = [
168            python_interp, '-u', pywebsocket_script,
169            '--server-host', '127.0.0.1',
170            '--port', str(self._port),
171            '--document-root', os.path.join(self._layout_tests, 'http', 'tests'),
172            '--scan-dir', self._web_socket_tests,
173            '--cgi-paths', '/websocket/tests',
174            '--log-file', error_log,
175        ]
176
177        handler_map_file = os.path.join(self._web_socket_tests,
178                                        'handler_map.txt')
179        if os.path.exists(handler_map_file):
180            _log.debug('Using handler_map_file: %s' % handler_map_file)
181            start_cmd.append('--websock-handlers-map-file')
182            start_cmd.append(handler_map_file)
183        else:
184            _log.warning('No handler_map_file found')
185
186        if self._use_tls:
187            start_cmd.extend(['-t', '-k', self._private_key,
188                              '-c', self._certificate])
189
190        env = self._port_obj.setup_environ_for_server()
191        env['PYTHONPATH'] = (pywebsocket_base + os.path.pathsep +
192                             env.get('PYTHONPATH', ''))
193
194        _log.debug('Starting %s server on %d.' % (
195                   self._server_name, self._port))
196        _log.debug('cmdline: %s' % ' '.join(start_cmd))
197        # FIXME: We should direct this call through Executive for testing.
198        # Note: Not thread safe: http://bugs.python.org/issue2320
199        self._process = subprocess.Popen(start_cmd,
200                                         stdin=open(os.devnull, 'r'),
201                                         stdout=self._wsout,
202                                         stderr=subprocess.STDOUT,
203                                         env=env)
204
205        if self._use_tls:
206            url = 'https'
207        else:
208            url = 'http'
209        url = url + '://127.0.0.1:%d/' % self._port
210        if not url_is_alive(url):
211            if self._process.returncode == None:
212                # FIXME: We should use a non-static Executive for easier
213                # testing.
214                Executive().kill_process(self._process.pid)
215            with codecs.open(output_log, "r", "utf-8") as fp:
216                for line in fp:
217                    _log.error(line)
218            raise PyWebSocketNotStarted(
219                'Failed to start %s server on port %s.' %
220                    (self._server_name, self._port))
221
222        # Our process terminated already
223        if self._process.returncode != None:
224            raise PyWebSocketNotStarted(
225                'Failed to start %s server.' % self._server_name)
226        if self._pidfile:
227            with codecs.open(self._pidfile, "w", "ascii") as file:
228                file.write("%d" % self._process.pid)
229
230    def stop(self, force=False):
231        if not force and not self.is_running():
232            return
233
234        pid = None
235        if self._process:
236            pid = self._process.pid
237        elif self._pidfile:
238            with codecs.open(self._pidfile, "r", "ascii") as file:
239                pid = int(file.read().strip())
240
241        if not pid:
242            raise PyWebSocketNotFound(
243                'Failed to find %s server pid.' % self._server_name)
244
245        _log.debug('Shutting down %s server %d.' % (self._server_name, pid))
246        # FIXME: We should use a non-static Executive for easier testing.
247        Executive().kill_process(pid)
248
249        if self._process:
250            # wait() is not threadsafe and can throw OSError due to:
251            # http://bugs.python.org/issue1731717
252            self._process.wait()
253            self._process = None
254
255        if self._wsout:
256            self._wsout.close()
257            self._wsout = None
258