feat: find RetroBat through the folder it records

RetroBat.exe writes its folder to HKCU\SOFTWARE\RetroBat
LatestKnownInstallPath on every start (retrobat-executable 702da66a,
Program.cs:57 and 99, RegistryTools.cs:15-50). Only C:\RetroBat and the
profile folder were looked at, so an install on D: was missed and a forced
--platform retrobat wrote into C:\RetroBat\bios. The value is read
through winreg, or reg.exe under WSL, before the fixed folders.
This commit is contained in:
Abdessamad Derraz committed 2026-10-21 05:28:35 +02:00
1 parent 4f20b5daaf
commit cdb2cfa6f0
6 files changed
+117 -2

No files matched your search

+1 -1
View File
@@ -11,7 +11,7 @@
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "caec72e5ebd146509e3b1fabc7d8d4c5c921567592d6d1581df6e5cc50f3e96f"
$defaultInstallSha256 = "f05f862fcebc16e99d692a20a09163665828ea0383643fcc96a41b11d77fcff7"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+59
View File
@@ -1092,6 +1092,64 @@ def _detect_by_executable(os_type: str, found: list[tuple[str, Path]]) -> None:
found.append((platform_name, newest.parent / subdir))
# The registry's registry rules: (os, platform, key, value, BIOS folder under
# the folder the value names). RetroBat.exe writes its own folder there on
# every start (retrobat-executable 702da66a, RetroBat/RetroBat/Program.cs:57
# and 99, RegistryTools.cs:15-50), wherever it was installed. The path rules
# only knew C:\RetroBat and the profile folder.
_REGISTRY_RULES: tuple[tuple[str, str, str, str, str], ...] = (
("windows", "retrobat", "HKCU\\SOFTWARE\\RetroBat", "LatestKnownInstallPath", "bios"),
)
def _read_registry(key: str, value: str) -> str | None:
"""A string value of the Windows registry, read natively or, under WSL,
through reg.exe. None when the key, the value or the means is missing."""
if os.name == "nt":
import winreg # noqa: PLC0415
hive, _, subkey = key.partition("\\")
roots = {"HKCU": winreg.HKEY_CURRENT_USER, "HKLM": winreg.HKEY_LOCAL_MACHINE}
try:
with winreg.OpenKey(roots[hive], subkey) as handle:
data, _kind = winreg.QueryValueEx(handle, value)
except (OSError, KeyError):
return None
return data.strip() if isinstance(data, str) and data.strip() else None
if not _WSL["active"]:
return None
try:
result = subprocess.run(
["reg.exe", "query", key, "/v", value],
capture_output=True, text=True, errors="replace", timeout=10,
stdin=subprocess.DEVNULL, cwd="/mnt/c" if Path("/mnt/c").is_dir() else None,
)
except (OSError, subprocess.SubprocessError):
return None
match = re.search(
rf"^\s*{re.escape(value)}\s+REG_(?:EXPAND_)?SZ\s+(.+?)\s*$", result.stdout, re.M
)
return match.group(1) if match else None
def _detect_by_registry(os_type: str, found: list[tuple[str, Path]]) -> None:
"""Add a platform whose own program names its folder in the registry.
A folder since moved or removed is passed over, so the path rules still
get their turn.
"""
systems = ("windows",) if os_type in ("windows", "wsl") else ()
for rule_os, platform_name, key, value, subdir in _REGISTRY_RULES:
if rule_os not in systems or any(name == platform_name for name, _ in found):
continue
folder = _read_registry(key, value)
if not folder:
continue
base = _host_path(folder.rstrip("\\/") or folder)
if base.is_dir():
found.append((platform_name, base / subdir))
def _detect_by_path(os_type: str, found: list[tuple[str, Path]]) -> None:
"""Add what a registry path rule finds, for a platform not found yet."""
systems = ("linux", "windows") if os_type == "wsl" else (os_type,)
@@ -1239,6 +1297,7 @@ def detect_platforms(os_type: str) -> list[tuple[str, Path]]:
found.append(("bizhawk", bizhawk))
_detect_by_executable(os_type, found)
_detect_by_registry(os_type, found)
_detect_by_path(os_type, found)
return found
+1 -1
View File
@@ -18,7 +18,7 @@ esac
TEMP_INSTALLER=""
TEMP_DIRECTORY=""
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
DEFAULT_INSTALL_SHA256="caec72e5ebd146509e3b1fabc7d8d4c5c921567592d6d1581df6e5cc50f3e96f"
DEFAULT_INSTALL_SHA256="f05f862fcebc16e99d692a20a09163665828ea0383643fcc96a41b11d77fcff7"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+8
View File
@@ -351,6 +351,14 @@ platforms:
target_source: null
install:
detect:
# RetroBat.exe writes its own folder there on every start, wherever it
# was installed (retrobat-executable 702da66a,
# RetroBat/RetroBat/Program.cs:57 and 99, RegistryTools.cs:15-50).
- os: windows
method: registry
key: 'HKCU\SOFTWARE\RetroBat'
value: LatestKnownInstallPath
bios_subdir: bios
# installer.iss: DefaultDirName=C:\RetroBat, the first place to look.
- os: windows
method: path_exists
+47
View File
@@ -3800,5 +3800,52 @@ class ANetworkFailureNamesTheNetwork(unittest.TestCase):
self.assertNotIn("being written to", text)
class RetroBatIsFoundWhereItSaysItIs(unittest.TestCase):
"""RetroBat on D:\\RetroBat was not detected, and --platform retrobat
wrote 7.6 GB into C:\\RetroBat\\bios, a folder it does not read.
RetroBat.exe writes its folder to the registry on every start."""
def test_the_table_is_the_registry(self):
import yaml # noqa: PLC0415
registry = yaml.safe_load((REPO_ROOT / "platforms" / "_registry.yml").read_text())
rules = {
(rule["os"], name, rule["key"], rule["value"], rule["bios_subdir"])
for name, entry in registry["platforms"].items()
for rule in (entry.get("install") or {}).get("detect", [])
if rule["method"] == "registry"
}
self.assertEqual(set(install._REGISTRY_RULES), rules)
def test_the_registry_folder_wins_over_the_default(self):
folder = Path(_tempdir(self))
with unittest.mock.patch.object(install, "_read_registry",
return_value=str(folder) + "\\"):
found: list = []
install._detect_by_registry("windows", found)
self.assertEqual(found, [("retrobat", folder / "bios")])
def test_a_folder_since_removed_is_passed_over(self):
gone = Path(_tempdir(self)) / "moved"
with unittest.mock.patch.object(install, "_read_registry", return_value=str(gone)):
found: list = []
install._detect_by_registry("windows", found)
self.assertEqual(found, [])
def test_wsl_reads_it_through_reg_exe(self):
output = ("\nHKEY_CURRENT_USER\\SOFTWARE\\RetroBat\n"
" LatestKnownInstallPath REG_SZ D:\\Games\\RetroBat\\\n\n")
with unittest.mock.patch.dict(install._WSL, {"active": True, "root": "/mnt"}), \
unittest.mock.patch.object(install.os, "name", "posix"), \
unittest.mock.patch.object(
install.subprocess, "run",
return_value=SimpleNamespace(stdout=output, returncode=0)) as run:
value = install._read_registry("HKCU\\SOFTWARE\\RetroBat", "LatestKnownInstallPath")
self.assertEqual(value, "D:\\Games\\RetroBat\\")
self.assertEqual(install._host_path(value.rstrip("\\")),
Path("/mnt/d/Games/RetroBat"))
self.assertEqual(run.call_args[0][0][:2], ["reg.exe", "query"])
if __name__ == "__main__":
unittest.main()
+1
View File
@@ -318,6 +318,7 @@ WSL does not pass them. A forced `--platform retrobat` falls back to
| Evidence | Platform | BIOS directory |
|----------|----------|----------------|
| `HKCU\SOFTWARE\RetroBat` `LatestKnownInstallPath`, which `RetroBat.exe` writes on every start (read through `reg.exe` under WSL) | RetroBat | that folder + `\bios`, before the fixed folders above |
| `~/.config/EmuDeck/settings.sh` | EmuDeck | `emulationPath` + `/bios` |
| `%APPDATA%\EmuDeck\settings.ps1` | EmuDeck | `$emulationPath` + `\bios` |
| `~/.var/app/net.retrodeck.retrodeck/config/retrodeck/retrodeck.json` | RetroDECK | `paths.rd_home_path`, falling back to the pre-migration `retrodeck.cfg` and its `rdhome` |