Skip to content

Commit 2ab6fe0

Browse files
committed
Further Error U01 fixes after 59428cf & 575040e.
This is first and foremost important for 1.6 as we need a reliable aotu-updater when the time to release comes; the class of U01 root causes being addressed here happens right after update from upcache.
1 parent 2b01718 commit 2ab6fe0

6 files changed

Lines changed: 267 additions & 65 deletions

File tree

Client/loader/CInstallManager.cpp

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -304,17 +304,28 @@ void CInstallManager::InitSequencer()
304304
//////////////////////////////////////////////////////////
305305
void CInstallManager::SetMTASAPathSource(const SString& strCommandLineIn)
306306
{
307-
// Update some settings which are used by ReportLog
308-
UpdateSettingsForReportLog();
309-
310307
InitSequencer();
311308
RestoreSequencerFromSnapshot(strCommandLineIn);
312309

313310
// If command line says we're not running from the launch directory, get the launch directory location from the registry
314311
if (m_pSequencer->GetVariable(INSTALL_LOCATION) == "far")
312+
{
313+
// Process B carried the real install root through the sequencer state. Honor it here so this
314+
// temp launcher process can resolve its base dir without depending only on the registry.
315+
// The override must be installed before UpdateSettingsForReportLog runs, because that helper
316+
// reaches GetInstallPathForLauncher / GetMTASAPath through UpdateMTAVersionApplicationSetting.
317+
const SString strRealInstallRoot = m_pSequencer->GetVariable(INSTALL_ROOT);
318+
if (!strRealInstallRoot.empty() && IsUsableMtasaInstallRoot(strRealInstallRoot) && !IsTemporaryUpdateLaunchPath(strRealInstallRoot))
319+
SharedUtil::SetMTASABaseDirOverride(strRealInstallRoot);
320+
315321
::SetMTASAPathSource(true);
322+
}
316323
else
317324
::SetMTASAPathSource(false);
325+
326+
// Update some settings which are used by ReportLog. Must run after the path source is resolved
327+
// so that report-log helpers see the real install root rather than a temp extraction directory.
328+
UpdateSettingsForReportLog();
318329
}
319330

320331
//////////////////////////////////////////////////////////
@@ -1026,6 +1037,11 @@ SString CInstallManager::_MaybeSwitchToTempExe()
10261037
// If a new "Multi Theft Auto.exe" exists, let that complete the install
10271038
if (m_pSequencer->GetVariable(INSTALL_LOCATION) == "far")
10281039
{
1040+
// Carry the current real install root forward to the temp launcher so it can set a base-dir
1041+
// override before reading the registry. Without this, the temp launcher has only Last Run
1042+
// Location to fall back on, which can be missing, stale, or denied by HKLM write failures.
1043+
m_pSequencer->SetVariable(INSTALL_ROOT, GetMTASAPath());
1044+
10291045
ReleaseSingleInstanceMutex();
10301046
if (ShellExecuteNonBlocking("open", GetLauncherPathFilename(), GetSequencerSnapshot()))
10311047
ExitProcess(0); // All done here
@@ -1049,8 +1065,13 @@ SString CInstallManager::_SwitchBackFromTempExe()
10491065
{
10501066
m_pSequencer->SetVariable(INSTALL_LOCATION, "near");
10511067

1052-
SString strLauncherPathFilename = PathJoin(GetInstallPathForLauncher(), MTA_EXE_NAME);
1053-
if (!FileExists(strLauncherPathFilename))
1068+
// GetInstallPathForLauncher returns empty when running from a temp update directory and no
1069+
// usable non-temp install root could be resolved. PathJoin would then produce a root-relative
1070+
// probe like "\Multi Theft Auto.exe", which could match an unrelated file on the current drive
1071+
// and cause us to launch the wrong executable. Skip straight to the GetMTASAPath() fallback.
1072+
const SString strRealInstallRoot = GetInstallPathForLauncher();
1073+
SString strLauncherPathFilename = strRealInstallRoot.empty() ? SString() : PathJoin(strRealInstallRoot, MTA_EXE_NAME);
1074+
if (strLauncherPathFilename.empty() || !FileExists(strLauncherPathFilename))
10541075
{
10551076
strLauncherPathFilename = PathJoin(GetMTASAPath(), MTA_EXE_NAME);
10561077
if (!FileExists(strLauncherPathFilename))
@@ -1707,7 +1728,10 @@ SString CInstallManager::_ProcessAppCompatChecks()
17071728
IsWow64Process(GetCurrentProcess(), &bIsWOW64);
17081729
uint uiHKLMFlags = bIsWOW64 ? KEY_WOW64_64KEY : 0;
17091730
WString strGTAExePathFilename = GetGameExecutablePath().wstring();
1710-
WString strMTAExePathFilename = FromUTF8(PathJoin(GetInstallPathForLauncher(), MTA_EXE_NAME));
1731+
SString strMTAInstallPath = GetInstallPathForLauncher();
1732+
if (strMTAInstallPath.empty())
1733+
strMTAInstallPath = GetMTASAPath();
1734+
WString strMTAExePathFilename = FromUTF8(PathJoin(strMTAInstallPath, MTA_EXE_NAME));
17111735
WString strCompatModeRegKey = L"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\AppCompatFlags\\Layers";
17121736
int bWin816BitColorOption = GetApplicationSettingInt("Win8Color16");
17131737
int bWin8MouseOption = GetApplicationSettingInt("Win8MouseFix");

Client/loader/CInstallManager.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
// Common command line keys
1616
#define INSTALL_STAGE "install_stage"
1717
#define INSTALL_LOCATION "install_loc"
18+
#define INSTALL_ROOT "install_root"
1819
#define HIDE_PROGRESS "hide_prog"
1920

2021
typedef CBadLang<class CInstallManager> CSequencerType;

Client/loader/MainFunctions.cpp

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -461,9 +461,17 @@ void InitLocalization(bool bShowErrors)
461461
return;
462462

463463
const SString strLaunchPath = GetLaunchPath();
464+
const SString strMTASAPath = GetMTASAPath();
465+
466+
// Probe the install root first and only fall back to the launch path. In the auto-update flow,
467+
// Process C runs from a temp directory under \upcache\_*_tmp_*\ which does not contain mta\core.dll;
468+
// the real install root is carried into g_strMTASAPath via the install manager's SetMTASABaseDirOverride
469+
// path, so GetMTASAPath returns it even though GetLaunchPath still points at the temp tree. On a normal
470+
// non-temp launch the two paths are equal and the first probe succeeds.
471+
SString strCoreDLL = PathJoin(strMTASAPath, "mta", MTA_DLL_NAME);
472+
if (!FileExists(strCoreDLL))
473+
strCoreDLL = PathJoin(strLaunchPath, "mta", MTA_DLL_NAME);
464474

465-
// Check for core.dll
466-
const SString strCoreDLL = PathJoin(strLaunchPath, "mta", MTA_DLL_NAME);
467475
if (!FileExists(strCoreDLL))
468476
{
469477
if (!bShowErrors)
@@ -474,7 +482,6 @@ void InitLocalization(bool bShowErrors)
474482
}
475483

476484
// Setup DLL search paths
477-
const SString strMTASAPath = GetMTASAPath();
478485
SetDllDirectory(PathJoin(strMTASAPath, "mta"));
479486

480487
// See if xinput is loadable (XInput9_1_0.dll or xinput1_3.dll)
@@ -817,7 +824,9 @@ void ConfigureWerDumpPath()
817824
{
818825
using WerRegisterAppLocalDumpFn = HRESULT(WINAPI*)(PCWSTR);
819826

820-
const SString strInstallPath = GetInstallPathForLauncher();
827+
SString strInstallPath = GetInstallPathForLauncher();
828+
if (strInstallPath.empty())
829+
strInstallPath = GetMTASAPath();
821830
if (strInstallPath.empty())
822831
return;
823832

Client/loader/Utils.cpp

Lines changed: 144 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -466,27 +466,6 @@ namespace
466466
return PathJoin(GetProductRegistryPath(), GetMajorVersionString(), strPath).TrimEnd("\\");
467467
}
468468

469-
bool IsTemporaryUpdateLaunchPath(const SString& strLaunchPath)
470-
{
471-
if (strLaunchPath.empty())
472-
return false;
473-
474-
if (!strLaunchPath.ContainsI("\\upcache\\"))
475-
return false;
476-
477-
return ExtractFilename(strLaunchPath).ContainsI("_tmp_");
478-
}
479-
480-
bool IsUsableMtasaInstallRoot(const SString& strPath)
481-
{
482-
if (strPath.empty())
483-
return false;
484-
485-
return FileExists(PathJoin(strPath, "Multi Theft Auto.exe")) || FileExists(PathJoin(strPath, "Multi Theft Auto_d.exe")) ||
486-
FileExists(PathJoin(strPath, "mta", "core.dll")) || FileExists(PathJoin(strPath, "MTA", "core.dll")) ||
487-
FileExists(PathJoin(strPath, "mta", "core_d.dll")) || FileExists(PathJoin(strPath, "MTA", "core_d.dll"));
488-
}
489-
490469
bool HasDistinct64BitRegistryView()
491470
{
492471
#if defined(_WIN64)
@@ -580,6 +559,62 @@ namespace
580559
static_cast<DWORD>((wstrValue.length() + 1) * sizeof(wchar_t)));
581560
RegCloseKey(hKey);
582561
}
562+
563+
// Read Last Run Location from a specific registry view. viewFlag should be one of
564+
// KEY_WOW64_64KEY, KEY_WOW64_32KEY, or 0 (no view override). Returns empty string on any failure.
565+
// Oversized values are rejected without allocation so a malformed registry entry on the U01
566+
// recovery path cannot turn into a large allocation; the same 1 MB cap is used by the generic
567+
// ReadRegistryStringValue helper in SharedUtil.Misc.hpp.
568+
SString ReadInstallRootRegistryView(REGSAM viewFlag)
569+
{
570+
constexpr DWORD kMaxRegistryValueBytes = 1024u * 1024u;
571+
572+
const WString wstrSubKey = FromUTF8(MakeCurrentVersionRegistryPath(""));
573+
const WString wstrValueName = FromUTF8("Last Run Location");
574+
575+
HKEY hKey = nullptr;
576+
if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, wstrSubKey.c_str(), 0, KEY_READ | viewFlag, &hKey) != ERROR_SUCCESS || !hKey)
577+
return "";
578+
579+
DWORD valueType = REG_SZ;
580+
DWORD valueSize = 0;
581+
LONG result = RegQueryValueExW(hKey, wstrValueName.c_str(), nullptr, &valueType, nullptr, &valueSize);
582+
if (result != ERROR_SUCCESS || (valueType != REG_SZ && valueType != REG_EXPAND_SZ) || valueSize == 0 || valueSize > kMaxRegistryValueBytes)
583+
{
584+
RegCloseKey(hKey);
585+
return "";
586+
}
587+
588+
std::vector<wchar_t> buffer((valueSize / sizeof(wchar_t)) + 1u, L'\0');
589+
result = RegQueryValueExW(hKey, wstrValueName.c_str(), nullptr, &valueType, reinterpret_cast<LPBYTE>(buffer.data()), &valueSize);
590+
RegCloseKey(hKey);
591+
592+
if (result != ERROR_SUCCESS || (valueType != REG_SZ && valueType != REG_EXPAND_SZ) || valueSize > kMaxRegistryValueBytes)
593+
return "";
594+
595+
if (valueSize >= sizeof(wchar_t))
596+
buffer[(valueSize / sizeof(wchar_t)) - 1u] = L'\0';
597+
else
598+
buffer[0] = L'\0';
599+
600+
// Expand environment variable references for REG_EXPAND_SZ values so callers see a real
601+
// filesystem path. Without this, a value like "%ProgramFiles%\..." would fail validation.
602+
// The expansion is bounded by the same 1 MB cap so a value containing a self-referential or
603+
// explosive expansion cannot trigger a large allocation here.
604+
if (valueType == REG_EXPAND_SZ)
605+
{
606+
constexpr DWORD kMaxExpandChars = kMaxRegistryValueBytes / sizeof(wchar_t);
607+
const DWORD expandedChars = ExpandEnvironmentStringsW(buffer.data(), nullptr, 0);
608+
if (expandedChars > 0 && expandedChars <= kMaxExpandChars)
609+
{
610+
std::vector<wchar_t> expanded(expandedChars, L'\0');
611+
if (ExpandEnvironmentStringsW(buffer.data(), expanded.data(), expandedChars))
612+
return ToUTF8(expanded.data());
613+
}
614+
}
615+
616+
return ToUTF8(buffer.data());
617+
}
583618
}
584619

585620
SString GetInstallPathForLauncher()
@@ -588,15 +623,63 @@ SString GetInstallPathForLauncher()
588623
if (!IsTemporaryUpdateLaunchPath(strLaunchPath))
589624
return strLaunchPath;
590625

591-
const SString strSavedInstallPath = GetRegistryValue("", "Last Run Location");
592-
if (IsUsableMtasaInstallRoot(strSavedInstallPath) && !IsTemporaryUpdateLaunchPath(strSavedInstallPath))
593-
return strSavedInstallPath;
626+
// Prefer the resolved base dir over the registry when one was already established. In the
627+
// far-update Process C, CInstallManager applies SetMTASABaseDirOverride from the sequencer
628+
// carried INSTALL_ROOT and runs ::SetMTASAPathSource(true) before any consumer reaches here,
629+
// so g_strMTASAPath holds the validated real install root that Process B was updating. A stale
630+
// or differently-installed registry "Last Run Location" must not win over that carried root and
631+
// aim consumers at a different MTA install. Read g_strMTASAPath directly rather than calling
632+
// GetMTASAPath(), because GetMTASAPath() lazy-inits via SetMTASAPathSource(false), which calls
633+
// back into GetInstallPathForLauncher() and would recurse for any temp launcher whose g_strMTASAPath
634+
// has not yet been populated by the far-update sequencer path. Empty string falls through to the
635+
// per-view registry loop below, preserving the temp-launcher-without-far-update fallback.
636+
if (!g_strMTASAPath.empty() && IsUsableMtasaInstallRoot(g_strMTASAPath) && !IsTemporaryUpdateLaunchPath(g_strMTASAPath) &&
637+
!g_strMTASAPath.CompareI(strLaunchPath))
638+
return g_strMTASAPath;
639+
640+
// Read each registry view independently so a stale value in one view cannot shadow a usable value in another.
641+
REGSAM viewFlags[3] = {0, 0, 0};
642+
int viewCount = 0;
643+
#if defined(KEY_WOW64_64KEY)
644+
viewFlags[viewCount++] = KEY_WOW64_64KEY;
645+
#endif
646+
#if defined(KEY_WOW64_32KEY)
647+
viewFlags[viewCount++] = KEY_WOW64_32KEY;
648+
#endif
649+
viewFlags[viewCount++] = 0;
650+
651+
for (int i = 0; i < viewCount; ++i)
652+
{
653+
const SString strSavedInstallPath = ReadInstallRootRegistryView(viewFlags[i]);
654+
if (IsUsableMtasaInstallRoot(strSavedInstallPath) && !IsTemporaryUpdateLaunchPath(strSavedInstallPath))
655+
return strSavedInstallPath;
656+
}
594657

595-
const SString strSavedInstallPath64 = GetRegistryValue64("", "Last Run Location");
596-
if (IsUsableMtasaInstallRoot(strSavedInstallPath64) && !IsTemporaryUpdateLaunchPath(strSavedInstallPath64))
597-
return strSavedInstallPath64;
658+
// No usable non-temp install root resolved. Returning empty forces callers to use their own fallbacks
659+
// (such as a base-dir override carried forward from the parent launcher) instead of treating a temp
660+
// update directory as the real install location.
661+
return SString();
662+
}
598663

599-
return strLaunchPath;
664+
bool IsUsableMtasaInstallRoot(const SString& strPath)
665+
{
666+
if (strPath.empty())
667+
return false;
668+
669+
return FileExists(PathJoin(strPath, "Multi Theft Auto.exe")) || FileExists(PathJoin(strPath, "Multi Theft Auto_d.exe")) ||
670+
FileExists(PathJoin(strPath, "mta", "core.dll")) || FileExists(PathJoin(strPath, "MTA", "core.dll")) ||
671+
FileExists(PathJoin(strPath, "mta", "core_d.dll")) || FileExists(PathJoin(strPath, "MTA", "core_d.dll"));
672+
}
673+
674+
bool IsTemporaryUpdateLaunchPath(const SString& strLaunchPath)
675+
{
676+
if (strLaunchPath.empty())
677+
return false;
678+
679+
if (!strLaunchPath.ContainsI("\\upcache\\"))
680+
return false;
681+
682+
return ExtractFilename(strLaunchPath).ContainsI("_tmp_");
600683
}
601684

602685
void SetMTASAPathSource(bool bReadFromRegistry)
@@ -612,6 +695,14 @@ void SetMTASAPathSource(bool bReadFromRegistry)
612695
SString strLaunchPath = GetLaunchPath();
613696
SString strInstallPath = GetInstallPathForLauncher();
614697

698+
// GetInstallPathForLauncher returns empty when running from a temp update directory and no
699+
// usable non-temp install root could be resolved. In that case the safest local fallback is
700+
// the launch path itself, which preserves the prior behavior for non-update launches without
701+
// contaminating the registry from the auto-update flow (Process C now goes through the far
702+
// branch with a base-dir override carried forward by CInstallManager).
703+
if (strInstallPath.empty())
704+
strInstallPath = strLaunchPath;
705+
615706
if (!strInstallPath.CompareI(strLaunchPath))
616707
{
617708
AddReportLog(1063, SString("SetMTASAPathSource: preserving install path '%s' for temp launcher '%s'", strInstallPath.c_str(),
@@ -620,6 +711,17 @@ void SetMTASAPathSource(bool bReadFromRegistry)
620711
return;
621712
}
622713

714+
// Refuse to write a temp update extraction directory into the install-location registry
715+
// values. A temp launcher started without the far-update sequencer state would otherwise
716+
// contaminate Last Run Location with an upcache\_*_tmp_* path that gets deleted later by
717+
// CleanDownloadCache, leaving a stale registry pointer that produces U01 on the next launch.
718+
if (IsTemporaryUpdateLaunchPath(strLaunchPath))
719+
{
720+
AddReportLog(1063, SString("SetMTASAPathSource: refusing to record temp launch path '%s' in registry", strLaunchPath.c_str()));
721+
g_strMTASAPath = strLaunchPath;
722+
return;
723+
}
724+
623725
SString strHash = "-";
624726
{
625727
MD5 md5;
@@ -1292,6 +1394,20 @@ void UpdateMTAVersionApplicationSetting(bool bQuiet)
12921394
bFreeModule = hModule != NULL;
12931395
}
12941396

1397+
// GetInstallPathForLauncher returns empty when running from a temp update directory and no
1398+
// usable non-temp install root could be resolved. GetMTASAPath consults SetMTASABaseDirOverride
1399+
// (which the install manager populates from the sequencer-carried INSTALL_ROOT in the far branch),
1400+
// so this attempt covers the recovered far-update case before falling back to the launch dir.
1401+
if (!hModule)
1402+
{
1403+
const SString strBaseDirPath = GetMTASAPath();
1404+
if (IsUsableMtasaInstallRoot(strBaseDirPath) && !strBaseDirPath.CompareI(strInstallPath))
1405+
{
1406+
hModule = LoadVersionModule(strBaseDirPath, dwLastError);
1407+
bFreeModule = hModule != NULL;
1408+
}
1409+
}
1410+
12951411
if (!hModule)
12961412
{
12971413
hModule = LoadVersionModule(GetLaunchPath(), dwLastError);

Client/loader/Utils.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ auto GetGameBaseDirectory() -> std::filesystem::path;
7979
auto GetGameLaunchDirectory() -> std::filesystem::path;
8080
auto GetGameExecutablePath() -> std::filesystem::path;
8181
SString GetInstallPathForLauncher();
82+
bool IsUsableMtasaInstallRoot(const SString& strPath);
83+
bool IsTemporaryUpdateLaunchPath(const SString& strLaunchPath);
8284

8385
void SetMTASAPathSource(bool bReadFromRegistry);
8486
SString GetMTASAPath();

0 commit comments

Comments
 (0)