summaryrefslogtreecommitdiff
path: root/waitress/runner.py
blob: 04cd78f12eee6c3b78049135585e81ef8c03bf37 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
##############################################################################
#
# Copyright (c) 2013 Zope Foundation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################
"""Command line runner.
"""

from __future__ import print_function, unicode_literals

import getopt
import os
import os.path
import re
import sys

from waitress import serve
from waitress.adjustments import Adjustments

HELP = """\
Usage:

    {0} [OPTS] MODULE:OBJECT

Standard options:

    --help
        Show this information.

    --call
        Call the given object to get the WSGI application.

    --host=ADDR
        Hostname or IP address on which to listen, default is '0.0.0.0',
        which means "all IP addresses on this host".

    --port=PORT
        TCP port on which to listen, default is '8080'

    --unix-socket=PATH
        Path of Unix socket. If a socket path is specified, a Unix domain
        socket is made instead of the usual inet domain socket.

        Not available on Windows.

    --unix-socket-perms=PERMS
        Octal permissions to use for the Unix domain socket, default is
        '600'.

    --url-scheme=STR
        Default wsgi.url_scheme value, default is 'http'.

   --url-prefix=STR
        The ``SCRIPT_NAME`` WSGI environment value.  Setting this to anything
        except the empty string will cause the WSGI ``SCRIPT_NAME`` value to be
        the value passed minus any trailing slashes you add, and it will cause
        the ``PATH_INFO`` of any request which is prefixed with this value to
        be stripped of the prefix.  Default is the empty string.

    --ident=STR
        Server identity used in the 'Server' header in responses. Default
        is 'waitress'.

Tuning options:

    --threads=INT
        Number of threads used to process application logic, default is 4.

    --backlog=INT
        Connection backlog for the server. Default is 1024.

    --recv-bytes=INT
        Number of bytes to request when calling socket.recv(). Default is
        8192.

    --send-bytes=INT
        Number of bytes to send to socket.send(). Default is 18000.
        Multiples of 9000 should avoid partly-filled TCP packets.

    --outbuf-overflow=INT
        A temporary file should be created if the pending output is larger
        than this. Default is 1048576 (1MB).

    --inbuf-overflow=INT
        A temporary file should be created if the pending input is larger
        than this. Default is 524288 (512KB).

    --connection-limit=INT
        Stop creating new channelse if too many are already active.
        Default is 100.

    --cleanup-interval=INT
        Minimum seconds between cleaning up inactive channels. Default
        is 30. See '--channel-timeout'.

    --channel-timeout=INT
        Maximum number of seconds to leave inactive connections open.
        Default is 120. 'Inactive' is defined as 'has recieved no data
        from the client and has sent no data to the client'.

    --[no-]log-socket-errors
        Toggle whether premature client disconnect tracepacks ought to be
        logged. On by default.

    --max-request-header-size=INT
        Maximum size of all request headers combined. Default is 262144
        (256KB).

    --max-request-body-size=INT
        Maximum size of request body. Default is 1073741824 (1GB).

    --[no-]expose-tracebacks
        Toggle whether to expose tracebacks of unhandled exceptions to the
        client. Off by default.

    --asyncore-loop-timeout=INT
        The timeout value in seconds passed to asyncore.loop(). Default is 1.

    --asyncore-use-poll
        The use_poll argument passed to ``asyncore.loop()``. Helps overcome
        open file descriptors limit. Default is False.

"""

RUNNER_PATTERN = re.compile(r"""
    ^
    (?P<module>
        [a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*
    )
    :
    (?P<object>
        [a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)*
    )
    $
    """, re.I | re.X)

def match(obj_name):
    matches = RUNNER_PATTERN.match(obj_name)
    if not matches:
        raise ValueError("Malformed application '{0}'".format(obj_name))
    return matches.group('module'), matches.group('object')

def resolve(module_name, object_name):
    """Resolve a named object in a module."""
    # We cast each segments due to an issue that has been found to manifest
    # in Python 2.6.6, but not 2.6.8, and may affect other revisions of Python
    # 2.6 and 2.7, whereby ``__import__`` chokes if the list passed in the
    # ``fromlist`` argument are unicode strings rather than 8-bit strings.
    # The error triggered is "TypeError: Item in ``fromlist '' not a string".
    # My guess is that this was fixed by checking against ``basestring``
    # rather than ``str`` sometime between the release of 2.6.6 and 2.6.8,
    # but I've yet to go over the commits. I know, however, that the NEWS
    # file makes no mention of such a change to the behaviour of
    # ``__import__``.
    segments = [str(segment) for segment in object_name.split('.')]
    obj = __import__(module_name, fromlist=segments[:1])
    for segment in segments:
        obj = getattr(obj, segment)
    return obj

def show_help(stream, name, error=None): # pragma: no cover
    if error is not None:
        print('Error: {0}\n'.format(error), file=stream)
    print(HELP.format(name), file=stream)

def show_exception(stream):
    exc_type, exc_value = sys.exc_info()[:2]
    args = getattr(exc_value, 'args', None)
    print(
        (
            'There was an exception ({0}) importing your module.\n'
        ).format(
            exc_type.__name__,
        ),
        file=stream
    )
    if args:
        print('It had these arguments: ', file=stream)
        for idx, arg in enumerate(args, start=1):
            print('{0}. {1}\n'.format(idx, arg), file=stream)
    else:
        print('It had no arguments.', file=stream)

def run(argv=sys.argv, _serve=serve):
    """Command line runner."""
    name = os.path.basename(argv[0])

    try:
        kw, args = Adjustments.parse_args(argv[1:])
    except getopt.GetoptError as exc:
        show_help(sys.stderr, name, str(exc))
        return 1

    if kw['help']:
        show_help(sys.stdout, name)
        return 0

    if len(args) != 1:
        show_help(sys.stderr, name, 'Specify one application only')
        return 1

    try:
        module, obj_name = match(args[0])
    except ValueError as exc:
        show_help(sys.stderr, name, str(exc))
        show_exception(sys.stderr)
        return 1

    # Add the current directory onto sys.path
    sys.path.append(os.getcwd())

    # Get the WSGI function.
    try:
        app = resolve(module, obj_name)
    except ImportError:
        show_help(sys.stderr, name, "Bad module '{0}'".format(module))
        show_exception(sys.stderr)
        return 1
    except AttributeError:
        show_help(sys.stderr, name, "Bad object name '{0}'".format(obj_name))
        show_exception(sys.stderr)
        return 1
    if kw['call']:
        app = app()

    # These arguments are specific to the runner, not waitress itself.
    del kw['call'], kw['help']

    _serve(app, **kw)
    return 0