Manhole - In-process Python Debugging Shell
Manhole is an in-process Python service that accepts Unix domain socket connections to provide stack traces for all threads and an interactive Python prompt. It can operate as a daemon thread or a signal handler. It is inspired by Twisted's manhole and focuses on simplicity with no external dependencies. The current version is 1.8.1, and releases appear to be on a somewhat irregular, feature-driven cadence.
Warnings
- breaking Support for Python 2.6, 3.3, and 3.4 was dropped in v1.6.0. Applications running on these older Python versions will require an older `manhole` release (e.g., <1.6.0).
- gotcha Previous versions (before v1.7.0) had a memory leak due to `sys.last_type`, `sys.last_value`, and `sys.last_traceback` not being cleared properly, and could also suffer from double-close bugs in stream handling.
- gotcha When `socket.setdefaulttimeout()` is used in your application, older `manhole` versions (before v1.6.0) might exhibit unexpected behavior. This was fixed in v1.6.0.
- gotcha `manhole-cli` in versions prior to v1.7.0 was more strict about PID argument parsing, primarily expecting paths prefixed with `/tmp`. This was loosened in v1.7.0 to allow paths with any prefix.
- gotcha By default, calling `manhole.install()` multiple times will raise an `AlreadyInstalled` exception. This is part of 'strict' mode.
- gotcha Integrating `manhole` with uWSGI requires special configuration because uWSGI overrides signal handling. The recommended approach involves using uWSGI's internal signals or a file-based PID mechanism.
- gotcha Manhole can also be installed via the `PYTHONMANHOLE` environment variable. If set, Manhole might be automatically activated, potentially conflicting with explicit `manhole.install()` calls or altering expected behavior.
Install
-
pip install manhole
Imports
- install
import manhole manhole.install()
- handle_connection_repl
from manhole import handle_connection_repl
- handle_connection_exec
from manhole import handle_connection_exec
Quickstart
import manhole
import time
import os
import sys
def main():
print(f"Manhole will listen on /tmp/manhole-{os.getpid()}\n")
manhole.install(sigmask=["USR1"], verbose=True) # Install manhole with default settings
print("Manhole installed. Waiting for connection...")
# Simulate a running application
i = 0
while True:
print(f"App running... {i}")
time.sleep(2)
i += 1
if i == 5: # Optionally demonstrate manual activation/deactivation
print("Sending USR1 to self to potentially activate/deactivate manhole if configured as 'activate_on'")
try:
os.kill(os.getpid(), 10) # SIGUSR1
except AttributeError:
print("SIGUSR1 not available on this OS, skipping manual signal.")
if __name__ == '__main__':
import subprocess
# Start the application with manhole installed
app_process = subprocess.Popen([sys.executable, __file__])
time.sleep(3)
# Connect to the manhole using manhole-cli
print(f"\nConnecting to manhole-cli for PID: {app_process.pid}...")
try:
# The `manhole-cli` attempts to connect to /tmp/manhole-<PID>
# Note: 'socat readline' provides a better interactive experience.
# 'manhole-cli' is simpler for demonstration.
subprocess.run(['manhole-cli', str(app_process.pid)], check=True)
except FileNotFoundError:
print("manhole-cli not found. Please ensure it's installed and in your PATH.")
except subprocess.CalledProcessError as e:
print(f"manhole-cli failed: {e}")
finally:
app_process.terminate()
app_process.wait()
print("\nApplication terminated.")