diff --git a/XboxGamingBarHelper/ElevationBootstrapper.cs b/XboxGamingBarHelper/ElevationBootstrapper.cs index bba80641..7179da62 100644 --- a/XboxGamingBarHelper/ElevationBootstrapper.cs +++ b/XboxGamingBarHelper/ElevationBootstrapper.cs @@ -304,6 +304,15 @@ public static bool PerformSetup() Logger.Info($"Target directory: {HelperDeploymentService.HelperFolder}"); Logger.Info($"Package version: {HelperDeploymentService.GetCurrentPackageVersion()}"); + // Step 0: stop any running/zombie helper instances first (#97). A helper + // still running from the deployed folder keeps its exe/DLLs locked, so + // every copy attempt fails, .version is never stamped, and each launch + // re-fires the UAC setup loop. We're elevated here, so we have the rights + // to kill them regardless of who started them. + Logger.Info("Step 0: Stopping running helper instances..."); + DebugLog("Step 0: Killing running helper instances..."); + KillRunningHelperInstances(DebugLog); + // Step 1: Deploy helper files Logger.Info("Step 1: Deploying helper files..."); DebugLog("Step 1: Deploying..."); @@ -344,6 +353,52 @@ public static bool PerformSetup() } } + /// + /// Kills every other XboxGamingBarHelper process so the deploy that follows + /// doesn't fail on locked files (#97). All sessions, not just ours — a stale + /// scheduled-task helper holds the same locks wherever it lives. The launcher + /// instance that spawned this setup has already exited by the time UAC consent + /// completes, so "everything except self" is safe. + /// + private static void KillRunningHelperInstances(Action debugLog) + { + try + { + int selfPid = Process.GetCurrentProcess().Id; + var peers = Process.GetProcessesByName("XboxGamingBarHelper"); + foreach (var p in peers) + { + try + { + if (p.Id == selfPid || p.HasExited) + { + continue; + } + + Logger.Info($"Killing running helper instance (PID={p.Id}) before deploy"); + debugLog?.Invoke($"Killing helper PID={p.Id}"); + p.Kill(); + if (!p.WaitForExit(5000)) + { + Logger.Warn($"Helper PID={p.Id} did not exit within 5s; deploy may hit locked files"); + } + } + catch (Exception ex) + { + Logger.Warn($"Could not kill helper PID={p.Id}: {ex.Message}"); + } + finally + { + try { p.Dispose(); } catch { } + } + } + } + catch (Exception ex) + { + Logger.Warn($"Could not enumerate helper instances: {ex.Message}"); + } + } + /// /// Checks if the current process is running with administrator privileges. /// diff --git a/XboxGamingBarHelper/Services/HelperDeploymentService.cs b/XboxGamingBarHelper/Services/HelperDeploymentService.cs index 5b6738bc..8d2dfadb 100644 --- a/XboxGamingBarHelper/Services/HelperDeploymentService.cs +++ b/XboxGamingBarHelper/Services/HelperDeploymentService.cs @@ -128,9 +128,13 @@ public static string GetMsixHelperPath() }; /// - /// Gets the current package version from MSIX + /// Gets the package version from MSIX identity, or null when the process has no + /// package identity (e.g. running from the deployed LocalCache copy). Use this + /// wherever a wrong-but-plausible version is worse than no version at all — + /// the assembly-version fallback below is a hardcoded 1.0.0.0 and stamping it + /// into .version would wedge IsDeploymentNeeded into a permanent mismatch. /// - public static string GetCurrentPackageVersion() + public static string GetPackageVersionFromIdentity() { try { @@ -141,17 +145,31 @@ public static string GetCurrentPackageVersion() catch (Exception ex) { Logger.Debug($"Could not get package version (not running in MSIX?): {ex.Message}"); - // Fall back to assembly version - try - { - var assembly = Assembly.GetExecutingAssembly(); - var version = assembly.GetName().Version; - return version?.ToString() ?? "0.0.0.0"; - } - catch - { - return "0.0.0.0"; - } + return null; + } + } + + /// + /// Gets the current package version from MSIX + /// + public static string GetCurrentPackageVersion() + { + var identityVersion = GetPackageVersionFromIdentity(); + if (identityVersion != null) + { + return identityVersion; + } + + // Fall back to assembly version + try + { + var assembly = Assembly.GetExecutingAssembly(); + var version = assembly.GetName().Version; + return version?.ToString() ?? "0.0.0.0"; + } + catch + { + return "0.0.0.0"; } } @@ -284,7 +302,29 @@ public static bool DeployHelper() if (PathsAreSame(sourceDir, HelperFolder)) { Logger.Warn($"Deploy source equals deployed folder ({HelperFolder}); skipping self-copy (files already in place)."); - return File.Exists(DeployedExePath); + + // #97: if the files are in place but .version is missing/stale, this + // no-op path used to leave the mismatch in place forever. Re-stamp — + // but only with a real package-identity version; the assembly-version + // fallback (1.0.0.0) would poison the stamp and recreate the loop. + if (File.Exists(DeployedExePath)) + { + var identityVersion = GetPackageVersionFromIdentity(); + if (identityVersion != null && GetDeployedVersion() != identityVersion) + { + try + { + File.WriteAllText(VersionFilePath, identityVersion); + Logger.Info($"Version file re-stamped on self-copy no-op: {identityVersion}"); + } + catch (Exception ex) + { + Logger.Warn($"Could not re-stamp version file: {ex.Message}"); + } + } + return true; + } + return false; } // Create target directory @@ -372,7 +412,11 @@ public static bool DeployHelper() } Logger.Info($"Deployment complete: {successCount} files copied, {failCount} failures"); - return successCount > 0 && File.Exists(DeployedExePath); + // #97: success must match the .version stamp condition. Returning true on a + // partial deploy (failCount>0) let PerformSetup report "Setup Complete" while + // .version was never written — every launch then re-detected a mismatch and + // re-fired UAC, forever. Fail loudly instead so setup surfaces the error. + return deployOk; } catch (Exception ex) {