From 2e65efcfa7fec1a4be5a2548fb5bd9311de13680 Mon Sep 17 00:00:00 2001 From: Tjebbe Kerstens Date: Wed, 14 Oct 2026 19:52:07 +0200 Subject: [PATCH] fix: seed redist markers again when the registry lost them (#793) * fix: redist markers a replaced registry lost are seeded again droiddeck-seed-redists recorded what it seeded in .droiddeck-redists and trusted that record from then on. A Proton update replaces its default_pfx system.reg but leaves our record beside it, and every new prefix copies both, so new games got a record of markers their registry never had: Steam then ran their install scripts for real. Batman: Arkham City's AMD Dual-Core Optimizer installer hangs there, leaving the game on "Running install script" on every launch. Each recorded marker is now looked up in the registry, in both views where both were written, and seeded again when it is gone; a record of markers nobody wants any more is rewritten without them. * Check every VC runtime architecture before keeping the vcrt marker vcrt writes Installed for x86, x64 and arm64 in both registry views, but present() only looked at x86. If only an x64 or arm64 value was gone the runtime would not be seeded again. Require all six values and test the loss of the arm64 Wow6432Node one. --- .../usr/local/bin/droiddeck-seed-redists | 56 +++++++++++++++++++ tools/tests/test_seed_redists.py | 47 ++++++++++++++++ 2 files changed, 103 insertions(+) diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists b/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists index e6a8ecb4d..5b314d71b 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-seed-redists @@ -139,6 +139,51 @@ unseed() { return 1 } +# The ledger lines whose values the registry still holds, in both views where we wrote both. A Proton +# update replaces its default_pfx system.reg but not our ledger beside it, and new prefixes copy both. +present() { awk -F'\t' ' + NR == FNR { + path = $1 + if (path != "vcrt") { + sub(/^HKEY_LOCAL_MACHINE\\\\/, "", path) + if (path == $1) + next + } + n++ + line[n] = $0 + need[n] = 0 + if (path == "vcrt") { + for (a = split("x86 x64 arm64", arch, " "); a > 0; a--) { + runtime = "microsoft\\\\visualstudio\\\\14.0\\\\vc\\\\runtimes\\\\" arch[a] + key[n, ++need[n]] = "[software\\\\" runtime "]" SUBSEP "installed" + key[n, ++need[n]] = "[software\\\\wow6432node\\\\" runtime "]" SUBSEP "installed" + } + } else { + both = substr(path, 1, 10) == "Software\\\\" + path = tolower(path) + key[n, ++need[n]] = "[" path "]" SUBSEP tolower($2) + if (both) + key[n, ++need[n]] = "[software\\\\wow6432node\\\\" substr(path, 11) "]" SUBSEP tolower($2) + } + for (j = 1; j <= need[n]; j++) wanted[key[n, j]] = 1 + next + } + /^\[/ { section = tolower(substr($0, 1, index($0, "]"))) } + /^"/ { + value = substr($0, 2) + value = tolower(substr(value, 1, index(value, "\"=") - 1)) + if ((section SUBSEP value) in wanted) + seen[section SUBSEP value] = 1 + } + END { + for (i = 1; i <= n; i++) { + ok = 1 + for (j = 1; j <= need[i]; j++) if (!(key[i, j] in seen)) ok = 0 + if (ok) print line[i] + } + } +' - "$1"; } + for reg in "$steam_root"/steamapps/common/Proton*/files/share/default_pfx*/system.reg \ /mnt/droiddeck-sd/steamapps/common/Proton*/files/share/default_pfx*/system.reg \ /root/Games/*/steamapps/common/Proton*/files/share/default_pfx*/system.reg \ @@ -166,6 +211,17 @@ for reg in "$steam_root"/steamapps/common/Proton*/files/share/default_pfx*/syste printf '%s\n' "${kept[@]}" >"$seeded" echo "== took Vulkan runtime markers out of $reg" fi + if [ ${#had[@]} -gt 0 ]; then + declare -A held=() + while IFS= read -r line; do [[ $line == *$'\t'* ]] && held[$line]=1; done < <(printf '%s\n' "${!had[@]}" | present "$reg") + if [ ${#held[@]} -lt ${#had[@]} ]; then + echo "== $((${#had[@]} - ${#held[@]})) recorded redist markers are gone from $reg" + had=() + for line in "${!held[@]}"; do had[$line]=1; done + printf '%s\n' "${!had[@]}" >"$seeded" + fi + unset held + fi missing=() for line in "${want[@]}"; do [ -n "${had[$line]:-}" ] || missing+=("$line"); done [ ${#missing[@]} -eq 0 ] && continue diff --git a/tools/tests/test_seed_redists.py b/tools/tests/test_seed_redists.py index 248e3f75a..e23672318 100644 --- a/tools/tests/test_seed_redists.py +++ b/tools/tests/test_seed_redists.py @@ -100,6 +100,53 @@ class SeedRedistsTest(unittest.TestCase): self.run_seed() self.assertIn('"VCRedist2022"=dword:00000001', self.reg.read_text()) + def test_markers_a_replaced_registry_lost_are_seeded_again(self): + self.game() + stock = self.reg.read_text() + self.run_seed() + ledger = (self.reg.parent / ".droiddeck-redists").read_text() + self.assertIn("VCRedist2022", ledger) + self.reg.write_text(stock) + self.run_seed() + reg = self.reg.read_text() + self.assertEqual(reg.count('"VCRedist2022"=dword:00000001'), 2, "both views again") + self.assertIn("[Software\\\\Microsoft\\\\VisualStudio\\\\14.0\\\\VC\\\\Runtimes\\\\x86]", reg) + self.assertEqual(sorted((self.reg.parent / ".droiddeck-redists").read_text().splitlines()), sorted(ledger.splitlines())) + self.run_seed() + self.assertEqual(reg, self.reg.read_text()) + + def test_a_marker_kept_in_only_one_view_is_seeded_again(self): + self.game() + self.run_seed() + wow = "[Software\\\\Wow6432Node\\\\Valve\\\\Steam\\\\Apps\\\\CommonRedist\\\\vcredist\\\\2022]" + blocks = self.reg.read_text().split("\n\n") + self.reg.write_text("\n\n".join(b for b in blocks if not (b.startswith(wow) and '"VCRedist2022"' in b))) + self.assertEqual(self.reg.read_text().count('"VCRedist2022"'), 1) + self.run_seed() + reg = self.reg.read_text() + self.assertEqual(reg.count('"VCRedist2022"'), 3, "the one left plus both views again") + self.assertTrue(any(b.startswith(wow) and '"VCRedist2022"' in b for b in reg.split("\n\n"))) + + def test_a_vc_runtime_another_architecture_lost_is_seeded_again(self): + self.run_seed() + arm64 = "[Software\\\\Wow6432Node\\\\Microsoft\\\\VisualStudio\\\\14.0\\\\VC\\\\Runtimes\\\\arm64]" + blocks = self.reg.read_text().split("\n\n") + self.reg.write_text("\n\n".join(b for b in blocks if not b.startswith(arm64))) + self.assertNotIn(arm64, self.reg.read_text()) + self.run_seed() + reg = self.reg.read_text() + self.assertIn(arm64, reg) + self.assertEqual(reg.count("[Software\\\\Microsoft\\\\VisualStudio\\\\14.0\\\\VC\\\\Runtimes\\\\x86]"), 2) + + def test_a_marker_written_in_one_view_only_is_kept_as_it_is(self): + self.game(script=GAME_SCRIPT.replace("Software\\\\Valve\\\\Steam\\\\Apps\\\\CommonRedist\\\\vcredist\\\\2022", "SOFTWARE\\\\AGEIA Technologies").replace("VCRedist2022", "PhysX Version")) + self.run_seed() + once = self.reg.read_text() + self.assertEqual(once.count('"PhysX Version"=dword:00000001'), 1) + result = self.run_seed() + self.assertEqual(once, self.reg.read_text()) + self.assertNotIn("gone", result.stdout) + def test_an_evaluator_script_nested_deeper_is_read(self): script = self.tmp / "legacycompat/evaluatorscript_409710.vdf" script.parent.mkdir(parents=True)