dinorefurb-dosbox-session
Owned DOSBox-X debugger sessions for a restoration's research tooling: the original program runs under DOSBox-X's structured debugger, and the tooling reads registers and memory, stops at breakpoints, observes operations and makes checked writes to stopped guest memory beside the static evidence. The design is ADR 0026.
The package owns the parts that decide whether a recorded run can be trusted and that carry no game knowledge: the emulator process, the machine-wide run lock, the guest drives, muted host audio, the session record, request IDs, operation observation and guarded writes. What a run means (executable fingerprints, address maps, state layouts and which fields may be written, input, screens) stays in the restoration. A restored game never depends on this package.
Windows only. On another platform a session refuses to start and says so.
The DOSBox-X client is yours to import
DOSBox-X's structured debugger ("Agent") and its Python client dosbox_agent are in the
DOSBox-X source tree under GPL-2.0, and are not on PyPI. This package is MIT and never imports,
vendors or depends on the client. Your tooling imports it from your own checkout and passes a
factory that builds a client for the session's endpoint:
import sys
from pathlib import Path
from dinorefurb_dosbox_session import DosboxSession, SessionSettings, Target, verify_checkout
checkout = verify_checkout(Path("artifacts/dosbox-x")) # refuses another revision or local changes
sys.path.insert(0, str(checkout.path / "client" / "python"))
from dosbox_agent import AgentClient
settings = SessionSettings(
checkout=checkout.path,
emulator=checkout.path / "bin/x64/Agent Debug SDL2/dosbox-x.exe",
run_directory=Path("artifacts/runs/2026-10-10-startup"), # new or empty for each run
target=Target("GAME.EXE"),
client_factory=lambda endpoint: AgentClient.from_config(endpoint.agent_config),
prepare_drive=lambda drive_c: ..., # put the target's files on the new, empty C:
)
with DosboxSession(settings) as session:
registers = session.client.get_registers(session.session_id)
operation = session.continue_()
observation = session.observe(operation, timeout=10)
while observation.pending:
observation = session.observe(operation, timeout=10)
The checkout must be at the revision the package was verified against,
b6abbd5980a885f5f310a4088c59a8688d1b116c (tag dosbox-x-v2026.10.01, PINNED_REVISION), with
no modified, staged, deleted or untracked files. The __pycache__ directories that importing the
client writes are not counted as changes. Moving to a new revision is a package release.
What a session does
Entering DosboxSession (or calling start()):
- Refuses a platform other than Windows, a checkout that fails the check above, a missing
emulator, and a run directory that is not empty (a
drive-cfrom an earlier run included). - Takes the run lock (below), or refuses with a report of the recorded owner and processes.
- Creates
drive-cempty in the run directory and callsprepare_driveon it. - Writes
dosbox.confandagent.envand launches the emulator with a native console that is created and hidden. Redirecting the console,-noconsoleandCREATE_NO_WINDOWeach broke debugger entry at the pinned revision. The emulator getsemulator_argumentsfirst, then-confand--agent-config. - Waits for the readiness marker the guest's
[autoexec]writes toC:\DRREADY.TXTafter its drives are mounted. An answering debugger server is not readiness. Fails when the emulator exits first or the marker does not appear withinreadiness_timeoutseconds. - Builds the first client through the factory, reads the server's capabilities, and starts the
target stopped at its entry. Fails unless the session stops with reason
startup.
Any failure cleans up as leaving does, then raises.
Leaving (or close()) stops the debugger session, closes every client, terminates the owned
emulator and releases the run lock. If the emulator is still running afterwards, it writes
cleanup-diagnostic.txt, keeps the lock and raises CleanupFailed; closing again after the
process has exited releases the lock. It never stops or removes anything the session did not
start.
The generated configuration
EmulatorConfig takes the media, extra dosbox.conf sections (such as
{"cpu": {"cycles": "fixed 10000"}}), keep_host_sound and the Agent limits.
- C: is the run's own
drive-c, writable. It is created empty for each run and never reused. - Each
Mediais mounted read-only on a letter from D to Z: anisowithimgmount -t iso, adirectorywithmount -ro. - Host audio is muted by default:
mixer master 0:0 /noshowin[autoexec]and[midi] mididevice=none. The emulated sound devices stay configured.keep_host_sound=Trueleaves both alone.nosoundset to anything DOSBox-X does not read as false is refused, becausenosound=truebroke structured readiness at the pinned revision. - The session writes
[autoexec]itself, so a section by that name is refused, as is a section name, key or value with a line break in it.
The run lock
One lock file per machine keeps two probes from sharing one machine's emulator, input or timing.
The path is C:\ProgramData\refurbished-dinosaurs\run.lock unless the environment variable
REFURBISHED_DINOSAURS_RUN_LOCK names another. That variable is a machine setting: two programs
that resolve the lock path differently do not exclude each other, so set it in your user
environment or not at all. SessionSettings.lock_path overrides both, for tests.
The lock records the session, the owner process and the emulator, each by process ID and start
time, so a process that later reuses an ID does not match. A session that finds the lock refuses
to start (LockHeld) and its report says which recorded processes still run. Nothing removes a
lock automatically. When every recorded process has exited, remove it with:
dosbox-session stale-lock [--lock PATH] [--json]
It checks the processes again first, and refuses (exit code 1, nothing removed) when one still runs or cannot be queried, when the file is not a record this package wrote, or when deleting it fails. Exit code 0 means it removed the lock or found none; 2 is a usage error.
The session record
session.json in the run directory is rewritten at each step and holds:
| Field | Meaning |
|---|---|
checkout |
The checkout's path and revision. |
emulator |
The emulator's path and SHA-256. |
build_link |
States that the two facts above are separate: nothing shows the emulator was built from that checkout. |
run_lock |
The lock path this session took. |
endpoint |
The session's named pipe, unique to the session. |
owner_process, emulator_process |
Process ID and start time of each. |
host_sound |
muted or kept. |
readiness |
observed once the guest wrote its marker. |
request_id_prefixes |
One per client. |
capabilities |
What the server reported. |
debugger_session |
The debugger session's ID. |
writes |
Each guarded write in order: the contract's name, the field, its address (as the address object's repr) and length, the expected hash, the hash of the data the write carried (null when it was not bytes), verified or failed, and the failure, which says how far a failed write got. |
run_failure |
Why the run failed, or null. |
Calls, capabilities and request IDs
session.client and each session.open_diagnostic_client() wrap a client from your factory.
Every call they make carries a request ID from that client's own namespace
(<session>.c<client>.<n>), so a diagnostic client on the same session cannot collide with the
first. A call that needs a capability the server did not report as true raises
CapabilityRefused and is not sent: every debugger call needs debugger, a memory_change
breakpoint needs breakpoints.memory_change, and CPU tracing needs trace.cpu. The wrappers offer
no writes to guest state; session.write makes them, as the next section describes.
Guarded writes
session.write(contract, field, data, expected_sha256) writes to the stopped guest's memory. The
contract is yours: a FieldContract with a name you choose and the WritableFields you support,
each with a name, an address built with the client's MemoryAddress and a length. The package
has no default contract and supports no field on its own, so a write without a contract is
refused. Field layouts and the rules for when a field may be written stay in your restoration.
import hashlib
from dosbox_agent import MemoryAddress
from dinorefurb_dosbox_session import FieldContract, WritableField
contract = FieldContract("startup-state/1", (WritableField("counter", MemoryAddress.segmented(cs, 0x0200), 2),))
session.write(contract, "counter", b"\x21\x43", expected_sha256=hashlib.sha256(b"\x34\x12").hexdigest())
Each write, in this order:
- Refuses a field the contract lacks,
datathat is not bytes, ordataof another length than the field (WriteOutsideContract). Nothing is sent. - Refuses an
expected_sha256that is not 64 hexadecimal digits, and a guest whose status is notstopped(WriteFailed). - Reads the field and refuses unless its bytes hash to
expected_sha256(WriteHashMismatch). Nothing is written. - Sends
memory.writewith the sameexpected_sha256, so the server checks it again. The hashes the server reports for the bytes it replaced and the bytes it left must match the expected hash anddata; when the server wrote but reports replacing other bytes, the field may hold the new bytes (WriteFailed). - Reads the field back and compares it with
data(WriteReadbackMismatch).
It returns a VerifiedWrite and appends it to writes in session.json.
Any refusal or failure, including a transport error during the write, fails the run. The write is
not retried, the failure goes into session.json as run_failure, and from then on further
writes, continue_ and step raise RunFailed without sending anything. pause is still sent,
so a guest that was running when the write failed can be stopped, and reads still work, so the
failed state can be inspected. Closing the session cleans up as usual. Start a new
run to try again.
Observation
session.observe(operation, timeout) waits on one operation. Each poll first checks that the owned
emulator still runs (EmulatorExited if not). When the time runs out the result is pending,
which is neither a failure nor a result: observe the same operation again to keep waiting.
Transport errors from the client propagate. Observation never restarts the guest or sends another
continuation, and while an operation is pending a further continue_ or step raises
OperationPending. A pause ends the continuation it interrupts, so observing either one clears
both. When a continue_ or pause request itself raises, the server may still have received it:
continue_ and step raise OperationPending until client.status() shows the guest
stopped, exited or failed.
Errors
| Error | Raised when |
|---|---|
PlatformRefused |
The platform is not Windows. |
CheckoutRefused |
The checkout is at another revision, has local changes, or cannot be read with git. |
RunDirectoryRefused |
The run directory is not empty, or prepare_drive wrote the readiness marker. |
ConfigurationRefused |
A setting is one the session owns or one known to break the debugger. |
LockHeld |
The run lock is held, or cannot be read. report describes it. |
EmulatorExited |
The owned emulator exited while the session needed it. |
ReadinessNotObserved |
The guest did not write its readiness marker in time. |
CapabilityRefused |
An operation needs a capability the server did not report. Not sent. |
OperationPending |
A continuation was asked for while another operation is pending. |
WriteOutsideContract |
A write names no field in the contract, has no contract, or differs from the field's length. The run fails. |
WriteHashMismatch |
The field's bytes do not hash to the expected value. Nothing was written; the run fails. |
WriteReadbackMismatch |
The field does not hold the written bytes afterwards. The run fails. |
WriteFailed |
A write was refused for another reason, such as a guest that is not stopped. The run fails. The three errors above derive from it. |
RunFailed |
A write, continuation or step was asked for after the run failed. Not sent. |
CleanupFailed |
The emulator still ran after teardown; the lock was kept. |
All of them derive from SessionError.
Native verification
CI runs the tests against a stand-in emulator and a stand-in client, with no DOSBox-X and no game. Before each release that changes process, transport or drive handling, the owner runs this procedure on the pinned revision and puts its output in the pull request:
-
Clone
https://github.com/joncampbell123/dosbox-xat tagdosbox-x-v2026.10.01and check thatgit rev-parse HEADprintsb6abbd5980a885f5f310a4088c59a8688d1b116c. -
Build the
Agent Debug SDL2configuration for x64. With Visual Studio 2019 Build Tools (toolset v142) and Windows SDK 10.0.19041.0:& 'C:/Program Files (x86)/Microsoft Visual Studio/2019/BuildTools/MSBuild/Current/Bin/MSBuild.exe' ` <checkout>/vs/dosbox-x.sln '/p:Configuration=Agent Debug SDL2' /p:Platform=x64 ` /p:PlatformToolset=v142 /p:WindowsTargetPlatformVersion=10.0.19041.0 /m:2 /v:minimal
-
From this directory, with the package installed:
python native/verify_native.py --checkout <checkout> ` --emulator '<checkout>/bin/x64/Agent Debug SDL2/dosbox-x.exe' --run-directory <new directory>
The script generates a synthetic .COM program, starts it in an owned session under the
machine's run lock, sets an execution breakpoint, continues to it, and reads the registers there.
At the breakpoint it makes a guarded write to a word the program loads next, under a contract with
that one field, reads the word back and steps over the load. It passes when the breakpoint stop is
observed, AX and BX hold the values the program set, the write verifies, the readback holds
the new bytes and AX holds the new word after the step. It prints the checks, the checkout
revision, the emulator hash, the reported capabilities and the recorded writes.
Tests
python -m pip install -e packages/dosbox-session
cd packages/dosbox-session && python -B -m unittest discover -s tests -p "test*.py"
The session tests need Windows and git; on another platform they are skipped.
Metadata
Release files for dinorefurb-dosbox-session 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| dinorefurb_dosbox_session-0.2.0.tar.gz | 38.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| dinorefurb_dosbox_session-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 74.0 kB
Release files / dinorefurb_dosbox_session-0.2.0.tar.gz
| Download URL | dinorefurb_dosbox_session-0.2.0.tar.gz |
|---|---|
| Size | 38.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1c6b9caa6485440758d7a813df9901643910055da7948b78034b5f1c02cc4067
|
|
BLAKE2b-256 checksum How to use checksums |
7c23814d7f670526ab3cee8c34bdd938699deef9173df123982436ca58ca0296
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.
Transparency logRelease files / dinorefurb_dosbox_session-0.2.0-py3-none-any.whl
| Download URL | dinorefurb_dosbox_session-0.2.0-py3-none-any.whl |
|---|---|
| Size | 35.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f040ba9e42e76a73e1245e651666cc98b5f9cf04fbd7357c6450997775787cc4
|
|
BLAKE2b-256 checksum How to use checksums |
bbe0299944564ca90536892773866939d65faf6ac5efe7ff11afa5f0835c1021
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 10, 2026.
Transparency log