├── .gitignore ├── Makefile ├── README.rst ├── example └── loop.py ├── pdbinject ├── __init__.py ├── debugger.py ├── rpdb.py └── runner.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info/ 3 | /dist 4 | /build 5 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | develop: 3 | pip install -e . 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Install 2 | ======= 3 | 4 | You'll need GDB compiled with python support, and the ``pdbinject`` package: 5 | 6 | :: 7 | 8 | easy_install pdbinject 9 | 10 | 11 | OS X Notes 12 | ---------- 13 | 14 | The default GDB does not come compiled with Python support. 15 | 16 | Homebrew fixes that: 17 | 18 | :: 19 | 20 | brew install gdb 21 | 22 | .. note:: There's more steps, I gave up on figuring out how to make it work. 23 | 24 | 25 | Usage 26 | ===== 27 | 28 | :: 29 | 30 | $ python example/loop.py & 31 | Process running with PID 6319 32 | 33 | $ sudo pdbinject 6319 34 | Remote PDB has been configured on port 4444 35 | 36 | nc 127.0.0.1 4444 37 | 38 | $ nc 127.0.0.1 4444 39 | --Return-- 40 | > /home/ubuntu/pdbinject/pdbinject/debugger.py(16)run()->None 41 | -> debugger.set_trace() 42 | 43 | Now have some fun: 44 | 45 | :: 46 | 47 | from guppy import hpy 48 | hp = hpy() 49 | heap = hp.heap() 50 | heap.get_rp() 51 | 52 | To print the stacktrace across all the threads: 53 | 54 | :: 55 | 56 | import sys, traceback 57 | for thread_id, stack in sys._current_frames().iteritems(): print 'Thread id: %s\n%s' % (thread_id, ''.join(traceback.format_stack(stack))) 58 | -------------------------------------------------------------------------------- /example/loop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import time 4 | import os 5 | 6 | 7 | def main(): 8 | print 'Process running with PID', os.getpid() 9 | while 1: 10 | time.sleep(5) 11 | 12 | 13 | if __name__ == '__main__': 14 | main() 15 | -------------------------------------------------------------------------------- /pdbinject/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dcramer/pdbinject/0c9d90b0b1d452c4572782909148d5f44af94f58/pdbinject/__init__.py -------------------------------------------------------------------------------- /pdbinject/debugger.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import 2 | 3 | from .rpdb import Rpdb 4 | 5 | from threading import Thread 6 | 7 | 8 | class RemoteDebuggerThread(Thread): 9 | def __init__(self, addr='127.0.0.1', port=4444): 10 | self.addr = addr 11 | self.port = port 12 | Thread.__init__(self) 13 | 14 | def run(self): 15 | debugger = Rpdb(addr=self.addr, port=self.port) 16 | debugger.set_trace() 17 | -------------------------------------------------------------------------------- /pdbinject/rpdb.py: -------------------------------------------------------------------------------- 1 | """Remote Python Debugger (pdb wrapper).""" 2 | # Based on https://github.com/tamentis/rpdb 3 | from __future__ import absolute_import 4 | 5 | __author__ = "Bertrand Janin " 6 | __version__ = "0.1.2" 7 | 8 | import pdb 9 | import socket 10 | import sys 11 | 12 | 13 | class Rpdb(pdb.Pdb): 14 | 15 | def __init__(self, addr="127.0.0.1", port=4444): 16 | """Initialize the socket and initialize pdb.""" 17 | 18 | # Writes to stdout are forbidden in mod_wsgi environments 19 | try: 20 | print("pdb is running on %s:%d" % (addr, port)) 21 | except IOError: 22 | pass 23 | 24 | # Backup stdin and stdout before replacing them by the socket handle 25 | self.old_stdout = sys.stdout 26 | self.old_stdin = sys.stdin 27 | 28 | # Open a 'reusable' socket to let the webapp reload on the same port 29 | self.skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 30 | self.skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True) 31 | self.skt.bind((addr, port)) 32 | self.skt.listen(1) 33 | (clientsocket, address) = self.skt.accept() 34 | handle = clientsocket.makefile('rw') 35 | pdb.Pdb.__init__(self, completekey='tab', stdin=handle, stdout=handle) 36 | sys.stdout = sys.stdin = handle 37 | 38 | def shutdown(self): 39 | """Revert stdin and stdout, close the socket.""" 40 | sys.stdout = self.old_stdout 41 | sys.stdin = self.old_stdin 42 | self.skt.close() 43 | self.set_continue() 44 | 45 | def do_continue(self, arg): 46 | """Stop all operation on ``continue``.""" 47 | self.shutdown() 48 | return 1 49 | 50 | do_EOF = do_quit = do_exit = do_c = do_cont = do_continue 51 | 52 | 53 | def set_trace(): 54 | """Wrapper function to keep the same import x; x.set_trace() interface. 55 | 56 | We catch all the possible exceptions from pdb and cleanup. 57 | 58 | """ 59 | debugger = Rpdb() 60 | try: 61 | debugger.set_trace(sys._getframe().f_back) 62 | finally: 63 | debugger.shutdown() 64 | -------------------------------------------------------------------------------- /pdbinject/runner.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import absolute_import, print_function 4 | 5 | import os 6 | import sys 7 | import subprocess 8 | import tempfile 9 | import textwrap 10 | import optparse 11 | 12 | 13 | def inject(pid, debugger, 14 | rpdb_addr='127.0.0.1', 15 | rpdb_port=4444): 16 | """Executes a file in a running Python process.""" 17 | # TODO: rpdb stuff 18 | 19 | with tempfile.NamedTemporaryFile() as f: 20 | s = textwrap.dedent("""\ 21 | import time, sys; 22 | sys.path.append('%(path)s'); 23 | from pdbinject.debugger import RemoteDebuggerThread; 24 | thread = RemoteDebuggerThread('%(rpdb_addr)s', %(rdpb_port)d); 25 | thread.start(); 26 | time.sleep(1); 27 | """ % dict( 28 | path=os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)), 29 | rpdb_addr=rpdb_addr, 30 | rdpb_port=rpdb_port, 31 | )) 32 | 33 | if debugger == 'gdb': 34 | print('call PyGILState_Ensure()', file=f) 35 | print('call PyRun_SimpleString("%s")' % s.replace('\n','').replace('"', '\\"'), file=f) 36 | print('call PyGILState_Release($1)', file=f) 37 | 38 | args = ['gdb', '-p', str(pid), '--batch', '--command', f.name] 39 | elif debugger == 'lldb': 40 | print('call (PyGILState_STATE)PyGILState_Ensure()', file=f) 41 | print('call (int)PyRun_SimpleString("%s")' % s.replace('\n','').replace('"', '\\"'), file=f) 42 | print('call (void)PyGILState_Release($0)', file=f) 43 | print('exit', file=f) 44 | 45 | args = ['lldb', '-p', str(pid), '-s', f.name] 46 | else: 47 | raise ValueError('unknown debugger') 48 | 49 | f.flush() 50 | 51 | with open(os.devnull) as stdin: 52 | subprocess.check_call(args, 53 | close_fds=True, 54 | stdin=stdin) 55 | 56 | print("Remote PDB has been configured on port %s" % rpdb_port) 57 | print("") 58 | print(" nc %s %s" % (rpdb_addr, rpdb_port,)) 59 | 60 | 61 | def main(): 62 | parser = optparse.OptionParser('usage: %prog [options] pid') 63 | parser.add_option('--address', 64 | default='127.0.0.1', 65 | help='address to launch the server on') 66 | parser.add_option('-p', '--port', 67 | default=4444, 68 | type='int', 69 | help='port to launch pdb on') 70 | 71 | if os.uname()[0] == 'Darwin': 72 | default_debugger = 'lldb' 73 | else: 74 | default_debugger = 'gdb' 75 | 76 | parser.add_option('-d', '--debugger', 77 | default=default_debugger, 78 | choices=('gdb', 'lldb'), 79 | help='which debugger to use to attach to the process (gdb on linux, lldb on OS X)') 80 | 81 | options, args = parser.parse_args() 82 | 83 | if len(args) != 1: 84 | print('pid not specified', file=sys.stderr) 85 | return 1 86 | 87 | try: 88 | pid = int(args[0]) 89 | except TypeError: 90 | print('pid must be an integer', file=sys.stderr) 91 | return 1 92 | 93 | inject(pid, options.debugger, 94 | rpdb_addr=options.address, 95 | rpdb_port=options.port) 96 | 97 | return 0 98 | 99 | 100 | if __name__ == '__main__': 101 | sys.exit(main()) 102 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup, find_packages 4 | 5 | setup( 6 | name='pdbinject', 7 | version='0.1.0', 8 | author='David Cramer', 9 | author_email='dcramer@gmail.com', 10 | url='http://github.com/dcramer/pdbinject', 11 | packages=find_packages(exclude=['example']), 12 | zip_safe=False, 13 | entry_points={ 14 | 'console_scripts': [ 15 | 'pdbinject = pdbinject.runner:main', 16 | ], 17 | }, 18 | classifiers=[ 19 | 'Intended Audience :: Developers', 20 | 'Intended Audience :: System Administrators', 21 | 'Operating System :: OS Independent', 22 | 'Topic :: Software Development' 23 | ], 24 | ) 25 | --------------------------------------------------------------------------------