Bump to v1.7.0: self-updater release-filtering fix
The client and the relay server are published from the same GitHub repo; the server's releases use "server-" prefixed tags. RemSoundUpdater hit /releases/latest, which is repo-wide — when a server release was newest, the updater fed "server-v2.3" to ParseTag (-> a bogus 0.0.3) and concluded "up to date", silently skipping real client updates. CheckForUpdateAsync now lists /releases and picks the highest-versioned release whose tag is a RemSound client tag (new IsClientReleaseTag: after an optional leading "v", first char must be a digit). Drafts and pre-releases are skipped. The server-side updater already filters to "server-" tags, so client + server coexist in one repo cleanly. Also rewrites build-release.ps1 with a data-safety check: it publishes to a fresh staging folder and aborts the release if any logs/, profiles/, recordings/ folder, .log file or remsound.config.json is present in the staged output or the finished zip — preventing a repeat of the v1.5/v1.6 zips that shipped with developer logs and profiles. No wire-format or audio-pipeline changes — v1.5/v1.6/v1.7 interoperate. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
918ca6cac0
commit
6b1ae2d017
+8
-8
@@ -1,16 +1,16 @@
|
|||||||
# RemSound v1.6
|
# RemSound v1.7
|
||||||
|
|
||||||
Three reliability fixes — peer-address recovery, a reconnect crash, and a long-run memory/CPU leak. Wire format and audio pipeline are unchanged from v1.4 / v1.5 — all three releases interoperate.
|
A self-updater fix. Wire format and audio pipeline are unchanged from v1.4 / v1.5 / v1.6 — all releases interoperate.
|
||||||
|
|
||||||
## Bug fixes
|
## Bug fix
|
||||||
|
|
||||||
- **Peer address recovery.** If the address you connected to stops responding — a peer rebooted onto a new IP, or a computer name resolved (via DNS / a router or Pi-hole record) to a stale address — RemSound now follows the peer to the live address it is still heartbeating from, instead of sending audio to a dead address indefinitely. Recovery is automatic within a few seconds. It only adopts private-network (LAN) addresses, so a relay's public address can never be mistaken for a moved peer.
|
- **Check for updates now reliably finds RemSound releases.** RemSound and the RemSound relay server are published from the same GitHub repository; the relay's releases use `server-` prefixed tags. The updater previously asked GitHub only for the single newest release of *any* kind — so whenever a relay release was the most recent, the updater misread its version and concluded RemSound was already up to date, silently skipping a real client update. The updater now scans the full release list and considers only RemSound client versions, ignoring server releases, drafts and pre-releases.
|
||||||
- **Reconnect crash fixed.** When a peer reconnected — typically after rebooting — the Connectivity tab's peer list could be read mid-rebuild with a stale list index, throwing an `IndexOutOfRangeException` from the 1-second status timer and bringing the app down with a crash dialog. The list reads are now bounds-checked, and the status tick is wrapped so a transient UI hiccup is logged instead of fatal.
|
|
||||||
- **Long-run memory and CPU leak fixed.** A receiver left running for hours could grow to several gigabytes of memory and climbing CPU. Decoder sessions orphaned by peer reconnects (each reconnect creates a fresh stream identity) were not being reliably reclaimed — they piled up, each holding a multi-megabyte playout buffer and costing render-thread time on every audio callback. Idle sessions are now reaped on their own activity timer, with a hard cap on live sessions as a backstop, so memory and CPU stay bounded however long RemSound runs.
|
If your copy is stuck on an older version because of this, install v1.7 once by hand (below) — from v1.7 onward, **Help → Check for updates** works correctly on its own.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
1. Download `RemSound-v1.6.zip` from this release.
|
1. Download `RemSound-v1.7.zip` from this release.
|
||||||
2. Extract somewhere with write permission (e.g. `C:\RemSound\`, `Documents\RemSound\`). Avoid `Program Files` unless you grant write permission so the self-updater can replace files in place.
|
2. Extract somewhere with write permission (e.g. `C:\RemSound\`, `Documents\RemSound\`). 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.
|
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. Press F1 (or use the Help menu) for the user manual.
|
||||||
@@ -19,6 +19,6 @@ Requires the .NET 10 Desktop Runtime. If it's missing, Windows offers to fetch i
|
|||||||
|
|
||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
v1.3 / v1.4 / v1.5 users on any install location can use **Help → Check for updates** — the hardened updater pulls v1.6 cleanly.
|
v1.5 / v1.6 users: use **Help → Check for updates** — it pulls v1.7 cleanly (their updater can still see v1.7 as long as it's the newest release at check time). To be certain, the manual install above always works.
|
||||||
|
|
||||||
If you installed RemSound inside a Dropbox-synced (or other file-sync) folder and your install is v1.0 / v1.1 / v1.2, see the [v1.3 release notes](https://github.com/Ednunp/RemSound/releases/tag/v1.3) for one-time manual install steps. From v1.3 onward Check-for-updates handles Dropbox correctly.
|
If you installed RemSound inside a Dropbox-synced (or other file-sync) folder and your install is v1.0 / v1.1 / v1.2, see the [v1.3 release notes](https://github.com/Ednunp/RemSound/releases/tag/v1.3) for one-time manual install steps. From v1.3 onward Check-for-updates handles Dropbox correctly.
|
||||||
|
|||||||
+89
-24
@@ -1,42 +1,107 @@
|
|||||||
# Build script for a tagged RemSound release.
|
# build-release.ps1 — produce the RemSound client release zip, and PROVE it carries
|
||||||
|
# no personal data before it can ship.
|
||||||
#
|
#
|
||||||
# .\build-release.ps1 v1.0
|
# Why this script exists
|
||||||
|
# ----------------------
|
||||||
|
# v1.5 and v1.6 were released with the developer's logs/, profiles/ and recordings/
|
||||||
|
# folders inside the zip — because those releases were hand-zipped from a publish/
|
||||||
|
# folder the app had been run from, which had accumulated that runtime data, instead
|
||||||
|
# of using this script. This version removes the human step that went wrong:
|
||||||
|
# * It ALWAYS publishes into a fresh, empty staging folder — never a reused dir.
|
||||||
|
# * It then SCANS the staged files AND the finished zip, and ABORTS (deletes the
|
||||||
|
# zip, exits non-zero) if anything that could carry personal data is present.
|
||||||
|
# Never hand-zip publish/ again. Run this. If it aborts, the release does not ship.
|
||||||
#
|
#
|
||||||
# Produces dist\RemSound-v1.0.zip, ready for `gh release create`. The asset name matches
|
# Usage:
|
||||||
# what RemSoundUpdater expects on the GitHub Releases page (RemSound-<tag>.zip); change
|
# powershell -ExecutionPolicy Bypass -File build-release.ps1 -Tag v1.7
|
||||||
# RemSoundUpdater.AssetNameTemplate if you rename here.
|
#
|
||||||
|
# The -Tag value must match the GitHub release tag. The zip is named RemSound-<Tag>.zip
|
||||||
|
# because the in-app updater downloads exactly that asset name (AssetNameTemplate in
|
||||||
|
# RemSoundUpdater.cs: "RemSound-{tag}.zip").
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory=$true, Position=0)]
|
[Parameter(Mandatory = $true, Position = 0)]
|
||||||
|
[ValidatePattern('^v[0-9]+\.[0-9]+$')]
|
||||||
[string]$Tag
|
[string]$Tag
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$repoRoot = $PSScriptRoot
|
|
||||||
|
|
||||||
Write-Host "Cleaning publish staging..." -ForegroundColor Cyan
|
$repo = $PSScriptRoot
|
||||||
$stage = Join-Path $repoRoot 'src\RemSound.App\bin\Release\net10.0-windows\publish'
|
$proj = Join-Path $repo 'src\RemSound.App\RemSound.App.csproj'
|
||||||
if (Test-Path $stage) { Remove-Item $stage -Recurse -Force }
|
$distDir = Join-Path $repo 'dist'
|
||||||
|
$zipPath = Join-Path $distDir "RemSound-$Tag.zip"
|
||||||
|
$staging = Join-Path ([System.IO.Path]::GetTempPath()) ("remsound-release-" + [guid]::NewGuid().ToString('N'))
|
||||||
|
|
||||||
Write-Host "Publishing framework-dependent..." -ForegroundColor Cyan
|
# Anything matching these must NEVER appear in a release. Folders by name; files by
|
||||||
& dotnet publish (Join-Path $repoRoot 'src\RemSound.App\RemSound.App.csproj') -c Release | Out-Null
|
# extension / exact name. RemSound.deps.json and RemSound.runtimeconfig.json are
|
||||||
if ($LASTEXITCODE -ne 0) { throw "dotnet publish failed (exit $LASTEXITCODE)" }
|
# legitimate app files and are deliberately NOT matched (different names).
|
||||||
|
$forbiddenFolders = @('logs', 'profiles', 'recordings')
|
||||||
|
function Test-Forbidden([string]$path) {
|
||||||
|
$p = $path -replace '\\', '/'
|
||||||
|
foreach ($f in $forbiddenFolders) {
|
||||||
|
if ($p -match "(^|/)$f/") { return $true }
|
||||||
|
}
|
||||||
|
if ($p -match '\.log$') { return $true }
|
||||||
|
if ($p -match '(^|/)remsound\.config\.json$') { return $true }
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
$distDir = Join-Path $repoRoot 'dist'
|
# 1. Fresh, empty staging folder — the whole point. The app has never run here, so
|
||||||
if (-not (Test-Path $distDir)) { New-Item -ItemType Directory -Path $distDir | Out-Null }
|
# there is nothing to leak.
|
||||||
|
if (Test-Path $staging) { Remove-Item $staging -Recurse -Force }
|
||||||
|
New-Item -ItemType Directory -Path $staging -Force | Out-Null
|
||||||
|
|
||||||
$zipName = "RemSound-$Tag.zip"
|
Write-Host "Publishing $Tag to clean staging: $staging" -ForegroundColor Cyan
|
||||||
$zipPath = Join-Path $distDir $zipName
|
& dotnet publish $proj -c Release -o $staging | Out-Null
|
||||||
if (Test-Path $zipPath) { Remove-Item $zipPath -Force }
|
if ($LASTEXITCODE -ne 0) { Remove-Item $staging -Recurse -Force; throw "dotnet publish failed (exit $LASTEXITCODE)" }
|
||||||
|
|
||||||
Write-Host "Zipping $zipName..." -ForegroundColor Cyan
|
# 2. Debug symbols are not personal data, but they don't belong in a release either.
|
||||||
Compress-Archive -Path (Join-Path $stage '*') -DestinationPath $zipPath -CompressionLevel Optimal
|
Get-ChildItem -Path $staging -Filter *.pdb -Recurse | Remove-Item -Force
|
||||||
|
|
||||||
|
# 3. SAFETY CHECK on the staged files.
|
||||||
|
$bad = @()
|
||||||
|
Get-ChildItem -Path $staging -Recurse -Force | ForEach-Object {
|
||||||
|
$rel = $_.FullName.Substring($staging.Length).TrimStart('\', '/')
|
||||||
|
if (Test-Forbidden $rel) { $bad += $rel }
|
||||||
|
}
|
||||||
|
if ($bad.Count -gt 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "RELEASE ABORTED - staged folder contains files that must not ship:" -ForegroundColor Red
|
||||||
|
$bad | Sort-Object -Unique | ForEach-Object { Write-Host " $_" -ForegroundColor Red }
|
||||||
|
Remove-Item $staging -Recurse -Force
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# 4. Zip it. Keep dist/ to a single artefact — drop any prior versioned zip.
|
||||||
|
New-Item -ItemType Directory -Path $distDir -Force | Out-Null
|
||||||
|
Get-ChildItem -Path $distDir -Filter 'RemSound-v*.zip' -ErrorAction SilentlyContinue | Remove-Item -Force
|
||||||
|
Compress-Archive -Path (Join-Path $staging '*') -DestinationPath $zipPath -CompressionLevel Optimal -Force
|
||||||
|
|
||||||
|
# 5. SAFETY CHECK again, on the finished zip itself — belt and braces.
|
||||||
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||||
|
$zip = [System.IO.Compression.ZipFile]::OpenRead($zipPath)
|
||||||
|
try {
|
||||||
|
$leaked = @($zip.Entries | Where-Object { Test-Forbidden $_.FullName })
|
||||||
|
$entryCount = $zip.Entries.Count
|
||||||
|
} finally {
|
||||||
|
$zip.Dispose()
|
||||||
|
}
|
||||||
|
if ($leaked.Count -gt 0) {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "RELEASE ABORTED - finished zip contains forbidden entries:" -ForegroundColor Red
|
||||||
|
$leaked | ForEach-Object { Write-Host " $($_.FullName)" -ForegroundColor Red }
|
||||||
|
Remove-Item $zipPath -Force
|
||||||
|
Remove-Item $staging -Recurse -Force
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Remove-Item $staging -Recurse -Force
|
||||||
$size = [math]::Round((Get-Item $zipPath).Length / 1MB, 2)
|
$size = [math]::Round((Get-Item $zipPath).Length / 1MB, 2)
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Built $zipPath ($size MB)" -ForegroundColor Green
|
Write-Host "OK - clean release zip verified: $zipPath ($size MB, $entryCount entries)" -ForegroundColor Green
|
||||||
|
Write-Host " No logs / profiles / recordings / config present." -ForegroundColor Green
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Next:"
|
Write-Host "Next:" -ForegroundColor Cyan
|
||||||
Write-Host " git add -A; git commit -m 'Release $Tag'; git push"
|
Write-Host " gh release create $Tag `"$zipPath`" --title `"RemSound $Tag`" --notes-file RELEASE_NOTES.md"
|
||||||
Write-Host " gh release create $Tag $zipPath --title `"$Tag`" --notes-file RELEASE_NOTES.md"
|
|
||||||
|
|||||||
@@ -20,6 +20,23 @@ internal sealed class AboutDialog : Form
|
|||||||
/// updates" path.</summary>
|
/// updates" path.</summary>
|
||||||
private const string ReleaseNotes =
|
private const string ReleaseNotes =
|
||||||
"""
|
"""
|
||||||
|
RemSound v1.7
|
||||||
|
|
||||||
|
Updater fix. No wire-format or audio-pipeline changes —
|
||||||
|
v1.5, v1.6 and v1.7 peers interoperate.
|
||||||
|
|
||||||
|
Bug fix:
|
||||||
|
* Check for updates now reliably finds RemSound
|
||||||
|
releases. RemSound and the relay server share one
|
||||||
|
GitHub repository; the server's releases use
|
||||||
|
"server-" tags. The updater previously looked only
|
||||||
|
at the single newest release of any kind, so a
|
||||||
|
server release could derail it (it would misread
|
||||||
|
the version and report "up to date"). It now scans
|
||||||
|
the release list and considers only RemSound client
|
||||||
|
versions, ignoring server releases, drafts and
|
||||||
|
pre-releases.
|
||||||
|
|
||||||
RemSound v1.6
|
RemSound v1.6
|
||||||
|
|
||||||
Three reliability fixes. No wire-format or audio-pipeline
|
Three reliability fixes. No wire-format or audio-pipeline
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
tag_name on the latest GitHub release; bump it on every public release. The
|
tag_name on the latest GitHub release; bump it on every public release. The
|
||||||
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
|
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
|
||||||
is what the About dialog and the updater both read. -->
|
is what the About dialog and the updater both read. -->
|
||||||
<Version>1.6.0</Version>
|
<Version>1.7.0</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -64,7 +64,13 @@ internal sealed class RemSoundUpdater : IDisposable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var url = $"https://api.github.com/repos/{RepoOwner}/{RepoName}/releases/latest";
|
// List releases — NOT /releases/latest. The repo also hosts the relay server's
|
||||||
|
// own "server-vX.Y" releases, and /releases/latest is repo-wide: it hands back
|
||||||
|
// whichever release is newest by date, server or client. A server release would
|
||||||
|
// then be fed to ParseTag ("server-v2.3" -> a bogus 0.0.3) and the updater would
|
||||||
|
// wrongly conclude "up to date". We pull the list and consider ONLY releases
|
||||||
|
// whose tag is a RemSound client tag (see IsClientReleaseTag). 2026-05-18.
|
||||||
|
var url = $"https://api.github.com/repos/{RepoOwner}/{RepoName}/releases";
|
||||||
Log?.Invoke($"updater: GET {url}");
|
Log?.Invoke($"updater: GET {url}");
|
||||||
using var req = new HttpRequestMessage(HttpMethod.Get, url);
|
using var req = new HttpRequestMessage(HttpMethod.Get, url);
|
||||||
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
|
req.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github+json"));
|
||||||
@@ -75,14 +81,30 @@ internal sealed class RemSoundUpdater : IDisposable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
await using var stream = await resp.Content.ReadAsStreamAsync(token).ConfigureAwait(false);
|
await using var stream = await resp.Content.ReadAsStreamAsync(token).ConfigureAwait(false);
|
||||||
var release = await JsonSerializer.DeserializeAsync<GitHubRelease>(stream, JsonOpts, token).ConfigureAwait(false);
|
var releases = await JsonSerializer.DeserializeAsync<List<GitHubRelease>>(stream, JsonOpts, token).ConfigureAwait(false);
|
||||||
if (release?.TagName is null)
|
if (releases is null || releases.Count == 0)
|
||||||
{
|
{
|
||||||
Log?.Invoke("updater: response had no tag_name");
|
Log?.Invoke("updater: releases list was empty");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highest-versioned RemSound client release. Skip drafts, prereleases, and any
|
||||||
|
// tag that isn't a client tag (notably the server-vX.Y relay releases).
|
||||||
|
GitHubRelease? release = null;
|
||||||
|
var latest = new Version(0, 0, 0);
|
||||||
|
foreach (var r in releases)
|
||||||
|
{
|
||||||
|
if (r.TagName is null || r.Draft || r.Prerelease) continue;
|
||||||
|
if (!IsClientReleaseTag(r.TagName)) continue;
|
||||||
|
var v = ParseTag(r.TagName);
|
||||||
|
if (v > latest) { latest = v; release = r; }
|
||||||
|
}
|
||||||
|
if (release?.TagName is null)
|
||||||
|
{
|
||||||
|
Log?.Invoke("updater: no RemSound client release found in the releases list");
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var latest = ParseTag(release.TagName);
|
|
||||||
var current = Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0);
|
var current = Assembly.GetExecutingAssembly().GetName().Version ?? new Version(0, 0, 0);
|
||||||
Log?.Invoke($"updater: current={current.ToString(3)} latest={latest.ToString(3)} ({release.TagName})");
|
Log?.Invoke($"updater: current={current.ToString(3)} latest={latest.ToString(3)} ({release.TagName})");
|
||||||
if (latest <= current) return null;
|
if (latest <= current) return null;
|
||||||
@@ -259,6 +281,19 @@ internal sealed class RemSoundUpdater : IDisposable
|
|||||||
/// <summary>Parses a release tag like <c>v1.2</c> or <c>1.2.3</c> into a <see cref="Version"/>.
|
/// <summary>Parses a release tag like <c>v1.2</c> or <c>1.2.3</c> into a <see cref="Version"/>.
|
||||||
/// Leading "v" is stripped. Missing minor/build parts get filled with zeros so the result
|
/// Leading "v" is stripped. Missing minor/build parts get filled with zeros so the result
|
||||||
/// always compares meaningfully against <see cref="Assembly.GetName"/>.Version.</summary>
|
/// always compares meaningfully against <see cref="Assembly.GetName"/>.Version.</summary>
|
||||||
|
/// <summary>True if <paramref name="tag"/> is a RemSound client release tag — e.g.
|
||||||
|
/// <c>v1.6</c>, <c>1.6</c>, <c>1.6.0</c> — rather than something else hosted in the same
|
||||||
|
/// GitHub repo, notably the relay server's <c>server-vX.Y</c> releases. Test: after an
|
||||||
|
/// optional leading <c>v</c>, the first character must be a digit. <c>server-v2.3</c>
|
||||||
|
/// starts with 's' and is rejected; <c>v1.6</c> is accepted. The updater must filter on
|
||||||
|
/// this because it lists all repo releases and the server publishes into the same repo.</summary>
|
||||||
|
public static bool IsClientReleaseTag(string? tag)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(tag)) return false;
|
||||||
|
var trimmed = tag.TrimStart('v', 'V').Trim();
|
||||||
|
return trimmed.Length > 0 && char.IsDigit(trimmed[0]);
|
||||||
|
}
|
||||||
|
|
||||||
public static Version ParseTag(string tag)
|
public static Version ParseTag(string tag)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(tag)) return new Version(0, 0, 0);
|
if (string.IsNullOrWhiteSpace(tag)) return new Version(0, 0, 0);
|
||||||
@@ -298,6 +333,8 @@ internal sealed class RemSoundUpdater : IDisposable
|
|||||||
[JsonPropertyName("tag_name")] public string? TagName { get; set; }
|
[JsonPropertyName("tag_name")] public string? TagName { get; set; }
|
||||||
[JsonPropertyName("body")] public string? Body { get; set; }
|
[JsonPropertyName("body")] public string? Body { get; set; }
|
||||||
[JsonPropertyName("html_url")] public string? HtmlUrl { get; set; }
|
[JsonPropertyName("html_url")] public string? HtmlUrl { get; set; }
|
||||||
|
[JsonPropertyName("draft")] public bool Draft { get; set; }
|
||||||
|
[JsonPropertyName("prerelease")] public bool Prerelease { get; set; }
|
||||||
[JsonPropertyName("assets")] public List<GitHubAsset>? Assets { get; set; }
|
[JsonPropertyName("assets")] public List<GitHubAsset>? Assets { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user