dinorefurb-dosbox-session 0.1.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.
- dinorefurb_dosbox_session-0.1.0/.gitignore +20 -0
- dinorefurb_dosbox_session-0.1.0/LICENSE +21 -0
- dinorefurb_dosbox_session-0.1.0/PKG-INFO +220 -0
- dinorefurb_dosbox_session-0.1.0/README.md +209 -0
- dinorefurb_dosbox_session-0.1.0/pyproject.toml +26 -0
- dinorefurb_dosbox_session-0.1.0/src/dinorefurb_dosbox_session/__init__.py +86 -0
- dinorefurb_dosbox_session-0.1.0/src/dinorefurb_dosbox_session/__main__.py +3 -0
- dinorefurb_dosbox_session-0.1.0/src/dinorefurb_dosbox_session/checkout.py +82 -0
- dinorefurb_dosbox_session-0.1.0/src/dinorefurb_dosbox_session/cli.py +57 -0
- dinorefurb_dosbox_session-0.1.0/src/dinorefurb_dosbox_session/client.py +268 -0
- dinorefurb_dosbox_session-0.1.0/src/dinorefurb_dosbox_session/config.py +129 -0
- dinorefurb_dosbox_session-0.1.0/src/dinorefurb_dosbox_session/errors.py +76 -0
- dinorefurb_dosbox_session-0.1.0/src/dinorefurb_dosbox_session/lock.py +222 -0
- dinorefurb_dosbox_session-0.1.0/src/dinorefurb_dosbox_session/processes.py +150 -0
- dinorefurb_dosbox_session-0.1.0/src/dinorefurb_dosbox_session/session.py +495 -0
- dinorefurb_dosbox_session-0.1.0/tests/standin_client.py +153 -0
- dinorefurb_dosbox_session-0.1.0/tests/standin_emulator.py +41 -0
- dinorefurb_dosbox_session-0.1.0/tests/support.py +92 -0
- dinorefurb_dosbox_session-0.1.0/tests/test_parts.py +181 -0
- dinorefurb_dosbox_session-0.1.0/tests/test_session.py +344 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.config/
|
|
2
|
+
.idea/
|
|
3
|
+
.vs/
|
|
4
|
+
**/bin/
|
|
5
|
+
**/obj/
|
|
6
|
+
artifacts/
|
|
7
|
+
template-next/
|
|
8
|
+
TestResults/
|
|
9
|
+
*.user
|
|
10
|
+
*.suo
|
|
11
|
+
|
|
12
|
+
__pycache__/
|
|
13
|
+
*.pyc
|
|
14
|
+
.venv/
|
|
15
|
+
analysis/original/
|
|
16
|
+
node_modules/
|
|
17
|
+
dist/
|
|
18
|
+
packages/*/LICENSE
|
|
19
|
+
# The reader's command-line entry point is source, unlike .NET build output.
|
|
20
|
+
!packages/executable-reader/bin/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Igor Savin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: dinorefurb-dosbox-session
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Owned DOSBox-X debugger sessions for clean-room restoration research: process, run lock, drives, records and observation.
|
|
5
|
+
Project-URL: Source, https://github.com/kibertoad/refurbished-dinosaurs-toolkit/tree/main/packages/dosbox-session
|
|
6
|
+
Author: kibertoad
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.12
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# dinorefurb-dosbox-session
|
|
13
|
+
|
|
14
|
+
Owned DOSBox-X debugger sessions for a restoration's research tooling: the original program runs
|
|
15
|
+
under DOSBox-X's structured debugger, and the tooling reads registers and memory, stops at
|
|
16
|
+
breakpoints and observes operations beside the static evidence. The design is
|
|
17
|
+
[ADR 0026](../../docs/decisions/0026-dosbox-x-session-package.md).
|
|
18
|
+
|
|
19
|
+
The package owns the parts that decide whether a recorded run can be trusted and that carry no
|
|
20
|
+
game knowledge: the emulator process, the machine-wide run lock, the guest drives, muted host
|
|
21
|
+
audio, the session record, request IDs and operation observation. What a run means (executable
|
|
22
|
+
fingerprints, address maps, state layouts, input, screens) stays in the restoration. A restored
|
|
23
|
+
game never depends on this package.
|
|
24
|
+
|
|
25
|
+
Windows only. On another platform a session refuses to start and says so.
|
|
26
|
+
|
|
27
|
+
## The DOSBox-X client is yours to import
|
|
28
|
+
|
|
29
|
+
DOSBox-X's structured debugger ("Agent") and its Python client `dosbox_agent` are in the
|
|
30
|
+
DOSBox-X source tree under GPL-2.0, and are not on PyPI. This package is MIT and never imports,
|
|
31
|
+
vendors or depends on the client. Your tooling imports it from your own checkout and passes a
|
|
32
|
+
factory that builds a client for the session's endpoint:
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import sys
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
|
|
38
|
+
from dinorefurb_dosbox_session import DosboxSession, SessionSettings, Target, verify_checkout
|
|
39
|
+
|
|
40
|
+
checkout = verify_checkout(Path("artifacts/dosbox-x")) # refuses another revision or local changes
|
|
41
|
+
sys.path.insert(0, str(checkout.path / "client" / "python"))
|
|
42
|
+
from dosbox_agent import AgentClient
|
|
43
|
+
|
|
44
|
+
settings = SessionSettings(
|
|
45
|
+
checkout=checkout.path,
|
|
46
|
+
emulator=checkout.path / "bin/x64/Agent Debug SDL2/dosbox-x.exe",
|
|
47
|
+
run_directory=Path("artifacts/runs/2026-10-10-startup"), # new or empty for each run
|
|
48
|
+
target=Target("GAME.EXE"),
|
|
49
|
+
client_factory=lambda endpoint: AgentClient.from_config(endpoint.agent_config),
|
|
50
|
+
prepare_drive=lambda drive_c: ..., # put the target's files on the new, empty C:
|
|
51
|
+
)
|
|
52
|
+
with DosboxSession(settings) as session:
|
|
53
|
+
registers = session.client.get_registers(session.session_id)
|
|
54
|
+
operation = session.continue_()
|
|
55
|
+
observation = session.observe(operation, timeout=10)
|
|
56
|
+
while observation.pending:
|
|
57
|
+
observation = session.observe(operation, timeout=10)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The checkout must be at the revision the package was verified against,
|
|
61
|
+
`b6abbd5980a885f5f310a4088c59a8688d1b116c` (tag `dosbox-x-v2026.10.01`, `PINNED_REVISION`), with
|
|
62
|
+
no modified, staged, deleted or untracked files. The `__pycache__` directories that importing the
|
|
63
|
+
client writes are not counted as changes. Moving to a new revision is a package release.
|
|
64
|
+
|
|
65
|
+
## What a session does
|
|
66
|
+
|
|
67
|
+
Entering `DosboxSession` (or calling `start()`):
|
|
68
|
+
|
|
69
|
+
1. Refuses a platform other than Windows, a checkout that fails the check above, a missing
|
|
70
|
+
emulator, and a run directory that is not empty (a `drive-c` from an earlier run included).
|
|
71
|
+
2. Takes the run lock (below), or refuses with a report of the recorded owner and processes.
|
|
72
|
+
3. Creates `drive-c` empty in the run directory and calls `prepare_drive` on it.
|
|
73
|
+
4. Writes `dosbox.conf` and `agent.env` and launches the emulator with a native console that is
|
|
74
|
+
created and hidden. Redirecting the console, `-noconsole` and `CREATE_NO_WINDOW` each broke
|
|
75
|
+
debugger entry at the pinned revision. The emulator gets `emulator_arguments` first, then
|
|
76
|
+
`-conf` and `--agent-config`.
|
|
77
|
+
5. Waits for the readiness marker the guest's `[autoexec]` writes to `C:\DRREADY.TXT` after its
|
|
78
|
+
drives are mounted. An answering debugger server is not readiness. Fails when the emulator
|
|
79
|
+
exits first or the marker does not appear within `readiness_timeout` seconds.
|
|
80
|
+
6. Builds the first client through the factory, reads the server's capabilities, and starts the
|
|
81
|
+
target stopped at its entry. Fails unless the session stops with reason `startup`.
|
|
82
|
+
|
|
83
|
+
Any failure cleans up as leaving does, then raises.
|
|
84
|
+
|
|
85
|
+
Leaving (or `close()`) stops the debugger session, closes every client, terminates the owned
|
|
86
|
+
emulator and releases the run lock. If the emulator is still running afterwards, it writes
|
|
87
|
+
`cleanup-diagnostic.txt`, keeps the lock and raises `CleanupFailed`; closing again after the
|
|
88
|
+
process has exited releases the lock. It never stops or removes anything the session did not
|
|
89
|
+
start.
|
|
90
|
+
|
|
91
|
+
### The generated configuration
|
|
92
|
+
|
|
93
|
+
`EmulatorConfig` takes the media, extra `dosbox.conf` sections (such as
|
|
94
|
+
`{"cpu": {"cycles": "fixed 10000"}}`), `keep_host_sound` and the Agent limits.
|
|
95
|
+
|
|
96
|
+
- C: is the run's own `drive-c`, writable. It is created empty for each run and never reused.
|
|
97
|
+
- Each `Media` is mounted read-only on a letter from D to Z: an `iso` with `imgmount -t iso`, a
|
|
98
|
+
`directory` with `mount -ro`.
|
|
99
|
+
- Host audio is muted by default: `mixer master 0:0 /noshow` in `[autoexec]` and `[midi]
|
|
100
|
+
mididevice=none`. The emulated sound devices stay configured. `keep_host_sound=True` leaves both
|
|
101
|
+
alone. `nosound` set to anything DOSBox-X does not read as false is refused, because
|
|
102
|
+
`nosound=true` broke structured readiness at the pinned revision.
|
|
103
|
+
- The session writes `[autoexec]` itself, so a section by that name is refused, as is a section
|
|
104
|
+
name, key or value with a line break in it.
|
|
105
|
+
|
|
106
|
+
### The run lock
|
|
107
|
+
|
|
108
|
+
One lock file per machine keeps two probes from sharing one machine's emulator, input or timing.
|
|
109
|
+
The path is `C:\ProgramData\refurbished-dinosaurs\run.lock` unless the environment variable
|
|
110
|
+
`REFURBISHED_DINOSAURS_RUN_LOCK` names another. That variable is a machine setting: two programs
|
|
111
|
+
that resolve the lock path differently do not exclude each other, so set it in your user
|
|
112
|
+
environment or not at all. `SessionSettings.lock_path` overrides both, for tests.
|
|
113
|
+
|
|
114
|
+
The lock records the session, the owner process and the emulator, each by process ID and start
|
|
115
|
+
time, so a process that later reuses an ID does not match. A session that finds the lock refuses
|
|
116
|
+
to start (`LockHeld`) and its report says which recorded processes still run. Nothing removes a
|
|
117
|
+
lock automatically. When every recorded process has exited, remove it with:
|
|
118
|
+
|
|
119
|
+
```text
|
|
120
|
+
dosbox-session stale-lock [--lock PATH] [--json]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
It checks the processes again first, and refuses (exit code 1, nothing removed) when one still
|
|
124
|
+
runs or cannot be queried, when the file is not a record this package wrote, or when deleting it
|
|
125
|
+
fails. Exit code 0 means it removed the lock or found none; 2 is a usage error.
|
|
126
|
+
|
|
127
|
+
### The session record
|
|
128
|
+
|
|
129
|
+
`session.json` in the run directory is rewritten at each step and holds:
|
|
130
|
+
|
|
131
|
+
| Field | Meaning |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `checkout` | The checkout's path and revision. |
|
|
134
|
+
| `emulator` | The emulator's path and SHA-256. |
|
|
135
|
+
| `build_link` | States that the two facts above are separate: nothing shows the emulator was built from that checkout. |
|
|
136
|
+
| `run_lock` | The lock path this session took. |
|
|
137
|
+
| `endpoint` | The session's named pipe, unique to the session. |
|
|
138
|
+
| `owner_process`, `emulator_process` | Process ID and start time of each. |
|
|
139
|
+
| `host_sound` | `muted` or `kept`. |
|
|
140
|
+
| `readiness` | `observed` once the guest wrote its marker. |
|
|
141
|
+
| `request_id_prefixes` | One per client. |
|
|
142
|
+
| `capabilities` | What the server reported. |
|
|
143
|
+
| `debugger_session` | The debugger session's ID. |
|
|
144
|
+
|
|
145
|
+
### Calls, capabilities and request IDs
|
|
146
|
+
|
|
147
|
+
`session.client` and each `session.open_diagnostic_client()` wrap a client from your factory.
|
|
148
|
+
Every call they make carries a request ID from that client's own namespace
|
|
149
|
+
(`<session>.c<client>.<n>`), so a diagnostic client on the same session cannot collide with the
|
|
150
|
+
first. A call that needs a capability the server did not report as `true` raises
|
|
151
|
+
`CapabilityRefused` and is not sent: every debugger call needs `debugger`, a `memory_change`
|
|
152
|
+
breakpoint needs `breakpoints.memory_change`, and CPU tracing needs `trace.cpu`. The wrappers offer
|
|
153
|
+
no writes to guest state.
|
|
154
|
+
|
|
155
|
+
### Observation
|
|
156
|
+
|
|
157
|
+
`session.observe(operation, timeout)` waits on one operation. Each poll first checks that the owned
|
|
158
|
+
emulator still runs (`EmulatorExited` if not). When the time runs out the result is `pending`,
|
|
159
|
+
which is neither a failure nor a result: observe the same operation again to keep waiting.
|
|
160
|
+
Transport errors from the client propagate. Observation never restarts the guest or sends another
|
|
161
|
+
continuation, and while an operation is pending a further `continue_` or `step` raises
|
|
162
|
+
`OperationPending`. A pause ends the continuation it interrupts, so observing either one clears
|
|
163
|
+
both. When a `continue_` or `pause` request itself raises, the server may still have received it:
|
|
164
|
+
`continue_` and `step` raise `OperationPending` until `client.status()` shows the guest
|
|
165
|
+
`stopped`, `exited` or `failed`.
|
|
166
|
+
|
|
167
|
+
## Errors
|
|
168
|
+
|
|
169
|
+
| Error | Raised when |
|
|
170
|
+
|---|---|
|
|
171
|
+
| `PlatformRefused` | The platform is not Windows. |
|
|
172
|
+
| `CheckoutRefused` | The checkout is at another revision, has local changes, or cannot be read with git. |
|
|
173
|
+
| `RunDirectoryRefused` | The run directory is not empty, or `prepare_drive` wrote the readiness marker. |
|
|
174
|
+
| `ConfigurationRefused` | A setting is one the session owns or one known to break the debugger. |
|
|
175
|
+
| `LockHeld` | The run lock is held, or cannot be read. `report` describes it. |
|
|
176
|
+
| `EmulatorExited` | The owned emulator exited while the session needed it. |
|
|
177
|
+
| `ReadinessNotObserved` | The guest did not write its readiness marker in time. |
|
|
178
|
+
| `CapabilityRefused` | An operation needs a capability the server did not report. Not sent. |
|
|
179
|
+
| `OperationPending` | A continuation was asked for while another operation is pending. |
|
|
180
|
+
| `CleanupFailed` | The emulator still ran after teardown; the lock was kept. |
|
|
181
|
+
|
|
182
|
+
All of them derive from `SessionError`.
|
|
183
|
+
|
|
184
|
+
## Native verification
|
|
185
|
+
|
|
186
|
+
CI runs the tests against a stand-in emulator and a stand-in client, with no DOSBox-X and no game.
|
|
187
|
+
Before each release that changes process, transport or drive handling, the owner runs this
|
|
188
|
+
procedure on the pinned revision and puts its output in the pull request:
|
|
189
|
+
|
|
190
|
+
1. Clone `https://github.com/joncampbell123/dosbox-x` at tag `dosbox-x-v2026.10.01` and check that
|
|
191
|
+
`git rev-parse HEAD` prints `b6abbd5980a885f5f310a4088c59a8688d1b116c`.
|
|
192
|
+
2. Build the `Agent Debug SDL2` configuration for x64. With Visual Studio 2019 Build Tools
|
|
193
|
+
(toolset v142) and Windows SDK 10.0.19041.0:
|
|
194
|
+
|
|
195
|
+
```powershell
|
|
196
|
+
& 'C:/Program Files (x86)/Microsoft Visual Studio/2019/BuildTools/MSBuild/Current/Bin/MSBuild.exe' `
|
|
197
|
+
<checkout>/vs/dosbox-x.sln '/p:Configuration=Agent Debug SDL2' /p:Platform=x64 `
|
|
198
|
+
/p:PlatformToolset=v142 /p:WindowsTargetPlatformVersion=10.0.19041.0 /m:2 /v:minimal
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
3. From this directory, with the package installed:
|
|
202
|
+
|
|
203
|
+
```powershell
|
|
204
|
+
python native/verify_native.py --checkout <checkout> `
|
|
205
|
+
--emulator '<checkout>/bin/x64/Agent Debug SDL2/dosbox-x.exe' --run-directory <new directory>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The script generates a synthetic `.COM` program, starts it in an owned session under the
|
|
209
|
+
machine's run lock, sets an execution breakpoint, continues to it, and reads the registers there.
|
|
210
|
+
It passes when the breakpoint stop is observed and `AX` and `BX` hold the values the program set.
|
|
211
|
+
It prints the checks, the checkout revision, the emulator hash and the reported capabilities.
|
|
212
|
+
|
|
213
|
+
## Tests
|
|
214
|
+
|
|
215
|
+
```sh
|
|
216
|
+
python -m pip install -e packages/dosbox-session
|
|
217
|
+
cd packages/dosbox-session && python -B -m unittest discover -s tests -p "test*.py"
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The session tests need Windows and git; on another platform they are skipped.
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# dinorefurb-dosbox-session
|
|
2
|
+
|
|
3
|
+
Owned DOSBox-X debugger sessions for a restoration's research tooling: the original program runs
|
|
4
|
+
under DOSBox-X's structured debugger, and the tooling reads registers and memory, stops at
|
|
5
|
+
breakpoints and observes operations beside the static evidence. The design is
|
|
6
|
+
[ADR 0026](../../docs/decisions/0026-dosbox-x-session-package.md).
|
|
7
|
+
|
|
8
|
+
The package owns the parts that decide whether a recorded run can be trusted and that carry no
|
|
9
|
+
game knowledge: the emulator process, the machine-wide run lock, the guest drives, muted host
|
|
10
|
+
audio, the session record, request IDs and operation observation. What a run means (executable
|
|
11
|
+
fingerprints, address maps, state layouts, input, screens) stays in the restoration. A restored
|
|
12
|
+
game never depends on this package.
|
|
13
|
+
|
|
14
|
+
Windows only. On another platform a session refuses to start and says so.
|
|
15
|
+
|
|
16
|
+
## The DOSBox-X client is yours to import
|
|
17
|
+
|
|
18
|
+
DOSBox-X's structured debugger ("Agent") and its Python client `dosbox_agent` are in the
|
|
19
|
+
DOSBox-X source tree under GPL-2.0, and are not on PyPI. This package is MIT and never imports,
|
|
20
|
+
vendors or depends on the client. Your tooling imports it from your own checkout and passes a
|
|
21
|
+
factory that builds a client for the session's endpoint:
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
import sys
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
from dinorefurb_dosbox_session import DosboxSession, SessionSettings, Target, verify_checkout
|
|
28
|
+
|
|
29
|
+
checkout = verify_checkout(Path("artifacts/dosbox-x")) # refuses another revision or local changes
|
|
30
|
+
sys.path.insert(0, str(checkout.path / "client" / "python"))
|
|
31
|
+
from dosbox_agent import AgentClient
|
|
32
|
+
|
|
33
|
+
settings = SessionSettings(
|
|
34
|
+
checkout=checkout.path,
|
|
35
|
+
emulator=checkout.path / "bin/x64/Agent Debug SDL2/dosbox-x.exe",
|
|
36
|
+
run_directory=Path("artifacts/runs/2026-10-10-startup"), # new or empty for each run
|
|
37
|
+
target=Target("GAME.EXE"),
|
|
38
|
+
client_factory=lambda endpoint: AgentClient.from_config(endpoint.agent_config),
|
|
39
|
+
prepare_drive=lambda drive_c: ..., # put the target's files on the new, empty C:
|
|
40
|
+
)
|
|
41
|
+
with DosboxSession(settings) as session:
|
|
42
|
+
registers = session.client.get_registers(session.session_id)
|
|
43
|
+
operation = session.continue_()
|
|
44
|
+
observation = session.observe(operation, timeout=10)
|
|
45
|
+
while observation.pending:
|
|
46
|
+
observation = session.observe(operation, timeout=10)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The checkout must be at the revision the package was verified against,
|
|
50
|
+
`b6abbd5980a885f5f310a4088c59a8688d1b116c` (tag `dosbox-x-v2026.10.01`, `PINNED_REVISION`), with
|
|
51
|
+
no modified, staged, deleted or untracked files. The `__pycache__` directories that importing the
|
|
52
|
+
client writes are not counted as changes. Moving to a new revision is a package release.
|
|
53
|
+
|
|
54
|
+
## What a session does
|
|
55
|
+
|
|
56
|
+
Entering `DosboxSession` (or calling `start()`):
|
|
57
|
+
|
|
58
|
+
1. Refuses a platform other than Windows, a checkout that fails the check above, a missing
|
|
59
|
+
emulator, and a run directory that is not empty (a `drive-c` from an earlier run included).
|
|
60
|
+
2. Takes the run lock (below), or refuses with a report of the recorded owner and processes.
|
|
61
|
+
3. Creates `drive-c` empty in the run directory and calls `prepare_drive` on it.
|
|
62
|
+
4. Writes `dosbox.conf` and `agent.env` and launches the emulator with a native console that is
|
|
63
|
+
created and hidden. Redirecting the console, `-noconsole` and `CREATE_NO_WINDOW` each broke
|
|
64
|
+
debugger entry at the pinned revision. The emulator gets `emulator_arguments` first, then
|
|
65
|
+
`-conf` and `--agent-config`.
|
|
66
|
+
5. Waits for the readiness marker the guest's `[autoexec]` writes to `C:\DRREADY.TXT` after its
|
|
67
|
+
drives are mounted. An answering debugger server is not readiness. Fails when the emulator
|
|
68
|
+
exits first or the marker does not appear within `readiness_timeout` seconds.
|
|
69
|
+
6. Builds the first client through the factory, reads the server's capabilities, and starts the
|
|
70
|
+
target stopped at its entry. Fails unless the session stops with reason `startup`.
|
|
71
|
+
|
|
72
|
+
Any failure cleans up as leaving does, then raises.
|
|
73
|
+
|
|
74
|
+
Leaving (or `close()`) stops the debugger session, closes every client, terminates the owned
|
|
75
|
+
emulator and releases the run lock. If the emulator is still running afterwards, it writes
|
|
76
|
+
`cleanup-diagnostic.txt`, keeps the lock and raises `CleanupFailed`; closing again after the
|
|
77
|
+
process has exited releases the lock. It never stops or removes anything the session did not
|
|
78
|
+
start.
|
|
79
|
+
|
|
80
|
+
### The generated configuration
|
|
81
|
+
|
|
82
|
+
`EmulatorConfig` takes the media, extra `dosbox.conf` sections (such as
|
|
83
|
+
`{"cpu": {"cycles": "fixed 10000"}}`), `keep_host_sound` and the Agent limits.
|
|
84
|
+
|
|
85
|
+
- C: is the run's own `drive-c`, writable. It is created empty for each run and never reused.
|
|
86
|
+
- Each `Media` is mounted read-only on a letter from D to Z: an `iso` with `imgmount -t iso`, a
|
|
87
|
+
`directory` with `mount -ro`.
|
|
88
|
+
- Host audio is muted by default: `mixer master 0:0 /noshow` in `[autoexec]` and `[midi]
|
|
89
|
+
mididevice=none`. The emulated sound devices stay configured. `keep_host_sound=True` leaves both
|
|
90
|
+
alone. `nosound` set to anything DOSBox-X does not read as false is refused, because
|
|
91
|
+
`nosound=true` broke structured readiness at the pinned revision.
|
|
92
|
+
- The session writes `[autoexec]` itself, so a section by that name is refused, as is a section
|
|
93
|
+
name, key or value with a line break in it.
|
|
94
|
+
|
|
95
|
+
### The run lock
|
|
96
|
+
|
|
97
|
+
One lock file per machine keeps two probes from sharing one machine's emulator, input or timing.
|
|
98
|
+
The path is `C:\ProgramData\refurbished-dinosaurs\run.lock` unless the environment variable
|
|
99
|
+
`REFURBISHED_DINOSAURS_RUN_LOCK` names another. That variable is a machine setting: two programs
|
|
100
|
+
that resolve the lock path differently do not exclude each other, so set it in your user
|
|
101
|
+
environment or not at all. `SessionSettings.lock_path` overrides both, for tests.
|
|
102
|
+
|
|
103
|
+
The lock records the session, the owner process and the emulator, each by process ID and start
|
|
104
|
+
time, so a process that later reuses an ID does not match. A session that finds the lock refuses
|
|
105
|
+
to start (`LockHeld`) and its report says which recorded processes still run. Nothing removes a
|
|
106
|
+
lock automatically. When every recorded process has exited, remove it with:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
dosbox-session stale-lock [--lock PATH] [--json]
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
It checks the processes again first, and refuses (exit code 1, nothing removed) when one still
|
|
113
|
+
runs or cannot be queried, when the file is not a record this package wrote, or when deleting it
|
|
114
|
+
fails. Exit code 0 means it removed the lock or found none; 2 is a usage error.
|
|
115
|
+
|
|
116
|
+
### The session record
|
|
117
|
+
|
|
118
|
+
`session.json` in the run directory is rewritten at each step and holds:
|
|
119
|
+
|
|
120
|
+
| Field | Meaning |
|
|
121
|
+
|---|---|
|
|
122
|
+
| `checkout` | The checkout's path and revision. |
|
|
123
|
+
| `emulator` | The emulator's path and SHA-256. |
|
|
124
|
+
| `build_link` | States that the two facts above are separate: nothing shows the emulator was built from that checkout. |
|
|
125
|
+
| `run_lock` | The lock path this session took. |
|
|
126
|
+
| `endpoint` | The session's named pipe, unique to the session. |
|
|
127
|
+
| `owner_process`, `emulator_process` | Process ID and start time of each. |
|
|
128
|
+
| `host_sound` | `muted` or `kept`. |
|
|
129
|
+
| `readiness` | `observed` once the guest wrote its marker. |
|
|
130
|
+
| `request_id_prefixes` | One per client. |
|
|
131
|
+
| `capabilities` | What the server reported. |
|
|
132
|
+
| `debugger_session` | The debugger session's ID. |
|
|
133
|
+
|
|
134
|
+
### Calls, capabilities and request IDs
|
|
135
|
+
|
|
136
|
+
`session.client` and each `session.open_diagnostic_client()` wrap a client from your factory.
|
|
137
|
+
Every call they make carries a request ID from that client's own namespace
|
|
138
|
+
(`<session>.c<client>.<n>`), so a diagnostic client on the same session cannot collide with the
|
|
139
|
+
first. A call that needs a capability the server did not report as `true` raises
|
|
140
|
+
`CapabilityRefused` and is not sent: every debugger call needs `debugger`, a `memory_change`
|
|
141
|
+
breakpoint needs `breakpoints.memory_change`, and CPU tracing needs `trace.cpu`. The wrappers offer
|
|
142
|
+
no writes to guest state.
|
|
143
|
+
|
|
144
|
+
### Observation
|
|
145
|
+
|
|
146
|
+
`session.observe(operation, timeout)` waits on one operation. Each poll first checks that the owned
|
|
147
|
+
emulator still runs (`EmulatorExited` if not). When the time runs out the result is `pending`,
|
|
148
|
+
which is neither a failure nor a result: observe the same operation again to keep waiting.
|
|
149
|
+
Transport errors from the client propagate. Observation never restarts the guest or sends another
|
|
150
|
+
continuation, and while an operation is pending a further `continue_` or `step` raises
|
|
151
|
+
`OperationPending`. A pause ends the continuation it interrupts, so observing either one clears
|
|
152
|
+
both. When a `continue_` or `pause` request itself raises, the server may still have received it:
|
|
153
|
+
`continue_` and `step` raise `OperationPending` until `client.status()` shows the guest
|
|
154
|
+
`stopped`, `exited` or `failed`.
|
|
155
|
+
|
|
156
|
+
## Errors
|
|
157
|
+
|
|
158
|
+
| Error | Raised when |
|
|
159
|
+
|---|---|
|
|
160
|
+
| `PlatformRefused` | The platform is not Windows. |
|
|
161
|
+
| `CheckoutRefused` | The checkout is at another revision, has local changes, or cannot be read with git. |
|
|
162
|
+
| `RunDirectoryRefused` | The run directory is not empty, or `prepare_drive` wrote the readiness marker. |
|
|
163
|
+
| `ConfigurationRefused` | A setting is one the session owns or one known to break the debugger. |
|
|
164
|
+
| `LockHeld` | The run lock is held, or cannot be read. `report` describes it. |
|
|
165
|
+
| `EmulatorExited` | The owned emulator exited while the session needed it. |
|
|
166
|
+
| `ReadinessNotObserved` | The guest did not write its readiness marker in time. |
|
|
167
|
+
| `CapabilityRefused` | An operation needs a capability the server did not report. Not sent. |
|
|
168
|
+
| `OperationPending` | A continuation was asked for while another operation is pending. |
|
|
169
|
+
| `CleanupFailed` | The emulator still ran after teardown; the lock was kept. |
|
|
170
|
+
|
|
171
|
+
All of them derive from `SessionError`.
|
|
172
|
+
|
|
173
|
+
## Native verification
|
|
174
|
+
|
|
175
|
+
CI runs the tests against a stand-in emulator and a stand-in client, with no DOSBox-X and no game.
|
|
176
|
+
Before each release that changes process, transport or drive handling, the owner runs this
|
|
177
|
+
procedure on the pinned revision and puts its output in the pull request:
|
|
178
|
+
|
|
179
|
+
1. Clone `https://github.com/joncampbell123/dosbox-x` at tag `dosbox-x-v2026.10.01` and check that
|
|
180
|
+
`git rev-parse HEAD` prints `b6abbd5980a885f5f310a4088c59a8688d1b116c`.
|
|
181
|
+
2. Build the `Agent Debug SDL2` configuration for x64. With Visual Studio 2019 Build Tools
|
|
182
|
+
(toolset v142) and Windows SDK 10.0.19041.0:
|
|
183
|
+
|
|
184
|
+
```powershell
|
|
185
|
+
& 'C:/Program Files (x86)/Microsoft Visual Studio/2019/BuildTools/MSBuild/Current/Bin/MSBuild.exe' `
|
|
186
|
+
<checkout>/vs/dosbox-x.sln '/p:Configuration=Agent Debug SDL2' /p:Platform=x64 `
|
|
187
|
+
/p:PlatformToolset=v142 /p:WindowsTargetPlatformVersion=10.0.19041.0 /m:2 /v:minimal
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
3. From this directory, with the package installed:
|
|
191
|
+
|
|
192
|
+
```powershell
|
|
193
|
+
python native/verify_native.py --checkout <checkout> `
|
|
194
|
+
--emulator '<checkout>/bin/x64/Agent Debug SDL2/dosbox-x.exe' --run-directory <new directory>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The script generates a synthetic `.COM` program, starts it in an owned session under the
|
|
198
|
+
machine's run lock, sets an execution breakpoint, continues to it, and reads the registers there.
|
|
199
|
+
It passes when the breakpoint stop is observed and `AX` and `BX` hold the values the program set.
|
|
200
|
+
It prints the checks, the checkout revision, the emulator hash and the reported capabilities.
|
|
201
|
+
|
|
202
|
+
## Tests
|
|
203
|
+
|
|
204
|
+
```sh
|
|
205
|
+
python -m pip install -e packages/dosbox-session
|
|
206
|
+
cd packages/dosbox-session && python -B -m unittest discover -s tests -p "test*.py"
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The session tests need Windows and git; on another platform they are skipped.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dinorefurb-dosbox-session"
|
|
7
|
+
# The release workflow writes the published version from the package's release tag.
|
|
8
|
+
version = "0.1.0"
|
|
9
|
+
description = "Owned DOSBox-X debugger sessions for clean-room restoration research: process, run lock, drives, records and observation."
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
license = "MIT"
|
|
13
|
+
authors = [{ name = "kibertoad" }]
|
|
14
|
+
dependencies = []
|
|
15
|
+
|
|
16
|
+
[project.scripts]
|
|
17
|
+
dosbox-session = "dinorefurb_dosbox_session.cli:main"
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Source = "https://github.com/kibertoad/refurbished-dinosaurs-toolkit/tree/main/packages/dosbox-session"
|
|
21
|
+
|
|
22
|
+
[tool.hatch.build.targets.wheel]
|
|
23
|
+
packages = ["src/dinorefurb_dosbox_session"]
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.sdist]
|
|
26
|
+
include = ["src", "tests", "README.md", "pyproject.toml"]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Owned DOSBox-X debugger sessions for restoration research tooling (ADR 0026).
|
|
2
|
+
|
|
3
|
+
The package owns the emulator process, the machine-wide run lock, the guest drives, muted host
|
|
4
|
+
audio, session records, request IDs and operation observation. It never imports the DOSBox-X
|
|
5
|
+
Agent client: the caller imports it from its own checkout and passes a :data:`ClientFactory`.
|
|
6
|
+
Windows only.
|
|
7
|
+
|
|
8
|
+
The supported imports are below. Everything else is internal.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .checkout import PINNED_REVISION, PINNED_TAG, CheckoutIdentity, verify_checkout
|
|
12
|
+
from .client import (
|
|
13
|
+
AgentClientLike,
|
|
14
|
+
ClientFactory,
|
|
15
|
+
Endpoint,
|
|
16
|
+
RequestIds,
|
|
17
|
+
SessionClient,
|
|
18
|
+
capability_value,
|
|
19
|
+
require,
|
|
20
|
+
)
|
|
21
|
+
from .config import READINESS_MARKER, AgentLimits, EmulatorConfig, Media
|
|
22
|
+
from .errors import (
|
|
23
|
+
CapabilityRefused,
|
|
24
|
+
CheckoutRefused,
|
|
25
|
+
CleanupFailed,
|
|
26
|
+
ConfigurationRefused,
|
|
27
|
+
EmulatorExited,
|
|
28
|
+
LockHeld,
|
|
29
|
+
OperationPending,
|
|
30
|
+
PlatformRefused,
|
|
31
|
+
ReadinessNotObserved,
|
|
32
|
+
RunDirectoryRefused,
|
|
33
|
+
SessionError,
|
|
34
|
+
)
|
|
35
|
+
from .lock import (
|
|
36
|
+
DEFAULT_LOCK_PATH,
|
|
37
|
+
LOCK_PATH_VARIABLE,
|
|
38
|
+
LockReport,
|
|
39
|
+
RecordedProcess,
|
|
40
|
+
read_lock,
|
|
41
|
+
remove_stale_lock,
|
|
42
|
+
resolve_lock_path,
|
|
43
|
+
)
|
|
44
|
+
from .processes import ProcessIdentity
|
|
45
|
+
from .session import DosboxSession, Observation, SessionSettings, Target
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"AgentClientLike",
|
|
49
|
+
"AgentLimits",
|
|
50
|
+
"CapabilityRefused",
|
|
51
|
+
"CheckoutIdentity",
|
|
52
|
+
"CheckoutRefused",
|
|
53
|
+
"CleanupFailed",
|
|
54
|
+
"ClientFactory",
|
|
55
|
+
"ConfigurationRefused",
|
|
56
|
+
"DEFAULT_LOCK_PATH",
|
|
57
|
+
"DosboxSession",
|
|
58
|
+
"EmulatorConfig",
|
|
59
|
+
"EmulatorExited",
|
|
60
|
+
"Endpoint",
|
|
61
|
+
"LOCK_PATH_VARIABLE",
|
|
62
|
+
"LockHeld",
|
|
63
|
+
"LockReport",
|
|
64
|
+
"Media",
|
|
65
|
+
"Observation",
|
|
66
|
+
"OperationPending",
|
|
67
|
+
"PINNED_REVISION",
|
|
68
|
+
"PINNED_TAG",
|
|
69
|
+
"PlatformRefused",
|
|
70
|
+
"ProcessIdentity",
|
|
71
|
+
"READINESS_MARKER",
|
|
72
|
+
"ReadinessNotObserved",
|
|
73
|
+
"RecordedProcess",
|
|
74
|
+
"RequestIds",
|
|
75
|
+
"RunDirectoryRefused",
|
|
76
|
+
"SessionClient",
|
|
77
|
+
"SessionError",
|
|
78
|
+
"SessionSettings",
|
|
79
|
+
"Target",
|
|
80
|
+
"capability_value",
|
|
81
|
+
"read_lock",
|
|
82
|
+
"remove_stale_lock",
|
|
83
|
+
"require",
|
|
84
|
+
"resolve_lock_path",
|
|
85
|
+
"verify_checkout",
|
|
86
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""The DOSBox-X checkout the caller imports the Agent client from, and the emulator's hash."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import subprocess
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .errors import CheckoutRefused
|
|
11
|
+
|
|
12
|
+
#: The DOSBox-X revision this package was verified against: tag ``dosbox-x-v2026.10.01``.
|
|
13
|
+
PINNED_REVISION = "b6abbd5980a885f5f310a4088c59a8688d1b116c"
|
|
14
|
+
|
|
15
|
+
#: The tag that names :data:`PINNED_REVISION`.
|
|
16
|
+
PINNED_TAG = "dosbox-x-v2026.10.01"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class CheckoutIdentity:
|
|
21
|
+
"""A checkout that passed :func:`verify_checkout`.
|
|
22
|
+
|
|
23
|
+
The revision describes the checkout only. It does not show which source an emulator
|
|
24
|
+
executable was built from.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
path: Path
|
|
28
|
+
revision: str
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _git(checkout: Path, *arguments: str) -> str:
|
|
32
|
+
try:
|
|
33
|
+
completed = subprocess.run(
|
|
34
|
+
["git", "-C", str(checkout), *arguments],
|
|
35
|
+
capture_output=True,
|
|
36
|
+
text=True,
|
|
37
|
+
encoding="utf-8",
|
|
38
|
+
check=False,
|
|
39
|
+
)
|
|
40
|
+
except FileNotFoundError as error:
|
|
41
|
+
raise CheckoutRefused("git is not installed, so the checkout's revision cannot be read.") from error
|
|
42
|
+
if completed.returncode != 0:
|
|
43
|
+
raise CheckoutRefused(f"{checkout} is not a readable git checkout: {completed.stderr.strip()}")
|
|
44
|
+
return completed.stdout
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _is_bytecode_cache(path: str) -> bool:
|
|
48
|
+
# Importing the client writes __pycache__ directories into the checkout. They are generated
|
|
49
|
+
# from the checkout's own source, so they are not a change to it.
|
|
50
|
+
return "__pycache__" in path.replace("\\", "/").split("/")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def verify_checkout(checkout: str | Path, revision: str | None = None) -> CheckoutIdentity:
|
|
54
|
+
"""Checks that ``checkout`` is at the pinned revision and has no local changes.
|
|
55
|
+
|
|
56
|
+
A modified, staged, deleted or untracked file is a local change, except for the
|
|
57
|
+
``__pycache__`` directories that importing the client writes. ``revision`` defaults to
|
|
58
|
+
:data:`PINNED_REVISION`.
|
|
59
|
+
|
|
60
|
+
:raises CheckoutRefused: the checkout is at another revision, has local changes, or cannot be
|
|
61
|
+
read with git.
|
|
62
|
+
"""
|
|
63
|
+
expected = revision if revision is not None else PINNED_REVISION
|
|
64
|
+
path = Path(checkout).resolve()
|
|
65
|
+
head = _git(path, "rev-parse", "HEAD").strip()
|
|
66
|
+
if head != expected:
|
|
67
|
+
raise CheckoutRefused(f"{path} is at revision {head}; this package was verified against {expected}.")
|
|
68
|
+
status = _git(path, "status", "--porcelain=v1", "--untracked-files=all")
|
|
69
|
+
changes = [line[3:] for line in status.splitlines() if line and not _is_bytecode_cache(line[3:])]
|
|
70
|
+
if changes:
|
|
71
|
+
shown = ", ".join(changes[:5]) + (f" and {len(changes) - 5} more" if len(changes) > 5 else "")
|
|
72
|
+
raise CheckoutRefused(f"{path} has local changes: {shown}.")
|
|
73
|
+
return CheckoutIdentity(path, head)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def file_sha256(path: str | Path) -> str:
|
|
77
|
+
"""Returns the SHA-256 of a file's bytes as lowercase hex."""
|
|
78
|
+
digest = hashlib.sha256()
|
|
79
|
+
with Path(path).open("rb") as handle:
|
|
80
|
+
for block in iter(lambda: handle.read(1 << 20), b""):
|
|
81
|
+
digest.update(block)
|
|
82
|
+
return digest.hexdigest()
|