Skip to content

Commit fe0222e

Browse files
authored
Merge branch 'main' into compatibility-for-python3.12
2 parents 0d91f6d + d2ef5ae commit fe0222e

File tree

10 files changed

+186
-20
lines changed

10 files changed

+186
-20
lines changed

README.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ English | [简体中文](README_zh.md)
55
It's a fork/optimized version from [elifiner/pydump](https://github.com/elifiner/pydump).The main optimization points are:
66
* Save the `Python traceback` anywhere, not just when it's an exception.
77
* Optimize code structure && remove redundant code
8-
* fix bug in python2.7 && support python3.10+
8+
* 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
@@ -22,11 +23,10 @@ pdb and with other popular debuggers (pudb, ipdb and pdbpp).
2223
* If we were able to save the exception error and then use the debugger to recover the traceback at that time, we could see the entire stack variables along the traceback as if you had caught the exception at the local breakpoint.
2324

2425
## Install pydumpling
25-
Python version:>= 2.7, >=3.6
26+
Python version:>=3.7
2627

27-
Not published in pypi,so use the `.whl` file install pydumpling in the dist path.
2828
```
29-
pip install dist/pydumpling-0.1.1-py2.py3-none-any.whl
29+
pip install pydumpling
3030
```
3131

3232
## How to use pydumpling
@@ -129,7 +129,7 @@ Type "help", "copyright", "credits" or "license" for more information.
129129

130130
### Use Command Line
131131

132-
Use command line to print the traceback:
132+
#### Use command line to print the traceback:
133133
`python -m pydumpling --print test.deump`
134134

135135
It will print:
@@ -145,7 +145,7 @@ TypeError: unsupported operand type(s) for +: 'int' and 'str'
145145
```
146146

147147

148-
Use command line to do pdb debug:
148+
#### Use command line to do pdb debug:
149149
`python -m pydumpling --debug test.deump`
150150

151151
It will open the pdb window:
@@ -154,5 +154,11 @@ It will open the pdb window:
154154
(Pdb)
155155
```
156156

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+
157163
## TODO
158164
- []

README_zh.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
这是 [elifiner/pydump](https://github.com/elifiner/pydump) 的fork/优化版本, 主要优化点有:
44
* 支持在任何地方保存`Python traceback`,而不是只在异常发生的时候
55
* 优化代码结构, 去除冗余代码
6-
* 修复其在2.7及3.10版本中的bug
6+
* 修复其在3.10+版本中的bug
77
* 支持更多的pdb命令
88
* 提供了一个方便用来调试的命令行工具
9+
* 支持服务器远程调试(remote pdb)
910

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

@@ -17,11 +18,10 @@ pydumpling可以在代码的任何位置中,将当前Python程序的traceback
1718
* 如果我们能够把线上的异常现场保存下来,然后通过调试器去恢复当时的异常堆栈,我们可以看到这个异常的整条调用链路以及链路上的堆栈变量,就如同你在本地断点捕获到了这个异常一样。
1819

1920
## 安装方法
20-
Python版本支持:>= 2.7, >=3.6
21+
Python版本支持:>=3.7
2122

22-
目前还没有在Pypi上面进行发布,因此直接使用PIP安装dist目录下的whl文件
2323
```
24-
pip install dist/pydumpling-0.1.1-py2.py3-none-any.whl
24+
pip install -i pydumpling
2525
```
2626

2727
## 使用方法
@@ -125,7 +125,7 @@ Type "help", "copyright", "credits" or "license" for more information.
125125

126126
### 命令行使用
127127

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

131131
将会输出:
@@ -141,7 +141,7 @@ TypeError: unsupported operand type(s) for +: 'int' and 'str'
141141
```
142142

143143

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

147147
将会打开pdb调试会话:
@@ -150,5 +150,11 @@ TypeError: unsupported operand type(s) for +: 'int' and 'str'
150150
(Pdb)
151151
```
152152

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+
153159
## TODO
154160
- []

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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,17 @@ def load_dumpling(filename):
1818
return pickle.load(f)
1919

2020

21-
def debug_dumpling(dump_file, pdb=pdb):
21+
def mock_inspect():
2222
inspect.isframe = lambda obj: isinstance(
2323
obj, types.FrameType) or obj.__class__ == FakeFrame
2424
inspect.istraceback = lambda obj: isinstance(
2525
obj, types.TracebackType) or obj.__class__ == FakeTraceback
2626
inspect.iscode = lambda obj: isinstance(
2727
obj, types.CodeType) or obj.__class__ == FakeCode
28+
29+
30+
def debug_dumpling(dump_file, pdb=pdb):
31+
mock_inspect()
2832
dumpling = load_dumpling(dump_file)
2933
if not parse("0.0.1") <= parse(dumpling["version"]) < parse("1.0.0"):
3034
raise ValueError("Unsupported dumpling version: %s" %

pydumpling/fake_types.py

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

33
import os
4-
import six
4+
import sys
55
import dill
66

77

@@ -43,7 +43,7 @@ def _convert(cls, v):
4343
else:
4444
from datetime import date, time, datetime, timedelta
4545

46-
BUILTIN = (str, unicode, int, long, float, date, time, datetime, timedelta) if six.PY2 \
46+
BUILTIN = (str, unicode, int, long, float, date, time, datetime, timedelta) if sys.version_info.major == 2 \
4747
else (str, int, float, date, time, datetime, timedelta) # noqa: F821
4848

4949
if type(v) in BUILTIN:

pydumpling/pydumpling.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import inspect
99
from .fake_types import FakeFrame, FakeTraceback
1010

11-
__version__ = "0.1.1"
11+
__version__ = "0.1.4"
1212

1313

1414
def save_dumping(filename=None, tb=None):

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

pyproject.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ includes = ["pydumpling/*.py"]
1111
test = [
1212
"pytest-order>=1.2.0",
1313
"flake8>=5.0.4",
14-
"six>=1.16.0",
1514
]
1615
dev = [
1716
"tox-pdm>=0.6.1",
@@ -23,8 +22,8 @@ build-backend = "pdm.backend"
2322

2423
[project]
2524
name = "pydumpling"
26-
version = "0.1.2"
27-
description = ""
25+
version = "0.1.4"
26+
description = "Python post-mortem debugger"
2827
authors = [
2928
{name = "cocolato", email = "[email protected]"},
3029
]
@@ -36,3 +35,7 @@ dependencies = [
3635
requires-python = ">=3.7"
3736
readme = "README.md"
3837
license = {text = "MIT"}
38+
39+
40+
[project.urls]
41+
homepage = "https://github.com/cocolato/pydumpling"

static/rpdb.png

11.7 KB
Loading

0 commit comments

Comments
 (0)