Full Code of Supervisor/superlance for AI

main ebde11bd8b57 cached
42 files
157.1 KB
39.7k tokens
183 symbols
1 requests
Download .txt
Repository: Supervisor/superlance
Branch: main
Commit: ebde11bd8b57
Files: 42
Total size: 157.1 KB

Directory structure:
gitextract_7woj_tmr/

├── .github/
│   └── workflows/
│       └── main.yml
├── .gitignore
├── .readthedocs.yaml
├── CHANGES.rst
├── COPYRIGHT.txt
├── LICENSE.txt
├── MANIFEST.in
├── README.rst
├── docs/
│   ├── Makefile
│   ├── conf.py
│   ├── crashmail.rst
│   ├── crashmailbatch.rst
│   ├── crashsms.rst
│   ├── development.rst
│   ├── fatalmailbatch.rst
│   ├── httpok.rst
│   ├── index.rst
│   └── memmon.rst
├── setup.cfg
├── setup.py
├── superlance/
│   ├── __init__.py
│   ├── compat.py
│   ├── crashmail.py
│   ├── crashmailbatch.py
│   ├── crashsms.py
│   ├── fatalmailbatch.py
│   ├── httpok.py
│   ├── memmon.py
│   ├── process_state_email_monitor.py
│   ├── process_state_monitor.py
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── dummy.py
│   │   ├── test_crashmail.py
│   │   ├── test_crashmailbatch.py
│   │   ├── test_crashsms.py
│   │   ├── test_fatalmailbatch.py
│   │   ├── test_httpok.py
│   │   ├── test_memmon.py
│   │   ├── test_process_state_email_monitor.py
│   │   └── test_process_state_monitor.py
│   └── timeoutconn.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]

    steps:
    - uses: actions/checkout@v4

    - name: Install dependencies
      run: $PIP install virtualenv tox

    - name: Run the unit tests
      run: TOXENV=${{ matrix.toxenv }} 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

  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

  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

  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

  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
*.pyc
*.pyo
*.swp
.DS_Store
.coverage
.eggs/
.tox/
build/
docs/_build/
dist/
env*/
htmlcov/
tmp/
coverage.xml
nosetests.xml



================================================
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
================================================
2.0.1.dev0 (Next Release)
-------------------------

2.0.0 (2021-12-26)
------------------

- Support for Python 2.6 has been dropped.  On Python 2, Superlance
  now requires Python 2.7.

- Support for Python 3.2 and 3.3 has been dropped.  On Python 3, Superlance
  now requires Python 3.4 or later.

- Fixed a bug introduced in 0.10 where if the timeout value is shorter
  than the time to wait between retries, the httpok check never executed.
  Issue #110.

- Fixed a bug where ``crashmailbatch`` and ``fatalmatchbatch`` did not set
  the intended default subject.  Patch by Joe Portela.

- Added a new ``--tls`` option to ``crashmailbatch``, ``fatalmailbath``, and
  ``crashsms`` to use Transport Layer Security (TLS).  Patch by Zhe Li.

1.0.0 (2016-10-02)
------------------

- Support for Python 2.5 has been dropped.  On Python 2, Superlance
  now requires Python 2.6 or later.

- Support for Python 3 has been added.  On Python 3, Superlance
  requires Python 3.2 or later.

- Fixed parsing of ``-n`` and ``--name`` options in ``httpok``.  Patch
  by DenisBY.

0.14 (2016-09-24)
-----------------

- Fixed docs build.

0.13 (2016-09-05)
-----------------

- ``httpok`` now allows multiple expected status codes to be specified.  Patch
  by valmiRe.

- ``httpok`` now has a ``--name`` option like ``memmon``.

- All commands now return exit status 0 from ``--help``.

0.12 (2016-09-03)
-----------------

- Fixed ``crashmail`` parsing of ``--optionalheader``.  Patch by Matt Dziuban.

0.11 (2014-08-15)
-----------------

- Added support for ``memmon`` to check against cumulative RSS of a process
  and all its child processes.  Patch by Lukas Graf.

- Fixed a bug introduced in 0.9 where the ``-u`` and ``-n`` options in
  ``memmon`` were parsed incorrectly.  Patch by Harald Friessnegger.

0.10 (2014-07-08)
-----------------

- Honor timeout in httok checks even on trying the connection.
  Without it, processes that take make than 60 seconds to accept connections
  and http_ok with TICK_60 events cause a permanent restart of the process.

- ``httpok`` now sends a ``User-Agent`` header of ``httpok``.

- Removed ``setuptools`` from the ``requires`` list in ``setup.py`` because
  it caused installation issues on some systems.

0.9 (2013-09-18)
----------------

- Added license.

- Fixed bug in cmd line option validator for ProcessStateEmailMonitor
  Bug report by Val Jordan

- Added ``-u`` option to memmon the only send an email in case the restarted
  process' uptime (in seconds) is below this limit.  This is useful to only
  get notified if a processes gets restarted too frequently.
  Patch by Harald Friessnegger.

0.8 (2013-05-26)
----------------

- Superlance will now refuse to install on an unsupported version of Python.

- Allow SMTP credentials to be supplied to ProcessStateEmailMonitor
  Patch by Steven Davidson.

- Added ``-n`` option to memmon that adds this name to the email
  subject to identify which memmon process restarted a process.
  Useful in case you run multiple supervisors that control
  different processes with the same name.
  Patch by Harald Friessnegger.

- ProcessStateEmailMonitor now adds Date and Message-ID headers to emails.
  Patch by Andrei Vereha.

0.7 (2012-08-22)
----------------

- The ``crashmailbatch --toEmail`` option now accepts a comma-separated
  list of email addresses.

0.6 (2011-08-27)
----------------

- Separated unit tests into their own files

- Created ``fatalmailbatch`` plugin

- Created ``crashmailbatch`` plugin

- Sphinxified documentation.

- Fixed ``test_suite`` to use the correct module name in setup.py.

- Fixed the tests for ``memmon`` to import the correct module.

- Applied patch from Sam Bartlett: processes which are not autostarted
  have pid "0".  This was crashing ``memmon``.

- Add ``smtpHost`` command line flag to ``mailbatch`` processors.

- Added ``crashsms`` from Juan Batiz-Benet

- Converted ``crashmailbatch`` and friends from camel case to pythonic style

- Fixed a bug where ``httpok`` would crash with the ``-b`` (in-body)
  option.  Patch by Joaquin Cuenca Abela.

- Fixed a bug where ``httpok`` would not handle a URL with a query string
  correctly.  Patch by Joaquin Cuenca Abela.

- Fixed a bug where ``httpok`` would not handle process names with a
  group ("group:process") properly.  Patch by Joaquin Cuenca Abela.


0.5 (2009-05-24)
----------------

- Added the ``memmon`` plugin, originally bundled with supervisor and
  now moved to superlance.


0.4 (2009-02-11)
----------------

- Added ``eager`` and ``not-eager`` options to the ``httpok`` plugin.

  If ``not-eager`` is set, and no process being monitored is in the
  ``RUNNING`` state, skip the URL check / mail message.


0.3 (2008-12-10)
----------------

- Added ``gcore`` and ``coredir`` options to the ``httpok`` plugin.


0.2 (2008-11-21)
----------------

- Added the ``crashmail`` plugin.


0.1 (2008-09-18)
----------------

- Initial release


================================================
FILE: COPYRIGHT.txt
================================================
Superlance is Copyright (c) 2008-2013 Agendaless Consulting and Contributors.
(http://www.agendaless.com), All Rights Reserved

  This software is subject to the provisions of the license at
  http://www.repoze.org/LICENSE.txt . A copy of this 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.


================================================
FILE: LICENSE.txt
================================================
Superlance 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.


================================================
FILE: MANIFEST.in
================================================
include CHANGES.rst
include COPYRIGHT.txt
include LICENSE.txt
include README.rst
include docs/Makefile
recursive-include docs *.py *.rst
recursive-exclude docs/_build *



================================================
FILE: README.rst
================================================
superlance README
=================

Superlance is a package of plugin utilities for monitoring and controlling
processes that run under `supervisor <http://supervisord.org>`_.

Please see ``docs/index.rst`` for complete documentation.


================================================
FILE: docs/Makefile
================================================
# Makefile for Sphinx documentation
#

# You can set these variables from the command line.
SPHINXOPTS    =
SPHINXBUILD   = sphinx-build
PAPER         =
BUILDDIR      = _build

# Internal variables.
PAPEROPT_a4     = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .

.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest

help:
	@echo "Please use \`make <target>' where <target> is one of"
	@echo "  html      to make standalone HTML files"
	@echo "  dirhtml   to make HTML files named index.html in directories"
	@echo "  pickle    to make pickle files"
	@echo "  json      to make JSON files"
	@echo "  htmlhelp  to make HTML files and a HTML help project"
	@echo "  qthelp    to make HTML files and a qthelp project"
	@echo "  latex     to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
	@echo "  changes   to make an overview of all changed/added/deprecated items"
	@echo "  linkcheck to check all external links for integrity"
	@echo "  doctest   to run all doctests embedded in the documentation (if enabled)"

clean:
	-rm -rf $(BUILDDIR)/*

html:
	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
	@echo
	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."

dirhtml:
	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
	@echo
	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."

pickle:
	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
	@echo
	@echo "Build finished; now you can process the pickle files."

json:
	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
	@echo
	@echo "Build finished; now you can process the JSON files."

htmlhelp:
	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
	@echo
	@echo "Build finished; now you can run HTML Help Workshop with the" \
	      ".hhp project file in $(BUILDDIR)/htmlhelp."

qthelp:
	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
	@echo
	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/superlance.qhcp"
	@echo "To view the help file:"
	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/superlance.qhc"

latex:
	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
	@echo
	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
	@echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
	      "run these through (pdf)latex."

changes:
	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
	@echo
	@echo "The overview file is in $(BUILDDIR)/changes."

linkcheck:
	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
	@echo
	@echo "Link check complete; look for any errors in the above output " \
	      "or in $(BUILDDIR)/linkcheck/output.txt."

doctest:
	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
	@echo "Testing of doctests in the sources finished, look at the " \
	      "results in $(BUILDDIR)/doctest/output.txt."


================================================
FILE: docs/conf.py
================================================
# -*- coding: utf-8 -*-
#
# superlance documentation build configuration file, created by
# sphinx-quickstart on Thu Jun 10 11:55:43 2010.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.

import sys, os

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path 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('.'))

# -- 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', 'sphinx.ext.doctest']

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']

# The suffix of source filenames.
source_suffix = '.rst'

# The encoding of source files.
#source_encoding = 'utf-8'

# The master toctree document.
master_doc = 'index'

# General information about the project.
project = u'superlance'
copyright = u'2010, Chris McDonough, Agendaless Consulting, Inc.'

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = '2.0.1.dev0'
# The full version, including alpha/beta/rc tags.
release = version

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None

# 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 directory, that shouldn't be searched
# for source files.
exclude_trees = ['_build']

# 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'

# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []


# -- Options for HTML output ---------------------------------------------------

# The theme to use for HTML and HTML Help pages.  Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'default'

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}

# Add any paths that contain custom themes here, relative to this directory.
#html_theme_path = []

# The name for this set of Sphinx documents.  If None, it defaults to
# "<project> v<release> 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 (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None

# 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, links to the reST sources are added to the pages.
#html_show_sourcelink = True

# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> 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 = 'superlancedoc'


# -- 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, documentclass [howto/manual]).
latex_documents = [
  ('index', 'superlance.tex', u'superlance Documentation',
   u'Chris McDonough, Agendaless Consulting, Inc.', 'manual'),
]

# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None

# 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/crashmail.rst
================================================
:command:`crashmail` Documentation
==================================

:command:`crashmail` is a supervisor "event listener", intended to be
subscribed to ``PROCESS_STATE_EXITED`` events. When :command:`crashmail`
receives that event, and the transition is "unexpected", :command:`crashmail`
sends an email notification to a configured address.

:command:`crashmail` is incapable of monitoring the process status of processes
which are not :command:`supervisord` child processes.

:command:`crashmail` is a "console script" installed when you install
:mod:`superlance`.  Although :command:`crashmail` is an executable program, it
isn't useful as a general-purpose script:  it must be run as a
:command:`supervisor` event listener to do anything useful.

Command-Line Syntax
-------------------

.. code-block:: sh

   $ crashmail [-p processname] [-a] [-o string] [-m mail_address] \
               [-s sendmail]

.. program:: crashmail

.. cmdoption:: -p <process_name>, --program=<process_name>

   Send mail when the specified :command:`supervisord` child process
   transitions unexpectedly to the ``EXITED`` state.

   This option can be provided more than once to have :command:`crashmail`
   monitor more than one program.

   To monitor a process which is part of a :command:`supervisord` group,
   specify its name as ``group_name:process_name``.

.. cmdoption:: -a, --any

   Send mail when any :command:`supervisord` child process transitions
   unexpectedly to the ``EXITED`` state.

   Overrides any ``-p`` parameters passed in the same :command:`crashmail`
   process invocation.

.. cmdoption:: -o <prefix>, --optionalheader=<prefix>

   Specify a parameter used as a prefix in the mail :mailheader:`Subject`
   header.

.. cmdoption:: -s <sendmail_command>, --sendmail_program=<sendmail_command>

   Specify the sendmail command to use to send email.

   Must be a command which accepts header and message data on stdin and
   sends mail.  Default is ``/usr/sbin/sendmail -t -i``.

.. cmdoption:: -m <email_address>, --email=<email_address>

   Specify an email address to which crash notification messages are sent.
   If no email address is specified, email will not be sent.


Configuring :command:`crashmail` Into the Supervisor Config
-----------------------------------------------------------

An ``[eventlistener:x]`` section must be placed in :file:`supervisord.conf`
in order for :command:`crashmail` to do its work. See the "Events" chapter in
the Supervisor manual for more information about event listeners.

The following example assumes that :command:`crashmail` is on your system
:envvar:`PATH`.

.. code-block:: ini

   [eventlistener:crashmail]
   command=crashmail -p program1 -p group1:program2 -m dev@example.com
   events=PROCESS_STATE_EXITED


================================================
FILE: docs/crashmailbatch.rst
================================================
:command:`crashmailbatch` Documentation
=======================================

:command:`crashmailbatch` is a supervisor "event listener", intended to be
subscribed to ``PROCESS_STATE`` and ``TICK_60`` events.  It monitors
all processes running under a given supervisord instance.

Similar to :command:`crashmail`, :command:`crashmailbatch` sends email 
alerts when processes die unexpectedly.  The difference is that all alerts 
generated within the configured time interval are batched together to avoid 
sending too many emails.   

:command:`crashmailbatch` is a "console script" installed when you install
:mod:`superlance`.  Although :command:`crashmailbatch` is an executable 
program, it isn't useful as a general-purpose script:  it must be run as a
:command:`supervisor` event listener to do anything useful.

Command-Line Syntax
-------------------

.. code-block:: sh

   $ crashmailbatch --toEmail=<email address> --fromEmail=<email address> \
           [--interval=<batch interval in minutes>] [--subject=<email subject>] \
		   [--tickEvent=<event name>] [--smtpHost=<SMTP server>] \
           [--userName=<SMTP username>] [--password=<STMP password>] \
           [--tls]
   
.. program:: crashmailbatch

.. cmdoption:: -t <destination email>, --toEmail=<destination email>
   
   Specify comma separated email addresses to which crash notification messages are sent.
 
.. cmdoption:: -f <source email>, --fromEmail=<source email>
   
   Specify an email address from which crash notification messages are sent.

.. cmdoption:: -i <interval>, --interval=<interval>
   
   Specify the time interval in minutes to use for batching notifcations.
   Defaults to 1.0 minute.

.. cmdoption:: -s <email subject>, --subject=<email subject>
   
   Override the email subject line.  Defaults to "Crash alert from supervisord"

.. cmdoption:: -e <event name>, --tickEvent=<event name>

   Override the TICK event name.  Defaults to "TICK_60"
   
.. cmdoption:: -H <STMP server> --smtpHost <SMTP server>

   Specify STMP server for sending email

.. cmdoption:: -u <STMP username> --userName <SMTP username>

   Specify STMP username

.. cmdoption:: -p <STMP password> --password <SMTP password>

   Specify STMP password

.. cmdoption:: --tls

   Use Transport Layer Security (TLS)

Configuring :command:`crashmailbatch` Into the Supervisor Config
----------------------------------------------------------------

An ``[eventlistener:x]`` section must be placed in :file:`supervisord.conf`
in order for :command:`crashmailbatch` to do its work. See the "Events" chapter in
the Supervisor manual for more information about event listeners.

The following example assumes that :command:`crashmailbatch` is on your system
:envvar:`PATH`.

.. code-block:: ini

   [eventlistener:crashmailbatch]
   command=crashmailbatch --toEmail="alertme@fubar.com" --fromEmail="supervisord@fubar.com" 
   events=PROCESS_STATE,TICK_60


================================================
FILE: docs/crashsms.rst
================================================
:command:`crashsms` Documentation
==================================

:command:`crashsms` is a supervisor "event listener", intended to be
subscribed to ``PROCESS_STATE`` events and ``TICK`` events such as ``TICK_60``.  It monitors
all processes running under a given supervisord instance.

Similar to :command:`crashmailbatch`, :command:`crashsms` sends SMS alerts
through an email gateway.  Messages are formatted to fit in SMS

:command:`crashsms` is a "console script" installed when you install
:mod:`superlance`.  Although :command:`crashsms` is an executable 
program, it isn't useful as a general-purpose script:  it must be run as a
:command:`supervisor` event listener to do anything useful.

Command-Line Syntax
-------------------

.. code-block:: sh

   $ crashsms --toEmail=<email address> --fromEmail=<email address> \
           [--interval=<batch interval in minutes>] [--subject=<email subject>] \
		   [--tickEvent=<event name>]
   
.. program:: crashsms

.. cmdoption:: -t <destination email>, --toEmail=<destination email>
   
   Specify comma separated email addresses to which crash notification messages are sent.
 
.. cmdoption:: -f <source email>, --fromEmail=<source email>
   
   Specify an email address from which crash notification messages are sent.

.. cmdoption:: -i <interval>, --interval=<interval>
   
   Specify the time interval in minutes to use for batching notifcations.
   Defaults to 1.0 minute.

.. cmdoption:: -s <email subject>, --subject=<email subject>
   
   Set the email subject line.  Default is None

.. cmdoption:: -e <event name>, --tickEvent=<event name>

   Override the TICK event name.  Defaults to "TICK_60"

Configuring :command:`crashsms` Into the Supervisor Config
-----------------------------------------------------------

An ``[eventlistener:x]`` section must be placed in :file:`supervisord.conf`
in order for :command:`crashsms` to do its work. See the "Events" chapter in
the Supervisor manual for more information about event listeners.

The following example assumes that :command:`crashsms` is on your system
:envvar:`PATH`.

.. code-block:: ini

   [eventlistener:crashsms]
   command=crashsms --toEmail="<mobile number>@<sms email gateway>" --fromEmail="supervisord@fubar.com" 
   events=PROCESS_STATE,TICK_60


================================================
FILE: docs/development.rst
================================================
Resources and Development
=========================

Bug Tracker
-----------

Superlance has a bug tracker where you may report any bugs or other
errors you find.  Please report bugs to the `GitHub issues page
<https://github.com/supervisor/Superlance/issues>`_.

Version Control Repository
--------------------------

You can also view the `Superlance version control repository
<https://github.com/Supervisor/superlance>`_.

Contributing
------------

`Pull requests <https://help.github.com/articles/about-pull-requests/>`_
can be submitted to the Superlance repository on GitHub.

In the time since Superlance was created,
there are now many `third party plugins for Supervisor <http://supervisord.org/plugins.html>`_.
Most new plugins should be in their own package rather than added to Superlance.

Author Information
------------------

The following people are responsible for creating Superlance.

Original Author
~~~~~~~~~~~~~~~

`Chris McDonough <http://plope.com>`_ is the original author of Superlance.

Contributors
~~~~~~~~~~~~

Contributors are tracked on the `GitHub contributions page
<https://github.com/Supervisor/Superlance/graphs/contributors>`_.

The list below is included for historical reasons.  It records contributors who
signed a legal agreement.  The legal agreement was
`introduced <https://github.com/Supervisor/superlance/commit/90889bef6f45edb6cb7e6dcc5c7826e718c38da7>`_
in January 2014 but later
`withdrawn <https://github.com/Supervisor/superlance/commit/e2f198d2652e3177aea8c4075a13d43f7da04b5b>`_
in April 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, 2008-09-18

- Tres Seaver, 2009-02-11

- Roger Hoover, 2010-07-30

- Joaquín Cuenca Abela, 2011-06-23

- Harald Friessnegger, 2012-11-01

- Mikhail Lukyanchenko, 2013-12-23

- Patrick Gerken, 2014-01-27


================================================
FILE: docs/fatalmailbatch.rst
================================================
:command:`fatalmailbatch` Documentation
=======================================

:command:`fatalmailbatch` is a supervisor "event listener", intended to be
subscribed to ``PROCESS_STATE`` and ``TICK_60`` events.  It monitors
all processes running under a given supervisord instance.

:command:`fatalmailbatch` sends email alerts when processes fail to start 
too many times such that supervisord gives up retrying.  All of the fatal
start events generated within the configured time interval are batched 
together to avoid sending too many emails.   

:command:`fatalmailbatch` is a "console script" installed when you install
:mod:`superlance`.  Although :command:`fatalmailbatch` is an executable 
program, it isn't useful as a general-purpose script:  it must be run as a
:command:`supervisor` event listener to do anything useful.

Command-Line Syntax
-------------------

.. code-block:: sh

   $ fatalmailbatch --toEmail=<email address> --fromEmail=<email address> \
           [--interval=<batch interval in minutes>] [--subject=<email subject>]
   
.. program:: fatalmailbatch

.. cmdoption:: -t <destination email>, --toEmail=<destination email>
   
   Specify comma separated email addresses to which fatal start notification messages are sent.
 
.. cmdoption:: -f <source email>, --fromEmail=<source email>
   
   Specify an email address from which fatal start notification messages 
   are sent.

.. cmdoption:: -i <interval>, --interval=<interval>
   
   Specify the time interval in minutes to use for batching notifcations.
   Defaults to 1 minute.

.. cmdoption:: -s <email subject>, --subject=<email subject>
   
   Override the email subject line.  Defaults to "Fatal start alert from 
   supervisord"

Configuring :command:`fatalmailbatch` Into the Supervisor Config
----------------------------------------------------------------

An ``[eventlistener:x]`` section must be placed in :file:`supervisord.conf`
in order for :command:`fatalmailbatch` to do its work. See the "Events" chapter in
the Supervisor manual for more information about event listeners.

The following example assumes that :command:`fatalmailbatch` is on your system
:envvar:`PATH`.

.. code-block:: ini

   [eventlistener:fatalmailbatch]
   command=fatalmailbatch --toEmail="alertme@fubar.com" --fromEmail="supervisord@fubar.com" 
   events=PROCESS_STATE,TICK_60


================================================
FILE: docs/httpok.rst
================================================
:command:`httpok` Documentation
==================================

:command:`httpok` is a supervisor "event listener" which may be subscribed to
a concrete ``TICK_5``, ``TICK_60`` or ``TICK_3600``  event.
When :command:`httpok` receives a ``TICK_x``
event (``TICK_60`` is recommended, indicating activity every 60 seconds),
:command:`httpok` makes an HTTP GET request to a confgured URL. If the request
fails or times out, :command:`httpok` will restart the "hung" child
process(es). :command:`httpok` can be configured to send an email notification
when it restarts a process.

:command:`httpok` can only monitor the process status of processes
which are :command:`supervisord` child processes.

:command:`httpok` is a "console script" installed when you install
:mod:`superlance`.  Although :command:`httpok` is an executable program, it
isn't useful as a general-purpose script:  it must be run as a
:command:`supervisor` event listener to do anything useful.

Command-Line Syntax
-------------------

.. code-block:: sh

   $ httpok [-p processname] [-a] [-g] [-t timeout] [-c status_code] \
            [-b inbody] [-m mail_address] [-s sendmail] URL

.. program:: httpok

.. cmdoption:: -p <process_name>, --program=<process_name>

   Restart the :command:`supervisord` child process named ``process_name``
   if it is in the ``RUNNING`` state when the URL returns an unexpected
   result or times out.

   This option can be provided more than once to have :command:`httpok`
   monitor more than one process.

   To monitor a process which is part of a :command:`supervisord` group,
   specify its name as ``group_name:process_name``.

.. cmdoption:: -a, --any

   Restart any child of :command:`supervisord` in the ``RUNNING`` state
   if the URL returns an unexpected result or times out.

   Overrides any ``-p`` parameters passed in the same :command:`httpok`
   process invocation.

.. cmdoption:: -g <gcore_program>, --gcore=<gcore_program>

   Use the specifed program to ``gcore`` the :command:`supervisord` child
   process.  The program should accept two arguments on the command line:
   a filename and a pid.  Defaults to ``/usr/bin/gcore -o``.

.. cmdoption:: -d <core_directory>, --coredir=<core_directory>

   If a core directory is specified, :command:`httpok` will try to use the
   ``gcore`` program (see ``-g``) to write a core file into this directory
   for each hung process before restarting it.  It will then append any gcore
   stdout output to the email message, if mail is configured (see the ``-m``
   option below).

.. cmdoption:: -t <timeout>, --timeout=<timeout>

   The number of seconds that :command:`httpok` should wait for a response
   to the HTTP request before timing out.

   If this timeout is exceeded, :command:`httpok` will attempt to restart
   child processes which are in the ``RUNNING`` state, and specified by
   ``-p`` or ``-a``.

   Defaults to 10 seconds.

.. cmdoption:: -c <http_status_code>, --code=<http_status_code>

   Specify the expected HTTP status code for the configured URL.

   If this status code is not the status code provided by the response,
   :command:`httpok` will attempt to restart child processes which are
   in the ``RUNNING`` state, and specified by ``-p`` or ``-a``.

   Defaults to 200.

.. cmdoption:: -b <body_string>, --body=<body_string>

   Specify a string which should be present in the body resulting
   from the GET request.

   If this string is not present in the response, :command:`httpok` will
   attempt to restart child processes which are in the RUNNING state,
   and specified by ``-p`` or ``-a``.

   The default is to ignore the body.

.. cmdoption:: -s <sendmail_command>, --sendmail_program=<sendmail_command>

   Specify the sendmail command to use to send email.

   Must be a command which accepts header and message data on stdin and
   sends mail.  Default is ``/usr/sbin/sendmail -t -i``.

.. cmdoption:: -m <email_address>, --email=<email_address>

   Specify an email address to which notification messages are sent.
   If no email address is specified, email will not be sent.

.. cmdoption:: -e, --eager

   Enable "eager" monitoring:  check the URL and emit mail even if no
   monitored child process is in the ``RUNNING`` state.

   Enabled by default.

.. cmdoption:: -E, --not-eager

   Disable "eager" monitoring:  do not check the URL or emit mail if no
   monitored process is in the RUNNING state.

.. cmdoption:: URL

   The URL to which to issue a GET request.

.. cmdoption:: -n <httpok name>, --name=<httpok name>

    An optional name that identifies this httpok process. If given, the
    email subject will start with ``httpok [<httpok name>]:`` instead
    of ``httpok:``
    In case you run multiple supervisors on a single host that control
    different processes with the same name (eg `zopeinstance1`) you can
    use this option to indicate which project the restarted instance
    belongs to.


Configuring :command:`httpok` Into the Supervisor Config
-----------------------------------------------------------

An ``[eventlistener:x]`` section must be placed in :file:`supervisord.conf`
in order for :command:`httpok` to do its work.
See the "Events" chapter in the
Supervisor manual for more information about event listeners.

The following example assumes that :command:`httpok` is on your system
:envvar:`PATH`.

.. code-block:: ini

   [eventlistener:httpok]
   command=httpok -p program1 -p group1:program2 http://localhost:8080/tasty
   events=TICK_60


================================================
FILE: docs/index.rst
================================================
superlance plugins for supervisor
=================================

Superlance is a package of plugin utilities for monitoring and
controlling processes that run under `Supervisor
<http://supervisord.org>`_.  It provides these plugins:

:command:`httpok`
    This plugin is meant to be used as a supervisor event listener,
    subscribed to ``TICK_*`` events.  It tests that a given child process
    which must in the ``RUNNING`` state, is viable via an HTTP ``GET``
    request to a configured URL.  If the request fails or times out,
    :command:`httpok` will restart the "hung" child process.

:command:`crashmail`
    This plugin is meant to be used as a supervisor event listener,
    subscribed to ``PROCESS_STATE_EXITED`` events.  It email a user when
    a process enters the ``EXITED`` state unexpectedly.

:command:`memmon`
    This plugin is meant to be used as a supervisor event listener,
    subscribed to ``TICK_*`` events.  It monitors memory usage for configured
    child processes, and restarts them when they exceed a configured
    maximum size.

:command:`crashmailbatch`
    Similar to :command:`crashmail`, :command:`crashmailbatch` sends email
    alerts when processes die unexpectedly.  The difference is that all alerts
    generated within the configured time interval are batched together to avoid
    sending too many emails.

:command:`fatalmailbatch`
    This plugin sends email alerts when processes fail to start
    too many times such that supervisord gives up retrying.  All of the fatal
    start events generated within the configured time interval are batched
    together to avoid sending too many emails.

:command:`crashsms`
    Similar to :command:`crashmailbatch` except it sends SMS alerts
    through an email gateway.  Messages are formatted to fit in SMS.

Contents:

.. toctree::
   :maxdepth: 2

   httpok
   crashmail
   memmon
   crashmailbatch
   fatalmailbatch
   crashsms
   development

Indices and tables
==================

* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`



================================================
FILE: docs/memmon.rst
================================================
:command:`memmon` Overview
==========================

:command:`memmon` is a supervisor "event listener" which may be subscribed to
a concrete ``TICK_x`` event. When :command:`memmon` receives a ``TICK_x``
event (``TICK_60`` is recommended, indicating activity every 60 seconds),
:command:`memmon` checks that a configurable list of programs (or all
programs running under supervisor) are not exceeding a configurable amount of
memory (resident segment size, or RSS).  If one or more of these processes is
consuming more than the amount of memory that :command:`memmon` believes it
should, :command:`memmon` will restart the process. :command:`memmon` can be
configured to send an email notification when it restarts a process.

:command:`memmon` is known to work on Linux and Mac OS X, but has not been
tested on other operating systems (it relies on :command:`ps` output and
command-line switches).

:command:`memmon` is incapable of monitoring the process status of processes
which are not :command:`supervisord` child processes. Without the
`--cumulative` option, only the RSS of immediate children of the
:command:`supervisord` process will be considered.

:command:`memmon` is a "console script" installed when you install
:mod:`superlance`.  Although :command:`memmon` is an executable program, it
isn't useful as a general-purpose script:  it must be run as a
:command:`supervisor` event listener to do anything useful.

:command:`memmon` uses Supervisor's XML-RPC interface.  Your ``supervisord.conf``
file must have a valid `[unix_http_server]
<http://supervisord.org/configuration.html#unix-http-server-section-settings>`_
or `[inet_http_server]
<http://supervisord.org/configuration.html#inet-http-server-section-settings>`_
section, and must have an `[rpcinterface:supervisor]
<http://supervisord.org/configuration.html#rpcinterface-x-section-settings>`_
section.  If you are able to control your ``supervisord`` instance with
``supervisorctl``, you have already met these requirements.

Command-Line Syntax
-------------------

.. code-block:: sh

   $ memmon [-c] [-p processname=byte_size] [-g groupname=byte_size] \
            [-a byte_size] [-s sendmail] [-m email_address] \
            [-u email_uptime_limit] [-n memmon_name]

.. program:: memmon

.. cmdoption:: -h, --help

   Show program help.

.. cmdoption:: -c, --cumulative

   Check against cumulative RSS. When calculating a process' RSS, also
   consider its child processes. With this option `memmon` will sum up
   the RSS of the process to be monitored and all its children.

.. cmdoption:: -p <name/size pair>, --program=<name/size pair>

   A name/size pair, e.g. "foo=1MB". The name represents the supervisor
   program name that you would like :command:`memmon` to monitor; the size
   represents the number of bytes (suffix-multiplied using "KB", "MB" or "GB")
   that should be considered "too much".

   This option can be provided more than once to have :command:`memmon`
   monitor more than one program.

   Programs can be specified using a "namespec", to disambiguate same-named
   programs in different groups, e.g. ``foo:bar`` represents the program
   ``bar`` in the ``foo`` group.

.. cmdoption:: -g <name/size pair>, --groupname=<name/size pair>

   A groupname/size pair, e.g. "group=1MB". The name represents the supervisor
   group name that you would like :command:`memmon` to monitor; the size
   represents the number of bytes (suffix-multiplied using "KB", "MB" or "GB")
   that should be considered "too much".

   Multiple ``-g`` options can be provided to have :command:`memmon` monitor
   more than one group.  If any process in this group exceeds the maximum,
   it will be restarted.

.. cmdoption:: -a <size>, --any=<size>

   A size (suffix-multiplied using "KB", "MB" or "GB") that should be
   considered "too much". If any program running as a child of supervisor
   exceeds this maximum, it will be restarted. E.g. 100MB.

.. cmdoption:: -s <command>, --sendmail=<command>

   A command that will send mail if passed the email body (including the
   headers).  Defaults to ``/usr/sbin/sendmail -t -i``.

.. note::

   Specifying this option doesn't cause memmon to send mail by itself:
   see the ``-m`` / ``--email`` option.

.. cmdoption:: -m <email address>, --email=<email address>

   An email address to which to send email when a process is restarted.
   By default, memmon will not send any mail unless an email address is
   specified.

.. cmdoption:: -u <email uptime limit>, --uptime=<email uptime limit>

   Only send an email in case the restarted process' uptime (in seconds)
   is below this limit.
   (Useful to only get notified if a processes gets restarted too frequently)

   Uptime is given in seconds (suffix-multiplied using "m" for minutes,
   "h" for hours or "d" for days)

.. cmdoption:: -n <memmon name>, --name=<memmon name>

   An optional name that identifies this memmon process. If given, the
   email subject will start with ``memmon [<memmon name>]:`` instead
   of ``memmon:``
   In case you run multiple supervisors on a single host that control
   different processes with the same name (eg `zopeinstance1`) you can
   use this option to indicate which project the restarted instance
   belongs to.



Configuring :command:`memmon` Into the Supervisor Config
--------------------------------------------------------

An ``[eventlistener:x]`` section must be placed in :file:`supervisord.conf`
in order for :command:`memmon` to do its work. See the "Events" chapter in the
Supervisor manual for more information about event listeners.

If the `[unix_http_server]
<http://supervisord.org/configuration.html#unix-http-server-section-settings>`_
or `[inet_http_server]
<http://supervisord.org/configuration.html#inet-http-server-section-settings>`_
has been configured to use authentication, add the environment variables
``SUPERVISOR_USERNAME`` and ``SUPERVISOR_PASSWORD`` in the ``[eventlistener:x]``
section as shown in Example Configuration 5.

The following examples assume that :command:`memmon` is on your system
:envvar:`PATH`.

Example Configuration 1
#######################

This configuration causes :command:`memmon` to restart any process which is
a child of :command:`supervisord` consuming more than 200MB of RSS, and will
send mail to ``bob@example.com`` when it restarts a process using the
default :command:`sendmail` command.

.. code-block:: ini

   [eventlistener:memmon]
   command=memmon -a 200MB -m bob@example.com
   events=TICK_60


Example Configuration 2
#######################

This configuration causes :command:`memmon` to restart any process with the
supervisor program name ``foo`` consuming more than 200MB of RSS, and
will send mail to ``bob@example.com`` when it restarts a process using
the default sendmail command.

.. code-block:: ini

   [eventlistener:memmon]
   command=memmon -p foo=200MB -m bob@example.com
   events=TICK_60


Example Configuration 3
#######################

This configuration causes :command:`memmon` to restart any process in the
process group "bar" consuming more than 200MB of RSS, and will send mail to
``bob@example.com`` when it restarts a process using the default
:command:`sendmail` command.

.. code-block:: ini

   [eventlistener:memmon]
   command=memmon -g bar=200MB -m bob@example.com
   events=TICK_60


Example Configuration 4
#######################

This configuration causes :command:`memmon` to restart any process meeting
the same requirements as in `Example Configuration 2`_ with one difference:

The email will only be sent if the process' uptime is less or equal than
2 days (172800 seconds)

.. code-block:: ini

   [eventlistener:memmon]
   command=memmon -p foo=200MB -m bob@example.com -u 2d
   events=TICK_60


Example Configuration 5 (Authentication)
########################################

This configuration is the same as the one in `Example Configuration 1`_ with
the only difference being that the `[unix_http_server]
<http://supervisord.org/configuration.html#unix-http-server-section-settings>`_
or `[inet_http_server]
<http://supervisord.org/configuration.html#inet-http-server-section-settings>`_
has been configured to use authentication.

.. code-block:: ini

   [eventlistener:memmon]
   command=memmon -a 200MB -m bob@example.com
   environment=SUPERVISOR_USERNAME="<username>",SUPERVISOR_PASSWORD="<password>"
   events=TICK_60


================================================
FILE: setup.cfg
================================================
;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) 2008-2013 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, superlance requires Python 2.7 or later')
elif (3, 0) < py_version < (3, 4):
    raise RuntimeError('On Python 3, superlance requires Python 3.4 or later')

from setuptools import setup, find_packages

here = os.path.abspath(os.path.dirname(__file__))
try:
    README = open(os.path.join(here, 'README.rst')).read()
except (IOError, OSError):
    README = ''
try:
    CHANGES = open(os.path.join(here, 'CHANGES.rst')).read()
except (IOError, OSError):
    CHANGES = ''

setup(name='superlance',
      version='2.0.1.dev0',
      license='BSD-derived (http://www.repoze.org/LICENSE.txt)',
      description='superlance plugins for supervisord',
      long_description=README + '\n\n' +  CHANGES,
      classifiers=[
        "Development Status :: 5 - Production/Stable",
        'Environment :: No Input/Output (Daemon)',
        'Intended Audience :: System Administrators',
        'Natural Language :: English',
        'Operating System :: POSIX',
        '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',
        'Topic :: System :: Boot',
        'Topic :: System :: Monitoring',
        'Topic :: System :: Systems Administration',
        ],
      author='Chris McDonough',
      author_email='chrism@plope.com',
      url='https://github.com/Supervisor/superlance',
      keywords = 'supervisor monitoring',
      packages = find_packages(),
      include_package_data=True,
      zip_safe=False,
      install_requires=['supervisor',],
      extras_require={'test': ['pytest'],},
      entry_points = """\
      [console_scripts]
      httpok = superlance.httpok:main
      crashsms = superlance.crashsms:main
      crashmail = superlance.crashmail:main
      crashmailbatch = superlance.crashmailbatch:main
      fatalmailbatch = superlance.fatalmailbatch:main
      memmon = superlance.memmon:main
      """
      )


================================================
FILE: superlance/__init__.py
================================================
# superlance package


================================================
FILE: superlance/compat.py
================================================
try:
    import http.client as httplib
except ImportError:
    import httplib

try:
    from StringIO import StringIO
except ImportError:
    from io import StringIO

try:
    from sys import maxsize as maxint
except ImportError:
    from sys import maxint

try:
    import urllib.parse as urlparse
    import urllib.parse as urllib
except ImportError:
    import urlparse
    import urllib

try:
    import xmlrpc.client as xmlrpclib
except ImportError:
    import xmlrpclib


================================================
FILE: superlance/crashmail.py
================================================
#!/usr/bin/env python -u
##############################################################################
#
# Copyright (c) 2007 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
#
##############################################################################

# A event listener meant to be subscribed to PROCESS_STATE_CHANGE
# events.  It will send mail when processes that are children of
# supervisord transition unexpectedly to the EXITED state.

# A supervisor config snippet that tells supervisor to use this script
# as a listener is below.
#
# [eventlistener:crashmail]
# command =
#     /usr/bin/crashmail
#         -o hostname -a -m notify-on-crash@domain.com
#         -s '/usr/sbin/sendmail -t -i -f crash-notifier@domain.com'
# events=PROCESS_STATE
#
# Sendmail is used explicitly here so that we can specify the 'from' address.

doc = """\
crashmail.py [-p processname] [-a] [-o string] [-m mail_address]
             [-s sendmail] URL

Options:

-p -- specify a supervisor process_name.  Send mail when this process
      transitions to the EXITED state unexpectedly. If this process is
      part of a group, it can be specified using the
      'group_name:process_name' syntax.

-a -- Send mail when any child of the supervisord transitions
      unexpectedly to the EXITED state unexpectedly.  Overrides any -p
      parameters passed in the same crashmail process invocation.

-o -- Specify a parameter used as a prefix in the mail subject header.

-s -- the sendmail command to use to send email
      (e.g. "/usr/sbin/sendmail -t -i").  Must be a command which accepts
      header and message data on stdin and sends mail.  Default is
      "/usr/sbin/sendmail -t -i".

-m -- specify an email address.  The script will send mail to this
      address when crashmail detects a process crash.  If no email
      address is specified, email will not be sent.

The -p option may be specified more than once, allowing for
specification of multiple processes.  Specifying -a overrides any
selection of -p.

A sample invocation:

crashmail.py -p program1 -p group1:program2 -m dev@example.com

"""

import getopt
import os
import sys

from supervisor import childutils


def usage(exitstatus=255):
    print(doc)
    sys.exit(exitstatus)


class CrashMail:

    def __init__(self, programs, any, email, sendmail, optionalheader):

        self.programs = programs
        self.any = any
        self.email = email
        self.sendmail = sendmail
        self.optionalheader = optionalheader
        self.stdin = sys.stdin
        self.stdout = sys.stdout
        self.stderr = sys.stderr

    def runforever(self, test=False):
        while 1:
            # we explicitly use self.stdin, self.stdout, and self.stderr
            # instead of sys.* so we can unit test this code
            headers, payload = childutils.listener.wait(
                self.stdin, self.stdout)

            if not headers['eventname'] == 'PROCESS_STATE_EXITED':
                # do nothing with non-TICK events
                childutils.listener.ok(self.stdout)
                if test:
                    self.stderr.write('non-exited event\n')
                    self.stderr.flush()
                    break
                continue

            pheaders, pdata = childutils.eventdata(payload+'\n')

            if int(pheaders['expected']):
                childutils.listener.ok(self.stdout)
                if test:
                    self.stderr.write('expected exit\n')
                    self.stderr.flush()
                    break
                continue

            msg = ('Process %(processname)s in group %(groupname)s exited '
                   'unexpectedly (pid %(pid)s) from state %(from_state)s' %
                   pheaders)

            subject = ' %s crashed at %s' % (pheaders['processname'],
                                             childutils.get_asctime())
            if self.optionalheader:
                subject = self.optionalheader + ':' + subject

            self.stderr.write('unexpected exit, mailing\n')
            self.stderr.flush()

            self.mail(self.email, subject, msg)

            childutils.listener.ok(self.stdout)
            if test:
                break

    def mail(self, email, subject, msg):
        body = 'To: %s\n' % self.email
        body += 'Subject: %s\n' % subject
        body += '\n'
        body += msg
        with os.popen(self.sendmail, 'w') as m:
            m.write(body)
        self.stderr.write('Mailed:\n\n%s' % body)
        self.mailed = body


def main(argv=sys.argv):
    short_args = "hp:ao:s:m:"
    long_args = [
        "help",
        "program=",
        "any",
        "optionalheader=",
        "sendmail_program=",
        "email=",
        ]
    arguments = argv[1:]
    try:
        opts, args = getopt.getopt(arguments, short_args, long_args)
    except:
        usage()

    programs = []
    any = False
    sendmail = '/usr/sbin/sendmail -t -i'
    email = None
    optionalheader = None

    for option, value in opts:

        if option in ('-h', '--help'):
            usage(exitstatus=0)

        if option in ('-p', '--program'):
            programs.append(value)

        if option in ('-a', '--any'):
            any = True

        if option in ('-s', '--sendmail_program'):
            sendmail = value

        if option in ('-m', '--email'):
            email = value

        if option in ('-o', '--optionalheader'):
            optionalheader = value

    if not 'SUPERVISOR_SERVER_URL' in os.environ:
        sys.stderr.write('crashmail must be run as a supervisor event '
                         'listener\n')
        sys.stderr.flush()
        return

    prog = CrashMail(programs, any, email, sendmail, optionalheader)
    prog.runforever()


if __name__ == '__main__':
    main()


================================================
FILE: superlance/crashmailbatch.py
================================================
#!/usr/bin/env python -u
##############################################################################
#
# Copyright (c) 2007 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
#
##############################################################################

# A event listener meant to be subscribed to PROCESS_STATE_CHANGE
# events.  It will send mail when processes that are children of
# supervisord transition unexpectedly to the EXITED state.

# A supervisor config snippet that tells supervisor to use this script
# as a listener is below.
#
# [eventlistener:crashmailbatch]
# command=python crashmailbatch --toEmail=you@bar.com --fromEmail=me@bar.com
# events=PROCESS_STATE,TICK_60

doc = """\
crashmailbatch.py [--interval=<batch interval in minutes>]
        [--toEmail=<email address>]
        [--fromEmail=<email address>]
        [--subject=<email subject>]
        [--smtpHost=<hostname or address>]

Options:

--interval  - batch cycle length (in minutes).  The default is 1.0 minute.
                  This means that all events in each cycle are batched together
                  and sent as a single email

--toEmail   - the email address(es) to send alerts to - comma separated

--fromEmail - the email address to send alerts from

--subject   - the email subject line

--smtpHost  - the SMTP server's hostname or address (defaults to 'localhost')

A sample invocation:

crashmailbatch.py --toEmail="you@bar.com" --fromEmail="me@bar.com"

"""

from supervisor import childutils
from superlance.process_state_email_monitor import ProcessStateEmailMonitor


class CrashMailBatch(ProcessStateEmailMonitor):

    process_state_events = ['PROCESS_STATE_EXITED']

    def __init__(self, **kwargs):
        if kwargs.get('subject') is None:
            kwargs['subject'] = 'Crash alert from supervisord'
        ProcessStateEmailMonitor.__init__(self, **kwargs)
        self.now = kwargs.get('now', None)

    def get_process_state_change_msg(self, headers, payload):
        pheaders, pdata = childutils.eventdata(payload+'\n')

        if int(pheaders['expected']):
            return None

        txt = 'Process %(groupname)s:%(processname)s (pid %(pid)s) died \
unexpectedly' % pheaders
        return '%s -- %s' % (childutils.get_asctime(self.now), txt)


def main():
    crash = CrashMailBatch.create_from_cmd_line()
    crash.run()


if __name__ == '__main__':
    main()


================================================
FILE: superlance/crashsms.py
================================================
#!/usr/bin/env python -u
##############################################################################
#
# Copyright (c) 2007 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
#
##############################################################################

##############################################################################
# crashsms
# author: Juan Batiz-Benet (http://github.com/jbenet)
# based on crashmailbatch.py
##############################################################################


# A event listener meant to be subscribed to PROCESS_STATE_CHANGE
# events.  It will send mail when processes that are children of
# supervisord transition unexpectedly to the EXITED state.

# A supervisor config snippet that tells supervisor to use this script
# as a listener is below.
#
# [eventlistener:crashsms]
# command =
#     python crashsms
#         -t <mobile phone>@<mobile provider> -f me@bar.com -e TICK_5
# events=PROCESS_STATE,TICK_5

doc = """\
crashsms.py [--interval=<batch interval in minutes>]
        [--toEmail=<email address>]
        [--fromEmail=<email address>]
        [--subject=<email subject>]

Options:

-i,--interval  - batch cycle length (in minutes).  The default is 1 minute.
                 This means that all events in each cycle are batched together
                 and sent as a single email

-t,--toEmail   - the comma separated email addresses to send alerts to. Mobile providers
                 tend to allow sms messages to be sent to their phone numbers
                 via an email address (e.g.: 1234567890@txt.att.net)

-f,--fromEmail - the email address to send alerts from

-s,--subject   - the email subject line

-e, --tickEvent - specify which TICK event to use (e.g. TICK_5, TICK_60,
                  TICK_3600)

A sample invocation:

crashsms.py -t <mobile phone>@<mobile provider> -f me@bar.com -e TICK_5

"""

from supervisor import childutils
from superlance.process_state_email_monitor import ProcessStateEmailMonitor


class CrashSMS(ProcessStateEmailMonitor):
    process_state_events = ['PROCESS_STATE_EXITED']

    def __init__(self, **kwargs):
        ProcessStateEmailMonitor.__init__(self, **kwargs)
        self.now = kwargs.get('now', None)

    def get_process_state_change_msg(self, headers, payload):
        pheaders, pdata = childutils.eventdata(payload+'\n')

        if int(pheaders['expected']):
            return None

        txt = '[%(groupname)s:%(processname)s](%(pid)s) exited unexpectedly' \
              % pheaders
        return '%s %s' % (txt, childutils.get_asctime(self.now))


def main():
    crash = CrashSMS.create_from_cmd_line()
    crash.run()


if __name__ == '__main__':
    main()


================================================
FILE: superlance/fatalmailbatch.py
================================================
#!/usr/bin/env python -u
##############################################################################
#
# Copyright (c) 2007 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
#
##############################################################################

# A event listener meant to be subscribed to PROCESS_STATE_CHANGE
# events.  It will send mail when processes that are children of
# supervisord transition unexpectedly to the EXITED state.

# A supervisor config snippet that tells supervisor to use this script
# as a listener is below.
#
# [eventlistener:fatalmailbatch]
# command=python fatalmailbatch
# events=PROCESS_STATE,TICK_60

doc = """\
fatalmailbatch.py [--interval=<batch interval in minutes>]
        [--toEmail=<email address>]
        [--fromEmail=<email address>]
        [--subject=<email subject>]
        [--smtpHost=<smtp server>]
        [--userName=<smtp server username>]
        [--password=<smtp server password]

Options:

--interval  - batch cycle length (in minutes).  The default is 1 minute.
                  This means that all events in each cycle are batched together
                  and sent as a single email

--toEmail   - the email address(es) to send alerts to - comma separated

--fromEmail - the email address to send alerts from

--subject - the email subject line

A sample invocation:

fatalmailbatch.py --toEmail="you@bar.com" --fromEmail="me@bar.com"

"""

from supervisor import childutils
from superlance.process_state_email_monitor import ProcessStateEmailMonitor

class FatalMailBatch(ProcessStateEmailMonitor):

    process_state_events = ['PROCESS_STATE_FATAL']

    def __init__(self, **kwargs):
        if kwargs.get('subject') is None:
            kwargs['subject'] = 'Fatal start alert from supervisord'
        ProcessStateEmailMonitor.__init__(self, **kwargs)
        self.now = kwargs.get('now', None)

    def get_process_state_change_msg(self, headers, payload):
        pheaders, pdata = childutils.eventdata(payload+'\n')

        txt = 'Process %(groupname)s:%(processname)s failed to start too many \
times' % pheaders
        return '%s -- %s' % (childutils.get_asctime(self.now), txt)

def main():
    fatal = FatalMailBatch.create_from_cmd_line()
    fatal.run()

if __name__ == '__main__':
    main()


================================================
FILE: superlance/httpok.py
================================================
#!/usr/bin/env python -u
##############################################################################
#
# Copyright (c) 2007 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
#
##############################################################################

# A event listener meant to be subscribed to TICK_60 (or TICK_5)
# events, which restarts processes that are children of
# supervisord based on the response from an HTTP port.

# A supervisor config snippet that tells supervisor to use this script
# as a listener is below.
#
# [eventlistener:httpok]
# command=python -u /bin/httpok http://localhost:8080/tasty/service
# events=TICK_60

doc = """\
httpok.py [-p processname] [-a] [-g] [-t timeout] [-c status_code] [-b inbody]
          [-m mail_address] [-s sendmail] URL

Options:

-p -- specify a supervisor process_name.  Restart the supervisor
      process named 'process_name' if it's in the RUNNING state when
      the URL returns an unexpected result or times out.  If this
      process is part of a group, it can be specified using the
      'group_name:process_name' syntax.

-a -- Restart any child of the supervisord under in the RUNNING state
      if the URL returns an unexpected result or times out.  Overrides
      any -p parameters passed in the same httpok process
      invocation.

-g -- The ``gcore`` program.  By default, this is ``/usr/bin/gcore
      -o``.  The program should accept two arguments on the command
      line: a filename and a pid.

-d -- Core directory.  If a core directory is specified, httpok will
      try to use the ``gcore`` program (see ``-g``) to write a core
      file into this directory against each hung process before we
      restart it.  Append gcore stdout output to email.

-t -- The number of seconds that httpok should wait for a response
      before timing out.  If this timeout is exceeded, httpok will
      attempt to restart processes in the RUNNING state specified by
      -p or -a.  This defaults to 10 seconds.

-c -- specify an expected HTTP status code from a GET request to the
      URL.  If this status code is not the status code provided by the
      response, httpok will attempt to restart processes in the
      RUNNING state specified by -p or -a.  Default is 200.

-b -- specify a string which should be present in the body resulting
      from the GET request.  If this string is not present in the
      response, the processes in the RUNNING state specified by -p
      or -a will be restarted.  The default is to ignore the
      body.

-s -- the sendmail command to use to send email
      (e.g. "/usr/sbin/sendmail -t -i").  Must be a command which accepts
      header and message data on stdin and sends mail.
      Default is "/usr/sbin/sendmail -t -i".

-m -- specify an email address.  The script will send mail to this
      address when httpok attempts to restart processes.  If no email
      address is specified, email will not be sent.

-e -- "eager":  check URL / emit mail even if no process we are monitoring
      is in the RUNNING state.  Enabled by default.

-E -- not "eager":  do not check URL / emit mail if no process we are
      monitoring is in the RUNNING state.

-n -- optionally specify the name of the httpok process.  This name will
      be used in the email subject to identify which httpok process
      restarted the process.

URL -- The URL to which to issue a GET request.

The -c option may be specified more than once, allowing for
specification of multiple expected HTTP status codes.

The -p option may be specified more than once, allowing for
specification of multiple processes.  Specifying -a overrides any
selection of -p.

A sample invocation:

httpok.py -p program1 -p group1:program2 http://localhost:8080/tasty

"""

import getopt
import os
import socket
import sys
import time
from superlance.compat import urlparse
from superlance.compat import xmlrpclib

from supervisor import childutils
from supervisor.states import ProcessStates
from supervisor.options import make_namespec

from superlance import timeoutconn

def usage(exitstatus=255):
    print(doc)
    sys.exit(exitstatus)

class HTTPOk:
    connclass = None
    def __init__(self, rpc, programs, any, url, timeout, statuses, inbody,
                 email, sendmail, coredir, gcore, eager, retry_time, name):
        self.rpc = rpc
        self.programs = programs
        self.any = any
        self.url = url
        self.timeout = timeout
        self.retry_time = retry_time
        self.statuses = statuses
        self.inbody = inbody
        self.email = email
        self.sendmail = sendmail
        self.coredir = coredir
        self.gcore = gcore
        self.eager = eager
        self.stdin = sys.stdin
        self.stdout = sys.stdout
        self.stderr = sys.stderr
        self.name = name

    def listProcesses(self, state=None):
        return [x for x in self.rpc.supervisor.getAllProcessInfo()
                   if x['name'] in self.programs and
                      (state is None or x['state'] == state)]

    def runforever(self, test=False):
        parsed = urlparse.urlsplit(self.url)
        scheme = parsed.scheme.lower()
        hostport = parsed.netloc
        path = parsed.path
        query = parsed.query

        if query:
            path += '?' + query

        if self.connclass:
            ConnClass = self.connclass
        elif scheme == 'http':
            ConnClass = timeoutconn.TimeoutHTTPConnection
        elif scheme == 'https':
            ConnClass = timeoutconn.TimeoutHTTPSConnection
        else:
            raise ValueError('Bad scheme %s' % scheme)

        while 1:
            # we explicitly use self.stdin, self.stdout, and self.stderr
            # instead of sys.* so we can unit test this code
            headers, payload = childutils.listener.wait(self.stdin, self.stdout)

            if not headers['eventname'].startswith('TICK'):
                # do nothing with non-TICK events
                childutils.listener.ok(self.stdout)
                if test:
                    break
                continue

            conn = ConnClass(hostport)
            conn.timeout = self.timeout

            specs = self.listProcesses(ProcessStates.RUNNING)
            if self.eager or len(specs) > 0:

                try:
                    # build a loop value that is guaranteed to execute at least
                    # once and at most until the timeout is reached and that
                    # has 0 as the last value (to allow raising an exception
                    # in the last iteration)
                    for will_retry in range(
                            (self.timeout - 1) // (self.retry_time or 1),
                            -1, -1):
                        try:
                            headers = {'User-Agent': 'httpok'}
                            conn.request('GET', path, headers=headers)
                            break
                        except socket.error as e:
                            if e.errno == 111 and will_retry:
                                time.sleep(self.retry_time)
                            else:
                                raise

                    res = conn.getresponse()
                    body = res.read()
                    status = res.status
                    msg = 'status contacting %s: %s %s' % (self.url,
                                                           res.status,
                                                           res.reason)
                except Exception as e:
                    body = ''
                    status = None
                    msg = 'error contacting %s:\n\n %s' % (self.url, e)

                if status not in self.statuses:
                    subject = self.format_subject(
                        '%s: bad status returned' % self.url
                        )
                    self.act(subject, msg)
                elif self.inbody and self.inbody not in body:
                    subject = self.format_subject(
                        '%s: bad body returned' % self.url
                    )
                    self.act(subject, msg)

            childutils.listener.ok(self.stdout)
            if test:
                break

    def format_subject(self, subject):
        if self.name is None:
            return 'httpok: %s' % subject
        else:
            return 'httpok [%s]: %s' % (self.name, subject)

    def act(self, subject, msg):
        messages = [msg]

        def write(msg):
            self.stderr.write('%s\n' % msg)
            self.stderr.flush()
            messages.append(msg)

        try:
            specs = self.rpc.supervisor.getAllProcessInfo()
        except Exception as e:
            write('Exception retrieving process info %s, not acting' % e)
            return

        waiting = list(self.programs)

        if self.any:
            write('Restarting all running processes')
            for spec in specs:
                name = spec['name']
                group = spec['group']
                self.restart(spec, write)
                namespec = make_namespec(group, name)
                if name in waiting:
                    waiting.remove(name)
                if namespec in waiting:
                    waiting.remove(namespec)
        else:
            write('Restarting selected processes %s' % self.programs)
            for spec in specs:
                name = spec['name']
                group = spec['group']
                namespec = make_namespec(group, name)
                if (name in self.programs) or (namespec in self.programs):
                    self.restart(spec, write)
                    if name in waiting:
                        waiting.remove(name)
                    if namespec in waiting:
                        waiting.remove(namespec)

        if waiting:
            write(
                'Programs not restarted because they did not exist: %s' %
                waiting)

        if self.email:
            message = '\n'.join(messages)
            self.mail(self.email, subject, message)

    def mail(self, email, subject, msg):
        body =  'To: %s\n' % self.email
        body += 'Subject: %s\n' % subject
        body += '\n'
        body += msg
        with os.popen(self.sendmail, 'w') as m:
            m.write(body)
        self.stderr.write('Mailed:\n\n%s' % body)
        self.mailed = body

    def restart(self, spec, write):
        namespec = make_namespec(spec['group'], spec['name'])
        if spec['state'] is ProcessStates.RUNNING:
            if self.coredir and self.gcore:
                corename = os.path.join(self.coredir, namespec)
                cmd = self.gcore + ' "%s" %s' % (corename, spec['pid'])
                with os.popen(cmd) as m:
                    write('gcore output for %s:\n\n %s' % (
                        namespec, m.read()))
            write('%s is in RUNNING state, restarting' % namespec)
            try:
                self.rpc.supervisor.stopProcess(namespec)
            except xmlrpclib.Fault as e:
                write('Failed to stop process %s: %s' % (
                    namespec, e))

            try:
                self.rpc.supervisor.startProcess(namespec)
            except xmlrpclib.Fault as e:
                write('Failed to start process %s: %s' % (
                    namespec, e))
            else:
                write('%s restarted' % namespec)

        else:
            write('%s not in RUNNING state, NOT restarting' % namespec)


def main(argv=sys.argv):
    short_args="hp:at:c:b:s:m:g:d:eEn:"
    long_args=[
        "help",
        "program=",
        "any",
        "timeout=",
        "code=",
        "body=",
        "sendmail_program=",
        "email=",
        "gcore=",
        "coredir=",
        "eager",
        "not-eager",
        "name=",
        ]
    arguments = argv[1:]
    try:
        opts, args = getopt.getopt(arguments, short_args, long_args)
    except:
        usage()

    # check for -h must be done before positional args check
    for option, value in opts:
        if option in ('-h', '--help'):
            usage(exitstatus=0)

    if not args:
        usage()
    if len(args) > 1:
        usage()

    programs = []
    any = False
    sendmail = '/usr/sbin/sendmail -t -i'
    gcore = '/usr/bin/gcore -o'
    coredir = None
    eager = True
    email = None
    timeout = 10
    retry_time = 10
    statuses = []
    inbody = None
    name = None

    for option, value in opts:

        if option in ('-p', '--program'):
            programs.append(value)

        if option in ('-a', '--any'):
            any = True

        if option in ('-s', '--sendmail_program'):
            sendmail = value

        if option in ('-m', '--email'):
            email = value

        if option in ('-t', '--timeout'):
            timeout = int(value)

        if option in ('-c', '--code'):
            statuses.append(int(value))

        if option in ('-b', '--body'):
            inbody = value

        if option in ('-g', '--gcore'):
            gcore = value

        if option in ('-d', '--coredir'):
            coredir = value

        if option in ('-e', '--eager'):
            eager = True

        if option in ('-E', '--not-eager'):
            eager = False

        if option in ('-n', '--name'):
            name = value

    if not statuses:
        statuses = [200]

    url = arguments[-1]

    try:
        rpc = childutils.getRPCInterface(os.environ)
    except KeyError as e:
        if e.args[0] != 'SUPERVISOR_SERVER_URL':
            raise
        sys.stderr.write('httpok must be run as a supervisor event '
                         'listener\n')
        sys.stderr.flush()
        return

    prog = HTTPOk(rpc, programs, any, url, timeout, statuses, inbody, email,
                  sendmail, coredir, gcore, eager, retry_time, name)
    prog.runforever()

if __name__ == '__main__':
    main()


================================================
FILE: superlance/memmon.py
================================================
#!/usr/bin/env python
##############################################################################
#
# Copyright (c) 2007 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
#
##############################################################################

# A event listener meant to be subscribed to TICK_60 (or TICK_5)
# events, which restarts any processes that are children of
# supervisord that consume "too much" memory.  Performs horrendous
# screenscrapes of ps output.  Works on Linux and OS X (Tiger/Leopard)
# as far as I know.

# A supervisor config snippet that tells supervisor to use this script
# as a listener is below.
#
# [eventlistener:memmon]
# command=python memmon.py [options]
# events=TICK_60

doc = """\
memmon.py [-c] [-p processname=byte_size] [-g groupname=byte_size]
          [-a byte_size] [-s sendmail] [-m email_address]
          [-u uptime] [-n memmon_name]

Options:

-c -- Check against cumulative RSS. When calculating a process' RSS, also
      consider its child processes. With this option `memmon` will sum up
      the RSS of the process to be monitored and all its children.

-p -- specify a process_name=byte_size pair.  Restart the supervisor
      process named 'process_name' when it uses more than byte_size
      RSS.  If this process is in a group, it can be specified using
      the 'group_name:process_name' syntax.

-g -- specify a group_name=byte_size pair.  Restart any process in this group
      when it uses more than byte_size RSS.

-a -- specify a global byte_size.  Restart any child of the supervisord
      under which this runs if it uses more than byte_size RSS.

-s -- the sendmail command to use to send email
      (e.g. "/usr/sbin/sendmail -t -i").  Must be a command which accepts
      header and message data on stdin and sends mail.
      Default is "/usr/sbin/sendmail -t -i".

-m -- specify an email address.  The script will send mail to this
      address when any process is restarted.  If no email address is
      specified, email will not be sent.

-u -- optionally specify the minimum uptime in seconds for the process.
      if the process uptime is longer than this value, no email is sent
      (useful to only be notified if processes are restarted too often/early)

      seconds can be specified as plain integer values or a suffix-multiplied
      integer (e.g. 1m). Valid suffixes are m (minute), h (hour) and d (day).

-n -- optionally specify the name of the memmon process.  This name will
      be used in the email subject to identify which memmon process
      restarted the process.

The -p and -g options may be specified more than once, allowing for
specification of multiple groups and processes.

Any byte_size can be specified as a plain integer (10000) or a
suffix-multiplied integer (e.g. 1GB).  Valid suffixes are 'KB', 'MB'
and 'GB'.

A sample invocation:

memmon.py -p program1=200MB -p theprog:thegroup=100MB -g thegroup=100MB -a 1GB -s "/usr/sbin/sendmail -t -i" -m chrism@plope.com -n "Project 1"
"""

import getopt
import os
import sys
import time
from collections import namedtuple
from superlance.compat import maxint
from superlance.compat import xmlrpclib

from supervisor import childutils
from supervisor.datatypes import byte_size, SuffixMultiplier

def usage(exitstatus=255):
    print(doc)
    sys.exit(exitstatus)

def shell(cmd):
    with os.popen(cmd) as f:
        return f.read()

class Memmon:
    def __init__(self, cumulative, programs, groups, any, sendmail, email, email_uptime_limit, name, rpc=None):
        self.cumulative = cumulative
        self.programs = programs
        self.groups = groups
        self.any = any
        self.sendmail = sendmail
        self.email = email
        self.email_uptime_limit = email_uptime_limit
        self.name = name
        self.rpc = rpc
        self.stdin = sys.stdin
        self.stdout = sys.stdout
        self.stderr = sys.stderr
        self.pscommand = 'ps -orss= -p %s'
        self.pstreecommand = 'ps ax -o "pid= ppid= rss="'
        self.mailed = False # for unit tests

    def runforever(self, test=False):
        while 1:
            # we explicitly use self.stdin, self.stdout, and self.stderr
            # instead of sys.* so we can unit test this code
            headers, payload = childutils.listener.wait(self.stdin, self.stdout)

            if not headers['eventname'].startswith('TICK'):
                # do nothing with non-TICK events
                childutils.listener.ok(self.stdout)
                if test:
                    break
                continue

            status = []
            if self.programs:
                keys = sorted(self.programs.keys())
                status.append(
                    'Checking programs %s' % ', '.join(
                    [ '%s=%s' % (k, self.programs[k]) for k in keys ])
                    )

            if self.groups:
                keys = sorted(self.groups.keys())
                status.append(
                    'Checking groups %s' % ', '.join(
                    [ '%s=%s' % (k, self.groups[k]) for k in keys ])
                    )
            if self.any is not None:
                status.append('Checking any=%s' % self.any)

            self.stderr.write('\n'.join(status) + '\n')

            infos = self.rpc.supervisor.getAllProcessInfo()

            for info in infos:
                pid = info['pid']
                name = info['name']
                group = info['group']
                pname = '%s:%s' % (group, name)

                if not pid:
                    # ps throws an error in this case (for processes
                    # in standby mode, non-auto-started).
                    continue

                rss = self.calc_rss(pid)
                if rss is None:
                    # no such pid (deal with race conditions) or
                    # rss couldn't be calculated for other reasons
                    continue

                for n in name, pname:
                    if n in self.programs:
                        self.stderr.write('RSS of %s is %s\n' % (pname, rss))
                        if  rss > self.programs[name]:
                            self.restart(pname, rss)
                            continue

                if group in self.groups:
                    self.stderr.write('RSS of %s is %s\n' % (pname, rss))
                    if rss > self.groups[group]:
                        self.restart(pname, rss)
                        continue

                if self.any is not None:
                    self.stderr.write('RSS of %s is %s\n' % (pname, rss))
                    if rss > self.any:
                        self.restart(pname, rss)
                        continue

            self.stderr.flush()
            childutils.listener.ok(self.stdout)
            if test:
                break

    def restart(self, name, rss):
        info = self.rpc.supervisor.getProcessInfo(name)
        uptime = info['now'] - info['start'] #uptime in seconds
        self.stderr.write('Restarting %s\n' % name)
        try:
            self.rpc.supervisor.stopProcess(name)
        except xmlrpclib.Fault as e:
            msg = ('Failed to stop process %s (RSS %s), exiting: %s' %
                   (name, rss, e))
            self.stderr.write(str(msg))
            if self.email:
                subject = self.format_subject(
                    'failed to stop process %s, exiting' % name
                    )
                self.mail(self.email, subject, msg)
            raise

        try:
            self.rpc.supervisor.startProcess(name)
        except xmlrpclib.Fault as e:
            msg = ('Failed to start process %s after stopping it, '
                   'exiting: %s' % (name, e))
            self.stderr.write(str(msg))
            if self.email:
                subject = self.format_subject(
                    'failed to start process %s, exiting' % name
                )
                self.mail(self.email, subject, msg)
            raise

        if self.email and uptime <= self.email_uptime_limit:
            now = time.asctime()
            timezone = time.strftime('%Z')
            msg = (
                'memmon.py restarted the process named %s at %s %s because '
                'it was consuming too much memory (%s bytes RSS)' % (
                name, now, timezone, rss)
                )
            subject = self.format_subject(
                'process %s restarted' % name
                )
            self.mail(self.email, subject, msg)

    def format_subject(self, subject):
        if self.name is None:
            return 'memmon: %s' % subject
        else:
            return 'memmon [%s]: %s' % (self.name, subject)

    def calc_rss(self, pid):
        ProcInfo = namedtuple('ProcInfo', ['pid', 'ppid', 'rss'])

        def find_children(parent_pid, procs):
            children = []
            for proc in procs:
                pid, ppid, rss = proc
                if ppid == parent_pid:
                    children.append(proc)
                    children.extend(find_children(pid, procs))
            return children

        def cum_rss(pid, procs):
            parent_proc = [p for p in procs if p.pid == pid][0]
            children = find_children(pid, procs)
            tree = [parent_proc] + children
            total_rss = sum(map(int, [p.rss for p in tree]))
            return total_rss

        def get_all_process_infos(data):
            data = data.strip()
            procs = []
            for line in data.splitlines():
                pid, ppid, rss = map(int, line.split())
                procs.append(ProcInfo(pid=pid, ppid=ppid, rss=rss))
            return procs

        if self.cumulative:
            data = shell(self.pstreecommand)
            procs = get_all_process_infos(data)

            try:
                rss = cum_rss(pid, procs)
            except (ValueError, IndexError):
                # Could not determine cumulative RSS
                return None

        else:
            data = shell(self.pscommand % pid)
            if not data:
                # no such pid (deal with race conditions)
                return None

            try:
                rss = data.lstrip().rstrip()
                rss = int(rss)
            except ValueError:
                # line doesn't contain any data, or rss cant be intified
                return None

        rss = rss * 1024  # rss is in KB
        return rss

    def mail(self, email, subject, msg):
        body = 'To: %s\n' % self.email
        body += 'Subject: %s\n' % subject
        body += '\n'
        body += msg
        with os.popen(self.sendmail, 'w') as m:
            m.write(body)
        self.mailed = body

def parse_namesize(option, value):
    try:
        name, size = value.split('=')
    except ValueError:
        print('Unparseable value %r for %r' % (value, option))
        usage()
    size = parse_size(option, size)
    return name, size

def parse_size(option, value):
    try:
        size = byte_size(value)
    except:
        print('Unparseable byte_size in %r for %r' % (value, option))
        usage()

    return size

seconds_size = SuffixMultiplier({'s': 1,
                                 'm': 60,
                                 'h': 60 * 60,
                                 'd': 60 * 60 * 24
                                 })

def parse_seconds(option, value):
    try:
        seconds = seconds_size(value)
    except:
        print('Unparseable value for time in %r for %s' % (value, option))
        usage()
    return seconds

help_request = object()  # returned from memmon_from_args to indicate --help

def memmon_from_args(arguments):
    short_args = "hcp:g:a:s:m:n:u:"
    long_args = [
        "help",
        "cumulative",
        "program=",
        "group=",
        "any=",
        "sendmail_program=",
        "email=",
        "uptime=",
        "name=",
        ]

    if not arguments:
        return None
    try:
        opts, args = getopt.getopt(arguments, short_args, long_args)
    except:
        return None

    cumulative = False
    programs = {}
    groups = {}
    any = None
    sendmail = '/usr/sbin/sendmail -t -i'
    email = None
    uptime_limit = maxint
    name = None

    for option, value in opts:

        if option in ('-h', '--help'):
            return help_request

        if option in ('-c', '--cumulative'):
            cumulative = True

        if option in ('-p', '--program'):
            name, size = parse_namesize(option, value)
            programs[name] = size

        if option in ('-g', '--group'):
            name, size = parse_namesize(option, value)
            groups[name] = size

        if option in ('-a', '--any'):
            size = parse_size(option, value)
            any = size

        if option in ('-s', '--sendmail_program'):
            sendmail = value

        if option in ('-m', '--email'):
            email = value

        if option in ('-u', '--uptime'):
            uptime_limit = parse_seconds(option, value)

        if option in ('-n', '--name'):
            name = value

    memmon = Memmon(cumulative=cumulative,
                    programs=programs,
                    groups=groups,
                    any=any,
                    sendmail=sendmail,
                    email=email,
                    email_uptime_limit=uptime_limit,
                    name=name)
    return memmon

def main():
    memmon = memmon_from_args(sys.argv[1:])
    if memmon is help_request:  # --help
        usage(exitstatus=0)
    elif memmon is None:  # something went wrong
        usage()
    memmon.rpc = childutils.getRPCInterface(os.environ)
    memmon.runforever()

if __name__ == '__main__':
    main()


================================================
FILE: superlance/process_state_email_monitor.py
================================================
#!/usr/bin/env python -u
##############################################################################
#
# Copyright (c) 2007 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 copy
import optparse
import os
import smtplib
import sys

from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
from superlance.process_state_monitor import ProcessStateMonitor

doc = """\
Base class for common functionality when monitoring process state changes
and sending email notification
"""

class ProcessStateEmailMonitor(ProcessStateMonitor):
    COMMASPACE = ', '

    @classmethod
    def _get_opt_parser(cls):
        parser = optparse.OptionParser()
        parser.add_option("-i", "--interval", dest="interval", type="float", default=1.0,
                        help="batch interval in minutes (defaults to 1 minute)")
        parser.add_option("-t", "--toEmail", dest="to_emails",
                        help="destination email address(es) - comma separated")
        parser.add_option("-f", "--fromEmail", dest="from_email",
                        help="source email address")
        parser.add_option("-s", "--subject", dest="subject",
                        help="email subject")
        parser.add_option("-H", "--smtpHost", dest="smtp_host", default="localhost",
                        help="SMTP server hostname or address")
        parser.add_option("-e", "--tickEvent", dest="eventname", default="TICK_60",
                        help="TICK event name (defaults to TICK_60)")
        parser.add_option("-u", "--userName", dest="smtp_user", default="",
                        help="SMTP server user name (defaults to nothing)")
        parser.add_option("-p", "--password", dest="smtp_password", default="",
                        help="SMTP server password (defaults to nothing)")
        parser.add_option("--tls", dest="use_tls", action="store_true", default=False,
                        help="Use Transport Layer Security (TLS), default to False")
        return parser

    @classmethod
    def parse_cmd_line_options(cls):
        parser = cls._get_opt_parser()
        (options, args) = parser.parse_args()
        return options

    @classmethod
    def validate_cmd_line_options(cls, options):
        parser = cls._get_opt_parser()
        if not options.to_emails:
            parser.print_help()
            sys.exit(1)
        if not options.from_email:
            parser.print_help()
            sys.exit(1)

        validated = copy.copy(options)
        validated.to_emails = [x.strip() for x in options.to_emails.split(",")]
        return validated

    @classmethod
    def get_cmd_line_options(cls):
        return cls.validate_cmd_line_options(cls.parse_cmd_line_options())

    @classmethod
    def create_from_cmd_line(cls):
        options = cls.get_cmd_line_options()

        if not 'SUPERVISOR_SERVER_URL' in os.environ:
            sys.stderr.write('Must run as a supervisor event listener\n')
            sys.exit(1)

        return cls(**options.__dict__)

    def __init__(self, **kwargs):
        ProcessStateMonitor.__init__(self, **kwargs)

        self.from_email = kwargs['from_email']
        self.to_emails = kwargs['to_emails']
        self.subject = kwargs.get('subject')
        self.smtp_host = kwargs.get('smtp_host', 'localhost')
        self.smtp_user = kwargs.get('smtp_user')
        self.smtp_password = kwargs.get('smtp_password')
        self.use_tls = kwargs.get('use_tls')
        self.digest_len = 76

    def send_batch_notification(self):
        email = self.get_batch_email()
        if email:
            self.send_email(email)
            self.log_email(email)

    def log_email(self, email):
        email_for_log = copy.copy(email)
        email_for_log['to'] = self.COMMASPACE.join(email['to'])
        if len(email_for_log['body']) > self.digest_len:
            email_for_log['body'] = '%s...' % email_for_log['body'][:self.digest_len]
        self.write_stderr("Sending notification email:\nTo: %(to)s\n\
From: %(from)s\nSubject: %(subject)s\nBody:\n%(body)s\n" % email_for_log)

    def get_batch_email(self):
        if len(self.batchmsgs):
            return {
                'to': self.to_emails,
                'from': self.from_email,
                'subject': self.subject,
                'body': '\n'.join(self.get_batch_msgs()),
            }
        return None

    def send_email(self, email):
        msg = MIMEText(email['body'])
        if self.subject:
          msg['Subject'] = email['subject']
        msg['From'] = email['from']
        msg['To'] = self.COMMASPACE.join(email['to'])
        msg['Date'] = formatdate()
        msg['Message-ID'] = make_msgid()

        try:
            self.send_smtp(msg, email['to'])
        except Exception as e:
            self.write_stderr("Error sending email: %s\n" % e)

    def send_smtp(self, mime_msg, to_emails):
        s = smtplib.SMTP(self.smtp_host)
        try:
            if self.smtp_user and self.smtp_password:
                if self.use_tls:
                    s.starttls()
                s.login(self.smtp_user, self.smtp_password)
            s.sendmail(mime_msg['From'], to_emails, mime_msg.as_string())
        except:
            s.quit()
            raise
        s.quit()



================================================
FILE: superlance/process_state_monitor.py
================================================
#!/usr/bin/env python -u
##############################################################################
#
# Copyright (c) 2007 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
#
##############################################################################
doc = """\
Base class for common functionality when monitoring process state changes
"""

import sys

from supervisor import childutils

class ProcessStateMonitor:

    # In child class, define a list of events to monitor
    process_state_events = []

    def __init__(self, **kwargs):
        self.interval = kwargs.get('interval', 1.0)

        self.debug = kwargs.get('debug', False)
        self.stdin = kwargs.get('stdin', sys.stdin)
        self.stdout = kwargs.get('stdout', sys.stdout)
        self.stderr = kwargs.get('stderr', sys.stderr)
        self.eventname = kwargs.get('eventname', 'TICK_60')
        self.tickmins = self._get_tick_mins(self.eventname)

        self.batchmsgs = []
        self.batchmins = 0.0

    def _get_tick_mins(self, eventname):
        return float(self._get_tick_secs(eventname))/60.0

    def _get_tick_secs(self, eventname):
        self._validate_tick_name(eventname)
        return int(eventname.split('_')[1])

    def _validate_tick_name(self, eventname):
        if not eventname.startswith('TICK_'):
            raise ValueError("Invalid TICK event name: %s" % eventname)

    def run(self):
        while 1:
            hdrs, payload = childutils.listener.wait(self.stdin, self.stdout)
            self.handle_event(hdrs, payload)
            childutils.listener.ok(self.stdout)

    def handle_event(self, headers, payload):
        if headers['eventname'] in self.process_state_events:
            self.handle_process_state_change_event(headers, payload)
        elif headers['eventname'] == self.eventname:
            self.handle_tick_event(headers, payload)

    def handle_process_state_change_event(self, headers, payload):
        msg = self.get_process_state_change_msg(headers, payload)
        if msg:
            self.write_stderr('%s\n' % msg)
            self.batchmsgs.append(msg)

    """
    Override this method in child classes to customize messaging
    """
    def get_process_state_change_msg(self, headers, payload):
        return None

    def handle_tick_event(self, headers, payload):
        self.batchmins += self.tickmins
        if self.batchmins >= self.interval:
            self.send_batch_notification()
            self.clear_batch()

    """
    Override this method in child classes to send notification
    """
    def send_batch_notification(self):
        pass

    def get_batch_minutes(self):
        return self.batchmins

    def get_batch_msgs(self):
        return self.batchmsgs

    def clear_batch(self):
        self.batchmins = 0.0
        self.batchmsgs = []

    def write_stderr(self, msg):
        self.stderr.write(msg)
        self.stderr.flush()


================================================
FILE: superlance/tests/__init__.py
================================================


================================================
FILE: superlance/tests/dummy.py
================================================
import time
from supervisor.process import ProcessStates

_NOW = time.time()

class DummyRPCServer:
    def __init__(self):
        self.supervisor = DummySupervisorRPCNamespace()
        self.system = DummySystemRPCNamespace()

class DummyResponse:
    status = 200
    reason = 'OK'
    body = 'OK'
    def read(self):
        return self.body

class DummySystemRPCNamespace:
    pass

class DummySupervisorRPCNamespace:
    _restartable = True
    _restarted = False
    _shutdown = False
    _readlog_error = False


    all_process_info = [
        {
        'name':'foo',
        'group':'foo',
        'pid':11,
        'state':ProcessStates.RUNNING,
        'statename':'RUNNING',
        'start':_NOW - 100,
        'stop':0,
        'spawnerr':'',
        'now':_NOW,
        'description':'foo description',
        },
        {
        'name':'bar',
        'group':'bar',
        'pid':12,
        'state':ProcessStates.FATAL,
        'statename':'FATAL',
        'start':_NOW - 100,
        'stop':_NOW - 50,
        'spawnerr':'screwed',
        'now':_NOW,
        'description':'bar description',
        },
        {
        'name':'baz_01',
        'group':'baz',
        'pid':12,
        'state':ProcessStates.STOPPED,
        'statename':'STOPPED',
        'start':_NOW - 100,
        'stop':_NOW - 25,
        'spawnerr':'',
        'now':_NOW,
        'description':'baz description',
        },
        ]

    def getAllProcessInfo(self):
        return self.all_process_info

    def getProcessInfo(self, name):
        for info in self.all_process_info:
            if info['name'] == name or name == '%s:%s' %(info['group'], info['name']):
                return info
        return None

    def startProcess(self, name):
        from supervisor import xmlrpc
        from superlance.compat import xmlrpclib
        if name.endswith('SPAWN_ERROR'):
            raise xmlrpclib.Fault(xmlrpc.Faults.SPAWN_ERROR, 'SPAWN_ERROR')
        return True

    def stopProcess(self, name):
        from supervisor import xmlrpc
        from superlance.compat import xmlrpclib
        if name == 'BAD_NAME:BAD_NAME':
            raise xmlrpclib.Fault(xmlrpc.Faults.BAD_NAME, 'BAD_NAME:BAD_NAME')
        if name.endswith('FAILED'):
            raise xmlrpclib.Fault(xmlrpc.Faults.FAILED, 'FAILED')
        return True



================================================
FILE: superlance/tests/test_crashmail.py
================================================
import unittest
from superlance.compat import StringIO

class CrashMailTests(unittest.TestCase):
    def _getTargetClass(self):
        from superlance.crashmail import CrashMail
        return CrashMail

    def _makeOne(self, *opts):
        return self._getTargetClass()(*opts)

    def setUp(self):
        import tempfile
        self.tempdir = tempfile.mkdtemp()

    def tearDown(self):
        import shutil
        shutil.rmtree(self.tempdir)

    def _makeOnePopulated(self, programs, any, response=None):
        import os
        sendmail = 'cat - > %s' % os.path.join(self.tempdir, 'email.log')
        email = 'chrism@plope.com'
        header = '[foo]'
        prog = self._makeOne(programs, any, email, sendmail, header)
        prog.stdin = StringIO()
        prog.stdout = StringIO()
        prog.stderr = StringIO()
        return prog

    def test_runforever_not_process_state_exited(self):
        programs = {'foo':0, 'bar':0, 'baz_01':0 }
        any = None
        prog = self._makeOnePopulated(programs, any)
        prog.stdin.write('eventname:PROCESS_STATE len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        self.assertEqual(prog.stderr.getvalue(), 'non-exited event\n')

    def test_runforever_expected_exit(self):
        programs = ['foo']
        any = None
        prog = self._makeOnePopulated(programs, any)
        payload=('expected:1 processname:foo groupname:bar '
                 'from_state:RUNNING pid:1')
        prog.stdin.write(
            'eventname:PROCESS_STATE_EXITED len:%s\n' % len(payload))
        prog.stdin.write(payload)
        prog.stdin.seek(0)
        prog.runforever(test=True)
        self.assertEqual(prog.stderr.getvalue(), 'expected exit\n')

    def test_runforever_unexpected_exit(self):
        programs = ['foo']
        any = None
        prog = self._makeOnePopulated(programs, any)
        payload=('expected:0 processname:foo groupname:bar '
                 'from_state:RUNNING pid:1')
        prog.stdin.write(
            'eventname:PROCESS_STATE_EXITED len:%s\n' % len(payload))
        prog.stdin.write(payload)
        prog.stdin.seek(0)
        prog.runforever(test=True)
        output = prog.stderr.getvalue()
        lines = output.split('\n')
        self.assertEqual(lines[0], 'unexpected exit, mailing')
        self.assertEqual(lines[1], 'Mailed:')
        self.assertEqual(lines[2], '')
        self.assertEqual(lines[3], 'To: chrism@plope.com')
        self.assertTrue('Subject: [foo]: foo crashed at' in lines[4])
        self.assertEqual(lines[5], '')
        self.assertTrue(
            'Process foo in group bar exited unexpectedly' in lines[6])
        import os
        f = open(os.path.join(self.tempdir, 'email.log'), 'r')
        mail = f.read()
        f.close()
        self.assertTrue(
            'Process foo in group bar exited unexpectedly' in mail)


================================================
FILE: superlance/tests/test_crashmailbatch.py
================================================
import unittest
try: # pragma: no cover
    from unittest.mock import Mock
except ImportError: # pragma: no cover
    from mock import Mock
from superlance.compat import StringIO

class CrashMailBatchTests(unittest.TestCase):
    from_email = 'testFrom@blah.com'
    to_emails = ('testTo@blah.com')
    subject = 'Test Alert'
    unexpected_err_msg = 'Process bar:foo (pid 58597) died unexpectedly'

    def _get_target_class(self):
        from superlance.crashmailbatch import CrashMailBatch
        return CrashMailBatch

    def _make_one_mocked(self, **kwargs):
        kwargs['stdin'] = StringIO()
        kwargs['stdout'] = StringIO()
        kwargs['stderr'] = StringIO()
        kwargs['from_email'] = kwargs.get('from_email', self.from_email)
        kwargs['to_emails'] = kwargs.get('to_emails', self.to_emails)
        kwargs['subject'] = kwargs.get('subject', self.subject)

        obj = self._get_target_class()(**kwargs)
        obj.send_email = Mock()
        return obj

    def get_process_exited_event(self, pname, gname, expected):
        headers = {
            'ver': '3.0', 'poolserial': '7', 'len': '71',
            'server': 'supervisor', 'eventname': 'PROCESS_STATE_EXITED',
            'serial': '7', 'pool': 'checkmailbatch',
        }
        payload = 'processname:%s groupname:%s from_state:RUNNING expected:%d \
pid:58597' % (pname, gname, expected)
        return (headers, payload)

    def test_get_process_state_change_msg_expected(self):
        crash = self._make_one_mocked()
        hdrs, payload = self.get_process_exited_event('foo', 'bar', 1)
        self.assertEqual(None, crash.get_process_state_change_msg(hdrs, payload))

    def test_get_process_state_change_msg_unexpected(self):
        crash = self._make_one_mocked()
        hdrs, payload = self.get_process_exited_event('foo', 'bar', 0)
        msg = crash.get_process_state_change_msg(hdrs, payload)
        self.assertTrue(self.unexpected_err_msg in msg)

    def test_handle_event_exit_expected(self):
        crash = self._make_one_mocked()
        hdrs, payload = self.get_process_exited_event('foo', 'bar', 1)
        crash.handle_event(hdrs, payload)
        self.assertEqual([], crash.get_batch_msgs())
        self.assertEqual('', crash.stderr.getvalue())

    def test_handle_event_exit_unexpected(self):
        crash = self._make_one_mocked()
        hdrs, payload = self.get_process_exited_event('foo', 'bar', 0)
        crash.handle_event(hdrs, payload)
        msgs = crash.get_batch_msgs()
        self.assertEqual(1, len(msgs))
        self.assertTrue(self.unexpected_err_msg in msgs[0])
        self.assertTrue(self.unexpected_err_msg in crash.stderr.getvalue())

    def test_sets_default_subject_when_None(self):
        crash = self._make_one_mocked(subject=None) # see issue #109
        self.assertEqual(crash.subject, "Crash alert from supervisord")


================================================
FILE: superlance/tests/test_crashsms.py
================================================
import unittest

from .test_crashmailbatch import CrashMailBatchTests

class CrashSMSTests(CrashMailBatchTests):
    subject = None
    unexpected_err_msg = '[bar:foo](58597) exited unexpectedly'

    def _get_target_class(self):
        from superlance.crashsms import CrashSMS
        return CrashSMS

    def test_sets_default_subject_when_None(self):
        crash = self._make_one_mocked(subject=None)
        self.assertEqual(crash.subject, self.subject)


================================================
FILE: superlance/tests/test_fatalmailbatch.py
================================================
import unittest
try: # pragma: no cover
    from unittest.mock import Mock
except ImportError: # pragma: no cover
    from mock import Mock
from superlance.compat import StringIO

class FatalMailBatchTests(unittest.TestCase):
    from_email = 'testFrom@blah.com'
    to_emails = ('testTo@blah.com')
    subject = 'Test Alert'
    unexpected_err_msg = 'Process bar:foo failed to start too many times'

    def _get_target_class(self):
        from superlance.fatalmailbatch import FatalMailBatch
        return FatalMailBatch

    def _make_one_mocked(self, **kwargs):
        kwargs['stdin'] = StringIO()
        kwargs['stdout'] = StringIO()
        kwargs['stderr'] = StringIO()
        kwargs['from_email'] = kwargs.get('from_email', self.from_email)
        kwargs['to_emails'] = kwargs.get('to_emails', self.to_emails)
        kwargs['subject'] = kwargs.get('subject', self.subject)

        obj = self._get_target_class()(**kwargs)
        obj.send_email = Mock()
        return obj

    def get_process_fatal_event(self, pname, gname):
        headers = {
            'ver': '3.0', 'poolserial': '7', 'len': '71',
            'server': 'supervisor', 'eventname': 'PROCESS_STATE_FATAL',
            'serial': '7', 'pool': 'checkmailbatch',
        }
        payload = 'processname:%s groupname:%s from_state:BACKOFF' \
                % (pname, gname)
        return (headers, payload)

    def test_get_process_state_change_msg(self):
        crash = self._make_one_mocked()
        hdrs, payload = self.get_process_fatal_event('foo', 'bar')
        msg = crash.get_process_state_change_msg(hdrs, payload)
        self.assertTrue(self.unexpected_err_msg in msg)

    def test_sets_default_subject_when_None(self):
        crash = self._make_one_mocked(subject=None) # see issue #109
        self.assertEqual(crash.subject, "Fatal start alert from supervisord")


================================================
FILE: superlance/tests/test_httpok.py
================================================
import socket
import time
import unittest
from superlance.compat import StringIO
from supervisor.process import ProcessStates
from superlance.tests.dummy import DummyResponse
from superlance.tests.dummy import DummyRPCServer
from superlance.tests.dummy import DummySupervisorRPCNamespace

_NOW = time.time()

_FAIL = [ {
        'name':'FAILED',
        'group':'foo',
        'pid':11,
        'state':ProcessStates.RUNNING,
        'statename':'RUNNING',
        'start':_NOW - 100,
        'stop':0,
        'spawnerr':'',
        'now':_NOW,
        'description':'foo description',
        },
{
        'name':'SPAWN_ERROR',
        'group':'foo',
        'pid':11,
        'state':ProcessStates.RUNNING,
        'statename':'RUNNING',
        'start':_NOW - 100,
        'stop':0,
        'spawnerr':'',
        'now':_NOW,
        'description':'foo description',
        },]

def make_connection(response, exc=None):
    class TestConnection:
        def __init__(self, hostport):
            self.hostport = hostport

        def request(self, method, path, headers):
            if exc:
                if exc is True:
                    raise ValueError('foo')
                else:
                    raise exc.pop()
            self.method = method
            self.path = path
            self.headers = headers

        def getresponse(self):
            return response

    return TestConnection

class HTTPOkTests(unittest.TestCase):
    def _getTargetClass(self):
        from superlance.httpok import HTTPOk
        return HTTPOk

    def _makeOne(self, *args, **kwargs):
        return self._getTargetClass()(*args, **kwargs)

    def _makeOnePopulated(self, programs, any=None, statuses=None, inbody=None,
                          eager=True, gcore=None, coredir=None,
                          response=None, exc=None, name=None,
                          timeout=10, retry_time=0):
        if statuses is None:
            statuses = [200]
        if response is None:
            response = DummyResponse()
        httpok = self._makeOne(
            programs=programs,
            any=any,
            statuses=statuses,
            inbody=inbody,
            eager=eager,
            coredir=coredir,
            gcore=gcore,
            name=name,
            rpc=DummyRPCServer(),
            url='http://foo/bar',
            timeout=timeout,
            email='chrism@plope.com',
            sendmail='cat - > /dev/null',
            retry_time=retry_time,
            )
        httpok.stdin = StringIO()
        httpok.stdout = StringIO()
        httpok.stderr = StringIO()
        httpok.connclass = make_connection(response, exc=exc)
        return httpok

    def test_listProcesses_no_programs(self):
        programs = []
        any = None
        prog = self._makeOnePopulated(programs, any)
        specs = list(prog.listProcesses())
        self.assertEqual(len(specs), 0)

    def test_listProcesses_w_RUNNING_programs_default_state(self):
        programs = ['foo']
        any = None
        prog = self._makeOnePopulated(programs, any)
        specs = list(prog.listProcesses())
        self.assertEqual(len(specs), 1)
        self.assertEqual(specs[0],
                         DummySupervisorRPCNamespace.all_process_info[0])

    def test_listProcesses_w_nonRUNNING_programs_default_state(self):
        programs = ['bar']
        any = None
        prog = self._makeOnePopulated(programs, any)
        specs = list(prog.listProcesses())
        self.assertEqual(len(specs), 1)
        self.assertEqual(specs[0],
                         DummySupervisorRPCNamespace.all_process_info[1])

    def test_listProcesses_w_nonRUNNING_programs_RUNNING_state(self):
        programs = ['bar']
        any = None
        prog = self._makeOnePopulated(programs, any)
        specs = list(prog.listProcesses(ProcessStates.RUNNING))
        self.assertEqual(len(specs), 0, (prog.programs, specs))

    def test_runforever_eager_notatick(self):
        programs = {'foo':0, 'bar':0, 'baz_01':0 }
        any = None
        prog = self._makeOnePopulated(programs, any)
        prog.stdin.write('eventname:NOTATICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        self.assertEqual(prog.stderr.getvalue(), '')

    def test_runforever_doesnt_act_if_status_is_expected(self):
        statuses = [200, 201]
        for status in statuses:
            response = DummyResponse()
            response.status = status # expected
            prog = self._makeOnePopulated(
                programs=['foo'],
                statuses=statuses,
                response=response,
                )
            prog.stdin.write('eventname:TICK len:0\n')
            prog.stdin.seek(0)
            prog.runforever(test=True)
            # status is expected so there should be no output
            self.assertEqual('', prog.stderr.getvalue())

    def test_runforever_acts_if_status_is_unexpected(self):
        statuses = [200, 201]
        response = DummyResponse()
        response.status = 500 # unexpected
        response.reason = 'Internal Server Error'
        prog = self._makeOnePopulated(
            programs=['foo'],
            statuses=[statuses],
            response=response,
            )
        prog.stdin.write('eventname:TICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        lines = prog.stderr.getvalue().split('\n')
        self.assertTrue('Subject: httpok: http://foo/bar: '
                        'bad status returned' in lines)
        self.assertTrue('status contacting http://foo/bar: '
                        '500 Internal Server Error' in lines)

    def test_runforever_doesnt_act_if_inbody_is_present(self):
        response = DummyResponse()
        response.body = 'It works'
        prog = self._makeOnePopulated(
            programs=['foo'],
            statuses=[response.status],
            response=response,
            inbody='works',
            )
        prog.stdin.write('eventname:TICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        # body is expected so there should be no output
        self.assertEqual('', prog.stderr.getvalue())

    def test_runforever_acts_if_inbody_isnt_present(self):
        response = DummyResponse()
        response.body = 'Some kind of error'
        prog = self._makeOnePopulated(
            programs=['foo'],
            statuses=[response.status],
            response=response,
            inbody="works",
            )
        prog.stdin.write('eventname:TICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        lines = prog.stderr.getvalue().split('\n')
        self.assertTrue('Subject: httpok: http://foo/bar: '
                        'bad body returned' in lines)

    def test_runforever_eager_error_on_request_some(self):
        programs = ['foo', 'bar', 'baz_01', 'notexisting']
        any = None
        prog = self._makeOnePopulated(programs, any, exc=True)
        prog.stdin.write('eventname:TICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        lines = prog.stderr.getvalue().split('\n')
        self.assertEqual(lines[0],
                         ("Restarting selected processes ['foo', 'bar', "
                          "'baz_01', 'notexisting']")
                         )
        self.assertEqual(lines[1], 'foo is in RUNNING state, restarting')
        self.assertEqual(lines[2], 'foo restarted')
        self.assertEqual(lines[3], 'bar not in RUNNING state, NOT restarting')
        self.assertEqual(lines[4],
                         'baz:baz_01 not in RUNNING state, NOT restarting')
        self.assertEqual(lines[5],
          "Programs not restarted because they did not exist: ['notexisting']")
        mailed = prog.mailed.split('\n')
        self.assertEqual(len(mailed), 12)
        self.assertEqual(mailed[0], 'To: chrism@plope.com')
        self.assertEqual(mailed[1],
                    'Subject: httpok: http://foo/bar: bad status returned')

    def test_runforever_eager_error_on_request_any(self):
        programs = []
        any = True
        prog = self._makeOnePopulated(programs, any, exc=True)
        prog.stdin.write('eventname:TICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        lines = prog.stderr.getvalue().split('\n')
        self.assertEqual(lines[0], 'Restarting all running processes')
        self.assertEqual(lines[1], 'foo is in RUNNING state, restarting')
        self.assertEqual(lines[2], 'foo restarted')
        self.assertEqual(lines[3], 'bar not in RUNNING state, NOT restarting')
        self.assertEqual(lines[4],
                         'baz:baz_01 not in RUNNING state, NOT restarting')
        mailed = prog.mailed.split('\n')
        self.assertEqual(len(mailed), 11)
        self.assertEqual(mailed[0], 'To: chrism@plope.com')
        self.assertEqual(mailed[1],
                    'Subject: httpok: http://foo/bar: bad status returned')

    def test_runforever_eager_error_on_process_stop(self):
        programs = ['FAILED']
        any = False
        prog = self._makeOnePopulated(programs, any, exc=True)
        prog.rpc.supervisor.all_process_info = _FAIL
        prog.stdin.write('eventname:TICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        lines = prog.stderr.getvalue().split('\n')
        self.assertEqual(lines[0], "Restarting selected processes ['FAILED']")
        self.assertEqual(lines[1], 'foo:FAILED is in RUNNING state, restarting')
        self.assertEqual(lines[2],
                    "Failed to stop process foo:FAILED: <Fault 30: 'FAILED'>")
        self.assertEqual(lines[3], 'foo:FAILED restarted')
        mailed = prog.mailed.split('\n')
        self.assertEqual(len(mailed), 10)
        self.assertEqual(mailed[0], 'To: chrism@plope.com')
        self.assertEqual(mailed[1],
                    'Subject: httpok: http://foo/bar: bad status returned')

    def test_runforever_eager_error_on_process_start(self):
        programs = ['SPAWN_ERROR']
        any = False
        prog = self._makeOnePopulated(programs, any, exc=True)
        prog.rpc.supervisor.all_process_info = _FAIL
        prog.stdin.write('eventname:TICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        lines = prog.stderr.getvalue().split('\n')
        self.assertEqual(lines[0],
                         "Restarting selected processes ['SPAWN_ERROR']")
        self.assertEqual(lines[1],
                         'foo:SPAWN_ERROR is in RUNNING state, restarting')
        self.assertEqual(lines[2],
           "Failed to start process foo:SPAWN_ERROR: <Fault 50: 'SPAWN_ERROR'>")
        mailed = prog.mailed.split('\n')
        self.assertEqual(len(mailed), 9)
        self.assertEqual(mailed[0], 'To: chrism@plope.com')
        self.assertEqual(mailed[1],
                    'Subject: httpok: http://foo/bar: bad status returned')

    def test_runforever_eager_gcore(self):
        programs = ['foo', 'bar', 'baz_01', 'notexisting']
        any = None
        prog = self._makeOnePopulated(programs, any, exc=True, gcore="true",
                                      coredir="/tmp")
        prog.stdin.write('eventname:TICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        lines = prog.stderr.getvalue().split('\n')
        self.assertEqual(lines[0],
                         ("Restarting selected processes ['foo', 'bar', "
                          "'baz_01', 'notexisting']")
                         )
        self.assertEqual(lines[1], 'gcore output for foo:')
        self.assertEqual(lines[2], '')
        self.assertEqual(lines[3], ' ')
        self.assertEqual(lines[4], 'foo is in RUNNING state, restarting')
        self.assertEqual(lines[5], 'foo restarted')
        self.assertEqual(lines[6], 'bar not in RUNNING state, NOT restarting')
        self.assertEqual(lines[7],
                         'baz:baz_01 not in RUNNING state, NOT restarting')
        self.assertEqual(lines[8],
          "Programs not restarted because they did not exist: ['notexisting']")
        mailed = prog.mailed.split('\n')
        self.assertEqual(len(mailed), 15)
        self.assertEqual(mailed[0], 'To: chrism@plope.com')
        self.assertEqual(mailed[1],
                    'Subject: httpok: http://foo/bar: bad status returned')

    def test_runforever_not_eager_none_running(self):
        programs = ['bar', 'baz_01']
        any = None
        prog = self._makeOnePopulated(programs, any, exc=True, gcore="true",
                                      coredir="/tmp", eager=False)
        prog.stdin.write('eventname:TICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        lines = [x for x in prog.stderr.getvalue().split('\n') if x]
        self.assertEqual(len(lines), 0, lines)
        self.assertFalse('mailed' in prog.__dict__)

    def test_runforever_not_eager_running(self):
        programs = ['foo', 'bar']
        any = None
        prog = self._makeOnePopulated(programs, any, exc=True, eager=False)
        prog.stdin.write('eventname:TICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        lines = [x for x in prog.stderr.getvalue().split('\n') if x]
        self.assertEqual(lines[0],
                         ("Restarting selected processes ['foo', 'bar']")
                         )
        self.assertEqual(lines[1], 'foo is in RUNNING state, restarting')
        self.assertEqual(lines[2], 'foo restarted')
        self.assertEqual(lines[3], 'bar not in RUNNING state, NOT restarting')
        mailed = prog.mailed.split('\n')
        self.assertEqual(len(mailed), 10)
        self.assertEqual(mailed[0], 'To: chrism@plope.com')
        self.assertEqual(mailed[1],
                    'Subject: httpok: http://foo/bar: bad status returned')

    def test_runforever_honor_timeout_on_connrefused(self):
        programs = ['foo', 'bar']
        any = None
        error = socket.error()
        error.errno = 111
        prog = self._makeOnePopulated(programs, any, exc=[error], eager=False)
        prog.stdin.write('eventname:TICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        self.assertEqual(prog.stderr.getvalue(), '')
        self.assertEqual(prog.stdout.getvalue(), 'READY\nRESULT 2\nOK')

    def test_runforever_connrefused_error(self):
        programs = ['foo', 'bar']
        any = None
        error = socket.error()
        error.errno = 111
        prog = self._makeOnePopulated(programs, any,
            exc=[error for x in range(100)], eager=False)
        prog.stdin.write('eventname:TICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        lines = [x for x in prog.stderr.getvalue().split('\n') if x]
        self.assertEqual(lines[0],
                         ("Restarting selected processes ['foo', 'bar']")
                         )
        self.assertEqual(lines[1], 'foo is in RUNNING state, restarting')
        self.assertEqual(lines[2], 'foo restarted')
        self.assertEqual(lines[3], 'bar not in RUNNING state, NOT restarting')
        mailed = prog.mailed.split('\n')
        self.assertEqual(len(mailed), 10)
        self.assertEqual(mailed[0], 'To: chrism@plope.com')
        self.assertEqual(mailed[1],
                    'Subject: httpok: http://foo/bar: bad status returned')

    def test_bug_110(self):
        error = socket.error()
        error.errno = 111
        prog = self._makeOnePopulated(programs=['foo'], any=None,
            exc=[error for x in range(100)], eager=False,
                                      timeout=1, retry_time=10)
        prog.stdin.write('eventname:TICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        lines = [x for x in prog.stderr.getvalue().split('\n') if x]
        self.assertEqual(lines[0],
                         ("Restarting selected processes ['foo']")
                         )
        self.assertEqual(lines[1], 'foo is in RUNNING state, restarting')
        self.assertEqual(lines[2], 'foo restarted')

    def test_subject_no_name(self):
        """set the name to None to check if subject formats to:
        httpok: %(subject)s
        """
        prog = self._makeOnePopulated(
            programs=['foo', 'bar'],
            any=None,
            eager=False,
            exc=[ValueError('this causes status to be None')],
            name=None,
            )
        prog.stdin.write('eventname:TICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        mailed = prog.mailed.split('\n')
        self.assertEqual(mailed[1],
          'Subject: httpok: http://foo/bar: bad status returned')

    def test_subject_with_name(self):
        """set the name to a string to check if subject formats to:
        httpok [%(name)s]: %(subject)s
        """
        prog = self._makeOnePopulated(
            programs=['foo', 'bar'],
            any=None,
            eager=False,
            exc=[ValueError('this causes status to be None')],
            name='thinko',
            )
        prog.stdin.write('eventname:TICK len:0\n')
        prog.stdin.seek(0)
        prog.runforever(test=True)
        mailed = prog.mailed.split('\n')
        self.assertEqual(mailed[1],
          'Subject: httpok [thinko]: http://foo/bar: bad status returned')


================================================
FILE: superlance/tests/test_memmon.py
================================================
# -*- coding: utf-8 -*-
import unittest
from superlance.compat import StringIO
from superlance.compat import maxint
from superlance.memmon import (
    help_request,
    memmon_from_args,
    seconds_size
    )
from superlance.tests.dummy import DummyRPCServer

class MemmonTests(unittest.TestCase):
    def _getTargetClass(self):
        from superlance.memmon import Memmon
        return Memmon

    def _makeOne(self, *args, **kwargs):
        return self._getTargetClass()(*args, **kwargs)

    def _makeOnePopulated(self, programs, groups, any, name=None):
        memmon = self._makeOne(
            programs=programs,
            groups=groups,
            any=any,
            name=name,
            rpc=DummyRPCServer(),
            cumulative=False,
            sendmail='cat - > /dev/null',
            email='chrism@plope.com',
            email_uptime_limit=2000,
            )
        memmon.stdin = StringIO()
        memmon.stdout = StringIO()
        memmon.stderr = StringIO()
        memmon.pscommand = 'echo 22%s'
        return memmon

    def test_runforever_notatick(self):
        programs = {'foo':0, 'bar':0, 'baz_01':0 }
        groups = {}
        any = None
        memmon = self._makeOnePopulated(programs, groups, any)
        memmon.stdin.write('eventname:NOTATICK len:0\n')
        memmon.stdin.seek(0)
        memmon.runforever(test=True)
        self.assertEqual(memmon.stderr.getvalue(), '')

    def test_runforever_tick_programs(self):
        programs = {'foo':0, 'bar':0, 'baz_01':0 }
        groups = {}
        any = None
        memmon = self._makeOnePopulated(programs, groups, any)
        memmon.stdin.write('eventname:TICK len:0\n')
        memmon.stdin.seek(0)
        memmon.runforever(test=True)
        lines = memmon.stderr.getvalue().split('\n')
        self.assertEqual(len(lines), 8)
        self.assertEqual(lines[0], 'Checking programs bar=0, baz_01=0, foo=0')
        self.assertEqual(lines[1], 'RSS of foo:foo is 2264064')
        self.assertEqual(lines[2], 'Restarting foo:foo')
        self.assertEqual(lines[3], 'RSS of bar:bar is 2265088')
        self.assertEqual(lines[4], 'Restarting bar:bar')
        self.assertEqual(lines[5], 'RSS of baz:baz_01 is 2265088')
        self.assertEqual(lines[6], 'Restarting baz:baz_01')
        self.assertEqual(lines[7], '')
        mailed = memmon.mailed.split('\n')
        self.assertEqual(len(mailed), 4)
        self.assertEqual(mailed[0], 'To: chrism@plope.com')
        self.assertEqual(mailed[1],
                         'Subject: memmon: process baz:baz_01 restarted')
        self.assertEqual(mailed[2], '')
        self.assertTrue(mailed[3].startswith('memmon.py restarted'))

    def test_runforever_tick_groups(self):
        programs = {}
        groups = {'foo':0}
        any = None
        memmon = self._makeOnePopulated(programs, groups, any)
        memmon.stdin.write('eventname:TICK len:0\n')
        memmon.stdin.seek(0)
        memmon.runforever(test=True)
        lines = memmon.stderr.getvalue().split('\n')
        self.assertEqual(len(lines), 4)
        self.assertEqual(lines[0], 'Checking groups foo=0')
        self.assertEqual(lines[1], 'RSS of foo:foo is 2264064')
        self.assertEqual(lines[2], 'Restarting foo:foo')
        self.assertEqual(lines[3], '')
        mailed = memmon.mailed.split('\n')
        self.assertEqual(len(mailed), 4)
        self.assertEqual(mailed[0], 'To: chrism@plope.com')
        self.assertEqual(mailed[1],
          'Subject: memmon: process foo:foo restarted')
        self.assertEqual(mailed[2], '')
        self.assertTrue(mailed[3].startswith('memmon.py restarted'))

    def test_runforever_tick_any(self):
        programs = {}
        groups = {}
        any = 0
        memmon = self._makeOnePopulated(programs, groups, any)
        memmon.stdin.write('eventname:TICK len:0\n')
        memmon.stdin.seek(0)
        memmon.runforever(test=True)
        lines = memmon.stderr.getvalue().split('\n')
        self.assertEqual(len(lines), 8)
        self.assertEqual(lines[0], 'Checking any=0')
        self.assertEqual(lines[1], 'RSS of foo:foo is 2264064')
        self.assertEqual(lines[2], 'Restarting foo:foo')
        self.assertEqual(lines[3], 'RSS of bar:bar is 2265088')
        self.assertEqual(lines[4], 'Restarting bar:bar')
        self.assertEqual(lines[5], 'RSS of baz:baz_01 is 2265088')
        self.assertEqual(lines[6], 'Restarting baz:baz_01')
        self.assertEqual(lines[7], '')
        mailed = memmon.mailed.split('\n')
        self.assertEqual(len(mailed), 4)

    def test_runforever_tick_programs_and_groups(self):
        programs = {'baz_01':0}
        groups = {'foo':0}
        any = None
        memmon = self._makeOnePopulated(programs, groups, any)
        memmon.stdin.write('eventname:TICK len:0\n')
        memmon.stdin.seek(0)
        memmon.runforever(test=True)
        lines = memmon.stderr.getvalue().split('\n')
        self.assertEqual(len(lines), 7)
        self.assertEqual(lines[0], 'Checking programs baz_01=0')
        self.assertEqual(lines[1], 'Checking groups foo=0')
        self.assertEqual(lines[2], 'RSS of foo:foo is 2264064')
        self.assertEqual(lines[3], 'Restarting foo:foo')
        self.assertEqual(lines[4], 'RSS of baz:baz_01 is 2265088')
        self.assertEqual(lines[5], 'Restarting baz:baz_01')
        self.assertEqual(lines[6], '')
        mailed = memmon.mailed.split('\n')
        self.assertEqual(len(mailed), 4)
        self.assertEqual(mailed[0], 'To: chrism@plope.com')
        self.assertEqual(mailed[1],
                         'Subject: memmon: process baz:baz_01 restarted')
        self.assertEqual(mailed[2], '')
        self.assertTrue(mailed[3].startswith('memmon.py restarted'))

    def test_runforever_tick_programs_norestart(self):
        programs = {'foo': maxint}
        groups = {}
        any = None
        memmon = self._makeOnePopulated(programs, groups, any)
        memmon.stdin.write('eventname:TICK len:0\n')
        memmon.stdin.seek(0)
        memmon.runforever(test=True)
        lines = memmon.stderr.getvalue().split('\n')
        self.assertEqual(len(lines), 3)
        self.assertEqual(lines[0], 'Checking programs foo=%s' % maxint)
        self.assertEqual(lines[1], 'RSS of foo:foo is 2264064')
        self.assertEqual(lines[2], '')
        self.assertEqual(memmon.mailed, False)

    def test_stopprocess_fault_tick_programs_norestart(self):
        programs = {'foo': maxint}
        groups = {}
        any = None
        memmon = self._makeOnePopulated(programs, groups, any)
        memmon.stdin.write('eventname:TICK len:0\n')
        memmon.stdin.seek(0)
        memmon.runforever(test=True)
        lines = memmon.stderr.getvalue().split('\n')
        self.assertEqual(len(lines), 3)
        self.assertEqual(lines[0], 'Checking programs foo=%s' % maxint)
        self.assertEqual(lines[1], 'RSS of foo:foo is 2264064')
        self.assertEqual(lines[2], '')
        self.assertEqual(memmon.mailed, False)

    def test_stopprocess_fails_to_stop(self):
        programs = {'BAD_NAME': 0}
        groups = {}
        any = None
        memmon = self._makeOnePopulated(programs, groups, any)
        memmon.stdin.write('eventname:TICK len:0\n')
        memmon.stdin.seek(0)
        from supervisor.process import ProcessStates
        memmon.rpc.supervisor.all_process_info = [ {
            'name':'BAD_NAME',
            'group':'BAD_NAME',
            'pid':11,
            'state':ProcessStates.RUNNING,
            'statename':'RUNNING',
            'start':0,
            'stop':0,
            'spawnerr':'',
            'now':0,
            'description':'BAD_NAME description',
             } ]
        from superlance.compat import xmlrpclib
        self.assertRaises(xmlrpclib.Fault, memmon.runforever, True)
        lines = memmon.stderr.getvalue().split('\n')
        self.assertEqual(len(lines), 4)
        self.assertEqual(lines[0], 'Checking programs BAD_NAME=%s' % 0)
        self.assertEqual(lines[1], 'RSS of BAD_NAME:BAD_NAME is 2264064')
        self.assertEqual(lines[2], 'Restarting BAD_NAME:BAD_NAME')
        self.assertTrue(lines[3].startswith('Failed'))
        mailed = memmon.mailed.split('\n')
        self.assertEqual(len(mailed), 4)
        self.assertEqual(mailed[0], 'To: chrism@plope.com')
        self.assertEqual(mailed[1],
          'Subject: memmon: failed to stop process BAD_NAME:BAD_NAME, exiting')
        self.assertEqual(mailed[2], '')
        self.assertTrue(mailed[3].startswith('Failed'))

    def test_subject_no_name(self):
        """set the name to None to check if subject formats to:
        memmon: %(subject)s
        """
        memmon = self._makeOnePopulated(
            programs={},
            groups={},
            any=0,
            name=None,
            )
        memmon.stdin.write('eventname:TICK len:0\n')
        memmon.stdin.seek(0)
        memmon.runforever(test=True)

        mailed = memmon.mailed.split('\n')
        self.assertEqual(mailed[1],
          'Subject: memmon: process baz:baz_01 restarted')

    def test_subject_with_name(self):
        """set the name to a string to check if subject formats to:
        memmon [%(name)s]: %(subject)s
        """
        memmon = self._makeOnePopulated(
            programs={},
            groups={},
            any=0,
            name='thinko',
            )
        memmon.stdin.write('eventname:TICK len:0\n')
        memmon.stdin.seek(0)
        memmon.runforever(test=True)

        mailed = memmon.mailed.split('\n')
        self.assertEqual(mailed[1],
          'Subject: memmon [thinko]: process baz:baz_01 restarted')

    def test_parse_uptime(self):
        """test parsing of time parameter for uptime
        """
        self.assertEqual(seconds_size('1'), 1, 'default is seconds')
        self.assertEqual(seconds_size('1s'), 1, 'seconds suffix is allowed, too')
        self.assertEqual(seconds_size('2m'), 120)
        self.assertEqual(seconds_size('3h'), 10800)
        self.assertEqual(seconds_size('1d'), 86400)
        self.assertRaises(ValueError, seconds_size, '1y')

    def test_uptime_short_email(self):
        """in case an email is provided and the restarted process' uptime
        is shorter than our uptime_limit we do send an email
        """
        programs = {'foo':0}
        groups = {}
        any = None
        memmon = self._makeOnePopulated(programs, groups, any)
        memmon.email_uptime_limit = 101

        memmon.stdin.write('eventname:TICK len:0\n')
        memmon.stdin.seek(0)
        memmon.runforever(test=True)
        self.assertTrue(memmon.mailed, 'email has been sent')

        #in case uptime == limit, we send an email too
        memmon = self._makeOnePopulated(programs, groups, any)
        memmon.email_uptime_limit = 100
        memmon.stdin.write('eventname:TICK len:0\n')
        memmon.stdin.seek(0)
        memmon.runforever(test=True)
        self.assertTrue(memmon.mailed, 'email has been sent')



    def test_uptime_long_no_email(self):
        """in case an email is provided and the restarted process' uptime
        is longer than our uptime_limit we do not send an email
        """
        programs = {'foo':0}
        groups = {}
        any = None
        memmon = self._makeOnePopulated(programs, groups, any)
        memmon.email_uptime_limit = 99

        memmon.stdin.write('eventname:TICK len:0\n')
        memmon.stdin.seek(0)
        memmon.runforever(test=True)
        self.assertFalse(memmon.mailed, 'no email should be sent because uptime is above limit')

    def test_calc_rss_not_cumulative(self):
        programs = {}
        groups = {}
        any = None
        memmon = self._makeOnePopulated(programs, groups, any)

        noop = '_=%s; '
        pid = 1

        memmon.pscommand = noop + 'echo 16'
        rss = memmon.calc_rss(pid)
        self.assertEqual(16 * 1024, rss)

        memmon.pscommand = noop + 'echo not_an_int'
        rss = memmon.calc_rss(pid)
        self.assertEqual(
            None, rss, 'Failure to parse an integer RSS value from the ps '
            'output should result in calc_rss() returning None.')

    def test_calc_rss_cumulative(self):
        """Let calc_rss() do its work on a fake process tree:

        ├─┬= 99
        │ └─┬= 1
        │   └─┬= 2
        │     ├─── 3
        │     └─── 4

        (Where the process with PID 1 is the one being monitored)
        """
        programs = {}
        groups = {}
        any = None
        memmon = self._makeOnePopulated(programs, groups, any)
        memmon.cumulative = True

        # output of ps ax -o "pid= ppid= rss=" representing the process
        # tree described above, including extraneous whitespace and
        # unrelated processes.
        ps_output = """
        11111 22222    333
        1     99       100
        2     1        200
        3     2        300
        4     2        400
        11111 22222    333
        """

        memmon.pstreecommand = 'echo "%s"' % ps_output
        rss = memmon.calc_rss(1)
        self.assertEqual(
            1000 * 1024, rss,
            'Cumulative RSS of the test process and its three children '
            'should add up to 1000 kb.')

    def test_argparser(self):
        """test if arguments are parsed correctly
        """
        # help
        arguments = ['-h', ]
        memmon = memmon_from_args(arguments)
        self.assertTrue(memmon is help_request,
            '-h returns help_request to make main() script print usage'
            )

        #all arguments
        arguments = ['-c',
                     '-p', 'foo=50MB',
                     '-g', 'bar=10kB',
                     '--any', '250',
                     '-s', 'mutt',
                     '-m', 'me@you.com',
                     '-u', '1d',
                     '-n', 'myproject']
        memmon = memmon_from_args(arguments)
        self.assertEqual(memmon.cumulative, True)
        self.assertEqual(memmon.programs['foo'], 50 * 1024 * 1024)
        self.assertEqual(memmon.groups['bar'], 10 * 1024)
        self.assertEqual(memmon.any, 250)
        self.assertEqual(memmon.sendmail, 'mutt')
        self.assertEqual(memmon.email, 'me@you.com')
        self.assertEqual(memmon.email_uptime_limit, 1 * 24 * 60 * 60)
        self.assertEqual(memmon.name, 'myproject')


        #default arguments
        arguments = ['-m', 'me@you.com']
        memmon = memmon_from_args(arguments)
        self.assertEqual(memmon.cumulative, False)
        self.assertEqual(memmon.programs, {})
        self.assertEqual(memmon.groups, {})
        self.assertEqual(memmon.any, None)
        self.assertTrue('sendmail' in memmon.sendmail, 'not using sendmail as default')
        self.assertEqual(memmon.email_uptime_limit, maxint)
        self.assertEqual(memmon.name, None)

        arguments = ['-p', 'foo=50MB']
        memmon = memmon_from_args(arguments)
        self.assertEqual(memmon.email, None)


================================================
FILE: superlance/tests/test_process_state_email_monitor.py
================================================
import unittest
try: # pragma: no cover
    from unittest.mock import Mock
except ImportError: # pragma: no cover
    from mock import Mock
from superlance.compat import StringIO

class ProcessStateEmailMonitorTestException(Exception):
    pass

class ProcessStateEmailMonitorTests(unittest.TestCase):
    from_email = 'testFrom@blah.com'
    to_emails = ('testTo@blah.com', 'testTo2@blah.com')
    to_str = 'testTo@blah.com, testTo2@blah.com'
    subject = 'Test Alert'

    def _get_target_class(self):
        from superlance.process_state_email_monitor \
        import ProcessStateEmailMonitor
        return ProcessStateEmailMonitor

    def _make_one(self, **kwargs):
        kwargs['stdin'] = StringIO()
        kwargs['stdout'] = StringIO()
        kwargs['stderr'] = StringIO()
        kwargs['from_email'] = kwargs.get('from_email', self.from_email)
        kwargs['to_emails'] = kwargs.get('to_emails', self.to_emails)
        kwargs['subject'] = kwargs.get('subject', self.subject)

        obj = self._get_target_class()(**kwargs)
        return obj

    def _make_one_mock_send_email(self, **kwargs):
        obj = self._make_one(**kwargs)
        obj.send_email = Mock()
        return obj

    def _make_one_mock_send_smtp(self, **kwargs):
        obj = self._make_one(**kwargs)
        obj.send_smtp = Mock()
        return obj

    def test_validate_cmd_line_options_single_to_email_ok(self):
        klass = self._get_target_class()

        options = Mock()
        options.from_email = 'blah'
        options.to_emails = 'frog'

        validated = klass.validate_cmd_line_options(options)
        self.assertEqual(['frog'], validated.to_emails)

    def test_validate_cmd_line_options_multi_to_emails_ok(self):
        klass = self._get_target_class()

        options = Mock()
        options.from_email = 'blah'
        options.to_emails = 'frog, log,dog'

        validated = klass.validate_cmd_line_options(options)
        self.assertEqual(['frog', 'log', 'dog'], validated.to_emails)

    def test_send_email_ok(self):
        email = {
            'body': 'msg1\nmsg2',
            'to': self.to_emails,
            'from': 'testFrom@blah.com',
            'subject': 'Test Alert',
        }
        monitor = self._make_one_mock_send_smtp()
        monitor.send_email(email)

        # Test that email was sent
        self.assertEqual(1, monitor.send_smtp.call_count)
        smtpCallArgs = monitor.send_smtp.call_args[0]
        mimeMsg = smtpCallArgs[0]
        self.assertEqual(self.to_str, mimeMsg['To'])
        self.assertEqual(email['from'], mimeMsg['From'])
        self.assertEqual(email['subject'], mimeMsg['Subject'])
        self.assertEqual(email['body'], mimeMsg.get_payload())

    def _raiseSTMPException(self, mime, to_emails):
        raise ProcessStateEmailMonitorTestException('test')

    def test_send_email_exception(self):
        email = {
            'body': 'msg1\nmsg2',
            'to': self.to_emails,
            'from': 'testFrom@blah.com',
            'subject': 'Test Alert',
        }
        monitor = self._make_one_mock_send_smtp()
        monitor.send_smtp.side_effect = self._raiseSTMPException
        monitor.send_email(email)

        # Test that error was logged to stderr
        self.assertEqual("Error sending email: test\n", monitor.stderr.getvalue())

    def test_send_batch_notification(self):
        test_msgs = ['msg1', 'msg2']
        monitor = self._make_one_mock_send_email()
        monitor.batchmsgs = test_msgs
        monitor.send_batch_notification()

        # Test that email was sent
        expected = {
            'body': 'msg1\nmsg2',
            'to': self.to_emails,
            'from': 'testFrom@blah.com',
            'subject': 'Test Alert',
        }
        self.assertEqual(1, monitor.send_email.call_count)
        monitor.send_email.assert_called_with(expected)

        # Test that email was logged
        self.assertEqual("""Sending notification email:
To: %s
From: testFrom@blah.com
Subject: Test Alert
Body:
msg1
msg2
""" % (self.to_str), monitor.stderr.getvalue())

    def test_log_email_with_body_digest(self):
        bodyLen = 80
        monitor = self._make_one_mock_send_email()
        email = {
            'to': ['you@fubar.com'],
            'from': 'me@fubar.com',
            'subject': 'yo yo',
            'body': 'a' * bodyLen,
        }
        monitor.log_email(email)
        self.assertEqual("""Sending notification email:
To: you@fubar.com
From: me@fubar.com
Subject: yo yo
Body:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa...
""", monitor.stderr.getvalue())
        self.assertEqual('a' * bodyLen, email['body'])

    def test_log_email_without_body_digest(self):
        monitor = self._make_one_mock_send_email()
        email = {
            'to': ['you@fubar.com'],
            'from': 'me@fubar.com',
            'subject': 'yo yo',
            'body': 'a' * 20,
        }
        monitor.log_email(email)
        self.assertEqual("""Sending notification email:
To: you@fubar.com
From: me@fubar.com
Subject: yo yo
Body:
aaaaaaaaaaaaaaaaaaaa
""", monitor.stderr.getvalue())


================================================
FILE: superlance/tests/test_process_state_monitor.py
================================================
import unittest
try: # pragma: no cover
    from unittest.mock import Mock
except ImportError: # pragma: no cover
    from mock import Mock
from superlance.compat import StringIO
from superlance.process_state_monitor import ProcessStateMonitor

class _TestProcessStateMonitor(ProcessStateMonitor):

    process_state_events = ['PROCESS_STATE_EXITED']

    def get_process_state_change_msg(self, headers, payload):
        return repr(payload)

class ProcessStateMonitorTests(unittest.TestCase):

    def _get_target_class(self):
        return _TestProcessStateMonitor

    def _make_one_mocked(self, **kwargs):
        kwargs['stdin'] = StringIO()
        kwargs['stdout'] = StringIO()
        kwargs['stderr'] = StringIO()

        obj = self._get_target_class()(**kwargs)
        obj.send_batch_notification = Mock()
        return obj

    def get_process_exited_event(self, pname, gname, expected,
                                eventname='PROCESS_STATE_EXITED'):
        headers = {
            'ver': '3.0', 'poolserial': '7', 'len': '71',
            'server': 'supervisor', 'eventname': eventname,
            'serial': '7', 'pool': 'checkmailbatch',
        }
        payload = 'processname:%s groupname:%s from_state:RUNNING expected:%d \
pid:58597' % (pname, gname, expected)
        return (headers, payload)

    def get_tick60_event(self):
        headers = {
            'ver': '3.0', 'poolserial': '5', 'len': '15',
            'server': 'supervisor', 'eventname': 'TICK_60',
            'serial': '5', 'pool': 'checkmailbatch',
        }
        payload = 'when:1279665240'
        return (headers, payload)

    def test__get_tick_secs(self):
        monitor = self._make_one_mocked()
        self.assertEqual(5, monitor._get_tick_secs('TICK_5'))
        self.assertEqual(60, monitor._get_tick_secs('TICK_60'))
        self.assertEqual(3600, monitor._get_tick_secs('TICK_3600'))
        self.assertRaises(ValueError, monitor._get_tick_secs, 'JUNK_60')

    def test__get_tick_mins(self):
        monitor = self._make_one_mocked()
        self.assertEqual(5.0/60.0, monitor._get_tick_mins('TICK_5'))

    def test_handle_event_exit(self):
        monitor = self._make_one_mocked()
        hdrs, payload = self.get_process_exited_event('foo', 'bar', 0)
        monitor.handle_event(hdrs, payload)
        unexpected_err_msg = repr(payload)
        self.assertEqual([unexpected_err_msg], monitor.get_batch_msgs())
        self.assertEqual('%s\n' % unexpected_err_msg, monitor.stderr.getvalue())

    def test_handle_event_non_exit(self):
        monitor = self._make_one_mocked()
        hdrs, payload = self.get_process_exited_event('foo', 'bar', 0,
                                            eventname='PROCESS_STATE_FATAL')
        monitor.handle_event(hdrs, payload)
        self.assertEqual([], monitor.get_batch_msgs())
        self.assertEqual('', monitor.stderr.getvalue())

    def test_handle_event_tick_interval_expired(self):
        monitor = self._make_one_mocked()
        #Put msgs in batch
        hdrs, payload = self.get_process_exited_event('foo', 'bar', 0)
        monitor.handle_event(hdrs, payload)
        hdrs, payload = self.get_process_exited_event('bark', 'dog', 0)
        monitor.handle_event(hdrs, payload)
        self.assertEqual(2, len(monitor.get_batch_msgs()))
        #Time expired
        hdrs, payload = self.get_tick60_event()
        monitor.handle_event(hdrs, payload)

        # Test that batch messages are now gone
        self.assertEqual([], monitor.get_batch_msgs())
        # Test that email was sent
        self.assertEqual(1, monitor.send_batch_notification.call_count)

    def test_handle_event_tick_interval_not_expired(self):
        monitor = self._make_one_mocked(interval=3)
        hdrs, payload = self.get_tick60_event()
        monitor.handle_event(hdrs, payload)
        self.assertEqual(1.0, monitor.get_batch_minutes())
        monitor.handle_event(hdrs, payload)
        self.assertEqual(2.0, monitor.get_batch_minutes())


================================================
FILE: superlance/timeoutconn.py
================================================
from superlance.compat import httplib
import socket
import ssl


class TimeoutHTTPConnection(httplib.HTTPConnection):
    """A customised HTTPConnection allowing a per-connection
    timeout, specified at construction."""
    timeout = None

    def connect(self):
        """Override HTTPConnection.connect to connect to
        host/port specified in __init__."""

        e = "getaddrinfo returns an empty list"
        for res in socket.getaddrinfo(self.host, self.port,
                                      0, socket.SOCK_STREAM):
            af, socktype, proto, canonname, sa = res
            try:
                self.sock = socket.socket(af, socktype, proto)
                if self.timeout:   # this is the new bit
                    self.sock.settimeout(self.timeout)
                self.sock.connect(sa)
            except socket.error:
                if self.sock:
                    self.sock.close()
                self.sock = None
                continue
            break
        if not self.sock:
            raise socket.error(e)


class TimeoutHTTPSConnection(httplib.HTTPSConnection):
    timeout = None

    def connect(self):
        "Connect to a host on a given (SSL) port."

        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        if self.timeout:
            sock.settimeout(self.timeout)
        sock.connect((self.host, self.port))
        self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file)


================================================
FILE: tox.ini
================================================
[tox]
envlist =
    docs,py27,py34,py35,py36,py37,py38,py39,py310,py311,py312,py313,py314

[testenv]
deps =
    pytest
commands =
    pytest

[testenv:py27]
deps =
    {[testenv]deps}
    mock >= 0.5.0
commands = {[testenv]commands}

[testenv:docs]
deps =
    Sphinx
    readme
    setuptools >= 18.5
allowlist_externals = make
commands =
    make -C docs html BUILDDIR={envtmpdir} "SPHINXOPTS=-W -E"
    python setup.py check -m -r -s
Download .txt
gitextract_7woj_tmr/

├── .github/
│   └── workflows/
│       └── main.yml
├── .gitignore
├── .readthedocs.yaml
├── CHANGES.rst
├── COPYRIGHT.txt
├── LICENSE.txt
├── MANIFEST.in
├── README.rst
├── docs/
│   ├── Makefile
│   ├── conf.py
│   ├── crashmail.rst
│   ├── crashmailbatch.rst
│   ├── crashsms.rst
│   ├── development.rst
│   ├── fatalmailbatch.rst
│   ├── httpok.rst
│   ├── index.rst
│   └── memmon.rst
├── setup.cfg
├── setup.py
├── superlance/
│   ├── __init__.py
│   ├── compat.py
│   ├── crashmail.py
│   ├── crashmailbatch.py
│   ├── crashsms.py
│   ├── fatalmailbatch.py
│   ├── httpok.py
│   ├── memmon.py
│   ├── process_state_email_monitor.py
│   ├── process_state_monitor.py
│   ├── tests/
│   │   ├── __init__.py
│   │   ├── dummy.py
│   │   ├── test_crashmail.py
│   │   ├── test_crashmailbatch.py
│   │   ├── test_crashsms.py
│   │   ├── test_fatalmailbatch.py
│   │   ├── test_httpok.py
│   │   ├── test_memmon.py
│   │   ├── test_process_state_email_monitor.py
│   │   └── test_process_state_monitor.py
│   └── timeoutconn.py
└── tox.ini
Download .txt
SYMBOL INDEX (183 symbols across 18 files)

FILE: superlance/crashmail.py
  function usage (line 75) | def usage(exitstatus=255):
  class CrashMail (line 80) | class CrashMail:
    method __init__ (line 82) | def __init__(self, programs, any, email, sendmail, optionalheader):
    method runforever (line 93) | def runforever(self, test=False):
    method mail (line 137) | def mail(self, email, subject, msg):
  function main (line 148) | def main(argv=sys.argv):

FILE: superlance/crashmailbatch.py
  class CrashMailBatch (line 58) | class CrashMailBatch(ProcessStateEmailMonitor):
    method __init__ (line 62) | def __init__(self, **kwargs):
    method get_process_state_change_msg (line 68) | def get_process_state_change_msg(self, headers, payload):
  function main (line 79) | def main():

FILE: superlance/crashsms.py
  class CrashSMS (line 69) | class CrashSMS(ProcessStateEmailMonitor):
    method __init__ (line 72) | def __init__(self, **kwargs):
    method get_process_state_change_msg (line 76) | def get_process_state_change_msg(self, headers, payload):
  function main (line 87) | def main():

FILE: superlance/fatalmailbatch.py
  class FatalMailBatch (line 57) | class FatalMailBatch(ProcessStateEmailMonitor):
    method __init__ (line 61) | def __init__(self, **kwargs):
    method get_process_state_change_msg (line 67) | def get_process_state_change_msg(self, headers, payload):
  function main (line 74) | def main():

FILE: superlance/httpok.py
  function usage (line 117) | def usage(exitstatus=255):
  class HTTPOk (line 121) | class HTTPOk:
    method __init__ (line 123) | def __init__(self, rpc, programs, any, url, timeout, statuses, inbody,
    method listProcesses (line 143) | def listProcesses(self, state=None):
    method runforever (line 148) | def runforever(self, test=False):
    method format_subject (line 229) | def format_subject(self, subject):
    method act (line 235) | def act(self, subject, msg):
    method mail (line 284) | def mail(self, email, subject, msg):
    method restart (line 294) | def restart(self, spec, write):
  function main (line 322) | def main(argv=sys.argv):

FILE: superlance/memmon.py
  function usage (line 94) | def usage(exitstatus=255):
  function shell (line 98) | def shell(cmd):
  class Memmon (line 102) | class Memmon:
    method __init__ (line 103) | def __init__(self, cumulative, programs, groups, any, sendmail, email,...
    method runforever (line 120) | def runforever(self, test=False):
    method restart (line 195) | def restart(self, name, rss):
    method format_subject (line 238) | def format_subject(self, subject):
    method calc_rss (line 244) | def calc_rss(self, pid):
    method mail (line 297) | def mail(self, email, subject, msg):
  function parse_namesize (line 306) | def parse_namesize(option, value):
  function parse_size (line 315) | def parse_size(option, value):
  function parse_seconds (line 330) | def parse_seconds(option, value):
  function memmon_from_args (line 340) | def memmon_from_args(arguments):
  function main (line 412) | def main():

FILE: superlance/process_state_email_monitor.py
  class ProcessStateEmailMonitor (line 30) | class ProcessStateEmailMonitor(ProcessStateMonitor):
    method _get_opt_parser (line 34) | def _get_opt_parser(cls):
    method parse_cmd_line_options (line 57) | def parse_cmd_line_options(cls):
    method validate_cmd_line_options (line 63) | def validate_cmd_line_options(cls, options):
    method get_cmd_line_options (line 77) | def get_cmd_line_options(cls):
    method create_from_cmd_line (line 81) | def create_from_cmd_line(cls):
    method __init__ (line 90) | def __init__(self, **kwargs):
    method send_batch_notification (line 102) | def send_batch_notification(self):
    method log_email (line 108) | def log_email(self, email):
    method get_batch_email (line 116) | def get_batch_email(self):
    method send_email (line 126) | def send_email(self, email):
    method send_smtp (line 140) | def send_smtp(self, mime_msg, to_emails):

FILE: superlance/process_state_monitor.py
  class ProcessStateMonitor (line 23) | class ProcessStateMonitor:
    method __init__ (line 28) | def __init__(self, **kwargs):
    method _get_tick_mins (line 41) | def _get_tick_mins(self, eventname):
    method _get_tick_secs (line 44) | def _get_tick_secs(self, eventname):
    method _validate_tick_name (line 48) | def _validate_tick_name(self, eventname):
    method run (line 52) | def run(self):
    method handle_event (line 58) | def handle_event(self, headers, payload):
    method handle_process_state_change_event (line 64) | def handle_process_state_change_event(self, headers, payload):
    method get_process_state_change_msg (line 73) | def get_process_state_change_msg(self, headers, payload):
    method handle_tick_event (line 76) | def handle_tick_event(self, headers, payload):
    method send_batch_notification (line 85) | def send_batch_notification(self):
    method get_batch_minutes (line 88) | def get_batch_minutes(self):
    method get_batch_msgs (line 91) | def get_batch_msgs(self):
    method clear_batch (line 94) | def clear_batch(self):
    method write_stderr (line 98) | def write_stderr(self, msg):

FILE: superlance/tests/dummy.py
  class DummyRPCServer (line 6) | class DummyRPCServer:
    method __init__ (line 7) | def __init__(self):
  class DummyResponse (line 11) | class DummyResponse:
    method read (line 15) | def read(self):
  class DummySystemRPCNamespace (line 18) | class DummySystemRPCNamespace:
  class DummySupervisorRPCNamespace (line 21) | class DummySupervisorRPCNamespace:
    method getAllProcessInfo (line 67) | def getAllProcessInfo(self):
    method getProcessInfo (line 70) | def getProcessInfo(self, name):
    method startProcess (line 76) | def startProcess(self, name):
    method stopProcess (line 83) | def stopProcess(self, name):

FILE: superlance/tests/test_crashmail.py
  class CrashMailTests (line 4) | class CrashMailTests(unittest.TestCase):
    method _getTargetClass (line 5) | def _getTargetClass(self):
    method _makeOne (line 9) | def _makeOne(self, *opts):
    method setUp (line 12) | def setUp(self):
    method tearDown (line 16) | def tearDown(self):
    method _makeOnePopulated (line 20) | def _makeOnePopulated(self, programs, any, response=None):
    method test_runforever_not_process_state_exited (line 31) | def test_runforever_not_process_state_exited(self):
    method test_runforever_expected_exit (line 40) | def test_runforever_expected_exit(self):
    method test_runforever_unexpected_exit (line 53) | def test_runforever_unexpected_exit(self):

FILE: superlance/tests/test_crashmailbatch.py
  class CrashMailBatchTests (line 8) | class CrashMailBatchTests(unittest.TestCase):
    method _get_target_class (line 14) | def _get_target_class(self):
    method _make_one_mocked (line 18) | def _make_one_mocked(self, **kwargs):
    method get_process_exited_event (line 30) | def get_process_exited_event(self, pname, gname, expected):
    method test_get_process_state_change_msg_expected (line 40) | def test_get_process_state_change_msg_expected(self):
    method test_get_process_state_change_msg_unexpected (line 45) | def test_get_process_state_change_msg_unexpected(self):
    method test_handle_event_exit_expected (line 51) | def test_handle_event_exit_expected(self):
    method test_handle_event_exit_unexpected (line 58) | def test_handle_event_exit_unexpected(self):
    method test_sets_default_subject_when_None (line 67) | def test_sets_default_subject_when_None(self):

FILE: superlance/tests/test_crashsms.py
  class CrashSMSTests (line 5) | class CrashSMSTests(CrashMailBatchTests):
    method _get_target_class (line 9) | def _get_target_class(self):
    method test_sets_default_subject_when_None (line 13) | def test_sets_default_subject_when_None(self):

FILE: superlance/tests/test_fatalmailbatch.py
  class FatalMailBatchTests (line 8) | class FatalMailBatchTests(unittest.TestCase):
    method _get_target_class (line 14) | def _get_target_class(self):
    method _make_one_mocked (line 18) | def _make_one_mocked(self, **kwargs):
    method get_process_fatal_event (line 30) | def get_process_fatal_event(self, pname, gname):
    method test_get_process_state_change_msg (line 40) | def test_get_process_state_change_msg(self):
    method test_sets_default_subject_when_None (line 46) | def test_sets_default_subject_when_None(self):

FILE: superlance/tests/test_httpok.py
  function make_connection (line 37) | def make_connection(response, exc=None):
  class HTTPOkTests (line 57) | class HTTPOkTests(unittest.TestCase):
    method _getTargetClass (line 58) | def _getTargetClass(self):
    method _makeOne (line 62) | def _makeOne(self, *args, **kwargs):
    method _makeOnePopulated (line 65) | def _makeOnePopulated(self, programs, any=None, statuses=None, inbody=...
    method test_listProcesses_no_programs (line 95) | def test_listProcesses_no_programs(self):
    method test_listProcesses_w_RUNNING_programs_default_state (line 102) | def test_listProcesses_w_RUNNING_programs_default_state(self):
    method test_listProcesses_w_nonRUNNING_programs_default_state (line 111) | def test_listProcesses_w_nonRUNNING_programs_default_state(self):
    method test_listProcesses_w_nonRUNNING_programs_RUNNING_state (line 120) | def test_listProcesses_w_nonRUNNING_programs_RUNNING_state(self):
    method test_runforever_eager_notatick (line 127) | def test_runforever_eager_notatick(self):
    method test_runforever_doesnt_act_if_status_is_expected (line 136) | def test_runforever_doesnt_act_if_status_is_expected(self):
    method test_runforever_acts_if_status_is_unexpected (line 152) | def test_runforever_acts_if_status_is_unexpected(self):
    method test_runforever_doesnt_act_if_inbody_is_present (line 171) | def test_runforever_doesnt_act_if_inbody_is_present(self):
    method test_runforever_acts_if_inbody_isnt_present (line 186) | def test_runforever_acts_if_inbody_isnt_present(self):
    method test_runforever_eager_error_on_request_some (line 202) | def test_runforever_eager_error_on_request_some(self):
    method test_runforever_eager_error_on_request_any (line 227) | def test_runforever_eager_error_on_request_any(self):
    method test_runforever_eager_error_on_process_stop (line 247) | def test_runforever_eager_error_on_process_stop(self):
    method test_runforever_eager_error_on_process_start (line 267) | def test_runforever_eager_error_on_process_start(self):
    method test_runforever_eager_gcore (line 288) | def test_runforever_eager_gcore(self):
    method test_runforever_not_eager_none_running (line 317) | def test_runforever_not_eager_none_running(self):
    method test_runforever_not_eager_running (line 329) | def test_runforever_not_eager_running(self):
    method test_runforever_honor_timeout_on_connrefused (line 349) | def test_runforever_honor_timeout_on_connrefused(self):
    method test_runforever_connrefused_error (line 361) | def test_runforever_connrefused_error(self):
    method test_bug_110 (line 384) | def test_bug_110(self):
    method test_subject_no_name (line 400) | def test_subject_no_name(self):
    method test_subject_with_name (line 418) | def test_subject_with_name(self):

FILE: superlance/tests/test_memmon.py
  class MemmonTests (line 12) | class MemmonTests(unittest.TestCase):
    method _getTargetClass (line 13) | def _getTargetClass(self):
    method _makeOne (line 17) | def _makeOne(self, *args, **kwargs):
    method _makeOnePopulated (line 20) | def _makeOnePopulated(self, programs, groups, any, name=None):
    method test_runforever_notatick (line 38) | def test_runforever_notatick(self):
    method test_runforever_tick_programs (line 48) | def test_runforever_tick_programs(self):
    method test_runforever_tick_groups (line 74) | def test_runforever_tick_groups(self):
    method test_runforever_tick_any (line 96) | def test_runforever_tick_any(self):
    method test_runforever_tick_programs_and_groups (line 117) | def test_runforever_tick_programs_and_groups(self):
    method test_runforever_tick_programs_norestart (line 142) | def test_runforever_tick_programs_norestart(self):
    method test_stopprocess_fault_tick_programs_norestart (line 157) | def test_stopprocess_fault_tick_programs_norestart(self):
    method test_stopprocess_fails_to_stop (line 172) | def test_stopprocess_fails_to_stop(self):
    method test_subject_no_name (line 208) | def test_subject_no_name(self):
    method test_subject_with_name (line 226) | def test_subject_with_name(self):
    method test_parse_uptime (line 244) | def test_parse_uptime(self):
    method test_uptime_short_email (line 254) | def test_uptime_short_email(self):
    method test_uptime_long_no_email (line 279) | def test_uptime_long_no_email(self):
    method test_calc_rss_not_cumulative (line 294) | def test_calc_rss_not_cumulative(self):
    method test_calc_rss_cumulative (line 313) | def test_calc_rss_cumulative(self):
    method test_argparser (line 349) | def test_argparser(self):

FILE: superlance/tests/test_process_state_email_monitor.py
  class ProcessStateEmailMonitorTestException (line 8) | class ProcessStateEmailMonitorTestException(Exception):
  class ProcessStateEmailMonitorTests (line 11) | class ProcessStateEmailMonitorTests(unittest.TestCase):
    method _get_target_class (line 17) | def _get_target_class(self):
    method _make_one (line 22) | def _make_one(self, **kwargs):
    method _make_one_mock_send_email (line 33) | def _make_one_mock_send_email(self, **kwargs):
    method _make_one_mock_send_smtp (line 38) | def _make_one_mock_send_smtp(self, **kwargs):
    method test_validate_cmd_line_options_single_to_email_ok (line 43) | def test_validate_cmd_line_options_single_to_email_ok(self):
    method test_validate_cmd_line_options_multi_to_emails_ok (line 53) | def test_validate_cmd_line_options_multi_to_emails_ok(self):
    method test_send_email_ok (line 63) | def test_send_email_ok(self):
    method _raiseSTMPException (line 82) | def _raiseSTMPException(self, mime, to_emails):
    method test_send_email_exception (line 85) | def test_send_email_exception(self):
    method test_send_batch_notification (line 99) | def test_send_batch_notification(self):
    method test_log_email_with_body_digest (line 125) | def test_log_email_with_body_digest(self):
    method test_log_email_without_body_digest (line 144) | def test_log_email_without_body_digest(self):

FILE: superlance/tests/test_process_state_monitor.py
  class _TestProcessStateMonitor (line 9) | class _TestProcessStateMonitor(ProcessStateMonitor):
    method get_process_state_change_msg (line 13) | def get_process_state_change_msg(self, headers, payload):
  class ProcessStateMonitorTests (line 16) | class ProcessStateMonitorTests(unittest.TestCase):
    method _get_target_class (line 18) | def _get_target_class(self):
    method _make_one_mocked (line 21) | def _make_one_mocked(self, **kwargs):
    method get_process_exited_event (line 30) | def get_process_exited_event(self, pname, gname, expected,
    method get_tick60_event (line 41) | def get_tick60_event(self):
    method test__get_tick_secs (line 50) | def test__get_tick_secs(self):
    method test__get_tick_mins (line 57) | def test__get_tick_mins(self):
    method test_handle_event_exit (line 61) | def test_handle_event_exit(self):
    method test_handle_event_non_exit (line 69) | def test_handle_event_non_exit(self):
    method test_handle_event_tick_interval_expired (line 77) | def test_handle_event_tick_interval_expired(self):
    method test_handle_event_tick_interval_not_expired (line 94) | def test_handle_event_tick_interval_not_expired(self):

FILE: superlance/timeoutconn.py
  class TimeoutHTTPConnection (line 6) | class TimeoutHTTPConnection(httplib.HTTPConnection):
    method connect (line 11) | def connect(self):
  class TimeoutHTTPSConnection (line 34) | class TimeoutHTTPSConnection(httplib.HTTPSConnection):
    method connect (line 37) | def connect(self):
Condensed preview — 42 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (169K chars).
[
  {
    "path": ".github/workflows/main.yml",
    "chars": 4168,
    "preview": "name: Run all tests\n\non: [push, pull_request]\n\nenv:\n  PIP: \"env PIP_DISABLE_PIP_VERSION_CHECK=1\n            PYTHONWARNIN"
  },
  {
    "path": ".gitignore",
    "chars": 145,
    "preview": "*~\n*.egg\n*.egg-info\n*.pyc\n*.pyo\n*.swp\n.DS_Store\n.coverage\n.eggs/\n.tox/\nbuild/\ndocs/_build/\ndist/\nenv*/\nhtmlcov/\ntmp/\ncov"
  },
  {
    "path": ".readthedocs.yaml",
    "chars": 578,
    "preview": "# .readthedocs.yaml\n# Read the Docs configuration file\n# See https://docs.readthedocs.io/en/stable/config-file/v2.html f"
  },
  {
    "path": "CHANGES.rst",
    "chars": 4941,
    "preview": "2.0.1.dev0 (Next Release)\n-------------------------\n\n2.0.0 (2021-12-26)\n------------------\n\n- Support for Python 2.6 has"
  },
  {
    "path": "COPYRIGHT.txt",
    "chars": 536,
    "preview": "Superlance is Copyright (c) 2008-2013 Agendaless Consulting and Contributors.\n(http://www.agendaless.com), All Rights Re"
  },
  {
    "path": "LICENSE.txt",
    "chars": 1834,
    "preview": "Superlance is licensed under the following license:\n\n  A copyright notice accompanies this license document that identif"
  },
  {
    "path": "MANIFEST.in",
    "chars": 170,
    "preview": "include CHANGES.rst\ninclude COPYRIGHT.txt\ninclude LICENSE.txt\ninclude README.rst\ninclude docs/Makefile\nrecursive-include"
  },
  {
    "path": "README.rst",
    "chars": 236,
    "preview": "superlance README\n=================\n\nSuperlance is a package of plugin utilities for monitoring and controlling\nprocesse"
  },
  {
    "path": "docs/Makefile",
    "chars": 3134,
    "preview": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD "
  },
  {
    "path": "docs/conf.py",
    "chars": 6422,
    "preview": "# -*- coding: utf-8 -*-\n#\n# superlance documentation build configuration file, created by\n# sphinx-quickstart on Thu Jun"
  },
  {
    "path": "docs/crashmail.rst",
    "chars": 2781,
    "preview": ":command:`crashmail` Documentation\n==================================\n\n:command:`crashmail` is a supervisor \"event liste"
  },
  {
    "path": "docs/crashmailbatch.rst",
    "chars": 2925,
    "preview": ":command:`crashmailbatch` Documentation\n=======================================\n\n:command:`crashmailbatch` is a supervis"
  },
  {
    "path": "docs/crashsms.rst",
    "chars": 2286,
    "preview": ":command:`crashsms` Documentation\n==================================\n\n:command:`crashsms` is a supervisor \"event listene"
  },
  {
    "path": "docs/development.rst",
    "chars": 1943,
    "preview": "Resources and Development\n=========================\n\nBug Tracker\n-----------\n\nSuperlance has a bug tracker where you may"
  },
  {
    "path": "docs/fatalmailbatch.rst",
    "chars": 2355,
    "preview": ":command:`fatalmailbatch` Documentation\n=======================================\n\n:command:`fatalmailbatch` is a supervis"
  },
  {
    "path": "docs/httpok.rst",
    "chars": 5506,
    "preview": ":command:`httpok` Documentation\n==================================\n\n:command:`httpok` is a supervisor \"event listener\" w"
  },
  {
    "path": "docs/index.rst",
    "chars": 2040,
    "preview": "superlance plugins for supervisor\n=================================\n\nSuperlance is a package of plugin utilities for mon"
  },
  {
    "path": "docs/memmon.rst",
    "chars": 8396,
    "preview": ":command:`memmon` Overview\n==========================\n\n:command:`memmon` is a supervisor \"event listener\" which may be s"
  },
  {
    "path": "setup.cfg",
    "chars": 309,
    "preview": ";Marking a wheel as universal with \"universal = 1\" was deprecated\n;in Setuptools 75.1.0.  Setting \"python_tag = py2.py3\""
  },
  {
    "path": "setup.py",
    "chars": 3241,
    "preview": "##############################################################################\n#\n# Copyright (c) 2008-2013 Agendaless Co"
  },
  {
    "path": "superlance/__init__.py",
    "chars": 21,
    "preview": "# superlance package\n"
  },
  {
    "path": "superlance/compat.py",
    "chars": 476,
    "preview": "try:\n    import http.client as httplib\nexcept ImportError:\n    import httplib\n\ntry:\n    from StringIO import StringIO\nex"
  },
  {
    "path": "superlance/crashmail.py",
    "chars": 6244,
    "preview": "#!/usr/bin/env python -u\n##############################################################################\n#\n# Copyright (c"
  },
  {
    "path": "superlance/crashmailbatch.py",
    "chars": 2821,
    "preview": "#!/usr/bin/env python -u\n##############################################################################\n#\n# Copyright (c"
  },
  {
    "path": "superlance/crashsms.py",
    "chars": 3128,
    "preview": "#!/usr/bin/env python -u\n##############################################################################\n#\n# Copyright (c"
  },
  {
    "path": "superlance/fatalmailbatch.py",
    "chars": 2710,
    "preview": "#!/usr/bin/env python -u\n##############################################################################\n#\n# Copyright (c"
  },
  {
    "path": "superlance/httpok.py",
    "chars": 14391,
    "preview": "#!/usr/bin/env python -u\n##############################################################################\n#\n# Copyright (c"
  },
  {
    "path": "superlance/memmon.py",
    "chars": 14182,
    "preview": "#!/usr/bin/env python\n##############################################################################\n#\n# Copyright (c) 2"
  },
  {
    "path": "superlance/process_state_email_monitor.py",
    "chars": 5788,
    "preview": "#!/usr/bin/env python -u\n##############################################################################\n#\n# Copyright (c"
  },
  {
    "path": "superlance/process_state_monitor.py",
    "chars": 3343,
    "preview": "#!/usr/bin/env python -u\n##############################################################################\n#\n# Copyright (c"
  },
  {
    "path": "superlance/tests/__init__.py",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "superlance/tests/dummy.py",
    "chars": 2336,
    "preview": "import time\nfrom supervisor.process import ProcessStates\n\n_NOW = time.time()\n\nclass DummyRPCServer:\n    def __init__(sel"
  },
  {
    "path": "superlance/tests/test_crashmail.py",
    "chars": 2888,
    "preview": "import unittest\nfrom superlance.compat import StringIO\n\nclass CrashMailTests(unittest.TestCase):\n    def _getTargetClass"
  },
  {
    "path": "superlance/tests/test_crashmailbatch.py",
    "chars": 2880,
    "preview": "import unittest\ntry: # pragma: no cover\n    from unittest.mock import Mock\nexcept ImportError: # pragma: no cover\n    fr"
  },
  {
    "path": "superlance/tests/test_crashsms.py",
    "chars": 461,
    "preview": "import unittest\n\nfrom .test_crashmailbatch import CrashMailBatchTests\n\nclass CrashSMSTests(CrashMailBatchTests):\n    sub"
  },
  {
    "path": "superlance/tests/test_fatalmailbatch.py",
    "chars": 1868,
    "preview": "import unittest\ntry: # pragma: no cover\n    from unittest.mock import Mock\nexcept ImportError: # pragma: no cover\n    fr"
  },
  {
    "path": "superlance/tests/test_httpok.py",
    "chars": 17423,
    "preview": "import socket\nimport time\nimport unittest\nfrom superlance.compat import StringIO\nfrom supervisor.process import ProcessS"
  },
  {
    "path": "superlance/tests/test_memmon.py",
    "chars": 14964,
    "preview": "# -*- coding: utf-8 -*-\nimport unittest\nfrom superlance.compat import StringIO\nfrom superlance.compat import maxint\nfrom"
  },
  {
    "path": "superlance/tests/test_process_state_email_monitor.py",
    "chars": 5150,
    "preview": "import unittest\ntry: # pragma: no cover\n    from unittest.mock import Mock\nexcept ImportError: # pragma: no cover\n    fr"
  },
  {
    "path": "superlance/tests/test_process_state_monitor.py",
    "chars": 4006,
    "preview": "import unittest\ntry: # pragma: no cover\n    from unittest.mock import Mock\nexcept ImportError: # pragma: no cover\n    fr"
  },
  {
    "path": "superlance/timeoutconn.py",
    "chars": 1459,
    "preview": "from superlance.compat import httplib\nimport socket\nimport ssl\n\n\nclass TimeoutHTTPConnection(httplib.HTTPConnection):\n  "
  },
  {
    "path": "tox.ini",
    "chars": 436,
    "preview": "[tox]\nenvlist =\n    docs,py27,py34,py35,py36,py37,py38,py39,py310,py311,py312,py313,py314\n\n[testenv]\ndeps =\n    pytest\nc"
  }
]

About this extraction

This page contains the full source code of the Supervisor/superlance GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 42 files (157.1 KB), approximately 39.7k tokens, and a symbol index with 183 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!