dbxdebug 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dbxdebug/__init__.py +134 -0
- dbxdebug/capture_io.py +210 -0
- dbxdebug/cli.py +618 -0
- dbxdebug/dbx_kbd.py +392 -0
- dbxdebug/gdb.py +297 -0
- dbxdebug/html.py +559 -0
- dbxdebug/keyboard.py +205 -0
- dbxdebug/qmp.py +239 -0
- dbxdebug/utils.py +92 -0
- dbxdebug/video.py +239 -0
- dbxdebug-0.2.1.dist-info/METADATA +164 -0
- dbxdebug-0.2.1.dist-info/RECORD +14 -0
- dbxdebug-0.2.1.dist-info/WHEEL +4 -0
- dbxdebug-0.2.1.dist-info/entry_points.txt +2 -0
dbxdebug/__init__.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
dbxdebug - Client library for DOSBox-X remote debug protocols.
|
|
3
|
+
|
|
4
|
+
Provides:
|
|
5
|
+
- GDBClient: GDB remote serial protocol for debugging (memory, registers, breakpoints)
|
|
6
|
+
- QMPClient: QEMU Monitor Protocol for keyboard input
|
|
7
|
+
- Video/screen capture utilities
|
|
8
|
+
- Keyboard helpers for building key sequences
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from importlib.metadata import version
|
|
12
|
+
|
|
13
|
+
from .capture_io import ScreenRecorder, get_capture_path, load_capture, save_capture
|
|
14
|
+
from .dbx_kbd import (
|
|
15
|
+
DBX_KEY,
|
|
16
|
+
DBX_KEY_TO_QCODE,
|
|
17
|
+
QCODE_TO_DBX_KEY,
|
|
18
|
+
char_needs_shift,
|
|
19
|
+
char_to_qcode,
|
|
20
|
+
dbx_key_to_qcode,
|
|
21
|
+
qcode_to_dbx_key,
|
|
22
|
+
)
|
|
23
|
+
from .gdb import GDBClient
|
|
24
|
+
from .html import (
|
|
25
|
+
VGA_COLOR_NAMES,
|
|
26
|
+
VGA_COLORS,
|
|
27
|
+
analyze_dos_video_colors,
|
|
28
|
+
dos_video_to_html,
|
|
29
|
+
save_dos_video_html,
|
|
30
|
+
)
|
|
31
|
+
from .keyboard import (
|
|
32
|
+
ALT_F4,
|
|
33
|
+
ALT_TAB,
|
|
34
|
+
BACKSPACE,
|
|
35
|
+
CTRL_A,
|
|
36
|
+
CTRL_ALT_DEL,
|
|
37
|
+
CTRL_C,
|
|
38
|
+
CTRL_S,
|
|
39
|
+
CTRL_V,
|
|
40
|
+
CTRL_X,
|
|
41
|
+
CTRL_Z,
|
|
42
|
+
DELETE,
|
|
43
|
+
ENTER,
|
|
44
|
+
ESCAPE,
|
|
45
|
+
SPACE,
|
|
46
|
+
TAB,
|
|
47
|
+
alt_key,
|
|
48
|
+
ctrl_alt_key,
|
|
49
|
+
ctrl_key,
|
|
50
|
+
ctrl_shift_key,
|
|
51
|
+
digit_key,
|
|
52
|
+
function_key,
|
|
53
|
+
key_list,
|
|
54
|
+
number_keys,
|
|
55
|
+
shift_key,
|
|
56
|
+
)
|
|
57
|
+
from .qmp import QMPClient, QMPError
|
|
58
|
+
from .utils import hexdump, parse_x86_address
|
|
59
|
+
from .video import (
|
|
60
|
+
BDA_TIMER_TICK,
|
|
61
|
+
DOS_VIDEO_MEMORY_SIZE,
|
|
62
|
+
DOS_VIDEO_PAGE_ONE,
|
|
63
|
+
DOS_VIDEO_PAGE_TWO,
|
|
64
|
+
TIMER_FREQUENCY,
|
|
65
|
+
DOSVideoTools,
|
|
66
|
+
decode_vga_attribute,
|
|
67
|
+
format_attribute_info,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
__version__ = version("dbxdebug")
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
# Clients
|
|
74
|
+
"GDBClient",
|
|
75
|
+
"QMPClient",
|
|
76
|
+
"QMPError",
|
|
77
|
+
# Video tools
|
|
78
|
+
"DOSVideoTools",
|
|
79
|
+
"decode_vga_attribute",
|
|
80
|
+
"format_attribute_info",
|
|
81
|
+
"dos_video_to_html",
|
|
82
|
+
"save_dos_video_html",
|
|
83
|
+
"analyze_dos_video_colors",
|
|
84
|
+
# Video constants
|
|
85
|
+
"DOS_VIDEO_PAGE_ONE",
|
|
86
|
+
"DOS_VIDEO_PAGE_TWO",
|
|
87
|
+
"DOS_VIDEO_MEMORY_SIZE",
|
|
88
|
+
"BDA_TIMER_TICK",
|
|
89
|
+
"TIMER_FREQUENCY",
|
|
90
|
+
"VGA_COLORS",
|
|
91
|
+
"VGA_COLOR_NAMES",
|
|
92
|
+
# Key codes
|
|
93
|
+
"DBX_KEY",
|
|
94
|
+
"DBX_KEY_TO_QCODE",
|
|
95
|
+
"QCODE_TO_DBX_KEY",
|
|
96
|
+
"dbx_key_to_qcode",
|
|
97
|
+
"qcode_to_dbx_key",
|
|
98
|
+
"char_to_qcode",
|
|
99
|
+
"char_needs_shift",
|
|
100
|
+
# Keyboard helpers
|
|
101
|
+
"key_list",
|
|
102
|
+
"ctrl_key",
|
|
103
|
+
"alt_key",
|
|
104
|
+
"shift_key",
|
|
105
|
+
"ctrl_alt_key",
|
|
106
|
+
"ctrl_shift_key",
|
|
107
|
+
"function_key",
|
|
108
|
+
"digit_key",
|
|
109
|
+
"number_keys",
|
|
110
|
+
# Common key constants
|
|
111
|
+
"ENTER",
|
|
112
|
+
"ESCAPE",
|
|
113
|
+
"TAB",
|
|
114
|
+
"BACKSPACE",
|
|
115
|
+
"SPACE",
|
|
116
|
+
"DELETE",
|
|
117
|
+
"CTRL_C",
|
|
118
|
+
"CTRL_V",
|
|
119
|
+
"CTRL_X",
|
|
120
|
+
"CTRL_Z",
|
|
121
|
+
"CTRL_A",
|
|
122
|
+
"CTRL_S",
|
|
123
|
+
"CTRL_ALT_DEL",
|
|
124
|
+
"ALT_F4",
|
|
125
|
+
"ALT_TAB",
|
|
126
|
+
# Capture I/O
|
|
127
|
+
"ScreenRecorder",
|
|
128
|
+
"load_capture",
|
|
129
|
+
"save_capture",
|
|
130
|
+
"get_capture_path",
|
|
131
|
+
# Utilities
|
|
132
|
+
"parse_x86_address",
|
|
133
|
+
"hexdump",
|
|
134
|
+
]
|
dbxdebug/capture_io.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Capture file I/O utilities for .capture.gz format.
|
|
3
|
+
|
|
4
|
+
The .capture.gz format is a gzip-compressed pickle file containing
|
|
5
|
+
timestamped VGA screen captures.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import gzip
|
|
9
|
+
import pickle
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING, Any
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from .video import DOSVideoTools
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ScreenRecorder:
|
|
19
|
+
"""
|
|
20
|
+
Records screen captures with timestamps.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
with DOSVideoTools() as video:
|
|
24
|
+
recorder = ScreenRecorder()
|
|
25
|
+
|
|
26
|
+
# Manual capture
|
|
27
|
+
recorder.capture(video)
|
|
28
|
+
|
|
29
|
+
# Or timed recording
|
|
30
|
+
recorder.record(video, duration=10.0, sample_rate=50)
|
|
31
|
+
|
|
32
|
+
# Save
|
|
33
|
+
recorder.save("session.capture.gz")
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, metadata: dict | None = None):
|
|
37
|
+
"""
|
|
38
|
+
Initialize a new recorder.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
metadata: Optional dict of metadata to include in capture file
|
|
42
|
+
"""
|
|
43
|
+
self.screens: dict[int, list[str]] = {}
|
|
44
|
+
self.metadata: dict = metadata or {}
|
|
45
|
+
|
|
46
|
+
def capture(self, video: "DOSVideoTools") -> bool:
|
|
47
|
+
"""
|
|
48
|
+
Capture a single screen frame with current timestamp.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
video: DOSVideoTools instance
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
True if capture succeeded
|
|
55
|
+
"""
|
|
56
|
+
timestamp_ns = time.time_ns()
|
|
57
|
+
screen = video.screen_dump()
|
|
58
|
+
if screen is not None:
|
|
59
|
+
self.screens[timestamp_ns] = screen
|
|
60
|
+
return True
|
|
61
|
+
return False
|
|
62
|
+
|
|
63
|
+
def capture_raw(self, video: "DOSVideoTools") -> bool:
|
|
64
|
+
"""
|
|
65
|
+
Capture raw video memory (with attributes) with current timestamp.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
video: DOSVideoTools instance
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
True if capture succeeded
|
|
72
|
+
"""
|
|
73
|
+
timestamp_ns = time.time_ns()
|
|
74
|
+
raw = video.screen_raw()
|
|
75
|
+
if raw is not None:
|
|
76
|
+
self.screens[timestamp_ns] = raw # type: ignore
|
|
77
|
+
return True
|
|
78
|
+
return False
|
|
79
|
+
|
|
80
|
+
def record(
|
|
81
|
+
self,
|
|
82
|
+
video: "DOSVideoTools",
|
|
83
|
+
duration: float,
|
|
84
|
+
sample_rate: float = 50.0,
|
|
85
|
+
raw: bool = False,
|
|
86
|
+
) -> int:
|
|
87
|
+
"""
|
|
88
|
+
Record screens for a duration at specified sample rate.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
video: DOSVideoTools instance
|
|
92
|
+
duration: Recording duration in seconds
|
|
93
|
+
sample_rate: Samples per second (Hz)
|
|
94
|
+
raw: If True, capture raw bytes instead of text
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Number of frames captured
|
|
98
|
+
"""
|
|
99
|
+
sample_interval = 1.0 / sample_rate
|
|
100
|
+
total_samples = int(duration * sample_rate)
|
|
101
|
+
start_time = time.time()
|
|
102
|
+
captured = 0
|
|
103
|
+
|
|
104
|
+
capture_fn = self.capture_raw if raw else self.capture
|
|
105
|
+
|
|
106
|
+
for i in range(total_samples):
|
|
107
|
+
if capture_fn(video):
|
|
108
|
+
captured += 1
|
|
109
|
+
|
|
110
|
+
# Sleep until next sample time
|
|
111
|
+
elapsed = time.time() - start_time
|
|
112
|
+
next_sample_time = (i + 1) * sample_interval
|
|
113
|
+
sleep_time = next_sample_time - elapsed
|
|
114
|
+
if sleep_time > 0:
|
|
115
|
+
time.sleep(sleep_time)
|
|
116
|
+
|
|
117
|
+
self.metadata["duration"] = duration
|
|
118
|
+
self.metadata["sample_rate"] = sample_rate
|
|
119
|
+
return captured
|
|
120
|
+
|
|
121
|
+
def save(self, filepath: str | Path) -> None:
|
|
122
|
+
"""Save captured screens to file."""
|
|
123
|
+
data = {
|
|
124
|
+
"screens": self.screens,
|
|
125
|
+
**self.metadata,
|
|
126
|
+
}
|
|
127
|
+
save_capture(data, filepath)
|
|
128
|
+
|
|
129
|
+
def clear(self) -> None:
|
|
130
|
+
"""Clear all captured screens."""
|
|
131
|
+
self.screens.clear()
|
|
132
|
+
|
|
133
|
+
def __len__(self) -> int:
|
|
134
|
+
return len(self.screens)
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def timestamps(self) -> list[int]:
|
|
138
|
+
"""Get sorted list of capture timestamps (ns)."""
|
|
139
|
+
return sorted(self.screens.keys())
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def duration_seconds(self) -> float:
|
|
143
|
+
"""Get duration of capture in seconds."""
|
|
144
|
+
if len(self.screens) < 2:
|
|
145
|
+
return 0.0
|
|
146
|
+
ts = self.timestamps
|
|
147
|
+
return (ts[-1] - ts[0]) / 1_000_000_000
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def load_capture(filepath: str | Path) -> Any:
|
|
151
|
+
"""
|
|
152
|
+
Load a capture file (.capture.gz or legacy .pickle).
|
|
153
|
+
|
|
154
|
+
Automatically detects format based on extension.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
filepath: Path to the capture file
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
The unpickled capture data
|
|
161
|
+
"""
|
|
162
|
+
filepath = Path(filepath)
|
|
163
|
+
|
|
164
|
+
if filepath.suffix == ".gz" or filepath.name.endswith(".capture.gz"):
|
|
165
|
+
with gzip.open(filepath, "rb") as f:
|
|
166
|
+
return pickle.load(f)
|
|
167
|
+
else:
|
|
168
|
+
# Legacy .pickle format
|
|
169
|
+
with open(filepath, "rb") as f:
|
|
170
|
+
return pickle.load(f)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def save_capture(data: Any, filepath: str | Path) -> None:
|
|
174
|
+
"""
|
|
175
|
+
Save capture data to a .capture.gz file.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
data: The capture data to save
|
|
179
|
+
filepath: Output path (will use .capture.gz extension)
|
|
180
|
+
"""
|
|
181
|
+
filepath = Path(filepath)
|
|
182
|
+
|
|
183
|
+
# Ensure .capture.gz extension
|
|
184
|
+
if not filepath.name.endswith(".capture.gz"):
|
|
185
|
+
if filepath.suffix in (".pickle", ".pkl", ".gz"):
|
|
186
|
+
filepath = filepath.with_suffix(".capture.gz")
|
|
187
|
+
else:
|
|
188
|
+
filepath = Path(str(filepath) + ".capture.gz")
|
|
189
|
+
|
|
190
|
+
with gzip.open(filepath, "wb") as f:
|
|
191
|
+
pickle.dump(data, f)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def get_capture_path(base_name: str, output_dir: str | Path = ".") -> Path:
|
|
195
|
+
"""
|
|
196
|
+
Generate a capture file path with proper extension.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
base_name: Base name for the file (without extension)
|
|
200
|
+
output_dir: Output directory
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
Path object for the capture file
|
|
204
|
+
"""
|
|
205
|
+
output_dir = Path(output_dir)
|
|
206
|
+
|
|
207
|
+
# Remove any existing extension
|
|
208
|
+
base_name = base_name.replace(".pickle", "").replace(".capture.gz", "")
|
|
209
|
+
|
|
210
|
+
return output_dir / f"{base_name}.capture.gz"
|