Release v3.6: rebuild the self-updater (in-app, rollback-safe); warnings to front

Replace the generated cmd.exe + robocopy update helper — which silently
failed on some machines — with an in-app C# installer:
  * Stage the new version to a per-user temp folder off the install and run
    the new RemSound.exe from there, so nothing in the install is locked by
    the updater itself.
  * Wait for the old process to fully exit (real WaitForExit), then
    back-up-and-swap files in C# with retry + rename-aside; roll the install
    back to the previous version on any failure, so a failed update can never
    leave a half-installed RemSound.
  * Log every step to updater.log; clean up old stages and legacy batch
    artefacts on launch. Removed the old BuildInstallScript batch generator.

Route the "RemSound is already running" dialog and its follow-up message
through ForegroundDialog so they surface in front from a background-relaunched
copy, matching the earlier post-update fix.

Docs: rewrite the readme update sections for the new mechanism, regenerate
MANUAL.md, refresh the About-box changelog and RELEASE_NOTES.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-06-10 13:37:19 +01:00
co-authored by Claude Opus 4.8
parent 4dbe9a47a0
commit 55bdcde0af
10 changed files with 470 additions and 258 deletions
+27 -3
View File
@@ -39,13 +39,23 @@ internal static class ForegroundDialog
Show<object?>(owner => { show(owner); return null; });
/// <summary>Force <paramref name="hWnd"/> to the foreground even when RemSound isn't the active
/// app. A plain SetForegroundWindow from a background process is refused by Windows; attaching
/// our input queue to the current foreground thread for the call lifts that restriction. The
/// owner is top-most regardless, so this is belt-and-braces for focus.</summary>
/// app — including the hardest case: a process the auto-updater RELAUNCHED in the background while
/// RemSound was minimised. Windows refuses SetForegroundWindow to a background-spawned process (it
/// only flashes the taskbar button — invisible to a screen-reader user), so we (1) drop the system
/// foreground-lock timeout to 0 for the duration of the call, and (2) attach our input queue to the
/// current foreground thread. Both the timeout and the attachment are restored immediately after.</summary>
private static void ForceForeground(IntPtr hWnd)
{
uint savedTimeout = 0;
var loweredTimeout = false;
try
{
// Lift Windows' foreground-stealing lock for this one call — the key fix for a dialog that
// must surface from a background-relaunched (post-update) process, where AttachThreadInput
// alone isn't enough and the dialog would otherwise open behind everything.
if (SystemParametersInfoGet(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, out savedTimeout, 0))
loweredTimeout = SystemParametersInfoSet(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, IntPtr.Zero, SPIF_SENDCHANGE);
var foreThread = GetWindowThreadProcessId(GetForegroundWindow(), out _);
var thisThread = GetCurrentThreadId();
if (foreThread != 0 && foreThread != thisThread)
@@ -65,12 +75,26 @@ internal static class ForegroundDialog
}
}
catch { /* best-effort; the owner is top-most anyway */ }
finally
{
if (loweredTimeout)
{
try { SystemParametersInfoSet(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, new IntPtr((long)savedTimeout), SPIF_SENDCHANGE); }
catch { /* restoring the user's setting is best-effort */ }
}
}
}
private const uint SPI_GETFOREGROUNDLOCKTIMEOUT = 0x2000;
private const uint SPI_SETFOREGROUNDLOCKTIMEOUT = 0x2001;
private const uint SPIF_SENDCHANGE = 0x0002;
[DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")] private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
[DllImport("user32.dll")] private static extern uint GetCurrentThreadId();
[DllImport("user32.dll")] private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
[DllImport("user32.dll")] private static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")] private static extern bool BringWindowToTop(IntPtr hWnd);
[DllImport("user32.dll", EntryPoint = "SystemParametersInfoW")] private static extern bool SystemParametersInfoGet(uint uiAction, uint uiParam, out uint pvParam, uint fWinIni);
[DllImport("user32.dll", EntryPoint = "SystemParametersInfoW")] private static extern bool SystemParametersInfoSet(uint uiAction, uint uiParam, IntPtr pvParam, uint fWinIni);
}