Skip to content

Commit ae96735

Browse files
authored
Merge pull request #23 from cocolato/feat/support_rpdb
support rpdb
2 parents bb6fc75 + 75e93ff commit ae96735

File tree

7 files changed

+170
-7
lines changed

7 files changed

+170
-7
lines changed

README.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ It's a fork/optimized version from [elifiner/pydump](https://github.com/elifiner
88
* fix bug in python3.10+
99
* supported more pdb commnd
1010
* a useful command line tool for debug
11+
* supported remote debug (rpdb)
1112

1213

1314
Pydumpling writes the `python current traceback` into a file and
@@ -128,7 +129,7 @@ Type "help", "copyright", "credits" or "license" for more information.
128129

129130
### Use Command Line
130131

131-
Use command line to print the traceback:
132+
#### Use command line to print the traceback:
132133
`python -m pydumpling --print test.deump`
133134

134135
It will print:
@@ -144,7 +145,7 @@ TypeError: unsupported operand type(s) for +: 'int' and 'str'
144145
```
145146

146147

147-
Use command line to do pdb debug:
148+
#### Use command line to do pdb debug:
148149
`python -m pydumpling --debug test.deump`
149150

150151
It will open the pdb window:
@@ -153,5 +154,11 @@ It will open the pdb window:
153154
(Pdb)
154155
```
155156

157+
#### Use command line to do remote pdb debug:
158+
`python -m pydumpling --rdebug test.deump`
159+
It will open the debugger on port 4444, then we can access pdb using telnet、netcat... :
160+
`nc 127.0.0.1 4444`
161+
![alt text](static/rpdb.png)
162+
156163
## TODO
157164
- []

README_zh.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* 修复其在3.10+版本中的bug
77
* 支持更多的pdb命令
88
* 提供了一个方便用来调试的命令行工具
9+
* 支持服务器远程调试(remote pdb)
910

1011
pydumpling可以在代码的任何位置中,将当前Python程序的traceback写到一个文件中,可以稍后在Python调试器中加载它。目前pydump支持很多兼容PDB api的调试器(pdbpp, udb, ipdb)
1112

@@ -124,7 +125,7 @@ Type "help", "copyright", "credits" or "license" for more information.
124125

125126
### 命令行使用
126127

127-
使用命令行来打印traceback:
128+
#### 使用命令行来打印traceback:
128129
`python -m pydumpling --print test.deump`
129130

130131
将会输出:
@@ -140,7 +141,7 @@ TypeError: unsupported operand type(s) for +: 'int' and 'str'
140141
```
141142

142143

143-
使用命令行来进行pdb调试:
144+
#### 使用命令行来进行pdb调试:
144145
`python -m pydumpling --debug test.deump`
145146

146147
将会打开pdb调试会话:
@@ -149,5 +150,11 @@ TypeError: unsupported operand type(s) for +: 'int' and 'str'
149150
(Pdb)
150151
```
151152

153+
#### 使用命令行来进行remote pdb调试
154+
`python -m pydumpling --rdebug test.deump`
155+
它会在机器的4444端口上打开pdb调试器,然后我们可以在另外一台机器上使用telnet、netcat来进行远程调试:
156+
`nc 127.0.0.1 4444`
157+
![alt text](static/rpdb.png)
158+
152159
## TODO
153160
- []

pydumpling/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
from __future__ import absolute_import, division, print_function, unicode_literals
22

3+
from .rpdb import r_post_mortem
34
from .debug_dumpling import debug_dumpling, load_dumpling
45
from .pydumpling import save_dumping, dump_current_traceback, __version__
56

7+
68
__version__ == __version__
7-
__all__ = ["debug_dumpling", "load_dumpling", "save_dumping", "dump_current_traceback"]
9+
__all__ = ["debug_dumpling", "load_dumpling", "save_dumping", "dump_current_traceback", "r_post_mortem"]

pydumpling/cli.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import argparse
22
import os.path
33
from .debug_dumpling import debug_dumpling, load_dumpling
4+
from .rpdb import r_post_mortem
45
from .helpers import print_traceback_and_except
56

67
DUMP_FILE_EXTENSION: str = ".dump"
@@ -36,6 +37,12 @@ def validate_file_name(file_name: str) -> str:
3637
help="enter pdb debugging interface"
3738
)
3839

40+
pydumpling_cli_action_group.add_argument(
41+
"--rdebug",
42+
action="store_true",
43+
help="enter rpdb debugging interface"
44+
)
45+
3946
parser.add_argument(
4047
"filename",
4148
type=validate_file_name,
@@ -51,3 +58,5 @@ def main() -> None:
5158
print_traceback_and_except(dumpling_)
5259
elif args.debug:
5360
debug_dumpling(file_name)
61+
elif args.rdebug:
62+
r_post_mortem(file_name)

pydumpling/debug_dumpling.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@ def load_dumpling(filename):
1717
except Exception:
1818
return pickle.load(f)
1919

20-
21-
def debug_dumpling(dump_file, pdb=pdb):
20+
def mock_inspect():
2221
inspect.isframe = lambda obj: isinstance(
2322
obj, types.FrameType) or obj.__class__ == FakeFrame
2423
inspect.istraceback = lambda obj: isinstance(
2524
obj, types.TracebackType) or obj.__class__ == FakeTraceback
2625
inspect.iscode = lambda obj: isinstance(
2726
obj, types.CodeType) or obj.__class__ == FakeCode
27+
28+
def debug_dumpling(dump_file, pdb=pdb):
29+
mock_inspect()
2830
dumpling = load_dumpling(dump_file)
2931
if not StrictVersion("0.0.1") <= StrictVersion(dumpling["version"]) < StrictVersion("1.0.0"):
3032
raise ValueError("Unsupported dumpling version: %s" %

pydumpling/rpdb.py

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import pdb
2+
import socket
3+
import threading
4+
import sys
5+
import traceback
6+
from .debug_dumpling import load_dumpling, mock_inspect
7+
8+
DEFAULT_ADDR = "127.0.0.1"
9+
DEFAULT_PORT = 4444
10+
11+
12+
class FileObjectWrapper(object):
13+
def __init__(self, fileobject, stdio):
14+
self._obj = fileobject
15+
self._io = stdio
16+
17+
def __getattr__(self, attr):
18+
if hasattr(self._obj, attr):
19+
attr = getattr(self._obj, attr)
20+
elif hasattr(self._io, attr):
21+
attr = getattr(self._io, attr)
22+
else:
23+
raise AttributeError("Attribute %s is not found" % attr)
24+
return attr
25+
26+
27+
class Rpdb(pdb.Pdb):
28+
29+
def __init__(self, addr=DEFAULT_ADDR, port=DEFAULT_PORT):
30+
"""Initialize the socket and initialize pdb."""
31+
32+
# Backup stdin and stdout before replacing them by the socket handle
33+
self.old_stdout = sys.stdout
34+
self.old_stdin = sys.stdin
35+
self.port = port
36+
37+
# Open a 'reusable' socket to let the webapp reload on the same port
38+
self.skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
39+
self.skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
40+
self.skt.bind((addr, port))
41+
self.skt.listen(1)
42+
43+
# Writes to stdout are forbidden in mod_wsgi environments
44+
try:
45+
sys.stderr.write("pdb is running on %s:%d\n"
46+
% self.skt.getsockname())
47+
except IOError:
48+
pass
49+
50+
(clientsocket, address) = self.skt.accept()
51+
handle = clientsocket.makefile('rw')
52+
pdb.Pdb.__init__(self, completekey='tab',
53+
stdin=FileObjectWrapper(handle, self.old_stdin),
54+
stdout=FileObjectWrapper(handle, self.old_stdin))
55+
sys.stdout = sys.stdin = handle
56+
self.handle = handle
57+
OCCUPIED.claim(port, sys.stdout)
58+
59+
def shutdown(self):
60+
"""Revert stdin and stdout, close the socket."""
61+
sys.stdout = self.old_stdout
62+
sys.stdin = self.old_stdin
63+
self.handle.close()
64+
OCCUPIED.unclaim(self.port)
65+
self.skt.shutdown(socket.SHUT_RDWR)
66+
self.skt.close()
67+
68+
def do_continue(self, arg):
69+
"""Clean-up and do underlying continue."""
70+
try:
71+
return pdb.Pdb.do_continue(self, arg)
72+
finally:
73+
self.shutdown()
74+
75+
do_c = do_cont = do_continue
76+
77+
def do_quit(self, arg):
78+
"""Clean-up and do underlying quit."""
79+
try:
80+
return pdb.Pdb.do_quit(self, arg)
81+
finally:
82+
self.shutdown()
83+
84+
do_q = do_exit = do_quit
85+
86+
def do_EOF(self, arg):
87+
"""Clean-up and do underlying EOF."""
88+
try:
89+
return pdb.Pdb.do_EOF(self, arg)
90+
finally:
91+
self.shutdown()
92+
93+
94+
class OccupiedPorts(object):
95+
"""Maintain rpdb port versus stdin/out file handles.
96+
97+
Provides the means to determine whether or not a collision binding to a
98+
particular port is with an already operating rpdb session.
99+
100+
Determination is according to whether a file handle is equal to what is
101+
registered against the specified port.
102+
"""
103+
104+
def __init__(self):
105+
self.lock = threading.RLock()
106+
self.claims = {}
107+
108+
def claim(self, port, handle):
109+
self.lock.acquire(True)
110+
self.claims[port] = id(handle)
111+
self.lock.release()
112+
113+
def is_claimed(self, port, handle):
114+
self.lock.acquire(True)
115+
got = (self.claims.get(port) == id(handle))
116+
self.lock.release()
117+
return got
118+
119+
def unclaim(self, port):
120+
self.lock.acquire(True)
121+
del self.claims[port]
122+
self.lock.release()
123+
124+
# {port: sys.stdout} pairs to track recursive rpdb invocation on same port.
125+
# This scheme doesn't interfere with recursive invocations on separate ports -
126+
# useful, eg, for concurrently debugging separate threads.
127+
OCCUPIED = OccupiedPorts()
128+
129+
130+
def r_post_mortem(dump_file, addr=DEFAULT_ADDR, port=DEFAULT_PORT):
131+
mock_inspect()
132+
dumpling = load_dumpling(dump_file)
133+
tb = dumpling["traceback"]
134+
debugger = Rpdb(addr=addr, port=port)
135+
debugger.reset()
136+
debugger.interaction(None, tb)

static/rpdb.png

11.7 KB
Loading

0 commit comments

Comments
 (0)