dbxdebug 0.4.0__tar.gz → 0.5.0__tar.gz
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-0.4.0 → dbxdebug-0.5.0}/.github/workflows/release.yml +3 -3
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/.github/workflows/test.yml +6 -6
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/.pre-commit-config.yaml +22 -7
- dbxdebug-0.5.0/.release-please-manifest.json +3 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/CHANGELOG.md +14 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/PKG-INFO +55 -18
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/README.md +54 -17
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/docs/migration.md +59 -35
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/pyproject.toml +17 -3
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/skills/debug-dos-programs/SKILL.md +8 -6
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/skills/debug-dos-programs/references/recipes.md +43 -11
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/skills/debug-dos-programs/references/troubleshooting.md +20 -10
- dbxdebug-0.5.0/src/dbxdebug/__init__.py +282 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/src/dbxdebug/addressing.py +11 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/src/dbxdebug/capture_io.py +7 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/src/dbxdebug/dbx_kbd.py +10 -0
- dbxdebug-0.5.0/src/dbxdebug/frames.py +427 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/src/dbxdebug/gdb.py +200 -10
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/src/dbxdebug/html.py +10 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/src/dbxdebug/keyboard.py +37 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/src/dbxdebug/paths.py +7 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/src/dbxdebug/qmp.py +140 -1
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/src/dbxdebug/registry.py +13 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/src/dbxdebug/session.py +10 -1
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/src/dbxdebug/utils.py +5 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/src/dbxdebug/video.py +17 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/integration/test_live_session.py +507 -1
- dbxdebug-0.5.0/tests/test_exports.py +195 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/test_frames.py +144 -11
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/test_gdb_framing.py +232 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/test_qmp_commands.py +126 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/uv.lock +36 -40
- dbxdebug-0.4.0/.release-please-manifest.json +0 -3
- dbxdebug-0.4.0/src/dbxdebug/__init__.py +0 -137
- dbxdebug-0.4.0/src/dbxdebug/frames.py +0 -285
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/.github/dependabot.yml +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/.gitignore +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/LICENSE +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/plugin.json +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/release-please-config.json +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/skills/dosbox-x-debug-protocol/SKILL.md +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/skills/dosbox-x-debug-protocol/references/history.md +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/skills/dosbox-x-debug-protocol/references/packets.md +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/skills/dosbox-x-debug-protocol/references/qmp-commands.md +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/src/dbxdebug/cli.py +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/src/dbxdebug/doctor.py +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/__init__.py +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/integration/__init__.py +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/integration/conftest.py +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/integration/test_headless.py +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/test_addressing.py +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/test_basic.py +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/test_cli_session.py +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/test_gdb_capabilities.py +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/test_gdb_registers.py +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/test_paths.py +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/test_read_bulk.py +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/test_registry.py +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/test_session.py +0 -0
- {dbxdebug-0.4.0 → dbxdebug-0.5.0}/tests/test_video_client_ownership.py +0 -0
|
@@ -39,7 +39,7 @@ jobs:
|
|
|
39
39
|
release_created: ${{ steps.release.outputs.release_created }}
|
|
40
40
|
tag_name: ${{ steps.release.outputs.tag_name }}
|
|
41
41
|
steps:
|
|
42
|
-
- uses: googleapis/release-please-action@
|
|
42
|
+
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
|
|
43
43
|
id: release
|
|
44
44
|
with:
|
|
45
45
|
config-file: release-please-config.json
|
|
@@ -65,13 +65,13 @@ jobs:
|
|
|
65
65
|
id-token: write # OIDC for PyPI Trusted Publishing
|
|
66
66
|
contents: write # upload built artifacts to the GitHub Release
|
|
67
67
|
steps:
|
|
68
|
-
- uses: actions/checkout@
|
|
68
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
69
69
|
with:
|
|
70
70
|
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || needs.release-please.outputs.tag_name }}
|
|
71
71
|
fetch-depth: 0 # Full history + tags for hatch-vcs version
|
|
72
72
|
|
|
73
73
|
- name: Install uv and set up Python
|
|
74
|
-
uses: astral-sh/setup-uv@
|
|
74
|
+
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
75
75
|
with:
|
|
76
76
|
python-version: "3.11"
|
|
77
77
|
|
|
@@ -18,10 +18,10 @@ jobs:
|
|
|
18
18
|
lint:
|
|
19
19
|
runs-on: ubuntu-latest
|
|
20
20
|
steps:
|
|
21
|
-
- uses: actions/checkout@
|
|
21
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
22
22
|
|
|
23
23
|
- name: Install uv and set up Python
|
|
24
|
-
uses: astral-sh/setup-uv@
|
|
24
|
+
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
25
25
|
with:
|
|
26
26
|
python-version: "3.11"
|
|
27
27
|
|
|
@@ -47,10 +47,10 @@ jobs:
|
|
|
47
47
|
matrix:
|
|
48
48
|
python-version: ["3.11", "3.12"]
|
|
49
49
|
steps:
|
|
50
|
-
- uses: actions/checkout@
|
|
50
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
51
51
|
|
|
52
52
|
- name: Install uv and set up Python ${{ matrix.python-version }}
|
|
53
|
-
uses: astral-sh/setup-uv@
|
|
53
|
+
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
54
54
|
with:
|
|
55
55
|
python-version: ${{ matrix.python-version }}
|
|
56
56
|
|
|
@@ -64,12 +64,12 @@ jobs:
|
|
|
64
64
|
needs: test
|
|
65
65
|
runs-on: ubuntu-latest
|
|
66
66
|
steps:
|
|
67
|
-
- uses: actions/checkout@
|
|
67
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
68
68
|
with:
|
|
69
69
|
fetch-depth: 0 # Full history for hatch-vcs
|
|
70
70
|
|
|
71
71
|
- name: Install uv
|
|
72
|
-
uses: astral-sh/setup-uv@
|
|
72
|
+
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
73
73
|
|
|
74
74
|
- name: Build (packaging smoke test)
|
|
75
75
|
run: uv build
|
|
@@ -27,19 +27,32 @@ repos:
|
|
|
27
27
|
hooks:
|
|
28
28
|
- id: gitleaks
|
|
29
29
|
|
|
30
|
-
# Ruff for linting and formatting
|
|
30
|
+
# Ruff for linting and formatting.
|
|
31
|
+
#
|
|
32
|
+
# This rev and the `ruff` floor in pyproject.toml are ONE version, pinned in
|
|
33
|
+
# two places, and they must be bumped together. Two ruffs run against this
|
|
34
|
+
# repo -- this hook on every commit, and the resolved dev environment in CI
|
|
35
|
+
# -- and when they drifted six minor versions apart they disagreed about
|
|
36
|
+
# formatting: 0.8.4 rewrote a multi-line `assert ..., "message"` into a shape
|
|
37
|
+
# 0.14.9's `--check` then rejected, so a commit blessed here was refused by
|
|
38
|
+
# CI and each tool undid the other (lokkju/dbxdebug#17). Neither reports its
|
|
39
|
+
# version in the diff it produces, which is what made that confusing.
|
|
31
40
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
32
|
-
rev: v0.
|
|
41
|
+
rev: v0.14.9
|
|
33
42
|
hooks:
|
|
34
|
-
-
|
|
43
|
+
# `ruff-check`, not `ruff`: the latter is a legacy alias in 0.14.x and
|
|
44
|
+
# says so on every run.
|
|
45
|
+
- id: ruff-check
|
|
35
46
|
types_or: [python, pyi]
|
|
36
47
|
args: [--fix]
|
|
37
48
|
- id: ruff-format
|
|
38
49
|
types_or: [python, pyi]
|
|
39
50
|
|
|
40
|
-
# Pyright for type checking
|
|
51
|
+
# Pyright for type checking. Same rule as ruff above: this rev and the
|
|
52
|
+
# `pyright` floor in pyproject.toml are one version in two places. A pyright
|
|
53
|
+
# that is stale here accepts code CI's newer one rejects.
|
|
41
54
|
- repo: https://github.com/RobertCraigie/pyright-python
|
|
42
|
-
rev: v1.1.
|
|
55
|
+
rev: v1.1.411
|
|
43
56
|
hooks:
|
|
44
57
|
- id: pyright
|
|
45
58
|
additional_dependencies:
|
|
@@ -68,8 +81,10 @@ repos:
|
|
|
68
81
|
hooks:
|
|
69
82
|
- id: actionlint
|
|
70
83
|
|
|
71
|
-
# UV for dependency management - ensures uv.lock is synced
|
|
84
|
+
# UV for dependency management - ensures uv.lock is synced. Matched to the
|
|
85
|
+
# uv that resolved the current lockfile: this hook REWRITES uv.lock, so an
|
|
86
|
+
# older uv here can churn the file the developer's own uv just wrote.
|
|
72
87
|
- repo: https://github.com/astral-sh/uv-pre-commit
|
|
73
|
-
rev: 0.
|
|
88
|
+
rev: 0.9.21
|
|
74
89
|
hooks:
|
|
75
90
|
- id: uv-lock
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0](https://github.com/lokkju/dbxdebug/compare/v0.4.0...v0.5.0) (2026-09-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **api:** derive the package root's exports from the modules' __all__ ([f4fbb2d](https://github.com/lokkju/dbxdebug/commit/f4fbb2d18a7a4b7c329c7593a26e39e99930b1c6)), closes [#7](https://github.com/lokkju/dbxdebug/issues/7)
|
|
9
|
+
* **qmp:** add mouse input to QMPClient ([a25a356](https://github.com/lokkju/dbxdebug/commit/a25a356c19115d5ea12189aca3c221413602dc9f)), closes [#2](https://github.com/lokkju/dbxdebug/issues/2)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* **frames:** end steps_out at the return address, and wrap segment reads ([40e2817](https://github.com/lokkju/dbxdebug/commit/40e28174473ada9997f650b84051cf296f72d0dd)), closes [#6](https://github.com/lokkju/dbxdebug/issues/6)
|
|
15
|
+
* **gdb:** make the pending-stop queue service itself ([908eae0](https://github.com/lokkju/dbxdebug/commit/908eae0ea621ea460c4627a4f7d4320eaaace81a)), closes [#18](https://github.com/lokkju/dbxdebug/issues/18)
|
|
16
|
+
|
|
3
17
|
## [0.4.0](https://github.com/lokkju/dbxdebug/compare/v0.3.0...v0.4.0) (2026-09-05)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: dbxdebug
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Client library and CLI for DOSBox-X remote debug protocols (GDB and QMP)
|
|
5
5
|
Author: lokkju
|
|
6
6
|
License-Expression: LicenseRef-Polyform-Shield-1.0.0
|
|
@@ -70,7 +70,7 @@ For working on dbxdebug itself, `uv sync`.
|
|
|
70
70
|
## Quick start
|
|
71
71
|
|
|
72
72
|
```python
|
|
73
|
-
from dbxdebug
|
|
73
|
+
from dbxdebug import DosboxSession
|
|
74
74
|
|
|
75
75
|
# Headless by default: no window, no keyboard focus, no audio device.
|
|
76
76
|
# Pass headless=False when you want to watch the guest.
|
|
@@ -231,10 +231,36 @@ old build fails at `start()` rather than at the first breakpoint.
|
|
|
231
231
|
| `dbxdebug.video` / `.html` / `.capture_io` | `DOSVideoTools`, HTML rendering, `ScreenRecorder`, `load_capture` |
|
|
232
232
|
| `dbxdebug.keyboard` / `.dbx_kbd` | key-chord helpers and constants (`CTRL_C`, `ctrl_key`, `DBX_KEY`, ...) |
|
|
233
233
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
234
|
+
### The export rule
|
|
235
|
+
|
|
236
|
+
Every module declares its supported surface in `__all__`, and
|
|
237
|
+
`dbxdebug/__init__.py` re-exports the union of the **library** modules'
|
|
238
|
+
`__all__`. So everything in the table above except the last three rows'
|
|
239
|
+
worth of command machinery is importable straight from `dbxdebug`:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
from dbxdebug import DosboxSession, GDBClient, QMPClient, linear, walk_frames
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The exceptions are `dbxdebug.cli`, `dbxdebug.registry` and `dbxdebug.doctor`
|
|
246
|
+
-- the `dbxdebug` command's own machinery. They act on the host's whole set
|
|
247
|
+
of sessions rather than on the one you launched, and their names mean
|
|
248
|
+
nothing unqualified: a package root should not own `run`, `reap`,
|
|
249
|
+
`list_sessions` or `free_port`. Import those from their modules:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
from dbxdebug.registry import list_sessions, reap
|
|
253
|
+
from dbxdebug import doctor
|
|
254
|
+
|
|
255
|
+
report = doctor.run()
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Leaving `cli` out is also what keeps `import dbxdebug` free of `click`.
|
|
259
|
+
|
|
260
|
+
Module-path imports keep working everywhere, and the examples below use
|
|
261
|
+
whichever form reads better in context -- `addressing.bp_addr` says more
|
|
262
|
+
about `bp_addr` than a bare name does. `tests/test_exports.py` holds the
|
|
263
|
+
root to the union so the two cannot drift.
|
|
238
264
|
|
|
239
265
|
### Locating the emulator
|
|
240
266
|
|
|
@@ -270,17 +296,22 @@ steps_out(gdb) # single-step until the current frame
|
|
|
270
296
|
BP, a saved BP that is not strictly above the current one (which is also what
|
|
271
297
|
terminates a cyclic chain), a short or failed read, or `max_depth`.
|
|
272
298
|
|
|
273
|
-
`steps_out`
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
299
|
+
`steps_out` reads the frame's own return address once, then steps until the
|
|
300
|
+
CPU is **at that address with the return slot popped**: `IP` equal to
|
|
301
|
+
`[BP+2]`, and either `CS` unchanged with `SP >= BP+4` (a near return) or `CS`
|
|
302
|
+
equal to `[BP+4]` with `SP >= BP+6` (a far one). Both halves matter -- SP
|
|
303
|
+
alone stops early on a callee that raises SP without returning
|
|
304
|
+
(`pop ax / add sp,N / jmp ax`), and the return address alone would accept a
|
|
305
|
+
callee merely branching through the caller's code. Nothing is decoded; every
|
|
306
|
+
value compared already arrives with each step's register read. Consequences:
|
|
277
307
|
|
|
278
308
|
* it raises `FrameWalkError` if `SP > BP` on entry (no frame pointer
|
|
279
|
-
established, or a stale BP) rather than returning after a single step
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
309
|
+
established, or a stale BP) rather than returning after a single step, and
|
|
310
|
+
likewise for a `BP` so near the top of SS that no return SP would fit in 16
|
|
311
|
+
bits;
|
|
312
|
+
* a return address that never arrives -- a guest that rewrites its own return
|
|
313
|
+
slot -- runs to `timeout`/`max_steps` and **raises**, naming the address it
|
|
314
|
+
was waiting for. It does not stop somewhere plausible;
|
|
284
315
|
* called at a procedure's first instruction, before the prologue has run, BP
|
|
285
316
|
still belongs to the caller and this measures the caller's frame;
|
|
286
317
|
* **clear every breakpoint first.** A breakpoint hit during one of these steps
|
|
@@ -394,10 +425,13 @@ with GDBClient() as gdb: # localhost:2159
|
|
|
394
425
|
gdb.set_breakpoint("1000:0020") # the same address, seg:off form
|
|
395
426
|
gdb.step()
|
|
396
427
|
gdb.continue_execution()
|
|
428
|
+
gdb.wait_for_stop(timeout=30.0) # a stop NOBODY asked for
|
|
397
429
|
|
|
398
430
|
with QMPClient() as qmp: # localhost:4444
|
|
399
431
|
qmp.send_key(CTRL_C)
|
|
400
432
|
qmp.type_text("Hello World!")
|
|
433
|
+
qmp.mouse_click("left") # buttons reach the guest
|
|
434
|
+
qmp.mouse_move(50, -20) # motion does NOT, headless
|
|
401
435
|
|
|
402
436
|
with DOSVideoTools() as video: # owns its own client
|
|
403
437
|
lines = video.screen_dump()
|
|
@@ -433,9 +467,12 @@ break-on-exec fires one nobody asked for) and a timed-out request leaving its
|
|
|
433
467
|
reply in the stream -- and both are handled at the framing layer:
|
|
434
468
|
|
|
435
469
|
* an unrequested stop reply is diverted to `gdb.pending_stops` instead of
|
|
436
|
-
being read as an answer. Drain it with `gdb.take_pending_stops()
|
|
437
|
-
|
|
438
|
-
|
|
470
|
+
being read as an answer. Drain it with `gdb.take_pending_stops()`, or wait
|
|
471
|
+
on it with `gdb.wait_for_stop(timeout=...)`; both read the socket
|
|
472
|
+
themselves, so no other request is needed to shake a stop loose
|
|
473
|
+
([#18](https://github.com/lokkju/dbxdebug/issues/18), fixed). The queue
|
|
474
|
+
keeps the most recent 64. This is how you learn the CPU stopped without
|
|
475
|
+
polling QMP;
|
|
439
476
|
* an abandoned exchange is drained before the next packet is sent, so the
|
|
440
477
|
request after a `GDBTimeoutError` gets its own reply rather than the
|
|
441
478
|
previous one's;
|
|
@@ -48,7 +48,7 @@ For working on dbxdebug itself, `uv sync`.
|
|
|
48
48
|
## Quick start
|
|
49
49
|
|
|
50
50
|
```python
|
|
51
|
-
from dbxdebug
|
|
51
|
+
from dbxdebug import DosboxSession
|
|
52
52
|
|
|
53
53
|
# Headless by default: no window, no keyboard focus, no audio device.
|
|
54
54
|
# Pass headless=False when you want to watch the guest.
|
|
@@ -209,10 +209,36 @@ old build fails at `start()` rather than at the first breakpoint.
|
|
|
209
209
|
| `dbxdebug.video` / `.html` / `.capture_io` | `DOSVideoTools`, HTML rendering, `ScreenRecorder`, `load_capture` |
|
|
210
210
|
| `dbxdebug.keyboard` / `.dbx_kbd` | key-chord helpers and constants (`CTRL_C`, `ctrl_key`, `DBX_KEY`, ...) |
|
|
211
211
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
212
|
+
### The export rule
|
|
213
|
+
|
|
214
|
+
Every module declares its supported surface in `__all__`, and
|
|
215
|
+
`dbxdebug/__init__.py` re-exports the union of the **library** modules'
|
|
216
|
+
`__all__`. So everything in the table above except the last three rows'
|
|
217
|
+
worth of command machinery is importable straight from `dbxdebug`:
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from dbxdebug import DosboxSession, GDBClient, QMPClient, linear, walk_frames
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The exceptions are `dbxdebug.cli`, `dbxdebug.registry` and `dbxdebug.doctor`
|
|
224
|
+
-- the `dbxdebug` command's own machinery. They act on the host's whole set
|
|
225
|
+
of sessions rather than on the one you launched, and their names mean
|
|
226
|
+
nothing unqualified: a package root should not own `run`, `reap`,
|
|
227
|
+
`list_sessions` or `free_port`. Import those from their modules:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from dbxdebug.registry import list_sessions, reap
|
|
231
|
+
from dbxdebug import doctor
|
|
232
|
+
|
|
233
|
+
report = doctor.run()
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Leaving `cli` out is also what keeps `import dbxdebug` free of `click`.
|
|
237
|
+
|
|
238
|
+
Module-path imports keep working everywhere, and the examples below use
|
|
239
|
+
whichever form reads better in context -- `addressing.bp_addr` says more
|
|
240
|
+
about `bp_addr` than a bare name does. `tests/test_exports.py` holds the
|
|
241
|
+
root to the union so the two cannot drift.
|
|
216
242
|
|
|
217
243
|
### Locating the emulator
|
|
218
244
|
|
|
@@ -248,17 +274,22 @@ steps_out(gdb) # single-step until the current frame
|
|
|
248
274
|
BP, a saved BP that is not strictly above the current one (which is also what
|
|
249
275
|
terminates a cyclic chain), a short or failed read, or `max_depth`.
|
|
250
276
|
|
|
251
|
-
`steps_out`
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
277
|
+
`steps_out` reads the frame's own return address once, then steps until the
|
|
278
|
+
CPU is **at that address with the return slot popped**: `IP` equal to
|
|
279
|
+
`[BP+2]`, and either `CS` unchanged with `SP >= BP+4` (a near return) or `CS`
|
|
280
|
+
equal to `[BP+4]` with `SP >= BP+6` (a far one). Both halves matter -- SP
|
|
281
|
+
alone stops early on a callee that raises SP without returning
|
|
282
|
+
(`pop ax / add sp,N / jmp ax`), and the return address alone would accept a
|
|
283
|
+
callee merely branching through the caller's code. Nothing is decoded; every
|
|
284
|
+
value compared already arrives with each step's register read. Consequences:
|
|
255
285
|
|
|
256
286
|
* it raises `FrameWalkError` if `SP > BP` on entry (no frame pointer
|
|
257
|
-
established, or a stale BP) rather than returning after a single step
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
287
|
+
established, or a stale BP) rather than returning after a single step, and
|
|
288
|
+
likewise for a `BP` so near the top of SS that no return SP would fit in 16
|
|
289
|
+
bits;
|
|
290
|
+
* a return address that never arrives -- a guest that rewrites its own return
|
|
291
|
+
slot -- runs to `timeout`/`max_steps` and **raises**, naming the address it
|
|
292
|
+
was waiting for. It does not stop somewhere plausible;
|
|
262
293
|
* called at a procedure's first instruction, before the prologue has run, BP
|
|
263
294
|
still belongs to the caller and this measures the caller's frame;
|
|
264
295
|
* **clear every breakpoint first.** A breakpoint hit during one of these steps
|
|
@@ -372,10 +403,13 @@ with GDBClient() as gdb: # localhost:2159
|
|
|
372
403
|
gdb.set_breakpoint("1000:0020") # the same address, seg:off form
|
|
373
404
|
gdb.step()
|
|
374
405
|
gdb.continue_execution()
|
|
406
|
+
gdb.wait_for_stop(timeout=30.0) # a stop NOBODY asked for
|
|
375
407
|
|
|
376
408
|
with QMPClient() as qmp: # localhost:4444
|
|
377
409
|
qmp.send_key(CTRL_C)
|
|
378
410
|
qmp.type_text("Hello World!")
|
|
411
|
+
qmp.mouse_click("left") # buttons reach the guest
|
|
412
|
+
qmp.mouse_move(50, -20) # motion does NOT, headless
|
|
379
413
|
|
|
380
414
|
with DOSVideoTools() as video: # owns its own client
|
|
381
415
|
lines = video.screen_dump()
|
|
@@ -411,9 +445,12 @@ break-on-exec fires one nobody asked for) and a timed-out request leaving its
|
|
|
411
445
|
reply in the stream -- and both are handled at the framing layer:
|
|
412
446
|
|
|
413
447
|
* an unrequested stop reply is diverted to `gdb.pending_stops` instead of
|
|
414
|
-
being read as an answer. Drain it with `gdb.take_pending_stops()
|
|
415
|
-
|
|
416
|
-
|
|
448
|
+
being read as an answer. Drain it with `gdb.take_pending_stops()`, or wait
|
|
449
|
+
on it with `gdb.wait_for_stop(timeout=...)`; both read the socket
|
|
450
|
+
themselves, so no other request is needed to shake a stop loose
|
|
451
|
+
([#18](https://github.com/lokkju/dbxdebug/issues/18), fixed). The queue
|
|
452
|
+
keeps the most recent 64. This is how you learn the CPU stopped without
|
|
453
|
+
polling QMP;
|
|
417
454
|
* an abandoned exchange is drained before the next packet is sent, so the
|
|
418
455
|
request after a `GDBTimeoutError` gets its own reply rather than the
|
|
419
456
|
previous one's;
|
|
@@ -63,36 +63,42 @@ Then delete the vendored copy. Nothing in the consumer should still import
|
|
|
63
63
|
|
|
64
64
|
| What you had | Where it lives now |
|
|
65
65
|
|---|---|
|
|
66
|
-
| The launcher class (`Popen`, ports, cleanup) | `from dbxdebug
|
|
66
|
+
| The launcher class (`Popen`, ports, cleanup) | `from dbxdebug import DosboxSession` |
|
|
67
67
|
| `dosbox_debug.GDBClient` | `from dbxdebug import GDBClient` |
|
|
68
68
|
| `dosbox_debug.QMPClient`, `QMPError` | `from dbxdebug import QMPClient, QMPError` |
|
|
69
69
|
| `dosbox_debug.GDBError` | No equivalent: see section 3 |
|
|
70
|
-
| The launcher's `bp_addr` | `from dbxdebug
|
|
70
|
+
| The launcher's `bp_addr` | `from dbxdebug import linear` (and see section 2.2) |
|
|
71
71
|
| Screen decoding on the GDB client | `DosboxSession.screen_lines()`, or `from dbxdebug import DOSVideoTools` |
|
|
72
|
-
| Frame walking, if you had it | `from dbxdebug
|
|
73
|
-
| Finding the emulator binary | `from dbxdebug
|
|
72
|
+
| Frame walking, if you had it | `from dbxdebug import walk_frames, steps_out` |
|
|
73
|
+
| Finding the emulator binary | `from dbxdebug import find_dosbox_x`, or `$DBXDEBUG_DOSBOX` |
|
|
74
74
|
| Nothing: this is new | `from dbxdebug.registry import list_sessions`; the `dbxdebug session` and `dbxdebug doctor` CLI |
|
|
75
75
|
|
|
76
|
-
###
|
|
76
|
+
### Where to import from
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
modules -- the session, the registry, the addressing helpers, the frame
|
|
80
|
-
walker -- are importable from their own modules and nowhere else. So the
|
|
81
|
-
package's primary entry point is the one thing you cannot import from the
|
|
82
|
-
package root:
|
|
78
|
+
Everything a debugging script needs comes from the package root:
|
|
83
79
|
|
|
84
80
|
```python
|
|
85
|
-
|
|
86
|
-
>>> from dbxdebug import DosboxSession
|
|
87
|
-
ImportError: cannot import name 'DosboxSession' from 'dbxdebug'
|
|
88
|
-
>>> from dbxdebug.session import DosboxSession # this is the way
|
|
81
|
+
from dbxdebug import DosboxSession, GDBClient, QMPClient, DOSVideoTools, linear
|
|
89
82
|
```
|
|
90
83
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
The rule is that every module declares its surface in `__all__` and the root
|
|
85
|
+
re-exports the union of the library modules' `__all__`. The exceptions are
|
|
86
|
+
`dbxdebug.cli`, `dbxdebug.registry` and `dbxdebug.doctor`, the `dbxdebug`
|
|
87
|
+
command's own machinery, whose names say nothing unqualified:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from dbxdebug.registry import list_sessions, reap
|
|
91
|
+
from dbxdebug import doctor
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Module-path imports work for everything, root or not, and the examples
|
|
95
|
+
further down use them where naming the module makes the call clearer
|
|
96
|
+
(`addressing.bp_addr`, `frames.steps_out`). Nothing you write either way
|
|
97
|
+
has to be rewritten later.
|
|
98
|
+
|
|
99
|
+
If `from dbxdebug import DosboxSession` raises `ImportError`, you are on a
|
|
100
|
+
build from before lokkju/dbxdebug#7 landed; `from dbxdebug.session import
|
|
101
|
+
DosboxSession` works there.
|
|
96
102
|
|
|
97
103
|
### The launcher itself
|
|
98
104
|
|
|
@@ -105,7 +111,7 @@ so only a SIGKILL of the owner can leave a stray emulator behind (which is
|
|
|
105
111
|
what `dbxdebug session reap` is for).
|
|
106
112
|
|
|
107
113
|
```python
|
|
108
|
-
from dbxdebug
|
|
114
|
+
from dbxdebug import DosboxSession
|
|
109
115
|
|
|
110
116
|
with DosboxSession(mounts={"c": "/path/to/host/dir"}, autoexec=["c:"]) as session:
|
|
111
117
|
session.qmp.type_text("PROG\r")
|
|
@@ -347,7 +353,8 @@ were two loops was the first item of lokkju/dbxdebug#7.
|
|
|
347
353
|
| `key_down(key) -> dict` / `key_up(key) -> dict` | `key_down(key) -> None` / `key_up(key) -> None` | Same `input-send-event` payload |
|
|
348
354
|
| `key_press(key, hold_time=0.1) -> dict` | `key_press(key, hold_time=0.05) -> None` | **Default hold time halved** |
|
|
349
355
|
| `type_text(text, delay=0.1)` | `type_text(text, delay=0.05)` | **Default delay halved.** Unmapped characters are logged and skipped rather than silently skipped; `\r`, `\n`, `\t`, space and shifted punctuation map the same |
|
|
350
|
-
| `input_send_event(events) -> dict` | -- | No raw passthrough. Use `key_down`/`key_up
|
|
356
|
+
| `input_send_event(events) -> dict` | -- | No raw passthrough. Use `key_down`/`key_up`, or the mouse methods below |
|
|
357
|
+
| -- | `mouse_move(dx, dy)`, `mouse_button(button, down)`, `mouse_click(button='left', hold_time=0.05)` | New. Relative motion only (the server ignores `abs`), and only `left`/`right`/`middle` -- `MOUSE_BUTTONS` lists them, and anything else raises `ValueError` rather than being accepted and dropped. **Motion has no effect headless**; buttons do. See `mouse_move`'s docstring |
|
|
351
358
|
| `query_commands() -> list` | `query_commands() -> list[str]` | Same |
|
|
352
359
|
| `query_status() -> dict` | `query_status() -> dict` | **Returns the `return` payload, not the envelope**: `resp["return"]["running"]` becomes `status["running"]` |
|
|
353
360
|
| `stop()`, `cont()`, `debug_break_on_exec(enabled)` | Same names | Same unwrapping change as `query_status` |
|
|
@@ -449,22 +456,38 @@ gdb.take_pending_stops() # [b'S05'] -- the stop, delivered once
|
|
|
449
456
|
Note that a `Z0` breakpoint armed while free-running is inert and never was a
|
|
450
457
|
trigger -- activation only happens on continue.
|
|
451
458
|
|
|
452
|
-
**The queue
|
|
453
|
-
|
|
454
|
-
`
|
|
455
|
-
|
|
456
|
-
non-empty" therefore spins forever on a stop that has genuinely happened:
|
|
459
|
+
**The queue services itself** (was
|
|
460
|
+
[#18](https://github.com/lokkju/dbxdebug/issues/18), fixed). Reading
|
|
461
|
+
`pending_stops` or calling `take_pending_stops()` reads the socket first,
|
|
462
|
+
without blocking, so asking the question also answers it:
|
|
457
463
|
|
|
458
464
|
```python
|
|
459
|
-
|
|
460
|
-
|
|
465
|
+
qmp.debug_break_on_exec(True)
|
|
466
|
+
# ... the break fires; no GDB request is issued at all ...
|
|
461
467
|
gdb.take_pending_stops() # (b'S05',)
|
|
462
468
|
```
|
|
463
469
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
470
|
+
Before that fix the queue filled only as a side effect of some OTHER request
|
|
471
|
+
passing through the framing layer, so a poller written as "loop calling
|
|
472
|
+
`take_pending_stops()` until it is non-empty" spun forever on a stop that had
|
|
473
|
+
genuinely happened, and its caller concluded the breakpoint had never fired.
|
|
474
|
+
A read interposed in the loop was the workaround; it is no longer needed.
|
|
475
|
+
|
|
476
|
+
Better still, do not write the loop:
|
|
477
|
+
|
|
478
|
+
```python
|
|
479
|
+
stop = gdb.wait_for_stop(timeout=30.0) # the payload, or None on timeout
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
`wait_for_stop` owns the poll and returns the oldest queued stop, removed
|
|
483
|
+
from the queue. It does **not** resume anything -- it is for stops nobody
|
|
484
|
+
asked for. It raises `GDBDesyncError` rather than spinning if the stub still
|
|
485
|
+
owes a reply to a request already on the wire, because the socket cannot be
|
|
486
|
+
polled in that state without stealing that request's own bytes; finish or
|
|
487
|
+
abandon that exchange first (the next request drains an abandoned one).
|
|
488
|
+
|
|
489
|
+
`qmp.query_status()` on the separate QMP socket is still a valid signal, and
|
|
490
|
+
is the one to use when you are not otherwise talking GDB.
|
|
468
491
|
|
|
469
492
|
**Trigger 2 -- a timed-out request.** The abandoned reply used to stay in the
|
|
470
493
|
stream and be handed to the next request:
|
|
@@ -493,9 +516,10 @@ What a consumer should still do:
|
|
|
493
516
|
* Do **not** add a read-retry loop. Two identical consecutive requests mask a
|
|
494
517
|
one-packet lag perfectly, so retrying looks like it works whether or not the
|
|
495
518
|
stream has shifted.
|
|
496
|
-
* If you arm `debug_break_on_exec`,
|
|
497
|
-
|
|
498
|
-
|
|
519
|
+
* If you arm `debug_break_on_exec`, learn that the CPU stopped from
|
|
520
|
+
`gdb.wait_for_stop()`, or from `gdb.take_pending_stops()` in a loop of your
|
|
521
|
+
own -- both read the socket themselves now. Polling `qmp.query_status()` on
|
|
522
|
+
the separate QMP socket still works too.
|
|
499
523
|
|
|
500
524
|
### 5.3 One GDB client at a time (lokkju/dbxdebug#8, open)
|
|
501
525
|
|
|
@@ -32,8 +32,13 @@ dbxdebug = "dbxdebug.cli:main"
|
|
|
32
32
|
dev = [
|
|
33
33
|
"pytest>=7.4.0",
|
|
34
34
|
"pytest-cov>=4.1.0",
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
# These two floors match the `rev:` pins in .pre-commit-config.yaml on
|
|
36
|
+
# purpose, and are bumped with them. Each tool runs twice against this
|
|
37
|
+
# repo -- once as a commit hook, once as the resolved dev environment CI
|
|
38
|
+
# uses -- and a floor low enough to resolve behind the hook lets the two
|
|
39
|
+
# disagree about the same file (lokkju/dbxdebug#17).
|
|
40
|
+
"ruff>=0.14.9",
|
|
41
|
+
"pyright>=1.1.411",
|
|
37
42
|
"pre-commit>=3.5.0",
|
|
38
43
|
]
|
|
39
44
|
|
|
@@ -66,7 +71,16 @@ target-version = "py311"
|
|
|
66
71
|
# `ruff check .` from the root lints other branches' in-progress code and
|
|
67
72
|
# reports their errors as this tree's. pyright already scopes itself via
|
|
68
73
|
# `include`; ruff needs telling.
|
|
69
|
-
extend-exclude = [
|
|
74
|
+
extend-exclude = [
|
|
75
|
+
".claude",
|
|
76
|
+
# ruff formats Python inside Markdown code blocks, and it wants to
|
|
77
|
+
# restretch the aligned trailing comments in our docs samples. CI runs
|
|
78
|
+
# `ruff format --check src/ tests/` and the pre-commit hook is scoped to
|
|
79
|
+
# `types_or: [python, pyi]`, so neither ever saw this -- only a bare
|
|
80
|
+
# `ruff format --check .` did, which made the repo look broken to anyone
|
|
81
|
+
# running the obvious command. Excluded so all three agree.
|
|
82
|
+
"*.md",
|
|
83
|
+
]
|
|
70
84
|
|
|
71
85
|
[tool.ruff.lint]
|
|
72
86
|
select = [
|
|
@@ -56,7 +56,7 @@ Point it at a specific build with `DBXDEBUG_DOSBOX=/path/to/dosbox-x`.
|
|
|
56
56
|
Always as a context manager. Never hand-roll a `subprocess.Popen`.
|
|
57
57
|
|
|
58
58
|
```python
|
|
59
|
-
from dbxdebug
|
|
59
|
+
from dbxdebug import DosboxSession # or dbxdebug.session; both work
|
|
60
60
|
|
|
61
61
|
with DosboxSession(mounts={"c": host_dir}) as session:
|
|
62
62
|
...
|
|
@@ -244,11 +244,13 @@ So, verified behaviour:
|
|
|
244
244
|
|
|
245
245
|
- The GDB connection **survives** the break. The read that follows is still
|
|
246
246
|
answered with its own bytes.
|
|
247
|
-
- Learn that the CPU stopped from **`gdb.
|
|
248
|
-
|
|
249
|
-
**`
|
|
250
|
-
|
|
251
|
-
|
|
247
|
+
- Learn that the CPU stopped from **`gdb.wait_for_stop(timeout=...)`** — it
|
|
248
|
+
polls for you and returns the stop reply, or None if none arrives — or from
|
|
249
|
+
**`gdb.take_pending_stops()`**, which returns the queued stop replies and
|
|
250
|
+
empties the queue. Both read the socket themselves, so neither needs any
|
|
251
|
+
other GDB request to shake a stop loose (lokkju/dbxdebug#18, fixed).
|
|
252
|
+
Polling **`qmp.query_status()["running"]`** on the separate QMP socket works
|
|
253
|
+
too, and is what you want when you are not otherwise talking GDB.
|
|
252
254
|
- A plain `Z0` breakpoint armed while free-running is **inert** and is not a
|
|
253
255
|
trigger — activation only happens on continue. This is why the ordinary
|
|
254
256
|
breakpoint recipe (`halt` → `set_breakpoint` → `continue_execution`) is
|