1 # Copyright 2009, Google Inc.
4 # Redistribution and use in source and binary forms, with or without
5 # modification, are permitted provided that the following conditions are
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
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.
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.
31 """Dispatch Web Socket request.
41 _SOURCE_PATH_PATTERN = re.compile(r'(?i)_wsh\.py$')
42 _SOURCE_SUFFIX = '_wsh.py'
43 _DO_EXTRA_HANDSHAKE_HANDLER_NAME = 'web_socket_do_extra_handshake'
44 _TRANSFER_DATA_HANDLER_NAME = 'web_socket_transfer_data'
47 class DispatchError(Exception):
48 """Exception in dispatching Web Socket request."""
53 def _normalize_path(path):
57 path: the path to normalize.
59 Path is converted to the absolute path.
60 The input path can use either '\\' or '/' as the separator.
61 The normalized path always uses '/' regardless of the platform.
64 path = path.replace('\\', os.path.sep)
65 path = os.path.abspath(path)
66 path = path.replace('\\', '/')
70 def _path_to_resource_converter(base_dir):
71 base_dir = _normalize_path(base_dir)
72 base_len = len(base_dir)
73 suffix_len = len(_SOURCE_SUFFIX)
75 if not path.endswith(_SOURCE_SUFFIX):
77 path = _normalize_path(path)
78 if not path.startswith(base_dir):
80 return path[base_len:-suffix_len]
84 def _source_file_paths(directory):
85 """Yield Web Socket Handler source file names in the given directory."""
87 for root, unused_dirs, files in os.walk(directory):
89 path = os.path.join(root, base)
90 if _SOURCE_PATH_PATTERN.search(path):
94 def _source(source_str):
95 """Source a handler definition string."""
99 exec source_str in global_dic
101 raise DispatchError('Error in sourcing handler:' +
102 util.get_stack_trace())
103 return (_extract_handler(global_dic, _DO_EXTRA_HANDSHAKE_HANDLER_NAME),
104 _extract_handler(global_dic, _TRANSFER_DATA_HANDLER_NAME))
107 def _extract_handler(dic, name):
109 raise DispatchError('%s is not defined.' % name)
111 if not callable(handler):
112 raise DispatchError('%s is not callable.' % name)
116 class Dispatcher(object):
117 """Dispatches Web Socket requests.
119 This class maintains a map from resource name to handlers.
122 def __init__(self, root_dir, scan_dir=None):
123 """Construct an instance.
126 root_dir: The directory where handler definition files are
128 scan_dir: The directory where handler definition files are
129 searched. scan_dir must be a directory under root_dir,
130 including root_dir itself. If scan_dir is None, root_dir
131 is used as scan_dir. scan_dir can be useful in saving
132 scan time when root_dir contains many subdirectories.
136 self._source_warnings = []
139 if not os.path.realpath(scan_dir).startswith(os.path.realpath(root_dir)):
140 raise DispatchError('scan_dir:%s must be a directory under '
141 'root_dir:%s.' % (scan_dir, root_dir))
142 self._source_files_in_dir(root_dir, scan_dir)
144 def source_warnings(self):
145 """Return warnings in sourcing handlers."""
147 return self._source_warnings
149 def do_extra_handshake(self, request):
150 """Do extra checking in Web Socket handshake.
152 Select a handler based on request.uri and call its
153 web_socket_do_extra_handshake function.
156 request: mod_python request.
159 do_extra_handshake_, unused_transfer_data = self._handler(request)
161 do_extra_handshake_(request)
163 raise DispatchError('%s raised exception: %s' %
164 (_DO_EXTRA_HANDSHAKE_HANDLER_NAME, util.get_stack_trace()))
166 def transfer_data(self, request):
167 """Let a handler transfer_data with a Web Socket client.
169 Select a handler based on request.ws_resource and call its
170 web_socket_transfer_data function.
173 request: mod_python request.
176 unused_do_extra_handshake, transfer_data_ = self._handler(request)
178 transfer_data_(request)
180 raise DispatchError('%s raised exception: %s' %
181 (_TRANSFER_DATA_HANDLER_NAME, util.get_stack_trace()))
183 def _handler(self, request):
185 return self._handlers[request.ws_resource]
187 raise DispatchError('No handler for: %r' % request.ws_resource)
189 def _source_files_in_dir(self, root_dir, scan_dir):
190 """Source all the handler source files in the scan_dir directory.
192 The resource path is determined relative to root_dir.
195 to_resource = _path_to_resource_converter(root_dir)
196 for path in _source_file_paths(scan_dir):
198 handlers = _source(open(path).read())
199 except DispatchError, e:
200 self._source_warnings.append('%s: %s' % (path, e))
202 self._handlers[to_resource(path)] = handlers