Bump to v1.3.0: harden updater helper for Dropbox-installed copies

v1.2's self-updater silently failed when the install folder lived
inside a Dropbox sync path. Dropbox held write locks on the existing
RemSound.exe / DLLs during the brief window between the parent exiting
and the helper script copying the new files in. The helper's robocopy
(/R:5 /W:1) gave up after 5 seconds, and the helper then
unconditionally relaunched the OLD binary — so the user saw the same
version they started with after pressing "Yes" on the install prompt,
with no visible error.

Helper script (BuildInstallScript) changes:

* Robocopy retries bumped to /R:60 /W:1 — up to 60 seconds per file.
  Dropbox lock release happens reliably within that window in
  practice.

* Robocopy exit code is captured and checked. Codes >= 8 are real
  failures. On a failure the helper writes update-failed.txt to the
  install folder with the cause + recovery steps, leaves the staging
  folder intact, and does NOT relaunch the old binary. Earlier
  versions silently relaunched the unmodified old binary, hiding the
  failure.

* Helper appends a step-by-step trace to _update-helper.log (in the
  install folder), with robocopy's own output included via /LOG+:.

* update-failed.txt, _update-helper.log, and _apply-update.cmd are
  added to the /XF exclusion list so the helper's own state files
  don't get copied to themselves on a repeat update run.

DownloadAndStageInstallAsync also clears any stale update-failed.txt
at the start of every new attempt, so a successful run leaves the
install folder clean.

readme.html "If install fails" section expanded with the new
update-failed.txt marker file behaviour and the _update-helper.log
location.

Wire format, audio pipeline, and recording feature unchanged from
v1.2 — this is updater-machinery-only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Ednunp
2026-05-15 13:07:36 +01:00
co-authored by Claude Opus 4.7
parent c59e413c1f
commit 9a5c46eecf
5 changed files with 111 additions and 33 deletions
+10 -22
View File
@@ -1,33 +1,21 @@
# RemSound v1.2
# RemSound v1.3
Recording, sound cues, and receiver-side drift compensation. The wire format and audio pipeline are unchanged from v1.1, so v1.1 and v1.2 peers interoperate.
Updater hardening for Dropbox-installed copies. The wire format, audio pipeline, and recording feature are unchanged from v1.2.
## Highlights
## What's fixed
- **Recording to disk.** Dedicated Record menu (Alt+O) with Start/Stop on Ctrl+R, settings dialog, per-profile source / format / bit-depth / channel-mode / folder. Pick **received only**, **sent only**, or **both** as the recording source. Files are crash-resilient — a process crash mid-recording leaves a playable file containing everything up to the last header refresh (~5 seconds).
- **Four output formats, all functional.** WAV (16/24-bit PCM or 32-bit float), MP3 (LAME, 128320 kbps CBR), **OGG-Opus** (96256 kbps VBR, reuses the Concentus encoder from the wire path), **FLAC** (pure-managed CUETools FLAKE, lossless ~50% the size of WAV). All four record at 48 kHz; labels make the rate explicit.
- **Recording start / stop sound cues.** Short audible confirmation when recording transitions on or off. Played via the default Windows output device, separate from the recording pipeline, so a normal recording does not include the cue.
- **Per-cue sound preferences.** The single "Mute connect/disconnect sounds" checkbox in Preferences is replaced by a per-cue CheckedListBox: Connect / Disconnect / Recording start / Recording stop. Old profiles with the legacy mute on are honoured automatically on first load.
- **Receiver-side drift compensation upgraded.** Continuous `WdlResampler` running at a slowly-updated rate ratio replaces v1.1's discrete single-frame splice corrector. Smooths long-session sender-vs-receiver clock drift without the occasional 21 µs splice.
The v1.2 self-updater silently failed on installs inside Dropbox-synced folders. The cause: Dropbox held write locks on the existing `RemSound.exe` / DLLs during the brief window between the parent process exiting and the helper script copying the new files in. Robocopy gave up after 5 seconds, and the helper unconditionally relaunched the old binary regardless — so users saw the same version they started with after pressing **Yes** on the install prompt.
## UI changes
## What's new
- **Record menu moved to Alt+O.** The old `Alt+R` chord collided with the **Receive audio (Alt+R)** checkbox on the main form. Inside the menu the item mnemonics are unchanged (S / T / O / C).
- **Auto-tune interval label is mode-aware.** In BothIndependent mode the interval combo's label reads "Auto-tune interval — WASAPI and ASIO" so it's clear the same combo drives both lanes' tick cadence — each lane still independently tunes to its own latency target.
## Diagnostics (only active with Enable logs ticked)
- Per-stage discontinuity probes: sender raw-capture, sender pre-encode (per-lane in BothIndependent), receiver post-decode, post-ring-buffer, post-resampler. Lets log inspection localise where in the chain an audio click was introduced.
- Wire-level sequence tracking on each PCM stream: in-order / missed / reordered / duplicated packet counts in the diag log.
- Clipped-sample delta in the diag log.
## Bug fixes
- Auto-tune interval combo no longer greys out when only the ASIO lane's auto-tune is ticked in BothIndependent mode. Previously the combo's enabled state followed only the WASAPI checkbox.
- **Robocopy retries bumped to 60 seconds per file** (was 5). Dropbox lock release happens reliably within that window in practice.
- **Helper checks robocopy's exit code.** On a true failure (exit code 8+), the helper writes `update-failed.txt` next to `RemSound.exe` with the cause and recovery steps, and does **not** relaunch the old binary. Earlier versions silently relaunched the unmodified old binary, hiding the failure.
- **Helper writes a step-by-step log** to `_update-helper.log` in the install folder. If a future update goes wrong this is the trace.
- **Stale failure markers are cleared** at the start of every new update attempt, so a successful run leaves the install folder clean.
## Install
1. Download `RemSound-v1.2.zip` from this release.
1. Download `RemSound-v1.3.zip` from this release.
2. Extract somewhere with write permission (e.g. `C:\RemSound\`, `Documents\RemSound\`, etc.). Avoid `Program Files` unless you grant write permission so the self-updater can replace files in place.
3. Run `RemSound.exe`. Allow on private networks when Windows Firewall prompts.
4. Press F1 (or use the Help menu) for the user manual.
+4
View File
@@ -766,6 +766,10 @@ Bind whatever key combinations you prefer (e.g. Ctrl+Shift+Up / Ctrl+Shift+Down
<p>The downloader is best-effort: a flaky network, a locked install folder, or a temporarily-unavailable release will pop a MessageBox saying it couldn&rsquo;t complete and leave the running version untouched. The release page URL is in the popup so you can grab the zip in a browser and install by hand if needed. If you installed RemSound into <code>Program Files</code> without granting your user write permission to the install folder, the install helper&rsquo;s file-copy step will fail too &mdash; either fix the permission or move RemSound to a writable folder (e.g. somewhere under your user profile).</p>
<p><strong>If the helper&rsquo;s file-copy step itself fails</strong> (most often because Dropbox or another file-sync app was holding write locks on the install folder when the helper tried to overwrite the existing binaries), the helper now leaves a file named <code>update-failed.txt</code> next to <code>RemSound.exe</code> with diagnostic detail and the staged new files still sitting in the <code>_update</code> subfolder. It does <em>not</em> relaunch the old binary in that case &mdash; so the next time you start RemSound by hand, you&rsquo;ll either get the (still-old) version with the marker file telling you what to do, or you can copy the contents of <code>_update</code> over the install folder manually. The most reliable recovery is: close RemSound, wait 30 seconds for Dropbox to settle, relaunch, and try <strong>Help &rarr; Check for updates</strong> again. The longer retry window in v1.3 onward almost always succeeds on the second attempt.</p>
<p>A step-by-step trace of every helper run is appended to <code>_update-helper.log</code> in the install folder &mdash; useful if a failure persists and you want to share the log for diagnosis.</p>
<h3>The About dialog and release notes</h3>
<p>To see what version you&rsquo;re currently on without checking for updates, open <strong>Help &rarr; About RemSound</strong> (Alt+H, A). The dialog shows the version number and the release notes for the build you&rsquo;re running, in a scrollable read-only text box you can tab into and arrow through under NVDA. Close (or Esc) dismisses.</p>
+30
View File
@@ -20,6 +20,36 @@ internal sealed class AboutDialog : Form
/// updates" path.</summary>
private const string ReleaseNotes =
"""
RemSound v1.3
Updater hardening. The v1.2 self-updater silently failed on
installs inside Dropbox-synced folders because Dropbox held
write locks on the existing files during the brief window
between the parent exiting and the helper script copying the
new files in. Robocopy gave up after 5 seconds and the helper
relaunched the old binary anyway so the user saw the same
version they started with after pressing "Yes" on the install
prompt.
What's new:
* Helper script retries robocopy for up to 60 seconds per
file (was 5). Dropbox lock release happens reliably within
that window in practice.
* Helper now checks robocopy's exit code. On a true failure
(exit code 8 or higher), it writes update-failed.txt next
to RemSound.exe with diagnostic detail and does NOT
relaunch the old binary. Earlier versions silently
relaunched the unmodified old binary, hiding the failure.
* Helper writes a step-by-step log to _update-helper.log in
the install folder. If a future update goes wrong this is
where the trace lives.
* Stale failure markers are cleared at the start of every
new update attempt, so a successful run leaves the install
folder clean.
Wire format, audio pipeline, and recording feature are
unchanged from v1.2.
RemSound v1.2
Recording, sound cues, and receiver-side drift compensation.
+1 -1
View File
@@ -14,7 +14,7 @@
tag_name on the latest GitHub release; bump it on every public release. The
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
is what the About dialog and the updater both read. -->
<Version>1.2.0</Version>
<Version>1.3.0</Version>
</PropertyGroup>
<ItemGroup>
+66 -10
View File
@@ -123,9 +123,12 @@ internal sealed class RemSoundUpdater : IDisposable
var zipPath = Path.Combine(Path.GetTempPath(), $"RemSound-update-{info.Tag}.zip");
var batchPath = Path.Combine(baseDir, "_apply-update.cmd");
// Tidy any leftover from a previous failed attempt before we start.
// Tidy any leftover from a previous failed attempt before we start. Also clear
// the failure marker — the new attempt starts clean and only re-creates the
// marker if THIS run fails.
TryDelete(zipPath);
TryDeleteDirectory(stagingDir);
TryDelete(Path.Combine(baseDir, "update-failed.txt"));
Log?.Invoke($"updater: downloading {info.DownloadUrl}");
await using (var src = await http.GetStreamAsync(info.DownloadUrl, token).ConfigureAwait(false))
@@ -168,26 +171,79 @@ internal sealed class RemSoundUpdater : IDisposable
/// <summary>One-shot installer batch. Waits for the supplied PID to exit (so file locks
/// release), robocopies the staged folder over the install folder, removes the staging
/// area, restarts RemSound.exe, and self-deletes. Robocopy's <c>/R:5 /W:1</c> flags give
/// the audio threads a few extra seconds to wind down if the exe is slow to release. The
/// helper is detached from RemSound at start time, so it survives the parent's exit.</summary>
private static string BuildInstallScript(string stagingRoot, string installDir) =>
$"""
/// area, restarts RemSound.exe, and self-deletes.
///
/// History: v1.0 of this helper used <c>/R:5 /W:1</c> on robocopy and unconditionally
/// restarted RemSound regardless of whether the copy actually succeeded. On
/// Dropbox-installed copies this failed silently — Dropbox held write locks on the
/// existing install files for ~1030 seconds after extraction kicked the sync off, robocopy
/// gave up after 5 seconds, and the helper relaunched the OLD binary. The user saw the same
/// version after "update".
///
/// v1.3 hardening (2026-05-15):
/// * Robocopy retries bumped to <c>/R:60 /W:1</c> — up to 60 seconds per file. Dropbox
/// locks reliably release inside that window.
/// * Robocopy exit code is captured and checked. Anything ≥ 8 is a true failure; the
/// helper writes <c>update-failed.txt</c> to the install dir with diagnostic detail,
/// does NOT relaunch the old binary, and leaves the staging folder intact so the
/// user (or a re-run of the updater) can recover. Codes 07 are robocopy's
/// "success-ish" range (0 = nothing changed, 1 = copied, 2 = extras, 3 = both, etc).
/// * Helper writes a step-by-step log to <c>_update-helper.log</c> in the install dir
/// for post-mortem when the copy goes wrong. Robocopy's own output is appended via
/// <c>/LOG+:</c>.
///
/// The helper is detached from RemSound at start time, so it survives the parent's exit.</summary>
private static string BuildInstallScript(string stagingRoot, string installDir)
{
var helperLog = Path.Combine(installDir, "_update-helper.log");
var failureMarker = Path.Combine(installDir, "update-failed.txt");
var stagingDir = Path.Combine(installDir, "_update");
var remsoundExe = Path.Combine(installDir, "RemSound.exe");
return $"""
@echo off
setlocal
rem RemSound auto-installer helper. Generated by RemSoundUpdater. Self-deleting.
rem RemSound auto-installer helper. Generated by RemSoundUpdater. Self-deleting on success.
set "PID=%~1"
set "LOG={helperLog}"
set "MARKER={failureMarker}"
echo. >> "%LOG%"
echo === %DATE% %TIME% update helper started, parent PID=%PID% === >> "%LOG%"
:wait_loop
tasklist /FI "PID eq %PID%" 2>nul | find "%PID%" >nul
if not errorlevel 1 (
timeout /t 1 /nobreak >nul
goto wait_loop
)
robocopy "{stagingRoot}" "{installDir}" /E /IS /IT /NFL /NDL /NJH /NJS /R:5 /W:1 /XF _apply-update.cmd >nul
rmdir /S /Q "{Path.Combine(installDir, "_update")}" 2>nul
start "" "{Path.Combine(installDir, "RemSound.exe")}"
echo %DATE% %TIME% parent exited, starting robocopy (R:60 W:1) >> "%LOG%"
robocopy "{stagingRoot}" "{installDir}" /E /IS /IT /NFL /NDL /NJH /NJS /R:60 /W:1 /XF _apply-update.cmd /XF _update-helper.log /XF update-failed.txt /LOG+:"%LOG%"
set "ROBO_EXIT=%ERRORLEVEL%"
echo %DATE% %TIME% robocopy exit=%ROBO_EXIT% >> "%LOG%"
if %ROBO_EXIT% GEQ 8 (
echo Update copy FAILED. > "%MARKER%"
echo Robocopy exit code = %ROBO_EXIT% ^(anything ^>= 8 is a real failure^). >> "%MARKER%"
echo Staged files are intact at: {stagingDir} >> "%MARKER%"
echo Helper log: %LOG% >> "%MARKER%"
echo. >> "%MARKER%"
echo Most common cause: Dropbox or another file-sync app was holding write locks >> "%MARKER%"
echo on the existing RemSound binaries during the update window. Close RemSound, >> "%MARKER%"
echo wait 30 seconds for the sync to settle, then either: >> "%MARKER%"
echo * Re-launch RemSound and try Help -^> Check for updates again, OR >> "%MARKER%"
echo * Manually copy everything from the staged folder above into this folder. >> "%MARKER%"
echo %DATE% %TIME% FAILURE: leaving staging intact, NOT restarting RemSound >> "%LOG%"
del "%~f0"
exit /b %ROBO_EXIT%
)
rmdir /S /Q "{stagingDir}" 2>nul
echo %DATE% %TIME% staging removed, restarting RemSound >> "%LOG%"
start "" "{remsoundExe}"
del "%~f0"
""";
}
/// <summary>If the zip extracted to a single subfolder (typical when GitHub zips a tag),
/// return that subfolder so the copy works from the inner level. Otherwise return the