Skip to content

Commit 1e23b48

Browse files
committed
fix(runtime): improve VC runtime detection stability
1 parent ea77967 commit 1e23b48

3 files changed

Lines changed: 333 additions & 17 deletions

File tree

frontend/src/hooks/useLauncher.ts

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const DEPENDENCY_CHECK_SESSION_KEYS = {
2929
gamingServices: "ll.session.dependency-check.gamingservices",
3030
vcRuntime: "ll.session.dependency-check.vcruntime",
3131
} as const;
32+
const VC_RUNTIME_CHECK_RETRY_DELAY_MS = 800;
3233

3334
const hasSessionDependencyCheckRun = (
3435
key: (typeof DEPENDENCY_CHECK_SESSION_KEYS)[keyof typeof DEPENDENCY_CHECK_SESSION_KEYS],
@@ -616,17 +617,43 @@ export const useLauncher = (args: any) => {
616617

617618
useEffect(() => {
618619
if (!hasBackend) return;
620+
let disposed = false;
621+
let vcRuntimeRetryTimer: number | null = null;
622+
623+
const runVcRuntimeDependencyCheck = (attempt: number) => {
624+
const checkVcRuntime = (minecraft as any)?.IsVcRuntimeInstalled;
625+
if (typeof checkVcRuntime !== "function") {
626+
return;
627+
}
628+
629+
Promise.resolve(checkVcRuntime())
630+
.then((ok: boolean) => {
631+
if (disposed) return;
632+
markSessionDependencyCheckRun(DEPENDENCY_CHECK_SESSION_KEYS.vcRuntime);
633+
if (!ok) {
634+
vcRuntimeMissingDisclosure.onOpen();
635+
}
636+
})
637+
.catch((error: unknown) => {
638+
if (disposed) return;
639+
console.warn("Failed to check VC runtime status", error);
640+
if (attempt > 0 || vcRuntimeRetryTimer !== null) {
641+
return;
642+
}
643+
vcRuntimeRetryTimer = window.setTimeout(() => {
644+
vcRuntimeRetryTimer = null;
645+
if (disposed) return;
646+
runVcRuntimeDependencyCheck(attempt + 1);
647+
}, VC_RUNTIME_CHECK_RETRY_DELAY_MS);
648+
});
649+
};
650+
619651
const timer = setTimeout(() => {
620652
if (
621653
!hasSessionDependencyCheckRun(DEPENDENCY_CHECK_SESSION_KEYS.vcRuntime)
622654
) {
623-
markSessionDependencyCheckRun(DEPENDENCY_CHECK_SESSION_KEYS.vcRuntime);
624655
try {
625-
(minecraft as any)?.IsVcRuntimeInstalled?.().then((ok: boolean) => {
626-
if (!ok) {
627-
vcRuntimeMissingDisclosure.onOpen();
628-
}
629-
});
656+
runVcRuntimeDependencyCheck(0);
630657
} catch {}
631658
}
632659
if (
@@ -661,7 +688,14 @@ export const useLauncher = (args: any) => {
661688
} catch {}
662689
}
663690
}, 0);
664-
return () => clearTimeout(timer);
691+
692+
return () => {
693+
disposed = true;
694+
clearTimeout(timer);
695+
if (vcRuntimeRetryTimer !== null) {
696+
clearTimeout(vcRuntimeRetryTimer);
697+
}
698+
};
665699
}, [
666700
hasBackend,
667701
gameInputMissingDisclosure,

internal/vcruntime/vcruntime.go

Lines changed: 106 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,26 @@ type EnsureProgress struct {
4141
Total int64
4242
}
4343

44+
type vcRuntimeRegistryState struct {
45+
Installed uint64
46+
HasInstalled bool
47+
Version string
48+
Major uint64
49+
HasMajor bool
50+
}
51+
4452
var (
45-
mu sync.Mutex
46-
ensuring bool
53+
mu sync.Mutex
54+
ensuring bool
55+
vcRuntimeMismatchLogOnce sync.Once
56+
vcRuntimeRegistryPaths = []string{
57+
`SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64`,
58+
`SOFTWARE\Wow6432Node\Microsoft\VisualStudio\14.0\VC\Runtimes\x64`,
59+
}
60+
vcRuntimeUninstallKeyPaths = []string{
61+
`SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`,
62+
`SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall`,
63+
}
4764
)
4865

4966
//go:embed vcruntime140_1.dll
@@ -64,18 +81,64 @@ func fileSHA256(p string) ([]byte, error) {
6481
return h.Sum(nil), nil
6582
}
6683

67-
func hasVCMinimumRuntime(path string) bool {
84+
func readVcRuntimeRegistryState(path string) (vcRuntimeRegistryState, bool) {
6885
key, err := winreg.OpenKey(winreg.LOCAL_MACHINE, path, winreg.READ)
6986
if err != nil {
87+
return vcRuntimeRegistryState{}, false
88+
}
89+
defer key.Close()
90+
91+
state := vcRuntimeRegistryState{}
92+
if installed, _, err := key.GetIntegerValue("Installed"); err == nil {
93+
state.Installed = installed
94+
state.HasInstalled = true
95+
}
96+
if version, _, err := key.GetStringValue("Version"); err == nil {
97+
state.Version = strings.TrimSpace(version)
98+
}
99+
if major, _, err := key.GetIntegerValue("Major"); err == nil {
100+
state.Major = major
101+
state.HasMajor = true
102+
}
103+
return state, true
104+
}
105+
106+
func isVcRuntimeRegistryStateInstalled(state vcRuntimeRegistryState) bool {
107+
if !state.HasInstalled || state.Installed != 1 {
70108
return false
71109
}
110+
if strings.TrimSpace(state.Version) != "" {
111+
return true
112+
}
113+
return state.HasMajor && state.Major >= 14
114+
}
115+
116+
func hasInstalledVcRuntime(paths []string, readState func(string) (vcRuntimeRegistryState, bool)) bool {
117+
for _, path := range paths {
118+
state, ok := readState(path)
119+
if !ok {
120+
continue
121+
}
122+
if isVcRuntimeRegistryStateInstalled(state) {
123+
return true
124+
}
125+
}
126+
return false
127+
}
128+
129+
func readVCUninstallDisplayNames(path string) ([]string, error) {
130+
key, err := winreg.OpenKey(winreg.LOCAL_MACHINE, path, winreg.READ)
131+
if err != nil {
132+
return nil, err
133+
}
72134
defer key.Close()
73135

74136
names, err := key.ReadSubKeyNames(-1)
75137
if err != nil {
76-
return false
138+
return nil, err
77139
}
78140

141+
displayNames := make([]string, 0, len(names))
79142
for _, name := range names {
80143
sub, err := winreg.OpenKey(key, name, winreg.QUERY_VALUE)
81144
if err != nil {
@@ -86,21 +149,54 @@ func hasVCMinimumRuntime(path string) bool {
86149
if err != nil {
87150
continue
88151
}
152+
displayNames = append(displayNames, displayName)
153+
}
154+
return displayNames, nil
155+
}
89156

157+
func hasVCUninstallEvidenceFromDisplayNames(displayNames []string) bool {
158+
for _, displayName := range displayNames {
90159
l := strings.ToLower(strings.TrimSpace(displayName))
91-
if strings.Contains(l, "visual c++") &&
92-
strings.Contains(l, "x64") &&
93-
strings.Contains(l, "2022") &&
94-
strings.Contains(l, "minimum runtime") {
160+
if !strings.Contains(l, "visual c++") || !strings.Contains(l, "x64") {
161+
continue
162+
}
163+
if !strings.Contains(l, "redistributable") && !strings.Contains(l, "runtime") {
164+
continue
165+
}
166+
if strings.Contains(l, "2015") ||
167+
strings.Contains(l, "2017") ||
168+
strings.Contains(l, "2019") ||
169+
strings.Contains(l, "2022") ||
170+
strings.Contains(l, "v14") {
171+
return true
172+
}
173+
}
174+
return false
175+
}
176+
177+
func hasVCUninstallEvidence(readDisplayNames func(string) ([]string, error)) bool {
178+
for _, path := range vcRuntimeUninstallKeyPaths {
179+
displayNames, err := readDisplayNames(path)
180+
if err != nil {
181+
continue
182+
}
183+
if hasVCUninstallEvidenceFromDisplayNames(displayNames) {
95184
return true
96185
}
97186
}
98187
return false
99188
}
100189

101190
func IsInstalled() bool {
102-
return hasVCMinimumRuntime(`SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall`) ||
103-
hasVCMinimumRuntime(`SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall`)
191+
if hasInstalledVcRuntime(vcRuntimeRegistryPaths, readVcRuntimeRegistryState) {
192+
return true
193+
}
194+
if hasVCUninstallEvidence(readVCUninstallDisplayNames) {
195+
vcRuntimeMismatchLogOnce.Do(func() {
196+
log.Println("vcruntime: found Visual C++ uninstall entries but official VC runtime registry keys are missing")
197+
})
198+
}
199+
return false
104200
}
105201

106202
func EnsureInteractive(ctx context.Context) {

0 commit comments

Comments
 (0)