fix(updater): prevent false positive triggers and unwanted app launches

This commit is contained in:
tomasalias 2026-10-13 19:33:59 +02:00
parent d1a415eb86
commit 1581dcd48b
2 changed files with 42 additions and 36 deletions

View file

@ -820,6 +820,10 @@ def is_curseforge_running() -> bool:
return False return False
def is_updater_running() -> bool: def is_updater_running() -> bool:
"""
Checks if a CurseForge or Overwolf updater/installer process is currently running.
Strictly scoped to avoid false positives from unrelated apps (Discord, Logitech, etc.).
"""
if os.name == "nt": if os.name == "nt":
try: try:
import ctypes import ctypes
@ -847,9 +851,9 @@ def is_updater_running() -> bool:
if kernel32.Process32First(h_snap, ctypes.byref(pe)): if kernel32.Process32First(h_snap, ctypes.byref(pe)):
while True: while True:
name = pe.szExeFile.decode('mbcs', errors='ignore').lower() name = pe.szExeFile.decode('mbcs', errors='ignore').lower()
if name in ("setup.exe", "installer.exe", "update.exe") or \ # Strictly match CurseForge or Overwolf installer/updater processes
("curseforge" in name and any(k in name for k in ("update", "setup", "install"))) or \ if ('curseforge' in name and any(k in name for k in ('update', 'setup', 'install'))) or \
("overwolf" in name and any(k in name for k in ("update", "setup", "install"))): ('overwolf' in name and any(k in name for k in ('installer', 'updater', 'setup'))):
found = True found = True
break break
if not kernel32.Process32Next(h_snap, ctypes.byref(pe)): if not kernel32.Process32Next(h_snap, ctypes.byref(pe)):
@ -860,22 +864,6 @@ def is_updater_running() -> bool:
except Exception: except Exception:
pass pass
try:
out = subprocess.check_output(
["tasklist", "/FO", "CSV", "/NH"],
text=True, stderr=subprocess.DEVNULL, creationflags=CREATE_NO_WINDOW
)
for line in out.splitlines():
line = line.strip().lower()
if not line:
continue
parts = [p.strip('"') for p in line.split('","')]
if parts:
pname = parts[0]
if pname in ("setup.exe", "installer.exe") or ("curseforge" in pname and "updater" in pname):
return True
except Exception:
pass
return False return False
def is_splash_running() -> bool: def is_splash_running() -> bool:
@ -1224,16 +1212,22 @@ def run_watcher_loop():
if is_splash_running(): if is_splash_running():
return return
needs_patch = not is_curseforge_patched(cf_dir) or is_updater_running() # ONLY trigger if CurseForge is genuinely unpatched
if needs_patch: if not is_curseforge_patched(cf_dir):
if now - last_splash_launch < 12.0: if now - last_splash_launch < 15.0:
return return
last_splash_launch = now last_splash_launch = now
wlog(f"CurseForge update or unpatched state detected ({trigger_reason}). Launching Splash Updater...") wlog(f"CurseForge unpatched state detected ({trigger_reason}). Launching Splash Updater...")
ok = launch_splash_updater() ok = launch_splash_updater()
if not ok: if not ok:
wlog("Could not launch Splash Updater; falling back to background patch.") wlog("Could not launch Splash Updater; falling back to background patch.")
settled = wait_for_files_settled(cf_dir, timeout=90.0, log_fn=wlog, intercept_unpatched=True) was_running = is_curseforge_running()
was_intercepted = [False]
def on_fb_log(msg: str):
if "silenc" in msg.lower():
was_intercepted[0] = True
wlog(msg)
settled = wait_for_files_settled(cf_dir, timeout=90.0, log_fn=on_fb_log, intercept_unpatched=True)
if settled: if settled:
kill_curseforge() kill_curseforge()
success = False success = False
@ -1244,6 +1238,7 @@ def run_watcher_loop():
time.sleep(2) time.sleep(2)
if success: if success:
wlog("★ CurseForge successfully re-patched after update!") wlog("★ CurseForge successfully re-patched after update!")
if was_running or was_intercepted[0]:
subprocess.Popen([str(cf_dir / "CurseForge.exe")], creationflags=CREATE_NO_WINDOW) subprocess.Popen([str(cf_dir / "CurseForge.exe")], creationflags=CREATE_NO_WINDOW)
else: else:
wlog("Failed to re-patch CurseForge after update.") wlog("Failed to re-patch CurseForge after update.")

View file

@ -642,6 +642,14 @@ class SplashUpdaterWindow:
time.sleep(3.0) time.sleep(3.0)
return return
# Early guard: if no target_pid, CurseForge is already patched, and no updater is running, exit immediately
if not self.target_pid and patch_core.is_curseforge_patched(cf_dir) and not patch_core.is_updater_running():
self.root.after(0, self.root.destroy)
return
was_running = patch_core.is_curseforge_running() or (self.target_pid is not None)
was_intercepted = [False]
if self.target_pid: if self.target_pid:
self.root.after(0, self.set_status, "Waiting for previous instance to exit...", f"CurseForge PID {self.target_pid}") self.root.after(0, self.set_status, "Waiting for previous instance to exit...", f"CurseForge PID {self.target_pid}")
for _ in range(30): for _ in range(30):
@ -658,6 +666,7 @@ class SplashUpdaterWindow:
elif "settle" in msg.lower() or "mb" in msg.lower(): elif "settle" in msg.lower() or "mb" in msg.lower():
self.root.after(0, self.set_status, "Finalizing update files...", msg) self.root.after(0, self.set_status, "Finalizing update files...", msg)
elif "silenc" in msg.lower(): elif "silenc" in msg.lower():
was_intercepted[0] = True
self.root.after(0, self.set_status, "Silenced vanilla launcher in background...", "Preventing unpatched window display...") self.root.after(0, self.set_status, "Silenced vanilla launcher in background...", "Preventing unpatched window display...")
settled = patch_core.wait_for_files_settled(cf_dir, timeout=90.0, log_fn=on_settle_log, intercept_unpatched=True) settled = patch_core.wait_for_files_settled(cf_dir, timeout=90.0, log_fn=on_settle_log, intercept_unpatched=True)
@ -696,11 +705,15 @@ class SplashUpdaterWindow:
time.sleep(2.0) time.sleep(2.0)
if success: if success:
should_launch = was_running or was_intercepted[0] or (self.target_pid is not None)
if should_launch:
self.root.after(0, self.set_status, "✓ Complete! Launching BetterCurseForge...", "Enjoy your ad-free experience!", "success") self.root.after(0, self.set_status, "✓ Complete! Launching BetterCurseForge...", "Enjoy your ad-free experience!", "success")
exe = cf_dir / "CurseForge.exe" exe = cf_dir / "CurseForge.exe"
if exe.exists(): if exe.exists():
creation_flags = 0x00000008 | 0x00000200 if os.name == "nt" else 0 # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP creation_flags = 0x00000008 | 0x00000200 if os.name == "nt" else 0 # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP
subprocess.Popen([str(exe)], cwd=str(cf_dir), creationflags=creation_flags) subprocess.Popen([str(exe)], cwd=str(cf_dir), creationflags=creation_flags)
else:
self.root.after(0, self.set_status, "✓ Complete! BetterCurseForge is updated.", "CurseForge updated in background.", "success")
time.sleep(0.9) time.sleep(0.9)
else: else:
self.root.after(0, self.set_status, "Failed to apply patch.", "Check watcher.log for details.", "error") self.root.after(0, self.set_status, "Failed to apply patch.", "Check watcher.log for details.", "error")
@ -723,13 +736,11 @@ if __name__ == "__main__":
if arg in ("--watch", "watch"): if arg in ("--watch", "watch"):
patch_core.run_watcher_loop() patch_core.run_watcher_loop()
sys.exit(0) sys.exit(0)
elif arg in ("--repatch-splash", "--splash", "splash"): elif arg in ("--repatch-splash", "--splash", "splash", "--wait-for-update", "wait-for-update"):
pid = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else None pid = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else None
splash = SplashUpdaterWindow(pid) cf = patch_core.get_curseforge_dir()
splash.run() if not pid and cf and patch_core.is_curseforge_patched(cf) and not patch_core.is_updater_running():
sys.exit(0) sys.exit(0)
elif arg in ("--wait-for-update", "wait-for-update"):
pid = int(sys.argv[2]) if len(sys.argv) > 2 and sys.argv[2].isdigit() else None
splash = SplashUpdaterWindow(pid) splash = SplashUpdaterWindow(pid)
splash.run() splash.run()
sys.exit(0) sys.exit(0)