Ryan Malloy 6805905287 Rename package from dosbox-mcp to mcdosbox-x
- Rename src/dosbox_mcp/ to src/mcdosbox_x/
- Update pyproject.toml: package name, entry point, build paths
- Update all internal imports
- Add fonts_list() MCP tool for font discovery
- Register logging and network tools in server.py
2026-01-28 12:34:03 -07:00

254 lines
6.4 KiB
Python

"""Control tools: pause, resume, reset, savestate, loadstate."""
from typing import Any
from .peripheral import _get_qmp_port, _qmp_command
def pause() -> dict:
"""Pause DOSBox-X emulation.
Stops the emulator at the current instruction. Use resume() to continue.
This is different from GDB breakpoints - it's an immediate halt.
Returns:
Pause status
"""
result = _qmp_command("localhost", _get_qmp_port(), "stop")
if "error" in result:
return {
"success": False,
"error": result.get("error", "Unknown error"),
}
return {
"success": True,
"message": "Emulator paused",
}
def resume() -> dict:
"""Resume DOSBox-X emulation after pause.
Continues emulator execution after a pause() call.
Returns:
Resume status
"""
result = _qmp_command("localhost", _get_qmp_port(), "cont")
if "error" in result:
return {
"success": False,
"error": result.get("error", "Unknown error"),
}
return {
"success": True,
"message": "Emulator resumed",
}
def reset(dos_only: bool = False) -> dict:
"""Reset DOSBox-X system.
Args:
dos_only: If True, only reset DOS (warm boot). If False, full system reset.
Returns:
Reset status
"""
args = {}
if dos_only:
args["dos"] = True
result = _qmp_command("localhost", _get_qmp_port(), "system_reset", args if args else None)
if "error" in result:
return {
"success": False,
"error": result.get("error", "Unknown error"),
}
return {
"success": True,
"message": "DOS reset" if dos_only else "System reset",
"reset_type": "dos" if dos_only else "full",
}
def savestate(filename: str) -> dict:
"""Save DOSBox-X emulator state to file.
Creates a snapshot of the entire emulator state that can be loaded later.
Useful for creating checkpoints during reverse engineering.
Args:
filename: Path to save state file (relative to DOSBox working dir)
Returns:
Save status with file path
Example:
savestate("checkpoint1.sav")
"""
result = _qmp_command(
"localhost", _get_qmp_port(), "savestate", {"file": filename}, timeout=30.0
)
if "error" in result:
return {
"success": False,
"error": result.get("error", "Unknown error"),
}
response = {
"success": True,
"message": f"State saved to {filename}",
"filename": filename,
}
# Include any return data from QMP
if "return" in result:
ret = result["return"]
if isinstance(ret, dict):
response.update(ret)
return response
def loadstate(filename: str) -> dict:
"""Load DOSBox-X emulator state from file.
Restores a previously saved snapshot. The emulator will be in exactly
the same state as when the savestate was created.
Args:
filename: Path to state file to load
Returns:
Load status
Example:
loadstate("checkpoint1.sav")
"""
result = _qmp_command(
"localhost", _get_qmp_port(), "loadstate", {"file": filename}, timeout=30.0
)
if "error" in result:
return {
"success": False,
"error": result.get("error", "Unknown error"),
}
response = {
"success": True,
"message": f"State loaded from {filename}",
"filename": filename,
}
if "return" in result:
ret = result["return"]
if isinstance(ret, dict):
response.update(ret)
return response
def memdump(address: str, length: int, filename: str | None = None) -> dict:
"""Dump memory region to file or return as data.
Uses QMP memdump for efficient binary memory extraction.
More efficient than GDB memory_read for large regions.
Args:
address: Start address in hex format (e.g., "0x0000", "1234:5678")
length: Number of bytes to dump
filename: Optional file to save dump (returns base64 if not specified)
Returns:
Memory dump result with either file path or base64 data
Example:
memdump("0xB8000", 4000) # Dump VGA text buffer
memdump("CS:IP", 256, "code_dump.bin")
"""
# Parse segment:offset if provided
if ":" in address:
seg, off = address.split(":")
flat_addr = (int(seg, 16) << 4) + int(off, 16)
elif address.startswith("0x"):
flat_addr = int(address, 16)
else:
flat_addr = int(address, 16)
args: dict[str, Any] = {
"address": flat_addr,
"length": length,
}
if filename:
args["filename"] = filename
result = _qmp_command("localhost", _get_qmp_port(), "memdump", args, timeout=30.0)
if "error" in result:
return {
"success": False,
"error": result.get("error", "Unknown error"),
}
response = {
"success": True,
"address": f"0x{flat_addr:X}",
"length": length,
}
if "return" in result:
ret = result["return"]
if isinstance(ret, dict):
if "file" in ret:
response["filename"] = ret["file"]
response["message"] = f"Memory dumped to {ret['file']}"
if "data" in ret:
# Return base64 data
response["data_base64"] = ret["data"]
response["message"] = f"Dumped {length} bytes"
return response
def query_status() -> dict:
"""Query detailed DOSBox-X emulator status via QMP.
Returns more detailed status than the GDB-based status() tool,
including pause state and debugger activity.
Returns:
Detailed emulator status
"""
result = _qmp_command("localhost", _get_qmp_port(), "query-status")
if "error" in result:
return {
"success": False,
"error": result.get("error", "Unknown error"),
}
response = {
"success": True,
}
if "return" in result:
ret = result["return"]
if isinstance(ret, dict):
response["running"] = ret.get("running", False)
response["paused"] = not ret.get("running", False)
response["debugger_active"] = ret.get("debugger", False)
if "reason" in ret:
response["pause_reason"] = ret["reason"]
return response