diff options
author | Graham Dumpleton <Graham.Dumpleton@gmail.com> | 2018-03-04 15:35:19 +1100 |
---|---|---|
committer | Graham Dumpleton <Graham.Dumpleton@gmail.com> | 2018-03-04 15:35:19 +1100 |
commit | 630dcf528eb2ae68c1b491d6feb0d0aa54b0d3b5 (patch) | |
tree | 824061a502807eace1705c2d54d4671d2bdf323d | |
parent | 538cf8f92004e28d73d07c681718fbd5d67783da (diff) | |
parent | 68a67c7d11a3608e97f8b89f8fefc27f5643d080 (diff) | |
download | mod_wsgi-630dcf528eb2ae68c1b491d6feb0d0aa54b0d3b5.tar.gz |
Merge branch 'release/4.6.0'4.6.0
42 files changed, 1083 insertions, 234 deletions
@@ -12,7 +12,7 @@ The first way of installing mod_wsgi is the traditional way that has been used by many software packages. This is where it is installed as a module directly into your Apache installation using the commands ``configure``, ``make`` and ``make install``, a method sometimes referred to by the -acyronym CMMI. This method works with most UNIX type systems. It cannot +acronym CMMI. This method works with most UNIX type systems. It cannot be used on Windows. The second way of installing mod_wsgi is to install it as a Python package @@ -49,7 +49,7 @@ they never fixed those problems either. This time there is no easy workaround as they no longer supply certain tools which are required to perform the installation. -The ``pip install`` method along with manual configuration of Apache +The ``pip install`` method along with the manual configuration of Apache is also the method you need to use on Windows. System Requirements @@ -59,7 +59,7 @@ With either installation method for mod_wsgi, you must have Apache installed. This must be a complete Apache installation. It is not enough to have only the runtime packages for Apache installed. You must have the corresponding development package for Apache installed, which contains the -Apache header files, as these are required to be able compile and install +Apache header files, as these are required to be able to compile and install third party Apache modules. Similarly with Python, you must have a complete Python installation which @@ -156,7 +156,7 @@ Note that nothing will be copied into your Apache installation at this point. As a result, you do not need to run this as the root user unless installing it into a site wide Python installation rather than a Python virtual environment. It is recommended you always use Python virtual -environments and never install any Python package direct into the system +environments and never install any Python package directly into the system Python installation. On a UNIX type system, to verify that the installation was successful, run @@ -233,7 +233,7 @@ additional steps. The first thing you must do is supply the ``--user`` and ``--group`` options to say what user and group your Python web application should run -as. Most Linux distributions will pre define a special user for Apache to +as. Most Linux distributions will predefine a special user for Apache to run as, so you can use that. Alternatively you can use any other special user account you have created for running the Python web application:: @@ -344,7 +344,7 @@ use the ``--setup-only`` option to the ``runmodwsgi`` management command. --user www-data --group www-data \ --server-root=/etc/mod_wsgi-express-80 -This will setup all the required files and you can use ``apachectl`` to +This will set up all the required files and you can use ``apachectl`` to start and stop the Apache instance as explained previously. Connecting into Apache installation diff --git a/configure.ac b/configure.ac index 25afe44..1a58137 100644 --- a/configure.ac +++ b/configure.ac @@ -1,6 +1,6 @@ dnl vim: set sw=4 expandtab : dnl -dnl Copyright 2007-2017 GRAHAM DUMPLETON +dnl Copyright 2007-2018 GRAHAM DUMPLETON dnl dnl Licensed under the Apache License, Version 2.0 (the "License"); dnl you may not use this file except in compliance with the License. diff --git a/docs/conf.py b/docs/conf.py index dfe26b4..14285a0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -41,7 +41,7 @@ master_doc = 'index' # General information about the project. project = u'mod_wsgi' -copyright = u'2007-2017, Graham Dumpleton' +copyright = u'2007-2018, Graham Dumpleton' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/configuration-directives/WSGIApplicationGroup.rst b/docs/configuration-directives/WSGIApplicationGroup.rst index d08228a..101043c 100644 --- a/docs/configuration-directives/WSGIApplicationGroup.rst +++ b/docs/configuration-directives/WSGIApplicationGroup.rst @@ -17,6 +17,17 @@ to. All WSGI applications within the same application group will execute within the context of the same Python sub interpreter of the process handling the request. +Setting ``WSGIApplicationGroup`` doesn't control what processes a request +is handled by, that is what the ``WSGIProcessGroup`` directive does. In +other words, the ``WSGIProcessGroup`` directive operates distinct from the +``WSGIApplicationGroup`` directive, with ``WSGIProcessGroup`` dictating +what named group of processes a request is handled by, and +``WSGIApplicationGroup`` dictating which named Python sub interpreter +context (application group) of those processes is used. In each distinct +process of a named group of processes, there will be a separate sub +interpreter instance of same name, for handling the requests accepted by +that process. + The argument to the ``WSGIApplicationGroup`` can be either one of four special expanding variables or an explicit name of your own choosing. The meaning of the special variables are: @@ -27,11 +38,12 @@ The meaning of the special variables are: Any WSGI applications in the global application group will always be executed within the context of the first interpreter created by Python - when it is initialised. Forcing a WSGI application to run within the - first interpreter can be necessary when a third party C extension - module for Python has used the simplified threading API for - manipulation of the Python GIL and thus will not run correctly within - any additional sub interpreters created by Python. + when it is initialised, of the process handling the request. Forcing a + WSGI application to run within the first interpreter can be necessary + when a third party C extension module for Python has used the + simplified threading API for manipulation of the Python GIL and thus + will not run correctly within any additional sub interpreters created + by Python. **%{SERVER}** diff --git a/docs/configuration-directives/WSGIDaemonProcess.rst b/docs/configuration-directives/WSGIDaemonProcess.rst index aaa4ee9..b478fc9 100644 --- a/docs/configuration-directives/WSGIDaemonProcess.rst +++ b/docs/configuration-directives/WSGIDaemonProcess.rst @@ -59,6 +59,20 @@ Options which can be supplied to the ``WSGIDaemonProcess`` directive are: where your code is I/O bound. If you code is CPU bound, you are better of using at most 3 to 5 threads per process and using more processes. + If you set the number of threads to 0 you will enable a special mode + intended for using a daemon process to run a managed set of processes. + You will need to use ``WSGIImportScript`` to pre-load a Python script + into the main application group specified by ``%{GLOBAL}`` where the + script runs a never ending task, or does an exec to run an external + program. If the script or external program exits, the process is + shutdown and replaced with a new one. For the case of using a Python + script to run a never ending task, a ``SystemExit`` exception will be + injected when a signal is received to shutdown the process. You can + use ``signal.signal()`` to register a signal handler for ``SIGTERM`` + if needing to run special actions before then exiting the process using + ``sys.exit()``, or to signal your own threads to exit any processing + so you can shutdown in an orderly manner. + **display-name=value** Defines a different name to show for the daemon process when using the ``ps`` command to list processes. If the value is ``%{GROUP}`` then the @@ -598,6 +612,37 @@ host, the following could be used:: ... </VirtualHost> +For historical reasons and the inability to change existing behaviour when +adding or changing features, many of the options to ``WSGIDaemonProcess``, +especially those related to timeouts are not enabled by default. It is +strongly recommended you explicitly set these options yourself as this will +give you a system which is better able to recover from backlogging due to +overloading when you have too many long running requests or hanging +requests. As a starting point you can see what ``mod_wsgi-express`` uses as +defaults, adjusting them as necessary to suit your specific application +after you research what each option does. For example, consider starting +out with: + +* ``display-name='%{GROUP}'`` + +* ``lang='en_US.UTF-8'`` +* ``locale='en_US.UTF-8'`` + +* ``threads=5`` + +* ``queue-timeout=45`` +* ``socket-timeout=60`` +* ``connect-timeout=15`` +* ``request-timeout=60`` +* ``inactivity-timeout=0`` +* ``startup-timeout=15`` +* ``deadlock-timeout=60`` +* ``graceful-timeout=15`` +* ``eviction-timeout=0`` +* ``restart-interval=0`` +* ``shutdown-timeout=5`` +* ``maximum-requests=0`` + Note that the ``WSGIDaemonProcess`` directive and corresponding features are not available on Windows. diff --git a/docs/configuration-directives/WSGIScriptAlias.rst b/docs/configuration-directives/WSGIScriptAlias.rst index 708dabe..b8fc49a 100644 --- a/docs/configuration-directives/WSGIScriptAlias.rst +++ b/docs/configuration-directives/WSGIScriptAlias.rst @@ -86,12 +86,12 @@ Options which can be supplied to the ``WSGIScriptAlias`` directive are: If the name is set to be ``%{GLOBAL}`` the application group will be set to the empty string. Any WSGI applications in the global application group will always be executed within the context of the - first interpreter created by Python when it is initialised. Forcing - a WSGI application to run within the first interpreter can be - necessary when a third party C extension module for Python has used - the simplified threading API for manipulation of the Python GIL and - thus will not run correctly within any additional sub interpreters - created by Python. + first interpreter created by Python when it is initialised, of the + process handling the request. Forcing a WSGI application to run within + the first interpreter can be necessary when a third party C extension + module for Python has used the simplified threading API for + manipulation of the Python GIL and thus will not run correctly within + any additional sub interpreters created by Python. If both ``process-group`` and ``application-group`` options are set, the WSGI script file will be pre-loaded when the process it is to run in is diff --git a/docs/configuration-directives/WSGIScriptAliasMatch.rst b/docs/configuration-directives/WSGIScriptAliasMatch.rst index 576797b..0d9c3ce 100644 --- a/docs/configuration-directives/WSGIScriptAliasMatch.rst +++ b/docs/configuration-directives/WSGIScriptAliasMatch.rst @@ -59,13 +59,15 @@ Options which can be supplied to the ``WSGIScriptAlias`` directive are: If the name is set to be ``%{GLOBAL}`` the application group will be set to the empty string. Any WSGI applications in the global application group will always be executed within the context of the - first interpreter created by Python when it is initialised. Forcing - a WSGI application to run within the first interpreter can be - necessary when a third party C extension module for Python has used - the simplified threading API for manipulation of the Python GIL and - thus will not run correctly within any additional sub interpreters - created by Python. + first interpreter created by Python when it is initialised, of the + process handling the request. Forcing a WSGI application to run within + the first interpreter can be necessary when a third party C extension + module for Python has used the simplified threading API for + manipulation of the Python GIL and thus will not run correctly within + any additional sub interpreters created by Python. -If both ``process-group`` and ``application-group`` options are set, the -WSGI script file will be pre-loaded when the process it is to run in is -started, rather than being lazily loaded on the first request. +If both ``process-group`` and ``application-group`` options are set, and +the WSGI script file doesn't include substiutions values to be supplied +from the matched URL pattern, the WSGI script file will be pre-loaded when +the process it is to run in is started, rather than being lazily loaded on +the first request. diff --git a/docs/release-notes.rst b/docs/release-notes.rst index 600e295..3ae5240 100644 --- a/docs/release-notes.rst +++ b/docs/release-notes.rst @@ -5,6 +5,8 @@ Release Notes .. toctree:: :maxdepth: 2 + release-notes/version-4.6.0 + release-notes/version-4.5.24 release-notes/version-4.5.23 release-notes/version-4.5.22 diff --git a/docs/release-notes/version-4.6.0.rst b/docs/release-notes/version-4.6.0.rst new file mode 100644 index 0000000..dc4460d --- /dev/null +++ b/docs/release-notes/version-4.6.0.rst @@ -0,0 +1,207 @@ +============= +Version 4.6.0 +============= + +Version 4.6.0 of mod_wsgi can be obtained from: + + https://codeload.github.com/GrahamDumpleton/mod_wsgi/tar.gz/4.6.0 + +Bugs Fixed +---------- + +* Management of reference counting on Python objects in the access, + authentication, authorization and dispatch hooks wasn't correct for + certain error cases. The error cases shouldn't have ever occurred, but + still fixed. + +* Point at which details of Python exceptions occuring during access, + authentication, authorization and dispatch hooks was incorrect and not + done, with exception cleared, before trying to close per callback error + log. That the exception hadn't been cleared would result in the call to + close the per callback error log to itself fail as it believed an + exception occurred in that call when it hadn't. The result was confusing + error messages in the Apache error log. + +* The deprecated backwards compatability mode enabled by setting the + directive ``WSGILazyInitialization Off``, to have Python initialised + in the Apache parent process before forking, was resulting in the Apache + parent process crashing on Apache shutdown or restart. This resulted in + Apache child processes and daemon process being orphaned. Issue has been + fixed, but you should never use this mode and it will be removed in a + future update. The reason it shouldn't be used is due to memory leaks + in Python interpreter re-initialisation in same process and also the risks + due to Python code potentially being run as root. + +* When stack traces were being dumped upon request timeout expiring, the + line numbers of the definition of each function in the stack trace was + being displayed, instead of the actual line number within the body of the + function that was executing at the time. + +* When stack traces were being dumped upon request timeout expiring, the + thread ID was being truncated to 32 bits when displayed, meaning it + wouldn't match the actual Python thread ID on 64 bit systems. + +Features Changed +---------------- + +* Now flagging mod_wsgi package when installing using ``setup.py`` as + being not ``zip_safe``. This is to workaround an apparent bug with + ``setuptools`` when using Python 3.7 alpha versions. Believe this will + disable use of egg file in certain cases. + +* When the connection to a client is lost when writing back the response, + the HTTP response code logged in the Apache access log will be that for + the original response from the WSGI application rather than a 500 error. + + This is done to avoid confusion where a 500 error is recorded in the + access log, making you think your WSGI application is at fault when it + wasn't, but there is no actual error recorded in the error log as to why + the 500 error was recorded in the access log. + + The reason no error is logged in the case of the connection to a client + being lost is that doing so would create a lot of noise due to the + regularity which it can happen. The only time an error is logged is when + a timeout occurs rather than connection being lost. That is done to + highlight that connections are hanging due to the effect it can have on + available server capacity when connections are kept open for long times. + + Thanks to Jesús Cea Avión for identifying how using the Apache C API it + could be identified that the connection had been aborted and in that + case the original HTTP response code could safely be used. + +* When using the Django integration for ``mod_wsgi-express``, if the + ``whitenoise.middleware.WhiteNoiseMiddleware`` middleware is listed in + ``MIDDLEWARE`` or ``MIDDLEWARE_CLASSES`` of the Django settings file, + Apache will now not be used to host Django's static files. This is being + done to allow WhiteNoise middleware to be used in conjunction with front + end content delivery networks or other caching systems. If you aren't + using such a front end and do want Apache to still host the static files, + either don't list the WhiteNoise middleware in the list of middleware + classes when using ``mod_wsgi-express``, or pass the ``--url-alias`` + option explictly, along with the URL mount point for static files and the + directory where they have been placed by the ``collectstatic`` management + command of Django. + +* When running ``mod_wsgi-express`` if the ``TMPDIR`` environment variable + is specified, it will be used as the directory under which the default + server root directory for generated files will be created. If ``TMPDIR`` + is not specified, then ``/tmp`` will be used. + + This allows ``TMPDIR`` to be used to control the directory used as a + default. On MacOS where ``TMPDIR`` is set to a unique directory for the + login session under ``/var/tmp``, this also avoids a problem where a + system cron job in MacOS will delete files under ``/tmp`` which are older + than a certain date, which can cause a long running instance of + ``mod_wsgi-express`` to start failing. + +* The "process_stopping" event previously would not be delivered when the + process was being shutdown and there were still active requests, such as + when a request timeout occurred. Seen as better to always deliver the + event if can, even if there were still requests that hadn't been completed. + This will allow the event handler to dump out details on what the active + requests were, helping to identify long running or stuck requests. + +New Features +------------ + +* When using ``--compress-responses`` option of ``mod_wsgi-express``, + content of type ``application/json`` will now be compressed. + +* Added directive ``WSGISocketRotation`` to allow the rotation of the daemon + socket file path on restarts of Apache to be disabled. By default it is + ``On`` to preserve existing behaviour but can be set to ``Off`` to have + the same socket file path always be used for lifetime of that Apache + instance. + + Rotation should only be disabled where the Apache configuration for the + mod_wsgi application stays constant over time. The rotation was + originally done to prevent a request received and handled by an Apache + worker process being proxied through to a daemon process created under a + newer configuration. This was done to avoid the possibility of an error, + or a security issue, due to the old and new configurations being + incompatible or out of sync. + + By setting rotation to ``Off``, when a graceful restart is done and the + Apache worker process survives for a period of time due to keep alive + connections, those subsequent requests on the keep alive connection will + now be proxied to the newer daemon processes rather than being failed as + occurred before due to no instances of daemon process existing under the + older configuration. + + Although socket rotation still defaults to ``On`` for mod_wsgi, this is + overridden for ``mod_wsgi-express`` where it is always now set to ``Off``. + This is okay as is not possible for configuration to change when using it. + +* The ``process-group`` and ``application-group`` options can now be used + with the ``WSGIScriptAliasMatch`` directive. If substitutions are not used + in the value for the WSGI script file target path, then the WSGI script + file will be pre-loaded if both ``process-group`` and ``application-group`` + options are used at the same time. + + Note that the documentation was wrongly updated recently to suggest that + these options were already supported by ``WSGIScriptAliaMatch``. This was + done in error. Instead of removing the documentation, the ability to use + the options with the directive was instead added with this release. + +* Raise an actual exception when installing using ``pip`` or using the + ``setup.py`` file on MacOS and it doesn't appear that Xcode application + has been installed. Lack of Xcode application will mean that cannot find + the SDK which has the Apache include files. + +* An explicit error message is now logged when the calculated daemon socket + path is too long and would be truncated, causing potential failures. A + shorter directory path should be set with the ``WSGISocketPrefix`` option. + +* Added the ``--socket-path`` option to ``mod_wsgi-express`` so you can set + the daemon socket prefix via the ``WSGISocketPrefix`` directive to an + alternate directory if the calculated path would be too long based on + where server root is set for ``mod_wsgi-express``. + +* Added the ``--isatty`` option to ``mod_wsgi-express`` to indicate that + running the command in an interactive terminal session. In this case + Apache will be run as a sub process rather than it replacing the current + script. Signals such as SIGINT, SIGTERM, SIGHUP and SIGUSR1 will be + intercepted and forwarded onto Apache, but the signal SIGWINCH will be + ignored. This will avoid the problems of Apache shutting down when the + terminal session Apache is run in is resized. + + Technically this could be done automatically by working out if the + attached terminal is a tty, but is being done using an option at this + point so the reliability of the mechanism used to run Apache as a sub + process and the handling of the signals, can be verified. If everything + checks out, it is likely that this will become the default behaviour + when the attached terminal is a tty. + +* When using ``WSGIDaemonProcess``, if you set the number of threads to zero + you will enable a special mode intended for using a daemon process to run + a managed task or program. You will need to use ``WSGIImportScript`` to + pre-load a Python script into the main application group specified by + ``%{GLOBAL}`` where the script runs a never ending task, or does an exec + to run an external program. If the script or external program exits, the + process is shutdown and replaced with a new one. For the case of using a + Python script to run a never ending task, a ``SystemExit`` exception will + be injected when a signal is received to shutdown the process. You can + use ``signal.signal()`` to register a signal handler for ``SIGTERM`` if + needing to run special actions before then exiting the process using + ``sys.exit()``, or to signal your own threads to exit any processing so + you can shutdown in an orderly manner. + + The ability to do something very similar did previously exist in that + you could use ``WSGIImportScript`` to run a never ending task even when + the number of threads was non zero. This was used by ``--service-script`` + option of ``mod_wsgi-express``. The difference in setting ``threads=0`` + is that signals will work correctly and be able to interupt the script. + Also once the script exits, the process will shutdown, to be replaced, + where as previously the process would stay running until Apache was + restart or shutdown. The ``--service-script`` option of ``mod_wsgi-express`` + has been updated to set the number of threads to zero. + +* Added ``mod_wsgi.active_requests`` dictionary. This is populated with the + per request data object for active requests, keyed by the Apache request ID. + +* Add ``--cpu-time-limit`` option to ``mod_wsgi-express`` so that limit can + be imposed on daemon process group as to how much CPU can be used for + process is restarted automatically. + +* Pass a "shutdown_reason" argument with "process_stopping" event so event + handler knows the reason the process is being shutdown. diff --git a/docs/user-guides/access-control-mechanisms.rst b/docs/user-guides/access-control-mechanisms.rst index f512224..e8f7763 100644 --- a/docs/user-guides/access-control-mechanisms.rst +++ b/docs/user-guides/access-control-mechanisms.rst @@ -95,13 +95,16 @@ is 'get_realm_hash()'. The result of the function must be 'None' if the user doesn't exist, or a hash string encoding the user name, authentication realm and password:: - import md5 + import hashlib def get_realm_hash(environ, user, realm): if user == 'spy': - value = md5.new() + value = hashlib.md5() # user:realm:password - value.update('%s:%s:%s' % (user, realm, 'secret')) + input = '%s:%s:%s' % (user, realm, 'secret') + if not isinstance(input, bytes): + input = input.encode('UTF-8') + value.update(input) hash = value.hexdigest() return hash return None @@ -217,7 +220,7 @@ configuration would be used:: AuthBasicProvider dbm AuthDBMUserFile /usr/local/wsgi/accounts.dbm WSGIAuthGroupScript /usr/local/wsgi/scripts/auth.wsgi - Require group secret-agents + Require wsgi-group secret-agents Require valid-user The 'auth.wsgi' script would then need to contain a 'groups_for_user()' @@ -330,6 +330,20 @@ else: APR_INCLUDES = get_apr_includes().split() APU_INCLUDES = get_apu_includes().split() +if not os.path.exists(APR_CONFIG) and not INCLUDEDIR: + if sys.platform == 'darwin': + # Likely no Xcode application installed or location of SDK in + # Xcode has changed with a new release of Xcode application. + + raise RuntimeError('No Apache installation can be found, do you ' + 'have the full Apple Xcode installed. It is not enough to ' + 'have just the xcode command line tools installed.') + else: + # Set INCLUDEDIR just to avoid having an empty path. Probably + # should raise an exception here. + + INCLUDEDIR = '/usr/include' + # Write out apxs_config.py which caches various configuration related to # Apache. For the case of using our own Apache build, this needs to # calculate values dynamically based on where binaries were installed. @@ -556,6 +570,7 @@ setup(name = 'mod_wsgi', 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP :: WSGI', 'Topic :: Internet :: WWW/HTTP :: WSGI :: Server' ], @@ -570,4 +585,5 @@ setup(name = 'mod_wsgi', ext_modules = [extension], entry_points = { 'console_scripts': ['mod_wsgi-express = mod_wsgi.server:main'],}, + zip_safe = False, ) diff --git a/src/server/__init__.py b/src/server/__init__.py index 1912091..3c56b0a 100644 --- a/src/server/__init__.py +++ b/src/server/__init__.py @@ -321,9 +321,17 @@ WSGIPythonHome '%(python_home)s' WSGIVerboseDebugging '%(verbose_debugging_flag)s' +<IfDefine MOD_WSGI_WITH_SOCKET_PREFIX> +WSGISocketPrefix %(socket_prefix)s/wsgi +</IfDefine> +<IfDefine !MOD_WSGI_WITH_SOCKET_PREFIX> +WSGISocketPrefix %(server_root)s/wsgi +</IfDefine> + +WSGISocketRotation Off + <IfDefine !ONE_PROCESS> WSGIRestrictEmbedded On -WSGISocketPrefix %(server_root)s/wsgi <IfDefine MOD_WSGI_MULTIPROCESS> WSGIDaemonProcess %(host)s:%(port)s \\ display-name='%(daemon_name)s' \\ @@ -346,6 +354,7 @@ WSGIDaemonProcess %(host)s:%(port)s \\ graceful-timeout=%(graceful_timeout)s \\ eviction-timeout=%(eviction_timeout)s \\ restart-interval=%(restart_interval)s \\ + cpu-time-limit=%(cpu_time_limit)s \\ shutdown-timeout=%(shutdown_timeout)s \\ send-buffer-size=%(send_buffer_size)s \\ receive-buffer-size=%(receive_buffer_size)s \\ @@ -375,6 +384,7 @@ WSGIDaemonProcess %(host)s:%(port)s \\ graceful-timeout=%(graceful_timeout)s \\ eviction-timeout=%(eviction_timeout)s \\ restart-interval=%(restart_interval)s \\ + cpu-time-limit=%(cpu_time_limit)s \\ shutdown-timeout=%(shutdown_timeout)s \\ send-buffer-size=%(send_buffer_size)s \\ receive-buffer-size=%(receive_buffer_size)s \\ @@ -432,6 +442,7 @@ AddOutputFilterByType DEFLATE text/css AddOutputFilterByType DEFLATE text/javascript AddOutputFilterByType DEFLATE application/xhtml+xml AddOutputFilterByType DEFLATE application/javascript +AddOutputFilterByType DEFLATE application/json </IfDefine> <IfDefine MOD_WSGI_ROTATE_LOGS> @@ -946,7 +957,7 @@ WSGIDaemonProcess 'service:%(name)s' \\ user='%(user)s' \\ group='%(group)s' \\ home='%(working_directory)s' \\ - threads=1 \\ + threads=0 \\ python-path='%(python_path)s' \\ python-eggs='%(python_eggs)s' \\ lang='%(lang)s' \\ @@ -971,7 +982,7 @@ WSGIDaemonProcess 'service:%(name)s' \\ user='%(user)s' \\ group='%(group)s' \\ home='%(working_directory)s' \\ - threads=1 \\ + threads=0 \\ python-path='%(python_path)s' \\ python-eggs='%(python_eggs)s' \\ lang='%(lang)s' \\ @@ -1913,7 +1924,7 @@ option_list = ( optparse.make_option('--http2', action='store_true', default=False, help='Flag indicating whether HTTP/2 should be enabled.' - 'Requires the mod_http2 module to be available.'), + 'Requires the mod_http2 module to be available.'), optparse.make_option('--https-port', type='int', metavar='NUMBER', help='The specific port to bind to and on which secure ' @@ -1964,7 +1975,7 @@ option_list = ( optparse.make_option('--https-only', action='store_true', default=False, help='Flag indicating whether any requests ' - 'made using a HTTP request over the non secure connection ' + 'made using a HTTP request over the non secure connection ' 'should be redirected automatically to use a HTTPS request ' 'over the secure connection.'), @@ -2054,6 +2065,13 @@ option_list = ( 'the process is forced to exit and restart. Not enabled by ' 'default.'), + optparse.make_option('--cpu-time-limit', type='int', default='0', + metavar='SECONDS', help='Number of seconds of CPU time the ' + 'process can use before it will be restarted. If graceful ' + 'timeout is also specified, active requests will be given ' + 'a chance to complete before the process is forced to exit ' + 'and restart. Not enabled by default.'), + optparse.make_option('--graceful-timeout', type='int', default=15, metavar='SECONDS', help='Grace period for requests to complete ' 'normally, while still accepting new requests, when worker ' @@ -2372,7 +2390,8 @@ option_list = ( optparse.make_option('--server-root', metavar='DIRECTORY-PATH', help='Specify an alternate directory for where the generated ' 'web server configuration, startup files and logs will be ' - 'stored. Defaults to a sub directory of /tmp.'), + 'stored. Defaults to the sub directory specified by the ' + 'TMPDIR environment variable, or /tmp if not specified.'), optparse.make_option('--server-mpm', action='append', dest='server_mpm_variables', metavar='NAME', help='Specify ' @@ -2457,6 +2476,11 @@ option_list = ( metavar='FILE-PATH', help='Override the path to the mime types ' 'file used by the web server.'), + optparse.make_option('--socket-prefix', metavar='DIRECTORY-PATH', + help='Specify an alternate directory name prefix to be used ' + 'for the UNIX domain sockets used by mod_wsgi to communicate ' + 'between the Apache child processes and the daemon processes.'), + optparse.make_option('--add-handler', action='append', nargs=2, dest='handler_scripts', metavar='EXTENSION SCRIPT-PATH', help='Specify a WSGI application to be used as a special ' @@ -2579,6 +2603,17 @@ option_list = ( 'the generation of the configuration with Apache then later ' 'being started separately using the generated \'apachectl\' ' 'script.'), + + optparse.make_option('--isatty', action='store_true', default=False, + help='Flag indicating whether should assume being run in an ' + 'interactive terminal session. In this case Apache will not ' + 'replace this wrapper script, but will be run as a sub process.' + 'Signals such as SIGINT, SIGTERM, SIGHUP and SIGUSR1 will be ' + 'forwarded onto Apache, but SIGWINCH will be blocked so that ' + 'resizing of a terminal session window will not cause Apache ' + 'to shutdown. This is a separate option at this time rather ' + 'than being determined automatically while the reliability of ' + 'intercepting and forwarding signals is verified.'), ) def cmd_setup_server(params): @@ -2625,8 +2660,11 @@ def _cmd_setup_server(command, args, options): options['port'], os.getuid()) if not options['server_root']: - options['server_root'] = '/tmp/mod_wsgi-%s:%s:%s' % (options['host'], - options['port'], os.getuid()) + tmpdir = os.environ.get('TMPDIR') + tmpdir = tmpdir or '/tmp' + tmpdir = tmpdir.rstrip('/') + options['server_root'] = '%s/mod_wsgi-%s:%s:%s' % (tmpdir, + options['host'], options['port'], os.getuid()) try: os.mkdir(options['server_root']) @@ -3207,6 +3245,8 @@ def _cmd_setup_server(command, args, options): options['httpd_arguments_list'].append('-DMOD_WSGI_WITH_TRUSTED_PROXIES') if options['python_path']: options['httpd_arguments_list'].append('-DMOD_WSGI_WITH_PYTHON_PATH') + if options['socket_prefix']: + options['httpd_arguments_list'].append('-DMOD_WSGI_WITH_SOCKET_PREFIX') if options['with_cgi']: if os.path.exists(os.path.join(options['modules_directory'], @@ -3335,7 +3375,31 @@ def cmd_start_server(params): return executable = os.path.join(config['server_root'], 'apachectl') - os.execl(executable, executable, 'start', '-DFOREGROUND') + + if config['isatty'] and sys.stdout.isatty(): + process = None + + def handler(signum, frame): + if process is None: + sys.exit(1) + + else: + if signum not in [signal.SIGWINCH]: + os.kill(process.pid, signum) + + signal.signal(signal.SIGINT, handler) + signal.signal(signal.SIGTERM, handler) + signal.signal(signal.SIGHUP, handler) + signal.signal(signal.SIGUSR1, handler) + signal.signal(signal.SIGWINCH, handler) + + process = subprocess.Popen([executable, 'start', '-DFOREGROUND'], + preexec_fn=os.setpgrp) + + process.wait() + + else: + os.execl(executable, executable, 'start', '-DFOREGROUND') def cmd_module_config(params): formatter = optparse.IndentedHelpFormatter() diff --git a/src/server/management/commands/runmodwsgi.py b/src/server/management/commands/runmodwsgi.py index 9d0ccd7..3e18703 100644 --- a/src/server/management/commands/runmodwsgi.py +++ b/src/server/management/commands/runmodwsgi.py @@ -1,6 +1,8 @@ import os import sys import inspect +import signal +import subprocess from django.core.management.base import BaseCommand @@ -97,22 +99,28 @@ class Command(BaseCommand): url_aliases = options.setdefault('url_aliases') or [] try: - if settings.STATIC_URL and settings.STATIC_URL.startswith('/'): - if settings.STATIC_ROOT: - # We need a fiddle here as depending on the Python - # version used, the list of URL aliases we are - # passed could be either list of tuples or list of - # lists. We need to ensure we use the same type so - # that sorting of items in the lists works later. - - if not url_aliases: - url_aliases.insert(0, ( - settings.STATIC_URL.rstrip('/') or '/', - settings.STATIC_ROOT)) - else: - url_aliases.insert(0, type(url_aliases[0])(( - settings.STATIC_URL.rstrip('/') or '/', - settings.STATIC_ROOT))) + middleware = getattr(settings, 'MIDDLEWARE', None) + + if middleware is None: + middleware = getattr(settings, 'MIDDLEWARE_CLASSES', []) + + if 'whitenoise.middleware.WhiteNoiseMiddleware' not in middleware: + if settings.STATIC_URL and settings.STATIC_URL.startswith('/'): + if settings.STATIC_ROOT: + # We need a fiddle here as depending on the Python + # version used, the list of URL aliases we are + # passed could be either list of tuples or list of + # lists. We need to ensure we use the same type so + # that sorting of items in the lists works later. + + if not url_aliases: + url_aliases.insert(0, ( + settings.STATIC_URL.rstrip('/') or '/', + settings.STATIC_ROOT)) + else: + url_aliases.insert(0, type(url_aliases[0])(( + settings.STATIC_URL.rstrip('/') or '/', + settings.STATIC_ROOT))) except AttributeError: pass @@ -127,4 +135,28 @@ class Command(BaseCommand): executable = os.path.join(options['server_root'], 'apachectl') name = executable.ljust(len(options['process_name'])) - os.execl(executable, name, 'start', '-DFOREGROUND') + + if options['isatty'] and sys.stdout.isatty(): + process = None + + def handler(signum, frame): + if process is None: + sys.exit(1) + + else: + if signum not in [signal.SIGWINCH]: + os.kill(process.pid, signum) + + signal.signal(signal.SIGINT, handler) + signal.signal(signal.SIGTERM, handler) + signal.signal(signal.SIGHUP, handler) + signal.signal(signal.SIGUSR1, handler) + signal.signal(signal.SIGWINCH, handler) + + process = subprocess.Popen([executable, 'start', '-DFOREGROUND'], + preexec_fn=os.setpgrp) + + process.wait() + + else: + os.execl(executable, name, 'start', '-DFOREGROUND') diff --git a/src/server/mod_wsgi.c b/src/server/mod_wsgi.c index 99f01b8..6913373 100644 --- a/src/server/mod_wsgi.c +++ b/src/server/mod_wsgi.c @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -80,6 +80,8 @@ static apr_pool_t *wsgi_parent_pool = NULL; int volatile wsgi_daemon_shutdown = 0; static int volatile wsgi_daemon_graceful = 0; +static int wsgi_dump_stack_traces = 0; +static char *wsgi_shutdown_reason = ""; #if defined(MOD_WSGI_WITH_DAEMONS) static apr_interval_time_t wsgi_startup_timeout = 0; @@ -2106,12 +2108,20 @@ static PyObject *Adapter_start_response(AdapterObject *self, PyObject *args) /* Publish event for the start of the response. */ if (wsgi_event_subscribers()) { + WSGIThreadInfo *thread_info; + + thread_info = wsgi_thread_info(0, 0); + event = PyDict_New(); PyDict_SetItemString(event, "response_status", status_line); PyDict_SetItemString(event, "response_headers", headers); PyDict_SetItemString(event, "exception_info", exc_info); +#if 0 + PyDict_SetItemString(event, "request_data", thread_info->request_data); +#endif + wsgi_publish_event("response_started", event); Py_DECREF(event); @@ -3002,6 +3012,8 @@ static int Adapter_run(AdapterObject *self, PyObject *object) WSGIThreadCPUUsage start_usage; WSGIThreadCPUUsage end_usage; + int aborted = 0; + #if defined(MOD_WSGI_WITH_DAEMONS) if (wsgi_idle_timeout && !self->config->ignore_activity) { apr_thread_mutex_lock(wsgi_monitor_lock); @@ -3116,6 +3128,10 @@ static int Adapter_run(AdapterObject *self, PyObject *object) PyDict_SetItemString(event, "application_start", value); Py_DECREF(value); +#if 0 + PyDict_SetItemString(event, "request_data", thread_handle->request_data); +#endif + wsgi_publish_event("request_started", event); evwrapper = PyDict_GetItemString(event, "application_object"); @@ -3144,8 +3160,6 @@ static int Adapter_run(AdapterObject *self, PyObject *object) if (self->sequence != NULL) { if (!Adapter_process_file_wrapper(self)) { - int aborted = 0; - iterator = PyObject_GetIter(self->sequence); if (iterator != NULL) { @@ -3181,9 +3195,36 @@ static int Adapter_run(AdapterObject *self, PyObject *object) } } - if (!PyErr_Occurred() && !aborted) { - if (Adapter_output(self, "", 0, NULL, 0)) + if (!PyErr_Occurred()) { + if (!aborted) { + /* + * In the case where the response was empty we + * need to ensure we explicitly flush out the + * headers. This is done by calling the output + * routine but with an empty string as content. + * This could be gated on whether any content + * had already been sent, but easier to just call + * it all the time. + */ + + if (Adapter_output(self, "", 0, NULL, 0)) + self->result = OK; + } + else { + /* + * If the client connection was already marked + * as aborted, then it indicates the client has + * closed the connection. In this case mark the + * final result as okay rather than an error so + * that the access log still records the original + * HTTP response code for the request rather than + * overriding it. If don't do this then access + * log will show 500 when the WSGI application + * itself had run fine. + */ + self->result = OK; + } } Py_XDECREF(iterator); @@ -3191,11 +3232,11 @@ static int Adapter_run(AdapterObject *self, PyObject *object) /* * Log warning if more response content generated than was - * indicated, or less if there was no errors generated by - * the application. + * indicated, or less, if there was no errors generated by + * the application and connection wasn't aborted. */ - if (self->content_length_set && ((!PyErr_Occurred() && + if (self->content_length_set && ((!PyErr_Occurred() && !aborted && self->output_length != self->content_length) || (self->output_length > self->content_length))) { ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, self->r, @@ -3337,6 +3378,10 @@ static int Adapter_run(AdapterObject *self, PyObject *object) PyDict_SetItemString(event, "application_time", value); Py_DECREF(value); +#if 0 + PyDict_SetItemString(event, "request_data", thread_handle->request_data); +#endif + wsgi_publish_event("request_finished", event); Py_DECREF(event); @@ -3548,7 +3593,8 @@ static PyObject *wsgi_load_source(apr_pool_t *pool, request_rec *r, const char *name, int exists, const char* filename, const char *process_group, - const char *application_group) + const char *application_group, + int ignore_system_exit) { FILE *fp = NULL; PyObject *m = NULL; @@ -3580,13 +3626,13 @@ static PyObject *wsgi_load_source(apr_pool_t *pool, request_rec *r, if (r) { ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "mod_wsgi (pid=%d, process='%s', application='%s'): " - "Loading WSGI script '%s'.", getpid(), + "Loading Python script file '%s'.", getpid(), process_group, application_group, filename); } else { ap_log_error(APLOG_MARK, APLOG_INFO, 0, wsgi_server, "mod_wsgi (pid=%d, process='%s', application='%s'): " - "Loading WSGI script '%s'.", getpid(), + "Loading Python script file '%s'.", getpid(), process_group, application_group, filename); } Py_END_ALLOW_THREADS @@ -3647,13 +3693,13 @@ static PyObject *wsgi_load_source(apr_pool_t *pool, request_rec *r, if (r) { ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, "mod_wsgi (pid=%d, process='%s', application='%s'): " - "Failed to parse WSGI script file '%s'.", getpid(), + "Failed to parse Python script file '%s'.", getpid(), process_group, application_group, filename); } else { ap_log_error(APLOG_MARK, APLOG_ERR, 0, wsgi_server, "mod_wsgi (pid=%d, process='%s', application='%s'): " - "Failed to parse WSGI script file '%s'.", getpid(), + "Failed to parse Python script file '%s'.", getpid(), process_group, application_group, filename); } Py_END_ALLOW_THREADS @@ -3687,20 +3733,40 @@ static PyObject *wsgi_load_source(apr_pool_t *pool, request_rec *r, PyModule_AddObject(m, "__mtime__", object); } else { - Py_BEGIN_ALLOW_THREADS - if (r) { - ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, - "mod_wsgi (pid=%d): Target WSGI script '%s' cannot " - "be loaded as Python module.", getpid(), filename); + if (PyErr_ExceptionMatches(PyExc_SystemExit)) { + if (!ignore_system_exit) { + Py_BEGIN_ALLOW_THREADS + if (r) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "mod_wsgi (pid=%d): SystemExit exception " + "raised when doing exec of Python script " + "file '%s'.", getpid(), filename); + } + else { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, wsgi_server, + "mod_wsgi (pid=%d): SystemExit exception " + "raised when doing exec of Python script " + "file '%s'.", getpid(), filename); + } + Py_END_ALLOW_THREADS + } } else { - ap_log_error(APLOG_MARK, APLOG_ERR, 0, wsgi_server, - "mod_wsgi (pid=%d): Target WSGI script '%s' cannot " - "be loaded as Python module.", getpid(), filename); - } - Py_END_ALLOW_THREADS + Py_BEGIN_ALLOW_THREADS + if (r) { + ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r, + "mod_wsgi (pid=%d): Failed to exec Python script " + "file '%s'.", getpid(), filename); + } + else { + ap_log_error(APLOG_MARK, APLOG_ERR, 0, wsgi_server, + "mod_wsgi (pid=%d): Failed to exec Python script " + "file '%s'.", getpid(), filename); + } + Py_END_ALLOW_THREADS - wsgi_log_python_error(r, NULL, filename, 0); + wsgi_log_python_error(r, NULL, filename, 0); + } } return m; @@ -4040,14 +4106,14 @@ static int wsgi_execute_script(request_rec *r) /* Setup metrics for start of request. */ - thread_info = wsgi_start_request(); + thread_info = wsgi_start_request(r); /* Load module if not already loaded. */ if (!module) { module = wsgi_load_source(r->pool, r, name, exists, script, config->process_group, - config->application_group); + config->application_group, 0); } /* Safe now to release the module lock. */ @@ -4173,6 +4239,15 @@ static apr_status_t wsgi_python_child_cleanup(void *data) { PyObject *interp = NULL; + /* + * If not a daemon process need to publish that process + * is shutting down here. For daemon we did it earlier + * before trying to wait on request threads. + */ + + if (!wsgi_daemon_process) + wsgi_publish_process_stopping(wsgi_shutdown_reason); + /* In a multithreaded MPM must protect table. */ #if APR_HAS_THREADS @@ -4249,6 +4324,8 @@ static void wsgi_python_child_init(apr_pool_t *p) PyGILState_STATE state; PyObject *object = NULL; + int ignore_system_exit = 0; + /* Working with Python, so must acquire GIL. */ state = PyGILState_Ensure(); @@ -4292,6 +4369,13 @@ static void wsgi_python_child_init(apr_pool_t *p) #endif /* + * Create an interpreters index using Apache data structure so + * can iterate over interpreter names without needing Python GIL. + */ + + wsgi_interpreters_index = apr_hash_make(p); + + /* * Initialise the key for data related to a thread and force * creation of thread info. */ @@ -4314,6 +4398,8 @@ static void wsgi_python_child_init(apr_pool_t *p) PyDict_SetItemString(wsgi_interpreters, "", object); Py_DECREF(object); + apr_hash_set(wsgi_interpreters_index, "", APR_HASH_KEY_STRING, ""); + /* Restore the prior thread state and release the GIL. */ PyGILState_Release(state); @@ -4325,6 +4411,9 @@ static void wsgi_python_child_init(apr_pool_t *p) /* Loop through import scripts for this process and load them. */ + if (wsgi_daemon_process && wsgi_daemon_process->group->threads == 0) + ignore_system_exit = 1; + if (wsgi_import_list) { apr_array_header_t *scripts = NULL; @@ -4339,6 +4428,14 @@ static void wsgi_python_child_init(apr_pool_t *p) for (i = 0; i < scripts->nelts; ++i) { entry = &entries[i]; + /* + * Stop loading scripts if this is a daemon process and + * we have already been flagged to be shutdown. + */ + + if (wsgi_daemon_shutdown) + break; + if (!strcmp(wsgi_daemon_group, entry->process_group)) { InterpreterObject *interp = NULL; PyObject *modules = NULL; @@ -4409,7 +4506,8 @@ static void wsgi_python_child_init(apr_pool_t *p) module = wsgi_load_source(p, NULL, name, exists, entry->handler_script, entry->process_group, - entry->application_group); + entry->application_group, + ignore_system_exit); if (PyErr_Occurred()) PyErr_Clear(); @@ -4522,7 +4620,7 @@ static const char *wsgi_add_script_alias(cmd_parms *cmd, void *mconfig, return "Invalid option to WSGI script alias definition."; } - if (!cmd->info && !strcmp(option, "application-group")) { + if (!strcmp(option, "application-group")) { if (!*value) return "Invalid name for WSGI application group."; @@ -4532,7 +4630,7 @@ static const char *wsgi_add_script_alias(cmd_parms *cmd, void *mconfig, application_group = value; } #if defined(MOD_WSGI_WITH_DAEMONS) - else if (!cmd->info && !strcmp(option, "process-group")) { + else if (!strcmp(option, "process-group")) { if (!*value) return "Invalid name for WSGI process group."; @@ -4579,9 +4677,17 @@ static const char *wsgi_add_script_alias(cmd_parms *cmd, void *mconfig, entry->callable_object = callable_object; entry->pass_authorization = pass_authorization; + /* + * Only add to import list if both process group and application + * group are specified, that they don't include substitution values, + * and in the case of WSGIScriptAliasMatch, that the WSGI script + * target path doesn't include substitutions from URL pattern. + */ + if (process_group && application_group && !strstr(process_group, "%{") && - !strstr(application_group, "%{")) { + !strstr(application_group, "%{") && + (!cmd->info || !strstr(a, "$"))) { WSGIScriptFile *object = NULL; @@ -6623,7 +6729,8 @@ static int wsgi_execute_dispatch(request_rec *r) } if (!module) { - module = wsgi_load_source(r->pool, r, name, exists, script, "", group); + module = wsgi_load_source(r->pool, r, name, exists, script, + "", group, 0); } /* Safe now to release the module lock. */ @@ -6632,6 +6739,11 @@ static int wsgi_execute_dispatch(request_rec *r) apr_thread_mutex_unlock(wsgi_module_lock); #endif + /* Log any details of exceptions if import failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + /* Assume everything will be okay for now. */ status = OK; @@ -6725,9 +6837,12 @@ static int wsgi_execute_dispatch(request_rec *r) } else status = HTTP_INTERNAL_SERVER_ERROR; + + /* Log any details of exceptions if execution failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); } - else - Py_DECREF(object); object = NULL; } @@ -6805,9 +6920,12 @@ static int wsgi_execute_dispatch(request_rec *r) } else status = HTTP_INTERNAL_SERVER_ERROR; + + /* Log any details of exceptions if execution failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); } - else - Py_DECREF(object); object = NULL; } @@ -6884,9 +7002,12 @@ static int wsgi_execute_dispatch(request_rec *r) } else status = HTTP_INTERNAL_SERVER_ERROR; + + /* Log any details of exceptions if execution failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); } - else - Py_DECREF(object); object = NULL; } @@ -7372,7 +7493,7 @@ static const char *wsgi_add_daemon_process(cmd_parms *cmd, void *mconfig, return "Invalid thread count for WSGI daemon process."; threads = atoi(value); - if (threads < 1 || threads >= WSGI_STACK_LAST-1) + if (threads < 0 || threads >= WSGI_STACK_LAST-1) return "Invalid thread count for WSGI daemon process."; } else if (!strcmp(option, "umask")) { @@ -7838,6 +7959,28 @@ static const char *wsgi_set_socket_prefix(cmd_parms *cmd, void *mconfig, return NULL; } +static const char *wsgi_set_socket_rotation(cmd_parms *cmd, void *mconfig, + const char *f) +{ + const char *error = NULL; + WSGIServerConfig *sconfig = NULL; + + error = ap_check_cmd_context(cmd, GLOBAL_ONLY); + if (error != NULL) + return error; + + sconfig = ap_get_module_config(cmd->server->module_config, &wsgi_module); + + if (strcasecmp(f, "Off") == 0) + sconfig->socket_rotation = 0; + else if (strcasecmp(f, "On") == 0) + sconfig->socket_rotation = 1; + else + return "WSGISocketRotation must be one of: Off | On"; + + return NULL; +} + static const char wsgi_valid_accept_mutex_string[] = "Valid accept mutex mechanisms for this platform are: default" #if APR_HAS_FLOCK_SERIALIZE @@ -8327,6 +8470,14 @@ static int wsgi_setup_socket(WSGIProcessGroup *process) } #endif + if (strlen(process->socket_path) > sizeof(addr.sun_path)) { + ap_log_error(APLOG_MARK, APLOG_ALERT, 0, wsgi_server, + "mod_wsgi (pid=%d): Length of path for daemon process " + "socket exceeds maxmimum allowed value and will be " + "truncated, resulting in likely failure to bind the " + "socket, or other later related failure.", getpid()); + } + memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; apr_cpystrn(addr.sun_path, process->socket_path, sizeof(addr.sun_path)); @@ -9061,6 +9212,8 @@ static void *wsgi_monitor_thread(apr_thread_t *thd, void *data) "time limit exceeded, stopping process " "'%s'.", getpid(), group->name); + wsgi_shutdown_reason = "request_timeout"; + wsgi_dump_stack_traces = 1; restart = 1; @@ -9075,6 +9228,8 @@ static void *wsgi_monitor_thread(apr_thread_t *thd, void *data) "timer expired, stopping process '%s'.", getpid(), group->name); + wsgi_shutdown_reason = "startup_timeout"; + restart = 1; } else { @@ -9109,6 +9264,8 @@ static void *wsgi_monitor_thread(apr_thread_t *thd, void *data) "stopping process '%s'.", getpid(), daemon->group->name); + wsgi_shutdown_reason = "restart_interval"; + restart = 1; } } @@ -9150,6 +9307,8 @@ static void *wsgi_monitor_thread(apr_thread_t *thd, void *data) "stopping process '%s'.", getpid(), group->name); + wsgi_shutdown_reason = "inactivity_timeout"; + restart = 1; } else { @@ -9262,7 +9421,7 @@ static void wsgi_log_stack_traces(void) "active Python threads.", getpid()); while (PyDict_Next(threads, &i, &id, &frame)) { - long thread_id = 0; + apr_int64_t thread_id = 0; PyFrameObject *current = NULL; @@ -9276,7 +9435,7 @@ static void wsgi_log_stack_traces(void) char *filename = NULL; char *name = NULL; - lineno = current->f_lineno; + lineno = PyFrame_GetLineNumber(current); #if PY_MAJOR_VERSION >= 3 filename = PyUnicode_AsUTF8(current->f_code->co_filename); @@ -9288,9 +9447,9 @@ static void wsgi_log_stack_traces(void) if (current == (PyFrameObject *)frame) { ap_log_error(APLOG_MARK, APLOG_INFO, 0, wsgi_server, - "mod_wsgi (pid=%d): Thread %ld executing " - "file \"%s\", line %d, in %s", getpid(), - thread_id, filename, lineno, name); + "mod_wsgi (pid=%d): Thread %" APR_INT64_T_FMT + " executing file \"%s\", line %d, in %s", + getpid(), thread_id, filename, lineno, name); } else { if (current->f_back) { @@ -9518,6 +9677,8 @@ static void wsgi_daemon_main(apr_pool_t *p, WSGIDaemonProcess *daemon) if (buf[0] == 'C') { if (!wsgi_daemon_graceful) { + wsgi_shutdown_reason = "cpu_time_limit"; + if (wsgi_active_requests) { wsgi_daemon_graceful++; @@ -9544,6 +9705,8 @@ static void wsgi_daemon_main(apr_pool_t *p, WSGIDaemonProcess *daemon) } else if (buf[0] == 'G') { if (!wsgi_daemon_graceful) { + wsgi_shutdown_reason = "graceful_signal"; + if (wsgi_active_requests) { wsgi_daemon_graceful++; @@ -9603,6 +9766,8 @@ static void wsgi_daemon_main(apr_pool_t *p, WSGIDaemonProcess *daemon) * which are running as a debugging aid. */ + wsgi_publish_process_stopping(wsgi_shutdown_reason); + #if (PY_MAJOR_VERSION >= 3) || (PY_MAJOR_VERSION == 2 && PY_MINOR_VERSION >= 5) if (wsgi_dump_stack_traces) wsgi_log_stack_traces(); @@ -10217,9 +10382,14 @@ static int wsgi_start_process(apr_pool_t *p, WSGIDaemonProcess *daemon) apr_os_sock_put(&daemon->listener, &daemon->group->listener_fd, p); - /* Run the main routine for the daemon process. */ + /* + * Run the main routine for the daemon process if there + * is a non zero number of threads. When number of threads + * is zero we actually go on and shutdown immediately. + */ - wsgi_daemon_main(p, daemon); + if (daemon->group->threads != 0) + wsgi_daemon_main(p, daemon); /* * Destroy the pool for the daemon process. This will @@ -10325,9 +10495,18 @@ static int wsgi_start_daemons(apr_pool_t *p) * create the socket. */ - entry->socket_path = apr_psprintf(p, "%s.%d.%d.%d.sock", - wsgi_server_config->socket_prefix, - getpid(), mpm_generation, entry->id); + entry->socket_rotation = wsgi_server_config->socket_rotation; + + if (entry->socket_rotation) { + entry->socket_path = apr_psprintf(p, "%s.%d.%d.%d.sock", + wsgi_server_config->socket_prefix, + getpid(), mpm_generation, entry->id); + } + else { + entry->socket_path = apr_psprintf(p, "%s.%d.u%d.%d.sock", + wsgi_server_config->socket_prefix, + getpid(), entry->uid, entry->id); + } apr_hash_set(wsgi_daemon_index, entry->name, APR_HASH_KEY_STRING, entry); @@ -10589,8 +10768,9 @@ static int wsgi_connect_daemon(request_rec *r, WSGIDaemonSocket *daemon) "mod_wsgi (pid=%d): Unable to connect to " "WSGI daemon process '%s' on '%s' after " "multiple attempts as listener backlog " - "limit was exceeded.", getpid(), - daemon->name, daemon->socket_path); + "limit was exceeded or the socket does " + "not exist.", getpid(), daemon->name, + daemon->socket_path); apr_socket_close(daemon->socket); @@ -11227,6 +11407,15 @@ static int wsgi_transfer_response(request_rec *r, apr_bucket_brigade *bb, if (rv != APR_SUCCESS) { apr_brigade_destroy(bb); + /* + * Don't flag error if client connection was aborted + * so that access log still records the original HTTP + * response code returned by the WSGI application. + */ + + if (r->connection->aborted) + return OK; + return HTTP_INTERNAL_SERVER_ERROR; } @@ -11270,6 +11459,15 @@ static int wsgi_transfer_response(request_rec *r, apr_bucket_brigade *bb, if (rv != APR_SUCCESS) { apr_brigade_destroy(bb); + /* + * Don't flag error if client connection was aborted + * so that access log still records the original HTTP + * response code returned by the WSGI application. + */ + + if (r->connection->aborted) + return OK; + return HTTP_INTERNAL_SERVER_ERROR; } @@ -11290,7 +11488,17 @@ static int wsgi_transfer_response(request_rec *r, apr_bucket_brigade *bb, } else if (rv != APR_SUCCESS) { apr_brigade_destroy(bb); - return HTTP_INTERNAL_SERVER_ERROR; + ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r, + "mod_wsgi (pid=%d): Failed to proxy response " + "from daemon.", getpid()); + + /* + * Don't flag error if couldn't read from daemon + * so that access log still records the original HTTP + * response code returned by the WSGI application. + */ + + return OK; } /* @@ -11362,6 +11570,15 @@ static int wsgi_transfer_response(request_rec *r, apr_bucket_brigade *bb, if (rv != APR_SUCCESS) { apr_brigade_destroy(bb); + /* + * Don't flag error if client connection was aborted + * so that access log still records the original HTTP + * response code returned by the WSGI application. + */ + + if (r->connection->aborted) + return OK; + return HTTP_INTERNAL_SERVER_ERROR; } } @@ -14411,7 +14628,8 @@ static authn_status wsgi_check_password(request_rec *r, const char *user, } if (!module) { - module = wsgi_load_source(r->pool, r, name, exists, script, "", group); + module = wsgi_load_source(r->pool, r, name, exists, script, + "", group, 0); } /* Safe now to release the module lock. */ @@ -14420,6 +14638,11 @@ static authn_status wsgi_check_password(request_rec *r, const char *user, apr_thread_mutex_unlock(wsgi_module_lock); #endif + /* Log any details of exceptions if import failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + /* Assume an internal server error unless everything okay. */ status = AUTH_GENERAL_ERROR; @@ -14503,6 +14726,11 @@ static authn_status wsgi_check_password(request_rec *r, const char *user, adapter->r = NULL; + /* Log any details of exceptions if execution failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + /* Close the log object so data is flushed. */ method = PyObject_GetAttrString(adapter->log, "close"); @@ -14514,19 +14742,22 @@ static authn_status wsgi_check_password(request_rec *r, const char *user, } else { args = PyTuple_New(0); - object = PyEval_CallObject(method, args); + result = PyEval_CallObject(method, args); + Py_XDECREF(result); Py_DECREF(args); } - Py_XDECREF(object); + /* Log any details of exceptions if execution failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + Py_XDECREF(method); /* No longer need adapter object. */ Py_DECREF((PyObject *)adapter); } - else - Py_DECREF(object); } else { Py_BEGIN_ALLOW_THREADS @@ -14536,11 +14767,6 @@ static authn_status wsgi_check_password(request_rec *r, const char *user, "'Basic' auth provider.", getpid(), script); Py_END_ALLOW_THREADS } - - /* Log any details of exceptions if execution failed. */ - - if (PyErr_Occurred()) - wsgi_log_python_error(r, NULL, script, 0); } /* Cleanup and release interpreter, */ @@ -14647,7 +14873,8 @@ static authn_status wsgi_get_realm_hash(request_rec *r, const char *user, } if (!module) { - module = wsgi_load_source(r->pool, r, name, exists, script, "", group); + module = wsgi_load_source(r->pool, r, name, exists, script, + "", group, 0); } /* Safe now to release the module lock. */ @@ -14656,6 +14883,11 @@ static authn_status wsgi_get_realm_hash(request_rec *r, const char *user, apr_thread_mutex_unlock(wsgi_module_lock); #endif + /* Log any details of exceptions if import failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + /* Assume an internal server error unless everything okay. */ status = AUTH_GENERAL_ERROR; @@ -14740,6 +14972,11 @@ static authn_status wsgi_get_realm_hash(request_rec *r, const char *user, adapter->r = NULL; + /* Log any details of exceptions if execution failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + /* Close the log object so data is flushed. */ method = PyObject_GetAttrString(adapter->log, "close"); @@ -14751,19 +14988,22 @@ static authn_status wsgi_get_realm_hash(request_rec *r, const char *user, } else { args = PyTuple_New(0); - object = PyEval_CallObject(method, args); + result = PyEval_CallObject(method, args); + Py_XDECREF(result); Py_DECREF(args); } - Py_XDECREF(object); + /* Log any details of exceptions if execution failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + Py_XDECREF(method); /* No longer need adapter object. */ Py_DECREF((PyObject *)adapter); } - else - Py_DECREF(object); } else { Py_BEGIN_ALLOW_THREADS @@ -14773,11 +15013,6 @@ static authn_status wsgi_get_realm_hash(request_rec *r, const char *user, "'Digest' auth provider.", getpid(), script); Py_END_ALLOW_THREADS } - - /* Log any details of exceptions if execution failed. */ - - if (PyErr_Occurred()) - wsgi_log_python_error(r, NULL, script, 0); } /* Cleanup and release interpreter, */ @@ -14889,7 +15124,8 @@ static int wsgi_groups_for_user(request_rec *r, WSGIRequestConfig *config, } if (!module) { - module = wsgi_load_source(r->pool, r, name, exists, script, "", group); + module = wsgi_load_source(r->pool, r, name, exists, script, + "", group, 0); } /* Safe now to release the module lock. */ @@ -14898,6 +15134,11 @@ static int wsgi_groups_for_user(request_rec *r, WSGIRequestConfig *config, apr_thread_mutex_unlock(wsgi_module_lock); #endif + /* Log any details of exceptions if import failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + /* Assume an internal server error unless everything okay. */ status = HTTP_INTERNAL_SERVER_ERROR; @@ -14914,7 +15155,7 @@ static int wsgi_groups_for_user(request_rec *r, WSGIRequestConfig *config, if (object) { PyObject *vars = NULL; PyObject *args = NULL; - PyObject *sequence = NULL; + PyObject *result = NULL; PyObject *method = NULL; AuthObject *adapter = NULL; @@ -14926,15 +15167,15 @@ static int wsgi_groups_for_user(request_rec *r, WSGIRequestConfig *config, Py_INCREF(object); args = Py_BuildValue("(Os)", vars, r->user); - sequence = PyEval_CallObject(object, args); + result = PyEval_CallObject(object, args); Py_DECREF(args); Py_DECREF(object); Py_DECREF(vars); - if (sequence) { + if (result) { PyObject *iterator; - iterator = PyObject_GetIter(sequence); + iterator = PyObject_GetIter(result); if (iterator) { PyObject *item; @@ -15010,7 +15251,7 @@ static int wsgi_groups_for_user(request_rec *r, WSGIRequestConfig *config, Py_END_ALLOW_THREADS } - Py_DECREF(sequence); + Py_DECREF(result); } /* @@ -15023,6 +15264,11 @@ static int wsgi_groups_for_user(request_rec *r, WSGIRequestConfig *config, adapter->r = NULL; + /* Log any details of exceptions if execution failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + /* Close the log object so data is flushed. */ method = PyObject_GetAttrString(adapter->log, "close"); @@ -15034,19 +15280,22 @@ static int wsgi_groups_for_user(request_rec *r, WSGIRequestConfig *config, } else { args = PyTuple_New(0); - object = PyEval_CallObject(method, args); + result = PyEval_CallObject(method, args); + Py_XDECREF(result); Py_DECREF(args); } - Py_XDECREF(object); + /* Log any details of exceptions if execution failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + Py_XDECREF(method); /* No longer need adapter object. */ Py_DECREF((PyObject *)adapter); } - else - Py_DECREF(object); } else { Py_BEGIN_ALLOW_THREADS @@ -15056,11 +15305,6 @@ static int wsgi_groups_for_user(request_rec *r, WSGIRequestConfig *config, "group provider.", getpid(), script); Py_END_ALLOW_THREADS } - - /* Log any details of exceptions if execution failed. */ - - if (PyErr_Occurred()) - wsgi_log_python_error(r, NULL, script, 0); } /* Cleanup and release interpreter, */ @@ -15087,7 +15331,7 @@ static int wsgi_allow_access(request_rec *r, WSGIRequestConfig *config, const char *script; const char *group; - int result = 0; + int allow = 0; if (!config->access_script) { ap_log_error(APLOG_MARK, APLOG_ERR, 0, wsgi_server, @@ -15166,7 +15410,8 @@ static int wsgi_allow_access(request_rec *r, WSGIRequestConfig *config, } if (!module) { - module = wsgi_load_source(r->pool, r, name, exists, script, "", group); + module = wsgi_load_source(r->pool, r, name, exists, script, + "", group, 0); } /* Safe now to release the module lock. */ @@ -15175,9 +15420,14 @@ static int wsgi_allow_access(request_rec *r, WSGIRequestConfig *config, apr_thread_mutex_unlock(wsgi_module_lock); #endif + /* Log any details of exceptions if import failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + /* Assume not allowed unless everything okay. */ - result = 0; + allow = 0; /* Determine if script exists and execute it. */ @@ -15191,7 +15441,7 @@ static int wsgi_allow_access(request_rec *r, WSGIRequestConfig *config, if (object) { PyObject *vars = NULL; PyObject *args = NULL; - PyObject *flag = NULL; + PyObject *result = NULL; PyObject *method = NULL; AuthObject *adapter = NULL; @@ -15203,18 +15453,18 @@ static int wsgi_allow_access(request_rec *r, WSGIRequestConfig *config, Py_INCREF(object); args = Py_BuildValue("(Oz)", vars, host); - flag = PyEval_CallObject(object, args); + result = PyEval_CallObject(object, args); Py_DECREF(args); Py_DECREF(object); Py_DECREF(vars); - if (flag) { - if (flag == Py_None) { - result = -1; + if (result) { + if (result == Py_None) { + allow = -1; } - else if (PyBool_Check(flag)) { - if (flag == Py_True) - result = 1; + else if (PyBool_Check(result)) { + if (result == Py_True) + allow = 1; } else { Py_BEGIN_ALLOW_THREADS @@ -15226,7 +15476,7 @@ static int wsgi_allow_access(request_rec *r, WSGIRequestConfig *config, Py_END_ALLOW_THREADS } - Py_DECREF(flag); + Py_DECREF(result); } /* @@ -15239,6 +15489,11 @@ static int wsgi_allow_access(request_rec *r, WSGIRequestConfig *config, adapter->r = NULL; + /* Log any details of exceptions if execution failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + /* Close the log object so data is flushed. */ method = PyObject_GetAttrString(adapter->log, "close"); @@ -15250,19 +15505,22 @@ static int wsgi_allow_access(request_rec *r, WSGIRequestConfig *config, } else { args = PyTuple_New(0); - object = PyEval_CallObject(method, args); + result = PyEval_CallObject(method, args); + Py_XDECREF(result); Py_DECREF(args); } - Py_XDECREF(object); + /* Log any details of exceptions if execution failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + Py_XDECREF(method); /* No longer need adapter object. */ Py_DECREF((PyObject *)adapter); } - else - Py_DECREF(object); } else { Py_BEGIN_ALLOW_THREADS @@ -15272,11 +15530,6 @@ static int wsgi_allow_access(request_rec *r, WSGIRequestConfig *config, "host validator.", getpid(), script); Py_END_ALLOW_THREADS } - - /* Log any details of exceptions if execution failed. */ - - if (PyErr_Occurred()) - wsgi_log_python_error(r, NULL, script, 0); } /* Cleanup and release interpreter, */ @@ -15285,7 +15538,7 @@ static int wsgi_allow_access(request_rec *r, WSGIRequestConfig *config, wsgi_release_interpreter(interp); - return result; + return allow; } static int wsgi_hook_access_checker(request_rec *r) @@ -15422,7 +15675,8 @@ static int wsgi_hook_check_user_id(request_rec *r) } if (!module) { - module = wsgi_load_source(r->pool, r, name, exists, script, "", group); + module = wsgi_load_source(r->pool, r, name, exists, script, + "", group, 0); } /* Safe now to release the module lock. */ @@ -15431,6 +15685,11 @@ static int wsgi_hook_check_user_id(request_rec *r) apr_thread_mutex_unlock(wsgi_module_lock); #endif + /* Log any details of exceptions if import failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + /* Assume an internal server error unless everything okay. */ status = HTTP_INTERNAL_SERVER_ERROR; @@ -15512,6 +15771,11 @@ static int wsgi_hook_check_user_id(request_rec *r) adapter->r = NULL; + /* Log any details of exceptions if execution failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + /* Close the log object so data is flushed. */ method = PyObject_GetAttrString(adapter->log, "close"); @@ -15523,19 +15787,22 @@ static int wsgi_hook_check_user_id(request_rec *r) } else { args = PyTuple_New(0); - object = PyEval_CallObject(method, args); + result = PyEval_CallObject(method, args); + Py_XDECREF(result); Py_DECREF(args); } - Py_XDECREF(object); + /* Log any details of exceptions if execution failed. */ + + if (PyErr_Occurred()) + wsgi_log_python_error(r, NULL, script, 0); + Py_XDECREF(method); /* No longer need adapter object. */ Py_DECREF((PyObject *)adapter); } - else - Py_DECREF(object); } else { Py_BEGIN_ALLOW_THREADS @@ -15545,11 +15812,6 @@ static int wsgi_hook_check_user_id(request_rec *r) "'Basic' auth provider.", getpid(), script); Py_END_ALLOW_THREADS } - - /* Log any details of exceptions if execution failed. */ - - if (PyErr_Occurred()) - wsgi_log_python_error(r, NULL, script, 0); } /* Cleanup and release interpreter, */ @@ -15798,6 +16060,8 @@ static const command_rec wsgi_commands[] = NULL, RSRC_CONF, "Specify details of daemon processes to start."), AP_INIT_TAKE1("WSGISocketPrefix", wsgi_set_socket_prefix, NULL, RSRC_CONF, "Path prefix for the daemon process sockets."), + AP_INIT_TAKE1("WSGISocketRotation", wsgi_set_socket_rotation, + NULL, RSRC_CONF, "Enable/Disable rotation of daemon process sockets."), AP_INIT_TAKE1("WSGIAcceptMutex", wsgi_set_accept_mutex, NULL, RSRC_CONF, "Set accept mutex type for daemon processes."), diff --git a/src/server/wsgi_apache.c b/src/server/wsgi_apache.c index 56f698a..c3f5412 100644 --- a/src/server/wsgi_apache.c +++ b/src/server/wsgi_apache.c @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_apache.h b/src/server/wsgi_apache.h index 7c0fa38..88daaf9 100644 --- a/src/server/wsgi_apache.h +++ b/src/server/wsgi_apache.h @@ -4,7 +4,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_buckets.c b/src/server/wsgi_buckets.c index 3d9968f..0ab679a 100644 --- a/src/server/wsgi_buckets.c +++ b/src/server/wsgi_buckets.c @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_buckets.h b/src/server/wsgi_buckets.h index 9a47984..7568476 100644 --- a/src/server/wsgi_buckets.h +++ b/src/server/wsgi_buckets.h @@ -4,7 +4,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_convert.c b/src/server/wsgi_convert.c index 1e167d6..7dc5e2e 100644 --- a/src/server/wsgi_convert.c +++ b/src/server/wsgi_convert.c @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_convert.h b/src/server/wsgi_convert.h index 3124d4b..a604b5e 100644 --- a/src/server/wsgi_convert.h +++ b/src/server/wsgi_convert.h @@ -4,7 +4,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_daemon.c b/src/server/wsgi_daemon.c index 2430caf..c2003d2 100644 --- a/src/server/wsgi_daemon.c +++ b/src/server/wsgi_daemon.c @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_daemon.h b/src/server/wsgi_daemon.h index 1101330..f8855f8 100644 --- a/src/server/wsgi_daemon.h +++ b/src/server/wsgi_daemon.h @@ -4,7 +4,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -134,6 +134,7 @@ typedef struct { rlim_t memory_limit; rlim_t virtual_memory_limit; const char *socket_path; + int socket_rotation; int listener_fd; const char* mutex_path; apr_proc_mutex_t* mutex; diff --git a/src/server/wsgi_interp.c b/src/server/wsgi_interp.c index 41aede7..00c803c 100644 --- a/src/server/wsgi_interp.c +++ b/src/server/wsgi_interp.c @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -159,6 +159,24 @@ PyTypeObject SignalIntercept_Type = { 0, /*tp_is_gc*/ }; +/* ------------------------------------------------------------------------- */ + +static PyObject *wsgi_system_exit(PyObject *self, PyObject *args) +{ + PyErr_SetObject(PyExc_SystemExit, 0); + + return NULL; +} + +/* ------------------------------------------------------------------------- */ + +static PyMethodDef wsgi_system_exit_method[] = { + { "system_exit", (PyCFunction)wsgi_system_exit, METH_VARARGS, 0 }, + { NULL }, +}; + +/* ------------------------------------------------------------------------- */ + /* Wrapper around Python interpreter instances. */ const char *wsgi_python_path = NULL; @@ -562,11 +580,64 @@ InterpreterObject *newInterpreterObject(const char *name) /* * Install intercept for signal handler registration - * if appropriate. + * if appropriate. Don't do this though if number of + * threads for daemon process was set as 0, indicating + * a potential daemon process which is running a + * service script. */ - if (wsgi_server_config->restrict_signal != 0) { + /* + * If running in daemon mode and there are no threads + * specified, must be running with service script, in + * which case we register default signal handler for + * SIGINT which throws a SystemExit exception. If + * instead restricting signals, replace function for + * registering signal handlers so they are ignored. + */ + + if (wsgi_daemon_process && wsgi_daemon_process->group->threads == 0) { + module = PyImport_ImportModule("signal"); + + if (module) { + PyObject *dict = NULL; + PyObject *func = NULL; + + dict = PyModule_GetDict(module); + func = PyDict_GetItemString(dict, "signal"); + + if (func) { + PyObject *res = NULL; + PyObject *args = NULL; + PyObject *callback = NULL; + + Py_INCREF(func); + + callback = PyCFunction_New(&wsgi_system_exit_method[0], NULL); + + args = Py_BuildValue("(iO)", SIGTERM, callback); + res = PyEval_CallObject(func, args); + + if (!res) { + Py_BEGIN_ALLOW_THREADS + ap_log_error(APLOG_MARK, APLOG_ERR, 0, wsgi_server, + "mod_wsgi (pid=%d): Call to " + "'signal.signal()' to register exit " + "function failed, ignoring.", getpid()); + Py_END_ALLOW_THREADS + } + + Py_XDECREF(res); + Py_XDECREF(args); + Py_XDECREF(callback); + + Py_DECREF(func); + } + } + + Py_XDECREF(module); + } + else if (wsgi_server_config->restrict_signal != 0) { module = PyImport_ImportModule("signal"); if (module) { @@ -1168,6 +1239,8 @@ InterpreterObject *newInterpreterObject(const char *name) PyModule_AddObject(module, "event_callbacks", PyList_New(0)); + PyModule_AddObject(module, "active_requests", PyDict_New()); + PyModule_AddObject(module, "request_data", PyCFunction_New( &wsgi_request_data_method[0], NULL)); @@ -1441,8 +1514,6 @@ static void Interpreter_dealloc(InterpreterObject *self) PyObject *exitfunc = NULL; #endif - PyObject *event = NULL; - /* * We should always enter here with the Python GIL * held and an active thread state. This should only @@ -1509,14 +1580,6 @@ static void Interpreter_dealloc(InterpreterObject *self) Py_END_ALLOW_THREADS } - /* Publish event that process is being stopped. */ - - event = PyDict_New(); - - wsgi_publish_event("process_stopping", event); - - Py_DECREF(event); - /* * Because the thread state we are using was created outside * of any Python code and is not the same as the Python main @@ -1994,7 +2057,8 @@ apr_status_t wsgi_python_term(void) * condition. */ - apr_thread_mutex_lock(wsgi_shutdown_lock); + if (wsgi_daemon_process) + apr_thread_mutex_lock(wsgi_shutdown_lock); #if defined(MOD_WSGI_WITH_DAEMONS) wsgi_daemon_shutdown++; @@ -2002,7 +2066,8 @@ apr_status_t wsgi_python_term(void) Py_Finalize(); - apr_thread_mutex_unlock(wsgi_shutdown_lock); + if (wsgi_daemon_process) + apr_thread_mutex_unlock(wsgi_shutdown_lock); wsgi_python_initialized = 0; @@ -2355,6 +2420,8 @@ apr_thread_mutex_t* wsgi_shutdown_lock = NULL; PyObject *wsgi_interpreters = NULL; +apr_hash_t *wsgi_interpreters_index = NULL; + InterpreterObject *wsgi_acquire_interpreter(const char *name) { PyThreadState *tstate = NULL; @@ -2414,6 +2481,16 @@ InterpreterObject *wsgi_acquire_interpreter(const char *name) } PyDict_SetItemString(wsgi_interpreters, name, (PyObject *)handle); + + /* + * Add interpreter name to index kept in Apache data + * strcuture as well. Make a copy of the name just in + * case we have been given temporary value. + */ + + apr_hash_set(wsgi_interpreters_index, apr_pstrdup( + apr_hash_pool_get(wsgi_interpreters_index), name), + APR_HASH_KEY_STRING, ""); } else Py_INCREF(handle); @@ -2525,4 +2602,39 @@ void wsgi_release_interpreter(InterpreterObject *handle) /* ------------------------------------------------------------------------- */ +void wsgi_publish_process_stopping(char *reason) +{ + InterpreterObject *interp = NULL; + apr_hash_index_t *hi; + + hi = apr_hash_first(NULL, wsgi_interpreters_index); + + while (hi) { + PyObject *event = NULL; + PyObject *object = NULL; + + interp = wsgi_acquire_interpreter((char *)apr_hash_this_key(hi)); + + event = PyDict_New(); + +#if PY_MAJOR_VERSION >= 3 + object = PyUnicode_DecodeLatin1(reason, strlen(reason), NULL); +#else + object = PyString_FromString(reason); +#endif + PyDict_SetItemString(event, "shutdown_reason", object); + Py_DECREF(object); + + wsgi_publish_event("process_stopping", event); + + Py_DECREF(event); + + wsgi_release_interpreter(interp); + + hi = apr_hash_next(hi); + } +} + +/* ------------------------------------------------------------------------- */ + /* vi: set sw=4 expandtab : */ diff --git a/src/server/wsgi_interp.h b/src/server/wsgi_interp.h index 6fad25b..831c078 100644 --- a/src/server/wsgi_interp.h +++ b/src/server/wsgi_interp.h @@ -4,7 +4,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -68,6 +68,8 @@ extern const char *wsgi_python_eggs; extern PyObject *wsgi_interpreters; +extern apr_hash_t *wsgi_interpreters_index; + #if APR_HAS_THREADS extern apr_thread_mutex_t *wsgi_interp_lock; extern apr_thread_mutex_t* wsgi_shutdown_lock; @@ -81,6 +83,8 @@ extern apr_status_t wsgi_python_term(void); extern InterpreterObject *wsgi_acquire_interpreter(const char *name); extern void wsgi_release_interpreter(InterpreterObject *handle); +extern void wsgi_publish_process_stopping(char *reason); + /* ------------------------------------------------------------------------- */ #endif diff --git a/src/server/wsgi_logger.c b/src/server/wsgi_logger.c index 6ee0f78..0cb2021 100644 --- a/src/server/wsgi_logger.c +++ b/src/server/wsgi_logger.c @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -694,12 +694,21 @@ void wsgi_log_python_error(request_rec *r, PyObject *log, PyObject *object = NULL; if (wsgi_event_subscribers()) { + WSGIThreadInfo *thread_info; + + thread_info = wsgi_thread_info(0, 0); + event = PyDict_New(); object = Py_BuildValue("(OOO)", type, value, traceback); PyDict_SetItemString(event, "exception_info", object); Py_DECREF(object); +#if 0 + PyDict_SetItemString(event, "request_data", + thread_info->request_data); +#endif + wsgi_publish_event("request_exception", event); Py_DECREF(event); diff --git a/src/server/wsgi_logger.h b/src/server/wsgi_logger.h index c64d1f4..2184ff2 100644 --- a/src/server/wsgi_logger.h +++ b/src/server/wsgi_logger.h @@ -4,7 +4,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_memory.h b/src/server/wsgi_memory.h index d32fb9b..dc343f5 100644 --- a/src/server/wsgi_memory.h +++ b/src/server/wsgi_memory.h @@ -4,7 +4,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_metrics.c b/src/server/wsgi_metrics.c index e8f0859..7fb61df 100644 --- a/src/server/wsgi_metrics.c +++ b/src/server/wsgi_metrics.c @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,7 +40,6 @@ apr_uint64_t wsgi_total_requests = 0; int wsgi_active_requests = 0; static double wsgi_thread_utilization = 0.0; static apr_time_t wsgi_utilization_last = 0; -int wsgi_dump_stack_traces = 0; /* Request tracking and timing. */ @@ -77,13 +76,43 @@ static double wsgi_utilization_time(int adjustment) return utilization; } -WSGIThreadInfo *wsgi_start_request(void) +WSGIThreadInfo *wsgi_start_request(request_rec *r) { WSGIThreadInfo *thread_info; + PyObject *module = NULL; + thread_info = wsgi_thread_info(1, 1); + thread_info->request_data = PyDict_New(); +#if AP_MODULE_MAGIC_AT_LEAST(20100923,2) +#if PY_MAJOR_VERSION >= 3 + thread_info->request_id = PyUnicode_DecodeLatin1(r->log_id, + strlen(r->log_id), NULL); +#else + thread_info->request_id = PyString_FromString(r->log_id); +#endif + + module = PyImport_ImportModule("mod_wsgi"); + + if (module) { + PyObject *dict = NULL; + PyObject *requests = NULL; + + dict = PyModule_GetDict(module); + requests = PyDict_GetItemString(dict, "active_requests"); + + if (requests) + PyDict_SetItem(requests, thread_info->request_id, + thread_info->request_data); + + Py_DECREF(module); + } + else + PyErr_Clear(); +#endif + wsgi_utilization_time(1); return thread_info; @@ -93,12 +122,34 @@ void wsgi_end_request(void) { WSGIThreadInfo *thread_info; + PyObject *module = NULL; + thread_info = wsgi_thread_info(0, 1); if (thread_info) { +#if AP_MODULE_MAGIC_AT_LEAST(20100923,2) + module = PyImport_ImportModule("mod_wsgi"); + + if (module) { + PyObject *dict = NULL; + PyObject *requests = NULL; + + dict = PyModule_GetDict(module); + requests = PyDict_GetItemString(dict, "active_requests"); + + PyDict_DelItem(requests, thread_info->request_id); + + Py_DECREF(module); + } + else + PyErr_Clear(); +#endif if (thread_info->log_buffer) Py_CLEAR(thread_info->log_buffer); + if (thread_info->request_id) + Py_CLEAR(thread_info->request_id); + if (thread_info->request_data) Py_CLEAR(thread_info->request_data); } @@ -611,12 +662,12 @@ static PyObject *wsgi_subscribe_events(PyObject *self, PyObject *args) PyList_Append(list, callback); else return NULL; + + Py_DECREF(module); } else return NULL; - Py_DECREF(module); - Py_INCREF(Py_None); return Py_None; } @@ -639,7 +690,7 @@ long wsgi_event_subscribers(void) if (list) result = PyList_Size(list); - Py_XDECREF(module); + Py_DECREF(module); return result; } @@ -663,6 +714,8 @@ void wsgi_publish_event(const char *name, PyObject *event) list = PyDict_GetItemString(dict, "event_callbacks"); Py_INCREF(list); + + Py_DECREF(module); } else { Py_BEGIN_ALLOW_THREADS @@ -676,8 +729,6 @@ void wsgi_publish_event(const char *name, PyObject *event) return; } - Py_XDECREF(module); - if (!list) { Py_BEGIN_ALLOW_THREADS ap_log_error(APLOG_MARK, APLOG_ERR, 0, wsgi_server, diff --git a/src/server/wsgi_metrics.h b/src/server/wsgi_metrics.h index 6b9eac0..03145e4 100644 --- a/src/server/wsgi_metrics.h +++ b/src/server/wsgi_metrics.h @@ -4,7 +4,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,13 +30,12 @@ extern apr_uint64_t wsgi_total_requests; extern int wsgi_active_requests; -extern int wsgi_dump_stack_traces; extern apr_thread_mutex_t* wsgi_monitor_lock; extern PyMethodDef wsgi_process_metrics_method[]; -extern WSGIThreadInfo *wsgi_start_request(void); +extern WSGIThreadInfo *wsgi_start_request(request_rec *r); extern void wsgi_end_request(void); extern PyMethodDef wsgi_server_metrics_method[]; diff --git a/src/server/wsgi_python.h b/src/server/wsgi_python.h index dea3c3b..8ae3cfc 100644 --- a/src/server/wsgi_python.h +++ b/src/server/wsgi_python.h @@ -4,7 +4,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_restrict.c b/src/server/wsgi_restrict.c index cbd27bf..5ebba4b 100644 --- a/src/server/wsgi_restrict.c +++ b/src/server/wsgi_restrict.c @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_restrict.h b/src/server/wsgi_restrict.h index 86ec64c..0c6c18d 100644 --- a/src/server/wsgi_restrict.h +++ b/src/server/wsgi_restrict.h @@ -4,7 +4,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_server.c b/src/server/wsgi_server.c index 22f7f49..229b028 100644 --- a/src/server/wsgi_server.c +++ b/src/server/wsgi_server.c @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -82,6 +82,8 @@ WSGIServerConfig *newWSGIServerConfig(apr_pool_t *p) object->socket_prefix = ap_server_root_relative(p, object->socket_prefix); #endif + object->socket_rotation = 1; + object->verbose_debugging = 0; object->python_warnings = NULL; diff --git a/src/server/wsgi_server.h b/src/server/wsgi_server.h index 20f5a42..02892ae 100644 --- a/src/server/wsgi_server.h +++ b/src/server/wsgi_server.h @@ -4,7 +4,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,6 +67,7 @@ typedef struct { apr_array_header_t *alias_list; const char *socket_prefix; + int socket_rotation; apr_lockmech_e lock_mechanism; int verbose_debugging; diff --git a/src/server/wsgi_stream.c b/src/server/wsgi_stream.c index 7e04d65..934479a 100644 --- a/src/server/wsgi_stream.c +++ b/src/server/wsgi_stream.c @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_stream.h b/src/server/wsgi_stream.h index 28ba958..1f3b8a1 100644 --- a/src/server/wsgi_stream.h +++ b/src/server/wsgi_stream.h @@ -4,7 +4,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_thread.h b/src/server/wsgi_thread.h index fd6da78..5a29d74 100644 --- a/src/server/wsgi_thread.h +++ b/src/server/wsgi_thread.h @@ -4,7 +4,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,7 @@ typedef struct { int thread_id; int request_thread; apr_int64_t request_count; + PyObject *request_id; PyObject *request_data; PyObject *log_buffer; } WSGIThreadInfo; diff --git a/src/server/wsgi_validate.c b/src/server/wsgi_validate.c index e8741b6..72259ca 100644 --- a/src/server/wsgi_validate.c +++ b/src/server/wsgi_validate.c @@ -1,7 +1,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_validate.h b/src/server/wsgi_validate.h index ecb7b1e..18a56be 100644 --- a/src/server/wsgi_validate.h +++ b/src/server/wsgi_validate.h @@ -4,7 +4,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/server/wsgi_version.h b/src/server/wsgi_version.h index 1d7e9ee..d1f1f40 100755 --- a/src/server/wsgi_version.h +++ b/src/server/wsgi_version.h @@ -4,7 +4,7 @@ /* ------------------------------------------------------------------------- */ /* - * Copyright 2007-2017 GRAHAM DUMPLETON + * Copyright 2007-2018 GRAHAM DUMPLETON * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,9 +24,9 @@ /* Module version information. */ #define MOD_WSGI_MAJORVERSION_NUMBER 4 -#define MOD_WSGI_MINORVERSION_NUMBER 5 -#define MOD_WSGI_MICROVERSION_NUMBER 24 -#define MOD_WSGI_VERSION_STRING "4.5.24" +#define MOD_WSGI_MINORVERSION_NUMBER 6 +#define MOD_WSGI_MICROVERSION_NUMBER 0 +#define MOD_WSGI_VERSION_STRING "4.6.0" /* ------------------------------------------------------------------------- */ diff --git a/tests/auth.wsgi b/tests/auth.wsgi index 37a90cb..77b6eae 100644 --- a/tests/auth.wsgi +++ b/tests/auth.wsgi @@ -1,3 +1,7 @@ +def allow_access(environ, host): + print('HOST', host, environ['REQUEST_URI']) + return True + def check_password(environ, user, password): print('USER', user, environ['REQUEST_URI']) if user == 'spy': @@ -10,14 +14,17 @@ def check_password(environ, user, password): return False return None -import md5 +import hashlib def get_realm_hash(environ, user, realm): print('USER', user, environ['REQUEST_URI']) if user == 'spy': - value = md5.new() + value = hashlib.md5() # user:realm:password - value.update('%s:%s:%s' % (user, realm, 'secret')) + input = '%s:%s:%s' % (user, realm, 'secret') + if not isinstance(input, bytes): + input = input.encode('UTF-8') + value.update(input) hash = value.hexdigest() return hash return None diff --git a/tests/events.wsgi b/tests/events.wsgi index ab3f10f..6d7dfb0 100644 --- a/tests/events.wsgi +++ b/tests/events.wsgi @@ -4,6 +4,7 @@ import mod_wsgi import traceback import time import os +import threading try: mod_wsgi.request_data() @@ -24,15 +25,24 @@ def event_handler(name, **kwargs): environ = kwargs['request_environ'] start_time = time.time() request['start_time'] = start_time + thread = threading.current_thread() + request['thread_name'] = thread.name + request['thread_id'] = thread.ident return dict(application_object=wrapper(kwargs['application_object'])) + elif name == 'response_started': + request = mod_wsgi.request_data() + print('CONTENT', request) + print('ACTIVE', mod_wsgi.active_requests) elif name == 'request_finished': request = mod_wsgi.request_data() print('REQUEST', request) print('FINISH', time.time()-request['start_time']) print('PROCESS', mod_wsgi.process_metrics()) elif name == 'request_exception': - exc_info = kwargs['exc_info'] - traceback.print_exception(*exc_info) + exception_info = kwargs['exception_info'] + traceback.print_exception(*exception_info) + elif name == 'process_stopping': + print('SHUTDOWN', mod_wsgi.active_requests) print('EVENTS', mod_wsgi.event_callbacks) @@ -42,9 +52,11 @@ print('CALLBACKS', mod_wsgi.event_callbacks) def application(environ, start_response): failure_mode = environ.get('HTTP_X_FAILURE_MODE', '') - failure_mode = failure_mode.split() + sleep_duration = environ.get('HTTP_X_SLEEP_DURATION', 0) + sleep_duration = float(sleep_duration or 0) + if 'application' in failure_mode: raise RuntimeError('application') @@ -57,6 +69,9 @@ def application(environ, start_response): environ['wsgi.input'].read() + if sleep_duration: + time.sleep(sleep_duration) + try: yield output |