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 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"