├── MANIFEST.in ├── tests ├── .gitignore ├── conftest.py ├── test_poll_build.py ├── test_package.py └── test_poll.py ├── docs ├── _static │ ├── custom.css │ ├── github-ribbons.png │ └── Makefile ├── requirements.in ├── index.rst ├── api.rst ├── Makefile ├── requirements.txt ├── development.rst └── conf.py ├── requirements-test.in ├── orphanage ├── poll.h ├── __init__.py ├── poll_build.py ├── poll.c └── poll.py ├── .editorconfig ├── setup.cfg ├── .travis.yml ├── tox.ini ├── requirements-test.txt ├── LICENSE ├── setup.py ├── .gitignore ├── Makefile └── README.rst /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst LICENSE 2 | recursive-include orphanage *.c *.h 3 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | *.c 2 | *.o 3 | *.so 4 | *.dylib 5 | *.gcno 6 | *.gcda 7 | *.gcov 8 | -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | img.github { 2 | width: 149px; 3 | height: 149px; 4 | } 5 | -------------------------------------------------------------------------------- /docs/requirements.in: -------------------------------------------------------------------------------- 1 | sphinx>=1.7 2 | 3 | jinja2>=2.10.1 # CVE-2016-10745 and CVE-2019-10906 4 | -------------------------------------------------------------------------------- /docs/_static/github-ribbons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tonyseek/python-orphanage/HEAD/docs/_static/github-ribbons.png -------------------------------------------------------------------------------- /requirements-test.in: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | pytest-pep8 4 | pytest-mock 5 | pytest-forked 6 | gcovr 7 | 8 | jinja2>=2.10.1 # CVE-2016-10745 and CVE-2019-10906 9 | -------------------------------------------------------------------------------- /docs/_static/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all 2 | 3 | all: github-ribbons.png 4 | 5 | github-ribbons.png: 6 | wget https://aral.github.io/fork-me-on-github-retina-ribbons/right-graphite@2x.png -O $@ 7 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from pytest import fixture 4 | 5 | from orphanage.poll import lib 6 | 7 | 8 | @fixture(autouse=True) 9 | def gcov_flush(): 10 | try: 11 | yield lib.__gcov_flush 12 | finally: 13 | lib.__gcov_flush() 14 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Document of orphanage 2 | ===================== 3 | 4 | Overview 5 | -------- 6 | 7 | .. include:: ../README.rst 8 | :start-line: 5 9 | 10 | 11 | Table of Content 12 | ---------------- 13 | 14 | .. toctree:: 15 | :maxdepth: 2 16 | 17 | index 18 | development 19 | api 20 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API Reference 2 | ------------- 3 | 4 | For most users, please use the public API instead of the internal one. 5 | 6 | .. _public_api: 7 | 8 | Public API 9 | ~~~~~~~~~~ 10 | 11 | .. automodule:: orphanage 12 | :members: 13 | 14 | 15 | Internal API 16 | ~~~~~~~~~~~~ 17 | 18 | .. automodule:: orphanage.poll 19 | :members: 20 | :undoc-members: 21 | -------------------------------------------------------------------------------- /orphanage/poll.h: -------------------------------------------------------------------------------- 1 | typedef struct orphanage_poll_t orphanage_poll_t; 2 | 3 | orphanage_poll_t *orphanage_poll_create(unsigned); 4 | void orphanage_poll_close(orphanage_poll_t *t); 5 | 6 | int orphanage_poll_start(orphanage_poll_t *t); 7 | int orphanage_poll_stop(orphanage_poll_t *t); 8 | 9 | extern "Python+C" { 10 | int orphanage_poll_routine_callback(orphanage_poll_t *t); 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.py] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{yml,yaml,json,js}] 16 | indent_style = space 17 | indent_size = 2 18 | 19 | [Makefile] 20 | indent_style = tab 21 | indent_size = 4 22 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.0 3 | commit = True 4 | tag = True 5 | 6 | [bdist_wheel] 7 | universal = 0 8 | 9 | [tool:pytest] 10 | addopts = --pep8 --forked --cov=orphanage --cov-report=term --cov-report=html 11 | pep8ignore = 12 | docs/conf.py ALL 13 | 14 | [bumpversion:file:setup.py] 15 | 16 | [bumpversion:file:docs/conf.py] 17 | 18 | [bumpversion:file:orphanage/__init__.py] 19 | 20 | -------------------------------------------------------------------------------- /tests/test_poll_build.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from orphanage.poll_build import ffibuilder, ensure_string 4 | 5 | 6 | def test_compile(tmpdir): 7 | with tmpdir.as_cwd(): 8 | ffibuilder.compile(verbose=True, debug=True) 9 | assert tmpdir.join('_orphanage_poll.c').exists() 10 | assert tmpdir.join('_orphanage_poll.o').exists() 11 | 12 | 13 | def test_ensure_string(): 14 | assert ensure_string(u'\u6d4b\u8bd5') == u'\u6d4b\u8bd5' 15 | assert ensure_string(b'\xe6\xb5\x8b\xe8\xaf\x95') == u'\u6d4b\u8bd5' 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | # pypy2.7 is broken (https://github.com/travis-ci/travis-ci/issues/8103#issuecomment-387327744) 4 | - "2.7" 5 | - "3.6" 6 | - "pypy3.5" 7 | before_install: 8 | - sudo apt-get -qq update 9 | - sudo apt-get install -y lcov 10 | install: 11 | - pip install tox-travis coveralls coveralls-merge 12 | - gem install coveralls-lcov 13 | script: 14 | - tox -- -v 15 | after_success: 16 | - cd tests 17 | - lcov --capture -d $PWD -b $PWD --output-file coverage.info 18 | - coveralls-lcov -v -n coverage.info > coverage.c.json 19 | - coveralls-merge coverage.c.json 20 | branches: 21 | only: 22 | - master 23 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = orphanage 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(MAKE) -C _static all 21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py34,py35,py36,pypy,pypy3,docs 3 | 4 | [testenv] 5 | changedir = tests 6 | deps = 7 | --requirement=requirements-test.txt 8 | setenv = 9 | CFLAGS={env:CFLAGS:} --coverage 10 | LDFLAGS={env:LDFLAGS:} --coverage 11 | whitelist_externals = 12 | mkdir 13 | git 14 | commands = 15 | git clean -fXd 16 | python -m orphanage.poll_build 17 | py.test {posargs} 18 | mkdir -p htmlcov/c-ext 19 | gcovr --print-summary -r . 20 | gcovr --html --html-details -r . -o htmlcov/c-ext/index.html 21 | 22 | [testenv:docs] 23 | basepython = python3.6 24 | changedir = docs 25 | deps = 26 | --requirement=docs/requirements.txt 27 | whitelist_externals = 28 | make 29 | commands = 30 | make html 31 | -------------------------------------------------------------------------------- /orphanage/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | 5 | 6 | __version__ = '0.1.0' 7 | __all__ = ['exit_when_orphaned'] 8 | 9 | 10 | _suicide_ctx = None 11 | _suicide_pid = None 12 | 13 | 14 | def exit_when_orphaned(): 15 | """Let the current process exit when it was orphaned. 16 | 17 | Calling multiple times and calling-and-forking are both safe. But this is 18 | not a thread safe function. Never call it concurrently. 19 | """ 20 | from orphanage.poll import Context 21 | 22 | global _suicide_ctx, _suicide_pid 23 | if _suicide_ctx is not None: 24 | if _suicide_pid == os.getpid(): 25 | return 26 | else: 27 | _suicide_ctx.stop() 28 | _suicide_ctx.close() 29 | _suicide_pid = os.getpid() 30 | _suicide_ctx = Context(suicide_instead=True) 31 | _suicide_ctx.start() 32 | -------------------------------------------------------------------------------- /orphanage/poll_build.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | from pkg_resources import resource_string 5 | 6 | from cffi import FFI 7 | 8 | 9 | DEBUG = '--coverage' in os.environ.get('CFLAGS', '').split() 10 | 11 | 12 | def ensure_string(text): 13 | if isinstance(text, bytes): 14 | return text.decode('utf-8') 15 | return text 16 | 17 | 18 | def yield_macros(): 19 | if DEBUG: 20 | yield ('DEBUG', '') 21 | 22 | 23 | ffibuilder = FFI() 24 | ffibuilder.set_source( 25 | '_orphanage_poll', 26 | ensure_string(resource_string('orphanage', 'poll.c')), 27 | libraries=['pthread'], 28 | define_macros=list(yield_macros()), 29 | ) 30 | ffibuilder.cdef( 31 | ensure_string(resource_string('orphanage', 'poll.h')), 32 | ) 33 | 34 | if DEBUG: 35 | ffibuilder.cdef('void __gcov_flush(void);') 36 | 37 | 38 | if __name__ == '__main__': # pragma: no cover 39 | ffibuilder.compile(verbose=True, debug=DEBUG) 40 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file docs/requirements.txt docs/requirements.in 6 | # 7 | alabaster==0.7.12 # via sphinx 8 | babel==2.6.0 # via sphinx 9 | certifi==2018.10.15 # via requests 10 | chardet==3.0.4 # via requests 11 | docutils==0.14 # via sphinx 12 | idna==2.7 # via requests 13 | imagesize==1.1.0 # via sphinx 14 | jinja2==2.10.1 15 | markupsafe==1.0 # via jinja2 16 | packaging==18.0 # via sphinx 17 | pygments==2.2.0 # via sphinx 18 | pyparsing==2.3.0 # via packaging 19 | pytz==2018.7 # via babel 20 | requests==2.20.0 # via sphinx 21 | six==1.11.0 # via packaging, sphinx 22 | snowballstemmer==1.2.1 # via sphinx 23 | sphinx==1.8.1 24 | sphinxcontrib-websupport==1.1.0 # via sphinx 25 | typing==3.6.6 # via sphinx 26 | urllib3==1.24.1 # via requests 27 | -------------------------------------------------------------------------------- /requirements-test.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile 3 | # To update, run: 4 | # 5 | # pip-compile --output-file requirements-test.txt requirements-test.in 6 | # 7 | apipkg==1.5 # via execnet 8 | atomicwrites==1.2.1 # via pytest 9 | attrs==18.2.0 # via pytest 10 | coverage==4.5.1 # via pytest-cov 11 | execnet==1.5.0 # via pytest-cache 12 | funcsigs==1.0.2 # via mock, pytest 13 | gcovr==4.1 14 | jinja2==2.10.1 15 | markupsafe==1.0 # via jinja2 16 | mock==2.0.0 # via pytest-mock 17 | more-itertools==4.3.0 # via pytest 18 | pathlib2==2.3.2 # via pytest 19 | pbr==5.1.0 # via mock 20 | pep8==1.7.1 # via pytest-pep8 21 | pluggy==0.8.0 # via pytest 22 | py==1.7.0 # via pytest 23 | pytest-cache==1.0 # via pytest-pep8 24 | pytest-cov==2.6.0 25 | pytest-forked==0.2 26 | pytest-mock==1.10.0 27 | pytest-pep8==1.0.6 28 | pytest==3.9.3 29 | scandir==1.9.0 # via pathlib2 30 | six==1.11.0 # via mock, more-itertools, pathlib2, pytest 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2018 Jiangge Zhang 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | 4 | with open('README.rst') as readme: 5 | next(readme) # Skip badges 6 | long_description = ''.join(readme).strip() 7 | 8 | 9 | setup( 10 | name='orphanage', 11 | version='0.1.0', 12 | url='https://github.com/tonyseek/python-orphanage', 13 | author='Jiangge Zhang', 14 | author_email='tonyseek@gmail.com', 15 | description='Let orphan processes suicide', 16 | long_description=long_description, 17 | packages=find_packages(), 18 | zip_safe=False, 19 | include_package_data=True, 20 | license='MIT', 21 | platforms=['POSIX', 'Linux'], 22 | keywords=['process', 'management', 'orphan'], 23 | classifiers=[ 24 | 'Development Status :: 3 - Alpha', 25 | 'Intended Audience :: Developers', 26 | 'Intended Audience :: System Administrators', 27 | 'License :: OSI Approved :: MIT License', 28 | 'Operating System :: POSIX', 29 | 'Operating System :: POSIX :: Linux', 30 | 'Programming Language :: Python :: 2.7', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: Implementation :: CPython', 33 | 'Programming Language :: Python :: Implementation :: PyPy', 34 | 'Topic :: Software Development :: Libraries :: Python Modules', 35 | 'Topic :: Utilities', 36 | ], 37 | setup_requires=[ 38 | 'cffi>=1.0.0', 39 | ], 40 | install_requires=[ 41 | 'cffi>=1.0.0', 42 | ], 43 | extras_require={}, 44 | cffi_modules=[ 45 | 'orphanage/poll_build.py:ffibuilder', 46 | ], 47 | ) 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | .static_storage/ 56 | .media/ 57 | local_settings.py 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | # py.test 107 | .pytest_cache/ 108 | -------------------------------------------------------------------------------- /tests/test_package.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | import signal 5 | import select 6 | 7 | from pkg_resources import get_distribution 8 | from pytest_cov.embed import init as init_cov, cleanup as cleanup_cov 9 | 10 | from orphanage import __version__ as orphanage_version, exit_when_orphaned 11 | 12 | 13 | def test_distribution(): 14 | distribution = get_distribution('orphanage') 15 | assert distribution.version == orphanage_version 16 | 17 | 18 | def test_suicide_prepare(mocker): 19 | ctx = mocker.patch('orphanage.poll.Context', autospec=True) 20 | exit_when_orphaned() 21 | exit_when_orphaned() 22 | ctx.assert_called_once_with(suicide_instead=True) 23 | 24 | 25 | def test_suicide(): 26 | pipe_r, pipe_w = os.pipe() 27 | spawner_pid = os.fork() 28 | if spawner_pid == 0: 29 | # spawner process 30 | os.setsid() 31 | spawnee_pid = os.fork() 32 | if spawnee_pid == 0: 33 | # spawnee process 34 | os.close(pipe_r) 35 | init_cov() 36 | exit_when_orphaned() 37 | cleanup_cov() 38 | os.write(pipe_w, b'ready') 39 | signal.pause() 40 | else: 41 | # still spawner process 42 | os.close(pipe_r) 43 | os.close(pipe_w) 44 | signal.pause() 45 | else: 46 | # main process 47 | try: 48 | os.close(pipe_w) 49 | 50 | assert os.read(pipe_r, 5) == b'ready' 51 | assert select.select([pipe_r], [], [], 1)[0] == [] 52 | 53 | os.kill(spawner_pid, signal.SIGTERM) 54 | os.waitpid(spawner_pid, 0) 55 | assert os.read(pipe_r, 6) == b'' # EOF: spawnee is dead 56 | finally: 57 | try: 58 | os.killpg(spawner_pid, signal.SIGTERM) 59 | os.waitpid(spawner_pid, 0) 60 | except OSError: 61 | pass 62 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: help deps test docs dist dist-wheel dist-repair clean 2 | 3 | help: 4 | @printf "Commands:\n" 5 | @printf " help\tShows this help information.\n" 6 | @printf " deps\tCompiles and locks dependencies.\n" 7 | @printf " test\tRuns test via tox.\n" 8 | @printf " docs\tGenerates documents via tox and sphinx.\n" 9 | @printf " dist\tBuilds distribution packages.\n" 10 | @printf " clean\tRemoves all untracked files.\n" 11 | 12 | deps: 13 | pip-compile --output-file docs/requirements.txt docs/requirements.in 14 | pip-compile --output-file requirements-test.txt requirements-test.in 15 | 16 | test: 17 | tox -re "$(shell tox -l | grep -v docs | paste -s -d ',' -)" --skip-missing-interpreters 18 | @printf "\nopen tests/htmlcov/index.html tests/htmlcov/c-ext/index.html\n" 19 | 20 | docs: 21 | tox -e docs 22 | @printf "\nopen docs/_build/html/index.html\n" 23 | 24 | dist: 25 | rm -rf dist 26 | python setup.py egg_info $(options) sdist 27 | # Build for macOS 28 | make dist-wheel PYTHON=python2.7 options="$(options)" 29 | make dist-wheel PYTHON=python3.6 options="$(options)" 30 | make dist-wheel PYTHON=pypy options="$(options)" 31 | make dist-wheel PYTHON=pypy3 options="$(options)" 32 | # Build for Linux 33 | docker run --rm -v $(PWD):/srv python:2.7 make -C /srv dist-wheel PYTHON=python2.7 options="$(options)" 34 | docker run --rm -v $(PWD):/srv python:3.6 make -C /srv dist-wheel PYTHON=python3.6 options="$(options)" 35 | docker run --rm -v $(PWD):/srv pypy:2 make -C /srv dist-wheel PYTHON=pypy options="$(options)" 36 | docker run --rm -v $(PWD):/srv pypy:3 make -C /srv dist-wheel PYTHON=pypy3 options="$(options)" 37 | # Repair for Linux 38 | docker run --rm -v $(PWD):/srv quay.io/pypa/manylinux1_x86_64 make -C /srv dist-repair 39 | 40 | dist-wheel: 41 | $(PYTHON) setup.py egg_info $(options) bdist_wheel 42 | 43 | dist-repair: 44 | find dist -name '*-linux_x86_64.whl' -exec auditwheel repair -w dist {} \; 45 | find dist -name '*-linux_x86_64.whl' -delete 46 | 47 | clean: 48 | git clean -fXd --exclude=.tox 49 | -------------------------------------------------------------------------------- /orphanage/poll.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | // Copy this to the Python modules since any modification 8 | const int ORPHANAGE_POLL_OK = 0x00000000; 9 | const int ORPHANAGE_POLL_PT_CREATE_ERROR = 0x00000001; 10 | const int ORPHANAGE_POLL_PT_DETACH_ERROR = 0x00000002; 11 | const int ORPHANAGE_POLL_PT_CANCEL_ERROR = 0x00000003; 12 | 13 | // It seems that the CFFI disallows to include local header files (for now). 14 | typedef struct orphanage_poll_t { 15 | pthread_t pt; 16 | pid_t pid, ppid; 17 | unsigned started; 18 | unsigned suicide_instead; 19 | } orphanage_poll_t; 20 | int orphanage_poll_routine_callback(orphanage_poll_t *t); 21 | #ifdef DEBUG 22 | void __gcov_flush(void); 23 | #endif 24 | 25 | static void *orphanage_poll_routine(void *userdata) { 26 | pid_t ppid; 27 | orphanage_poll_t *t = userdata; 28 | while (1) { 29 | ppid = getppid(); 30 | if (ppid != t->ppid) { 31 | if (t->suicide_instead) { 32 | kill(t->pid, SIGTERM); 33 | } else { 34 | orphanage_poll_routine_callback(t); 35 | } 36 | break; 37 | } 38 | pthread_testcancel(); 39 | sleep(1); 40 | } 41 | return NULL; 42 | } 43 | 44 | orphanage_poll_t *orphanage_poll_create(unsigned suicide_instead) { 45 | orphanage_poll_t *t = malloc(sizeof(orphanage_poll_t)); 46 | if (t != NULL) { 47 | t->pt = 0; 48 | t->pid = getpid(); 49 | t->ppid = getppid(); 50 | t->started = 0; 51 | t->suicide_instead = suicide_instead; 52 | } 53 | return t; 54 | } 55 | 56 | void orphanage_poll_close(orphanage_poll_t *t) { 57 | free(t); 58 | } 59 | 60 | int orphanage_poll_start(orphanage_poll_t *t) { 61 | errno = pthread_create(&t->pt, NULL, &orphanage_poll_routine, (void *) t); 62 | if (errno) { 63 | return ORPHANAGE_POLL_PT_CREATE_ERROR; 64 | } 65 | 66 | errno = pthread_detach(t->pt); 67 | if (errno) { 68 | return ORPHANAGE_POLL_PT_DETACH_ERROR; 69 | } 70 | 71 | t->started = 1; 72 | 73 | return ORPHANAGE_POLL_OK; 74 | } 75 | 76 | int orphanage_poll_stop(orphanage_poll_t *t) { 77 | if (!t->started) { 78 | return ORPHANAGE_POLL_OK; 79 | } 80 | 81 | errno = pthread_cancel(t->pt); 82 | if (errno && errno != ESRCH) { 83 | return ORPHANAGE_POLL_PT_CANCEL_ERROR; 84 | } 85 | 86 | return ORPHANAGE_POLL_OK; 87 | } 88 | -------------------------------------------------------------------------------- /docs/development.rst: -------------------------------------------------------------------------------- 1 | Development Guide 2 | ----------------- 3 | 4 | There is a ``Makefile`` for development in local environment. 5 | See available commands via invoking ``make help``. 6 | 7 | You will need to install pyenv_ and tox_ globally in your local environment:: 8 | 9 | brew install pyenv tox 10 | pyenv install 2.7.14 # and also 11 | 12 | 13 | Requirement 14 | ~~~~~~~~~~~ 15 | 16 | The requirement changes of testing and document building environment need to be 17 | included in ``requirements-test.in`` and ``docs/requirements.in``. You will 18 | need to invoke ``make deps`` to compile them into ``requirements*.txt``. 19 | 20 | 21 | Test 22 | ~~~~ 23 | 24 | For running test in all supported Python versions, you will need pyenv_:: 25 | 26 | # Enter the multi-version Python environment (2.7, 3.6, pypy2, pypy3) 27 | pyenv shell 2.7.14:3.6.5:pypy2.7-5.10.0:pypy3.5-5.10.1 28 | 29 | make test 30 | 31 | For debugging, you may want to test in a specific Python version, such as 2.7:: 32 | 33 | tox -e py27 # Default pytest options 34 | tox -e py27 -- -vxs --log-cli-level=DEBUG # Custom pytest options 35 | 36 | 37 | Package 38 | ~~~~~~~ 39 | 40 | For packaging a new distribution, ``make dist`` will be helpful. It assumes you 41 | are using macOS and the Docker for Mac has been installed and started also. The 42 | binary wheel packages for macOS (with your current ABI) and Linux (with 43 | manylinux API) will be present. Using pyenv_ and bumpversion_ is a good idea:: 44 | 45 | # Enter the multi-version Python environment (2.7, 3.6, pypy2, pypy3) 46 | pyenv shell 2.7.14:3.6.5:pypy2.7-5.10.0:pypy3.5-5.10.1 47 | 48 | bumpversion minor # Commit and tag a new major/minor/patch release 49 | make dist # Build release packages 50 | make dist options="-b dev0" # Build pre-release packages 51 | 52 | 53 | Clean up 54 | ~~~~~~~~ 55 | 56 | You could clean up the workspace with ``make clean``. It removes files which 57 | was ignored in the version control except the ``.tox``. 58 | 59 | 60 | Debugg C Extension 61 | ~~~~~~~~~~~~~~~~~~ 62 | 63 | Debugging the C extension of Python needs different toolchains and skills. The 64 | ``lldb`` or ``gdb`` will be useful in that:: 65 | 66 | tox -e py27 # Run test until it hangs 67 | vim tests/_orphanage_poll.c # Inspect the CFFI generated code 68 | lldb --attach-pid=100001 # Attach to the target process 69 | lldb> breakpoint set -f _orphanage_poll.c -l 434 70 | lldb> continue 71 | lldb> bt all 72 | 73 | For unexpected crashing, the coredump will include useful information:: 74 | 75 | ulimit -c unlimited # Turn on coredump in current shell 76 | tox -e py27 # Run test until it crashes 77 | lldb --core /cores/cores.10 # Open the coredump named with its pid 78 | lldb> bt all # Print the backtrace 79 | 80 | 81 | .. _pyenv: https://github.com/pyenv/pyenv 82 | .. _bumpversion: https://github.com/peritus/bumpversion 83 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | |Build Status| |Coverage Status| |PyPI Version| 2 | 3 | orphanage 4 | ========= 5 | 6 | Let child processes in Python suicide if they became orphans. 7 | 8 | Installation 9 | ------------ 10 | 11 | .. code-block:: bash 12 | 13 | pip install orphanage 14 | 15 | Don't forget to put it in ``setup.py`` / ``requirements.txt``. 16 | 17 | 18 | Usage 19 | ----- 20 | 21 | .. code-block:: python 22 | 23 | from orphanage import exit_when_orphaned 24 | 25 | exit_when_orphaned() 26 | 27 | 28 | Motivation 29 | ---------- 30 | 31 | Some application server softwares (e.g. Gunicorn_) work on a multiple-process 32 | architect which we call the master-worker model. They must clean up the worker 33 | processes if the master process is stopped, to prevent them from becoming 34 | orphan processes. 35 | 36 | In the gevent-integration scene, the worker processes of Gunicorn poll their 37 | ``ppid`` in an user thread (a.k.a greenlet) to be orphan-aware. But the user 38 | thread may be hanged once the master process crashed because of the blocked 39 | writing on a pipe, the communicating channel between master process and 40 | worker processes. 41 | 42 | We want to perform this ``ppid`` polling in a real kernel thread. That is the 43 | intent of this library. 44 | 45 | 46 | Principle 47 | --------- 48 | 49 | This library spawns an internal thread to poll the ``ppid`` at regular 50 | intervals (for now it is one second). Once the ``ppid`` changed, the original 51 | parent process should be dead and the current process should be orphaned. The 52 | internal thread will send ``SIGTERM`` to the current process. 53 | 54 | In the plan, the ``prctl`` & ``SIGHUP`` pattern may be introduced in Linux 55 | platforms to avoid from creating threads. For now, the only supported strategy 56 | is the ``ppid`` polling, for being portable. 57 | 58 | 59 | Alternatives 60 | ------------ 61 | 62 | CaoE_ is an alternative to this library which developed by the Douban Inc. It 63 | uses ``prctl`` and a twice-forking pattern. It has a pure Python implementation 64 | without any C extension compiling requirement. If you don't mind to twist the 65 | process tree, that will be a good choice too. 66 | 67 | 68 | Contributing 69 | ------------ 70 | 71 | If you want to report bugs or request features, please feel free to open 72 | issues on GitHub_. 73 | 74 | Of course, pull requests are always welcome. 75 | 76 | .. _Gunicorn: https://github.com/benoitc/gunicorn 77 | .. _CaoE: https://github.com/douban/CaoE 78 | .. _GitHub: https://github.com/tonyseek/python-orphanage/issues 79 | 80 | .. |Build Status| image:: https://travis-ci.org/tonyseek/python-orphanage.svg?branch=master 81 | :target: https://travis-ci.org/tonyseek/python-orphanage 82 | :alt: Build Status 83 | .. |Coverage Status| image:: https://coveralls.io/repos/github/tonyseek/python-orphanage/badge.svg?branch=master 84 | :target: https://coveralls.io/github/tonyseek/python-orphanage?branch=master 85 | :alt: Coverage Status 86 | .. |PyPI Version| image:: https://img.shields.io/pypi/v/orphanage.svg 87 | :target: https://pypi.org/project/orphanage/ 88 | :alt: PyPI Version 89 | -------------------------------------------------------------------------------- /orphanage/poll.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from logging import getLogger 4 | from errno import errorcode 5 | from weakref import WeakValueDictionary 6 | 7 | from _orphanage_poll import ffi, lib 8 | 9 | 10 | # Copied from "poll.c" only 11 | ORPHANAGE_POLL_OK = 0x00000000 12 | ORPHANAGE_POLL_PT_CREATE_ERROR = 0x00000001 13 | ORPHANAGE_POLL_PT_DETACH_ERROR = 0x00000002 14 | ORPHANAGE_POLL_PT_CANCEL_ERROR = 0x00000003 15 | 16 | 17 | logger = getLogger(__name__) 18 | callback_registry = WeakValueDictionary() 19 | 20 | 21 | @ffi.def_extern() 22 | def orphanage_poll_routine_callback(ptr): 23 | """The external callback function of CFFI. 24 | 25 | This function invokes the :meth:`Context.trigger_callbacks` method. 26 | 27 | :param ptr: The C pointer of context. 28 | :returns: ``0`` for nonerror calls. 29 | """ 30 | ctx = callback_registry.get(ptr) 31 | if ctx is None: 32 | logger.debug('Context of %r is not found', ptr) 33 | return 1 34 | logger.debug('Prepare to trigger callbacks on %r', ctx) 35 | ctx.trigger_callbacks() 36 | logger.debug('Finished to trigger callbacks on %r', ctx) 37 | return 0 38 | 39 | 40 | def perror(description): 41 | """Raises a runtime error from the specified description and ``errno``.""" 42 | errno = ffi.errno 43 | errname = errorcode.get(errno, str(errno)) 44 | return RuntimeError('{0}: errno = {1}'.format(description, errname)) 45 | 46 | 47 | def raise_for_return_value(return_value): 48 | """Checks the return value from C area. 49 | 50 | A runtime error will be raised if the return value is nonzero. 51 | """ 52 | if return_value == ORPHANAGE_POLL_OK: 53 | return 54 | elif return_value == ORPHANAGE_POLL_PT_CREATE_ERROR: 55 | raise perror('pthread_create') 56 | elif return_value == ORPHANAGE_POLL_PT_DETACH_ERROR: 57 | raise perror('pthread_detach') 58 | elif return_value == ORPHANAGE_POLL_PT_CANCEL_ERROR: 59 | raise perror('pthread_cancel') 60 | else: 61 | raise perror('unknown') 62 | 63 | 64 | class Context(object): 65 | """The context of orphans polling which acts as the CFFI wrapper. 66 | 67 | .. caution:: It is dangerous to use this class directly except you are 68 | familiar with the implementation of CPython and you know what 69 | you are doing clearly. It is recommended to use the 70 | :ref:`public_api` instead, for most users. 71 | 72 | The context must be closed via :meth:`~Context.close` or the memory will 73 | be leaked. 74 | 75 | :param callbacks: Optional. The list of callback functions. A callback 76 | function will be passed one parameter, the instance of 77 | this context. Be careful, never invoking any Python 78 | built-in and C/C++ extended functions which use the 79 | ``Py_BEGIN_ALLOW_THREADS``, such as ``os.close`` and all 80 | methods on this context, to avoid from deadlock and other 81 | undefined behaviors. 82 | """ 83 | 84 | def __init__(self, callbacks=None, suicide_instead=False): 85 | self.callbacks = list(callbacks or []) 86 | self.suicide_instead = suicide_instead 87 | self.ptr = lib.orphanage_poll_create(int(suicide_instead)) 88 | if self.ptr == ffi.NULL: 89 | raise RuntimeError('out of memory') 90 | callback_registry[self.ptr] = self 91 | 92 | def close(self): 93 | """Closes this context and release the memory from C area.""" 94 | lib.orphanage_poll_close(self.ptr) 95 | callback_registry.pop(self.ptr, None) 96 | self.ptr = None 97 | 98 | def _started(self): 99 | if self.ptr: 100 | return 101 | raise RuntimeError('context has been closed') 102 | 103 | def start(self): 104 | """Starts the polling thread.""" 105 | self._started() 106 | r = lib.orphanage_poll_start(self.ptr) 107 | raise_for_return_value(r) 108 | 109 | def stop(self): 110 | """Stops the polling thread. 111 | 112 | Don't forget to release allocated memory by calling 113 | :meth:`~Context.close` if you won't use it anymore. 114 | """ 115 | self._started() 116 | r = lib.orphanage_poll_stop(self.ptr) 117 | raise_for_return_value(r) 118 | 119 | def trigger_callbacks(self): 120 | """Triggers the callback functions. 121 | 122 | This method is expected to be called from C area. 123 | """ 124 | for callback in self.callbacks: 125 | logger.debug('triggering callback %r on %r', callback, self) 126 | try: 127 | callback(self) 128 | except Exception: 129 | logger.exception('triggering callback') 130 | else: 131 | logger.debug('triggered callback %r on %r', callback, self) 132 | -------------------------------------------------------------------------------- /tests/test_poll.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | import os 4 | import signal 5 | import time 6 | import contextlib 7 | import errno 8 | 9 | from pytest import fixture, raises 10 | 11 | from orphanage.poll import ( 12 | Context, ffi, orphanage_poll_routine_callback, 13 | ORPHANAGE_POLL_OK, ORPHANAGE_POLL_PT_CREATE_ERROR, 14 | ORPHANAGE_POLL_PT_CANCEL_ERROR, ORPHANAGE_POLL_PT_DETACH_ERROR) 15 | 16 | 17 | @fixture 18 | def mocked_lib(mocker): 19 | return mocker.patch('orphanage.poll.lib', autospec=True) 20 | 21 | 22 | @fixture 23 | def mocked_ffi(mocker): 24 | return mocker.patch('orphanage.poll.ffi', autospec=True) 25 | 26 | 27 | def test_allocation(): 28 | ctx = Context() 29 | assert ctx.ptr is not None 30 | assert ctx.ptr != ffi.NULL 31 | 32 | ctx.close() 33 | assert ctx.ptr is None 34 | 35 | 36 | def test_allocation_memory_error(mocked_lib): 37 | mocked_lib.orphanage_poll_create.return_value = ffi.NULL 38 | with raises(RuntimeError) as error: 39 | Context() 40 | error.match('out of memory') 41 | 42 | 43 | def test_polling_smoke(): 44 | with contextlib.closing(Context()) as ctx: 45 | ctx.start() 46 | ctx.stop() 47 | with contextlib.closing(Context()) as ctx: 48 | ctx.stop() 49 | 50 | 51 | def test_polling_starting_error(mocked_lib, mocked_ffi): 52 | mocked_lib.orphanage_poll_start.return_value = ORPHANAGE_POLL_OK 53 | with contextlib.closing(Context()) as ctx: 54 | ctx.start() 55 | 56 | mocked_lib.orphanage_poll_start.return_value = \ 57 | ORPHANAGE_POLL_PT_CREATE_ERROR 58 | mocked_ffi.errno = errno.EPERM 59 | with raises(RuntimeError) as error, contextlib.closing(Context()) as ctx: 60 | ctx.start() 61 | error.match('pthread_create: errno = EPERM') 62 | 63 | 64 | def test_polling_stopping_error(mocked_lib, mocked_ffi): 65 | mocked_lib.orphanage_poll_stop.return_value = ORPHANAGE_POLL_OK 66 | with contextlib.closing(Context()) as ctx: 67 | ctx.stop() 68 | 69 | mocked_lib.orphanage_poll_stop.return_value = \ 70 | ORPHANAGE_POLL_PT_DETACH_ERROR 71 | mocked_ffi.errno = -65535 72 | with raises(RuntimeError) as error, contextlib.closing(Context()) as ctx: 73 | ctx.stop() 74 | error.match('pthread_detach: errno = -65535') 75 | 76 | mocked_lib.orphanage_poll_stop.return_value = \ 77 | ORPHANAGE_POLL_PT_CANCEL_ERROR 78 | mocked_ffi.errno = errno.EINVAL 79 | with raises(RuntimeError) as error, contextlib.closing(Context()) as ctx: 80 | ctx.stop() 81 | error.match('pthread_cancel: errno = EINVAL') 82 | 83 | mocked_lib.orphanage_poll_stop.return_value = -1 84 | mocked_ffi.errno = errno.EINVAL 85 | with raises(RuntimeError) as error, contextlib.closing(Context()) as ctx: 86 | ctx.stop() 87 | error.match('unknown: errno = EINVAL') 88 | 89 | 90 | def test_polling_closing_error(): 91 | ctx = Context() 92 | ctx.close() 93 | 94 | with raises(RuntimeError) as error: 95 | ctx.start() 96 | error.match('has been closed') 97 | 98 | with raises(RuntimeError) as error: 99 | ctx.stop() 100 | error.match('has been closed') 101 | 102 | 103 | def test_polling_callback_registry(mocker): 104 | stub = mocker.stub() 105 | stub2 = mocker.stub() 106 | with contextlib.closing(Context([stub])) as ctx, \ 107 | contextlib.closing(Context([stub2])) as ctx2: 108 | assert ctx.ptr != ctx2.ptr 109 | orphanage_poll_routine_callback(ctx.ptr) 110 | stub.assert_called_once_with(ctx) 111 | stub2.assert_not_called() 112 | 113 | 114 | def test_polling_callback_registry_fault_tolerance(mocker): 115 | stub = mocker.stub() 116 | with contextlib.closing(Context([stub])) as ctx: 117 | assert ctx.ptr != ffi.NULL 118 | orphanage_poll_routine_callback(ffi.NULL) 119 | stub.assert_not_called() 120 | 121 | 122 | def test_polling_callback_unexploded(mocker): 123 | stub = mocker.stub() 124 | with contextlib.closing(Context([stub])) as ctx: 125 | ctx.start() 126 | stub.assert_not_called() 127 | ctx.stop() 128 | 129 | 130 | def test_polling_callback_exception_tolerance(mocker): 131 | stub1 = mocker.stub() 132 | stub1.side_effect = ValueError() 133 | stub2 = mocker.stub() 134 | with contextlib.closing(Context([stub1, stub2])) as ctx: 135 | ctx.trigger_callbacks() 136 | stub1.assert_called_once_with(ctx) 137 | stub2.assert_called_once_with(ctx) 138 | with raises(ValueError): 139 | stub1() # It is truly fiery 140 | 141 | 142 | def test_polling_callback_exploded(gcov_flush): 143 | pipe_r, pipe_w = os.pipe() 144 | child_pid = os.fork() 145 | 146 | if child_pid == 0: 147 | # child process 148 | os.setsid() 149 | grandchild_pid = os.fork() 150 | if grandchild_pid == 0: 151 | # grandchild process 152 | os.close(pipe_r) 153 | 154 | ctx = Context([ 155 | lambda ctx: os.write(pipe_w, b'called'), 156 | lambda ctx: os.kill(os.getpid(), signal.SIGTERM), 157 | ]) 158 | ctx.start() 159 | 160 | def term(signum, frame): 161 | ctx.stop() 162 | ctx.close() 163 | gcov_flush() 164 | 165 | signal.signal(signal.SIGTERM, term) 166 | 167 | os.write(pipe_w, b'did_not_call_yet') 168 | signal.pause() 169 | else: 170 | # still child process 171 | os.close(pipe_r) 172 | os.close(pipe_w) 173 | signal.pause() 174 | else: 175 | # main process 176 | try: 177 | os.close(pipe_w) 178 | assert os.read(pipe_r, 1024) == b'did_not_call_yet' 179 | 180 | time.sleep(1) # suspend for waiting the pthread 181 | os.kill(child_pid, signal.SIGTERM) 182 | os.waitpid(child_pid, 0) 183 | assert os.read(pipe_r, 1024) == b'called' 184 | assert os.read(pipe_r, 1024) == b'' # EOF 185 | finally: 186 | try: 187 | os.killpg(child_pid, signal.SIGTERM) 188 | os.waitpid(child_pid, 0) 189 | except OSError: 190 | pass 191 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = u'orphanage' 23 | copyright = u'2018, Jiangge Zhang' 24 | author = u'Jiangge Zhang' 25 | 26 | # The short X.Y version 27 | version = u'' 28 | # The full version, including alpha/beta/rc tags 29 | release = u'0.1.0' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.intersphinx', 44 | 'sphinx.ext.coverage', 45 | ] 46 | 47 | # Add any paths that contain templates here, relative to this directory. 48 | templates_path = ['_templates'] 49 | 50 | # The suffix(es) of source filenames. 51 | # You can specify multiple suffix as a list of string: 52 | # 53 | # source_suffix = ['.rst', '.md'] 54 | source_suffix = '.rst' 55 | 56 | # The master toctree document. 57 | master_doc = 'index' 58 | 59 | # The language for content autogenerated by Sphinx. Refer to documentation 60 | # for a list of supported languages. 61 | # 62 | # This is also used if you do content translation via gettext catalogs. 63 | # Usually you set "language" from the command line for these cases. 64 | language = None 65 | 66 | # List of patterns, relative to source directory, that match files and 67 | # directories to ignore when looking for source files. 68 | # This pattern also affects html_static_path and html_extra_path . 69 | exclude_patterns = [u'_build', u'Thumbs.db', u'.DS_Store', u'.gitignore', 70 | u'Makefile'] 71 | 72 | # The name of the Pygments (syntax highlighting) style to use. 73 | pygments_style = 'sphinx' 74 | 75 | 76 | # -- Options for HTML output ------------------------------------------------- 77 | 78 | # The theme to use for HTML and HTML Help pages. See the documentation for 79 | # a list of builtin themes. 80 | # 81 | html_theme = 'alabaster' 82 | 83 | # Theme options are theme-specific and customize the look and feel of a theme 84 | # further. For a list of options available for each theme, see the 85 | # documentation. 86 | # 87 | html_theme_options = { 88 | 'logo_name': True, 89 | 'description': 'Let orphan processes suicide', 90 | 'github_user': 'tonyseek', 91 | 'github_repo': 'python-orphanage', 92 | 'github_type': 'star', 93 | 'github_count': True, 94 | 'github_banner': 'github-ribbons.png', 95 | 'github_button': True, 96 | 'travis_button': True, 97 | } 98 | 99 | # Add any paths that contain custom static files (such as style sheets) here, 100 | # relative to this directory. They are copied after the builtin static files, 101 | # so a file named "default.css" will overwrite the builtin "default.css". 102 | html_static_path = ['_static'] 103 | 104 | # Custom sidebar templates, must be a dictionary that maps document names 105 | # to template names. 106 | # 107 | # The default sidebars (for documents that don't match any pattern) are 108 | # defined by theme itself. Builtin themes are using these templates by 109 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 110 | # 'searchbox.html']``. 111 | # 112 | html_sidebars = { 113 | '**': [ 114 | 'about.html', 115 | 'navigation.html', 116 | 'relations.html', 117 | 'searchbox.html', 118 | 'donate.html', 119 | ], 120 | } 121 | 122 | 123 | # -- Options for HTMLHelp output --------------------------------------------- 124 | 125 | # Output file base name for HTML help builder. 126 | htmlhelp_basename = 'orphanagedoc' 127 | 128 | 129 | # -- Options for LaTeX output ------------------------------------------------ 130 | 131 | latex_elements = { 132 | # The paper size ('letterpaper' or 'a4paper'). 133 | # 134 | # 'papersize': 'letterpaper', 135 | 136 | # The font size ('10pt', '11pt' or '12pt'). 137 | # 138 | # 'pointsize': '10pt', 139 | 140 | # Additional stuff for the LaTeX preamble. 141 | # 142 | # 'preamble': '', 143 | 144 | # Latex figure (float) alignment 145 | # 146 | # 'figure_align': 'htbp', 147 | } 148 | 149 | # Grouping the document tree into LaTeX files. List of tuples 150 | # (source start file, target name, title, 151 | # author, documentclass [howto, manual, or own class]). 152 | latex_documents = [ 153 | (master_doc, 'orphanage.tex', u'orphanage Documentation', 154 | u'Jiangge Zhang', 'manual'), 155 | ] 156 | 157 | 158 | # -- Options for manual page output ------------------------------------------ 159 | 160 | # One entry per manual page. List of tuples 161 | # (source start file, name, description, authors, manual section). 162 | man_pages = [ 163 | (master_doc, 'orphanage', u'orphanage Documentation', 164 | [author], 1) 165 | ] 166 | 167 | 168 | # -- Options for Texinfo output ---------------------------------------------- 169 | 170 | # Grouping the document tree into Texinfo files. List of tuples 171 | # (source start file, target name, title, author, 172 | # dir menu entry, description, category) 173 | texinfo_documents = [ 174 | (master_doc, 'orphanage', u'orphanage Documentation', 175 | author, 'orphanage', 'One line description of project.', 176 | 'Miscellaneous'), 177 | ] 178 | 179 | 180 | # -- Extension configuration ------------------------------------------------- 181 | 182 | # -- Options for intersphinx extension --------------------------------------- 183 | 184 | # Example configuration for intersphinx: refer to the Python standard library. 185 | intersphinx_mapping = {'https://docs.python.org/': None} 186 | --------------------------------------------------------------------------------