dbxdebug 0.3.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.3.0 → dbxdebug-0.5.0}/.github/workflows/release.yml +21 -8
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/.github/workflows/test.yml +6 -6
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/.pre-commit-config.yaml +22 -7
- dbxdebug-0.5.0/.release-please-manifest.json +3 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/CHANGELOG.md +45 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/PKG-INFO +187 -62
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/README.md +186 -61
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/docs/migration.md +180 -100
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/pyproject.toml +21 -2
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/skills/debug-dos-programs/SKILL.md +79 -54
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/skills/debug-dos-programs/references/recipes.md +103 -44
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/skills/debug-dos-programs/references/troubleshooting.md +113 -75
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/skills/dosbox-x-debug-protocol/references/history.md +13 -4
- dbxdebug-0.5.0/src/dbxdebug/__init__.py +282 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/src/dbxdebug/addressing.py +11 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/src/dbxdebug/capture_io.py +7 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/src/dbxdebug/cli.py +80 -14
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/src/dbxdebug/dbx_kbd.py +10 -0
- dbxdebug-0.5.0/src/dbxdebug/frames.py +427 -0
- dbxdebug-0.5.0/src/dbxdebug/gdb.py +994 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/src/dbxdebug/html.py +10 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/src/dbxdebug/keyboard.py +37 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/src/dbxdebug/paths.py +7 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/src/dbxdebug/qmp.py +203 -6
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/src/dbxdebug/registry.py +13 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/src/dbxdebug/session.py +219 -25
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/src/dbxdebug/utils.py +5 -0
- dbxdebug-0.5.0/src/dbxdebug/video.py +333 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/tests/integration/conftest.py +14 -11
- dbxdebug-0.5.0/tests/integration/test_headless.py +282 -0
- dbxdebug-0.5.0/tests/integration/test_live_session.py +1275 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/tests/test_cli_session.py +10 -0
- dbxdebug-0.5.0/tests/test_exports.py +195 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/tests/test_frames.py +144 -11
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/tests/test_gdb_capabilities.py +10 -0
- dbxdebug-0.5.0/tests/test_gdb_framing.py +668 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/tests/test_gdb_registers.py +10 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/tests/test_qmp_commands.py +129 -0
- dbxdebug-0.5.0/tests/test_read_bulk.py +325 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/tests/test_session.py +69 -2
- dbxdebug-0.5.0/tests/test_video_client_ownership.py +345 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/uv.lock +36 -40
- dbxdebug-0.3.0/.release-please-manifest.json +0 -3
- dbxdebug-0.3.0/src/dbxdebug/__init__.py +0 -134
- dbxdebug-0.3.0/src/dbxdebug/frames.py +0 -282
- dbxdebug-0.3.0/src/dbxdebug/gdb.py +0 -439
- dbxdebug-0.3.0/src/dbxdebug/video.py +0 -239
- dbxdebug-0.3.0/tests/integration/test_live_session.py +0 -538
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/.github/dependabot.yml +0 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/.gitignore +0 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/LICENSE +0 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/plugin.json +0 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/release-please-config.json +0 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/skills/dosbox-x-debug-protocol/SKILL.md +0 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/skills/dosbox-x-debug-protocol/references/packets.md +0 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/skills/dosbox-x-debug-protocol/references/qmp-commands.md +0 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/src/dbxdebug/doctor.py +0 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/tests/__init__.py +0 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/tests/integration/__init__.py +0 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/tests/test_addressing.py +0 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/tests/test_basic.py +0 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/tests/test_paths.py +0 -0
- {dbxdebug-0.3.0 → dbxdebug-0.5.0}/tests/test_registry.py +0 -0
|
@@ -9,6 +9,18 @@ name: Release
|
|
|
9
9
|
on:
|
|
10
10
|
push:
|
|
11
11
|
branches: [main]
|
|
12
|
+
# Recovery path. A tag/release created with GITHUB_TOKEN does not trigger a
|
|
13
|
+
# workflow, so a publish that fails for an environmental reason (a bad
|
|
14
|
+
# publisher/metadata pairing, a PyPI outage) cannot be retried by re-running
|
|
15
|
+
# the original run -- a re-run replays the workflow file from that commit,
|
|
16
|
+
# including whatever was broken. Dispatch this against the existing tag
|
|
17
|
+
# instead. It rebuilds from that tag and republishes; it never creates a tag.
|
|
18
|
+
workflow_dispatch:
|
|
19
|
+
inputs:
|
|
20
|
+
tag:
|
|
21
|
+
description: "Existing tag to build and publish, e.g. v0.3.0"
|
|
22
|
+
required: true
|
|
23
|
+
type: string
|
|
12
24
|
|
|
13
25
|
permissions: {}
|
|
14
26
|
|
|
@@ -18,6 +30,7 @@ concurrency:
|
|
|
18
30
|
|
|
19
31
|
jobs:
|
|
20
32
|
release-please:
|
|
33
|
+
if: ${{ github.event_name == 'push' }}
|
|
21
34
|
runs-on: ubuntu-latest
|
|
22
35
|
permissions:
|
|
23
36
|
contents: write # create the release + tag
|
|
@@ -26,7 +39,7 @@ jobs:
|
|
|
26
39
|
release_created: ${{ steps.release.outputs.release_created }}
|
|
27
40
|
tag_name: ${{ steps.release.outputs.tag_name }}
|
|
28
41
|
steps:
|
|
29
|
-
- uses: googleapis/release-please-action@
|
|
42
|
+
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
|
|
30
43
|
id: release
|
|
31
44
|
with:
|
|
32
45
|
config-file: release-please-config.json
|
|
@@ -36,14 +49,14 @@ jobs:
|
|
|
36
49
|
# If anything here fails, the publish job below is skipped and nothing ships.
|
|
37
50
|
ci:
|
|
38
51
|
needs: release-please
|
|
39
|
-
if: ${{ needs.release-please.outputs.release_created == 'true' }}
|
|
52
|
+
if: ${{ !cancelled() && (needs.release-please.outputs.release_created == 'true' || github.event_name == 'workflow_dispatch') }}
|
|
40
53
|
permissions:
|
|
41
54
|
contents: read
|
|
42
55
|
uses: ./.github/workflows/test.yml
|
|
43
56
|
|
|
44
57
|
publish:
|
|
45
58
|
needs: [release-please, ci]
|
|
46
|
-
if: ${{ needs.release-please.outputs.release_created == 'true' }}
|
|
59
|
+
if: ${{ !cancelled() && needs.ci.result == 'success' && (needs.release-please.outputs.release_created == 'true' || github.event_name == 'workflow_dispatch') }}
|
|
47
60
|
runs-on: ubuntu-latest
|
|
48
61
|
environment:
|
|
49
62
|
name: pypi
|
|
@@ -52,13 +65,13 @@ jobs:
|
|
|
52
65
|
id-token: write # OIDC for PyPI Trusted Publishing
|
|
53
66
|
contents: write # upload built artifacts to the GitHub Release
|
|
54
67
|
steps:
|
|
55
|
-
- uses: actions/checkout@
|
|
68
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
56
69
|
with:
|
|
57
|
-
ref: ${{ needs.release-please.outputs.tag_name }}
|
|
70
|
+
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.tag || needs.release-please.outputs.tag_name }}
|
|
58
71
|
fetch-depth: 0 # Full history + tags for hatch-vcs version
|
|
59
72
|
|
|
60
73
|
- name: Install uv and set up Python
|
|
61
|
-
uses: astral-sh/setup-uv@
|
|
74
|
+
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
|
62
75
|
with:
|
|
63
76
|
python-version: "3.11"
|
|
64
77
|
|
|
@@ -66,9 +79,9 @@ jobs:
|
|
|
66
79
|
run: uv build
|
|
67
80
|
|
|
68
81
|
- name: Publish to PyPI
|
|
69
|
-
uses: pypa/gh-action-pypi-publish@
|
|
82
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
70
83
|
|
|
71
84
|
- name: Attach artifacts to the GitHub Release
|
|
72
85
|
env:
|
|
73
86
|
GH_TOKEN: ${{ github.token }}
|
|
74
|
-
run: gh release upload "${{ needs.release-please.outputs.tag_name }}" dist/* --clobber
|
|
87
|
+
run: gh release upload "${{ github.event_name == 'workflow_dispatch' && inputs.tag || needs.release-please.outputs.tag_name }}" dist/* --clobber
|
|
@@ -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,50 @@
|
|
|
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
|
+
|
|
17
|
+
## [0.4.0](https://github.com/lokkju/dbxdebug/compare/v0.3.0...v0.4.0) (2026-09-05)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### ⚠ BREAKING CHANGES
|
|
21
|
+
|
|
22
|
+
* **session:** DosboxSession now runs the emulator headless by default. A caller that relied on seeing the window -- to watch the guest while debugging, or to drive it by hand -- must now pass headless=False explicitly. Everything else is unaffected: the debug surface is identical headless, and screen capture was verified byte-identical between the two modes. Note also that `headless` sits mid-dataclass, so positional construction past `connect=` shifts.
|
|
23
|
+
|
|
24
|
+
### Features
|
|
25
|
+
|
|
26
|
+
* **session:** add DosboxSession(headless=True), on by default ([8af1f56](https://github.com/lokkju/dbxdebug/commit/8af1f56528977c2dbc4a2ddec442f4aa6f76933d)), closes [#3](https://github.com/lokkju/dbxdebug/issues/3)
|
|
27
|
+
* **session:** add read_bulk, a one-call bulk memory read ([f8e228b](https://github.com/lokkju/dbxdebug/commit/f8e228b101b14e682e3b4ec98b434f1fd7e5e7c7)), closes [#9](https://github.com/lokkju/dbxdebug/issues/9)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Bug Fixes
|
|
31
|
+
|
|
32
|
+
* **ci:** unblock publishing and add a recovery path for a failed one ([9c69a87](https://github.com/lokkju/dbxdebug/commit/9c69a87c73f30b0a249e1bd5ec3c4cc4f2af0736))
|
|
33
|
+
* **gdb:** bound reads and resynchronise the packet stream ([5940242](https://github.com/lokkju/dbxdebug/commit/5940242035181668c4d35e61db56d5b2342aa620))
|
|
34
|
+
* **session:** record that headless is now the default ([d649d70](https://github.com/lokkju/dbxdebug/commit/d649d705bb46261d38c30a23fc9c39bd390f7c87))
|
|
35
|
+
* **video,cli:** let a GDB client be borrowed instead of reopened ([bb9d351](https://github.com/lokkju/dbxdebug/commit/bb9d35170283407f9c7fbd64f4cfd4f1d75f1d64))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### Performance Improvements
|
|
39
|
+
|
|
40
|
+
* **clients:** set TCP_NODELAY, and correct what read_bulk is now worth ([c535e3b](https://github.com/lokkju/dbxdebug/commit/c535e3bafe2799e708714e4f771046fd35a55d81))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### Documentation
|
|
44
|
+
|
|
45
|
+
* correct the hazard write-ups now that the GDB stream is fixed ([a1821aa](https://github.com/lokkju/dbxdebug/commit/a1821aa8904987afd73ad1ba97d022fee5e30213)), closes [#4](https://github.com/lokkju/dbxdebug/issues/4) [#5](https://github.com/lokkju/dbxdebug/issues/5)
|
|
46
|
+
* **migration:** fix the gaps a real migration hit ([96ffaa7](https://github.com/lokkju/dbxdebug/commit/96ffaa7a901a35529e14a81828f8a36d550664d9))
|
|
47
|
+
|
|
3
48
|
## [0.3.0](https://github.com/lokkju/dbxdebug/compare/v0.2.1...v0.3.0) (2026-09-05)
|
|
4
49
|
|
|
5
50
|
|
|
@@ -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,12 +70,11 @@ 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
|
-
#
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
with DosboxSession(env=HEADLESS) as session:
|
|
75
|
+
# Headless by default: no window, no keyboard focus, no audio device.
|
|
76
|
+
# Pass headless=False when you want to watch the guest.
|
|
77
|
+
with DosboxSession() as session:
|
|
79
78
|
print(f"pid={session.pid} gdb={session.gdb_port} qmp={session.qmp_port}")
|
|
80
79
|
|
|
81
80
|
gdb = session.gdb
|
|
@@ -103,15 +102,56 @@ Leaving the `with` block kills the emulator's whole process group, deletes the
|
|
|
103
102
|
scratch workdir, and removes the registry entry. `atexit` and SIGINT/SIGTERM
|
|
104
103
|
handlers repeat that teardown for a process that leaves by some other door.
|
|
105
104
|
|
|
105
|
+
### Headless is the default
|
|
106
|
+
|
|
107
|
+
A session runs with SDL's `dummy` video and audio drivers, so it opens **no
|
|
108
|
+
window**, takes no keyboard focus, and claims no audio device. Starting one
|
|
109
|
+
does not take over the display of whoever is at the machine, which is what
|
|
110
|
+
makes a test suite -- or several sessions at once -- usable on a workstation.
|
|
111
|
+
|
|
112
|
+
What you give up is real: you cannot watch the guest. `headless=False` gives
|
|
113
|
+
you a normal window, and that window will take the focus of whoever is at the
|
|
114
|
+
keyboard, so it is for looking at one session, not for a suite.
|
|
115
|
+
|
|
116
|
+
The debug surface is unaffected. `screen_lines()` reads guest video memory
|
|
117
|
+
over GDB and `qmp.screendump()` renders through DOSBox-X's own capture path;
|
|
118
|
+
`dummy` removes the window, not the rendering. Compared live, a headless
|
|
119
|
+
session and a windowed session showing the same screen returned
|
|
120
|
+
**byte-identical** 720x400 PNGs and identical text lines.
|
|
121
|
+
|
|
122
|
+
`env=` still works and takes precedence over the headless variables, so
|
|
123
|
+
`DosboxSession(headless=True, env={"SDL_VIDEODRIVER": "x11"})` keeps the dummy
|
|
124
|
+
audio driver and gets an x11 window. Precedence, lowest to highest: inherited
|
|
125
|
+
`os.environ`, then the headless variables, then `env`.
|
|
126
|
+
|
|
106
127
|
Useful `DosboxSession` arguments: `mounts={"c": path}`, `program=` and
|
|
107
128
|
`files=` to stage host files onto the first mounted drive, `autoexec=`,
|
|
108
|
-
`conf=` for your own conf template, `cycles=`, `
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
129
|
+
`conf=` for your own conf template, `cycles=`, `headless=False` for a visible
|
|
130
|
+
window, `connect=False` for the handle without clients, `boot_settle=`
|
|
131
|
+
(default 2.5s -- the debug ports accept long before the guest reaches a
|
|
132
|
+
prompt), and `label=` to name the scratch workdir.
|
|
133
|
+
Conveniences on the handle: `screen_lines()`, `read_bulk()`, `wait_for_text()`,
|
|
112
134
|
`assert_screen_readable()`, `running`, `set_breakpoint()`,
|
|
113
135
|
`remove_breakpoint()`.
|
|
114
136
|
|
|
137
|
+
### Reading a region: `session.read_bulk(address, length)`
|
|
138
|
+
|
|
139
|
+
One QMP `memdump` reply instead of thousands of GDB `m` round-trips.
|
|
140
|
+
Measured on this build, one 64 KB segment: **2.8 ms** through `read_bulk` against **33.5 ms** for the same bytes through 64 one-kilobyte `gdb.read_memory` calls -- 12x against a running guest, and 2.6x (2.1 ms against 5.5 ms) when the CPU is already halted, where the loop has no emulation competing with it. Both paths return
|
|
141
|
+
identical bytes; the live suite asserts it.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
data = session.read_bulk(0xF0000, 0x10000) # -> 65536 bytes
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
It exists because the raw sequence has two traps. `memdump` is refused
|
|
148
|
+
while the CPU runs, and the obvious way to stop it -- `qmp.stop()` --
|
|
149
|
+
parks the emulation thread that services the GDB stub, so the dump
|
|
150
|
+
succeeds and every later GDB request goes unanswered. `read_bulk` halts
|
|
151
|
+
through GDB, dumps, and resumes. If the CPU was ALREADY stopped (a
|
|
152
|
+
breakpoint, the interactive debugger, or a QMP stop) it takes the dump
|
|
153
|
+
and leaves it stopped: it resumes only what it halted itself.
|
|
154
|
+
|
|
115
155
|
## Addressing -- read this once
|
|
116
156
|
|
|
117
157
|
This is the most consequential behaviour in the package, and the thing most
|
|
@@ -182,7 +222,7 @@ old build fails at `start()` rather than at the first breakpoint.
|
|
|
182
222
|
|---|---|
|
|
183
223
|
| `dbxdebug.session` | `DosboxSession` -- launch, connect, tear down. Also `DEFAULT_CONF`, `render_conf`, `DosboxLaunchError` |
|
|
184
224
|
| `dbxdebug.gdb` | `GDBClient`, `IncompatibleStubError`, `REGISTER_NAMES` |
|
|
185
|
-
| `dbxdebug.qmp` | `QMPClient`, `QMPError` -- keys, `memdump`, `screendump`, save/load state, `stop`/`cont`, `debug_break_on_exec` |
|
|
225
|
+
| `dbxdebug.qmp` | `QMPClient`, `QMPError`, `CpuNotStoppedError` -- keys, `memdump`, `screendump`, save/load state, `stop`/`cont`, `debug_break_on_exec` |
|
|
186
226
|
| `dbxdebug.addressing` | `linear`, `linear_pc`, `parse_address`, `bp_addr`, `PackedAddressError` |
|
|
187
227
|
| `dbxdebug.frames` | `walk_frames`, `steps_out`, `Frame`, `FrameWalkError` |
|
|
188
228
|
| `dbxdebug.registry` | `list_sessions`, `reap`, `format_table`, `free_port`, `kill_group` |
|
|
@@ -191,10 +231,36 @@ old build fails at `start()` rather than at the first breakpoint.
|
|
|
191
231
|
| `dbxdebug.video` / `.html` / `.capture_io` | `DOSVideoTools`, HTML rendering, `ScreenRecorder`, `load_capture` |
|
|
192
232
|
| `dbxdebug.keyboard` / `.dbx_kbd` | key-chord helpers and constants (`CTRL_C`, `ctrl_key`, `DBX_KEY`, ...) |
|
|
193
233
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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.
|
|
198
264
|
|
|
199
265
|
### Locating the emulator
|
|
200
266
|
|
|
@@ -230,22 +296,29 @@ steps_out(gdb) # single-step until the current frame
|
|
|
230
296
|
BP, a saved BP that is not strictly above the current one (which is also what
|
|
231
297
|
terminates a cyclic chain), a short or failed read, or `max_depth`.
|
|
232
298
|
|
|
233
|
-
`steps_out`
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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:
|
|
237
307
|
|
|
238
308
|
* it raises `FrameWalkError` if `SP > BP` on entry (no frame pointer
|
|
239
|
-
established, or a stale BP) rather than returning after a single step
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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;
|
|
244
315
|
* called at a procedure's first instruction, before the prologue has run, BP
|
|
245
316
|
still belongs to the caller and this measures the caller's frame;
|
|
246
317
|
* **clear every breakpoint first.** A breakpoint hit during one of these steps
|
|
247
|
-
makes the stub emit an unsolicited stop reply
|
|
248
|
-
|
|
318
|
+
makes the stub emit an unsolicited stop reply. The connection no longer
|
|
319
|
+
desyncs on one -- it is queued on `gdb.pending_stops` -- but the stop you
|
|
320
|
+
get is still not the step you asked for, so the walk ends up measuring a
|
|
321
|
+
frame you did not mean to be in. See Known hazards.
|
|
249
322
|
|
|
250
323
|
## CLI
|
|
251
324
|
|
|
@@ -352,56 +425,99 @@ with GDBClient() as gdb: # localhost:2159
|
|
|
352
425
|
gdb.set_breakpoint("1000:0020") # the same address, seg:off form
|
|
353
426
|
gdb.step()
|
|
354
427
|
gdb.continue_execution()
|
|
428
|
+
gdb.wait_for_stop(timeout=30.0) # a stop NOBODY asked for
|
|
355
429
|
|
|
356
430
|
with QMPClient() as qmp: # localhost:4444
|
|
357
431
|
qmp.send_key(CTRL_C)
|
|
358
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
|
|
359
435
|
|
|
360
|
-
with DOSVideoTools() as video:
|
|
436
|
+
with DOSVideoTools() as video: # owns its own client
|
|
361
437
|
lines = video.screen_dump()
|
|
362
438
|
lines, ticks = video.screen_dump_with_ticks()
|
|
439
|
+
|
|
440
|
+
with DOSVideoTools(gdb=session.gdb) as video: # borrows a session's client
|
|
441
|
+
lines = video.screen_dump() # and never closes it
|
|
363
442
|
```
|
|
364
443
|
|
|
365
444
|
## Known hazards
|
|
366
445
|
|
|
367
|
-
|
|
368
|
-
|
|
446
|
+
One open defect, and two that are fixed but still shape how you should write
|
|
447
|
+
against this library.
|
|
448
|
+
|
|
449
|
+
**Unanswered GDB packets: bounded, not silent** (was
|
|
450
|
+
[#4](https://github.com/lokkju/dbxdebug/issues/4), fixed). `GDBClient` arms a
|
|
451
|
+
30 s read timeout on every read, not just the connect, and raises
|
|
452
|
+
`GDBTimeoutError` naming the packet that went unanswered. Override it per
|
|
453
|
+
client with `GDBClient(timeout=...)`, or pass `timeout=None` for the old
|
|
454
|
+
unbounded blocking. The underlying interaction is unchanged and still worth
|
|
455
|
+
knowing: while the emulator is QMP-stopped the GDB stub is not serviced at
|
|
456
|
+
all, so `qmp.stop()` followed by any GDB request cannot be answered. It now
|
|
457
|
+
fails in 30 s with a message instead of deadlocking. To read memory, reach
|
|
458
|
+
for `session.read_bulk()`, which halts over GDB for you; otherwise halt
|
|
459
|
+
with `gdb.halt()` rather than stopping over QMP. A `memdump` refused for
|
|
460
|
+
this reason now raises `CpuNotStoppedError` (a `QMPError`) naming the fix,
|
|
461
|
+
rather than surfacing the stub's refusal alone.
|
|
462
|
+
|
|
463
|
+
**Stream desync: resynchronised, or refused** (was
|
|
464
|
+
[#5](https://github.com/lokkju/dbxdebug/issues/5), fixed). Both triggers were
|
|
465
|
+
reproduced against a live build -- an unsolicited `$S05` stop reply (QMP
|
|
466
|
+
break-on-exec fires one nobody asked for) and a timed-out request leaving its
|
|
467
|
+
reply in the stream -- and both are handled at the framing layer:
|
|
468
|
+
|
|
469
|
+
* an unrequested stop reply is diverted to `gdb.pending_stops` instead of
|
|
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;
|
|
476
|
+
* an abandoned exchange is drained before the next packet is sent, so the
|
|
477
|
+
request after a `GDBTimeoutError` gets its own reply rather than the
|
|
478
|
+
previous one's;
|
|
479
|
+
* if that drain cannot complete, the client marks itself **permanently
|
|
480
|
+
unusable** and every later call raises `GDBDesyncError`. That is
|
|
481
|
+
deliberate: a loud failure beats a plausible wrong answer. Open a new
|
|
482
|
+
`GDBClient`.
|
|
483
|
+
|
|
484
|
+
Still true, and still worth doing: keep GDB traffic serialised on one thread,
|
|
485
|
+
and never add a read-retry loop. Two identical consecutive requests mask a
|
|
486
|
+
one-packet lag perfectly, so retrying would look like it worked whether or
|
|
487
|
+
not the stream had shifted.
|
|
488
|
+
|
|
489
|
+
**One GDB client at a time**
|
|
490
|
+
([#8](https://github.com/lokkju/dbxdebug/issues/8)). The stub serves a single
|
|
491
|
+
GDB client. A second one completes the TCP connect and then never gets its
|
|
492
|
+
`qSupported` reply -- no refusal, and nothing on the wire. Two things changed
|
|
493
|
+
independently: it now fails after the read timeout instead of hanging forever
|
|
494
|
+
([#5](https://github.com/lokkju/dbxdebug/issues/5)), and nothing in this
|
|
495
|
+
package opens a competing connection behind your back any more
|
|
496
|
+
([#11](https://github.com/lokkju/dbxdebug/issues/11)). The stub limitation
|
|
497
|
+
itself is still open ([#8](https://github.com/lokkju/dbxdebug/issues/8)).
|
|
369
498
|
|
|
370
|
-
|
|
371
|
-
`GDBClient` never calls `settimeout`, so any packet the stub does not answer
|
|
372
|
-
hangs the caller forever with no diagnostic. This is easy to reach by
|
|
373
|
-
accident, because the two protocols interact: while the emulator is
|
|
374
|
-
QMP-stopped the GDB stub does not answer at all, so `qmp.stop()` followed by
|
|
375
|
-
any GDB request is a deadlock. Arm the socket yourself right after `start()`:
|
|
499
|
+
Lend the session's client out instead of opening a second one:
|
|
376
500
|
|
|
377
501
|
```python
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
```
|
|
502
|
+
from dbxdebug.cli import GDB_CLIENT_KEY, main
|
|
503
|
+
from dbxdebug.video import DOSVideoTools
|
|
381
504
|
|
|
382
|
-
|
|
383
|
-
(
|
|
384
|
-
|
|
385
|
-
resynchronises. Once a reply is left unread, every later request returns the
|
|
386
|
-
*previous* request's payload, silently and permanently. Both triggers are
|
|
387
|
-
confirmed against a live build: an unsolicited `$S05` stop reply (QMP
|
|
388
|
-
break-on-exec fires one nobody asked for), and a timed-out request leaving its
|
|
389
|
-
reply in the stream -- so the `settimeout` above converts a hang into a
|
|
390
|
-
`TimeoutError` that lands you here instead. Keep GDB traffic serialised on one
|
|
391
|
-
thread, treat `TimeoutError` as fatal to the connection rather than retryable,
|
|
392
|
-
and check the length of every `read_memory` result against what you asked for:
|
|
393
|
-
it is the one cheap symptom visible from outside. Do not add a read-retry
|
|
394
|
-
loop -- two identical consecutive requests mask a one-packet lag perfectly, so
|
|
395
|
-
it would look like it worked whether or not the stream had shifted.
|
|
505
|
+
with DosboxSession(...) as session:
|
|
506
|
+
with DOSVideoTools(gdb=session.gdb) as video: # borrowed, not reopened
|
|
507
|
+
lines = video.screen_dump()
|
|
396
508
|
|
|
397
|
-
|
|
398
|
-
([
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
509
|
+
# the CLI, driven in-process, borrows the same client
|
|
510
|
+
main(["screen", "show"], obj={GDB_CLIENT_KEY: session.gdb}, standalone_mode=False)
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
A borrowed client is never closed by the borrower -- the session stays its
|
|
514
|
+
owner. `DOSVideoTools()` with no `gdb=` still builds and closes its own, which
|
|
515
|
+
is the right thing when it is the only client, and so does a `dbxdebug` command
|
|
516
|
+
run as a separate process. Running the CLI as a separate process against a
|
|
517
|
+
session that holds its own client is still a second connection however it is
|
|
518
|
+
spelled, so it now fails on the timeout rather than working: use
|
|
519
|
+
`DosboxSession(connect=False)` there, or drive the session's `session.gdb`
|
|
520
|
+
from Python.
|
|
405
521
|
|
|
406
522
|
QMP is a separate socket and is undisturbed by any of this, which is why
|
|
407
523
|
`qmp.query_status()` is the way to learn that the CPU stopped.
|
|
@@ -425,11 +541,20 @@ prove the library actually drives one: the vendor GDB capabilities, `eip` as an
|
|
|
425
541
|
offset rather than a linear address, a breakpoint above 64 KB firing, `memdump`
|
|
426
542
|
agreeing with GDB reads and refusing while the CPU runs, `frames.steps_out`
|
|
427
543
|
stopping after a real 16-bit `ret`, and what the GDB client does when the
|
|
428
|
-
stream is disturbed --
|
|
429
|
-
|
|
544
|
+
stream is disturbed -- an unrequested stop reply queued rather than read as an
|
|
545
|
+
answer, an abandoned reply drained rather than handed to the next request, and
|
|
546
|
+
a request the stub will never answer bounded rather than deadlocked. The
|
|
547
|
+
framing paths a live emulator will not produce on demand are covered against a
|
|
548
|
+
fake socket in `tests/test_gdb_framing.py`.
|
|
549
|
+
The binary is located with `dbxdebug.paths.find_dosbox_x`
|
|
430
550
|
-- set `DBXDEBUG_DOSBOX` to choose a specific build -- and the tests skip when
|
|
431
551
|
none is found.
|
|
432
552
|
|
|
553
|
+
One test in `tests/integration/test_headless.py` is skipped even under the
|
|
554
|
+
`integration` marker: the one that compares a headless screen capture against
|
|
555
|
+
a windowed one has to launch a real window, which takes the keyboard focus of
|
|
556
|
+
whoever is at the machine. Set `DBXDEBUG_ALLOW_WINDOWED=1` to run it.
|
|
557
|
+
|
|
433
558
|
The other gates:
|
|
434
559
|
|
|
435
560
|
```bash
|