Repository: Supervisor/supervisor
Branch: main
Commit: abc60468ea4b
Files: 135
Total size: 1.7 MB
Directory structure:
gitextract_lqosy3py/
├── .github/
│ └── workflows/
│ └── main.yml
├── .gitignore
├── .readthedocs.yaml
├── CHANGES.rst
├── COPYRIGHT.txt
├── LICENSES.txt
├── MANIFEST.in
├── README.rst
├── docs/
│ ├── .static/
│ │ └── repoze.css
│ ├── Makefile
│ ├── api.rst
│ ├── conf.py
│ ├── configuration.rst
│ ├── development.rst
│ ├── events.rst
│ ├── faq.rst
│ ├── glossary.rst
│ ├── index.rst
│ ├── installing.rst
│ ├── introduction.rst
│ ├── logging.rst
│ ├── plugins.rst
│ ├── running.rst
│ ├── subprocess.rst
│ ├── upgrading.rst
│ └── xmlrpc.rst
├── setup.cfg
├── setup.py
├── supervisor/
│ ├── __init__.py
│ ├── childutils.py
│ ├── compat.py
│ ├── confecho.py
│ ├── datatypes.py
│ ├── dispatchers.py
│ ├── events.py
│ ├── http.py
│ ├── http_client.py
│ ├── loggers.py
│ ├── medusa/
│ │ ├── CHANGES.txt
│ │ ├── LICENSE.txt
│ │ ├── README.txt
│ │ ├── TODO.txt
│ │ ├── __init__.py
│ │ ├── asynchat_25.py
│ │ ├── asyncore_25.py
│ │ ├── auth_handler.py
│ │ ├── counter.py
│ │ ├── default_handler.py
│ │ ├── docs/
│ │ │ ├── README.html
│ │ │ ├── async_blurbs.txt
│ │ │ ├── data_flow.html
│ │ │ ├── programming.html
│ │ │ ├── proxy_notes.txt
│ │ │ ├── threads.txt
│ │ │ └── tkinter.txt
│ │ ├── filesys.py
│ │ ├── http_date.py
│ │ ├── http_server.py
│ │ ├── logger.py
│ │ ├── producers.py
│ │ ├── util.py
│ │ └── xmlrpc_handler.py
│ ├── options.py
│ ├── pidproxy.py
│ ├── poller.py
│ ├── process.py
│ ├── rpcinterface.py
│ ├── skel/
│ │ └── sample.conf
│ ├── socket_manager.py
│ ├── states.py
│ ├── supervisorctl.py
│ ├── supervisord.py
│ ├── templating.py
│ ├── tests/
│ │ ├── __init__.py
│ │ ├── base.py
│ │ ├── fixtures/
│ │ │ ├── donothing.conf
│ │ │ ├── example/
│ │ │ │ └── included.conf
│ │ │ ├── hello.sh
│ │ │ ├── include.conf
│ │ │ ├── issue-1054.conf
│ │ │ ├── issue-1170a.conf
│ │ │ ├── issue-1170b.conf
│ │ │ ├── issue-1170c.conf
│ │ │ ├── issue-1224.conf
│ │ │ ├── issue-1231a.conf
│ │ │ ├── issue-1231b.conf
│ │ │ ├── issue-1231c.conf
│ │ │ ├── issue-1298.conf
│ │ │ ├── issue-1483a.conf
│ │ │ ├── issue-1483b.conf
│ │ │ ├── issue-1483c.conf
│ │ │ ├── issue-1596.conf
│ │ │ ├── issue-291a.conf
│ │ │ ├── issue-550.conf
│ │ │ ├── issue-565.conf
│ │ │ ├── issue-638.conf
│ │ │ ├── issue-663.conf
│ │ │ ├── issue-664.conf
│ │ │ ├── issue-733.conf
│ │ │ ├── issue-835.conf
│ │ │ ├── issue-836.conf
│ │ │ ├── issue-986.conf
│ │ │ ├── listener.py
│ │ │ ├── print_env.py
│ │ │ ├── spew.py
│ │ │ ├── test_1231.py
│ │ │ └── unkillable_spew.py
│ │ ├── test_childutils.py
│ │ ├── test_confecho.py
│ │ ├── test_datatypes.py
│ │ ├── test_dispatchers.py
│ │ ├── test_end_to_end.py
│ │ ├── test_events.py
│ │ ├── test_http.py
│ │ ├── test_http_client.py
│ │ ├── test_loggers.py
│ │ ├── test_options.py
│ │ ├── test_pidproxy.py
│ │ ├── test_poller.py
│ │ ├── test_process.py
│ │ ├── test_rpcinterfaces.py
│ │ ├── test_socket_manager.py
│ │ ├── test_states.py
│ │ ├── test_supervisorctl.py
│ │ ├── test_supervisord.py
│ │ ├── test_templating.py
│ │ ├── test_web.py
│ │ └── test_xmlrpc.py
│ ├── ui/
│ │ ├── status.html
│ │ ├── stylesheets/
│ │ │ └── supervisor.css
│ │ └── tail.html
│ ├── version.txt
│ ├── web.py
│ └── xmlrpc.py
└── tox.ini
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/main.yml
================================================
name: Run all tests
on: [push, pull_request]
env:
PIP: "env PIP_DISABLE_PIP_VERSION_CHECK=1
PYTHONWARNINGS=ignore:DEPRECATION
pip --no-cache-dir"
jobs:
tests_py2x:
runs-on: ubuntu-22.04
container:
image: python:2.7
strategy:
fail-fast: false
matrix:
toxenv: [py27, py27-configparser]
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: $PIP install virtualenv tox
- name: Run the unit tests
run: TOXENV=${{ matrix.toxenv }} tox
- name: Run the end-to-end tests
run: TOXENV=${{ matrix.toxenv }} END_TO_END=1 tox
tests_py34:
runs-on: ubuntu-22.04
container:
image: ubuntu:20.04
env:
LANG: C.UTF-8
steps:
- uses: actions/checkout@v4
- name: Install build dependencies
run: |
apt-get update
apt-get install -y build-essential unzip wget \
libncurses5-dev libgdbm-dev libnss3-dev \
libreadline-dev zlib1g-dev
- name: Build OpenSSL 1.0.2 (required by Python 3.4)
run: |
cd $RUNNER_TEMP
wget https://github.com/openssl/openssl/releases/download/OpenSSL_1_0_2u/openssl-1.0.2u.tar.gz
tar -xf openssl-1.0.2u.tar.gz
cd openssl-1.0.2u
./config --prefix=/usr/local/ssl --openssldir=/usr/local/ssl shared zlib-dynamic
make
make install
echo CFLAGS="-I/usr/local/ssl/include $CFLAGS" >> $GITHUB_ENV
echo LDFLAGS="-L/usr/local/ssl/lib $LDFLAGS" >> $GITHUB_ENV
echo LD_LIBRARY_PATH="/usr/local/ssl/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV
ln -s /usr/local/ssl/lib/libssl.so.1.0.0 /usr/lib/libssl.so.1.0.0
ln -s /usr/local/ssl/lib/libcrypto.so.1.0.0 /usr/lib/libcrypto.so.1.0.0
ldconfig
- name: Build Python 3.4
run: |
cd $RUNNER_TEMP
wget -O cpython-3.4.10.zip https://github.com/python/cpython/archive/refs/tags/v3.4.10.zip
unzip cpython-3.4.10.zip
cd cpython-3.4.10
./configure --with-ensurepip=install
make
make install
python3.4 --version
python3.4 -c 'import ssl'
pip3.4 --version
ln -s /usr/local/bin/python3.4 /usr/local/bin/python
ln -s /usr/local/bin/pip3.4 /usr/local/bin/pip
- name: Install Python dependencies
run: |
$PIP install virtualenv==20.4.7 tox==3.14.0
- name: Run the unit tests
run: TOXENV=py34 tox
- name: Run the end-to-end tests
run: TOXENV=py34 END_TO_END=1 tox
tests_py35:
runs-on: ubuntu-22.04
container:
image: python:3.5
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: $PIP install virtualenv tox
- name: Run the unit tests
run: TOXENV=py35 tox
- name: Run the end-to-end tests
run: TOXENV=py35 END_TO_END=1 tox
tests_py36:
runs-on: ubuntu-22.04
container:
image: python:3.6
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: $PIP install virtualenv tox
- name: Run the unit tests
run: TOXENV=py36 tox
- name: Run the end-to-end tests
run: TOXENV=py36 END_TO_END=1 tox
tests_py3x:
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
python-version: [3.7, 3.8, 3.9, "3.10", 3.11, 3.12, 3.13, 3.14]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: $PIP install virtualenv tox
- name: Set variable for TOXENV based on Python version
id: toxenv
run: python -c 'import sys; print("TOXENV=py%d%d" % (sys.version_info.major, sys.version_info.minor))' | tee -a $GITHUB_OUTPUT
- name: Run the unit tests
run: TOXENV=${{steps.toxenv.outputs.TOXENV}} tox
- name: Run the end-to-end tests
run: TOXENV=${{steps.toxenv.outputs.TOXENV}} END_TO_END=1 tox
coverage_py27:
runs-on: ubuntu-22.04
container:
image: python:2.7
strategy:
fail-fast: false
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: $PIP install virtualenv tox
- name: Run unit test coverage
run: TOXENV=cover tox
coverage_py3x:
runs-on: ubuntu-22.04
strategy:
fail-fast: false
matrix:
python-version: [3.8]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: $PIP install virtualenv tox
- name: Run unit test coverage
run: TOXENV=cover3 tox
docs:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: "3.8"
- name: Install dependencies
run: $PIP install virtualenv tox>=4.0.0
- name: Build the docs
run: TOXENV=docs tox
================================================
FILE: .gitignore
================================================
*~
*.egg
*.egg-info
*.log
*.pyc
*.pyo
*.swp
*.pss
.DS_Store
.coverage*
.eggs/
.pytest_cache/
.tox/
build/
docs/.build/
dist/
env*/
venv*/
htmlcov/
tmp/
coverage.xml
nosetests.xml
.cache/
================================================
FILE: .readthedocs.yaml
================================================
# .readthedocs.yaml
# Read the Docs configuration file
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
# Required
version: 2
# Set the version of Python and other tools you might need
build:
os: ubuntu-22.04
tools:
python: "3.11"
# Build documentation in the docs/ directory with Sphinx
sphinx:
configuration: docs/conf.py
# We recommend specifying your dependencies to enable reproducible builds:
# https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
# python:
# install:
# - requirements: docs/requirements.txt
================================================
FILE: CHANGES.rst
================================================
4.4.0.dev0 (Next Release)
-------------------------
- Fixed a bug where ``supervisord`` would wait 1 second on startup before
starting any programs. Patch by Stepan Blyshchak.
- Fixed a bug where the XML-RPC method ``supervisor.getAllConfigInfo()``
did not return the value of the ``autorestart`` program option.
- Fixed a bug where an escaped percent sign (``%%``) could not be used
in ``environment=`` in the ``[supervisord]`` section of the config file.
The bug did not affect ``[program:x]`` sections, where an escaped
percent sign in ``environment=`` already worked. Patch by yuk1pedia.
- Parsing ``environment=`` in the config file now uses ``shlex`` in POSIX
mode instead of legacy mode to allow for escaped quotes in the values.
However, on Python 2 before 2.7.13 and Python 3 before 3.5.3, POSIX mode
can't be used because of a `bug `_
in ``shlex``. If ``supervisord`` is run on a Python version with the bug,
it will fall back to legacy mode. Patch by Stefan Friesel.
- The old example scripts in the ``supervisor/scripts/`` directory of
the package, which were largely undocumented, had no test coverage, and
were last updated over a decade ago, have been removed.
- When importing a plugin fails, the error message printed by ``supervisord``
now includes the Python exception message for easier debugging.
Patch by Sandro Jäckel.
4.3.0 (2025-08-23)
------------------
- Fixed a bug where the poller would not unregister a closed
file descriptor under some circumstances, which caused excessive
polling, resulting in higher CPU usage. Patch by aftersnow.
- Fixed a bug where restarting ``supervisord`` may have failed with
the message ``Error: Another program is already listening
on a port that one of our HTTP servers is configured to use.``
if an HTTP request was made during restart. Patch by Julien Le Cléach.
- Fixed a unit test that failed only on Python 3.13. Only test code was
changed; no changes to ``supervisord`` itself. Patch by Colin Watson.
- On Python 3.8 and later, ``setuptools`` is no longer a runtime
dependency. Patch by Ofek Lev.
- On Python versions before 3.8, ``setuptools`` is still a runtime
dependency (for ``pkg_resources``) but it is no longer declared in
``setup.py`` as such. This is because adding a conditional dependency
with an environment marker (``setuptools; python_version < '3.8'``)
breaks installation in some scenarios, e.g. ``setup.py install`` or
older versions of ``pip``. Ensure that ``setuptools`` is installed
if using Python before 3.8.
4.2.5 (2022-12-23)
------------------
- Fixed a bug where the XML-RPC method ``supervisor.startProcess()`` would
return 500 Internal Server Error instead of an XML-RPC fault response
if the command could not be parsed. Patch by Julien Le Cléach.
- Fixed a bug on Python 2.7 where a ``UnicodeDecodeError`` may have occurred
when using the web interface. Patch by Vinay Sajip.
- Removed use of ``urllib.parse`` functions ``splithost``, ``splitport``, and
``splittype`` deprecated in Python 3.8.
- Removed use of ``asynchat`` and ``asyncore`` deprecated in Python 3.10.
- The return value of the XML-RPC method ``supervisor.getAllConfigInfo()``
now includes the ``directory``, ``uid``, and ``serverurl`` of the
program. Patch by Yellmean.
- If a subprocess exits with a unexpected exit code (one not listed in
``exitcodes=`` in a ``[program:x]`` section) then the exit will now be logged
at the ``WARN`` level instead of ``INFO``. Patch by Precy Lee.
- ``supervisorctl shutdown`` now shows an error message if an argument is
given.
- File descriptors are now closed using the faster ``os.closerange()`` instead
of calling ``os.close()`` in a loop. Patch by tyong920.
4.2.4 (2021-12-30)
------------------
- Fixed a bug where the ``--identifier`` command line argument was ignored.
It was broken since at least 3.0a7 (released in 2009) and probably earlier.
Patch by Julien Le Cléach.
4.2.3 (2021-12-27)
------------------
- Fixed a race condition where an ``rpcinterface`` extension that subscribed
to events would not see the correct process state if it accessed the
the ``state`` attribute on a ``Subprocess`` instance immediately in the
event callback. Patch by Chao Wang.
- Added the ``setuptools`` package to the list of dependencies in
``setup.py`` because it is a runtime dependency. Patch by Louis Sautier.
- The web interface will now return a 404 Not Found response if a log file
is missing. Previously, it would return 410 Gone. It was changed because
410 is intended to mean that the condition is likely to be permanent. A
log file missing is usually temporary, e.g. a process that was never started
will not have a log file but will have one as soon as it is started.
4.2.2 (2021-02-26)
------------------
- Fixed a bug where ``supervisord`` could crash if a subprocess exited
immediately before trying to kill it.
- Fixed a bug where the ``stdout_syslog`` and ``stderr_syslog`` options
of a ``[program:x]`` section could not be used unless file logging for
the same program had also been configured. The file and syslog options
can now be used independently. Patch by Scott Stroupe.
- Fixed a bug where the ``logfile`` option in the ``[supervisord]``
section would not log to syslog when the special filename of
``syslog`` was supplied, as is supported by all other log filename
options. Patch by Franck Cuny.
- Fixed a bug where environment variables defined in ``environment=``
in the ``[supervisord]`` section or a ``[program:x]`` section could
not be used in ``%(ENV_x)s`` expansions. Patch by MythRen.
- The ``supervisorctl signal`` command now allows a signal to be sent
when a process is in the ``STOPPING`` state. Patch by Mike Gould.
- ``supervisorctl`` and ``supervisord`` now print help when given ``-?``
in addition to the existing ``-h``/``--help``.
4.2.1 (2020-08-20)
------------------
- Fixed a bug on Python 3 where a network error could cause ``supervisord``
to crash with the error ``:can't concat str to bytes``.
Patch by Vinay Sajip.
- Fixed a bug where a test would fail on systems with glibc 2.3.1 because
the default value of SOMAXCONN changed.
4.2.0 (2020-04-30)
------------------
- When ``supervisord`` is run in the foreground, a new ``--silent`` option
suppresses the main log from being echoed to ``stdout`` as it normally
would. Patch by Trevor Foster.
- Parsing ``command=`` now supports a new expansion, ``%(numprocs)d``, that
expands to the value of ``numprocs=`` in the same section. Patch by
Santjago Corkez.
- Web UI buttons no longer use background images. Patch by Dmytro Karpovych.
- The Web UI now has a link to view ``tail -f stderr`` for a process in
addition to the existing ``tail -f stdout`` link. Based on a
patch by OuroborosCoding.
- The HTTP server will now send an ``X-Accel-Buffering: no`` header in
logtail responses to fix Nginx proxy buffering. Patch by Weizhao Li.
- When ``supervisord`` reaps an unknown PID, it will now log a description
of the ``waitpid`` status. Patch by Andrey Zelenchuk.
- Fixed a bug introduced in 4.0.3 where ``supervisorctl tail -f foo | grep bar``
would fail with the error ``NoneType object has no attribute 'lower'``. This
only occurred on Python 2.7 and only when piped. Patch by Slawa Pidgorny.
4.1.0 (2019-10-19)
------------------
- Fixed a bug on Python 3 only where logging to syslog did not work and
would log the exception ``TypeError: a bytes-like object is required, not 'str'``
to the main ``supervisord`` log file. Patch by Vinay Sajip and Josh Staley.
- Fixed a Python 3.8 compatibility issue caused by the removal of
``cgi.escape()``. Patch by Mattia Procopio.
- The ``meld3`` package is no longer a dependency. A version of ``meld3``
is now included within the ``supervisor`` package itself.
4.0.4 (2019-07-15)
------------------
- Fixed a bug where ``supervisorctl tail stdout`` would actually tail
``stderr``. Note that ``tail `` without the explicit ``stdout``
correctly tailed ``stdout``. The bug existed since 3.0a3 (released in
2007). Patch by Arseny Hofman.
- Improved the warning message added in 4.0.3 so it is now emitted for
both ``tail`` and ``tail -f``. Patch by Vinay Sajip.
- CVE-2019-12105. Documentation addition only, no code changes. This CVE
states that ``inet_http_server`` does not use authentication by default
(`details `_). Note that
``inet_http_server`` is not enabled by default, and is also not enabled
in the example configuration output by ``echo_supervisord_conf``. The
behavior of the ``inet_http_server`` options have been correctly documented,
and have not changed, since the feature was introduced in 2006. A new
`warning message `_
was added to the documentation.
4.0.3 (2019-05-22)
------------------
- Fixed an issue on Python 2 where running ``supervisorctl tail -f ``
would fail with the message
``Cannot connect, error: `` where it
may have worked on Supervisor 3.x. The issue was introduced in Supervisor
4.0.0 due to new bytes/strings conversions necessary to add Python 3 support.
For ``supervisorctl`` to correctly display logs with Unicode characters, the
terminal encoding specified by the environment must support it. If not, the
``UnicodeEncodeError`` may still occur on either Python 2 or 3. A new
warning message is now printed if a problematic terminal encoding is
detected. Patch by Vinay Sajip.
4.0.2 (2019-04-17)
------------------
- Fixed a bug where inline comments in the config file were not parsed
correctly such that the comments were included as part of the values.
This only occurred on Python 2, and only where the environment had an
extra ``configparser`` module installed. The bug was introduced in
Supervisor 4.0.0 because of Python 2/3 compatibility code that expected
a Python 2 environment to only have a ``ConfigParser`` module.
4.0.1 (2019-04-10)
------------------
- Fixed an issue on Python 3 where an ``OSError: [Errno 29] Illegal seek``
would occur if ``logfile`` in the ``[supervisord]`` section was set to
a special file like ``/dev/stdout`` that was not seekable, even if
``logfile_maxbytes = 0`` was set to disable rotation. The issue only
affected the main log and not child logs. Patch by Martin Falatic.
4.0.0 (2019-04-05)
------------------
- Support for Python 3 has been added. On Python 3, Supervisor requires
Python 3.4 or later. Many thanks to Vinay Sajip, Scott Maxwell, Palm Kevin,
Tres Seaver, Marc Abramowitz, Son Nguyen, Shane Hathaway, Evan Andrews,
and Ethan Hann who all made major contributions to the Python 3 porting
effort. Thanks also to all contributors who submitted issue reports and
patches towards this effort.
- Support for Python 2.4, 2.5, and 2.6 has been dropped. On Python 2,
Supervisor now requires Python 2.7.
- The ``supervisor`` package is no longer a namespace package.
- The behavior of the config file expansion ``%(here)s`` has changed. In
previous versions, a bug caused ``%(here)s`` to always expand to the
directory of the root config file. Now, when ``%(here)s`` is used inside
a file included via ``[include]``, it will expand to the directory of
that file. Thanks to Alex Eftimie and Zoltan Toth-Czifra for the patches.
- The default value for the config file setting ``exitcodes=``, the expected
exit codes of a program, has changed. In previous versions, it was ``0,2``.
This caused issues with Golang programs where ``panic()`` causes the exit
code to be ``2``. The default value for ``exitcodes`` is now ``0``.
- An undocumented feature where multiple ``supervisorctl`` commands could be
combined on a single line separated by semicolons has been removed.
- ``supervisorctl`` will now set its exit code to a non-zero value when an
error condition occurs. Previous versions did not set the exit code for
most error conditions so it was almost always 0. Patch by Luke Weber.
- Added new ``stdout_syslog`` and ``stderr_syslog`` options to the config
file. These are boolean options that indicate whether process output will
be sent to syslog. Supervisor can now log to both files and syslog at the
same time. Specifying a log filename of ``syslog`` is still supported
but deprecated. Patch by Jason R. Coombs.
3.4.0 (2019-04-05)
------------------
- FastCGI programs (``[fcgi-program:x]`` sections) can now be used in
groups (``[group:x]``). Patch by Florian Apolloner.
- Added a new ``socket_backlog`` option to the ``[fcgi-program:x]`` section
to set the listen(2) socket backlog. Patch by Nenad Merdanovic.
- Fixed a bug where ``SupervisorTransport`` (the XML-RPC transport used with
Unix domain sockets) did not close the connection when ``close()`` was
called on it. Patch by Jérome Perrin.
- Fixed a bug where ``supervisorctl start `` could hang for a long time
if the system clock rolled back. Patch by Joe LeVeque.
3.3.5 (2018-12-22)
------------------
- Fixed a race condition where ``supervisord`` would cancel a shutdown
already in progress if it received ``SIGHUP``. Now, ``supervisord`` will
ignore ``SIGHUP`` if shutdown is already in progress. Patch by Livanh.
- Fixed a bug where searching for a relative command ignored changes to
``PATH`` made in ``environment=``. Based on a patch by dongweiming.
- ``childutils.ProcessCommunicationsProtocol`` now does an explicit
``flush()`` after writing to ``stdout``.
- A more descriptive error message is now emitted if a name in the config
file contains a disallowed character. Patch by Rick van Hattem.
3.3.4 (2018-02-15)
------------------
- Fixed a bug where rereading the configuration would not detect changes to
eventlisteners. Patch by Michael Ihde.
- Fixed a bug where the warning ``Supervisord is running as root and it is
searching for its config file`` may have been incorrectly shown by
``supervisorctl`` if its executable name was changed.
- Fixed a bug where ``supervisord`` would continue starting up if the
``[supervisord]`` section of the config file specified ``user=`` but
``setuid()`` to that user failed. It will now exit immediately if it
cannot drop privileges.
- Fixed a bug in the web interface where redirect URLs did not have a slash
between the host and query string, which caused issues when proxying with
Nginx. Patch by Luke Weber.
- When ``supervisord`` successfully drops privileges during startup, it is now
logged at the ``INFO`` level instead of ``CRIT``.
- The HTTP server now returns a Content-Type header specifying UTF-8 encoding.
This may fix display issues in some browsers. Patch by Katenkka.
3.3.3 (2017-07-24)
------------------
- Fixed CVE-2017-11610. A vulnerability was found where an authenticated
client can send a malicious XML-RPC request to ``supervisord`` that will
run arbitrary shell commands on the server. The commands will be run as
the same user as ``supervisord``. Depending on how ``supervisord`` has been
configured, this may be root. See
https://github.com/Supervisor/supervisor/issues/964 for details.
3.3.2 (2017-06-03)
------------------
- Fixed a bug introduced in 3.3.0 where the ``supervisorctl reload`` command
would crash ``supervisord`` with the error ``OSError: [Errno 9] Bad file
descriptor`` if the ``kqueue`` poller was used. Patch by Jared Suttles.
- Fixed a bug introduced in 3.3.0 where ``supervisord`` could get stuck in a
polling loop after the web interface was used, causing high CPU usage.
Patch by Jared Suttles.
- Fixed a bug where if ``supervisord`` attempted to start but aborted due to
another running instance of ``supervisord`` with the same config, the
pidfile of the running instance would be deleted. Patch by coldnight.
- Fixed a bug where ``supervisorctl fg`` would swallow most XML-RPC faults.
``fg`` now prints the fault and exits.
- Parsing the config file will now fail with an error message if a process
or group name contains a forward slash character (``/``) since it would
break the URLs used by the web interface.
- ``supervisorctl reload`` now shows an error message if an argument is
given. Patch by Joel Krauska.
- ``supervisorctl`` commands ``avail``, ``reread``, and ``version`` now show
an error message if an argument is given.
3.3.1 (2016-08-02)
------------------
- Fixed an issue where ``supervisord`` could hang when responding to HTTP
requests (including ``supervisorctl`` commands) if the system time was set
back after ``supervisord`` was started.
- Zope ``trackrefs``, a debugging tool that was included in the ``tests``
directory but hadn't been used for years, has been removed.
3.3.0 (2016-05-14)
------------------
- ``supervisord`` will now use ``kqueue``, ``poll``, or ``select`` to monitor
its file descriptors, in that order, depending on what is available on the
system. Previous versions used ``select`` only and would crash with the error
``ValueError: filedescriptor out of range in select()`` when running a large
number of subprocesses (whatever number resulted in enough file descriptors
to exceed the fixed-size file descriptor table used by ``select``, which is
typically 1024). Patch by Igor Sobreira.
- ``/etc/supervisor/supervisord.conf`` has been added to the config file search
paths. Many versions of Supervisor packaged for Debian and Ubuntu have
included a patch that added this path. This difference was reported in a
number of tickets as a source of confusion and upgrade difficulties, so the
path has been added. Patch by Kelvin Wong.
- Glob patterns in the ``[include]`` section now support the
``host_node_name`` expansion. Patch by Paul Lockaby.
- Files included via the ``[include]`` section are now logged at the ``INFO``
level instead of ``WARN``. Patch by Daniel Hahler.
3.2.4 (2017-07-24)
------------------
- Backported from Supervisor 3.3.3: Fixed CVE-2017-11610. A vulnerability
was found where an authenticated client can send a malicious XML-RPC request
to ``supervisord`` that will run arbitrary shell commands on the server.
The commands will be run as the same user as ``supervisord``. Depending on
how ``supervisord`` has been configured, this may be root. See
https://github.com/Supervisor/supervisor/issues/964 for details.
3.2.3 (2016-03-19)
------------------
- 400 Bad Request is now returned if an XML-RPC request is received with
invalid body data. In previous versions, 500 Internal Server Error
was returned.
3.2.2 (2016-03-04)
------------------
- Parsing the config file will now fail with an error message if an
``inet_http_server`` or ``unix_http_server`` section contains a ``username=``
but no ``password=``. In previous versions, ``supervisord`` would start with
this invalid configuration but the HTTP server would always return a 500
Internal Server Error. Thanks to Chris Ergatides for reporting this issue.
3.2.1 (2016-02-06)
------------------
- Fixed a server exception ``OverflowError: int exceeds XML-RPC limits`` that
made ``supervisorctl status`` unusable if the system time was far into the
future. The XML-RPC API returns timestamps as XML-RPC integers, but
timestamps will exceed the maximum value of an XML-RPC integer in January
2038 ("Year 2038 Problem"). For now, timestamps exceeding the maximum
integer will be capped at the maximum to avoid the exception and retain
compatibility with existing API clients. In a future version of the API,
the return type for timestamps will be changed.
3.2.0 (2015-11-30)
------------------
- Files included via the ``[include]`` section are read in sorted order. In
past versions, the order was undefined. Patch by Ionel Cristian Mărieș.
- ``supervisorctl start`` and ``supervisorctl stop`` now complete more quickly
when handling many processes. Thanks to Chris McDonough for this patch.
See: https://github.com/Supervisor/supervisor/issues/131
- Environment variables are now expanded for all config file options.
Patch by Dexter Tad-y.
- Added ``signalProcess``, ``signalProcessGroup``, and ``signalAllProcesses``
XML-RPC methods to supervisor RPC interface. Thanks to Casey Callendrello,
Marc Abramowitz, and Moriyoshi Koizumi for the patches.
- Added ``signal`` command to supervisorctl. Thanks to Moriyoshi Koizumi and
Marc Abramowitz for the patches.
- Errors caused by bad values in a config file now show the config section
to make debugging easier. Patch by Marc Abramowitz.
- Setting ``redirect_stderr=true`` in an ``[eventlistener:x]`` section is now
disallowed because any messages written to ``stderr`` would interfere
with the eventlistener protocol on ``stdout``.
- Fixed a bug where spawning a process could cause ``supervisord`` to crash
if an ``IOError`` occurred while setting up logging. One way this could
happen is if a log filename was accidentally set to a directory instead
of a file. Thanks to Grzegorz Nosek for reporting this issue.
- Fixed a bug introduced in 3.1.0 where ``supervisord`` could crash when
attempting to display a resource limit error.
- Fixed a bug where ``supervisord`` could crash with the message
``Assertion failed for processname: RUNNING not in STARTING`` if a time
change caused the last start time of the process to be in the future.
Thanks to Róbert Nagy, Sergey Leschenko, and samhair for the patches.
- A warning is now logged if an eventlistener enters the UNKNOWN state,
which usually indicates a bug in the eventlistener. Thanks to Steve
Winton and detailyang for reporting issues that led to this change.
- Errors from the web interface are now logged at the ``ERROR`` level.
Previously, they were logged at the ``TRACE`` level and easily
missed. Thanks to Thomas Güttler for reporting this issue.
- Fixed ``DeprecationWarning: Parameters to load are deprecated. Call
.resolve and .require separately.`` on setuptools >= 11.3.
- If ``redirect_stderr=true`` and ``stderr_logfile=auto``, no stderr log
file will be created. In previous versions, an empty stderr log file
would be created. Thanks to Łukasz Kożuchowski for the initial patch.
- Fixed an issue in Medusa that would cause ``supervisorctl tail -f`` to
disconnect if many other ``supervisorctl`` commands were run in parallel.
Patch by Stefan Friesel.
3.1.4 (2017-07-24)
------------------
- Backported from Supervisor 3.3.3: Fixed CVE-2017-11610. A vulnerability
was found where an authenticated client can send a malicious XML-RPC request
to ``supervisord`` that will run arbitrary shell commands on the server.
The commands will be run as the same user as ``supervisord``. Depending on
how ``supervisord`` has been configured, this may be root. See
https://github.com/Supervisor/supervisor/issues/964 for details.
3.1.3 (2014-10-28)
------------------
- Fixed an XML-RPC bug where the ElementTree-based parser handled strings
like ``hello`` but not strings like
``hello``, which are valid in the XML-RPC spec. This
fixes compatibility with the Apache XML-RPC client for Java and
possibly other clients.
3.1.2 (2014-09-07)
------------------
- Fixed a bug where ``tail group:*`` in ``supervisorctl`` would show a 500
Internal Server Error rather than a BAD_NAME fault.
- Fixed a bug where the web interface would show a 500 Internal Server Error
instead of an error message for some process start faults.
- Removed medusa files not used by Supervisor.
3.1.1 (2014-08-11)
------------------
- Fixed a bug where ``supervisorctl tail -f name`` output would stop if log
rotation occurred while tailing.
- Prevent a crash when a greater number of file descriptors were attempted to
be opened than permitted by the environment when starting a bunch of
programs. Now, instead a spawn error is logged.
- Compute "channel delay" properly, fixing symptoms where a supervisorctl
start command would hang for a very long time when a process (or many
processes) are spewing to their stdout or stderr. See comments attached to
https://github.com/Supervisor/supervisor/pull/263 .
- Added ``docs/conf.py``, ``docs/Makefile``, and ``supervisor/scripts/*.py``
to the release package.
3.1.0 (2014-07-29)
------------------
- The output of the ``start``, ``stop``, ``restart``, and ``clear`` commands
in ``supervisorctl`` has been changed to be consistent with the ``status``
command. Previously, the ``status`` command would show a process like
``foo:foo_01`` but starting that process would show ``foo_01: started``
(note the group prefix ``foo:`` was missing). Now, starting the process
will show ``foo:foo_01: started``. Suggested by Chris Wood.
- The ``status`` command in ``supervisorctl`` now supports group name
syntax: ``status group:*``.
- The process column in the table output by the ``status`` command in
``supervisorctl`` now expands to fit the widest name.
- The ``update`` command in ``supervisorctl`` now accepts optional group
names. When group names are specified, only those groups will be
updated. Patch by Gary M. Josack.
- Tab completion in ``supervisorctl`` has been improved and now works for
more cases. Thanks to Mathieu Longtin and Marc Abramowitz for the patches.
- Attempting to start or stop a process group in ``supervisorctl`` with the
``group:*`` syntax will now show the same error message as the ``process``
syntax if the name does not exist. Previously, it would show a Python
exception. Patch by George Ang.
- Added new ``PROCESS_GROUP_ADDED`` and ``PROCESS_GROUP_REMOVED`` events.
These events are fired when process groups are added or removed from
Supervisor's runtime configuration when using the ``add`` and ``remove``
commands in ``supervisorctl``. Patch by Brent Tubbs.
- Stopping a process in the backoff state now changes it to the stopped
state. Previously, an attempt to stop a process in backoff would be
ignored. Patch by Pascal Varet.
- The ``directory`` option is now expanded separately for each process in
a homogeneous process group. This allows each process to have its own
working directory. Patch by Perttu Ranta-aho.
- Removed ``setuptools`` from the ``requires`` list in ``setup.py`` because
it caused installation issues on some systems.
- Fixed a bug in Medusa where the HTTP Basic authorizer would cause an
exception if the password contained a colon. Thanks to Thomas Güttler
for reporting this issue.
- Fixed an XML-RPC bug where calling supervisor.clearProcessLogs() with a
name like ``group:*`` would cause a 500 Internal Server Error rather than
returning a BAD_NAME fault.
- Fixed a hang that could occur in ``supervisord`` if log rotation is used
and an outside program deletes an active log file. Patch by Magnus Lycka.
- A warning is now logged if a glob pattern in an ``[include]`` section does
not match any files. Patch by Daniel Hahler.
3.0.1 (2017-07-24)
------------------
- Backported from Supervisor 3.3.3: Fixed CVE-2017-11610. A vulnerability
was found where an authenticated client can send a malicious XML-RPC request
to ``supervisord`` that will run arbitrary shell commands on the server.
The commands will be run as the same user as ``supervisord``. Depending on
how ``supervisord`` has been configured, this may be root. See
https://github.com/Supervisor/supervisor/issues/964 for details.
3.0 (2013-07-30)
----------------
- Parsing the config file will now fail with an error message if a process
or group name contains characters that are not compatible with the
eventlistener protocol.
- Fixed a bug where the ``tail -f`` command in ``supervisorctl`` would fail
if the combined length of the username and password was over 56 characters.
- Reading the config file now gives a separate error message when the config
file exists but can't be read. Previously, any error reading the file
would be reported as "could not find config file". Patch by Jens Rantil.
- Fixed an XML-RPC bug where array elements after the first would be ignored
when using the ElementTree-based XML parser. Patch by Zev Benjamin.
- Fixed the usage message output by ``supervisorctl`` to show the correct
default config file path. Patch by Alek Storm.
3.0b2 (2013-05-28)
------------------
- The behavior of the program option ``user`` has changed. In all previous
versions, if ``supervisord`` failed to switch to the user, a warning would
be sent to the stderr log but the child process would still be spawned.
This means that a mistake in the config file could result in a child
process being unintentionally spawned as root. Now, ``supervisord`` will
not spawn the child unless it was able to successfully switch to the user.
Thanks to Igor Partola for reporting this issue.
- If a user specified in the config file does not exist on the system,
``supervisord`` will now print an error and refuse to start.
- Reverted a change to logging introduced in 3.0b1 that was intended to allow
multiple processes to log to the same file with the rotating log handler.
The implementation caused supervisord to crash during reload and to leak
file handles. Also, since log rotation options are given on a per-program
basis, impossible configurations could be created (conflicting rotation
options for the same file). Given this and that supervisord now has syslog
support, it was decided to remove this feature. A warning was added to the
documentation that two processes may not log to the same file.
- Fixed a bug where parsing ``command=`` could cause supervisord to crash if
shlex.split() fails, such as a bad quoting. Patch by Scott Wilson.
- It is now possible to use ``supervisorctl`` on a machine with no
``supervisord.conf`` file by supplying the connection information in
command line options. Patch by Jens Rantil.
- Fixed a bug where supervisord would crash if the syslog handler was used
and supervisord received SIGUSR2 (log reopen request).
- Fixed an XML-RPC bug where calling supervisor.getProcessInfo() with a bad
name would cause a 500 Internal Server Error rather than the returning
a BAD_NAME fault.
- Added a favicon to the web interface. Patch by Caio Ariede.
- Fixed a test failure due to incorrect handling of daylight savings time
in the childutils tests. Patch by Ildar Hizbulin.
- Fixed a number of pyflakes warnings for unused variables, imports, and
dead code. Patch by Philippe Ombredanne.
3.0b1 (2012-09-10)
------------------
- Fixed a bug where parsing ``environment=`` did not verify that key/value
pairs were correctly separated. Patch by Martijn Pieters.
- Fixed a bug in the HTTP server code that could cause unnecessary delays
when sending large responses. Patch by Philip Zeyliger.
- When supervisord starts up as root, if the ``-c`` flag was not provided, a
warning is now emitted to the console. Rationale: supervisord looks in the
current working directory for a ``supervisord.conf`` file; someone might
trick the root user into starting supervisord while cd'ed into a directory
that has a rogue ``supervisord.conf``.
- A warning was added to the documentation about the security implications of
starting supervisord without the ``-c`` flag.
- Add a boolean program option ``stopasgroup``, defaulting to false.
When true, the flag causes supervisor to send the stop signal to the
whole process group. This is useful for programs, such as Flask in debug
mode, that do not propagate stop signals to their children, leaving them
orphaned.
- Python 2.3 is no longer supported. The last version that supported Python
2.3 is Supervisor 3.0a12.
- Removed the unused "supervisor_rpc" entry point from setup.py.
- Fixed a bug in the rotating log handler that would cause unexpected
results when two processes were set to log to the same file. Patch
by Whit Morriss.
- Fixed a bug in config file reloading where each reload could leak memory
because a list of warning messages would be appended but never cleared.
Patch by Philip Zeyliger.
- Added a new Syslog log handler. Thanks to Denis Bilenko, Nathan L. Smith,
and Jason R. Coombs, who each contributed to the patch.
- Put all change history into a single file (CHANGES.txt).
3.0a12 (2011-12-06)
-------------------
- Released to replace a broken 3.0a11 package where non-Python files were
not included in the package.
3.0a11 (2011-12-06)
-------------------
- Added a new file, ``PLUGINS.rst``, with a listing of third-party plugins
for Supervisor. Contributed by Jens Rantil.
- The ``pid`` command in supervisorctl can now be used to retrieve the PIDs
of child processes. See ``help pid``. Patch by Gregory Wisniewski.
- Added a new ``host_node_name`` expansion that will be expanded to the
value returned by Python's ``platform.node`` (see
http://docs.python.org/library/platform.html#platform.node).
Patch by Joseph Kondel.
- Fixed a bug in the web interface where pages over 64K would be truncated.
Thanks to Drew Perttula and Timothy Jones for reporting this.
- Renamed ``README.txt`` to ``README.rst`` so GitHub renders the file as
ReStructuredText.
- The XML-RPC server is now compatible with clients that do not send empty
when there are no parameters for the method call. Thanks to
Johannes Becker for reporting this.
- Fixed ``supervisorctl --help`` output to show the correct program name.
- The behavior of the configuration options ``minfds`` and ``minprocs`` has
changed. Previously, if a hard limit was less than ``minfds`` or
``minprocs``, supervisord would unconditionally abort with an error. Now,
supervisord will attempt to raise the hard limit. This may succeed if
supervisord is run as root, otherwise the error is printed as before.
Patch by Benoit Sigoure.
- Add a boolean program option ``killasgroup``, defaulting to false,
if true when resorting to send SIGKILL to stop/terminate the process
send it to its whole process group instead to take care of possible
children as well and not leave them behind. Patch by Samuele Pedroni.
- Environment variables may now be used in the configuration file
for options that support string expansion. Patch by Aleksey Sivokon.
- Fixed a race condition where supervisord might not act on a signal sent
to it. Thanks to Adar Dembo for reporting the issue and supplying the
initial patch.
- Updated the output of ``echo_supervisord_conf`` to fix typos and
improve comments. Thanks to Jens Rantil for noticing these.
- Fixed a possible 500 Server Error from the web interface. This was
observed when using Supervisor on a domain socket behind Nginx, where
Supervisor would raise an exception because REMOTE_ADDR was not set.
Patch by David Bennett.
3.0a10 (2011-03-30)
-------------------
- Fixed the stylesheet of the web interface so the footer line won't overlap
a long process list. Thanks to Derek DeVries for the patch.
- Allow rpc interface plugins to register new events types.
- Bug fix for FCGI sockets not getting cleaned up when the ``reload`` command
is issued from supervisorctl. Also, the default behavior has changed for
FCGI sockets. They are now closed whenever the number of running processes
in a group hits zero. Previously, the sockets were kept open unless a
group-level stop command was issued.
- Better error message when HTTP server cannot reverse-resolve a hostname to
an IP address. Previous behavior: show a socket error. Current behavior:
spit out a suggestion to stdout.
- Environment variables set via ``environment=`` value within
``[supervisord]`` section had no effect. Thanks to Wyatt Baldwin
for a patch.
- Fix bug where stopping process would cause process output that happened
after the stop request was issued to be lost. See
https://github.com/Supervisor/supervisor/issues/11.
- Moved 2.X change log entries into ``HISTORY.txt``.
- Converted ``CHANGES.txt`` and ``README.txt`` into proper ReStructuredText
and included them in the ``long_description`` in ``setup.py``.
- Added a tox.ini to the package (run via ``tox`` in the package dir). Tests
supervisor on multiple Python versions.
3.0a9 (2010-08-13)
------------------
- Use rich comparison methods rather than __cmp__ to sort process configs and
process group configs to better straddle Python versions. (thanks to
Jonathan Riboux for identifying the problem and supplying an initial
patch).
- Fixed test_supervisorctl.test_maintail_dashf test for Python 2.7. (thanks
to Jonathan Riboux for identifying the problem and supplying an initial
patch).
- Fixed the way that supervisor.datatypes.url computes a "good" URL
for compatibility with Python 2.7 and Python >= 2.6.5. URLs with
bogus "schemes://" will now be accepted as a version-straddling
compromise (before they were rejected before supervisor would
start). (thanks to Jonathan Riboux for identifying the problem
and supplying an initial patch).
- Add a ``-v`` / ``--version`` option to supervisord: Print the
supervisord version number out to stdout and exit. (Roger Hoover)
- Import iterparse from xml.etree when available (eg: Python 2.6). Patch
by Sidnei da Silva.
- Fixed the url to the supervisor-users mailing list. Patch by
Sidnei da Silva
- When parsing "environment=" in the config file, changes introduced in
3.0a8 prevented Supervisor from parsing some characters commonly
found in paths unless quoting was used as in this example::
environment=HOME='/home/auser'
Supervisor once again allows the above line to be written as::
environment=HOME=/home/auser
Alphanumeric characters, "_", "/", ".", "+", "-", "(", ")", and ":" can all
be used as a value without quoting. If any other characters are needed in
the value, please quote it as in the first example above. Thanks to Paul
Heideman for reporting this issue.
- Supervisor will now look for its config file in locations relative to the
executable path, allowing it to be used more easily in virtual
environments. If sys.argv[0] is ``/path/to/venv/bin/supervisorctl``,
supervisor will now look for it's config file in
``/path/to/venv/etc/supervisord.conf`` and
``/path/to/venv/supervisord.conf`` in addition to the other standard
locations. Patch by Chris Rossi.
3.0a8 (2010-01-20)
------------------
- Don't cleanup file descriptors on first supervisord invocation:
this is a lame workaround for Snow Leopard systems that use
libdispatch and are receiving "Illegal instruction" messages at
supervisord startup time. Restarting supervisord via
"supervisorctl restart" may still cause a crash on these systems.
- Got rid of Medusa hashbang headers in various files to ease RPM
packaging.
- Allow umask to be 000 (patch contributed by Rowan Nairn).
- Fixed a bug introduced in 3.0a7 where supervisorctl wouldn't ask
for a username/password combination properly from a
password-protected supervisord if it wasn't filled in within the
"[supervisorctl]" section username/password values. It now
properly asks for a username and password.
- Fixed a bug introduced in 3.0a7 where setup.py would not detect the
Python version correctly. Patch by Daniele Paolella.
- Fixed a bug introduced in 3.0a7 where parsing a string of key/value
pairs failed on Python 2.3 due to use of regular expression syntax
introduced in Python 2.4.
- Removed the test suite for the ``memmon`` console script, which was
moved to the Superlance package in 3.0a7.
- Added release dates to CHANGES.txt.
- Reloading the config for an fcgi process group did not close the fcgi
socket - now, the socket is closed whenever the group is stopped as a unit
(including during config update). However, if you stop all the processes
in a group individually, the socket will remain open to allow for graceful
restarts of FCGI daemons. (Roger Hoover)
- Rereading the config did not pick up changes to the socket parameter in a
fcgi-program section. (Roger Hoover)
- Made a more friendly exception message when a FCGI socket cannot be
created. (Roger Hoover)
- Fixed a bug where the --serverurl option of supervisorctl would not
accept a URL with a "unix" scheme. (Jason Kirtland)
- Running the tests now requires the "mock" package. This dependency has
been added to "tests_require" in setup.py. (Roger Hoover)
- Added support for setting the ownership and permissions for an FCGI socket.
This is done using new "socket_owner" and "socket_mode" options in an
[fcgi-program:x] section. See the manual for details. (Roger Hoover)
- Fixed a bug where the FCGI socket reference count was not getting
decremented on spawn error. (Roger Hoover)
- Fixed a Python 2.6 deprecation warning on use of the "sha" module.
- Updated ez_setup.py to one that knows about setuptools 0.6c11.
- Running "supervisorctl shutdown" no longer dumps a Python backtrace
when it can't connect to supervisord on the expected socket. Thanks
to Benjamin Smith for reporting this.
- Removed use of collections.deque in our bundled version of asynchat
because it broke compatibility with Python 2.3.
- The sample configuration output by "echo_supervisord_conf" now correctly
shows the default for "autorestart" as "unexpected". Thanks to
William Dode for noticing it showed the wrong value.
3.0a7 (2009-05-24)
------------------
- We now bundle our own patched version of Medusa contributed by Jason
Kirtland to allow Supervisor to run on Python 2.6. This was done
because Python 2.6 introduced backwards incompatible changes to
asyncore and asynchat in the stdlib.
- The console script ``memmon``, introduced in Supervisor 3.0a4, has
been moved to Superlance (http://pypi.python.org/pypi/superlance).
The Superlance package contains other useful monitoring tools designed
to run under Supervisor.
- Supervisorctl now correctly interprets all of the error codes that can
be returned when starting a process. Patch by Francesc Alted.
- New ``stdout_events_enabled`` and ``stderr_events_enabled`` config options
have been added to the ``[program:x]``, ``[fcgi-program:x]``, and
``[eventlistener:x]`` sections. These enable the emitting of new
PROCESS_LOG events for a program. If unspecified, the default is False.
If enabled for a subprocess, and data is received from the stdout or
stderr of the subprocess while not in the special capture mode used by
PROCESS_COMMUNICATION, an event will be emitted.
Event listeners can subscribe to either PROCESS_LOG_STDOUT or
PROCESS_LOG_STDERR individually, or PROCESS_LOG for both.
- Values for subprocess environment variables specified with environment=
in supervisord.conf can now be optionally quoted, allowing them to
contain commas. Patch by Tim Godfrey.
- Added a new event type, REMOTE_COMMUNICATION, that is emitted by a new
RPC method, supervisor.sendRemoteCommEvent().
- Patch for bug #268 (KeyError on ``here`` expansion for
stdout/stderr_logfile) from David E. Kindred.
- Add ``reread``, ``update``, and ``avail`` commands based on Anders
Quist's ``online_config_reload.diff`` patch. This patch extends
the "add" and "drop" commands with automagical behavior::
In supervisorctl:
supervisor> status
bar RUNNING pid 14864, uptime 18:03:42
baz RUNNING pid 23260, uptime 0:10:16
foo RUNNING pid 14866, uptime 18:03:42
gazonk RUNNING pid 23261, uptime 0:10:16
supervisor> avail
bar in use auto 999:999
baz in use auto 999:999
foo in use auto 999:999
gazonk in use auto 999:999
quux avail auto 999:999
Now we add this to our conf:
[group:zegroup]
programs=baz,gazonk
Then we reread conf:
supervisor> reread
baz: disappeared
gazonk: disappeared
quux: available
zegroup: available
supervisor> avail
bar in use auto 999:999
foo in use auto 999:999
quux avail auto 999:999
zegroup:baz avail auto 999:999
zegroup:gazonk avail auto 999:999
supervisor> status
bar RUNNING pid 14864, uptime 18:04:18
baz RUNNING pid 23260, uptime 0:10:52
foo RUNNING pid 14866, uptime 18:04:18
gazonk RUNNING pid 23261, uptime 0:10:52
The magic make-it-so command:
supervisor> update
baz: stopped
baz: removed process group
gazonk: stopped
gazonk: removed process group
zegroup: added process group
quux: added process group
supervisor> status
bar RUNNING pid 14864, uptime 18:04:43
foo RUNNING pid 14866, uptime 18:04:43
quux RUNNING pid 23561, uptime 0:00:02
zegroup:baz RUNNING pid 23559, uptime 0:00:02
zegroup:gazonk RUNNING pid 23560, uptime 0:00:02
supervisor> avail
bar in use auto 999:999
foo in use auto 999:999
quux in use auto 999:999
zegroup:baz in use auto 999:999
zegroup:gazonk in use auto 999:999
- Fix bug with symptom "KeyError: 'process_name'" when using a logfile name
including documented``process_name`` Python string expansions.
- Tab completions in the supervisorctl shell, and a foreground mode for
Supervisor, implemented as a part of GSoC. The supervisorctl program now
has a ``fg`` command, which makes it possible to supply inputs to a
process, and see its output/error stream in real time.
- Process config reloading implemented by Anders Quist. The
supervisorctl program now has the commands "add" and "drop".
"add " adds the process group implied by
in the config file. "drop " removes the process
group from the running configuration (it must already be stopped).
This makes it possible to add processes to and remove processes from
a running supervisord without restarting the supervisord process.
- Fixed a bug where opening the HTTP servers would fail silently
for socket errors other than errno.EADDRINUSE.
- Thanks to Dave Peticolas, using "reload" against a supervisord
that is running in the background no longer causes supervisord
to crash.
- Configuration options for logfiles now accept mixed case reserved
words (e.g. "AUTO" or "auto") for consistency with other options.
- childutils.eventdata was buggy, it could not deal with carriage returns
in data. See http://www.plope.com/software/collector/257. Thanks
to Ian Bicking.
- Per-process exitcodes= configuration now will not accept exit
codes that are not 8-bit unsigned integers (supervisord will not
start when one of the exit codes is outside the range of 0 - 255).
- Per-process ``directory`` value can now contain expandable values like
``%(here)s``. (See http://www.plope.com/software/collector/262).
- Accepted patch from Roger Hoover to allow for a new sort of
process group: "fcgi-program". Adding one of these to your
supervisord.conf allows you to control fastcgi programs. FastCGI
programs cannot belong to heterogenous groups.
The configuration for FastCGI programs is the same as regular programs
except an additional "socket" parameter. Substitution happens on the
socket parameter with the ``here`` and ``program_name`` variables::
[fcgi-program:fcgi_test]
;socket=tcp://localhost:8002
socket=unix:///path/to/fcgi/socket
- Supervisorctl now supports a plugin model for supervisorctl
commands.
- Added the ability to retrieve supervisord's own pid through
supervisor.getPID() on the XML-RPC interface or a new
"pid" command on supervisorctl.
3.0a6 (2008-04-07)
------------------
- The RotatingFileLogger had a race condition in its doRollover
method whereby a file might not actually exist despite a call to
os.path.exists on the line above a place where we try to remove
it. We catch the exception now and ignore the missing file.
3.0a5 (2008-03-13)
------------------
- Supervisorctl now supports persistent readline history. To
enable, add "history_file = " to the ``[supervisorctl]``
section in your supervisord.conf file.
- Multiple commands may now be issued on one supervisorctl command
line, e.g. "restart prog; tail -f prog". Separate commands with a
single semicolon; they will be executed in order as you would
expect.
3.0a4 (2008-01-30)
------------------
- 3.0a3 broke Python 2.3 backwards compatibility.
- On Debian Sarge, one user reported that a call to
options.mktempfile would fail with an "[Errno 9] Bad file
descriptor" at supervisord startup time. I was unable to
reproduce this, but we found a workaround that seemed to work for
him and it's included in this release. See
http://www.plope.com/software/collector/252 for more information.
Thanks to William Dode.
- The fault ``ALREADY_TERMINATED`` has been removed. It was only raised by
supervisor.sendProcessStdin(). That method now returns ``NOT_RUNNING``
for parity with the other methods. (Mike Naberezny)
- The fault TIMED_OUT has been removed. It was not used.
- Supervisor now depends on meld3 0.6.4, which does not compile its
C extensions by default, so there is no more need to faff around
with NO_MELD3_EXTENSION_MODULES during installation if you don't
have a C compiler or the Python development libraries on your
system.
- Instead of making a user root around for the sample.conf file,
provide a convenience command "echo_supervisord_conf", which he can
use to echo the sample.conf to his terminal (and redirect to a file
appropriately). This is a new user convenience (especially one who
has no Python experience).
- Added ``numprocs_start`` config option to ``[program:x]`` and
``[eventlistener:x]`` sections. This is an offset used to compute
the first integer that ``numprocs`` will begin to start from.
Contributed by Antonio Beamud Montero.
- Added capability for ``[include]`` config section to config format.
This section must contain a single key "files", which must name a
space-separated list of file globs that will be included in
supervisor's configuration. Contributed by Ian Bicking.
- Invoking the ``reload`` supervisorctl command could trigger a bug in
supervisord which caused it to crash. See
http://www.plope.com/software/collector/253 . Thanks to William Dode for
a bug report.
- The ``pidproxy`` script was made into a console script.
- The ``password`` value in both the ``[inet_http_server]`` and
``[unix_http_server]`` sections can now optionally be specified as a SHA
hexdigest instead of as cleartext. Values prefixed with ``{SHA}`` will be
considered SHA hex digests. To encrypt a password to a form suitable for
pasting into the configuration file using Python, do, e.g.::
>>> import sha
>>> '{SHA}' + sha.new('thepassword').hexdigest()
'{SHA}82ab876d1387bfafe46cc1c8a2ef074eae50cb1d'
- The subtypes of the events PROCESS_STATE_CHANGE (and
PROCESS_STATE_CHANGE itself) have been removed, replaced with a
simpler set of PROCESS_STATE subscribable event types.
The new event types are:
PROCESS_STATE_STOPPED
PROCESS_STATE_EXITED
PROCESS_STATE_STARTING
PROCESS_STATE_STOPPING
PROCESS_STATE_BACKOFF
PROCESS_STATE_FATAL
PROCESS_STATE_RUNNING
PROCESS_STATE_UNKNOWN
PROCESS_STATE # abstract
PROCESS_STATE_STARTING replaces:
PROCESS_STATE_CHANGE_STARTING_FROM_STOPPED
PROCESS_STATE_CHANGE_STARTING_FROM_BACKOFF
PROCESS_STATE_CHANGE_STARTING_FROM_EXITED
PROCESS_STATE_CHANGE_STARTING_FROM_FATAL
PROCESS_STATE_RUNNING replaces
PROCESS_STATE_CHANGE_RUNNING_FROM_STARTED
PROCESS_STATE_BACKOFF replaces
PROCESS_STATE_CHANGE_BACKOFF_FROM_STARTING
PROCESS_STATE_STOPPING replaces:
PROCESS_STATE_CHANGE_STOPPING_FROM_RUNNING
PROCESS_STATE_CHANGE_STOPPING_FROM_STARTING
PROCESS_STATE_EXITED replaces
PROCESS_STATE_CHANGE_EXITED_FROM_RUNNING
PROCESS_STATE_STOPPED replaces
PROCESS_STATE_CHANGE_STOPPED_FROM_STOPPING
PROCESS_STATE_FATAL replaces
PROCESS_STATE_CHANGE_FATAL_FROM_BACKOFF
PROCESS_STATE_UNKNOWN replaces PROCESS_STATE_CHANGE_TO_UNKNOWN
PROCESS_STATE replaces PROCESS_STATE_CHANGE
The PROCESS_STATE_CHANGE_EXITED_OR_STOPPED abstract event is gone.
All process state changes have at least "processname",
"groupname", and "from_state" (the name of the previous state) in
their serializations.
PROCESS_STATE_EXITED additionally has "expected" (1 or 0) and "pid"
(the process id) in its serialization.
PROCESS_STATE_RUNNING, PROCESS_STATE_STOPPING,
PROCESS_STATE_STOPPED additionally have "pid" in their
serializations.
PROCESS_STATE_STARTING and PROCESS_STATE_BACKOFF have "tries" in
their serialization (initially "0", bumped +1 each time a start
retry happens).
- Remove documentation from README.txt, point people to
http://supervisord.org/manual/ .
- The eventlistener request/response protocol has changed. OK/FAIL
must now be wrapped in a RESULT envelope so we can use it for more
specialized communications.
Previously, to signify success, an event listener would write the string
``OK\n`` to its stdout. To signify that the event was seen but couldn't
be handled by the listener and should be rebuffered, an event listener
would write the string ``FAIL\n`` to its stdout.
In the new protocol, the listener must write the string::
RESULT {resultlen}\n{result}
For example, to signify OK::
RESULT 2\nOK
To signify FAIL::
RESULT 4\nFAIL
See the scripts/sample_eventlistener.py script for an example.
- To provide a hook point for custom results returned from event
handlers (see above) the [eventlistener:x] configuration sections
now accept a "result_handler=" parameter,
e.g. "result_handler=supervisor.dispatchers:default_handler" (the
default) or "handler=mypackage:myhandler". The keys are pkgutil
"entry point" specifications (importable Python function names).
Result handlers must be callables which accept two arguments: one
named "event" which represents the event, and the other named
"result", which represents the listener's result. A result
handler either executes successfully or raises an exception. If
it raises a supervisor.dispatchers.RejectEvent exception, the
event will be rebuffered, and the eventhandler will be placed back
into the ACKNOWLEDGED state. If it raises any other exception,
the event handler will be placed in the UNKNOWN state. If it does
not raise any exception, the event is considered successfully
processed. A result handler's return value is ignored. Writing a
result handler is a "in case of emergency break glass" sort of
thing, it is not something to be used for arbitrary business code.
In particular, handlers *must not block* for any appreciable
amount of time.
The standard eventlistener result handler
(supervisor.dispatchers:default_handler) does nothing if it receives an
"OK" and will raise a supervisor.dispatchers.RejectEvent exception if it
receives any other value.
- Supervisord now emits TICK events, which happen every N seconds.
Three types of TICK events are available: TICK_5 (every five
seconds), TICK_60 (every minute), TICK_3600 (every hour). Event
listeners may subscribe to one of these types of events to perform
every-so-often processing. TICK events are subtypes of the EVENT
type.
- Get rid of OSX platform-specific memory monitor and replace with
memmon.py, which works on both Linux and Mac OS. This script is
now a console script named "memmon".
- Allow "web handler" (the handler which receives http requests from
browsers visiting the web UI of supervisor) to deal with POST requests.
- RPC interface methods stopProcess(), stopProcessGroup(), and
stopAllProcesses() now take an optional "wait" argument that defaults
to True for parity with the start methods.
3.0a3 (2007-10-02)
------------------
- Supervisorctl now reports a better error message when the main supervisor
XML-RPC namespace is not registered. Thanks to Mike Orr for reporting
this. (Mike Naberezny)
- Create ``scripts`` directory within supervisor package, move
``pidproxy.py`` there, and place sample event listener and comm event
programs within the directory.
- When an event notification is buffered (either because a listener rejected
it or because all listeners were busy when we attempted to send it
originally), we now rebuffer it in a way that will result in it being
retried earlier than it used to be.
- When a listener process exits (unexpectedly) before transitioning from the
BUSY state, rebuffer the event that was being processed.
- supervisorctl ``tail`` command now accepts a trailing specifier: ``stderr``
or ``stdout``, which respectively, allow a user to tail the stderr or
stdout of the named process. When this specifier is not provided, tail
defaults to stdout.
- supervisor ``clear`` command now clears both stderr and stdout logs for the
given process.
- When a process encounters a spawn error as a result of a failed execve or
when it cannot setuid to a given uid, it now puts this info into the
process' stderr log rather than its stdout log.
- The event listener protocol header now contains the ``server`` identifier,
the ``pool`` that the event emanated from, and the ``poolserial`` as well
as the values it previously contained (version, event name, serial, and
length). The server identifier is taken from the config file options value
``identifier``, the ``pool`` value is the name of the listener pool that
this event emanates from, and the ``poolserial`` is a serial number
assigned to the event local to the pool that is processing it.
- The event listener protocol header is now a sequence of key-value
pairs rather than a list of positional values. Previously, a
representative header looked like::
SUPERVISOR3.0 PROCESS_COMMUNICATION_STDOUT 30 22\n
Now it looks like::
ver:3.0 server:supervisor serial:21 ...
- Specific event payload serializations have changed. All event
types that deal with processes now include the pid of the process
that the event is describing. In event serialization "header"
values, we've removed the space between the header name and the
value and headers are now separated by a space instead of a line
feed. The names of keys in all event types have had underscores
removed.
- Abandon the use of the Python stdlib ``logging`` module for speed
and cleanliness purposes. We've rolled our own.
- Fix crash on start if AUTO logging is used with a max_bytes of
zero for a process.
- Improve process communication event performance.
- The process config parameters ``stdout_capturefile`` and
``stderr_capturefile`` are no longer valid. They have been replaced with
the ``stdout_capture_maxbytes`` and ``stderr_capture_maxbytes`` parameters,
which are meant to be suffix-multiplied integers. They both default to
zero. When they are zero, process communication event capturing is not
performed. When either is nonzero, the value represents the maximum number
of bytes that will be captured between process event start and end tags.
This change was to support the fact that we no longer keep capture data in
a separate file, we just use a FIFO in RAM to maintain capture info. For
users whom don't care about process communication events, or whom haven't
changed the defaults for ``stdout_capturefile`` or ``stderr_capturefile``,
they needn't do anything to their configurations to deal with this change.
- Log message levels have been normalized. In particular, process
stdin/stdout is now logged at ``debug`` level rather than at ``trace``
level (``trace`` level is now reserved for output useful typically for
debugging supervisor itself). See "Supervisor Log Levels" in the
documentation for more info.
- When an event is rebuffered (because all listeners are busy or a
listener rejected the event), the rebuffered event is now inserted
in the head of the listener event queue. This doesn't guarantee
event emission in natural ordering, because if a listener rejects
an event or dies while it's processing an event, it can take an
arbitrary amount of time for the event to be rebuffered, and other
events may be processed in the meantime. But if pool listeners
never reject an event or don't die while processing an event, this
guarantees that events will be emitted in the order that they were
received because if all listeners are busy, the rebuffered event
will be tried again "first" on the next go-around.
- Removed EVENT_BUFFER_OVERFLOW event type.
- The supervisorctl xmlrpc proxy can now communicate with
supervisord using a persistent HTTP connection.
- A new module "supervisor.childutils" was added. This module
provides utilities for Python scripts which act as children of
supervisord. Most notably, it contains an API method
"getRPCInterface" allows you to obtain an xmlrpclib ServerProxy
that is willing to communicate with the parent supervisor. It
also contains utility functions that allow for parsing of
supervisor event listener protocol headers. A pair of scripts
(loop_eventgen.py and loop_listener.py) were added to the script
directory that serve as examples about how to use the childutils
module.
- A new envvar is added to child process environments:
SUPERVISOR_SERVER_URL. This contains the server URL for the
supervisord running the child.
- An ``OK`` URL was added at ``/ok.html`` which just returns the string
``OK`` (can be used for up checks or speed checks via plain-old-HTTP).
- An additional command-line option ``--profile_options`` is accepted
by the supervisord script for developer use::
supervisord -n -c sample.conf --profile_options=cumulative,calls
The values are sort_stats options that can be passed to the
standard Python profiler's PStats sort_stats method.
When you exit supervisor, it will print Python profiling output to
stdout.
- If cElementTree is installed in the Python used to invoke
supervisor, an alternate (faster, by about 2X) XML parser will be
used to parse XML-RPC request bodies. cElementTree was added as
an "extras_require" option in setup.py.
- Added the ability to start, stop, and restart process groups to
supervisorctl. To start a group, use ``start groupname:*``. To start
multiple groups, use ``start groupname1:* groupname2:*``. Equivalent
commands work for "stop" and "restart". You can mix and match short
processnames, fully-specified group:process names, and groupsplats on the
same line for any of these commands.
- Added ``directory`` option to process config. If you set this
option, supervisor will chdir to this directory before executing
the child program (and thus it will be the child's cwd).
- Added ``umask`` option to process config. If you set this option,
supervisor will set the umask of the child program. (Thanks to
Ian Bicking for the suggestion).
- A pair of scripts ``osx_memmon_eventgen.py`` and `osx_memmon_listener.py``
have been added to the scripts directory. If they are used together as
described in their comments, processes which are consuming "too much"
memory will be restarted. The ``eventgen`` script only works on OSX (my
main development platform) but it should be trivially generalizable to
other operating systems.
- The long form ``--configuration`` (-c) command line option for
supervisord was broken. Reported by Mike Orr. (Mike Naberezny)
- New log level: BLAT (blather). We log all
supervisor-internal-related debugging info here. Thanks to Mike
Orr for the suggestion.
- We now allow supervisor to listen on both a UNIX domain socket and an inet
socket instead of making them mutually exclusive. As a result, the options
"http_port", "http_username", "http_password", "sockchmod" and "sockchown"
are no longer part of the ``[supervisord]`` section configuration. These
have been supplanted by two other sections: ``[unix_http_server]`` and
``[inet_http_server]``. You'll need to insert one or the other (depending
on whether you want to listen on a UNIX domain socket or a TCP socket
respectively) or both into your supervisord.conf file. These sections have
their own options (where applicable) for port, username, password, chmod,
and chown. See README.txt for more information about these sections.
- All supervisord command-line options related to "http_port",
"http_username", "http_password", "sockchmod" and "sockchown" have
been removed (see above point for rationale).
- The option that *used* to be ``sockchown`` within the ``[supervisord]``
section (and is now named ``chown`` within the ``[unix_http_server]``
section) used to accept a dot-separated user.group value. The separator
now must be a colon ":", e.g. "user:group". Unices allow for dots in
usernames, so this change is a bugfix. Thanks to Ian Bicking for the bug
report.
- If a '-c' option is not specified on the command line, both supervisord and
supervisorctl will search for one in the paths ``./supervisord.conf`` ,
``./etc/supervisord.conf`` (relative to the current working dir when
supervisord or supervisorctl is invoked) or in ``/etc/supervisord.conf``
(the old default path). These paths are searched in order, and supervisord
and supervisorctl will use the first one found. If none are found,
supervisor will fail to start.
- The Python string expression ``%(here)s`` (referring to the directory in
which the configuration file was found) can be used within the
following sections/options within the config file::
unix_http_server:file
supervisor:directory
supervisor:logfile
supervisor:pidfile
supervisor:childlogdir
supervisor:environment
program:environment
program:stdout_logfile
program:stderr_logfile
program:process_name
program:command
- The ``--environment`` aka ``-b`` option was removed from the list of
available command-line switches to supervisord (use "A=1 B=2
bin/supervisord" instead).
- If the socket filename (the tail-end of the unix:// URL) was
longer than 64 characters, supervisorctl would fail with an
encoding error at startup.
- The ``identifier`` command-line argument was not functional.
- Fixed http://www.plope.com/software/collector/215 (bad error
message in supervisorctl when program command not found on PATH).
- Some child processes may not have been shut down properly at
supervisor shutdown time.
- Move to ZPL-derived (but not ZPL) license available from
http://www.repoze.org/LICENSE.txt; it's slightly less restrictive
than the ZPL (no servicemark clause).
- Spurious errors related to unclosed files ("bad file descriptor",
typically) were evident at supervisord "reload" time (when using
the "reload" command from supervisorctl).
- We no longer bundle ez_setup to bootstrap setuptools installation.
3.0a2 (2007-08-24)
------------------
- Fixed the README.txt example for defining the supervisor RPC
interface in the configuration file. Thanks to Drew Perttula.
- Fixed a bug where process communication events would not have the
proper payload if the payload data was very short.
- when supervisord attempted to kill a process with SIGKILL after
the process was not killed within "stopwaitsecs" using a "normal"
kill signal, supervisord would crash with an improper
AssertionError. Thanks to Calvin Hendryx-Parker.
- On Linux, Supervisor would consume too much CPU in an effective
"busywait" between the time a subprocess exited and the time at
which supervisor was notified of its exit status. Thanks to Drew
Perttula.
- RPC interface behavior change: if the RPC method
"sendProcessStdin" is called against a process that has closed its
stdin file descriptor (e.g. it has done the equivalent of
"sys.stdin.close(); os.close(0)"), we return a NO_FILE fault
instead of accepting the data.
- Changed the semantics of the process configuration ``autorestart``
parameter with respect to processes which move between the RUNNING and
EXITED state. ``autorestart`` was previously a boolean. Now it's a
trinary, accepting one of ``false``, ``unexpected``, or ``true``. If it's
``false``, a process will never be automatically restarted from the EXITED
state. If it's ``unexpected``, a process that enters the EXITED state will
be automatically restarted if it exited with an exit code that was not
named in the process config's ``exitcodes`` list. If it's ``true``, a
process that enters the EXITED state will be automatically restarted
unconditionally. The default is now ``unexpected`` (it was previously
``true``). The readdition of this feature is a reversion of the behavior
change note in the changelog notes for 3.0a1 that asserted we never cared
about the process' exit status when determining whether to restart it or
not.
- setup.py develop (and presumably setup.py install) would fail under Python
2.3.3, because setuptools attempted to import ``splituser`` from urllib2,
and it didn't exist.
- It's now possible to use ``setup.py install`` and ``setup.py develop`` on
systems which do not have a C compiler if you set the environment variable
"NO_MELD3_EXTENSION_MODULES=1" in the shell in which you invoke these
commands (versions of meld3 > 0.6.1 respect this envvar and do not try to
compile optional C extensions when it's set).
- The test suite would fail on Python versions <= 2.3.3 because
the "assertTrue" and "assertFalse" methods of unittest.TestCase
didn't exist in those versions.
- The ``supervisorctl`` and ``supervisord`` wrapper scripts were disused in
favor of using setuptools' ``console_scripts`` entry point settings.
- Documentation files and the sample configuration file are put into
the generated supervisor egg's ``doc`` directory.
- Using the web interface would cause fairly dramatic memory
leakage. We now require a version of meld3 that does not appear
to leak memory from its C extensions (0.6.3).
3.0a1 (2007-08-16)
------------------
- Default config file comment documented 10 secs as default for ``startsecs``
value in process config, in reality it was 1 sec. Thanks to Christoph
Zwerschke.
- Make note of subprocess environment behavior in README.txt.
Thanks to Christoph Zwerschke.
- New "strip_ansi" config file option attempts to strip ANSI escape
sequences from logs for smaller/more readable logs (submitted by
Mike Naberezny).
- The XML-RPC method supervisor.getVersion() has been renamed for
clarity to supervisor.getAPIVersion(). The old name is aliased
for compatibility but is deprecated and will be removed in a
future version (Mike Naberezny).
- Improved web interface styling (Mike Naberezny, Derek DeVries)
- The XML-RPC method supervisor.startProcess() now checks that
the file exists and is executable (Mike Naberezny).
- Two environment variables, "SUPERVISOR_PROCESS_NAME" and
"SUPERVISOR_PROCESS_GROUP" are set in the environment of child
processes, representing the name of the process and group in
supervisor's configuration.
- Process state map change: a process may now move directly from the
STARTING state to the STOPPING state (as a result of a stop
request).
- Behavior change: if ``autorestart`` is true, even if a process exits with
an "expected" exit code, it will still be restarted. In the immediately
prior release of supervisor, this was true anyway, and no one complained,
so we're going to consider that the "officially correct" behavior from now
on.
- Supervisor now logs subprocess stdout and stderr independently.
The old program config keys "logfile", "logfile_backups" and
"logfile_maxbytes" are superseded by "stdout_logfile",
"stdout_logfile_backups", and "stdout_logfile_maxbytes". Added
keys include "stderr_logfile", "stderr_logfile_backups", and
"stderr_logfile_maxbytes". An additional "redirect_stderr" key is
used to cause program stderr output to be sent to its stdout
channel. The keys "log_stderr" and "log_stdout" have been
removed.
- ``[program:x]`` config file sections now represent "homogeneous process
groups" instead of single processes. A "numprocs" key in the section
represents the number of processes that are in the group. A "process_name"
key in the section allows composition of the each process' name within the
homogeneous group.
- A new kind of config file section, ``[group:x]`` now exists, allowing users
to group heterogeneous processes together into a process group that can be
controlled as a unit from a client.
- Supervisord now emits "events" at certain points in its normal
operation. These events include supervisor state change events,
process state change events, and "process communication events".
- A new kind of config file section ``[eventlistener:x]`` now exists. Each
section represents an "event listener pool", which is a special kind of
homogeneous process group. Each process in the pool is meant to receive
supervisor "events" via its stdin and perform some notification (e.g. send
a mail, log, make an http request, etc.)
- Supervisord can now capture data between special tokens in
subprocess stdout/stderr output and emit a "process communications
event" as a result.
- Supervisor's XML-RPC interface may be extended arbitrarily by programmers.
Additional top-level namespace XML-RPC interfaces can be added using the
``[rpcinterface:foo]`` declaration in the configuration file.
- New ``supervisor``-namespace XML-RPC methods have been added:
getAPIVersion (returns the XML-RPC API version, the older
"getVersion" is now deprecated), "startProcessGroup" (starts all
processes in a supervisor process group), "stopProcessGroup"
(stops all processes in a supervisor process group), and
"sendProcessStdin" (sends data to a process' stdin file
descriptor).
- ``supervisor``-namespace XML-RPC methods which previously accepted
ony a process name as "name" (startProcess, stopProcess,
getProcessInfo, readProcessLog, tailProcessLog, and
clearProcessLog) now accept a "name" which may contain both the
process name and the process group name in the form
``groupname:procname``. For backwards compatibility purposes,
"simple" names will also be accepted but will be expanded
internally (e.g. if "foo" is sent as a name, it will be expanded
to "foo:foo", representing the foo process within the foo process
group).
- 2.X versions of supervisorctl will work against supervisor 3.0
servers in a degraded fashion, but 3.X versions of supervisorctl
will not work at all against supervisor 2.X servers.
2.2b1 (2007-03-31)
------------------
- Individual program configuration sections can now specify an
environment.
- Added a 'version' command to supervisorctl. This returns the
version of the supervisor2 package which the remote supervisord
process is using.
2.1 (2007-03-17)
----------------
- When supervisord was invoked more than once, and its configuration
was set up to use a UNIX domain socket as the HTTP server, the
socket file would be erased in error. The symptom of this was
that a subsequent invocation of supervisorctl could not find the
socket file, so the process could not be controlled (it and all of
its subprocesses would need to be killed by hand).
- Close subprocess file descriptors properly when a subprocess exits
or otherwise dies. This should result in fewer "too many open
files to spawn foo" messages when supervisor is left up for long
periods of time.
- When a process was not killable with a "normal" signal at shutdown
time, too many "INFO: waiting for x to die" messages would be sent
to the log until we ended up killing the process with a SIGKILL.
Now a maximum of one every three seconds is sent up until SIGKILL
time. Thanks to Ian Bicking.
- Add an assertion: we never want to try to marshal None to XML-RPC
callers. Issue 223 in the collector from vgatto indicates that
somehow a supervisor XML-RPC method is returning None (which
should never happen), but I cannot identify how. Maybe the
assertion will give us more clues if it happens again.
- Supervisor would crash when run under Python 2.5 because the
xmlrpclib.Transport class in Python 2.5 changed in a
backward-incompatible way. Thanks to Eric Westra for the bug
report and a fix.
- Tests now pass under Python 2.5.
- Better supervisorctl reporting on stop requests that have a FAILED
status.
- Removed duplicated code (readLog/readMainLog), thanks to Mike
Naberezny.
- Added tailProcessLog command to the XML-RPC API. It provides a
more efficient way to tail logs than readProcessLog(). Use
readProcessLog() to read chunks and tailProcessLog() to tail.
(thanks to Mike Naberezny).
2.1b1 (2006-08-30)
------------------
- "supervisord -h" and "supervisorctl -h" did not work (traceback
instead of showing help view (thanks to Damjan from Macedonia for
the bug report).
- Processes which started successfully after failing to start
initially are no longer reported in BACKOFF state once they are
started successfully (thanks to Damjan from Macedonia for the bug
report).
- Add new 'maintail' command to supervisorctl shell, which allows
you to tail the 'main' supervisor log. This uses a new
readMainLog xmlrpc API.
- Various process-state-transition related changes, all internal.
README.txt updated with new state transition map.
- startProcess and startAllProcesses xmlrpc APIs changed: instead of
accepting a timeout integer, these accept a wait boolean (timeout
is implied by process' "startsecs" configuration). If wait is
False, do not wait for startsecs.
Known issues:
- Code does not match state transition map. Processes which are
configured as autorestarting which start "successfully" but
subsequently die after 'startsecs' go through the transitions
RUNNING -> BACKOFF -> STARTING instead of the correct transitions
RUNNING -> EXITED -> STARTING. This has no real negative effect,
but should be fixed for correctness.
2.0 (2006-08-30)
----------------
- pidfile written in daemon mode had incorrect pid.
- supervisorctl: tail (non -f) did not pass through proper error
messages when supplied by the server.
- Log signal name used to kill processes at debug level.
- supervisorctl "tail -f" didn't work with supervisorctl sections
configured with an absolute unix:// URL
- New "environment" config file option allows you to add environment
variable values to supervisord environment from config file.
2.0b1 (2006-07-12)
------------------
- Fundamental rewrite based on 1.0.7, use distutils (only) for
installation, use ConfigParser rather than ZConfig, use HTTP for
wire protocol, web interface, less lies in supervisorctl.
1.0.7 (2006-07-11)
------------------
- Don't log a waitpid error if the error value is "no children".
- Use select() against child file descriptor pipes and bump up select
timeout appropriately.
1.0.6 (2005-11-20)
------------------
- Various tweaks to make run more effectively on Mac OS X
(including fixing tests to run there, no more "error reading
from fd XXX" in logtail output, reduced disk/CPU usage as a
result of not writing to log file unnecessarily on Mac OS).
1.0.5 (2004-07-29)
------------------
- Short description: In previous releases, managed programs that
created voluminous stdout/stderr output could run more slowly
than usual when invoked under supervisor, now they do not.
Long description: The supervisord manages child output by
polling pipes related to child process stderr/stdout. Polling
operations are performed in the mainloop, which also performs a
'select' on the filedescriptor(s) related to client/server
operations. In prior releases, the select timeout was set to 2
seconds. This release changes the timeout to 1/10th of a second
in order to keep up with client stdout/stderr output.
Gory description: On Linux, at least, there is a pipe buffer
size fixed by the kernel of somewhere between 512 - 4096 bytes;
when a child process writes enough data to fill the pipe buffer,
it will block on further stdout/stderr output until supervisord
comes along and clears out the buffer by reading bytes from the
pipe within the mainloop. We now clear these buffers much more
quickly than we did before due to the increased frequency of
buffer reads in the mainloop; the timeout value of 1/10th of a
second seems to be fast enough to clear out the buffers of child
process pipes when managing programs on even a very fast system
while still enabling the supervisord process to be in a sleeping
state for most of the time.
1.0.4 or "Alpha 4" (2004-06-30)
-------------------------------
- Forgot to update version tag in configure.py, so the supervisor version
in a3 is listed as "1.0.1", where it should be "1.0.3". a4 will be
listed as "1.0.4'.
- Instead of preventing a process from starting if setuid() can't
be called (if supervisord is run as nonroot, for example), just log
the error and proceed.
1.0.3 or "Alpha 3" (2004-05-26)
-------------------------------
- The daemon could chew up a lot of CPU time trying to select()
on real files (I didn't know select() failed to block when a file
is at EOF). Fixed by polling instead of using select().
- Processes could "leak" and become zombies due to a bug in
reaping dead children.
- supervisord now defaults to daemonizing itself.
- 'daemon' config file option and -d/--daemon command-line option
removed from supervisord acceptable options. In place of these
options, we now have a 'nodaemon' config file option and a
-n/--nodaemon command-line option.
- logtail now works.
- pidproxy changed slightly to reap children synchronously.
- in alpha2 changelist, supervisord was reported to have a
"noauth" command-line option. This was not accurate. The way
to turn off auth on the server is to disinclude the "passwdfile"
config file option from the server config file. The client
however does indeed still have a noauth option, which prevents
it from ever attempting to send authentication credentials to
servers.
- ZPL license added for ZConfig to LICENSE.txt
1.0.2 or "Alpha 2" (Unreleased)
-------------------------------
- supervisorctl and supervisord no longer need to run on the same machine
due to the addition of internet socket support.
- supervisorctl and supervisord no longer share a common configuration
file format.
- supervisorctl now uses a persistent connection to supervisord
(as opposed to creating a fresh connection for each command).
- SRP (Secure Remote Password) authentication is now a supported form
of access control for supervisord. In supervisorctl interactive mode,
by default, users will be asked for credentials when attempting to
talk to a supervisord that requires SRP authentication.
- supervisord has a new command-line option and configuration file
option for specifying "noauth" mode, which signifies that it
should not require authentication from clients.
- supervisorctl has a new command-line option and configuration
option for specifying "noauth" mode, which signifies that it
should never attempt to send authentication info to servers.
- supervisorctl has new commands: open: opens a connection to a new
supervisord; close: closes the current connection.
- supervisorctl's "logtail" command now retrieves log data from
supervisord's log file remotely (as opposed to reading it
directly from a common filesystem). It also no longer emulates
"tail -f", it just returns lines of the server's log file.
- The supervisord/supervisorctl wire protocol now has protocol versioning
and is documented in "protocol.txt".
- "configfile" command-line override -C changed to -c
- top-level section name for supervisor schema changed to 'supervisord'
from 'supervisor'
- Added 'pidproxy' shim program.
Known issues in alpha 2:
- If supervisorctl loses a connection to a supervisord or if the
remote supervisord crashes or shuts down unexpectedly, it is
possible that any supervisorctl talking to it will "hang"
indefinitely waiting for data. Pressing Ctrl-C will allow you
to restart supervisorctl.
- Only one supervisorctl process may talk to a given supervisord
process at a time. If two supervisorctl processes attempt to talk
to the same supervisord process, one will "win" and the other will
be disconnected.
- Sometimes if a pidproxy is used to start a program, the pidproxy
program itself will "leak".
1.0.0 or "Alpha 1" (Unreleased)
-------------------------------
Initial release.
================================================
FILE: COPYRIGHT.txt
================================================
Supervisor is Copyright (c) 2006-2015 Agendaless Consulting and Contributors.
(http://www.agendaless.com), All Rights Reserved
medusa was (is?) Copyright (c) Sam Rushing.
http_client.py code Copyright (c) by Daniel Krech, http://eikeon.com/.
================================================
FILE: LICENSES.txt
================================================
Supervisor is licensed under the following license:
A copyright notice accompanies this license document that identifies
the copyright holders.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions in source code must retain the accompanying
copyright notice, this list of conditions, and the following
disclaimer.
2. Redistributions in binary form must reproduce the accompanying
copyright notice, this list of conditions, and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
3. Names of the copyright holders must not be used to endorse or
promote products derived from this software without prior
written permission from the copyright holders.
4. If any files are modified, you must cause the modified files to
carry prominent notices stating that you changed the files and
the date of any change.
Disclaimer
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND
ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
http_client.py code is based on code by Daniel Krech, which was
released under this license:
LICENSE AGREEMENT FOR RDFLIB 0.9.0 THROUGH 2.3.1
------------------------------------------------
Copyright (c) 2002-2005, Daniel Krech, http://eikeon.com/
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* Neither the name of Daniel Krech nor the names of its
contributors may be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Medusa, the asynchronous communications framework upon which
supervisor's server and client code is based, was created by Sam
Rushing:
Medusa was once distributed under a 'free for non-commercial use'
license, but in May of 2000 Sam Rushing changed the license to be
identical to the standard Python license at the time. The standard
Python license has always applied to the core components of Medusa,
this change just frees up the rest of the system, including the http
server, ftp server, utilities, etc. Medusa is therefore under the
following license:
==============================
Permission to use, copy, modify, and distribute this software and
its documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and
that both that copyright notice and this permission notice appear in
supporting documentation, and that the name of Sam Rushing not be
used in advertising or publicity pertaining to distribution of the
software without specific, written prior permission.
SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR
CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
==============================
================================================
FILE: MANIFEST.in
================================================
include CHANGES.rst
include COPYRIGHT.txt
include LICENSES.txt
include README.rst
include tox.ini
include supervisor/version.txt
include supervisor/skel/*.conf
recursive-include supervisor/tests/fixtures *.conf *.py
recursive-include supervisor/ui *.html *.css *.png *.gif
include docs/Makefile
recursive-include docs *.py *.rst *.css *.gif *.png
recursive-exclude docs/.build *
================================================
FILE: README.rst
================================================
Supervisor
==========
Supervisor is a client/server system that allows its users to
control a number of processes on UNIX-like operating systems.
Supported Platforms
-------------------
Supervisor has been tested and is known to run on Linux (Ubuntu), Mac OS X
(10.4, 10.5, 10.6), and Solaris (10 for Intel) and FreeBSD 6.1. It will
likely work fine on most UNIX systems.
Supervisor will not run at all under any version of Windows.
Supervisor is intended to work on Python 3 version 3.4 or later
and on Python 2 version 2.7.
Documentation
-------------
You can view the current Supervisor documentation online `in HTML format
`_ . This is where you should go for detailed
installation and configuration documentation.
Reporting Bugs and Viewing the Source Repository
------------------------------------------------
Please report bugs in the `GitHub issue tracker
`_.
You can view the source repository for supervisor via
`https://github.com/Supervisor/supervisor
`_.
Contributing
------------
We'll review contributions from the community in
`pull requests `_
on GitHub.
================================================
FILE: docs/.static/repoze.css
================================================
@import url('default.css');
body {
background-color: #006339;
}
div.document {
background-color: #dad3bd;
}
div.sphinxsidebar h3, h4, h5, a {
color: #127c56 !important;
}
div.related {
color: #dad3bd !important;
background-color: #00744a;
}
div.related a {
color: #dad3bd !important;
}
/* override the justify text align of the default */
div.body p {
text-align: left !important;
}
/* fix google chrome
tag renderings */
pre {
line-height: normal !important;
}
================================================
FILE: docs/Makefile
================================================
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d .build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html web pickle htmlhelp latex changes linkcheck
help:
@echo "Please use \`make ' where is one of"
@echo " html to make standalone HTML files"
@echo " pickle to make pickle files (usable by e.g. sphinx-web)"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " changes to make an overview over all changed/added/deprecated items"
@echo " linkcheck to check all external links for integrity"
clean:
-rm -rf .build/*
html:
mkdir -p .build/html .build/doctrees
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) .build/html
@echo
@echo "Build finished. The HTML pages are in .build/html."
pickle:
mkdir -p .build/pickle .build/doctrees
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) .build/pickle
@echo
@echo "Build finished; now you can process the pickle files or run"
@echo " sphinx-web .build/pickle"
@echo "to start the sphinx-web server."
web: pickle
htmlhelp:
mkdir -p .build/htmlhelp .build/doctrees
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) .build/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in .build/htmlhelp."
latex:
mkdir -p .build/latex .build/doctrees
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) .build/latex
@echo
@echo "Build finished; the LaTeX files are in .build/latex."
@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
"run these through (pdf)latex."
changes:
mkdir -p .build/changes .build/doctrees
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) .build/changes
@echo
@echo "The overview file is in .build/changes."
linkcheck:
mkdir -p .build/linkcheck .build/doctrees
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) .build/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in .build/linkcheck/output.txt."
================================================
FILE: docs/api.rst
================================================
.. _xml_rpc:
XML-RPC API Documentation
=========================
To use the XML-RPC interface, first make sure you have configured the interface
factory properly by setting the default factory. See :ref:`rpcinterface_factories`.
Then you can connect to supervisor's HTTP port
with any XML-RPC client library and run commands against it.
An example of doing this using Python 2's ``xmlrpclib`` client library
is as follows.
.. code-block:: python
import xmlrpclib
server = xmlrpclib.Server('http://localhost:9001/RPC2')
An example of doing this using Python 3's ``xmlrpc.client`` library
is as follows.
.. code-block:: python
from xmlrpc.client import ServerProxy
server = ServerProxy('http://localhost:9001/RPC2')
You may call methods against :program:`supervisord` and its
subprocesses by using the ``supervisor`` namespace. An example is
provided below.
.. code-block:: python
server.supervisor.getState()
You can get a list of methods supported by the
:program:`supervisord` XML-RPC interface by using the XML-RPC
``system.listMethods`` API:
.. code-block:: python
server.system.listMethods()
You can see help on a method by using the ``system.methodHelp`` API
against the method:
.. code-block:: python
server.system.methodHelp('supervisor.shutdown')
The :program:`supervisord` XML-RPC interface also supports the
`XML-RPC multicall API
`_.
You can extend :program:`supervisord` functionality with new XML-RPC
API methods by adding new top-level RPC interfaces as necessary.
See :ref:`rpcinterface_factories`.
.. note::
Any XML-RPC method call may result in a fault response. This includes errors caused
by the client such as bad arguments, and any errors that make :program:`supervisord`
unable to fulfill the request. Many XML-RPC client programs will raise an exception
when a fault response is encountered.
.. automodule:: supervisor.rpcinterface
Status and Control
------------------
.. autoclass:: SupervisorNamespaceRPCInterface
.. automethod:: getAPIVersion
This API is versioned separately from Supervisor itself. The API version
returned by ``getAPIVersion`` only changes when the API changes. Its purpose
is to help the client identify with which version of the Supervisor API it
is communicating.
When writing software that communicates with this API, it is highly
recommended that you first test the API version for compatibility before
making method calls.
.. note::
The ``getAPIVersion`` method replaces ``getVersion`` found in Supervisor
versions prior to 3.0a1. It is aliased for compatibility but getVersion()
is deprecated and support will be dropped from Supervisor in a future
version.
.. automethod:: getSupervisorVersion
.. automethod:: getIdentification
This method allows the client to identify with which Supervisor
instance it is communicating in the case of environments where
multiple Supervisors may be running.
The identification is a string that must be set in Supervisor’s
configuration file. This method simply returns that value back to the
client.
.. automethod:: getState
This is an internal value maintained by Supervisor that determines what
Supervisor believes to be its current operational state.
Some method calls can alter the current state of the Supervisor. For
example, calling the method supervisor.shutdown() while the station is
in the RUNNING state places the Supervisor in the SHUTDOWN state while
it is shutting down.
The supervisor.getState() method provides a means for the client to check
Supervisor's state, both for informational purposes and to ensure that the
methods it intends to call will be permitted.
The return value is a struct:
.. code-block:: python
{'statecode': 1,
'statename': 'RUNNING'}
The possible return values are:
+---------+----------+----------------------------------------------+
|statecode|statename |Description |
+=========+==========+==============================================+
| 2 |FATAL |Supervisor has experienced a serious error. |
+---------+----------+----------------------------------------------+
| 1 |RUNNING |Supervisor is working normally. |
+---------+----------+----------------------------------------------+
| 0 |RESTARTING|Supervisor is in the process of restarting. |
+---------+----------+----------------------------------------------+
| -1 |SHUTDOWN |Supervisor is in the process of shutting down.|
+---------+----------+----------------------------------------------+
The ``FATAL`` state reports unrecoverable errors, such as internal
errors inside Supervisor or system runaway conditions. Once set to
``FATAL``, the Supervisor can never return to any other state without
being restarted.
In the ``FATAL`` state, all future methods except
supervisor.shutdown() and supervisor.restart() will automatically fail
without being called and the fault ``FATAL_STATE`` will be raised.
In the ``SHUTDOWN`` or ``RESTARTING`` states, all method calls are
ignored and their possible return values are undefined.
.. automethod:: getPID
.. automethod:: readLog
It can either return the entire log, a number of characters from the
tail of the log, or a slice of the log specified by the offset and
length parameters:
+--------+---------+------------------------------------------------+
| Offset | Length | Behavior of ``readProcessLog`` |
+========+=========+================================================+
|Negative|Not Zero | Bad arguments. This will raise the fault |
| | | ``BAD_ARGUMENTS``. |
+--------+---------+------------------------------------------------+
|Negative|Zero | This will return the tail of the log, or offset|
| | | number of characters from the end of the log. |
| | | For example, if ``offset`` = -4 and ``length`` |
| | | = 0, then the last four characters will be |
| | | returned from the end of the log. |
+--------+---------+------------------------------------------------+
|Zero or |Negative | Bad arguments. This will raise the fault |
|Positive| | ``BAD_ARGUMENTS``. |
+--------+---------+------------------------------------------------+
|Zero or |Zero | All characters will be returned from the |
|Positive| | ``offset`` specified. |
+--------+---------+------------------------------------------------+
|Zero or |Positive | A number of characters length will be returned |
|Positive| | from the ``offset``. |
+--------+---------+------------------------------------------------+
If the log is empty and the entire log is requested, an empty string
is returned.
If either offset or length is out of range, the fault
``BAD_ARGUMENTS`` will be returned.
If the log cannot be read, this method will raise either the
``NO_FILE`` error if the file does not exist or the ``FAILED`` error
if any other problem was encountered.
.. note::
The readLog() method replaces readMainLog() found in Supervisor
versions prior to 2.1. It is aliased for compatibility but
readMainLog() is deprecated and support will be dropped from
Supervisor in a future version.
.. automethod:: clearLog
If the log cannot be cleared because the log file does not exist, the
fault ``NO_FILE`` will be raised. If the log cannot be cleared for any
other reason, the fault ``FAILED`` will be raised.
.. automethod:: shutdown
This method shuts down the Supervisor daemon. If any processes are running,
they are automatically killed without warning.
Unlike most other methods, if Supervisor is in the ``FATAL`` state,
this method will still function.
.. automethod:: restart
This method soft restarts the Supervisor daemon. If any processes are
running, they are automatically killed without warning. Note that the
actual UNIX process for Supervisor cannot restart; only Supervisor’s
main program loop. This has the effect of resetting the internal
states of Supervisor.
Unlike most other methods, if Supervisor is in the ``FATAL`` state,
this method will still function.
Process Control
---------------
.. autoclass:: SupervisorNamespaceRPCInterface
:noindex:
.. automethod:: getProcessInfo
The return value is a struct:
.. code-block:: python
{'name': 'process name',
'group': 'group name',
'description': 'pid 18806, uptime 0:03:12'
'start': 1200361776,
'stop': 0,
'now': 1200361812,
'state': 20,
'statename': 'RUNNING',
'spawnerr': '',
'exitstatus': 0,
'logfile': '/path/to/stdout-log', # deprecated, b/c only
'stdout_logfile': '/path/to/stdout-log',
'stderr_logfile': '/path/to/stderr-log',
'pid': 1}
.. describe:: name
Name of the process
.. describe:: group
Name of the process' group
.. describe:: description
If process state is running description's value is process_id
and uptime. Example "pid 18806, uptime 0:03:12 ".
If process state is stopped description's value is stop time.
Example:"Jun 5 03:16 PM ".
.. describe:: start
UNIX timestamp of when the process was started
.. describe:: stop
UNIX timestamp of when the process last ended, or 0 if the process
has never been stopped.
.. describe:: now
UNIX timestamp of the current time, which can be used to calculate
process up-time.
.. describe:: state
State code, see :ref:`process_states`.
.. describe:: statename
String description of `state`, see :ref:`process_states`.
.. describe:: logfile
Deprecated alias for ``stdout_logfile``. This is provided only
for compatibility with clients written for Supervisor 2.x and
may be removed in the future. Use ``stdout_logfile`` instead.
.. describe:: stdout_logfile
Absolute path and filename to the STDOUT logfile
.. describe:: stderr_logfile
Absolute path and filename to the STDERR logfile
.. describe:: spawnerr
Description of error that occurred during spawn, or empty string
if none.
.. describe:: exitstatus
Exit status (errorlevel) of process, or 0 if the process is still
running.
.. describe:: pid
UNIX process ID (PID) of the process, or 0 if the process is not
running.
.. automethod:: getAllProcessInfo
Each element contains a struct, and this struct contains the exact
same elements as the struct returned by ``getProcessInfo``. If the process
table is empty, an empty array is returned.
.. automethod:: getAllConfigInfo
.. automethod:: startProcess
.. automethod:: startAllProcesses
.. automethod:: startProcessGroup
.. automethod:: stopProcess
.. automethod:: stopProcessGroup
.. automethod:: stopAllProcesses
.. automethod:: signalProcess
.. automethod:: signalProcessGroup
.. automethod:: signalAllProcesses
.. automethod:: sendProcessStdin
.. automethod:: sendRemoteCommEvent
.. automethod:: reloadConfig
.. automethod:: addProcessGroup
.. automethod:: removeProcessGroup
Process Logging
---------------
.. autoclass:: SupervisorNamespaceRPCInterface
:noindex:
.. automethod:: readProcessStdoutLog
.. automethod:: readProcessStderrLog
.. automethod:: tailProcessStdoutLog
.. automethod:: tailProcessStderrLog
.. automethod:: clearProcessLogs
.. automethod:: clearAllProcessLogs
.. automodule:: supervisor.xmlrpc
System Methods
--------------
.. autoclass:: SystemNamespaceRPCInterface
.. automethod:: listMethods
.. automethod:: methodHelp
.. automethod:: methodSignature
.. automethod:: multicall
================================================
FILE: docs/conf.py
================================================
# -*- coding: utf-8 -*-
#
# Supervisor documentation build configuration file
#
# This file is execfile()d with the current directory set to its containing
# dir.
#
# The contents of this file are pickled, so don't put values in the
# namespace that aren't pickleable (module imports are okay, they're
# removed automatically).
#
# All configuration values have a default value; values that are commented
# out serve to show the default value.
import sys, os
from datetime import date
# If your extensions are in another directory, add it here. If the
# directory is relative to the documentation root, use os.path.abspath to
# make it absolute, like shown here.
#sys.path.append(os.path.abspath('some/directory'))
parent = os.path.dirname(os.path.dirname(__file__))
sys.path.append(os.path.abspath(parent))
version_txt = os.path.join(parent, 'supervisor/version.txt')
supervisor_version = open(version_txt).read().strip()
# General configuration
# ---------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc']
# Add any paths that contain templates here, relative to this directory.
templates_path = ['.templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The master toctree document.
master_doc = 'index'
# General substitutions.
project = 'Supervisor'
year = date.today().year
copyright = '2004-%d, Agendaless Consulting and Contributors' % year
# The default replacements for |version| and |release|, also used in various
# other places throughout the built documents.
#
# The short X.Y version.
version = supervisor_version
# The full version, including alpha/beta/rc tags.
release = version
# There are two options for replacing |today|: either, you set today to
# some non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
today_fmt = '%B %d, %Y'
# List of documents that shouldn't be included in the build.
#unused_docs = []
# List of directories, relative to source directories, that shouldn't be
# searched for source files.
#exclude_dirs = []
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
# Options for HTML output
# -----------------------
# The style sheet to use for HTML and HTML Help pages. A file of that name
# must exist either in Sphinx' static/ path, or in one of the custom paths
# given in html_static_path.
html_style = 'repoze.css'
# The name for this set of Sphinx documents. If None, it defaults to
# " v documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as
# html_title.
#html_short_title = None
# The name of an image file (within the static path) to place at the top of
# the sidebar.
html_logo = '.static/logo_hi.gif'
# The name of an image file (within the static path) to use as favicon of
# the docs. This file should be a Windows icon file (.ico) being 16x16 or
# 32x32 pixels large.
#html_favicon = None
# Add any paths that contain custom static files (such as style sheets)
# here, relative to this directory. They are copied after the builtin
# static files, so a file named "default.css" will overwrite the builtin
# "default.css".
html_static_path = ['.static']
# If not '', a 'Last updated on:' timestamp is inserted at every page
# bottom, using the given strftime format.
html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
#html_additional_pages = {}
# If false, no module index is generated.
#html_use_modindex = True
# If false, no index is generated.
#html_use_index = True
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, the reST sources are included in the HTML build as
# _sources/.
#html_copy_source = True
# If true, an OpenSearch description file will be output, and all pages
# will contain a tag referring to it. The value of this option must
# be the base URL from which the finished HTML is served.
#html_use_opensearch = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = ''
# Output file base name for HTML help builder.
htmlhelp_basename = 'supervisor'
# Options for LaTeX output
# ------------------------
# The paper size ('letter' or 'a4').
#latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
#latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, document class [howto/manual]).
latex_documents = [
('index', 'supervisor.tex', 'supervisor Documentation',
'Supervisor Developers', 'manual'),
]
# The name of an image file (relative to this directory) to place at the
# top of the title page.
latex_logo = '.static/logo_hi.gif'
# For "manual" documents, if this is true, then toplevel headings are
# parts, not chapters.
#latex_use_parts = False
# Additional stuff for the LaTeX preamble.
#latex_preamble = ''
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_use_modindex = True
================================================
FILE: docs/configuration.rst
================================================
Configuration File
==================
The Supervisor configuration file is conventionally named
:file:`supervisord.conf`. It is used by both :program:`supervisord`
and :program:`supervisorctl`. If either application is started
without the ``-c`` option (the option which is used to tell the
application the configuration filename explicitly), the application
will look for a file named :file:`supervisord.conf` within the
following locations, in the specified order. It will use the first
file it finds.
#. :file:`../etc/supervisord.conf` (Relative to the executable)
#. :file:`../supervisord.conf` (Relative to the executable)
#. :file:`$CWD/supervisord.conf`
#. :file:`$CWD/etc/supervisord.conf`
#. :file:`/etc/supervisord.conf`
#. :file:`/etc/supervisor/supervisord.conf` (since Supervisor 3.3.0)
.. note::
Many versions of Supervisor packaged for Debian and Ubuntu included a patch
that added ``/etc/supervisor/supervisord.conf`` to the search paths. The
first PyPI package of Supervisor to include it was Supervisor 3.3.0.
File Format
-----------
:file:`supervisord.conf` is a Windows-INI-style (Python ConfigParser)
file. It has sections (each denoted by a ``[header]``) and key / value
pairs within the sections. The sections and their allowable values
are described below.
Environment Variables
~~~~~~~~~~~~~~~~~~~~~
Environment variables that are present in the environment at the time that
:program:`supervisord` is started can be used in the configuration file
using the Python string expression syntax ``%(ENV_X)s``:
.. code-block:: ini
[program:example]
command=/usr/bin/example --loglevel=%(ENV_LOGLEVEL)s
In the example above, the expression ``%(ENV_LOGLEVEL)s`` would be expanded
to the value of the environment variable ``LOGLEVEL``.
.. note::
In Supervisor 3.2 and later, ``%(ENV_X)s`` expressions are supported in
all options. In prior versions, some options support them, but most
do not. See the documentation for each option below.
``[unix_http_server]`` Section Settings
---------------------------------------
The :file:`supervisord.conf` file contains a section named
``[unix_http_server]`` under which configuration parameters for an
HTTP server that listens on a UNIX domain socket should be inserted.
If the configuration file has no ``[unix_http_server]`` section, a
UNIX domain socket HTTP server will not be started. The allowable
configuration values are as follows.
``[unix_http_server]`` Section Values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``file``
A path to a UNIX domain socket on which supervisor will listen for
HTTP/XML-RPC requests. :program:`supervisorctl` uses XML-RPC to
communicate with :program:`supervisord` over this port. This option
can include the value ``%(here)s``, which expands to the directory
in which the :program:`supervisord` configuration file was found.
*Default*: None.
*Required*: No.
*Introduced*: 3.0
.. warning::
The example configuration output by :program:`echo_supervisord_conf` uses
``/tmp/supervisor.sock`` as the socket file. That path is an example only
and will likely need to be changed to a location more appropriate for your
system. Some systems periodically delete older files in ``/tmp``. If the
socket file is deleted, :program:`supervisorctl` will be unable to
connect to :program:`supervisord`.
``chmod``
Change the UNIX permission mode bits of the UNIX domain socket to
this value at startup.
*Default*: ``0700``
*Required*: No.
*Introduced*: 3.0
``chown``
Change the user and group of the socket file to this value. May be
a UNIX username (e.g. ``chrism``) or a UNIX username and group
separated by a colon (e.g. ``chrism:wheel``).
*Default*: Use the username and group of the user who starts supervisord.
*Required*: No.
*Introduced*: 3.0
``username``
The username required for authentication to this HTTP server.
*Default*: No username required.
*Required*: No.
*Introduced*: 3.0
``password``
The password required for authentication to this HTTP server. This
can be a cleartext password, or can be specified as a SHA-1 hash if
prefixed by the string ``{SHA}``. For example,
``{SHA}82ab876d1387bfafe46cc1c8a2ef074eae50cb1d`` is the SHA-stored
version of the password "thepassword".
Note that hashed password must be in hex format.
*Default*: No password required.
*Required*: No.
*Introduced*: 3.0
``[unix_http_server]`` Section Example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: ini
[unix_http_server]
file = /tmp/supervisor.sock
chmod = 0777
chown= nobody:nogroup
username = user
password = 123
``[inet_http_server]`` Section Settings
---------------------------------------
The :file:`supervisord.conf` file contains a section named
``[inet_http_server]`` under which configuration parameters for an
HTTP server that listens on a TCP (internet) socket should be
inserted. If the configuration file has no ``[inet_http_server]``
section, an inet HTTP server will not be started. The allowable
configuration values are as follows.
.. warning::
The inet HTTP server is not enabled by default. If you choose to enable it,
please read the following security warning. The inet HTTP server is intended
for use within a trusted environment only. It should only be bound to localhost
or only accessible from within an isolated, trusted network. The inet HTTP server
does not support any form of encryption. The inet HTTP server does not use
authentication by default (see the ``username=`` and ``password=`` options).
The inet HTTP server can be controlled remotely from :program:`supervisorctl`.
It also serves a web interface that allows subprocesses to be started or stopped,
and subprocess logs to be viewed. **Never expose the inet HTTP server to the
public internet.**
``[inet_http_server]`` Section Values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``port``
A TCP host:port value or (e.g. ``127.0.0.1:9001``) on which
supervisor will listen for HTTP/XML-RPC requests.
:program:`supervisorctl` will use XML-RPC to communicate with
:program:`supervisord` over this port. To listen on all interfaces
in the machine, use ``:9001`` or ``*:9001``. Please read the security
warning above.
*Default*: No default.
*Required*: Yes.
*Introduced*: 3.0
``username``
The username required for authentication to this HTTP server.
*Default*: No username required.
*Required*: No.
*Introduced*: 3.0
``password``
The password required for authentication to this HTTP server. This
can be a cleartext password, or can be specified as a SHA-1 hash if
prefixed by the string ``{SHA}``. For example,
``{SHA}82ab876d1387bfafe46cc1c8a2ef074eae50cb1d`` is the SHA-stored
version of the password "thepassword".
Note that hashed password must be in hex format.
*Default*: No password required.
*Required*: No.
*Introduced*: 3.0
``[inet_http_server]`` Section Example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: ini
[inet_http_server]
port = 127.0.0.1:9001
username = user
password = 123
``[supervisord]`` Section Settings
----------------------------------
The :file:`supervisord.conf` file contains a section named
``[supervisord]`` in which global settings related to the
:program:`supervisord` process should be inserted. These are as
follows.
``[supervisord]`` Section Values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``logfile``
The path to the activity log of the supervisord process. This
option can include the value ``%(here)s``, which expands to the
directory in which the supervisord configuration file was found.
.. note::
If ``logfile`` is set to a special file like ``/dev/stdout`` that is
not seekable, log rotation must be disabled by setting
``logfile_maxbytes = 0``.
*Default*: :file:`$CWD/supervisord.log`
*Required*: No.
*Introduced*: 3.0
``logfile_maxbytes``
The maximum number of bytes that may be consumed by the activity log
file before it is rotated (suffix multipliers like "KB", "MB", and
"GB" can be used in the value). Set this value to 0 to indicate an
unlimited log size.
*Default*: 50MB
*Required*: No.
*Introduced*: 3.0
``logfile_backups``
The number of backups to keep around resulting from activity log
file rotation. If set to 0, no backups will be kept.
*Default*: 10
*Required*: No.
*Introduced*: 3.0
``loglevel``
The logging level, dictating what is written to the supervisord
activity log. One of ``critical``, ``error``, ``warn``, ``info``,
``debug``, ``trace``, or ``blather``. Note that at log level
``debug``, the supervisord log file will record the stderr/stdout
output of its child processes and extended info about process
state changes, which is useful for debugging a process which isn't
starting properly. See also: :ref:`activity_log_levels`.
*Default*: info
*Required*: No.
*Introduced*: 3.0
``pidfile``
The location in which supervisord keeps its pid file. This option
can include the value ``%(here)s``, which expands to the directory
in which the supervisord configuration file was found.
*Default*: :file:`$CWD/supervisord.pid`
*Required*: No.
*Introduced*: 3.0
``umask``
The :term:`umask` of the supervisord process.
*Default*: ``022``
*Required*: No.
*Introduced*: 3.0
``nodaemon``
If true, supervisord will start in the foreground instead of
daemonizing.
*Default*: false
*Required*: No.
*Introduced*: 3.0
``silent``
If true and not daemonized, logs will not be directed to stdout.
*Default*: false
*Required*: No.
*Introduced*: 4.2.0
``minfds``
The minimum number of file descriptors that must be available before
supervisord will start successfully. A call to setrlimit will be made
to attempt to raise the soft and hard limits of the supervisord process to
satisfy ``minfds``. The hard limit may only be raised if supervisord
is run as root. supervisord uses file descriptors liberally, and will
enter a failure mode when one cannot be obtained from the OS, so it's
useful to be able to specify a minimum value to ensure it doesn't run out
of them during execution. These limits will be inherited by the managed
subprocesses. This option is particularly useful on Solaris,
which has a low per-process fd limit by default.
*Default*: 1024
*Required*: No.
*Introduced*: 3.0
``minprocs``
The minimum number of process descriptors that must be available
before supervisord will start successfully. A call to setrlimit will be
made to attempt to raise the soft and hard limits of the supervisord process
to satisfy ``minprocs``. The hard limit may only be raised if supervisord
is run as root. supervisord will enter a failure mode when the OS runs out
of process descriptors, so it's useful to ensure that enough process
descriptors are available upon :program:`supervisord` startup.
*Default*: 200
*Required*: No.
*Introduced*: 3.0
``nocleanup``
Prevent supervisord from clearing any existing ``AUTO``
child log files at startup time. Useful for debugging.
*Default*: false
*Required*: No.
*Introduced*: 3.0
``childlogdir``
The directory used for ``AUTO`` child log files. This option can
include the value ``%(here)s``, which expands to the directory in
which the :program:`supervisord` configuration file was found.
*Default*: value of Python's :func:`tempfile.gettempdir`
*Required*: No.
*Introduced*: 3.0
``user``
Instruct :program:`supervisord` to switch users to this UNIX user
account before doing any meaningful processing. The user can only
be switched if :program:`supervisord` is started as the root user.
*Default*: do not switch users
*Required*: No.
*Introduced*: 3.0
*Changed*: 3.3.4. If :program:`supervisord` can't switch to the
specified user, it will write an error message to ``stderr`` and
then exit immediately. In earlier versions, it would continue to
run but would log a message at the ``critical`` level.
``directory``
When :program:`supervisord` daemonizes, switch to this directory.
This option can include the value ``%(here)s``, which expands to the
directory in which the :program:`supervisord` configuration file was
found.
*Default*: do not cd
*Required*: No.
*Introduced*: 3.0
``strip_ansi``
Strip all ANSI escape sequences from child log files.
*Default*: false
*Required*: No.
*Introduced*: 3.0
``environment``
A list of key/value pairs in the form ``KEY="val",KEY2="val2"`` that
will be placed in the environment of all child processes. This does
not change the environment of :program:`supervisord` itself. This
option can include the value ``%(here)s``, which expands to the
directory in which the supervisord configuration file was found.
Values containing non-alphanumeric characters should be quoted
(e.g. ``KEY="val:123",KEY2="val,456"``). Otherwise, quoting the
values is optional but recommended. To escape percent characters,
simply use two. (e.g. ``URI="/first%%20name"``) **Note** that
subprocesses will inherit the environment variables of the shell
used to start :program:`supervisord` except for the ones overridden
here and within the program's ``environment`` option. See
:ref:`subprocess_environment`.
*Default*: no values
*Required*: No.
*Introduced*: 3.0
``identifier``
The identifier string for this supervisor process, used by the RPC
interface.
*Default*: supervisor
*Required*: No.
*Introduced*: 3.0
``[supervisord]`` Section Example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: ini
[supervisord]
logfile = /tmp/supervisord.log
logfile_maxbytes = 50MB
logfile_backups=10
loglevel = info
pidfile = /tmp/supervisord.pid
nodaemon = false
minfds = 1024
minprocs = 200
umask = 022
user = chrism
identifier = supervisor
directory = /tmp
nocleanup = true
childlogdir = /tmp
strip_ansi = false
environment = KEY1="value1",KEY2="value2"
``[supervisorctl]`` Section Settings
------------------------------------
The configuration file may contain settings for the
:program:`supervisorctl` interactive shell program. These options
are listed below.
``[supervisorctl]`` Section Values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``serverurl``
The URL that should be used to access the supervisord server,
e.g. ``http://localhost:9001``. For UNIX domain sockets, use
``unix:///absolute/path/to/file.sock``.
*Default*: ``http://localhost:9001``
*Required*: No.
*Introduced*: 3.0
``username``
The username to pass to the supervisord server for use in
authentication. This should be same as ``username`` from the
supervisord server configuration for the port or UNIX domain socket
you're attempting to access.
*Default*: No username
*Required*: No.
*Introduced*: 3.0
``password``
The password to pass to the supervisord server for use in
authentication. This should be the cleartext version of ``password``
from the supervisord server configuration for the port or UNIX
domain socket you're attempting to access. This value cannot be
passed as a SHA hash. Unlike other passwords specified in this
file, it must be provided in cleartext.
*Default*: No password
*Required*: No.
*Introduced*: 3.0
``prompt``
String used as supervisorctl prompt.
*Default*: ``supervisor``
*Required*: No.
*Introduced*: 3.0
``history_file``
A path to use as the ``readline`` persistent history file. If you
enable this feature by choosing a path, your supervisorctl commands
will be kept in the file, and you can use readline (e.g. arrow-up)
to invoke commands you performed in your last supervisorctl session.
*Default*: No file
*Required*: No.
*Introduced*: 3.0a5
``[supervisorctl]`` Section Example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: ini
[supervisorctl]
serverurl = unix:///tmp/supervisor.sock
username = chris
password = 123
prompt = mysupervisor
.. _programx_section:
``[program:x]`` Section Settings
--------------------------------
The configuration file must contain one or more ``program`` sections
in order for supervisord to know which programs it should start and
control. The header value is composite value. It is the word
"program", followed directly by a colon, then the program name. A
header value of ``[program:foo]`` describes a program with the name of
"foo". The name is used within client applications that control the
processes that are created as a result of this configuration. It is
an error to create a ``program`` section that does not have a name.
The name must not include a colon character or a bracket character.
The value of the name is used as the value for the
``%(program_name)s`` string expression expansion within other values
where specified.
.. note::
A ``[program:x]`` section actually represents a "homogeneous
process group" to supervisor (as of 3.0). The members of the group
are defined by the combination of the ``numprocs`` and
``process_name`` parameters in the configuration. By default, if
numprocs and process_name are left unchanged from their defaults,
the group represented by ``[program:x]`` will be named ``x`` and
will have a single process named ``x`` in it. This provides a
modicum of backwards compatibility with older supervisor releases,
which did not treat program sections as homogeneous process group
definitions.
But for instance, if you have a ``[program:foo]`` section with a
``numprocs`` of 3 and a ``process_name`` expression of
``%(program_name)s_%(process_num)02d``, the "foo" group will
contain three processes, named ``foo_00``, ``foo_01``, and
``foo_02``. This makes it possible to start a number of very
similar processes using a single ``[program:x]`` section. All
logfile names, all environment strings, and the command of programs
can also contain similar Python string expressions, to pass
slightly different parameters to each process.
``[program:x]`` Section Values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``command``
The command that will be run when this program is started. The
command can be either absolute (e.g. ``/path/to/programname``) or
relative (e.g. ``programname``). If it is relative, the
supervisord's environment ``$PATH`` will be searched for the
executable. Programs can accept arguments, e.g. ``/path/to/program
foo bar``. The command line can use double quotes to group
arguments with spaces in them to pass to the program,
e.g. ``/path/to/program/name -p "foo bar"``. Note that the value of
``command`` may include Python string expressions,
e.g. ``/path/to/programname --port=80%(process_num)02d`` might
expand to ``/path/to/programname --port=8000`` at runtime. String
expressions are evaluated against a dictionary containing the keys
``group_name``, ``host_node_name``, ``program_name``, ``process_num``,
``numprocs``, ``here`` (the directory of the supervisord config file),
and all supervisord's environment variables prefixed with ``ENV_``.
Controlled programs should themselves not be daemons, as supervisord
assumes it is responsible for daemonizing its subprocesses (see
:ref:`nondaemonizing_of_subprocesses`).
.. note::
The command will be truncated if it looks like a config file comment,
e.g. ``command=bash -c 'foo ; bar'`` will be truncated to
``command=bash -c 'foo``. Quoting will not prevent this behavior,
since the configuration file reader does not parse the command like
a shell would.
*Default*: No default.
*Required*: Yes.
*Introduced*: 3.0
*Changed*: 4.2.0. Added support for the ``numprocs`` expansion.
``process_name``
A Python string expression that is used to compose the supervisor
process name for this process. You usually don't need to worry
about setting this unless you change ``numprocs``. The string
expression is evaluated against a dictionary that includes
``group_name``, ``host_node_name``, ``process_num``, ``program_name``,
and ``here`` (the directory of the supervisord config file).
*Default*: ``%(program_name)s``
*Required*: No.
*Introduced*: 3.0
``numprocs``
Supervisor will start as many instances of this program as named by
numprocs. Note that if numprocs > 1, the ``process_name``
expression must include ``%(process_num)s`` (or any other
valid Python string expression that includes ``process_num``) within
it.
*Default*: 1
*Required*: No.
*Introduced*: 3.0
``numprocs_start``
An integer offset that is used to compute the number at which
``process_num`` starts.
*Default*: 0
*Required*: No.
*Introduced*: 3.0
``priority``
The relative priority of the program in the start and shutdown
ordering. Lower priorities indicate programs that start first and
shut down last at startup and when aggregate commands are used in
various clients (e.g. "start all"/"stop all"). Higher priorities
indicate programs that start last and shut down first.
*Default*: 999
*Required*: No.
*Introduced*: 3.0
``autostart``
If true, this program will start automatically when supervisord is
started.
*Default*: true
*Required*: No.
*Introduced*: 3.0
``startsecs``
The total number of seconds which the program needs to stay running
after a startup to consider the start successful (moving the process
from the ``STARTING`` state to the ``RUNNING`` state). Set to ``0``
to indicate that the program needn't stay running for any particular
amount of time.
.. note::
Even if a process exits with an "expected" exit code (see
``exitcodes``), the start will still be considered a failure
if the process exits quicker than ``startsecs``.
*Default*: 1
*Required*: No.
*Introduced*: 3.0
``startretries``
The number of serial failure attempts that :program:`supervisord`
will allow when attempting to start the program before giving up and
putting the process into an ``FATAL`` state.
.. note::
After each failed restart, process will be put in ``BACKOFF`` state
and each retry attempt will take increasingly more time.
See :ref:`process_states` for explanation of the ``FATAL`` and
``BACKOFF`` states.
*Default*: 3
*Required*: No.
*Introduced*: 3.0
``autorestart``
Specifies if :program:`supervisord` should automatically restart a
process if it exits when it is in the ``RUNNING`` state. May be
one of ``false``, ``unexpected``, or ``true``. If ``false``, the
process will not be autorestarted. If ``unexpected``, the process
will be restarted when the program exits with an exit code that is
not one of the exit codes associated with this process' configuration
(see ``exitcodes``). If ``true``, the process will be unconditionally
restarted when it exits, without regard to its exit code.
.. note::
``autorestart`` controls whether :program:`supervisord` will
autorestart a program if it exits after it has successfully started
up (the process is in the ``RUNNING`` state).
:program:`supervisord` has a different restart mechanism for when the
process is starting up (the process is in the ``STARTING`` state).
Retries during process startup are controlled by ``startsecs``
and ``startretries``.
*Default*: unexpected
*Required*: No.
*Introduced*: 3.0
``exitcodes``
The list of "expected" exit codes for this program used with ``autorestart``.
If the ``autorestart`` parameter is set to ``unexpected``, and the process
exits in any other way than as a result of a supervisor stop
request, :program:`supervisord` will restart the process if it exits
with an exit code that is not defined in this list.
*Default*: 0
*Required*: No.
*Introduced*: 3.0
.. note::
In Supervisor versions prior to 4.0, the default was ``0,2``. In
Supervisor 4.0, the default was changed to ``0``.
``stopsignal``
The signal used to kill the program when a stop is requested. This can be
specified using the signal's name or its number. It is normally one of:
``TERM``, ``HUP``, ``INT``, ``QUIT``, ``KILL``, ``USR1``, or ``USR2``.
*Default*: TERM
*Required*: No.
*Introduced*: 3.0
``stopwaitsecs``
The number of seconds to wait for the OS to return a SIGCHLD to
:program:`supervisord` after the program has been sent a stopsignal.
If this number of seconds elapses before :program:`supervisord`
receives a SIGCHLD from the process, :program:`supervisord` will
attempt to kill it with a final SIGKILL.
*Default*: 10
*Required*: No.
*Introduced*: 3.0
``stopasgroup``
If true, the flag causes supervisor to send the stop signal to the
whole process group and implies ``killasgroup`` is true. This is useful
for programs, such as Flask in debug mode, that do not propagate
stop signals to their children, leaving them orphaned.
*Default*: false
*Required*: No.
*Introduced*: 3.0b1
``killasgroup``
If true, when resorting to send SIGKILL to the program to terminate
it send it to its whole process group instead, taking care of its
children as well, useful e.g with Python programs using
:mod:`multiprocessing`.
*Default*: false
*Required*: No.
*Introduced*: 3.0a11
``user``
Instruct :program:`supervisord` to use this UNIX user account as the
account which runs the program. The user can only be switched if
:program:`supervisord` is run as the root user. If :program:`supervisord`
can't switch to the specified user, the program will not be started.
.. note::
The user will be changed using ``setuid`` only. This does not start
a login shell and does not change environment variables like
``USER`` or ``HOME``. See :ref:`subprocess_environment` for details.
*Default*: Do not switch users
*Required*: No.
*Introduced*: 3.0
``redirect_stderr``
If true, cause the process' stderr output to be sent back to
:program:`supervisord` on its stdout file descriptor (in UNIX shell
terms, this is the equivalent of executing ``/the/program 2>&1``).
.. note::
Do not set ``redirect_stderr=true`` in an ``[eventlistener:x]`` section.
Eventlisteners use ``stdout`` and ``stdin`` to communicate with
``supervisord``. If ``stderr`` is redirected, output from
``stderr`` will interfere with the eventlistener protocol.
*Default*: false
*Required*: No.
*Introduced*: 3.0, replaces 2.0's ``log_stdout`` and ``log_stderr``
``stdout_logfile``
Put process stdout output in this file (and if redirect_stderr is
true, also place stderr output in this file). If ``stdout_logfile``
is unset or set to ``AUTO``, supervisor will automatically choose a
file location. If this is set to ``NONE``, supervisord will create
no log file. ``AUTO`` log files and their backups will be deleted
when :program:`supervisord` restarts. The ``stdout_logfile`` value
can contain Python string expressions that will evaluated against a
dictionary that contains the keys ``group_name``, ``host_node_name``,
``process_num``, ``program_name``, and ``here`` (the directory of the
supervisord config file).
.. note::
It is not possible for two processes to share a single log file
(``stdout_logfile``) when rotation (``stdout_logfile_maxbytes``)
is enabled. This will result in the file being corrupted.
.. note::
If ``stdout_logfile`` is set to a special file like ``/dev/stdout``
that is not seekable, log rotation must be disabled by setting
``stdout_logfile_maxbytes = 0``.
*Default*: ``AUTO``
*Required*: No.
*Introduced*: 3.0, replaces 2.0's ``logfile``
``stdout_logfile_maxbytes``
The maximum number of bytes that may be consumed by
``stdout_logfile`` before it is rotated (suffix multipliers like
"KB", "MB", and "GB" can be used in the value). Set this value to 0
to indicate an unlimited log size.
*Default*: 50MB
*Required*: No.
*Introduced*: 3.0, replaces 2.0's ``logfile_maxbytes``
``stdout_logfile_backups``
The number of ``stdout_logfile`` backups to keep around resulting
from process stdout log file rotation. If set to 0, no backups
will be kept.
*Default*: 10
*Required*: No.
*Introduced*: 3.0, replaces 2.0's ``logfile_backups``
``stdout_capture_maxbytes``
Max number of bytes written to capture FIFO when process is in
"stdout capture mode" (see :ref:`capture_mode`). Should be an
integer (suffix multipliers like "KB", "MB" and "GB" can used in the
value). If this value is 0, process capture mode will be off.
*Default*: 0
*Required*: No.
*Introduced*: 3.0
``stdout_events_enabled``
If true, PROCESS_LOG_STDOUT events will be emitted when the process
writes to its stdout file descriptor. The events will only be
emitted if the file descriptor is not in capture mode at the time
the data is received (see :ref:`capture_mode`).
*Default*: 0
*Required*: No.
*Introduced*: 3.0a7
``stdout_syslog``
If true, stdout will be directed to syslog along with the process name.
*Default*: False
*Required*: No.
*Introduced*: 4.0.0
``stderr_logfile``
Put process stderr output in this file unless ``redirect_stderr`` is
true. Accepts the same value types as ``stdout_logfile`` and may
contain the same Python string expressions.
.. note::
It is not possible for two processes to share a single log file
(``stderr_logfile``) when rotation (``stderr_logfile_maxbytes``)
is enabled. This will result in the file being corrupted.
.. note::
If ``stderr_logfile`` is set to a special file like ``/dev/stderr``
that is not seekable, log rotation must be disabled by setting
``stderr_logfile_maxbytes = 0``.
*Default*: ``AUTO``
*Required*: No.
*Introduced*: 3.0
``stderr_logfile_maxbytes``
The maximum number of bytes before logfile rotation for
``stderr_logfile``. Accepts the same value types as
``stdout_logfile_maxbytes``.
*Default*: 50MB
*Required*: No.
*Introduced*: 3.0
``stderr_logfile_backups``
The number of backups to keep around resulting from process stderr
log file rotation. If set to 0, no backups will be kept.
*Default*: 10
*Required*: No.
*Introduced*: 3.0
``stderr_capture_maxbytes``
Max number of bytes written to capture FIFO when process is in
"stderr capture mode" (see :ref:`capture_mode`). Should be an
integer (suffix multipliers like "KB", "MB" and "GB" can used in the
value). If this value is 0, process capture mode will be off.
*Default*: 0
*Required*: No.
*Introduced*: 3.0
``stderr_events_enabled``
If true, PROCESS_LOG_STDERR events will be emitted when the process
writes to its stderr file descriptor. The events will only be
emitted if the file descriptor is not in capture mode at the time
the data is received (see :ref:`capture_mode`).
*Default*: false
*Required*: No.
*Introduced*: 3.0a7
``stderr_syslog``
If true, stderr will be directed to syslog along with the process name.
*Default*: False
*Required*: No.
*Introduced*: 4.0.0
``environment``
A list of key/value pairs in the form ``KEY="val",KEY2="val2"`` that
will be placed in the child process' environment. The environment
string may contain Python string expressions that will be evaluated
against a dictionary containing ``group_name``, ``host_node_name``,
``process_num``, ``program_name``, and ``here`` (the directory of the
supervisord config file). Values containing non-alphanumeric characters
should be quoted (e.g. ``KEY="val:123",KEY2="val,456"``). Otherwise,
quoting the values is optional but recommended. **Note** that the
subprocess will inherit the environment variables of the shell used to
start "supervisord" except for the ones overridden here. See
:ref:`subprocess_environment`.
*Default*: No extra environment
*Required*: No.
*Introduced*: 3.0
``directory``
A file path representing a directory to which :program:`supervisord`
should temporarily chdir before exec'ing the child.
*Default*: No chdir (inherit supervisor's)
*Required*: No.
*Introduced*: 3.0
``umask``
An octal number (e.g. 002, 022) representing the umask of the
process.
*Default*: No special umask (inherit supervisor's)
*Required*: No.
*Introduced*: 3.0
``serverurl``
The URL passed in the environment to the subprocess process as
``SUPERVISOR_SERVER_URL`` (see :mod:`supervisor.childutils`) to
allow the subprocess to easily communicate with the internal HTTP
server. If provided, it should have the same syntax and structure
as the ``[supervisorctl]`` section option of the same name. If this
is set to AUTO, or is unset, supervisor will automatically construct
a server URL, giving preference to a server that listens on UNIX
domain sockets over one that listens on an internet socket.
*Default*: AUTO
*Required*: No.
*Introduced*: 3.0
``[program:x]`` Section Example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: ini
[program:cat]
command=/bin/cat
process_name=%(program_name)s
numprocs=1
directory=/tmp
umask=022
priority=999
autostart=true
autorestart=unexpected
startsecs=10
startretries=3
exitcodes=0
stopsignal=TERM
stopwaitsecs=10
stopasgroup=false
killasgroup=false
user=chrism
redirect_stderr=false
stdout_logfile=/a/path
stdout_logfile_maxbytes=1MB
stdout_logfile_backups=10
stdout_capture_maxbytes=1MB
stdout_events_enabled=false
stderr_logfile=/a/path
stderr_logfile_maxbytes=1MB
stderr_logfile_backups=10
stderr_capture_maxbytes=1MB
stderr_events_enabled=false
environment=A="1",B="2"
serverurl=AUTO
``[include]`` Section Settings
------------------------------
The :file:`supervisord.conf` file may contain a section named
``[include]``. If the configuration file contains an ``[include]``
section, it must contain a single key named "files". The values in
this key specify other configuration files to be included within the
configuration.
.. note::
The ``[include]`` section is processed only by ``supervisord``. It is
ignored by ``supervisorctl``.
``[include]`` Section Values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``files``
A space-separated sequence of file globs. Each file glob may be
absolute or relative. If the file glob is relative, it is
considered relative to the location of the configuration file which
includes it. A "glob" is a file pattern which matches a specified
pattern according to the rules used by the Unix shell. No tilde
expansion is done, but ``*``, ``?``, and character ranges expressed
with ``[]`` will be correctly matched. The string expression is
evaluated against a dictionary that includes ``host_node_name``
and ``here`` (the directory of the supervisord config file). Recursive
includes from included files are not supported.
*Default*: No default (required)
*Required*: Yes.
*Introduced*: 3.0
*Changed*: 3.3.0. Added support for the ``host_node_name`` expansion.
``[include]`` Section Example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: ini
[include]
files = /an/absolute/filename.conf /an/absolute/*.conf foo.conf config??.conf
``[group:x]`` Section Settings
------------------------------
It is often useful to group "homogeneous" process groups (aka
"programs") together into a "heterogeneous" process group so they can
be controlled as a unit from Supervisor's various controller
interfaces.
To place programs into a group so you can treat them as a unit, define
a ``[group:x]`` section in your configuration file. The group header
value is a composite. It is the word "group", followed directly by a
colon, then the group name. A header value of ``[group:foo]``
describes a group with the name of "foo". The name is used within
client applications that control the processes that are created as a
result of this configuration. It is an error to create a ``group``
section that does not have a name. The name must not include a colon
character or a bracket character.
For a ``[group:x]``, there must be one or more ``[program:x]``
sections elsewhere in your configuration file, and the group must
refer to them by name in the ``programs`` value.
If "homogeneous" process groups (represented by program sections) are
placed into a "heterogeneous" group via ``[group:x]`` section's
``programs`` line, the homogeneous groups that are implied by the
program section will not exist at runtime in supervisor. Instead, all
processes belonging to each of the homogeneous groups will be placed
into the heterogeneous group. For example, given the following group
configuration:
.. code-block:: ini
[group:foo]
programs=bar,baz
priority=999
Given the above, at supervisord startup, the ``bar`` and ``baz``
homogeneous groups will not exist, and the processes that would have
been under them will now be moved into the ``foo`` group.
``[group:x]`` Section Values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``programs``
A comma-separated list of program names. The programs which are
listed become members of the group.
*Default*: No default (required)
*Required*: Yes.
*Introduced*: 3.0
``priority``
A priority number analogous to a ``[program:x]`` priority value
assigned to the group.
*Default*: 999
*Required*: No.
*Introduced*: 3.0
``[group:x]`` Section Example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: ini
[group:foo]
programs=bar,baz
priority=999
``[fcgi-program:x]`` Section Settings
-------------------------------------
Supervisor can manage groups of `FastCGI `_
processes that all listen on the same socket. Until now, deployment
flexibility for FastCGI was limited. To get full process management,
you could use mod_fastcgi under Apache but then you were stuck with
Apache's inefficient concurrency model of one process or thread per
connection. In addition to requiring more CPU and memory resources,
the process/thread per connection model can be quickly saturated by a
slow resource, preventing other resources from being served. In order
to take advantage of newer event-driven web servers such as lighttpd
or nginx which don't include a built-in process manager, you had to
use scripts like cgi-fcgi or spawn-fcgi. These can be used in
conjunction with a process manager such as supervisord or daemontools
but require each FastCGI child process to bind to its own socket.
The disadvantages of this are: unnecessarily complicated web server
configuration, ungraceful restarts, and reduced fault tolerance. With
fewer sockets to configure, web server configurations are much smaller
if groups of FastCGI processes can share sockets. Shared sockets
allow for graceful restarts because the socket remains bound by the
parent process while any of the child processes are being restarted.
Finally, shared sockets are more fault tolerant because if a given
process fails, other processes can continue to serve inbound
connections.
With integrated FastCGI spawning support, Supervisor gives you the
best of both worlds. You get full-featured process management with
groups of FastCGI processes sharing sockets without being tied to a
particular web server. It's a clean separation of concerns, allowing
the web server and the process manager to each do what they do best.
.. note::
The socket manager in Supervisor was originally developed to support
FastCGI processes but it is not limited to FastCGI. Other protocols may
be used as well with no special configuration. Any program that can
access an open socket from a file descriptor (e.g. with
`socket.fromfd `_
in Python) can use the socket manager. Supervisor will automatically
create the socket, bind, and listen before forking the first child in a
group. The socket will be passed to each child on file descriptor
number ``0`` (zero). When the last child in the group exits,
Supervisor will close the socket.
.. note::
Prior to Supervisor 3.4.0, FastCGI programs (``[fcgi-program:x]``)
could not be referenced in groups (``[group:x]``).
All the options available to ``[program:x]`` sections are
also respected by ``fcgi-program`` sections.
``[fcgi-program:x]`` Section Values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``[fcgi-program:x]`` sections have a few keys which ``[program:x]``
sections do not have.
``socket``
The FastCGI socket for this program, either TCP or UNIX domain
socket. For TCP sockets, use this format: ``tcp://localhost:9002``.
For UNIX domain sockets, use ``unix:///absolute/path/to/file.sock``.
String expressions are evaluated against a dictionary containing the
keys "program_name" and "here" (the directory of the supervisord
config file).
*Default*: No default.
*Required*: Yes.
*Introduced*: 3.0
``socket_backlog``
Sets socket listen(2) backlog.
*Default*: socket.SOMAXCONN
*Required*: No.
*Introduced*: 3.4.0
``socket_owner``
For UNIX domain sockets, this parameter can be used to specify the user
and group for the FastCGI socket. May be a UNIX username (e.g. chrism)
or a UNIX username and group separated by a colon (e.g. chrism:wheel).
*Default*: Uses the user and group set for the fcgi-program
*Required*: No.
*Introduced*: 3.0
``socket_mode``
For UNIX domain sockets, this parameter can be used to specify the
permission mode.
*Default*: 0700
*Required*: No.
*Introduced*: 3.0
Consult :ref:`programx_section` for other allowable keys, delta the
above constraints and additions.
``[fcgi-program:x]`` Section Example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: ini
[fcgi-program:fcgiprogramname]
command=/usr/bin/example.fcgi
socket=unix:///var/run/supervisor/%(program_name)s.sock
socket_owner=chrism
socket_mode=0700
process_name=%(program_name)s_%(process_num)02d
numprocs=5
directory=/tmp
umask=022
priority=999
autostart=true
autorestart=unexpected
startsecs=1
startretries=3
exitcodes=0
stopsignal=QUIT
stopasgroup=false
killasgroup=false
stopwaitsecs=10
user=chrism
redirect_stderr=true
stdout_logfile=/a/path
stdout_logfile_maxbytes=1MB
stdout_logfile_backups=10
stdout_events_enabled=false
stderr_logfile=/a/path
stderr_logfile_maxbytes=1MB
stderr_logfile_backups=10
stderr_events_enabled=false
environment=A="1",B="2"
serverurl=AUTO
``[eventlistener:x]`` Section Settings
--------------------------------------
Supervisor allows specialized homogeneous process groups ("event
listener pools") to be defined within the configuration file. These
pools contain processes that are meant to receive and respond to event
notifications from supervisor's event system. See :ref:`events` for
an explanation of how events work and how to implement programs that
can be declared as event listeners.
Note that all the options available to ``[program:x]`` sections are
respected by eventlistener sections *except* for ``stdout_capture_maxbytes``.
Eventlisteners cannot emit process communication events on ``stdout``,
but can emit on ``stderr`` (see :ref:`capture_mode`).
``[eventlistener:x]`` Section Values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``[eventlistener:x]`` sections have a few keys which ``[program:x]``
sections do not have.
``buffer_size``
The event listener pool's event queue buffer size. When a listener
pool's event buffer is overflowed (as can happen when an event
listener pool cannot keep up with all of the events sent to it), the
oldest event in the buffer is discarded.
``events``
A comma-separated list of event type names that this listener is
"interested" in receiving notifications for (see
:ref:`event_types` for a list of valid event type names).
``result_handler``
An `entry point object reference
`_
string that resolves to a Python callable. The default value is
``supervisor.dispatchers:default_handler``. Specifying an alternate
result handler is a very uncommon thing to need to do, and as a
result, how to create one is not documented.
Consult :ref:`programx_section` for other allowable keys, delta the
above constraints and additions.
``[eventlistener:x]`` Section Example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: ini
[eventlistener:theeventlistenername]
command=/bin/eventlistener
process_name=%(program_name)s_%(process_num)02d
numprocs=5
events=PROCESS_STATE
buffer_size=10
directory=/tmp
umask=022
priority=-1
autostart=true
autorestart=unexpected
startsecs=1
startretries=3
exitcodes=0
stopsignal=QUIT
stopwaitsecs=10
stopasgroup=false
killasgroup=false
user=chrism
redirect_stderr=false
stdout_logfile=/a/path
stdout_logfile_maxbytes=1MB
stdout_logfile_backups=10
stdout_events_enabled=false
stderr_logfile=/a/path
stderr_logfile_maxbytes=1MB
stderr_logfile_backups=10
stderr_events_enabled=false
environment=A="1",B="2"
serverurl=AUTO
``[rpcinterface:x]`` Section Settings
-------------------------------------
Adding ``rpcinterface:x`` settings in the configuration file is only
useful for people who wish to extend supervisor with additional custom
behavior.
In the sample config file (see :ref:`create_config`), there is a section
which is named ``[rpcinterface:supervisor]``. By default it looks like the
following.
.. code-block:: ini
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
The ``[rpcinterface:supervisor]`` section *must* remain in the
configuration for the standard setup of supervisor to work properly.
If you don't want supervisor to do anything it doesn't already do out
of the box, this is all you need to know about this type of section.
However, if you wish to add rpc interface namespaces in order to
customize supervisor, you may add additional ``[rpcinterface:foo]``
sections, where "foo" represents the namespace of the interface (from
the web root), and the value named by
``supervisor.rpcinterface_factory`` is a factory callable which should
have a function signature that accepts a single positional argument
``supervisord`` and as many keyword arguments as required to perform
configuration. Any extra key/value pairs defined within the
``[rpcinterface:x]`` section will be passed as keyword arguments to
the factory.
Here's an example of a factory function, created in the
``__init__.py`` file of the Python package ``my.package``.
.. code-block:: python
from my.package.rpcinterface import AnotherRPCInterface
def make_another_rpcinterface(supervisord, **config):
retries = int(config.get('retries', 0))
another_rpc_interface = AnotherRPCInterface(supervisord, retries)
return another_rpc_interface
And a section in the config file meant to configure it.
.. code-block:: ini
[rpcinterface:another]
supervisor.rpcinterface_factory = my.package:make_another_rpcinterface
retries = 1
``[rpcinterface:x]`` Section Values
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
``supervisor.rpcinterface_factory``
``entry point object reference`` dotted name to your RPC interface's
factory function.
*Default*: N/A
*Required*: No.
*Introduced*: 3.0
``[rpcinterface:x]`` Section Example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: ini
[rpcinterface:another]
supervisor.rpcinterface_factory = my.package:make_another_rpcinterface
retries = 1
================================================
FILE: docs/development.rst
================================================
Resources and Development
=========================
Bug Tracker
-----------
Supervisor has a bugtracker where you may report any bugs or other
errors you find. Please report bugs to the `GitHub issues page
`_.
Version Control Repository
--------------------------
You can also view the `Supervisor version control repository
`_.
Contributing
------------
We'll review contributions from the community in
`pull requests `_
on GitHub.
Author Information
------------------
The following people are responsible for creating Supervisor.
Original Author
~~~~~~~~~~~~~~~
- `Chris McDonough `_ is the original author of
Supervisor.
Contributors
~~~~~~~~~~~~
Contributors are tracked on the `GitHub contributions page
`_. The two lists
below are included for historical reasons.
This first list recognizes significant contributions that were made
before the repository moved to GitHub.
- Anders Quist: Anders contributed the patch that was the basis for
Supervisor’s ability to reload parts of its configuration without
restarting.
- Derek DeVries: Derek did the web design of Supervisor’s internal web
interface and website logos.
- Guido van Rossum: Guido authored ``zdrun`` and ``zdctl``, the
programs from Zope that were the original basis for Supervisor. He
also created Python, the programming language that Supervisor is
written in.
- Jason Kirtland: Jason fixed Supervisor to run on Python 2.6 by
contributing a patched version of Medusa (a Supervisor dependency)
that we now bundle.
- Roger Hoover: Roger added support for spawning FastCGI programs. He
has also been one of the most active mailing list users, providing
his testing and feedback.
- Siddhant Goel: Siddhant worked on :program:`supervisorctl` as our
Google Summer of Code student for 2008. He implemented the ``fg``
command and also added tab completion.
This second list records contributors who signed a legal agreement.
The legal agreement was
`introduced `_
in January 2014 but later
`withdrawn `_
in March 2014. This list is being preserved in case it is useful
later (e.g. if at some point there was a desire to donate the project
to a foundation that required such agreements).
- Chris McDonough, 2006-06-26
- Siddhant Goel, 2008-06-15
- Chris Rossi, 2010-02-02
- Roger Hoover, 2010-08-17
- Benoit Sigoure, 2011-06-21
- John Szakmeister, 2011-09-06
- Gunnlaugur Þór Briem, 2011-11-26
- Jens Rantil, 2011-11-27
- Michael Blume, 2012-01-09
- Philip Zeyliger, 2012-02-21
- Marcelo Vanzin, 2012-05-03
- Martijn Pieters, 2012-06-04
- Marcin Kuźmiński, 2012-06-21
- Jean Jordaan, 2012-06-28
- Perttu Ranta-aho, 2012-09-27
- Chris Streeter, 2013-03-23
- Caio Ariede, 2013-03-25
- David Birdsong, 2013-04-11
- Lukas Rist, 2013-04-18
- Honza Pokorny, 2013-07-23
- Thúlio Costa, 2013-10-31
- Gary M. Josack, 2013-11-12
- Márk Sági-Kazár, 2013-12-16
================================================
FILE: docs/events.rst
================================================
.. _events:
Events
======
Events are an advanced feature of Supervisor introduced in version
3.0. You don't need to understand events if you simply want to use
Supervisor as a mechanism to restart crashed processes or as a system
to manually control process state. You do need to understand events
if you want to use Supervisor as part of a process
monitoring/notification framework.
Event Listeners and Event Notifications
---------------------------------------
Supervisor provides a way for a specially written program (which it
runs as a subprocess) called an "event listener" to subscribe to
"event notifications". An event notification implies that something
happened related to a subprocess controlled by :program:`supervisord`
or to :program:`supervisord` itself. Event notifications are grouped
into types in order to make it possible for event listeners to
subscribe to a limited subset of event notifications. Supervisor
continually emits event notifications as its running even if there are
no listeners configured. If a listener is configured and subscribed
to an event type that is emitted during a :program:`supervisord`
lifetime, that listener will be notified.
The purpose of the event notification/subscription system is to
provide a mechanism for arbitrary code to be run (e.g. send an email,
make an HTTP request, etc) when some condition is met. That condition
usually has to do with subprocess state. For instance, you may want
to notify someone via email when a process crashes and is restarted by
Supervisor.
The event notification protocol is based on communication via a
subprocess' stdin and stdout. Supervisor sends specially-formatted
input to an event listener process' stdin and expects
specially-formatted output from an event listener's stdout, forming a
request-response cycle. A protocol agreed upon between supervisor and
the listener's implementer allows listeners to process event
notifications. Event listeners can be written in any language
supported by the platform you're using to run Supervisor. Although
event listeners may be written in any language, there is special
library support for Python in the form of a
:mod:`supervisor.childutils` module, which makes creating event
listeners in Python slightly easier than in other languages.
Configuring an Event Listener
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A supervisor event listener is specified via a ``[eventlistener:x]``
section in the configuration file. Supervisor ``[eventlistener:x]``
sections are treated almost exactly like supervisor ``[program:x]``
section with the respect to the keys allowed in their configuration
except that Supervisor does not respect "capture mode" output from
event listener processes (ie. event listeners cannot be
``PROCESS_COMMUNICATIONS_EVENT`` event generators). Therefore it is
an error to specify ``stdout_capture_maxbytes`` or
``stderr_capture_maxbytes`` in the configuration of an eventlistener.
There is no artificial constraint on the number of eventlistener
sections that can be placed into the configuration file.
When an ``[eventlistener:x]`` section is defined, it actually defines
a "pool", where the number of event listeners in the pool is
determined by the ``numprocs`` value within the section.
The ``events`` parameter of the ``[eventlistener:x]`` section
specifies the events that will be sent to a listener pool. A
well-written event listener will ignore events that it cannot process,
but there is no guarantee that a specific event listener won't crash
as a result of receiving an event type it cannot handle. Therefore,
depending on the listener implementation, it may be important to
specify in the configuration that it may receive only certain types of
events. The implementor of the event listener is the only person who
can tell you what these are (and therefore what value to put in the
``events`` configuration). Examples of eventlistener
configurations that can be placed in ``supervisord.conf`` are as
follows.
.. code-block:: ini
[eventlistener:memmon]
command=memmon -a 200MB -m bob@example.com
events=TICK_60
.. code-block:: ini
[eventlistener:mylistener]
command=my_custom_listener.py
events=PROCESS_STATE,TICK_60
.. note::
An advanced feature, specifying an alternate "result handler" for a
pool, can be specified via the ``result_handler`` parameter of an
``[eventlistener:x]`` section in the form of an `entry point object reference
`_
string. The default result handler is
``supervisord.dispatchers:default_handler``. Creating an alternate
result handler is not currently documented.
When an event notification is sent by supervisor, all event listener
pools which are subscribed to receive events for the event's type
(filtered by the ``events`` value in the eventlistener
section) will be found. One of the listeners in each listener pool
will receive the event notification (any "available" listener).
Every process in an event listener pool is treated equally by
supervisor. If a process in the pool is unavailable (because it is
already processing an event, because it has crashed, or because it has
elected to removed itself from the pool), supervisor will choose
another process from the pool. If the event cannot be sent because
all listeners in the pool are "busy", the event will be buffered and
notification will be retried later. "Later" is defined as "the next
time that the :program:`supervisord` select loop executes". For
satisfactory event processing performance, you should configure a pool
with as many event listener processes as appropriate to handle your
event load. This can only be determined empirically for any given
workload, there is no "magic number" but to help you determine the
optimal number of listeners in a given pool, Supervisor will emit
warning messages to its activity log when an event cannot be sent
immediately due to pool congestion. There is no artificial constraint
placed on the number of processes that can be in a pool, it is limited
only by your platform constraints.
A listener pool has an event buffer queue. The queue is sized via the
listener pool's ``buffer_size`` config file option. If the queue is
full and supervisor attempts to buffer an event, supervisor will throw
away the oldest event in the buffer and log an error.
Writing an Event Listener
~~~~~~~~~~~~~~~~~~~~~~~~~
An event listener implementation is a program that is willing to
accept structured input on its stdin stream and produce structured
output on its stdout stream. An event listener implementation should
operate in "unbuffered" mode or should flush its stdout every time it
needs to communicate back to the supervisord process. Event listeners
can be written to be long-running or may exit after a single request
(depending on the implementation and the ``autorestart`` parameter in
the eventlistener's configuration).
An event listener can send arbitrary output to its stderr, which will
be logged or ignored by supervisord depending on the stderr-related
logfile configuration in its ``[eventlistener:x]`` section.
Event Notification Protocol
+++++++++++++++++++++++++++
When supervisord sends a notification to an event listener process,
the listener will first be sent a single "header" line on its
stdin. The composition of the line is a set of colon-separated tokens
(each of which represents a key-value pair) separated from each other
by a single space. The line is terminated with a ``\n`` (linefeed)
character. The tokens on the line are not guaranteed to be in any
particular order. The types of tokens currently defined are in the
table below.
Header Tokens
@@@@@@@@@@@@@
=========== ============================================= ===================
Key Description Example
=========== ============================================= ===================
ver The event system protocol version 3.0
server The identifier of the supervisord sending the
event (see config file ``[supervisord]``
section ``identifier`` value.
serial An integer assigned to each event. No two 30
events generated during the lifetime of
a :program:`supervisord` process will have
the same serial number. The value is useful
for functional testing and detecting event
ordering anomalies.
pool The name of the event listener pool which myeventpool
generated this event.
poolserial An integer assigned to each event by the 30
eventlistener pool which it is being sent
from. No two events generated by the same
eventlistener pool during the lifetime of a
:program:`supervisord` process will have the
same ``poolserial`` number. This value can
be used to detect event ordering anomalies.
eventname The specific event type name (see TICK_5
:ref:`event_types`)
len An integer indicating the number of bytes in 22
the event payload, aka the ``PAYLOAD_LENGTH``
=========== ============================================= ===================
An example of a complete header line is as follows.
.. code-block:: text
ver:3.0 server:supervisor serial:21 pool:listener poolserial:10 eventname:PROCESS_COMMUNICATION_STDOUT len:54
Directly following the linefeed character in the header is the event
payload. It consists of ``PAYLOAD_LENGTH`` bytes representing a
serialization of the event data. See :ref:`event_types` for the
specific event data serialization definitions.
An example payload for a ``PROCESS_COMMUNICATION_STDOUT`` event
notification is as follows.
.. code-block:: text
processname:foo groupname:bar pid:123
This is the data that was sent between the tags
The payload structure of any given event is determined only by the
event's type.
Event Listener States
+++++++++++++++++++++
An event listener process has three possible states that are
maintained by supervisord:
============================= ==============================================
Name Description
============================= ==============================================
ACKNOWLEDGED The event listener has acknowledged (accepted
or rejected) an event send.
READY Event notifications may be sent to this event
listener
BUSY Event notifications may not be sent to this
event listener.
============================= ==============================================
When an event listener process first starts, supervisor automatically
places it into the ``ACKNOWLEDGED`` state to allow for startup
activities or guard against startup failures (hangs). Until the
listener sends a ``READY\n`` string to its stdout, it will stay in
this state.
When supervisor sends an event notification to a listener in the
``READY`` state, the listener will be placed into the ``BUSY`` state
until it receives an ``OK`` or ``FAIL`` response from the listener, at
which time, the listener will be transitioned back into the
``ACKNOWLEDGED`` state.
Event Listener Notification Protocol
++++++++++++++++++++++++++++++++++++
Supervisor will notify an event listener in the ``READY`` state of an
event by sending data to the stdin of the process. Supervisor will
never send anything to the stdin of an event listener process while
that process is in the ``BUSY`` or ``ACKNOWLEDGED`` state. Supervisor
starts by sending the header.
Once it has processed the header, the event listener implementation
should read ``PAYLOAD_LENGTH`` bytes from its stdin, perform an
arbitrary action based on the values in the header and the data parsed
out of the serialization. It is free to block for an arbitrary amount
of time while doing this. Supervisor will continue processing
normally as it waits for a response and it will send other events of
the same type to other listener processes in the same pool as
necessary.
After the event listener has processed the event serialization, in
order to notify supervisord about the result, it should send back a
result structure on its stdout. A result structure is the word
"RESULT", followed by a space, followed by the result length, followed
by a line feed, followed by the result content. For example,
``RESULT 2\nOK`` is the result "OK". Conventionally, an event
listener will use either ``OK`` or ``FAIL`` as the result content.
These strings have special meaning to the default result handler.
If the default result handler receives ``OK`` as result content, it
will assume that the listener processed the event notification
successfully. If it receives ``FAIL``, it will assume that the
listener has failed to process the event, and the event will be
rebuffered and sent again at a later time. The event listener may
reject the event for any reason by returning a ``FAIL`` result. This
does not indicate a problem with the event data or the event listener.
Once an ``OK`` or ``FAIL`` result is received by supervisord, the
event listener is placed into the ``ACKNOWLEDGED`` state.
Once the listener is in the ``ACKNOWLEDGED`` state, it may either exit
(and subsequently may be restarted by supervisor if its
``autorestart`` config parameter is ``true``), or it may continue
running. If it continues to run, in order to be placed back into the
``READY`` state by supervisord, it must send a ``READY`` token
followed immediately by a line feed to its stdout.
Example Event Listener Implementation
+++++++++++++++++++++++++++++++++++++
A Python implementation of a "long-running" event listener which
accepts an event notification, prints the header and payload to its
stderr, and responds with an ``OK`` result, and then subsequently a
``READY`` is as follows.
.. code-block:: python
import sys
def write_stdout(s):
# only eventlistener protocol messages may be sent to stdout
sys.stdout.write(s)
sys.stdout.flush()
def write_stderr(s):
sys.stderr.write(s)
sys.stderr.flush()
def main():
while 1:
# transition from ACKNOWLEDGED to READY
write_stdout('READY\n')
# read header line and print it to stderr
line = sys.stdin.readline()
write_stderr(line)
# read event payload and print it to stderr
headers = dict([ x.split(':') for x in line.split() ])
data = sys.stdin.read(int(headers['len']))
write_stderr(data)
# transition from READY to ACKNOWLEDGED
write_stdout('RESULT 2\nOK')
if __name__ == '__main__':
main()
Other sample event listeners are present within the :term:`Superlance`
package, including one which can monitor supervisor subprocesses and
restart a process if it is using "too much" memory.
Event Listener Error Conditions
+++++++++++++++++++++++++++++++
If the event listener process dies while the event is being
transmitted to its stdin, or if it dies before sending an result
structure back to supervisord, the event is assumed to not be
processed and will be rebuffered by supervisord and sent again later.
If an event listener sends data to its stdout which supervisor does
not recognize as an appropriate response based on the state that the
event listener is in, the event listener will be placed into the
``UNKNOWN`` state, and no further event notifications will be sent to
it. If an event was being processed by the listener during this time,
it will be rebuffered and sent again later.
Miscellaneous
+++++++++++++
Event listeners may use the Supervisor XML-RPC interface to call "back
in" to Supervisor. As such, event listeners can impact the state of a
Supervisor subprocess as a result of receiving an event notification.
For example, you may want to generate an event every few minutes
related to process usage of Supervisor-controlled subprocesses, and if
any of those processes exceed some memory threshold, you would like
to restart it. You would write a program that caused supervisor to
generate ``PROCESS_COMMUNICATION`` events every so often with memory
information in them, and an event listener to perform an action based
on processing the data it receives from these events.
.. _event_types:
Event Types
-----------
The event types are a controlled set, defined by Supervisor itself.
There is no way to add an event type without changing
:program:`supervisord` itself. This is typically not a problem,
though, because metadata is attached to events that can be used by
event listeners as additional filter criterion, in conjunction with
its type.
Event types that may be subscribed to by event listeners are
predefined by supervisor and fall into several major categories,
including "process state change", "process communication", and
"supervisor state change" events. Below are tables describing
these event types.
In the below list, we indicate that some event types have a "body"
which is a a *token set*. A token set consists of a set of characters
with space-separated tokens. Each token represents a key-value pair.
The key and value are separated by a colon. For example:
.. code-block:: text
processname:cat groupname:cat from_state:STOPPED
Token sets do not have a linefeed or carriage return character at
their end.
``EVENT`` Event Type
~~~~~~~~~~~~~~~~~~~~
The base event type. This event type is abstract. It will never be
sent directly. Subscribing to this event type will cause a subscriber
to receive all event notifications emitted by Supervisor.
*Name*: ``EVENT``
*Subtype Of*: N/A
*Body Description*: N/A
``PROCESS_STATE`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
This process type indicates a process has moved from one state to
another. See :ref:`process_states` for a description of the states
that a process moves through during its lifetime. This event type is
abstract, it will never be sent directly. Subscribing to this event
type will cause a subscriber to receive event notifications of all the
event types that are subtypes of ``PROCESS_STATE``.
*Name*: ``PROCESS_STATE``
*Subtype Of*: ``EVENT``
Body Description
++++++++++++++++
All subtypes of ``PROCESS_STATE`` have a body which is a token set.
Additionally, each ``PROCESS_STATE`` subtype's token set has a default
set of key/value pairs: ``processname``, ``groupname``, and
``from_state``. ``processname`` represents the process name which
supervisor knows this process as. ``groupname`` represents the name of
the supervisord group which this process is in. ``from_state`` is the
name of the state from which this process is transitioning (the new
state is implied by the concrete event type). Concrete subtypes may
include additional key/value pairs in the token set.
``PROCESS_STATE_STARTING`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates a process has moved from a state to the STARTING state.
*Name*: ``PROCESS_STATE_STARTING``
*Subtype Of*: ``PROCESS_STATE``
Body Description
++++++++++++++++
This body is a token set. It has the default set of key/value pairs
plus an additional ``tries`` key. ``tries`` represents the number of
times this process has entered this state before transitioning to
RUNNING or FATAL (it will never be larger than the "startretries"
parameter of the process). For example:
.. code-block:: text
processname:cat groupname:cat from_state:STOPPED tries:0
``PROCESS_STATE_RUNNING`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates a process has moved from the ``STARTING`` state to the
``RUNNING`` state. This means that the process has successfully
started as far as Supervisor is concerned.
*Name*: ``PROCESS_STATE_RUNNING``
*Subtype Of*: ``PROCESS_STATE``
Body Description
++++++++++++++++
This body is a token set. It has the default set of key/value pairs
plus an additional ``pid`` key. ``pid`` represents the UNIX
process id of the process that was started. For example:
.. code-block:: text
processname:cat groupname:cat from_state:STARTING pid:2766
``PROCESS_STATE_BACKOFF`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates a process has moved from the ``STARTING`` state to the
``BACKOFF`` state. This means that the process did not successfully
enter the RUNNING state, and Supervisor is going to try to restart it
unless it has exceeded its "startretries" configuration limit.
*Name*: ``PROCESS_STATE_BACKOFF``
*Subtype Of*: ``PROCESS_STATE``
Body Description
++++++++++++++++
This body is a token set. It has the default set of key/value pairs
plus an additional ``tries`` key. ``tries`` represents the number of
times this process has entered this state before transitioning to
``RUNNING`` or ``FATAL`` (it will never be larger than the
"startretries" parameter of the process). For example:
.. code-block:: text
processname:cat groupname:cat from_state:STOPPED tries:0
``PROCESS_STATE_STOPPING`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates a process has moved from either the ``RUNNING`` state or the
``STARTING`` state to the ``STOPPING`` state.
*Name*: ``PROCESS_STATE_STOPPING``
*Subtype Of*: ``PROCESS_STATE``
Body Description
++++++++++++++++
This body is a token set. It has the default set of key/value pairs
plus an additional ``pid`` key. ``pid`` represents the UNIX process
id of the process that was started. For example:
.. code-block:: text
processname:cat groupname:cat from_state:STARTING pid:2766
``PROCESS_STATE_EXITED`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates a process has moved from the ``RUNNING`` state to the
``EXITED`` state.
*Name*: ``PROCESS_STATE_EXITED``
*Subtype Of*: ``PROCESS_STATE``
Body Description
++++++++++++++++
This body is a token set. It has the default set of key/value pairs
plus two additional keys: ``pid`` and ``expected``. ``pid``
represents the UNIX process id of the process that exited.
``expected`` represents whether the process exited with an expected
exit code or not. It will be ``0`` if the exit code was unexpected,
or ``1`` if the exit code was expected. For example:
.. code-block:: text
processname:cat groupname:cat from_state:RUNNING expected:0 pid:2766
``PROCESS_STATE_STOPPED`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates a process has moved from the ``STOPPING`` state to the
``STOPPED`` state.
*Name*: ``PROCESS_STATE_STOPPED``
*Subtype Of*: ``PROCESS_STATE``
Body Description
++++++++++++++++
This body is a token set. It has the default set of key/value pairs
plus an additional ``pid`` key. ``pid`` represents the UNIX process
id of the process that was started. For example:
.. code-block:: text
processname:cat groupname:cat from_state:STOPPING pid:2766
``PROCESS_STATE_FATAL`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates a process has moved from the ``BACKOFF`` state to the
``FATAL`` state. This means that Supervisor tried ``startretries``
number of times unsuccessfully to start the process, and gave up
attempting to restart it.
*Name*: ``PROCESS_STATE_FATAL``
*Subtype Of*: ``PROCESS_STATE``
Body Description
++++++++++++++++
This event type is a token set with the default key/value pairs. For
example:
.. code-block:: text
processname:cat groupname:cat from_state:BACKOFF
``PROCESS_STATE_UNKNOWN`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates a process has moved from any state to the ``UNKNOWN`` state
(indicates an error in :program:`supervisord`). This state transition
will only happen if :program:`supervisord` itself has a programming
error.
*Name*: ``PROCESS_STATE_UNKNOWN``
*Subtype Of*: ``PROCESS_STATE``
Body Description
++++++++++++++++
This event type is a token set with the default key/value pairs. For
example:
.. code-block:: text
processname:cat groupname:cat from_state:BACKOFF
``REMOTE_COMMUNICATION`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An event type raised when the ``supervisor.sendRemoteCommEvent()``
method is called on Supervisor's RPC interface. The ``type`` and
``data`` are arguments of the RPC method.
*Name*: ``REMOTE_COMMUNICATION``
*Subtype Of*: ``EVENT``
Body Description
++++++++++++++++
.. code-block:: text
type:type
data
``PROCESS_LOG`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~
An event type emitted when a process writes to stdout or stderr. The
event will only be emitted if the file descriptor is not in capture
mode and if ``stdout_events_enabled`` or ``stderr_events_enabled``
config options are set to ``true``. This event type is abstract, it
will never be sent directly. Subscribing to this event type will
cause a subscriber to receive event notifications for all subtypes of
``PROCESS_LOG``.
*Name*: ``PROCESS_LOG``
*Subtype Of*: ``EVENT``
*Body Description*: N/A
``PROCESS_LOG_STDOUT`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates a process has written to its stdout file descriptor. The
event will only be emitted if the file descriptor is not in capture
mode and if the ``stdout_events_enabled`` config option is set to
``true``.
*Name*: ``PROCESS_LOG_STDOUT``
*Subtype Of*: ``PROCESS_LOG``
Body Description
++++++++++++++++
.. code-block:: text
processname:name groupname:name pid:pid channel:stdout
data
``PROCESS_LOG_STDERR`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates a process has written to its stderr file descriptor. The
event will only be emitted if the file descriptor is not in capture
mode and if the ``stderr_events_enabled`` config option is set to
``true``.
*Name*: ``PROCESS_LOG_STDERR``
*Subtype Of*: ``PROCESS_LOG``
Body Description
++++++++++++++++
.. code-block:: text
processname:name groupname:name pid:pid channel:stderr
data
``PROCESS_COMMUNICATION`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An event type raised when any process attempts to send information
between ```` and ````
tags in its output. This event type is abstract, it will never be
sent directly. Subscribing to this event type will cause a subscriber
to receive event notifications for all subtypes of
``PROCESS_COMMUNICATION``.
*Name*: ``PROCESS_COMMUNICATION``
*Subtype Of*: ``EVENT``
*Body Description*: N/A
``PROCESS_COMMUNICATION_STDOUT`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates a process has sent a message to Supervisor on its stdout
file descriptor.
*Name*: ``PROCESS_COMMUNICATION_STDOUT``
*Subtype Of*: ``PROCESS_COMMUNICATION``
Body Description
++++++++++++++++
.. code-block:: text
processname:name groupname:name pid:pid
data
``PROCESS_COMMUNICATION_STDERR`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates a process has sent a message to Supervisor on its stderr
file descriptor.
*Name*: ``PROCESS_COMMUNICATION_STDERR``
*Subtype Of*: ``PROCESS_COMMUNICATION``
Body Description
++++++++++++++++
.. code-block:: text
processname:name groupname:name pid:pid
data
``SUPERVISOR_STATE_CHANGE`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An event type raised when the state of the :program:`supervisord`
process changes. This type is abstract, it will never be sent
directly. Subscribing to this event type will cause a subscriber to
receive event notifications of all the subtypes of
``SUPERVISOR_STATE_CHANGE``.
*Name*: ``SUPERVISOR_STATE_CHANGE``
*Subtype Of*: ``EVENT``
*Body Description*: N/A
``SUPERVISOR_STATE_CHANGE_RUNNING`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates that :program:`supervisord` has started.
*Name*: ``SUPERVISOR_STATE_CHANGE_RUNNING``
*Subtype Of*: ``SUPERVISOR_STATE_CHANGE``
*Body Description*: Empty string
``SUPERVISOR_STATE_CHANGE_STOPPING`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates that :program:`supervisord` is stopping.
*Name*: ``SUPERVISOR_STATE_CHANGE_STOPPING``
*Subtype Of*: ``SUPERVISOR_STATE_CHANGE``
*Body Description*: Empty string
``TICK`` Event Type
~~~~~~~~~~~~~~~~~~~
An event type that may be subscribed to for event listeners to receive
"wake-up" notifications every N seconds. This event type is abstract,
it will never be sent directly. Subscribing to this event type will
cause a subscriber to receive event notifications for all subtypes of
``TICK``.
Note that the only ``TICK`` events available are the ones listed below.
You cannot subscribe to an arbitrary ``TICK`` interval. If you need an
interval not provided below, you can subscribe to one of the shorter
intervals given below and keep track of the time between runs in your
event listener.
*Name*: ``TICK``
*Subtype Of*: ``EVENT``
*Body Description*: N/A
``TICK_5`` Event Type
~~~~~~~~~~~~~~~~~~~~~
An event type that may be subscribed to for event listeners to receive
"wake-up" notifications every 5 seconds.
*Name*: ``TICK_5``
*Subtype Of*: ``TICK``
Body Description
++++++++++++++++
This event type is a token set with a single key: "when", which
indicates the epoch time for which the tick was sent.
.. code-block:: text
when:1201063880
``TICK_60`` Event Type
~~~~~~~~~~~~~~~~~~~~~~
An event type that may be subscribed to for event listeners to receive
"wake-up" notifications every 60 seconds.
*Name*: ``TICK_60``
*Subtype Of*: ``TICK``
Body Description
++++++++++++++++
This event type is a token set with a single key: "when", which
indicates the epoch time for which the tick was sent.
.. code-block:: text
when:1201063880
``TICK_3600`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~
An event type that may be subscribed to for event listeners to receive
"wake-up" notifications every 3600 seconds (1 hour).
*Name*: ``TICK_3600``
*Subtype Of*: ``TICK``
Body Description
++++++++++++++++
This event type is a token set with a single key: "when", which
indicates the epoch time for which the tick was sent.
.. code-block:: text
when:1201063880
``PROCESS_GROUP`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
An event type raised when a process group is added to or removed from
Supervisor. This type is abstract, it will never be sent
directly. Subscribing to this event type will cause a subscriber to
receive event notifications of all the subtypes of
``PROCESS_GROUP``.
*Name*: ``PROCESS_GROUP``
*Subtype Of*: ``EVENT``
*Body Description*: N/A
``PROCESS_GROUP_ADDED`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates that a process group has been added to Supervisor's configuration.
*Name*: ``PROCESS_GROUP_ADDED``
*Subtype Of*: ``PROCESS_GROUP``
*Body Description*: This body is a token set with just a groupname key/value.
.. code-block:: text
groupname:cat
``PROCESS_GROUP_REMOVED`` Event Type
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Indicates that a process group has been removed from Supervisor's configuration.
*Name*: ``PROCESS_GROUP_REMOVED``
*Subtype Of*: ``PROCESS_GROUP``
*Body Description*: This body is a token set with just a groupname key/value.
.. code-block:: text
groupname:cat
================================================
FILE: docs/faq.rst
================================================
Frequently Asked Questions
==========================
Q
My program never starts and supervisor doesn't indicate any error?
A
Make sure the ``x`` bit is set on the executable file you're using in
the ``command=`` line of your program section.
Q
I am a software author and I want my program to behave differently
when it's running under :program:`supervisord`. How can I tell if
my program is running under :program:`supervisord`?
A
Supervisor and its subprocesses share an environment variable
:envvar:`SUPERVISOR_ENABLED`. When your program is run under
:program:`supervisord`, it can check for the presence of this
environment variable to determine whether it is running as a
:program:`supervisord` subprocess.
Q
My command works fine when I invoke it by hand from a shell prompt,
but when I use the same command line in a supervisor program
``command=`` section, the program fails mysteriously. Why?
A
This may be due to your process' dependence on environment variable
settings. See :ref:`subprocess_environment`.
Q
How can I make Supervisor restart a process that's using "too much"
memory automatically?
A
The :term:`Superlance` package contains a console script that can be
used as a Supervisor event listener named ``memmon`` which helps
with this task. It works on Linux and Mac OS X.
================================================
FILE: docs/glossary.rst
================================================
.. _glossary:
Glossary
========
.. glossary::
:sorted:
daemontools
A `process control system by D.J. Bernstein
`_.
runit
A `process control system `_.
launchd
A `process control system used by Apple
`_ as process 1 under Mac
OS X.
umask
Abbreviation of *user mask*: sets the file mode creation mask of
the current process. See `http://en.wikipedia.org/wiki/Umask
`_.
Superlance
A package which provides various event listener implementations
that plug into Supervisor which can help monitor process memory
usage and crash status: `https://pypi.org/pypi/superlance/
`_.
================================================
FILE: docs/index.rst
================================================
Supervisor: A Process Control System
====================================
Supervisor is a client/server system that allows its users to monitor
and control a number of processes on UNIX-like operating systems.
It shares some of the same goals of programs like :term:`launchd`,
:term:`daemontools`, and :term:`runit`. Unlike some of these programs,
it is not meant to be run as a substitute for ``init`` as "process id
1". Instead it is meant to be used to control processes related to a
project or a customer, and is meant to start like any other program at
boot time.
Narrative Documentation
-----------------------
.. toctree::
:maxdepth: 2
introduction.rst
installing.rst
running.rst
configuration.rst
subprocess.rst
logging.rst
events.rst
xmlrpc.rst
upgrading.rst
faq.rst
development.rst
glossary.rst
API Documentation
-----------------
.. toctree::
:maxdepth: 2
api.rst
Plugins
-------
.. toctree::
:maxdepth: 2
plugins.rst
Indices and tables
------------------
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`
================================================
FILE: docs/installing.rst
================================================
Installing
==========
Installation instructions depend whether the system on which
you're attempting to install Supervisor has internet access.
Installing to A System With Internet Access
-------------------------------------------
Internet-Installing With Pip
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Supervisor can be installed with ``pip install``:
.. code-block:: bash
pip install supervisor
Depending on the permissions of your system's Python, you might need
to be the root user to install Supervisor successfully using
``pip``.
You can also install supervisor in a virtualenv via ``pip``.
.. note::
If installing on a Python version before 3.8, first ensure that the
``setuptools`` package is installed because it is a runtime
dependency of Supervisor.
Internet-Installing Without Pip
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If your system does not have ``pip`` installed, you will need to download
the Supervisor distribution and install it by hand. Current and previous
Supervisor releases may be downloaded from `PyPi
`_. After unpacking the software
archive, run ``python setup.py install``. This requires internet access. It
will download and install all distributions depended upon by Supervisor and
finally install Supervisor itself.
.. note::
Depending on the permissions of your system's Python, you might
need to be the root user to successfully invoke ``python
setup.py install``.
.. note::
The ``setuptools`` package is required to run ``python setup.py install``.
On Python versions before 3.8, ``setuptools`` is also a runtime
dependency of Supervisor.
Installing To A System Without Internet Access
----------------------------------------------
If the system that you want to install Supervisor to does not have
Internet access, you'll need to perform installation slightly
differently. Since both ``pip`` and ``python setup.py
install`` depend on internet access to perform downloads of dependent
software, neither will work on machines without internet access until
dependencies are installed. To install to a machine which is not
internet-connected, obtain the dependencies listed in ``setup.py``
using a machine which is internet-connected.
Copy these files to removable media and put them on the target
machine. Install each onto the target machine as per its
instructions. This typically just means unpacking each file and
invoking ``python setup.py install`` in the unpacked directory.
Finally, run supervisor's ``python setup.py install``.
.. note::
Depending on the permissions of your system's Python, you might
need to be the root user to invoke ``python setup.py install``
successfully for each package.
.. note::
The ``setuptools`` package is required to run ``python setup.py install``.
On Python versions before 3.8, ``setuptools`` is also a runtime
dependency of Supervisor.
Installing a Distribution Package
---------------------------------
Some Linux distributions offer a version of Supervisor that is installable
through the system package manager. These packages are made by third parties,
not the Supervisor developers, and often include distribution-specific changes
to Supervisor.
Use the package management tools of your distribution to check availability;
e.g. on Ubuntu you can run ``apt-cache show supervisor``, and on CentOS
you can run ``yum info supervisor``.
A feature of distribution packages of Supervisor is that they will usually
include integration into the service management infrastructure of the
distribution, e.g. allowing ``supervisord`` to automatically start when
the system boots.
.. note::
Distribution packages of Supervisor can lag considerably behind the
official Supervisor packages released to PyPI. For example, Ubuntu
12.04 (released April 2012) offered a package based on Supervisor 3.0a8
(released January 2010). Lag is often caused by the software release
policy set by a given distribution.
.. note::
Users reported that the distribution package of Supervisor for Ubuntu 16.04
had different behavior than previous versions. On Ubuntu 10.04, 12.04, and
14.04, installing the package will configure the system to start
``supervisord`` when the system boots. On Ubuntu 16.04, this was not done
by the initial release of the package. The package was fixed later. See
`Ubuntu Bug #1594740 `_
for more information.
.. _create_config:
Creating a Configuration File
-----------------------------
Once the Supervisor installation has completed, run
``echo_supervisord_conf``. This will print a "sample" Supervisor
configuration file to your terminal's stdout.
Once you see the file echoed to your terminal, reinvoke the command as
``echo_supervisord_conf > /etc/supervisord.conf``. This won't work if
you do not have root access.
If you don't have root access, or you'd rather not put the
:file:`supervisord.conf` file in :file:`/etc/supervisord.conf`, you
can place it in the current directory (``echo_supervisord_conf >
supervisord.conf``) and start :program:`supervisord` with the
``-c`` flag in order to specify the configuration file
location.
For example, ``supervisord -c supervisord.conf``. Using the ``-c``
flag actually is redundant in this case, because
:program:`supervisord` searches the current directory for a
:file:`supervisord.conf` before it searches any other locations for
the file, but it will work. See :ref:`running` for more information
about the ``-c`` flag.
Once you have a configuration file on your filesystem, you can
begin modifying it to your liking.
================================================
FILE: docs/introduction.rst
================================================
Introduction
============
Overview
--------
Supervisor is a client/server system that allows its users to control
a number of processes on UNIX-like operating systems. It was inspired
by the following:
Convenience
It is often inconvenient to need to write ``rc.d`` scripts for every
single process instance. ``rc.d`` scripts are a great
lowest-common-denominator form of process
initialization/autostart/management, but they can be painful to
write and maintain. Additionally, ``rc.d`` scripts cannot
automatically restart a crashed process and many programs do not
restart themselves properly on a crash. Supervisord starts
processes as its subprocesses, and can be configured to
automatically restart them on a crash. It can also automatically be
configured to start processes on its own invocation.
Accuracy
It's often difficult to get accurate up/down status on processes on
UNIX. Pidfiles often lie. Supervisord starts processes as
subprocesses, so it always knows the true up/down status of its
children and can be queried conveniently for this data.
Delegation
Users who need to control process state often need only to do that.
They don't want or need full-blown shell access to the machine on
which the processes are running. Processes which listen on "low"
TCP ports often need to be started and restarted as the root user (a
UNIX misfeature). It's usually the case that it's perfectly fine to
allow "normal" people to stop or restart such a process, but
providing them with shell access is often impractical, and providing
them with root access or sudo access is often impossible. It's also
(rightly) difficult to explain to them why this problem exists. If
supervisord is started as root, it is possible to allow "normal"
users to control such processes without needing to explain the
intricacies of the problem to them. Supervisorctl allows a very
limited form of access to the machine, essentially allowing users to
see process status and control supervisord-controlled subprocesses
by emitting "stop", "start", and "restart" commands from a simple
shell or web UI.
Process Groups
Processes often need to be started and stopped in groups, sometimes
even in a "priority order". It's often difficult to explain to
people how to do this. Supervisor allows you to assign priorities
to processes, and allows user to emit commands via the supervisorctl
client like "start all", and "restart all", which starts them in the
preassigned priority order. Additionally, processes can be grouped
into "process groups" and a set of logically related processes can
be stopped and started as a unit.
Features
--------
Simple
Supervisor is configured through a simple INI-style config file
that’s easy to learn. It provides many per-process options that make
your life easier like restarting failed processes and automatic log
rotation.
Centralized
Supervisor provides you with one place to start, stop, and monitor
your processes. Processes can be controlled individually or in
groups. You can configure Supervisor to provide a local or remote
command line and web interface.
Efficient
Supervisor starts its subprocesses via fork/exec and subprocesses
don’t daemonize. The operating system signals Supervisor immediately
when a process terminates, unlike some solutions that rely on
troublesome PID files and periodic polling to restart failed
processes.
Extensible
Supervisor has a simple event notification protocol that programs
written in any language can use to monitor it, and an XML-RPC
interface for control. It is also built with extension points that
can be leveraged by Python developers.
Compatible
Supervisor works on just about everything except for Windows. It is
tested and supported on Linux, Mac OS X, Solaris, and FreeBSD. It is
written entirely in Python, so installation does not require a C
compiler.
Proven
While Supervisor is very actively developed today, it is not new
software. Supervisor has been around for years and is already in use
on many servers.
Supervisor Components
---------------------
:program:`supervisord`
The server piece of supervisor is named :program:`supervisord`. It
is responsible for starting child programs at its own invocation,
responding to commands from clients, restarting crashed or exited
subprocesseses, logging its subprocess ``stdout`` and ``stderr``
output, and generating and handling "events" corresponding to points
in subprocess lifetimes.
The server process uses a configuration file. This is typically
located in :file:`/etc/supervisord.conf`. This configuration file
is a "Windows-INI" style config file. It is important to keep this
file secure via proper filesystem permissions because it may contain
unencrypted usernames and passwords.
:program:`supervisorctl`
The command-line client piece of the supervisor is named
:program:`supervisorctl`. It provides a shell-like interface to the
features provided by :program:`supervisord`. From
:program:`supervisorctl`, a user can connect to different
:program:`supervisord` processes (one at a time), get status on the
subprocesses controlled by, stop and start subprocesses of, and get lists of
running processes of a :program:`supervisord`.
The command-line client talks to the server across a UNIX domain
socket or an internet (TCP) socket. The server can assert that the
user of a client should present authentication credentials before it
allows them to perform commands. The client process typically uses
the same configuration file as the server but any configuration file
with a ``[supervisorctl]`` section in it will work.
Web Server
A (sparse) web user interface with functionality comparable to
:program:`supervisorctl` may be accessed via a browser if you start
:program:`supervisord` against an internet socket. Visit the server
URL (e.g. ``http://localhost:9001/``) to view and control process
status through the web interface after activating the configuration
file's ``[inet_http_server]`` section.
XML-RPC Interface
The same HTTP server which serves the web UI serves up an XML-RPC
interface that can be used to interrogate and control supervisor and
the programs it runs. See :ref:`xml_rpc`.
Platform Requirements
---------------------
Supervisor has been tested and is known to run on Linux (Ubuntu 18.04),
Mac OS X (10.4/10.5/10.6), and Solaris (10 for Intel) and FreeBSD 6.1.
It will likely work fine on most UNIX systems.
Supervisor will *not* run at all under any version of Windows.
Supervisor is intended to work on Python 3 version 3.4 or later
and on Python 2 version 2.7.
================================================
FILE: docs/logging.rst
================================================
Logging
=======
One of the main tasks that :program:`supervisord` performs is logging.
:program:`supervisord` logs an activity log detailing what it's doing
as it runs. It also logs child process stdout and stderr output to
other files if configured to do so.
Activity Log
------------
The activity log is the place where :program:`supervisord` logs
messages about its own health, its subprocess' state changes, any
messages that result from events, and debug and informational
messages. The path to the activity log is configured via the
``logfile`` parameter in the ``[supervisord]`` section of the
configuration file, defaulting to :file:`$CWD/supervisord.log`. If
the value of this option is the special string ``syslog``, the
activity log will be routed to the syslog service instead of being
written to a file. Sample activity log traffic is shown in the
example below. Some lines have been broken to better fit the screen.
Sample Activity Log Output
~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: text
2007-09-08 14:43:22,886 DEBG 127.0.0.1:Medusa (V1.11) started at Sat Sep 8 14:43:22 2007
Hostname: kingfish
Port:9001
2007-09-08 14:43:22,961 INFO RPC interface 'supervisor' initialized
2007-09-08 14:43:22,961 CRIT Running without any HTTP authentication checking
2007-09-08 14:43:22,962 INFO supervisord started with pid 27347
2007-09-08 14:43:23,965 INFO spawned: 'listener_00' with pid 27349
2007-09-08 14:43:23,970 INFO spawned: 'eventgen' with pid 27350
2007-09-08 14:43:23,990 INFO spawned: 'grower' with pid 27351
2007-09-08 14:43:24,059 DEBG 'listener_00' stderr output:
/Users/chrism/projects/supervisor/supervisor2/dev-sandbox/bin/python:
can't open file '/Users/chrism/projects/supervisor/supervisor2/src/supervisor/scripts/osx_eventgen_listener.py':
[Errno 2] No such file or directory
2007-09-08 14:43:24,060 DEBG fd 7 closed, stopped monitoring (stdout)>
2007-09-08 14:43:24,060 INFO exited: listener_00 (exit status 2; not expected)
2007-09-08 14:43:24,061 DEBG received SIGCHLD indicating a child quit
The activity log "level" is configured in the config file via the
``loglevel`` parameter in the ``[supervisord]`` ini file section.
When ``loglevel`` is set, messages of the specified priority, plus
those with any higher priority are logged to the activity log. For
example, if ``loglevel`` is ``error``, messages of ``error`` and
``critical`` priority will be logged. However, if loglevel is
``warn``, messages of ``warn``, ``error``, and ``critical`` will be
logged.
.. _activity_log_levels:
Activity Log Levels
~~~~~~~~~~~~~~~~~~~
The below table describes the logging levels in more detail, ordered
in highest priority to lowest. The "Config File Value" is the string
provided to the ``loglevel`` parameter in the ``[supervisord]``
section of configuration file and the "Output Code" is the code that
shows up in activity log output lines.
================= =========== ============================================
Config File Value Output Code Description
================= =========== ============================================
critical CRIT Messages that indicate a condition that
requires immediate user attention, a
supervisor state change, or an error in
supervisor itself.
error ERRO Messages that indicate a potentially
ignorable error condition (e.g. unable to
clear a log directory).
warn WARN Messages that indicate an anomalous
condition which isn't an error.
info INFO Normal informational output. This is the
default log level if none is explicitly
configured.
debug DEBG Messages useful for users trying to debug
process configuration and communications
behavior (process output, listener state
changes, event notifications).
trace TRAC Messages useful for developers trying to
debug supervisor plugins, and information
about HTTP and RPC requests and responses.
blather BLAT Messages useful for developers trying to
debug supervisor itself.
================= =========== ============================================
Activity Log Rotation
~~~~~~~~~~~~~~~~~~~~~
The activity log is "rotated" by :program:`supervisord` based on the
combination of the ``logfile_maxbytes`` and the ``logfile_backups``
parameters in the ``[supervisord]`` section of the configuration file.
When the activity log reaches ``logfile_maxbytes`` bytes, the current
log file is moved to a backup file and a new activity log file is
created. When this happens, if the number of existing backup files is
greater than or equal to ``logfile_backups``, the oldest backup file
is removed and the backup files are renamed accordingly. If the file
being written to is named :file:`supervisord.log`, when it exceeds
``logfile_maxbytes``, it is closed and renamed to
:file:`supervisord.log.1`, and if files :file:`supervisord.log.1`,
:file:`supervisord.log.2` etc. exist, then they are renamed to
:file:`supervisord.log.2`, :file:`supervisord.log.3` etc.
respectively. If ``logfile_maxbytes`` is 0, the logfile is never
rotated (and thus backups are never made). If ``logfile_backups`` is
0, no backups will be kept.
Child Process Logs
------------------
The stdout of child processes spawned by supervisor, by default, is
captured for redisplay to users of :program:`supervisorctl` and other
clients. If no specific logfile-related configuration is performed in
a ``[program:x]``, ``[fcgi-program:x]``, or ``[eventlistener:x]``
section in the configuration file, the following is true:
- :program:`supervisord` will capture the child process' stdout and
stderr output into temporary files. Each stream is captured to a
separate file. This is known as ``AUTO`` log mode.
- ``AUTO`` log files are named automatically and placed in the
directory configured as ``childlogdir`` of the ``[supervisord]``
section of the config file.
- The size of each ``AUTO`` log file is bounded by the
``{streamname}_logfile_maxbytes`` value of the program section
(where {streamname} is "stdout" or "stderr"). When it reaches that
number, it is rotated (like the activity log), based on the
``{streamname}_logfile_backups``.
The configuration keys that influence child process logging in
``[program:x]`` and ``[fcgi-program:x]`` sections are these:
``redirect_stderr``, ``stdout_logfile``, ``stdout_logfile_maxbytes``,
``stdout_logfile_backups``, ``stdout_capture_maxbytes``, ``stdout_syslog``,
``stderr_logfile``, ``stderr_logfile_maxbytes``,
``stderr_logfile_backups``, ``stderr_capture_maxbytes``, and
``stderr_syslog``.
``[eventlistener:x]`` sections may not specify
``redirect_stderr``, ``stdout_capture_maxbytes``, or
``stderr_capture_maxbytes``, but otherwise they accept the same values.
The configuration keys that influence child process logging in the
``[supervisord]`` config file section are these:
``childlogdir``, and ``nocleanup``.
.. _capture_mode:
Capture Mode
~~~~~~~~~~~~
Capture mode is an advanced feature of Supervisor. You needn't
understand capture mode unless you want to take actions based on data
parsed from subprocess output.
If a ``[program:x]`` section in the configuration file defines a
non-zero ``stdout_capture_maxbytes`` or ``stderr_capture_maxbytes``
parameter, each process represented by the program section may emit
special tokens on its stdout or stderr stream (respectively) which
will effectively cause supervisor to emit a ``PROCESS_COMMUNICATION``
event (see :ref:`events` for a description of events).
The process communications protocol relies on two tags, one which
commands supervisor to enter "capture mode" for the stream and one
which commands it to exit. When a process stream enters "capture
mode", data sent to the stream will be sent to a separate buffer in
memory, the "capture buffer", which is allowed to contain a maximum of
``capture_maxbytes`` bytes. During capture mode, when the buffer's
length exceeds ``capture_maxbytes`` bytes, the earliest data in the
buffer is discarded to make room for new data. When a process stream
exits capture mode, a ``PROCESS_COMMUNICATION`` event subtype is
emitted by supervisor, which may be intercepted by event listeners.
The tag to begin "capture mode" in a process stream is
````. The tag to exit capture mode is
````. The data between these tags may be
arbitrary, and forms the payload of the ``PROCESS_COMMUNICATION``
event. For example, if a program is set up with a
``stdout_capture_maxbytes`` of "1MB", and it emits the following on
its stdout stream:
.. code-block:: text
Hello!
In this circumstance, :program:`supervisord` will emit a
``PROCESS_COMMUNICATIONS_STDOUT`` event with data in the payload of
"Hello!".
The output of processes specified as "event listeners"
(``[eventlistener:x]`` sections) is not processed this way.
Output from these processes cannot enter capture mode.
================================================
FILE: docs/plugins.rst
================================================
Third Party Applications and Libraries
======================================
There are a number of third party applications that can be useful together
with Supervisor. This list aims to summarize them and make them easier
to find.
See README.rst for information on how to contribute to this list.
Dashboards and Tools for Multiple Supervisor Instances
------------------------------------------------------
These are tools that can monitor or control a number of Supervisor
instances running on different servers.
`cesi `_
Web-based dashboard written in Python.
`Django-Dashvisor `_
Web-based dashboard written in Python. Requires Django 1.3 or 1.4.
`Nodervisor `_
Web-based dashboard written in Node.js.
`Supervisord-Monitor `_
Web-based dashboard written in PHP.
`Supervisord-Monitor 2 `_
Modern and adaptive next gen web-based dashboard written in PHP.
`SupervisorUI `_
Another Web-based dashboard written in PHP.
`supervisorclusterctl `_
Command line tool for controlling multiple Supervisor instances
using Ansible.
`suponoff `_
Web-based dashboard written in Python 3. Requires Django 1.7 or later.
`Supvisors `_
Designed for distributed applications, written in Python 3.6. Includes an extended XML-RPC API,
a Web-based dashboard and special features such as staged start and stop.
`multivisor `_
Centralized supervisor web-based dashboard. The frontend is based on
`VueJS `_. The backend runs a `flask `_
web server. It communicates with each supervisor through a specialized supervisor
event-listener based on `zerorpc `_.
`Dart `_
Web-based dashboard and command line tool written in Python using PostgreSQL
with a REST API, event monitoring, and configuration management.
`Polyvisor `_
Web-based dashboard written in Python using `flask `_ web server.
Frontend based on `Svelte `_ result in lightweighted packages. Communicate via supervisor's event-listener.
Providing system resource management via visualized charts & easy to config processes configs via web interface.
Third Party Plugins and Libraries for Supervisor
------------------------------------------------
These are plugins and libraries that add new functionality to Supervisor.
These also includes various event listeners.
`superlance `_
Provides set of common eventlisteners that can be used to monitor
and, for example, restart when it uses too much memory etc.
`superhooks `_
Send Supervisor event notifications to HTTP1.1 webhooks.
`mr.rubber `_
An event listener that makes it possible to scale the number of
processes to the number of cores on the supervisor host.
`supervisor-wildcards `_
Implements start/stop/restart commands with wildcard support for
Supervisor. These commands run in parallel and can be much faster
than the built-in start/stop/restart commands.
`mr.laforge `_
Lets you easily make sure that ``supervisord`` and specific
processes controlled by it are running from within shell and
Python scripts. Also adds a ``kill`` command to supervisor that
makes it possible to send arbitrary signals to child processes.
`supervisor_cache `_
An extension for Supervisor that provides the ability to cache
arbitrary data directly inside a Supervisor instance as key/value
pairs. Also serves as a reference for how to write Supervisor
extensions.
`supervisor_twiddler `_
An RPC extension for Supervisor that allows Supervisor's
configuration and state to be manipulated in ways that are not
normally possible at runtime.
`supervisor-stdout `_
An event listener that sends process output to supervisord's stdout.
`supervisor-serialrestart `_
Adds a ``serialrestart`` command to ``supervisorctl`` that restarts
processes one after another rather than all at once.
`supervisor-quick `_
Adds ``quickstart``, ``quickstop``, and ``quickrestart`` commands to
``supervisorctl`` that can be faster than the built-in commands. It
works by using the non-blocking mode of the XML-RPC methods and then
polling ``supervisord``. The built-in commands use the blocking mode,
which can be slower due to ``supervisord`` implementation details.
`supervisor-logging `_
An event listener that sends process log events to an external
Syslog instance (e.g. Logstash).
`supervisor-logstash-notifier `_
An event listener plugin to stream state events to a Logstash instance.
`supervisor_cgroups `_
An event listener that enables tying Supervisor processes to a cgroup
hierarchy. It is intended to be used as a replacement for
`cgrules.conf `_.
`supervisor_checks `_
Framework to build health checks for Supervisor-based services. Health
check applications are supposed to run as event listeners in Supervisor
environment. On check failure Supervisor will attempt to restart
monitored process.
`Superfsmon `_
Watch a directory and restart programs when files change. It can monitor
a directory for changes, filter the file paths by glob patterns or regular
expressions and restart Supervisor programs individually or by group.
Libraries that integrate Third Party Applications with Supervisor
-----------------------------------------------------------------
These are libraries and plugins that makes it easier to use Supervisor
with third party applications:
`collective.recipe.supervisor `_
A buildout recipe to install supervisor.
`puppet-module-supervisor `_
Puppet module for configuring the supervisor daemon tool.
`puppet-supervisord `_
Puppet module to manage the supervisord process control system.
`ngx_supervisord `_
An nginx module providing API to communicate with supervisord and
manage (start/stop) backends on-demand.
`Supervisord-Nagios-Plugin `_
A Nagios/Icinga plugin written in Python to monitor individual supervisord processes.
`nagios-supervisord-processes `_
A Nagios/Icinga plugin written in PHP to monitor individual supervisord processes.
`supervisord-nagios `_
A plugin for supervisorctl to allow one to perform nagios-style checks
against supervisord-managed processes.
`php-supervisor-event `_
PHP classes for interacting with Supervisor event notifications.
`PHP5 Supervisor wrapper `_
PHP 5 library to manage Supervisor instances as object.
`Symfony2 SupervisorBundle `_
Provide full integration of Supervisor multiple servers management into Symfony2 project.
`sd-supervisord `_
`Server Density `_ plugin for
supervisor.
`node-supervisord `_
Node.js client for Supervisor's XML-RPC interface.
`node-supervisord-eventlistener `_
Node.js implementation of an event listener for Supervisor.
`ruby-supervisor `_
Ruby client library for Supervisor's XML-RPC interface.
`Sulphite `_
Sends supervisord events to `Graphite `_.
`supervisord.tmbundle `_
`TextMate `_ bundle for supervisord.conf.
`capistrano-supervisord `_
`Capistrano `_ recipe to deploy supervisord based services.
`capistrano-supervisor `_
Another package to control supervisord from `Capistrano `_.
`chef-supervisor `_
`Chef `_ cookbook install and configure supervisord.
`SupervisorPHP `_
Complete Supervisor suite in PHP: Client using XML-RPC interface, event listener and configuration builder implementation, console application and monitor UI.
`Supervisord-Client `_
Perl client for the supervisord XML-RPC interface.
`supervisord4j `_
Java client for Supervisor's XML-RPC interface.
`Supermann `_
Supermann monitors processes running under Supervisor and sends metrics
to `Riemann `_.
`gulp-supervisor `_
Run Supervisor as a `Gulp `_ task.
`Yeebase.Supervisor `_
Control and monitor Supervisor from a TYPO3 Flow application.
`dokku-supervisord `_
`Dokku `_ plugin that injects ``supervisord`` to run
applications.
`dokku-logging-supervisord `_
`Dokku `_ plugin that injects ``supervisord`` to run
applications. It also redirects ``stdout`` and ``stderr`` from processes to log files
(rather than the Docker default per-container JSON files).
`superslacker `_
Send Supervisor event notifications to `Slack `_.
`supervisor-alert `_
Send event notifications over `Telegram `_ or to an
arbitrary command.
`supervisor-discord `_
Send event notifications to `Discord `_ via webhooks.
================================================
FILE: docs/running.rst
================================================
.. _running:
Running Supervisor
==================
This section makes reference to a :envvar:`BINDIR` when explaining how
to run the :command:`supervisord` and :command:`supervisorctl`
commands. This is the "bindir" directory that your Python
installation has been configured with. For example, for an
installation of Python installed via ``./configure
--prefix=/usr/local/py; make; make install``, :envvar:`BINDIR` would
be :file:`/usr/local/py/bin`. Python interpreters on different
platforms use a different :envvar:`BINDIR`. Look at the output of
``setup.py install`` if you can't figure out where yours is.
Adding a Program
----------------
Before :program:`supervisord` will do anything useful for you, you'll
need to add at least one ``program`` section to its configuration.
The ``program`` section will define a program that is run and managed
when you invoke the :command:`supervisord` command. To add a program,
you'll need to edit the :file:`supervisord.conf` file.
One of the simplest possible programs to run is the UNIX
:program:`cat` program. A ``program`` section that will run ``cat``
when the :program:`supervisord` process starts up is shown below.
.. code-block:: ini
[program:foo]
command=/bin/cat
This stanza may be cut and pasted into the :file:`supervisord.conf`
file. This is the simplest possible program configuration, because it
only names a command. Program configuration sections have many other
configuration options which aren't shown here. See
:ref:`programx_section` for more information.
Running :program:`supervisord`
------------------------------
To start :program:`supervisord`, run :file:`$BINDIR/supervisord`. The
resulting process will daemonize itself and detach from the terminal.
It keeps an operations log at :file:`$CWD/supervisor.log` by default.
You may start the :command:`supervisord` executable in the foreground
by passing the ``-n`` flag on its command line. This is useful to
debug startup problems.
.. warning::
When :program:`supervisord` starts up, it will search for its
configuration file in default locations *including the current working
directory*. If you are security-conscious you will probably want to
specify a "-c" argument after the :program:`supervisord` command
specifying an absolute path to a configuration file to ensure that someone
doesn't trick you into running supervisor from within a directory that
contains a rogue ``supervisord.conf`` file. A warning is emitted when
supervisor is started as root without this ``-c`` argument.
To change the set of programs controlled by :program:`supervisord`,
edit the :file:`supervisord.conf` file and ``kill -HUP`` or otherwise
restart the :program:`supervisord` process. This file has several
example program definitions.
The :command:`supervisord` command accepts a number of command-line
options. Each of these command line options overrides any equivalent
value in the configuration file.
:command:`supervisord` Command-Line Options
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-c FILE, --configuration=FILE
The path to a :program:`supervisord` configuration file.
-n, --nodaemon
Run :program:`supervisord` in the foreground.
-s, --silent
No output directed to stdout.
-h, --help
Show :command:`supervisord` command help.
-u USER, --user=USER
UNIX username or numeric user id. If :program:`supervisord` is
started as the root user, setuid to this user as soon as possible
during startup.
-m OCTAL, --umask=OCTAL
Octal number (e.g. 022) representing the :term:`umask` that should
be used by :program:`supervisord` after it starts.
-d PATH, --directory=PATH
When supervisord is run as a daemon, cd to this directory before
daemonizing.
-l FILE, --logfile=FILE
Filename path to use as the supervisord activity log.
-y BYTES, --logfile_maxbytes=BYTES
Max size of the supervisord activity log file before a rotation
occurs. The value is suffix-multiplied, e.g "1" is one byte, "1MB"
is 1 megabyte, "1GB" is 1 gigabyte.
-z NUM, --logfile_backups=NUM
Number of backup copies of the supervisord activity log to keep
around. Each logfile will be of size ``logfile_maxbytes``.
-e LEVEL, --loglevel=LEVEL
The logging level at which supervisor should write to the activity
log. Valid levels are ``trace``, ``debug``, ``info``, ``warn``,
``error``, and ``critical``.
-j FILE, --pidfile=FILE
The filename to which supervisord should write its pid file.
-i STRING, --identifier=STRING
Arbitrary string identifier exposed by various client UIs for this
instance of supervisor.
-q PATH, --childlogdir=PATH
A path to a directory (it must already exist) where supervisor will
write its ``AUTO`` -mode child process logs.
-k, --nocleanup
Prevent :program:`supervisord` from performing cleanup (removal of
old ``AUTO`` process log files) at startup.
-a NUM, --minfds=NUM
The minimum number of file descriptors that must be available to
the supervisord process before it will start successfully.
-t, --strip_ansi
Strip ANSI escape sequences from all child log process.
-v, --version
Print the supervisord version number out to stdout and exit.
--profile_options=LIST
Comma-separated options list for profiling. Causes
:program:`supervisord` to run under a profiler, and output results
based on the options, which is a comma-separated list of the
following: ``cumulative``, ``calls``, ``callers``.
E.g. ``cumulative,callers``.
--minprocs=NUM
The minimum number of OS process slots that must be available to
the supervisord process before it will start successfully.
Running :program:`supervisorctl`
--------------------------------
To start :program:`supervisorctl`, run ``$BINDIR/supervisorctl``. A
shell will be presented that will allow you to control the processes
that are currently managed by :program:`supervisord`. Type "help" at
the prompt to get information about the supported commands.
The :command:`supervisorctl` executable may be invoked with "one time"
commands when invoked with arguments from a command line. An example:
``supervisorctl stop all``. If arguments are present on the
command-line, it will prevent the interactive shell from being
invoked. Instead, the command will be executed and ``supervisorctl``
will exit with a code of 0 for success or running and non-zero for
error. An example: ``supervisorctl status all`` would return non-zero
if any single process was not running.
If :command:`supervisorctl` is invoked in interactive mode against a
:program:`supervisord` that requires authentication, you will be asked
for authentication credentials.
:command:`supervisorctl` Command-Line Options
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-c, --configuration
Configuration file path (default /etc/supervisord.conf)
-h, --help
Print usage message and exit
-i, --interactive
Start an interactive shell after executing commands
-s, --serverurl URL
URL on which supervisord server is listening (default "http://localhost:9001").
-u, --username
Username to use for authentication with server
-p, --password
Password to use for authentication with server
-r, --history-file
Keep a readline history (if readline is available)
`action [arguments]`
Actions are commands like "tail" or "stop". If -i is specified or no action is
specified on the command line, a "shell" interpreting actions typed
interactively is started. Use the action "help" to find out about available
actions.
:command:`supervisorctl` Actions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
help
Print a list of available actions
help
Print help for
add [...]
Activates any updates in config for process/group
remove [...]
Removes process/group from active config
update
Reload config and add/remove as necessary, and will restart affected programs
update all
Reload config and add/remove as necessary, and will restart affected programs
update [...]
Update specific groups, and will restart affected programs
clear
Clear a process' log files.
clear
Clear multiple process' log files
clear all
Clear all process' log files
fg
Connect to a process in foreground mode
Press Ctrl+C to exit foreground
pid
Get the PID of supervisord.
pid
Get the PID of a single child process by name.
pid all
Get the PID of every child process, one per line.
reload
Restarts the remote supervisord
reread
Reload the daemon's configuration files, without add/remove (no restarts)
restart
Restart a process
Note: restart does not reread config files. For that, see reread and update.
restart :*
Restart all processes in a group
Note: restart does not reread config files. For that, see reread and update.
restart
Restart multiple processes or groups
Note: restart does not reread config files. For that, see reread and update.
restart all
Restart all processes
Note: restart does not reread config files. For that, see reread and update.
signal
No help on signal
start
Start a process
start :*
Start all processes in a group
start
Start multiple processes or groups
start all
Start all processes
status
Get all process status info.
status
Get status on a single process by name.
status
Get status on multiple named processes.
stop
Stop a process
stop :*
Stop all processes in a group
stop
Stop multiple processes or groups
stop all
Stop all processes
tail [-f] [stdout|stderr] (default stdout)
Output the last part of process logs
Ex:
tail -f Continuous tail of named process stdout Ctrl-C to exit.
tail -100 last 100 *bytes* of process stdout
tail stderr last 1600 *bytes* of process stderr
Signals
-------
The :program:`supervisord` program may be sent signals which cause it
to perform certain actions while it's running.
You can send any of these signals to the single :program:`supervisord`
process id. This process id can be found in the file represented by
the ``pidfile`` parameter in the ``[supervisord]`` section of the
configuration file (by default it's :file:`$CWD/supervisord.pid`).
Signal Handlers
~~~~~~~~~~~~~~~
``SIGTERM``
:program:`supervisord` and all its subprocesses will shut down.
This may take several seconds.
``SIGINT``
:program:`supervisord` and all its subprocesses will shut down.
This may take several seconds.
``SIGQUIT``
:program:`supervisord` and all its subprocesses will shut down.
This may take several seconds.
``SIGHUP``
:program:`supervisord` will stop all processes, reload the
configuration from the first config file it finds, and start all
processes.
``SIGUSR2``
:program:`supervisord` will close and reopen the main activity log
and all child log files.
Runtime Security
----------------
The developers have done their best to assure that use of a
:program:`supervisord` process running as root cannot lead to
unintended privilege escalation. But **caveat emptor**. Supervisor
is not as paranoid as something like DJ Bernstein's
:term:`daemontools`, inasmuch as :program:`supervisord` allows for
arbitrary path specifications in its configuration file to which data
may be written. Allowing arbitrary path selections can create
vulnerabilities from symlink attacks. Be careful when specifying
paths in your configuration. Ensure that the :program:`supervisord`
configuration file cannot be read from or written to by unprivileged
users and that all files installed by the supervisor package have
"sane" file permission protection settings. Additionally, ensure that
your ``PYTHONPATH`` is sane and that all Python standard
library files have adequate file permission protections.
Running :program:`supervisord` automatically on startup
-------------------------------------------------------
If you are using a distribution-packaged version of Supervisor, it should
already be integrated into the service management infrastructure of your
distribution.
There are user-contributed scripts for various operating systems at:
https://github.com/Supervisor/initscripts
There are some answers at Serverfault in case you get stuck:
`How to automatically start supervisord on Linux (Ubuntu)`__
.. __: http://serverfault.com/questions/96499/how-to-automatically-start-supervisord-on-linux-ubuntu
================================================
FILE: docs/subprocess.rst
================================================
Subprocesses
============
:program:`supervisord`'s primary purpose is to create and manage
processes based on data in its configuration file. It does this by
creating subprocesses. Each subprocess spawned by supervisor is
managed for the entirety of its lifetime by supervisord
(:program:`supervisord` is the parent process of each process it
creates). When a child dies, supervisor is notified of its death via
the ``SIGCHLD`` signal, and it performs the appropriate operation.
.. _nondaemonizing_of_subprocesses:
Nondaemonizing of Subprocesses
------------------------------
Programs meant to be run under supervisor should not daemonize
themselves. Instead, they should run in the foreground. They should
not detach from the terminal from which they are started.
The easiest way to tell if a program will run in the foreground is to
run the command that invokes the program from a shell prompt. If it
gives you control of the terminal back, but continues running, it's
daemonizing itself and that will almost certainly be the wrong way to
run it under supervisor. You want to run a command that essentially
requires you to press :kbd:`Ctrl-C` to get control of the terminal
back. If it gives you a shell prompt back after running it without
needing to press :kbd:`Ctrl-C`, it's not useful under supervisor. All
programs have options to be run in the foreground but there's no
"standard way" to do it; you'll need to read the documentation for
each program.
Below are configuration file examples that are known to start
common programs in "foreground" mode under Supervisor.
Examples of Program Configurations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Here are some "real world" program configuration examples:
Apache 2.2.6
++++++++++++
.. code-block:: ini
[program:apache2]
command=/path/to/httpd -c "ErrorLog /dev/stdout" -DFOREGROUND
redirect_stderr=true
Two Zope 2.X instances and one ZEO server
+++++++++++++++++++++++++++++++++++++++++
.. code-block:: ini
[program:zeo]
command=/path/to/runzeo
priority=1
[program:zope1]
command=/path/to/instance/home/bin/runzope
priority=2
redirect_stderr=true
[program:zope2]
command=/path/to/another/instance/home/bin/runzope
priority=2
redirect_stderr=true
Postgres 8.X
++++++++++++
.. code-block:: ini
[program:postgres]
command=/path/to/postmaster
; we use the "fast" shutdown signal SIGINT
stopsignal=INT
redirect_stderr=true
OpenLDAP :program:`slapd`
+++++++++++++++++++++++++
.. code-block:: ini
[program:slapd]
command=/path/to/slapd -f /path/to/slapd.conf -h ldap://0.0.0.0:8888
redirect_stderr=true
Other Examples
~~~~~~~~~~~~~~
Other examples of shell scripts that could be used to start services
under :program:`supervisord` can be found at
`http://thedjbway.b0llix.net/services.html
`_. These examples are
actually for :program:`daemontools` but the premise is the same for
supervisor.
Another collection of recipes for starting various programs in the
foreground is available from `http://smarden.org/runit/runscripts.html
`_.
:program:`pidproxy` Program
---------------------------
Some processes (like :program:`mysqld`) ignore signals sent to the
actual process which is spawned by :program:`supervisord`. Instead, a
"special" thread/process is created by these kinds of programs which
is responsible for handling signals. This is problematic because
:program:`supervisord` can only kill a process which it creates
itself. If a process created by :program:`supervisord` creates its
own child processes, :program:`supervisord` cannot kill them.
Fortunately, these types of programs typically write a "pidfile" which
contains the "special" process' PID, and is meant to be read and used
in order to kill the process. As a workaround for this case, a
special :program:`pidproxy` program can handle startup of these kinds
of processes. The :program:`pidproxy` program is a small shim that
starts a process, and upon the receipt of a signal, sends the signal
to the pid provided in a pidfile. A sample configuration program
entry for a pidproxy-enabled program is provided below.
.. code-block:: ini
[program:mysql]
command=/path/to/pidproxy /path/to/pidfile /path/to/mysqld_safe
The :program:`pidproxy` program is put into your configuration's
``$BINDIR`` when supervisor is installed (it is a "console script").
.. _subprocess_environment:
Subprocess Environment
----------------------
Subprocesses will inherit the environment of the shell used to start
the :program:`supervisord` program. Several environment variables
will be set by :program:`supervisord` itself in the child's
environment also, including :envvar:`SUPERVISOR_ENABLED` (a flag
indicating the process is under supervisor control),
:envvar:`SUPERVISOR_PROCESS_NAME` (the config-file-specified process
name for this process) and :envvar:`SUPERVISOR_GROUP_NAME` (the
config-file-specified process group name for the child process).
These environment variables may be overridden within the
``[supervisord]`` section config option named ``environment`` (applies
to all subprocesses) or within the per- ``[program:x]`` section
``environment`` config option (applies only to the subprocess
specified within the ``[program:x]`` section). These "environment"
settings are additive. In other words, each subprocess' environment
will consist of:
The environment variables set within the shell used to start
supervisord...
... added-to/overridden-by ...
... the environment variables set within the "environment" global
config option ...
... added-to/overridden-by ...
... supervisor-specific environment variables
(:envvar:`SUPERVISOR_ENABLED`,
:envvar:`SUPERVISOR_PROCESS_NAME`,
:envvar:`SUPERVISOR_GROUP_NAME`) ..
... added-to/overridden-by ...
... the environment variables set within the per-process
"environment" config option.
No shell is executed by :program:`supervisord` when it runs a
subprocess, so environment variables such as :envvar:`USER`,
:envvar:`PATH`, :envvar:`HOME`, :envvar:`SHELL`, :envvar:`LOGNAME`,
etc. are not changed from their defaults or otherwise reassigned.
This is particularly important to note when you are running a program
from a :program:`supervisord` run as root with a ``user=`` stanza in
the configuration. Unlike :program:`cron`, :program:`supervisord`
does not attempt to divine and override "fundamental" environment
variables like :envvar:`USER`, :envvar:`PATH`, :envvar:`HOME`, and
:envvar:`LOGNAME` when it performs a setuid to the user defined within
the ``user=`` program config option. If you need to set environment
variables for a particular program that might otherwise be set by a
shell invocation for a particular user, you must do it explicitly
within the ``environment=`` program config option. An
example of setting these environment variables is as below.
.. code-block:: ini
[program:apache2]
command=/home/chrism/bin/httpd -c "ErrorLog /dev/stdout" -DFOREGROUND
user=chrism
environment=HOME="/home/chrism",USER="chrism"
.. _process_states:
Process States
--------------
A process controlled by supervisord will be in one of the below states
at any given time. You may see these state names in various user
interface elements in clients.
``STOPPED`` (0)
The process has been stopped due to a stop request or
has never been started.
``STARTING`` (10)
The process is starting due to a start request.
``RUNNING`` (20)
The process is running.
``BACKOFF`` (30)
The process entered the ``STARTING`` state but subsequently exited
too quickly (before the time defined in ``startsecs``) to move to
the ``RUNNING`` state.
``STOPPING`` (40)
The process is stopping due to a stop request.
``EXITED`` (100)
The process exited from the ``RUNNING`` state (expectedly or
unexpectedly).
``FATAL`` (200)
The process could not be started successfully.
``UNKNOWN`` (1000)
The process is in an unknown state (:program:`supervisord`
programming error).
Each process run under supervisor progresses through these states as
per the following directed graph.
.. figure:: subprocess-transitions.png
:alt: Subprocess State Transition Graph
Subprocess State Transition Graph
A process is in the ``STOPPED`` state if it has been stopped
administratively or if it has never been started.
When an autorestarting process is in the ``BACKOFF`` state, it will be
automatically restarted by :program:`supervisord`. It will switch
between ``STARTING`` and ``BACKOFF`` states until it becomes evident
that it cannot be started because the number of ``startretries`` has
exceeded the maximum, at which point it will transition to the
``FATAL`` state.
.. note::
Retries will take increasingly more time depending on the number of
subsequent attempts made, adding one second each time.
So if you set ``startretries=3``, :program:`supervisord` will wait one,
two and then three seconds between each restart attempt, for a total of
6 seconds.
When a process is in the ``EXITED`` state, it will
automatically restart:
- never if its ``autorestart`` parameter is set to ``false``.
- unconditionally if its ``autorestart`` parameter is set to ``true``.
- conditionally if its ``autorestart`` parameter is set to
``unexpected``. If it exited with an exit code that doesn't match
one of the exit codes defined in the ``exitcodes`` configuration
parameter for the process, it will be restarted.
A process automatically transitions from ``EXITED`` to ``RUNNING`` as
a result of being configured to autorestart conditionally or
unconditionally. The number of transitions between ``RUNNING`` and
``EXITED`` is not limited in any way: it is possible to create a
configuration that endlessly restarts an exited process. This is a
feature, not a bug.
An autorestarted process will never be automatically restarted if it
ends up in the ``FATAL`` state (it must be manually restarted from
this state).
A process transitions into the ``STOPPING`` state via an
administrative stop request, and will then end up in the
``STOPPED`` state.
A process that cannot be stopped successfully will stay in the
``STOPPING`` state forever. This situation should never be reached
during normal operations as it implies that the process did not
respond to a final ``SIGKILL`` signal sent to it by supervisor, which
is "impossible" under UNIX.
State transitions which always require user action to invoke are
these:
``FATAL`` -> ``STARTING``
``RUNNING`` -> ``STOPPING``
State transitions which typically, but not always, require user
action to invoke are these, with exceptions noted:
``STOPPED`` -> ``STARTING`` (except at supervisord startup if process
is configured to autostart)
``EXITED`` -> ``STARTING`` (except if process is configured to
autorestart)
All other state transitions are managed by supervisord automatically.
================================================
FILE: docs/upgrading.rst
================================================
Upgrading Supervisor 2 to 3
===========================
The following is true when upgrading an installation from Supervisor
2.X to Supervisor 3.X:
#. In ``[program:x]`` sections, the keys ``logfile``,
``logfile_backups``, ``logfile_maxbytes``, ``log_stderr`` and
``log_stdout`` are no longer valid. Supervisor2 logged both
stderr and stdout to a single log file. Supervisor 3 logs stderr
and stdout to separate log files. You'll need to rename
``logfile`` to ``stdout_logfile``, ``logfile_backups`` to
``stdout_logfile_backups``, and ``logfile_maxbytes`` to
``stdout_logfile_maxbytes`` at the very least to preserve your
configuration. If you created program sections where
``log_stderr`` was true, to preserve the behavior of sending
stderr output to the stdout log, use the ``redirect_stderr``
boolean in the program section instead.
#. The supervisor configuration file must include the following
section verbatim for the XML-RPC interface (and thus the web
interface and :program:`supervisorctl`) to work properly:
.. code-block:: ini
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
#. The semantics of the ``autorestart`` parameter within
``[program:x]`` sections has changed. This parameter used to
accept only ``true`` or ``false``. It now accepts an additional
value, ``unexpected``, which indicates that the process should
restart from the ``EXITED`` state only if its exit code does not
match any of those represented by the ``exitcode`` parameter in
the process' configuration (implying a process crash). In
addition, the default for ``autorestart`` is now ``unexpected``
(it used to be ``true``, which meant restart unconditionally).
#. We now allow :program:`supervisord` to listen on both a UNIX
domain socket and an inet socket instead of making listening on
one mutually exclusive with listening on the other. As a result,
the options ``http_port``, ``http_username``, ``http_password``,
``sockchmod`` and ``sockchown`` are no longer part of
the ``[supervisord]`` section configuration. These have been
supplanted by two other sections: ``[unix_http_server]`` and
``[inet_http_server]``. You'll need to insert one or the other
(depending on whether you want to listen on a UNIX domain socket
or a TCP socket respectively) or both into your
:file:`supervisord.conf` file. These sections have their own
options (where applicable) for ``port``, ``username``,
``password``, ``chmod``, and ``chown``.
#. All supervisord command-line options related to ``http_port``,
``http_username``, ``http_password``, ``sockchmod`` and
``sockchown`` have been removed (see above point for rationale).
#. The option that used to be ``sockchown`` within the
``[supervisord]`` section (and is now named ``chown`` within the
``[unix_http_server]`` section) used to accept a dot-separated
(``user.group``) value. The separator now must be a
colon, e.g. ``user:group``. Unices allow for dots in
usernames, so this change is a bugfix.
================================================
FILE: docs/xmlrpc.rst
================================================
Extending Supervisor's XML-RPC API
==================================
Supervisor can be extended with new XML-RPC APIs. Several third-party
plugins already exist that can be wired into your Supervisor
configuration. You may additionally write your own. Extensible
XML-RPC interfaces is an advanced feature, introduced in version 3.0.
You needn't understand it unless you wish to use an existing
third-party RPC interface plugin or if you wish to write your own RPC
interface plugin.
.. _rpcinterface_factories:
Configuring XML-RPC Interface Factories
---------------------------------------
An additional RPC interface is configured into a supervisor
installation by adding a ``[rpcinterface:x]`` section in the
Supervisor configuration file.
In the sample config file, there is a section which is named
``[rpcinterface:supervisor]``. By default it looks like this:
.. code-block:: ini
[rpcinterface:supervisor]
supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface
This section *must* remain in the configuration for the standard setup
of supervisor to work properly. If you don't want supervisor to do
anything it doesn't already do out of the box, this is all you need to
know about this type of section.
However, if you wish to add additional XML-RPC interface namespaces to
a configuration of supervisor, you may add additional
``[rpcinterface:foo]`` sections, where "foo" represents the namespace
of the interface (from the web root), and the value named by
``supervisor.rpcinterface_factory`` is a factory callable written in
Python which should have a function signature that accepts a single
positional argument ``supervisord`` and as many keyword arguments as
required to perform configuration. Any key/value pairs defined within
the ``rpcinterface:foo`` section will be passed as keyword arguments
to the factory. Here's an example of a factory function, created in
the package ``my.package``.
.. code-block:: python
def make_another_rpcinterface(supervisord, **config):
retries = int(config.get('retries', 0))
another_rpc_interface = AnotherRPCInterface(supervisord, retries)
return another_rpc_interface
And a section in the config file meant to configure it.
.. code-block:: ini
[rpcinterface:another]
supervisor.rpcinterface_factory = my.package:make_another_rpcinterface
retries = 1
================================================
FILE: setup.cfg
================================================
[easy_install]
zip_ok = false
;Marking a wheel as universal with "universal = 1" was deprecated
;in Setuptools 75.1.0. Setting "python_tag = py2.py3" should do
;the equivalent on Setuptools 30.3.0 or later.
;
;https://github.com/pypa/setuptools/pull/4617
;https://github.com/pypa/setuptools/pull/4939
;
[bdist_wheel]
python_tag = py2.py3
================================================
FILE: setup.py
================================================
##############################################################################
#
# Copyright (c) 2006-2015 Agendaless Consulting and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the BSD-like license at
# http://www.repoze.org/LICENSE.txt. A copy of the license should accompany
# this distribution. THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL
# EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND
# FITNESS FOR A PARTICULAR PURPOSE
#
##############################################################################
import os
import sys
py_version = sys.version_info[:2]
if py_version < (2, 7):
raise RuntimeError('On Python 2, Supervisor requires Python 2.7 or later')
elif (3, 0) < py_version < (3, 4):
raise RuntimeError('On Python 3, Supervisor requires Python 3.4 or later')
# setuptools is required as a runtime dependency only on Python < 3.8.
# See the comments in supervisor/compat.py. An environment marker
# like "setuptools; python_version < '3.8'" is not used here because
# it breaks installation via "python setup.py install". See also the
# discussion at: https://github.com/Supervisor/supervisor/issues/1692
if py_version < (3, 8):
try:
import pkg_resources
except ImportError:
raise RuntimeError(
"On Python < 3.8, Supervisor requires setuptools as a runtime"
" dependency because pkg_resources is used to load plugins"
)
from setuptools import setup, find_packages
here = os.path.abspath(os.path.dirname(__file__))
try:
with open(os.path.join(here, 'README.rst'), 'r') as f:
README = f.read()
with open(os.path.join(here, 'CHANGES.rst'), 'r') as f:
CHANGES = f.read()
except Exception:
README = """\
Supervisor is a client/server system that allows its users to
control a number of processes on UNIX-like operating systems. """
CHANGES = ''
CLASSIFIERS = [
'Development Status :: 5 - Production/Stable',
'Environment :: No Input/Output (Daemon)',
'Intended Audience :: System Administrators',
'Natural Language :: English',
'Operating System :: POSIX',
'Topic :: System :: Boot',
'Topic :: System :: Monitoring',
'Topic :: System :: Systems Administration',
"Programming Language :: Python",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
]
version_txt = os.path.join(here, 'supervisor/version.txt')
with open(version_txt, 'r') as f:
supervisor_version = f.read().strip()
dist = setup(
name='supervisor',
version=supervisor_version,
license='BSD-derived (http://www.repoze.org/LICENSE.txt)',
url='http://supervisord.org/',
project_urls={
'Changelog': 'http://supervisord.org/changelog',
'Documentation': 'http://supervisord.org',
'Issue Tracker': 'https://github.com/Supervisor/supervisor',
},
description="A system for controlling process state under UNIX",
long_description=README + '\n\n' + CHANGES,
classifiers=CLASSIFIERS,
author="Chris McDonough",
author_email="chrism@plope.com",
packages=find_packages(),
install_requires=[],
extras_require={
'test': ['pytest', 'pytest-cov']
},
include_package_data=True,
zip_safe=False,
entry_points={
'console_scripts': [
'supervisord = supervisor.supervisord:main',
'supervisorctl = supervisor.supervisorctl:main',
'echo_supervisord_conf = supervisor.confecho:main',
'pidproxy = supervisor.pidproxy:main',
],
},
)
================================================
FILE: supervisor/__init__.py
================================================
# this is a package
================================================
FILE: supervisor/childutils.py
================================================
import sys
import time
from supervisor.compat import xmlrpclib
from supervisor.compat import long
from supervisor.compat import as_string
from supervisor.xmlrpc import SupervisorTransport
from supervisor.events import ProcessCommunicationEvent
from supervisor.dispatchers import PEventListenerDispatcher
def getRPCTransport(env):
u = env.get('SUPERVISOR_USERNAME', '')
p = env.get('SUPERVISOR_PASSWORD', '')
return SupervisorTransport(u, p, env['SUPERVISOR_SERVER_URL'])
def getRPCInterface(env):
# dumbass ServerProxy won't allow us to pass in a non-HTTP url,
# so we fake the url we pass into it and always use the transport's
# 'serverurl' to figure out what to attach to
return xmlrpclib.ServerProxy('http://127.0.0.1', getRPCTransport(env))
def get_headers(line):
return dict([ x.split(':') for x in line.split() ])
def eventdata(payload):
headerinfo, data = payload.split('\n', 1)
headers = get_headers(headerinfo)
return headers, data
def get_asctime(now=None):
if now is None: # for testing
now = time.time() # pragma: no cover
msecs = (now - long(now)) * 1000
part1 = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(now))
asctime = '%s,%03d' % (part1, msecs)
return asctime
class ProcessCommunicationsProtocol:
def send(self, msg, fp=sys.stdout):
fp.write(ProcessCommunicationEvent.BEGIN_TOKEN)
fp.write(msg)
fp.write(ProcessCommunicationEvent.END_TOKEN)
fp.flush()
def stdout(self, msg):
return self.send(msg, sys.stdout)
def stderr(self, msg):
return self.send(msg, sys.stderr)
pcomm = ProcessCommunicationsProtocol()
class EventListenerProtocol:
def wait(self, stdin=sys.stdin, stdout=sys.stdout):
self.ready(stdout)
line = stdin.readline()
headers = get_headers(line)
payload = stdin.read(int(headers['len']))
return headers, payload
def ready(self, stdout=sys.stdout):
stdout.write(as_string(PEventListenerDispatcher.READY_FOR_EVENTS_TOKEN))
stdout.flush()
def ok(self, stdout=sys.stdout):
self.send('OK', stdout)
def fail(self, stdout=sys.stdout):
self.send('FAIL', stdout)
def send(self, data, stdout=sys.stdout):
resultlen = len(data)
result = '%s%s\n%s' % (as_string(PEventListenerDispatcher.RESULT_TOKEN_START),
str(resultlen),
data)
stdout.write(result)
stdout.flush()
listener = EventListenerProtocol()
================================================
FILE: supervisor/compat.py
================================================
from __future__ import absolute_import
import sys
PY2 = sys.version_info[0] == 2
if PY2: # pragma: no cover
long = long
raw_input = raw_input
unicode = unicode
unichr = unichr
basestring = basestring
def as_bytes(s, encoding='utf-8'):
if isinstance(s, str):
return s
else:
return s.encode(encoding)
def as_string(s, encoding='utf-8'):
if isinstance(s, unicode):
return s
else:
return s.decode(encoding)
def is_text_stream(stream):
try:
if isinstance(stream, file):
return 'b' not in stream.mode
except NameError: # python 3
pass
try:
import _io
return isinstance(stream, _io._TextIOBase)
except ImportError:
import io
return isinstance(stream, io.TextIOWrapper)
else: # pragma: no cover
long = int
basestring = str
raw_input = input
unichr = chr
class unicode(str):
def __init__(self, string, encoding, errors):
str.__init__(self, string)
def as_bytes(s, encoding='utf8'):
if isinstance(s, bytes):
return s
else:
return s.encode(encoding)
def as_string(s, encoding='utf8'):
if isinstance(s, str):
return s
else:
return s.decode(encoding)
def is_text_stream(stream):
import _io
return isinstance(stream, _io._TextIOBase)
try: # pragma: no cover
import xmlrpc.client as xmlrpclib
except ImportError: # pragma: no cover
import xmlrpclib
try: # pragma: no cover
import urllib.parse as urlparse
import urllib.parse as urllib
except ImportError: # pragma: no cover
import urlparse
import urllib
try: # pragma: no cover
from hashlib import sha1
except ImportError: # pragma: no cover
from sha import new as sha1
try: # pragma: no cover
import syslog
except ImportError: # pragma: no cover
syslog = None
try: # pragma: no cover
import ConfigParser
except ImportError: # pragma: no cover
import configparser as ConfigParser
try: # pragma: no cover
from StringIO import StringIO
except ImportError: # pragma: no cover
from io import StringIO
try: # pragma: no cover
from sys import maxint
except ImportError: # pragma: no cover
from sys import maxsize as maxint
try: # pragma: no cover
import http.client as httplib
except ImportError: # pragma: no cover
import httplib
try: # pragma: no cover
from base64 import decodebytes as decodestring, encodebytes as encodestring
except ImportError: # pragma: no cover
from base64 import decodestring, encodestring
try: # pragma: no cover
from xmlrpc.client import Fault
except ImportError: # pragma: no cover
from xmlrpclib import Fault
try: # pragma: no cover
from string import ascii_letters as letters
except ImportError: # pragma: no cover
from string import letters
try: # pragma: no cover
from hashlib import md5
except ImportError: # pragma: no cover
from md5 import md5
try: # pragma: no cover
import thread
except ImportError: # pragma: no cover
import _thread as thread
try: # pragma: no cover
from types import StringTypes
except ImportError: # pragma: no cover
StringTypes = (str,)
try: # pragma: no cover
from html import escape
except ImportError: # pragma: no cover
from cgi import escape
try: # pragma: no cover
import html.entities as htmlentitydefs
except ImportError: # pragma: no cover
import htmlentitydefs
try: # pragma: no cover
from html.parser import HTMLParser
except ImportError: # pragma: no cover
from HTMLParser import HTMLParser
# Begin check for working shlex posix mode
# https://github.com/Supervisor/supervisor/issues/328
# https://github.com/Supervisor/supervisor/issues/873
# https://bugs.python.org/issue21999
from shlex import shlex as _shlex
_shlex_posix_expectations = {
'foo="",bar=a': ['foo', '=', '', ',', 'bar', '=', 'a'],
"'')abc": ['', ')', 'abc']
}
shlex_posix_works = all(
list(_shlex(_input, posix=True)) == _expected
for _input, _expected in _shlex_posix_expectations.items()
)
# End check for working shlex posix mode
# Begin importlib/setuptools compatibility code
# Supervisor used pkg_resources (a part of setuptools) to load package
# resources for 15 years, until setuptools 67.5.0 (2023-03-05) deprecated
# the use of pkg_resources. On Python 3.8 or later, Supervisor now uses
# importlib (part of Python 3 stdlib). Unfortunately, on Python < 3.8,
# Supervisor needs to use pkg_resources despite its deprecation. The PyPI
# backport packages "importlib-resources" and "importlib-metadata" couldn't
# be added as dependencies to Supervisor because they require even more
# dependencies that would likely cause some Supervisor installs to fail.
from warnings import filterwarnings as _fw
_fw("ignore", message="pkg_resources is deprecated as an API")
try: # pragma: no cover
from importlib.metadata import EntryPoint as _EntryPoint
def import_spec(spec):
return _EntryPoint(None, spec, None).load()
except ImportError: # pragma: no cover
from pkg_resources import EntryPoint as _EntryPoint
def import_spec(spec):
ep = _EntryPoint.parse("x=" + spec)
if hasattr(ep, 'resolve'):
# this is available on setuptools >= 10.2
return ep.resolve()
else:
# this causes a DeprecationWarning on setuptools >= 11.3
return ep.load(False)
try: # pragma: no cover
import importlib.resources as _importlib_resources
if hasattr(_importlib_resources, "files"):
def resource_filename(package, path):
return str(_importlib_resources.files(package).joinpath(path))
else:
# fall back to deprecated .path if .files is not available
def resource_filename(package, path):
with _importlib_resources.path(package, '__init__.py') as p:
return str(p.parent.joinpath(path))
except ImportError: # pragma: no cover
from pkg_resources import resource_filename
# End importlib/setuptools compatibility code
================================================
FILE: supervisor/confecho.py
================================================
import sys
from supervisor.compat import as_string
from supervisor.compat import resource_filename
def main(out=sys.stdout):
with open(resource_filename(__package__, 'skel/sample.conf'), 'r') as f:
out.write(as_string(f.read()))
================================================
FILE: supervisor/datatypes.py
================================================
import grp
import os
import pwd
import signal
import socket
import shlex
from supervisor.compat import shlex_posix_works
from supervisor.compat import urlparse
from supervisor.compat import long
from supervisor.loggers import getLevelNumByDescription
def process_or_group_name(name):
"""Ensures that a process or group name is not created with
characters that break the eventlistener protocol or web UI URLs"""
s = str(name).strip()
for character in ' :/':
if character in s:
raise ValueError("Invalid name: %r because of character: %r" % (name, character))
return s
def integer(value):
try:
return int(value)
except (ValueError, OverflowError):
return long(value) # why does this help ValueError? (CM)
TRUTHY_STRINGS = ('yes', 'true', 'on', '1')
FALSY_STRINGS = ('no', 'false', 'off', '0')
def boolean(s):
"""Convert a string value to a boolean value."""
ss = str(s).lower()
if ss in TRUTHY_STRINGS:
return True
elif ss in FALSY_STRINGS:
return False
else:
raise ValueError("not a valid boolean value: " + repr(s))
def list_of_strings(arg):
if not arg:
return []
try:
return [x.strip() for x in arg.split(',')]
except:
raise ValueError("not a valid list of strings: " + repr(arg))
def list_of_ints(arg):
if not arg:
return []
else:
try:
return list(map(int, arg.split(",")))
except:
raise ValueError("not a valid list of ints: " + repr(arg))
def list_of_exitcodes(arg):
try:
vals = list_of_ints(arg)
for val in vals:
if (val > 255) or (val < 0):
raise ValueError('Invalid exit code "%s"' % val)
return vals
except:
raise ValueError("not a valid list of exit codes: " + repr(arg))
def dict_of_key_value_pairs(arg):
""" parse KEY=val,KEY2=val2 into {'KEY':'val', 'KEY2':'val2'}
Quotes can be used to allow commas in the value
"""
lexer = shlex.shlex(str(arg), posix=shlex_posix_works)
lexer.wordchars += '/.+-():'
tokens = list(lexer)
tokens_len = len(tokens)
D = {}
i = 0
while i < tokens_len:
k_eq_v = tokens[i:i+3]
if len(k_eq_v) != 3 or k_eq_v[1] != '=':
raise ValueError(
"Unexpected end of key/value pairs in value '%s'" % arg)
k, v = k_eq_v[0], k_eq_v[2]
if not shlex_posix_works:
v = v.strip('\'"')
D[k] = v
i += 4
return D
class Automatic:
pass
class Syslog:
"""TODO deprecated; remove this special 'syslog' filename in the future"""
pass
LOGFILE_NONES = ('none', 'off', None)
LOGFILE_AUTOS = (Automatic, 'auto')
LOGFILE_SYSLOGS = (Syslog, 'syslog')
def logfile_name(val):
if hasattr(val, 'lower'):
coerced = val.lower()
else:
coerced = val
if coerced in LOGFILE_NONES:
return None
elif coerced in LOGFILE_AUTOS:
return Automatic
elif coerced in LOGFILE_SYSLOGS:
return Syslog
else:
return existing_dirpath(val)
class RangeCheckedConversion:
"""Conversion helper that range checks another conversion."""
def __init__(self, conversion, min=None, max=None):
self._min = min
self._max = max
self._conversion = conversion
def __call__(self, value):
v = self._conversion(value)
if self._min is not None and v < self._min:
raise ValueError("%s is below lower bound (%s)"
% (repr(v), repr(self._min)))
if self._max is not None and v > self._max:
raise ValueError("%s is above upper bound (%s)"
% (repr(v), repr(self._max)))
return v
port_number = RangeCheckedConversion(integer, min=1, max=0xffff).__call__
def inet_address(s):
# returns (host, port) tuple
host = ''
if ":" in s:
host, s = s.rsplit(":", 1)
if not s:
raise ValueError("no port number specified in %r" % s)
port = port_number(s)
host = host.lower()
else:
try:
port = port_number(s)
except ValueError:
raise ValueError("not a valid port number: %r " %s)
if not host or host == '*':
host = ''
return host, port
class SocketAddress:
def __init__(self, s):
# returns (family, address) tuple
if "/" in s or s.find(os.sep) >= 0 or ":" not in s:
self.family = getattr(socket, "AF_UNIX", None)
self.address = s
else:
self.family = socket.AF_INET
self.address = inet_address(s)
class SocketConfig:
""" Abstract base class which provides a uniform abstraction
for TCP vs Unix sockets """
url = '' # socket url
addr = None #socket addr
backlog = None # socket listen backlog
def __repr__(self):
return '<%s at %s for %s>' % (self.__class__,
id(self),
self.url)
def __str__(self):
return str(self.url)
def __eq__(self, other):
if not isinstance(other, SocketConfig):
return False
if self.url != other.url:
return False
return True
def __ne__(self, other):
return not self.__eq__(other)
def get_backlog(self):
return self.backlog
def addr(self): # pragma: no cover
raise NotImplementedError
def create_and_bind(self): # pragma: no cover
raise NotImplementedError
class InetStreamSocketConfig(SocketConfig):
""" TCP socket config helper """
host = None # host name or ip to bind to
port = None # integer port to bind to
def __init__(self, host, port, **kwargs):
self.host = host.lower()
self.port = port_number(port)
self.url = 'tcp://%s:%d' % (self.host, self.port)
self.backlog = kwargs.get('backlog', None)
def addr(self):
return self.host, self.port
def create_and_bind(self):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(self.addr())
except:
sock.close()
raise
return sock
class UnixStreamSocketConfig(SocketConfig):
""" Unix domain socket config helper """
path = None # Unix domain socket path
mode = None # Unix permission mode bits for socket
owner = None # Tuple (uid, gid) for Unix ownership of socket
sock = None # socket object
def __init__(self, path, **kwargs):
self.path = path
self.url = 'unix://%s' % path
self.mode = kwargs.get('mode', None)
self.owner = kwargs.get('owner', None)
self.backlog = kwargs.get('backlog', None)
def addr(self):
return self.path
def create_and_bind(self):
if os.path.exists(self.path):
os.unlink(self.path)
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
sock.bind(self.addr())
self._chown()
self._chmod()
except:
sock.close()
if os.path.exists(self.path):
os.unlink(self.path)
raise
return sock
def get_mode(self):
return self.mode
def get_owner(self):
return self.owner
def _chmod(self):
if self.mode is not None:
try:
os.chmod(self.path, self.mode)
except Exception as e:
raise ValueError("Could not change permissions of socket "
+ "file: %s" % e)
def _chown(self):
if self.owner is not None:
try:
os.chown(self.path, self.owner[0], self.owner[1])
except Exception as e:
raise ValueError("Could not change ownership of socket file: "
+ "%s" % e)
def colon_separated_user_group(arg):
""" Find a user ID and group ID from a string like 'user:group'. Returns
a tuple (uid, gid). If the string only contains a user like 'user'
then (uid, -1) will be returned. Raises ValueError if either
the user or group can't be resolved to valid IDs on the system. """
try:
parts = arg.split(':', 1)
if len(parts) == 1:
uid = name_to_uid(parts[0])
gid = -1
else:
uid = name_to_uid(parts[0])
gid = name_to_gid(parts[1])
return (uid, gid)
except:
raise ValueError('Invalid user:group definition %s' % arg)
def name_to_uid(name):
""" Find a user ID from a string containing a user name or ID.
Raises ValueError if the string can't be resolved to a valid
user ID on the system. """
try:
uid = int(name)
except ValueError:
try:
pwdrec = pwd.getpwnam(name)
except KeyError:
raise ValueError("Invalid user name %s" % name)
uid = pwdrec[2]
else:
try:
pwd.getpwuid(uid) # check if uid is valid
except KeyError:
raise ValueError("Invalid user id %s" % name)
return uid
def name_to_gid(name):
""" Find a group ID from a string containing a group name or ID.
Raises ValueError if the string can't be resolved to a valid
group ID on the system. """
try:
gid = int(name)
except ValueError:
try:
grprec = grp.getgrnam(name)
except KeyError:
raise ValueError("Invalid group name %s" % name)
gid = grprec[2]
else:
try:
grp.getgrgid(gid) # check if gid is valid
except KeyError:
raise ValueError("Invalid group id %s" % name)
return gid
def gid_for_uid(uid):
pwrec = pwd.getpwuid(uid)
return pwrec[3]
def octal_type(arg):
try:
return int(arg, 8)
except (TypeError, ValueError):
raise ValueError('%s can not be converted to an octal type' % arg)
def existing_directory(v):
nv = os.path.expanduser(v)
if os.path.isdir(nv):
return nv
raise ValueError('%s is not an existing directory' % v)
def existing_dirpath(v):
nv = os.path.expanduser(v)
dir = os.path.dirname(nv)
if not dir:
# relative pathname with no directory component
return nv
if os.path.isdir(dir):
return nv
raise ValueError('The directory named as part of the path %s '
'does not exist' % v)
def logging_level(value):
s = str(value).lower()
level = getLevelNumByDescription(s)
if level is None:
raise ValueError('bad logging level name %r' % value)
return level
class SuffixMultiplier:
# d is a dictionary of suffixes to integer multipliers. If no suffixes
# match, default is the multiplier. Matches are case insensitive. Return
# values are in the fundamental unit.
def __init__(self, d, default=1):
self._d = d
self._default = default
# all keys must be the same size
self._keysz = None
for k in d.keys():
if self._keysz is None:
self._keysz = len(k)
else:
assert self._keysz == len(k)
def __call__(self, v):
v = v.lower()
for s, m in self._d.items():
if v[-self._keysz:] == s:
return int(v[:-self._keysz]) * m
return int(v) * self._default
byte_size = SuffixMultiplier({'kb': 1024,
'mb': 1024*1024,
'gb': 1024*1024*long(1024),})
def url(value):
scheme, netloc, path, params, query, fragment = urlparse.urlparse(value)
if scheme and (netloc or path):
return value
raise ValueError("value %r is not a URL" % value)
# all valid signal numbers
SIGNUMS = [ getattr(signal, k) for k in dir(signal) if k.startswith('SIG') ]
def signal_number(value):
try:
num = int(value)
except (ValueError, TypeError):
name = value.strip().upper()
if not name.startswith('SIG'):
name = 'SIG' + name
num = getattr(signal, name, None)
if num is None:
raise ValueError('value %r is not a valid signal name' % value)
if num not in SIGNUMS:
raise ValueError('value %r is not a valid signal number' % value)
return num
class RestartWhenExitUnexpected:
pass
class RestartUnconditionally:
pass
def auto_restart(value):
value = str(value.lower())
computed_value = value
if value in TRUTHY_STRINGS:
computed_value = RestartUnconditionally
elif value in FALSY_STRINGS:
computed_value = False
elif value == 'unexpected':
computed_value = RestartWhenExitUnexpected
if computed_value not in (RestartWhenExitUnexpected,
RestartUnconditionally, False):
raise ValueError("invalid 'autorestart' value %r" % value)
return computed_value
def profile_options(value):
options = [x.lower() for x in list_of_strings(value) ]
sort_options = []
callers = False
for thing in options:
if thing != 'callers':
sort_options.append(thing)
else:
callers = True
return sort_options, callers
================================================
FILE: supervisor/dispatchers.py
================================================
import errno
from supervisor.medusa.asynchat_25 import find_prefix_at_end
from supervisor.medusa.asyncore_25 import compact_traceback
from supervisor.compat import as_string
from supervisor.events import notify
from supervisor.events import EventRejectedEvent
from supervisor.events import ProcessLogStderrEvent
from supervisor.events import ProcessLogStdoutEvent
from supervisor.states import EventListenerStates
from supervisor.states import getEventListenerStateDescription
from supervisor import loggers
class PDispatcher:
""" Asyncore dispatcher for mainloop, representing a process channel
(stdin, stdout, or stderr). This class is abstract. """
closed = False # True if close() has been called
def __init__(self, process, channel, fd):
self.process = process # process which "owns" this dispatcher
self.channel = channel # 'stderr' or 'stdout'
self.fd = fd
self.closed = False # True if close() has been called
def __repr__(self):
return '<%s at %s for %s (%s)>' % (self.__class__.__name__,
id(self),
self.process,
self.channel)
def readable(self):
raise NotImplementedError
def writable(self):
raise NotImplementedError
def handle_read_event(self):
raise NotImplementedError
def handle_write_event(self):
raise NotImplementedError
def handle_error(self):
nil, t, v, tbinfo = compact_traceback()
self.process.config.options.logger.critical(
'uncaptured python exception, closing channel %s (%s:%s %s)' % (
repr(self),
t,
v,
tbinfo
)
)
self.close()
def close(self):
if not self.closed:
self.process.config.options.logger.debug(
'fd %s closed, stopped monitoring %s' % (self.fd, self))
self.closed = True
def flush(self):
pass
class POutputDispatcher(PDispatcher):
"""
Dispatcher for one channel (stdout or stderr) of one process.
Serves several purposes:
- capture output sent within and
tags and signal a ProcessCommunicationEvent
by calling notify(event).
- route the output to the appropriate log handlers as specified in the
config.
"""
childlog = None # the current logger (normallog or capturelog)
normallog = None # the "normal" (non-capture) logger
capturelog = None # the logger used while we're in capturemode
capturemode = False # are we capturing process event data
output_buffer = b'' # data waiting to be logged
def __init__(self, process, event_type, fd):
"""
Initialize the dispatcher.
`event_type` should be one of ProcessLogStdoutEvent or
ProcessLogStderrEvent
"""
self.process = process
self.event_type = event_type
self.fd = fd
self.channel = self.event_type.channel
self._init_normallog()
self._init_capturelog()
self.childlog = self.normallog
# all code below is purely for minor speedups
begintoken = self.event_type.BEGIN_TOKEN
endtoken = self.event_type.END_TOKEN
self.begintoken_data = (begintoken, len(begintoken))
self.endtoken_data = (endtoken, len(endtoken))
self.mainlog_level = loggers.LevelsByName.DEBG
config = self.process.config
self.log_to_mainlog = config.options.loglevel <= self.mainlog_level
self.stdout_events_enabled = config.stdout_events_enabled
self.stderr_events_enabled = config.stderr_events_enabled
def _init_normallog(self):
"""
Configure the "normal" (non-capture) log for this channel of this
process. Sets self.normallog if logging is enabled.
"""
config = self.process.config
channel = self.channel
logfile = getattr(config, '%s_logfile' % channel)
maxbytes = getattr(config, '%s_logfile_maxbytes' % channel)
backups = getattr(config, '%s_logfile_backups' % channel)
to_syslog = getattr(config, '%s_syslog' % channel)
if logfile or to_syslog:
self.normallog = config.options.getLogger()
if logfile:
loggers.handle_file(
self.normallog,
filename=logfile,
fmt='%(message)s',
rotating=not not maxbytes, # optimization
maxbytes=maxbytes,
backups=backups
)
if to_syslog:
loggers.handle_syslog(
self.normallog,
fmt=config.name + ' %(message)s'
)
def _init_capturelog(self):
"""
Configure the capture log for this process. This log is used to
temporarily capture output when special output is detected.
Sets self.capturelog if capturing is enabled.
"""
capture_maxbytes = getattr(self.process.config,
'%s_capture_maxbytes' % self.channel)
if capture_maxbytes:
self.capturelog = self.process.config.options.getLogger()
loggers.handle_boundIO(
self.capturelog,
fmt='%(message)s',
maxbytes=capture_maxbytes,
)
def removelogs(self):
for log in (self.normallog, self.capturelog):
if log is not None:
for handler in log.handlers:
handler.remove()
handler.reopen()
def reopenlogs(self):
for log in (self.normallog, self.capturelog):
if log is not None:
for handler in log.handlers:
handler.reopen()
def _log(self, data):
if data:
config = self.process.config
if config.options.strip_ansi:
data = stripEscapes(data)
if self.childlog:
self.childlog.info(data)
if self.log_to_mainlog:
if not isinstance(data, bytes):
text = data
else:
try:
text = data.decode('utf-8')
except UnicodeDecodeError:
text = 'Undecodable: %r' % data
msg = '%(name)r %(channel)s output:\n%(data)s'
config.options.logger.log(
self.mainlog_level, msg, name=config.name,
channel=self.channel, data=text)
if self.channel == 'stdout':
if self.stdout_events_enabled:
notify(
ProcessLogStdoutEvent(self.process,
self.process.pid, data)
)
else: # channel == stderr
if self.stderr_events_enabled:
notify(
ProcessLogStderrEvent(self.process,
self.process.pid, data)
)
def record_output(self):
if self.capturelog is None:
# shortcut trying to find capture data
data = self.output_buffer
self.output_buffer = b''
self._log(data)
return
if self.capturemode:
token, tokenlen = self.endtoken_data
else:
token, tokenlen = self.begintoken_data
if len(self.output_buffer) <= tokenlen:
return # not enough data
data = self.output_buffer
self.output_buffer = b''
try:
before, after = data.split(token, 1)
except ValueError:
after = None
index = find_prefix_at_end(data, token)
if index:
self.output_buffer = self.output_buffer + data[-index:]
data = data[:-index]
self._log(data)
else:
self._log(before)
self.toggle_capturemode()
self.output_buffer = after
if after:
self.record_output()
def toggle_capturemode(self):
self.capturemode = not self.capturemode
if self.capturelog is not None:
if self.capturemode:
self.childlog = self.capturelog
else:
for handler in self.capturelog.handlers:
handler.flush()
data = self.capturelog.getvalue()
channel = self.channel
procname = self.process.config.name
event = self.event_type(self.process, self.process.pid, data)
notify(event)
msg = "%(procname)r %(channel)s emitted a comm event"
self.process.config.options.logger.debug(msg,
procname=procname,
channel=channel)
for handler in self.capturelog.handlers:
handler.remove()
handler.reopen()
self.childlog = self.normallog
def writable(self):
return False
def readable(self):
if self.closed:
return False
return True
def handle_read_event(self):
data = self.process.config.options.readfd(self.fd)
self.output_buffer += data
self.record_output()
if not data:
# if we get no data back from the pipe, it means that the
# child process has ended. See
# mail.python.org/pipermail/python-dev/2004-August/046850.html
self.close()
class PEventListenerDispatcher(PDispatcher):
""" An output dispatcher that monitors and changes a process'
listener_state """
childlog = None # the logger
state_buffer = b'' # data waiting to be reviewed for state changes
READY_FOR_EVENTS_TOKEN = b'READY\n'
RESULT_TOKEN_START = b'RESULT '
READY_FOR_EVENTS_LEN = len(READY_FOR_EVENTS_TOKEN)
RESULT_TOKEN_START_LEN = len(RESULT_TOKEN_START)
def __init__(self, process, channel, fd):
PDispatcher.__init__(self, process, channel, fd)
# the initial state of our listener is ACKNOWLEDGED; this is a
# "busy" state that implies we're awaiting a READY_FOR_EVENTS_TOKEN
self.process.listener_state = EventListenerStates.ACKNOWLEDGED
self.process.event = None
self.result = b''
self.resultlen = None
logfile = getattr(process.config, '%s_logfile' % channel)
if logfile:
maxbytes = getattr(process.config, '%s_logfile_maxbytes' % channel)
backups = getattr(process.config, '%s_logfile_backups' % channel)
self.childlog = process.config.options.getLogger()
loggers.handle_file(
self.childlog,
logfile,
'%(message)s',
rotating=not not maxbytes, # optimization
maxbytes=maxbytes,
backups=backups,
)
def removelogs(self):
if self.childlog is not None:
for handler in self.childlog.handlers:
handler.remove()
handler.reopen()
def reopenlogs(self):
if self.childlog is not None:
for handler in self.childlog.handlers:
handler.reopen()
def writable(self):
return False
def readable(self):
if self.closed:
return False
return True
def handle_read_event(self):
data = self.process.config.options.readfd(self.fd)
if data:
self.state_buffer += data
procname = self.process.config.name
msg = '%r %s output:\n%s' % (procname, self.channel, data)
self.process.config.options.logger.debug(msg)
if self.childlog:
if self.process.config.options.strip_ansi:
data = stripEscapes(data)
self.childlog.info(data)
else:
# if we get no data back from the pipe, it means that the
# child process has ended. See
# mail.python.org/pipermail/python-dev/2004-August/046850.html
self.close()
self.handle_listener_state_change()
def handle_listener_state_change(self):
data = self.state_buffer
if not data:
return
process = self.process
procname = process.config.name
state = process.listener_state
if state == EventListenerStates.UNKNOWN:
# this is a fatal state
self.state_buffer = b''
return
if state == EventListenerStates.ACKNOWLEDGED:
if len(data) < self.READY_FOR_EVENTS_LEN:
# not enough info to make a decision
return
elif data.startswith(self.READY_FOR_EVENTS_TOKEN):
self._change_listener_state(EventListenerStates.READY)
tokenlen = self.READY_FOR_EVENTS_LEN
self.state_buffer = self.state_buffer[tokenlen:]
process.event = None
else:
self._change_listener_state(EventListenerStates.UNKNOWN)
self.state_buffer = b''
process.event = None
if self.state_buffer:
# keep going til its too short
self.handle_listener_state_change()
else:
return
elif state == EventListenerStates.READY:
# the process sent some spurious data, be strict about it
self._change_listener_state(EventListenerStates.UNKNOWN)
self.state_buffer = b''
process.event = None
return
elif state == EventListenerStates.BUSY:
if self.resultlen is None:
# we haven't begun gathering result data yet
pos = data.find(b'\n')
if pos == -1:
# we can't make a determination yet, we dont have a full
# results line
return
result_line = self.state_buffer[:pos]
self.state_buffer = self.state_buffer[pos+1:] # rid LF
resultlen = result_line[self.RESULT_TOKEN_START_LEN:]
try:
self.resultlen = int(resultlen)
except ValueError:
try:
result_line = as_string(result_line)
except UnicodeDecodeError:
result_line = 'Undecodable: %r' % result_line
process.config.options.logger.warn(
'%s: bad result line: \'%s\'' % (procname, result_line)
)
self._change_listener_state(EventListenerStates.UNKNOWN)
self.state_buffer = b''
notify(EventRejectedEvent(process, process.event))
process.event = None
return
else:
needed = self.resultlen - len(self.result)
if needed:
self.result += self.state_buffer[:needed]
self.state_buffer = self.state_buffer[needed:]
needed = self.resultlen - len(self.result)
if not needed:
self.handle_result(self.result)
self.process.event = None
self.result = b''
self.resultlen = None
if self.state_buffer:
# keep going til its too short
self.handle_listener_state_change()
def handle_result(self, result):
process = self.process
procname = process.config.name
logger = process.config.options.logger
try:
self.process.group.config.result_handler(process.event, result)
logger.debug('%s: event was processed' % procname)
self._change_listener_state(EventListenerStates.ACKNOWLEDGED)
except RejectEvent:
logger.warn('%s: event was rejected' % procname)
self._change_listener_state(EventListenerStates.ACKNOWLEDGED)
notify(EventRejectedEvent(process, process.event))
except:
logger.warn('%s: event caused an error' % procname)
self._change_listener_state(EventListenerStates.UNKNOWN)
notify(EventRejectedEvent(process, process.event))
def _change_listener_state(self, new_state):
process = self.process
procname = process.config.name
old_state = process.listener_state
msg = '%s: %s -> %s' % (
procname,
getEventListenerStateDescription(old_state),
getEventListenerStateDescription(new_state)
)
process.config.options.logger.debug(msg)
process.listener_state = new_state
if new_state == EventListenerStates.UNKNOWN:
msg = ('%s: has entered the UNKNOWN state and will no longer '
'receive events, this usually indicates the process '
'violated the eventlistener protocol' % procname)
process.config.options.logger.warn(msg)
class PInputDispatcher(PDispatcher):
""" Input (stdin) dispatcher """
def __init__(self, process, channel, fd):
PDispatcher.__init__(self, process, channel, fd)
self.input_buffer = b''
def writable(self):
if self.input_buffer and not self.closed:
return True
return False
def readable(self):
return False
def flush(self):
# other code depends on this raising EPIPE if the pipe is closed
sent = self.process.config.options.write(self.fd,
self.input_buffer)
self.input_buffer = self.input_buffer[sent:]
def handle_write_event(self):
if self.input_buffer:
try:
self.flush()
except OSError as why:
if why.args[0] == errno.EPIPE:
self.input_buffer = b''
self.close()
else:
raise
ANSI_ESCAPE_BEGIN = b'\x1b['
ANSI_TERMINATORS = (b'H', b'f', b'A', b'B', b'C', b'D', b'R', b's', b'u', b'J',
b'K', b'h', b'l', b'p', b'm')
def stripEscapes(s):
"""
Remove all ANSI color escapes from the given string.
"""
result = b''
show = 1
i = 0
L = len(s)
while i < L:
if show == 0 and s[i:i + 1] in ANSI_TERMINATORS:
show = 1
elif show:
n = s.find(ANSI_ESCAPE_BEGIN, i)
if n == -1:
return result + s[i:]
else:
result = result + s[i:n]
i = n
show = 0
i += 1
return result
class RejectEvent(Exception):
""" The exception type expected by a dispatcher when a handler wants
to reject an event """
def default_handler(event, response):
if response != b'OK':
raise RejectEvent(response)
================================================
FILE: supervisor/events.py
================================================
from supervisor.states import getProcessStateDescription
from supervisor.compat import as_string
callbacks = []
def subscribe(type, callback):
callbacks.append((type, callback))
def unsubscribe(type, callback):
callbacks.remove((type, callback))
def notify(event):
for type, callback in callbacks:
if isinstance(event, type):
callback(event)
def clear():
callbacks[:] = []
class Event:
""" Abstract event type """
pass
class ProcessLogEvent(Event):
""" Abstract """
channel = None
def __init__(self, process, pid, data):
self.process = process
self.pid = pid
self.data = data
def payload(self):
groupname = ''
if self.process.group is not None:
groupname = self.process.group.config.name
try:
data = as_string(self.data)
except UnicodeDecodeError:
data = 'Undecodable: %r' % self.data
# On Python 2, stuff needs to be in Unicode before invoking the
# % operator, otherwise implicit encodings to ASCII can cause
# failures
fmt = as_string('processname:%s groupname:%s pid:%s channel:%s\n%s')
result = fmt % (as_string(self.process.config.name),
as_string(groupname), self.pid,
as_string(self.channel), data)
return result
class ProcessLogStdoutEvent(ProcessLogEvent):
channel = 'stdout'
class ProcessLogStderrEvent(ProcessLogEvent):
channel = 'stderr'
class ProcessCommunicationEvent(Event):
""" Abstract """
# event mode tokens
BEGIN_TOKEN = b''
END_TOKEN = b''
def __init__(self, process, pid, data):
self.process = process
self.pid = pid
self.data = data
def payload(self):
groupname = ''
if self.process.group is not None:
groupname = self.process.group.config.name
try:
data = as_string(self.data)
except UnicodeDecodeError:
data = 'Undecodable: %r' % self.data
return 'processname:%s groupname:%s pid:%s\n%s' % (
self.process.config.name,
groupname,
self.pid,
data)
class ProcessCommunicationStdoutEvent(ProcessCommunicationEvent):
channel = 'stdout'
class ProcessCommunicationStderrEvent(ProcessCommunicationEvent):
channel = 'stderr'
class RemoteCommunicationEvent(Event):
def __init__(self, type, data):
self.type = type
self.data = data
def payload(self):
return 'type:%s\n%s' % (self.type, self.data)
class SupervisorStateChangeEvent(Event):
""" Abstract class """
def payload(self):
return ''
class SupervisorRunningEvent(SupervisorStateChangeEvent):
pass
class SupervisorStoppingEvent(SupervisorStateChangeEvent):
pass
class EventRejectedEvent: # purposely does not subclass Event
def __init__(self, process, event):
self.process = process
self.event = event
class ProcessStateEvent(Event):
""" Abstract class, never raised directly """
frm = None
to = None
def __init__(self, process, from_state, expected=True):
self.process = process
self.from_state = from_state
self.expected = expected
# we eagerly render these so if the process pid, etc changes beneath
# us, we stash the values at the time the event was sent
self.extra_values = self.get_extra_values()
def payload(self):
groupname = ''
if self.process.group is not None:
groupname = self.process.group.config.name
L = [('processname', self.process.config.name), ('groupname', groupname),
('from_state', getProcessStateDescription(self.from_state))]
L.extend(self.extra_values)
s = ' '.join( [ '%s:%s' % (name, val) for (name, val) in L ] )
return s
def get_extra_values(self):
return []
class ProcessStateFatalEvent(ProcessStateEvent):
pass
class ProcessStateUnknownEvent(ProcessStateEvent):
pass
class ProcessStateStartingOrBackoffEvent(ProcessStateEvent):
def get_extra_values(self):
return [('tries', int(self.process.backoff))]
class ProcessStateBackoffEvent(ProcessStateStartingOrBackoffEvent):
pass
class ProcessStateStartingEvent(ProcessStateStartingOrBackoffEvent):
pass
class ProcessStateExitedEvent(ProcessStateEvent):
def get_extra_values(self):
return [('expected', int(self.expected)), ('pid', self.process.pid)]
class ProcessStateRunningEvent(ProcessStateEvent):
def get_extra_values(self):
return [('pid', self.process.pid)]
class ProcessStateStoppingEvent(ProcessStateEvent):
def get_extra_values(self):
return [('pid', self.process.pid)]
class ProcessStateStoppedEvent(ProcessStateEvent):
def get_extra_values(self):
return [('pid', self.process.pid)]
class ProcessGroupEvent(Event):
def __init__(self, group):
self.group = group
def payload(self):
return 'groupname:%s\n' % self.group
class ProcessGroupAddedEvent(ProcessGroupEvent):
pass
class ProcessGroupRemovedEvent(ProcessGroupEvent):
pass
class TickEvent(Event):
""" Abstract """
def __init__(self, when, supervisord):
self.when = when
self.supervisord = supervisord
def payload(self):
return 'when:%s' % self.when
class Tick5Event(TickEvent):
period = 5
class Tick60Event(TickEvent):
period = 60
class Tick3600Event(TickEvent):
period = 3600
TICK_EVENTS = [ Tick5Event, Tick60Event, Tick3600Event ] # imported elsewhere
class EventTypes:
EVENT = Event # abstract
PROCESS_STATE = ProcessStateEvent # abstract
PROCESS_STATE_STOPPED = ProcessStateStoppedEvent
PROCESS_STATE_EXITED = ProcessStateExitedEvent
PROCESS_STATE_STARTING = ProcessStateStartingEvent
PROCESS_STATE_STOPPING = ProcessStateStoppingEvent
PROCESS_STATE_BACKOFF = ProcessStateBackoffEvent
PROCESS_STATE_FATAL = ProcessStateFatalEvent
PROCESS_STATE_RUNNING = ProcessStateRunningEvent
PROCESS_STATE_UNKNOWN = ProcessStateUnknownEvent
PROCESS_COMMUNICATION = ProcessCommunicationEvent # abstract
PROCESS_COMMUNICATION_STDOUT = ProcessCommunicationStdoutEvent
PROCESS_COMMUNICATION_STDERR = ProcessCommunicationStderrEvent
PROCESS_LOG = ProcessLogEvent
PROCESS_LOG_STDOUT = ProcessLogStdoutEvent
PROCESS_LOG_STDERR = ProcessLogStderrEvent
REMOTE_COMMUNICATION = RemoteCommunicationEvent
SUPERVISOR_STATE_CHANGE = SupervisorStateChangeEvent # abstract
SUPERVISOR_STATE_CHANGE_RUNNING = SupervisorRunningEvent
SUPERVISOR_STATE_CHANGE_STOPPING = SupervisorStoppingEvent
TICK = TickEvent # abstract
TICK_5 = Tick5Event
TICK_60 = Tick60Event
TICK_3600 = Tick3600Event
PROCESS_GROUP = ProcessGroupEvent # abstract
PROCESS_GROUP_ADDED = ProcessGroupAddedEvent
PROCESS_GROUP_REMOVED = ProcessGroupRemovedEvent
def getEventNameByType(requested):
for name, typ in EventTypes.__dict__.items():
if typ is requested:
return name
def register(name, event):
setattr(EventTypes, name, event)
================================================
FILE: supervisor/http.py
================================================
import os
import stat
import time
import sys
import socket
import errno
import weakref
import traceback
try:
import pwd
except ImportError: # Windows
import getpass as pwd
from supervisor.compat import urllib
from supervisor.compat import sha1
from supervisor.compat import as_bytes
from supervisor.compat import as_string
from supervisor.medusa import asyncore_25 as asyncore
from supervisor.medusa import http_date
from supervisor.medusa import http_server
from supervisor.medusa import producers
from supervisor.medusa import filesys
from supervisor.medusa import default_handler
from supervisor.medusa.auth_handler import auth_handler
class NOT_DONE_YET:
pass
class deferring_chunked_producer:
"""A producer that implements the 'chunked' transfer coding for HTTP/1.1.
Here is a sample usage:
request['Transfer-Encoding'] = 'chunked'
request.push (
producers.chunked_producer (your_producer)
)
request.done()
"""
def __init__ (self, producer, footers=None):
self.producer = producer
self.footers = footers
self.delay = 0.1
def more (self):
if self.producer:
data = self.producer.more()
if data is NOT_DONE_YET:
return NOT_DONE_YET
elif data:
s = '%x' % len(data)
return as_bytes(s) + b'\r\n' + data + b'\r\n'
else:
self.producer = None
if self.footers:
return b'\r\n'.join([b'0'] + self.footers) + b'\r\n\r\n'
else:
return b'0\r\n\r\n'
else:
return b''
class deferring_composite_producer:
"""combine a fifo of producers into one"""
def __init__ (self, producers):
self.producers = producers
self.delay = 0.1
def more (self):
while len(self.producers):
p = self.producers[0]
d = p.more()
if d is NOT_DONE_YET:
return NOT_DONE_YET
if d:
return d
else:
self.producers.pop(0)
else:
return b''
class deferring_globbing_producer:
"""
'glob' the output from a producer into a particular buffer size.
helps reduce the number of calls to send(). [this appears to
gain about 30% performance on requests to a single channel]
"""
def __init__ (self, producer, buffer_size=1<<16):
self.producer = producer
self.buffer = b''
self.buffer_size = buffer_size
self.delay = 0.1
def more (self):
while len(self.buffer) < self.buffer_size:
data = self.producer.more()
if data is NOT_DONE_YET:
return NOT_DONE_YET
if data:
try:
self.buffer = self.buffer + data
except TypeError:
self.buffer = as_bytes(self.buffer) + as_bytes(data)
else:
break
r = self.buffer
self.buffer = b''
return r
class deferring_hooked_producer:
"""
A producer that will call when it empties,.
with an argument of the number of bytes produced. Useful
for logging/instrumentation purposes.
"""
def __init__ (self, producer, function):
self.producer = producer
self.function = function
self.bytes = 0
self.delay = 0.1
def more (self):
if self.producer:
result = self.producer.more()
if result is NOT_DONE_YET:
return NOT_DONE_YET
if not result:
self.producer = None
self.function (self.bytes)
else:
self.bytes += len(result)
return result
else:
return b''
class deferring_http_request(http_server.http_request):
""" The medusa http_request class uses the default set of producers in
medusa.producers. We can't use these because they don't know anything
about deferred responses, so we override various methods here. This was
added to support tail -f like behavior on the logtail handler """
def done(self, *arg, **kw):
""" I didn't want to override this, but there's no way around
it in order to support deferreds - CM
finalize this transaction - send output to the http channel"""
# ----------------------------------------
# persistent connection management
# ----------------------------------------
# --- BUCKLE UP! ----
connection = http_server.get_header(http_server.CONNECTION,self.header)
connection = connection.lower()
close_it = 0
wrap_in_chunking = 0
globbing = 1
if self.version == '1.0':
if connection == 'keep-alive':
if not 'Content-Length' in self:
close_it = 1
else:
self['Connection'] = 'Keep-Alive'
else:
close_it = 1
elif self.version == '1.1':
if connection == 'close':
close_it = 1
elif not 'Content-Length' in self:
if 'Transfer-Encoding' in self:
if not self['Transfer-Encoding'] == 'chunked':
close_it = 1
elif self.use_chunked:
self['Transfer-Encoding'] = 'chunked'
wrap_in_chunking = 1
# globbing slows down tail -f output, so only use it if
# we're not in chunked mode
globbing = 0
else:
close_it = 1
elif self.version is None:
# Although we don't *really* support http/0.9 (because
# we'd have to use \r\n as a terminator, and it would just
# yuck up a lot of stuff) it's very common for developers
# to not want to type a version number when using telnet
# to debug a server.
close_it = 1
outgoing_header = producers.simple_producer(self.build_reply_header())
if close_it:
self['Connection'] = 'close'
if wrap_in_chunking:
outgoing_producer = deferring_chunked_producer(
deferring_composite_producer(self.outgoing)
)
# prepend the header
outgoing_producer = deferring_composite_producer(
[outgoing_header, outgoing_producer]
)
else:
# prepend the header
self.outgoing.insert(0, outgoing_header)
outgoing_producer = deferring_composite_producer(self.outgoing)
# hook logging into the output
outgoing_producer = deferring_hooked_producer(outgoing_producer,
self.log)
if globbing:
outgoing_producer = deferring_globbing_producer(outgoing_producer)
self.channel.push_with_producer(outgoing_producer)
self.channel.current_request = None
if close_it:
self.channel.close_when_done()
def log (self, bytes):
""" We need to override this because UNIX domain sockets return
an empty string for the addr rather than a (host, port) combination """
if self.channel.addr:
host = self.channel.addr[0]
port = self.channel.addr[1]
else:
host = 'localhost'
port = 0
self.channel.server.logger.log (
host,
'%d - - [%s] "%s" %d %d\n' % (
port,
self.log_date_string (time.time()),
self.request,
self.reply_code,
bytes
)
)
def cgi_environment(self):
env = {}
# maps request some headers to environment variables.
# (those that don't start with 'HTTP_')
header2env= {'content-length' : 'CONTENT_LENGTH',
'content-type' : 'CONTENT_TYPE',
'connection' : 'CONNECTION_TYPE'}
workdir = os.getcwd()
(path, params, query, fragment) = self.split_uri()
if params:
path = path + params # undo medusa bug!
while path and path[0] == '/':
path = path[1:]
if '%' in path:
path = http_server.unquote(path)
if query:
query = query[1:]
server = self.channel.server
env['REQUEST_METHOD'] = self.command.upper()
env['SERVER_PORT'] = str(server.port)
env['SERVER_NAME'] = server.server_name
env['SERVER_SOFTWARE'] = server.SERVER_IDENT
env['SERVER_PROTOCOL'] = "HTTP/" + self.version
env['channel.creation_time'] = self.channel.creation_time
env['SCRIPT_NAME'] = ''
env['PATH_INFO'] = '/' + path
env['PATH_TRANSLATED'] = os.path.normpath(os.path.join(
workdir, env['PATH_INFO']))
if query:
env['QUERY_STRING'] = query
env['GATEWAY_INTERFACE'] = 'CGI/1.1'
if self.channel.addr:
env['REMOTE_ADDR'] = self.channel.addr[0]
else:
env['REMOTE_ADDR'] = '127.0.0.1'
for header in self.header:
key,value=header.split(":",1)
key=key.lower()
value=value.strip()
if key in header2env and value:
env[header2env.get(key)]=value
else:
key='HTTP_%s' % ("_".join(key.split( "-"))).upper()
if value and key not in env:
env[key]=value
return env
def get_server_url(self):
""" Functionality that medusa's http request doesn't have; set an
attribute named 'server_url' on the request based on the Host: header
"""
default_port={'http': '80', 'https': '443'}
environ = self.cgi_environment()
if (environ.get('HTTPS') in ('on', 'ON') or
environ.get('SERVER_PORT_SECURE') == "1"):
# XXX this will currently never be true
protocol = 'https'
else:
protocol = 'http'
if 'HTTP_HOST' in environ:
host = environ['HTTP_HOST'].strip()
hostname, port = urllib.splitport(host)
else:
hostname = environ['SERVER_NAME'].strip()
port = environ['SERVER_PORT']
if port is None or default_port[protocol] == port:
host = hostname
else:
host = hostname + ':' + port
server_url = '%s://%s' % (protocol, host)
if server_url[-1:]=='/':
server_url=server_url[:-1]
return server_url
class deferring_http_channel(http_server.http_channel):
# use a 4096-byte buffer size instead of the default 65536-byte buffer in
# order to spew tail -f output faster (speculative)
ac_out_buffer_size = 4096
delay = 0 # seconds
last_writable_check = 0 # timestamp of last writable check; 0 if never
def writable(self, now=None):
if now is None: # for unit tests
now = time.time()
if self.delay:
# we called a deferred producer via this channel (see refill_buffer)
elapsed = now - self.last_writable_check
if (elapsed > self.delay) or (elapsed < 0):
self.last_writable_check = now
return True
else:
return False
return http_server.http_channel.writable(self)
def refill_buffer (self):
""" Implement deferreds """
while 1:
if len(self.producer_fifo):
p = self.producer_fifo.first()
# a 'None' in the producer fifo is a sentinel,
# telling us to close the channel.
if p is None:
if not self.ac_out_buffer:
self.producer_fifo.pop()
self.close()
return
elif isinstance(p, bytes):
self.producer_fifo.pop()
self.ac_out_buffer += p
return
data = p.more()
if data is NOT_DONE_YET:
self.delay = p.delay
return
elif data:
self.ac_out_buffer = self.ac_out_buffer + data
self.delay = False
return
else:
self.producer_fifo.pop()
else:
return
def found_terminator (self):
""" We only override this to use 'deferring_http_request' class
instead of the normal http_request class; it sucks to need to override
this """
if self.current_request:
self.current_request.found_terminator()
else:
# we convert the header to text to facilitate processing.
# some of the underlying APIs (such as splitquery)
# expect text rather than bytes.
header = as_string(self.in_buffer)
self.in_buffer = b''
lines = header.split('\r\n')
# --------------------------------------------------
# crack the request header
# --------------------------------------------------
while lines and not lines[0]:
# as per the suggestion of http-1.1 section 4.1, (and
# Eric Parker ), ignore a leading
# blank lines (buggy browsers tack it onto the end of
# POST requests)
lines = lines[1:]
if not lines:
self.close_when_done()
return
request = lines[0]
command, uri, version = http_server.crack_request (request)
header = http_server.join_headers (lines[1:])
# unquote path if necessary (thanks to Skip Montanaro for pointing
# out that we must unquote in piecemeal fashion).
rpath, rquery = http_server.splitquery(uri)
if '%' in rpath:
if rquery:
uri = http_server.unquote(rpath) + '?' + rquery
else:
uri = http_server.unquote(rpath)
r = deferring_http_request(self, request, command, uri, version,
header)
self.request_counter.increment()
self.server.total_requests.increment()
if command is None:
self.log_info ('Bad HTTP request: %s' % repr(request), 'error')
r.error (400)
return
# --------------------------------------------------
# handler selection and dispatch
# --------------------------------------------------
for h in self.server.handlers:
if h.match (r):
try:
self.current_request = r
# This isn't used anywhere.
# r.handler = h # CYCLE
h.handle_request (r)
except:
self.server.exceptions.increment()
(file, fun, line), t, v, tbinfo = \
asyncore.compact_traceback()
self.server.log_info(
'Server Error: %s, %s: file: %s line: %s' %
(t,v,file,line),
'error')
try:
r.error (500)
except:
pass
return
# no handlers, so complain
r.error (404)
class supervisor_http_server(http_server.http_server):
channel_class = deferring_http_channel
ip = None
def prebind(self, sock, logger_object):
""" Override __init__ to do logger setup earlier so it can
go to our logger object instead of stdout """
from supervisor.medusa import logger
if not logger_object:
logger_object = logger.file_logger(sys.stdout)
logger_object = logger.unresolving_logger(logger_object)
self.logger = logger_object
asyncore.dispatcher.__init__ (self)
self.set_socket(sock)
self.handlers = []
sock.setblocking(0)
self.set_reuse_addr()
def postbind(self):
from supervisor.medusa.counter import counter
from supervisor.medusa.http_server import VERSION_STRING
self.listen(1024)
self.total_clients = counter()
self.total_requests = counter()
self.exceptions = counter()
self.bytes_out = counter()
self.bytes_in = counter()
self.log_info (
'Medusa (V%s) started at %s'
'\n\tHostname: %s'
'\n\tPort:%s'
'\n' % (
VERSION_STRING,
time.ctime(time.time()),
self.server_name,
self.port,
)
)
def log_info(self, message, type='info'):
ip = ''
if getattr(self, 'ip', None) is not None:
ip = self.ip
self.logger.log(ip, message)
class supervisor_af_inet_http_server(supervisor_http_server):
""" AF_INET version of supervisor HTTP server """
def __init__(self, ip, port, logger_object):
self.ip = ip
self.port = port
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.prebind(sock, logger_object)
self.bind((ip, port))
if not ip:
self.log_info('Computing default hostname', 'warning')
hostname = socket.gethostname()
try:
ip = socket.gethostbyname(hostname)
except socket.error:
raise ValueError(
'Could not determine IP address for hostname %s, '
'please try setting an explicit IP address in the "port" '
'setting of your [inet_http_server] section. For example, '
'instead of "port = 9001", try "port = 127.0.0.1:9001."'
% hostname)
try:
self.server_name = socket.gethostbyaddr (ip)[0]
except socket.error:
self.log_info('Cannot do reverse lookup', 'warning')
self.server_name = ip # use the IP address as the "hostname"
self.postbind()
class supervisor_af_unix_http_server(supervisor_http_server):
""" AF_UNIX version of supervisor HTTP server """
def __init__(self, socketname, sockchmod, sockchown, logger_object):
self.ip = socketname
self.port = socketname
# XXX this is insecure. We really should do something like
# http://developer.apple.com/samplecode/CFLocalServer/listing6.html
# (see also http://developer.apple.com/technotes/tn2005/tn2083.html#SECUNIXDOMAINSOCKETS)
# but it would be very inconvenient for the user to need to get all
# the directory setup right.
tempname = "%s.%d" % (socketname, os.getpid())
try:
os.unlink(tempname)
except OSError:
pass
while 1:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
sock.bind(tempname)
os.chmod(tempname, sockchmod)
try:
# hard link
os.link(tempname, socketname)
except OSError:
# Lock contention, or stale socket.
used = self.checkused(socketname)
if used:
# cooperate with 'openhttpserver' in supervisord
raise socket.error(errno.EADDRINUSE)
# Stale socket -- delete, sleep, and try again.
msg = "Unlinking stale socket %s\n" % socketname
sys.stderr.write(msg)
try:
os.unlink(socketname)
except:
pass
sock.close()
time.sleep(.3)
continue
else:
try:
os.chown(socketname, sockchown[0], sockchown[1])
except OSError as why:
if why.args[0] == errno.EPERM:
msg = ('Not permitted to chown %s to uid/gid %s; '
'adjust "sockchown" value in config file or '
'on command line to values that the '
'current user (%s) can successfully chown')
raise ValueError(msg % (socketname,
repr(sockchown),
pwd.getpwuid(
os.geteuid())[0],
),
)
else:
raise
self.prebind(sock, logger_object)
break
finally:
try:
os.unlink(tempname)
except OSError:
pass
self.server_name = ''
self.postbind()
def checkused(self, socketname):
s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
s.connect(socketname)
s.send(as_bytes("GET / HTTP/1.0\r\n\r\n"))
s.recv(1)
s.close()
except socket.error:
return False
else:
return True
class tail_f_producer:
def __init__(self, request, filename, head):
self.request = weakref.ref(request)
self.filename = filename
self.delay = 0.1
self._open()
sz = self._fsize()
if sz >= head:
self.sz = sz - head
def __del__(self):
self._close()
def more(self):
self._follow()
try:
newsz = self._fsize()
except (OSError, ValueError):
# file descriptor was closed
return b''
bytes_added = newsz - self.sz
if bytes_added < 0:
self.sz = 0
return "==> File truncated <==\n"
if bytes_added > 0:
self.file.seek(-bytes_added, 2)
bytes = self.file.read(bytes_added)
self.sz = newsz
return bytes
return NOT_DONE_YET
def _open(self):
self.file = open(self.filename, 'rb')
self.ino = os.fstat(self.file.fileno())[stat.ST_INO]
self.sz = 0
def _close(self):
self.file.close()
def _follow(self):
try:
ino = os.stat(self.filename)[stat.ST_INO]
except (OSError, ValueError):
# file was unlinked
return
if self.ino != ino: # log rotation occurred
self._close()
self._open()
def _fsize(self):
return os.fstat(self.file.fileno())[stat.ST_SIZE]
class logtail_handler:
IDENT = 'Logtail HTTP Request Handler'
path = '/logtail'
def __init__(self, supervisord):
self.supervisord = supervisord
def match(self, request):
return request.uri.startswith(self.path)
def handle_request(self, request):
if request.command != 'GET':
request.error (400) # bad request
return
path, params, query, fragment = request.split_uri()
if '%' in path:
path = http_server.unquote(path)
# strip off all leading slashes
while path and path[0] == '/':
path = path[1:]
path, process_name_and_channel = path.split('/', 1)
try:
process_name, channel = process_name_and_channel.split('/', 1)
except ValueError:
# no channel specified, default channel to stdout
process_name = process_name_and_channel
channel = 'stdout'
from supervisor.options import split_namespec
group_name, process_name = split_namespec(process_name)
group = self.supervisord.process_groups.get(group_name)
if group is None:
request.error(404) # not found
return
process = group.processes.get(process_name)
if process is None:
request.error(404) # not found
return
logfile = getattr(process.config, '%s_logfile' % channel, None)
if logfile is None or not os.path.exists(logfile):
# we return 404 because no logfile is a temporary condition.
# if the process has never been started, no logfile will exist
# on disk. a logfile of None is also a temporary condition,
# since the config file can be reloaded.
request.error(404) # not found
return
mtime = os.stat(logfile)[stat.ST_MTIME]
request['Last-Modified'] = http_date.build_http_date(mtime)
request['Content-Type'] = 'text/plain;charset=utf-8'
# the lack of a Content-Length header makes the outputter
# send a 'Transfer-Encoding: chunked' response
request['X-Accel-Buffering'] = 'no'
# tell reverse proxy server (e.g., nginx) to disable proxy buffering
# (see also http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_buffering)
request.push(tail_f_producer(request, logfile, 1024))
request.done()
class mainlogtail_handler:
IDENT = 'Main Logtail HTTP Request Handler'
path = '/mainlogtail'
def __init__(self, supervisord):
self.supervisord = supervisord
def match(self, request):
return request.uri.startswith(self.path)
def handle_request(self, request):
if request.command != 'GET':
request.error (400) # bad request
return
logfile = self.supervisord.options.logfile
if logfile is None or not os.path.exists(logfile):
# we return 404 because no logfile is a temporary condition.
# even if a log file of None is configured, the config file
# may be reloaded, and the new config may have a logfile.
request.error(404) # not found
return
mtime = os.stat(logfile)[stat.ST_MTIME]
request['Last-Modified'] = http_date.build_http_date(mtime)
request['Content-Type'] = 'text/plain;charset=utf-8'
# the lack of a Content-Length header makes the outputter
# send a 'Transfer-Encoding: chunked' response
request.push(tail_f_producer(request, logfile, 1024))
request.done()
def make_http_servers(options, supervisord):
servers = []
wrapper = LogWrapper(options.logger)
for config in options.server_configs:
family = config['family']
if family == socket.AF_INET:
host, port = config['host'], config['port']
hs = supervisor_af_inet_http_server(host, port,
logger_object=wrapper)
elif family == socket.AF_UNIX:
socketname = config['file']
sockchmod = config['chmod']
sockchown = config['chown']
hs = supervisor_af_unix_http_server(socketname,sockchmod, sockchown,
logger_object=wrapper)
else:
raise ValueError('Cannot determine socket type %r' % family)
from supervisor.xmlrpc import supervisor_xmlrpc_handler
from supervisor.xmlrpc import SystemNamespaceRPCInterface
from supervisor.web import supervisor_ui_handler
subinterfaces = []
for name, factory, d in options.rpcinterface_factories:
try:
inst = factory(supervisord, **d)
except:
tb = traceback.format_exc()
options.logger.warn(tb)
raise ValueError('Could not make %s rpc interface' % name)
subinterfaces.append((name, inst))
options.logger.info('RPC interface %r initialized' % name)
subinterfaces.append(('system',
SystemNamespaceRPCInterface(subinterfaces)))
xmlrpchandler = supervisor_xmlrpc_handler(supervisord, subinterfaces)
tailhandler = logtail_handler(supervisord)
maintailhandler = mainlogtail_handler(supervisord)
uihandler = supervisor_ui_handler(supervisord)
here = os.path.abspath(os.path.dirname(__file__))
templatedir = os.path.join(here, 'ui')
filesystem = filesys.os_filesystem(templatedir)
defaulthandler = default_handler.default_handler(filesystem)
username = config['username']
password = config['password']
if username:
# wrap the xmlrpc handler and tailhandler in an authentication
# handler
users = {username:password}
xmlrpchandler = supervisor_auth_handler(users, xmlrpchandler)
tailhandler = supervisor_auth_handler(users, tailhandler)
maintailhandler = supervisor_auth_handler(users, maintailhandler)
uihandler = supervisor_auth_handler(users, uihandler)
defaulthandler = supervisor_auth_handler(users, defaulthandler)
else:
options.logger.critical(
'Server %r running without any HTTP '
'authentication checking' % config['section'])
# defaulthandler must be consulted last as its match method matches
# everything, so it's first here (indicating last checked)
hs.install_handler(defaulthandler)
hs.install_handler(uihandler)
hs.install_handler(maintailhandler)
hs.install_handler(tailhandler)
hs.install_handler(xmlrpchandler) # last for speed (first checked)
servers.append((config, hs))
return servers
class LogWrapper:
'''Receives log messages from the Medusa servers and forwards
them to the Supervisor logger'''
def __init__(self, logger):
self.logger = logger
def log(self, msg):
'''Medusa servers call this method. There is no log level so
we have to sniff the message. We want "Server Error" messages
from medusa.http_server logged as errors at least.'''
if msg.endswith('\n'):
msg = msg[:-1]
if 'error' in msg.lower():
self.logger.error(msg)
else:
self.logger.trace(msg)
class encrypted_dictionary_authorizer:
def __init__ (self, dict):
self.dict = dict
def authorize(self, auth_info):
username, password = auth_info
if username in self.dict:
stored_password = self.dict[username]
if stored_password.startswith('{SHA}'):
password_hash = sha1(as_bytes(password)).hexdigest()
return stored_password[5:] == password_hash
else:
return stored_password == password
else:
return False
class supervisor_auth_handler(auth_handler):
def __init__(self, dict, handler, realm='default'):
auth_handler.__init__(self, dict, handler, realm)
# override the authorizer with one that knows about SHA hashes too
self.authorizer = encrypted_dictionary_authorizer(dict)
================================================
FILE: supervisor/http_client.py
================================================
# this code based on Daniel Krech's RDFLib HTTP client code (see rdflib.dev)
import sys
import socket
from supervisor.compat import as_bytes
from supervisor.compat import as_string
from supervisor.compat import encodestring
from supervisor.compat import PY2
from supervisor.compat import urlparse
from supervisor.medusa import asynchat_25 as asynchat
CR = b'\x0d'
LF = b'\x0a'
CRLF = CR+LF
class Listener(object):
def status(self, url, status):
pass
def error(self, url, error):
sys.stderr.write("%s %s\n" % (url, error))
def response_header(self, url, name, value):
pass
def done(self, url):
pass
def feed(self, url, data):
try:
sdata = as_string(data)
except UnicodeDecodeError:
sdata = 'Undecodable: %r' % data
# We've got Unicode data in sdata now, but writing to stdout sometimes
# fails - see issue #1231.
try:
sys.stdout.write(sdata)
except UnicodeEncodeError:
if PY2:
# This might seem like The Wrong Thing To Do (writing bytes
# rather than text to an output stream), but it seems to work
# OK for Python 2.7.
sys.stdout.write(data)
else:
s = ('Unable to write Unicode to stdout because it has '
'encoding %s' % sys.stdout.encoding)
raise ValueError(s)
sys.stdout.flush()
def close(self, url):
pass
class HTTPHandler(asynchat.async_chat):
def __init__(
self,
listener,
username='',
password=None,
conn=None,
map=None
):
asynchat.async_chat.__init__(self, conn, map)
self.listener = listener
self.user_agent = 'Supervisor HTTP Client'
self.buffer = b''
self.set_terminator(CRLF)
self.connected = 0
self.part = self.status_line
self.chunk_size = 0
self.chunk_read = 0
self.length_read = 0
self.length = 0
self.encoding = None
self.username = username
self.password = password
self.url = None
self.error_handled = False
def get(self, serverurl, path=''):
if self.url is not None:
raise AssertionError('Already doing a get')
self.url = serverurl + path
scheme, host, path_ignored, params, query, fragment = urlparse.urlparse(
self.url)
if not scheme in ("http", "unix"):
raise NotImplementedError
self.host = host
if ":" in host:
hostname, port = host.split(":", 1)
port = int(port)
else:
hostname = host
port = 80
self.path = path
self.port = port
if scheme == "http":
ip = hostname
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.connect((ip, self.port))
elif scheme == "unix":
socketname = serverurl[7:]
self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.connect(socketname)
def close(self):
self.listener.close(self.url)
self.connected = 0
self.del_channel()
self.socket.close()
self.url = "CLOSED"
def header(self, name, value):
self.push('%s: %s' % (name, value))
self.push(CRLF)
def handle_error(self):
if self.error_handled:
return
if 1 or self.connected:
t,v,tb = sys.exc_info()
msg = 'Cannot connect, error: %s (%s)' % (t, v)
self.listener.error(self.url, msg)
self.part = self.ignore
self.close()
self.error_handled = True
del t
del v
del tb
def handle_connect(self):
self.connected = 1
method = "GET"
version = "HTTP/1.1"
self.push("%s %s %s" % (method, self.path, version))
self.push(CRLF)
self.header("Host", self.host)
self.header('Accept-Encoding', 'chunked')
self.header('Accept', '*/*')
self.header('User-agent', self.user_agent)
if self.password:
auth = '%s:%s' % (self.username, self.password)
auth = as_string(encodestring(as_bytes(auth))).strip()
self.header('Authorization', 'Basic %s' % auth)
self.push(CRLF)
self.push(CRLF)
def feed(self, data):
self.listener.feed(self.url, data)
def collect_incoming_data(self, bytes):
self.buffer = self.buffer + bytes
if self.part==self.body:
self.feed(self.buffer)
self.buffer = b''
def found_terminator(self):
self.part()
self.buffer = b''
def ignore(self):
self.buffer = b''
def status_line(self):
line = self.buffer
version, status, reason = line.split(None, 2)
status = int(status)
if not version.startswith(b'HTTP/'):
raise ValueError(line)
self.listener.status(self.url, status)
if status == 200:
self.part = self.headers
else:
self.part = self.ignore
msg = 'Cannot read, status code %s' % status
self.listener.error(self.url, msg)
self.close()
return version, status, reason
def headers(self):
line = self.buffer
if not line:
if self.encoding == b'chunked':
self.part = self.chunked_size
else:
self.part = self.body
self.set_terminator(self.length)
else:
name, value = line.split(b':', 1)
if name and value:
name = name.lower()
value = value.strip()
if name == b'transfer-encoding':
self.encoding = value
elif name == b'content-length':
self.length = int(value)
self.response_header(name, value)
def response_header(self, name, value):
self.listener.response_header(self.url, name, value)
def body(self):
self.done()
self.close()
def done(self):
self.listener.done(self.url)
def chunked_size(self):
line = self.buffer
if not line:
return
chunk_size = int(line.split()[0], 16)
if chunk_size==0:
self.part = self.trailer
else:
self.set_terminator(chunk_size)
self.part = self.chunked_body
self.length += chunk_size
def chunked_body(self):
line = self.buffer
self.set_terminator(CRLF)
self.part = self.chunked_size
self.feed(line)
def trailer(self):
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.6.1
# trailer = *(entity-header CRLF)
line = self.buffer
if line == CRLF:
self.done()
self.close()
================================================
FILE: supervisor/loggers.py
================================================
"""
Logger implementation loosely modeled on PEP 282. We don't use the
PEP 282 logger implementation in the stdlib ('logging') because it's
idiosyncratic and a bit slow for our purposes (we don't use threads).
"""
# This module must not depend on any non-stdlib modules to
# avoid circular import problems
import os
import errno
import sys
import time
import traceback
from supervisor.compat import syslog
from supervisor.compat import long
from supervisor.compat import is_text_stream
from supervisor.compat import as_string
class LevelsByName:
CRIT = 50 # messages that probably require immediate user attention
ERRO = 40 # messages that indicate a potentially ignorable error condition
WARN = 30 # messages that indicate issues which aren't errors
INFO = 20 # normal informational output
DEBG = 10 # messages useful for users trying to debug configurations
TRAC = 5 # messages useful to developers trying to debug plugins
BLAT = 3 # messages useful for developers trying to debug supervisor
class LevelsByDescription:
critical = LevelsByName.CRIT
error = LevelsByName.ERRO
warn = LevelsByName.WARN
info = LevelsByName.INFO
debug = LevelsByName.DEBG
trace = LevelsByName.TRAC
blather = LevelsByName.BLAT
def _levelNumbers():
bynumber = {}
for name, number in LevelsByName.__dict__.items():
if not name.startswith('_'):
bynumber[number] = name
return bynumber
LOG_LEVELS_BY_NUM = _levelNumbers()
def getLevelNumByDescription(description):
num = getattr(LevelsByDescription, description, None)
return num
class Handler:
fmt = '%(message)s'
level = LevelsByName.INFO
def __init__(self, stream=None):
self.stream = stream
self.closed = False
def setFormat(self, fmt):
self.fmt = fmt
def setLevel(self, level):
self.level = level
def flush(self):
try:
self.stream.flush()
except IOError as why:
# if supervisor output is piped, EPIPE can be raised at exit
if why.args[0] != errno.EPIPE:
raise
def close(self):
if not self.closed:
if hasattr(self.stream, 'fileno'):
try:
fd = self.stream.fileno()
except IOError:
# on python 3, io.IOBase objects always have fileno()
# but calling it may raise io.UnsupportedOperation
pass
else:
if fd < 3: # don't ever close stdout or stderr
return
self.stream.close()
self.closed = True
def emit(self, record):
try:
binary = (self.fmt == '%(message)s' and
isinstance(record.msg, bytes) and
(not record.kw or record.kw == {'exc_info': None}))
binary_stream = not is_text_stream(self.stream)
if binary:
msg = record.msg
else:
msg = self.fmt % record.asdict()
if binary_stream:
msg = msg.encode('utf-8')
try:
self.stream.write(msg)
except UnicodeError:
# TODO sort out later
# this only occurs because of a test stream type
# which deliberately raises an exception the first
# time it's called. So just do it again
self.stream.write(msg)
self.flush()
except:
self.handleError()
def handleError(self):
ei = sys.exc_info()
traceback.print_exception(ei[0], ei[1], ei[2], None, sys.stderr)
del ei
class StreamHandler(Handler):
def __init__(self, strm=None):
Handler.__init__(self, strm)
def remove(self):
if hasattr(self.stream, 'clear'):
self.stream.clear()
def reopen(self):
pass
class BoundIO:
def __init__(self, maxbytes, buf=b''):
self.maxbytes = maxbytes
self.buf = buf
def flush(self):
pass
def close(self):
self.clear()
def write(self, b):
blen = len(b)
if len(self.buf) + blen > self.maxbytes:
self.buf = self.buf[blen:]
self.buf += b
def getvalue(self):
return self.buf
def clear(self):
self.buf = b''
class FileHandler(Handler):
"""File handler which supports reopening of logs.
"""
def __init__(self, filename, mode='ab'):
Handler.__init__(self)
try:
self.stream = open(filename, mode)
except OSError as e:
if mode == 'ab' and e.errno == errno.ESPIPE:
# Python 3 can't open special files like
# /dev/stdout in 'a' mode due to an implicit seek call
# that fails with ESPIPE. Retry in 'w' mode.
# See: http://bugs.python.org/issue27805
mode = 'wb'
self.stream = open(filename, mode)
else:
raise
self.baseFilename = filename
self.mode = mode
def reopen(self):
self.close()
self.stream = open(self.baseFilename, self.mode)
self.closed = False
def remove(self):
self.close()
try:
os.remove(self.baseFilename)
except OSError as why:
if why.args[0] != errno.ENOENT:
raise
class RotatingFileHandler(FileHandler):
def __init__(self, filename, mode='ab', maxBytes=512*1024*1024,
backupCount=10):
"""
Open the specified file and use it as the stream for logging.
By default, the file grows indefinitely. You can specify particular
values of maxBytes and backupCount to allow the file to rollover at
a predetermined size.
Rollover occurs whenever the current log file is nearly maxBytes in
length. If backupCount is >= 1, the system will successively create
new files with the same pathname as the base file, but with extensions
".1", ".2" etc. appended to it. For example, with a backupCount of 5
and a base file name of "app.log", you would get "app.log",
"app.log.1", "app.log.2", ... through to "app.log.5". The file being
written to is always "app.log" - when it gets filled up, it is closed
and renamed to "app.log.1", and if files "app.log.1", "app.log.2" etc.
exist, then they are renamed to "app.log.2", "app.log.3" etc.
respectively.
If maxBytes is zero, rollover never occurs.
"""
if maxBytes > 0:
mode = 'ab' # doesn't make sense otherwise!
FileHandler.__init__(self, filename, mode)
self.maxBytes = maxBytes
self.backupCount = backupCount
self.counter = 0
self.every = 10
def emit(self, record):
"""
Emit a record.
Output the record to the file, catering for rollover as described
in doRollover().
"""
FileHandler.emit(self, record)
self.doRollover()
def _remove(self, fn): # pragma: no cover
# this is here to service stubbing in unit tests
return os.remove(fn)
def _rename(self, src, tgt): # pragma: no cover
# this is here to service stubbing in unit tests
return os.rename(src, tgt)
def _exists(self, fn): # pragma: no cover
# this is here to service stubbing in unit tests
return os.path.exists(fn)
def removeAndRename(self, sfn, dfn):
if self._exists(dfn):
try:
self._remove(dfn)
except OSError as why:
# catch race condition (destination already deleted)
if why.args[0] != errno.ENOENT:
raise
try:
self._rename(sfn, dfn)
except OSError as why:
# catch exceptional condition (source deleted)
# E.g. cleanup script removes active log.
if why.args[0] != errno.ENOENT:
raise
def doRollover(self):
"""
Do a rollover, as described in __init__().
"""
if self.maxBytes <= 0:
return
if not (self.stream.tell() >= self.maxBytes):
return
self.stream.close()
if self.backupCount > 0:
for i in range(self.backupCount - 1, 0, -1):
sfn = "%s.%d" % (self.baseFilename, i)
dfn = "%s.%d" % (self.baseFilename, i + 1)
if os.path.exists(sfn):
self.removeAndRename(sfn, dfn)
dfn = self.baseFilename + ".1"
self.removeAndRename(self.baseFilename, dfn)
self.stream = open(self.baseFilename, 'wb')
class LogRecord:
def __init__(self, level, msg, **kw):
self.level = level
self.msg = msg
self.kw = kw
self.dictrepr = None
def asdict(self):
if self.dictrepr is None:
now = time.time()
msecs = (now - long(now)) * 1000
part1 = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(now))
asctime = '%s,%03d' % (part1, msecs)
levelname = LOG_LEVELS_BY_NUM[self.level]
msg = as_string(self.msg)
if self.kw:
msg = msg % self.kw
self.dictrepr = {'message':msg, 'levelname':levelname,
'asctime':asctime}
return self.dictrepr
class Logger:
def __init__(self, level=None, handlers=None):
if level is None:
level = LevelsByName.INFO
self.level = level
if handlers is None:
handlers = []
self.handlers = handlers
def close(self):
for handler in self.handlers:
handler.close()
def blather(self, msg, **kw):
if LevelsByName.BLAT >= self.level:
self.log(LevelsByName.BLAT, msg, **kw)
def trace(self, msg, **kw):
if LevelsByName.TRAC >= self.level:
self.log(LevelsByName.TRAC, msg, **kw)
def debug(self, msg, **kw):
if LevelsByName.DEBG >= self.level:
self.log(LevelsByName.DEBG, msg, **kw)
def info(self, msg, **kw):
if LevelsByName.INFO >= self.level:
self.log(LevelsByName.INFO, msg, **kw)
def warn(self, msg, **kw):
if LevelsByName.WARN >= self.level:
self.log(LevelsByName.WARN, msg, **kw)
def error(self, msg, **kw):
if LevelsByName.ERRO >= self.level:
self.log(LevelsByName.ERRO, msg, **kw)
def critical(self, msg, **kw):
if LevelsByName.CRIT >= self.level:
self.log(LevelsByName.CRIT, msg, **kw)
def log(self, level, msg, **kw):
record = LogRecord(level, msg, **kw)
for handler in self.handlers:
if level >= handler.level:
handler.emit(record)
def addHandler(self, hdlr):
self.handlers.append(hdlr)
def getvalue(self):
raise NotImplementedError
class SyslogHandler(Handler):
def __init__(self):
Handler.__init__(self)
assert syslog is not None, "Syslog module not present"
def close(self):
pass
def reopen(self):
pass
def _syslog(self, msg): # pragma: no cover
# this exists only for unit test stubbing
syslog.syslog(msg)
def emit(self, record):
try:
params = record.asdict()
message = params['message']
for line in message.rstrip('\n').split('\n'):
params['message'] = line
msg = self.fmt % params
try:
self._syslog(msg)
except UnicodeError:
self._syslog(msg.encode("UTF-8"))
except:
self.handleError()
def getLogger(level=None):
return Logger(level)
_2MB = 1<<21
def handle_boundIO(logger, fmt, maxbytes=_2MB):
"""Attach a new BoundIO handler to an existing Logger"""
io = BoundIO(maxbytes)
handler = StreamHandler(io)
handler.setLevel(logger.level)
handler.setFormat(fmt)
logger.addHandler(handler)
logger.getvalue = io.getvalue
def handle_stdout(logger, fmt):
"""Attach a new StreamHandler with stdout handler to an existing Logger"""
handler = StreamHandler(sys.stdout)
handler.setFormat(fmt)
handler.setLevel(logger.level)
logger.addHandler(handler)
def handle_syslog(logger, fmt):
"""Attach a new Syslog handler to an existing Logger"""
handler = SyslogHandler()
handler.setFormat(fmt)
handler.setLevel(logger.level)
logger.addHandler(handler)
def handle_file(logger, filename, fmt, rotating=False, maxbytes=0, backups=0):
"""Attach a new file handler to an existing Logger. If the filename
is the magic name of 'syslog' then make it a syslog handler instead."""
if filename == 'syslog': # TODO remove this
handler = SyslogHandler()
else:
if rotating is False:
handler = FileHandler(filename)
else:
handler = RotatingFileHandler(filename, 'a', maxbytes, backups)
handler.setFormat(fmt)
handler.setLevel(logger.level)
logger.addHandler(handler)
================================================
FILE: supervisor/medusa/CHANGES.txt
================================================
PATCHES MADE ONLY TO THIS MEDUSA PACKAGE BUNDLED WITH SUPERVISOR
* Re-added asyncore.py as asyncore_25.py and asynchat.py as asynchat_25.py
and updated Medusa throughout to use these. The Python 2.6 stdlib version
of these modules introduced backward-incompatible changes.
* Changed imports throughout from "medusa" to "supervisor.medusa".
* Removed medusa files not used by Supervisor.
* Fixed a bug in auth_handler.py where colons could not be used in passwords
for HTTP Basic authentication (Supervisor issue #309).
* Time out connections based on inactivity instead of age (Supervisor issue
#651).
Version 0.5.5:
* [Patch #855389] ADD RNFR & RNTO commands to FTP server (Robin Becker)
* [Patch #852089] In status_handler, catch any exception raised by the status()
method.
* [Patch #855695] Bugfix for filesys.msdos_date
* [Patch from Jason Sibre] Improve performance of xmlrpc_handler
by avoiding string concatenation
* [Patch from Jason Sibre] Add interface to http_request for multiple
headers with the same name.
Version 0.5.4:
* Open syslog using datagram sockets, and only try streams if that fails
(Alxy Sav)
* Fix bug in http_server.crack_request() (Contributed by Canis Lupus)
* Incorporate bugfixes to thread/select_trigger.py from ZEO's version
(Zope Corporation)
* Add demo/winFTPserver.py, an FTP server that uses Windows
authorization to determine who can access the server. (Contributed
by John Abel)
* Add control files for creating a Debian package of Medusa (python2.2-medusa).
Version 0.5.3:
* Delete the broken and rather boring dual_server and simple_httpd
demo scripts. start_medusa.py should be sufficient as an example.
* Fix indentation bug in demo/script_server.py noted by Richard Philips
* Fix bug in producers.composite_producer, spotted and fixed by Daniel Krech
* Added test suite for producers.py
* Fix timestamps in http_server logs
* Fix unix_user_handler bug, spotted and fixed by Sergio Fernndez.
* Fix auth_handler bug, spotted and fixed by Sergio Fernndez.
* Delete unused http_server.fifo class and fifo.py module.
Version 0.5.2:
* Fix syntax error and missing import in default_handler.py
* Fix various scripts in demo/
Version 0.5.1:
* Apply cleanup patch from Donovan Baarda
* Fix bug reported by Van Gale: counter.py and auth_handler.py did
long(...)[:-1] to chop off a trailing L generated in earlier
versions of Python.
* Fix bug in ftp_server.py that I introduced in 0.5
* Remove some duplicated producer classes
* Removed work_in_progress/ directory and the 'continuation' module
* Remove MIME type table code and use the stdlib's mimelib module
Version 0.5:
* Added a setup.py installation script, which will install all the code
under the package name 'medusa'.
* Added README.txt and CHANGES.txt.
* Fixed NameError in util/convert_mime_type_table.py
* Fixed TypeError in test/test_medusa.py
* Fixed several problems detected by PyChecker
* Changed demos to use 'from medusa import ...'
* Rearranged files to reduce the number of subdirectories.
* Removed or updated uses of the obsolete regsub module
* Removed asyncore.py and asynchat.py; these modules were added to Python's
standard library with version 1.5.2, and Medusa now assumes that they're
present.
* Removed many obsolete files:
poll/pollmodule.c, as Python's select module now supports poll()
patches/posixmodule.c, as that patch was incorporated in Python
old/*, script_handler_demo/*, sendfile/*
The old ANNOUNCE files
* Reindented all files to use four-space indents
The last version of Medusa released by Sam Rushing was medusa-20010416.
================================================
FILE: supervisor/medusa/LICENSE.txt
================================================
Medusa was once distributed under a 'free for non-commercial use'
license, but in May of 2000 Sam Rushing changed the license to be
identical to the standard Python license at the time. The standard
Python license has always applied to the core components of Medusa,
this change just frees up the rest of the system, including the http
server, ftp server, utilities, etc. Medusa is therefore under the
following license:
==============================
Permission to use, copy, modify, and distribute this software and
its documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and
that both that copyright notice and this permission notice appear in
supporting documentation, and that the name of Sam Rushing not be
used in advertising or publicity pertaining to distribution of the
software without specific, written prior permission.
SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR
CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION
WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
==============================
Sam would like to take this opportunity to thank all of the folks who
supported Medusa over the years by purchasing commercial licenses.
================================================
FILE: supervisor/medusa/README.txt
================================================
Medusa is a 'server platform' -- it provides a framework for
implementing asynchronous socket-based servers (TCP/IP and on Unix,
Unix domain, sockets).
An asynchronous socket server is a server that can communicate with many
other clients simultaneously by multiplexing I/O within a single
process/thread. In the context of an HTTP server, this means a single
process can serve hundreds or even thousands of clients, depending only on
the operating system's configuration and limitations.
There are several advantages to this approach:
o performance - no fork() or thread() start-up costs per hit.
o scalability - the overhead per client can be kept rather small,
on the order of several kilobytes of memory.
o persistence - a single-process server can easily coordinate the
actions of several different connections. This makes things like
proxy servers and gateways easy to implement. It also makes it
possible to share resources like database handles.
Medusa includes HTTP, FTP, and 'monitor' (remote python interpreter)
servers. Medusa can simultaneously support several instances of
either the same or different server types - for example you could
start up two HTTP servers, an FTP server, and a monitor server. Then
you could connect to the monitor server to control and manipulate
medusa while it is running.
Other servers and clients have been written (SMTP, POP3, NNTP), and
several are in the planning stages.
Medusa was originally written by Sam Rushing ,
and its original Web page is at . After
Sam moved on to other things, A.M. Kuchling
took over maintenance of the Medusa package.
--amk
================================================
FILE: supervisor/medusa/TODO.txt
================================================
Things to do
============
Bring remaining code up to current standards
Translate docs to RST
Write README, INSTALL, docs
What should __init__ import? Anything? Every single class?
Add abo's support for blocking producers
Get all the producers into the producers module and write tests for them
Test suites for protocols: how could that be implemented?
================================================
FILE: supervisor/medusa/__init__.py
================================================
"""medusa.__init__
"""
# created 2002/03/19, AMK
__revision__ = "$Id: __init__.py,v 1.2 2002/03/19 22:49:34 amk Exp $"
================================================
FILE: supervisor/medusa/asynchat_25.py
================================================
# -*- Mode: Python; tab-width: 4 -*-
# Id: asynchat.py,v 2.26 2000/09/07 22:29:26 rushing Exp
# Author: Sam Rushing
# ======================================================================
# Copyright 1996 by Sam Rushing
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Sam
# Rushing not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# ======================================================================
r"""A class supporting chat-style (command/response) protocols.
This class adds support for 'chat' style protocols - where one side
sends a 'command', and the other sends a response (examples would be
the common internet protocols - smtp, nntp, ftp, etc..).
The handle_read() method looks at the input stream for the current
'terminator' (usually '\r\n' for single-line responses, '\r\n.\r\n'
for multi-line output), calling self.found_terminator() on its
receipt.
for example:
Say you build an async nntp client using this class. At the start
of the connection, you'll have self.terminator set to '\r\n', in
order to process the single-line greeting. Just before issuing a
'LIST' command you'll set it to '\r\n.\r\n'. The output of the LIST
command will be accumulated (using your own 'collect_incoming_data'
method) up to the terminator, and then control will be returned to
you - by calling your self.found_terminator() method.
"""
import socket
from supervisor.medusa import asyncore_25 as asyncore
from supervisor.compat import long
from supervisor.compat import as_bytes
class async_chat (asyncore.dispatcher):
"""This is an abstract class. You must derive from this class, and add
the two methods collect_incoming_data() and found_terminator()"""
# these are overridable defaults
ac_in_buffer_size = 4096
ac_out_buffer_size = 4096
def __init__ (self, conn=None, map=None):
self.ac_in_buffer = b''
self.ac_out_buffer = b''
self.producer_fifo = fifo()
asyncore.dispatcher.__init__ (self, conn, map)
def collect_incoming_data(self, data):
raise NotImplementedError("must be implemented in subclass")
def found_terminator(self):
raise NotImplementedError("must be implemented in subclass")
def set_terminator (self, term):
"""Set the input delimiter. Can be a fixed string of any length, an integer, or None"""
self.terminator = term
def get_terminator (self):
return self.terminator
# grab some more data from the socket,
# throw it to the collector method,
# check for the terminator,
# if found, transition to the next state.
def handle_read (self):
try:
data = self.recv (self.ac_in_buffer_size)
except socket.error:
self.handle_error()
return
self.ac_in_buffer += data
# Continue to search for self.terminator in self.ac_in_buffer,
# while calling self.collect_incoming_data. The while loop
# is necessary because we might read several data+terminator
# combos with a single recv(1024).
while self.ac_in_buffer:
lb = len(self.ac_in_buffer)
terminator = self.get_terminator()
if not terminator:
# no terminator, collect it all
self.collect_incoming_data (self.ac_in_buffer)
self.ac_in_buffer = b''
elif isinstance(terminator, int) or isinstance(terminator, long):
# numeric terminator
n = terminator
if lb < n:
self.collect_incoming_data (self.ac_in_buffer)
self.ac_in_buffer = b''
self.terminator -= lb
else:
self.collect_incoming_data (self.ac_in_buffer[:n])
self.ac_in_buffer = self.ac_in_buffer[n:]
self.terminator = 0
self.found_terminator()
else:
# 3 cases:
# 1) end of buffer matches terminator exactly:
# collect data, transition
# 2) end of buffer matches some prefix:
# collect data to the prefix
# 3) end of buffer does not match any prefix:
# collect data
terminator_len = len(terminator)
index = self.ac_in_buffer.find(terminator)
if index != -1:
# we found the terminator
if index > 0:
# don't bother reporting the empty string (source of subtle bugs)
self.collect_incoming_data (self.ac_in_buffer[:index])
self.ac_in_buffer = self.ac_in_buffer[index+terminator_len:]
# This does the Right Thing if the terminator is changed here.
self.found_terminator()
else:
# check for a prefix of the terminator
index = find_prefix_at_end (self.ac_in_buffer, terminator)
if index:
if index != lb:
# we found a prefix, collect up to the prefix
self.collect_incoming_data (self.ac_in_buffer[:-index])
self.ac_in_buffer = self.ac_in_buffer[-index:]
break
else:
# no prefix, collect it all
self.collect_incoming_data (self.ac_in_buffer)
self.ac_in_buffer = b''
def handle_write (self):
self.initiate_send ()
def handle_close (self):
self.close()
def push (self, data):
data = as_bytes(data)
self.producer_fifo.push(simple_producer(data))
self.initiate_send()
def push_with_producer (self, producer):
self.producer_fifo.push (producer)
self.initiate_send()
def readable (self):
"""predicate for inclusion in the readable for select()"""
return len(self.ac_in_buffer) <= self.ac_in_buffer_size
def writable (self):
"""predicate for inclusion in the writable for select()"""
# return len(self.ac_out_buffer) or len(self.producer_fifo) or (not self.connected)
# this is about twice as fast, though not as clear.
return not (
(self.ac_out_buffer == b'') and
self.producer_fifo.is_empty() and
self.connected
)
def close_when_done (self):
"""automatically close this channel once the outgoing queue is empty"""
self.producer_fifo.push (None)
# refill the outgoing buffer by calling the more() method
# of the first producer in the queue
def refill_buffer (self):
while 1:
if len(self.producer_fifo):
p = self.producer_fifo.first()
# a 'None' in the producer fifo is a sentinel,
# telling us to close the channel.
if p is None:
if not self.ac_out_buffer:
self.producer_fifo.pop()
self.close()
return
elif isinstance(p, bytes):
self.producer_fifo.pop()
self.ac_out_buffer += p
return
data = p.more()
if data:
self.ac_out_buffer = self.ac_out_buffer + data
return
else:
self.producer_fifo.pop()
else:
return
def initiate_send (self):
obs = self.ac_out_buffer_size
# try to refill the buffer
if len (self.ac_out_buffer) < obs:
self.refill_buffer()
if self.ac_out_buffer and self.connected:
# try to send the buffer
try:
num_sent = self.send (self.ac_out_buffer[:obs])
if num_sent:
self.ac_out_buffer = self.ac_out_buffer[num_sent:]
except socket.error:
self.handle_error()
return
def discard_buffers (self):
# Emergencies only!
self.ac_in_buffer = b''
self.ac_out_buffer = b''
while self.producer_fifo:
self.producer_fifo.pop()
class simple_producer:
def __init__ (self, data, buffer_size=512):
self.data = data
self.buffer_size = buffer_size
def more (self):
if len (self.data) > self.buffer_size:
result = self.data[:self.buffer_size]
self.data = self.data[self.buffer_size:]
return result
else:
result = self.data
self.data = b''
return result
class fifo:
def __init__ (self, list=None):
if not list:
self.list = []
else:
self.list = list
def __len__ (self):
return len(self.list)
def is_empty (self):
return self.list == []
def first (self):
return self.list[0]
def push (self, data):
self.list.append(data)
def pop (self):
if self.list:
return 1, self.list.pop(0)
else:
return 0, None
# Given 'haystack', see if any prefix of 'needle' is at its end. This
# assumes an exact match has already been checked. Return the number of
# characters matched.
# for example:
# f_p_a_e ("qwerty\r", "\r\n") => 1
# f_p_a_e ("qwertydkjf", "\r\n") => 0
# f_p_a_e ("qwerty\r\n", "\r\n") =>
# this could maybe be made faster with a computed regex?
# [answer: no; circa Python-2.0, Jan 2001]
# new python: 28961/s
# old python: 18307/s
# re: 12820/s
# regex: 14035/s
def find_prefix_at_end (haystack, needle):
l = len(needle) - 1
while l and not haystack.endswith(needle[:l]):
l -= 1
return l
================================================
FILE: supervisor/medusa/asyncore_25.py
================================================
# -*- Mode: Python -*-
# Id: asyncore.py,v 2.51 2000/09/07 22:29:26 rushing Exp
# Author: Sam Rushing
# ======================================================================
# Copyright 1996 by Sam Rushing
#
# All Rights Reserved
#
# Permission to use, copy, modify, and distribute this software and
# its documentation for any purpose and without fee is hereby
# granted, provided that the above copyright notice appear in all
# copies and that both that copyright notice and this permission
# notice appear in supporting documentation, and that the name of Sam
# Rushing not be used in advertising or publicity pertaining to
# distribution of the software without specific, written prior
# permission.
#
# SAM RUSHING DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE,
# INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN
# NO EVENT SHALL SAM RUSHING BE LIABLE FOR ANY SPECIAL, INDIRECT OR
# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
# ======================================================================
"""Basic infrastructure for asynchronous socket service clients and servers.
There are only two ways to have a program on a single processor do "more
than one thing at a time". Multi-threaded programming is the simplest and
most popular way to do it, but there is another very different technique,
that lets you have nearly all the advantages of multi-threading, without
actually using multiple threads. it's really only practical if your program
is largely I/O bound. If your program is CPU bound, then preemptive
scheduled threads are probably what you really need. Network servers are
rarely CPU-bound, however.
If your operating system supports the select() system call in its I/O
library (and nearly all do), then you can use it to juggle multiple
communication channels at once; doing other work while your I/O is taking
place in the "background." Although this strategy can seem strange and
complex, especially at first, it is in many ways easier to understand and
control than multi-threaded programming. The module documented here solves
many of the difficult problems for you, making the task of building
sophisticated high-performance network servers and clients a snap.
"""
import select
import socket
import sys
import time
import os
from errno import EALREADY, EINPROGRESS, EWOULDBLOCK, ECONNRESET, \
ENOTCONN, ESHUTDOWN, EINTR, EISCONN, errorcode
from supervisor.compat import as_string, as_bytes
try:
socket_map
except NameError:
socket_map = {}
class ExitNow(Exception):
pass
def read(obj):
try:
obj.handle_read_event()
except ExitNow:
raise
except:
obj.handle_error()
def write(obj):
try:
obj.handle_write_event()
except ExitNow:
raise
except:
obj.handle_error()
def _exception (obj):
try:
obj.handle_expt_event()
except ExitNow:
raise
except:
obj.handle_error()
def readwrite(obj, flags):
try:
if flags & (select.POLLIN | select.POLLPRI):
obj.handle_read_event()
if flags & select.POLLOUT:
obj.handle_write_event()
if flags & (select.POLLERR | select.POLLHUP | select.POLLNVAL):
obj.handle_expt_event()
except ExitNow:
raise
except:
obj.handle_error()
def poll(timeout=0.0, map=None):
if map is None:
map = socket_map
if map:
r = []; w = []; e = []
for fd, obj in map.items():
is_r = obj.readable()
is_w = obj.writable()
if is_r:
r.append(fd)
if is_w:
w.append(fd)
if is_r or is_w:
e.append(fd)
if [] == r == w == e:
time.sleep(timeout)
else:
try:
r, w, e = select.select(r, w, e, timeout)
except select.error as err:
if err.args[0] != EINTR:
raise
else:
return
for fd in r:
obj = map.get(fd)
if obj is None:
continue
read(obj)
for fd in w:
obj = map.get(fd)
if obj is None:
continue
write(obj)
for fd in e:
obj = map.get(fd)
if obj is None:
continue
_exception(obj)
def poll2(timeout=0.0, map=None):
# Use the poll() support added to the select module in Python 2.0
if map is None:
map = socket_map
if timeout is not None:
# timeout is in milliseconds
timeout = int(timeout*1000)
pollster = select.poll()
if map:
for fd, obj in map.items():
flags = 0
if obj.readable():
flags |= select.POLLIN | select.POLLPRI
if obj.writable():
flags |= select.POLLOUT
if flags:
# Only check for exceptions if object was either readable
# or writable.
flags |= select.POLLERR | select.POLLHUP | select.POLLNVAL
pollster.register(fd, flags)
try:
r = pollster.poll(timeout)
except select.error as err:
if err.args[0] != EINTR:
raise
r = []
for fd, flags in r:
obj = map.get(fd)
if obj is None:
continue
readwrite(obj, flags)
poll3 = poll2 # Alias for backward compatibility
def loop(timeout=30.0, use_poll=False, map=None, count=None):
if map is None:
map = socket_map
if use_poll and hasattr(select, 'poll'):
poll_fun = poll2
else:
poll_fun = poll
if count is None:
while map:
poll_fun(timeout, map)
else:
while map and count > 0:
poll_fun(timeout, map)
count -= 1
class dispatcher:
debug = False
connected = False
accepting = False
closing = False
addr = None
def __init__(self, sock=None, map=None):
if map is None:
self._map = socket_map
else:
self._map = map
if sock:
self.set_socket(sock, map)
# I think it should inherit this anyway
self.socket.setblocking(0)
self.connected = True
# XXX Does the constructor require that the socket passed
# be connected?
try:
self.addr = sock.getpeername()
except socket.error:
# The addr isn't crucial
pass
else:
self.socket = None
def __repr__(self):
status = [self.__class__.__module__+"."+self.__class__.__name__]
if self.accepting and self.addr:
status.append('listening')
elif self.connected:
status.append('connected')
if self.addr is not None:
try:
status.append('%s:%d' % self.addr)
except TypeError:
status.append(repr(self.addr))
return '<%s at %#x>' % (' '.join(status), id(self))
def add_channel(self, map=None):
#self.log_info('adding channel %s' % self)
if map is None:
map = self._map
map[self._fileno] = self
def del_channel(self, map=None):
fd = self._fileno
if map is None:
map = self._map
if fd in map:
#self.log_info('closing channel %d:%s' % (fd, self))
del map[fd]
self._fileno = None
def create_socket(self, family, type):
self.family_and_type = family, type
self.socket = socket.socket(family, type)
self.socket.setblocking(0)
self._fileno = self.socket.fileno()
self.add_channel()
def set_socket(self, sock, map=None):
self.socket = sock
## self.__dict__['socket'] = sock
self._fileno = sock.fileno()
self.add_channel(map)
def set_reuse_addr(self):
# try to re-use a server port if possible
try:
self.socket.setsockopt(
socket.SOL_SOCKET, socket.SO_REUSEADDR,
self.socket.getsockopt(socket.SOL_SOCKET,
socket.SO_REUSEADDR) | 1
)
except socket.error:
pass
# ==================================================
# predicates for select()
# these are used as filters for the lists of sockets
# to pass to select().
# ==================================================
def readable(self):
return True
def writable(self):
return True
# ==================================================
# socket object methods.
# ==================================================
def listen(self, num):
self.accepting = True
if os.name == 'nt' and num > 5:
num = 1
return self.socket.listen(num)
def bind(self, addr):
self.addr = addr
return self.socket.bind(addr)
def connect(self, address):
self.connected = False
err = self.socket.connect_ex(address)
# XXX Should interpret Winsock return values
if err in (EINPROGRESS, EALREADY, EWOULDBLOCK):
return
if err in (0, EISCONN):
self.addr = address
self.connected = True
self.handle_connect()
else:
raise socket.error(err, errorcode[err])
def accept(self):
# XXX can return either an address pair or None
try:
conn, addr = self.socket.accept()
return conn, addr
except socket.error as why:
if why.args[0] == EWOULDBLOCK:
pass
else:
raise
def send(self, data):
try:
result = self.socket.send(data)
return result
except socket.error as why:
if why.args[0] == EWOULDBLOCK:
return 0
else:
raise
def recv(self, buffer_size):
try:
data = self.socket.recv(buffer_size)
if not data:
# a closed connection is indicated by signaling
# a read condition, and having recv() return 0.
self.handle_close()
return b''
else:
return data
except socket.error as why:
# winsock sometimes throws ENOTCONN
if why.args[0] in [ECONNRESET, ENOTCONN, ESHUTDOWN]:
self.handle_close()
return b''
else:
raise
def close(self):
self.del_channel()
try:
self.socket.shutdown(socket.SHUT_RDWR)
except socket.error:
# must swallow exception from already-closed socket
# (at least with Python 3.11.7 on macOS 14.2.1)
pass
# does not raise if called on already-closed socket
self.socket.close()
# cheap inheritance, used to pass all other attribute
# references to the underlying socket object.
def __getattr__(self, attr):
return getattr(self.socket, attr)
# log and log_info may be overridden to provide more sophisticated
# logging and warning methods. In general, log is for 'hit' logging
# and 'log_info' is for informational, warning and error logging.
def log(self, message):
sys.stderr.write('log: %s\n' % str(message))
def log_info(self, message, type='info'):
if __debug__ or type != 'info':
print('%s: %s' % (type, message))
def handle_read_event(self):
if self.accepting:
# for an accepting socket, getting a read implies
# that we are connected
if not self.connected:
self.connected = True
self.handle_accept()
elif not self.connected:
self.handle_connect()
self.connected = True
self.handle_read()
else:
self.handle_read()
def handle_write_event(self):
# getting a write implies that we are connected
if not self.connected:
self.handle_connect()
self.connected = True
self.handle_write()
def handle_expt_event(self):
self.handle_expt()
def handle_error(self):
nil, t, v, tbinfo = compact_traceback()
# sometimes a user repr method will crash.
try:
self_repr = repr(self)
except:
self_repr = '<__repr__(self) failed for object at %0x>' % id(self)
self.log_info(
'uncaptured python exception, closing channel %s (%s:%s %s)' % (
self_repr,
t,
v,
tbinfo
),
'error'
)
self.close()
def handle_expt(self):
self.log_info('unhandled exception', 'warning')
def handle_read(self):
self.log_info('unhandled read event', 'warning')
def handle_write(self):
self.log_info('unhandled write event', 'warning')
def handle_connect(self):
self.log_info('unhandled connect event', 'warning')
def handle_accept(self):
self.log_info('unhandled accept event', 'warning')
def handle_close(self):
self.log_info('unhandled close event', 'warning')
self.close()
# ---------------------------------------------------------------------------
# adds simple buffered output capability, useful for simple clients.
# [for more sophisticated usage use asynchat.async_chat]
# ---------------------------------------------------------------------------
class dispatcher_with_send(dispatcher):
def __init__(self, sock=None, map=None):
dispatcher.__init__(self, sock, map)
self.out_buffer = b''
def initiate_send(self):
num_sent = dispatcher.send(self, self.out_buffer[:512])
self.out_buffer = self.out_buffer[num_sent:]
def handle_write(self):
self.initiate_send()
def writable(self):
return (not self.connected) or len(self.out_buffer)
def send(self, data):
if self.debug:
self.log_info('sending %s' % repr(data))
self.out_buffer = self.out_buffer + data
self.initiate_send()
# ---------------------------------------------------------------------------
# used for debugging.
# ---------------------------------------------------------------------------
def compact_traceback():
t, v, tb = sys.exc_info()
tbinfo = []
assert tb # Must have a traceback
while tb:
tbinfo.append((
tb.tb_frame.f_code.co_filename,
tb.tb_frame.f_code.co_name,
str(tb.tb_lineno)
))
tb = tb.tb_next
# just to be safe
del tb
file, function, line = tbinfo[-1]
info = ' '.join(['[%s|%s|%s]' % x for x in tbinfo])
return (file, function, line), t, v, info
def close_all(map=None):
if map is None:
map = socket_map
for x in map.values():
x.socket.close()
map.clear()
# Asynchronous File I/O:
#
# After a little research (reading man pages on various unixen, and
# digging through the linux kernel), I've determined that select()
# isn't meant for doing asynchronous file i/o.
# Heartening, though - reading linux/mm/filemap.c shows that linux
# supports asynchronous read-ahead. So _MOST_ of the time, the data
# will be sitting in memory for us already when we go to read it.
#
# What other OS's (besides NT) support async file i/o? [VMS?]
#
# Regardless, this is useful for pipes, and stdin/stdout...
if os.name == 'posix':
import fcntl
class file_wrapper:
# here we override just enough to make a file
# look like a socket for the purposes of asyncore.
def __init__(self, fd):
self.fd = fd
def recv(self, buffersize):
return as_string(os.read(self.fd, buffersize))
def send(self, s):
return os.write(self.fd, as_bytes(s))
read = recv
write = send
def close(self):
os.close(self.fd)
def fileno(self):
return self.fd
class file_dispatcher(dispatcher):
def __init__(self, fd, map=None):
dispatcher.__init__(self, None, map)
self.connected = True
self.set_file(fd)
# set it to non-blocking mode
flags = fcntl.fcntl(fd, fcntl.F_GETFL, 0)
flags |= os.O_NONBLOCK
fcntl.fcntl(fd, fcntl.F_SETFL, flags)
def set_file(self, fd):
self._fileno = fd
self.socket = file_wrapper(fd)
self.add_channel()
================================================
FILE: supervisor/medusa/auth_handler.py
================================================
# -*- Mode: Python -*-
#
# Author: Sam Rushing
# Copyright 1996-2000 by Sam Rushing
# All Rights Reserved.
#
RCS_ID = '$Id: auth_handler.py,v 1.6 2002/11/25 19:40:23 akuchling Exp $'
# support for 'basic' authentication.
import re
import sys
import time
from supervisor.compat import as_string, as_bytes
from supervisor.compat import encodestring, decodestring
from supervisor.compat import long
from supervisor.compat import md5
import supervisor.medusa.counter as counter
import supervisor.medusa.default_handler as default_handler
get_header = default_handler.get_header
import supervisor.medusa.producers as producers
# This is a 'handler' that wraps an authorization method
# around access to the resources normally served up by
# another handler.
# does anyone support digest authentication? (rfc2069)
class auth_handler:
def __init__ (self, dict, handler, realm='default'):
self.authorizer = dictionary_authorizer (dict)
self.handler = handler
self.realm = realm
self.pass_count = counter.counter()
self.fail_count = counter.counter()
def match (self, request):
# by default, use the given handler's matcher
return self.handler.match (request)
def handle_request (self, request):
# authorize a request before handling it...
scheme = get_header (AUTHORIZATION, request.header)
if scheme:
scheme = scheme.lower()
if scheme == 'basic':
cookie = get_header (AUTHORIZATION, request.header, 2)
try:
decoded = as_string(decodestring(as_bytes(cookie)))
except:
sys.stderr.write('malformed authorization info <%s>\n' % cookie)
request.error (400)
return
auth_info = decoded.split(':', 1)
if self.authorizer.authorize (auth_info):
self.pass_count.increment()
request.auth_info = auth_info
self.handler.handle_request (request)
else:
self.handle_unauthorized (request)
#elif scheme == 'digest':
# print 'digest: ',AUTHORIZATION.group(2)
else:
sys.stderr.write('unknown/unsupported auth method: %s\n' % scheme)
self.handle_unauthorized(request)
else:
# list both? prefer one or the other?
# you could also use a 'nonce' here. [see below]
#auth = 'Basic realm="%s" Digest realm="%s"' % (self.realm, self.realm)
#nonce = self.make_nonce (request)
#auth = 'Digest realm="%s" nonce="%s"' % (self.realm, nonce)
#request['WWW-Authenticate'] = auth
#print 'sending header: %s' % request['WWW-Authenticate']
self.handle_unauthorized (request)
def handle_unauthorized (self, request):
# We are now going to receive data that we want to ignore.
# to ignore the file data we're not interested in.
self.fail_count.increment()
request.channel.set_terminator (None)
request['Connection'] = 'close'
request['WWW-Authenticate'] = 'Basic realm="%s"' % self.realm
request.error (401)
def make_nonce (self, request):
"""A digest-authentication , constructed as suggested in RFC 2069"""
ip = request.channel.server.ip
now = str(long(time.time()))
if now[-1:] == 'L':
now = now[:-1]
private_key = str (id (self))
nonce = ':'.join([ip, now, private_key])
return self.apply_hash (nonce)
def apply_hash (self, s):
"""Apply MD5 to a string , then wrap it in base64 encoding."""
m = md5()
m.update (s)
d = m.digest()
# base64.encodestring tacks on an extra linefeed.
return encodestring (d)[:-1]
def status (self):
# Thanks to mwm@contessa.phone.net (Mike Meyer)
r = [
producers.simple_producer (
'
')
)
return producers.composite_producer(r)
class dictionary_authorizer:
def __init__ (self, dict):
self.dict = dict
def authorize (self, auth_info):
[username, password] = auth_info
if username in self.dict and self.dict[username] == password:
return 1
else:
return 0
AUTHORIZATION = re.compile (
# scheme challenge
'Authorization: ([^ ]+) (.*)',
re.IGNORECASE
)
================================================
FILE: supervisor/medusa/counter.py
================================================
# -*- Mode: Python -*-
# It is tempting to add an __int__ method to this class, but it's not
# a good idea. This class tries to gracefully handle integer
# overflow, and to hide this detail from both the programmer and the
# user. Note that the __str__ method can be relied on for printing out
# the value of a counter:
#
# >>> print 'Total Client: %s' % self.total_clients
#
# If you need to do arithmetic with the value, then use the 'as_long'
# method, the use of long arithmetic is a reminder that the counter
# will overflow.
from supervisor.compat import long
class counter:
"""general-purpose counter"""
def __init__ (self, initial_value=0):
self.value = initial_value
def increment (self, delta=1):
result = self.value
try:
self.value = self.value + delta
except OverflowError:
self.value = long(self.value) + delta
return result
def decrement (self, delta=1):
result = self.value
try:
self.value = self.value - delta
except OverflowError:
self.value = long(self.value) - delta
return result
def as_long (self):
return long(self.value)
def __nonzero__ (self):
return self.value != 0
__bool__ = __nonzero__
def __repr__ (self):
return '' % (self.value, id(self))
def __str__ (self):
s = str(long(self.value))
if s[-1:] == 'L':
s = s[:-1]
return s
================================================
FILE: supervisor/medusa/default_handler.py
================================================
# -*- Mode: Python -*-
#
# Author: Sam Rushing
# Copyright 1997 by Sam Rushing
# All Rights Reserved.
#
RCS_ID = '$Id: default_handler.py,v 1.8 2002/08/01 18:15:45 akuchling Exp $'
# standard python modules
import mimetypes
import re
import stat
# medusa modules
import supervisor.medusa.http_date as http_date
import supervisor.medusa.http_server as http_server
import supervisor.medusa.producers as producers
from supervisor.medusa.util import html_repr
unquote = http_server.unquote
# This is the 'default' handler. it implements the base set of
# features expected of a simple file-delivering HTTP server. file
# services are provided through a 'filesystem' object, the very same
# one used by the FTP server.
#
# You can replace or modify this handler if you want a non-standard
# HTTP server. You can also derive your own handler classes from
# it.
#
# support for handling POST requests is available in the derived
# class , defined below.
#
from supervisor.medusa.counter import counter
class default_handler:
valid_commands = ['GET', 'HEAD']
IDENT = 'Default HTTP Request Handler'
# Pathnames that are tried when a URI resolves to a directory name
directory_defaults = [
'index.html',
'default.html'
]
default_file_producer = producers.file_producer
def __init__ (self, filesystem):
self.filesystem = filesystem
# count total hits
self.hit_counter = counter()
# count file deliveries
self.file_counter = counter()
# count cache hits
self.cache_counter = counter()
hit_counter = 0
def __repr__ (self):
return '<%s (%s hits) at %x>' % (
self.IDENT,
self.hit_counter,
id (self)
)
# always match, since this is a default
def match (self, request):
return 1
# handle a file request, with caching.
def handle_request (self, request):
if request.command not in self.valid_commands:
request.error (400) # bad request
return
self.hit_counter.increment()
path, params, query, fragment = request.split_uri()
if '%' in path:
path = unquote (path)
# strip off all leading slashes
while path and path[0] == '/':
path = path[1:]
if self.filesystem.isdir (path):
if path and path[-1] != '/':
request['Location'] = 'http://%s/%s/' % (
request.channel.server.server_name,
path
)
request.error (301)
return
# we could also generate a directory listing here,
# may want to move this into another method for that
# purpose
found = 0
if path and path[-1] != '/':
path += '/'
for default in self.directory_defaults:
p = path + default
if self.filesystem.isfile (p):
path = p
found = 1
break
if not found:
request.error (404) # Not Found
return
elif not self.filesystem.isfile (path):
request.error (404) # Not Found
return
file_length = self.filesystem.stat (path)[stat.ST_SIZE]
ims = get_header_match (IF_MODIFIED_SINCE, request.header)
length_match = 1
if ims:
length = ims.group (4)
if length:
try:
length = int(length)
if length != file_length:
length_match = 0
except:
pass
ims_date = 0
if ims:
ims_date = http_date.parse_http_date (ims.group (1))
try:
mtime = self.filesystem.stat (path)[stat.ST_MTIME]
except:
request.error (404)
return
if length_match and ims_date:
if mtime <= ims_date:
request.reply_code = 304
request.done()
self.cache_counter.increment()
return
try:
file = self.filesystem.open (path, 'rb')
except IOError:
request.error (404)
return
request['Last-Modified'] = http_date.build_http_date (mtime)
request['Content-Length'] = file_length
self.set_content_type (path, request)
if request.command == 'GET':
request.push (self.default_file_producer (file))
self.file_counter.increment()
request.done()
def set_content_type (self, path, request):
typ, encoding = mimetypes.guess_type(path)
if typ is not None:
request['Content-Type'] = typ
else:
# TODO: test a chunk off the front of the file for 8-bit
# characters, and use application/octet-stream instead.
request['Content-Type'] = 'text/plain'
def status (self):
return producers.simple_producer (
'
%s' % html_repr (self)
+ '
'
+ '
Total Hits: %s' % self.hit_counter
+ '
Files Delivered: %s' % self.file_counter
+ '
Cache Hits: %s' % self.cache_counter
+ '
'
)
# HTTP/1.0 doesn't say anything about the "; length=nnnn" addition
# to this header. I suppose its purpose is to avoid the overhead
# of parsing dates...
IF_MODIFIED_SINCE = re.compile (
'If-Modified-Since: ([^;]+)((; length=([0-9]+)$)|$)',
re.IGNORECASE
)
USER_AGENT = re.compile ('User-Agent: (.*)', re.IGNORECASE)
CONTENT_TYPE = re.compile (
r'Content-Type: ([^;]+)((; boundary=([A-Za-z0-9\'\(\)+_,./:=?-]+)$)|$)',
re.IGNORECASE
)
get_header = http_server.get_header
get_header_match = http_server.get_header_match
def get_extension (path):
dirsep = path.rfind('/')
dotsep = path.rfind('.')
if dotsep > dirsep:
return path[dotsep+1:]
else:
return ''
================================================
FILE: supervisor/medusa/docs/README.html
================================================
What is Medusa?
Medusa is an architecture for very-high-performance TCP/IP servers
(like HTTP, FTP, and NNTP). Medusa is different from most other
servers because it runs as a single process, multiplexing I/O with its
various client and server connections within a single process/thread.
It is capable of smoother and higher performance than most other
servers, while placing a dramatically reduced load on the server
machine. The single-process, single-thread model simplifies design
and enables some new persistence capabilities that are otherwise
difficult or impossible to implement.
Medusa is supported on any platform that can run Python and includes a
functional implementation of the <socket> and <select>
modules. This includes the majority of Unix implementations.
During development, it is constantly tested on Linux and Win32
[Win95/WinNT], but the core asynchronous capability has been shown to
work on several other platforms, including the Macintosh. It might
even work on VMS.
The Power of Python
A distinguishing feature of Medusa is that it is written entirely in
Python. Python (http://www.python.org/) is a
'very-high-level' object-oriented language developed by Guido van
Rossum (currently at CNRI). It is easy to learn, and includes many
modern programming features such as storage management, dynamic
typing, and an extremely flexible object system. It also provides
convenient interfaces to C and C++.
The rapid prototyping and delivery capabilities are hard to exaggerate;
for example
It took me longer to read the documentation for persistent HTTP
connections (the 'Keep-Alive' connection token) than to add the
feature to Medusa.
A simple IRC-like chat server system was written in about 90 minutes.
I've heard similar stories from alpha test sites, and other users of
the core async library.
Server Notes
Both the FTP and HTTP servers use an abstracted 'filesystem object' to
gain access to a given directory tree. One possible server extension
technique would be to build behavior into this filesystem object,
rather than directly into the server: Then the extension could be
shared with both the FTP and HTTP servers.
HTTP
The core HTTP server itself is quite simple - all functionality is
provided through 'extensions'. Extensions can be plugged in
dynamically. [i.e., you could log in to the server via the monitor
service and add or remove an extension on the fly]. The basic
file-delivery service is provided by a 'default' extension, which
matches all URI's. You can build more complex behavior by replacing
or extending this class.
The default extension includes support for the 'Connection: Keep-Alive'
token, and will re-use a client channel when requested by the client.
FTP
On Unix, the ftp server includes support for 'real' users, so that it
may be used as a drop-in replacement for the normal ftp server. Since
most ftp servers on Unix use the 'forking' model, each child process
changes its user/group persona after a successful login. This is a
appears to be a secure design.
Medusa takes a different approach - whenever Medusa performs an
operation for a particular user [listing a directory, opening a file],
it temporarily switches to that user's persona _only_ for the duration
of the operation. [and each such operation is protected by a
try/finally exception handler].
To do this Medusa MUST run with super-user privileges. This is a
HIGHLY experimental approach, and although it has been thoroughly
tested on Linux, security problems may still exist. If you are
concerned about the security of your server machine, AND YOU SHOULD
BE, I suggest running Medusa's ftp server in anonymous-only mode,
under an account with limited privileges ('nobody' is usually used for
this purpose).
I am very interested in any feedback on this feature, most
especially information on how the server behaves on different
implementations of Unix, and of course any security problems that are
found.
Monitor
The monitor server gives you remote, 'back-door' access to your server
while it is running. It implements a remote python interpreter. Once
connected to the monitor, you can do just about anything you can do from
the normal python interpreter. You can examine data structures, servers,
connection objects. You can enable or disable extensions, restart the server,
reload modules, etc...
The monitor server is protected with an MD5-based authentication
similar to that proposed in RFC1725 for the POP3 protocol. The server
sends the client a timestamp, which is then appended to a secret
password. The resulting md5 digest is sent back to the server, which
then compares this to the expected result. Failed login attempts are
logged and immediately disconnected. The password itself is not sent
over the network (unless you have foolishly transmitted it yourself
through an insecure telnet or X11 session. 8^)
For this reason telnet cannot be used to connect to the monitor
server when it is in a secure mode (the default). A client program is
provided for this purpose. You will be prompted for a password when
starting up the server, and by the monitor client.
For extra added security on Unix, the monitor server will
eventually be able to use a Unix-domain socket, which can be protected
behind a 'firewall' directory (similar to the InterNet News server).
Performance Notes
The select() function
At the heart of Medusa is a single select() loop.
This loop handles all open socket connections, both servers and
clients. It is in effect constantly asking the system: 'which of
these sockets has activity?'. Performance of this system call can
vary widely between operating systems.
There are also often builtin limitations to the number of sockets
('file descriptors') that a single process, or a whole system, can
manipulate at the same time. Early versions of Linux placed draconian
limits (256) that have since been raised. Windows 95 has a limit of
64, while OSF/1 seems to allow up to 4096.
These limits don't affect only Medusa, you will find them described
in the documentation for other web and ftp servers, too.
The documentation for the Apache web server has some excellent
notes on tweaking performance for various Unix implementations. See
http://www.apache.org/docs/misc/perf.html
for more information.
Buffer sizes
The default buffer sizes used by Medusa are set with a bias toward
Internet-based servers: They are relatively small, so that the buffer
overhead for each connection is low. The assumption is that Medusa
will be talking to a large number of low-bandwidth connections, rather
than a smaller number of high bandwidth.
This choice trades run-time memory use for efficiency - the down
side of this is that high-speed local connections (i.e., over a local
ethernet) will transfer data at a slower rate than necessary.
This parameter can easily be tweaked by the site designer, and can
in fact be adjusted on a per-server or even per-client basis. For
example, you could have the FTP server use larger buffer sizes for
connections from certain domains.
If there's enough interest, I have some rough ideas for how to make
these buffer sizes automatically adjust to an optimal setting. Send
email if you'd like to see this feature.
See ./medusa.html for a brief overview of
some of the ideas behind Medusa's design, and for a description of
current and upcoming features.
Enjoy!
-Sam Rushing
rushing@nightmare.com
================================================
FILE: supervisor/medusa/docs/async_blurbs.txt
================================================
[from the win32 sdk named pipe documentation]
==================================================
The simplest server process can use the CreateNamedPipe function to
create a single instance of a pipe, connect to a single client,
communicate with the client, disconnect the pipe, close the pipe
handle, and terminate. Typically, however, a server process must
communicate with multiple client processes. A server process can use a
single pipe instance by connecting to and disconnecting from each
client in sequence, but performance would be poor. To handle multiple
clients simultaneously, the server process must create multiple pipe
instances.
There are three basic strategies for servicing multiple pipe instances.
Create multiple threads (and/or processes) with a separate thread
for each instance of the pipe. For an example of a multithreaded
server process, see Multithreaded Server.
Overlap operations by specifying an OVERLAPPED structure in the
ReadFile, WriteFile, and ConnectNamedPipe functions. For an example of
a server process that uses overlapped operations, see Server Using
Overlapped Input and Output.
Overlap operations by using the ReadFileEx and WriteFileEx
functions, which specify a completion routine to be executed when the
operation is complete. For an example of a server process that uses
completion routines, see Server Using Completion Routines.
The multithreaded server strategy is easy to write, because the thread
for each instance handles communications for only a single client. The
system allocates processor time to each thread as needed. But each
thread uses system resources, which is a potential disadvantage for a
server that handles a large number of clients. Other complications
occur if the actions of one client necessitate communications with
other clients (as for a network game program, where a move by one
player must be communicated to the other players).
With a single-threaded server, it is easier to coordinate operations
that affect multiple clients, and it is easier to protect shared
resources (for example, a database file) from simultaneous access by
multiple clients. The challenge of a single-threaded server is that it
requires coordination of overlapped operations in order to allocate
processor time for handling the simultaneous needs of the clients.
==================================================
================================================
FILE: supervisor/medusa/docs/data_flow.html
================================================
Data Flow in Medusa
Data flow, both input and output, is asynchronous. This is
signified by the request and reply queues in the above
diagram. This means that both requests and replies can get 'backed
up', and are still handled correctly. For instance, HTTP/1.1 supports
the concept of pipelined requests, where a series of requests
are sent immediately to a server, and the replies are sent as they are
processed. With a synchronous request, the client would have
to wait for a reply to each request before sending the next.
The input data is partitioned into requests by looking for a
terminator. A terminator is simply a protocol-specific
delimiter - often simply CRLF (carriage-return line-feed), though it
can be longer (for example, MIME multi-part boundaries can be
specified as terminators). The protocol handler is notified whenever
a complete request has been received.
The protocol handler then generates a reply, which is enqueued for
output back to the client. Sometimes, instead of queuing the actual
data, an object that will generate this data is used, called a
producer.
The use of producers gives the programmer
extraordinary control over how output is generated and inserted into
the output queue. Though they are simple objects (requiring only a
single method, more(), to be defined), they can be
composed - simple producers can be wrapped around each other to
create arbitrarily complex behaviors. [now would be a good time to
browse through some of the producer classes in
producers.py.]
The HTTP/1.1 producers make an excellent example. HTTP allows
replies to be encoded in various ways - for example a reply consisting
of dynamically-generated output might use the 'chunked' transfer
encoding to send data that is compressed on-the-fly.
In the diagram, green producers actually generate output, and grey
ones transform it in some manner. This producer might generate output
looking like this:
HTTP/1.1 200 OK
Content-Encoding: gzip
Transfer-Encoding: chunked
Header ==> Date: Mon, 04 Aug 1997 21:31:44 GMT
Content-Type: text/html
Server: Medusa/3.0
Chunking ==> 0x200
Compression ==> <512 bytes of compressed html>
0x200
<512 bytes of compressed html>
...
0
Still more can be done with this output stream: For the purpose of
efficiency, it makes sense to send output in large, fixed-size chunks:
This transformation can be applied by wrapping a 'globbing' producer
around the whole thing.
An important feature of Medusa's producers is that they are
actually rather small objects that do not expand into actual output
data until the moment they are needed: The async_chat
class will only call on a producer for output when the outgoing socket
has indicated that it is ready for data. Thus Medusa is extremely
efficient when faced with network delays, 'hiccups', and low bandwidth
clients.
One final note: The mechanisms described above are completely
general - although the examples given demonstrate application to the
http protocol, Medusa's asynchronous core has been
applied to many different protocols, including smtp,
pop3, ftp, and even dns.
================================================
FILE: supervisor/medusa/docs/programming.html
================================================
Programming in Python with Medusa and the Async Sockets Library
Programming in Python with Medusa and the Async Sockets Library
Introduction
Why Asynchronous?
There are only two ways to have a program on a single processor do
'more than one thing at a time'. Multi-threaded programming is
the simplest and most popular way to do it, but there is another
very different technique, that lets you have nearly all the
advantages of multi-threading, without actually using multiple
threads. It's really only practical if your program is I/O
bound (I/O is the principle bottleneck). If your program is
CPU bound, then pre-emptive scheduled threads are probably what
you really need. Network servers are rarely CPU-bound, however.
If your operating system supports the select()
system call in its I/O library (and nearly all do), then you can
use it to juggle multiple communication channels at once; doing
other work while your I/O is taking place in the "background".
Although this strategy can seem strange and complex (especially
at first), it is in many ways easier to understand and control
than multi-threaded programming. The library documented here
solves many of the difficult problems for you, making the task
of building sophisticated high-performance network servers and
clients a snap.
Select-based multiplexing in the real world
Several well-known Web servers (and other programs) are written using
exactly this technique:
the thttpd
and Zeus,
and Squid Internet Object Cache servers
are excellent examples..
The InterNet News server (INN) used
this technique for several years before the web exploded.
An interesting web server comparison chart is available at the
thttpd web site
Variations on a Theme: poll() and WaitForMultipleObjects
Of similar (but better) design is the poll() system
call. The main advantage of poll() (for our
purposes) is that it does not used fixed-size file-descriptor
tables, and is thus more easily scalable than
select(). poll() is only recently becoming
widely available, so you need to check for availability on your particular
operating system.
In the Windows world, the Win32 API provides a bewildering array
of features for multiplexing. Although slightly different in
semantics, the combination of Event objects and the
WaitForMultipleObjects() interface gives
essentially the same power as select() on Unix. A
version of this library specific to Win32 has not been written
yet, mostly because Win32 also provides select()
(at least for sockets). If such an interface were written, it
would have the advantage of allowing us to multiplex on other
objects types, like named pipes and files.
select()
Here's what select() does: you pass in a set of
file descriptors, in effect asking the operating system, "let me
know when anything happens to any of these descriptors". (A
descriptor is simply a numeric handle used by the
operating system to keep track of a file, socket, pipe, or other
I/O object. It is usually an index into a system table of some
kind). You can also use a timeout, so that if nothing
happens in the allotted period, select() will return
control to your program.
select() takes three fd_set arguments;
one for each of the following possible states/events:
readability, writability, and exceptional conditions. The last set
is less useful than it sounds; in the context of TCP/IP it refers
to the presence of out-of-band (OOB) data. OOB is a relatively unportable
and poorly used feature that you can (and should) ignore unless you really
need it.
So that leaves only two types of events to build our programs
around; read events and write events. As it turns
out, this is actually enough to get by with, because other types
of events can be implied by the sequencing of these two. It
also keeps the low-level interface as simple as possible -
always a good thing in my book.
The polling loop
Now that you know what select() does, you're ready
for the final piece of the puzzle: the main polling loop. This
is nothing more than a simple while loop that continually calls
select() with a timeout (I usually use a 30-second
timeout). Such a program will use virtually no CPU if your
server is idle; it spends most of its time letting the operating
system do the waiting for it. This is much more efficient than a
busy-wait
loop.
Here is a pseudo-code example of a polling loop:
while (any_descriptors_left):
events = select (descriptors, timeout)
for event in events:
handle_event (event)
If you take a look at the code used by the library, it looks
very similar to this. (see the file asyncore.py,
the functions poll() and loop()). Now, on to the magic that must
take place to handle the events...
The Code
Blocking vs. Non-Blocking
File descriptors can be in either blocking or non-blocking mode.
A descriptor in blocking mode will stop (or 'block') your entire
program until the requested event takes place. For example, if
you ask to read 64 bytes from a descriptor attached to a socket
which is ultimately connected to a modem deep in the backwaters
of the Internet, you may wait a while for those 64 bytes.
If you put the descriptor in non-blocking mode, then one of two
things might happen: if the data is sitting in a local buffer,
it will be returned to you immediately; otherwise you will get
back a code (usually EWOULDBLOCK) telling you that
the read is in progress, and you should check back later to see
if it's done.
sockets vs. other kinds of descriptors
Although most of our discussion will be about TCP/IP sockets, on
Unix you can use select() to multiplex other kinds
of communications objects, like pipes and ttys. (Unfortunately,
select() cannot be used to do non-blocking file I/O. Please
correct me if you have information to the contrary!)
The socket_map
We use a global dictionary (asyncore.socket_map) to
keep track of all the active socket objects. The keys for this
dictionary are the objects themselves. Nothing is stored in the
value slot. Each time through the loop, this dictionary is scanned.
Each object is asked which fd_sets it wants to be in.
These sets are then passed on to select().
asyncore.dispatcher
The first class we'll introduce you to is the
dispatcher class. This is a thin wrapper around a
low-level socket object. We have attached a few methods for
event-handling to it. Otherwise, it can be treated as a normal
non-blocking socket object.
The direct interface between the select loop and the socket object
are the handle_read_event and handle_write_event
methods. These are called whenever an object 'fires' that event.
The firing of these low-level events can tell us whether certain
higher-level events have taken place, depending on the timing
and state of the connection. For example, if we have asked for
a socket to connect to another host, we know that the connection
has been made when the socket fires a write event (at this point
you know that you may write to it with the expectation of
success).
The implied events are
handle_connect.
implied by a write event.
handle_close
implied by a read event with no data available.
handle_accept
implied by a read event on a listening socket.
Thus, the set of user-level events is a little larger than simply
readable and writeable. The full set of
events your code may handle are:
handle_read
handle_write
handle_expt (OOB data)
handle_connect
handle_close
handle_accept
A quick terminology note: In order to distinguish between
low-level socket objects and those based on the async library
classes, I call these higher-level objects channels.
Enough Gibberish, let's write some code
Ok, that's enough abstract talk. Let's do something useful and
concrete with this stuff. We'll write a simple HTTP client that
demonstrates how easy it is to build a powerful tool in only a few
lines of code.
HTTP is (in theory, at least) a very simple protocol. You connect to the
web server, send the string "GET /some/path HTTP/1.0", and the
server will send a short header, followed by the file you asked for. It will
then close the connection.
We have defined a single new class, http_client, derived
from the abstract class asyncore.dispatcher. There are three
event handlers defined.
handle_connect Once we have made the connection, we send the request string.
handle_read As the server sends data back to us, we simply print it out.
handle_write Ignore this for the moment, I'm brushing over a technical detail
we'll clean up in a moment.
Go ahead and run this demo - giving a single URL as an argument, like this:
$ python asynhttp.py http://www.nightmare.com/
You should see something like this:
[rushing@gnome demo]$ python asynhttp.py http://www.nightmare.com/
log: adding channel <http_client at 80ef3e8>
HTTP/1.0 200 OK
Server: Medusa/3.19
Content-Type: text/html
Content-Length: 1649
Last-Modified: Sun, 26 Jul 1998 23:57:51 GMT
Date: Sat, 16 Jan 1999 13:04:30 GMT
[... body of the file ...]
log: unhandled close event
log: closing channel 4:<http_client connected at 80ef3e8>
The 'log' messages are there to help, they are useful when
debugging but you will want to disable them later. The first log message
tells you that a new http_client object has been added to the
socket map. At the end, you'll notice there's a warning that you haven't
bothered to handle the close event. No big deal, for now.
Now at this point we haven't seen anything revolutionary, but that's
because we've only looked at one URL. Go ahead and add a few other URL's
to the argument list; as many as you like - and make sure they're on different
hosts...
Now you begin to see why select() is so powerful. Depending
on your operating system (and its configuration), select() can be
fed hundreds, or even thousands of descriptors like this. (I've recently tested
select() on a FreeBSD box with over 10,000 descriptors).
A really good way to understand select() is to put a print statement
into the asyncore.poll() function:
[...]
(r,w,e) = select.select (r,w,e, timeout)
print '---'
print 'read', r
print 'write', w
[...]
Each time through the loop you will see which channels have fired
which events. If you haven't skipped ahead, you'll also notice a pointless
barrage of events, with all your http_client objects in the 'writable' set.
This is because we were a bit lazy earlier; sweeping some ugliness under
the rug. Let's fix that now.
Buffered Output
In our handle_connect, we cheated a bit by calling
send without examining its return code. In truth,
since we are using a non-blocking socket, it's (theoretically)
possible that our data didn't get sent. To do this correctly,
we actually need to set up a buffer of outgoing data, and then send
as much of the buffer as we can whenever we see a write
event:
The handle_connect method no longer assumes it can
send its request string successfully. We move its work over to
handle_write; which trims self.buffer
as pieces of it are sent successfully.
We also introduce the writable method. Each time
through the loop, the set of sockets is scanned, the
readable and writable methods of each
object are called to see if are interested in those events. The
default methods simply return 1, indicating that by default all
channels will be in both sets. In this case, however, we are only
interested in writing as long as we have something to write. So
we override this method, making its behavior dependent on the length
of self.buffer.
If you try the client now (with the print statements in
asyncore.poll()), you'll see that
select is firing more efficiently.
asynchat.py
The dispatcher class is useful, but somewhat limited in
capability. As you might guess, managing input and output
buffers manually can get complex, especially if you're working
with a protocol more complicated than HTTP.
The async_chat class does a lot of the heavy
lifting for you. It automatically handles the buffering of both
input and output, and provides a "line terminator" facility that
partitions an input stream into logical lines for you. It is
also carefully designed to support pipelining - a nice
feature that we'll explain later.
There are four new methods to introduce:
set_terminator (self, <eol-string>) Set the string used to identify end-of-line. For most
Internet protocols, this is the string \r\n, that is;
a carriage return followed by a line feed. To turn off input scanning,
use None
collect_incoming_data (self, data) Called whenever data is available from
a socket. Usually, your implementation will accumulate this
data into a buffer of some kind.
found_terminator (self) Called whenever an end-of-line marker has been seen. Typically
your code will process and clear the input buffer.
push (data) This is a buffered version of send. It will place
the data in an outgoing buffer.
These methods build on the underlying capabilities of
dispatcher by providing implementations of
handle_readhandle_write, etc...
handle_read collects data into an input buffer, which
is continually scanned for the terminator string. Data in between
terminators is feed to your collect_incoming_data method.
The implementation of handle_write and writable
examine an outgoing-data queue, and automatically send data whenever
possible.
A Proxy Server
In order to demonstrate the async_chat class, we will
put together a simple proxy server. A proxy server combines a server
and a client together, in effect sitting between the real server and
client. You can use this to monitor or debug protocol traffic.
To try out the proxy, find a server (any SMTP, NNTP, or HTTP server should do fine),
and give its hostname and port as arguments:
python proxy.py localhost 25
The proxy server will start up its server on port n +
8000, in this case port 8025. Now, use a telnet program
to connect to that port on your server host. Issue a few
commands. See how the whole session is being echoed by your
proxy server. Try opening up several simultaneous connections
through your proxy. You might also try pointing a real client
(a news reader [port 119] or web browser [port 80]) at your proxy.
Pipelining
Pipelining refers to a protocol capability. Normally, a conversation
with a server has a back-and-forth quality to it. The client sends a
command, and waits for the response. If a client needs to send many commands
over a high-latency connection, waiting for each response can take a long
time.
For example, when sending a mail message to many recipients with
SMTP, the client will send a series of RCPT
commands, one for each recipient. For each of these commands,
the server will send back a reply indicating whether the mailbox
specified is valid. If you want to send a message to several
hundred recipients, this can be rather tedious if the round-trip
time for each command is long. You'd like to be able to send a
bunch of RCPT commands in one batch, and then count
off the responses to them as they come.
I have a favorite visual when explaining the advantages of
pipelining. Imagine each request to the server is a boxcar on a
train. The client is in Los Angeles, and the server is in New
York. Pipelining lets you hook all your cars in one long chain;
send them to New York, where they are filled and sent back to you.
Without pipelining you have to send one car at a time.
Not all protocols allow pipelining. Not all servers support it;
Sendmail, for example, does not support pipelining because it tends
to fork unpredictably, leaving buffered data in a questionable state.
A recent extension to the SMTP protocol allows a server to specify
whether it supports pipelining. HTTP/1.1 explicitly requires that
a server support pipelining.
Servers built on top of async_chat automatically
support pipelining. It is even possible to change the
terminator repeatedly when processing data already in the
input buffer. See the handle_read method if you're
interested in the gory details.
Producers
async_chat supports a sophisticated output
buffering model, using a queue of data-producing objects. For
most purposes, you will use the push() method to
send string data - but for more sophisticated usage you can push
a producer
A producer is a very simple object, requiring only
a single method in its implementation, more(). See
the code for simple_producer in
asynchat.py for an example. Many more examples are
available in the Medusa distribution, in the file
producers.py
Samual M. Rushing
Last modified: Fri Apr 30 21:42:52 PDT 1999
================================================
FILE: supervisor/medusa/docs/proxy_notes.txt
================================================
# we can build 'promises' to produce external data. Each producer
# contains a 'promise' to fetch external data (or an error
# message). writable() for that channel will only return true if the
# top-most producer is ready. This state can be flagged by the dns
# client making a callback.
# So, say 5 proxy requests come in, we can send out DNS queries for
# them immediately. If the replies to these come back before the
# promises get to the front of the queue, so much the better: no
# resolve delay. 8^)
#
# ok, there's still another complication:
# how to maintain replies in order?
# say three requests come in, (to different hosts? can this happen?)
# yet the connections happen third, second, and first. We can't buffer
# the entire request! We need to be able to specify how much to buffer.
#
# ===========================================================================
#
# the current setup is a 'pull' model: whenever the channel fires FD_WRITE,
# we 'pull' data from the producer fifo. what we need is a 'push' option/mode,
# where
# 1) we only check for FD_WRITE when data is in the buffer
# 2) whoever is 'pushing' is responsible for calling 'refill_buffer()'
#
# what is necessary to support this 'mode'?
# 1) writable() only fires when data is in the buffer
# 2) refill_buffer() is only called by the 'pusher'.
#
# how would such a mode affect things? with this mode could we support
# a true http/1.1 proxy? [i.e, support pipelined proxy requests, possibly
# to different hosts, possibly even mixed in with non-proxy requests?] For
# example, it would be nice if we could have the proxy automatically apply the
# 1.1 chunking for 1.0 close-on-eof replies when feeding it to the client. This
# would let us keep our persistent connection.
================================================
FILE: supervisor/medusa/docs/threads.txt
================================================
# -*- Mode: Text; tab-width: 4 -*-
[note, a better solution is now available, see the various modules in
the 'thread' directory (SMR 990105)]
A Workable Approach to Mixing Threads and Medusa.
---------------------------------------------------------------------------
When Medusa receives a request that needs to be handled by a separate
thread, have the thread remove the socket from Medusa's control, by
calling the 'del_channel()' method, and put the socket into
blocking-mode:
request.channel.del_channel()
request.channel.socket.setblocking (0)
Now your thread is responsible for managing the rest of the HTTP
'session'. In particular, you need to send the HTTP response, followed
by any headers, followed by the response body.
Since the most common need for mixing threads and Medusa is to support
CGI, there's one final hurdle that should be pointed out: CGI scripts
sometimes make use of a 'Status:' hack (oops, I meant to say 'header')
in order to tell the server to return a reply other than '200 OK'. To
support this it is necessary to scan the output _before_ it is sent.
Here is a sample 'write' method for a file-like object that performs
this scan:
HEADER_LINE = regex.compile ('\([A-Za-z0-9-]+\): \(.*\)')
def write (self, data):
if self.got_header:
self._write (data)
else:
# CGI scripts may optionally provide extra headers.
#
# If they do not, then the output is assumed to be
# text/html, with an HTTP reply code of '200 OK'.
#
# If they do, we need to scan those headers for one in
# particular: the 'Status:' header, which will tell us
# to use a different HTTP reply code [like '302 Moved']
#
self.buffer = self.buffer + data
lines = self.buffer.split('\n')
# look for something un-header-like
for i in range(len(lines)):
if i == (len(lines)-1):
if lines[i] == '':
break
elif HEADER_LINE.match (lines[i]) == -1:
# this is not a header line.
self.got_header = 1
self.buffer = self.build_header (lines[:i])
# rejoin the rest of the data
self._write('\n'.join(lines[i:]))
break
================================================
FILE: supervisor/medusa/docs/tkinter.txt
================================================
Here are some notes on combining the Tk Event loop with the async lib
and/or Medusa. Many thanks to Aaron Rhodes (alrhodes@cpis.net) for
the info!
> Sam,
>
> Just wanted to send you a quick message about how I managed to
> finally integrate Tkinter with asyncore. This solution is pretty
> straightforward. From the main tkinter event loop i simply added
> a repeating alarm that calls asyncore.poll() every so often. So
> the code looks like this:
>
> in main:
> import asyncore
>
> self.socket_check()
>
> ...
>
> then, socket_check() is:
>
> def socket_check(self):
> asyncore.poll(timeout=0.0)
> self.after(100, self.socket_check)
>
>
> This simply causes asyncore to poll all the sockets every 100ms
> during the tkinter event loop. The GUI doesn't block on IO since
> all the IO calls are now handled with asyncore.
================================================
FILE: supervisor/medusa/filesys.py
================================================
# -*- Mode: Python -*-
# $Id: filesys.py,v 1.9 2003/12/24 16:10:56 akuchling Exp $
# Author: Sam Rushing
#
# Generic filesystem interface.
#
# We want to provide a complete wrapper around any and all
# filesystem operations.
# this class is really just for documentation,
# identifying the API for a filesystem object.
# opening files for reading, and listing directories, should
# return a producer.
from supervisor.compat import long
class abstract_filesystem:
def __init__ (self):
pass
def current_directory (self):
"""Return a string representing the current directory."""
pass
def listdir (self, path, long=0):
"""Return a listing of the directory at 'path' The empty string
indicates the current directory. If 'long' is set, instead
return a list of (name, stat_info) tuples
"""
pass
def open (self, path, mode):
"""Return an open file object"""
pass
def stat (self, path):
"""Return the equivalent of os.stat() on the given path."""
pass
def isdir (self, path):
"""Does the path represent a directory?"""
pass
def isfile (self, path):
"""Does the path represent a plain file?"""
pass
def cwd (self, path):
"""Change the working directory."""
pass
def cdup (self):
"""Change to the parent of the current directory."""
pass
def longify (self, path):
"""Return a 'long' representation of the filename
[for the output of the LIST command]"""
pass
# standard wrapper around a unix-like filesystem, with a 'false root'
# capability.
# security considerations: can symbolic links be used to 'escape' the
# root? should we allow it? if not, then we could scan the
# filesystem on startup, but that would not help if they were added
# later. We will probably need to check for symlinks in the cwd method.
# what to do if wd is an invalid directory?
import os
import stat
import re
def safe_stat (path):
try:
return path, os.stat (path)
except:
return None
class os_filesystem:
path_module = os.path
# set this to zero if you want to disable pathname globbing.
# [we currently don't glob, anyway]
do_globbing = 1
def __init__ (self, root, wd='/'):
self.root = root
self.wd = wd
def current_directory (self):
return self.wd
def isfile (self, path):
p = self.normalize (self.path_module.join (self.wd, path))
return self.path_module.isfile (self.translate(p))
def isdir (self, path):
p = self.normalize (self.path_module.join (self.wd, path))
return self.path_module.isdir (self.translate(p))
def cwd (self, path):
p = self.normalize (self.path_module.join (self.wd, path))
translated_path = self.translate(p)
if not self.path_module.isdir (translated_path):
return 0
else:
old_dir = os.getcwd()
# temporarily change to that directory, in order
# to see if we have permission to do so.
can = 0
try:
try:
os.chdir (translated_path)
can = 1
self.wd = p
except:
pass
finally:
if can:
os.chdir (old_dir)
return can
def cdup (self):
return self.cwd ('..')
def listdir (self, path, long=0):
p = self.translate (path)
# I think we should glob, but limit it to the current
# directory only.
ld = os.listdir (p)
if not long:
return list_producer (ld, None)
else:
old_dir = os.getcwd()
try:
os.chdir (p)
# if os.stat fails we ignore that file.
result = [_f for _f in map (safe_stat, ld) if _f]
finally:
os.chdir (old_dir)
return list_producer (result, self.longify)
# TODO: implement a cache w/timeout for stat()
def stat (self, path):
p = self.translate (path)
return os.stat (p)
def open (self, path, mode):
p = self.translate (path)
return open (p, mode)
def unlink (self, path):
p = self.translate (path)
return os.unlink (p)
def mkdir (self, path):
p = self.translate (path)
return os.mkdir (p)
def rmdir (self, path):
p = self.translate (path)
return os.rmdir (p)
def rename(self, src, dst):
return os.rename(self.translate(src),self.translate(dst))
# utility methods
def normalize (self, path):
# watch for the ever-sneaky '/+' path element
path = re.sub('/+', '/', path)
p = self.path_module.normpath (path)
# remove 'dangling' cdup's.
if len(p) > 2 and p[:3] == '/..':
p = '/'
return p
def translate (self, path):
# we need to join together three separate
# path components, and do it safely.
# //
# use the operating system's path separator.
path = os.sep.join(path.split('/'))
p = self.normalize (self.path_module.join (self.wd, path))
p = self.normalize (self.path_module.join (self.root, p[1:]))
return p
def longify (self, path_stat_info_tuple):
(path, stat_info) = path_stat_info_tuple
return unix_longify (path, stat_info)
def __repr__ (self):
return '' % (
self.root,
self.wd
)
if os.name == 'posix':
class unix_filesystem (os_filesystem):
pass
class schizophrenic_unix_filesystem (os_filesystem):
PROCESS_UID = os.getuid()
PROCESS_EUID = os.geteuid()
PROCESS_GID = os.getgid()
PROCESS_EGID = os.getegid()
def __init__ (self, root, wd='/', persona=(None, None)):
os_filesystem.__init__ (self, root, wd)
self.persona = persona
def become_persona (self):
if self.persona != (None, None):
uid, gid = self.persona
# the order of these is important!
os.setegid (gid)
os.seteuid (uid)
def become_nobody (self):
if self.persona != (None, None):
os.seteuid (self.PROCESS_UID)
os.setegid (self.PROCESS_GID)
# cwd, cdup, open, listdir
def cwd (self, path):
try:
self.become_persona()
return os_filesystem.cwd (self, path)
finally:
self.become_nobody()
def cdup (self):
try:
self.become_persona()
return os_filesystem.cdup (self)
finally:
self.become_nobody()
def open (self, filename, mode):
try:
self.become_persona()
return os_filesystem.open (self, filename, mode)
finally:
self.become_nobody()
def listdir (self, path, long=0):
try:
self.become_persona()
return os_filesystem.listdir (self, path, long)
finally:
self.become_nobody()
# For the 'real' root, we could obtain a list of drives, and then
# use that. Doesn't win32 provide such a 'real' filesystem?
# [yes, I think something like this "\\.\c\windows"]
class msdos_filesystem (os_filesystem):
def longify (self, path_stat_info_tuple):
(path, stat_info) = path_stat_info_tuple
return msdos_longify (path, stat_info)
# A merged filesystem will let you plug other filesystems together.
# We really need the equivalent of a 'mount' capability - this seems
# to be the most general idea. So you'd use a 'mount' method to place
# another filesystem somewhere in the hierarchy.
# Note: this is most likely how I will handle ~user directories
# with the http server.
class merged_filesystem:
def __init__ (self, *fsys):
pass
# this matches the output of NT's ftp server (when in
# MSDOS mode) exactly.
def msdos_longify (file, stat_info):
if stat.S_ISDIR (stat_info[stat.ST_MODE]):
dir = ''
else:
dir = ' '
date = msdos_date (stat_info[stat.ST_MTIME])
return '%s %s %8d %s' % (
date,
dir,
stat_info[stat.ST_SIZE],
file
)
def msdos_date (t):
try:
info = time.gmtime (t)
except:
info = time.gmtime (0)
# year, month, day, hour, minute, second, ...
hour = info[3]
if hour > 11:
merid = 'PM'
hour -= 12
else:
merid = 'AM'
return '%02d-%02d-%02d %02d:%02d%s' % (
info[1],
info[2],
info[0]%100,
hour,
info[4],
merid
)
months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
mode_table = {
'0':'---',
'1':'--x',
'2':'-w-',
'3':'-wx',
'4':'r--',
'5':'r-x',
'6':'rw-',
'7':'rwx'
}
import time
def unix_longify (file, stat_info):
# for now, only pay attention to the lower bits
mode = ('%o' % stat_info[stat.ST_MODE])[-3:]
mode = ''.join([mode_table[x] for x in mode])
if stat.S_ISDIR (stat_info[stat.ST_MODE]):
dirchar = 'd'
else:
dirchar = '-'
date = ls_date (long(time.time()), stat_info[stat.ST_MTIME])
return '%s%s %3d %-8d %-8d %8d %s %s' % (
dirchar,
mode,
stat_info[stat.ST_NLINK],
stat_info[stat.ST_UID],
stat_info[stat.ST_GID],
stat_info[stat.ST_SIZE],
date,
file
)
# Emulate the unix 'ls' command's date field.
# it has two formats - if the date is more than 180
# days in the past, then it's like this:
# Oct 19 1995
# otherwise, it looks like this:
# Oct 19 17:33
def ls_date (now, t):
try:
info = time.gmtime (t)
except:
info = time.gmtime (0)
# 15,600,000 == 86,400 * 180
if (now - t) > 15600000:
return '%s %2d %d' % (
months[info[1]-1],
info[2],
info[0]
)
else:
return '%s %2d %02d:%02d' % (
months[info[1]-1],
info[2],
info[3],
info[4]
)
# ===========================================================================
# Producers
# ===========================================================================
class list_producer:
def __init__ (self, list, func=None):
self.list = list
self.func = func
# this should do a pushd/popd
def more (self):
if not self.list:
return ''
else:
# do a few at a time
bunch = self.list[:50]
if self.func is not None:
bunch = map (self.func, bunch)
self.list = self.list[50:]
return '\r\n'.join(bunch) + '\r\n'
================================================
FILE: supervisor/medusa/http_date.py
================================================
# -*- Mode: Python -*-
import re
import time
def concat (*args):
return ''.join (args)
def join (seq, field=' '):
return field.join (seq)
def group (s):
return '(' + s + ')'
short_days = ['sun','mon','tue','wed','thu','fri','sat']
long_days = ['sunday','monday','tuesday','wednesday','thursday','friday','saturday']
short_day_reg = group (join (short_days, '|'))
long_day_reg = group (join (long_days, '|'))
daymap = {}
for i in range(7):
daymap[short_days[i]] = i
daymap[long_days[i]] = i
hms_reg = join (3 * [group('[0-9][0-9]')], ':')
months = ['jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec']
monmap = {}
for i in range(12):
monmap[months[i]] = i+1
months_reg = group (join (months, '|'))
# From draft-ietf-http-v11-spec-07.txt/3.3.1
# Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123
# Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036
# Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format
# rfc822 format
rfc822_date = join (
[concat (short_day_reg,','), # day
group('[0-9][0-9]?'), # date
months_reg, # month
group('[0-9]+'), # year
hms_reg, # hour minute second
'gmt'
],
' '
)
rfc822_reg = re.compile (rfc822_date)
def unpack_rfc822(m):
g = m.group
i = int
return (
i(g(4)), # year
monmap[g(3)], # month
i(g(2)), # day
i(g(5)), # hour
i(g(6)), # minute
i(g(7)), # second
0,
0,
0
)
# rfc850 format
rfc850_date = join (
[concat (long_day_reg,','),
join (
[group ('[0-9][0-9]?'),
months_reg,
group ('[0-9]+')
],
'-'
),
hms_reg,
'gmt'
],
' '
)
rfc850_reg = re.compile (rfc850_date)
# they actually unpack the same way
def unpack_rfc850(m):
g = m.group
i = int
return (
i(g(4)), # year
monmap[g(3)], # month
i(g(2)), # day
i(g(5)), # hour
i(g(6)), # minute
i(g(7)), # second
0,
0,
0
)
# parsedate.parsedate - ~700/sec.
# parse_http_date - ~1333/sec.
def build_http_date (when):
return time.strftime ('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(when))
def parse_http_date (d):
d = d.lower()
tz = time.timezone
m = rfc850_reg.match (d)
if m and m.end() == len(d):
retval = int (time.mktime (unpack_rfc850(m)) - tz)
else:
m = rfc822_reg.match (d)
if m and m.end() == len(d):
retval = int (time.mktime (unpack_rfc822(m)) - tz)
else:
return 0
# Thanks to Craig Silverstein for pointing
# out the DST discrepancy
if time.daylight and time.localtime(retval)[-1] == 1: # DST correction
retval += tz - time.altzone
return retval
================================================
FILE: supervisor/medusa/http_server.py
================================================
# -*- Mode: Python -*-
#
# Author: Sam Rushing
# Copyright 1996-2000 by Sam Rushing
# All Rights Reserved.
#
RCS_ID = '$Id: http_server.py,v 1.12 2004/04/21 15:11:44 akuchling Exp $'
# python modules
import re
import socket
import sys
import time
from supervisor.compat import as_bytes
# async modules
import supervisor.medusa.asyncore_25 as asyncore
import supervisor.medusa.asynchat_25 as asynchat
# medusa modules
import supervisor.medusa.http_date as http_date
import supervisor.medusa.producers as producers
import supervisor.medusa.logger as logger
VERSION_STRING = RCS_ID.split()[2]
from supervisor.medusa.counter import counter
try:
from urllib import unquote, splitquery
except ImportError:
from urllib.parse import unquote, splitquery
# ===========================================================================
# Request Object
# ===========================================================================
class http_request:
# default reply code
reply_code = 200
request_counter = counter()
# Whether to automatically use chunked encoding when
#
# HTTP version is 1.1
# Content-Length is not set
# Chunked encoding is not already in effect
#
# If your clients are having trouble, you might want to disable this.
use_chunked = 1
# by default, this request object ignores user data.
collector = None
def __init__ (self, *args):
# unpack information about the request
(self.channel, self.request,
self.command, self.uri, self.version,
self.header) = args
self.outgoing = []
self.reply_headers = {
'Server' : 'Medusa/%s' % VERSION_STRING,
'Date' : http_date.build_http_date (time.time())
}
# New reply header list (to support multiple
# headers with same name)
self.__reply_header_list = []
self.request_number = http_request.request_counter.increment()
self._split_uri = None
self._header_cache = {}
# --------------------------------------------------
# reply header management
# --------------------------------------------------
def __setitem__ (self, key, value):
self.reply_headers[key] = value
def __getitem__ (self, key):
return self.reply_headers[key]
def __contains__(self, key):
return key in self.reply_headers
def has_key (self, key):
return key in self.reply_headers
def build_reply_header (self):
header_items = ['%s: %s' % item for item in self.reply_headers.items()]
result = '\r\n'.join (
[self.response(self.reply_code)] + header_items) + '\r\n\r\n'
return as_bytes(result)
####################################################
# multiple reply header management
####################################################
# These are intended for allowing multiple occurrences
# of the same header.
# Usually you can fold such headers together, separating
# their contents by a comma (e.g. Accept: text/html, text/plain)
# but the big exception is the Set-Cookie header.
# dictionary centric.
#---------------------------------------------------
def add_header(self, name, value):
""" Adds a header to the reply headers """
self.__reply_header_list.append((name, value))
def clear_headers(self):
""" Clears the reply header list """
# Remove things from the old dict as well
self.reply_headers.clear()
self.__reply_header_list[:] = []
def remove_header(self, name, value=None):
""" Removes the specified header.
If a value is provided, the name and
value must match to remove the header.
If the value is None, removes all headers
with that name."""
found_it = 0
# Remove things from the old dict as well
if (name in self.reply_headers and
(value is None or
self.reply_headers[name] == value)):
del self.reply_headers[name]
found_it = 1
removed_headers = []
if not value is None:
if (name, value) in self.__reply_header_list:
removed_headers = [(name, value)]
found_it = 1
else:
for h in self.__reply_header_list:
if h[0] == name:
removed_headers.append(h)
found_it = 1
if not found_it:
if value is None:
search_value = "%s" % name
else:
search_value = "%s: %s" % (name, value)
raise LookupError("Header '%s' not found" % search_value)
for h in removed_headers:
self.__reply_header_list.remove(h)
def get_reply_headers(self):
""" Get the tuple of headers that will be used
for generating reply headers"""
header_tuples = self.__reply_header_list[:]
# The idea here is to insert the headers from
# the old header dict into the new header list,
# UNLESS there's already an entry in the list
# that would have overwritten the dict entry
# if the dict was the only storage...
header_names = [n for n,v in header_tuples]
for n,v in self.reply_headers.items():
if n not in header_names:
header_tuples.append((n,v))
header_names.append(n)
# Ok, that should do it. Now, if there were any
# headers in the dict that weren't in the list,
# they should have been copied in. If the name
# was already in the list, we didn't copy it,
# because the value from the dict has been
# 'overwritten' by the one in the list.
return header_tuples
def get_reply_header_text(self):
""" Gets the reply header (including status and
additional crlf)"""
header_tuples = self.get_reply_headers()
headers = [self.response(self.reply_code)]
headers += ["%s: %s" % h for h in header_tuples]
return '\r\n'.join(headers) + '\r\n\r\n'
#---------------------------------------------------
# This is the end of the new reply header
# management section.
####################################################
# --------------------------------------------------
# split a uri
# --------------------------------------------------
# ;?#
path_regex = re.compile (
# path params query fragment
r'([^;?#]*)(;[^?#]*)?(\?[^#]*)?(#.*)?'
)
def split_uri (self):
if self._split_uri is None:
m = self.path_regex.match (self.uri)
if m.end() != len(self.uri):
raise ValueError("Broken URI")
else:
self._split_uri = m.groups()
return self._split_uri
def get_header_with_regex (self, head_reg, group):
for line in self.header:
m = head_reg.match (line)
if m.end() == len(line):
return m.group (group)
return ''
def get_header (self, header):
header = header.lower()
hc = self._header_cache
if header not in hc:
h = header + ': '
hl = len(h)
for line in self.header:
if line[:hl].lower() == h:
r = line[hl:]
hc[header] = r
return r
hc[header] = None
return None
else:
return hc[header]
# --------------------------------------------------
# user data
# --------------------------------------------------
def collect_incoming_data (self, data):
if self.collector:
self.collector.collect_incoming_data (data)
else:
self.log_info(
'Dropping %d bytes of incoming request data' % len(data),
'warning'
)
def found_terminator (self):
if self.collector:
self.collector.found_terminator()
else:
self.log_info (
'Unexpected end-of-record for incoming request',
'warning'
)
def push (self, thing):
# Sometimes, text gets pushed by XMLRPC logic for later
# processing.
if isinstance(thing, str):
thing = as_bytes(thing)
if isinstance(thing, bytes):
thing = producers.simple_producer(thing, buffer_size=len(thing))
self.outgoing.append(thing)
def response (self, code=200):
message = self.responses[code]
self.reply_code = code
return 'HTTP/%s %d %s' % (self.version, code, message)
def error (self, code):
self.reply_code = code
message = self.responses[code]
s = self.DEFAULT_ERROR_MESSAGE % {
'code': code,
'message': message,
}
s = as_bytes(s)
self['Content-Length'] = len(s)
self['Content-Type'] = 'text/html'
# make an error reply
self.push(s)
self.done()
# can also be used for empty replies
reply_now = error
def done (self):
"""finalize this transaction - send output to the http channel"""
# ----------------------------------------
# persistent connection management
# ----------------------------------------
# --- BUCKLE UP! ----
connection = get_header(CONNECTION, self.header).lower()
close_it = 0
wrap_in_chunking = 0
if self.version == '1.0':
if connection == 'keep-alive':
if 'Content-Length' not in self:
close_it = 1
else:
self['Connection'] = 'Keep-Alive'
else:
close_it = 1
elif self.version == '1.1':
if connection == 'close':
close_it = 1
elif 'Content-Length' not in self:
if 'Transfer-Encoding' in self:
if not self['Transfer-Encoding'] == 'chunked':
close_it = 1
elif self.use_chunked:
self['Transfer-Encoding'] = 'chunked'
wrap_in_chunking = 1
else:
close_it = 1
elif self.version is None:
# Although we don't *really* support http/0.9 (because we'd have to
# use \r\n as a terminator, and it would just yuck up a lot of stuff)
# it's very common for developers to not want to type a version number
# when using telnet to debug a server.
close_it = 1
outgoing_header = producers.simple_producer(self.get_reply_header_text())
if close_it:
self['Connection'] = 'close'
if wrap_in_chunking:
outgoing_producer = producers.chunked_producer (
producers.composite_producer (self.outgoing)
)
# prepend the header
outgoing_producer = producers.composite_producer(
[outgoing_header, outgoing_producer]
)
else:
# prepend the header
self.outgoing.insert(0, outgoing_header)
outgoing_producer = producers.composite_producer (self.outgoing)
# apply a few final transformations to the output
self.channel.push_with_producer (
# globbing gives us large packets
producers.globbing_producer (
# hooking lets us log the number of bytes sent
producers.hooked_producer (
outgoing_producer,
self.log
)
)
)
self.channel.current_request = None
if close_it:
self.channel.close_when_done()
def log_date_string (self, when):
gmt = time.gmtime(when)
if time.daylight and gmt[8]:
tz = time.altzone
else:
tz = time.timezone
if tz > 0:
neg = 1
else:
neg = 0
tz = -tz
h, rem = divmod (tz, 3600)
m, rem = divmod (rem, 60)
if neg:
offset = '-%02d%02d' % (h, m)
else:
offset = '+%02d%02d' % (h, m)
return time.strftime ( '%d/%b/%Y:%H:%M:%S ', gmt) + offset
def log (self, bytes):
self.channel.server.logger.log (
self.channel.addr[0],
'%d - - [%s] "%s" %d %d\n' % (
self.channel.addr[1],
self.log_date_string (time.time()),
self.request,
self.reply_code,
bytes
)
)
responses = {
100: "Continue",
101: "Switching Protocols",
200: "OK",
201: "Created",
202: "Accepted",
203: "Non-Authoritative Information",
204: "No Content",
205: "Reset Content",
206: "Partial Content",
300: "Multiple Choices",
301: "Moved Permanently",
302: "Moved Temporarily",
303: "See Other",
304: "Not Modified",
305: "Use Proxy",
400: "Bad Request",
401: "Unauthorized",
402: "Payment Required",
403: "Forbidden",
404: "Not Found",
405: "Method Not Allowed",
406: "Not Acceptable",
407: "Proxy Authentication Required",
408: "Request Time-out",
409: "Conflict",
410: "Gone",
411: "Length Required",
412: "Precondition Failed",
413: "Request Entity Too Large",
414: "Request-URI Too Large",
415: "Unsupported Media Type",
500: "Internal Server Error",
501: "Not Implemented",
502: "Bad Gateway",
503: "Service Unavailable",
504: "Gateway Time-out",
505: "HTTP Version not supported"
}
# Default error message
DEFAULT_ERROR_MESSAGE = '\r\n'.join(
('',
'Error response',
'',
'',
'
Error response
',
'
Error code %(code)d.',
'
Message: %(message)s.',
'',
''
)
)
def log_info(self, msg, level):
pass
# ===========================================================================
# HTTP Channel Object
# ===========================================================================
class http_channel (asynchat.async_chat):
# use a larger default output buffer
ac_out_buffer_size = 1<<16
current_request = None
channel_counter = counter()
def __init__ (self, server, conn, addr):
self.channel_number = http_channel.channel_counter.increment()
self.request_counter = counter()
asynchat.async_chat.__init__ (self, conn)
self.server = server
self.addr = addr
self.set_terminator (b'\r\n\r\n')
self.in_buffer = b''
self.creation_time = int (time.time())
self.last_used = self.creation_time
self.check_maintenance()
def __repr__ (self):
ar = asynchat.async_chat.__repr__(self)[1:-1]
return '<%s channel#: %s requests:%s>' % (
ar,
self.channel_number,
self.request_counter
)
# Channel Counter, Maintenance Interval...
maintenance_interval = 500
def check_maintenance (self):
if not self.channel_number % self.maintenance_interval:
self.maintenance()
def maintenance (self):
self.kill_zombies()
# 30-minute zombie timeout. status_handler also knows how to kill zombies.
zombie_timeout = 30 * 60
def kill_zombies (self):
now = int (time.time())
for channel in list(asyncore.socket_map.values()):
if channel.__class__ == self.__class__:
if (now - channel.last_used) > channel.zombie_timeout:
channel.close()
# --------------------------------------------------
# send/recv overrides, good place for instrumentation.
# --------------------------------------------------
# this information needs to get into the request object,
# so that it may log correctly.
def send (self, data):
result = asynchat.async_chat.send (self, data)
self.server.bytes_out.increment (len(data))
self.last_used = int (time.time())
return result
def recv (self, buffer_size):
try:
result = asynchat.async_chat.recv (self, buffer_size)
self.server.bytes_in.increment (len(result))
self.last_used = int (time.time())
return result
except MemoryError:
# --- Save a Trip to Your Service Provider ---
# It's possible for a process to eat up all the memory of
# the machine, and put it in an extremely wedged state,
# where medusa keeps running and can't be shut down. This
# is where MemoryError tends to get thrown, though of
# course it could get thrown elsewhere.
sys.exit ("Out of Memory!")
def handle_error (self):
t, v = sys.exc_info()[:2]
if t is SystemExit:
raise t(v)
else:
asynchat.async_chat.handle_error (self)
def log (self, *args):
pass
# --------------------------------------------------
# async_chat methods
# --------------------------------------------------
def collect_incoming_data (self, data):
if self.current_request:
# we are receiving data (probably POST data) for a request
self.current_request.collect_incoming_data (data)
else:
# we are receiving header (request) data
self.in_buffer = self.in_buffer + data
def found_terminator (self):
if self.current_request:
self.current_request.found_terminator()
else:
header = self.in_buffer
self.in_buffer = b''
lines = header.split(b'\r\n')
# --------------------------------------------------
# crack the request header
# --------------------------------------------------
while lines and not lines[0]:
# as per the suggestion of http-1.1 section 4.1, (and
# Eric Parker ), ignore a leading
# blank lines (buggy browsers tack it onto the end of
# POST requests)
lines = lines[1:]
if not lines:
self.close_when_done()
return
request = lines[0]
command, uri, version = crack_request (request)
header = join_headers (lines[1:])
# unquote path if necessary (thanks to Skip Montanaro for pointing
# out that we must unquote in piecemeal fashion).
rpath, rquery = splitquery(uri)
if '%' in rpath:
if rquery:
uri = unquote (rpath) + '?' + rquery
else:
uri = unquote (rpath)
r = http_request (self, request, command, uri, version, header)
self.request_counter.increment()
self.server.total_requests.increment()
if command is None:
self.log_info ('Bad HTTP request: %s' % repr(request), 'error')
r.error (400)
return
# --------------------------------------------------
# handler selection and dispatch
# --------------------------------------------------
for h in self.server.handlers:
if h.match (r):
try:
self.current_request = r
# This isn't used anywhere.
# r.handler = h # CYCLE
h.handle_request (r)
except:
self.server.exceptions.increment()
(file, fun, line), t, v, tbinfo = asyncore.compact_traceback()
self.log_info(
'Server Error: %s, %s: file: %s line: %s' % (t,v,file,line),
'error')
try:
r.error (500)
except:
pass
return
# no handlers, so complain
r.error (404)
def writable_for_proxy (self):
# this version of writable supports the idea of a 'stalled' producer
# [i.e., it's not ready to produce any output yet] This is needed by
# the proxy, which will be waiting for the magic combination of
# 1) hostname resolved
# 2) connection made
# 3) data available.
if self.ac_out_buffer:
return 1
elif len(self.producer_fifo):
p = self.producer_fifo.first()
if hasattr (p, 'stalled'):
return not p.stalled()
else:
return 1
# ===========================================================================
# HTTP Server Object
# ===========================================================================
class http_server (asyncore.dispatcher):
SERVER_IDENT = 'HTTP Server (V%s)' % VERSION_STRING
channel_class = http_channel
def __init__ (self, ip, port, resolver=None, logger_object=None):
self.ip = ip
self.port = port
asyncore.dispatcher.__init__ (self)
self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
self.handlers = []
if not logger_object:
logger_object = logger.file_logger (sys.stdout)
self.set_reuse_addr()
self.bind ((ip, port))
# lower this to 5 if your OS complains
self.listen (1024)
host, port = self.socket.getsockname()
if not ip:
self.log_info('Computing default hostname', 'warning')
ip = socket.gethostbyname (socket.gethostname())
try:
self.server_name = socket.gethostbyaddr (ip)[0]
except socket.error:
self.log_info('Cannot do reverse lookup', 'warning')
self.server_name = ip # use the IP address as the "hostname"
self.server_port = port
self.total_clients = counter()
self.total_requests = counter()
self.exceptions = counter()
self.bytes_out = counter()
self.bytes_in = counter()
if not logger_object:
logger_object = logger.file_logger (sys.stdout)
if resolver:
self.logger = logger.resolving_logger (resolver, logger_object)
else:
self.logger = logger.unresolving_logger (logger_object)
self.log_info (
'Medusa (V%s) started at %s'
'\n\tHostname: %s'
'\n\tPort:%d'
'\n' % (
VERSION_STRING,
time.ctime(time.time()),
self.server_name,
port,
)
)
def writable (self):
return 0
def handle_read (self):
pass
def readable (self):
return self.accepting
def handle_connect (self):
pass
def handle_accept (self):
self.total_clients.increment()
try:
conn, addr = self.accept()
except socket.error:
# linux: on rare occasions we get a bogus socket back from
# accept. socketmodule.c:makesockaddr complains that the
# address family is unknown. We don't want the whole server
# to shut down because of this.
self.log_info ('warning: server accept() threw an exception', 'warning')
return
except TypeError:
# unpack non-sequence. this can happen when a read event
# fires on a listening socket, but when we call accept()
# we get EWOULDBLOCK, so dispatcher.accept() returns None.
# Seen on FreeBSD3.
self.log_info ('warning: server accept() threw EWOULDBLOCK', 'warning')
return
self.channel_class (self, conn, addr)
def install_handler (self, handler, back=0):
if back:
self.handlers.append (handler)
else:
self.handlers.insert (0, handler)
def remove_handler (self, handler):
self.handlers.remove (handler)
def status (self):
from supervisor.medusa.util import english_bytes
def nice_bytes (n):
return ''.join(english_bytes (n))
handler_stats = [_f for _f in map (maybe_status, self.handlers) if _f]
if self.total_clients:
ratio = self.total_requests.as_long() / float(self.total_clients.as_long())
else:
ratio = 0.0
return producers.composite_producer (
[producers.lines_producer (
['
', encoding='latin1'))
def test_shortrepr2(self):
from supervisor.templating import parse_xmlstring
from supervisor.compat import as_bytes
root = parse_xmlstring(_COMPLEX_XHTML)
r = root.shortrepr()
self.assertEqual(r,
as_bytes('\n'
' \n'
' \n'
' [...]\n\n'
' \n'
' [...]\n'
'', encoding='latin1'))
def test_diffmeld1(self):
from supervisor.templating import parse_xmlstring
from supervisor.templating import _MELD_ID
root = parse_xmlstring(_COMPLEX_XHTML)
clone = root.clone()
div = self._makeOne('div', {_MELD_ID:'newdiv'})
clone.append(div)
tr = clone.findmeld('tr')
tr.deparent()
title = clone.findmeld('title')
title.deparent()
clone.append(title)
# unreduced
diff = root.diffmeld(clone)
changes = diff['unreduced']
addedtags = [ x.attrib[_MELD_ID] for x in changes['added'] ]
removedtags = [x.attrib[_MELD_ID] for x in changes['removed'] ]
movedtags = [ x.attrib[_MELD_ID] for x in changes['moved'] ]
addedtags.sort()
removedtags.sort()
movedtags.sort()
self.assertEqual(addedtags,['newdiv'])
self.assertEqual(removedtags,['td1', 'td2', 'tr'])
self.assertEqual(movedtags, ['title'])
# reduced
changes = diff['reduced']
addedtags = [ x.attrib[_MELD_ID] for x in changes['added'] ]
removedtags = [x.attrib[_MELD_ID] for x in changes['removed'] ]
movedtags = [ x.attrib[_MELD_ID] for x in changes['moved'] ]
addedtags.sort()
removedtags.sort()
movedtags.sort()
self.assertEqual(addedtags,['newdiv'])
self.assertEqual(removedtags,['tr'])
self.assertEqual(movedtags, ['title'])
def test_diffmeld2(self):
source = """
"""
target = """
"""
from supervisor.templating import parse_htmlstring
source_root = parse_htmlstring(source)
target_root = parse_htmlstring(target)
changes = source_root.diffmeld(target_root)
# unreduced
actual = [x.meldid() for x in changes['unreduced']['moved']]
expected = ['b']
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['unreduced']['added']]
expected = []
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['unreduced']['removed']]
expected = []
self.assertEqual(expected, actual)
# reduced
actual = [x.meldid() for x in changes['reduced']['moved']]
expected = ['b']
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['reduced']['added']]
expected = []
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['reduced']['removed']]
expected = []
self.assertEqual(expected, actual)
def test_diffmeld3(self):
source = """
"""
target = """
"""
from supervisor.templating import parse_htmlstring
source_root = parse_htmlstring(source)
target_root = parse_htmlstring(target)
changes = source_root.diffmeld(target_root)
# unreduced
actual = [x.meldid() for x in changes['unreduced']['moved']]
expected = ['b', 'c']
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['unreduced']['added']]
expected = ['d', 'e']
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['unreduced']['removed']]
expected = ['z', 'y']
self.assertEqual(expected, actual)
# reduced
actual = [x.meldid() for x in changes['reduced']['moved']]
expected = ['b']
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['reduced']['added']]
expected = ['d']
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['reduced']['removed']]
expected = ['z']
self.assertEqual(expected, actual)
def test_diffmeld4(self):
source = """
"""
target = """
"""
from supervisor.templating import parse_htmlstring
source_root = parse_htmlstring(source)
target_root = parse_htmlstring(target)
changes = source_root.diffmeld(target_root)
# unreduced
actual = [x.meldid() for x in changes['unreduced']['moved']]
expected = ['a', 'b']
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['unreduced']['added']]
expected = ['m', 'n']
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['unreduced']['removed']]
expected = ['c', 'd', 'z', 'y']
self.assertEqual(expected, actual)
# reduced
actual = [x.meldid() for x in changes['reduced']['moved']]
expected = ['a']
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['reduced']['added']]
expected = ['m']
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['reduced']['removed']]
expected = ['c', 'z']
self.assertEqual(expected, actual)
def test_diffmeld5(self):
source = """
"""
target = """
"""
from supervisor.templating import parse_htmlstring
source_root = parse_htmlstring(source)
target_root = parse_htmlstring(target)
changes = source_root.diffmeld(target_root)
# unreduced
actual = [x.meldid() for x in changes['unreduced']['moved']]
expected = ['a', 'b', 'c', 'd']
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['unreduced']['added']]
expected = []
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['unreduced']['removed']]
expected = []
self.assertEqual(expected, actual)
# reduced
actual = [x.meldid() for x in changes['reduced']['moved']]
expected = ['a', 'c']
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['reduced']['added']]
expected = []
self.assertEqual(expected, actual)
actual = [x.meldid() for x in changes['reduced']['removed']]
expected = []
self.assertEqual(expected, actual)
class ParserTests(unittest.TestCase):
def _parse(self, *args):
from supervisor.templating import parse_xmlstring
root = parse_xmlstring(*args)
return root
def _parse_html(self, *args):
from supervisor.templating import parse_htmlstring
root = parse_htmlstring(*args)
return root
def test_parse_simple_xml(self):
from supervisor.templating import _MELD_ID
root = self._parse(_SIMPLE_XML)
self.assertEqual(root.tag, 'root')
self.assertEqual(root.parent, None)
l1st = root[0]
self.assertEqual(l1st.tag, 'list')
self.assertEqual(l1st.parent, root)
self.assertEqual(l1st.attrib[_MELD_ID], 'list')
item = l1st[0]
self.assertEqual(item.tag, 'item')
self.assertEqual(item.parent, l1st)
self.assertEqual(item.attrib[_MELD_ID], 'item')
name = item[0]
description = item[1]
self.assertEqual(name.tag, 'name')
self.assertEqual(name.parent, item)
self.assertEqual(name.attrib[_MELD_ID], 'name')
self.assertEqual(description.tag, 'description')
self.assertEqual(description.parent, item)
self.assertEqual(description.attrib[_MELD_ID], 'description')
def test_parse_simple_xhtml(self):
xhtml_ns = '{http://www.w3.org/1999/xhtml}%s'
from supervisor.templating import _MELD_ID
root = self._parse(_SIMPLE_XHTML)
self.assertEqual(root.tag, xhtml_ns % 'html')
self.assertEqual(root.attrib, {})
self.assertEqual(root.parent, None)
body = root[0]
self.assertEqual(body.tag, xhtml_ns % 'body')
self.assertEqual(body.attrib[_MELD_ID], 'body')
self.assertEqual(body.parent, root)
def test_parse_complex_xhtml(self):
xhtml_ns = '{http://www.w3.org/1999/xhtml}%s'
from supervisor.templating import _MELD_ID
root = self._parse(_COMPLEX_XHTML)
self.assertEqual(root.tag, xhtml_ns % 'html')
self.assertEqual(root.attrib, {})
self.assertEqual(root.parent, None)
head = root[0]
self.assertEqual(head.tag, xhtml_ns % 'head')
self.assertEqual(head.attrib, {})
self.assertEqual(head.parent, root)
meta = head[0]
self.assertEqual(meta.tag, xhtml_ns % 'meta')
self.assertEqual(meta.attrib['content'],
'text/html; charset=ISO-8859-1')
self.assertEqual(meta.parent, head)
title = head[1]
self.assertEqual(title.tag, xhtml_ns % 'title')
self.assertEqual(title.attrib[_MELD_ID], 'title')
self.assertEqual(title.parent, head)
body = root[2]
self.assertEqual(body.tag, xhtml_ns % 'body')
self.assertEqual(body.attrib, {})
self.assertEqual(body.parent, root)
div1 = body[0]
self.assertEqual(div1.tag, xhtml_ns % 'div')
self.assertEqual(div1.attrib, {'{http://foo/bar}baz': 'slab'})
self.assertEqual(div1.parent, body)
div2 = body[1]
self.assertEqual(div2.tag, xhtml_ns % 'div')
self.assertEqual(div2.attrib[_MELD_ID], 'content_well')
self.assertEqual(div2.parent, body)
form = div2[0]
self.assertEqual(form.tag, xhtml_ns % 'form')
self.assertEqual(form.attrib[_MELD_ID], 'form1')
self.assertEqual(form.attrib['action'], '.')
self.assertEqual(form.attrib['method'], 'POST')
self.assertEqual(form.parent, div2)
img = form[0]
self.assertEqual(img.tag, xhtml_ns % 'img')
self.assertEqual(img.parent, form)
table = form[1]
self.assertEqual(table.tag, xhtml_ns % 'table')
self.assertEqual(table.attrib[_MELD_ID], 'table1')
self.assertEqual(table.attrib['border'], '0')
self.assertEqual(table.parent, form)
tbody = table[0]
self.assertEqual(tbody.tag, xhtml_ns % 'tbody')
self.assertEqual(tbody.attrib[_MELD_ID], 'tbody')
self.assertEqual(tbody.parent, table)
tr = tbody[0]
self.assertEqual(tr.tag, xhtml_ns % 'tr')
self.assertEqual(tr.attrib[_MELD_ID], 'tr')
self.assertEqual(tr.attrib['class'], 'foo')
self.assertEqual(tr.parent, tbody)
td1 = tr[0]
self.assertEqual(td1.tag, xhtml_ns % 'td')
self.assertEqual(td1.attrib[_MELD_ID], 'td1')
self.assertEqual(td1.parent, tr)
td2 = tr[1]
self.assertEqual(td2.tag, xhtml_ns % 'td')
self.assertEqual(td2.attrib[_MELD_ID], 'td2')
self.assertEqual(td2.parent, tr)
def test_nvu_html(self):
from supervisor.templating import _MELD_ID
from supervisor.templating import Comment
root = self._parse_html(_NVU_HTML)
self.assertEqual(root.tag, 'html')
self.assertEqual(root.attrib, {})
self.assertEqual(root.parent, None)
head = root[0]
self.assertEqual(head.tag, 'head')
self.assertEqual(head.attrib, {})
self.assertEqual(head.parent, root)
meta = head[0]
self.assertEqual(meta.tag, 'meta')
self.assertEqual(meta.attrib['content'],
'text/html; charset=ISO-8859-1')
title = head[1]
self.assertEqual(title.tag, 'title')
self.assertEqual(title.attrib[_MELD_ID], 'title')
self.assertEqual(title.parent, head)
body = root[1]
self.assertEqual(body.tag, 'body')
self.assertEqual(body.attrib, {})
self.assertEqual(body.parent, root)
comment = body[0]
self.assertEqual(comment.tag, Comment)
table = body[3]
self.assertEqual(table.tag, 'table')
self.assertEqual(table.attrib, {'style':
'text-align: left; width: 100px;',
'border':'1',
'cellpadding':'2',
'cellspacing':'2'})
self.assertEqual(table.parent, body)
href = body[5]
self.assertEqual(href.tag, 'a')
img = body[8]
self.assertEqual(img.tag, 'img')
def test_dupe_meldids_fails_parse_xml(self):
meld_ns = "https://github.com/Supervisor/supervisor"
repeated = (''
'' % meld_ns)
self.assertRaises(ValueError, self._parse, repeated)
def test_dupe_meldids_fails_parse_html(self):
meld_ns = "https://github.com/Supervisor/supervisor"
repeated = (''
'' % meld_ns)
self.assertRaises(ValueError, self._parse_html, repeated)
class UtilTests(unittest.TestCase):
def test_insert_xhtml_doctype(self):
from supervisor.templating import insert_doctype
orig = ''
actual = insert_doctype(orig)
expected = ''
self.assertEqual(actual, expected)
def test_insert_doctype_after_xmldecl(self):
from supervisor.templating import insert_doctype
orig = ''
actual = insert_doctype(orig)
expected = ''
self.assertEqual(actual, expected)
def test_insert_meld_ns_decl(self):
from supervisor.templating import insert_meld_ns_decl
orig = ''
actual = insert_meld_ns_decl(orig)
expected = ''
self.assertEqual(actual, expected)
def test_prefeed_preserves_existing_meld_ns(self):
from supervisor.templating import prefeed
orig = ''
actual = prefeed(orig)
expected = ''
self.assertEqual(actual, expected)
def test_prefeed_preserves_existing_doctype(self):
from supervisor.templating import prefeed
orig = ''
actual = prefeed(orig)
self.assertEqual(actual, orig)
class WriterTests(unittest.TestCase):
def _parse(self, xml):
from supervisor.templating import parse_xmlstring
root = parse_xmlstring(xml)
return root
def _parse_html(self, xml):
from supervisor.templating import parse_htmlstring
root = parse_htmlstring(xml)
return root
def _write(self, fn, **kw):
try:
from io import BytesIO
except: # python 2.5
from StringIO import StringIO as BytesIO
out = BytesIO()
fn(out, **kw)
out.seek(0)
actual = out.read()
return actual
def _write_xml(self, node, **kw):
return self._write(node.write_xml, **kw)
def _write_html(self, node, **kw):
return self._write(node.write_html, **kw)
def _write_xhtml(self, node, **kw):
return self._write(node.write_xhtml, **kw)
def assertNormalizedXMLEqual(self, a, b):
from supervisor.compat import as_string
a = normalize_xml(as_string(a, encoding='latin1'))
b = normalize_xml(as_string(b, encoding='latin1'))
self.assertEqual(a, b)
def assertNormalizedHTMLEqual(self, a, b):
from supervisor.compat import as_string
a = normalize_xml(as_string(a, encoding='latin1'))
b = normalize_xml(as_string(b, encoding='latin1'))
self.assertEqual(a, b)
def test_write_simple_xml(self):
root = self._parse(_SIMPLE_XML)
actual = self._write_xml(root)
expected = """NameDescription"""
self.assertNormalizedXMLEqual(actual, expected)
for el, data in root.findmeld('item').repeat(((1,2),)):
el.findmeld('name').text = str(data[0])
el.findmeld('description').text = str(data[1])
actual = self._write_xml(root)
expected = """12"""
self.assertNormalizedXMLEqual(actual, expected)
def test_write_simple_xhtml(self):
root = self._parse(_SIMPLE_XHTML)
actual = self._write_xhtml(root)
expected = """Hello!"""
self.assertNormalizedXMLEqual(actual, expected)
def test_write_simple_xhtml_as_html(self):
root = self._parse(_SIMPLE_XHTML)
actual = self._write_html(root)
expected = """
Hello!
"""
self.assertNormalizedHTMLEqual(actual, expected)
def test_write_complex_xhtml_as_html(self):
root = self._parse(_COMPLEX_XHTML)
actual = self._write_html(root)
expected = """
This will be escaped in html output: &
"""
self.assertNormalizedHTMLEqual(actual, expected)
def test_write_complex_xhtml_as_xhtml(self):
# I'm not entirely sure if the cdata "script" quoting in this
# test is entirely correct for XHTML. Ryan Tomayko suggests
# that escaped entities are handled properly in script tags by
# XML-aware browsers at
# http://sourceforge.net/mailarchive/message.php?msg_id=10835582
# but I haven't tested it at all. ZPT does not seem to do
# this; it outputs unescaped data.
root = self._parse(_COMPLEX_XHTML)
actual = self._write_xhtml(root)
expected = """
This will be escaped in html output: &
"""
self.assertNormalizedXMLEqual(actual, expected)
def test_write_emptytags_html(self):
from supervisor.compat import as_string
root = self._parse(_EMPTYTAGS_HTML)
actual = self._write_html(root)
expected = """