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:
co-authored by
Claude Opus 4.7
parent
c59e413c1f
commit
9a5c46eecf
+10
-22
@@ -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, 128–320 kbps CBR), **OGG-Opus** (96–256 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.
|
||||
|
||||
@@ -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’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’s file-copy step will fail too — either fix the permission or move RemSound to a writable folder (e.g. somewhere under your user profile).</p>
|
||||
|
||||
<p><strong>If the helper’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 — so the next time you start RemSound by hand, you’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 → 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 — 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’re currently on without checking for updates, open <strong>Help → About RemSound</strong> (Alt+H, A). The dialog shows the version number and the release notes for the build you’re running, in a scrollable read-only text box you can tab into and arrow through under NVDA. Close (or Esc) dismisses.</p>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ~10–30 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 0–7 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
|
||||
|
||||
Reference in New Issue
Block a user