pax_global_header00006660000000000000000000000064150161011020014475gustar00rootroot0000000000000052 comment=734c2fcd5a735e3e7b211c13f9277340ebb632bf manhole-1.8.1/000077500000000000000000000000001501610110200131275ustar00rootroot00000000000000manhole-1.8.1/.bumpversion.cfg000066400000000000000000000013401501610110200162350ustar00rootroot00000000000000[bumpversion] current_version = 1.8.1 commit = True tag = True [bumpversion:file:setup.py] search = version='{current_version}' replace = version='{new_version}' [bumpversion:file (badge):README.rst] search = /v{current_version}.svg replace = /v{new_version}.svg [bumpversion:file (link):README.rst] search = /v{current_version}...master replace = /v{new_version}...master [bumpversion:file:docs/conf.py] search = version = release = '{current_version}' replace = version = release = '{new_version}' [bumpversion:file:src/manhole/__init__.py] search = __version__ = '{current_version}' replace = __version__ = '{new_version}' [bumpversion:file:.cookiecutterrc] search = version: {current_version} replace = version: {new_version} manhole-1.8.1/.cookiecutterrc000066400000000000000000000030261501610110200161560ustar00rootroot00000000000000# Generated by cookiepatcher, a small shim around cookiecutter (pip install cookiepatcher) default_context: c_extension_optional: 'no' c_extension_support: 'no' codacy: 'no' codacy_projectid: '[Get ID from https://app.codacy.com/app/ionelmc/python-manhole/settings]' codeclimate: 'no' codecov: 'yes' command_line_interface: 'no' command_line_interface_bin_name: '-' coveralls: 'yes' distribution_name: manhole email: contact@ionelmc.ro formatter_quote_style: single full_name: Ionel Cristian Mărieș function_name: compute github_actions: 'yes' github_actions_osx: 'yes' github_actions_windows: 'no' license: BSD 2-Clause License module_name: core package_name: manhole pre_commit: 'yes' project_name: manhole project_short_description: Manhole is in-process service that will accept unix domain socket connections and present the pypi_badge: 'yes' pypi_disable_upload: 'no' release_date: '2021-04-08' repo_hosting: github.com repo_hosting_domain: github.com repo_main_branch: master repo_name: python-manhole repo_username: ionelmc scrutinizer: 'no' setup_py_uses_setuptools_scm: 'no' sphinx_docs: 'yes' sphinx_docs_hosting: https://python-manhole.readthedocs.io/ sphinx_doctest: 'no' sphinx_theme: furo test_matrix_separate_coverage: 'yes' tests_inside_package: 'no' version: 1.8.1 version_manager: bump2version website: http://blog.ionelmc.ro year_from: '2012' year_to: '2024' manhole-1.8.1/.coveragerc000066400000000000000000000002251501610110200152470ustar00rootroot00000000000000[paths] source = src [run] branch = true source = src tests parallel = true [report] show_missing = true precision = 2 omit = *migrations* manhole-1.8.1/.editorconfig000066400000000000000000000005411501610110200156040ustar00rootroot00000000000000# see https://editorconfig.org/ root = true [*] # Use Unix-style newlines for most files (except Windows files, see below). end_of_line = lf trim_trailing_whitespace = true indent_style = space insert_final_newline = true indent_size = 4 charset = utf-8 [*.{bat,cmd,ps1}] end_of_line = crlf [*.{yml,yaml}] indent_size = 2 [*.tsv] indent_style = tab manhole-1.8.1/.github/000077500000000000000000000000001501610110200144675ustar00rootroot00000000000000manhole-1.8.1/.github/workflows/000077500000000000000000000000001501610110200165245ustar00rootroot00000000000000manhole-1.8.1/.github/workflows/github-actions.yml000066400000000000000000001323561501610110200222010ustar00rootroot00000000000000name: build on: [push, pull_request, workflow_dispatch] jobs: test: name: ${{ matrix.name }} runs-on: ${{ matrix.os }} timeout-minutes: 30 strategy: fail-fast: false matrix: include: - name: 'check' python: '3.11' toxpython: 'python3.11' tox_env: 'check' os: 'ubuntu-latest' - name: 'docs' python: '3.11' toxpython: 'python3.11' tox_env: 'docs' os: 'ubuntu-latest' - name: 'py38-normal-normal-cover (ubuntu)' python: '3.8' toxpython: 'python3.8' python_arch: 'x64' tox_env: 'py38-normal-normal-cover' os: 'ubuntu-latest' - name: 'py38-normal-normal-cover (macos)' python: '3.8' toxpython: 'python3.8' python_arch: 'arm64' tox_env: 'py38-normal-normal-cover' os: 'macos-latest' - name: 'py38-normal-normal-nocov (ubuntu)' python: '3.8' toxpython: 'python3.8' python_arch: 'x64' tox_env: 'py38-normal-normal-nocov' os: 'ubuntu-latest' - name: 'py38-normal-normal-nocov (macos)' python: '3.8' toxpython: 'python3.8' python_arch: 'arm64' tox_env: 'py38-normal-normal-nocov' os: 'macos-latest' - name: 'py38-normal-gevent-cover (ubuntu)' python: '3.8' toxpython: 'python3.8' python_arch: 'x64' tox_env: 'py38-normal-gevent-cover' os: 'ubuntu-latest' - name: 'py38-normal-gevent-cover (macos)' python: '3.8' toxpython: 'python3.8' python_arch: 'arm64' tox_env: 'py38-normal-gevent-cover' os: 'macos-latest' - name: 'py38-normal-gevent-nocov (ubuntu)' python: '3.8' toxpython: 'python3.8' python_arch: 'x64' tox_env: 'py38-normal-gevent-nocov' os: 'ubuntu-latest' - name: 'py38-normal-gevent-nocov (macos)' python: '3.8' toxpython: 'python3.8' python_arch: 'arm64' tox_env: 'py38-normal-gevent-nocov' os: 'macos-latest' - name: 'py38-normal-eventlet-cover (ubuntu)' python: '3.8' toxpython: 'python3.8' python_arch: 'x64' tox_env: 'py38-normal-eventlet-cover' os: 'ubuntu-latest' - name: 'py38-normal-eventlet-cover (macos)' python: '3.8' toxpython: 'python3.8' python_arch: 'arm64' tox_env: 'py38-normal-eventlet-cover' os: 'macos-latest' - name: 'py38-normal-eventlet-nocov (ubuntu)' python: '3.8' toxpython: 'python3.8' python_arch: 'x64' tox_env: 'py38-normal-eventlet-nocov' os: 'ubuntu-latest' - name: 'py38-normal-eventlet-nocov (macos)' python: '3.8' toxpython: 'python3.8' python_arch: 'arm64' tox_env: 'py38-normal-eventlet-nocov' os: 'macos-latest' - name: 'py38-signalfd-normal-cover (ubuntu)' python: '3.8' toxpython: 'python3.8' python_arch: 'x64' tox_env: 'py38-signalfd-normal-cover' os: 'ubuntu-latest' - name: 'py38-signalfd-normal-cover (macos)' python: '3.8' toxpython: 'python3.8' python_arch: 'arm64' tox_env: 'py38-signalfd-normal-cover' os: 'macos-latest' - name: 'py38-signalfd-normal-nocov (ubuntu)' python: '3.8' toxpython: 'python3.8' python_arch: 'x64' tox_env: 'py38-signalfd-normal-nocov' os: 'ubuntu-latest' - name: 'py38-signalfd-normal-nocov (macos)' python: '3.8' toxpython: 'python3.8' python_arch: 'arm64' tox_env: 'py38-signalfd-normal-nocov' os: 'macos-latest' - name: 'py38-signalfd-gevent-cover (ubuntu)' python: '3.8' toxpython: 'python3.8' python_arch: 'x64' tox_env: 'py38-signalfd-gevent-cover' os: 'ubuntu-latest' - name: 'py38-signalfd-gevent-cover (macos)' python: '3.8' toxpython: 'python3.8' python_arch: 'arm64' tox_env: 'py38-signalfd-gevent-cover' os: 'macos-latest' - name: 'py38-signalfd-gevent-nocov (ubuntu)' python: '3.8' toxpython: 'python3.8' python_arch: 'x64' tox_env: 'py38-signalfd-gevent-nocov' os: 'ubuntu-latest' - name: 'py38-signalfd-gevent-nocov (macos)' python: '3.8' toxpython: 'python3.8' python_arch: 'arm64' tox_env: 'py38-signalfd-gevent-nocov' os: 'macos-latest' - name: 'py38-signalfd-eventlet-cover (ubuntu)' python: '3.8' toxpython: 'python3.8' python_arch: 'x64' tox_env: 'py38-signalfd-eventlet-cover' os: 'ubuntu-latest' - name: 'py38-signalfd-eventlet-cover (macos)' python: '3.8' toxpython: 'python3.8' python_arch: 'arm64' tox_env: 'py38-signalfd-eventlet-cover' os: 'macos-latest' - name: 'py38-signalfd-eventlet-nocov (ubuntu)' python: '3.8' toxpython: 'python3.8' python_arch: 'x64' tox_env: 'py38-signalfd-eventlet-nocov' os: 'ubuntu-latest' - name: 'py38-signalfd-eventlet-nocov (macos)' python: '3.8' toxpython: 'python3.8' python_arch: 'arm64' tox_env: 'py38-signalfd-eventlet-nocov' os: 'macos-latest' - name: 'py39-normal-normal-cover (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39-normal-normal-cover' os: 'ubuntu-latest' - name: 'py39-normal-normal-cover (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' tox_env: 'py39-normal-normal-cover' os: 'macos-latest' - name: 'py39-normal-normal-nocov (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39-normal-normal-nocov' os: 'ubuntu-latest' - name: 'py39-normal-normal-nocov (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' tox_env: 'py39-normal-normal-nocov' os: 'macos-latest' - name: 'py39-normal-gevent-cover (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39-normal-gevent-cover' os: 'ubuntu-latest' - name: 'py39-normal-gevent-cover (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' tox_env: 'py39-normal-gevent-cover' os: 'macos-latest' - name: 'py39-normal-gevent-nocov (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39-normal-gevent-nocov' os: 'ubuntu-latest' - name: 'py39-normal-gevent-nocov (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' tox_env: 'py39-normal-gevent-nocov' os: 'macos-latest' - name: 'py39-normal-eventlet-cover (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39-normal-eventlet-cover' os: 'ubuntu-latest' - name: 'py39-normal-eventlet-cover (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' tox_env: 'py39-normal-eventlet-cover' os: 'macos-latest' - name: 'py39-normal-eventlet-nocov (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39-normal-eventlet-nocov' os: 'ubuntu-latest' - name: 'py39-normal-eventlet-nocov (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' tox_env: 'py39-normal-eventlet-nocov' os: 'macos-latest' - name: 'py39-signalfd-normal-cover (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39-signalfd-normal-cover' os: 'ubuntu-latest' - name: 'py39-signalfd-normal-cover (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' tox_env: 'py39-signalfd-normal-cover' os: 'macos-latest' - name: 'py39-signalfd-normal-nocov (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39-signalfd-normal-nocov' os: 'ubuntu-latest' - name: 'py39-signalfd-normal-nocov (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' tox_env: 'py39-signalfd-normal-nocov' os: 'macos-latest' - name: 'py39-signalfd-gevent-cover (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39-signalfd-gevent-cover' os: 'ubuntu-latest' - name: 'py39-signalfd-gevent-cover (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' tox_env: 'py39-signalfd-gevent-cover' os: 'macos-latest' - name: 'py39-signalfd-gevent-nocov (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39-signalfd-gevent-nocov' os: 'ubuntu-latest' - name: 'py39-signalfd-gevent-nocov (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' tox_env: 'py39-signalfd-gevent-nocov' os: 'macos-latest' - name: 'py39-signalfd-eventlet-cover (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39-signalfd-eventlet-cover' os: 'ubuntu-latest' - name: 'py39-signalfd-eventlet-cover (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' tox_env: 'py39-signalfd-eventlet-cover' os: 'macos-latest' - name: 'py39-signalfd-eventlet-nocov (ubuntu)' python: '3.9' toxpython: 'python3.9' python_arch: 'x64' tox_env: 'py39-signalfd-eventlet-nocov' os: 'ubuntu-latest' - name: 'py39-signalfd-eventlet-nocov (macos)' python: '3.9' toxpython: 'python3.9' python_arch: 'arm64' tox_env: 'py39-signalfd-eventlet-nocov' os: 'macos-latest' - name: 'py310-normal-normal-cover (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310-normal-normal-cover' os: 'ubuntu-latest' - name: 'py310-normal-normal-cover (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' tox_env: 'py310-normal-normal-cover' os: 'macos-latest' - name: 'py310-normal-normal-nocov (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310-normal-normal-nocov' os: 'ubuntu-latest' - name: 'py310-normal-normal-nocov (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' tox_env: 'py310-normal-normal-nocov' os: 'macos-latest' - name: 'py310-normal-gevent-cover (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310-normal-gevent-cover' os: 'ubuntu-latest' - name: 'py310-normal-gevent-cover (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' tox_env: 'py310-normal-gevent-cover' os: 'macos-latest' - name: 'py310-normal-gevent-nocov (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310-normal-gevent-nocov' os: 'ubuntu-latest' - name: 'py310-normal-gevent-nocov (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' tox_env: 'py310-normal-gevent-nocov' os: 'macos-latest' - name: 'py310-normal-eventlet-cover (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310-normal-eventlet-cover' os: 'ubuntu-latest' - name: 'py310-normal-eventlet-cover (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' tox_env: 'py310-normal-eventlet-cover' os: 'macos-latest' - name: 'py310-normal-eventlet-nocov (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310-normal-eventlet-nocov' os: 'ubuntu-latest' - name: 'py310-normal-eventlet-nocov (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' tox_env: 'py310-normal-eventlet-nocov' os: 'macos-latest' - name: 'py310-signalfd-normal-cover (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310-signalfd-normal-cover' os: 'ubuntu-latest' - name: 'py310-signalfd-normal-cover (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' tox_env: 'py310-signalfd-normal-cover' os: 'macos-latest' - name: 'py310-signalfd-normal-nocov (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310-signalfd-normal-nocov' os: 'ubuntu-latest' - name: 'py310-signalfd-normal-nocov (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' tox_env: 'py310-signalfd-normal-nocov' os: 'macos-latest' - name: 'py310-signalfd-gevent-cover (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310-signalfd-gevent-cover' os: 'ubuntu-latest' - name: 'py310-signalfd-gevent-cover (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' tox_env: 'py310-signalfd-gevent-cover' os: 'macos-latest' - name: 'py310-signalfd-gevent-nocov (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310-signalfd-gevent-nocov' os: 'ubuntu-latest' - name: 'py310-signalfd-gevent-nocov (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' tox_env: 'py310-signalfd-gevent-nocov' os: 'macos-latest' - name: 'py310-signalfd-eventlet-cover (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310-signalfd-eventlet-cover' os: 'ubuntu-latest' - name: 'py310-signalfd-eventlet-cover (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' tox_env: 'py310-signalfd-eventlet-cover' os: 'macos-latest' - name: 'py310-signalfd-eventlet-nocov (ubuntu)' python: '3.10' toxpython: 'python3.10' python_arch: 'x64' tox_env: 'py310-signalfd-eventlet-nocov' os: 'ubuntu-latest' - name: 'py310-signalfd-eventlet-nocov (macos)' python: '3.10' toxpython: 'python3.10' python_arch: 'arm64' tox_env: 'py310-signalfd-eventlet-nocov' os: 'macos-latest' - name: 'py311-normal-normal-cover (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311-normal-normal-cover' os: 'ubuntu-latest' - name: 'py311-normal-normal-cover (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'arm64' tox_env: 'py311-normal-normal-cover' os: 'macos-latest' - name: 'py311-normal-normal-nocov (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311-normal-normal-nocov' os: 'ubuntu-latest' - name: 'py311-normal-normal-nocov (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'arm64' tox_env: 'py311-normal-normal-nocov' os: 'macos-latest' - name: 'py311-normal-gevent-cover (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311-normal-gevent-cover' os: 'ubuntu-latest' - name: 'py311-normal-gevent-cover (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'arm64' tox_env: 'py311-normal-gevent-cover' os: 'macos-latest' - name: 'py311-normal-gevent-nocov (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311-normal-gevent-nocov' os: 'ubuntu-latest' - name: 'py311-normal-gevent-nocov (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'arm64' tox_env: 'py311-normal-gevent-nocov' os: 'macos-latest' - name: 'py311-normal-eventlet-cover (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311-normal-eventlet-cover' os: 'ubuntu-latest' - name: 'py311-normal-eventlet-cover (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'arm64' tox_env: 'py311-normal-eventlet-cover' os: 'macos-latest' - name: 'py311-normal-eventlet-nocov (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311-normal-eventlet-nocov' os: 'ubuntu-latest' - name: 'py311-normal-eventlet-nocov (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'arm64' tox_env: 'py311-normal-eventlet-nocov' os: 'macos-latest' - name: 'py311-signalfd-normal-cover (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311-signalfd-normal-cover' os: 'ubuntu-latest' - name: 'py311-signalfd-normal-cover (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'arm64' tox_env: 'py311-signalfd-normal-cover' os: 'macos-latest' - name: 'py311-signalfd-normal-nocov (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311-signalfd-normal-nocov' os: 'ubuntu-latest' - name: 'py311-signalfd-normal-nocov (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'arm64' tox_env: 'py311-signalfd-normal-nocov' os: 'macos-latest' - name: 'py311-signalfd-gevent-cover (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311-signalfd-gevent-cover' os: 'ubuntu-latest' - name: 'py311-signalfd-gevent-cover (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'arm64' tox_env: 'py311-signalfd-gevent-cover' os: 'macos-latest' - name: 'py311-signalfd-gevent-nocov (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311-signalfd-gevent-nocov' os: 'ubuntu-latest' - name: 'py311-signalfd-gevent-nocov (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'arm64' tox_env: 'py311-signalfd-gevent-nocov' os: 'macos-latest' - name: 'py311-signalfd-eventlet-cover (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311-signalfd-eventlet-cover' os: 'ubuntu-latest' - name: 'py311-signalfd-eventlet-cover (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'arm64' tox_env: 'py311-signalfd-eventlet-cover' os: 'macos-latest' - name: 'py311-signalfd-eventlet-nocov (ubuntu)' python: '3.11' toxpython: 'python3.11' python_arch: 'x64' tox_env: 'py311-signalfd-eventlet-nocov' os: 'ubuntu-latest' - name: 'py311-signalfd-eventlet-nocov (macos)' python: '3.11' toxpython: 'python3.11' python_arch: 'arm64' tox_env: 'py311-signalfd-eventlet-nocov' os: 'macos-latest' - name: 'py312-normal-normal-cover (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312-normal-normal-cover' os: 'ubuntu-latest' - name: 'py312-normal-normal-cover (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312-normal-normal-cover' os: 'macos-latest' - name: 'py312-normal-normal-nocov (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312-normal-normal-nocov' os: 'ubuntu-latest' - name: 'py312-normal-normal-nocov (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312-normal-normal-nocov' os: 'macos-latest' - name: 'py312-normal-gevent-cover (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312-normal-gevent-cover' os: 'ubuntu-latest' - name: 'py312-normal-gevent-cover (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312-normal-gevent-cover' os: 'macos-latest' - name: 'py312-normal-gevent-nocov (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312-normal-gevent-nocov' os: 'ubuntu-latest' - name: 'py312-normal-gevent-nocov (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312-normal-gevent-nocov' os: 'macos-latest' - name: 'py312-normal-eventlet-cover (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312-normal-eventlet-cover' os: 'ubuntu-latest' - name: 'py312-normal-eventlet-cover (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312-normal-eventlet-cover' os: 'macos-latest' - name: 'py312-normal-eventlet-nocov (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312-normal-eventlet-nocov' os: 'ubuntu-latest' - name: 'py312-normal-eventlet-nocov (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312-normal-eventlet-nocov' os: 'macos-latest' - name: 'py312-signalfd-normal-cover (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312-signalfd-normal-cover' os: 'ubuntu-latest' - name: 'py312-signalfd-normal-cover (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312-signalfd-normal-cover' os: 'macos-latest' - name: 'py312-signalfd-normal-nocov (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312-signalfd-normal-nocov' os: 'ubuntu-latest' - name: 'py312-signalfd-normal-nocov (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312-signalfd-normal-nocov' os: 'macos-latest' - name: 'py312-signalfd-gevent-cover (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312-signalfd-gevent-cover' os: 'ubuntu-latest' - name: 'py312-signalfd-gevent-cover (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312-signalfd-gevent-cover' os: 'macos-latest' - name: 'py312-signalfd-gevent-nocov (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312-signalfd-gevent-nocov' os: 'ubuntu-latest' - name: 'py312-signalfd-gevent-nocov (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312-signalfd-gevent-nocov' os: 'macos-latest' - name: 'py312-signalfd-eventlet-cover (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312-signalfd-eventlet-cover' os: 'ubuntu-latest' - name: 'py312-signalfd-eventlet-cover (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312-signalfd-eventlet-cover' os: 'macos-latest' - name: 'py312-signalfd-eventlet-nocov (ubuntu)' python: '3.12' toxpython: 'python3.12' python_arch: 'x64' tox_env: 'py312-signalfd-eventlet-nocov' os: 'ubuntu-latest' - name: 'py312-signalfd-eventlet-nocov (macos)' python: '3.12' toxpython: 'python3.12' python_arch: 'arm64' tox_env: 'py312-signalfd-eventlet-nocov' os: 'macos-latest' - name: 'pypy38-normal-normal-cover (ubuntu)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'x64' tox_env: 'pypy38-normal-normal-cover' os: 'ubuntu-latest' - name: 'pypy38-normal-normal-cover (macos)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'arm64' tox_env: 'pypy38-normal-normal-cover' os: 'macos-latest' - name: 'pypy38-normal-normal-nocov (ubuntu)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'x64' tox_env: 'pypy38-normal-normal-nocov' os: 'ubuntu-latest' - name: 'pypy38-normal-normal-nocov (macos)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'arm64' tox_env: 'pypy38-normal-normal-nocov' os: 'macos-latest' - name: 'pypy38-normal-gevent-cover (ubuntu)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'x64' tox_env: 'pypy38-normal-gevent-cover' os: 'ubuntu-latest' - name: 'pypy38-normal-gevent-cover (macos)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'arm64' tox_env: 'pypy38-normal-gevent-cover' os: 'macos-latest' - name: 'pypy38-normal-gevent-nocov (ubuntu)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'x64' tox_env: 'pypy38-normal-gevent-nocov' os: 'ubuntu-latest' - name: 'pypy38-normal-gevent-nocov (macos)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'arm64' tox_env: 'pypy38-normal-gevent-nocov' os: 'macos-latest' - name: 'pypy38-normal-eventlet-cover (ubuntu)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'x64' tox_env: 'pypy38-normal-eventlet-cover' os: 'ubuntu-latest' - name: 'pypy38-normal-eventlet-cover (macos)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'arm64' tox_env: 'pypy38-normal-eventlet-cover' os: 'macos-latest' - name: 'pypy38-normal-eventlet-nocov (ubuntu)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'x64' tox_env: 'pypy38-normal-eventlet-nocov' os: 'ubuntu-latest' - name: 'pypy38-normal-eventlet-nocov (macos)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'arm64' tox_env: 'pypy38-normal-eventlet-nocov' os: 'macos-latest' - name: 'pypy38-signalfd-normal-cover (ubuntu)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'x64' tox_env: 'pypy38-signalfd-normal-cover' os: 'ubuntu-latest' - name: 'pypy38-signalfd-normal-cover (macos)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'arm64' tox_env: 'pypy38-signalfd-normal-cover' os: 'macos-latest' - name: 'pypy38-signalfd-normal-nocov (ubuntu)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'x64' tox_env: 'pypy38-signalfd-normal-nocov' os: 'ubuntu-latest' - name: 'pypy38-signalfd-normal-nocov (macos)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'arm64' tox_env: 'pypy38-signalfd-normal-nocov' os: 'macos-latest' - name: 'pypy38-signalfd-gevent-cover (ubuntu)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'x64' tox_env: 'pypy38-signalfd-gevent-cover' os: 'ubuntu-latest' - name: 'pypy38-signalfd-gevent-cover (macos)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'arm64' tox_env: 'pypy38-signalfd-gevent-cover' os: 'macos-latest' - name: 'pypy38-signalfd-gevent-nocov (ubuntu)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'x64' tox_env: 'pypy38-signalfd-gevent-nocov' os: 'ubuntu-latest' - name: 'pypy38-signalfd-gevent-nocov (macos)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'arm64' tox_env: 'pypy38-signalfd-gevent-nocov' os: 'macos-latest' - name: 'pypy38-signalfd-eventlet-cover (ubuntu)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'x64' tox_env: 'pypy38-signalfd-eventlet-cover' os: 'ubuntu-latest' - name: 'pypy38-signalfd-eventlet-cover (macos)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'arm64' tox_env: 'pypy38-signalfd-eventlet-cover' os: 'macos-latest' - name: 'pypy38-signalfd-eventlet-nocov (ubuntu)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'x64' tox_env: 'pypy38-signalfd-eventlet-nocov' os: 'ubuntu-latest' - name: 'pypy38-signalfd-eventlet-nocov (macos)' python: 'pypy-3.8' toxpython: 'pypy3.8' python_arch: 'arm64' tox_env: 'pypy38-signalfd-eventlet-nocov' os: 'macos-latest' - name: 'pypy39-normal-normal-cover (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39-normal-normal-cover' os: 'ubuntu-latest' - name: 'pypy39-normal-normal-cover (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' tox_env: 'pypy39-normal-normal-cover' os: 'macos-latest' - name: 'pypy39-normal-normal-nocov (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39-normal-normal-nocov' os: 'ubuntu-latest' - name: 'pypy39-normal-normal-nocov (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' tox_env: 'pypy39-normal-normal-nocov' os: 'macos-latest' - name: 'pypy39-normal-gevent-cover (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39-normal-gevent-cover' os: 'ubuntu-latest' - name: 'pypy39-normal-gevent-cover (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' tox_env: 'pypy39-normal-gevent-cover' os: 'macos-latest' - name: 'pypy39-normal-gevent-nocov (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39-normal-gevent-nocov' os: 'ubuntu-latest' - name: 'pypy39-normal-gevent-nocov (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' tox_env: 'pypy39-normal-gevent-nocov' os: 'macos-latest' - name: 'pypy39-normal-eventlet-cover (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39-normal-eventlet-cover' os: 'ubuntu-latest' - name: 'pypy39-normal-eventlet-cover (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' tox_env: 'pypy39-normal-eventlet-cover' os: 'macos-latest' - name: 'pypy39-normal-eventlet-nocov (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39-normal-eventlet-nocov' os: 'ubuntu-latest' - name: 'pypy39-normal-eventlet-nocov (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' tox_env: 'pypy39-normal-eventlet-nocov' os: 'macos-latest' - name: 'pypy39-signalfd-normal-cover (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39-signalfd-normal-cover' os: 'ubuntu-latest' - name: 'pypy39-signalfd-normal-cover (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' tox_env: 'pypy39-signalfd-normal-cover' os: 'macos-latest' - name: 'pypy39-signalfd-normal-nocov (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39-signalfd-normal-nocov' os: 'ubuntu-latest' - name: 'pypy39-signalfd-normal-nocov (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' tox_env: 'pypy39-signalfd-normal-nocov' os: 'macos-latest' - name: 'pypy39-signalfd-gevent-cover (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39-signalfd-gevent-cover' os: 'ubuntu-latest' - name: 'pypy39-signalfd-gevent-cover (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' tox_env: 'pypy39-signalfd-gevent-cover' os: 'macos-latest' - name: 'pypy39-signalfd-gevent-nocov (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39-signalfd-gevent-nocov' os: 'ubuntu-latest' - name: 'pypy39-signalfd-gevent-nocov (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' tox_env: 'pypy39-signalfd-gevent-nocov' os: 'macos-latest' - name: 'pypy39-signalfd-eventlet-cover (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39-signalfd-eventlet-cover' os: 'ubuntu-latest' - name: 'pypy39-signalfd-eventlet-cover (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' tox_env: 'pypy39-signalfd-eventlet-cover' os: 'macos-latest' - name: 'pypy39-signalfd-eventlet-nocov (ubuntu)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'x64' tox_env: 'pypy39-signalfd-eventlet-nocov' os: 'ubuntu-latest' - name: 'pypy39-signalfd-eventlet-nocov (macos)' python: 'pypy-3.9' toxpython: 'pypy3.9' python_arch: 'arm64' tox_env: 'pypy39-signalfd-eventlet-nocov' os: 'macos-latest' - name: 'pypy310-normal-normal-cover (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310-normal-normal-cover' os: 'ubuntu-latest' - name: 'pypy310-normal-normal-cover (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' tox_env: 'pypy310-normal-normal-cover' os: 'macos-latest' - name: 'pypy310-normal-normal-nocov (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310-normal-normal-nocov' os: 'ubuntu-latest' - name: 'pypy310-normal-normal-nocov (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' tox_env: 'pypy310-normal-normal-nocov' os: 'macos-latest' - name: 'pypy310-normal-gevent-cover (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310-normal-gevent-cover' os: 'ubuntu-latest' - name: 'pypy310-normal-gevent-cover (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' tox_env: 'pypy310-normal-gevent-cover' os: 'macos-latest' - name: 'pypy310-normal-gevent-nocov (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310-normal-gevent-nocov' os: 'ubuntu-latest' - name: 'pypy310-normal-gevent-nocov (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' tox_env: 'pypy310-normal-gevent-nocov' os: 'macos-latest' - name: 'pypy310-normal-eventlet-cover (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310-normal-eventlet-cover' os: 'ubuntu-latest' - name: 'pypy310-normal-eventlet-cover (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' tox_env: 'pypy310-normal-eventlet-cover' os: 'macos-latest' - name: 'pypy310-normal-eventlet-nocov (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310-normal-eventlet-nocov' os: 'ubuntu-latest' - name: 'pypy310-normal-eventlet-nocov (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' tox_env: 'pypy310-normal-eventlet-nocov' os: 'macos-latest' - name: 'pypy310-signalfd-normal-cover (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310-signalfd-normal-cover' os: 'ubuntu-latest' - name: 'pypy310-signalfd-normal-cover (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' tox_env: 'pypy310-signalfd-normal-cover' os: 'macos-latest' - name: 'pypy310-signalfd-normal-nocov (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310-signalfd-normal-nocov' os: 'ubuntu-latest' - name: 'pypy310-signalfd-normal-nocov (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' tox_env: 'pypy310-signalfd-normal-nocov' os: 'macos-latest' - name: 'pypy310-signalfd-gevent-cover (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310-signalfd-gevent-cover' os: 'ubuntu-latest' - name: 'pypy310-signalfd-gevent-cover (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' tox_env: 'pypy310-signalfd-gevent-cover' os: 'macos-latest' - name: 'pypy310-signalfd-gevent-nocov (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310-signalfd-gevent-nocov' os: 'ubuntu-latest' - name: 'pypy310-signalfd-gevent-nocov (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' tox_env: 'pypy310-signalfd-gevent-nocov' os: 'macos-latest' - name: 'pypy310-signalfd-eventlet-cover (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310-signalfd-eventlet-cover' os: 'ubuntu-latest' - name: 'pypy310-signalfd-eventlet-cover (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' tox_env: 'pypy310-signalfd-eventlet-cover' os: 'macos-latest' - name: 'pypy310-signalfd-eventlet-nocov (ubuntu)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'x64' tox_env: 'pypy310-signalfd-eventlet-nocov' os: 'ubuntu-latest' - name: 'pypy310-signalfd-eventlet-nocov (macos)' python: 'pypy-3.10' toxpython: 'pypy3.10' python_arch: 'arm64' tox_env: 'pypy310-signalfd-eventlet-nocov' os: 'macos-latest' steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} architecture: ${{ matrix.python_arch }} - name: install dependencies run: | python -mpip install --progress-bar=off -r ci/requirements.txt virtualenv --version pip --version tox --version pip list --format=freeze - name: test env: TOXPYTHON: '${{ matrix.toxpython }}' MANHOLE_TEST_TIMEOUT: 60 run: > tox -e ${{ matrix.tox_env }} -v finish: needs: test if: ${{ always() }} runs-on: ubuntu-latest steps: - uses: coverallsapp/github-action@v2 with: parallel-finished: true - uses: codecov/codecov-action@v3 with: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} manhole-1.8.1/.gitignore000066400000000000000000000012461501610110200151220ustar00rootroot00000000000000*.py[cod] __pycache__ # Temp files .*.sw[po] *~ *.bak .DS_Store # C extensions *.so # Build and package files *.egg *.egg-info .bootstrap .build .cache .eggs .env .installed.cfg .ve bin build develop-eggs dist eggs lib lib64 parts pip-wheel-metadata/ pyvenv*/ sdist var venv*/ wheelhouse # Installer logs pip-log.txt # Unit test / coverage reports .benchmarks .coverage .coverage.* .pytest .pytest_cache/ .tox coverage.xml htmlcov nosetests.xml # Translations *.mo # Buildout .mr.developer.cfg # IDE project files *.iml *.komodoproject .idea .project .pydevproject .vscode # Complexity output/*.html output/*/index.html # Sphinx docs/_build # Mypy Cache .mypy_cache/ manhole-1.8.1/.pre-commit-config.yaml000066400000000000000000000012021501610110200174030ustar00rootroot00000000000000# To install the git pre-commit hooks run: # pre-commit install --install-hooks # To update the versions: # pre-commit autoupdate exclude: '^(\.tox|ci/templates|\.bumpversion\.cfg)(/|$)' # Note the order is intentional to avoid multiple passes of the hooks repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.5.0 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix, --show-fixes, --unsafe-fixes] - id: ruff-format - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: debug-statements manhole-1.8.1/.readthedocs.yml000066400000000000000000000004321501610110200162140ustar00rootroot00000000000000# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 sphinx: configuration: docs/conf.py formats: all build: os: ubuntu-22.04 tools: python: "3" python: install: - requirements: docs/requirements.txt - method: pip path: . manhole-1.8.1/AUTHORS.rst000066400000000000000000000004421501610110200150060ustar00rootroot00000000000000 Authors ======= * Ionel Cristian Mărieș - http://blog.ionelmc.ro * Saulius Menkevičius - https://github.com/razzmatazz * Nir Soffer - https://github.com/nirs * Jesús Cea - https://github.com/jcea * "honnix" - https://github.com/honnix * Anton Ryzhov - https://github.com/anton-ryzhov manhole-1.8.1/CHANGELOG.rst000066400000000000000000000103561501610110200151550ustar00rootroot00000000000000 Changelog ========= 1.8.1 (2024-07-24) ------------------ * Fixed buffering issue on Python 3.11. See :issue:`66`. * Cleaned up some packaging/test problems. * Removed more leftover Python 2 code. * Fixed license metadata. See: :issue:`68`. 1.8.0 (2021-04-08) ------------------ * Simplified connection closing code. Contributed by Anton Ryzhov in :pr:`62`. * Made connection shutdown in ``manhole-cli`` more graceful. Contributed by Anton Ryzhov in :pr:`63`. 1.7.0 (2021-03-22) ------------------ * Fixed memory leak via ``sys.last_type``, ``sys.last_value``, ``sys.last_traceback``. Contributed by Anton Ryzhov in :pr:`59`. * Fixed a bunch of double-close bugs and simplified stream handler code. Contributed by Anton Ryzhov in :pr:`58`. * Loosen up ``pid`` argument parsing in ``manhole-cli`` to allow using paths with any prefix (not just ``/tmp``). 1.6.0 (2019-01-19) ------------------ * Testing improvements (changed some skips to xfail, added osx in Travis). * Fixed long standing Python 2.7 bug where ``sys.getfilesystemencoding()`` would be broken after installing a threaded manhole. See :issue:`51`. * Dropped support for Python 2.6, 3.3 and 3.4. * Fixed handling when ``socket.setdefaulttimeout()`` is used. Contributed by "honnix" in :pr:`53`. * Fixed some typos. Contributed by Jesús Cea in :pr:`43`. * Fixed handling in ``manhole-cli`` so that timeout is actually seconds and not milliseconds. Contributed by Nir Soffer in :pr:`45`. * Cleaned up useless polling options in ``manhole-cli``. Contributed by Nir Soffer in :pr:`46`. * Documented and implemented a solution for using Manhole with Eventlet. See :issue:`49`. 1.5.0 (2017-08-31) ------------------ * Added two string aliases for ``connection_handler`` option. Now you can conveniently use ``connection_handler="exec"``. * Improved ``handle_connection_exec``. It now has a clean way to exit (``exit()``) and properly closes the socket. 1.4.0 (2017-08-29) ------------------ * Added the ``connection_handler`` install option. Default value is ``manhole.handle_connection_repl``, and alternate ``manhole.handle_connection_exec`` is provided (very simple: no output redirection, no stacktrace dumping). * Dropped Python 3.2 from the test grid. It may work but it's a huge pain to support (pip/pytest don't support it anymore). * Added Python 3.5 and 3.6 in the test grid. * Fixed issues with piping to ``manhole-cli``. Now ``echo foobar | manhole-cli`` will wait 1 second for output from manhole (you can customize this with the ``--timeout`` option). * Fixed issues with newer PyPy (caused by gevent/eventlet socket unwrapping). 1.3.0 (2015-09-03) ------------------ * Allowed Manhole to be configured without any thread or activation (in case you want to manually activate). * Added an example and tests for using Manhole with uWSGi. * Fixed error handling in ``manhole-cli`` on Python 3 (exc vars don't leak anymore). * Fixed support for running in gevent/eventlet-using apps on Python 3 (now that they support Python 3). * Allowed reinstalling the manhole (in non-``strict`` mode). Previous install is undone. 1.2.0 (2015-07-06) ------------------ * Changed ``manhole-cli``: * Won't spam the terminal with errors if socket file doesn't exist. * Allowed sending any signal (new ``--signal`` argument). * Fixed some validation issues for the ``PID`` argument. 1.1.0 (2015-06-06) ------------------ * Added support for installing the manhole via the ``PYTHONMANHOLE`` environment variable. * Added a ``strict`` install option. Set it to false to avoid getting the ``AlreadyInstalled`` exception. * Added a ``manhole-cli`` script that emulates ``socat readline unix-connect:/tmp/manhole-1234``. 1.0.0 (2014-10-13) ------------------ * Added ``socket_path`` install option (contributed by `Nir Soffer`_). * Added ``reinstall_delay`` install option. * Added ``locals`` install option (contributed by `Nir Soffer`_). * Added ``redirect_stderr`` install option (contributed by `Nir Soffer`_). * Lots of internals cleanup (contributed by `Nir Soffer`_). 0.6.2 (2014-04-28) ------------------ * Fix OS X regression. 0.6.1 (2014-04-28) ------------------ * Support for OS X (contributed by `Saulius Menkevičius`_). .. _Saulius Menkevičius: https://github.com/razzmatazz .. _Nir Soffer: https://github.com/nirs manhole-1.8.1/CONTRIBUTING.rst000066400000000000000000000044551501610110200156000ustar00rootroot00000000000000============ Contributing ============ Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. Bug reports =========== When `reporting a bug `_ please include: * Your operating system name and version. * Any details about your local setup that might be helpful in troubleshooting. * Detailed steps to reproduce the bug. Documentation improvements ========================== manhole could always use more documentation, whether as part of the official manhole docs, in docstrings, or even on the web in blog posts, articles, and such. Feature requests and feedback ============================= The best way to send feedback is to file an issue at https://github.com/ionelmc/python-manhole/issues. If you are proposing a feature: * Explain in detail how it would work. * Keep the scope as narrow as possible, to make it easier to implement. * Remember that this is a volunteer-driven project, and that code contributions are welcome :) Development =========== To set up `python-manhole` for local development: 1. Fork `python-manhole `_ (look for the "Fork" button). 2. Clone your fork locally:: git clone git@github.com:YOURGITHUBNAME/python-manhole.git 3. Create a branch for local development:: git checkout -b name-of-your-bugfix-or-feature Now you can make your changes locally. 4. When you're done making changes run all the checks and docs builder with one command:: tox 5. Commit your changes and push your branch to GitHub:: git add . git commit -m "Your detailed description of your changes." git push origin name-of-your-bugfix-or-feature 6. Submit a pull request through the GitHub website. Pull Request Guidelines ----------------------- If you need some code review or feedback while you're developing the code just make the pull request. For merging, you should: 1. Include passing tests (run ``tox``). 2. Update documentation when there's new API, functionality etc. 3. Add a note to ``CHANGELOG.rst`` about the changes. 4. Add yourself to ``AUTHORS.rst``. Tips ---- To run a subset of tests:: tox -e envname -- pytest -k test_myfeature To run all the test environments in *parallel*:: tox -p auto manhole-1.8.1/LICENSE000066400000000000000000000024621501610110200141400ustar00rootroot00000000000000BSD 2-Clause License Copyright (c) 2012-2024, Ionel Cristian Mărieș. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. manhole-1.8.1/MANIFEST.in000066400000000000000000000006531501610110200146710ustar00rootroot00000000000000graft docs graft src graft ci graft tests include .bumpversion.cfg include .cookiecutterrc include .coveragerc include .editorconfig include .github/workflows/github-actions.yml include .pre-commit-config.yaml include .readthedocs.yml include pytest.ini include tox.ini include AUTHORS.rst include CHANGELOG.rst include CONTRIBUTING.rst include LICENSE include README.rst global-exclude *.py[cod] __pycache__/* *.so *.dylib manhole-1.8.1/PKG-INFO000066400000000000000000000326651501610110200142400ustar00rootroot00000000000000Metadata-Version: 2.1 Name: manhole Version: 1.8.1 Summary: Manhole is in-process service that will accept unix domain socket connections and present thestacktraces for all threads and an interactive prompt. Home-page: https://github.com/ionelmc/python-manhole Author: Ionel Cristian Mărieș Author-email: contact@ionelmc.ro License: BSD-2-Clause Project-URL: Documentation, https://python-manhole.readthedocs.io/ Project-URL: Changelog, https://python-manhole.readthedocs.io/en/latest/changelog.html Project-URL: Issue Tracker, https://github.com/ionelmc/python-manhole/issues Keywords: debugging,manhole,thread,socket,unix domain socket Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: Unix Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Debuggers Classifier: Topic :: Utilities Classifier: Topic :: System :: Monitoring Classifier: Topic :: System :: Networking Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Utilities Requires-Python: >=3.8 License-File: LICENSE License-File: AUTHORS.rst ======== Overview ======== Features ======== * Uses unix domain sockets, only root or same effective user can connect. * Can run the connection in a thread or in a signal handler (see ``oneshot_on`` option). * Can start the thread listening for connections from a signal handler (see ``activate_on`` option) * Compatible with apps that fork, reinstalls the Manhole thread after fork - had to monkeypatch os.fork/os.forkpty for this. * Compatible with gevent and eventlet with some limitations - you need to either: * Use ``oneshot_on``, *or* * Disable thread monkeypatching (eg: ``gevent.monkey.patch_all(thread=False)``, ``eventlet.monkey_patch(thread=False)`` Note: on eventlet `you might `_ need to setup the hub first to prevent circular import problems: .. sourcecode:: python import eventlet eventlet.hubs.get_hub() # do this first eventlet.monkey_patch(thread=False) * The thread is compatible with apps that use signalfd (will mask all signals for the Manhole threads). Options ------- .. code-block:: python manhole.install( verbose=True, verbose_destination=2, patch_fork=True, activate_on=None, oneshot_on=None, sigmask=manhole.ALL_SIGNALS, socket_path=None, reinstall_delay=0.5, locals=None, strict=True, ) * ``verbose`` - Set it to ``False`` to squelch the logging. * ``verbose_destination`` - Destination for verbose messages. Set it to a file descriptor or handle. Default is unbuffered stderr (stderr ``2`` file descriptor). * ``patch_fork`` - Set it to ``False`` if you don't want your ``os.fork`` and ``os.forkpy`` monkeypatched * ``activate_on`` - Set to ``"USR1"``, ``"USR2"`` or some other signal name, or a number if you want the Manhole thread to start when this signal is sent. This is desirable in case you don't want the thread active all the time. * ``thread`` - Set to ``True`` to start the always-on ManholeThread. Default: ``True``. Automatically switched to ``False`` if ``oneshot_on`` or ``activate_on`` are used. * ``oneshot_on`` - Set to ``"USR1"``, ``"USR2"`` or some other signal name, or a number if you want the Manhole to listen for connection in the signal handler. This is desireable in case you don't want threads at all. * ``sigmask`` - Will set the signal mask to the given list (using ``signalfd.sigprocmask``). No action is done if ``signalfd`` is not importable. **NOTE**: This is done so that the Manhole thread doesn't *steal* any signals; Normally that is fine because Python will force all the signal handling to be run in the main thread but signalfd doesn't. * ``socket_path`` - Use a specific path for the unix domain socket (instead of ``/tmp/manhole-``). This disables ``patch_fork`` as children cannot reuse the same path. * ``reinstall_delay`` - Delay the unix domain socket creation *reinstall_delay* seconds. This alleviates cleanup failures when using fork+exec patterns. * ``locals`` - Names to add to manhole interactive shell locals. * ``daemon_connection`` - The connection thread is daemonic (dies on app exit). Default: ``False``. * ``redirect_stderr`` - Redirect output from stderr to manhole console. Default: ``True``. * ``strict`` - If ``True`` then ``AlreadyInstalled`` will be raised when attempting to install manhole twice. Default: ``True``. Environment variable installation --------------------------------- Manhole can be installed via the ``PYTHONMANHOLE`` environment variable. This:: PYTHONMANHOLE='' python yourapp.py Is equivalent to having this in ``yourapp.py``:: import manhole manhole.install() Any extra text in the environment variable is passed to ``manhole.install()``. Example:: PYTHONMANHOLE='oneshot_on="USR2"' python yourapp.py What happens when you actually connect to the socket ---------------------------------------------------- 1. Credentials are checked (if it's same user or root) 2. ``sys.__std*__``/``sys.std*`` are redirected to the UDS 3. Stacktraces for each thread are written to the UDS 4. REPL is started so you can fiddle with the process Known issues ============ * Using threads and file handle (not raw file descriptor) ``verbose_destination`` can cause deadlocks. See bug reports: `PyPy `_ and `Python 3.4 `_. SIGTERM and socket cleanup -------------------------- By default Python doesn't call the ``atexit`` callbacks with the default SIGTERM handling. This makes manhole leave stray socket files around. If this is undesirable you should install a custom SIGTERM handler so ``atexit`` is properly invoked. Example: .. code-block:: python import signal import sys def handle_sigterm(signo, frame): sys.exit(128 + signo) # this will raise SystemExit and cause atexit to be called signal.signal(signal.SIGTERM, handle_sigterm) Using Manhole with uWSGI ------------------------ Because uWSGI overrides signal handling Manhole is a bit more tricky to setup. One way is to use "uWSGI signals" (not the POSIX signals) and have the workers check a file for the pid you want to open the Manhole in. Stick something this in your WSGI application file: .. sourcecode:: python from __future__ import print_function import sys import os import manhole stack_dump_file = '/tmp/manhole-pid' uwsgi_signal_number = 17 try: import uwsgi if not os.path.exists(stack_dump_file): open(stack_dump_file, 'w') def open_manhole(dummy_signum): with open(stack_dump_file, 'r') as fh: pid = fh.read().strip() if pid == str(os.getpid()): inst = manhole.install(strict=False, thread=False) inst.handle_oneshot(dummy_signum, dummy_signum) uwsgi.register_signal(uwsgi_signal_number, 'workers', open_manhole) uwsgi.add_file_monitor(uwsgi_signal_number, stack_dump_file) print("Listening for stack mahole requests via %r" % (stack_dump_file,), file=sys.stderr) except ImportError: print("Not running under uwsgi; unable to configure manhole trigger", file=sys.stderr) except IOError: print("IOError creating manhole trigger %r" % (stack_dump_file,), file=sys.stderr) # somewhere bellow you'd have something like from django.core.wsgi import get_wsgi_application application = get_wsgi_application() # or def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', '2')]) yield b'OK' To open the Manhole just run `echo 1234 > /tmp/manhole-pid` and then `manhole-cli 1234`. Requirements ============ :OS: Linux, OS X :Runtime: Python 2.7, 3.4, 3.5, 3.6 or PyPy Similar projects ================ * Twisted's `manhole `__ - it has colors and server-side history. * `wsgi-shell `_ - spawns a thread. * `pyrasite `_ - uses gdb to inject code. * `pydbattach `_ - uses gdb to inject code. * `pystuck `_ - very similar, uses `rpyc `_ for communication. * `pyringe `_ - uses gdb to inject code, more reliable, but relies on `dbg` python builds unfortunatelly. * `pdb-clone `_ - uses gdb to inject code, with a `different strategy `_. Changelog ========= 1.8.1 (2024-07-24) ------------------ * Fixed buffering issue on Python 3.11. See ``66``. * Cleaned up some packaging/test problems. * Removed more leftover Python 2 code. * Fixed license metadata. See: ``68``. 1.8.0 (2021-04-08) ------------------ * Simplified connection closing code. Contributed by Anton Ryzhov in ``62``. * Made connection shutdown in ``manhole-cli`` more graceful. Contributed by Anton Ryzhov in ``63``. 1.7.0 (2021-03-22) ------------------ * Fixed memory leak via ``sys.last_type``, ``sys.last_value``, ``sys.last_traceback``. Contributed by Anton Ryzhov in ``59``. * Fixed a bunch of double-close bugs and simplified stream handler code. Contributed by Anton Ryzhov in ``58``. * Loosen up ``pid`` argument parsing in ``manhole-cli`` to allow using paths with any prefix (not just ``/tmp``). 1.6.0 (2019-01-19) ------------------ * Testing improvements (changed some skips to xfail, added osx in Travis). * Fixed long standing Python 2.7 bug where ``sys.getfilesystemencoding()`` would be broken after installing a threaded manhole. See ``51``. * Dropped support for Python 2.6, 3.3 and 3.4. * Fixed handling when ``socket.setdefaulttimeout()`` is used. Contributed by "honnix" in ``53``. * Fixed some typos. Contributed by Jesús Cea in ``43``. * Fixed handling in ``manhole-cli`` so that timeout is actually seconds and not milliseconds. Contributed by Nir Soffer in ``45``. * Cleaned up useless polling options in ``manhole-cli``. Contributed by Nir Soffer in ``46``. * Documented and implemented a solution for using Manhole with Eventlet. See ``49``. 1.5.0 (2017-08-31) ------------------ * Added two string aliases for ``connection_handler`` option. Now you can conveniently use ``connection_handler="exec"``. * Improved ``handle_connection_exec``. It now has a clean way to exit (``exit()``) and properly closes the socket. 1.4.0 (2017-08-29) ------------------ * Added the ``connection_handler`` install option. Default value is ``manhole.handle_connection_repl``, and alternate ``manhole.handle_connection_exec`` is provided (very simple: no output redirection, no stacktrace dumping). * Dropped Python 3.2 from the test grid. It may work but it's a huge pain to support (pip/pytest don't support it anymore). * Added Python 3.5 and 3.6 in the test grid. * Fixed issues with piping to ``manhole-cli``. Now ``echo foobar | manhole-cli`` will wait 1 second for output from manhole (you can customize this with the ``--timeout`` option). * Fixed issues with newer PyPy (caused by gevent/eventlet socket unwrapping). 1.3.0 (2015-09-03) ------------------ * Allowed Manhole to be configured without any thread or activation (in case you want to manually activate). * Added an example and tests for using Manhole with uWSGi. * Fixed error handling in ``manhole-cli`` on Python 3 (exc vars don't leak anymore). * Fixed support for running in gevent/eventlet-using apps on Python 3 (now that they support Python 3). * Allowed reinstalling the manhole (in non-``strict`` mode). Previous install is undone. 1.2.0 (2015-07-06) ------------------ * Changed ``manhole-cli``: * Won't spam the terminal with errors if socket file doesn't exist. * Allowed sending any signal (new ``--signal`` argument). * Fixed some validation issues for the ``PID`` argument. 1.1.0 (2015-06-06) ------------------ * Added support for installing the manhole via the ``PYTHONMANHOLE`` environment variable. * Added a ``strict`` install option. Set it to false to avoid getting the ``AlreadyInstalled`` exception. * Added a ``manhole-cli`` script that emulates ``socat readline unix-connect:/tmp/manhole-1234``. 1.0.0 (2014-10-13) ------------------ * Added ``socket_path`` install option (contributed by `Nir Soffer`_). * Added ``reinstall_delay`` install option. * Added ``locals`` install option (contributed by `Nir Soffer`_). * Added ``redirect_stderr`` install option (contributed by `Nir Soffer`_). * Lots of internals cleanup (contributed by `Nir Soffer`_). 0.6.2 (2014-04-28) ------------------ * Fix OS X regression. 0.6.1 (2014-04-28) ------------------ * Support for OS X (contributed by `Saulius Menkevičius`_). .. _Saulius Menkevičius: https://github.com/razzmatazz .. _Nir Soffer: https://github.com/nirs manhole-1.8.1/README.rst000066400000000000000000000300471501610110200146220ustar00rootroot00000000000000======== Overview ======== .. start-badges .. list-table:: :stub-columns: 1 * - docs - |docs| * - tests - |github-actions| |coveralls| |codecov| * - package - |version| |wheel| |supported-versions| |supported-implementations| |commits-since| .. |docs| image:: https://readthedocs.org/projects/python-manhole/badge/?style=flat :target: https://readthedocs.org/projects/python-manhole/ :alt: Documentation Status .. |github-actions| image:: https://github.com/ionelmc/python-manhole/actions/workflows/github-actions.yml/badge.svg :alt: GitHub Actions Build Status :target: https://github.com/ionelmc/python-manhole/actions .. |coveralls| image:: https://coveralls.io/repos/github/ionelmc/python-manhole/badge.svg?branch=master :alt: Coverage Status :target: https://coveralls.io/github/ionelmc/python-manhole?branch=master .. |codecov| image:: https://codecov.io/gh/ionelmc/python-manhole/branch/master/graphs/badge.svg?branch=master :alt: Coverage Status :target: https://app.codecov.io/github/ionelmc/python-manhole .. |version| image:: https://img.shields.io/pypi/v/manhole.svg :alt: PyPI Package latest release :target: https://pypi.org/project/manhole .. |wheel| image:: https://img.shields.io/pypi/wheel/manhole.svg :alt: PyPI Wheel :target: https://pypi.org/project/manhole .. |supported-versions| image:: https://img.shields.io/pypi/pyversions/manhole.svg :alt: Supported versions :target: https://pypi.org/project/manhole .. |supported-implementations| image:: https://img.shields.io/pypi/implementation/manhole.svg :alt: Supported implementations :target: https://pypi.org/project/manhole .. |commits-since| image:: https://img.shields.io/github/commits-since/ionelmc/python-manhole/v1.8.1.svg :alt: Commits since latest release :target: https://github.com/ionelmc/python-manhole/compare/v1.8.1...master .. end-badges Manhole is in-process service that will accept unix domain socket connections and present the stacktraces for all threads and an interactive prompt. It can either work as a python daemon thread waiting for connections at all times *or* a signal handler (stopping your application and waiting for a connection). Access to the socket is restricted to the application's effective user id or root. This is just like Twisted's `manhole `__. It's simpler (no dependencies), it only runs on Unix domain sockets (in contrast to Twisted's manhole which can run on telnet or ssh) and it integrates well with various types of applications. :Documentation: http://python-manhole.readthedocs.org/en/latest/ Usage ===== Install it:: pip install manhole You can put this in your django settings, wsgi app file, some module that's always imported early etc: .. code-block:: python import manhole manhole.install() # this will start the daemon thread # and now you start your app, eg: server.serve_forever() Now in a shell you can do either of these:: netcat -U /tmp/manhole-1234 socat - unix-connect:/tmp/manhole-1234 socat readline unix-connect:/tmp/manhole-1234 Socat with readline is best (history, editing etc). If your socat doesn't have readline try `this `_. Sample output:: $ nc -U /tmp/manhole-1234 Python 2.7.3 (default, Apr 10 2013, 06:20:15) [GCC 4.6.3] on linux2 Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> dir() ['__builtins__', 'dump_stacktraces', 'os', 'socket', 'sys', 'traceback'] >>> print 'foobar' foobar Alternative client ------------------ There's a new experimental ``manhole-cli`` bin since 1.1.0, that emulates ``socat``:: usage: manhole-cli [-h] [-t TIMEOUT] [-1 | -2 | -s SIGNAL] PID Connect to a manhole. positional arguments: PID A numerical process id, or a path in the form: /tmp/manhole-1234 optional arguments: -h, --help show this help message and exit -t TIMEOUT, --timeout TIMEOUT Timeout to use. Default: 1 seconds. -1, -USR1 Send USR1 (10) to the process before connecting. -2, -USR2 Send USR2 (12) to the process before connecting. -s SIGNAL, --signal SIGNAL Send the given SIGNAL to the process before connecting. .. end-badges Features ======== * Uses unix domain sockets, only root or same effective user can connect. * Can run the connection in a thread or in a signal handler (see ``oneshot_on`` option). * Can start the thread listening for connections from a signal handler (see ``activate_on`` option) * Compatible with apps that fork, reinstalls the Manhole thread after fork - had to monkeypatch os.fork/os.forkpty for this. * Compatible with gevent and eventlet with some limitations - you need to either: * Use ``oneshot_on``, *or* * Disable thread monkeypatching (eg: ``gevent.monkey.patch_all(thread=False)``, ``eventlet.monkey_patch(thread=False)`` Note: on eventlet `you might `_ need to setup the hub first to prevent circular import problems: .. sourcecode:: python import eventlet eventlet.hubs.get_hub() # do this first eventlet.monkey_patch(thread=False) * The thread is compatible with apps that use signalfd (will mask all signals for the Manhole threads). Options ------- .. code-block:: python manhole.install( verbose=True, verbose_destination=2, patch_fork=True, activate_on=None, oneshot_on=None, sigmask=manhole.ALL_SIGNALS, socket_path=None, reinstall_delay=0.5, locals=None, strict=True, ) * ``verbose`` - Set it to ``False`` to squelch the logging. * ``verbose_destination`` - Destination for verbose messages. Set it to a file descriptor or handle. Default is unbuffered stderr (stderr ``2`` file descriptor). * ``patch_fork`` - Set it to ``False`` if you don't want your ``os.fork`` and ``os.forkpy`` monkeypatched * ``activate_on`` - Set to ``"USR1"``, ``"USR2"`` or some other signal name, or a number if you want the Manhole thread to start when this signal is sent. This is desirable in case you don't want the thread active all the time. * ``thread`` - Set to ``True`` to start the always-on ManholeThread. Default: ``True``. Automatically switched to ``False`` if ``oneshot_on`` or ``activate_on`` are used. * ``oneshot_on`` - Set to ``"USR1"``, ``"USR2"`` or some other signal name, or a number if you want the Manhole to listen for connection in the signal handler. This is desireable in case you don't want threads at all. * ``sigmask`` - Will set the signal mask to the given list (using ``signalfd.sigprocmask``). No action is done if ``signalfd`` is not importable. **NOTE**: This is done so that the Manhole thread doesn't *steal* any signals; Normally that is fine because Python will force all the signal handling to be run in the main thread but signalfd doesn't. * ``socket_path`` - Use a specific path for the unix domain socket (instead of ``/tmp/manhole-``). This disables ``patch_fork`` as children cannot reuse the same path. * ``reinstall_delay`` - Delay the unix domain socket creation *reinstall_delay* seconds. This alleviates cleanup failures when using fork+exec patterns. * ``locals`` - Names to add to manhole interactive shell locals. * ``daemon_connection`` - The connection thread is daemonic (dies on app exit). Default: ``False``. * ``redirect_stderr`` - Redirect output from stderr to manhole console. Default: ``True``. * ``strict`` - If ``True`` then ``AlreadyInstalled`` will be raised when attempting to install manhole twice. Default: ``True``. Environment variable installation --------------------------------- Manhole can be installed via the ``PYTHONMANHOLE`` environment variable. This:: PYTHONMANHOLE='' python yourapp.py Is equivalent to having this in ``yourapp.py``:: import manhole manhole.install() Any extra text in the environment variable is passed to ``manhole.install()``. Example:: PYTHONMANHOLE='oneshot_on="USR2"' python yourapp.py What happens when you actually connect to the socket ---------------------------------------------------- 1. Credentials are checked (if it's same user or root) 2. ``sys.__std*__``/``sys.std*`` are redirected to the UDS 3. Stacktraces for each thread are written to the UDS 4. REPL is started so you can fiddle with the process Known issues ============ * Using threads and file handle (not raw file descriptor) ``verbose_destination`` can cause deadlocks. See bug reports: `PyPy `_ and `Python 3.4 `_. SIGTERM and socket cleanup -------------------------- By default Python doesn't call the ``atexit`` callbacks with the default SIGTERM handling. This makes manhole leave stray socket files around. If this is undesirable you should install a custom SIGTERM handler so ``atexit`` is properly invoked. Example: .. code-block:: python import signal import sys def handle_sigterm(signo, frame): sys.exit(128 + signo) # this will raise SystemExit and cause atexit to be called signal.signal(signal.SIGTERM, handle_sigterm) Using Manhole with uWSGI ------------------------ Because uWSGI overrides signal handling Manhole is a bit more tricky to setup. One way is to use "uWSGI signals" (not the POSIX signals) and have the workers check a file for the pid you want to open the Manhole in. Stick something this in your WSGI application file: .. sourcecode:: python from __future__ import print_function import sys import os import manhole stack_dump_file = '/tmp/manhole-pid' uwsgi_signal_number = 17 try: import uwsgi if not os.path.exists(stack_dump_file): open(stack_dump_file, 'w') def open_manhole(dummy_signum): with open(stack_dump_file, 'r') as fh: pid = fh.read().strip() if pid == str(os.getpid()): inst = manhole.install(strict=False, thread=False) inst.handle_oneshot(dummy_signum, dummy_signum) uwsgi.register_signal(uwsgi_signal_number, 'workers', open_manhole) uwsgi.add_file_monitor(uwsgi_signal_number, stack_dump_file) print("Listening for stack mahole requests via %r" % (stack_dump_file,), file=sys.stderr) except ImportError: print("Not running under uwsgi; unable to configure manhole trigger", file=sys.stderr) except IOError: print("IOError creating manhole trigger %r" % (stack_dump_file,), file=sys.stderr) # somewhere bellow you'd have something like from django.core.wsgi import get_wsgi_application application = get_wsgi_application() # or def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', '2')]) yield b'OK' To open the Manhole just run `echo 1234 > /tmp/manhole-pid` and then `manhole-cli 1234`. Requirements ============ :OS: Linux, OS X :Runtime: Python 2.7, 3.4, 3.5, 3.6 or PyPy Similar projects ================ * Twisted's `manhole `__ - it has colors and server-side history. * `wsgi-shell `_ - spawns a thread. * `pyrasite `_ - uses gdb to inject code. * `pydbattach `_ - uses gdb to inject code. * `pystuck `_ - very similar, uses `rpyc `_ for communication. * `pyringe `_ - uses gdb to inject code, more reliable, but relies on `dbg` python builds unfortunatelly. * `pdb-clone `_ - uses gdb to inject code, with a `different strategy `_. manhole-1.8.1/ci/000077500000000000000000000000001501610110200135225ustar00rootroot00000000000000manhole-1.8.1/ci/bootstrap.py000077500000000000000000000054631501610110200161240ustar00rootroot00000000000000#!/usr/bin/env python import os import pathlib import subprocess import sys base_path: pathlib.Path = pathlib.Path(__file__).resolve().parent.parent templates_path = base_path / 'ci' / 'templates' def check_call(args): print('+', *args) subprocess.check_call(args) def exec_in_env(): env_path = base_path / '.tox' / 'bootstrap' if sys.platform == 'win32': bin_path = env_path / 'Scripts' else: bin_path = env_path / 'bin' if not env_path.exists(): import subprocess print(f'Making bootstrap env in: {env_path} ...') try: check_call([sys.executable, '-m', 'venv', env_path]) except subprocess.CalledProcessError: try: check_call([sys.executable, '-m', 'virtualenv', env_path]) except subprocess.CalledProcessError: check_call(['virtualenv', env_path]) print('Installing `jinja2` into bootstrap environment...') check_call([bin_path / 'pip', 'install', 'jinja2', 'tox']) python_executable = bin_path / 'python' if not python_executable.exists(): python_executable = python_executable.with_suffix('.exe') print(f'Re-executing with: {python_executable}') print('+ exec', python_executable, __file__, '--no-env') os.execv(python_executable, [python_executable, __file__, '--no-env']) def main(): import jinja2 print(f'Project path: {base_path}') jinja = jinja2.Environment( loader=jinja2.FileSystemLoader(str(templates_path)), trim_blocks=True, lstrip_blocks=True, keep_trailing_newline=True, ) tox_environments = [ line.strip() # 'tox' need not be installed globally, but must be importable # by the Python that is running this script. # This uses sys.executable the same way that the call in # cookiecutter-pylibrary/hooks/post_gen_project.py # invokes this bootstrap.py itself. for line in subprocess.check_output([sys.executable, '-m', 'tox', '--listenvs'], universal_newlines=True).splitlines() ] tox_environments = [line for line in tox_environments if line.startswith('py')] for template in templates_path.rglob('*'): if template.is_file(): template_path = template.relative_to(templates_path).as_posix() destination = base_path / template_path destination.parent.mkdir(parents=True, exist_ok=True) destination.write_text(jinja.get_template(template_path).render(tox_environments=tox_environments)) print(f'Wrote {template_path}') print('DONE.') if __name__ == '__main__': args = sys.argv[1:] if args == ['--no-env']: main() elif not args: exec_in_env() else: print(f'Unexpected arguments: {args}', file=sys.stderr) sys.exit(1) manhole-1.8.1/ci/requirements.txt000066400000000000000000000001101501610110200167760ustar00rootroot00000000000000virtualenv>=16.6.0 pip>=19.1.1 setuptools>=18.0.1 six>=1.14.0 tox twine manhole-1.8.1/ci/templates/000077500000000000000000000000001501610110200155205ustar00rootroot00000000000000manhole-1.8.1/ci/templates/.github/000077500000000000000000000000001501610110200170605ustar00rootroot00000000000000manhole-1.8.1/ci/templates/.github/workflows/000077500000000000000000000000001501610110200211155ustar00rootroot00000000000000manhole-1.8.1/ci/templates/.github/workflows/github-actions.yml000066400000000000000000000044071501610110200245650ustar00rootroot00000000000000name: build on: [push, pull_request, workflow_dispatch] jobs: test: name: {{ '${{ matrix.name }}' }} runs-on: {{ '${{ matrix.os }}' }} timeout-minutes: 30 strategy: fail-fast: false matrix: include: - name: 'check' python: '3.11' toxpython: 'python3.11' tox_env: 'check' os: 'ubuntu-latest' - name: 'docs' python: '3.11' toxpython: 'python3.11' tox_env: 'docs' os: 'ubuntu-latest' {% for env in tox_environments %} {% set prefix = env.split('-')[0] -%} {% if prefix.startswith('pypy') %} {% set python %}pypy-{{ prefix[4] }}.{{ prefix[5:] }}{% endset %} {% set cpython %}pp{{ prefix[4:5] }}{% endset %} {% set toxpython %}pypy{{ prefix[4] }}.{{ prefix[5:] }}{% endset %} {% else %} {% set python %}{{ prefix[2] }}.{{ prefix[3:] }}{% endset %} {% set cpython %}cp{{ prefix[2:] }}{% endset %} {% set toxpython %}python{{ prefix[2] }}.{{ prefix[3:] }}{% endset %} {% endif %} {% for os, python_arch in [ ['ubuntu', 'x64'], ['macos', 'arm64'], ] %} - name: '{{ env }} ({{ os }})' python: '{{ python }}' toxpython: '{{ toxpython }}' python_arch: '{{ python_arch }}' tox_env: '{{ env }}' os: '{{ os }}-latest' {% endfor %} {% endfor %} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: {{ '${{ matrix.python }}' }} architecture: {{ '${{ matrix.python_arch }}' }} - name: install dependencies run: | python -mpip install --progress-bar=off -r ci/requirements.txt virtualenv --version pip --version tox --version pip list --format=freeze - name: test env: TOXPYTHON: '{{ '${{ matrix.toxpython }}' }}' MANHOLE_TEST_TIMEOUT: 60 run: > tox -e {{ '${{ matrix.tox_env }}' }} -v finish: needs: test if: {{ '${{ always() }}' }} runs-on: ubuntu-latest steps: - uses: coverallsapp/github-action@v2 with: parallel-finished: true - uses: codecov/codecov-action@v3 with: CODECOV_TOKEN: {% raw %}${{ secrets.CODECOV_TOKEN }}{% endraw %} {{ '' }} manhole-1.8.1/docs/000077500000000000000000000000001501610110200140575ustar00rootroot00000000000000manhole-1.8.1/docs/authors.rst000066400000000000000000000000341501610110200162730ustar00rootroot00000000000000.. include:: ../AUTHORS.rst manhole-1.8.1/docs/changelog.rst000066400000000000000000000000361501610110200165370ustar00rootroot00000000000000.. include:: ../CHANGELOG.rst manhole-1.8.1/docs/conf.py000066400000000000000000000017131501610110200153600ustar00rootroot00000000000000extensions = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', 'sphinx.ext.coverage', 'sphinx.ext.doctest', 'sphinx.ext.extlinks', 'sphinx.ext.ifconfig', 'sphinx.ext.napoleon', 'sphinx.ext.todo', 'sphinx.ext.viewcode', ] source_suffix = '.rst' master_doc = 'index' project = 'manhole' year = '2012-2024' author = 'Ionel Cristian Mărieș' copyright = f'{year}, {author}' version = release = '1.8.1' pygments_style = 'trac' templates_path = ['.'] extlinks = { 'issue': ('https://github.com/ionelmc/python-manhole/issues/%s', '#%s'), 'pr': ('https://github.com/ionelmc/python-manhole/pull/%s', 'PR #%s'), } html_theme = 'furo' html_theme_options = { 'githuburl': 'https://github.com/ionelmc/python-manhole/', } html_use_smartypants = True html_last_updated_fmt = '%b %d, %Y' html_split_index = False html_short_title = f'{project}-{version}' napoleon_use_ivar = True napoleon_use_rtype = False napoleon_use_param = False manhole-1.8.1/docs/contributing.rst000066400000000000000000000000411501610110200173130ustar00rootroot00000000000000.. include:: ../CONTRIBUTING.rst manhole-1.8.1/docs/index.rst000066400000000000000000000003641501610110200157230ustar00rootroot00000000000000======== Contents ======== .. toctree:: :maxdepth: 2 readme installation usage reference/index contributing authors changelog Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` manhole-1.8.1/docs/installation.rst000066400000000000000000000001271501610110200173120ustar00rootroot00000000000000============ Installation ============ At the command line:: pip install manhole manhole-1.8.1/docs/readme.rst000066400000000000000000000000331501610110200160420ustar00rootroot00000000000000.. include:: ../README.rst manhole-1.8.1/docs/reference/000077500000000000000000000000001501610110200160155ustar00rootroot00000000000000manhole-1.8.1/docs/reference/index.rst000066400000000000000000000000731501610110200176560ustar00rootroot00000000000000Reference ========= .. toctree:: :glob: manhole* manhole-1.8.1/docs/reference/manhole.rst000066400000000000000000000002361501610110200201730ustar00rootroot00000000000000manhole ======= .. testsetup:: from manhole import * .. automodule:: manhole :members: :undoc-members: :special-members: __init__, __len__ manhole-1.8.1/docs/requirements.txt000066400000000000000000000000211501610110200173340ustar00rootroot00000000000000sphinx>=1.3 furo manhole-1.8.1/docs/spelling_wordlist.txt000066400000000000000000000001551501610110200203650ustar00rootroot00000000000000builtin builtins classmethod staticmethod classmethods staticmethods args kwargs callstack Changelog Indices manhole-1.8.1/docs/usage.rst000066400000000000000000000001021501610110200157060ustar00rootroot00000000000000===== Usage ===== To use manhole in a project:: import manhole manhole-1.8.1/pyproject.toml000066400000000000000000000024411501610110200160440ustar00rootroot00000000000000[build-system] requires = [ "setuptools>=64", ] [tool.ruff.per-file-ignores] "ci/*" = ["S"] [tool.ruff] extend-exclude = ["static", "ci/templates"] line-length = 140 src = ["src", "tests"] target-version = "py38" [tool.ruff.lint.per-file-ignores] "ci/*" = ["S"] [tool.ruff.lint] ignore = [ "RUF001", # ruff-specific rules ambiguous-unicode-character-string "S101", # flake8-bandit assert "S308", # flake8-bandit suspicious-mark-safe-usage "E501", # pycodestyle line-too-long "B008", "S108", "S110", "S307", "S603", "S606", "S607", ] select = [ "B", # flake8-bugbear "C4", # flake8-comprehensions "DTZ", # flake8-datetimez "E", # pycodestyle errors "EXE", # flake8-executable "F", # pyflakes "I", # isort "INT", # flake8-gettext "PIE", # flake8-pie "PLC", # pylint convention "PLE", # pylint errors "PT", # flake8-pytest-style # "PTH", # flake8-use-pathlib "RSE", # flake8-raise "RUF", # ruff-specific rules "S", # flake8-bandit "UP", # pyupgrade "W", # pycodestyle warnings ] [tool.ruff.lint.flake8-pytest-style] fixture-parentheses = false mark-parentheses = false [tool.ruff.lint.isort] forced-separate = ["conftest"] force-single-line = true [tool.ruff.format] quote-style = "single" manhole-1.8.1/pytest.ini000066400000000000000000000017451501610110200151670ustar00rootroot00000000000000[pytest] # If a pytest section is found in one of the possible config files # (pytest.ini, tox.ini or setup.cfg), then pytest will not look for any others, # so if you add a pytest config section elsewhere, # you will need to delete this section from setup.cfg. norecursedirs = .git .tox .env dist build migrations python_files = test_*.py *_test.py tests.py addopts = -ra --strict-markers --ignore=docs/conf.py --ignore=setup.py --ignore=ci --ignore=.eggs --doctest-modules --doctest-glob=\*.rst --tb=short testpaths = tests # If you want to switch back to tests outside package just remove --pyargs # and edit testpaths to have "tests/" instead of "manhole". # Idea from: https://til.simonwillison.net/pytest/treat-warnings-as-errors filterwarnings = error # You can add exclusions, some examples: ignore:unclosed:ResourceWarning:: # ignore:The {{% if::: # ignore:Coverage disabled via --no-cov switch! manhole-1.8.1/setup.cfg000066400000000000000000000000461501610110200147500ustar00rootroot00000000000000[egg_info] tag_build = tag_date = 0 manhole-1.8.1/setup.py000077500000000000000000000126511501610110200146510ustar00rootroot00000000000000#!/usr/bin/env python import re from distutils.command.build import build from itertools import chain from os import fspath from pathlib import Path from setuptools import Command from setuptools import find_packages from setuptools import setup from setuptools.command.develop import develop from setuptools.command.easy_install import easy_install from setuptools.command.editable_wheel import editable_wheel from setuptools.command.install_lib import install_lib pth_file = Path(__file__).parent.joinpath('src', 'manhole.pth') class BuildWithPTH(build): def run(self): super().run() self.copy_file(fspath(pth_file), fspath(Path(self.build_lib, pth_file.name))) class PTHWheelPiggyback: def __init__(self, strategy): self.strategy = strategy def __enter__(self): self.strategy.__enter__() def __exit__(self, exc_type, exc_val, exc_tb): self.strategy.__exit__(exc_type, exc_val, exc_tb) def __call__(self, wheel, files, mapping): self.strategy(wheel, files, mapping) wheel.writestr(fspath(pth_file.name), pth_file.read_bytes()) class EditableWheelWithPTH(editable_wheel): def _select_strategy(self, dist_name, tag, lib): return PTHWheelPiggyback(super()._select_strategy(dist_name, tag, lib)) class EasyInstallWithPTH(easy_install): def run(self, *args, **kwargs): super().run(*args, **kwargs) self.copy_file(fspath(pth_file), str(Path(self.install_dir, pth_file.name))) class InstallLibWithPTH(install_lib): def run(self): super().run() dest = str(Path(self.install_dir, pth_file.name)) self.copy_file(fspath(pth_file), dest) self.outputs = [dest] def get_outputs(self): return chain(install_lib.get_outputs(self), self.outputs) class DevelopWithPTH(develop): def run(self): super().run() self.copy_file(fspath(pth_file), str(Path(self.install_dir, pth_file.name))) class GeneratePTH(Command): user_options = [] # noqa: RUF012 def initialize_options(self): pass def finalize_options(self): pass def run(self): with pth_file.open('w') as fh: with pth_file.with_suffix('.embed').open() as sh: fh.write(f"import os, sys;exec({sh.read().replace(' ', ' ')!r})") def read(*names, **kwargs): with Path(__file__).parent.joinpath(*names).open(encoding=kwargs.get('encoding', 'utf8')) as fh: return fh.read() setup( name='manhole', version='1.8.1', license='BSD-2-Clause', description='Manhole is in-process service that will accept unix domain socket connections and present the' 'stacktraces for all threads and an interactive prompt.', long_description='{}\n{}'.format( re.compile('^.. start-badges.*^.. end-badges', re.M | re.S).sub('', read('README.rst')), re.sub(':[a-z]+:`~?(.*?)`', r'``\1``', read('CHANGELOG.rst')), ), author='Ionel Cristian Mărieș', author_email='contact@ionelmc.ro', url='https://github.com/ionelmc/python-manhole', packages=find_packages('src'), package_dir={'': 'src'}, py_modules=[path.stem for path in Path('src').glob('*.py')], include_package_data=True, zip_safe=False, classifiers=[ # complete classifier list: http://pypi.python.org/pypi?%3Aaction=list_classifiers 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: BSD License', 'Operating System :: Unix', 'Operating System :: POSIX', 'Programming Language :: Python', 'Topic :: Software Development :: Debuggers', 'Topic :: Utilities', 'Topic :: System :: Monitoring', 'Topic :: System :: Networking', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3 :: Only', '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 :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', # uncomment if you test on these interpreters: # "Programming Language :: Python :: Implementation :: IronPython", # "Programming Language :: Python :: Implementation :: Jython", # "Programming Language :: Python :: Implementation :: Stackless", 'Topic :: Utilities', ], project_urls={ 'Documentation': 'https://python-manhole.readthedocs.io/', 'Changelog': 'https://python-manhole.readthedocs.io/en/latest/changelog.html', 'Issue Tracker': 'https://github.com/ionelmc/python-manhole/issues', }, entry_points={ 'console_scripts': [ 'manhole-cli = manhole.cli:main', ] }, keywords=['debugging', 'manhole', 'thread', 'socket', 'unix domain socket'], python_requires='>=3.8', install_requires=[ # eg: "aspectlib==1.1.1", "six>=1.7", ], extras_require={ # eg: # "rst": ["docutils>=0.11"], # ":python_version=="2.6"": ["argparse"], }, cmdclass={ 'build': BuildWithPTH, 'easy_install': EasyInstallWithPTH, 'install_lib': InstallLibWithPTH, 'develop': DevelopWithPTH, 'editable_wheel': EditableWheelWithPTH, 'genpth': GeneratePTH, }, ) manhole-1.8.1/src/000077500000000000000000000000001501610110200137165ustar00rootroot00000000000000manhole-1.8.1/src/manhole.egg-info/000077500000000000000000000000001501610110200170335ustar00rootroot00000000000000manhole-1.8.1/src/manhole.egg-info/PKG-INFO000066400000000000000000000326651501610110200201440ustar00rootroot00000000000000Metadata-Version: 2.1 Name: manhole Version: 1.8.1 Summary: Manhole is in-process service that will accept unix domain socket connections and present thestacktraces for all threads and an interactive prompt. Home-page: https://github.com/ionelmc/python-manhole Author: Ionel Cristian Mărieș Author-email: contact@ionelmc.ro License: BSD-2-Clause Project-URL: Documentation, https://python-manhole.readthedocs.io/ Project-URL: Changelog, https://python-manhole.readthedocs.io/en/latest/changelog.html Project-URL: Issue Tracker, https://github.com/ionelmc/python-manhole/issues Keywords: debugging,manhole,thread,socket,unix domain socket Classifier: Development Status :: 5 - Production/Stable Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Operating System :: Unix Classifier: Operating System :: POSIX Classifier: Programming Language :: Python Classifier: Topic :: Software Development :: Debuggers Classifier: Topic :: Utilities Classifier: Topic :: System :: Monitoring Classifier: Topic :: System :: Networking Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Utilities Requires-Python: >=3.8 License-File: LICENSE License-File: AUTHORS.rst ======== Overview ======== Features ======== * Uses unix domain sockets, only root or same effective user can connect. * Can run the connection in a thread or in a signal handler (see ``oneshot_on`` option). * Can start the thread listening for connections from a signal handler (see ``activate_on`` option) * Compatible with apps that fork, reinstalls the Manhole thread after fork - had to monkeypatch os.fork/os.forkpty for this. * Compatible with gevent and eventlet with some limitations - you need to either: * Use ``oneshot_on``, *or* * Disable thread monkeypatching (eg: ``gevent.monkey.patch_all(thread=False)``, ``eventlet.monkey_patch(thread=False)`` Note: on eventlet `you might `_ need to setup the hub first to prevent circular import problems: .. sourcecode:: python import eventlet eventlet.hubs.get_hub() # do this first eventlet.monkey_patch(thread=False) * The thread is compatible with apps that use signalfd (will mask all signals for the Manhole threads). Options ------- .. code-block:: python manhole.install( verbose=True, verbose_destination=2, patch_fork=True, activate_on=None, oneshot_on=None, sigmask=manhole.ALL_SIGNALS, socket_path=None, reinstall_delay=0.5, locals=None, strict=True, ) * ``verbose`` - Set it to ``False`` to squelch the logging. * ``verbose_destination`` - Destination for verbose messages. Set it to a file descriptor or handle. Default is unbuffered stderr (stderr ``2`` file descriptor). * ``patch_fork`` - Set it to ``False`` if you don't want your ``os.fork`` and ``os.forkpy`` monkeypatched * ``activate_on`` - Set to ``"USR1"``, ``"USR2"`` or some other signal name, or a number if you want the Manhole thread to start when this signal is sent. This is desirable in case you don't want the thread active all the time. * ``thread`` - Set to ``True`` to start the always-on ManholeThread. Default: ``True``. Automatically switched to ``False`` if ``oneshot_on`` or ``activate_on`` are used. * ``oneshot_on`` - Set to ``"USR1"``, ``"USR2"`` or some other signal name, or a number if you want the Manhole to listen for connection in the signal handler. This is desireable in case you don't want threads at all. * ``sigmask`` - Will set the signal mask to the given list (using ``signalfd.sigprocmask``). No action is done if ``signalfd`` is not importable. **NOTE**: This is done so that the Manhole thread doesn't *steal* any signals; Normally that is fine because Python will force all the signal handling to be run in the main thread but signalfd doesn't. * ``socket_path`` - Use a specific path for the unix domain socket (instead of ``/tmp/manhole-``). This disables ``patch_fork`` as children cannot reuse the same path. * ``reinstall_delay`` - Delay the unix domain socket creation *reinstall_delay* seconds. This alleviates cleanup failures when using fork+exec patterns. * ``locals`` - Names to add to manhole interactive shell locals. * ``daemon_connection`` - The connection thread is daemonic (dies on app exit). Default: ``False``. * ``redirect_stderr`` - Redirect output from stderr to manhole console. Default: ``True``. * ``strict`` - If ``True`` then ``AlreadyInstalled`` will be raised when attempting to install manhole twice. Default: ``True``. Environment variable installation --------------------------------- Manhole can be installed via the ``PYTHONMANHOLE`` environment variable. This:: PYTHONMANHOLE='' python yourapp.py Is equivalent to having this in ``yourapp.py``:: import manhole manhole.install() Any extra text in the environment variable is passed to ``manhole.install()``. Example:: PYTHONMANHOLE='oneshot_on="USR2"' python yourapp.py What happens when you actually connect to the socket ---------------------------------------------------- 1. Credentials are checked (if it's same user or root) 2. ``sys.__std*__``/``sys.std*`` are redirected to the UDS 3. Stacktraces for each thread are written to the UDS 4. REPL is started so you can fiddle with the process Known issues ============ * Using threads and file handle (not raw file descriptor) ``verbose_destination`` can cause deadlocks. See bug reports: `PyPy `_ and `Python 3.4 `_. SIGTERM and socket cleanup -------------------------- By default Python doesn't call the ``atexit`` callbacks with the default SIGTERM handling. This makes manhole leave stray socket files around. If this is undesirable you should install a custom SIGTERM handler so ``atexit`` is properly invoked. Example: .. code-block:: python import signal import sys def handle_sigterm(signo, frame): sys.exit(128 + signo) # this will raise SystemExit and cause atexit to be called signal.signal(signal.SIGTERM, handle_sigterm) Using Manhole with uWSGI ------------------------ Because uWSGI overrides signal handling Manhole is a bit more tricky to setup. One way is to use "uWSGI signals" (not the POSIX signals) and have the workers check a file for the pid you want to open the Manhole in. Stick something this in your WSGI application file: .. sourcecode:: python from __future__ import print_function import sys import os import manhole stack_dump_file = '/tmp/manhole-pid' uwsgi_signal_number = 17 try: import uwsgi if not os.path.exists(stack_dump_file): open(stack_dump_file, 'w') def open_manhole(dummy_signum): with open(stack_dump_file, 'r') as fh: pid = fh.read().strip() if pid == str(os.getpid()): inst = manhole.install(strict=False, thread=False) inst.handle_oneshot(dummy_signum, dummy_signum) uwsgi.register_signal(uwsgi_signal_number, 'workers', open_manhole) uwsgi.add_file_monitor(uwsgi_signal_number, stack_dump_file) print("Listening for stack mahole requests via %r" % (stack_dump_file,), file=sys.stderr) except ImportError: print("Not running under uwsgi; unable to configure manhole trigger", file=sys.stderr) except IOError: print("IOError creating manhole trigger %r" % (stack_dump_file,), file=sys.stderr) # somewhere bellow you'd have something like from django.core.wsgi import get_wsgi_application application = get_wsgi_application() # or def application(environ, start_response): start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', '2')]) yield b'OK' To open the Manhole just run `echo 1234 > /tmp/manhole-pid` and then `manhole-cli 1234`. Requirements ============ :OS: Linux, OS X :Runtime: Python 2.7, 3.4, 3.5, 3.6 or PyPy Similar projects ================ * Twisted's `manhole `__ - it has colors and server-side history. * `wsgi-shell `_ - spawns a thread. * `pyrasite `_ - uses gdb to inject code. * `pydbattach `_ - uses gdb to inject code. * `pystuck `_ - very similar, uses `rpyc `_ for communication. * `pyringe `_ - uses gdb to inject code, more reliable, but relies on `dbg` python builds unfortunatelly. * `pdb-clone `_ - uses gdb to inject code, with a `different strategy `_. Changelog ========= 1.8.1 (2024-07-24) ------------------ * Fixed buffering issue on Python 3.11. See ``66``. * Cleaned up some packaging/test problems. * Removed more leftover Python 2 code. * Fixed license metadata. See: ``68``. 1.8.0 (2021-04-08) ------------------ * Simplified connection closing code. Contributed by Anton Ryzhov in ``62``. * Made connection shutdown in ``manhole-cli`` more graceful. Contributed by Anton Ryzhov in ``63``. 1.7.0 (2021-03-22) ------------------ * Fixed memory leak via ``sys.last_type``, ``sys.last_value``, ``sys.last_traceback``. Contributed by Anton Ryzhov in ``59``. * Fixed a bunch of double-close bugs and simplified stream handler code. Contributed by Anton Ryzhov in ``58``. * Loosen up ``pid`` argument parsing in ``manhole-cli`` to allow using paths with any prefix (not just ``/tmp``). 1.6.0 (2019-01-19) ------------------ * Testing improvements (changed some skips to xfail, added osx in Travis). * Fixed long standing Python 2.7 bug where ``sys.getfilesystemencoding()`` would be broken after installing a threaded manhole. See ``51``. * Dropped support for Python 2.6, 3.3 and 3.4. * Fixed handling when ``socket.setdefaulttimeout()`` is used. Contributed by "honnix" in ``53``. * Fixed some typos. Contributed by Jesús Cea in ``43``. * Fixed handling in ``manhole-cli`` so that timeout is actually seconds and not milliseconds. Contributed by Nir Soffer in ``45``. * Cleaned up useless polling options in ``manhole-cli``. Contributed by Nir Soffer in ``46``. * Documented and implemented a solution for using Manhole with Eventlet. See ``49``. 1.5.0 (2017-08-31) ------------------ * Added two string aliases for ``connection_handler`` option. Now you can conveniently use ``connection_handler="exec"``. * Improved ``handle_connection_exec``. It now has a clean way to exit (``exit()``) and properly closes the socket. 1.4.0 (2017-08-29) ------------------ * Added the ``connection_handler`` install option. Default value is ``manhole.handle_connection_repl``, and alternate ``manhole.handle_connection_exec`` is provided (very simple: no output redirection, no stacktrace dumping). * Dropped Python 3.2 from the test grid. It may work but it's a huge pain to support (pip/pytest don't support it anymore). * Added Python 3.5 and 3.6 in the test grid. * Fixed issues with piping to ``manhole-cli``. Now ``echo foobar | manhole-cli`` will wait 1 second for output from manhole (you can customize this with the ``--timeout`` option). * Fixed issues with newer PyPy (caused by gevent/eventlet socket unwrapping). 1.3.0 (2015-09-03) ------------------ * Allowed Manhole to be configured without any thread or activation (in case you want to manually activate). * Added an example and tests for using Manhole with uWSGi. * Fixed error handling in ``manhole-cli`` on Python 3 (exc vars don't leak anymore). * Fixed support for running in gevent/eventlet-using apps on Python 3 (now that they support Python 3). * Allowed reinstalling the manhole (in non-``strict`` mode). Previous install is undone. 1.2.0 (2015-07-06) ------------------ * Changed ``manhole-cli``: * Won't spam the terminal with errors if socket file doesn't exist. * Allowed sending any signal (new ``--signal`` argument). * Fixed some validation issues for the ``PID`` argument. 1.1.0 (2015-06-06) ------------------ * Added support for installing the manhole via the ``PYTHONMANHOLE`` environment variable. * Added a ``strict`` install option. Set it to false to avoid getting the ``AlreadyInstalled`` exception. * Added a ``manhole-cli`` script that emulates ``socat readline unix-connect:/tmp/manhole-1234``. 1.0.0 (2014-10-13) ------------------ * Added ``socket_path`` install option (contributed by `Nir Soffer`_). * Added ``reinstall_delay`` install option. * Added ``locals`` install option (contributed by `Nir Soffer`_). * Added ``redirect_stderr`` install option (contributed by `Nir Soffer`_). * Lots of internals cleanup (contributed by `Nir Soffer`_). 0.6.2 (2014-04-28) ------------------ * Fix OS X regression. 0.6.1 (2014-04-28) ------------------ * Support for OS X (contributed by `Saulius Menkevičius`_). .. _Saulius Menkevičius: https://github.com/razzmatazz .. _Nir Soffer: https://github.com/nirs manhole-1.8.1/src/manhole.egg-info/SOURCES.txt000066400000000000000000000016751501610110200207300ustar00rootroot00000000000000.bumpversion.cfg .cookiecutterrc .coveragerc .editorconfig .gitignore .pre-commit-config.yaml .readthedocs.yml AUTHORS.rst CHANGELOG.rst CONTRIBUTING.rst LICENSE MANIFEST.in README.rst pyproject.toml pytest.ini setup.py tox.ini .github/workflows/github-actions.yml ci/bootstrap.py ci/requirements.txt ci/templates/.github/workflows/github-actions.yml docs/authors.rst docs/changelog.rst docs/conf.py docs/contributing.rst docs/index.rst docs/installation.rst docs/readme.rst docs/requirements.txt docs/spelling_wordlist.txt docs/usage.rst docs/reference/index.rst docs/reference/manhole.rst src/manhole.embed src/manhole.pth src/manhole/__init__.py src/manhole/cli.py src/manhole.egg-info/PKG-INFO src/manhole.egg-info/SOURCES.txt src/manhole.egg-info/dependency_links.txt src/manhole.egg-info/entry_points.txt src/manhole.egg-info/not-zip-safe src/manhole.egg-info/top_level.txt tests/helper.py tests/test_manhole.py tests/test_manhole_cli.py tests/wsgi.pymanhole-1.8.1/src/manhole.egg-info/dependency_links.txt000066400000000000000000000000011501610110200231010ustar00rootroot00000000000000 manhole-1.8.1/src/manhole.egg-info/entry_points.txt000066400000000000000000000000611501610110200223260ustar00rootroot00000000000000[console_scripts] manhole-cli = manhole.cli:main manhole-1.8.1/src/manhole.egg-info/not-zip-safe000066400000000000000000000000011501610110200212610ustar00rootroot00000000000000 manhole-1.8.1/src/manhole.egg-info/top_level.txt000066400000000000000000000000101501610110200215540ustar00rootroot00000000000000manhole manhole-1.8.1/src/manhole.embed000066400000000000000000000004241501610110200163370ustar00rootroot00000000000000if "PYTHONMANHOLE" in os.environ: try: from manhole import install eval("install({0[PYTHONMANHOLE]})".format(os.environ)) except Exception as exc: sys.stderr.write("Failed to manhole.install({[PYTHONMANHOLE]}): {!r}\n".format(os.environ, exc)) manhole-1.8.1/src/manhole.pth000066400000000000000000000004331501610110200160560ustar00rootroot00000000000000import os, sys;exec('if "PYTHONMANHOLE" in os.environ:\n try:\n from manhole import install\n eval("install({0[PYTHONMANHOLE]})".format(os.environ))\n except Exception as exc:\n sys.stderr.write("Failed to manhole.install({[PYTHONMANHOLE]}): {!r}\\n".format(os.environ, exc))\n') manhole-1.8.1/src/manhole/000077500000000000000000000000001501610110200153415ustar00rootroot00000000000000manhole-1.8.1/src/manhole/__init__.py000066400000000000000000000544561501610110200174700ustar00rootroot00000000000000import atexit import code import errno import os import signal import socket import struct import sys import traceback from contextlib import closing __version__ = '1.8.1' from io import TextIOWrapper try: import signalfd except ImportError: signalfd = None try: string = basestring except NameError: # python 3 string = str try: InterruptedError = InterruptedError except NameError: # python <= 3.2 InterruptedError = OSError try: BrokenPipeError = BrokenPipeError except NameError: # old python class BrokenPipeError(Exception): pass if hasattr(sys, 'setswitchinterval'): setinterval = sys.setswitchinterval getinterval = sys.getswitchinterval else: setinterval = sys.setcheckinterval getinterval = sys.getcheckinterval try: from eventlet.patcher import original as _original def _get_original(mod, name): return getattr(_original(mod), name) except ImportError: try: from gevent.monkey import get_original as _get_original except ImportError: def _get_original(mod, name): return getattr(__import__(mod), name) _ORIGINAL_SOCKET = _get_original('socket', 'socket') try: _ORIGINAL_ALLOCATE_LOCK = _get_original('thread', 'allocate_lock') except ImportError: # python 3 _ORIGINAL_ALLOCATE_LOCK = _get_original('_thread', 'allocate_lock') _ORIGINAL_THREAD = _get_original('threading', 'Thread') _ORIGINAL_EVENT = _get_original('threading', 'Event') _ORIGINAL__ACTIVE = _get_original('threading', '_active') _ORIGINAL_SLEEP = _get_original('time', 'sleep') try: import ctypes import ctypes.util libpthread_path = ctypes.util.find_library('pthread') if not libpthread_path: raise ImportError libpthread = ctypes.CDLL(libpthread_path) if not hasattr(libpthread, 'pthread_setname_np'): raise ImportError _pthread_setname_np = libpthread.pthread_setname_np _pthread_setname_np.argtypes = [ctypes.c_void_p, ctypes.c_char_p] _pthread_setname_np.restype = ctypes.c_int def pthread_setname_np(ident, name): _pthread_setname_np(ident, name[:15]) except ImportError: def pthread_setname_np(ident, name): pass if sys.platform == 'darwin' or sys.platform.startswith('freebsd'): _PEERCRED_LEVEL = getattr(socket, 'SOL_LOCAL', 0) _PEERCRED_OPTION = getattr(socket, 'LOCAL_PEERCRED', 1) else: _PEERCRED_LEVEL = socket.SOL_SOCKET # TODO: Is this missing on some platforms? _PEERCRED_OPTION = getattr(socket, 'SO_PEERCRED', 17) _ALL_SIGNALS = tuple(getattr(signal, sig) for sig in dir(signal) if sig.startswith('SIG') and '_' not in sig) # These (_LOG and _MANHOLE) will hold instances after install _MANHOLE = None _LOCK = _ORIGINAL_ALLOCATE_LOCK() def force_original_socket(sock): with closing(sock): if hasattr(sock, 'detach'): return _ORIGINAL_SOCKET(sock.family, sock.type, sock.proto, sock.detach()) else: assert hasattr(_ORIGINAL_SOCKET, '_sock') return _ORIGINAL_SOCKET(_sock=sock._sock) def get_peercred(sock): """Gets the (pid, uid, gid) for the client on the given *connected* socket.""" buf = sock.getsockopt(_PEERCRED_LEVEL, _PEERCRED_OPTION, struct.calcsize('3i')) return struct.unpack('3i', buf) class AlreadyInstalled(Exception): pass class NotInstalled(Exception): pass class ConfigurationConflict(Exception): pass class SuspiciousClient(Exception): pass class ManholeThread(_ORIGINAL_THREAD): """ Thread that runs the infamous "Manhole". This thread is a `daemon` thread - it will exit if the main thread exits. On connect, a different, non-daemon thread will be started - so that the process won't exit while there's a connection to the manhole. Args: sigmask (list of signal numbers): Signals to block in this thread. start_timeout (float): Seconds to wait for the thread to start. Emits a message if the thread is not running when calling ``start()``. bind_delay (float): Seconds to delay socket binding. Default: `no delay`. daemon_connection (bool): The connection thread is daemonic (dies on app exit). Default: ``False``. """ def __init__(self, get_socket, sigmask, start_timeout, connection_handler, bind_delay=None, daemon_connection=False): super().__init__() self.daemon = True self.daemon_connection = daemon_connection self.name = 'Manhole' self.psname = b'Manhole' self.sigmask = sigmask self.serious = _ORIGINAL_EVENT() # time to wait for the manhole to get serious (to have a complete start) # see: http://emptysqua.re/blog/dawn-of-the-thread/ self.start_timeout = start_timeout self.bind_delay = bind_delay self.connection_handler = connection_handler self.get_socket = get_socket self.should_run = False def stop(self): self.should_run = False def clone(self, **kwargs): """ Make a fresh thread with the same options. This is usually used on dead threads. """ return ManholeThread( self.get_socket, self.sigmask, self.start_timeout, connection_handler=self.connection_handler, daemon_connection=self.daemon_connection, **kwargs, ) def start(self): self.should_run = True super().start() if not self.serious.wait(self.start_timeout): _LOG(f"WARNING: Waited {self.start_timeout} seconds but Manhole thread didn't start yet :(") def run(self): """ Runs the manhole loop. Only accepts one connection at a time because: * This thread is a daemon thread (exits when main thread exists). * The connection need exclusive access to stdin, stderr and stdout so it can redirect inputs and outputs. """ self.serious.set() if signalfd and self.sigmask: signalfd.sigprocmask(signalfd.SIG_BLOCK, self.sigmask) pthread_setname_np(self.ident, self.psname) if self.bind_delay: _LOG(f'Delaying UDS binding {self.bind_delay} seconds ...') _ORIGINAL_SLEEP(self.bind_delay) sock = self.get_socket() while self.should_run: _LOG(f'Waiting for new connection (in pid:{os.getpid()}) ...') try: client = ManholeConnectionThread(sock.accept()[0], self.connection_handler, self.daemon_connection) client.start() client.join() except socket.timeout: continue except (OSError, InterruptedError) as e: if e.errno != errno.EINTR: raise continue finally: client = None class ManholeConnectionThread(_ORIGINAL_THREAD): """ Manhole thread that handles the connection. This thread is a normal thread (non-daemon) - it won't exit if the main thread exits. """ def __init__(self, client, connection_handler, daemon=False): super().__init__() self.daemon = daemon self.client = force_original_socket(client) self.connection_handler = connection_handler self.name = 'ManholeConnectionThread' self.psname = b'ManholeConnectionThread' def run(self): _LOG('Started ManholeConnectionThread thread. Checking credentials ...') pthread_setname_np(self.ident, b'Manhole -------') pid, _, _ = check_credentials(self.client) pthread_setname_np(self.ident, b'Manhole < PID:%d' % pid) try: self.connection_handler(self.client) except BaseException as exc: _LOG(f'ManholeConnectionThread failure: {exc!r}') def check_credentials(client): """ Checks credentials for given socket. """ pid, uid, gid = get_peercred(client) euid = os.geteuid() client_name = f'PID:{pid} UID:{uid} GID:{gid}' if uid not in (0, euid): raise SuspiciousClient(f"Can't accept client with {client_name}. It doesn't match the current EUID:{euid} or ROOT.") _LOG(f'Accepted connection on fd:{client.fileno()} from {client_name}') return pid, uid, gid def handle_connection_exec(client): """ Alternate connection handler. No output redirection. """ class ExitExecLoop(Exception): pass def exit(): raise ExitExecLoop client.settimeout(None) fh = client.makefile() with closing(client): with closing(fh): try: payload = fh.readline() while payload: _LOG(f'Running: {payload!r}.') eval(compile(payload, '', 'exec'), {'exit': exit}, _MANHOLE.locals) payload = fh.readline() except ExitExecLoop: _LOG('Exiting exec loop.') def handle_connection_repl(client: socket.socket): """ Handles connection. """ client.settimeout(None) # # disable this till we have evidence that it's needed # client.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 0) # # Note: setting SO_RCVBUF on UDS has no effect, see: http://man7.org/linux/man-pages/man7/unix.7.html backup = [] old_interval = getinterval() patches = [('r', ('stdin', '__stdin__')), ('w', ('stdout', '__stdout__'))] if _MANHOLE.redirect_stderr: patches.append(('w', ('stderr', '__stderr__'))) try: for mode, names in patches: for name in names: backup.append((name, backup_fh := getattr(sys, name))) setattr(sys, name, wrapped_fh := TextIOWrapper(client.makefile(f'{mode}b', 0), encoding=backup_fh.encoding)) wrapped_fh.mode = mode try: handle_repl(_MANHOLE.locals) except BrokenPipeError: _LOG('REPL client disconnected') except Exception as exc: _LOG(f'REPL failed with {exc!r}.') _LOG('DONE.') finally: try: # Change the switch/check interval to something ridiculous. We don't want to have other thread try # to write to the redirected sys.__std*/sys.std* - it would fail horribly. setinterval(2147483647) for name, fh in backup: try: getattr(sys, name).close() except OSError: pass setattr(sys, name, fh) try: client.close() except OSError: pass finally: setinterval(old_interval) _LOG('Cleaned up.') _CONNECTION_HANDLER_ALIASES = {'repl': handle_connection_repl, 'exec': handle_connection_exec} class ManholeConsole(code.InteractiveConsole): def __init__(self, *args, **kw): code.InteractiveConsole.__init__(self, *args, **kw) if _MANHOLE.redirect_stderr: self.file = sys.stderr else: self.file = sys.stdout def write(self, data): self.file.write(data) def handle_repl(locals): """ Dumps stacktraces and runs an interactive prompt (REPL). """ dump_stacktraces() namespace = { 'dump_stacktraces': dump_stacktraces, 'sys': sys, 'os': os, 'socket': socket, 'traceback': traceback, } if locals: namespace.update(locals) try: ManholeConsole(namespace).interact() except SystemExit: pass finally: for attribute in ['last_type', 'last_value', 'last_traceback']: try: delattr(sys, attribute) except AttributeError: pass class Logger: """ Internal object used for logging. Initially this is not configured. Until you call ``manhole.install()``, this logger object won't work (will raise ``NotInstalled``). """ time = _get_original('time', 'time') enabled = True destination = None def configure(self, enabled, destination): self.enabled = enabled self.destination = destination def release(self): self.enabled = True self.destination = None def __call__(self, message): """ Fail-ignorant logging function. """ if self.enabled: if self.destination is None: raise NotInstalled('Manhole is not installed!') try: full_message = f'Manhole[{os.getpid()}:{self.time():.4f}]: {message}\n' if isinstance(self.destination, int): os.write(self.destination, full_message.encode('ascii', 'ignore')) else: self.destination.write(full_message) except Exception: pass _LOG = Logger() class Manhole: # Manhole core configuration # These are initialized when manhole is installed. daemon_connection = False locals = None original_os_fork = None original_os_forkpty = None redirect_stderr = True reinstall_delay = 0.5 should_restart = None sigmask = _ALL_SIGNALS socket_path = None start_timeout = 0.5 connection_handler = None previous_signal_handlers = None _thread = None def configure( self, patch_fork=True, activate_on=None, sigmask=_ALL_SIGNALS, oneshot_on=None, thread=True, start_timeout=0.5, socket_path=None, reinstall_delay=0.5, locals=None, daemon_connection=False, redirect_stderr=True, connection_handler=handle_connection_repl, ): self.socket_path = socket_path self.reinstall_delay = reinstall_delay self.redirect_stderr = redirect_stderr self.locals = locals self.sigmask = sigmask self.daemon_connection = daemon_connection self.start_timeout = start_timeout self.previous_signal_handlers = {} self.connection_handler = _CONNECTION_HANDLER_ALIASES.get(connection_handler, connection_handler) if oneshot_on is None and activate_on is None and thread: self.thread.start() self.should_restart = True if oneshot_on is not None: oneshot_on = getattr(signal, 'SIG' + oneshot_on) if isinstance(oneshot_on, string) else oneshot_on self.previous_signal_handlers.setdefault(oneshot_on, signal.signal(oneshot_on, self.handle_oneshot)) if activate_on is not None: activate_on = getattr(signal, 'SIG' + activate_on) if isinstance(activate_on, string) else activate_on if activate_on == oneshot_on: raise ConfigurationConflict( 'You cannot do activation of the Manhole thread on the same signal ' 'that you want to do oneshot activation !' ) self.previous_signal_handlers.setdefault(activate_on, signal.signal(activate_on, self.activate_on_signal)) atexit.register(self.remove_manhole_uds) if patch_fork: if activate_on is None and oneshot_on is None and socket_path is None: self.patch_os_fork_functions() else: if activate_on: _LOG(f'Not patching os.fork and os.forkpty. Activation is done by signal {activate_on}') elif oneshot_on: _LOG(f'Not patching os.fork and os.forkpty. Oneshot activation is done by signal {oneshot_on}') elif socket_path: _LOG(f'Not patching os.fork and os.forkpty. Using user socket path {socket_path}') def release(self): if self._thread: self._thread.stop() self._thread = None self.remove_manhole_uds() self.restore_os_fork_functions() for sig, handler in self.previous_signal_handlers.items(): signal.signal(sig, handler) self.previous_signal_handlers.clear() @property def thread(self): if self._thread is None: self._thread = ManholeThread( self.get_socket, self.sigmask, self.start_timeout, self.connection_handler, daemon_connection=self.daemon_connection ) return self._thread @thread.setter def thread(self, value): self._thread = value def get_socket(self): sock = _ORIGINAL_SOCKET(socket.AF_UNIX, socket.SOCK_STREAM) name = self.remove_manhole_uds() sock.bind(name) sock.listen(5) _LOG('Manhole UDS path: ' + name) return sock def reinstall(self): """ Reinstalls the manhole. Checks if the thread is running. If not, it starts it again. """ with _LOCK: if not (self.thread.is_alive() and self.thread in _ORIGINAL__ACTIVE): self.thread = self.thread.clone(bind_delay=self.reinstall_delay) if self.should_restart: self.thread.start() def handle_oneshot(self, _signum=None, _frame=None): try: try: sock = self.get_socket() _LOG(f'Waiting for new connection (in pid:{os.getpid()}) ...') client = force_original_socket(sock.accept()[0]) check_credentials(client) self.connection_handler(client) finally: self.remove_manhole_uds() except BaseException as exc: # pylint: disable=W0702 # we don't want to let any exception out, it might make the application misbehave _LOG(f'Oneshot failure: {exc!r}') def remove_manhole_uds(self): name = self.uds_name if os.path.exists(name): os.unlink(name) return name @property def uds_name(self): if self.socket_path is None: return f'/tmp/manhole-{os.getpid()}' return self.socket_path def patched_fork(self): """Fork a child process.""" pid = self.original_os_fork() if not pid: _LOG('Fork detected. Reinstalling Manhole.') self.reinstall() return pid def patched_forkpty(self): """Fork a new process with a new pseudo-terminal as controlling tty.""" pid, master_fd = self.original_os_forkpty() if not pid: _LOG('Fork detected. Reinstalling Manhole.') self.reinstall() return pid, master_fd def patch_os_fork_functions(self): self.original_os_fork, os.fork = os.fork, self.patched_fork self.original_os_forkpty, os.forkpty = os.forkpty, self.patched_forkpty _LOG(f'Patched {self.original_os_fork} and {self.original_os_forkpty}.') def restore_os_fork_functions(self): if self.original_os_fork: os.fork = self.original_os_fork if self.original_os_forkpty: os.forkpty = self.original_os_forkpty def activate_on_signal(self, _signum, _frame): self.thread.start() def install( verbose=True, verbose_destination=sys.__stderr__.fileno() if hasattr(sys.__stderr__, 'fileno') else sys.__stderr__, strict=True, **kwargs, ): """ Installs the manhole. Args: verbose (bool): Set it to ``False`` to squelch the logging. verbose_destination (file descriptor or handle): Destination for verbose messages. Default is unbuffered stderr (stderr ``2`` file descriptor). patch_fork (bool): Set it to ``False`` if you don't want your ``os.fork`` and ``os.forkpy`` monkeypatched activate_on (int or signal name): set to ``"USR1"``, ``"USR2"`` or some other signal name, or a number if you want the Manhole thread to start when this signal is sent. This is desireable in case you don't want the thread active all the time. oneshot_on (int or signal name): Set to ``"USR1"``, ``"USR2"`` or some other signal name, or a number if you want the Manhole to listen for connection in the signal handler. This is desireable in case you don't want threads at all. thread (bool): Start the always-on ManholeThread. Default: ``True``. Automatically switched to ``False`` if ``oneshort_on`` or ``activate_on`` are used. sigmask (list of ints or signal names): Will set the signal mask to the given list (using ``signalfd.sigprocmask``). No action is done if ``signalfd`` is not importable. **NOTE**: This is done so that the Manhole thread doesn't *steal* any signals; Normally that is fine because Python will force all the signal handling to be run in the main thread but signalfd doesn't. socket_path (str): Use a specific path for the unix domain socket (instead of ``/tmp/manhole-``). This disables ``patch_fork`` as children cannot reuse the same path. reinstall_delay (float): Delay the unix domain socket creation *reinstall_delay* seconds. This alleviates cleanup failures when using fork+exec patterns. locals (dict): Names to add to manhole interactive shell locals. daemon_connection (bool): The connection thread is daemonic (dies on app exit). Default: ``False``. redirect_stderr (bool): Redirect output from stderr to manhole console. Default: ``True``. connection_handler (function): Connection handler to use. Use ``"exec"`` for simple implementation without output redirection or your own function. (warning: this is for advanced users). Default: ``"repl"``. """ # pylint: disable=W0603 global _MANHOLE with _LOCK: if _MANHOLE is None: _MANHOLE = Manhole() else: if strict: raise AlreadyInstalled('Manhole already installed!') else: _LOG.release() _MANHOLE.release() # Threads might be started here _LOG.configure(verbose, verbose_destination) _MANHOLE.configure(**kwargs) # Threads might be started here return _MANHOLE def dump_stacktraces(): """ Dumps thread ids and tracebacks to stdout. """ lines = [] for thread_id, stack in sys._current_frames().items(): # pylint: disable=W0212 lines.append(f'\n######### ProcessID={os.getpid()}, ThreadID={thread_id} #########') for filename, lineno, name, line in traceback.extract_stack(stack): lines.append('File: "%s", line %d, in %s' % (filename, lineno, name)) if line: lines.append(f' {line.strip()}') lines.append('#############################################\n\n') print('\n'.join(lines), file=sys.stderr if _MANHOLE.redirect_stderr else sys.stdout) manhole-1.8.1/src/manhole/cli.py000077500000000000000000000101251501610110200164640ustar00rootroot00000000000000#!/usr/bin/env python import argparse import errno import os import re import readline import signal import socket import sys import threading import time try: input = raw_input except NameError: pass SIG_NAMES = {} SIG_NUMBERS = set() for sig, num in vars(signal).items(): if sig.startswith('SIG') and '_' not in sig: SIG_NAMES[sig] = num SIG_NAMES[sig[3:]] = num SIG_NUMBERS.add(num) def parse_pid(value, regex=re.compile(r'^(.*/manhole-)?(?P\d+)$')): match = regex.match(value) if not match: raise argparse.ArgumentTypeError('PID must be in one of these forms: 1234 or /tmp/manhole-1234') return int(match.group('pid')) def parse_signal(value): try: value = int(value) except ValueError: pass else: if value in SIG_NUMBERS: return value else: raise argparse.ArgumentTypeError( 'Invalid signal number {}. Expected one of: {}'.format(value, ', '.join(str(i) for i in SIG_NUMBERS)) ) value = value.upper() if value in SIG_NAMES: return SIG_NAMES[value] else: raise argparse.ArgumentTypeError(f'Invalid signal name {value!r}.') parser = argparse.ArgumentParser(description='Connect to a manhole.') parser.add_argument( 'pid', metavar='PID', type=parse_pid, help='A numerical process id, or a path in the form: /tmp/manhole-1234', # nargs='?', ) parser.add_argument('-t', '--timeout', dest='timeout', default=1, type=float, help='Timeout to use. Default: %(default)s seconds.') group = parser.add_mutually_exclusive_group() group.add_argument( '-1', '-USR1', dest='signal', action='store_const', const=int(signal.SIGUSR1), help='Send USR1 (%(const)s) to the process before connecting.', ) group.add_argument( '-2', '-USR2', dest='signal', action='store_const', const=int(signal.SIGUSR2), help='Send USR2 (%(const)s) to the process before connecting.', ) group.add_argument( '-s', '--signal', dest='signal', type=parse_signal, metavar='SIGNAL', help='Send the given SIGNAL to the process before connecting.' ) class ConnectionHandler(threading.Thread): def __init__(self, sock, is_closing): super().__init__() self.sock = sock self.is_closing = is_closing def run(self): while True: try: data = self.sock.recv(1024**2) if not data: break sys.stdout.write(data.decode('utf8')) sys.stdout.flush() readline.redisplay() except socket.timeout: pass if not self.is_closing.is_set(): # Break waiting for input() os.kill(os.getpid(), signal.SIGINT) def main(): args = parser.parse_args() histfile = os.path.join(os.path.expanduser('~'), '.manhole_history') try: readline.read_history_file(histfile) except OSError: pass import atexit atexit.register(readline.write_history_file, histfile) del histfile if args.signal: os.kill(args.pid, args.signal) start = time.time() uds_path = f'/tmp/manhole-{args.pid}' sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(args.timeout) while time.time() - start < args.timeout: try: sock.connect(uds_path) except Exception as exc: if exc.errno not in (errno.ENOENT, errno.ECONNREFUSED): print(f'Failed to connect to {uds_path!r}: {exc!r}', file=sys.stderr) else: break else: print(f'Failed to connect to {uds_path!r}: Timeout', file=sys.stderr) sys.exit(5) is_closing = threading.Event() thread = ConnectionHandler(sock, is_closing) thread.start() try: while thread.is_alive(): data = input() data += '\n' sock.sendall(data.encode('utf8')) except (EOFError, KeyboardInterrupt): pass finally: is_closing.set() sock.shutdown(socket.SHUT_WR) thread.join() sock.close() manhole-1.8.1/tests/000077500000000000000000000000001501610110200142715ustar00rootroot00000000000000manhole-1.8.1/tests/helper.py000066400000000000000000000242011501610110200161210ustar00rootroot00000000000000import atexit import errno import logging import os import signal import sys import time from functools import partial from typing import ClassVar TIMEOUT = int(os.getenv('MANHOLE_TEST_TIMEOUT', 10)) SOCKET_PATH = '/tmp/manhole-socket' OUTPUT = sys.__stdout__ def handle_sigterm(signo, _frame): # Simulate real termination print('Terminated', file=OUTPUT) sys.exit(128 + signo) # Handling sigterm ensure that atexit functions are called, and we do not leave # leftover /tmp/manhole-pid sockets. signal.signal(signal.SIGTERM, handle_sigterm) @atexit.register def log_exit(): print('In atexit handler.', file=OUTPUT) def setup_greenthreads(patch_threads=False): try: from gevent import monkey monkey.patch_all(thread=False) except (ImportError, SyntaxError): pass try: import eventlet eventlet.hubs.get_hub() # workaround for circular import issue in eventlet, # see https://github.com/eventlet/eventlet/issues/401 eventlet.monkey_patch(thread=False) except (ImportError, SyntaxError): pass def do_fork(): pid = os.fork() if pid: @atexit.register def cleanup(): try: os.kill(pid, signal.SIGINT) time.sleep(0.2) os.kill(pid, signal.SIGTERM) except OSError as e: if e.errno != errno.ESRCH: raise os.waitpid(pid, 0) else: time.sleep(TIMEOUT * 10) if __name__ == '__main__': logging.basicConfig( level=logging.DEBUG, format='[pid=%(process)d - %(asctime)s]: %(name)s - %(levelname)s - %(message)s', ) test_name = sys.argv[1] try: if os.getenv('PATCH_THREAD', False): import manhole setup_greenthreads(True) else: setup_greenthreads(True) import manhole if test_name == 'test_environ_variable_activation': print(f'Sleeping {TIMEOUT} seconds...') time.sleep(TIMEOUT) elif test_name == 'test_install_twice_not_strict': manhole.install(oneshot_on='USR2') manhole.install(strict=False) time.sleep(TIMEOUT) elif test_name == 'test_unbuffered': manhole.install(verbose=True) print(os.getpid()) for i in range(5): time.sleep(1) print(f'line{i}') sys.stdout.flush() elif test_name == 'test_log_fd': manhole.install(verbose=True, verbose_destination=2) manhole._LOG('whatever-1') manhole._LOG('whatever-2') elif test_name == 'test_log_fh': class Output: data: ClassVar = [] write = data.append manhole.install(verbose=True, verbose_destination=Output) manhole._LOG('whatever') if Output.data and ']: whatever' in Output.data[-1]: print('SUCCESS') elif test_name == 'test_activate_on_usr2': manhole.install(activate_on='USR2') for _ in range(TIMEOUT * 100): time.sleep(0.1) elif test_name == 'test_install_once': manhole.install() try: manhole.install() except manhole.AlreadyInstalled: print('ALREADY_INSTALLED') else: raise AssertionError('Did not raise AlreadyInstalled') elif test_name == 'test_stderr_doesnt_deadlock': import subprocess manhole.install() for i in range(50): print('running iteration', i) p = subprocess.Popen(['true']) print('waiting for process', p.pid) p.wait() print('process ended') path = '/tmp/manhole-%d' % p.pid if os.path.exists(path): os.unlink(path) raise AssertionError(path + ' exists !') print('SUCCESS') elif test_name == 'test_fork_exec': manhole.install(reinstall_delay=5) print('Installed.') time.sleep(0.2) pid = os.fork() print('Forked, pid =', pid) if pid: os.waitpid(pid, 0) path = '/tmp/manhole-%d' % pid if os.path.exists(path): os.unlink(path) raise AssertionError(path + ' exists !') else: try: time.sleep(1) print('Exec-ing `true`') os.execvp('true', ['true']) finally: os._exit(1) print('SUCCESS') elif test_name == 'test_activate_on_with_oneshot_on': manhole.install(activate_on='USR2', oneshot_on='USR2') for _ in range(TIMEOUT * 100): time.sleep(0.1) elif test_name == 'test_interrupt_on_accept': def handle_usr2(_sig, _frame): print('Got USR2') signal.signal(signal.SIGUSR2, handle_usr2) import ctypes import ctypes.util libpthread_path = ctypes.util.find_library('pthread') if not libpthread_path: raise ImportError('ctypes.util.find_library("pthread") failed') libpthread = ctypes.CDLL(libpthread_path) if not hasattr(libpthread, 'pthread_setname_np'): raise ImportError('libpthread.pthread_setname_np missing') pthread_kill = libpthread.pthread_kill pthread_kill.argtypes = [ctypes.c_void_p, ctypes.c_int] pthread_kill.restype = ctypes.c_int manhole.install(sigmask=None) for _ in range(15): time.sleep(0.1) print('Sending signal to manhole thread ...') pthread_kill(manhole._MANHOLE.thread.ident, signal.SIGUSR2) for _ in range(TIMEOUT * 100): time.sleep(0.1) elif test_name == 'test_oneshot_on_usr2': manhole.install(oneshot_on='USR2') for _ in range(TIMEOUT * 100): time.sleep(0.1) elif test_name.startswith('test_signalfd_weirdness'): signalled = False @partial(signal.signal, signal.SIGUSR1) def signal_handler(sig, _): print(f'Received signal {sig}') global signalled signalled = True if 'negative' in test_name: manhole.install(sigmask=None) else: manhole.install(sigmask=[signal.SIGUSR1]) time.sleep(0.3) # give the manhole a bit enough time to start print('Starting ...') import signalfd signalfd.sigprocmask(signalfd.SIG_BLOCK, [signal.SIGUSR1]) sys.setcheckinterval(1) for _ in range(100000): os.kill(os.getpid(), signal.SIGUSR1) print(f'signalled={signalled}') time.sleep(TIMEOUT * 10) elif test_name == 'test_auth_fail': manhole.get_peercred = lambda _: (-1, -1, -1) manhole.install() time.sleep(TIMEOUT * 10) elif test_name == 'test_socket_path': manhole.install(socket_path=SOCKET_PATH) time.sleep(TIMEOUT * 10) elif test_name == 'test_daemon_connection': manhole.install(daemon_connection=True) time.sleep(TIMEOUT) elif test_name == 'test_socket_path_with_fork': manhole.install(socket_path=SOCKET_PATH) time.sleep(TIMEOUT) do_fork() elif test_name == 'test_locals': manhole.install(socket_path=SOCKET_PATH, locals={'k1': 'v1', 'k2': 'v2'}) time.sleep(TIMEOUT) elif test_name == 'test_locals_after_fork': manhole.install(locals={'k1': 'v1', 'k2': 'v2'}) do_fork() elif test_name == 'test_redirect_stderr_default': manhole.install(socket_path=SOCKET_PATH) time.sleep(TIMEOUT) elif test_name == 'test_redirect_stderr_disabled': manhole.install(socket_path=SOCKET_PATH, redirect_stderr=False) time.sleep(TIMEOUT) elif test_name == 'test_sigmask': manhole.install(socket_path=SOCKET_PATH, sigmask=[signal.SIGUSR1]) time.sleep(TIMEOUT) elif test_name == 'test_connection_handler_exec_func': manhole.install(connection_handler=manhole.handle_connection_exec, locals={'tete': lambda: print('TETE')}) time.sleep(TIMEOUT * 10) elif test_name == 'test_connection_handler_exec_str': manhole.install(connection_handler='exec', locals={'tete': lambda: print('TETE')}) time.sleep(TIMEOUT * 10) else: manhole.install() time.sleep(0.3) # give the manhole a bit enough time to start if test_name == 'test_simple': time.sleep(TIMEOUT * 10) elif test_name == 'test_with_forkpty': time.sleep(1) pid, masterfd = os.forkpty() if pid: @atexit.register def cleanup(): try: os.kill(pid, signal.SIGINT) time.sleep(0.2) os.kill(pid, signal.SIGTERM) except OSError as e: if e.errno != errno.ESRCH: raise while not os.waitpid(pid, os.WNOHANG)[0]: try: os.write(2, os.read(masterfd, 1024)) except OSError as e: print('Error while reading from masterfd:', e) else: time.sleep(TIMEOUT * 10) elif test_name == 'test_with_fork': time.sleep(1) do_fork() else: raise RuntimeError('Invalid test spec.') except: # noqa print(f'Died with {sys.exc_info()[0].__name__}.', file=OUTPUT) import traceback traceback.print_exc(file=OUTPUT) print('DIED.', file=OUTPUT) manhole-1.8.1/tests/test_manhole.py000066400000000000000000000611601501610110200173310ustar00rootroot00000000000000import importlib.util import os import re import select import signal import socket import sys import time from contextlib import closing from ctypes.util import find_library import pytest import requests from process_tests import TestProcess from process_tests import TestSocket from process_tests import dump_on_error from process_tests import wait_for_strings TIMEOUT = int(os.getenv('MANHOLE_TEST_TIMEOUT', 10)) SOCKET_PATH = '/tmp/manhole-socket' HELPER = os.path.join(os.path.dirname(__file__), 'helper.py') def is_lib_available(lib): return find_library(lib) def is_module_available(mod): try: return importlib.util.find_spec(mod) except ImportError: return False def connect_to_manhole(uds_path): sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(0.5) for i in range(TIMEOUT): try: sock.connect(uds_path) return sock except Exception as exc: print(f'Failed to connect to {uds_path}: {exc}') if i + 1 == TIMEOUT: sock.close() raise time.sleep(1) def assert_manhole_running(proc, uds_path, oneshot=False, extra=None): sock = connect_to_manhole(uds_path) with TestSocket(sock) as client: with dump_on_error(client.read): wait_for_strings(client.read, TIMEOUT, 'ProcessID', 'ThreadID', '>>>') sock.send(b"print('FOOBAR')\n") wait_for_strings(client.read, TIMEOUT, 'FOOBAR') wait_for_strings(proc.read, TIMEOUT, f'UID:{os.getuid()}') if extra: extra(client) wait_for_strings(proc.read, TIMEOUT, 'Cleaned up.', *[] if oneshot else ['Waiting for new connection']) def test_log_when_uninstalled(): import manhole pytest.raises(manhole.NotInstalled, manhole._LOG, 'whatever') def test_log_fd(capfd): with TestProcess(sys.executable, HELPER, 'test_log_fd') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, ']: whatever-1', ']: whatever-2') def test_log_fh(monkeypatch, capfd): with TestProcess(sys.executable, HELPER, 'test_log_fh') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'SUCCESS') def test_simple(): with TestProcess(sys.executable, HELPER, 'test_simple') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') for _ in range(20): proc.buff.reset() assert_manhole_running(proc, uds_path) @pytest.mark.parametrize('variant', ['str', 'func']) def test_connection_handler_exec(variant): with TestProcess(sys.executable, HELPER, 'test_connection_handler_exec_' + variant) as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') for _ in range(200): proc.buff.reset() sock = connect_to_manhole(uds_path) wait_for_strings( proc.read, TIMEOUT, f'UID:{os.getuid()}', ) with TestSocket(sock) as client: with dump_on_error(client.read): sock.send(b"print('FOOBAR')\n") wait_for_strings(proc.read, TIMEOUT, 'FOOBAR') sock.send(b'tete()\n') wait_for_strings(proc.read, TIMEOUT, 'TETE') sock.send(b'exit()\n') wait_for_strings(proc.read, TIMEOUT, 'Exiting exec loop.') def test_install_once(): with TestProcess(sys.executable, HELPER, 'test_install_once') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'ALREADY_INSTALLED') def test_install_twice_not_strict(): with TestProcess(sys.executable, HELPER, 'test_install_twice_not_strict') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Not patching os.fork and os.forkpty. Oneshot activation is done by signal') wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') assert_manhole_running(proc, uds_path) @pytest.mark.xfail('sys.gettrace() and is_module_available("gevent") and is_module_available("__pypy__")') def test_daemon_connection(): with TestProcess(sys.executable, HELPER, 'test_daemon_connection') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') def terminate_and_read(client): proc.proc.send_signal(signal.SIGINT) wait_for_strings(proc.read, TIMEOUT, 'Died with KeyboardInterrupt', 'DIED.') for _ in range(5): client.sock.send(b'bogus()\n') time.sleep(0.05) print(repr(client.sock.recv(1024))) pytest.raises((socket.error, OSError), assert_manhole_running, proc, uds_path, extra=terminate_and_read) wait_for_strings(proc.read, TIMEOUT, 'In atexit handler') @pytest.mark.xfail( 'sys.gettrace() and is_module_available("gevent") and is_module_available("__pypy__") ' 'or is_module_available("eventlet") ' ) def test_non_daemon_connection(): with TestProcess(sys.executable, HELPER, 'test_simple') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') def terminate_and_read(client): proc.proc.send_signal(signal.SIGINT) wait_for_strings(proc.read, TIMEOUT, 'Died with KeyboardInterrupt') client.sock.send(b'bogus()\n') wait_for_strings(client.read, TIMEOUT, 'bogus') client.sock.send(b'doofus()\n') wait_for_strings(client.read, TIMEOUT, 'doofus') assert_manhole_running(proc, uds_path, extra=terminate_and_read, oneshot=True) wait_for_strings(proc.read, TIMEOUT, 'In atexit handler') def test_locals(): with TestProcess(sys.executable, HELPER, 'test_locals') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') check_locals(SOCKET_PATH) def test_locals_after_fork(): with TestProcess(sys.executable, HELPER, 'test_locals_after_fork') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Fork detected') proc.buff.reset() wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') child_uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') check_locals(child_uds_path) def check_locals(uds_path): sock = connect_to_manhole(uds_path) with TestSocket(sock) as client: with dump_on_error(client.read): wait_for_strings(client.read, TIMEOUT, '>>>') sock.send(b'from __future__ import print_function\n' b'print(k1, k2)\n') wait_for_strings(client.read, TIMEOUT, 'v1 v2') def test_fork_exec(): with TestProcess(sys.executable, HELPER, 'test_fork_exec') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'SUCCESS') def test_socket_path(): with TestProcess(sys.executable, HELPER, 'test_socket_path') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') proc.buff.reset() assert_manhole_running(proc, SOCKET_PATH) def test_socket_path_with_fork(): with TestProcess(sys.executable, '-u', HELPER, 'test_socket_path_with_fork') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Not patching os.fork and os.forkpty. Using user socket path') wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') sock = connect_to_manhole(SOCKET_PATH) with TestSocket(sock) as client: with dump_on_error(client.read): wait_for_strings(client.read, TIMEOUT, 'ProcessID', 'ThreadID', '>>>') sock.send(b"print('BEFORE FORK')\n") wait_for_strings(client.read, TIMEOUT, 'BEFORE FORK') time.sleep(2) sock.send(b"print('AFTER FORK')\n") wait_for_strings(client.read, TIMEOUT, 'AFTER FORK') def test_redirect_stderr_default(): with TestProcess(sys.executable, HELPER, 'test_redirect_stderr_default') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') sock = connect_to_manhole(SOCKET_PATH) with TestSocket(sock) as client: with dump_on_error(client.read): wait_for_strings(client.read, 1, '>>>') client.reset() sock.send(b'import sys\n' b"sys.stderr.write('OK')\n") wait_for_strings(client.read, 1, 'OK') def test_redirect_stderr_default_dump_stacktraces(): with TestProcess(sys.executable, HELPER, 'test_redirect_stderr_default') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') check_dump_stacktraces(SOCKET_PATH) def test_redirect_stderr_default_print_tracebacks(): with TestProcess(sys.executable, HELPER, 'test_redirect_stderr_default') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') check_print_tracebacks(SOCKET_PATH) def test_redirect_stderr_disabled(): with TestProcess(sys.executable, HELPER, 'test_redirect_stderr_disabled') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') sock = connect_to_manhole(SOCKET_PATH) with TestSocket(sock) as client: with dump_on_error(client.read): wait_for_strings(client.read, 1, '>>>') client.reset() sock.send(b'import sys\n' b"sys.stderr.write('STDERR')\n" b"sys.stdout.write('STDOUT')\n") wait_for_strings(client.read, 1, 'STDOUT') assert 'STDERR' not in client.read() def test_redirect_stderr_disabled_dump_stacktraces(): with TestProcess(sys.executable, HELPER, 'test_redirect_stderr_disabled') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') check_dump_stacktraces(SOCKET_PATH) def test_redirect_stderr_disabled_print_tracebacks(): with TestProcess(sys.executable, HELPER, 'test_redirect_stderr_disabled') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') check_print_tracebacks(SOCKET_PATH) def check_dump_stacktraces(uds_path): sock = connect_to_manhole(uds_path) with TestSocket(sock) as client: with dump_on_error(client.read): wait_for_strings(client.read, 1, '>>>') sock.send(b'dump_stacktraces()\n') # Start of dump wait_for_strings(client.read, 1, '#########', 'ThreadID=', '#########') # End of dump wait_for_strings(client.read, 1, '#############################################', '>>>') def check_print_tracebacks(uds_path): sock = connect_to_manhole(uds_path) with TestSocket(sock) as client: with dump_on_error(client.read): wait_for_strings(client.read, 1, '>>>') sock.send(b'NO_SUCH_NAME\n') wait_for_strings(client.read, 1, 'NameError:', "name 'NO_SUCH_NAME' is not defined", '>>>') def test_exit_with_grace(): with TestProcess(sys.executable, '-u', HELPER, 'test_simple') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.settimeout(0.05) sock.connect(uds_path) with TestSocket(sock) as client: with dump_on_error(client.read): wait_for_strings(client.read, TIMEOUT, 'ThreadID', 'ProcessID', '>>>') sock.send(b"print('FOOBAR')\n") wait_for_strings(client.read, TIMEOUT, 'FOOBAR') wait_for_strings(proc.read, TIMEOUT, f'UID:{os.getuid()}') sock.shutdown(socket.SHUT_WR) select.select([sock], [], [], 5) sock.recv(1024) try: sock.shutdown(socket.SHUT_RD) except Exception as exc: print(f'Failed to SHUT_RD: {exc}') try: sock.close() except Exception as exc: print(f'Failed to close socket: {exc}') wait_for_strings(proc.read, TIMEOUT, 'DONE.', 'Cleaned up.', 'Waiting for new connection') def test_with_fork(): with TestProcess(sys.executable, '-u', HELPER, 'test_with_fork') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') for _ in range(2): proc.buff.reset() assert_manhole_running(proc, uds_path) proc.buff.reset() wait_for_strings(proc.read, TIMEOUT, 'Fork detected') wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') new_uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] assert uds_path != new_uds_path wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') for _ in range(2): proc.buff.reset() assert_manhole_running(proc, new_uds_path) def test_with_forkpty(): with TestProcess(sys.executable, '-u', HELPER, 'test_with_forkpty') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') for _ in range(2): proc.buff.reset() assert_manhole_running(proc, uds_path) proc.buff.reset() wait_for_strings(proc.read, TIMEOUT, 'Fork detected') wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') new_uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] assert uds_path != new_uds_path wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') for _ in range(2): proc.buff.reset() assert_manhole_running(proc, new_uds_path) def test_auth_fail(): with TestProcess(sys.executable, '-u', HELPER, 'test_auth_fail') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') with closing(socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)) as sock: sock.settimeout(1) sock.connect(uds_path) try: assert b'' == sock.recv(1024) except socket.timeout: pass wait_for_strings( proc.read, TIMEOUT, "SuspiciousClient: Can't accept client with PID:-1 UID:-1 GID:-1. It doesn't match the current " 'EUID:', 'Waiting for new connection', ) proc.proc.send_signal(signal.SIGINT) @pytest.mark.skipif('not is_module_available("signalfd")') def test_sigprocmask(): with TestProcess(sys.executable, '-u', HELPER, 'test_signalfd_weirdness') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') wait_for_strings(proc.read, TIMEOUT, 'signalled=False') assert_manhole_running(proc, uds_path) @pytest.mark.skipif('not is_module_available("signalfd")') def test_sigprocmask_negative(): with TestProcess(sys.executable, '-u', HELPER, 'test_signalfd_weirdness_negative') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') wait_for_strings(proc.read, TIMEOUT, 'signalled=True') assert_manhole_running(proc, uds_path) def test_activate_on_usr2(): with TestProcess(sys.executable, '-u', HELPER, 'test_activate_on_usr2') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Not patching os.fork and os.forkpty. Activation is done by signal') pytest.raises(AssertionError, wait_for_strings, proc.read, TIMEOUT, '/tmp/manhole-') proc.signal(signal.SIGUSR2) wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') assert_manhole_running(proc, uds_path) def test_activate_on_with_oneshot_on(): with TestProcess(sys.executable, '-u', HELPER, 'test_activate_on_with_oneshot_on') as proc: with dump_on_error(proc.read): wait_for_strings( proc.read, TIMEOUT, 'You cannot do activation of the Manhole thread on the same signal that you want to do ' 'oneshot activation !', ) def test_oneshot_on_usr2(): with TestProcess(sys.executable, '-u', HELPER, 'test_oneshot_on_usr2') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Not patching os.fork and os.forkpty. Oneshot activation is done by signal') pytest.raises(AssertionError, wait_for_strings, proc.read, TIMEOUT, '/tmp/manhole-') proc.signal(signal.SIGUSR2) wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') assert_manhole_running(proc, uds_path, oneshot=True) def test_oneshot_on_usr2_error(): with TestProcess(sys.executable, '-u', HELPER, 'test_oneshot_on_usr2') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Not patching os.fork and os.forkpty. Oneshot activation is done by signal') pytest.raises(AssertionError, wait_for_strings, proc.read, TIMEOUT, '/tmp/manhole-') proc.signal(signal.SIGUSR2) wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') assert_manhole_running(proc, uds_path, oneshot=True, extra=lambda client: client.sock.send(b'raise SystemExit()\n')) proc.buff.reset() proc.signal(signal.SIGUSR2) wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') assert_manhole_running(proc, uds_path, oneshot=True) @pytest.mark.skipif('not is_lib_available("pthread")') def test_interrupt_on_accept(): with TestProcess(sys.executable, '-u', HELPER, 'test_interrupt_on_accept') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] only_on_old_python = ['Waiting for new connection'] if sys.version_info < (3, 5) else [] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection', 'Sending signal to manhole thread', *only_on_old_python) assert_manhole_running(proc, uds_path) def test_environ_variable_activation(): with TestProcess( sys.executable, '-u', HELPER, 'test_environ_variable_activation', env=dict(os.environ, PYTHONMANHOLE="oneshot_on='USR2',verbose=True"), ) as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Not patching os.fork and os.forkpty. Oneshot activation is done by signal') proc.signal(signal.SIGUSR2) wait_for_strings(proc.read, TIMEOUT, '/tmp/manhole-') uds_path = re.findall(r'(/tmp/manhole-\d+)', proc.read())[0] wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') assert_manhole_running(proc, uds_path, oneshot=True) @pytest.mark.skipif('not is_module_available("signalfd")') def test_sigmask(): with TestProcess(sys.executable, HELPER, 'test_sigmask') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'Waiting for new connection') sock = connect_to_manhole(SOCKET_PATH) with TestSocket(sock) as client: with dump_on_error(client.read): wait_for_strings(client.read, 1, '>>>') client.reset() # Python 2.7 returns [10L], Python 3 returns [10] sock.send( b'from __future__ import print_function\n' b'import signalfd\n' b'mask = signalfd.sigprocmask(signalfd.SIG_BLOCK, [])\n' b'print([int(n) for n in mask])\n' ) wait_for_strings(client.read, 1, '%s' % [int(signal.SIGUSR1)]) def test_stderr_doesnt_deadlock(): for _ in range(25 if is_module_available('__pypy__') else 100): with TestProcess(sys.executable, HELPER, 'test_stderr_doesnt_deadlock') as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'SUCCESS') @pytest.mark.skipif('is_module_available("__pypy__")') def test_uwsgi(): with TestProcess( 'uwsgi', '--master', '--processes', '1', '--no-orphans', '--log-5xx', '--single-interpreter', '--shared-socket', ':0', '--no-default-app', '--manage-script-name', '--http', '=0', '--mount', '=wsgi:application', *['--virtualenv', os.environ['VIRTUAL_ENV']] if 'VIRTUAL_ENV' in os.environ else [], ) as proc: with dump_on_error(proc.read): wait_for_strings(proc.read, TIMEOUT, 'uWSGI http bound') port = re.findall(r'uWSGI http bound on :(\d+) fd', proc.read())[0] assert requests.get(f'http://127.0.0.1:{port}/', timeout=TIMEOUT).text == 'OK' wait_for_strings(proc.read, TIMEOUT, 'spawned uWSGI worker 1') pid = re.findall(r'spawned uWSGI worker 1 \(pid: (\d+), ', proc.read())[0] for retry in range(5)[::-1]: with open('/tmp/manhole-pid', 'w') as fh: fh.write(pid) try: assert_manhole_running(proc, f'/tmp/manhole-{pid}', oneshot=True) except Exception: if not retry: raise else: break manhole-1.8.1/tests/test_manhole_cli.py000066400000000000000000000145111501610110200201560ustar00rootroot00000000000000import os import signal import sys import pytest from process_tests import TestProcess from process_tests import dump_on_error from process_tests import wait_for_strings try: import subprocess32 as subprocess except ImportError: import subprocess TIMEOUT = int(os.getenv('MANHOLE_TEST_TIMEOUT', 10)) HELPER = os.path.join(os.path.dirname(__file__), 'helper.py') pytest_plugins = ('pytester',) def test_pid_validation(): exc = pytest.raises(subprocess.CalledProcessError, subprocess.check_output, ['manhole-cli', 'asdfasdf'], stderr=subprocess.STDOUT) assert ( exc.value.output == b"""usage: manhole-cli [-h] [-t TIMEOUT] [-1 | -2 | -s SIGNAL] PID manhole-cli: error: argument PID: PID must be in one of these forms: 1234 or /tmp/manhole-1234 """ ) def test_sig_number_validation(): exc = pytest.raises( subprocess.CalledProcessError, subprocess.check_output, ['manhole-cli', '-s', '12341234', '12341234'], stderr=subprocess.STDOUT ) assert exc.value.output.startswith( b"""usage: manhole-cli [-h] [-t TIMEOUT] [-1 | -2 | -s SIGNAL] PID manhole-cli: error: argument -s/--signal: Invalid signal number 12341234. Expected one of: """ ) def test_help(testdir): result = testdir.run('manhole-cli', '--help') result.stdout.fnmatch_lines( [ 'usage: manhole-cli [-h] [-t TIMEOUT] [-1 | -2 | -s SIGNAL] PID', 'Connect to a manhole.', 'positional arguments:', ' PID A numerical process id, or a path in the form:*', ' -h, --help show this help message and exit', ' -t TIMEOUT, --timeout TIMEOUT', ' Timeout to use. Default: 1 seconds.', ' -1, -USR1 Send USR1 (*) to the process before connecting.', ' -2, -USR2 Send USR2 (*) to the process before connecting.', ' -s SIGNAL, --signal SIGNAL', ' Send the given SIGNAL to the process before*', ] ) def test_usr2(): with TestProcess(sys.executable, '-u', HELPER, 'test_oneshot_on_usr2') as service: with dump_on_error(service.read): wait_for_strings(service.read, TIMEOUT, 'Not patching os.fork and os.forkpty. Oneshot activation is done by signal') with TestProcess('manhole-cli', '-USR2', str(service.proc.pid), bufsize=0, stdin=subprocess.PIPE) as client: with dump_on_error(client.read): wait_for_strings(client.read, TIMEOUT, '(ManholeConsole)', '>>>') client.proc.stdin.write('1234+2345\n') wait_for_strings(client.read, TIMEOUT, '3579') def test_pid(): with TestProcess(sys.executable, HELPER, 'test_simple') as service: with dump_on_error(service.read): wait_for_strings(service.read, TIMEOUT, '/tmp/manhole-') with TestProcess('manhole-cli', str(service.proc.pid), bufsize=0, stdin=subprocess.PIPE) as client: with dump_on_error(client.read): wait_for_strings(client.read, TIMEOUT, '(ManholeConsole)', '>>>') client.proc.stdin.write('1234+2345\n') wait_for_strings(client.read, TIMEOUT, '3579') def test_path(): with TestProcess(sys.executable, HELPER, 'test_simple') as service: with dump_on_error(service.read): wait_for_strings(service.read, TIMEOUT, '/tmp/manhole-') with TestProcess('manhole-cli', f'/tmp/manhole-{service.proc.pid}', bufsize=0, stdin=subprocess.PIPE) as client: with dump_on_error(client.read): wait_for_strings(client.read, TIMEOUT, '(ManholeConsole)', '>>>') client.proc.stdin.write('1234+2345\n') wait_for_strings(client.read, TIMEOUT, '3579') def test_sig_usr2(): with TestProcess(sys.executable, '-u', HELPER, 'test_oneshot_on_usr2') as service: with dump_on_error(service.read): wait_for_strings(service.read, TIMEOUT, 'Not patching os.fork and os.forkpty. Oneshot activation is done by signal') with TestProcess('manhole-cli', '--signal=USR2', str(service.proc.pid), bufsize=0, stdin=subprocess.PIPE) as client: with dump_on_error(client.read): wait_for_strings(client.read, TIMEOUT, '(ManholeConsole)', '>>>') client.proc.stdin.write('1234+2345\n') wait_for_strings(client.read, TIMEOUT, '3579') def test_sig_usr2_full(): with TestProcess(sys.executable, '-u', HELPER, 'test_oneshot_on_usr2') as service: with dump_on_error(service.read): wait_for_strings(service.read, TIMEOUT, 'Not patching os.fork and os.forkpty. Oneshot activation is done by signal') with TestProcess('manhole-cli', '-s', 'SIGUSR2', str(service.proc.pid), bufsize=0, stdin=subprocess.PIPE) as client: with dump_on_error(client.read): wait_for_strings(client.read, TIMEOUT, '(ManholeConsole)', '>>>') client.proc.stdin.write('1234+2345\n') wait_for_strings(client.read, TIMEOUT, '3579') def test_sig_usr2_number(): with TestProcess(sys.executable, '-u', HELPER, 'test_oneshot_on_usr2') as service: with dump_on_error(service.read): wait_for_strings(service.read, TIMEOUT, 'Not patching os.fork and os.forkpty. Oneshot activation is done by signal') with TestProcess( 'manhole-cli', '-s', str(int(signal.SIGUSR2)), str(service.proc.pid), bufsize=0, stdin=subprocess.PIPE ) as client: with dump_on_error(client.read): wait_for_strings(client.read, TIMEOUT, '(ManholeConsole)', '>>>') client.proc.stdin.write('1234+2345\n') wait_for_strings(client.read, TIMEOUT, '3579') def test_unbuffered(): with TestProcess(sys.executable, '-u', HELPER, 'test_unbuffered') as service: with dump_on_error(service.read): with TestProcess('manhole-cli', str(service.proc.pid), bufsize=0, stdin=subprocess.PIPE) as client: with dump_on_error(client.read): wait_for_strings(client.read, TIMEOUT, '(ManholeConsole)', '>>>') for i in range(5): wait_for_strings(client.read, 5, f'line{i}') manhole-1.8.1/tests/wsgi.py000066400000000000000000000020331501610110200156120ustar00rootroot00000000000000import os import sys import manhole stack_dump_file = '/tmp/manhole-pid' uwsgi_signal_number = 17 try: import uwsgi if not os.path.exists(stack_dump_file): open(stack_dump_file, 'w') def open_manhole(dummy_signum): with open(stack_dump_file) as fh: pid = fh.read().strip() if pid == str(os.getpid()): inst = manhole.install(strict=False, thread=False) inst.handle_oneshot(dummy_signum, dummy_signum) uwsgi.register_signal(uwsgi_signal_number, 'workers', open_manhole) uwsgi.add_file_monitor(uwsgi_signal_number, stack_dump_file) print(f'Listening for stack manhole requests via {stack_dump_file!r}', file=sys.stderr) except ImportError: print('Not running under uwsgi; unable to configure manhole trigger', file=sys.stderr) except OSError: print(f'IOError creating manhole trigger {stack_dump_file!r}', file=sys.stderr) def application(env, sr): sr('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', '2')]) yield b'OK' manhole-1.8.1/tox.ini000066400000000000000000000037561501610110200144550ustar00rootroot00000000000000[testenv:bootstrap] deps = jinja2 tox skip_install = true commands = python ci/bootstrap.py --no-env passenv = * ; a generative tox configuration, see: https://tox.wiki/en/latest/user_guide.html#generative-environments [tox] envlist = clean, check, docs, {py38,py39,py310,py311,py312,pypy38,pypy39,pypy310}-{normal,signalfd}-{normal,gevent,eventlet}-{cover,nocov}, report ignore_basepython_conflict = true [testenv] basepython = pypy38: {env:TOXPYTHON:pypy3.8} pypy39: {env:TOXPYTHON:pypy3.9} pypy310: {env:TOXPYTHON:pypy3.10} py38: {env:TOXPYTHON:python3.8} py39: {env:TOXPYTHON:python3.9} py310: {env:TOXPYTHON:python3.10} py311: {env:TOXPYTHON:python3.11} py312: {env:TOXPYTHON:python3.12} {bootstrap,clean,check,report,docs,codecov,coveralls}: {env:TOXPYTHON:python3} setenv = PYTHONPATH={toxinidir}/tests PYTHONUNBUFFERED=yes TERM=xterm passenv = * usedevelop = cover: true nocov: false deps = pytest cover: pytest-cov requests process-tests eventlet: eventlet==0.36.1 gevent: gevent==24.2.1 {py27,py36,py37,py38,py39,py310,py311,py312}: uwsgi==2.0.26 commands = nocov: {posargs:pytest -vv --ignore=src} cover: {posargs:pytest --cov --cov-report=term-missing --cov-report=xml -vv} [testenv:check] deps = docutils check-manifest pre-commit readme-renderer pygments isort skip_install = true commands = python setup.py check --strict --metadata --restructuredtext check-manifest . pre-commit run --all-files --show-diff-on-failure [testenv:docs] usedevelop = true deps = -r{toxinidir}/docs/requirements.txt commands = sphinx-build {posargs:-E} -b html docs dist/docs sphinx-build -b linkcheck docs dist/docs [testenv:report] deps = coverage skip_install = true commands = coverage report coverage html [testenv:clean] commands = python setup.py clean coverage erase skip_install = true deps = setuptools coverage