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/video.py ADDED
@@ -0,0 +1,239 @@
1
+ """
2
+ DOS video memory access utilities.
3
+
4
+ Provides screen capture and video memory inspection for DOS text mode.
5
+ """
6
+
7
+ from loguru import logger
8
+
9
+ from .gdb import GDBClient
10
+
11
+ # DOS video memory addresses
12
+ DOS_VIDEO_PAGE_ONE = "0xB800:0000"
13
+ DOS_VIDEO_PAGE_TWO = "0xB800:1000"
14
+ DOS_VIDEO_MEMORY_SIZE = 0xFA0 # 4000 bytes (80 * 25 * 2)
15
+
16
+ # BIOS Data Area addresses
17
+ BDA_MODE = "0x0040:0049"
18
+ BDA_COLUMN_COUNT = "0x0040:004A"
19
+ BDA_ROW_COUNT = "0x0040:0084"
20
+ BDA_TIMER_TICK = "0x0040:006C" # 4-byte tick counter, 18.2065 Hz
21
+
22
+ # Timer constants
23
+ TIMER_FREQUENCY = 18.2065 # Hz
24
+
25
+
26
+ class DOSVideoTools:
27
+ """Tools for analyzing DOS program screen output."""
28
+
29
+ def __init__(self, host: str = "localhost", port: int = GDBClient.DEFAULT_PORT):
30
+ """
31
+ Initialize video tools with GDB connection.
32
+
33
+ Args:
34
+ host: GDB server hostname
35
+ port: GDB server port
36
+ """
37
+ self.gdb = GDBClient(host, port)
38
+
39
+ def close(self) -> None:
40
+ """Close the GDB connection."""
41
+ self.gdb.close()
42
+
43
+ def __enter__(self) -> "DOSVideoTools":
44
+ return self
45
+
46
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
47
+ self.close()
48
+
49
+ def read_timer_ticks(self) -> int | None:
50
+ """
51
+ Read the BIOS timer tick counter (18.2065 Hz).
52
+
53
+ Returns:
54
+ Tick count or None on error
55
+ """
56
+ try:
57
+ data = self.gdb.read_memory(BDA_TIMER_TICK, 4)
58
+ return int.from_bytes(data, "little")
59
+ except Exception as e:
60
+ logger.exception(e)
61
+ return None
62
+
63
+ def read_video_mode(self) -> int | None:
64
+ """
65
+ Read the current video mode.
66
+
67
+ Returns:
68
+ Video mode number or None on error
69
+ """
70
+ try:
71
+ data = self.gdb.read_memory(BDA_MODE, 1)
72
+ return data[0]
73
+ except Exception as e:
74
+ logger.exception(e)
75
+ return None
76
+
77
+ def screen_dump(self, page: int = 1) -> list[str] | None:
78
+ """
79
+ Dump the DOS text screen as a list of strings.
80
+
81
+ Args:
82
+ page: Video page number (1 or 2)
83
+
84
+ Returns:
85
+ List of 25 strings (80 chars each) or None on error
86
+ """
87
+ try:
88
+ addr = DOS_VIDEO_PAGE_ONE if page == 1 else DOS_VIDEO_PAGE_TWO
89
+ memory = self.gdb.read_memory(addr, DOS_VIDEO_MEMORY_SIZE)
90
+
91
+ lines = []
92
+ for row in range(25):
93
+ line_text = ""
94
+ for col in range(80):
95
+ char_index = (row * 80 + col) * 2
96
+ if char_index < len(memory):
97
+ char = memory[char_index]
98
+ if char == 0:
99
+ line_text += " "
100
+ elif 32 <= char <= 126:
101
+ line_text += chr(char)
102
+ else:
103
+ line_text += chr(char)
104
+ else:
105
+ line_text += " "
106
+ lines.append(line_text)
107
+ return lines
108
+ except Exception as e:
109
+ logger.exception(e)
110
+ return None
111
+
112
+ def screen_dump_with_ticks(self) -> tuple[list[str] | None, int | None]:
113
+ """
114
+ Dump screen and timer ticks together for timing correlation.
115
+
116
+ Returns:
117
+ Tuple of (lines, ticks) or (None, None) on error
118
+ """
119
+ try:
120
+ # Read timer first (small read, fast)
121
+ tick_data = self.gdb.read_memory(BDA_TIMER_TICK, 4)
122
+ ticks = int.from_bytes(tick_data, "little")
123
+
124
+ # Then read screen
125
+ memory = self.gdb.read_memory(DOS_VIDEO_PAGE_ONE, DOS_VIDEO_MEMORY_SIZE)
126
+
127
+ lines = []
128
+ for row in range(25):
129
+ line_text = ""
130
+ for col in range(80):
131
+ char_index = (row * 80 + col) * 2
132
+ if char_index < len(memory):
133
+ char = memory[char_index]
134
+ if char == 0 or char == 32:
135
+ line_text += " "
136
+ elif 32 <= char <= 126:
137
+ line_text += chr(char)
138
+ else:
139
+ line_text += chr(char)
140
+ else:
141
+ line_text += " "
142
+ lines.append(line_text)
143
+ return (lines, ticks)
144
+ except Exception as e:
145
+ logger.exception(e)
146
+ return (None, None)
147
+
148
+ def screen_raw(self, page: int = 1) -> bytes | None:
149
+ """
150
+ Read raw video memory (characters and attributes).
151
+
152
+ Args:
153
+ page: Video page number (1 or 2)
154
+
155
+ Returns:
156
+ Raw bytes or None on error
157
+ """
158
+ try:
159
+ addr = DOS_VIDEO_PAGE_ONE if page == 1 else DOS_VIDEO_PAGE_TWO
160
+ return self.gdb.read_memory(addr, DOS_VIDEO_MEMORY_SIZE)
161
+ except Exception as e:
162
+ logger.exception(e)
163
+ return None
164
+
165
+ def screen_debug(self) -> list[bytes] | None:
166
+ """
167
+ Read raw video memory from both pages.
168
+
169
+ Returns:
170
+ List of [page1_bytes, page2_bytes] or None on error
171
+ """
172
+ try:
173
+ return [
174
+ self.gdb.read_memory(DOS_VIDEO_PAGE_ONE, DOS_VIDEO_MEMORY_SIZE),
175
+ self.gdb.read_memory(DOS_VIDEO_PAGE_TWO, DOS_VIDEO_MEMORY_SIZE),
176
+ ]
177
+ except Exception as e:
178
+ logger.exception(e)
179
+ return None
180
+
181
+
182
+ def decode_vga_attribute(attr_byte: int) -> dict:
183
+ """
184
+ Decode a VGA text mode attribute byte.
185
+
186
+ Attribute format: IRGB irgb
187
+ - Upper 4 bits: background color (IRGB)
188
+ - Lower 4 bits: foreground color (irgb)
189
+ - Bit 7: blink flag
190
+
191
+ Args:
192
+ attr_byte: Attribute byte value
193
+
194
+ Returns:
195
+ Dict with foreground, background, colors, and blink flag
196
+ """
197
+ color_names = [
198
+ "Black",
199
+ "Blue",
200
+ "Green",
201
+ "Cyan",
202
+ "Red",
203
+ "Magenta",
204
+ "Brown",
205
+ "Light Gray",
206
+ "Dark Gray",
207
+ "Light Blue",
208
+ "Light Green",
209
+ "Light Cyan",
210
+ "Light Red",
211
+ "Light Magenta",
212
+ "Yellow",
213
+ "White",
214
+ ]
215
+
216
+ foreground = attr_byte & 0x0F
217
+ background = (attr_byte & 0x70) >> 4
218
+ blink = (attr_byte & 0x80) != 0
219
+
220
+ return {
221
+ "foreground": foreground,
222
+ "background": background,
223
+ "fg_color": color_names[foreground],
224
+ "bg_color": color_names[background],
225
+ "blink": blink,
226
+ "raw_value": attr_byte,
227
+ }
228
+
229
+
230
+ def format_attribute_info(attr_byte: int) -> str:
231
+ """Format attribute information as a readable string."""
232
+ info = decode_vga_attribute(attr_byte)
233
+
234
+ result = f"Attribute 0x{attr_byte:02X}:\n"
235
+ result += f" Foreground: {info['fg_color']} (0x{info['foreground']:X})\n"
236
+ result += f" Background: {info['bg_color']} (0x{info['background']:X})\n"
237
+ result += f" Blinking: {'Yes' if info['blink'] else 'No'}\n"
238
+
239
+ return result
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: dbxdebug
3
+ Version: 0.2.1
4
+ Summary: Client library and CLI for DOSBox-X remote debug protocols (GDB and QMP)
5
+ Author: lokkju
6
+ License-Expression: MIT
7
+ Keywords: debugging,dosbox,dosbox-x,emulator,gdb,qmp
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: POSIX
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Software Development :: Debuggers
16
+ Classifier: Topic :: System :: Emulators
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: click>=8.0
19
+ Requires-Dist: loguru>=0.7
20
+ Description-Content-Type: text/markdown
21
+
22
+ # dbxdebug
23
+
24
+ Client library and CLI for DOSBox-X remote debug protocols.
25
+
26
+ ## Features
27
+
28
+ - **GDB Client**: Remote debugging via GDB Remote Serial Protocol
29
+ - Memory read/write
30
+ - Register inspection
31
+ - Breakpoint management
32
+ - Execution control (step, continue, halt)
33
+
34
+ - **QMP Client**: Keyboard input via QEMU Monitor Protocol
35
+ - Key press/release with timing control
36
+ - Text typing with shift handling
37
+ - Key combinations (Ctrl+C, Alt+F4, etc.)
38
+
39
+ - **Video Tools**: DOS text mode screen capture
40
+ - Screen capture (text, raw, HTML)
41
+ - Timed multi-frame recording
42
+ - Color analysis
43
+ - BIOS timer correlation
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ uv sync
49
+ ```
50
+
51
+ ## CLI Usage
52
+
53
+ ```
54
+ dbxdebug
55
+ ├── mem # Memory operations (alias: gdb)
56
+ │ ├── read # Read memory
57
+ │ └── write # Write hex bytes
58
+
59
+ ├── cpu # CPU and execution control
60
+ │ ├── regs # Display registers
61
+ │ ├── break # Set breakpoint
62
+ │ ├── delete # Remove breakpoint
63
+ │ ├── step # Single step
64
+ │ ├── cont # Continue execution
65
+ │ └── halt # Stop execution
66
+
67
+ ├── key # Keyboard input (alias: qmp)
68
+ │ ├── send # Key chord (e.g., ctrl c)
69
+ │ ├── type # Type text string
70
+ │ ├── down # Press and hold key
71
+ │ ├── up # Release key
72
+ │ └── list # List QMP commands
73
+
74
+ └── screen # Screen capture
75
+ ├── show # Display text to stdout
76
+ ├── capture # Save frame to file (-f raw|html|text)
77
+ ├── record # Multi-frame timed capture
78
+ ├── watch # Real-time display
79
+ ├── info # Video mode, BIOS ticks
80
+ └── colors # Analyze color palette
81
+ ```
82
+
83
+ ### Examples
84
+
85
+ ```bash
86
+ # Memory
87
+ dbxdebug mem read b800:0000 4000 --hex
88
+ dbxdebug mem write 0x1000 90909090
89
+
90
+ # CPU / Debugging
91
+ dbxdebug cpu regs
92
+ dbxdebug cpu break 0x1000
93
+ dbxdebug cpu step
94
+ dbxdebug cpu cont
95
+
96
+ # Keyboard
97
+ dbxdebug key send a
98
+ dbxdebug key send ctrl c
99
+ dbxdebug key send ctrl alt delete
100
+ dbxdebug key type "Hello World!"
101
+
102
+ # Screen
103
+ dbxdebug screen show
104
+ dbxdebug screen capture -f html -o snapshot
105
+ dbxdebug screen record -d 60 -r 30 -o session.capture.gz
106
+ dbxdebug screen watch
107
+ dbxdebug screen colors
108
+ ```
109
+
110
+ ## Library Usage
111
+
112
+ ```python
113
+ from dbxdebug import (
114
+ GDBClient, QMPClient, DOSVideoTools,
115
+ ScreenRecorder, load_capture,
116
+ ctrl_key, CTRL_C, DBX_KEY,
117
+ )
118
+
119
+ # Memory and debugging
120
+ with GDBClient() as gdb:
121
+ regs = gdb.read_registers()
122
+ mem = gdb.read_memory("b800:0000", 4000)
123
+ gdb.write_memory(0x1000, b"\x90\x90")
124
+ gdb.set_breakpoint(0x1000)
125
+ gdb.step()
126
+ gdb.continue_execution()
127
+
128
+ # Keyboard input
129
+ with QMPClient() as qmp:
130
+ qmp.send_key(["ctrl", "c"]) # Key chord
131
+ qmp.send_key(CTRL_C) # Using constant
132
+ qmp.type_text("Hello World!") # Type string
133
+ qmp.key_down("shift") # Hold key
134
+ qmp.key_up("shift") # Release key
135
+
136
+ # Screen capture
137
+ with DOSVideoTools() as video:
138
+ lines = video.screen_dump() # Text lines
139
+ raw = video.screen_raw() # Raw bytes with attrs
140
+ lines, ticks = video.screen_dump_with_ticks()
141
+
142
+ # Timed recording
143
+ with DOSVideoTools() as video:
144
+ recorder = ScreenRecorder()
145
+ recorder.record(video, duration=10.0, sample_rate=50)
146
+ recorder.save("session.capture.gz")
147
+
148
+ # Load and analyze capture
149
+ data = load_capture("session.capture.gz")
150
+ ```
151
+
152
+ ## Ports
153
+
154
+ | Protocol | Default Port | Purpose |
155
+ |----------|--------------|---------|
156
+ | GDB | 2159 | Debugging, memory access |
157
+ | QMP | 4444 | Keyboard input |
158
+
159
+ Enable in DOSBox-X config:
160
+ ```ini
161
+ [dosbox]
162
+ gdbserver=true
163
+ qmpserver=true
164
+ ```
@@ -0,0 +1,14 @@
1
+ dbxdebug/__init__.py,sha256=9VE7gig7l6WQMdXdYa2_NFrYkBDldAIjw2lRlgESu1M,2650
2
+ dbxdebug/capture_io.py,sha256=fxHHLnXky29_K8r0j2FpeOP4nx9BtmLtjJ8NT4zgkNc,5681
3
+ dbxdebug/cli.py,sha256=kIfWbLntUgHj_xGq19hoerEwU4J-lL1yxZx8fSzX8CM,21269
4
+ dbxdebug/dbx_kbd.py,sha256=nVE9Br9G-FajK0ZX3wrlm-sngtQ2aQilNGTlhsT3smY,9409
5
+ dbxdebug/gdb.py,sha256=RzJM0cekLk6-v6-x0ldLBIoKHLR4iFYnXc_dRMEOQyo,8730
6
+ dbxdebug/html.py,sha256=AQLSsRU5gQ-O7UO0vgqSE7L6D40yZja_jH0Yz9Ce4lQ,15098
7
+ dbxdebug/keyboard.py,sha256=CdS5i7g4PYLCsjtJkopS3mY5DE6zz1d-BmAK5grDOZc,4025
8
+ dbxdebug/qmp.py,sha256=hkNiMyg_VBHX8RH4SKIXY36T4BqLbmvcHlErTI8lUTo,7212
9
+ dbxdebug/utils.py,sha256=bEai_WicXvnGTTaTf0DJtNJ78vI0g-m0KfnuegZ7fXY,2703
10
+ dbxdebug/video.py,sha256=knQdG0hmtLzufjrA_MqWAmh-lMWyKQ4ZWf7uKMMVIC0,6981
11
+ dbxdebug-0.2.1.dist-info/METADATA,sha256=Li4B-9M91UQqPeRl20F4qXxjSoQ3L01McxJ6aZVZ15E,4562
12
+ dbxdebug-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
13
+ dbxdebug-0.2.1.dist-info/entry_points.txt,sha256=lzVCl-Qns7sa1y8IO6Jil3LeQPKupF7vTESe7LeUICg,47
14
+ dbxdebug-0.2.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dbxdebug = dbxdebug.cli:main